@agimon-ai/po-web-security 0.0.0-alpha.129 → 0.0.0-alpha.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/dist/services/browserBundleVerifier/index.cjs.map +1 -1
  2. package/dist/services/browserBundleVerifier/index.mjs.map +1 -1
  3. package/dist/services/browserSealedChannel/index.cjs.map +1 -1
  4. package/dist/services/browserSealedChannel/index.mjs.map +1 -1
  5. package/dist/services/bundleSigner/index.cjs.map +1 -1
  6. package/dist/services/bundleSigner/index.mjs.map +1 -1
  7. package/dist/services/clientIdentity/index.cjs.map +1 -1
  8. package/dist/services/clientIdentity/index.mjs.map +1 -1
  9. package/dist/services/deviceSignature/index.cjs.map +1 -1
  10. package/dist/services/deviceSignature/index.mjs.map +1 -1
  11. package/dist/services/federationEnvelope/index.cjs.map +1 -1
  12. package/dist/services/federationEnvelope/index.mjs.map +1 -1
  13. package/dist/services/federationIssuerHttp/index.cjs.map +1 -1
  14. package/dist/services/federationIssuerHttp/index.mjs.map +1 -1
  15. package/dist/services/federationVerifier/index.cjs.map +1 -1
  16. package/dist/services/federationVerifier/index.mjs.map +1 -1
  17. package/dist/services/nodeSealedChannel/index.cjs.map +1 -1
  18. package/dist/services/nodeSealedChannel/index.mjs.map +1 -1
  19. package/dist/services/outboundHost/index.cjs.map +1 -1
  20. package/dist/services/outboundHost/index.mjs.map +1 -1
  21. package/dist/services/sealedTransport/index.cjs.map +1 -1
  22. package/dist/services/sealedTransport/index.mjs.map +1 -1
  23. package/dist/services/serialQueue/index.cjs.map +1 -1
  24. package/dist/services/serialQueue/index.mjs.map +1 -1
  25. package/dist/types/bundleManifest.cjs.map +1 -1
  26. package/dist/types/bundleManifest.mjs.map +1 -1
  27. package/dist/types/federation.cjs.map +1 -1
  28. package/dist/types/federation.mjs.map +1 -1
  29. package/dist/types/sealedChannel.cjs.map +1 -1
  30. package/dist/types/sealedChannel.mjs.map +1 -1
  31. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["isSignedBundleManifest","canonicalManifest","attestationBytes","assetFor","require_bundleManifest","decodeBase64Url","standard","value","padded","binary","bytes","index","hex","byte","BASE64URL","importVerifyKey","spki","verifySignedBundleManifest","profile","signed","trustedPublicKey","minimumRevision","key","verifyAttestation","message","tag","fields","signature","verifyBundleAsset","asset","manifest","assetPath"],"sources":["../../../src/services/browserBundleVerifier/index.ts"],"sourcesContent":["import {\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n assetFor,\n attestationBytes,\n canonicalManifest,\n isSignedBundleManifest,\n} from '../../types/bundleManifest';\n\nexport type BundleVerificationFailure =\n | { code: 'invalid-envelope' }\n | { code: 'untrusted-public-key' }\n | { code: 'stale-revision'; minimumRevision: number; actualRevision: number }\n | { code: 'invalid-public-key' }\n | { code: 'invalid-signature' };\n\nexport type BundleVerificationResult =\n | { ok: true; manifest: BundleManifest }\n | { ok: false; failure: BundleVerificationFailure };\n\nexport type BundleAssetVerificationFailure =\n | { code: 'asset-not-listed'; path: string }\n | { code: 'byte-length-mismatch'; expected: number; actual: number }\n | { code: 'digest-mismatch' };\n\nexport type BundleAssetVerificationResult = { ok: true } | { ok: false; failure: BundleAssetVerificationFailure };\n\nfunction decodeBase64Url(value: string): ArrayBuffer {\n const standard = value.replaceAll('-', '+').replaceAll('_', '/');\n const padded = standard.padEnd(Math.ceil(standard.length / 4) * 4, '=');\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);\n return bytes.buffer;\n}\n\nfunction hex(bytes: ArrayBuffer): string {\n return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nconst P1363_SIGNATURE_LENGTH = 86;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\nconst MAX_SPKI_LENGTH = 512;\n\nfunction importVerifyKey(spki: string): Promise<CryptoKey> {\n return crypto.subtle.importKey('spki', decodeBase64Url(spki), { name: 'ECDSA', namedCurve: 'P-256' }, false, [\n 'verify',\n ]);\n}\n\n/** Verifies a strict envelope against a separately trusted key, revision floor, and profile. */\nexport async function verifySignedBundleManifest(\n value: unknown,\n trustedPublicKey: string,\n minimumRevision = 0,\n profile: BundleProfile = 'web',\n): Promise<BundleVerificationResult> {\n if (!isSignedBundleManifest(value, profile)) return { ok: false, failure: { code: 'invalid-envelope' } };\n const signed: SignedBundleManifest = value;\n if (signed.publicKey !== trustedPublicKey) return { ok: false, failure: { code: 'untrusted-public-key' } };\n if (!Number.isSafeInteger(minimumRevision) || minimumRevision < 0 || signed.manifest.revision < minimumRevision) {\n return {\n ok: false,\n failure: { code: 'stale-revision', minimumRevision, actualRevision: signed.manifest.revision },\n };\n }\n\n let key: CryptoKey;\n try {\n key = await importVerifyKey(trustedPublicKey);\n } catch {\n return { ok: false, failure: { code: 'invalid-public-key' } };\n }\n\n try {\n const verified = await crypto.subtle.verify(\n { name: 'ECDSA', hash: 'SHA-256' },\n key,\n decodeBase64Url(signed.signature),\n new TextEncoder().encode(canonicalManifest(signed.manifest)),\n );\n return verified ? { ok: true, manifest: signed.manifest } : { ok: false, failure: { code: 'invalid-signature' } };\n } catch {\n return { ok: false, failure: { code: 'invalid-signature' } };\n }\n}\n\n/**\n * Verifies a tagged P-256 attestation against a separately trusted base64url SPKI. False for a malformed\n * key or signature and for any signature mismatch; throws only on a malformed tag, which is a caller bug.\n */\nexport async function verifyAttestation(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): Promise<boolean> {\n const message = attestationBytes(tag, fields);\n if (typeof spki !== 'string' || spki.length === 0 || spki.length > MAX_SPKI_LENGTH || !BASE64URL.test(spki)) {\n return false;\n }\n if (typeof signature !== 'string' || signature.length !== P1363_SIGNATURE_LENGTH || !BASE64URL.test(signature)) {\n return false;\n }\n try {\n const key = await importVerifyKey(spki);\n return await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, decodeBase64Url(signature), message);\n } catch {\n return false;\n }\n}\n\n/** Verifies fetched raw response bytes against an authenticated manifest. */\nexport async function verifyBundleAsset(\n manifest: BundleManifest,\n assetPath: string,\n bytes: ArrayBuffer,\n): Promise<BundleAssetVerificationResult> {\n const asset = assetFor(manifest, assetPath);\n if (asset === undefined) return { ok: false, failure: { code: 'asset-not-listed', path: assetPath } };\n if (bytes.byteLength !== asset.byteLength) {\n return {\n ok: false,\n failure: { code: 'byte-length-mismatch', expected: asset.byteLength, actual: bytes.byteLength },\n };\n }\n const digest = hex(await crypto.subtle.digest('SHA-256', bytes));\n return digest === asset.sha256 ? { ok: true } : { ok: false, failure: { code: 'digest-mismatch' } };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["isSignedBundleManifest","canonicalManifest","attestationBytes","assetFor","require_bundleManifest","decodeBase64Url","standard","value","padded","binary","bytes","index","hex","byte","BASE64URL","importVerifyKey","spki","verifySignedBundleManifest","profile","signed","trustedPublicKey","minimumRevision","key","verifyAttestation","message","tag","fields","signature","verifyBundleAsset","asset","manifest","assetPath"],"sources":["../../../src/services/browserBundleVerifier/index.ts"],"sourcesContent":["import {\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n assetFor,\n attestationBytes,\n canonicalManifest,\n isSignedBundleManifest,\n} from '../../types/bundleManifest';\n\nexport type BundleVerificationFailure =\n | { code: 'invalid-envelope' }\n | { code: 'untrusted-public-key' }\n | { code: 'stale-revision'; minimumRevision: number; actualRevision: number }\n | { code: 'invalid-public-key' }\n | { code: 'invalid-signature' };\n\nexport type BundleVerificationResult =\n | { ok: true; manifest: BundleManifest }\n | { ok: false; failure: BundleVerificationFailure };\n\nexport type BundleAssetVerificationFailure =\n | { code: 'asset-not-listed'; path: string }\n | { code: 'byte-length-mismatch'; expected: number; actual: number }\n | { code: 'digest-mismatch' };\n\nexport type BundleAssetVerificationResult = { ok: true } | { ok: false; failure: BundleAssetVerificationFailure };\n\nfunction decodeBase64Url(value: string): ArrayBuffer {\n const standard = value.replaceAll('-', '+').replaceAll('_', '/');\n const padded = standard.padEnd(Math.ceil(standard.length / 4) * 4, '=');\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);\n return bytes.buffer;\n}\n\nfunction hex(bytes: ArrayBuffer): string {\n return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nconst P1363_SIGNATURE_LENGTH = 86;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\nconst MAX_SPKI_LENGTH = 512;\n\nfunction importVerifyKey(spki: string): Promise<CryptoKey> {\n return crypto.subtle.importKey('spki', decodeBase64Url(spki), { name: 'ECDSA', namedCurve: 'P-256' }, false, [\n 'verify',\n ]);\n}\n\n/** Verifies a strict envelope against a separately trusted key, revision floor, and profile. */\nexport async function verifySignedBundleManifest(\n value: unknown,\n trustedPublicKey: string,\n minimumRevision = 0,\n profile: BundleProfile = 'web',\n): Promise<BundleVerificationResult> {\n if (!isSignedBundleManifest(value, profile)) return { ok: false, failure: { code: 'invalid-envelope' } };\n const signed: SignedBundleManifest = value;\n if (signed.publicKey !== trustedPublicKey) return { ok: false, failure: { code: 'untrusted-public-key' } };\n if (!Number.isSafeInteger(minimumRevision) || minimumRevision < 0 || signed.manifest.revision < minimumRevision) {\n return {\n ok: false,\n failure: { code: 'stale-revision', minimumRevision, actualRevision: signed.manifest.revision },\n };\n }\n\n let key: CryptoKey;\n try {\n key = await importVerifyKey(trustedPublicKey);\n } catch {\n return { ok: false, failure: { code: 'invalid-public-key' } };\n }\n\n try {\n const verified = await crypto.subtle.verify(\n { name: 'ECDSA', hash: 'SHA-256' },\n key,\n decodeBase64Url(signed.signature),\n new TextEncoder().encode(canonicalManifest(signed.manifest)),\n );\n return verified ? { ok: true, manifest: signed.manifest } : { ok: false, failure: { code: 'invalid-signature' } };\n } catch {\n return { ok: false, failure: { code: 'invalid-signature' } };\n }\n}\n\n/**\n * Verifies a tagged P-256 attestation against a separately trusted base64url SPKI. False for a malformed\n * key or signature and for any signature mismatch; throws only on a malformed tag, which is a caller bug.\n */\nexport async function verifyAttestation(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): Promise<boolean> {\n const message = attestationBytes(tag, fields);\n if (typeof spki !== 'string' || spki.length === 0 || spki.length > MAX_SPKI_LENGTH || !BASE64URL.test(spki)) {\n return false;\n }\n if (typeof signature !== 'string' || signature.length !== P1363_SIGNATURE_LENGTH || !BASE64URL.test(signature)) {\n return false;\n }\n try {\n const key = await importVerifyKey(spki);\n return await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, decodeBase64Url(signature), message);\n } catch {\n return false;\n }\n}\n\n/** Verifies fetched raw response bytes against an authenticated manifest. */\nexport async function verifyBundleAsset(\n manifest: BundleManifest,\n assetPath: string,\n bytes: ArrayBuffer,\n): Promise<BundleAssetVerificationResult> {\n const asset = assetFor(manifest, assetPath);\n if (asset === undefined) return { ok: false, failure: { code: 'asset-not-listed', path: assetPath } };\n if (bytes.byteLength !== asset.byteLength) {\n return {\n ok: false,\n failure: { code: 'byte-length-mismatch', expected: asset.byteLength, actual: bytes.byteLength },\n };\n }\n const digest = hex(await crypto.subtle.digest('SHA-256', bytes));\n return digest === asset.sha256 ? { ok: true } : { ok: false, failure: { code: 'digest-mismatch' } };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["decodeBase64Url","standard","value","padded","binary","bytes","index","hex","byte","BASE64URL","importVerifyKey","spki","verifySignedBundleManifest","isSignedBundleManifest","profile","signed","trustedPublicKey","minimumRevision","key","canonicalManifest","verifyAttestation","message","attestationBytes","tag","fields","signature","verifyBundleAsset","asset","assetFor","manifest","assetPath"],"sources":["../../../src/services/browserBundleVerifier/index.ts"],"sourcesContent":["import {\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n assetFor,\n attestationBytes,\n canonicalManifest,\n isSignedBundleManifest,\n} from '../../types/bundleManifest';\n\nexport type BundleVerificationFailure =\n | { code: 'invalid-envelope' }\n | { code: 'untrusted-public-key' }\n | { code: 'stale-revision'; minimumRevision: number; actualRevision: number }\n | { code: 'invalid-public-key' }\n | { code: 'invalid-signature' };\n\nexport type BundleVerificationResult =\n | { ok: true; manifest: BundleManifest }\n | { ok: false; failure: BundleVerificationFailure };\n\nexport type BundleAssetVerificationFailure =\n | { code: 'asset-not-listed'; path: string }\n | { code: 'byte-length-mismatch'; expected: number; actual: number }\n | { code: 'digest-mismatch' };\n\nexport type BundleAssetVerificationResult = { ok: true } | { ok: false; failure: BundleAssetVerificationFailure };\n\nfunction decodeBase64Url(value: string): ArrayBuffer {\n const standard = value.replaceAll('-', '+').replaceAll('_', '/');\n const padded = standard.padEnd(Math.ceil(standard.length / 4) * 4, '=');\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);\n return bytes.buffer;\n}\n\nfunction hex(bytes: ArrayBuffer): string {\n return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nconst P1363_SIGNATURE_LENGTH = 86;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\nconst MAX_SPKI_LENGTH = 512;\n\nfunction importVerifyKey(spki: string): Promise<CryptoKey> {\n return crypto.subtle.importKey('spki', decodeBase64Url(spki), { name: 'ECDSA', namedCurve: 'P-256' }, false, [\n 'verify',\n ]);\n}\n\n/** Verifies a strict envelope against a separately trusted key, revision floor, and profile. */\nexport async function verifySignedBundleManifest(\n value: unknown,\n trustedPublicKey: string,\n minimumRevision = 0,\n profile: BundleProfile = 'web',\n): Promise<BundleVerificationResult> {\n if (!isSignedBundleManifest(value, profile)) return { ok: false, failure: { code: 'invalid-envelope' } };\n const signed: SignedBundleManifest = value;\n if (signed.publicKey !== trustedPublicKey) return { ok: false, failure: { code: 'untrusted-public-key' } };\n if (!Number.isSafeInteger(minimumRevision) || minimumRevision < 0 || signed.manifest.revision < minimumRevision) {\n return {\n ok: false,\n failure: { code: 'stale-revision', minimumRevision, actualRevision: signed.manifest.revision },\n };\n }\n\n let key: CryptoKey;\n try {\n key = await importVerifyKey(trustedPublicKey);\n } catch {\n return { ok: false, failure: { code: 'invalid-public-key' } };\n }\n\n try {\n const verified = await crypto.subtle.verify(\n { name: 'ECDSA', hash: 'SHA-256' },\n key,\n decodeBase64Url(signed.signature),\n new TextEncoder().encode(canonicalManifest(signed.manifest)),\n );\n return verified ? { ok: true, manifest: signed.manifest } : { ok: false, failure: { code: 'invalid-signature' } };\n } catch {\n return { ok: false, failure: { code: 'invalid-signature' } };\n }\n}\n\n/**\n * Verifies a tagged P-256 attestation against a separately trusted base64url SPKI. False for a malformed\n * key or signature and for any signature mismatch; throws only on a malformed tag, which is a caller bug.\n */\nexport async function verifyAttestation(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): Promise<boolean> {\n const message = attestationBytes(tag, fields);\n if (typeof spki !== 'string' || spki.length === 0 || spki.length > MAX_SPKI_LENGTH || !BASE64URL.test(spki)) {\n return false;\n }\n if (typeof signature !== 'string' || signature.length !== P1363_SIGNATURE_LENGTH || !BASE64URL.test(signature)) {\n return false;\n }\n try {\n const key = await importVerifyKey(spki);\n return await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, decodeBase64Url(signature), message);\n } catch {\n return false;\n }\n}\n\n/** Verifies fetched raw response bytes against an authenticated manifest. */\nexport async function verifyBundleAsset(\n manifest: BundleManifest,\n assetPath: string,\n bytes: ArrayBuffer,\n): Promise<BundleAssetVerificationResult> {\n const asset = assetFor(manifest, assetPath);\n if (asset === undefined) return { ok: false, failure: { code: 'asset-not-listed', path: assetPath } };\n if (bytes.byteLength !== asset.byteLength) {\n return {\n ok: false,\n failure: { code: 'byte-length-mismatch', expected: asset.byteLength, actual: bytes.byteLength },\n };\n }\n const digest = hex(await crypto.subtle.digest('SHA-256', bytes));\n return digest === asset.sha256 ? { ok: true } : { ok: false, failure: { code: 'digest-mismatch' } };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["decodeBase64Url","standard","value","padded","binary","bytes","index","hex","byte","BASE64URL","importVerifyKey","spki","verifySignedBundleManifest","isSignedBundleManifest","profile","signed","trustedPublicKey","minimumRevision","key","canonicalManifest","verifyAttestation","message","attestationBytes","tag","fields","signature","verifyBundleAsset","asset","assetFor","manifest","assetPath"],"sources":["../../../src/services/browserBundleVerifier/index.ts"],"sourcesContent":["import {\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n assetFor,\n attestationBytes,\n canonicalManifest,\n isSignedBundleManifest,\n} from '../../types/bundleManifest';\n\nexport type BundleVerificationFailure =\n | { code: 'invalid-envelope' }\n | { code: 'untrusted-public-key' }\n | { code: 'stale-revision'; minimumRevision: number; actualRevision: number }\n | { code: 'invalid-public-key' }\n | { code: 'invalid-signature' };\n\nexport type BundleVerificationResult =\n | { ok: true; manifest: BundleManifest }\n | { ok: false; failure: BundleVerificationFailure };\n\nexport type BundleAssetVerificationFailure =\n | { code: 'asset-not-listed'; path: string }\n | { code: 'byte-length-mismatch'; expected: number; actual: number }\n | { code: 'digest-mismatch' };\n\nexport type BundleAssetVerificationResult = { ok: true } | { ok: false; failure: BundleAssetVerificationFailure };\n\nfunction decodeBase64Url(value: string): ArrayBuffer {\n const standard = value.replaceAll('-', '+').replaceAll('_', '/');\n const padded = standard.padEnd(Math.ceil(standard.length / 4) * 4, '=');\n const binary = atob(padded);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);\n return bytes.buffer;\n}\n\nfunction hex(bytes: ArrayBuffer): string {\n return [...new Uint8Array(bytes)].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\nconst P1363_SIGNATURE_LENGTH = 86;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\nconst MAX_SPKI_LENGTH = 512;\n\nfunction importVerifyKey(spki: string): Promise<CryptoKey> {\n return crypto.subtle.importKey('spki', decodeBase64Url(spki), { name: 'ECDSA', namedCurve: 'P-256' }, false, [\n 'verify',\n ]);\n}\n\n/** Verifies a strict envelope against a separately trusted key, revision floor, and profile. */\nexport async function verifySignedBundleManifest(\n value: unknown,\n trustedPublicKey: string,\n minimumRevision = 0,\n profile: BundleProfile = 'web',\n): Promise<BundleVerificationResult> {\n if (!isSignedBundleManifest(value, profile)) return { ok: false, failure: { code: 'invalid-envelope' } };\n const signed: SignedBundleManifest = value;\n if (signed.publicKey !== trustedPublicKey) return { ok: false, failure: { code: 'untrusted-public-key' } };\n if (!Number.isSafeInteger(minimumRevision) || minimumRevision < 0 || signed.manifest.revision < minimumRevision) {\n return {\n ok: false,\n failure: { code: 'stale-revision', minimumRevision, actualRevision: signed.manifest.revision },\n };\n }\n\n let key: CryptoKey;\n try {\n key = await importVerifyKey(trustedPublicKey);\n } catch {\n return { ok: false, failure: { code: 'invalid-public-key' } };\n }\n\n try {\n const verified = await crypto.subtle.verify(\n { name: 'ECDSA', hash: 'SHA-256' },\n key,\n decodeBase64Url(signed.signature),\n new TextEncoder().encode(canonicalManifest(signed.manifest)),\n );\n return verified ? { ok: true, manifest: signed.manifest } : { ok: false, failure: { code: 'invalid-signature' } };\n } catch {\n return { ok: false, failure: { code: 'invalid-signature' } };\n }\n}\n\n/**\n * Verifies a tagged P-256 attestation against a separately trusted base64url SPKI. False for a malformed\n * key or signature and for any signature mismatch; throws only on a malformed tag, which is a caller bug.\n */\nexport async function verifyAttestation(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): Promise<boolean> {\n const message = attestationBytes(tag, fields);\n if (typeof spki !== 'string' || spki.length === 0 || spki.length > MAX_SPKI_LENGTH || !BASE64URL.test(spki)) {\n return false;\n }\n if (typeof signature !== 'string' || signature.length !== P1363_SIGNATURE_LENGTH || !BASE64URL.test(signature)) {\n return false;\n }\n try {\n const key = await importVerifyKey(spki);\n return await crypto.subtle.verify({ name: 'ECDSA', hash: 'SHA-256' }, key, decodeBase64Url(signature), message);\n } catch {\n return false;\n }\n}\n\n/** Verifies fetched raw response bytes against an authenticated manifest. */\nexport async function verifyBundleAsset(\n manifest: BundleManifest,\n assetPath: string,\n bytes: ArrayBuffer,\n): Promise<BundleAssetVerificationResult> {\n const asset = assetFor(manifest, assetPath);\n if (asset === undefined) return { ok: false, failure: { code: 'asset-not-listed', path: assetPath } };\n if (bytes.byteLength !== asset.byteLength) {\n return {\n ok: false,\n failure: { code: 'byte-length-mismatch', expected: asset.byteLength, actual: bytes.byteLength },\n };\n }\n const digest = hex(await crypto.subtle.digest('SHA-256', bytes));\n return digest === asset.sha256 ? { ok: true } : { ok: false, failure: { code: 'digest-mismatch' } };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["infoFor","MAX_MESSAGES_PER_KEY","isSealedEnvelope","require_sealedChannel","KEY_ALGORITHM","CIPHER","bytes","binary","value","out","index","base64Url","byte","view","copy","derive","material","secret","direction","nonceFor","nonce","prefix","counter","channelFrom","sendCounter","highestReceived","noncePrefix","sealed","sendKey","plaintext","shape","envelope","body","receiveKey","connectSealedChannel","pair","hostKey","hostPublicKey","peer","clientPublicKey","channelFromSecret","sendDirection"],"sources":["../../../src/services/browserSealedChannel/index.ts"],"sourcesContent":["import {\n MAX_MESSAGES_PER_KEY,\n NONCE_BYTES,\n NONCE_PREFIX_BYTES,\n SEALED_VERSION,\n type SealedDirection,\n type SealedEnvelope,\n type SealedFailure,\n infoFor,\n isSealedEnvelope,\n} from '../../types/sealedChannel';\n\n/**\n * The browser half of the sealed channel.\n *\n * Mirrors `nodeSealedChannel.ts` exactly, on WebCrypto instead of\n * `node:crypto`. Two implementations rather than one because a shared one would\n * drag node builtins into every browser bundle that imports it, and this\n * package is imported by plugins that ship to the page. The envelope contract\n * they both read lives in `../types/sealedChannel.ts`, and a round-trip test\n * pins the two together so they cannot drift.\n */\n\nconst KEY_ALGORITHM = { name: 'ECDH', namedCurve: 'P-256' } as const;\nconst CIPHER = 'AES-GCM';\nconst KEY_BITS = 256;\nconst TAG_BITS = 128;\nconst TAG_BYTES = 16;\nconst HKDF_SALT = 'po/sealed/v1';\n\nexport type SealResult = { ok: true; envelope: SealedEnvelope } | { ok: false; failure: SealedFailure };\nexport type OpenResult = { ok: true; plaintext: Uint8Array } | { ok: false; failure: SealedFailure };\n\nexport interface SealedChannel {\n seal(plaintext: Uint8Array): Promise<SealResult>;\n open(envelope: unknown): Promise<OpenResult>;\n}\n\n/** Undefined rather than a throw: a malformed field is a verdict, not an exception. */\nfunction bytes(value: string): Uint8Array | undefined {\n let binary: string;\n try {\n binary = atob(value.replaceAll('-', '+').replaceAll('_', '/'));\n } catch {\n return undefined;\n }\n const out = new Uint8Array(new ArrayBuffer(binary.length));\n for (let index = 0; index < binary.length; index += 1) out[index] = binary.charCodeAt(index);\n return out;\n}\n\nfunction base64Url(value: Uint8Array): string {\n let binary = '';\n for (const byte of value) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n}\n\nfunction view(value: Uint8Array): ArrayBuffer {\n const copy = new Uint8Array(new ArrayBuffer(value.length));\n copy.set(value);\n return copy.buffer;\n}\n\nasync function derive(secret: ArrayBuffer, direction: SealedDirection): Promise<CryptoKey> {\n const material = await crypto.subtle.importKey('raw', secret, 'HKDF', false, ['deriveKey']);\n return await crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: view(new TextEncoder().encode(HKDF_SALT)),\n info: view(new TextEncoder().encode(infoFor(direction))),\n },\n material,\n { name: CIPHER, length: KEY_BITS },\n false,\n ['encrypt', 'decrypt'],\n );\n}\n\nfunction nonceFor(prefix: Uint8Array, counter: bigint): Uint8Array {\n const nonce = new Uint8Array(NONCE_BYTES);\n nonce.set(prefix.subarray(0, NONCE_PREFIX_BYTES), 0);\n new DataView(nonce.buffer).setBigUint64(NONCE_PREFIX_BYTES, counter, false);\n return nonce;\n}\n\nfunction channelFrom(sendKey: CryptoKey, receiveKey: CryptoKey, noncePrefix: Uint8Array): SealedChannel {\n let sendCounter = 0n;\n let highestReceived = -1n;\n\n return {\n async seal(plaintext) {\n if (sendCounter >= BigInt(MAX_MESSAGES_PER_KEY)) return { ok: false, failure: 'exhausted' };\n const nonce = nonceFor(noncePrefix, sendCounter);\n const sealed = await crypto.subtle.encrypt(\n { name: CIPHER, iv: view(nonce), tagLength: TAG_BITS },\n sendKey,\n view(plaintext),\n );\n sendCounter += 1n;\n return {\n ok: true,\n envelope: { v: SEALED_VERSION, n: base64Url(nonce), c: base64Url(new Uint8Array(sealed)) },\n };\n },\n\n async open(envelope) {\n // Shape before version, matching the server: something with no `v` at all\n // is malformed rather than a protocol skew.\n const shape = envelope as { v?: unknown; n?: unknown; c?: unknown } | null | undefined;\n if (typeof shape?.n !== 'string' || typeof shape.c !== 'string') return { ok: false, failure: 'malformed' };\n if (shape.v !== SEALED_VERSION) return { ok: false, failure: 'version' };\n if (!isSealedEnvelope(envelope)) return { ok: false, failure: 'malformed' };\n const nonce = bytes(envelope.n);\n const body = bytes(envelope.c);\n if (nonce === undefined || body === undefined) return { ok: false, failure: 'malformed' };\n if (nonce.length !== NONCE_BYTES || body.length < TAG_BYTES) return { ok: false, failure: 'malformed' };\n const counter = new DataView(view(nonce)).getBigUint64(NONCE_PREFIX_BYTES, false);\n if (counter <= highestReceived) return { ok: false, failure: 'replay' };\n let plaintext: ArrayBuffer;\n try {\n plaintext = await crypto.subtle.decrypt(\n { name: CIPHER, iv: view(nonce), tagLength: TAG_BITS },\n receiveKey,\n view(body),\n );\n } catch {\n // A failed tag means the message was altered in transit; there is no\n // partial result worth salvaging.\n return { ok: false, failure: 'auth' };\n }\n // Another open may have finished while decrypt was pending, including\n // after its caller aborted. Never lower the authenticated high-water mark.\n if (counter <= highestReceived) return { ok: false, failure: 'replay' };\n // Only after the tag verified, so a forged high counter cannot lock out\n // every message that legitimately follows it. Keep the recheck and commit synchronous.\n highestReceived = counter;\n return { ok: true, plaintext: new Uint8Array(plaintext) };\n },\n };\n}\n\n/**\n * Completes the handshake against the host key read out of the QR fragment.\n *\n * That key never travelled through the tunnel, which is what makes this\n * authenticated: a relay can watch the client's public key go past and learn\n * nothing, and it cannot substitute the host's because it never carried it.\n */\nexport async function connectSealedChannel(\n hostPublicKey: string,\n): Promise<{ channel: SealedChannel; clientPublicKey: string } | undefined> {\n if (globalThis.crypto?.subtle === undefined) return undefined;\n try {\n const pair = await crypto.subtle.generateKey(KEY_ALGORITHM, false, ['deriveBits']);\n const hostKey = bytes(hostPublicKey);\n if (hostKey === undefined) return undefined;\n const peer = await crypto.subtle.importKey('raw', view(hostKey), KEY_ALGORITHM, false, []);\n const secret = await crypto.subtle.deriveBits({ name: 'ECDH', public: peer }, pair.privateKey, KEY_BITS);\n const clientPublicKey = base64Url(new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey)));\n return {\n channel: channelFrom(\n await derive(secret, 'c2s'),\n await derive(secret, 's2c'),\n new Uint8Array(NONCE_PREFIX_BYTES),\n ),\n clientPublicKey,\n };\n } catch {\n // A malformed host key or a context without ECDH; the caller falls back to\n // an unsealed channel and says so rather than failing silently.\n return undefined;\n }\n}\n\n/** Exposed so a test can build the browser side against a known secret. */\nexport async function channelFromSecret(\n secret: ArrayBuffer,\n sendDirection: SealedDirection,\n noncePrefix: Uint8Array,\n): Promise<SealedChannel> {\n const send = await derive(secret, sendDirection);\n const receive = await derive(secret, sendDirection === 'c2s' ? 's2c' : 'c2s');\n return channelFrom(send, receive, noncePrefix);\n}\n"],"mappings":"iDAuBMI,EAAgB,CAAE,KAAM,OAAQ,WAAY,OAAQ,EACpDC,EAAS,UAef,SAASC,EAAM,EAAuC,CACpD,IAAIC,EACJ,GAAI,CACF,EAAS,KAAKC,EAAM,WAAW,IAAK,GAAG,CAAC,CAAC,WAAW,IAAK,GAAG,CAAC,CAC/D,MAAQ,CACN,MACF,CACA,IAAMC,EAAM,IAAI,WAAW,IAAI,YAAYF,EAAO,MAAM,CAAC,EACzD,IAAK,IAAIG,EAAQ,EAAGA,EAAQH,EAAO,OAAQ,GAAS,EAAG,EAAIG,GAASH,EAAO,WAAWG,CAAK,EAC3F,OAAOD,CACT,CAEA,SAASE,EAAU,EAA2B,CAC5C,IAAIJ,EAAS,GACb,IAAK,IAAMK,KAAQJ,EAAO,GAAU,OAAO,aAAaI,CAAI,EAC5D,OAAO,KAAKL,CAAM,CAAC,CAAC,WAAW,IAAK,GAAG,CAAC,CAAC,WAAW,IAAK,GAAG,CAAC,CAAC,WAAW,IAAK,EAAE,CAClF,CAEA,SAASM,EAAK,EAAgC,CAC5C,IAAMC,EAAO,IAAI,WAAW,IAAI,YAAYN,EAAM,MAAM,CAAC,EAEzD,OADAM,EAAK,IAAIN,CAAK,EACPM,EAAK,MACd,CAEA,eAAeC,EAAO,EAAqB,EAAgD,CACzF,IAAMC,EAAW,MAAM,OAAO,OAAO,UAAU,MAAOC,EAAQ,OAAQ,GAAO,CAAC,WAAW,CAAC,EAC1F,OAAO,MAAM,OAAO,OAAO,UACzB,CACE,KAAM,OACN,KAAM,UACN,KAAMJ,EAAK,IAAI,YAAY,CAAC,CAAC,OAAO,cAAS,CAAC,EAC9C,KAAMA,EAAK,IAAI,YAAY,CAAC,CAAC,OAAOb,EAAAA,QAAQkB,CAAS,CAAC,CAAC,CACzD,EACAF,EACA,CAAE,KAAMX,EAAQ,OAAQ,GAAS,EACjC,GACA,CAAC,UAAW,SAAS,CACvB,CACF,CAEA,SAASc,EAAS,EAAoB,EAA6B,CACjE,IAAMC,kBAAQ,IAAI,WAAA,EAAsB,EAGxC,OAFAA,EAAM,IAAIC,EAAO,SAAS,EAAA,CAAqB,EAAG,CAAC,EACnD,IAAI,SAASD,EAAM,MAAM,CAAC,CAAC,aAAA,EAAiCE,EAAS,EAAK,EACnEF,CACT,CAEA,SAASG,EAAY,EAAoB,EAAuB,EAAwC,CACtG,IAAIC,EAAc,GACdC,EAAkB,CAAC,GAEvB,MAAO,CACL,MAAM,KAAK,EAAW,CACpB,GAAID,GAAe,OAAOvB,EAAAA,oBAAoB,EAAG,MAAO,CAAE,GAAI,GAAO,QAAS,WAAY,EAC1F,IAAMmB,EAAQD,EAASO,EAAaF,CAAW,EACzCG,EAAS,MAAM,OAAO,OAAO,QACjC,CAAE,KAAMtB,EAAQ,GAAIQ,EAAKO,CAAK,EAAG,UAAW,GAAS,EACrDQ,EACAf,EAAKgB,CAAS,CAChB,EAEA,MADA,IAAe,GACR,CACL,GAAI,GACJ,SAAU,CAAE,EAAA,EAAmB,EAAGlB,EAAUS,CAAK,EAAG,EAAGT,EAAU,IAAI,WAAWgB,CAAM,CAAC,CAAE,CAC3F,CACF,EAEA,MAAM,KAAK,EAAU,CAGnB,IAAMG,EAAQC,EACd,GAAI,OAAOD,GAAO,GAAM,UAAY,OAAOA,EAAM,GAAM,SAAU,MAAO,CAAE,GAAI,GAAO,QAAS,WAAY,EAC1G,GAAIA,EAAM,IAAA,EAAsB,MAAO,CAAE,GAAI,GAAO,QAAS,SAAU,EACvE,GAAI,CAAC5B,EAAAA,iBAAiB6B,CAAQ,EAAG,MAAO,CAAE,GAAI,GAAO,QAAS,WAAY,EAC1E,IAAMX,EAAQd,EAAMyB,EAAS,CAAC,EACxBC,EAAO1B,EAAMyB,EAAS,CAAC,EAE7B,GADIX,IAAU,IAAA,IAAaY,IAAS,IAAA,IAChCZ,EAAM,SAAA,IAA0BY,EAAK,OAAS,GAAW,MAAO,CAAE,GAAI,GAAO,QAAS,WAAY,EACtG,IAAMV,EAAU,IAAI,SAAST,EAAKO,CAAK,CAAC,CAAC,CAAC,aAAA,EAAiC,EAAK,EAChF,GAAIE,GAAWG,EAAiB,MAAO,CAAE,GAAI,GAAO,QAAS,QAAS,EACtE,IAAII,EACJ,GAAI,CACF,EAAY,MAAM,OAAO,OAAO,QAC9B,CAAE,KAAMxB,EAAQ,GAAIQ,EAAKO,CAAK,EAAG,UAAW,GAAS,EACrDa,EACApB,EAAKmB,CAAI,CACX,CACF,MAAQ,CAGN,MAAO,CAAE,GAAI,GAAO,QAAS,MAAO,CACtC,CAOA,OAJIV,GAAWG,EAAwB,CAAE,GAAI,GAAO,QAAS,QAAS,GAGtE,EAAkBH,EACX,CAAE,GAAI,GAAM,UAAW,IAAI,WAAWO,CAAS,CAAE,EAC1D,CACF,CACF,CASA,eAAsBK,EACpB,EAC0E,CAC1E,GAAI,WAAW,QAAQ,SAAW,IAAA,GAClC,GAAI,CACF,IAAMC,EAAO,MAAM,OAAO,OAAO,YAAY/B,EAAe,GAAO,CAAC,YAAY,CAAC,EAC3EgC,EAAU9B,EAAM+B,CAAa,EACnC,GAAID,IAAY,IAAA,GAAW,OAC3B,IAAME,EAAO,MAAM,OAAO,OAAO,UAAU,MAAOzB,EAAKuB,CAAO,EAAGhC,EAAe,GAAO,CAAC,CAAC,EACnFa,EAAS,MAAM,OAAO,OAAO,WAAW,CAAE,KAAM,OAAQ,OAAQqB,CAAK,EAAGH,EAAK,WAAY,GAAQ,EACjGI,EAAkB5B,EAAU,IAAI,WAAW,MAAM,OAAO,OAAO,UAAU,MAAOwB,EAAK,SAAS,CAAC,CAAC,EACtG,MAAO,CACL,QAASZ,EACP,MAAMR,EAAOE,EAAQ,KAAK,EAC1B,MAAMF,EAAOE,EAAQ,KAAK,kBAC1B,IAAI,WAAA,CAA6B,CACnC,EACA,iBACF,CACF,MAAQ,CAGN,MACF,CACF,CAGA,eAAsBuB,EACpB,EACA,EACA,EACwB,CAGxB,OAAOjB,EAAY,MAFAR,EAAOE,EAAQwB,CAAa,EAEtB,MADH1B,EAAOE,EAAQwB,IAAkB,MAAQ,MAAQ,KAAK,EAC1Cf,CAAW,CAC/C"}
1
+ {"version":3,"file":"index.cjs","names":["infoFor","MAX_MESSAGES_PER_KEY","isSealedEnvelope","require_sealedChannel","KEY_ALGORITHM","CIPHER","bytes","binary","value","out","index","base64Url","byte","view","copy","derive","material","secret","direction","nonceFor","nonce","prefix","counter","channelFrom","sendCounter","highestReceived","noncePrefix","sealed","sendKey","plaintext","shape","envelope","body","receiveKey","connectSealedChannel","pair","hostKey","hostPublicKey","peer","clientPublicKey","channelFromSecret","sendDirection"],"sources":["../../../src/services/browserSealedChannel/index.ts"],"sourcesContent":["import {\n MAX_MESSAGES_PER_KEY,\n NONCE_BYTES,\n NONCE_PREFIX_BYTES,\n SEALED_VERSION,\n type SealedDirection,\n type SealedEnvelope,\n type SealedFailure,\n infoFor,\n isSealedEnvelope,\n} from '../../types/sealedChannel';\n\n/**\n * The browser half of the sealed channel.\n *\n * Mirrors `nodeSealedChannel.ts` exactly, on WebCrypto instead of\n * `node:crypto`. Two implementations rather than one because a shared one would\n * drag node builtins into every browser bundle that imports it, and this\n * package is imported by plugins that ship to the page. The envelope contract\n * they both read lives in `../types/sealedChannel.ts`, and a round-trip test\n * pins the two together so they cannot drift.\n */\n\nconst KEY_ALGORITHM = { name: 'ECDH', namedCurve: 'P-256' } as const;\nconst CIPHER = 'AES-GCM';\nconst KEY_BITS = 256;\nconst TAG_BITS = 128;\nconst TAG_BYTES = 16;\nconst HKDF_SALT = 'po/sealed/v1';\n\nexport type SealResult = { ok: true; envelope: SealedEnvelope } | { ok: false; failure: SealedFailure };\nexport type OpenResult = { ok: true; plaintext: Uint8Array } | { ok: false; failure: SealedFailure };\n\nexport interface SealedChannel {\n seal(plaintext: Uint8Array): Promise<SealResult>;\n open(envelope: unknown): Promise<OpenResult>;\n}\n\n/** Undefined rather than a throw: a malformed field is a verdict, not an exception. */\nfunction bytes(value: string): Uint8Array | undefined {\n let binary: string;\n try {\n binary = atob(value.replaceAll('-', '+').replaceAll('_', '/'));\n } catch {\n return undefined;\n }\n const out = new Uint8Array(new ArrayBuffer(binary.length));\n for (let index = 0; index < binary.length; index += 1) out[index] = binary.charCodeAt(index);\n return out;\n}\n\nfunction base64Url(value: Uint8Array): string {\n let binary = '';\n for (const byte of value) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n}\n\nfunction view(value: Uint8Array): ArrayBuffer {\n const copy = new Uint8Array(new ArrayBuffer(value.length));\n copy.set(value);\n return copy.buffer;\n}\n\nasync function derive(secret: ArrayBuffer, direction: SealedDirection): Promise<CryptoKey> {\n const material = await crypto.subtle.importKey('raw', secret, 'HKDF', false, ['deriveKey']);\n return await crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: view(new TextEncoder().encode(HKDF_SALT)),\n info: view(new TextEncoder().encode(infoFor(direction))),\n },\n material,\n { name: CIPHER, length: KEY_BITS },\n false,\n ['encrypt', 'decrypt'],\n );\n}\n\nfunction nonceFor(prefix: Uint8Array, counter: bigint): Uint8Array {\n const nonce = new Uint8Array(NONCE_BYTES);\n nonce.set(prefix.subarray(0, NONCE_PREFIX_BYTES), 0);\n new DataView(nonce.buffer).setBigUint64(NONCE_PREFIX_BYTES, counter, false);\n return nonce;\n}\n\nfunction channelFrom(sendKey: CryptoKey, receiveKey: CryptoKey, noncePrefix: Uint8Array): SealedChannel {\n let sendCounter = 0n;\n let highestReceived = -1n;\n\n return {\n async seal(plaintext) {\n if (sendCounter >= BigInt(MAX_MESSAGES_PER_KEY)) return { ok: false, failure: 'exhausted' };\n const nonce = nonceFor(noncePrefix, sendCounter);\n const sealed = await crypto.subtle.encrypt(\n { name: CIPHER, iv: view(nonce), tagLength: TAG_BITS },\n sendKey,\n view(plaintext),\n );\n sendCounter += 1n;\n return {\n ok: true,\n envelope: { v: SEALED_VERSION, n: base64Url(nonce), c: base64Url(new Uint8Array(sealed)) },\n };\n },\n\n async open(envelope) {\n // Shape before version, matching the server: something with no `v` at all\n // is malformed rather than a protocol skew.\n const shape = envelope as { v?: unknown; n?: unknown; c?: unknown } | null | undefined;\n if (typeof shape?.n !== 'string' || typeof shape.c !== 'string') return { ok: false, failure: 'malformed' };\n if (shape.v !== SEALED_VERSION) return { ok: false, failure: 'version' };\n if (!isSealedEnvelope(envelope)) return { ok: false, failure: 'malformed' };\n const nonce = bytes(envelope.n);\n const body = bytes(envelope.c);\n if (nonce === undefined || body === undefined) return { ok: false, failure: 'malformed' };\n if (nonce.length !== NONCE_BYTES || body.length < TAG_BYTES) return { ok: false, failure: 'malformed' };\n const counter = new DataView(view(nonce)).getBigUint64(NONCE_PREFIX_BYTES, false);\n if (counter <= highestReceived) return { ok: false, failure: 'replay' };\n let plaintext: ArrayBuffer;\n try {\n plaintext = await crypto.subtle.decrypt(\n { name: CIPHER, iv: view(nonce), tagLength: TAG_BITS },\n receiveKey,\n view(body),\n );\n } catch {\n // A failed tag means the message was altered in transit; there is no\n // partial result worth salvaging.\n return { ok: false, failure: 'auth' };\n }\n // Another open may have finished while decrypt was pending, including\n // after its caller aborted. Never lower the authenticated high-water mark.\n if (counter <= highestReceived) return { ok: false, failure: 'replay' };\n // Only after the tag verified, so a forged high counter cannot lock out\n // every message that legitimately follows it. Keep the recheck and commit synchronous.\n highestReceived = counter;\n return { ok: true, plaintext: new Uint8Array(plaintext) };\n },\n };\n}\n\n/**\n * Completes the handshake against the host key read out of the QR fragment.\n *\n * That key never travelled through the tunnel, which is what makes this\n * authenticated: a relay can watch the client's public key go past and learn\n * nothing, and it cannot substitute the host's because it never carried it.\n */\nexport async function connectSealedChannel(\n hostPublicKey: string,\n): Promise<{ channel: SealedChannel; clientPublicKey: string } | undefined> {\n if (globalThis.crypto?.subtle === undefined) return undefined;\n try {\n const pair = await crypto.subtle.generateKey(KEY_ALGORITHM, false, ['deriveBits']);\n const hostKey = bytes(hostPublicKey);\n if (hostKey === undefined) return undefined;\n const peer = await crypto.subtle.importKey('raw', view(hostKey), KEY_ALGORITHM, false, []);\n const secret = await crypto.subtle.deriveBits({ name: 'ECDH', public: peer }, pair.privateKey, KEY_BITS);\n const clientPublicKey = base64Url(new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey)));\n return {\n channel: channelFrom(\n await derive(secret, 'c2s'),\n await derive(secret, 's2c'),\n new Uint8Array(NONCE_PREFIX_BYTES),\n ),\n clientPublicKey,\n };\n } catch {\n // A malformed host key or a context without ECDH; the caller falls back to\n // an unsealed channel and says so rather than failing silently.\n return undefined;\n }\n}\n\n/** Exposed so a test can build the browser side against a known secret. */\nexport async function channelFromSecret(\n secret: ArrayBuffer,\n sendDirection: SealedDirection,\n noncePrefix: Uint8Array,\n): Promise<SealedChannel> {\n const send = await derive(secret, sendDirection);\n const receive = await derive(secret, sendDirection === 'c2s' ? 's2c' : 'c2s');\n return channelFrom(send, receive, noncePrefix);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["KEY_ALGORITHM","CIPHER","bytes","binary","value","out","index","base64Url","byte","view","copy","derive","material","secret","infoFor","direction","nonceFor","nonce","prefix","counter","channelFrom","sendCounter","highestReceived","MAX_MESSAGES_PER_KEY","noncePrefix","sealed","sendKey","plaintext","shape","envelope","isSealedEnvelope","body","receiveKey","connectSealedChannel","pair","hostKey","hostPublicKey","peer","clientPublicKey","channelFromSecret","sendDirection"],"sources":["../../../src/services/browserSealedChannel/index.ts"],"sourcesContent":["import {\n MAX_MESSAGES_PER_KEY,\n NONCE_BYTES,\n NONCE_PREFIX_BYTES,\n SEALED_VERSION,\n type SealedDirection,\n type SealedEnvelope,\n type SealedFailure,\n infoFor,\n isSealedEnvelope,\n} from '../../types/sealedChannel';\n\n/**\n * The browser half of the sealed channel.\n *\n * Mirrors `nodeSealedChannel.ts` exactly, on WebCrypto instead of\n * `node:crypto`. Two implementations rather than one because a shared one would\n * drag node builtins into every browser bundle that imports it, and this\n * package is imported by plugins that ship to the page. The envelope contract\n * they both read lives in `../types/sealedChannel.ts`, and a round-trip test\n * pins the two together so they cannot drift.\n */\n\nconst KEY_ALGORITHM = { name: 'ECDH', namedCurve: 'P-256' } as const;\nconst CIPHER = 'AES-GCM';\nconst KEY_BITS = 256;\nconst TAG_BITS = 128;\nconst TAG_BYTES = 16;\nconst HKDF_SALT = 'po/sealed/v1';\n\nexport type SealResult = { ok: true; envelope: SealedEnvelope } | { ok: false; failure: SealedFailure };\nexport type OpenResult = { ok: true; plaintext: Uint8Array } | { ok: false; failure: SealedFailure };\n\nexport interface SealedChannel {\n seal(plaintext: Uint8Array): Promise<SealResult>;\n open(envelope: unknown): Promise<OpenResult>;\n}\n\n/** Undefined rather than a throw: a malformed field is a verdict, not an exception. */\nfunction bytes(value: string): Uint8Array | undefined {\n let binary: string;\n try {\n binary = atob(value.replaceAll('-', '+').replaceAll('_', '/'));\n } catch {\n return undefined;\n }\n const out = new Uint8Array(new ArrayBuffer(binary.length));\n for (let index = 0; index < binary.length; index += 1) out[index] = binary.charCodeAt(index);\n return out;\n}\n\nfunction base64Url(value: Uint8Array): string {\n let binary = '';\n for (const byte of value) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n}\n\nfunction view(value: Uint8Array): ArrayBuffer {\n const copy = new Uint8Array(new ArrayBuffer(value.length));\n copy.set(value);\n return copy.buffer;\n}\n\nasync function derive(secret: ArrayBuffer, direction: SealedDirection): Promise<CryptoKey> {\n const material = await crypto.subtle.importKey('raw', secret, 'HKDF', false, ['deriveKey']);\n return await crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: view(new TextEncoder().encode(HKDF_SALT)),\n info: view(new TextEncoder().encode(infoFor(direction))),\n },\n material,\n { name: CIPHER, length: KEY_BITS },\n false,\n ['encrypt', 'decrypt'],\n );\n}\n\nfunction nonceFor(prefix: Uint8Array, counter: bigint): Uint8Array {\n const nonce = new Uint8Array(NONCE_BYTES);\n nonce.set(prefix.subarray(0, NONCE_PREFIX_BYTES), 0);\n new DataView(nonce.buffer).setBigUint64(NONCE_PREFIX_BYTES, counter, false);\n return nonce;\n}\n\nfunction channelFrom(sendKey: CryptoKey, receiveKey: CryptoKey, noncePrefix: Uint8Array): SealedChannel {\n let sendCounter = 0n;\n let highestReceived = -1n;\n\n return {\n async seal(plaintext) {\n if (sendCounter >= BigInt(MAX_MESSAGES_PER_KEY)) return { ok: false, failure: 'exhausted' };\n const nonce = nonceFor(noncePrefix, sendCounter);\n const sealed = await crypto.subtle.encrypt(\n { name: CIPHER, iv: view(nonce), tagLength: TAG_BITS },\n sendKey,\n view(plaintext),\n );\n sendCounter += 1n;\n return {\n ok: true,\n envelope: { v: SEALED_VERSION, n: base64Url(nonce), c: base64Url(new Uint8Array(sealed)) },\n };\n },\n\n async open(envelope) {\n // Shape before version, matching the server: something with no `v` at all\n // is malformed rather than a protocol skew.\n const shape = envelope as { v?: unknown; n?: unknown; c?: unknown } | null | undefined;\n if (typeof shape?.n !== 'string' || typeof shape.c !== 'string') return { ok: false, failure: 'malformed' };\n if (shape.v !== SEALED_VERSION) return { ok: false, failure: 'version' };\n if (!isSealedEnvelope(envelope)) return { ok: false, failure: 'malformed' };\n const nonce = bytes(envelope.n);\n const body = bytes(envelope.c);\n if (nonce === undefined || body === undefined) return { ok: false, failure: 'malformed' };\n if (nonce.length !== NONCE_BYTES || body.length < TAG_BYTES) return { ok: false, failure: 'malformed' };\n const counter = new DataView(view(nonce)).getBigUint64(NONCE_PREFIX_BYTES, false);\n if (counter <= highestReceived) return { ok: false, failure: 'replay' };\n let plaintext: ArrayBuffer;\n try {\n plaintext = await crypto.subtle.decrypt(\n { name: CIPHER, iv: view(nonce), tagLength: TAG_BITS },\n receiveKey,\n view(body),\n );\n } catch {\n // A failed tag means the message was altered in transit; there is no\n // partial result worth salvaging.\n return { ok: false, failure: 'auth' };\n }\n // Another open may have finished while decrypt was pending, including\n // after its caller aborted. Never lower the authenticated high-water mark.\n if (counter <= highestReceived) return { ok: false, failure: 'replay' };\n // Only after the tag verified, so a forged high counter cannot lock out\n // every message that legitimately follows it. Keep the recheck and commit synchronous.\n highestReceived = counter;\n return { ok: true, plaintext: new Uint8Array(plaintext) };\n },\n };\n}\n\n/**\n * Completes the handshake against the host key read out of the QR fragment.\n *\n * That key never travelled through the tunnel, which is what makes this\n * authenticated: a relay can watch the client's public key go past and learn\n * nothing, and it cannot substitute the host's because it never carried it.\n */\nexport async function connectSealedChannel(\n hostPublicKey: string,\n): Promise<{ channel: SealedChannel; clientPublicKey: string } | undefined> {\n if (globalThis.crypto?.subtle === undefined) return undefined;\n try {\n const pair = await crypto.subtle.generateKey(KEY_ALGORITHM, false, ['deriveBits']);\n const hostKey = bytes(hostPublicKey);\n if (hostKey === undefined) return undefined;\n const peer = await crypto.subtle.importKey('raw', view(hostKey), KEY_ALGORITHM, false, []);\n const secret = await crypto.subtle.deriveBits({ name: 'ECDH', public: peer }, pair.privateKey, KEY_BITS);\n const clientPublicKey = base64Url(new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey)));\n return {\n channel: channelFrom(\n await derive(secret, 'c2s'),\n await derive(secret, 's2c'),\n new Uint8Array(NONCE_PREFIX_BYTES),\n ),\n clientPublicKey,\n };\n } catch {\n // A malformed host key or a context without ECDH; the caller falls back to\n // an unsealed channel and says so rather than failing silently.\n return undefined;\n }\n}\n\n/** Exposed so a test can build the browser side against a known secret. */\nexport async function channelFromSecret(\n secret: ArrayBuffer,\n sendDirection: SealedDirection,\n noncePrefix: Uint8Array,\n): Promise<SealedChannel> {\n const send = await derive(secret, sendDirection);\n const receive = await derive(secret, sendDirection === 'c2s' ? 's2c' : 'c2s');\n return channelFrom(send, receive, noncePrefix);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["KEY_ALGORITHM","CIPHER","bytes","binary","value","out","index","base64Url","byte","view","copy","derive","material","secret","infoFor","direction","nonceFor","nonce","prefix","counter","channelFrom","sendCounter","highestReceived","MAX_MESSAGES_PER_KEY","noncePrefix","sealed","sendKey","plaintext","shape","envelope","isSealedEnvelope","body","receiveKey","connectSealedChannel","pair","hostKey","hostPublicKey","peer","clientPublicKey","channelFromSecret","sendDirection"],"sources":["../../../src/services/browserSealedChannel/index.ts"],"sourcesContent":["import {\n MAX_MESSAGES_PER_KEY,\n NONCE_BYTES,\n NONCE_PREFIX_BYTES,\n SEALED_VERSION,\n type SealedDirection,\n type SealedEnvelope,\n type SealedFailure,\n infoFor,\n isSealedEnvelope,\n} from '../../types/sealedChannel';\n\n/**\n * The browser half of the sealed channel.\n *\n * Mirrors `nodeSealedChannel.ts` exactly, on WebCrypto instead of\n * `node:crypto`. Two implementations rather than one because a shared one would\n * drag node builtins into every browser bundle that imports it, and this\n * package is imported by plugins that ship to the page. The envelope contract\n * they both read lives in `../types/sealedChannel.ts`, and a round-trip test\n * pins the two together so they cannot drift.\n */\n\nconst KEY_ALGORITHM = { name: 'ECDH', namedCurve: 'P-256' } as const;\nconst CIPHER = 'AES-GCM';\nconst KEY_BITS = 256;\nconst TAG_BITS = 128;\nconst TAG_BYTES = 16;\nconst HKDF_SALT = 'po/sealed/v1';\n\nexport type SealResult = { ok: true; envelope: SealedEnvelope } | { ok: false; failure: SealedFailure };\nexport type OpenResult = { ok: true; plaintext: Uint8Array } | { ok: false; failure: SealedFailure };\n\nexport interface SealedChannel {\n seal(plaintext: Uint8Array): Promise<SealResult>;\n open(envelope: unknown): Promise<OpenResult>;\n}\n\n/** Undefined rather than a throw: a malformed field is a verdict, not an exception. */\nfunction bytes(value: string): Uint8Array | undefined {\n let binary: string;\n try {\n binary = atob(value.replaceAll('-', '+').replaceAll('_', '/'));\n } catch {\n return undefined;\n }\n const out = new Uint8Array(new ArrayBuffer(binary.length));\n for (let index = 0; index < binary.length; index += 1) out[index] = binary.charCodeAt(index);\n return out;\n}\n\nfunction base64Url(value: Uint8Array): string {\n let binary = '';\n for (const byte of value) binary += String.fromCharCode(byte);\n return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');\n}\n\nfunction view(value: Uint8Array): ArrayBuffer {\n const copy = new Uint8Array(new ArrayBuffer(value.length));\n copy.set(value);\n return copy.buffer;\n}\n\nasync function derive(secret: ArrayBuffer, direction: SealedDirection): Promise<CryptoKey> {\n const material = await crypto.subtle.importKey('raw', secret, 'HKDF', false, ['deriveKey']);\n return await crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: view(new TextEncoder().encode(HKDF_SALT)),\n info: view(new TextEncoder().encode(infoFor(direction))),\n },\n material,\n { name: CIPHER, length: KEY_BITS },\n false,\n ['encrypt', 'decrypt'],\n );\n}\n\nfunction nonceFor(prefix: Uint8Array, counter: bigint): Uint8Array {\n const nonce = new Uint8Array(NONCE_BYTES);\n nonce.set(prefix.subarray(0, NONCE_PREFIX_BYTES), 0);\n new DataView(nonce.buffer).setBigUint64(NONCE_PREFIX_BYTES, counter, false);\n return nonce;\n}\n\nfunction channelFrom(sendKey: CryptoKey, receiveKey: CryptoKey, noncePrefix: Uint8Array): SealedChannel {\n let sendCounter = 0n;\n let highestReceived = -1n;\n\n return {\n async seal(plaintext) {\n if (sendCounter >= BigInt(MAX_MESSAGES_PER_KEY)) return { ok: false, failure: 'exhausted' };\n const nonce = nonceFor(noncePrefix, sendCounter);\n const sealed = await crypto.subtle.encrypt(\n { name: CIPHER, iv: view(nonce), tagLength: TAG_BITS },\n sendKey,\n view(plaintext),\n );\n sendCounter += 1n;\n return {\n ok: true,\n envelope: { v: SEALED_VERSION, n: base64Url(nonce), c: base64Url(new Uint8Array(sealed)) },\n };\n },\n\n async open(envelope) {\n // Shape before version, matching the server: something with no `v` at all\n // is malformed rather than a protocol skew.\n const shape = envelope as { v?: unknown; n?: unknown; c?: unknown } | null | undefined;\n if (typeof shape?.n !== 'string' || typeof shape.c !== 'string') return { ok: false, failure: 'malformed' };\n if (shape.v !== SEALED_VERSION) return { ok: false, failure: 'version' };\n if (!isSealedEnvelope(envelope)) return { ok: false, failure: 'malformed' };\n const nonce = bytes(envelope.n);\n const body = bytes(envelope.c);\n if (nonce === undefined || body === undefined) return { ok: false, failure: 'malformed' };\n if (nonce.length !== NONCE_BYTES || body.length < TAG_BYTES) return { ok: false, failure: 'malformed' };\n const counter = new DataView(view(nonce)).getBigUint64(NONCE_PREFIX_BYTES, false);\n if (counter <= highestReceived) return { ok: false, failure: 'replay' };\n let plaintext: ArrayBuffer;\n try {\n plaintext = await crypto.subtle.decrypt(\n { name: CIPHER, iv: view(nonce), tagLength: TAG_BITS },\n receiveKey,\n view(body),\n );\n } catch {\n // A failed tag means the message was altered in transit; there is no\n // partial result worth salvaging.\n return { ok: false, failure: 'auth' };\n }\n // Another open may have finished while decrypt was pending, including\n // after its caller aborted. Never lower the authenticated high-water mark.\n if (counter <= highestReceived) return { ok: false, failure: 'replay' };\n // Only after the tag verified, so a forged high counter cannot lock out\n // every message that legitimately follows it. Keep the recheck and commit synchronous.\n highestReceived = counter;\n return { ok: true, plaintext: new Uint8Array(plaintext) };\n },\n };\n}\n\n/**\n * Completes the handshake against the host key read out of the QR fragment.\n *\n * That key never travelled through the tunnel, which is what makes this\n * authenticated: a relay can watch the client's public key go past and learn\n * nothing, and it cannot substitute the host's because it never carried it.\n */\nexport async function connectSealedChannel(\n hostPublicKey: string,\n): Promise<{ channel: SealedChannel; clientPublicKey: string } | undefined> {\n if (globalThis.crypto?.subtle === undefined) return undefined;\n try {\n const pair = await crypto.subtle.generateKey(KEY_ALGORITHM, false, ['deriveBits']);\n const hostKey = bytes(hostPublicKey);\n if (hostKey === undefined) return undefined;\n const peer = await crypto.subtle.importKey('raw', view(hostKey), KEY_ALGORITHM, false, []);\n const secret = await crypto.subtle.deriveBits({ name: 'ECDH', public: peer }, pair.privateKey, KEY_BITS);\n const clientPublicKey = base64Url(new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey)));\n return {\n channel: channelFrom(\n await derive(secret, 'c2s'),\n await derive(secret, 's2c'),\n new Uint8Array(NONCE_PREFIX_BYTES),\n ),\n clientPublicKey,\n };\n } catch {\n // A malformed host key or a context without ECDH; the caller falls back to\n // an unsealed channel and says so rather than failing silently.\n return undefined;\n }\n}\n\n/** Exposed so a test can build the browser side against a known secret. */\nexport async function channelFromSecret(\n secret: ArrayBuffer,\n sendDirection: SealedDirection,\n noncePrefix: Uint8Array,\n): Promise<SealedChannel> {\n const send = await derive(secret, sendDirection);\n const receive = await derive(secret, sendDirection === 'c2s' ? 's2c' : 'c2s');\n return channelFrom(send, receive, noncePrefix);\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["fs","path","randomBytes","DatabaseSync","generateKeyPairSync","createHash","createPrivateKey","isBundleManifest","createSign","canonicalManifest","attestationBytes","createPublicKey","require_runtime","require_bundleManifest","node_crypto","node_fs","node_path","node_sqlite","SIGNING_FILE","LOCK_FILE","SIGN_ALGORITHM","SKIPPED","PROFILES","MIME_TYPES","ensureStateDirectory","stateDir","stat","withStateLock","lockPath","temporary","initial","error","descriptor","header","database","operation","atomicWriteState","destination","state","parseState","value","keyPath","candidate","revision","derived","publicKeyOf","loadOrCreateState","parsed","pair","reserveRevision","expectedPublicKey","next","contentTypeFor","filePath","walk","entries","current","entry","full","root","into","bytes","segment","createBundleSigner","key","privateKey","cache","cacheKeyOf","profile","assetsDir","signFresh","rootStat","assets","left","right","manifest","sign","cacheKey","signed","tag","fields","privateKeyPem"],"sources":["../../../src/services/bundleSigner/index.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n randomBytes,\n} from 'node:crypto';\n// sync: Public createBundleSigner and BundleSigner.sign/refresh contracts must remain synchronous until phase 2.\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n attestationBytes,\n canonicalManifest,\n isBundleManifest,\n} from '../../types/bundleManifest';\n\nconst SIGNING_FILE = 'signing.json';\nconst LOCK_FILE = 'signing.lock';\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst SKIPPED = new Set(['.map']);\nconst PROFILES: readonly BundleProfile[] = ['web', 'native'];\nconst MIME_TYPES: Readonly<Record<string, string>> = {\n '.css': 'text/css',\n '.gif': 'image/gif',\n '.html': 'text/html',\n '.ico': 'image/x-icon',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.mjs': 'text/javascript',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n};\n\nexport interface BundleSigner {\n /** Base64url SPKI for an independently trusted verifier to pin. */\n publicKey(): string;\n /**\n * Hashes and signs once per asset directory and profile. Undefined when the bundle is empty. Throws\n * when the assets do not match the profile.\n */\n sign(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates and immediately rebuilds one cached manifest. */\n refresh(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates one cached directory under every profile, or every cached directory when omitted. */\n invalidate(assetsDir?: string): void;\n /**\n * Base64url IEEE P1363 signature over `attestationBytes(tag, fields)` with the bundle key. Throws on a\n * malformed tag.\n */\n attest(tag: string, fields: readonly string[]): string;\n}\n\ninterface StoredState {\n privateKey: string;\n publicKey: string;\n revision: number;\n}\n\nfunction ensureStateDirectory(stateDir: string): void {\n fs.mkdirSync(stateDir, { recursive: true, mode: DIRECTORY_MODE });\n const stat = fs.lstatSync(stateDir);\n if (!stat.isDirectory() || stat.isSymbolicLink())\n throw new Error(`bundle signing state is not a directory: ${stateDir}`);\n fs.chmodSync(stateDir, DIRECTORY_MODE);\n}\n\nfunction withStateLock<T>(stateDir: string, operation: () => T): T {\n ensureStateDirectory(stateDir);\n const lockPath = path.join(stateDir, LOCK_FILE);\n if (!fs.existsSync(lockPath)) {\n const temporary = path.join(stateDir, `.${LOCK_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n fs.closeSync(fs.openSync(temporary, 'wx', FILE_MODE));\n try {\n const initial = new DatabaseSync(temporary);\n try {\n initial.exec('PRAGMA user_version = 1');\n } finally {\n initial.close();\n }\n // Publish a complete database, never an empty file another writer could mistake for a legacy lock.\n try {\n fs.linkSync(temporary, lockPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n }\n } finally {\n fs.rmSync(temporary, { force: true });\n }\n }\n if (!fs.lstatSync(lockPath).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const descriptor = fs.openSync(lockPath, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW);\n try {\n if (!fs.fstatSync(descriptor).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const header = Buffer.alloc(16);\n fs.readSync(descriptor, header, 0, header.length, 0);\n if (header.toString() !== 'SQLite format 3\\u0000') {\n // The old empty wx lock has no owner information. Never assume it is stale or unlink a live writer's lock.\n throw new Error(\n `legacy or invalid bundle signing state lock: ${lockPath}; stop all Po servers and inspect the lock before manual recovery`,\n );\n }\n fs.fchmodSync(descriptor, FILE_MODE);\n const database = new DatabaseSync(lockPath, { timeout: 5000 });\n try {\n // The kernel releases this lock even on SIGKILL. Keep its inode so concurrent writers share one lock.\n database.exec('BEGIN EXCLUSIVE');\n return operation();\n } finally {\n database.close();\n }\n } finally {\n fs.closeSync(descriptor);\n }\n}\n\nfunction atomicWriteState(stateDir: string, state: StoredState): void {\n const destination = path.join(stateDir, SIGNING_FILE);\n const temporary = path.join(stateDir, `.${SIGNING_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n const descriptor = fs.openSync(temporary, 'wx', FILE_MODE);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify(state, undefined, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n try {\n fs.renameSync(temporary, destination);\n fs.chmodSync(destination, FILE_MODE);\n } catch (error) {\n fs.rmSync(temporary, { force: true });\n throw error;\n }\n}\n\nfunction parseState(value: unknown, keyPath: string): StoredState {\n if (typeof value !== 'object' || value === null) throw new Error(`invalid bundle signing state: ${keyPath}`);\n const candidate = value as Partial<StoredState>;\n if (typeof candidate.privateKey !== 'string' || typeof candidate.publicKey !== 'string') {\n throw new Error(`invalid bundle signing state: ${keyPath}`);\n }\n const revision = candidate.revision === undefined ? 0 : candidate.revision;\n if (!Number.isSafeInteger(revision) || revision < 0) throw new Error(`invalid bundle signing revision: ${keyPath}`);\n let derived: string;\n try {\n derived = publicKeyOf(candidate.privateKey);\n } catch {\n throw new Error(`invalid bundle signing private key: ${keyPath}`);\n }\n if (derived !== candidate.publicKey) throw new Error(`bundle signing keys do not match: ${keyPath}`);\n return { privateKey: candidate.privateKey, publicKey: candidate.publicKey, revision };\n}\n\nfunction loadOrCreateState(stateDir: string): StoredState {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n try {\n const stat = fs.lstatSync(keyPath);\n if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`invalid bundle signing state file: ${keyPath}`);\n const parsed: unknown = JSON.parse(fs.readFileSync(keyPath, 'utf8'));\n const state = parseState(parsed, keyPath);\n fs.chmodSync(keyPath, FILE_MODE);\n if ((parsed as Partial<StoredState>).revision === undefined) atomicWriteState(stateDir, state);\n return state;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n\n const pair = generateKeyPairSync('ec', {\n namedCurve: CURVE,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' },\n publicKeyEncoding: { type: 'spki', format: 'der' },\n });\n const state: StoredState = {\n privateKey: pair.privateKey,\n publicKey: Buffer.from(pair.publicKey).toString('base64url'),\n revision: 0,\n };\n atomicWriteState(stateDir, state);\n return state;\n });\n}\n\nfunction reserveRevision(stateDir: string, expectedPublicKey: string): number {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n const state = parseState(JSON.parse(fs.readFileSync(keyPath, 'utf8')) as unknown, keyPath);\n if (state.publicKey !== expectedPublicKey)\n throw new Error('bundle signing key changed while the signer was active');\n if (state.revision === Number.MAX_SAFE_INTEGER) throw new Error('bundle signing revision is exhausted');\n const next = { ...state, revision: state.revision + 1 };\n atomicWriteState(stateDir, next);\n return next.revision;\n });\n}\n\nfunction contentTypeFor(filePath: string): string {\n return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/** Every servable regular file under the asset root, as browser paths. */\nfunction walk(root: string, current: string, into: BundleAsset[]): void {\n const entries = fs.readdirSync(current, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(current, entry.name);\n const stat = fs.lstatSync(full);\n if (stat.isSymbolicLink()) throw new Error(`refusing to sign symbolic link: ${full}`);\n if (stat.isDirectory()) {\n walk(root, full, into);\n continue;\n }\n if (!stat.isFile()) throw new Error(`refusing to omit unsupported asset: ${full}`);\n if (SKIPPED.has(path.extname(entry.name))) continue;\n const bytes = fs.readFileSync(full);\n into.push({\n path: `/${path\n .relative(root, full)\n .split(path.sep)\n .map((segment) => encodeURIComponent(segment))\n .join('/')}`,\n sha256: createHash('sha256').update(bytes).digest('hex'),\n byteLength: bytes.byteLength,\n contentType: contentTypeFor(full),\n });\n }\n}\n\nexport function createBundleSigner(stateDir: string, _onNotice: (message: string) => void = () => {}): BundleSigner {\n const key = loadOrCreateState(stateDir);\n const privateKey = createPrivateKey(key.privateKey);\n const cache = new Map<string, SignedBundleManifest | undefined>();\n const cacheKeyOf = (assetsDir: string, profile: BundleProfile): string => `${profile}:${path.resolve(assetsDir)}`;\n\n const signFresh = (assetsDir: string, profile: BundleProfile): SignedBundleManifest | undefined => {\n const rootStat = fs.lstatSync(assetsDir);\n if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {\n throw new Error(`refusing to sign an unsafe asset root: ${assetsDir}`);\n }\n const assets: BundleAsset[] = [];\n walk(assetsDir, assetsDir, assets);\n if (assets.length === 0) return undefined;\n assets.sort((left, right) => left.path.localeCompare(right.path));\n const manifest: BundleManifest = {\n version: BUNDLE_MANIFEST_VERSION,\n revision: reserveRevision(stateDir, key.publicKey),\n builtAt: Date.now(),\n assets,\n };\n if (!isBundleManifest(manifest, profile)) throw new Error(`refusing to sign an invalid ${profile} bundle manifest`);\n const signature = createSign(SIGN_ALGORITHM)\n .update(canonicalManifest(manifest))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n return { manifest, signature, publicKey: key.publicKey };\n };\n\n const sign = (assetsDir: string, profile: BundleProfile = 'web'): SignedBundleManifest | undefined => {\n const cacheKey = cacheKeyOf(assetsDir, profile);\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n const signed = signFresh(path.resolve(assetsDir), profile);\n cache.set(cacheKey, signed);\n return signed;\n };\n\n return {\n publicKey: () => key.publicKey,\n sign,\n refresh(assetsDir, profile = 'web') {\n cache.delete(cacheKeyOf(assetsDir, profile));\n return sign(assetsDir, profile);\n },\n invalidate(assetsDir) {\n if (assetsDir === undefined) {\n cache.clear();\n return;\n }\n for (const profile of PROFILES) cache.delete(cacheKeyOf(assetsDir, profile));\n },\n attest(tag, fields) {\n return createSign(SIGN_ALGORITHM)\n .update(attestationBytes(tag, fields))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n },\n };\n}\n\n/** Re-derives the public key from a stored private key. */\nexport function publicKeyOf(privateKeyPem: string): string {\n return Buffer.from(createPublicKey(privateKeyPem).export({ type: 'spki', format: 'der' }) as Buffer).toString(\n 'base64url',\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["fs","path","randomBytes","DatabaseSync","generateKeyPairSync","createHash","createPrivateKey","isBundleManifest","createSign","canonicalManifest","attestationBytes","createPublicKey","require_runtime","require_bundleManifest","node_crypto","node_fs","node_path","node_sqlite","SIGNING_FILE","LOCK_FILE","SIGN_ALGORITHM","SKIPPED","PROFILES","MIME_TYPES","ensureStateDirectory","stateDir","stat","withStateLock","lockPath","temporary","initial","error","descriptor","header","database","operation","atomicWriteState","destination","state","parseState","value","keyPath","candidate","revision","derived","publicKeyOf","loadOrCreateState","parsed","pair","reserveRevision","expectedPublicKey","next","contentTypeFor","filePath","walk","entries","current","entry","full","root","into","bytes","segment","createBundleSigner","key","privateKey","cache","cacheKeyOf","profile","assetsDir","signFresh","rootStat","assets","left","right","manifest","sign","cacheKey","signed","tag","fields","privateKeyPem"],"sources":["../../../src/services/bundleSigner/index.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n randomBytes,\n} from 'node:crypto';\n// sync: Public createBundleSigner and BundleSigner.sign/refresh contracts must remain synchronous until phase 2.\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n attestationBytes,\n canonicalManifest,\n isBundleManifest,\n} from '../../types/bundleManifest';\n\nconst SIGNING_FILE = 'signing.json';\nconst LOCK_FILE = 'signing.lock';\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst SKIPPED = new Set(['.map']);\nconst PROFILES: readonly BundleProfile[] = ['web', 'native'];\nconst MIME_TYPES: Readonly<Record<string, string>> = {\n '.css': 'text/css',\n '.gif': 'image/gif',\n '.html': 'text/html',\n '.ico': 'image/x-icon',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.mjs': 'text/javascript',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n};\n\nexport interface BundleSigner {\n /** Base64url SPKI for an independently trusted verifier to pin. */\n publicKey(): string;\n /**\n * Hashes and signs once per asset directory and profile. Undefined when the bundle is empty. Throws\n * when the assets do not match the profile.\n */\n sign(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates and immediately rebuilds one cached manifest. */\n refresh(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates one cached directory under every profile, or every cached directory when omitted. */\n invalidate(assetsDir?: string): void;\n /**\n * Base64url IEEE P1363 signature over `attestationBytes(tag, fields)` with the bundle key. Throws on a\n * malformed tag.\n */\n attest(tag: string, fields: readonly string[]): string;\n}\n\ninterface StoredState {\n privateKey: string;\n publicKey: string;\n revision: number;\n}\n\nfunction ensureStateDirectory(stateDir: string): void {\n fs.mkdirSync(stateDir, { recursive: true, mode: DIRECTORY_MODE });\n const stat = fs.lstatSync(stateDir);\n if (!stat.isDirectory() || stat.isSymbolicLink())\n throw new Error(`bundle signing state is not a directory: ${stateDir}`);\n fs.chmodSync(stateDir, DIRECTORY_MODE);\n}\n\nfunction withStateLock<T>(stateDir: string, operation: () => T): T {\n ensureStateDirectory(stateDir);\n const lockPath = path.join(stateDir, LOCK_FILE);\n if (!fs.existsSync(lockPath)) {\n const temporary = path.join(stateDir, `.${LOCK_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n fs.closeSync(fs.openSync(temporary, 'wx', FILE_MODE));\n try {\n const initial = new DatabaseSync(temporary);\n try {\n initial.exec('PRAGMA user_version = 1');\n } finally {\n initial.close();\n }\n // Publish a complete database, never an empty file another writer could mistake for a legacy lock.\n try {\n fs.linkSync(temporary, lockPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n }\n } finally {\n fs.rmSync(temporary, { force: true });\n }\n }\n if (!fs.lstatSync(lockPath).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const descriptor = fs.openSync(lockPath, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW);\n try {\n if (!fs.fstatSync(descriptor).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const header = Buffer.alloc(16);\n fs.readSync(descriptor, header, 0, header.length, 0);\n if (header.toString() !== 'SQLite format 3\\u0000') {\n // The old empty wx lock has no owner information. Never assume it is stale or unlink a live writer's lock.\n throw new Error(\n `legacy or invalid bundle signing state lock: ${lockPath}; stop all Po servers and inspect the lock before manual recovery`,\n );\n }\n fs.fchmodSync(descriptor, FILE_MODE);\n const database = new DatabaseSync(lockPath, { timeout: 5000 });\n try {\n // The kernel releases this lock even on SIGKILL. Keep its inode so concurrent writers share one lock.\n database.exec('BEGIN EXCLUSIVE');\n return operation();\n } finally {\n database.close();\n }\n } finally {\n fs.closeSync(descriptor);\n }\n}\n\nfunction atomicWriteState(stateDir: string, state: StoredState): void {\n const destination = path.join(stateDir, SIGNING_FILE);\n const temporary = path.join(stateDir, `.${SIGNING_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n const descriptor = fs.openSync(temporary, 'wx', FILE_MODE);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify(state, undefined, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n try {\n fs.renameSync(temporary, destination);\n fs.chmodSync(destination, FILE_MODE);\n } catch (error) {\n fs.rmSync(temporary, { force: true });\n throw error;\n }\n}\n\nfunction parseState(value: unknown, keyPath: string): StoredState {\n if (typeof value !== 'object' || value === null) throw new Error(`invalid bundle signing state: ${keyPath}`);\n const candidate = value as Partial<StoredState>;\n if (typeof candidate.privateKey !== 'string' || typeof candidate.publicKey !== 'string') {\n throw new Error(`invalid bundle signing state: ${keyPath}`);\n }\n const revision = candidate.revision === undefined ? 0 : candidate.revision;\n if (!Number.isSafeInteger(revision) || revision < 0) throw new Error(`invalid bundle signing revision: ${keyPath}`);\n let derived: string;\n try {\n derived = publicKeyOf(candidate.privateKey);\n } catch {\n throw new Error(`invalid bundle signing private key: ${keyPath}`);\n }\n if (derived !== candidate.publicKey) throw new Error(`bundle signing keys do not match: ${keyPath}`);\n return { privateKey: candidate.privateKey, publicKey: candidate.publicKey, revision };\n}\n\nfunction loadOrCreateState(stateDir: string): StoredState {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n try {\n const stat = fs.lstatSync(keyPath);\n if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`invalid bundle signing state file: ${keyPath}`);\n const parsed: unknown = JSON.parse(fs.readFileSync(keyPath, 'utf8'));\n const state = parseState(parsed, keyPath);\n fs.chmodSync(keyPath, FILE_MODE);\n if ((parsed as Partial<StoredState>).revision === undefined) atomicWriteState(stateDir, state);\n return state;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n\n const pair = generateKeyPairSync('ec', {\n namedCurve: CURVE,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' },\n publicKeyEncoding: { type: 'spki', format: 'der' },\n });\n const state: StoredState = {\n privateKey: pair.privateKey,\n publicKey: Buffer.from(pair.publicKey).toString('base64url'),\n revision: 0,\n };\n atomicWriteState(stateDir, state);\n return state;\n });\n}\n\nfunction reserveRevision(stateDir: string, expectedPublicKey: string): number {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n const state = parseState(JSON.parse(fs.readFileSync(keyPath, 'utf8')) as unknown, keyPath);\n if (state.publicKey !== expectedPublicKey)\n throw new Error('bundle signing key changed while the signer was active');\n if (state.revision === Number.MAX_SAFE_INTEGER) throw new Error('bundle signing revision is exhausted');\n const next = { ...state, revision: state.revision + 1 };\n atomicWriteState(stateDir, next);\n return next.revision;\n });\n}\n\nfunction contentTypeFor(filePath: string): string {\n return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/** Every servable regular file under the asset root, as browser paths. */\nfunction walk(root: string, current: string, into: BundleAsset[]): void {\n const entries = fs.readdirSync(current, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(current, entry.name);\n const stat = fs.lstatSync(full);\n if (stat.isSymbolicLink()) throw new Error(`refusing to sign symbolic link: ${full}`);\n if (stat.isDirectory()) {\n walk(root, full, into);\n continue;\n }\n if (!stat.isFile()) throw new Error(`refusing to omit unsupported asset: ${full}`);\n if (SKIPPED.has(path.extname(entry.name))) continue;\n const bytes = fs.readFileSync(full);\n into.push({\n path: `/${path\n .relative(root, full)\n .split(path.sep)\n .map((segment) => encodeURIComponent(segment))\n .join('/')}`,\n sha256: createHash('sha256').update(bytes).digest('hex'),\n byteLength: bytes.byteLength,\n contentType: contentTypeFor(full),\n });\n }\n}\n\nexport function createBundleSigner(stateDir: string, _onNotice: (message: string) => void = () => {}): BundleSigner {\n const key = loadOrCreateState(stateDir);\n const privateKey = createPrivateKey(key.privateKey);\n const cache = new Map<string, SignedBundleManifest | undefined>();\n const cacheKeyOf = (assetsDir: string, profile: BundleProfile): string => `${profile}:${path.resolve(assetsDir)}`;\n\n const signFresh = (assetsDir: string, profile: BundleProfile): SignedBundleManifest | undefined => {\n const rootStat = fs.lstatSync(assetsDir);\n if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {\n throw new Error(`refusing to sign an unsafe asset root: ${assetsDir}`);\n }\n const assets: BundleAsset[] = [];\n walk(assetsDir, assetsDir, assets);\n if (assets.length === 0) return undefined;\n assets.sort((left, right) => left.path.localeCompare(right.path));\n const manifest: BundleManifest = {\n version: BUNDLE_MANIFEST_VERSION,\n revision: reserveRevision(stateDir, key.publicKey),\n builtAt: Date.now(),\n assets,\n };\n if (!isBundleManifest(manifest, profile)) throw new Error(`refusing to sign an invalid ${profile} bundle manifest`);\n const signature = createSign(SIGN_ALGORITHM)\n .update(canonicalManifest(manifest))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n return { manifest, signature, publicKey: key.publicKey };\n };\n\n const sign = (assetsDir: string, profile: BundleProfile = 'web'): SignedBundleManifest | undefined => {\n const cacheKey = cacheKeyOf(assetsDir, profile);\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n const signed = signFresh(path.resolve(assetsDir), profile);\n cache.set(cacheKey, signed);\n return signed;\n };\n\n return {\n publicKey: () => key.publicKey,\n sign,\n refresh(assetsDir, profile = 'web') {\n cache.delete(cacheKeyOf(assetsDir, profile));\n return sign(assetsDir, profile);\n },\n invalidate(assetsDir) {\n if (assetsDir === undefined) {\n cache.clear();\n return;\n }\n for (const profile of PROFILES) cache.delete(cacheKeyOf(assetsDir, profile));\n },\n attest(tag, fields) {\n return createSign(SIGN_ALGORITHM)\n .update(attestationBytes(tag, fields))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n },\n };\n}\n\n/** Re-derives the public key from a stored private key. */\nexport function publicKeyOf(privateKeyPem: string): string {\n return Buffer.from(createPublicKey(privateKeyPem).export({ type: 'spki', format: 'der' }) as Buffer).toString(\n 'base64url',\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["fs","path","SIGNING_FILE","LOCK_FILE","SIGN_ALGORITHM","SKIPPED","PROFILES","MIME_TYPES","ensureStateDirectory","stateDir","stat","withStateLock","lockPath","temporary","randomBytes","initial","DatabaseSync","error","descriptor","header","database","operation","atomicWriteState","destination","state","parseState","value","keyPath","candidate","revision","derived","publicKeyOf","loadOrCreateState","parsed","pair","generateKeyPairSync","reserveRevision","expectedPublicKey","next","contentTypeFor","filePath","walk","entries","current","entry","full","root","into","bytes","segment","createHash","createBundleSigner","key","privateKey","createPrivateKey","cache","cacheKeyOf","profile","assetsDir","signFresh","rootStat","assets","left","right","manifest","isBundleManifest","createSign","canonicalManifest","sign","cacheKey","signed","attestationBytes","tag","fields","createPublicKey","privateKeyPem"],"sources":["../../../src/services/bundleSigner/index.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n randomBytes,\n} from 'node:crypto';\n// sync: Public createBundleSigner and BundleSigner.sign/refresh contracts must remain synchronous until phase 2.\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n attestationBytes,\n canonicalManifest,\n isBundleManifest,\n} from '../../types/bundleManifest';\n\nconst SIGNING_FILE = 'signing.json';\nconst LOCK_FILE = 'signing.lock';\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst SKIPPED = new Set(['.map']);\nconst PROFILES: readonly BundleProfile[] = ['web', 'native'];\nconst MIME_TYPES: Readonly<Record<string, string>> = {\n '.css': 'text/css',\n '.gif': 'image/gif',\n '.html': 'text/html',\n '.ico': 'image/x-icon',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.mjs': 'text/javascript',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n};\n\nexport interface BundleSigner {\n /** Base64url SPKI for an independently trusted verifier to pin. */\n publicKey(): string;\n /**\n * Hashes and signs once per asset directory and profile. Undefined when the bundle is empty. Throws\n * when the assets do not match the profile.\n */\n sign(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates and immediately rebuilds one cached manifest. */\n refresh(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates one cached directory under every profile, or every cached directory when omitted. */\n invalidate(assetsDir?: string): void;\n /**\n * Base64url IEEE P1363 signature over `attestationBytes(tag, fields)` with the bundle key. Throws on a\n * malformed tag.\n */\n attest(tag: string, fields: readonly string[]): string;\n}\n\ninterface StoredState {\n privateKey: string;\n publicKey: string;\n revision: number;\n}\n\nfunction ensureStateDirectory(stateDir: string): void {\n fs.mkdirSync(stateDir, { recursive: true, mode: DIRECTORY_MODE });\n const stat = fs.lstatSync(stateDir);\n if (!stat.isDirectory() || stat.isSymbolicLink())\n throw new Error(`bundle signing state is not a directory: ${stateDir}`);\n fs.chmodSync(stateDir, DIRECTORY_MODE);\n}\n\nfunction withStateLock<T>(stateDir: string, operation: () => T): T {\n ensureStateDirectory(stateDir);\n const lockPath = path.join(stateDir, LOCK_FILE);\n if (!fs.existsSync(lockPath)) {\n const temporary = path.join(stateDir, `.${LOCK_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n fs.closeSync(fs.openSync(temporary, 'wx', FILE_MODE));\n try {\n const initial = new DatabaseSync(temporary);\n try {\n initial.exec('PRAGMA user_version = 1');\n } finally {\n initial.close();\n }\n // Publish a complete database, never an empty file another writer could mistake for a legacy lock.\n try {\n fs.linkSync(temporary, lockPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n }\n } finally {\n fs.rmSync(temporary, { force: true });\n }\n }\n if (!fs.lstatSync(lockPath).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const descriptor = fs.openSync(lockPath, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW);\n try {\n if (!fs.fstatSync(descriptor).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const header = Buffer.alloc(16);\n fs.readSync(descriptor, header, 0, header.length, 0);\n if (header.toString() !== 'SQLite format 3\\u0000') {\n // The old empty wx lock has no owner information. Never assume it is stale or unlink a live writer's lock.\n throw new Error(\n `legacy or invalid bundle signing state lock: ${lockPath}; stop all Po servers and inspect the lock before manual recovery`,\n );\n }\n fs.fchmodSync(descriptor, FILE_MODE);\n const database = new DatabaseSync(lockPath, { timeout: 5000 });\n try {\n // The kernel releases this lock even on SIGKILL. Keep its inode so concurrent writers share one lock.\n database.exec('BEGIN EXCLUSIVE');\n return operation();\n } finally {\n database.close();\n }\n } finally {\n fs.closeSync(descriptor);\n }\n}\n\nfunction atomicWriteState(stateDir: string, state: StoredState): void {\n const destination = path.join(stateDir, SIGNING_FILE);\n const temporary = path.join(stateDir, `.${SIGNING_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n const descriptor = fs.openSync(temporary, 'wx', FILE_MODE);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify(state, undefined, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n try {\n fs.renameSync(temporary, destination);\n fs.chmodSync(destination, FILE_MODE);\n } catch (error) {\n fs.rmSync(temporary, { force: true });\n throw error;\n }\n}\n\nfunction parseState(value: unknown, keyPath: string): StoredState {\n if (typeof value !== 'object' || value === null) throw new Error(`invalid bundle signing state: ${keyPath}`);\n const candidate = value as Partial<StoredState>;\n if (typeof candidate.privateKey !== 'string' || typeof candidate.publicKey !== 'string') {\n throw new Error(`invalid bundle signing state: ${keyPath}`);\n }\n const revision = candidate.revision === undefined ? 0 : candidate.revision;\n if (!Number.isSafeInteger(revision) || revision < 0) throw new Error(`invalid bundle signing revision: ${keyPath}`);\n let derived: string;\n try {\n derived = publicKeyOf(candidate.privateKey);\n } catch {\n throw new Error(`invalid bundle signing private key: ${keyPath}`);\n }\n if (derived !== candidate.publicKey) throw new Error(`bundle signing keys do not match: ${keyPath}`);\n return { privateKey: candidate.privateKey, publicKey: candidate.publicKey, revision };\n}\n\nfunction loadOrCreateState(stateDir: string): StoredState {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n try {\n const stat = fs.lstatSync(keyPath);\n if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`invalid bundle signing state file: ${keyPath}`);\n const parsed: unknown = JSON.parse(fs.readFileSync(keyPath, 'utf8'));\n const state = parseState(parsed, keyPath);\n fs.chmodSync(keyPath, FILE_MODE);\n if ((parsed as Partial<StoredState>).revision === undefined) atomicWriteState(stateDir, state);\n return state;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n\n const pair = generateKeyPairSync('ec', {\n namedCurve: CURVE,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' },\n publicKeyEncoding: { type: 'spki', format: 'der' },\n });\n const state: StoredState = {\n privateKey: pair.privateKey,\n publicKey: Buffer.from(pair.publicKey).toString('base64url'),\n revision: 0,\n };\n atomicWriteState(stateDir, state);\n return state;\n });\n}\n\nfunction reserveRevision(stateDir: string, expectedPublicKey: string): number {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n const state = parseState(JSON.parse(fs.readFileSync(keyPath, 'utf8')) as unknown, keyPath);\n if (state.publicKey !== expectedPublicKey)\n throw new Error('bundle signing key changed while the signer was active');\n if (state.revision === Number.MAX_SAFE_INTEGER) throw new Error('bundle signing revision is exhausted');\n const next = { ...state, revision: state.revision + 1 };\n atomicWriteState(stateDir, next);\n return next.revision;\n });\n}\n\nfunction contentTypeFor(filePath: string): string {\n return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/** Every servable regular file under the asset root, as browser paths. */\nfunction walk(root: string, current: string, into: BundleAsset[]): void {\n const entries = fs.readdirSync(current, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(current, entry.name);\n const stat = fs.lstatSync(full);\n if (stat.isSymbolicLink()) throw new Error(`refusing to sign symbolic link: ${full}`);\n if (stat.isDirectory()) {\n walk(root, full, into);\n continue;\n }\n if (!stat.isFile()) throw new Error(`refusing to omit unsupported asset: ${full}`);\n if (SKIPPED.has(path.extname(entry.name))) continue;\n const bytes = fs.readFileSync(full);\n into.push({\n path: `/${path\n .relative(root, full)\n .split(path.sep)\n .map((segment) => encodeURIComponent(segment))\n .join('/')}`,\n sha256: createHash('sha256').update(bytes).digest('hex'),\n byteLength: bytes.byteLength,\n contentType: contentTypeFor(full),\n });\n }\n}\n\nexport function createBundleSigner(stateDir: string, _onNotice: (message: string) => void = () => {}): BundleSigner {\n const key = loadOrCreateState(stateDir);\n const privateKey = createPrivateKey(key.privateKey);\n const cache = new Map<string, SignedBundleManifest | undefined>();\n const cacheKeyOf = (assetsDir: string, profile: BundleProfile): string => `${profile}:${path.resolve(assetsDir)}`;\n\n const signFresh = (assetsDir: string, profile: BundleProfile): SignedBundleManifest | undefined => {\n const rootStat = fs.lstatSync(assetsDir);\n if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {\n throw new Error(`refusing to sign an unsafe asset root: ${assetsDir}`);\n }\n const assets: BundleAsset[] = [];\n walk(assetsDir, assetsDir, assets);\n if (assets.length === 0) return undefined;\n assets.sort((left, right) => left.path.localeCompare(right.path));\n const manifest: BundleManifest = {\n version: BUNDLE_MANIFEST_VERSION,\n revision: reserveRevision(stateDir, key.publicKey),\n builtAt: Date.now(),\n assets,\n };\n if (!isBundleManifest(manifest, profile)) throw new Error(`refusing to sign an invalid ${profile} bundle manifest`);\n const signature = createSign(SIGN_ALGORITHM)\n .update(canonicalManifest(manifest))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n return { manifest, signature, publicKey: key.publicKey };\n };\n\n const sign = (assetsDir: string, profile: BundleProfile = 'web'): SignedBundleManifest | undefined => {\n const cacheKey = cacheKeyOf(assetsDir, profile);\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n const signed = signFresh(path.resolve(assetsDir), profile);\n cache.set(cacheKey, signed);\n return signed;\n };\n\n return {\n publicKey: () => key.publicKey,\n sign,\n refresh(assetsDir, profile = 'web') {\n cache.delete(cacheKeyOf(assetsDir, profile));\n return sign(assetsDir, profile);\n },\n invalidate(assetsDir) {\n if (assetsDir === undefined) {\n cache.clear();\n return;\n }\n for (const profile of PROFILES) cache.delete(cacheKeyOf(assetsDir, profile));\n },\n attest(tag, fields) {\n return createSign(SIGN_ALGORITHM)\n .update(attestationBytes(tag, fields))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n },\n };\n}\n\n/** Re-derives the public key from a stored private key. */\nexport function publicKeyOf(privateKeyPem: string): string {\n return Buffer.from(createPublicKey(privateKeyPem).export({ type: 'spki', format: 'der' }) as Buffer).toString(\n 'base64url',\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["fs","path","SIGNING_FILE","LOCK_FILE","SIGN_ALGORITHM","SKIPPED","PROFILES","MIME_TYPES","ensureStateDirectory","stateDir","stat","withStateLock","lockPath","temporary","randomBytes","initial","DatabaseSync","error","descriptor","header","database","operation","atomicWriteState","destination","state","parseState","value","keyPath","candidate","revision","derived","publicKeyOf","loadOrCreateState","parsed","pair","generateKeyPairSync","reserveRevision","expectedPublicKey","next","contentTypeFor","filePath","walk","entries","current","entry","full","root","into","bytes","segment","createHash","createBundleSigner","key","privateKey","createPrivateKey","cache","cacheKeyOf","profile","assetsDir","signFresh","rootStat","assets","left","right","manifest","isBundleManifest","createSign","canonicalManifest","sign","cacheKey","signed","attestationBytes","tag","fields","createPublicKey","privateKeyPem"],"sources":["../../../src/services/bundleSigner/index.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n randomBytes,\n} from 'node:crypto';\n// sync: Public createBundleSigner and BundleSigner.sign/refresh contracts must remain synchronous until phase 2.\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n attestationBytes,\n canonicalManifest,\n isBundleManifest,\n} from '../../types/bundleManifest';\n\nconst SIGNING_FILE = 'signing.json';\nconst LOCK_FILE = 'signing.lock';\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst SKIPPED = new Set(['.map']);\nconst PROFILES: readonly BundleProfile[] = ['web', 'native'];\nconst MIME_TYPES: Readonly<Record<string, string>> = {\n '.css': 'text/css',\n '.gif': 'image/gif',\n '.html': 'text/html',\n '.ico': 'image/x-icon',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.mjs': 'text/javascript',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n};\n\nexport interface BundleSigner {\n /** Base64url SPKI for an independently trusted verifier to pin. */\n publicKey(): string;\n /**\n * Hashes and signs once per asset directory and profile. Undefined when the bundle is empty. Throws\n * when the assets do not match the profile.\n */\n sign(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates and immediately rebuilds one cached manifest. */\n refresh(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates one cached directory under every profile, or every cached directory when omitted. */\n invalidate(assetsDir?: string): void;\n /**\n * Base64url IEEE P1363 signature over `attestationBytes(tag, fields)` with the bundle key. Throws on a\n * malformed tag.\n */\n attest(tag: string, fields: readonly string[]): string;\n}\n\ninterface StoredState {\n privateKey: string;\n publicKey: string;\n revision: number;\n}\n\nfunction ensureStateDirectory(stateDir: string): void {\n fs.mkdirSync(stateDir, { recursive: true, mode: DIRECTORY_MODE });\n const stat = fs.lstatSync(stateDir);\n if (!stat.isDirectory() || stat.isSymbolicLink())\n throw new Error(`bundle signing state is not a directory: ${stateDir}`);\n fs.chmodSync(stateDir, DIRECTORY_MODE);\n}\n\nfunction withStateLock<T>(stateDir: string, operation: () => T): T {\n ensureStateDirectory(stateDir);\n const lockPath = path.join(stateDir, LOCK_FILE);\n if (!fs.existsSync(lockPath)) {\n const temporary = path.join(stateDir, `.${LOCK_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n fs.closeSync(fs.openSync(temporary, 'wx', FILE_MODE));\n try {\n const initial = new DatabaseSync(temporary);\n try {\n initial.exec('PRAGMA user_version = 1');\n } finally {\n initial.close();\n }\n // Publish a complete database, never an empty file another writer could mistake for a legacy lock.\n try {\n fs.linkSync(temporary, lockPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n }\n } finally {\n fs.rmSync(temporary, { force: true });\n }\n }\n if (!fs.lstatSync(lockPath).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const descriptor = fs.openSync(lockPath, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW);\n try {\n if (!fs.fstatSync(descriptor).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const header = Buffer.alloc(16);\n fs.readSync(descriptor, header, 0, header.length, 0);\n if (header.toString() !== 'SQLite format 3\\u0000') {\n // The old empty wx lock has no owner information. Never assume it is stale or unlink a live writer's lock.\n throw new Error(\n `legacy or invalid bundle signing state lock: ${lockPath}; stop all Po servers and inspect the lock before manual recovery`,\n );\n }\n fs.fchmodSync(descriptor, FILE_MODE);\n const database = new DatabaseSync(lockPath, { timeout: 5000 });\n try {\n // The kernel releases this lock even on SIGKILL. Keep its inode so concurrent writers share one lock.\n database.exec('BEGIN EXCLUSIVE');\n return operation();\n } finally {\n database.close();\n }\n } finally {\n fs.closeSync(descriptor);\n }\n}\n\nfunction atomicWriteState(stateDir: string, state: StoredState): void {\n const destination = path.join(stateDir, SIGNING_FILE);\n const temporary = path.join(stateDir, `.${SIGNING_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n const descriptor = fs.openSync(temporary, 'wx', FILE_MODE);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify(state, undefined, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n try {\n fs.renameSync(temporary, destination);\n fs.chmodSync(destination, FILE_MODE);\n } catch (error) {\n fs.rmSync(temporary, { force: true });\n throw error;\n }\n}\n\nfunction parseState(value: unknown, keyPath: string): StoredState {\n if (typeof value !== 'object' || value === null) throw new Error(`invalid bundle signing state: ${keyPath}`);\n const candidate = value as Partial<StoredState>;\n if (typeof candidate.privateKey !== 'string' || typeof candidate.publicKey !== 'string') {\n throw new Error(`invalid bundle signing state: ${keyPath}`);\n }\n const revision = candidate.revision === undefined ? 0 : candidate.revision;\n if (!Number.isSafeInteger(revision) || revision < 0) throw new Error(`invalid bundle signing revision: ${keyPath}`);\n let derived: string;\n try {\n derived = publicKeyOf(candidate.privateKey);\n } catch {\n throw new Error(`invalid bundle signing private key: ${keyPath}`);\n }\n if (derived !== candidate.publicKey) throw new Error(`bundle signing keys do not match: ${keyPath}`);\n return { privateKey: candidate.privateKey, publicKey: candidate.publicKey, revision };\n}\n\nfunction loadOrCreateState(stateDir: string): StoredState {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n try {\n const stat = fs.lstatSync(keyPath);\n if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`invalid bundle signing state file: ${keyPath}`);\n const parsed: unknown = JSON.parse(fs.readFileSync(keyPath, 'utf8'));\n const state = parseState(parsed, keyPath);\n fs.chmodSync(keyPath, FILE_MODE);\n if ((parsed as Partial<StoredState>).revision === undefined) atomicWriteState(stateDir, state);\n return state;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n\n const pair = generateKeyPairSync('ec', {\n namedCurve: CURVE,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' },\n publicKeyEncoding: { type: 'spki', format: 'der' },\n });\n const state: StoredState = {\n privateKey: pair.privateKey,\n publicKey: Buffer.from(pair.publicKey).toString('base64url'),\n revision: 0,\n };\n atomicWriteState(stateDir, state);\n return state;\n });\n}\n\nfunction reserveRevision(stateDir: string, expectedPublicKey: string): number {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n const state = parseState(JSON.parse(fs.readFileSync(keyPath, 'utf8')) as unknown, keyPath);\n if (state.publicKey !== expectedPublicKey)\n throw new Error('bundle signing key changed while the signer was active');\n if (state.revision === Number.MAX_SAFE_INTEGER) throw new Error('bundle signing revision is exhausted');\n const next = { ...state, revision: state.revision + 1 };\n atomicWriteState(stateDir, next);\n return next.revision;\n });\n}\n\nfunction contentTypeFor(filePath: string): string {\n return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/** Every servable regular file under the asset root, as browser paths. */\nfunction walk(root: string, current: string, into: BundleAsset[]): void {\n const entries = fs.readdirSync(current, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(current, entry.name);\n const stat = fs.lstatSync(full);\n if (stat.isSymbolicLink()) throw new Error(`refusing to sign symbolic link: ${full}`);\n if (stat.isDirectory()) {\n walk(root, full, into);\n continue;\n }\n if (!stat.isFile()) throw new Error(`refusing to omit unsupported asset: ${full}`);\n if (SKIPPED.has(path.extname(entry.name))) continue;\n const bytes = fs.readFileSync(full);\n into.push({\n path: `/${path\n .relative(root, full)\n .split(path.sep)\n .map((segment) => encodeURIComponent(segment))\n .join('/')}`,\n sha256: createHash('sha256').update(bytes).digest('hex'),\n byteLength: bytes.byteLength,\n contentType: contentTypeFor(full),\n });\n }\n}\n\nexport function createBundleSigner(stateDir: string, _onNotice: (message: string) => void = () => {}): BundleSigner {\n const key = loadOrCreateState(stateDir);\n const privateKey = createPrivateKey(key.privateKey);\n const cache = new Map<string, SignedBundleManifest | undefined>();\n const cacheKeyOf = (assetsDir: string, profile: BundleProfile): string => `${profile}:${path.resolve(assetsDir)}`;\n\n const signFresh = (assetsDir: string, profile: BundleProfile): SignedBundleManifest | undefined => {\n const rootStat = fs.lstatSync(assetsDir);\n if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {\n throw new Error(`refusing to sign an unsafe asset root: ${assetsDir}`);\n }\n const assets: BundleAsset[] = [];\n walk(assetsDir, assetsDir, assets);\n if (assets.length === 0) return undefined;\n assets.sort((left, right) => left.path.localeCompare(right.path));\n const manifest: BundleManifest = {\n version: BUNDLE_MANIFEST_VERSION,\n revision: reserveRevision(stateDir, key.publicKey),\n builtAt: Date.now(),\n assets,\n };\n if (!isBundleManifest(manifest, profile)) throw new Error(`refusing to sign an invalid ${profile} bundle manifest`);\n const signature = createSign(SIGN_ALGORITHM)\n .update(canonicalManifest(manifest))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n return { manifest, signature, publicKey: key.publicKey };\n };\n\n const sign = (assetsDir: string, profile: BundleProfile = 'web'): SignedBundleManifest | undefined => {\n const cacheKey = cacheKeyOf(assetsDir, profile);\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n const signed = signFresh(path.resolve(assetsDir), profile);\n cache.set(cacheKey, signed);\n return signed;\n };\n\n return {\n publicKey: () => key.publicKey,\n sign,\n refresh(assetsDir, profile = 'web') {\n cache.delete(cacheKeyOf(assetsDir, profile));\n return sign(assetsDir, profile);\n },\n invalidate(assetsDir) {\n if (assetsDir === undefined) {\n cache.clear();\n return;\n }\n for (const profile of PROFILES) cache.delete(cacheKeyOf(assetsDir, profile));\n },\n attest(tag, fields) {\n return createSign(SIGN_ALGORITHM)\n .update(attestationBytes(tag, fields))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n },\n };\n}\n\n/** Re-derives the public key from a stored private key. */\nexport function publicKeyOf(privateKeyPem: string): string {\n return Buffer.from(createPublicKey(privateKeyPem).export({ type: 'spki', format: 'der' }) as Buffer).toString(\n 'base64url',\n );\n}\n"],"mappings":"A,wVAwBA,MAAME,EAAe,eACfC,EAAY,eAIZC,EAAiB,SACjBC,kBAAU,IAAI,IAAI,CAAC,MAAM,CAAC,EAC1BC,EAAqC,CAAC,MAAO,QAAQ,EACrDC,EAA+C,CACnD,OAAQ,WACR,OAAQ,YACR,QAAS,YACT,OAAQ,eACR,QAAS,aACT,OAAQ,aACR,MAAO,kBACP,QAAS,mBACT,OAAQ,kBACR,OAAQ,YACR,OAAQ,gBACR,OAAQ,aACR,QAAS,mBACT,QAAS,aACT,QAAS,YACT,SAAU,YACZ,EA2BA,SAASC,EAAqB,EAAwB,CACpDR,EAAG,UAAUS,EAAU,CAAE,UAAW,GAAM,KAAM,GAAe,CAAC,EAChE,IAAMC,EAAOV,EAAG,UAAUS,CAAQ,EAClC,GAAI,CAACC,EAAK,YAAY,GAAKA,EAAK,eAAe,EAC7C,MAAU,MAAM,4CAA4CD,GAAU,EACxET,EAAG,UAAUS,EAAU,GAAc,CACvC,CAEA,SAASE,EAAiB,EAAkB,EAAuB,CACjEH,EAAqBC,CAAQ,EAC7B,IAAMG,EAAWX,EAAK,KAAKQ,EAAUN,CAAS,EAC9C,GAAI,CAACH,EAAG,WAAWY,CAAQ,EAAG,CAC5B,IAAMC,EAAYZ,EAAK,KAAKQ,EAAU,IAAIN,EAAU,GAAG,QAAQ,IAAI,GAAGW,EAAY,CAAC,CAAC,CAAC,SAAS,KAAK,GAAG,EACtGd,EAAG,UAAUA,EAAG,SAASa,EAAW,KAAM,GAAS,CAAC,EACpD,GAAI,CACF,IAAME,EAAU,IAAIC,EAAaH,CAAS,EAC1C,GAAI,CACFE,EAAQ,KAAK,yBAAyB,CACxC,QAAU,CACRA,EAAQ,MAAM,CAChB,CAEA,GAAI,CACFf,EAAG,SAASa,EAAWD,CAAQ,CACjC,OAASK,EAAO,CACd,GAAKA,EAAgC,OAAS,SAAU,MAAMA,CAChE,CACF,QAAU,CACRjB,EAAG,OAAOa,EAAW,CAAE,MAAO,EAAK,CAAC,CACtC,CACF,CACA,GAAI,CAACb,EAAG,UAAUY,CAAQ,CAAC,CAAC,OAAO,EAAG,MAAU,MAAM,qCAAqCA,GAAU,EACrG,IAAMM,EAAalB,EAAG,SAASY,EAAUZ,EAAG,UAAU,OAASA,EAAG,UAAU,UAAU,EACtF,GAAI,CACF,GAAI,CAACA,EAAG,UAAUkB,CAAU,CAAC,CAAC,OAAO,EAAG,MAAU,MAAM,qCAAqCN,GAAU,EACvG,IAAMO,EAAS,OAAO,MAAM,EAAE,EAE9B,GADAnB,EAAG,SAASkB,EAAYC,EAAQ,EAAGA,EAAO,OAAQ,CAAC,EAC/CA,EAAO,SAAS,IAAM,oBAExB,MAAU,MACR,gDAAgDP,EAAS,kEAC3D,EAEFZ,EAAG,WAAWkB,EAAY,GAAS,EACnC,IAAME,EAAW,IAAIJ,EAAaJ,EAAU,CAAE,QAAS,GAAK,CAAC,EAC7D,GAAI,CAGF,OADAQ,EAAS,KAAK,iBAAiB,EACxBC,EAAU,CACnB,QAAU,CACRD,EAAS,MAAM,CACjB,CACF,QAAU,CACRpB,EAAG,UAAUkB,CAAU,CACzB,CACF,CAEA,SAASI,EAAiB,EAAkB,EAA0B,CACpE,IAAMC,EAActB,EAAK,KAAKQ,EAAUP,CAAY,EAC9CW,EAAYZ,EAAK,KAAKQ,EAAU,IAAIP,EAAa,GAAG,QAAQ,IAAI,GAAGY,EAAY,CAAC,CAAC,CAAC,SAAS,KAAK,GAAG,EACnGI,EAAalB,EAAG,SAASa,EAAW,KAAM,GAAS,EACzD,GAAI,CACFb,EAAG,cAAckB,EAAY,GAAG,KAAK,UAAUM,EAAO,IAAA,GAAW,CAAC,EAAE,GAAG,EACvExB,EAAG,UAAUkB,CAAU,CACzB,QAAU,CACRlB,EAAG,UAAUkB,CAAU,CACzB,CACA,GAAI,CACFlB,EAAG,WAAWa,EAAWU,CAAW,EACpCvB,EAAG,UAAUuB,EAAa,GAAS,CACrC,OAASN,EAAO,CAEd,MADAjB,EAAG,OAAOa,EAAW,CAAE,MAAO,EAAK,CAAC,EAC9BI,CACR,CACF,CAEA,SAASQ,EAAW,EAAgB,EAA8B,CAChE,GAAI,OAAOC,GAAU,WAAYA,EAAgB,MAAU,MAAM,iCAAiCC,GAAS,EAC3G,IAAMC,EAAYF,EAClB,GAAI,OAAOE,EAAU,YAAe,UAAY,OAAOA,EAAU,WAAc,SAC7E,MAAU,MAAM,iCAAiCD,GAAS,EAE5D,IAAME,EAAWD,EAAU,WAAa,IAAA,GAAY,EAAIA,EAAU,SAClE,GAAI,CAAC,OAAO,cAAcC,CAAQ,GAAKA,EAAW,EAAG,MAAU,MAAM,oCAAoCF,GAAS,EAClH,IAAIG,EACJ,GAAI,CACF,EAAUC,EAAYH,EAAU,UAAU,CAC5C,MAAQ,CACN,MAAU,MAAM,uCAAuCD,GAAS,CAClE,CACA,GAAIG,IAAYF,EAAU,UAAW,MAAU,MAAM,qCAAqCD,GAAS,EACnG,MAAO,CAAE,WAAYC,EAAU,WAAY,UAAWA,EAAU,UAAW,UAAS,CACtF,CAEA,SAASI,EAAkB,EAA+B,CACxD,OAAOrB,EAAcF,MAAgB,CACnC,IAAMkB,EAAU1B,EAAK,KAAKQ,EAAUP,CAAY,EAChD,GAAI,CACF,IAAMQ,EAAOV,EAAG,UAAU2B,CAAO,EACjC,GAAI,CAACjB,EAAK,OAAO,GAAKA,EAAK,eAAe,EAAG,MAAU,MAAM,sCAAsCiB,GAAS,EAC5G,IAAMM,EAAkB,KAAK,MAAMjC,EAAG,aAAa2B,EAAS,MAAM,CAAC,EAC7DH,EAAQC,EAAWQ,EAAQN,CAAO,EAGxC,OAFA3B,EAAG,UAAU2B,EAAS,GAAS,EAC1BM,EAAgC,WAAa,IAAA,IAAWX,EAAiBb,EAAUe,CAAK,EACtFA,CACT,OAASP,EAAO,CACd,GAAKA,EAAgC,OAAS,SAAU,MAAMA,CAChE,CAEA,IAAMiB,EAAOC,EAAoB,KAAM,CACrC,WAAY,aACZ,mBAAoB,CAAE,KAAM,QAAS,OAAQ,KAAM,EACnD,kBAAmB,CAAE,KAAM,OAAQ,OAAQ,KAAM,CACnD,CAAC,EACKX,EAAqB,CACzB,WAAYU,EAAK,WACjB,UAAW,OAAO,KAAKA,EAAK,SAAS,CAAC,CAAC,SAAS,WAAW,EAC3D,SAAU,CACZ,EAEA,OADAZ,EAAiBb,EAAUe,CAAK,EACzBA,CACT,CAAC,CACH,CAEA,SAASY,EAAgB,EAAkB,EAAmC,CAC5E,OAAOzB,EAAcF,MAAgB,CACnC,IAAMkB,EAAU1B,EAAK,KAAKQ,EAAUP,CAAY,EAC1CsB,EAAQC,EAAW,KAAK,MAAMzB,EAAG,aAAa2B,EAAS,MAAM,CAAC,EAAcA,CAAO,EACzF,GAAIH,EAAM,YAAca,EACtB,MAAU,MAAM,wDAAwD,EAC1E,GAAIb,EAAM,mBAAsC,MAAU,MAAM,sCAAsC,EACtG,IAAMc,EAAO,CAAE,GAAGd,EAAO,SAAUA,EAAM,SAAW,CAAE,EAEtD,OADAF,EAAiBb,EAAU6B,CAAI,EACxBA,EAAK,QACd,CAAC,CACH,CAEA,SAASC,EAAe,EAA0B,CAChD,OAAOhC,EAAWN,EAAK,QAAQuC,CAAQ,CAAC,CAAC,YAAY,IAAM,0BAC7D,CAGA,SAASC,EAAK,EAAc,EAAiB,EAA2B,CACtE,IAAMC,EAAU1C,EAAG,YAAY2C,EAAS,CAAE,cAAe,EAAK,CAAC,EAC/D,IAAK,IAAMC,KAASF,EAAS,CAC3B,IAAMG,EAAO5C,EAAK,KAAK0C,EAASC,EAAM,IAAI,EACpClC,EAAOV,EAAG,UAAU6C,CAAI,EAC9B,GAAInC,EAAK,eAAe,EAAG,MAAU,MAAM,mCAAmCmC,GAAM,EACpF,GAAInC,EAAK,YAAY,EAAG,CACtB+B,EAAKK,EAAMD,EAAME,CAAI,EACrB,QACF,CACA,GAAI,CAACrC,EAAK,OAAO,EAAG,MAAU,MAAM,uCAAuCmC,GAAM,EACjF,GAAIxC,EAAQ,IAAIJ,EAAK,QAAQ2C,EAAM,IAAI,CAAC,EAAG,SAC3C,IAAMI,EAAQhD,EAAG,aAAa6C,CAAI,EAClCE,EAAK,KAAK,CACR,KAAM,IAAI9C,EACP,SAAS6C,EAAMD,CAAI,CAAC,CACpB,MAAM5C,EAAK,GAAG,CAAC,CACf,IAAK,GAAY,mBAAmBgD,CAAO,CAAC,CAAC,CAC7C,KAAK,GAAG,IACX,OAAQC,EAAW,QAAQ,CAAC,CAAC,OAAOF,CAAK,CAAC,CAAC,OAAO,KAAK,EACvD,WAAYA,EAAM,WAClB,YAAaT,EAAeM,CAAI,CAClC,CAAC,CACH,CACF,CAEA,SAAgBM,EAAmB,EAAkB,MAA6C,CAAC,EAAiB,CAClH,IAAMC,EAAMpB,EAAkBvB,CAAQ,EAChC4C,EAAaC,EAAiBF,EAAI,UAAU,EAC5CG,kBAAQ,IAAI,IACZC,GAAc,EAAmB,IAAmC,GAAGC,EAAQ,GAAGxD,EAAK,QAAQyD,CAAS,IAExGC,GAAa,EAAmB,IAA6D,CACjG,IAAMC,EAAW5D,EAAG,UAAU0D,CAAS,EACvC,GAAI,CAACE,EAAS,YAAY,GAAKA,EAAS,eAAe,EACrD,MAAU,MAAM,0CAA0CF,GAAW,EAEvE,IAAMG,EAAwB,CAAC,EAE/B,GADApB,EAAKiB,EAAWA,EAAWG,CAAM,EAC7BA,EAAO,SAAW,EAAG,OACzBA,EAAO,MAAM,EAAM,IAAUC,EAAK,KAAK,cAAcC,EAAM,IAAI,CAAC,EAChE,IAAMC,EAA2B,CAC/B,QAAA,EACA,SAAU5B,EAAgB3B,EAAU2C,EAAI,SAAS,EACjD,QAAS,KAAK,IAAI,EAClB,QACF,EACA,GAAI,CAACa,EAAiBD,EAAUP,CAAO,EAAG,MAAU,MAAM,+BAA+BA,EAAQ,iBAAiB,EAKlH,MAAO,CAAE,WAAU,UAJDS,EAAW9D,CAAc,CAAC,CACzC,OAAO+D,EAAkBH,CAAQ,CAAC,CAAC,CACnC,KAAK,CAAE,IAAKX,EAAY,YAAa,YAAa,CAAC,CAAC,CACpD,SAAS,WACe,EAAG,UAAWD,EAAI,SAAU,CACzD,EAEMgB,GAAQ,EAAmB,EAAyB,QAA4C,CACpG,IAAMC,EAAWb,EAAWE,EAAWD,CAAO,EAC9C,GAAIF,EAAM,IAAIc,CAAQ,EAAG,OAAOd,EAAM,IAAIc,CAAQ,EAClD,IAAMC,EAASX,EAAU1D,EAAK,QAAQyD,CAAS,EAAGD,CAAO,EAEzD,OADAF,EAAM,IAAIc,EAAUC,CAAM,EACnBA,CACT,EAEA,MAAO,CACL,cAAiBlB,EAAI,UACrB,OACA,QAAQ,EAAW,EAAU,MAAO,CAElC,OADAG,EAAM,OAAOC,EAAWE,EAAWD,CAAO,CAAC,EACpCW,EAAKV,EAAWD,CAAO,CAChC,EACA,WAAW,EAAW,CACpB,GAAIC,IAAc,IAAA,GAChBH,EAAM,MAAM,OAGd,IAAK,IAAME,KAAWnD,EAAUiD,EAAM,OAAOC,EAAWE,EAAWD,CAAO,CAAC,CAC7E,EACA,OAAO,EAAK,EAAQ,CAClB,OAAOS,EAAW9D,CAAc,CAAC,CAC9B,OAAOmE,EAAiBC,EAAKC,CAAM,CAAC,CAAC,CACrC,KAAK,CAAE,IAAKpB,EAAY,YAAa,YAAa,CAAC,CAAC,CACpD,SAAS,WAAW,CACzB,CACF,CACF,CAGA,SAAgBtB,EAAY,EAA+B,CACzD,OAAO,OAAO,KAAK2C,EAAgBC,CAAa,CAAC,CAAC,OAAO,CAAE,KAAM,OAAQ,OAAQ,KAAM,CAAC,CAAW,CAAC,CAAC,SACnG,WACF,CACF,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["metadataProvider","nameUpdater","bindClientMetadata","provider","updateName","updateClientDeviceName","name","timestamp","clientMetadata","refreshClientDeviceName","response","request","value","own","device","identity","DEVICE_KEY","INSTANCE_KEY","createClientIdentity","current","options","persistent","listeners","saved","_instanceId","listener","updatedAt","update","next","createBrowserClientIdentity","instanceId","sessionPersistent","inherited","text","metadata","operation","observed","changed","unsubscribe","stopStorage"],"sources":["../../../src/services/clientIdentity/index.ts"],"sourcesContent":["import type { BrowserIdentityOptions, ClientIdentity, ClientIdentityOptions, IdentityRecord } from './type';\n\nlet metadataProvider: (() => string | undefined) | undefined;\nlet nameUpdater: ((name: unknown, timestamp: number) => Promise<void>) | undefined;\n\n/** The shell binds its current encoded metadata, keeping the codec out of this package. */\nexport function bindClientMetadata(\n provider: () => string | undefined,\n updateName?: (name: unknown, timestamp: number) => Promise<void>,\n): () => void {\n metadataProvider = provider;\n nameUpdater = updateName;\n return () => {\n if (metadataProvider === provider) {\n metadataProvider = undefined;\n nameUpdater = undefined;\n }\n };\n}\n\n/** Cache a canonical name through the owning shell, without importing its identity codec. */\nexport async function updateClientDeviceName(name: unknown, timestamp: number): Promise<void> {\n if (!nameUpdater) throw new Error('Client identity name updates are unavailable.');\n await nameUpdater(name, timestamp);\n}\n\nexport function clientMetadata(): string | undefined {\n return metadataProvider?.();\n}\n\n/** Registry names are display-only. Only a valid canonical record for this device is cached. */\nexport async function refreshClientDeviceName<T extends IdentityRecord>(\n identity: ClientIdentity<T>,\n request: () => Promise<Response>,\n): Promise<void> {\n const response = await request();\n if (response.status === 404) return;\n if (!response.ok) throw new Error(`Client device refresh failed (${response.status}).`);\n const value: unknown = await response.json();\n if (typeof value !== 'object' || value === null || !('devices' in value) || !Array.isArray(value.devices)) return;\n const own: unknown = value.devices.find(\n (device: unknown) =>\n typeof device === 'object' &&\n device !== null &&\n 'deviceId' in device &&\n device.deviceId === identity.current().deviceId,\n );\n if (typeof own !== 'object' || own === null || !('deviceName' in own) || !('nameUpdatedAt' in own)) return;\n if (\n typeof own.deviceName !== 'string' ||\n typeof own.nameUpdatedAt !== 'number' ||\n !Number.isSafeInteger(own.nameUpdatedAt) ||\n own.nameUpdatedAt < 0\n )\n return;\n await identity.updateName(own.deviceName, own.nameUpdatedAt);\n}\n\nconst DEVICE_KEY = 'po.client.device.v1';\nconst INSTANCE_KEY = 'po.client.instance.v1';\n\n/** Storage failures explicitly degrade to one in-memory identity, never an auth identity. */\nexport async function createClientIdentity<T extends IdentityRecord>(\n options: ClientIdentityOptions<T>,\n): Promise<ClientIdentity<T>> {\n let current = { ...options.initial };\n let persistent = true;\n const listeners = new Set<() => void>();\n try {\n await options.exclusive(async () => {\n const saved = options.parse(await options.read());\n if (saved) current = { ...saved, ...(current.instanceId ? { instanceId: current.instanceId } : {}) };\n else {\n const { instanceId: _instanceId, ...device } = current;\n await options.write(device as T);\n }\n });\n } catch {\n persistent = false;\n }\n return {\n current: () => ({ ...current }),\n persistent: () => persistent,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async updateName(value, timestamp) {\n const name = options.normalizeName(value);\n if (!name) throw new Error('Enter a device name from 1 to 64 characters.');\n const updatedAt = timestamp ?? Math.max(options.now(), current.nameUpdatedAt + 1);\n if (!Number.isSafeInteger(updatedAt) || updatedAt < 0) throw new Error('Invalid device name timestamp.');\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const update = async (): Promise<void> => {\n const saved = options.parse(await options.read());\n if (saved && saved.deviceId === current.deviceId && saved.nameUpdatedAt > current.nameUpdatedAt) {\n current = { ...current, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n }\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const next = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n const { instanceId: _instanceId, ...device } = next;\n await options.write(device as T);\n current = next;\n };\n if (persistent) {\n try {\n await options.exclusive(update);\n } catch {\n persistent = false;\n current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n }\n } else current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n for (const listener of listeners) listener();\n },\n };\n}\n\nexport async function createBrowserClientIdentity<T extends IdentityRecord>(\n options: BrowserIdentityOptions<T>,\n): Promise<ClientIdentity<T> & { dispose(): void }> {\n let instanceId = options.randomId();\n let sessionPersistent = true;\n try {\n const inherited = options.sessionStorage.getItem(INSTANCE_KEY);\n if (options.isToken(inherited) && inherited && (await options.claimInstance(inherited))) instanceId = inherited;\n else if (!(await options.claimInstance(instanceId))) throw new Error('The tab identity could not be claimed.');\n options.sessionStorage.setItem(INSTANCE_KEY, instanceId);\n } catch {\n sessionPersistent = false;\n }\n const identity = await createClientIdentity({\n ...options,\n initial: { ...options.initial, instanceId },\n read: async () => {\n const text = options.localStorage.getItem(DEVICE_KEY);\n return text === null ? undefined : (JSON.parse(text) as unknown);\n },\n write: async (metadata) => options.localStorage.setItem(DEVICE_KEY, JSON.stringify(metadata)),\n exclusive: (operation) => options.exclusive(DEVICE_KEY, operation),\n });\n let observed = identity.current();\n const listeners = new Set<() => void>();\n const changed = (): void => {\n for (const listener of listeners) listener();\n };\n const unsubscribe = identity.subscribe(() => {\n observed = identity.current();\n changed();\n });\n const stopStorage = options.onStorage(() => {\n try {\n const text = options.localStorage.getItem(DEVICE_KEY);\n const saved = text === null ? undefined : options.parse(JSON.parse(text));\n if (\n saved &&\n saved.deviceId === observed.deviceId &&\n (saved.nameUpdatedAt > observed.nameUpdatedAt ||\n (saved.nameUpdatedAt === observed.nameUpdatedAt && saved.deviceName !== observed.deviceName))\n ) {\n observed = { ...observed, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n changed();\n }\n } catch {\n sessionPersistent = false;\n }\n });\n return {\n current: () => ({ ...observed }),\n persistent: () => sessionPersistent && identity.persistent(),\n async updateName(name, timestamp) {\n // Merge storage notifications before assigning a local timestamp.\n await identity.updateName(name, timestamp ?? Math.max(options.now(), observed.nameUpdatedAt + 1));\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n dispose() {\n stopStorage();\n unsubscribe();\n listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["metadataProvider","nameUpdater","bindClientMetadata","provider","updateName","updateClientDeviceName","name","timestamp","clientMetadata","refreshClientDeviceName","response","request","value","own","device","identity","DEVICE_KEY","INSTANCE_KEY","createClientIdentity","current","options","persistent","listeners","saved","_instanceId","listener","updatedAt","update","next","createBrowserClientIdentity","instanceId","sessionPersistent","inherited","text","metadata","operation","observed","changed","unsubscribe","stopStorage"],"sources":["../../../src/services/clientIdentity/index.ts"],"sourcesContent":["import type { BrowserIdentityOptions, ClientIdentity, ClientIdentityOptions, IdentityRecord } from './type';\n\nlet metadataProvider: (() => string | undefined) | undefined;\nlet nameUpdater: ((name: unknown, timestamp: number) => Promise<void>) | undefined;\n\n/** The shell binds its current encoded metadata, keeping the codec out of this package. */\nexport function bindClientMetadata(\n provider: () => string | undefined,\n updateName?: (name: unknown, timestamp: number) => Promise<void>,\n): () => void {\n metadataProvider = provider;\n nameUpdater = updateName;\n return () => {\n if (metadataProvider === provider) {\n metadataProvider = undefined;\n nameUpdater = undefined;\n }\n };\n}\n\n/** Cache a canonical name through the owning shell, without importing its identity codec. */\nexport async function updateClientDeviceName(name: unknown, timestamp: number): Promise<void> {\n if (!nameUpdater) throw new Error('Client identity name updates are unavailable.');\n await nameUpdater(name, timestamp);\n}\n\nexport function clientMetadata(): string | undefined {\n return metadataProvider?.();\n}\n\n/** Registry names are display-only. Only a valid canonical record for this device is cached. */\nexport async function refreshClientDeviceName<T extends IdentityRecord>(\n identity: ClientIdentity<T>,\n request: () => Promise<Response>,\n): Promise<void> {\n const response = await request();\n if (response.status === 404) return;\n if (!response.ok) throw new Error(`Client device refresh failed (${response.status}).`);\n const value: unknown = await response.json();\n if (typeof value !== 'object' || value === null || !('devices' in value) || !Array.isArray(value.devices)) return;\n const own: unknown = value.devices.find(\n (device: unknown) =>\n typeof device === 'object' &&\n device !== null &&\n 'deviceId' in device &&\n device.deviceId === identity.current().deviceId,\n );\n if (typeof own !== 'object' || own === null || !('deviceName' in own) || !('nameUpdatedAt' in own)) return;\n if (\n typeof own.deviceName !== 'string' ||\n typeof own.nameUpdatedAt !== 'number' ||\n !Number.isSafeInteger(own.nameUpdatedAt) ||\n own.nameUpdatedAt < 0\n )\n return;\n await identity.updateName(own.deviceName, own.nameUpdatedAt);\n}\n\nconst DEVICE_KEY = 'po.client.device.v1';\nconst INSTANCE_KEY = 'po.client.instance.v1';\n\n/** Storage failures explicitly degrade to one in-memory identity, never an auth identity. */\nexport async function createClientIdentity<T extends IdentityRecord>(\n options: ClientIdentityOptions<T>,\n): Promise<ClientIdentity<T>> {\n let current = { ...options.initial };\n let persistent = true;\n const listeners = new Set<() => void>();\n try {\n await options.exclusive(async () => {\n const saved = options.parse(await options.read());\n if (saved) current = { ...saved, ...(current.instanceId ? { instanceId: current.instanceId } : {}) };\n else {\n const { instanceId: _instanceId, ...device } = current;\n await options.write(device as T);\n }\n });\n } catch {\n persistent = false;\n }\n return {\n current: () => ({ ...current }),\n persistent: () => persistent,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async updateName(value, timestamp) {\n const name = options.normalizeName(value);\n if (!name) throw new Error('Enter a device name from 1 to 64 characters.');\n const updatedAt = timestamp ?? Math.max(options.now(), current.nameUpdatedAt + 1);\n if (!Number.isSafeInteger(updatedAt) || updatedAt < 0) throw new Error('Invalid device name timestamp.');\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const update = async (): Promise<void> => {\n const saved = options.parse(await options.read());\n if (saved && saved.deviceId === current.deviceId && saved.nameUpdatedAt > current.nameUpdatedAt) {\n current = { ...current, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n }\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const next = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n const { instanceId: _instanceId, ...device } = next;\n await options.write(device as T);\n current = next;\n };\n if (persistent) {\n try {\n await options.exclusive(update);\n } catch {\n persistent = false;\n current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n }\n } else current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n for (const listener of listeners) listener();\n },\n };\n}\n\nexport async function createBrowserClientIdentity<T extends IdentityRecord>(\n options: BrowserIdentityOptions<T>,\n): Promise<ClientIdentity<T> & { dispose(): void }> {\n let instanceId = options.randomId();\n let sessionPersistent = true;\n try {\n const inherited = options.sessionStorage.getItem(INSTANCE_KEY);\n if (options.isToken(inherited) && inherited && (await options.claimInstance(inherited))) instanceId = inherited;\n else if (!(await options.claimInstance(instanceId))) throw new Error('The tab identity could not be claimed.');\n options.sessionStorage.setItem(INSTANCE_KEY, instanceId);\n } catch {\n sessionPersistent = false;\n }\n const identity = await createClientIdentity({\n ...options,\n initial: { ...options.initial, instanceId },\n read: async () => {\n const text = options.localStorage.getItem(DEVICE_KEY);\n return text === null ? undefined : (JSON.parse(text) as unknown);\n },\n write: async (metadata) => options.localStorage.setItem(DEVICE_KEY, JSON.stringify(metadata)),\n exclusive: (operation) => options.exclusive(DEVICE_KEY, operation),\n });\n let observed = identity.current();\n const listeners = new Set<() => void>();\n const changed = (): void => {\n for (const listener of listeners) listener();\n };\n const unsubscribe = identity.subscribe(() => {\n observed = identity.current();\n changed();\n });\n const stopStorage = options.onStorage(() => {\n try {\n const text = options.localStorage.getItem(DEVICE_KEY);\n const saved = text === null ? undefined : options.parse(JSON.parse(text));\n if (\n saved &&\n saved.deviceId === observed.deviceId &&\n (saved.nameUpdatedAt > observed.nameUpdatedAt ||\n (saved.nameUpdatedAt === observed.nameUpdatedAt && saved.deviceName !== observed.deviceName))\n ) {\n observed = { ...observed, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n changed();\n }\n } catch {\n sessionPersistent = false;\n }\n });\n return {\n current: () => ({ ...observed }),\n persistent: () => sessionPersistent && identity.persistent(),\n async updateName(name, timestamp) {\n // Merge storage notifications before assigning a local timestamp.\n await identity.updateName(name, timestamp ?? Math.max(options.now(), observed.nameUpdatedAt + 1));\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n dispose() {\n stopStorage();\n unsubscribe();\n listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["metadataProvider","nameUpdater","bindClientMetadata","provider","updateName","updateClientDeviceName","name","timestamp","clientMetadata","refreshClientDeviceName","response","request","value","own","device","identity","DEVICE_KEY","INSTANCE_KEY","createClientIdentity","current","options","persistent","listeners","saved","_instanceId","listener","updatedAt","update","next","createBrowserClientIdentity","instanceId","sessionPersistent","inherited","text","metadata","operation","observed","changed","unsubscribe","stopStorage"],"sources":["../../../src/services/clientIdentity/index.ts"],"sourcesContent":["import type { BrowserIdentityOptions, ClientIdentity, ClientIdentityOptions, IdentityRecord } from './type';\n\nlet metadataProvider: (() => string | undefined) | undefined;\nlet nameUpdater: ((name: unknown, timestamp: number) => Promise<void>) | undefined;\n\n/** The shell binds its current encoded metadata, keeping the codec out of this package. */\nexport function bindClientMetadata(\n provider: () => string | undefined,\n updateName?: (name: unknown, timestamp: number) => Promise<void>,\n): () => void {\n metadataProvider = provider;\n nameUpdater = updateName;\n return () => {\n if (metadataProvider === provider) {\n metadataProvider = undefined;\n nameUpdater = undefined;\n }\n };\n}\n\n/** Cache a canonical name through the owning shell, without importing its identity codec. */\nexport async function updateClientDeviceName(name: unknown, timestamp: number): Promise<void> {\n if (!nameUpdater) throw new Error('Client identity name updates are unavailable.');\n await nameUpdater(name, timestamp);\n}\n\nexport function clientMetadata(): string | undefined {\n return metadataProvider?.();\n}\n\n/** Registry names are display-only. Only a valid canonical record for this device is cached. */\nexport async function refreshClientDeviceName<T extends IdentityRecord>(\n identity: ClientIdentity<T>,\n request: () => Promise<Response>,\n): Promise<void> {\n const response = await request();\n if (response.status === 404) return;\n if (!response.ok) throw new Error(`Client device refresh failed (${response.status}).`);\n const value: unknown = await response.json();\n if (typeof value !== 'object' || value === null || !('devices' in value) || !Array.isArray(value.devices)) return;\n const own: unknown = value.devices.find(\n (device: unknown) =>\n typeof device === 'object' &&\n device !== null &&\n 'deviceId' in device &&\n device.deviceId === identity.current().deviceId,\n );\n if (typeof own !== 'object' || own === null || !('deviceName' in own) || !('nameUpdatedAt' in own)) return;\n if (\n typeof own.deviceName !== 'string' ||\n typeof own.nameUpdatedAt !== 'number' ||\n !Number.isSafeInteger(own.nameUpdatedAt) ||\n own.nameUpdatedAt < 0\n )\n return;\n await identity.updateName(own.deviceName, own.nameUpdatedAt);\n}\n\nconst DEVICE_KEY = 'po.client.device.v1';\nconst INSTANCE_KEY = 'po.client.instance.v1';\n\n/** Storage failures explicitly degrade to one in-memory identity, never an auth identity. */\nexport async function createClientIdentity<T extends IdentityRecord>(\n options: ClientIdentityOptions<T>,\n): Promise<ClientIdentity<T>> {\n let current = { ...options.initial };\n let persistent = true;\n const listeners = new Set<() => void>();\n try {\n await options.exclusive(async () => {\n const saved = options.parse(await options.read());\n if (saved) current = { ...saved, ...(current.instanceId ? { instanceId: current.instanceId } : {}) };\n else {\n const { instanceId: _instanceId, ...device } = current;\n await options.write(device as T);\n }\n });\n } catch {\n persistent = false;\n }\n return {\n current: () => ({ ...current }),\n persistent: () => persistent,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async updateName(value, timestamp) {\n const name = options.normalizeName(value);\n if (!name) throw new Error('Enter a device name from 1 to 64 characters.');\n const updatedAt = timestamp ?? Math.max(options.now(), current.nameUpdatedAt + 1);\n if (!Number.isSafeInteger(updatedAt) || updatedAt < 0) throw new Error('Invalid device name timestamp.');\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const update = async (): Promise<void> => {\n const saved = options.parse(await options.read());\n if (saved && saved.deviceId === current.deviceId && saved.nameUpdatedAt > current.nameUpdatedAt) {\n current = { ...current, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n }\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const next = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n const { instanceId: _instanceId, ...device } = next;\n await options.write(device as T);\n current = next;\n };\n if (persistent) {\n try {\n await options.exclusive(update);\n } catch {\n persistent = false;\n current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n }\n } else current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n for (const listener of listeners) listener();\n },\n };\n}\n\nexport async function createBrowserClientIdentity<T extends IdentityRecord>(\n options: BrowserIdentityOptions<T>,\n): Promise<ClientIdentity<T> & { dispose(): void }> {\n let instanceId = options.randomId();\n let sessionPersistent = true;\n try {\n const inherited = options.sessionStorage.getItem(INSTANCE_KEY);\n if (options.isToken(inherited) && inherited && (await options.claimInstance(inherited))) instanceId = inherited;\n else if (!(await options.claimInstance(instanceId))) throw new Error('The tab identity could not be claimed.');\n options.sessionStorage.setItem(INSTANCE_KEY, instanceId);\n } catch {\n sessionPersistent = false;\n }\n const identity = await createClientIdentity({\n ...options,\n initial: { ...options.initial, instanceId },\n read: async () => {\n const text = options.localStorage.getItem(DEVICE_KEY);\n return text === null ? undefined : (JSON.parse(text) as unknown);\n },\n write: async (metadata) => options.localStorage.setItem(DEVICE_KEY, JSON.stringify(metadata)),\n exclusive: (operation) => options.exclusive(DEVICE_KEY, operation),\n });\n let observed = identity.current();\n const listeners = new Set<() => void>();\n const changed = (): void => {\n for (const listener of listeners) listener();\n };\n const unsubscribe = identity.subscribe(() => {\n observed = identity.current();\n changed();\n });\n const stopStorage = options.onStorage(() => {\n try {\n const text = options.localStorage.getItem(DEVICE_KEY);\n const saved = text === null ? undefined : options.parse(JSON.parse(text));\n if (\n saved &&\n saved.deviceId === observed.deviceId &&\n (saved.nameUpdatedAt > observed.nameUpdatedAt ||\n (saved.nameUpdatedAt === observed.nameUpdatedAt && saved.deviceName !== observed.deviceName))\n ) {\n observed = { ...observed, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n changed();\n }\n } catch {\n sessionPersistent = false;\n }\n });\n return {\n current: () => ({ ...observed }),\n persistent: () => sessionPersistent && identity.persistent(),\n async updateName(name, timestamp) {\n // Merge storage notifications before assigning a local timestamp.\n await identity.updateName(name, timestamp ?? Math.max(options.now(), observed.nameUpdatedAt + 1));\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n dispose() {\n stopStorage();\n unsubscribe();\n listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["metadataProvider","nameUpdater","bindClientMetadata","provider","updateName","updateClientDeviceName","name","timestamp","clientMetadata","refreshClientDeviceName","response","request","value","own","device","identity","DEVICE_KEY","INSTANCE_KEY","createClientIdentity","current","options","persistent","listeners","saved","_instanceId","listener","updatedAt","update","next","createBrowserClientIdentity","instanceId","sessionPersistent","inherited","text","metadata","operation","observed","changed","unsubscribe","stopStorage"],"sources":["../../../src/services/clientIdentity/index.ts"],"sourcesContent":["import type { BrowserIdentityOptions, ClientIdentity, ClientIdentityOptions, IdentityRecord } from './type';\n\nlet metadataProvider: (() => string | undefined) | undefined;\nlet nameUpdater: ((name: unknown, timestamp: number) => Promise<void>) | undefined;\n\n/** The shell binds its current encoded metadata, keeping the codec out of this package. */\nexport function bindClientMetadata(\n provider: () => string | undefined,\n updateName?: (name: unknown, timestamp: number) => Promise<void>,\n): () => void {\n metadataProvider = provider;\n nameUpdater = updateName;\n return () => {\n if (metadataProvider === provider) {\n metadataProvider = undefined;\n nameUpdater = undefined;\n }\n };\n}\n\n/** Cache a canonical name through the owning shell, without importing its identity codec. */\nexport async function updateClientDeviceName(name: unknown, timestamp: number): Promise<void> {\n if (!nameUpdater) throw new Error('Client identity name updates are unavailable.');\n await nameUpdater(name, timestamp);\n}\n\nexport function clientMetadata(): string | undefined {\n return metadataProvider?.();\n}\n\n/** Registry names are display-only. Only a valid canonical record for this device is cached. */\nexport async function refreshClientDeviceName<T extends IdentityRecord>(\n identity: ClientIdentity<T>,\n request: () => Promise<Response>,\n): Promise<void> {\n const response = await request();\n if (response.status === 404) return;\n if (!response.ok) throw new Error(`Client device refresh failed (${response.status}).`);\n const value: unknown = await response.json();\n if (typeof value !== 'object' || value === null || !('devices' in value) || !Array.isArray(value.devices)) return;\n const own: unknown = value.devices.find(\n (device: unknown) =>\n typeof device === 'object' &&\n device !== null &&\n 'deviceId' in device &&\n device.deviceId === identity.current().deviceId,\n );\n if (typeof own !== 'object' || own === null || !('deviceName' in own) || !('nameUpdatedAt' in own)) return;\n if (\n typeof own.deviceName !== 'string' ||\n typeof own.nameUpdatedAt !== 'number' ||\n !Number.isSafeInteger(own.nameUpdatedAt) ||\n own.nameUpdatedAt < 0\n )\n return;\n await identity.updateName(own.deviceName, own.nameUpdatedAt);\n}\n\nconst DEVICE_KEY = 'po.client.device.v1';\nconst INSTANCE_KEY = 'po.client.instance.v1';\n\n/** Storage failures explicitly degrade to one in-memory identity, never an auth identity. */\nexport async function createClientIdentity<T extends IdentityRecord>(\n options: ClientIdentityOptions<T>,\n): Promise<ClientIdentity<T>> {\n let current = { ...options.initial };\n let persistent = true;\n const listeners = new Set<() => void>();\n try {\n await options.exclusive(async () => {\n const saved = options.parse(await options.read());\n if (saved) current = { ...saved, ...(current.instanceId ? { instanceId: current.instanceId } : {}) };\n else {\n const { instanceId: _instanceId, ...device } = current;\n await options.write(device as T);\n }\n });\n } catch {\n persistent = false;\n }\n return {\n current: () => ({ ...current }),\n persistent: () => persistent,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async updateName(value, timestamp) {\n const name = options.normalizeName(value);\n if (!name) throw new Error('Enter a device name from 1 to 64 characters.');\n const updatedAt = timestamp ?? Math.max(options.now(), current.nameUpdatedAt + 1);\n if (!Number.isSafeInteger(updatedAt) || updatedAt < 0) throw new Error('Invalid device name timestamp.');\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const update = async (): Promise<void> => {\n const saved = options.parse(await options.read());\n if (saved && saved.deviceId === current.deviceId && saved.nameUpdatedAt > current.nameUpdatedAt) {\n current = { ...current, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n }\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const next = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n const { instanceId: _instanceId, ...device } = next;\n await options.write(device as T);\n current = next;\n };\n if (persistent) {\n try {\n await options.exclusive(update);\n } catch {\n persistent = false;\n current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n }\n } else current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n for (const listener of listeners) listener();\n },\n };\n}\n\nexport async function createBrowserClientIdentity<T extends IdentityRecord>(\n options: BrowserIdentityOptions<T>,\n): Promise<ClientIdentity<T> & { dispose(): void }> {\n let instanceId = options.randomId();\n let sessionPersistent = true;\n try {\n const inherited = options.sessionStorage.getItem(INSTANCE_KEY);\n if (options.isToken(inherited) && inherited && (await options.claimInstance(inherited))) instanceId = inherited;\n else if (!(await options.claimInstance(instanceId))) throw new Error('The tab identity could not be claimed.');\n options.sessionStorage.setItem(INSTANCE_KEY, instanceId);\n } catch {\n sessionPersistent = false;\n }\n const identity = await createClientIdentity({\n ...options,\n initial: { ...options.initial, instanceId },\n read: async () => {\n const text = options.localStorage.getItem(DEVICE_KEY);\n return text === null ? undefined : (JSON.parse(text) as unknown);\n },\n write: async (metadata) => options.localStorage.setItem(DEVICE_KEY, JSON.stringify(metadata)),\n exclusive: (operation) => options.exclusive(DEVICE_KEY, operation),\n });\n let observed = identity.current();\n const listeners = new Set<() => void>();\n const changed = (): void => {\n for (const listener of listeners) listener();\n };\n const unsubscribe = identity.subscribe(() => {\n observed = identity.current();\n changed();\n });\n const stopStorage = options.onStorage(() => {\n try {\n const text = options.localStorage.getItem(DEVICE_KEY);\n const saved = text === null ? undefined : options.parse(JSON.parse(text));\n if (\n saved &&\n saved.deviceId === observed.deviceId &&\n (saved.nameUpdatedAt > observed.nameUpdatedAt ||\n (saved.nameUpdatedAt === observed.nameUpdatedAt && saved.deviceName !== observed.deviceName))\n ) {\n observed = { ...observed, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n changed();\n }\n } catch {\n sessionPersistent = false;\n }\n });\n return {\n current: () => ({ ...observed }),\n persistent: () => sessionPersistent && identity.persistent(),\n async updateName(name, timestamp) {\n // Merge storage notifications before assigning a local timestamp.\n await identity.updateName(name, timestamp ?? Math.max(options.now(), observed.nameUpdatedAt + 1));\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n dispose() {\n stopStorage();\n unsubscribe();\n listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["createPublicKey","attestationBytes","verify","require_bundleManifest","node_crypto","BASE64URL","isBase64Url","value","maximum","p256Key","key","spki","verifyDeviceSignature","message","tag","fields","signature"],"sources":["../../../src/services/deviceSignature/index.ts"],"sourcesContent":["import { type KeyObject, createPublicKey, verify } from 'node:crypto';\n\nimport { attestationBytes } from '../../types/bundleManifest';\n\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst P1363_SIGNATURE_LENGTH = 86;\nconst MAX_SPKI_LENGTH = 512;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\n\nfunction isBase64Url(value: unknown, maximum: number): value is string {\n return typeof value === 'string' && value.length > 0 && value.length <= maximum && BASE64URL.test(value);\n}\n\n/** The key for a base64url SPKI only when it is an EC P-256 public key. */\nfunction p256Key(spki: string): KeyObject | undefined {\n try {\n const key = createPublicKey({ key: Buffer.from(spki, 'base64url'), format: 'der', type: 'spki' });\n if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== CURVE) return undefined;\n return key;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Verifies a tagged device signature with `node:crypto`. The key must be an EC `prime256v1` SPKI and the\n * signature a base64url IEEE P1363 ECDSA SHA-256 signature over `attestationBytes(tag, fields)`.\n * False for any malformed key or signature and for a mismatch; throws only on a malformed tag.\n */\nexport function verifyDeviceSignature(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): boolean {\n const message = attestationBytes(tag, fields);\n if (!isBase64Url(spki, MAX_SPKI_LENGTH)) return false;\n if (!isBase64Url(signature, P1363_SIGNATURE_LENGTH) || signature.length !== P1363_SIGNATURE_LENGTH) return false;\n const key = p256Key(spki);\n if (key === undefined) return false;\n try {\n return verify(\n SIGN_ALGORITHM,\n message,\n { key: key.export({ format: 'pem', type: 'spki' }), dsaEncoding: 'ieee-p1363' },\n Buffer.from(signature, 'base64url'),\n );\n } catch {\n return false;\n }\n}\n"],"mappings":"+EAIA,MAIMK,EAAY,oBAElB,SAASC,EAAY,EAAgB,EAAkC,CACrE,OAAO,OAAOC,GAAU,UAAYA,EAAM,OAAS,GAAKA,EAAM,QAAUC,GAAWH,EAAU,KAAKE,CAAK,CACzG,CAGA,SAASE,EAAQ,EAAqC,CACpD,GAAI,CACF,IAAMC,GAAA,EAAMV,EAAAA,gBAAAA,CAAgB,CAAE,IAAK,OAAO,KAAKW,EAAM,WAAW,EAAG,OAAQ,MAAO,KAAM,MAAO,CAAC,EAEhG,OADID,EAAI,oBAAsB,MAAQA,EAAI,sBAAsB,aAAe,aAAO,OAC/EA,CACT,MAAQ,CACN,MACF,CACF,CAOA,SAAgBE,EACd,EACA,EACA,EACA,EACS,CACT,IAAMC,EAAUZ,EAAAA,iBAAiBa,EAAKC,CAAM,EAE5C,GADI,CAACT,EAAYK,EAAM,GAAe,GAClC,CAACL,EAAYU,EAAW,EAAsB,GAAKA,EAAU,SAAW,GAAwB,MAAO,GAC3G,IAAMN,EAAMD,EAAQE,CAAI,EACxB,GAAID,IAAQ,IAAA,GAAW,MAAO,GAC9B,GAAI,CACF,OAAA,EAAOR,EAAAA,OAAAA,CACL,SACAW,EACA,CAAE,IAAKH,EAAI,OAAO,CAAE,OAAQ,MAAO,KAAM,MAAO,CAAC,EAAG,YAAa,YAAa,EAC9E,OAAO,KAAKM,EAAW,WAAW,CACpC,CACF,MAAQ,CACN,MAAO,EACT,CACF"}
1
+ {"version":3,"file":"index.cjs","names":["createPublicKey","attestationBytes","verify","require_bundleManifest","node_crypto","BASE64URL","isBase64Url","value","maximum","p256Key","key","spki","verifyDeviceSignature","message","tag","fields","signature"],"sources":["../../../src/services/deviceSignature/index.ts"],"sourcesContent":["import { type KeyObject, createPublicKey, verify } from 'node:crypto';\n\nimport { attestationBytes } from '../../types/bundleManifest';\n\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst P1363_SIGNATURE_LENGTH = 86;\nconst MAX_SPKI_LENGTH = 512;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\n\nfunction isBase64Url(value: unknown, maximum: number): value is string {\n return typeof value === 'string' && value.length > 0 && value.length <= maximum && BASE64URL.test(value);\n}\n\n/** The key for a base64url SPKI only when it is an EC P-256 public key. */\nfunction p256Key(spki: string): KeyObject | undefined {\n try {\n const key = createPublicKey({ key: Buffer.from(spki, 'base64url'), format: 'der', type: 'spki' });\n if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== CURVE) return undefined;\n return key;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Verifies a tagged device signature with `node:crypto`. The key must be an EC `prime256v1` SPKI and the\n * signature a base64url IEEE P1363 ECDSA SHA-256 signature over `attestationBytes(tag, fields)`.\n * False for any malformed key or signature and for a mismatch; throws only on a malformed tag.\n */\nexport function verifyDeviceSignature(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): boolean {\n const message = attestationBytes(tag, fields);\n if (!isBase64Url(spki, MAX_SPKI_LENGTH)) return false;\n if (!isBase64Url(signature, P1363_SIGNATURE_LENGTH) || signature.length !== P1363_SIGNATURE_LENGTH) return false;\n const key = p256Key(spki);\n if (key === undefined) return false;\n try {\n return verify(\n SIGN_ALGORITHM,\n message,\n { key: key.export({ format: 'pem', type: 'spki' }), dsaEncoding: 'ieee-p1363' },\n Buffer.from(signature, 'base64url'),\n );\n } catch {\n return false;\n }\n}\n"],"mappings":"A,+EAIA,MAIMK,EAAY,oBAElB,SAASC,EAAY,EAAgB,EAAkC,CACrE,OAAO,OAAOC,GAAU,UAAYA,EAAM,OAAS,GAAKA,EAAM,QAAUC,GAAWH,EAAU,KAAKE,CAAK,CACzG,CAGA,SAASE,EAAQ,EAAqC,CACpD,GAAI,CACF,IAAMC,GAAA,EAAMV,EAAAA,gBAAAA,CAAgB,CAAE,IAAK,OAAO,KAAKW,EAAM,WAAW,EAAG,OAAQ,MAAO,KAAM,MAAO,CAAC,EAEhG,OADID,EAAI,oBAAsB,MAAQA,EAAI,sBAAsB,aAAe,aAAO,OAC/EA,CACT,MAAQ,CACN,MACF,CACF,CAOA,SAAgBE,EACd,EACA,EACA,EACA,EACS,CACT,IAAMC,EAAUZ,EAAAA,iBAAiBa,EAAKC,CAAM,EAE5C,GADI,CAACT,EAAYK,EAAM,GAAe,GAClC,CAACL,EAAYU,EAAW,EAAsB,GAAKA,EAAU,SAAW,GAAwB,MAAO,GAC3G,IAAMN,EAAMD,EAAQE,CAAI,EACxB,GAAID,IAAQ,IAAA,GAAW,MAAO,GAC9B,GAAI,CACF,OAAA,EAAOR,EAAAA,OAAAA,CACL,SACAW,EACA,CAAE,IAAKH,EAAI,OAAO,CAAE,OAAQ,MAAO,KAAM,MAAO,CAAC,EAAG,YAAa,YAAa,EAC9E,OAAO,KAAKM,EAAW,WAAW,CACpC,CACF,MAAQ,CACN,MAAO,EACT,CACF,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["BASE64URL","isBase64Url","value","maximum","p256Key","key","createPublicKey","spki","verifyDeviceSignature","message","attestationBytes","tag","fields","signature","verify"],"sources":["../../../src/services/deviceSignature/index.ts"],"sourcesContent":["import { type KeyObject, createPublicKey, verify } from 'node:crypto';\n\nimport { attestationBytes } from '../../types/bundleManifest';\n\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst P1363_SIGNATURE_LENGTH = 86;\nconst MAX_SPKI_LENGTH = 512;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\n\nfunction isBase64Url(value: unknown, maximum: number): value is string {\n return typeof value === 'string' && value.length > 0 && value.length <= maximum && BASE64URL.test(value);\n}\n\n/** The key for a base64url SPKI only when it is an EC P-256 public key. */\nfunction p256Key(spki: string): KeyObject | undefined {\n try {\n const key = createPublicKey({ key: Buffer.from(spki, 'base64url'), format: 'der', type: 'spki' });\n if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== CURVE) return undefined;\n return key;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Verifies a tagged device signature with `node:crypto`. The key must be an EC `prime256v1` SPKI and the\n * signature a base64url IEEE P1363 ECDSA SHA-256 signature over `attestationBytes(tag, fields)`.\n * False for any malformed key or signature and for a mismatch; throws only on a malformed tag.\n */\nexport function verifyDeviceSignature(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): boolean {\n const message = attestationBytes(tag, fields);\n if (!isBase64Url(spki, MAX_SPKI_LENGTH)) return false;\n if (!isBase64Url(signature, P1363_SIGNATURE_LENGTH) || signature.length !== P1363_SIGNATURE_LENGTH) return false;\n const key = p256Key(spki);\n if (key === undefined) return false;\n try {\n return verify(\n SIGN_ALGORITHM,\n message,\n { key: key.export({ format: 'pem', type: 'spki' }), dsaEncoding: 'ieee-p1363' },\n Buffer.from(signature, 'base64url'),\n );\n } catch {\n return false;\n }\n}\n"],"mappings":"4HAIA,MAIMA,EAAY,oBAElB,SAASC,EAAY,EAAgB,EAAkC,CACrE,OAAO,OAAOC,GAAU,UAAYA,EAAM,OAAS,GAAKA,EAAM,QAAUC,GAAWH,EAAU,KAAKE,CAAK,CACzG,CAGA,SAASE,EAAQ,EAAqC,CACpD,GAAI,CACF,IAAMC,EAAMC,EAAgB,CAAE,IAAK,OAAO,KAAKC,EAAM,WAAW,EAAG,OAAQ,MAAO,KAAM,MAAO,CAAC,EAEhG,OADIF,EAAI,oBAAsB,MAAQA,EAAI,sBAAsB,aAAe,aAAO,OAC/EA,CACT,MAAQ,CACN,MACF,CACF,CAOA,SAAgBG,EACd,EACA,EACA,EACA,EACS,CACT,IAAMC,EAAUC,EAAiBC,EAAKC,CAAM,EAE5C,GADI,CAACX,EAAYM,EAAM,GAAe,GAClC,CAACN,EAAYY,EAAW,EAAsB,GAAKA,EAAU,SAAW,GAAwB,MAAO,GAC3G,IAAMR,EAAMD,EAAQG,CAAI,EACxB,GAAIF,IAAQ,IAAA,GAAW,MAAO,GAC9B,GAAI,CACF,OAAOS,EACL,SACAL,EACA,CAAE,IAAKJ,EAAI,OAAO,CAAE,OAAQ,MAAO,KAAM,MAAO,CAAC,EAAG,YAAa,YAAa,EAC9E,OAAO,KAAKQ,EAAW,WAAW,CACpC,CACF,MAAQ,CACN,MAAO,EACT,CACF"}
1
+ {"version":3,"file":"index.mjs","names":["BASE64URL","isBase64Url","value","maximum","p256Key","key","createPublicKey","spki","verifyDeviceSignature","message","attestationBytes","tag","fields","signature","verify"],"sources":["../../../src/services/deviceSignature/index.ts"],"sourcesContent":["import { type KeyObject, createPublicKey, verify } from 'node:crypto';\n\nimport { attestationBytes } from '../../types/bundleManifest';\n\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst P1363_SIGNATURE_LENGTH = 86;\nconst MAX_SPKI_LENGTH = 512;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\n\nfunction isBase64Url(value: unknown, maximum: number): value is string {\n return typeof value === 'string' && value.length > 0 && value.length <= maximum && BASE64URL.test(value);\n}\n\n/** The key for a base64url SPKI only when it is an EC P-256 public key. */\nfunction p256Key(spki: string): KeyObject | undefined {\n try {\n const key = createPublicKey({ key: Buffer.from(spki, 'base64url'), format: 'der', type: 'spki' });\n if (key.asymmetricKeyType !== 'ec' || key.asymmetricKeyDetails?.namedCurve !== CURVE) return undefined;\n return key;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Verifies a tagged device signature with `node:crypto`. The key must be an EC `prime256v1` SPKI and the\n * signature a base64url IEEE P1363 ECDSA SHA-256 signature over `attestationBytes(tag, fields)`.\n * False for any malformed key or signature and for a mismatch; throws only on a malformed tag.\n */\nexport function verifyDeviceSignature(\n spki: string,\n tag: string,\n fields: readonly string[],\n signature: string,\n): boolean {\n const message = attestationBytes(tag, fields);\n if (!isBase64Url(spki, MAX_SPKI_LENGTH)) return false;\n if (!isBase64Url(signature, P1363_SIGNATURE_LENGTH) || signature.length !== P1363_SIGNATURE_LENGTH) return false;\n const key = p256Key(spki);\n if (key === undefined) return false;\n try {\n return verify(\n SIGN_ALGORITHM,\n message,\n { key: key.export({ format: 'pem', type: 'spki' }), dsaEncoding: 'ieee-p1363' },\n Buffer.from(signature, 'base64url'),\n );\n } catch {\n return false;\n }\n}\n"],"mappings":"A,4HAIA,MAIMA,EAAY,oBAElB,SAASC,EAAY,EAAgB,EAAkC,CACrE,OAAO,OAAOC,GAAU,UAAYA,EAAM,OAAS,GAAKA,EAAM,QAAUC,GAAWH,EAAU,KAAKE,CAAK,CACzG,CAGA,SAASE,EAAQ,EAAqC,CACpD,GAAI,CACF,IAAMC,EAAMC,EAAgB,CAAE,IAAK,OAAO,KAAKC,EAAM,WAAW,EAAG,OAAQ,MAAO,KAAM,MAAO,CAAC,EAEhG,OADIF,EAAI,oBAAsB,MAAQA,EAAI,sBAAsB,aAAe,aAAO,OAC/EA,CACT,MAAQ,CACN,MACF,CACF,CAOA,SAAgBG,EACd,EACA,EACA,EACA,EACS,CACT,IAAMC,EAAUC,EAAiBC,EAAKC,CAAM,EAE5C,GADI,CAACX,EAAYM,EAAM,GAAe,GAClC,CAACN,EAAYY,EAAW,EAAsB,GAAKA,EAAU,SAAW,GAAwB,MAAO,GAC3G,IAAMR,EAAMD,EAAQG,CAAI,EACxB,GAAIF,IAAQ,IAAA,GAAW,MAAO,GAC9B,GAAI,CACF,OAAOS,EACL,SACAL,EACA,CAAE,IAAKJ,EAAI,OAAO,CAAE,OAAQ,MAAO,KAAM,MAAO,CAAC,EAAG,YAAa,YAAa,EAC9E,OAAO,KAAKQ,EAAW,WAAW,CACpC,CACF,MAAQ,CACN,MAAO,EACT,CACF,C"}