@agimon-ai/doompi-web-security 0.0.1-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +92 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +1 -0
  4. package/dist/adapters/browserBundleVerifier.cjs +2 -0
  5. package/dist/adapters/browserBundleVerifier.cjs.map +1 -0
  6. package/dist/adapters/browserBundleVerifier.d.cts +45 -0
  7. package/dist/adapters/browserBundleVerifier.d.cts.map +1 -0
  8. package/dist/adapters/browserBundleVerifier.d.mts +45 -0
  9. package/dist/adapters/browserBundleVerifier.d.mts.map +1 -0
  10. package/dist/adapters/browserBundleVerifier.mjs +2 -0
  11. package/dist/adapters/browserBundleVerifier.mjs.map +1 -0
  12. package/dist/adapters/browserSealedChannel.cjs +2 -0
  13. package/dist/adapters/browserSealedChannel.cjs.map +1 -0
  14. package/dist/adapters/browserSealedChannel.d.cts +36 -0
  15. package/dist/adapters/browserSealedChannel.d.cts.map +1 -0
  16. package/dist/adapters/browserSealedChannel.d.mts +36 -0
  17. package/dist/adapters/browserSealedChannel.d.mts.map +1 -0
  18. package/dist/adapters/browserSealedChannel.mjs +2 -0
  19. package/dist/adapters/browserSealedChannel.mjs.map +1 -0
  20. package/dist/adapters/bundleSigner.cjs +2 -0
  21. package/dist/adapters/bundleSigner.cjs.map +1 -0
  22. package/dist/adapters/bundleSigner.d.cts +18 -0
  23. package/dist/adapters/bundleSigner.d.cts.map +1 -0
  24. package/dist/adapters/bundleSigner.d.mts +18 -0
  25. package/dist/adapters/bundleSigner.d.mts.map +1 -0
  26. package/dist/adapters/bundleSigner.mjs +2 -0
  27. package/dist/adapters/bundleSigner.mjs.map +1 -0
  28. package/dist/adapters/nodeSealedChannel.cjs +2 -0
  29. package/dist/adapters/nodeSealedChannel.cjs.map +1 -0
  30. package/dist/adapters/nodeSealedChannel.d.cts +40 -0
  31. package/dist/adapters/nodeSealedChannel.d.cts.map +1 -0
  32. package/dist/adapters/nodeSealedChannel.d.mts +40 -0
  33. package/dist/adapters/nodeSealedChannel.d.mts.map +1 -0
  34. package/dist/adapters/nodeSealedChannel.mjs +2 -0
  35. package/dist/adapters/nodeSealedChannel.mjs.map +1 -0
  36. package/dist/adapters/sealedTransport.cjs +2 -0
  37. package/dist/adapters/sealedTransport.cjs.map +1 -0
  38. package/dist/adapters/sealedTransport.d.cts +43 -0
  39. package/dist/adapters/sealedTransport.d.cts.map +1 -0
  40. package/dist/adapters/sealedTransport.d.mts +43 -0
  41. package/dist/adapters/sealedTransport.d.mts.map +1 -0
  42. package/dist/adapters/sealedTransport.mjs +2 -0
  43. package/dist/adapters/sealedTransport.mjs.map +1 -0
  44. package/dist/browser.cjs +1 -0
  45. package/dist/browser.d.cts +5 -0
  46. package/dist/browser.d.mts +5 -0
  47. package/dist/browser.mjs +1 -0
  48. package/dist/index.cjs +1 -0
  49. package/dist/index.d.cts +4 -0
  50. package/dist/index.d.mts +4 -0
  51. package/dist/index.mjs +1 -0
  52. package/dist/node.cjs +1 -0
  53. package/dist/node.d.cts +3 -0
  54. package/dist/node.d.mts +3 -0
  55. package/dist/node.mjs +1 -0
  56. package/dist/services/serialQueue.cjs +2 -0
  57. package/dist/services/serialQueue.cjs.map +1 -0
  58. package/dist/services/serialQueue.d.cts +23 -0
  59. package/dist/services/serialQueue.d.cts.map +1 -0
  60. package/dist/services/serialQueue.d.mts +23 -0
  61. package/dist/services/serialQueue.d.mts.map +1 -0
  62. package/dist/services/serialQueue.mjs +2 -0
  63. package/dist/services/serialQueue.mjs.map +1 -0
  64. package/dist/types/bundleManifest.cjs +3 -0
  65. package/dist/types/bundleManifest.cjs.map +1 -0
  66. package/dist/types/bundleManifest.d.cts +45 -0
  67. package/dist/types/bundleManifest.d.cts.map +1 -0
  68. package/dist/types/bundleManifest.d.mts +45 -0
  69. package/dist/types/bundleManifest.d.mts.map +1 -0
  70. package/dist/types/bundleManifest.mjs +3 -0
  71. package/dist/types/bundleManifest.mjs.map +1 -0
  72. package/dist/types/sealedChannel.cjs +2 -0
  73. package/dist/types/sealedChannel.cjs.map +1 -0
  74. package/dist/types/sealedChannel.d.cts +77 -0
  75. package/dist/types/sealedChannel.d.cts.map +1 -0
  76. package/dist/types/sealedChannel.d.mts +77 -0
  77. package/dist/types/sealedChannel.d.mts.map +1 -0
  78. package/dist/types/sealedChannel.mjs +2 -0
  79. package/dist/types/sealedChannel.mjs.map +1 -0
  80. package/llms.txt +9 -0
  81. package/package.json +76 -0
@@ -0,0 +1,40 @@
1
+ import { SealedEnvelope, SealedFailure } from "../types/sealedChannel.cjs";
2
+ //#region src/adapters/nodeSealedChannel.d.ts
3
+ type SealResult = {
4
+ ok: true;
5
+ envelope: SealedEnvelope;
6
+ } | {
7
+ ok: false;
8
+ failure: SealedFailure;
9
+ };
10
+ type OpenResult = {
11
+ ok: true;
12
+ plaintext: Uint8Array;
13
+ } | {
14
+ ok: false;
15
+ failure: SealedFailure;
16
+ };
17
+ interface SealedChannel {
18
+ /** Seals one message. Counters advance only on success. */
19
+ seal(plaintext: Uint8Array): SealResult;
20
+ open(envelope: unknown): OpenResult;
21
+ }
22
+ interface HostHandshake {
23
+ /** Base64url uncompressed P-256 point, printed in the QR. */
24
+ publicKey: string;
25
+ /** Completes the exchange against the peer's key and yields the channel. */
26
+ accept(peerPublicKey: string): SealedChannel | undefined;
27
+ }
28
+ /**
29
+ * The host half of the handshake.
30
+ *
31
+ * The ephemeral key pair is generated per tunnel and its public half goes in the
32
+ * QR, which is the out-of-band channel that makes this authenticated. A relay
33
+ * that swaps the client's key gets a channel with itself and nothing from the
34
+ * host, because the host only ever derives against the key the client presents
35
+ * and the client only ever derives against the key it read off the screen.
36
+ */
37
+ declare function createHostHandshake(): HostHandshake;
38
+ //#endregion
39
+ export { HostHandshake, OpenResult, SealResult, SealedChannel, createHostHandshake };
40
+ //# sourceMappingURL=nodeSealedChannel.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nodeSealedChannel.d.cts","names":[],"sources":["../../src/adapters/nodeSealedChannel.ts"],"mappings":";;KAqBY;EAAe;EAAU,UAAU;;EAAqB;EAAW,SAAS;;KAC5E;EAAe;EAAU,WAAW;;EAAiB;EAAW,SAAS;;UAEpE;;EAEf,KAAK,WAAW,aAAa;EAC7B,KAAK,oBAAoB;;UAGV;;EAEf;;EAEA,OAAO,wBAAwB;;;;;;;;;;;iBAwFjB,uBAAuB"}
@@ -0,0 +1,40 @@
1
+ import { SealedEnvelope, SealedFailure } from "../types/sealedChannel.mjs";
2
+ //#region src/adapters/nodeSealedChannel.d.ts
3
+ type SealResult = {
4
+ ok: true;
5
+ envelope: SealedEnvelope;
6
+ } | {
7
+ ok: false;
8
+ failure: SealedFailure;
9
+ };
10
+ type OpenResult = {
11
+ ok: true;
12
+ plaintext: Uint8Array;
13
+ } | {
14
+ ok: false;
15
+ failure: SealedFailure;
16
+ };
17
+ interface SealedChannel {
18
+ /** Seals one message. Counters advance only on success. */
19
+ seal(plaintext: Uint8Array): SealResult;
20
+ open(envelope: unknown): OpenResult;
21
+ }
22
+ interface HostHandshake {
23
+ /** Base64url uncompressed P-256 point, printed in the QR. */
24
+ publicKey: string;
25
+ /** Completes the exchange against the peer's key and yields the channel. */
26
+ accept(peerPublicKey: string): SealedChannel | undefined;
27
+ }
28
+ /**
29
+ * The host half of the handshake.
30
+ *
31
+ * The ephemeral key pair is generated per tunnel and its public half goes in the
32
+ * QR, which is the out-of-band channel that makes this authenticated. A relay
33
+ * that swaps the client's key gets a channel with itself and nothing from the
34
+ * host, because the host only ever derives against the key the client presents
35
+ * and the client only ever derives against the key it read off the screen.
36
+ */
37
+ declare function createHostHandshake(): HostHandshake;
38
+ //#endregion
39
+ export { HostHandshake, OpenResult, SealResult, SealedChannel, createHostHandshake };
40
+ //# sourceMappingURL=nodeSealedChannel.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nodeSealedChannel.d.mts","names":[],"sources":["../../src/adapters/nodeSealedChannel.ts"],"mappings":";;KAqBY;EAAe;EAAU,UAAU;;EAAqB;EAAW,SAAS;;KAC5E;EAAe;EAAU,WAAW;;EAAiB;EAAW,SAAS;;UAEpE;;EAEf,KAAK,WAAW,aAAa;EAC7B,KAAK,oBAAoB;;UAGV;;EAEf;;EAEA,OAAO,wBAAwB;;;;;;;;;;;iBAwFjB,uBAAuB"}
@@ -0,0 +1,2 @@
1
+ import{MAX_MESSAGES_PER_KEY as e,infoFor as t,isSealedEnvelope as n}from"../types/sealedChannel.mjs";import{ECDH as r,createCipheriv as i,createDecipheriv as a,createECDH as o,hkdfSync as s,randomBytes as c}from"node:crypto";const l=`prime256v1`,u=`aes-256-gcm`,d=Buffer.from(`doompi/sealed/v1`);function f(e,n){return Buffer.from(s(`sha256`,e,d,Buffer.from(t(n)),32))}function p(e,t){let n=Buffer.alloc(12);return e.copy(n,0,0,4),n.writeBigUInt64BE(t,4),n}function m(t,r,o){let s=f(t,r),c=f(t,r===`c2s`?`s2c`:`c2s`),l=0n,d=-1n;return{seal(t){if(l>=BigInt(e))return{ok:!1,failure:`exhausted`};let n=p(o,l),r=i(u,s,n),a=Buffer.concat([r.update(t),r.final(),r.getAuthTag()]);return l+=1n,{ok:!0,envelope:{v:1,n:n.toString(`base64url`),c:a.toString(`base64url`)}}},open(e){let t=e;if(typeof t?.n!=`string`||typeof t.c!=`string`)return{ok:!1,failure:`malformed`};if(t.v!==1)return{ok:!1,failure:`version`};if(!n(e))return{ok:!1,failure:`malformed`};let r=Buffer.from(e.n,`base64url`),i=Buffer.from(e.c,`base64url`);if(r.length!==12||i.length<16)return{ok:!1,failure:`malformed`};let o=r.readBigUInt64BE(4);if(o<=d)return{ok:!1,failure:`replay`};let s=a(u,c,r);s.setAuthTag(i.subarray(i.length-16));let l;try{l=Buffer.concat([s.update(i.subarray(0,i.length-16)),s.final()])}catch{return{ok:!1,failure:`auth`}}return d=o,{ok:!0,plaintext:new Uint8Array(l)}}}}function h(){let e=o(l);e.generateKeys();let t=new Set;return{publicKey:e.getPublicKey().toString(`base64url`),accept(n){let i,a;try{if(i=r.convertKey(Buffer.from(n,`base64url`),l,void 0,void 0,`uncompressed`),t.has(i.toString(`base64url`)))return;a=e.computeSecret(i)}catch{return}return t.add(i.toString(`base64url`)),m(a,`s2c`,c(4))}}}export{h as createHostHandshake};
2
+ //# sourceMappingURL=nodeSealedChannel.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nodeSealedChannel.mjs","names":[],"sources":["../../src/adapters/nodeSealedChannel.ts"],"sourcesContent":["import { ECDH, createCipheriv, createDecipheriv, createECDH, hkdfSync, randomBytes } from 'node:crypto';\nimport {\n MAX_MESSAGES_PER_KEY,\n NONCE_BYTES,\n NONCE_PREFIX_BYTES,\n SEALED_VERSION,\n type SealedDirection,\n type SealedEnvelope,\n type SealedFailure,\n infoFor,\n isSealedEnvelope,\n} from '../types/sealedChannel.ts';\n\nconst CURVE = 'prime256v1';\nconst CIPHER = 'aes-256-gcm';\nconst KEY_BYTES = 32;\nconst TAG_BYTES = 16;\nconst HKDF_HASH = 'sha256';\n/** Fixed and public: HKDF's salt need not be secret, and a constant keeps both sides in step. */\nconst HKDF_SALT = Buffer.from('doompi/sealed/v1');\n\nexport type SealResult = { ok: true; envelope: SealedEnvelope } | { ok: false; failure: SealedFailure };\nexport type OpenResult = { ok: true; plaintext: Uint8Array } | { ok: false; failure: SealedFailure };\n\nexport interface SealedChannel {\n /** Seals one message. Counters advance only on success. */\n seal(plaintext: Uint8Array): SealResult;\n open(envelope: unknown): OpenResult;\n}\n\nexport interface HostHandshake {\n /** Base64url uncompressed P-256 point, printed in the QR. */\n publicKey: string;\n /** Completes the exchange against the peer's key and yields the channel. */\n accept(peerPublicKey: string): SealedChannel | undefined;\n}\n\nfunction derive(secret: Buffer, direction: SealedDirection): Buffer {\n return Buffer.from(hkdfSync(HKDF_HASH, secret, HKDF_SALT, Buffer.from(infoFor(direction)), KEY_BYTES));\n}\n\n/**\n * Builds the nonce for one message.\n *\n * A random per-channel prefix plus a monotonic counter, rather than a random\n * nonce per message: 96 bits is small enough that random nonces collide at a\n * rate worth caring about, and a repeated nonce under one AES-GCM key is\n * catastrophic rather than merely weak.\n */\nfunction nonceFor(prefix: Buffer, counter: bigint): Buffer {\n const nonce = Buffer.alloc(NONCE_BYTES);\n prefix.copy(nonce, 0, 0, NONCE_PREFIX_BYTES);\n nonce.writeBigUInt64BE(counter, NONCE_PREFIX_BYTES);\n return nonce;\n}\n\n/**\n * One direction's sealing state.\n *\n * Send and receive are separate keys and separate counters, so a message the\n * server sent can never be replayed back at it as though the client had sent it.\n */\nfunction createChannel(secret: Buffer, sendDirection: SealedDirection, noncePrefix: Buffer): SealedChannel {\n const sendKey = derive(secret, sendDirection);\n const receiveKey = derive(secret, sendDirection === 'c2s' ? 's2c' : 'c2s');\n let sendCounter = 0n;\n /** Strictly increasing: anything at or below what has been seen is a replay. */\n let highestReceived = -1n;\n\n return {\n seal(plaintext) {\n if (sendCounter >= BigInt(MAX_MESSAGES_PER_KEY)) return { ok: false, failure: 'exhausted' };\n const nonce = nonceFor(noncePrefix, sendCounter);\n const cipher = createCipheriv(CIPHER, sendKey, nonce);\n const body = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]);\n sendCounter += 1n;\n return {\n ok: true,\n envelope: { v: SEALED_VERSION, n: nonce.toString('base64url'), c: body.toString('base64url') },\n };\n },\n\n open(envelope) {\n // Shape before version: something with no `v` at all is malformed, and\n // calling it a version mismatch would send a reader hunting for a\n // protocol skew that is not there.\n const shape = envelope as { v?: unknown; n?: unknown; c?: unknown } | null | undefined;\n if (typeof shape?.n !== 'string' || typeof shape.c !== 'string') return { ok: false, failure: 'malformed' };\n if (shape.v !== SEALED_VERSION) return { ok: false, failure: 'version' };\n if (!isSealedEnvelope(envelope)) return { ok: false, failure: 'malformed' };\n const nonce = Buffer.from(envelope.n, 'base64url');\n const body = Buffer.from(envelope.c, 'base64url');\n if (nonce.length !== NONCE_BYTES || body.length < TAG_BYTES) return { ok: false, failure: 'malformed' };\n const counter = nonce.readBigUInt64BE(NONCE_PREFIX_BYTES);\n if (counter <= highestReceived) return { ok: false, failure: 'replay' };\n const decipher = createDecipheriv(CIPHER, receiveKey, nonce);\n decipher.setAuthTag(body.subarray(body.length - TAG_BYTES));\n let plaintext: Buffer;\n try {\n plaintext = Buffer.concat([decipher.update(body.subarray(0, body.length - TAG_BYTES)), decipher.final()]);\n } catch {\n // A failed tag means the message was altered, which is the whole point\n // of having one. Never fall back to the ciphertext.\n return { ok: false, failure: 'auth' };\n }\n // Advanced only after the tag verified, so a forged high counter cannot\n // lock out the messages that follow it.\n highestReceived = counter;\n return { ok: true, plaintext: new Uint8Array(plaintext) };\n },\n };\n}\n\n/**\n * The host half of the handshake.\n *\n * The ephemeral key pair is generated per tunnel and its public half goes in the\n * QR, which is the out-of-band channel that makes this authenticated. A relay\n * that swaps the client's key gets a channel with itself and nothing from the\n * host, because the host only ever derives against the key the client presents\n * and the client only ever derives against the key it read off the screen.\n */\nexport function createHostHandshake(): HostHandshake {\n const exchange = createECDH(CURVE);\n exchange.generateKeys();\n const acceptedPeers = new Set<string>();\n return {\n publicKey: exchange.getPublicKey().toString('base64url'),\n accept(peerPublicKey) {\n let peer: Buffer;\n let secret: Buffer;\n try {\n peer = ECDH.convertKey(Buffer.from(peerPublicKey, 'base64url'), CURVE, undefined, undefined, 'uncompressed');\n if (acceptedPeers.has(peer.toString('base64url'))) return undefined;\n secret = exchange.computeSecret(peer);\n } catch {\n // A key that is not a point on the curve; there is no channel to make.\n return undefined;\n }\n acceptedPeers.add(peer.toString('base64url'));\n // The prefix is the host's to choose and travels in every nonce, so the\n // client reads it off the wire rather than needing it up front.\n return createChannel(secret, 's2c', randomBytes(NONCE_PREFIX_BYTES));\n },\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("../types/sealedChannel.cjs"),t=require("./browserSealedChannel.cjs"),n=require("../services/serialQueue.cjs"),r=new TextEncoder,i=new TextDecoder;function a(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t)}function o(e){let t=atob(e);return Uint8Array.from(t,e=>e.charCodeAt(0))}async function s(e){if(e!=null){if(typeof e==`string`)return r.encode(e);if(e instanceof URLSearchParams)return r.encode(e.toString());if(e instanceof Blob)return new Uint8Array(await e.arrayBuffer());if(e instanceof ArrayBuffer)return new Uint8Array(e);if(ArrayBuffer.isView(e))return new Uint8Array(e.buffer,e.byteOffset,e.byteLength);throw Error(`The sealed HTTP transport does not support this request body type.`)}}function c(e){if(!e.startsWith(`/`))throw Error(`A sealed HTTP request must use a root-relative URL.`);return e}function l(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.v===1&&typeof t.status==`number`&&Array.isArray(t.headers)&&t.headers.every(e=>Array.isArray(e)&&e.length===2&&e.every(e=>typeof e==`string`))&&typeof t.body==`string`}function u(){let u,d,f=n.createSerialQueue(),p=n.createSerialQueue(),m=n.createSerialQueue(),h,g=t=>{d=e.describeSealedFailure(t)},_=async e=>{if(u===void 0)return;let t=await u.seal(e);return t.ok?JSON.stringify(t.envelope):g(t.failure)},v=async e=>{if(u===void 0)return;let t;try{t=JSON.parse(e)}catch{return g(`malformed`)}let n=await u.open(t);return n.ok?n.plaintext:g(n.failure)};return{active:()=>u!==void 0,lastFailure:()=>d,async connect(e){let n=await t.connectSealedChannel(e);if(n===void 0){d=`This browser could not establish a sealed channel.`;return}return u=n.channel,d=void 0,n.clientPublicKey},relayRequestsThrough(e){h=e},reset(){u=void 0,d=void 0},async sealText(e){return u===void 0?e:await f.run(async()=>{let t=await _(r.encode(e));if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return t})},async openText(e){return u===void 0?e:await p.run(async()=>{let t=await v(e);return t===void 0?void 0:i.decode(t)})},async sealBinary(e){return u===void 0?e:await f.run(async()=>{let t=await _(e);if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return r.encode(t)})},async openBinary(e){return u===void 0?e:await p.run(async()=>await v(i.decode(e)))},async fetch(e,t){let n=h;return u===void 0||n===void 0?await fetch(e,t):await m.run(async()=>{let u=await s(t?.body),f={v:1,method:(t?.method??`GET`).toUpperCase(),target:c(e),headers:Array.from(new Headers(t?.headers).entries()),...u===void 0?{}:{body:a(u)}},p=await _(r.encode(JSON.stringify(f)));if(p===void 0)return new Response(JSON.stringify({error:d}),{status:502});let m=await fetch(n,{method:`POST`,credentials:`same-origin`,headers:{"content-type":`application/json`},body:p}),h=await v(await m.text());if(h===void 0)return new Response(JSON.stringify({error:d}),{status:502});let g;try{g=JSON.parse(i.decode(h))}catch{g=void 0}if(!l(g))return d=`The sealed HTTP response was malformed.`,new Response(JSON.stringify({error:d}),{status:502});let y=o(g.body);return new Response(g.status===204||g.status===205||g.status===304?null:y,{status:g.status,headers:g.headers})})}}}const d=u();exports.createSealedTransport=u,exports.sealedTransport=d;
2
+ //# sourceMappingURL=sealedTransport.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealedTransport.cjs","names":["createSerialQueue","describeSealedFailure","connectSealedChannel"],"sources":["../../src/adapters/sealedTransport.ts"],"sourcesContent":["import { createSerialQueue } from '../services/serialQueue.ts';\nimport { describeSealedFailure, type SealedFailure } from '../types/sealedChannel.ts';\nimport { connectSealedChannel, type SealedChannel } from './browserSealedChannel.ts';\n\n/**\n * The one object the cockpit and every plugin use to talk over a sealed tunnel.\n *\n * Two things make this worth having rather than each caller reaching for the\n * channel directly. First, ordering: sealing advances a nonce counter and\n * opening enforces that it strictly increases, so both directions are pushed\n * through a serial queue and overlapping calls can never reorder into a\n * self-inflicted replay. Second, reach: a plugin that calls `fetch` itself\n * sends plaintext to whoever is relaying the tunnel, and the host cannot stop\n * it. Giving every caller one obvious helper is the only enforcement available.\n *\n * When no channel is established every method is a pass-through, so the same\n * code runs unchanged on the loopback listener where there is nothing to seal.\n */\n\nconst JSON_CONTENT_TYPE = 'application/json';\n\nexport interface SealedTransport {\n /** False on loopback and before the handshake completes, where everything passes through. */\n active(): boolean;\n sealText(text: string): Promise<string>;\n /** Undefined when the message could not be opened; `lastFailure` says why. */\n openText(text: string): Promise<string | undefined>;\n sealBinary(bytes: Uint8Array): Promise<Uint8Array>;\n openBinary(bytes: Uint8Array): Promise<Uint8Array | undefined>;\n /** Drop-in for `fetch`, relaying the complete request through the sealed channel when configured. */\n fetch(input: string, init?: RequestInit): Promise<Response>;\n lastFailure(): string | undefined;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\ninterface SealedHttpRequest {\n v: 1;\n method: string;\n target: string;\n headers: Array<[string, string]>;\n body?: string;\n}\n\ninterface SealedHttpResponse {\n v: 1;\n status: number;\n headers: Array<[string, string]>;\n body: string;\n}\n\nfunction encodeBase64(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nfunction decodeBase64(value: string): Uint8Array {\n const binary = atob(value);\n return Uint8Array.from(binary, (character) => character.charCodeAt(0));\n}\n\nasync function bodyBytes(body: RequestInit['body']): Promise<Uint8Array | undefined> {\n if (body === undefined || body === null) return undefined;\n if (typeof body === 'string') return encoder.encode(body);\n if (body instanceof URLSearchParams) return encoder.encode(body.toString());\n if (body instanceof Blob) return new Uint8Array(await body.arrayBuffer());\n if (body instanceof ArrayBuffer) return new Uint8Array(body);\n if (ArrayBuffer.isView(body)) return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);\n throw new Error('The sealed HTTP transport does not support this request body type.');\n}\n\nfunction requestTarget(input: string): string {\n if (!input.startsWith('/')) throw new Error('A sealed HTTP request must use a root-relative URL.');\n return input;\n}\n\nfunction isSealedHttpResponse(value: unknown): value is SealedHttpResponse {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<SealedHttpResponse>;\n return (\n candidate.v === 1 &&\n typeof candidate.status === 'number' &&\n Array.isArray(candidate.headers) &&\n candidate.headers.every(\n (header) => Array.isArray(header) && header.length === 2 && header.every((part) => typeof part === 'string'),\n ) &&\n typeof candidate.body === 'string'\n );\n}\nexport function createSealedTransport(): SealedTransport & {\n /** Completes the handshake against the host key from the QR. */\n connect: (hostPublicKey: string) => Promise<string | undefined>;\n /** Drops the channel, so everything falls back to pass-through. */\n reset: () => void;\n /** Selects the same-origin endpoint that carries sealed HTTP exchanges. */\n relayRequestsThrough: (path: string) => void;\n} {\n let channel: SealedChannel | undefined;\n let failure: string | undefined;\n const outbound = createSerialQueue();\n const inbound = createSerialQueue();\n const exchange = createSerialQueue();\n let relayPath: string | undefined;\n const note = (reason: SealedFailure): undefined => {\n // A decryption failure otherwise shows up as a blank page with no cause, so\n // every one of them names itself.\n failure = describeSealedFailure(reason);\n return undefined;\n };\n\n const sealBytes = async (bytes: Uint8Array): Promise<string | undefined> => {\n if (channel === undefined) return undefined;\n const sealed = await channel.seal(bytes);\n if (!sealed.ok) return note(sealed.failure);\n return JSON.stringify(sealed.envelope);\n };\n\n const openBytes = async (text: string): Promise<Uint8Array | undefined> => {\n if (channel === undefined) return undefined;\n let envelope: unknown;\n try {\n envelope = JSON.parse(text);\n } catch {\n return note('malformed');\n }\n const opened = await channel.open(envelope);\n if (!opened.ok) return note(opened.failure);\n return opened.plaintext;\n };\n\n return {\n active: () => channel !== undefined,\n lastFailure: () => failure,\n\n async connect(hostPublicKey) {\n const connected = await connectSealedChannel(hostPublicKey);\n if (connected === undefined) {\n failure = 'This browser could not establish a sealed channel.';\n return undefined;\n }\n channel = connected.channel;\n failure = undefined;\n return connected.clientPublicKey;\n },\n\n relayRequestsThrough(path) {\n relayPath = path;\n },\n\n reset() {\n channel = undefined;\n failure = undefined;\n },\n\n async sealText(text) {\n if (channel === undefined) return text;\n return await outbound.run(async () => {\n const sealed = await sealBytes(encoder.encode(text));\n if (sealed === undefined) throw new Error(failure ?? 'The active sealed channel could not encrypt a message.');\n return sealed;\n });\n },\n\n async openText(text) {\n if (channel === undefined) return text;\n return await inbound.run(async () => {\n const opened = await openBytes(text);\n return opened === undefined ? undefined : decoder.decode(opened);\n });\n },\n\n async sealBinary(bytes) {\n if (channel === undefined) return bytes;\n return await outbound.run(async () => {\n const sealed = await sealBytes(bytes);\n if (sealed === undefined) throw new Error(failure ?? 'The active sealed channel could not encrypt a message.');\n return encoder.encode(sealed);\n });\n },\n\n async openBinary(bytes) {\n if (channel === undefined) return bytes;\n return await inbound.run(async () => await openBytes(decoder.decode(bytes)));\n },\n\n async fetch(input, init) {\n const gateway = relayPath;\n if (channel === undefined || gateway === undefined) return await fetch(input, init);\n return await exchange.run(async () => {\n const bytes = await bodyBytes(init?.body);\n const request: SealedHttpRequest = {\n v: 1,\n method: (init?.method ?? 'GET').toUpperCase(),\n target: requestTarget(input),\n headers: Array.from(new Headers(init?.headers).entries()),\n ...(bytes === undefined ? {} : { body: encodeBase64(bytes) }),\n };\n const sealed = await sealBytes(encoder.encode(JSON.stringify(request)));\n if (sealed === undefined) {\n return new Response(JSON.stringify({ error: failure }), { status: 502 });\n }\n const relayResponse = await fetch(gateway, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'content-type': JSON_CONTENT_TYPE },\n body: sealed,\n });\n const opened = await openBytes(await relayResponse.text());\n if (opened === undefined) {\n return new Response(JSON.stringify({ error: failure }), { status: 502 });\n }\n let decoded: unknown;\n try {\n decoded = JSON.parse(decoder.decode(opened));\n } catch {\n decoded = undefined;\n }\n if (!isSealedHttpResponse(decoded)) {\n failure = 'The sealed HTTP response was malformed.';\n return new Response(JSON.stringify({ error: failure }), { status: 502 });\n }\n const body = decodeBase64(decoded.body);\n return new Response(decoded.status === 204 || decoded.status === 205 || decoded.status === 304 ? null : body, {\n status: decoded.status,\n headers: decoded.headers,\n });\n });\n },\n };\n}\n\n/**\n * The page's one sealed channel.\n *\n * A module singleton rather than an instance per caller, because the cockpit\n * and every plugin are bundled into the same page and all of them must share\n * one pair of nonce counters. Two transports over one handshake would each\n * start counting at zero and the receiver would reject the second as a replay.\n *\n * Inactive until a handshake completes, and inactive forever on the loopback\n * listener, so `sealedTransport.fetch` is a plain `fetch` there.\n */\nexport const sealedTransport = createSealedTransport();\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ //#region src/adapters/sealedTransport.d.ts
2
+ interface SealedTransport {
3
+ /** False on loopback and before the handshake completes, where everything passes through. */
4
+ active(): boolean;
5
+ sealText(text: string): Promise<string>;
6
+ /** Undefined when the message could not be opened; `lastFailure` says why. */
7
+ openText(text: string): Promise<string | undefined>;
8
+ sealBinary(bytes: Uint8Array): Promise<Uint8Array>;
9
+ openBinary(bytes: Uint8Array): Promise<Uint8Array | undefined>;
10
+ /** Drop-in for `fetch`, relaying the complete request through the sealed channel when configured. */
11
+ fetch(input: string, init?: RequestInit): Promise<Response>;
12
+ lastFailure(): string | undefined;
13
+ }
14
+ declare function createSealedTransport(): SealedTransport & {
15
+ /** Completes the handshake against the host key from the QR. */
16
+ connect: (hostPublicKey: string) => Promise<string | undefined>;
17
+ /** Drops the channel, so everything falls back to pass-through. */
18
+ reset: () => void;
19
+ /** Selects the same-origin endpoint that carries sealed HTTP exchanges. */
20
+ relayRequestsThrough: (path: string) => void;
21
+ };
22
+ /**
23
+ * The page's one sealed channel.
24
+ *
25
+ * A module singleton rather than an instance per caller, because the cockpit
26
+ * and every plugin are bundled into the same page and all of them must share
27
+ * one pair of nonce counters. Two transports over one handshake would each
28
+ * start counting at zero and the receiver would reject the second as a replay.
29
+ *
30
+ * Inactive until a handshake completes, and inactive forever on the loopback
31
+ * listener, so `sealedTransport.fetch` is a plain `fetch` there.
32
+ */
33
+ declare const sealedTransport: SealedTransport & {
34
+ /** Completes the handshake against the host key from the QR. */
35
+ connect: (hostPublicKey: string) => Promise<string | undefined>;
36
+ /** Drops the channel, so everything falls back to pass-through. */
37
+ reset: () => void;
38
+ /** Selects the same-origin endpoint that carries sealed HTTP exchanges. */
39
+ relayRequestsThrough: (path: string) => void;
40
+ };
41
+ //#endregion
42
+ export { SealedTransport, createSealedTransport, sealedTransport };
43
+ //# sourceMappingURL=sealedTransport.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealedTransport.d.cts","names":[],"sources":["../../src/adapters/sealedTransport.ts"],"mappings":";UAqBiB;;EAEf;EACA,SAAS,eAAe;;EAExB,SAAS,eAAe;EACxB,WAAW,OAAO,aAAa,QAAQ;EACvC,WAAW,OAAO,aAAa,QAAQ;;EAEvC,MAAM,eAAe,OAAO,cAAc,QAAQ;EAClD;;iBA4Dc,yBAAyB;;EAEvC,UAAU,0BAA0B;;EAEpC;;EAEA,uBAAuB;;;;;;;;;;;;;cAmJZ,iBAAe;;EAvJjB,UAAC,0BAA0B;;EAE7B;;EAEe,uBAAC"}
@@ -0,0 +1,43 @@
1
+ //#region src/adapters/sealedTransport.d.ts
2
+ interface SealedTransport {
3
+ /** False on loopback and before the handshake completes, where everything passes through. */
4
+ active(): boolean;
5
+ sealText(text: string): Promise<string>;
6
+ /** Undefined when the message could not be opened; `lastFailure` says why. */
7
+ openText(text: string): Promise<string | undefined>;
8
+ sealBinary(bytes: Uint8Array): Promise<Uint8Array>;
9
+ openBinary(bytes: Uint8Array): Promise<Uint8Array | undefined>;
10
+ /** Drop-in for `fetch`, relaying the complete request through the sealed channel when configured. */
11
+ fetch(input: string, init?: RequestInit): Promise<Response>;
12
+ lastFailure(): string | undefined;
13
+ }
14
+ declare function createSealedTransport(): SealedTransport & {
15
+ /** Completes the handshake against the host key from the QR. */
16
+ connect: (hostPublicKey: string) => Promise<string | undefined>;
17
+ /** Drops the channel, so everything falls back to pass-through. */
18
+ reset: () => void;
19
+ /** Selects the same-origin endpoint that carries sealed HTTP exchanges. */
20
+ relayRequestsThrough: (path: string) => void;
21
+ };
22
+ /**
23
+ * The page's one sealed channel.
24
+ *
25
+ * A module singleton rather than an instance per caller, because the cockpit
26
+ * and every plugin are bundled into the same page and all of them must share
27
+ * one pair of nonce counters. Two transports over one handshake would each
28
+ * start counting at zero and the receiver would reject the second as a replay.
29
+ *
30
+ * Inactive until a handshake completes, and inactive forever on the loopback
31
+ * listener, so `sealedTransport.fetch` is a plain `fetch` there.
32
+ */
33
+ declare const sealedTransport: SealedTransport & {
34
+ /** Completes the handshake against the host key from the QR. */
35
+ connect: (hostPublicKey: string) => Promise<string | undefined>;
36
+ /** Drops the channel, so everything falls back to pass-through. */
37
+ reset: () => void;
38
+ /** Selects the same-origin endpoint that carries sealed HTTP exchanges. */
39
+ relayRequestsThrough: (path: string) => void;
40
+ };
41
+ //#endregion
42
+ export { SealedTransport, createSealedTransport, sealedTransport };
43
+ //# sourceMappingURL=sealedTransport.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealedTransport.d.mts","names":[],"sources":["../../src/adapters/sealedTransport.ts"],"mappings":";UAqBiB;;EAEf;EACA,SAAS,eAAe;;EAExB,SAAS,eAAe;EACxB,WAAW,OAAO,aAAa,QAAQ;EACvC,WAAW,OAAO,aAAa,QAAQ;;EAEvC,MAAM,eAAe,OAAO,cAAc,QAAQ;EAClD;;iBA4Dc,yBAAyB;;EAEvC,UAAU,0BAA0B;;EAEpC;;EAEA,uBAAuB;;;;;;;;;;;;;cAmJZ,iBAAe;;EAvJjB,UAAC,0BAA0B;;EAE7B;;EAEe,uBAAC"}
@@ -0,0 +1,2 @@
1
+ import{describeSealedFailure as e}from"../types/sealedChannel.mjs";import{connectSealedChannel as t}from"./browserSealedChannel.mjs";import{createSerialQueue as n}from"../services/serialQueue.mjs";const r=new TextEncoder,i=new TextDecoder;function a(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t)}function o(e){let t=atob(e);return Uint8Array.from(t,e=>e.charCodeAt(0))}async function s(e){if(e!=null){if(typeof e==`string`)return r.encode(e);if(e instanceof URLSearchParams)return r.encode(e.toString());if(e instanceof Blob)return new Uint8Array(await e.arrayBuffer());if(e instanceof ArrayBuffer)return new Uint8Array(e);if(ArrayBuffer.isView(e))return new Uint8Array(e.buffer,e.byteOffset,e.byteLength);throw Error(`The sealed HTTP transport does not support this request body type.`)}}function c(e){if(!e.startsWith(`/`))throw Error(`A sealed HTTP request must use a root-relative URL.`);return e}function l(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.v===1&&typeof t.status==`number`&&Array.isArray(t.headers)&&t.headers.every(e=>Array.isArray(e)&&e.length===2&&e.every(e=>typeof e==`string`))&&typeof t.body==`string`}function u(){let u,d,f=n(),p=n(),m=n(),h,g=t=>{d=e(t)},_=async e=>{if(u===void 0)return;let t=await u.seal(e);return t.ok?JSON.stringify(t.envelope):g(t.failure)},v=async e=>{if(u===void 0)return;let t;try{t=JSON.parse(e)}catch{return g(`malformed`)}let n=await u.open(t);return n.ok?n.plaintext:g(n.failure)};return{active:()=>u!==void 0,lastFailure:()=>d,async connect(e){let n=await t(e);if(n===void 0){d=`This browser could not establish a sealed channel.`;return}return u=n.channel,d=void 0,n.clientPublicKey},relayRequestsThrough(e){h=e},reset(){u=void 0,d=void 0},async sealText(e){return u===void 0?e:await f.run(async()=>{let t=await _(r.encode(e));if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return t})},async openText(e){return u===void 0?e:await p.run(async()=>{let t=await v(e);return t===void 0?void 0:i.decode(t)})},async sealBinary(e){return u===void 0?e:await f.run(async()=>{let t=await _(e);if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return r.encode(t)})},async openBinary(e){return u===void 0?e:await p.run(async()=>await v(i.decode(e)))},async fetch(e,t){let n=h;return u===void 0||n===void 0?await fetch(e,t):await m.run(async()=>{let u=await s(t?.body),f={v:1,method:(t?.method??`GET`).toUpperCase(),target:c(e),headers:Array.from(new Headers(t?.headers).entries()),...u===void 0?{}:{body:a(u)}},p=await _(r.encode(JSON.stringify(f)));if(p===void 0)return new Response(JSON.stringify({error:d}),{status:502});let m=await fetch(n,{method:`POST`,credentials:`same-origin`,headers:{"content-type":`application/json`},body:p}),h=await v(await m.text());if(h===void 0)return new Response(JSON.stringify({error:d}),{status:502});let g;try{g=JSON.parse(i.decode(h))}catch{g=void 0}if(!l(g))return d=`The sealed HTTP response was malformed.`,new Response(JSON.stringify({error:d}),{status:502});let y=o(g.body);return new Response(g.status===204||g.status===205||g.status===304?null:y,{status:g.status,headers:g.headers})})}}}const d=u();export{u as createSealedTransport,d as sealedTransport};
2
+ //# sourceMappingURL=sealedTransport.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sealedTransport.mjs","names":[],"sources":["../../src/adapters/sealedTransport.ts"],"sourcesContent":["import { createSerialQueue } from '../services/serialQueue.ts';\nimport { describeSealedFailure, type SealedFailure } from '../types/sealedChannel.ts';\nimport { connectSealedChannel, type SealedChannel } from './browserSealedChannel.ts';\n\n/**\n * The one object the cockpit and every plugin use to talk over a sealed tunnel.\n *\n * Two things make this worth having rather than each caller reaching for the\n * channel directly. First, ordering: sealing advances a nonce counter and\n * opening enforces that it strictly increases, so both directions are pushed\n * through a serial queue and overlapping calls can never reorder into a\n * self-inflicted replay. Second, reach: a plugin that calls `fetch` itself\n * sends plaintext to whoever is relaying the tunnel, and the host cannot stop\n * it. Giving every caller one obvious helper is the only enforcement available.\n *\n * When no channel is established every method is a pass-through, so the same\n * code runs unchanged on the loopback listener where there is nothing to seal.\n */\n\nconst JSON_CONTENT_TYPE = 'application/json';\n\nexport interface SealedTransport {\n /** False on loopback and before the handshake completes, where everything passes through. */\n active(): boolean;\n sealText(text: string): Promise<string>;\n /** Undefined when the message could not be opened; `lastFailure` says why. */\n openText(text: string): Promise<string | undefined>;\n sealBinary(bytes: Uint8Array): Promise<Uint8Array>;\n openBinary(bytes: Uint8Array): Promise<Uint8Array | undefined>;\n /** Drop-in for `fetch`, relaying the complete request through the sealed channel when configured. */\n fetch(input: string, init?: RequestInit): Promise<Response>;\n lastFailure(): string | undefined;\n}\n\nconst encoder = new TextEncoder();\nconst decoder = new TextDecoder();\n\ninterface SealedHttpRequest {\n v: 1;\n method: string;\n target: string;\n headers: Array<[string, string]>;\n body?: string;\n}\n\ninterface SealedHttpResponse {\n v: 1;\n status: number;\n headers: Array<[string, string]>;\n body: string;\n}\n\nfunction encodeBase64(bytes: Uint8Array): string {\n let binary = '';\n for (const byte of bytes) binary += String.fromCharCode(byte);\n return btoa(binary);\n}\n\nfunction decodeBase64(value: string): Uint8Array {\n const binary = atob(value);\n return Uint8Array.from(binary, (character) => character.charCodeAt(0));\n}\n\nasync function bodyBytes(body: RequestInit['body']): Promise<Uint8Array | undefined> {\n if (body === undefined || body === null) return undefined;\n if (typeof body === 'string') return encoder.encode(body);\n if (body instanceof URLSearchParams) return encoder.encode(body.toString());\n if (body instanceof Blob) return new Uint8Array(await body.arrayBuffer());\n if (body instanceof ArrayBuffer) return new Uint8Array(body);\n if (ArrayBuffer.isView(body)) return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);\n throw new Error('The sealed HTTP transport does not support this request body type.');\n}\n\nfunction requestTarget(input: string): string {\n if (!input.startsWith('/')) throw new Error('A sealed HTTP request must use a root-relative URL.');\n return input;\n}\n\nfunction isSealedHttpResponse(value: unknown): value is SealedHttpResponse {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<SealedHttpResponse>;\n return (\n candidate.v === 1 &&\n typeof candidate.status === 'number' &&\n Array.isArray(candidate.headers) &&\n candidate.headers.every(\n (header) => Array.isArray(header) && header.length === 2 && header.every((part) => typeof part === 'string'),\n ) &&\n typeof candidate.body === 'string'\n );\n}\nexport function createSealedTransport(): SealedTransport & {\n /** Completes the handshake against the host key from the QR. */\n connect: (hostPublicKey: string) => Promise<string | undefined>;\n /** Drops the channel, so everything falls back to pass-through. */\n reset: () => void;\n /** Selects the same-origin endpoint that carries sealed HTTP exchanges. */\n relayRequestsThrough: (path: string) => void;\n} {\n let channel: SealedChannel | undefined;\n let failure: string | undefined;\n const outbound = createSerialQueue();\n const inbound = createSerialQueue();\n const exchange = createSerialQueue();\n let relayPath: string | undefined;\n const note = (reason: SealedFailure): undefined => {\n // A decryption failure otherwise shows up as a blank page with no cause, so\n // every one of them names itself.\n failure = describeSealedFailure(reason);\n return undefined;\n };\n\n const sealBytes = async (bytes: Uint8Array): Promise<string | undefined> => {\n if (channel === undefined) return undefined;\n const sealed = await channel.seal(bytes);\n if (!sealed.ok) return note(sealed.failure);\n return JSON.stringify(sealed.envelope);\n };\n\n const openBytes = async (text: string): Promise<Uint8Array | undefined> => {\n if (channel === undefined) return undefined;\n let envelope: unknown;\n try {\n envelope = JSON.parse(text);\n } catch {\n return note('malformed');\n }\n const opened = await channel.open(envelope);\n if (!opened.ok) return note(opened.failure);\n return opened.plaintext;\n };\n\n return {\n active: () => channel !== undefined,\n lastFailure: () => failure,\n\n async connect(hostPublicKey) {\n const connected = await connectSealedChannel(hostPublicKey);\n if (connected === undefined) {\n failure = 'This browser could not establish a sealed channel.';\n return undefined;\n }\n channel = connected.channel;\n failure = undefined;\n return connected.clientPublicKey;\n },\n\n relayRequestsThrough(path) {\n relayPath = path;\n },\n\n reset() {\n channel = undefined;\n failure = undefined;\n },\n\n async sealText(text) {\n if (channel === undefined) return text;\n return await outbound.run(async () => {\n const sealed = await sealBytes(encoder.encode(text));\n if (sealed === undefined) throw new Error(failure ?? 'The active sealed channel could not encrypt a message.');\n return sealed;\n });\n },\n\n async openText(text) {\n if (channel === undefined) return text;\n return await inbound.run(async () => {\n const opened = await openBytes(text);\n return opened === undefined ? undefined : decoder.decode(opened);\n });\n },\n\n async sealBinary(bytes) {\n if (channel === undefined) return bytes;\n return await outbound.run(async () => {\n const sealed = await sealBytes(bytes);\n if (sealed === undefined) throw new Error(failure ?? 'The active sealed channel could not encrypt a message.');\n return encoder.encode(sealed);\n });\n },\n\n async openBinary(bytes) {\n if (channel === undefined) return bytes;\n return await inbound.run(async () => await openBytes(decoder.decode(bytes)));\n },\n\n async fetch(input, init) {\n const gateway = relayPath;\n if (channel === undefined || gateway === undefined) return await fetch(input, init);\n return await exchange.run(async () => {\n const bytes = await bodyBytes(init?.body);\n const request: SealedHttpRequest = {\n v: 1,\n method: (init?.method ?? 'GET').toUpperCase(),\n target: requestTarget(input),\n headers: Array.from(new Headers(init?.headers).entries()),\n ...(bytes === undefined ? {} : { body: encodeBase64(bytes) }),\n };\n const sealed = await sealBytes(encoder.encode(JSON.stringify(request)));\n if (sealed === undefined) {\n return new Response(JSON.stringify({ error: failure }), { status: 502 });\n }\n const relayResponse = await fetch(gateway, {\n method: 'POST',\n credentials: 'same-origin',\n headers: { 'content-type': JSON_CONTENT_TYPE },\n body: sealed,\n });\n const opened = await openBytes(await relayResponse.text());\n if (opened === undefined) {\n return new Response(JSON.stringify({ error: failure }), { status: 502 });\n }\n let decoded: unknown;\n try {\n decoded = JSON.parse(decoder.decode(opened));\n } catch {\n decoded = undefined;\n }\n if (!isSealedHttpResponse(decoded)) {\n failure = 'The sealed HTTP response was malformed.';\n return new Response(JSON.stringify({ error: failure }), { status: 502 });\n }\n const body = decodeBase64(decoded.body);\n return new Response(decoded.status === 204 || decoded.status === 205 || decoded.status === 304 ? null : body, {\n status: decoded.status,\n headers: decoded.headers,\n });\n });\n },\n };\n}\n\n/**\n * The page's one sealed channel.\n *\n * A module singleton rather than an instance per caller, because the cockpit\n * and every plugin are bundled into the same page and all of them must share\n * one pair of nonce counters. Two transports over one handshake would each\n * start counting at zero and the receiver would reject the second as a replay.\n *\n * Inactive until a handshake completes, and inactive forever on the loopback\n * listener, so `sealedTransport.fetch` is a plain `fetch` there.\n */\nexport const sealedTransport = createSealedTransport();\n"],"mappings":"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"}
@@ -0,0 +1 @@
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;
@@ -0,0 +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";
3
+ import { OpenResult, SealResult, SealedChannel, channelFromSecret, connectSealedChannel } from "./adapters/browserSealedChannel.cjs";
4
+ import { SealedTransport, createSealedTransport, sealedTransport } from "./adapters/sealedTransport.cjs";
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 };
@@ -0,0 +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";
3
+ import { OpenResult, SealResult, SealedChannel, channelFromSecret, connectSealedChannel } from "./adapters/browserSealedChannel.mjs";
4
+ import { SealedTransport, createSealedTransport, sealedTransport } from "./adapters/sealedTransport.mjs";
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 };
@@ -0,0 +1 @@
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 ADDED
@@ -0,0 +1 @@
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;
@@ -0,0 +1,4 @@
1
+ import { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, assetFor, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest } from "./types/bundleManifest.cjs";
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";
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, assetFor, canonicalManifest, createSerialQueue, describeSealedFailure, digestFor, infoFor, isBundleManifest, isSealedEnvelope, isSignedBundleManifest };
@@ -0,0 +1,4 @@
1
+ import { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, assetFor, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest } from "./types/bundleManifest.mjs";
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";
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, assetFor, canonicalManifest, createSerialQueue, describeSealedFailure, digestFor, infoFor, isBundleManifest, isSealedEnvelope, isSignedBundleManifest };
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
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};
package/dist/node.cjs ADDED
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./adapters/nodeSealedChannel.cjs"),t=require("./adapters/bundleSigner.cjs");exports.createBundleSigner=t.createBundleSigner,exports.createHostHandshake=e.createHostHandshake,exports.publicKeyOf=t.publicKeyOf;
@@ -0,0 +1,3 @@
1
+ import { HostHandshake, OpenResult, SealResult, SealedChannel, createHostHandshake } from "./adapters/nodeSealedChannel.cjs";
2
+ import { BundleSigner, createBundleSigner, publicKeyOf } from "./adapters/bundleSigner.cjs";
3
+ export { type BundleSigner, type HostHandshake, type OpenResult, type SealResult, type SealedChannel, createBundleSigner, createHostHandshake, publicKeyOf };
@@ -0,0 +1,3 @@
1
+ import { HostHandshake, OpenResult, SealResult, SealedChannel, createHostHandshake } from "./adapters/nodeSealedChannel.mjs";
2
+ import { BundleSigner, createBundleSigner, publicKeyOf } from "./adapters/bundleSigner.mjs";
3
+ export { type BundleSigner, type HostHandshake, type OpenResult, type SealResult, type SealedChannel, createBundleSigner, createHostHandshake, publicKeyOf };
package/dist/node.mjs ADDED
@@ -0,0 +1 @@
1
+ import{createHostHandshake as e}from"./adapters/nodeSealedChannel.mjs";import{createBundleSigner as t,publicKeyOf as n}from"./adapters/bundleSigner.mjs";export{t as createBundleSigner,e as createHostHandshake,n as publicKeyOf};
@@ -0,0 +1,2 @@
1
+ function e(){let e=Promise.resolve();return{run(t){let n=e.then(t,t);return e=n.catch(()=>void 0),n}}}exports.createSerialQueue=e;
2
+ //# sourceMappingURL=serialQueue.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialQueue.cjs","names":[],"sources":["../../src/services/serialQueue.ts"],"sourcesContent":["/**\n * Runs async work strictly one at a time, in the order it was handed over.\n *\n * The sealed channel needs this on both directions and it is not an\n * optimisation. Sealing increments a nonce counter and opening enforces that\n * the counter strictly increases, so two overlapping operations are a\n * correctness bug rather than a race worth tolerating: two concurrent seals can\n * resolve out of order and reach the wire with descending counters, and two\n * concurrent opens can advance the high-water mark past a message that has not\n * been read yet, which the receiver then rejects as a replay.\n *\n * Deliberately not a general work pool. A pool with any concurrency above one\n * reintroduces exactly the reordering this exists to prevent.\n */\nexport interface SerialQueue {\n /** Resolves with the task's result once every task queued before it has settled. */\n run<T>(task: () => Promise<T>): Promise<T>;\n}\n\nexport function createSerialQueue(): SerialQueue {\n /** The tail of the chain. Rejections are absorbed so one failure cannot stall the rest. */\n let tail: Promise<unknown> = Promise.resolve();\n\n return {\n run<T>(task: () => Promise<T>): Promise<T> {\n const result = tail.then(task, task);\n tail = result.catch(() => undefined);\n return result;\n },\n };\n}\n"],"mappings":"AAmBA,SAAgB,GAAiC,CAE/C,IAAI,EAAyB,QAAQ,QAAQ,EAE7C,MAAO,CACL,IAAO,EAAoC,CACzC,IAAM,EAAS,EAAK,KAAK,EAAM,CAAI,EAEnC,MADA,GAAO,EAAO,UAAY,IAAA,EAAS,EAC5B,CACT,CACF,CACF"}
@@ -0,0 +1,23 @@
1
+ //#region src/services/serialQueue.d.ts
2
+ /**
3
+ * Runs async work strictly one at a time, in the order it was handed over.
4
+ *
5
+ * The sealed channel needs this on both directions and it is not an
6
+ * optimisation. Sealing increments a nonce counter and opening enforces that
7
+ * the counter strictly increases, so two overlapping operations are a
8
+ * correctness bug rather than a race worth tolerating: two concurrent seals can
9
+ * resolve out of order and reach the wire with descending counters, and two
10
+ * concurrent opens can advance the high-water mark past a message that has not
11
+ * been read yet, which the receiver then rejects as a replay.
12
+ *
13
+ * Deliberately not a general work pool. A pool with any concurrency above one
14
+ * reintroduces exactly the reordering this exists to prevent.
15
+ */
16
+ interface SerialQueue {
17
+ /** Resolves with the task's result once every task queued before it has settled. */
18
+ run<T>(task: () => Promise<T>): Promise<T>;
19
+ }
20
+ declare function createSerialQueue(): SerialQueue;
21
+ //#endregion
22
+ export { SerialQueue, createSerialQueue };
23
+ //# sourceMappingURL=serialQueue.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialQueue.d.cts","names":[],"sources":["../../src/services/serialQueue.ts"],"mappings":";;;;;;;;;;;;;;;UAciB;;EAEf,IAAI,GAAG,YAAY,QAAQ,KAAK,QAAQ;;iBAG1B,qBAAqB"}
@@ -0,0 +1,23 @@
1
+ //#region src/services/serialQueue.d.ts
2
+ /**
3
+ * Runs async work strictly one at a time, in the order it was handed over.
4
+ *
5
+ * The sealed channel needs this on both directions and it is not an
6
+ * optimisation. Sealing increments a nonce counter and opening enforces that
7
+ * the counter strictly increases, so two overlapping operations are a
8
+ * correctness bug rather than a race worth tolerating: two concurrent seals can
9
+ * resolve out of order and reach the wire with descending counters, and two
10
+ * concurrent opens can advance the high-water mark past a message that has not
11
+ * been read yet, which the receiver then rejects as a replay.
12
+ *
13
+ * Deliberately not a general work pool. A pool with any concurrency above one
14
+ * reintroduces exactly the reordering this exists to prevent.
15
+ */
16
+ interface SerialQueue {
17
+ /** Resolves with the task's result once every task queued before it has settled. */
18
+ run<T>(task: () => Promise<T>): Promise<T>;
19
+ }
20
+ declare function createSerialQueue(): SerialQueue;
21
+ //#endregion
22
+ export { SerialQueue, createSerialQueue };
23
+ //# sourceMappingURL=serialQueue.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialQueue.d.mts","names":[],"sources":["../../src/services/serialQueue.ts"],"mappings":";;;;;;;;;;;;;;;UAciB;;EAEf,IAAI,GAAG,YAAY,QAAQ,KAAK,QAAQ;;iBAG1B,qBAAqB"}
@@ -0,0 +1,2 @@
1
+ function e(){let e=Promise.resolve();return{run(t){let n=e.then(t,t);return e=n.catch(()=>void 0),n}}}export{e as createSerialQueue};
2
+ //# sourceMappingURL=serialQueue.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialQueue.mjs","names":[],"sources":["../../src/services/serialQueue.ts"],"sourcesContent":["/**\n * Runs async work strictly one at a time, in the order it was handed over.\n *\n * The sealed channel needs this on both directions and it is not an\n * optimisation. Sealing increments a nonce counter and opening enforces that\n * the counter strictly increases, so two overlapping operations are a\n * correctness bug rather than a race worth tolerating: two concurrent seals can\n * resolve out of order and reach the wire with descending counters, and two\n * concurrent opens can advance the high-water mark past a message that has not\n * been read yet, which the receiver then rejects as a replay.\n *\n * Deliberately not a general work pool. A pool with any concurrency above one\n * reintroduces exactly the reordering this exists to prevent.\n */\nexport interface SerialQueue {\n /** Resolves with the task's result once every task queued before it has settled. */\n run<T>(task: () => Promise<T>): Promise<T>;\n}\n\nexport function createSerialQueue(): SerialQueue {\n /** The tail of the chain. Rejections are absorbed so one failure cannot stall the rest. */\n let tail: Promise<unknown> = Promise.resolve();\n\n return {\n run<T>(task: () => Promise<T>): Promise<T> {\n const result = tail.then(task, task);\n tail = result.catch(() => undefined);\n return result;\n },\n };\n}\n"],"mappings":"AAmBA,SAAgB,GAAiC,CAE/C,IAAI,EAAyB,QAAQ,QAAQ,EAE7C,MAAO,CACL,IAAO,EAAoC,CACzC,IAAM,EAAS,EAAK,KAAK,EAAM,CAAI,EAEnC,MADA,GAAO,EAAO,UAAY,IAAA,EAAS,EAC5B,CACT,CACF,CACF"}
@@ -0,0 +1,3 @@
1
+ const e=/^[0-9a-f]{64}$/u,t=/^[A-Za-z0-9_-]+$/u,n=/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/u;function r(e,t){let n=Object.keys(e).sort();return n.length===t.length&&n.every((e,n)=>e===t[n])}function i(e,t){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=t}function a(e){return typeof e!=`string`||e.length<2||!e.startsWith(`/`)||e.includes(`\\`)||e.includes(`\0`)||e.includes(`?`)||e.includes(`#`)||/[^\x21-\x7e]/u.test(e)||e.includes(`//`)||/%(?![0-9a-f]{2})/iu.test(e)||/%(?:2e|2f|5c)/iu.test(e)?!1:e.split(`/`).slice(1).every(e=>e!==``&&e!==`.`&&e!==`..`)}function o(e){let t=[...e.assets].sort((e,t)=>e.path<t.path?-1:+(e.path>t.path)).map(e=>`${e.path}\0${e.sha256}\0${String(e.byteLength)}\0${e.contentType}`);return[String(e.version),String(e.revision),String(e.builtAt),...t].join(`
2
+ `)}function s(t){if(typeof t!=`object`||!t||!r(t,[`assets`,`builtAt`,`revision`,`version`]))return!1;let o=t;if(o.version!==2||!i(o.revision,1)||!i(o.builtAt,0)||!Array.isArray(o.assets)||o.assets.length===0)return!1;let s=new Set;for(let t of o.assets){if(typeof t!=`object`||!t||!r(t,[`byteLength`,`contentType`,`path`,`sha256`]))return!1;let o=t;if(!a(o.path)||s.has(o.path)||typeof o.sha256!=`string`||!e.test(o.sha256)||!i(o.byteLength,0)||typeof o.contentType!=`string`||!n.test(o.contentType))return!1;s.add(o.path)}return s.has(`/index.html`)}function c(e){if(typeof e!=`object`||!e||!r(e,[`manifest`,`publicKey`,`signature`]))return!1;let n=e;return s(n.manifest)&&typeof n.signature==`string`&&n.signature.length===86&&t.test(n.signature)&&typeof n.publicKey==`string`&&n.publicKey.length<=512&&t.test(n.publicKey)}function l(e,t){return e.assets.find(e=>e.path===t)}function u(e,t){return l(e,t)?.sha256}exports.BUNDLE_MANIFEST_ROUTE=`/bundle-manifest.json`,exports.BUNDLE_MANIFEST_VERSION=2,exports.assetFor=l,exports.canonicalManifest=o,exports.digestFor=u,exports.isBundleManifest=s,exports.isSignedBundleManifest=c;
3
+ //# sourceMappingURL=bundleManifest.cjs.map
@@ -0,0 +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. 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":"AAQA,MAGM,EAAS,kBACT,EAAY,oBACZ,EAAe,4DA6BrB,SAAS,EAAa,EAAe,EAAsC,CACzE,IAAM,EAAO,OAAO,KAAK,CAAK,CAAC,CAAC,KAAK,EACrC,OAAO,EAAK,SAAW,EAAS,QAAU,EAAK,OAAO,EAAK,IAAU,IAAQ,EAAS,EAAM,CAC9F,CAEA,SAAS,EAAc,EAAgB,EAAkC,CACvE,OAAO,OAAO,GAAU,UAAY,OAAO,cAAc,CAAK,GAAK,GAAS,CAC9E,CAEA,SAAS,EAAgB,EAAiC,CAKxD,OAJI,OAAO,GAAU,UAAY,EAAM,OAAS,GAAK,CAAC,EAAM,WAAW,GAAG,GACtE,EAAM,SAAS,IAAI,GAAK,EAAM,SAAS,IAAI,GAAK,EAAM,SAAS,GAAG,GAAK,EAAM,SAAS,GAAG,GACzF,gBAAgB,KAAK,CAAK,GAAK,EAAM,SAAS,IAAI,GAAK,qBAAqB,KAAK,CAAK,GACtF,kBAAkB,KAAK,CAAK,EAAU,GACnC,EACJ,MAAM,GAAG,CAAC,CACV,MAAM,CAAC,CAAC,CACR,MAAO,GAAY,IAAY,IAAM,IAAY,KAAO,IAAY,IAAI,CAC7E,CAGA,SAAgB,EAAkB,EAAkC,CAIlE,IAAM,EAHS,CAAC,GAAG,EAAS,MAAM,CAAC,CAAC,MAAM,EAAM,IAC9C,EAAK,KAAO,EAAM,KAAO,GAAK,IAAK,KAAO,EAAM,KAE/B,CAAC,CAAC,IAClB,GAAU,GAAG,EAAM,KAAK,IAAI,EAAM,OAAO,IAAI,OAAO,EAAM,UAAU,EAAE,IAAI,EAAM,aACnF,EACA,MAAO,CAAC,OAAO,EAAS,OAAO,EAAG,OAAO,EAAS,QAAQ,EAAG,OAAO,EAAS,OAAO,EAAG,GAAG,CAAK,CAAC,CAAC,KAAK;CAAI,CAC5G,CAEA,SAAgB,EAAiB,EAAyC,CAExE,GADI,OAAO,GAAU,WAAY,GAC7B,CAAC,EAAa,EAAO,CAAC,SAAU,UAAW,WAAY,SAAS,CAAC,EAAG,MAAO,GAC/E,IAAM,EAAY,EAGlB,GAFI,EAAU,UAAA,GACV,CAAC,EAAc,EAAU,SAAU,CAAC,GAAK,CAAC,EAAc,EAAU,QAAS,CAAC,GAC5E,CAAC,MAAM,QAAQ,EAAU,MAAM,GAAK,EAAU,OAAO,SAAW,EAAG,MAAO,GAE9E,IAAM,EAAQ,IAAI,IAClB,IAAK,IAAM,KAAc,EAAU,OAAQ,CAEzC,GADI,OAAO,GAAe,WAAY,GAClC,CAAC,EAAa,EAAY,CAAC,aAAc,cAAe,OAAQ,QAAQ,CAAC,EAAG,MAAO,GACvF,IAAM,EAAQ,EAId,GAHI,CAAC,EAAgB,EAAM,IAAI,GAAK,EAAM,IAAI,EAAM,IAAI,GACpD,OAAO,EAAM,QAAW,UAAY,CAAC,EAAO,KAAK,EAAM,MAAM,GAC7D,CAAC,EAAc,EAAM,WAAY,CAAC,GAClC,OAAO,EAAM,aAAgB,UAAY,CAAC,EAAa,KAAK,EAAM,WAAW,EAAG,MAAO,GAC3F,EAAM,IAAI,EAAM,IAAI,CACtB,CACA,OAAO,EAAM,IAAI,aAAa,CAChC,CAEA,SAAgB,EAAuB,EAA+C,CAEpF,GADI,OAAO,GAAU,WAAY,GAC7B,CAAC,EAAa,EAAO,CAAC,WAAY,YAAa,WAAW,CAAC,EAAG,MAAO,GACzE,IAAM,EAAY,EAClB,OACE,EAAiB,EAAU,QAAQ,GACnC,OAAO,EAAU,WAAc,UAC/B,EAAU,UAAU,SAAW,IAC/B,EAAU,KAAK,EAAU,SAAS,GAClC,OAAO,EAAU,WAAc,UAC/B,EAAU,UAAU,QAAU,KAC9B,EAAU,KAAK,EAAU,SAAS,CAEtC,CAGA,SAAgB,EAAS,EAA0B,EAAuC,CACxF,OAAO,EAAS,OAAO,KAAM,GAAU,EAAM,OAAS,CAAI,CAC5D,CAGA,SAAgB,EAAU,EAA0B,EAAkC,CACpF,OAAO,EAAS,EAAU,CAAI,CAAC,EAAE,MACnC"}
@@ -0,0 +1,45 @@
1
+ //#region src/types/bundleManifest.d.ts
2
+ /**
3
+ * A signed description of cockpit assets for an independently trusted verifier.
4
+ *
5
+ * A manifest does not let a browser SPA defend itself against the server or TLS
6
+ * edge that supplied its executable code. Consumers need a separately trusted
7
+ * bootstrap and key before these signatures become a delivery guarantee.
8
+ */
9
+ declare const BUNDLE_MANIFEST_ROUTE = "/bundle-manifest.json";
10
+ declare const BUNDLE_MANIFEST_VERSION = 2;
11
+ interface BundleAsset {
12
+ /** Normalized absolute URL path, without a query or fragment. */
13
+ path: string;
14
+ /** Lowercase hex SHA-256 of the served bytes. */
15
+ sha256: string;
16
+ byteLength: number;
17
+ /** MIME type of the response, without parameters. */
18
+ contentType: string;
19
+ }
20
+ interface BundleManifest {
21
+ version: typeof BUNDLE_MANIFEST_VERSION;
22
+ /** Monotonically increasing persistent signer revision. */
23
+ revision: number;
24
+ /** Unix time in milliseconds. */
25
+ builtAt: number;
26
+ assets: BundleAsset[];
27
+ }
28
+ interface SignedBundleManifest {
29
+ manifest: BundleManifest;
30
+ /** Base64url IEEE P1363 ECDSA P-256 signature over the canonical serialization. */
31
+ signature: string;
32
+ /** Base64url SPKI of the signing key. Trust must be established out of band. */
33
+ publicKey: string;
34
+ }
35
+ /** The exact bytes signed by Node and verified by WebCrypto. */
36
+ declare function canonicalManifest(manifest: BundleManifest): string;
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;
41
+ /** The recorded digest for one asset, or undefined if the manifest does not cover it. */
42
+ declare function digestFor(manifest: BundleManifest, path: string): string | undefined;
43
+ //#endregion
44
+ export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, assetFor, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest };
45
+ //# sourceMappingURL=bundleManifest.d.cts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,45 @@
1
+ //#region src/types/bundleManifest.d.ts
2
+ /**
3
+ * A signed description of cockpit assets for an independently trusted verifier.
4
+ *
5
+ * A manifest does not let a browser SPA defend itself against the server or TLS
6
+ * edge that supplied its executable code. Consumers need a separately trusted
7
+ * bootstrap and key before these signatures become a delivery guarantee.
8
+ */
9
+ declare const BUNDLE_MANIFEST_ROUTE = "/bundle-manifest.json";
10
+ declare const BUNDLE_MANIFEST_VERSION = 2;
11
+ interface BundleAsset {
12
+ /** Normalized absolute URL path, without a query or fragment. */
13
+ path: string;
14
+ /** Lowercase hex SHA-256 of the served bytes. */
15
+ sha256: string;
16
+ byteLength: number;
17
+ /** MIME type of the response, without parameters. */
18
+ contentType: string;
19
+ }
20
+ interface BundleManifest {
21
+ version: typeof BUNDLE_MANIFEST_VERSION;
22
+ /** Monotonically increasing persistent signer revision. */
23
+ revision: number;
24
+ /** Unix time in milliseconds. */
25
+ builtAt: number;
26
+ assets: BundleAsset[];
27
+ }
28
+ interface SignedBundleManifest {
29
+ manifest: BundleManifest;
30
+ /** Base64url IEEE P1363 ECDSA P-256 signature over the canonical serialization. */
31
+ signature: string;
32
+ /** Base64url SPKI of the signing key. Trust must be established out of band. */
33
+ publicKey: string;
34
+ }
35
+ /** The exact bytes signed by Node and verified by WebCrypto. */
36
+ declare function canonicalManifest(manifest: BundleManifest): string;
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;
41
+ /** The recorded digest for one asset, or undefined if the manifest does not cover it. */
42
+ declare function digestFor(manifest: BundleManifest, path: string): string | undefined;
43
+ //#endregion
44
+ export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, assetFor, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest };
45
+ //# sourceMappingURL=bundleManifest.d.mts.map
@@ -0,0 +1 @@
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"}
@@ -0,0 +1,3 @@
1
+ const e=`/bundle-manifest.json`,t=2,n=/^[0-9a-f]{64}$/u,r=/^[A-Za-z0-9_-]+$/u,i=/^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/u;function a(e,t){let n=Object.keys(e).sort();return n.length===t.length&&n.every((e,n)=>e===t[n])}function o(e,t){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=t}function s(e){return typeof e!=`string`||e.length<2||!e.startsWith(`/`)||e.includes(`\\`)||e.includes(`\0`)||e.includes(`?`)||e.includes(`#`)||/[^\x21-\x7e]/u.test(e)||e.includes(`//`)||/%(?![0-9a-f]{2})/iu.test(e)||/%(?:2e|2f|5c)/iu.test(e)?!1:e.split(`/`).slice(1).every(e=>e!==``&&e!==`.`&&e!==`..`)}function c(e){let t=[...e.assets].sort((e,t)=>e.path<t.path?-1:+(e.path>t.path)).map(e=>`${e.path}\0${e.sha256}\0${String(e.byteLength)}\0${e.contentType}`);return[String(e.version),String(e.revision),String(e.builtAt),...t].join(`
2
+ `)}function l(e){if(typeof e!=`object`||!e||!a(e,[`assets`,`builtAt`,`revision`,`version`]))return!1;let t=e;if(t.version!==2||!o(t.revision,1)||!o(t.builtAt,0)||!Array.isArray(t.assets)||t.assets.length===0)return!1;let r=new Set;for(let e of t.assets){if(typeof e!=`object`||!e||!a(e,[`byteLength`,`contentType`,`path`,`sha256`]))return!1;let t=e;if(!s(t.path)||r.has(t.path)||typeof t.sha256!=`string`||!n.test(t.sha256)||!o(t.byteLength,0)||typeof t.contentType!=`string`||!i.test(t.contentType))return!1;r.add(t.path)}return r.has(`/index.html`)}function u(e){if(typeof e!=`object`||!e||!a(e,[`manifest`,`publicKey`,`signature`]))return!1;let t=e;return l(t.manifest)&&typeof t.signature==`string`&&t.signature.length===86&&r.test(t.signature)&&typeof t.publicKey==`string`&&t.publicKey.length<=512&&r.test(t.publicKey)}function d(e,t){return e.assets.find(e=>e.path===t)}function f(e,t){return d(e,t)?.sha256}export{e as BUNDLE_MANIFEST_ROUTE,t as BUNDLE_MANIFEST_VERSION,d as assetFor,c as canonicalManifest,f as digestFor,l as isBundleManifest,u as isSignedBundleManifest};
3
+ //# sourceMappingURL=bundleManifest.mjs.map