@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.
- package/README.md +14 -7
- package/dist/adapters/browserBundleVerifier.cjs +2 -0
- package/dist/adapters/browserBundleVerifier.cjs.map +1 -0
- package/dist/adapters/browserBundleVerifier.d.cts +45 -0
- package/dist/adapters/browserBundleVerifier.d.cts.map +1 -0
- package/dist/adapters/browserBundleVerifier.d.mts +45 -0
- package/dist/adapters/browserBundleVerifier.d.mts.map +1 -0
- package/dist/adapters/browserBundleVerifier.mjs +2 -0
- package/dist/adapters/browserBundleVerifier.mjs.map +1 -0
- package/dist/adapters/bundleSigner.cjs +1 -1
- package/dist/adapters/bundleSigner.cjs.map +1 -1
- package/dist/adapters/bundleSigner.d.cts +7 -3
- package/dist/adapters/bundleSigner.d.cts.map +1 -1
- package/dist/adapters/bundleSigner.d.mts +7 -3
- package/dist/adapters/bundleSigner.d.mts.map +1 -1
- package/dist/adapters/bundleSigner.mjs +1 -1
- package/dist/adapters/bundleSigner.mjs.map +1 -1
- package/dist/browser.cjs +1 -1
- package/dist/browser.d.cts +3 -1
- package/dist/browser.d.mts +3 -1
- package/dist/browser.mjs +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/types/bundleManifest.cjs +0 -0
- package/dist/types/bundleManifest.cjs.map +1 -1
- package/dist/types/bundleManifest.d.cts +18 -21
- package/dist/types/bundleManifest.d.cts.map +1 -1
- package/dist/types/bundleManifest.d.mts +18 -21
- package/dist/types/bundleManifest.d.mts.map +1 -1
- package/dist/types/bundleManifest.mjs +0 -0
- package/dist/types/bundleManifest.mjs.map +1 -1
- 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
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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":"6MAmBA,MAAM,EAAe,eAMf,EAAU,IAAI,IAAI,CAAC,MAAM,CAAC,EAC1B,EAA+C,CACnD,OAAQ,WACR,OAAQ,YACR,QAAS,YACT,OAAQ,eACR,QAAS,aACT,OAAQ,aACR,MAAO,kBACP,QAAS,mBACT,OAAQ,kBACR,OAAQ,YACR,OAAQ,gBACR,OAAQ,aACR,QAAS,mBACT,QAAS,aACT,QAAS,YACT,SAAU,YACZ,EAmBA,SAAS,EAAqB,EAAwB,CACpD,EAAA,QAAG,UAAU,EAAU,CAAE,UAAW,GAAM,KAAM,GAAe,CAAC,EAChE,IAAM,EAAOA,EAAAA,QAAG,UAAU,CAAQ,EAClC,GAAI,CAAC,EAAK,YAAY,GAAK,EAAK,eAAe,EAC7C,MAAU,MAAM,4CAA4C,GAAU,EACxE,EAAA,QAAG,UAAU,EAAU,GAAc,CACvC,CAEA,SAAS,EAAiB,EAAkB,EAAuB,CACjE,EAAqB,CAAQ,EAC7B,IAAM,EAAWC,EAAAA,QAAK,KAAK,EAAU,cAAS,EAC1C,EACJ,IAAK,IAAI,EAAU,EAAG,EAAU,IAAK,GAAW,EAC9C,GAAI,CACF,EAAaD,EAAAA,QAAG,SAAS,EAAU,KAAM,GAAS,EAClD,KACF,OAAS,EAAO,CACd,GAAK,EAAgC,OAAS,SAAU,MAAM,EAC9D,QAAQ,KAAK,IAAI,WAAW,IAAI,kBAAkB,CAAC,CAAC,EAAG,EAAG,EAAG,EAAE,CACjE,CAEF,GAAI,IAAe,IAAA,GAAW,MAAU,MAAM,oDAAoD,GAAU,EAC5G,GAAI,CACF,OAAO,EAAU,CACnB,QAAU,CACR,EAAA,QAAG,UAAU,CAAU,EACvB,EAAA,QAAG,OAAO,EAAU,CAAE,MAAO,EAAK,CAAC,CACrC,CACF,CAEA,SAAS,EAAiB,EAAkB,EAA0B,CACpE,IAAM,EAAcC,EAAAA,QAAK,KAAK,EAAU,CAAY,EAC9C,EAAYA,EAAAA,QAAK,KAAK,EAAU,IAAI,EAAa,GAAG,QAAQ,IAAI,IAAA,EAAGC,EAAAA,YAAAA,CAAY,CAAC,CAAC,CAAC,SAAS,KAAK,GAAG,EACnG,EAAaF,EAAAA,QAAG,SAAS,EAAW,KAAM,GAAS,EACzD,GAAI,CACF,EAAA,QAAG,cAAc,EAAY,GAAG,KAAK,UAAU,EAAO,IAAA,GAAW,CAAC,EAAE,GAAG,EACvE,EAAA,QAAG,UAAU,CAAU,CACzB,QAAU,CACR,EAAA,QAAG,UAAU,CAAU,CACzB,CACA,GAAI,CACF,EAAA,QAAG,WAAW,EAAW,CAAW,EACpC,EAAA,QAAG,UAAU,EAAa,GAAS,CACrC,OAAS,EAAO,CAEd,MADA,EAAA,QAAG,OAAO,EAAW,CAAE,MAAO,EAAK,CAAC,EAC9B,CACR,CACF,CAEA,SAAS,EAAW,EAAgB,EAA8B,CAChE,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAU,MAAM,iCAAiC,GAAS,EAC3G,IAAM,EAAY,EAClB,GAAI,OAAO,EAAU,YAAe,UAAY,OAAO,EAAU,WAAc,SAC7E,MAAU,MAAM,iCAAiC,GAAS,EAE5D,IAAM,EAAW,EAAU,WAAa,IAAA,GAAY,EAAI,EAAU,SAClE,GAAI,CAAC,OAAO,cAAc,CAAQ,GAAK,EAAW,EAAG,MAAU,MAAM,oCAAoC,GAAS,EAClH,IAAI,EACJ,GAAI,CACF,EAAU,EAAY,EAAU,UAAU,CAC5C,MAAQ,CACN,MAAU,MAAM,uCAAuC,GAAS,CAClE,CACA,GAAI,IAAY,EAAU,UAAW,MAAU,MAAM,qCAAqC,GAAS,EACnG,MAAO,CAAE,WAAY,EAAU,WAAY,UAAW,EAAU,UAAW,UAAS,CACtF,CAEA,SAAS,EAAkB,EAA+B,CACxD,OAAO,EAAc,MAAgB,CACnC,IAAM,EAAUC,EAAAA,QAAK,KAAK,EAAU,CAAY,EAChD,GAAI,CACF,IAAM,EAAOD,EAAAA,QAAG,UAAU,CAAO,EACjC,GAAI,CAAC,EAAK,OAAO,GAAK,EAAK,eAAe,EAAG,MAAU,MAAM,sCAAsC,GAAS,EAC5G,IAAM,EAAkB,KAAK,MAAMA,EAAAA,QAAG,aAAa,EAAS,MAAM,CAAC,EAC7D,EAAQ,EAAW,EAAQ,CAAO,EAGxC,OAFA,EAAA,QAAG,UAAU,EAAS,GAAS,EAC1B,EAAgC,WAAa,IAAA,IAAW,EAAiB,EAAU,CAAK,EACtF,CACT,OAAS,EAAO,CACd,GAAK,EAAgC,OAAS,SAAU,MAAM,CAChE,CAEA,IAAM,GAAA,EAAOG,EAAAA,oBAAAA,CAAoB,KAAM,CACrC,WAAY,aACZ,mBAAoB,CAAE,KAAM,QAAS,OAAQ,KAAM,EACnD,kBAAmB,CAAE,KAAM,OAAQ,OAAQ,KAAM,CACnD,CAAC,EACK,EAAqB,CACzB,WAAY,EAAK,WACjB,UAAW,OAAO,KAAK,EAAK,SAAS,CAAC,CAAC,SAAS,WAAW,EAC3D,SAAU,CACZ,EAEA,OADA,EAAiB,EAAU,CAAK,EACzB,CACT,CAAC,CACH,CAEA,SAAS,EAAgB,EAAkB,EAAmC,CAC5E,OAAO,EAAc,MAAgB,CACnC,IAAM,EAAUF,EAAAA,QAAK,KAAK,EAAU,CAAY,EAC1C,EAAQ,EAAW,KAAK,MAAMD,EAAAA,QAAG,aAAa,EAAS,MAAM,CAAC,EAAc,CAAO,EACzF,GAAI,EAAM,YAAc,EACtB,MAAU,MAAM,wDAAwD,EAC1E,GAAI,EAAM,mBAAsC,MAAU,MAAM,sCAAsC,EACtG,IAAM,EAAO,CAAE,GAAG,EAAO,SAAU,EAAM,SAAW,CAAE,EAEtD,OADA,EAAiB,EAAU,CAAI,EACxB,EAAK,QACd,CAAC,CACH,CAEA,SAAS,EAAe,EAA0B,CAChD,OAAO,EAAWC,EAAAA,QAAK,QAAQ,CAAQ,CAAC,CAAC,YAAY,IAAM,0BAC7D,CAGA,SAAS,EAAK,EAAc,EAAiB,EAA2B,CACtE,IAAM,EAAUD,EAAAA,QAAG,YAAY,EAAS,CAAE,cAAe,EAAK,CAAC,EAC/D,IAAK,IAAM,KAAS,EAAS,CAC3B,IAAM,EAAOC,EAAAA,QAAK,KAAK,EAAS,EAAM,IAAI,EACpC,EAAOD,EAAAA,QAAG,UAAU,CAAI,EAC9B,GAAI,EAAK,eAAe,EAAG,MAAU,MAAM,mCAAmC,GAAM,EACpF,GAAI,EAAK,YAAY,EAAG,CACtB,EAAK,EAAM,EAAM,CAAI,EACrB,QACF,CACA,GAAI,CAAC,EAAK,OAAO,EAAG,MAAU,MAAM,uCAAuC,GAAM,EACjF,GAAI,EAAQ,IAAIC,EAAAA,QAAK,QAAQ,EAAM,IAAI,CAAC,EAAG,SAC3C,IAAM,EAAQD,EAAAA,QAAG,aAAa,CAAI,EAClC,EAAK,KAAK,CACR,KAAM,IAAIC,EAAAA,QACP,SAAS,EAAM,CAAI,CAAC,CACpB,MAAMA,EAAAA,QAAK,GAAG,CAAC,CACf,IAAK,GAAY,mBAAmB,CAAO,CAAC,CAAC,CAC7C,KAAK,GAAG,IACX,QAAA,EAAQG,EAAAA,WAAAA,CAAW,QAAQ,CAAC,CAAC,OAAO,CAAK,CAAC,CAAC,OAAO,KAAK,EACvD,WAAY,EAAM,WAClB,YAAa,EAAe,CAAI,CAClC,CAAC,CACH,CACF,CAEA,SAAgB,EAAmB,EAAkB,MAA6C,CAAC,EAAiB,CAClH,IAAM,EAAM,EAAkB,CAAQ,EAChC,GAAA,EAAaC,EAAAA,iBAAAA,CAAiB,EAAI,UAAU,EAC5C,EAAQ,IAAI,IAEZ,EAAa,GAAwD,CACzE,IAAM,EAAWL,EAAAA,QAAG,UAAU,CAAS,EACvC,GAAI,CAAC,EAAS,YAAY,GAAK,EAAS,eAAe,EACrD,MAAU,MAAM,0CAA0C,GAAW,EAEvE,IAAM,EAAwB,CAAC,EAE/B,GADA,EAAK,EAAW,EAAW,CAAM,EAC7B,EAAO,SAAW,EAAG,OACzB,EAAO,MAAM,EAAM,IAAU,EAAK,KAAK,cAAc,EAAM,IAAI,CAAC,EAChE,IAAM,EAA2B,CAC/B,QAAA,EACA,SAAU,EAAgB,EAAU,EAAI,SAAS,EACjD,QAAS,KAAK,IAAI,EAClB,QACF,EACA,GAAI,CAACM,EAAAA,iBAAiB,CAAQ,EAAG,MAAU,MAAM,6CAA6C,EAK9F,MAAO,CAAE,WAAU,WAAA,EAJDC,EAAAA,WAAAA,CAAW,QAAc,CAAC,CACzC,OAAOC,EAAAA,kBAAkB,CAAQ,CAAC,CAAC,CACnC,KAAK,CAAE,IAAK,EAAY,YAAa,YAAa,CAAC,CAAC,CACpD,SAAS,WACe,EAAG,UAAW,EAAI,SAAU,CACzD,EAEM,EAAQ,GAAwD,CACpE,IAAM,EAAWP,EAAAA,QAAK,QAAQ,CAAS,EACvC,GAAI,EAAM,IAAI,CAAQ,EAAG,OAAO,EAAM,IAAI,CAAQ,EAClD,IAAM,EAAS,EAAU,CAAQ,EAEjC,OADA,EAAM,IAAI,EAAU,CAAM,EACnB,CACT,EAEA,MAAO,CACL,cAAiB,EAAI,UACrB,OACA,QAAQ,EAAW,CACjB,IAAM,EAAWA,EAAAA,QAAK,QAAQ,CAAS,EAEvC,OADA,EAAM,OAAO,CAAQ,EACd,EAAK,CAAQ,CACtB,EACA,WAAW,EAAW,CAChB,IAAc,IAAA,GAAW,EAAM,MAAM,EACpC,EAAM,OAAOA,EAAAA,QAAK,QAAQ,CAAS,CAAC,CAC3C,CACF,CACF,CAGA,SAAgB,EAAY,EAA+B,CACzD,OAAO,OAAO,MAAA,EAAKQ,EAAAA,gBAAAA,CAAgB,CAAa,CAAC,CAAC,OAAO,CAAE,KAAM,OAAQ,OAAQ,KAAM,CAAC,CAAW,CAAC,CAAC,SACnG,WACF,CACF"}
|
|
@@ -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
|
|
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,
|
|
10
|
-
/** Re-derives the public key from a stored private key
|
|
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":";;
|
|
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
|
|
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,
|
|
10
|
-
/** Re-derives the public key from a stored private key
|
|
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":";;
|
|
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
|
|
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"),
|
|
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;
|
package/dist/browser.d.cts
CHANGED
|
@@ -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 };
|
package/dist/browser.d.mts
CHANGED
|
@@ -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{
|
|
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/
|
|
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{
|
|
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.
|
|
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.
|
|
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 =
|
|
10
|
+
declare const BUNDLE_MANIFEST_VERSION = 2;
|
|
15
11
|
interface BundleAsset {
|
|
16
|
-
/**
|
|
12
|
+
/** Normalized absolute URL path, without a query or fragment. */
|
|
17
13
|
path: string;
|
|
18
|
-
/** Lowercase hex SHA-256 of the
|
|
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:
|
|
23
|
-
/**
|
|
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
|
|
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":"
|
|
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.
|
|
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 =
|
|
10
|
+
declare const BUNDLE_MANIFEST_VERSION = 2;
|
|
15
11
|
interface BundleAsset {
|
|
16
|
-
/**
|
|
12
|
+
/** Normalized absolute URL path, without a query or fragment. */
|
|
17
13
|
path: string;
|
|
18
|
-
/** Lowercase hex SHA-256 of the
|
|
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:
|
|
23
|
-
/**
|
|
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
|
|
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":"
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|