@agimon-ai/doompi-web-security 0.0.1-alpha.4 → 0.0.1-alpha.6

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 (34) hide show
  1. package/README.md +14 -7
  2. package/dist/adapters/browserBundleVerifier.cjs +2 -0
  3. package/dist/adapters/browserBundleVerifier.cjs.map +1 -0
  4. package/dist/adapters/browserBundleVerifier.d.cts +45 -0
  5. package/dist/adapters/browserBundleVerifier.d.cts.map +1 -0
  6. package/dist/adapters/browserBundleVerifier.d.mts +45 -0
  7. package/dist/adapters/browserBundleVerifier.d.mts.map +1 -0
  8. package/dist/adapters/browserBundleVerifier.mjs +2 -0
  9. package/dist/adapters/browserBundleVerifier.mjs.map +1 -0
  10. package/dist/adapters/bundleSigner.cjs +1 -1
  11. package/dist/adapters/bundleSigner.cjs.map +1 -1
  12. package/dist/adapters/bundleSigner.d.cts +7 -3
  13. package/dist/adapters/bundleSigner.d.cts.map +1 -1
  14. package/dist/adapters/bundleSigner.d.mts +7 -3
  15. package/dist/adapters/bundleSigner.d.mts.map +1 -1
  16. package/dist/adapters/bundleSigner.mjs +1 -1
  17. package/dist/adapters/bundleSigner.mjs.map +1 -1
  18. package/dist/browser.cjs +1 -1
  19. package/dist/browser.d.cts +3 -1
  20. package/dist/browser.d.mts +3 -1
  21. package/dist/browser.mjs +1 -1
  22. package/dist/index.cjs +1 -1
  23. package/dist/index.d.cts +2 -2
  24. package/dist/index.d.mts +2 -2
  25. package/dist/index.mjs +1 -1
  26. package/dist/types/bundleManifest.cjs +0 -0
  27. package/dist/types/bundleManifest.cjs.map +1 -1
  28. package/dist/types/bundleManifest.d.cts +18 -21
  29. package/dist/types/bundleManifest.d.cts.map +1 -1
  30. package/dist/types/bundleManifest.d.mts +18 -21
  31. package/dist/types/bundleManifest.d.mts.map +1 -1
  32. package/dist/types/bundleManifest.mjs +0 -0
  33. package/dist/types/bundleManifest.mjs.map +1 -1
  34. package/package.json +2 -2
package/README.md CHANGED
@@ -50,13 +50,20 @@ go on.
50
50
 
51
51
  ## The signed bundle-manifest primitive
52
52
 
53
- The signer can support a separately distributed verifier whose bootstrap and public key are already
54
- trusted. It hashes every asset and signs a canonical manifest with ECDSA P-256.
55
-
56
- It is not a self-protection mechanism for a browser SPA. A server or TLS edge that can replace the page
57
- can also remove an in-page verifier or replace the key it trusts. DoomPi's Cloudflare transport treats
58
- Cloudflare as a trusted code-delivery boundary, so the cockpit does not use this primitive to claim
59
- protection from a malicious edge.
53
+ The Node signer traverses a public asset root without following symlinks, requires `/index.html`, hashes each
54
+ byte sequence, and signs a strict canonical manifest v2 with ECDSA P-256. Revision state and the private key
55
+ are persisted atomically at mode `0600`; malformed or replaced state fails closed instead of rotating
56
+ silently.
57
+
58
+ The browser verifier accepts only the public SPKI and minimum revision pinned by the physical QR. DoomPi's
59
+ package-owned service worker verifies the envelope and every asset before committing a Cache Storage revision,
60
+ then atomically records the active bundle in IndexedDB. A same-revision digest conflict, signer mismatch,
61
+ missing cache, bad MIME metadata, length mismatch, or hash mismatch is refused. The last-known-good bundle
62
+ remains active during a failed refresh.
63
+
64
+ This does not make the package-owned bootstrap self-protecting. A TLS edge that replaces `/pair` or `/sw.js`
65
+ could remove the verifier before it runs. Once the intended bootstrap is installed, however, host and plugin
66
+ JavaScript cannot execute until its signed bytes pass verification.
60
67
 
61
68
  `canonicalManifest` is hand-rolled rather than `JSON.stringify` of the whole object, because a signature
62
69
  is only worth anything if signer and verifier agree byte for byte, and key order in a JSON object is an
@@ -0,0 +1,2 @@
1
+ const e=require("../types/bundleManifest.cjs");function t(e){let t=e.replaceAll(`-`,`+`).replaceAll(`_`,`/`),n=t.padEnd(Math.ceil(t.length/4)*4,`=`),r=atob(n),i=new Uint8Array(r.length);for(let e=0;e<r.length;e+=1)i[e]=r.charCodeAt(e);return i.buffer}function n(e){return[...new Uint8Array(e)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function r(n,r,i=0){if(!e.isSignedBundleManifest(n))return{ok:!1,failure:{code:`invalid-envelope`}};let a=n;if(a.publicKey!==r)return{ok:!1,failure:{code:`untrusted-public-key`}};if(!Number.isSafeInteger(i)||i<0||a.manifest.revision<i)return{ok:!1,failure:{code:`stale-revision`,minimumRevision:i,actualRevision:a.manifest.revision}};let o;try{o=await crypto.subtle.importKey(`spki`,t(r),{name:`ECDSA`,namedCurve:`P-256`},!1,[`verify`])}catch{return{ok:!1,failure:{code:`invalid-public-key`}}}try{return await crypto.subtle.verify({name:`ECDSA`,hash:`SHA-256`},o,t(a.signature),new TextEncoder().encode(e.canonicalManifest(a.manifest)))?{ok:!0,manifest:a.manifest}:{ok:!1,failure:{code:`invalid-signature`}}}catch{return{ok:!1,failure:{code:`invalid-signature`}}}}async function i(t,r,i){let a=e.assetFor(t,r);return a===void 0?{ok:!1,failure:{code:`asset-not-listed`,path:r}}:i.byteLength===a.byteLength?n(await crypto.subtle.digest(`SHA-256`,i))===a.sha256?{ok:!0}:{ok:!1,failure:{code:`digest-mismatch`}}:{ok:!1,failure:{code:`byte-length-mismatch`,expected:a.byteLength,actual:i.byteLength}}}exports.verifyBundleAsset=i,exports.verifySignedBundleManifest=r;
2
+ //# sourceMappingURL=browserBundleVerifier.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browserBundleVerifier.cjs","names":["isSignedBundleManifest","canonicalManifest","assetFor"],"sources":["../../src/adapters/browserBundleVerifier.ts"],"sourcesContent":["import {\n type BundleManifest,\n type SignedBundleManifest,\n assetFor,\n canonicalManifest,\n isSignedBundleManifest,\n} from '../types/bundleManifest.ts';\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\n/** Verifies a strict envelope against a separately trusted key and revision floor. */\nexport async function verifySignedBundleManifest(\n value: unknown,\n trustedPublicKey: string,\n minimumRevision = 0,\n): Promise<BundleVerificationResult> {\n if (!isSignedBundleManifest(value)) 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 crypto.subtle.importKey(\n 'spki',\n decodeBase64Url(trustedPublicKey),\n { name: 'ECDSA', namedCurve: 'P-256' },\n false,\n ['verify'],\n );\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/** 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":"+CA0BA,SAAS,EAAgB,EAA4B,CACnD,IAAM,EAAW,EAAM,WAAW,IAAK,GAAG,CAAC,CAAC,WAAW,IAAK,GAAG,EACzD,EAAS,EAAS,OAAO,KAAK,KAAK,EAAS,OAAS,CAAC,EAAI,EAAG,GAAG,EAChE,EAAS,KAAK,CAAM,EACpB,EAAQ,IAAI,WAAW,EAAO,MAAM,EAC1C,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAO,OAAQ,GAAS,EAAG,EAAM,GAAS,EAAO,WAAW,CAAK,EAC7F,OAAO,EAAM,MACf,CAEA,SAAS,EAAI,EAA4B,CACvC,MAAO,CAAC,GAAG,IAAI,WAAW,CAAK,CAAC,CAAC,CAAC,IAAK,GAAS,EAAK,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CAC7F,CAGA,eAAsB,EACpB,EACA,EACA,EAAkB,EACiB,CACnC,GAAI,CAACA,EAAAA,uBAAuB,CAAK,EAAG,MAAO,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,kBAAmB,CAAE,EAC9F,IAAM,EAA+B,EACrC,GAAI,EAAO,YAAc,EAAkB,MAAO,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,sBAAuB,CAAE,EACzG,GAAI,CAAC,OAAO,cAAc,CAAe,GAAK,EAAkB,GAAK,EAAO,SAAS,SAAW,EAC9F,MAAO,CACL,GAAI,GACJ,QAAS,CAAE,KAAM,iBAAkB,kBAAiB,eAAgB,EAAO,SAAS,QAAS,CAC/F,EAGF,IAAI,EACJ,GAAI,CACF,EAAM,MAAM,OAAO,OAAO,UACxB,OACA,EAAgB,CAAgB,EAChC,CAAE,KAAM,QAAS,WAAY,OAAQ,EACrC,GACA,CAAC,QAAQ,CACX,CACF,MAAQ,CACN,MAAO,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,oBAAqB,CAAE,CAC9D,CAEA,GAAI,CAOF,OAAO,MANgB,OAAO,OAAO,OACnC,CAAE,KAAM,QAAS,KAAM,SAAU,EACjC,EACA,EAAgB,EAAO,SAAS,EAChC,IAAI,YAAY,CAAC,CAAC,OAAOC,EAAAA,kBAAkB,EAAO,QAAQ,CAAC,CAC7D,EACkB,CAAE,GAAI,GAAM,SAAU,EAAO,QAAS,EAAI,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,mBAAoB,CAAE,CAClH,MAAQ,CACN,MAAO,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,mBAAoB,CAAE,CAC7D,CACF,CAGA,eAAsB,EACpB,EACA,EACA,EACwC,CACxC,IAAM,EAAQC,EAAAA,SAAS,EAAU,CAAS,EAS1C,OARI,IAAU,IAAA,GAAkB,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,mBAAoB,KAAM,CAAU,CAAE,EAChG,EAAM,aAAe,EAAM,WAMhB,EAAI,MAAM,OAAO,OAAO,OAAO,UAAW,CAAK,CAClD,IAAM,EAAM,OAAS,CAAE,GAAI,EAAK,EAAI,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,iBAAkB,CAAE,EANzF,CACL,GAAI,GACJ,QAAS,CAAE,KAAM,uBAAwB,SAAU,EAAM,WAAY,OAAQ,EAAM,UAAW,CAChG,CAIJ"}
@@ -0,0 +1,45 @@
1
+ import { BundleManifest } from "../types/bundleManifest.cjs";
2
+ //#region src/adapters/browserBundleVerifier.d.ts
3
+ type BundleVerificationFailure = {
4
+ code: 'invalid-envelope';
5
+ } | {
6
+ code: 'untrusted-public-key';
7
+ } | {
8
+ code: 'stale-revision';
9
+ minimumRevision: number;
10
+ actualRevision: number;
11
+ } | {
12
+ code: 'invalid-public-key';
13
+ } | {
14
+ code: 'invalid-signature';
15
+ };
16
+ type BundleVerificationResult = {
17
+ ok: true;
18
+ manifest: BundleManifest;
19
+ } | {
20
+ ok: false;
21
+ failure: BundleVerificationFailure;
22
+ };
23
+ type BundleAssetVerificationFailure = {
24
+ code: 'asset-not-listed';
25
+ path: string;
26
+ } | {
27
+ code: 'byte-length-mismatch';
28
+ expected: number;
29
+ actual: number;
30
+ } | {
31
+ code: 'digest-mismatch';
32
+ };
33
+ type BundleAssetVerificationResult = {
34
+ ok: true;
35
+ } | {
36
+ ok: false;
37
+ failure: BundleAssetVerificationFailure;
38
+ };
39
+ /** Verifies a strict envelope against a separately trusted key and revision floor. */
40
+ declare function verifySignedBundleManifest(value: unknown, trustedPublicKey: string, minimumRevision?: number): Promise<BundleVerificationResult>;
41
+ /** Verifies fetched raw response bytes against an authenticated manifest. */
42
+ declare function verifyBundleAsset(manifest: BundleManifest, assetPath: string, bytes: ArrayBuffer): Promise<BundleAssetVerificationResult>;
43
+ //#endregion
44
+ export { BundleAssetVerificationFailure, BundleAssetVerificationResult, BundleVerificationFailure, BundleVerificationResult, verifyBundleAsset, verifySignedBundleManifest };
45
+ //# sourceMappingURL=browserBundleVerifier.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browserBundleVerifier.d.cts","names":[],"sources":["../../src/adapters/browserBundleVerifier.ts"],"mappings":";;KAQY;EACN;;EACA;;EACA;EAAwB;EAAyB;;EACjD;;EACA;;KAEM;EACN;EAAU,UAAU;;EACpB;EAAW,SAAS;;KAEd;EACN;EAA0B;;EAC1B;EAA8B;EAAkB;;EAChD;;KAEM;EAAkC;;EAAe;EAAW,SAAS;;;iBAgB3D,2BACpB,gBACA,0BACA,2BACC,QAAQ;;iBAsCW,kBACpB,UAAU,gBACV,mBACA,OAAO,cACN,QAAQ"}
@@ -0,0 +1,45 @@
1
+ import { BundleManifest } from "../types/bundleManifest.mjs";
2
+ //#region src/adapters/browserBundleVerifier.d.ts
3
+ type BundleVerificationFailure = {
4
+ code: 'invalid-envelope';
5
+ } | {
6
+ code: 'untrusted-public-key';
7
+ } | {
8
+ code: 'stale-revision';
9
+ minimumRevision: number;
10
+ actualRevision: number;
11
+ } | {
12
+ code: 'invalid-public-key';
13
+ } | {
14
+ code: 'invalid-signature';
15
+ };
16
+ type BundleVerificationResult = {
17
+ ok: true;
18
+ manifest: BundleManifest;
19
+ } | {
20
+ ok: false;
21
+ failure: BundleVerificationFailure;
22
+ };
23
+ type BundleAssetVerificationFailure = {
24
+ code: 'asset-not-listed';
25
+ path: string;
26
+ } | {
27
+ code: 'byte-length-mismatch';
28
+ expected: number;
29
+ actual: number;
30
+ } | {
31
+ code: 'digest-mismatch';
32
+ };
33
+ type BundleAssetVerificationResult = {
34
+ ok: true;
35
+ } | {
36
+ ok: false;
37
+ failure: BundleAssetVerificationFailure;
38
+ };
39
+ /** Verifies a strict envelope against a separately trusted key and revision floor. */
40
+ declare function verifySignedBundleManifest(value: unknown, trustedPublicKey: string, minimumRevision?: number): Promise<BundleVerificationResult>;
41
+ /** Verifies fetched raw response bytes against an authenticated manifest. */
42
+ declare function verifyBundleAsset(manifest: BundleManifest, assetPath: string, bytes: ArrayBuffer): Promise<BundleAssetVerificationResult>;
43
+ //#endregion
44
+ export { BundleAssetVerificationFailure, BundleAssetVerificationResult, BundleVerificationFailure, BundleVerificationResult, verifyBundleAsset, verifySignedBundleManifest };
45
+ //# sourceMappingURL=browserBundleVerifier.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browserBundleVerifier.d.mts","names":[],"sources":["../../src/adapters/browserBundleVerifier.ts"],"mappings":";;KAQY;EACN;;EACA;;EACA;EAAwB;EAAyB;;EACjD;;EACA;;KAEM;EACN;EAAU,UAAU;;EACpB;EAAW,SAAS;;KAEd;EACN;EAA0B;;EAC1B;EAA8B;EAAkB;;EAChD;;KAEM;EAAkC;;EAAe;EAAW,SAAS;;;iBAgB3D,2BACpB,gBACA,0BACA,2BACC,QAAQ;;iBAsCW,kBACpB,UAAU,gBACV,mBACA,OAAO,cACN,QAAQ"}
@@ -0,0 +1,2 @@
1
+ import{assetFor as e,canonicalManifest as t,isSignedBundleManifest as n}from"../types/bundleManifest.mjs";function r(e){let t=e.replaceAll(`-`,`+`).replaceAll(`_`,`/`),n=t.padEnd(Math.ceil(t.length/4)*4,`=`),r=atob(n),i=new Uint8Array(r.length);for(let e=0;e<r.length;e+=1)i[e]=r.charCodeAt(e);return i.buffer}function i(e){return[...new Uint8Array(e)].map(e=>e.toString(16).padStart(2,`0`)).join(``)}async function a(e,i,a=0){if(!n(e))return{ok:!1,failure:{code:`invalid-envelope`}};let o=e;if(o.publicKey!==i)return{ok:!1,failure:{code:`untrusted-public-key`}};if(!Number.isSafeInteger(a)||a<0||o.manifest.revision<a)return{ok:!1,failure:{code:`stale-revision`,minimumRevision:a,actualRevision:o.manifest.revision}};let s;try{s=await crypto.subtle.importKey(`spki`,r(i),{name:`ECDSA`,namedCurve:`P-256`},!1,[`verify`])}catch{return{ok:!1,failure:{code:`invalid-public-key`}}}try{return await crypto.subtle.verify({name:`ECDSA`,hash:`SHA-256`},s,r(o.signature),new TextEncoder().encode(t(o.manifest)))?{ok:!0,manifest:o.manifest}:{ok:!1,failure:{code:`invalid-signature`}}}catch{return{ok:!1,failure:{code:`invalid-signature`}}}}async function o(t,n,r){let a=e(t,n);return a===void 0?{ok:!1,failure:{code:`asset-not-listed`,path:n}}:r.byteLength===a.byteLength?i(await crypto.subtle.digest(`SHA-256`,r))===a.sha256?{ok:!0}:{ok:!1,failure:{code:`digest-mismatch`}}:{ok:!1,failure:{code:`byte-length-mismatch`,expected:a.byteLength,actual:r.byteLength}}}export{o as verifyBundleAsset,a as verifySignedBundleManifest};
2
+ //# sourceMappingURL=browserBundleVerifier.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browserBundleVerifier.mjs","names":[],"sources":["../../src/adapters/browserBundleVerifier.ts"],"sourcesContent":["import {\n type BundleManifest,\n type SignedBundleManifest,\n assetFor,\n canonicalManifest,\n isSignedBundleManifest,\n} from '../types/bundleManifest.ts';\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\n/** Verifies a strict envelope against a separately trusted key and revision floor. */\nexport async function verifySignedBundleManifest(\n value: unknown,\n trustedPublicKey: string,\n minimumRevision = 0,\n): Promise<BundleVerificationResult> {\n if (!isSignedBundleManifest(value)) 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 crypto.subtle.importKey(\n 'spki',\n decodeBase64Url(trustedPublicKey),\n { name: 'ECDSA', namedCurve: 'P-256' },\n false,\n ['verify'],\n );\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/** 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":"0GA0BA,SAAS,EAAgB,EAA4B,CACnD,IAAM,EAAW,EAAM,WAAW,IAAK,GAAG,CAAC,CAAC,WAAW,IAAK,GAAG,EACzD,EAAS,EAAS,OAAO,KAAK,KAAK,EAAS,OAAS,CAAC,EAAI,EAAG,GAAG,EAChE,EAAS,KAAK,CAAM,EACpB,EAAQ,IAAI,WAAW,EAAO,MAAM,EAC1C,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAO,OAAQ,GAAS,EAAG,EAAM,GAAS,EAAO,WAAW,CAAK,EAC7F,OAAO,EAAM,MACf,CAEA,SAAS,EAAI,EAA4B,CACvC,MAAO,CAAC,GAAG,IAAI,WAAW,CAAK,CAAC,CAAC,CAAC,IAAK,GAAS,EAAK,SAAS,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,CAC7F,CAGA,eAAsB,EACpB,EACA,EACA,EAAkB,EACiB,CACnC,GAAI,CAAC,EAAuB,CAAK,EAAG,MAAO,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,kBAAmB,CAAE,EAC9F,IAAM,EAA+B,EACrC,GAAI,EAAO,YAAc,EAAkB,MAAO,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,sBAAuB,CAAE,EACzG,GAAI,CAAC,OAAO,cAAc,CAAe,GAAK,EAAkB,GAAK,EAAO,SAAS,SAAW,EAC9F,MAAO,CACL,GAAI,GACJ,QAAS,CAAE,KAAM,iBAAkB,kBAAiB,eAAgB,EAAO,SAAS,QAAS,CAC/F,EAGF,IAAI,EACJ,GAAI,CACF,EAAM,MAAM,OAAO,OAAO,UACxB,OACA,EAAgB,CAAgB,EAChC,CAAE,KAAM,QAAS,WAAY,OAAQ,EACrC,GACA,CAAC,QAAQ,CACX,CACF,MAAQ,CACN,MAAO,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,oBAAqB,CAAE,CAC9D,CAEA,GAAI,CAOF,OAAO,MANgB,OAAO,OAAO,OACnC,CAAE,KAAM,QAAS,KAAM,SAAU,EACjC,EACA,EAAgB,EAAO,SAAS,EAChC,IAAI,YAAY,CAAC,CAAC,OAAO,EAAkB,EAAO,QAAQ,CAAC,CAC7D,EACkB,CAAE,GAAI,GAAM,SAAU,EAAO,QAAS,EAAI,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,mBAAoB,CAAE,CAClH,MAAQ,CACN,MAAO,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,mBAAoB,CAAE,CAC7D,CACF,CAGA,eAAsB,EACpB,EACA,EACA,EACwC,CACxC,IAAM,EAAQ,EAAS,EAAU,CAAS,EAS1C,OARI,IAAU,IAAA,GAAkB,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,mBAAoB,KAAM,CAAU,CAAE,EAChG,EAAM,aAAe,EAAM,WAMhB,EAAI,MAAM,OAAO,OAAO,OAAO,UAAW,CAAK,CAClD,IAAM,EAAM,OAAS,CAAE,GAAI,EAAK,EAAI,CAAE,GAAI,GAAO,QAAS,CAAE,KAAM,iBAAkB,CAAE,EANzF,CACL,GAAI,GACJ,QAAS,CAAE,KAAM,uBAAwB,SAAU,EAAM,WAAY,OAAQ,EAAM,UAAW,CAChG,CAIJ"}
@@ -1,2 +1,2 @@
1
- const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/bundleManifest.cjs");let n=require("node:crypto"),r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);const a=new Set([`.map`]);function o(e,t){let a=i.default.join(e,`signing.json`);try{let e=JSON.parse(r.default.readFileSync(a,`utf8`));if(typeof e==`object`&&e&&typeof e.privateKey==`string`&&typeof e.publicKey==`string`)return e;t(`the bundle signing key at ${a} is unreadable; generating a new one, so existing pins must be updated`)}catch{}let o=(0,n.generateKeyPairSync)(`ec`,{namedCurve:`prime256v1`,privateKeyEncoding:{type:`pkcs8`,format:`pem`},publicKeyEncoding:{type:`spki`,format:`der`}}),s={privateKey:o.privateKey,publicKey:Buffer.from(o.publicKey).toString(`base64url`)};try{r.default.mkdirSync(e,{recursive:!0,mode:448}),r.default.writeFileSync(a,`${JSON.stringify(s,void 0,2)}\n`,{mode:384})}catch(e){t(`the bundle signing key could not be saved: ${e instanceof Error?e.message:String(e)}`)}return s}function s(e,t,o){let c;try{c=r.default.readdirSync(t,{withFileTypes:!0})}catch{return}for(let l of c){let c=i.default.join(t,l.name);if(l.isDirectory()){s(e,c,o);continue}if(!(!l.isFile()||a.has(i.default.extname(l.name))))try{let t=r.default.readFileSync(c);o.push({path:`/${i.default.relative(e,c).split(i.default.sep).join(`/`)}`,sha256:(0,n.createHash)(`sha256`).update(t).digest(`hex`)})}catch{}}}function c(e,r=()=>{}){let i=o(e,r),a=(0,n.createPrivateKey)(i.privateKey),c=new Map;return{publicKey:()=>i.publicKey,sign(e){let r=c.get(e);if(r!==void 0||c.has(e))return r;let o=[];if(s(e,e,o),o.length===0){c.set(e,void 0);return}let l={version:1,builtAt:Date.now(),assets:o},u={manifest:l,signature:(0,n.createSign)(`sha256`).update(t.canonicalManifest(l)).sign({key:a,dsaEncoding:`ieee-p1363`}).toString(`base64url`),publicKey:i.publicKey};return c.set(e,u),u}}}function l(e){return Buffer.from((0,n.createPublicKey)(e).export({type:`spki`,format:`der`})).toString(`base64url`)}exports.createBundleSigner=c,exports.publicKeyOf=l;
1
+ const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../types/bundleManifest.cjs");let n=require("node:crypto"),r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);const a=`signing.json`,o=new Set([`.map`]),s={".css":`text/css`,".gif":`image/gif`,".html":`text/html`,".ico":`image/x-icon`,".jpeg":`image/jpeg`,".jpg":`image/jpeg`,".js":`text/javascript`,".json":`application/json`,".mjs":`text/javascript`,".png":`image/png`,".svg":`image/svg+xml`,".txt":`text/plain`,".wasm":`application/wasm`,".webp":`image/webp`,".woff":`font/woff`,".woff2":`font/woff2`};function c(e){r.default.mkdirSync(e,{recursive:!0,mode:448});let t=r.default.lstatSync(e);if(!t.isDirectory()||t.isSymbolicLink())throw Error(`bundle signing state is not a directory: ${e}`);r.default.chmodSync(e,448)}function l(e,t){c(e);let n=i.default.join(e,`signing.lock`),a;for(let e=0;e<500;e+=1)try{a=r.default.openSync(n,`wx`,384);break}catch(e){if(e.code!==`EEXIST`)throw e;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,10)}if(a===void 0)throw Error(`timed out waiting for bundle signing state lock: ${n}`);try{return t()}finally{r.default.closeSync(a),r.default.rmSync(n,{force:!0})}}function u(e,t){let o=i.default.join(e,a),s=i.default.join(e,`.${a}.${process.pid}.${(0,n.randomBytes)(8).toString(`hex`)}`),c=r.default.openSync(s,`wx`,384);try{r.default.writeFileSync(c,`${JSON.stringify(t,void 0,2)}\n`),r.default.fsyncSync(c)}finally{r.default.closeSync(c)}try{r.default.renameSync(s,o),r.default.chmodSync(o,384)}catch(e){throw r.default.rmSync(s,{force:!0}),e}}function d(e,t){if(typeof e!=`object`||!e)throw Error(`invalid bundle signing state: ${t}`);let n=e;if(typeof n.privateKey!=`string`||typeof n.publicKey!=`string`)throw Error(`invalid bundle signing state: ${t}`);let r=n.revision===void 0?0:n.revision;if(!Number.isSafeInteger(r)||r<0)throw Error(`invalid bundle signing revision: ${t}`);let i;try{i=_(n.privateKey)}catch{throw Error(`invalid bundle signing private key: ${t}`)}if(i!==n.publicKey)throw Error(`bundle signing keys do not match: ${t}`);return{privateKey:n.privateKey,publicKey:n.publicKey,revision:r}}function f(e){return l(e,()=>{let t=i.default.join(e,a);try{let n=r.default.lstatSync(t);if(!n.isFile()||n.isSymbolicLink())throw Error(`invalid bundle signing state file: ${t}`);let i=JSON.parse(r.default.readFileSync(t,`utf8`)),a=d(i,t);return r.default.chmodSync(t,384),i.revision===void 0&&u(e,a),a}catch(e){if(e.code!==`ENOENT`)throw e}let o=(0,n.generateKeyPairSync)(`ec`,{namedCurve:`prime256v1`,privateKeyEncoding:{type:`pkcs8`,format:`pem`},publicKeyEncoding:{type:`spki`,format:`der`}}),s={privateKey:o.privateKey,publicKey:Buffer.from(o.publicKey).toString(`base64url`),revision:0};return u(e,s),s})}function p(e,t){return l(e,()=>{let n=i.default.join(e,a),o=d(JSON.parse(r.default.readFileSync(n,`utf8`)),n);if(o.publicKey!==t)throw Error(`bundle signing key changed while the signer was active`);if(o.revision===2**53-1)throw Error(`bundle signing revision is exhausted`);let s={...o,revision:o.revision+1};return u(e,s),s.revision})}function m(e){return s[i.default.extname(e).toLowerCase()]??`application/octet-stream`}function h(e,t,a){let s=r.default.readdirSync(t,{withFileTypes:!0});for(let c of s){let s=i.default.join(t,c.name),l=r.default.lstatSync(s);if(l.isSymbolicLink())throw Error(`refusing to sign symbolic link: ${s}`);if(l.isDirectory()){h(e,s,a);continue}if(!l.isFile())throw Error(`refusing to omit unsupported asset: ${s}`);if(o.has(i.default.extname(c.name)))continue;let u=r.default.readFileSync(s);a.push({path:`/${i.default.relative(e,s).split(i.default.sep).map(e=>encodeURIComponent(e)).join(`/`)}`,sha256:(0,n.createHash)(`sha256`).update(u).digest(`hex`),byteLength:u.byteLength,contentType:m(s)})}}function g(e,a=()=>{}){let o=f(e),s=(0,n.createPrivateKey)(o.privateKey),c=new Map,l=i=>{let a=r.default.lstatSync(i);if(!a.isDirectory()||a.isSymbolicLink())throw Error(`refusing to sign an unsafe asset root: ${i}`);let c=[];if(h(i,i,c),c.length===0)return;c.sort((e,t)=>e.path.localeCompare(t.path));let l={version:2,revision:p(e,o.publicKey),builtAt:Date.now(),assets:c};if(!t.isBundleManifest(l))throw Error(`refusing to sign an invalid bundle manifest`);return{manifest:l,signature:(0,n.createSign)(`sha256`).update(t.canonicalManifest(l)).sign({key:s,dsaEncoding:`ieee-p1363`}).toString(`base64url`),publicKey:o.publicKey}},u=e=>{let t=i.default.resolve(e);if(c.has(t))return c.get(t);let n=l(t);return c.set(t,n),n};return{publicKey:()=>o.publicKey,sign:u,refresh(e){let t=i.default.resolve(e);return c.delete(t),u(t)},invalidate(e){e===void 0?c.clear():c.delete(i.default.resolve(e))}}}function _(e){return Buffer.from((0,n.createPublicKey)(e).export({type:`spki`,format:`der`})).toString(`base64url`)}exports.createBundleSigner=g,exports.publicKeyOf=_;
2
2
  //# sourceMappingURL=bundleSigner.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bundleSigner.cjs","names":["path","fs","generateKeyPairSync","createHash","createPrivateKey","createSign","canonicalManifest","createPublicKey"],"sources":["../../src/adapters/bundleSigner.ts"],"sourcesContent":["import { createHash, createPrivateKey, createPublicKey, createSign, generateKeyPairSync } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type SignedBundleManifest,\n canonicalManifest,\n} from '../types/bundleManifest.ts';\n\nconst SIGNING_FILE = 'signing.json';\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\n/** P-256 rather than Ed25519: universally available in WebCrypto, which Ed25519 is not yet. */\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\n/** Files a browser never fetches, so hashing them would only slow a rebuild. */\nconst SKIPPED = new Set(['.map']);\n\nexport interface BundleSigner {\n /** Base64url SPKI for an independently trusted verifier to pin. */\n publicKey(): string;\n /** Hashes the asset tree and signs the result. Undefined when there is no bundle to sign. */\n sign(assetsDir: string): SignedBundleManifest | undefined;\n}\n\ninterface StoredKey {\n privateKey: string;\n publicKey: string;\n}\n\n/**\n * Loads the hub's signing key, generating one on first run.\n *\n * Long-lived so an external verifier can pin it. Rotating the key invalidates\n * those pins, and losing the file has the same effect.\n */\nfunction loadKey(stateDir: string, onNotice: (message: string) => void): StoredKey {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(keyPath, 'utf8'));\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n typeof (parsed as StoredKey).privateKey === 'string' &&\n typeof (parsed as StoredKey).publicKey === 'string'\n ) {\n return parsed as StoredKey;\n }\n onNotice(\n `the bundle signing key at ${keyPath} is unreadable; generating a new one, so existing pins must be updated`,\n );\n } catch {\n // First run, which is the normal path and not worth a notice.\n }\n const pair = generateKeyPairSync('ec', {\n namedCurve: CURVE,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' },\n publicKeyEncoding: { type: 'spki', format: 'der' },\n });\n const stored: StoredKey = {\n privateKey: pair.privateKey,\n publicKey: Buffer.from(pair.publicKey).toString('base64url'),\n };\n try {\n fs.mkdirSync(stateDir, { recursive: true, mode: DIRECTORY_MODE });\n fs.writeFileSync(keyPath, `${JSON.stringify(stored, undefined, 2)}\\n`, { mode: FILE_MODE });\n } catch (error) {\n onNotice(`the bundle signing key could not be saved: ${error instanceof Error ? error.message : String(error)}`);\n }\n return stored;\n}\n\n/** Every servable file under the asset root, as browser paths. */\nfunction walk(root: string, current: string, into: BundleAsset[]): void {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(current, { withFileTypes: true });\n } catch {\n return; // A directory that vanished mid-walk contributes nothing.\n }\n for (const entry of entries) {\n const full = path.join(current, entry.name);\n if (entry.isDirectory()) {\n walk(root, full, into);\n continue;\n }\n if (!entry.isFile() || SKIPPED.has(path.extname(entry.name))) continue;\n try {\n const bytes = fs.readFileSync(full);\n into.push({\n path: `/${path.relative(root, full).split(path.sep).join('/')}`,\n sha256: createHash('sha256').update(bytes).digest('hex'),\n });\n } catch {\n // Unreadable file; leaving it out of the manifest means it will fail\n // verification, which is the right direction to be wrong in.\n }\n }\n}\n\nexport function createBundleSigner(stateDir: string, onNotice: (message: string) => void = () => {}): BundleSigner {\n const key = loadKey(stateDir, onNotice);\n const privateKey = createPrivateKey(key.privateKey);\n /** Cached per asset directory: hashing a bundle is not free and it never changes under us mid-run. */\n const cache = new Map<string, SignedBundleManifest | undefined>();\n\n return {\n publicKey: () => key.publicKey,\n\n sign(assetsDir) {\n const cached = cache.get(assetsDir);\n if (cached !== undefined || cache.has(assetsDir)) return cached;\n const assets: BundleAsset[] = [];\n walk(assetsDir, assetsDir, assets);\n if (assets.length === 0) {\n cache.set(assetsDir, undefined);\n return undefined;\n }\n const manifest: BundleManifest = { version: BUNDLE_MANIFEST_VERSION, builtAt: Date.now(), assets };\n const signature = createSign(SIGN_ALGORITHM)\n .update(canonicalManifest(manifest))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n const signed: SignedBundleManifest = { manifest, signature, publicKey: key.publicKey };\n cache.set(assetsDir, signed);\n return signed;\n },\n };\n}\n\n/** Re-derives the public key from a stored private key, for a test that wants to verify. */\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":"bundleSigner.cjs","names":["fs","path","randomBytes","generateKeyPairSync","createHash","createPrivateKey","isBundleManifest","createSign","canonicalManifest","createPublicKey"],"sources":["../../src/adapters/bundleSigner.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n randomBytes,\n} from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type SignedBundleManifest,\n canonicalManifest,\n isBundleManifest,\n} from '../types/bundleManifest.ts';\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 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 /** Hashes and signs once per asset directory. Undefined when the bundle is empty. */\n sign(assetsDir: string): SignedBundleManifest | undefined;\n /** Invalidates and immediately rebuilds one cached manifest. */\n refresh(assetsDir: string): SignedBundleManifest | undefined;\n /** Invalidates one cached directory, or every cached directory when omitted. */\n invalidate(assetsDir?: string): void;\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 let descriptor: number | undefined;\n for (let attempt = 0; attempt < 500; attempt += 1) {\n try {\n descriptor = fs.openSync(lockPath, 'wx', FILE_MODE);\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);\n }\n }\n if (descriptor === undefined) throw new Error(`timed out waiting for bundle signing state lock: ${lockPath}`);\n try {\n return operation();\n } finally {\n fs.closeSync(descriptor);\n fs.rmSync(lockPath, { force: true });\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\n const signFresh = (assetsDir: string): 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)) throw new Error('refusing to sign an invalid 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): SignedBundleManifest | undefined => {\n const cacheKey = path.resolve(assetsDir);\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n const signed = signFresh(cacheKey);\n cache.set(cacheKey, signed);\n return signed;\n };\n\n return {\n publicKey: () => key.publicKey,\n sign,\n refresh(assetsDir) {\n const cacheKey = path.resolve(assetsDir);\n cache.delete(cacheKey);\n return sign(cacheKey);\n },\n invalidate(assetsDir) {\n if (assetsDir === undefined) cache.clear();\n else cache.delete(path.resolve(assetsDir));\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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"}
@@ -3,11 +3,15 @@ import { SignedBundleManifest } from "../types/bundleManifest.cjs";
3
3
  interface BundleSigner {
4
4
  /** Base64url SPKI for an independently trusted verifier to pin. */
5
5
  publicKey(): string;
6
- /** Hashes the asset tree and signs the result. Undefined when there is no bundle to sign. */
6
+ /** Hashes and signs once per asset directory. Undefined when the bundle is empty. */
7
7
  sign(assetsDir: string): SignedBundleManifest | undefined;
8
+ /** Invalidates and immediately rebuilds one cached manifest. */
9
+ refresh(assetsDir: string): SignedBundleManifest | undefined;
10
+ /** Invalidates one cached directory, or every cached directory when omitted. */
11
+ invalidate(assetsDir?: string): void;
8
12
  }
9
- declare function createBundleSigner(stateDir: string, onNotice?: (message: string) => void): BundleSigner;
10
- /** Re-derives the public key from a stored private key, for a test that wants to verify. */
13
+ declare function createBundleSigner(stateDir: string, _onNotice?: (message: string) => void): BundleSigner;
14
+ /** Re-derives the public key from a stored private key. */
11
15
  declare function publicKeyOf(privateKeyPem: string): string;
12
16
  //#endregion
13
17
  export { BundleSigner, createBundleSigner, publicKeyOf };
@@ -1 +1 @@
1
- {"version":3,"file":"bundleSigner.d.cts","names":[],"sources":["../../src/adapters/bundleSigner.ts"],"mappings":";;UAoBiB;;EAEf;;EAEA,KAAK,oBAAoB;;iBA8EX,mBAAmB,kBAAkB,YAAW,2BAAsC;;iBA+BtF,YAAY"}
1
+ {"version":3,"file":"bundleSigner.d.cts","names":[],"sources":["../../src/adapters/bundleSigner.ts"],"mappings":";;UA6CiB;;EAEf;;EAEA,KAAK,oBAAoB;;EAEzB,QAAQ,oBAAoB;;EAE5B,WAAW;;iBAsJG,mBAAmB,kBAAkB,aAAY,2BAAsC;;iBAoDvF,YAAY"}
@@ -3,11 +3,15 @@ import { SignedBundleManifest } from "../types/bundleManifest.mjs";
3
3
  interface BundleSigner {
4
4
  /** Base64url SPKI for an independently trusted verifier to pin. */
5
5
  publicKey(): string;
6
- /** Hashes the asset tree and signs the result. Undefined when there is no bundle to sign. */
6
+ /** Hashes and signs once per asset directory. Undefined when the bundle is empty. */
7
7
  sign(assetsDir: string): SignedBundleManifest | undefined;
8
+ /** Invalidates and immediately rebuilds one cached manifest. */
9
+ refresh(assetsDir: string): SignedBundleManifest | undefined;
10
+ /** Invalidates one cached directory, or every cached directory when omitted. */
11
+ invalidate(assetsDir?: string): void;
8
12
  }
9
- declare function createBundleSigner(stateDir: string, onNotice?: (message: string) => void): BundleSigner;
10
- /** Re-derives the public key from a stored private key, for a test that wants to verify. */
13
+ declare function createBundleSigner(stateDir: string, _onNotice?: (message: string) => void): BundleSigner;
14
+ /** Re-derives the public key from a stored private key. */
11
15
  declare function publicKeyOf(privateKeyPem: string): string;
12
16
  //#endregion
13
17
  export { BundleSigner, createBundleSigner, publicKeyOf };
@@ -1 +1 @@
1
- {"version":3,"file":"bundleSigner.d.mts","names":[],"sources":["../../src/adapters/bundleSigner.ts"],"mappings":";;UAoBiB;;EAEf;;EAEA,KAAK,oBAAoB;;iBA8EX,mBAAmB,kBAAkB,YAAW,2BAAsC;;iBA+BtF,YAAY"}
1
+ {"version":3,"file":"bundleSigner.d.mts","names":[],"sources":["../../src/adapters/bundleSigner.ts"],"mappings":";;UA6CiB;;EAEf;;EAEA,KAAK,oBAAoB;;EAEzB,QAAQ,oBAAoB;;EAE5B,WAAW;;iBAsJG,mBAAmB,kBAAkB,aAAY,2BAAsC;;iBAoDvF,YAAY"}
@@ -1,2 +1,2 @@
1
- import{canonicalManifest as e}from"../types/bundleManifest.mjs";import{createHash as t,createPrivateKey as n,createPublicKey as r,createSign as i,generateKeyPairSync as a}from"node:crypto";import o from"node:fs";import s from"node:path";const c=new Set([`.map`]);function l(e,t){let n=s.join(e,`signing.json`);try{let e=JSON.parse(o.readFileSync(n,`utf8`));if(typeof e==`object`&&e&&typeof e.privateKey==`string`&&typeof e.publicKey==`string`)return e;t(`the bundle signing key at ${n} is unreadable; generating a new one, so existing pins must be updated`)}catch{}let r=a(`ec`,{namedCurve:`prime256v1`,privateKeyEncoding:{type:`pkcs8`,format:`pem`},publicKeyEncoding:{type:`spki`,format:`der`}}),i={privateKey:r.privateKey,publicKey:Buffer.from(r.publicKey).toString(`base64url`)};try{o.mkdirSync(e,{recursive:!0,mode:448}),o.writeFileSync(n,`${JSON.stringify(i,void 0,2)}\n`,{mode:384})}catch(e){t(`the bundle signing key could not be saved: ${e instanceof Error?e.message:String(e)}`)}return i}function u(e,n,r){let i;try{i=o.readdirSync(n,{withFileTypes:!0})}catch{return}for(let a of i){let i=s.join(n,a.name);if(a.isDirectory()){u(e,i,r);continue}if(!(!a.isFile()||c.has(s.extname(a.name))))try{let n=o.readFileSync(i);r.push({path:`/${s.relative(e,i).split(s.sep).join(`/`)}`,sha256:t(`sha256`).update(n).digest(`hex`)})}catch{}}}function d(t,r=()=>{}){let a=l(t,r),o=n(a.privateKey),s=new Map;return{publicKey:()=>a.publicKey,sign(t){let n=s.get(t);if(n!==void 0||s.has(t))return n;let r=[];if(u(t,t,r),r.length===0){s.set(t,void 0);return}let c={version:1,builtAt:Date.now(),assets:r},l={manifest:c,signature:i(`sha256`).update(e(c)).sign({key:o,dsaEncoding:`ieee-p1363`}).toString(`base64url`),publicKey:a.publicKey};return s.set(t,l),l}}}function f(e){return Buffer.from(r(e).export({type:`spki`,format:`der`})).toString(`base64url`)}export{d as createBundleSigner,f as publicKeyOf};
1
+ import{canonicalManifest as e,isBundleManifest as t}from"../types/bundleManifest.mjs";import{createHash as n,createPrivateKey as r,createPublicKey as i,createSign as a,generateKeyPairSync as o,randomBytes as s}from"node:crypto";import c from"node:fs";import l from"node:path";const u=`signing.json`,d=new Set([`.map`]),f={".css":`text/css`,".gif":`image/gif`,".html":`text/html`,".ico":`image/x-icon`,".jpeg":`image/jpeg`,".jpg":`image/jpeg`,".js":`text/javascript`,".json":`application/json`,".mjs":`text/javascript`,".png":`image/png`,".svg":`image/svg+xml`,".txt":`text/plain`,".wasm":`application/wasm`,".webp":`image/webp`,".woff":`font/woff`,".woff2":`font/woff2`};function p(e){c.mkdirSync(e,{recursive:!0,mode:448});let t=c.lstatSync(e);if(!t.isDirectory()||t.isSymbolicLink())throw Error(`bundle signing state is not a directory: ${e}`);c.chmodSync(e,448)}function m(e,t){p(e);let n=l.join(e,`signing.lock`),r;for(let e=0;e<500;e+=1)try{r=c.openSync(n,`wx`,384);break}catch(e){if(e.code!==`EEXIST`)throw e;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,10)}if(r===void 0)throw Error(`timed out waiting for bundle signing state lock: ${n}`);try{return t()}finally{c.closeSync(r),c.rmSync(n,{force:!0})}}function h(e,t){let n=l.join(e,u),r=l.join(e,`.${u}.${process.pid}.${s(8).toString(`hex`)}`),i=c.openSync(r,`wx`,384);try{c.writeFileSync(i,`${JSON.stringify(t,void 0,2)}\n`),c.fsyncSync(i)}finally{c.closeSync(i)}try{c.renameSync(r,n),c.chmodSync(n,384)}catch(e){throw c.rmSync(r,{force:!0}),e}}function g(e,t){if(typeof e!=`object`||!e)throw Error(`invalid bundle signing state: ${t}`);let n=e;if(typeof n.privateKey!=`string`||typeof n.publicKey!=`string`)throw Error(`invalid bundle signing state: ${t}`);let r=n.revision===void 0?0:n.revision;if(!Number.isSafeInteger(r)||r<0)throw Error(`invalid bundle signing revision: ${t}`);let i;try{i=S(n.privateKey)}catch{throw Error(`invalid bundle signing private key: ${t}`)}if(i!==n.publicKey)throw Error(`bundle signing keys do not match: ${t}`);return{privateKey:n.privateKey,publicKey:n.publicKey,revision:r}}function _(e){return m(e,()=>{let t=l.join(e,u);try{let n=c.lstatSync(t);if(!n.isFile()||n.isSymbolicLink())throw Error(`invalid bundle signing state file: ${t}`);let r=JSON.parse(c.readFileSync(t,`utf8`)),i=g(r,t);return c.chmodSync(t,384),r.revision===void 0&&h(e,i),i}catch(e){if(e.code!==`ENOENT`)throw e}let n=o(`ec`,{namedCurve:`prime256v1`,privateKeyEncoding:{type:`pkcs8`,format:`pem`},publicKeyEncoding:{type:`spki`,format:`der`}}),r={privateKey:n.privateKey,publicKey:Buffer.from(n.publicKey).toString(`base64url`),revision:0};return h(e,r),r})}function v(e,t){return m(e,()=>{let n=l.join(e,u),r=g(JSON.parse(c.readFileSync(n,`utf8`)),n);if(r.publicKey!==t)throw Error(`bundle signing key changed while the signer was active`);if(r.revision===2**53-1)throw Error(`bundle signing revision is exhausted`);let i={...r,revision:r.revision+1};return h(e,i),i.revision})}function y(e){return f[l.extname(e).toLowerCase()]??`application/octet-stream`}function b(e,t,r){let i=c.readdirSync(t,{withFileTypes:!0});for(let a of i){let i=l.join(t,a.name),o=c.lstatSync(i);if(o.isSymbolicLink())throw Error(`refusing to sign symbolic link: ${i}`);if(o.isDirectory()){b(e,i,r);continue}if(!o.isFile())throw Error(`refusing to omit unsupported asset: ${i}`);if(d.has(l.extname(a.name)))continue;let s=c.readFileSync(i);r.push({path:`/${l.relative(e,i).split(l.sep).map(e=>encodeURIComponent(e)).join(`/`)}`,sha256:n(`sha256`).update(s).digest(`hex`),byteLength:s.byteLength,contentType:y(i)})}}function x(n,i=()=>{}){let o=_(n),s=r(o.privateKey),u=new Map,d=r=>{let i=c.lstatSync(r);if(!i.isDirectory()||i.isSymbolicLink())throw Error(`refusing to sign an unsafe asset root: ${r}`);let l=[];if(b(r,r,l),l.length===0)return;l.sort((e,t)=>e.path.localeCompare(t.path));let u={version:2,revision:v(n,o.publicKey),builtAt:Date.now(),assets:l};if(!t(u))throw Error(`refusing to sign an invalid bundle manifest`);return{manifest:u,signature:a(`sha256`).update(e(u)).sign({key:s,dsaEncoding:`ieee-p1363`}).toString(`base64url`),publicKey:o.publicKey}},f=e=>{let t=l.resolve(e);if(u.has(t))return u.get(t);let n=d(t);return u.set(t,n),n};return{publicKey:()=>o.publicKey,sign:f,refresh(e){let t=l.resolve(e);return u.delete(t),f(t)},invalidate(e){e===void 0?u.clear():u.delete(l.resolve(e))}}}function S(e){return Buffer.from(i(e).export({type:`spki`,format:`der`})).toString(`base64url`)}export{x as createBundleSigner,S as publicKeyOf};
2
2
  //# sourceMappingURL=bundleSigner.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"bundleSigner.mjs","names":[],"sources":["../../src/adapters/bundleSigner.ts"],"sourcesContent":["import { createHash, createPrivateKey, createPublicKey, createSign, generateKeyPairSync } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type SignedBundleManifest,\n canonicalManifest,\n} from '../types/bundleManifest.ts';\n\nconst SIGNING_FILE = 'signing.json';\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\n/** P-256 rather than Ed25519: universally available in WebCrypto, which Ed25519 is not yet. */\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\n/** Files a browser never fetches, so hashing them would only slow a rebuild. */\nconst SKIPPED = new Set(['.map']);\n\nexport interface BundleSigner {\n /** Base64url SPKI for an independently trusted verifier to pin. */\n publicKey(): string;\n /** Hashes the asset tree and signs the result. Undefined when there is no bundle to sign. */\n sign(assetsDir: string): SignedBundleManifest | undefined;\n}\n\ninterface StoredKey {\n privateKey: string;\n publicKey: string;\n}\n\n/**\n * Loads the hub's signing key, generating one on first run.\n *\n * Long-lived so an external verifier can pin it. Rotating the key invalidates\n * those pins, and losing the file has the same effect.\n */\nfunction loadKey(stateDir: string, onNotice: (message: string) => void): StoredKey {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(keyPath, 'utf8'));\n if (\n typeof parsed === 'object' &&\n parsed !== null &&\n typeof (parsed as StoredKey).privateKey === 'string' &&\n typeof (parsed as StoredKey).publicKey === 'string'\n ) {\n return parsed as StoredKey;\n }\n onNotice(\n `the bundle signing key at ${keyPath} is unreadable; generating a new one, so existing pins must be updated`,\n );\n } catch {\n // First run, which is the normal path and not worth a notice.\n }\n const pair = generateKeyPairSync('ec', {\n namedCurve: CURVE,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' },\n publicKeyEncoding: { type: 'spki', format: 'der' },\n });\n const stored: StoredKey = {\n privateKey: pair.privateKey,\n publicKey: Buffer.from(pair.publicKey).toString('base64url'),\n };\n try {\n fs.mkdirSync(stateDir, { recursive: true, mode: DIRECTORY_MODE });\n fs.writeFileSync(keyPath, `${JSON.stringify(stored, undefined, 2)}\\n`, { mode: FILE_MODE });\n } catch (error) {\n onNotice(`the bundle signing key could not be saved: ${error instanceof Error ? error.message : String(error)}`);\n }\n return stored;\n}\n\n/** Every servable file under the asset root, as browser paths. */\nfunction walk(root: string, current: string, into: BundleAsset[]): void {\n let entries: fs.Dirent[];\n try {\n entries = fs.readdirSync(current, { withFileTypes: true });\n } catch {\n return; // A directory that vanished mid-walk contributes nothing.\n }\n for (const entry of entries) {\n const full = path.join(current, entry.name);\n if (entry.isDirectory()) {\n walk(root, full, into);\n continue;\n }\n if (!entry.isFile() || SKIPPED.has(path.extname(entry.name))) continue;\n try {\n const bytes = fs.readFileSync(full);\n into.push({\n path: `/${path.relative(root, full).split(path.sep).join('/')}`,\n sha256: createHash('sha256').update(bytes).digest('hex'),\n });\n } catch {\n // Unreadable file; leaving it out of the manifest means it will fail\n // verification, which is the right direction to be wrong in.\n }\n }\n}\n\nexport function createBundleSigner(stateDir: string, onNotice: (message: string) => void = () => {}): BundleSigner {\n const key = loadKey(stateDir, onNotice);\n const privateKey = createPrivateKey(key.privateKey);\n /** Cached per asset directory: hashing a bundle is not free and it never changes under us mid-run. */\n const cache = new Map<string, SignedBundleManifest | undefined>();\n\n return {\n publicKey: () => key.publicKey,\n\n sign(assetsDir) {\n const cached = cache.get(assetsDir);\n if (cached !== undefined || cache.has(assetsDir)) return cached;\n const assets: BundleAsset[] = [];\n walk(assetsDir, assetsDir, assets);\n if (assets.length === 0) {\n cache.set(assetsDir, undefined);\n return undefined;\n }\n const manifest: BundleManifest = { version: BUNDLE_MANIFEST_VERSION, builtAt: Date.now(), assets };\n const signature = createSign(SIGN_ALGORITHM)\n .update(canonicalManifest(manifest))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n const signed: SignedBundleManifest = { manifest, signature, publicKey: key.publicKey };\n cache.set(assetsDir, signed);\n return signed;\n },\n };\n}\n\n/** Re-derives the public key from a stored private key, for a test that wants to verify. */\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":"bundleSigner.mjs","names":[],"sources":["../../src/adapters/bundleSigner.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n randomBytes,\n} from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type SignedBundleManifest,\n canonicalManifest,\n isBundleManifest,\n} from '../types/bundleManifest.ts';\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 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 /** Hashes and signs once per asset directory. Undefined when the bundle is empty. */\n sign(assetsDir: string): SignedBundleManifest | undefined;\n /** Invalidates and immediately rebuilds one cached manifest. */\n refresh(assetsDir: string): SignedBundleManifest | undefined;\n /** Invalidates one cached directory, or every cached directory when omitted. */\n invalidate(assetsDir?: string): void;\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 let descriptor: number | undefined;\n for (let attempt = 0; attempt < 500; attempt += 1) {\n try {\n descriptor = fs.openSync(lockPath, 'wx', FILE_MODE);\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);\n }\n }\n if (descriptor === undefined) throw new Error(`timed out waiting for bundle signing state lock: ${lockPath}`);\n try {\n return operation();\n } finally {\n fs.closeSync(descriptor);\n fs.rmSync(lockPath, { force: true });\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\n const signFresh = (assetsDir: string): 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)) throw new Error('refusing to sign an invalid 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): SignedBundleManifest | undefined => {\n const cacheKey = path.resolve(assetsDir);\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n const signed = signFresh(cacheKey);\n cache.set(cacheKey, signed);\n return signed;\n };\n\n return {\n publicKey: () => key.publicKey,\n sign,\n refresh(assetsDir) {\n const cacheKey = path.resolve(assetsDir);\n cache.delete(cacheKey);\n return sign(cacheKey);\n },\n invalidate(assetsDir) {\n if (assetsDir === undefined) cache.clear();\n else cache.delete(path.resolve(assetsDir));\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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"}
package/dist/browser.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./adapters/browserSealedChannel.cjs"),t=require("./adapters/sealedTransport.cjs");exports.channelFromSecret=e.channelFromSecret,exports.connectSealedChannel=e.connectSealedChannel,exports.createSealedTransport=t.createSealedTransport,exports.sealedTransport=t.sealedTransport;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types/bundleManifest.cjs"),t=require("./adapters/browserBundleVerifier.cjs"),n=require("./adapters/browserSealedChannel.cjs"),r=require("./adapters/sealedTransport.cjs");exports.BUNDLE_MANIFEST_ROUTE=e.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=e.BUNDLE_MANIFEST_VERSION,exports.assetFor=e.assetFor,exports.canonicalManifest=e.canonicalManifest,exports.channelFromSecret=n.channelFromSecret,exports.connectSealedChannel=n.connectSealedChannel,exports.createSealedTransport=r.createSealedTransport,exports.digestFor=e.digestFor,exports.isBundleManifest=e.isBundleManifest,exports.isSignedBundleManifest=e.isSignedBundleManifest,exports.sealedTransport=r.sealedTransport,exports.verifyBundleAsset=t.verifyBundleAsset,exports.verifySignedBundleManifest=t.verifySignedBundleManifest;
@@ -1,3 +1,5 @@
1
+ import { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, assetFor, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest } from "./types/bundleManifest.cjs";
2
+ import { BundleAssetVerificationFailure, BundleAssetVerificationResult, BundleVerificationFailure, BundleVerificationResult, verifyBundleAsset, verifySignedBundleManifest } from "./adapters/browserBundleVerifier.cjs";
1
3
  import { OpenResult, SealResult, SealedChannel, channelFromSecret, connectSealedChannel } from "./adapters/browserSealedChannel.cjs";
2
4
  import { SealedTransport, createSealedTransport, sealedTransport } from "./adapters/sealedTransport.cjs";
3
- export { type OpenResult, type SealResult, type SealedChannel, type SealedTransport, channelFromSecret, connectSealedChannel, createSealedTransport, sealedTransport };
5
+ export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleAssetVerificationFailure, type BundleAssetVerificationResult, type BundleManifest, type BundleVerificationFailure, type BundleVerificationResult, type OpenResult, type SealResult, type SealedChannel, type SealedTransport, type SignedBundleManifest, assetFor, canonicalManifest, channelFromSecret, connectSealedChannel, createSealedTransport, digestFor, isBundleManifest, isSignedBundleManifest, sealedTransport, verifyBundleAsset, verifySignedBundleManifest };
@@ -1,3 +1,5 @@
1
+ import { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, assetFor, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest } from "./types/bundleManifest.mjs";
2
+ import { BundleAssetVerificationFailure, BundleAssetVerificationResult, BundleVerificationFailure, BundleVerificationResult, verifyBundleAsset, verifySignedBundleManifest } from "./adapters/browserBundleVerifier.mjs";
1
3
  import { OpenResult, SealResult, SealedChannel, channelFromSecret, connectSealedChannel } from "./adapters/browserSealedChannel.mjs";
2
4
  import { SealedTransport, createSealedTransport, sealedTransport } from "./adapters/sealedTransport.mjs";
3
- export { type OpenResult, type SealResult, type SealedChannel, type SealedTransport, channelFromSecret, connectSealedChannel, createSealedTransport, sealedTransport };
5
+ export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleAssetVerificationFailure, type BundleAssetVerificationResult, type BundleManifest, type BundleVerificationFailure, type BundleVerificationResult, type OpenResult, type SealResult, type SealedChannel, type SealedTransport, type SignedBundleManifest, assetFor, canonicalManifest, channelFromSecret, connectSealedChannel, createSealedTransport, digestFor, isBundleManifest, isSignedBundleManifest, sealedTransport, verifyBundleAsset, verifySignedBundleManifest };
package/dist/browser.mjs CHANGED
@@ -1 +1 @@
1
- import{channelFromSecret as e,connectSealedChannel as t}from"./adapters/browserSealedChannel.mjs";import{createSealedTransport as n,sealedTransport as r}from"./adapters/sealedTransport.mjs";export{e as channelFromSecret,t as connectSealedChannel,n as createSealedTransport,r as sealedTransport};
1
+ import{BUNDLE_MANIFEST_ROUTE as e,BUNDLE_MANIFEST_VERSION as t,assetFor as n,canonicalManifest as r,digestFor as i,isBundleManifest as a,isSignedBundleManifest as o}from"./types/bundleManifest.mjs";import{verifyBundleAsset as s,verifySignedBundleManifest as c}from"./adapters/browserBundleVerifier.mjs";import{channelFromSecret as l,connectSealedChannel as u}from"./adapters/browserSealedChannel.mjs";import{createSealedTransport as d,sealedTransport as f}from"./adapters/sealedTransport.mjs";export{e as BUNDLE_MANIFEST_ROUTE,t as BUNDLE_MANIFEST_VERSION,n as assetFor,r as canonicalManifest,l as channelFromSecret,u as connectSealedChannel,d as createSealedTransport,i as digestFor,a as isBundleManifest,o as isSignedBundleManifest,f as sealedTransport,s as verifyBundleAsset,c as verifySignedBundleManifest};
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types/sealedChannel.cjs"),t=require("./services/serialQueue.cjs"),n=require("./types/bundleManifest.cjs");exports.BUNDLE_MANIFEST_ROUTE=n.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=n.BUNDLE_MANIFEST_VERSION,exports.CLIENT_TO_SERVER_INFO=e.CLIENT_TO_SERVER_INFO,exports.MAX_MESSAGES_PER_KEY=e.MAX_MESSAGES_PER_KEY,exports.NONCE_BYTES=e.NONCE_BYTES,exports.NONCE_COUNTER_BYTES=e.NONCE_COUNTER_BYTES,exports.NONCE_PREFIX_BYTES=e.NONCE_PREFIX_BYTES,exports.SEALED_BODY_HEADER=e.SEALED_BODY_HEADER,exports.SEALED_CLIENT_KEY_HEADER=e.SEALED_CLIENT_KEY_HEADER,exports.SEALED_KEY_PARAM=e.SEALED_KEY_PARAM,exports.SEALED_VERSION=e.SEALED_VERSION,exports.SERVER_TO_CLIENT_INFO=e.SERVER_TO_CLIENT_INFO,exports.canonicalManifest=n.canonicalManifest,exports.createSerialQueue=t.createSerialQueue,exports.describeSealedFailure=e.describeSealedFailure,exports.digestFor=n.digestFor,exports.infoFor=e.infoFor,exports.isBundleManifest=n.isBundleManifest,exports.isSealedEnvelope=e.isSealedEnvelope;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types/bundleManifest.cjs"),t=require("./types/sealedChannel.cjs"),n=require("./services/serialQueue.cjs");exports.BUNDLE_MANIFEST_ROUTE=e.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=e.BUNDLE_MANIFEST_VERSION,exports.CLIENT_TO_SERVER_INFO=t.CLIENT_TO_SERVER_INFO,exports.MAX_MESSAGES_PER_KEY=t.MAX_MESSAGES_PER_KEY,exports.NONCE_BYTES=t.NONCE_BYTES,exports.NONCE_COUNTER_BYTES=t.NONCE_COUNTER_BYTES,exports.NONCE_PREFIX_BYTES=t.NONCE_PREFIX_BYTES,exports.SEALED_BODY_HEADER=t.SEALED_BODY_HEADER,exports.SEALED_CLIENT_KEY_HEADER=t.SEALED_CLIENT_KEY_HEADER,exports.SEALED_KEY_PARAM=t.SEALED_KEY_PARAM,exports.SEALED_VERSION=t.SEALED_VERSION,exports.SERVER_TO_CLIENT_INFO=t.SERVER_TO_CLIENT_INFO,exports.assetFor=e.assetFor,exports.canonicalManifest=e.canonicalManifest,exports.createSerialQueue=n.createSerialQueue,exports.describeSealedFailure=t.describeSealedFailure,exports.digestFor=e.digestFor,exports.infoFor=t.infoFor,exports.isBundleManifest=e.isBundleManifest,exports.isSealedEnvelope=t.isSealedEnvelope,exports.isSignedBundleManifest=e.isSignedBundleManifest;
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
+ import { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, assetFor, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest } from "./types/bundleManifest.cjs";
1
2
  import { CLIENT_TO_SERVER_INFO, MAX_MESSAGES_PER_KEY, NONCE_BYTES, NONCE_COUNTER_BYTES, NONCE_PREFIX_BYTES, SEALED_BODY_HEADER, SEALED_CLIENT_KEY_HEADER, SEALED_KEY_PARAM, SEALED_VERSION, SERVER_TO_CLIENT_INFO, SealedDirection, SealedEnvelope, SealedFailure, describeSealedFailure, infoFor, isSealedEnvelope } from "./types/sealedChannel.cjs";
2
- import { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, canonicalManifest, digestFor, isBundleManifest } from "./types/bundleManifest.cjs";
3
3
  import { SerialQueue, createSerialQueue } from "./services/serialQueue.cjs";
4
- export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleManifest, CLIENT_TO_SERVER_INFO, MAX_MESSAGES_PER_KEY, NONCE_BYTES, NONCE_COUNTER_BYTES, NONCE_PREFIX_BYTES, SEALED_BODY_HEADER, SEALED_CLIENT_KEY_HEADER, SEALED_KEY_PARAM, SEALED_VERSION, SERVER_TO_CLIENT_INFO, type SealedDirection, type SealedEnvelope, type SealedFailure, type SerialQueue, type SignedBundleManifest, canonicalManifest, createSerialQueue, describeSealedFailure, digestFor, infoFor, isBundleManifest, isSealedEnvelope };
4
+ export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleManifest, CLIENT_TO_SERVER_INFO, MAX_MESSAGES_PER_KEY, NONCE_BYTES, NONCE_COUNTER_BYTES, NONCE_PREFIX_BYTES, SEALED_BODY_HEADER, SEALED_CLIENT_KEY_HEADER, SEALED_KEY_PARAM, SEALED_VERSION, SERVER_TO_CLIENT_INFO, type SealedDirection, type SealedEnvelope, type SealedFailure, type SerialQueue, type SignedBundleManifest, assetFor, canonicalManifest, createSerialQueue, describeSealedFailure, digestFor, infoFor, isBundleManifest, isSealedEnvelope, isSignedBundleManifest };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
+ import { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, assetFor, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest } from "./types/bundleManifest.mjs";
1
2
  import { CLIENT_TO_SERVER_INFO, MAX_MESSAGES_PER_KEY, NONCE_BYTES, NONCE_COUNTER_BYTES, NONCE_PREFIX_BYTES, SEALED_BODY_HEADER, SEALED_CLIENT_KEY_HEADER, SEALED_KEY_PARAM, SEALED_VERSION, SERVER_TO_CLIENT_INFO, SealedDirection, SealedEnvelope, SealedFailure, describeSealedFailure, infoFor, isSealedEnvelope } from "./types/sealedChannel.mjs";
2
- import { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, canonicalManifest, digestFor, isBundleManifest } from "./types/bundleManifest.mjs";
3
3
  import { SerialQueue, createSerialQueue } from "./services/serialQueue.mjs";
4
- export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleManifest, CLIENT_TO_SERVER_INFO, MAX_MESSAGES_PER_KEY, NONCE_BYTES, NONCE_COUNTER_BYTES, NONCE_PREFIX_BYTES, SEALED_BODY_HEADER, SEALED_CLIENT_KEY_HEADER, SEALED_KEY_PARAM, SEALED_VERSION, SERVER_TO_CLIENT_INFO, type SealedDirection, type SealedEnvelope, type SealedFailure, type SerialQueue, type SignedBundleManifest, canonicalManifest, createSerialQueue, describeSealedFailure, digestFor, infoFor, isBundleManifest, isSealedEnvelope };
4
+ export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleManifest, CLIENT_TO_SERVER_INFO, MAX_MESSAGES_PER_KEY, NONCE_BYTES, NONCE_COUNTER_BYTES, NONCE_PREFIX_BYTES, SEALED_BODY_HEADER, SEALED_CLIENT_KEY_HEADER, SEALED_KEY_PARAM, SEALED_VERSION, SERVER_TO_CLIENT_INFO, type SealedDirection, type SealedEnvelope, type SealedFailure, type SerialQueue, type SignedBundleManifest, assetFor, canonicalManifest, createSerialQueue, describeSealedFailure, digestFor, infoFor, isBundleManifest, isSealedEnvelope, isSignedBundleManifest };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{CLIENT_TO_SERVER_INFO as e,MAX_MESSAGES_PER_KEY as t,NONCE_BYTES as n,NONCE_COUNTER_BYTES as r,NONCE_PREFIX_BYTES as i,SEALED_BODY_HEADER as a,SEALED_CLIENT_KEY_HEADER as o,SEALED_KEY_PARAM as s,SEALED_VERSION as c,SERVER_TO_CLIENT_INFO as l,describeSealedFailure as u,infoFor as d,isSealedEnvelope as f}from"./types/sealedChannel.mjs";import{createSerialQueue as p}from"./services/serialQueue.mjs";import{BUNDLE_MANIFEST_ROUTE as m,BUNDLE_MANIFEST_VERSION as h,canonicalManifest as g,digestFor as _,isBundleManifest as v}from"./types/bundleManifest.mjs";export{m as BUNDLE_MANIFEST_ROUTE,h as BUNDLE_MANIFEST_VERSION,e as CLIENT_TO_SERVER_INFO,t as MAX_MESSAGES_PER_KEY,n as NONCE_BYTES,r as NONCE_COUNTER_BYTES,i as NONCE_PREFIX_BYTES,a as SEALED_BODY_HEADER,o as SEALED_CLIENT_KEY_HEADER,s as SEALED_KEY_PARAM,c as SEALED_VERSION,l as SERVER_TO_CLIENT_INFO,g as canonicalManifest,p as createSerialQueue,u as describeSealedFailure,_ as digestFor,d as infoFor,v as isBundleManifest,f as isSealedEnvelope};
1
+ import{BUNDLE_MANIFEST_ROUTE as e,BUNDLE_MANIFEST_VERSION as t,assetFor as n,canonicalManifest as r,digestFor as i,isBundleManifest as a,isSignedBundleManifest as o}from"./types/bundleManifest.mjs";import{CLIENT_TO_SERVER_INFO as s,MAX_MESSAGES_PER_KEY as c,NONCE_BYTES as l,NONCE_COUNTER_BYTES as u,NONCE_PREFIX_BYTES as d,SEALED_BODY_HEADER as f,SEALED_CLIENT_KEY_HEADER as p,SEALED_KEY_PARAM as m,SEALED_VERSION as h,SERVER_TO_CLIENT_INFO as g,describeSealedFailure as _,infoFor as v,isSealedEnvelope as y}from"./types/sealedChannel.mjs";import{createSerialQueue as b}from"./services/serialQueue.mjs";export{e as BUNDLE_MANIFEST_ROUTE,t as BUNDLE_MANIFEST_VERSION,s as CLIENT_TO_SERVER_INFO,c as MAX_MESSAGES_PER_KEY,l as NONCE_BYTES,u as NONCE_COUNTER_BYTES,d as NONCE_PREFIX_BYTES,f as SEALED_BODY_HEADER,p as SEALED_CLIENT_KEY_HEADER,m as SEALED_KEY_PARAM,h as SEALED_VERSION,g as SERVER_TO_CLIENT_INFO,n as assetFor,r as canonicalManifest,b as createSerialQueue,_ as describeSealedFailure,i as digestFor,v as infoFor,a as isBundleManifest,y as isSealedEnvelope,o as isSignedBundleManifest};
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"bundleManifest.cjs","names":[],"sources":["../../src/types/bundleManifest.ts"],"sourcesContent":["/**\n * A signed description of cockpit assets for an independently trusted verifier.\n *\n * A manifest does not let a browser SPA defend itself against the server or TLS\n * edge that supplied its executable code. Such an attacker can omit the\n * verifier or substitute its trust key. Consumers need a separately trusted\n * bootstrap and key before these signatures become a delivery guarantee.\n *\n * Lives in types rather than services because any verifier must use the same\n * canonical encoding the server signs, byte for byte.\n */\n\nexport const BUNDLE_MANIFEST_ROUTE = '/bundle-manifest.json';\nexport const BUNDLE_MANIFEST_VERSION = 1;\n\nexport interface BundleAsset {\n /** Path as the browser requests it, leading slash included. */\n path: string;\n /** Lowercase hex SHA-256 of the bytes served at that path. */\n sha256: string;\n}\n\nexport interface BundleManifest {\n version: number;\n /** Milliseconds, so a stale manifest is visible without another lookup. */\n builtAt: number;\n assets: BundleAsset[];\n}\n\nexport interface SignedBundleManifest {\n manifest: BundleManifest;\n /** Base64url ECDSA P-256 signature over the canonical serialization. */\n signature: string;\n /** Base64url SPKI of the key that signed it; trust must be established out of band. */\n publicKey: string;\n}\n\n/**\n * The exact bytes that get signed.\n *\n * Hand-rolled rather than `JSON.stringify` of the whole object, because a\n * signature is only worth anything if both sides agree byte for byte on what\n * was signed. Key order in a JSON object is an implementation detail; a sorted\n * line-per-asset encoding is not.\n */\nexport function canonicalManifest(manifest: BundleManifest): string {\n const assets = [...manifest.assets].sort((left, right) =>\n left.path < right.path ? -1 : left.path > right.path ? 1 : 0,\n );\n const lines = assets.map((asset) => `${asset.path}\u0000${asset.sha256}`);\n return [String(manifest.version), String(manifest.builtAt), ...lines].join('\\n');\n}\n\nexport function isBundleManifest(value: unknown): value is BundleManifest {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<BundleManifest>;\n if (candidate.version !== BUNDLE_MANIFEST_VERSION) return false;\n if (typeof candidate.builtAt !== 'number' || !Array.isArray(candidate.assets)) return false;\n return candidate.assets.every(\n (asset: unknown) =>\n typeof asset === 'object' &&\n asset !== null &&\n typeof (asset as BundleAsset).path === 'string' &&\n /^[0-9a-f]{64}$/u.test((asset as BundleAsset).sha256),\n );\n}\n\n/** The recorded digest for one asset, or undefined if the manifest does not cover it. */\nexport function digestFor(manifest: BundleManifest, path: string): string | undefined {\n return manifest.assets.find((asset) => asset.path === path)?.sha256;\n}\n"],"mappings":"AA6CA,SAAgB,EAAkB,EAAkC,CAIlE,IAAM,EAHS,CAAC,GAAG,EAAS,MAAM,CAAC,CAAC,MAAM,EAAM,IAC9C,EAAK,KAAO,EAAM,KAAO,GAAK,IAAK,KAAO,EAAM,KAE/B,CAAC,CAAC,IAAK,GAAU,GAAG,EAAM,KAAK,GAAG,EAAM,QAAQ,EACnE,MAAO,CAAC,OAAO,EAAS,OAAO,EAAG,OAAO,EAAS,OAAO,EAAG,GAAG,CAAK,CAAC,CAAC,KAAK;CAAI,CACjF,CAEA,SAAgB,EAAiB,EAAyC,CACxE,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAY,EAGlB,OAFI,EAAU,UAAA,GACV,OAAO,EAAU,SAAY,UAAY,CAAC,MAAM,QAAQ,EAAU,MAAM,EAAU,GAC/E,EAAU,OAAO,MACrB,GACC,OAAO,GAAU,YACjB,GACA,OAAQ,EAAsB,MAAS,UACvC,kBAAkB,KAAM,EAAsB,MAAM,CACxD,CACF,CAGA,SAAgB,EAAU,EAA0B,EAAkC,CACpF,OAAO,EAAS,OAAO,KAAM,GAAU,EAAM,OAAS,CAAI,CAAC,EAAE,MAC/D"}
1
+ {"version":3,"file":"bundleManifest.cjs","names":[],"sources":["../../src/types/bundleManifest.ts"],"sourcesContent":["/**\n * A signed description of cockpit assets for an independently trusted verifier.\n *\n * A manifest does not let a browser SPA defend itself against the server or TLS\n * edge that supplied its executable code. Consumers need a separately trusted\n * bootstrap and key before these signatures become a delivery guarantee.\n */\n\nexport const BUNDLE_MANIFEST_ROUTE = '/bundle-manifest.json';\nexport const BUNDLE_MANIFEST_VERSION = 2;\n\nconst SHA256 = /^[0-9a-f]{64}$/u;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\nconst CONTENT_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\\/[a-z0-9][a-z0-9!#$&^_.+-]*$/u;\n\nexport interface BundleAsset {\n /** Normalized absolute URL path, without a query or fragment. */\n path: string;\n /** Lowercase hex SHA-256 of the served bytes. */\n sha256: string;\n byteLength: number;\n /** MIME type of the response, without parameters. */\n contentType: string;\n}\n\nexport interface BundleManifest {\n version: typeof BUNDLE_MANIFEST_VERSION;\n /** Monotonically increasing persistent signer revision. */\n revision: number;\n /** Unix time in milliseconds. */\n builtAt: number;\n assets: BundleAsset[];\n}\n\nexport interface SignedBundleManifest {\n manifest: BundleManifest;\n /** Base64url IEEE P1363 ECDSA P-256 signature over the canonical serialization. */\n signature: string;\n /** Base64url SPKI of the signing key. Trust must be established out of band. */\n publicKey: string;\n}\n\nfunction hasExactKeys(value: object, expected: readonly string[]): boolean {\n const keys = Object.keys(value).sort();\n return keys.length === expected.length && keys.every((key, index) => key === expected[index]);\n}\n\nfunction isSafeInteger(value: unknown, minimum: number): value is number {\n return typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum;\n}\n\nfunction isSafeAssetPath(value: unknown): value is string {\n if (typeof value !== 'string' || value.length < 2 || !value.startsWith('/')) return false;\n if (value.includes('\\\\') || value.includes('\\0') || value.includes('?') || value.includes('#')) return false;\n if (/[^\\x21-\\x7e]/u.test(value) || value.includes('//') || /%(?![0-9a-f]{2})/iu.test(value)) return false;\n if (/%(?:2e|2f|5c)/iu.test(value)) return false;\n return value\n .split('/')\n .slice(1)\n .every((segment) => segment !== '' && segment !== '.' && segment !== '..');\n}\n\n/** The exact bytes signed by Node and verified by WebCrypto. */\nexport function canonicalManifest(manifest: BundleManifest): string {\n const assets = [...manifest.assets].sort((left, right) =>\n left.path < right.path ? -1 : left.path > right.path ? 1 : 0,\n );\n const lines = assets.map(\n (asset) => `${asset.path}\\0${asset.sha256}\\0${String(asset.byteLength)}\\0${asset.contentType}`,\n );\n return [String(manifest.version), String(manifest.revision), String(manifest.builtAt), ...lines].join('\\n');\n}\n\nexport function isBundleManifest(value: unknown): value is BundleManifest {\n if (typeof value !== 'object' || value === null) return false;\n if (!hasExactKeys(value, ['assets', 'builtAt', 'revision', 'version'])) return false;\n const candidate = value as Partial<BundleManifest>;\n if (candidate.version !== BUNDLE_MANIFEST_VERSION) return false;\n if (!isSafeInteger(candidate.revision, 1) || !isSafeInteger(candidate.builtAt, 0)) return false;\n if (!Array.isArray(candidate.assets) || candidate.assets.length === 0) return false;\n\n const paths = new Set<string>();\n for (const valueAsset of candidate.assets) {\n if (typeof valueAsset !== 'object' || valueAsset === null) return false;\n if (!hasExactKeys(valueAsset, ['byteLength', 'contentType', 'path', 'sha256'])) return false;\n const asset = valueAsset as Partial<BundleAsset>;\n if (!isSafeAssetPath(asset.path) || paths.has(asset.path)) return false;\n if (typeof asset.sha256 !== 'string' || !SHA256.test(asset.sha256)) return false;\n if (!isSafeInteger(asset.byteLength, 0)) return false;\n if (typeof asset.contentType !== 'string' || !CONTENT_TYPE.test(asset.contentType)) return false;\n paths.add(asset.path);\n }\n return paths.has('/index.html');\n}\n\nexport function isSignedBundleManifest(value: unknown): value is SignedBundleManifest {\n if (typeof value !== 'object' || value === null) return false;\n if (!hasExactKeys(value, ['manifest', 'publicKey', 'signature'])) return false;\n const candidate = value as Partial<SignedBundleManifest>;\n return (\n isBundleManifest(candidate.manifest) &&\n typeof candidate.signature === 'string' &&\n candidate.signature.length === 86 &&\n BASE64URL.test(candidate.signature) &&\n typeof candidate.publicKey === 'string' &&\n candidate.publicKey.length <= 512 &&\n BASE64URL.test(candidate.publicKey)\n );\n}\n\n/** The recorded asset for one path, or undefined if the manifest does not cover it. */\nexport function assetFor(manifest: BundleManifest, path: string): BundleAsset | undefined {\n return manifest.assets.find((asset) => asset.path === path);\n}\n\n/** The recorded digest for one asset, or undefined if the manifest does not cover it. */\nexport function digestFor(manifest: BundleManifest, path: string): string | undefined {\n return assetFor(manifest, path)?.sha256;\n}\n"],"mappings":"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"}
@@ -3,46 +3,43 @@
3
3
  * A signed description of cockpit assets for an independently trusted verifier.
4
4
  *
5
5
  * A manifest does not let a browser SPA defend itself against the server or TLS
6
- * edge that supplied its executable code. Such an attacker can omit the
7
- * verifier or substitute its trust key. Consumers need a separately trusted
6
+ * edge that supplied its executable code. Consumers need a separately trusted
8
7
  * bootstrap and key before these signatures become a delivery guarantee.
9
- *
10
- * Lives in types rather than services because any verifier must use the same
11
- * canonical encoding the server signs, byte for byte.
12
8
  */
13
9
  declare const BUNDLE_MANIFEST_ROUTE = "/bundle-manifest.json";
14
- declare const BUNDLE_MANIFEST_VERSION = 1;
10
+ declare const BUNDLE_MANIFEST_VERSION = 2;
15
11
  interface BundleAsset {
16
- /** Path as the browser requests it, leading slash included. */
12
+ /** Normalized absolute URL path, without a query or fragment. */
17
13
  path: string;
18
- /** Lowercase hex SHA-256 of the bytes served at that path. */
14
+ /** Lowercase hex SHA-256 of the served bytes. */
19
15
  sha256: string;
16
+ byteLength: number;
17
+ /** MIME type of the response, without parameters. */
18
+ contentType: string;
20
19
  }
21
20
  interface BundleManifest {
22
- version: number;
23
- /** Milliseconds, so a stale manifest is visible without another lookup. */
21
+ version: typeof BUNDLE_MANIFEST_VERSION;
22
+ /** Monotonically increasing persistent signer revision. */
23
+ revision: number;
24
+ /** Unix time in milliseconds. */
24
25
  builtAt: number;
25
26
  assets: BundleAsset[];
26
27
  }
27
28
  interface SignedBundleManifest {
28
29
  manifest: BundleManifest;
29
- /** Base64url ECDSA P-256 signature over the canonical serialization. */
30
+ /** Base64url IEEE P1363 ECDSA P-256 signature over the canonical serialization. */
30
31
  signature: string;
31
- /** Base64url SPKI of the key that signed it; trust must be established out of band. */
32
+ /** Base64url SPKI of the signing key. Trust must be established out of band. */
32
33
  publicKey: string;
33
34
  }
34
- /**
35
- * The exact bytes that get signed.
36
- *
37
- * Hand-rolled rather than `JSON.stringify` of the whole object, because a
38
- * signature is only worth anything if both sides agree byte for byte on what
39
- * was signed. Key order in a JSON object is an implementation detail; a sorted
40
- * line-per-asset encoding is not.
41
- */
35
+ /** The exact bytes signed by Node and verified by WebCrypto. */
42
36
  declare function canonicalManifest(manifest: BundleManifest): string;
43
37
  declare function isBundleManifest(value: unknown): value is BundleManifest;
38
+ declare function isSignedBundleManifest(value: unknown): value is SignedBundleManifest;
39
+ /** The recorded asset for one path, or undefined if the manifest does not cover it. */
40
+ declare function assetFor(manifest: BundleManifest, path: string): BundleAsset | undefined;
44
41
  /** The recorded digest for one asset, or undefined if the manifest does not cover it. */
45
42
  declare function digestFor(manifest: BundleManifest, path: string): string | undefined;
46
43
  //#endregion
47
- export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, canonicalManifest, digestFor, isBundleManifest };
44
+ export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, assetFor, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest };
48
45
  //# sourceMappingURL=bundleManifest.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bundleManifest.d.cts","names":[],"sources":["../../src/types/bundleManifest.ts"],"mappings":";;;;;;;;;;;;cAYa;cACA;UAEI;;EAEf;;EAEA;;UAGe;EACf;;EAEA;EACA,QAAQ;;UAGO;EACf,UAAU;;EAEV;;EAEA;;;;;;;;;;iBAWc,kBAAkB,UAAU;iBAQ5B,iBAAiB,iBAAiB,SAAS;;iBAe3C,UAAU,UAAU,gBAAgB"}
1
+ {"version":3,"file":"bundleManifest.d.cts","names":[],"sources":["../../src/types/bundleManifest.ts"],"mappings":";;;;;;;;cAQa;cACA;UAMI;;EAEf;;EAEA;EACA;;EAEA;;UAGe;EACf,gBAAgB;;EAEhB;;EAEA;EACA,QAAQ;;UAGO;EACf,UAAU;;EAEV;;EAEA;;;iBAwBc,kBAAkB,UAAU;iBAU5B,iBAAiB,iBAAiB,SAAS;iBAsB3C,uBAAuB,iBAAiB,SAAS;;iBAgBjD,SAAS,UAAU,gBAAgB,eAAe;;iBAKlD,UAAU,UAAU,gBAAgB"}
@@ -3,46 +3,43 @@
3
3
  * A signed description of cockpit assets for an independently trusted verifier.
4
4
  *
5
5
  * A manifest does not let a browser SPA defend itself against the server or TLS
6
- * edge that supplied its executable code. Such an attacker can omit the
7
- * verifier or substitute its trust key. Consumers need a separately trusted
6
+ * edge that supplied its executable code. Consumers need a separately trusted
8
7
  * bootstrap and key before these signatures become a delivery guarantee.
9
- *
10
- * Lives in types rather than services because any verifier must use the same
11
- * canonical encoding the server signs, byte for byte.
12
8
  */
13
9
  declare const BUNDLE_MANIFEST_ROUTE = "/bundle-manifest.json";
14
- declare const BUNDLE_MANIFEST_VERSION = 1;
10
+ declare const BUNDLE_MANIFEST_VERSION = 2;
15
11
  interface BundleAsset {
16
- /** Path as the browser requests it, leading slash included. */
12
+ /** Normalized absolute URL path, without a query or fragment. */
17
13
  path: string;
18
- /** Lowercase hex SHA-256 of the bytes served at that path. */
14
+ /** Lowercase hex SHA-256 of the served bytes. */
19
15
  sha256: string;
16
+ byteLength: number;
17
+ /** MIME type of the response, without parameters. */
18
+ contentType: string;
20
19
  }
21
20
  interface BundleManifest {
22
- version: number;
23
- /** Milliseconds, so a stale manifest is visible without another lookup. */
21
+ version: typeof BUNDLE_MANIFEST_VERSION;
22
+ /** Monotonically increasing persistent signer revision. */
23
+ revision: number;
24
+ /** Unix time in milliseconds. */
24
25
  builtAt: number;
25
26
  assets: BundleAsset[];
26
27
  }
27
28
  interface SignedBundleManifest {
28
29
  manifest: BundleManifest;
29
- /** Base64url ECDSA P-256 signature over the canonical serialization. */
30
+ /** Base64url IEEE P1363 ECDSA P-256 signature over the canonical serialization. */
30
31
  signature: string;
31
- /** Base64url SPKI of the key that signed it; trust must be established out of band. */
32
+ /** Base64url SPKI of the signing key. Trust must be established out of band. */
32
33
  publicKey: string;
33
34
  }
34
- /**
35
- * The exact bytes that get signed.
36
- *
37
- * Hand-rolled rather than `JSON.stringify` of the whole object, because a
38
- * signature is only worth anything if both sides agree byte for byte on what
39
- * was signed. Key order in a JSON object is an implementation detail; a sorted
40
- * line-per-asset encoding is not.
41
- */
35
+ /** The exact bytes signed by Node and verified by WebCrypto. */
42
36
  declare function canonicalManifest(manifest: BundleManifest): string;
43
37
  declare function isBundleManifest(value: unknown): value is BundleManifest;
38
+ declare function isSignedBundleManifest(value: unknown): value is SignedBundleManifest;
39
+ /** The recorded asset for one path, or undefined if the manifest does not cover it. */
40
+ declare function assetFor(manifest: BundleManifest, path: string): BundleAsset | undefined;
44
41
  /** The recorded digest for one asset, or undefined if the manifest does not cover it. */
45
42
  declare function digestFor(manifest: BundleManifest, path: string): string | undefined;
46
43
  //#endregion
47
- export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, canonicalManifest, digestFor, isBundleManifest };
44
+ export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, assetFor, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest };
48
45
  //# sourceMappingURL=bundleManifest.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bundleManifest.d.mts","names":[],"sources":["../../src/types/bundleManifest.ts"],"mappings":";;;;;;;;;;;;cAYa;cACA;UAEI;;EAEf;;EAEA;;UAGe;EACf;;EAEA;EACA,QAAQ;;UAGO;EACf,UAAU;;EAEV;;EAEA;;;;;;;;;;iBAWc,kBAAkB,UAAU;iBAQ5B,iBAAiB,iBAAiB,SAAS;;iBAe3C,UAAU,UAAU,gBAAgB"}
1
+ {"version":3,"file":"bundleManifest.d.mts","names":[],"sources":["../../src/types/bundleManifest.ts"],"mappings":";;;;;;;;cAQa;cACA;UAMI;;EAEf;;EAEA;EACA;;EAEA;;UAGe;EACf,gBAAgB;;EAEhB;;EAEA;EACA,QAAQ;;UAGO;EACf,UAAU;;EAEV;;EAEA;;;iBAwBc,kBAAkB,UAAU;iBAU5B,iBAAiB,iBAAiB,SAAS;iBAsB3C,uBAAuB,iBAAiB,SAAS;;iBAgBjD,SAAS,UAAU,gBAAgB,eAAe;;iBAKlD,UAAU,UAAU,gBAAgB"}
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"bundleManifest.mjs","names":[],"sources":["../../src/types/bundleManifest.ts"],"sourcesContent":["/**\n * A signed description of cockpit assets for an independently trusted verifier.\n *\n * A manifest does not let a browser SPA defend itself against the server or TLS\n * edge that supplied its executable code. Such an attacker can omit the\n * verifier or substitute its trust key. Consumers need a separately trusted\n * bootstrap and key before these signatures become a delivery guarantee.\n *\n * Lives in types rather than services because any verifier must use the same\n * canonical encoding the server signs, byte for byte.\n */\n\nexport const BUNDLE_MANIFEST_ROUTE = '/bundle-manifest.json';\nexport const BUNDLE_MANIFEST_VERSION = 1;\n\nexport interface BundleAsset {\n /** Path as the browser requests it, leading slash included. */\n path: string;\n /** Lowercase hex SHA-256 of the bytes served at that path. */\n sha256: string;\n}\n\nexport interface BundleManifest {\n version: number;\n /** Milliseconds, so a stale manifest is visible without another lookup. */\n builtAt: number;\n assets: BundleAsset[];\n}\n\nexport interface SignedBundleManifest {\n manifest: BundleManifest;\n /** Base64url ECDSA P-256 signature over the canonical serialization. */\n signature: string;\n /** Base64url SPKI of the key that signed it; trust must be established out of band. */\n publicKey: string;\n}\n\n/**\n * The exact bytes that get signed.\n *\n * Hand-rolled rather than `JSON.stringify` of the whole object, because a\n * signature is only worth anything if both sides agree byte for byte on what\n * was signed. Key order in a JSON object is an implementation detail; a sorted\n * line-per-asset encoding is not.\n */\nexport function canonicalManifest(manifest: BundleManifest): string {\n const assets = [...manifest.assets].sort((left, right) =>\n left.path < right.path ? -1 : left.path > right.path ? 1 : 0,\n );\n const lines = assets.map((asset) => `${asset.path}\u0000${asset.sha256}`);\n return [String(manifest.version), String(manifest.builtAt), ...lines].join('\\n');\n}\n\nexport function isBundleManifest(value: unknown): value is BundleManifest {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<BundleManifest>;\n if (candidate.version !== BUNDLE_MANIFEST_VERSION) return false;\n if (typeof candidate.builtAt !== 'number' || !Array.isArray(candidate.assets)) return false;\n return candidate.assets.every(\n (asset: unknown) =>\n typeof asset === 'object' &&\n asset !== null &&\n typeof (asset as BundleAsset).path === 'string' &&\n /^[0-9a-f]{64}$/u.test((asset as BundleAsset).sha256),\n );\n}\n\n/** The recorded digest for one asset, or undefined if the manifest does not cover it. */\nexport function digestFor(manifest: BundleManifest, path: string): string | undefined {\n return manifest.assets.find((asset) => asset.path === path)?.sha256;\n}\n"],"mappings":"AAYA,MAAa,EAAwB,wBACxB,EAA0B,EAgCvC,SAAgB,EAAkB,EAAkC,CAIlE,IAAM,EAHS,CAAC,GAAG,EAAS,MAAM,CAAC,CAAC,MAAM,EAAM,IAC9C,EAAK,KAAO,EAAM,KAAO,GAAK,IAAK,KAAO,EAAM,KAE/B,CAAC,CAAC,IAAK,GAAU,GAAG,EAAM,KAAK,GAAG,EAAM,QAAQ,EACnE,MAAO,CAAC,OAAO,EAAS,OAAO,EAAG,OAAO,EAAS,OAAO,EAAG,GAAG,CAAK,CAAC,CAAC,KAAK;CAAI,CACjF,CAEA,SAAgB,EAAiB,EAAyC,CACxE,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAY,EAGlB,OAFI,EAAU,UAAA,GACV,OAAO,EAAU,SAAY,UAAY,CAAC,MAAM,QAAQ,EAAU,MAAM,EAAU,GAC/E,EAAU,OAAO,MACrB,GACC,OAAO,GAAU,YACjB,GACA,OAAQ,EAAsB,MAAS,UACvC,kBAAkB,KAAM,EAAsB,MAAM,CACxD,CACF,CAGA,SAAgB,EAAU,EAA0B,EAAkC,CACpF,OAAO,EAAS,OAAO,KAAM,GAAU,EAAM,OAAS,CAAI,CAAC,EAAE,MAC/D"}
1
+ {"version":3,"file":"bundleManifest.mjs","names":[],"sources":["../../src/types/bundleManifest.ts"],"sourcesContent":["/**\n * A signed description of cockpit assets for an independently trusted verifier.\n *\n * A manifest does not let a browser SPA defend itself against the server or TLS\n * edge that supplied its executable code. Consumers need a separately trusted\n * bootstrap and key before these signatures become a delivery guarantee.\n */\n\nexport const BUNDLE_MANIFEST_ROUTE = '/bundle-manifest.json';\nexport const BUNDLE_MANIFEST_VERSION = 2;\n\nconst SHA256 = /^[0-9a-f]{64}$/u;\nconst BASE64URL = /^[A-Za-z0-9_-]+$/u;\nconst CONTENT_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\\/[a-z0-9][a-z0-9!#$&^_.+-]*$/u;\n\nexport interface BundleAsset {\n /** Normalized absolute URL path, without a query or fragment. */\n path: string;\n /** Lowercase hex SHA-256 of the served bytes. */\n sha256: string;\n byteLength: number;\n /** MIME type of the response, without parameters. */\n contentType: string;\n}\n\nexport interface BundleManifest {\n version: typeof BUNDLE_MANIFEST_VERSION;\n /** Monotonically increasing persistent signer revision. */\n revision: number;\n /** Unix time in milliseconds. */\n builtAt: number;\n assets: BundleAsset[];\n}\n\nexport interface SignedBundleManifest {\n manifest: BundleManifest;\n /** Base64url IEEE P1363 ECDSA P-256 signature over the canonical serialization. */\n signature: string;\n /** Base64url SPKI of the signing key. Trust must be established out of band. */\n publicKey: string;\n}\n\nfunction hasExactKeys(value: object, expected: readonly string[]): boolean {\n const keys = Object.keys(value).sort();\n return keys.length === expected.length && keys.every((key, index) => key === expected[index]);\n}\n\nfunction isSafeInteger(value: unknown, minimum: number): value is number {\n return typeof value === 'number' && Number.isSafeInteger(value) && value >= minimum;\n}\n\nfunction isSafeAssetPath(value: unknown): value is string {\n if (typeof value !== 'string' || value.length < 2 || !value.startsWith('/')) return false;\n if (value.includes('\\\\') || value.includes('\\0') || value.includes('?') || value.includes('#')) return false;\n if (/[^\\x21-\\x7e]/u.test(value) || value.includes('//') || /%(?![0-9a-f]{2})/iu.test(value)) return false;\n if (/%(?:2e|2f|5c)/iu.test(value)) return false;\n return value\n .split('/')\n .slice(1)\n .every((segment) => segment !== '' && segment !== '.' && segment !== '..');\n}\n\n/** The exact bytes signed by Node and verified by WebCrypto. */\nexport function canonicalManifest(manifest: BundleManifest): string {\n const assets = [...manifest.assets].sort((left, right) =>\n left.path < right.path ? -1 : left.path > right.path ? 1 : 0,\n );\n const lines = assets.map(\n (asset) => `${asset.path}\\0${asset.sha256}\\0${String(asset.byteLength)}\\0${asset.contentType}`,\n );\n return [String(manifest.version), String(manifest.revision), String(manifest.builtAt), ...lines].join('\\n');\n}\n\nexport function isBundleManifest(value: unknown): value is BundleManifest {\n if (typeof value !== 'object' || value === null) return false;\n if (!hasExactKeys(value, ['assets', 'builtAt', 'revision', 'version'])) return false;\n const candidate = value as Partial<BundleManifest>;\n if (candidate.version !== BUNDLE_MANIFEST_VERSION) return false;\n if (!isSafeInteger(candidate.revision, 1) || !isSafeInteger(candidate.builtAt, 0)) return false;\n if (!Array.isArray(candidate.assets) || candidate.assets.length === 0) return false;\n\n const paths = new Set<string>();\n for (const valueAsset of candidate.assets) {\n if (typeof valueAsset !== 'object' || valueAsset === null) return false;\n if (!hasExactKeys(valueAsset, ['byteLength', 'contentType', 'path', 'sha256'])) return false;\n const asset = valueAsset as Partial<BundleAsset>;\n if (!isSafeAssetPath(asset.path) || paths.has(asset.path)) return false;\n if (typeof asset.sha256 !== 'string' || !SHA256.test(asset.sha256)) return false;\n if (!isSafeInteger(asset.byteLength, 0)) return false;\n if (typeof asset.contentType !== 'string' || !CONTENT_TYPE.test(asset.contentType)) return false;\n paths.add(asset.path);\n }\n return paths.has('/index.html');\n}\n\nexport function isSignedBundleManifest(value: unknown): value is SignedBundleManifest {\n if (typeof value !== 'object' || value === null) return false;\n if (!hasExactKeys(value, ['manifest', 'publicKey', 'signature'])) return false;\n const candidate = value as Partial<SignedBundleManifest>;\n return (\n isBundleManifest(candidate.manifest) &&\n typeof candidate.signature === 'string' &&\n candidate.signature.length === 86 &&\n BASE64URL.test(candidate.signature) &&\n typeof candidate.publicKey === 'string' &&\n candidate.publicKey.length <= 512 &&\n BASE64URL.test(candidate.publicKey)\n );\n}\n\n/** The recorded asset for one path, or undefined if the manifest does not cover it. */\nexport function assetFor(manifest: BundleManifest, path: string): BundleAsset | undefined {\n return manifest.assets.find((asset) => asset.path === path);\n}\n\n/** The recorded digest for one asset, or undefined if the manifest does not cover it. */\nexport function digestFor(manifest: BundleManifest, path: string): string | undefined {\n return assetFor(manifest, path)?.sha256;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-web-security",
3
- "version": "0.0.1-alpha.4",
3
+ "version": "0.0.1-alpha.6",
4
4
  "description": "Shared security primitives for the DoomPi web cockpit: sealed channels and signed bundle manifests for independently trusted verifiers.",
5
5
  "keywords": [
6
6
  "ai",
@@ -57,7 +57,7 @@
57
57
  "access": "public"
58
58
  },
59
59
  "devDependencies": {
60
- "@types/node": "26.2.0",
60
+ "@types/node": "26.4.0",
61
61
  "@vitest/coverage-v8": "4.1.11",
62
62
  "tsdown": "0.22.14",
63
63
  "typescript": "7.0.2",