@@ -25,6 +25,14 @@ type ChakraDay =
2525 | "Third Eye"
2626 | "Crown" ;
2727
28+ type UnknownRecord = Record < string , unknown > ;
29+
30+ const SVG_NS = "http://www.w3.org/2000/svg" ;
31+ const PROOF_METADATA_ID = "kai-voh-proof" ;
32+
33+ const isRecord = ( value : unknown ) : value is UnknownRecord =>
34+ typeof value === "object" && value !== null && ! Array . isArray ( value ) ;
35+
2836function stableStringify ( v : unknown ) : string {
2937 if ( v === null || typeof v !== "object" ) return JSON . stringify ( v ) ;
3038 if ( Array . isArray ( v ) ) return "[" + v . map ( stableStringify ) . join ( "," ) + "]" ;
@@ -58,6 +66,7 @@ export type ExportableSigilMeta = {
5866 /** misc */
5967 attachment ?: { name ?: string | null } | null ;
6068 provenance ?: Array < Record < string , unknown > > | null ;
69+ payloadExtras ?: Record < string , unknown > ;
6170} ;
6271
6372/** Coerce any free-form value into a valid ChakraDay; default to "Root". */
@@ -158,6 +167,47 @@ function updateSvgUrlSurfaces(svgEl: SVGSVGElement, fullUrl: string): void {
158167 } ) ;
159168}
160169
170+ function buildProphecyZkBundle ( payloadExtras ?: Record < string , unknown > , shareUrl ?: string ) : UnknownRecord | null {
171+ if ( ! payloadExtras ) return null ;
172+ const rawProphecy = isRecord ( payloadExtras . prophecyPayload ) ? payloadExtras . prophecyPayload : null ;
173+ const rawZk = rawProphecy && isRecord ( rawProphecy . zk ) ? rawProphecy . zk : null ;
174+ if ( ! rawZk ) return null ;
175+
176+ const zkPoseidonHash = typeof rawZk . poseidonHash === "string" ? rawZk . poseidonHash : undefined ;
177+ const zkProof = "proof" in rawZk ? rawZk . proof : undefined ;
178+ const zkPublicInputs =
179+ Array . isArray ( rawZk . publicInputs ) || typeof rawZk . publicInputs === "string"
180+ ? rawZk . publicInputs
181+ : undefined ;
182+
183+ if ( ! zkPoseidonHash && zkProof === undefined && zkPublicInputs === undefined ) return null ;
184+
185+ const bundle : UnknownRecord = {
186+ shareUrl,
187+ zkPoseidonHash,
188+ zkProof,
189+ zkPublicInputs,
190+ } ;
191+
192+ Object . keys ( bundle ) . forEach ( ( key ) => {
193+ if ( bundle [ key ] === undefined ) delete bundle [ key ] ;
194+ } ) ;
195+
196+ return bundle ;
197+ }
198+
199+ function upsertProofMetadata ( svgEl : SVGSVGElement , bundle : UnknownRecord ) : void {
200+ const doc = svgEl . ownerDocument ?? document ;
201+ let meta = svgEl . querySelector < SVGMetadataElement > ( `metadata#${ PROOF_METADATA_ID } ` ) ;
202+ if ( ! meta ) {
203+ meta = doc . createElementNS ( SVG_NS , "metadata" ) as SVGMetadataElement ;
204+ meta . setAttribute ( "id" , PROOF_METADATA_ID ) ;
205+ meta . setAttribute ( "type" , "application/json" ) ;
206+ svgEl . appendChild ( meta ) ;
207+ }
208+ meta . textContent = JSON . stringify ( bundle ) ;
209+ }
210+
161211export async function exportZIP ( ctx : {
162212 expired : boolean ;
163213 exporting : boolean ;
@@ -240,6 +290,7 @@ export async function exportZIP(ctx: {
240290 claimExtendUnit : payload . claimExtendUnit ?? expiryUnit ,
241291 claimExtendAmount : payload . claimExtendAmount ?? expiryAmount ,
242292 canonicalHash : ( localHash || payload . canonicalHash || routeHash || null ) ?. toString ( ) ?? null ,
293+ payloadExtras : payload . payloadExtras ,
243294 } ;
244295
245296 // canonical Σ and Φ (0-based stepIndex)
@@ -258,8 +309,11 @@ export async function exportZIP(ctx: {
258309 const claimedMetaCanon : ExportableSigilMeta = {
259310 ...claimedMeta ,
260311 kaiSignature : canonicalSig ,
261- userPhiKey : claimedMeta . userPhiKey || phiKeyCanon ,
312+ userPhiKey : phiKeyCanon ,
262313 } ;
314+ svgEl . setAttribute ( "data-kai-signature" , canonicalSig ) ;
315+ svgEl . setAttribute ( "data-phi-key" , phiKeyCanon ) ;
316+ const payloadExtras = claimedMetaCanon . payloadExtras ?? { } ;
263317
264318 // Build the canonical share URL for manifest — canonical is in the path, NOT the payload
265319 const canonicalLower = ( localHash || routeHash || "" ) . toLowerCase ( ) ;
@@ -279,20 +333,29 @@ export async function exportZIP(ctx: {
279333
280334 const fullUrlForManifest = rewriteUrlPayload (
281335 baseUrlForManifest ,
282- sharePayloadForManifest ,
336+ { ... payloadExtras , ... sharePayloadForManifest } ,
283337 tokenForManifest
284338 ) ;
285339
286340 // Canonical payload write only (single call) — include URL hints for readers
287341 const { putMetadata } = await import ( "../../utils/svgMeta" ) ;
288342 const metaForSvg : Record < string , unknown > = {
343+ ...payloadExtras ,
289344 ...claimedMetaCanon ,
290345 stepsPerBeat : stepsNum ,
291346 shareUrl : fullUrlForManifest , // hint for consumers
292347 fullUrl : fullUrlForManifest , // alias
293348 } ;
349+ if ( typeof metaForSvg . userPhiKey === "string" && ! metaForSvg . phiKey ) {
350+ metaForSvg . phiKey = metaForSvg . userPhiKey ;
351+ }
294352 putMetadata ( svgEl , metaForSvg ) ;
295353
354+ const zkBundle = buildProphecyZkBundle ( payloadExtras , fullUrlForManifest ) ;
355+ if ( zkBundle ) {
356+ upsertProofMetadata ( svgEl , zkBundle ) ;
357+ }
358+
296359 // Display-only exposure (non-canonical marker)
297360 try {
298361 svgEl . setAttribute ( "data-step-index" , String ( sealedStepIndex ) ) ;
@@ -383,6 +446,7 @@ export async function exportZIP(ctx: {
383446 fullUrl : fullUrlForManifest ,
384447 p : pValue ,
385448 urlQuery : { p : pValue , t : tValue } ,
449+ payloadExtras : Object . keys ( payloadExtras ) . length ? payloadExtras : null ,
386450 } ;
387451 const manifestHash = await sha256HexCanon ( stableStringify ( manifestPayload ) ) ;
388452 const manifest = { ...manifestPayload , manifestHash } ;
0 commit comments