@agimon-ai/doompi-web-security 0.0.1-alpha.2
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/LICENSE +22 -0
- package/README.md +85 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/adapters/browserSealedChannel.cjs +2 -0
- package/dist/adapters/browserSealedChannel.cjs.map +1 -0
- package/dist/adapters/browserSealedChannel.d.cts +36 -0
- package/dist/adapters/browserSealedChannel.d.cts.map +1 -0
- package/dist/adapters/browserSealedChannel.d.mts +36 -0
- package/dist/adapters/browserSealedChannel.d.mts.map +1 -0
- package/dist/adapters/browserSealedChannel.mjs +2 -0
- package/dist/adapters/browserSealedChannel.mjs.map +1 -0
- package/dist/adapters/bundleSigner.cjs +2 -0
- package/dist/adapters/bundleSigner.cjs.map +1 -0
- package/dist/adapters/bundleSigner.d.cts +14 -0
- package/dist/adapters/bundleSigner.d.cts.map +1 -0
- package/dist/adapters/bundleSigner.d.mts +14 -0
- package/dist/adapters/bundleSigner.d.mts.map +1 -0
- package/dist/adapters/bundleSigner.mjs +2 -0
- package/dist/adapters/bundleSigner.mjs.map +1 -0
- package/dist/adapters/nodeSealedChannel.cjs +2 -0
- package/dist/adapters/nodeSealedChannel.cjs.map +1 -0
- package/dist/adapters/nodeSealedChannel.d.cts +40 -0
- package/dist/adapters/nodeSealedChannel.d.cts.map +1 -0
- package/dist/adapters/nodeSealedChannel.d.mts +40 -0
- package/dist/adapters/nodeSealedChannel.d.mts.map +1 -0
- package/dist/adapters/nodeSealedChannel.mjs +2 -0
- package/dist/adapters/nodeSealedChannel.mjs.map +1 -0
- package/dist/adapters/sealedTransport.cjs +2 -0
- package/dist/adapters/sealedTransport.cjs.map +1 -0
- package/dist/adapters/sealedTransport.d.cts +43 -0
- package/dist/adapters/sealedTransport.d.cts.map +1 -0
- package/dist/adapters/sealedTransport.d.mts +43 -0
- package/dist/adapters/sealedTransport.d.mts.map +1 -0
- package/dist/adapters/sealedTransport.mjs +2 -0
- package/dist/adapters/sealedTransport.mjs.map +1 -0
- package/dist/browser.cjs +1 -0
- package/dist/browser.d.cts +3 -0
- package/dist/browser.d.mts +3 -0
- package/dist/browser.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +4 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.mjs +1 -0
- package/dist/node.cjs +1 -0
- package/dist/node.d.cts +3 -0
- package/dist/node.d.mts +3 -0
- package/dist/node.mjs +1 -0
- package/dist/services/serialQueue.cjs +2 -0
- package/dist/services/serialQueue.cjs.map +1 -0
- package/dist/services/serialQueue.d.cts +23 -0
- package/dist/services/serialQueue.d.cts.map +1 -0
- package/dist/services/serialQueue.d.mts +23 -0
- package/dist/services/serialQueue.d.mts.map +1 -0
- package/dist/services/serialQueue.mjs +2 -0
- package/dist/services/serialQueue.mjs.map +1 -0
- package/dist/types/bundleManifest.cjs +0 -0
- package/dist/types/bundleManifest.cjs.map +1 -0
- package/dist/types/bundleManifest.d.cts +48 -0
- package/dist/types/bundleManifest.d.cts.map +1 -0
- package/dist/types/bundleManifest.d.mts +48 -0
- package/dist/types/bundleManifest.d.mts.map +1 -0
- package/dist/types/bundleManifest.mjs +0 -0
- package/dist/types/bundleManifest.mjs.map +1 -0
- package/dist/types/sealedChannel.cjs +2 -0
- package/dist/types/sealedChannel.cjs.map +1 -0
- package/dist/types/sealedChannel.d.cts +77 -0
- package/dist/types/sealedChannel.d.cts.map +1 -0
- package/dist/types/sealedChannel.d.mts +77 -0
- package/dist/types/sealedChannel.d.mts.map +1 -0
- package/dist/types/sealedChannel.mjs +2 -0
- package/dist/types/sealedChannel.mjs.map +1 -0
- package/llms.txt +9 -0
- package/package.json +76 -0
|
@@ -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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"}
|
package/dist/browser.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./adapters/browserSealedChannel.cjs"),t=require("./adapters/sealedTransport.cjs");exports.channelFromSecret=e.channelFromSecret,exports.connectSealedChannel=e.connectSealedChannel,exports.createSealedTransport=t.createSealedTransport,exports.sealedTransport=t.sealedTransport;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { OpenResult, SealResult, SealedChannel, channelFromSecret, connectSealedChannel } from "./adapters/browserSealedChannel.cjs";
|
|
2
|
+
import { SealedTransport, createSealedTransport, sealedTransport } from "./adapters/sealedTransport.cjs";
|
|
3
|
+
export { type OpenResult, type SealResult, type SealedChannel, type SealedTransport, channelFromSecret, connectSealedChannel, createSealedTransport, sealedTransport };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { OpenResult, SealResult, SealedChannel, channelFromSecret, connectSealedChannel } from "./adapters/browserSealedChannel.mjs";
|
|
2
|
+
import { SealedTransport, createSealedTransport, sealedTransport } from "./adapters/sealedTransport.mjs";
|
|
3
|
+
export { type OpenResult, type SealResult, type SealedChannel, type SealedTransport, channelFromSecret, connectSealedChannel, createSealedTransport, sealedTransport };
|
package/dist/browser.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{channelFromSecret as e,connectSealedChannel as t}from"./adapters/browserSealedChannel.mjs";import{createSealedTransport as n,sealedTransport as r}from"./adapters/sealedTransport.mjs";export{e as channelFromSecret,t as connectSealedChannel,n as createSealedTransport,r as sealedTransport};
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types/sealedChannel.cjs"),t=require("./services/serialQueue.cjs"),n=require("./types/bundleManifest.cjs");exports.BUNDLE_MANIFEST_ROUTE=n.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=n.BUNDLE_MANIFEST_VERSION,exports.CLIENT_TO_SERVER_INFO=e.CLIENT_TO_SERVER_INFO,exports.MAX_MESSAGES_PER_KEY=e.MAX_MESSAGES_PER_KEY,exports.NONCE_BYTES=e.NONCE_BYTES,exports.NONCE_COUNTER_BYTES=e.NONCE_COUNTER_BYTES,exports.NONCE_PREFIX_BYTES=e.NONCE_PREFIX_BYTES,exports.SEALED_BODY_HEADER=e.SEALED_BODY_HEADER,exports.SEALED_CLIENT_KEY_HEADER=e.SEALED_CLIENT_KEY_HEADER,exports.SEALED_KEY_PARAM=e.SEALED_KEY_PARAM,exports.SEALED_VERSION=e.SEALED_VERSION,exports.SERVER_TO_CLIENT_INFO=e.SERVER_TO_CLIENT_INFO,exports.canonicalManifest=n.canonicalManifest,exports.createSerialQueue=t.createSerialQueue,exports.describeSealedFailure=e.describeSealedFailure,exports.digestFor=n.digestFor,exports.infoFor=e.infoFor,exports.isBundleManifest=n.isBundleManifest,exports.isSealedEnvelope=e.isSealedEnvelope;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
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
|
+
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 };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
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
|
+
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 };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CLIENT_TO_SERVER_INFO as e,MAX_MESSAGES_PER_KEY as t,NONCE_BYTES as n,NONCE_COUNTER_BYTES as r,NONCE_PREFIX_BYTES as i,SEALED_BODY_HEADER as a,SEALED_CLIENT_KEY_HEADER as o,SEALED_KEY_PARAM as s,SEALED_VERSION as c,SERVER_TO_CLIENT_INFO as l,describeSealedFailure as u,infoFor as d,isSealedEnvelope as f}from"./types/sealedChannel.mjs";import{createSerialQueue as p}from"./services/serialQueue.mjs";import{BUNDLE_MANIFEST_ROUTE as m,BUNDLE_MANIFEST_VERSION as h,canonicalManifest as g,digestFor as _,isBundleManifest as v}from"./types/bundleManifest.mjs";export{m as BUNDLE_MANIFEST_ROUTE,h as BUNDLE_MANIFEST_VERSION,e as CLIENT_TO_SERVER_INFO,t as MAX_MESSAGES_PER_KEY,n as NONCE_BYTES,r as NONCE_COUNTER_BYTES,i as NONCE_PREFIX_BYTES,a as SEALED_BODY_HEADER,o as SEALED_CLIENT_KEY_HEADER,s as SEALED_KEY_PARAM,c as SEALED_VERSION,l as SERVER_TO_CLIENT_INFO,g as canonicalManifest,p as createSerialQueue,u as describeSealedFailure,_ as digestFor,d as infoFor,v as isBundleManifest,f as isSealedEnvelope};
|
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;
|
package/dist/node.d.cts
ADDED
|
@@ -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 };
|
package/dist/node.d.mts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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"}
|
|
Binary file
|
|
@@ -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. Such an attacker can omit the\n * verifier or substitute its trust key. Consumers need a separately trusted\n * bootstrap and key before these signatures become a delivery guarantee.\n *\n * Lives in types rather than services because any verifier must use the same\n * canonical encoding the server signs, byte for byte.\n */\n\nexport const BUNDLE_MANIFEST_ROUTE = '/bundle-manifest.json';\nexport const BUNDLE_MANIFEST_VERSION = 1;\n\nexport interface BundleAsset {\n /** Path as the browser requests it, leading slash included. */\n path: string;\n /** Lowercase hex SHA-256 of the bytes served at that path. */\n sha256: string;\n}\n\nexport interface BundleManifest {\n version: number;\n /** Milliseconds, so a stale manifest is visible without another lookup. */\n builtAt: number;\n assets: BundleAsset[];\n}\n\nexport interface SignedBundleManifest {\n manifest: BundleManifest;\n /** Base64url ECDSA P-256 signature over the canonical serialization. */\n signature: string;\n /** Base64url SPKI of the key that signed it; trust must be established out of band. */\n publicKey: string;\n}\n\n/**\n * The exact bytes that get signed.\n *\n * Hand-rolled rather than `JSON.stringify` of the whole object, because a\n * signature is only worth anything if both sides agree byte for byte on what\n * was signed. Key order in a JSON object is an implementation detail; a sorted\n * line-per-asset encoding is not.\n */\nexport function canonicalManifest(manifest: BundleManifest): string {\n const assets = [...manifest.assets].sort((left, right) =>\n left.path < right.path ? -1 : left.path > right.path ? 1 : 0,\n );\n const lines = assets.map((asset) => `${asset.path}\u0000${asset.sha256}`);\n return [String(manifest.version), String(manifest.builtAt), ...lines].join('\\n');\n}\n\nexport function isBundleManifest(value: unknown): value is BundleManifest {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<BundleManifest>;\n if (candidate.version !== BUNDLE_MANIFEST_VERSION) return false;\n if (typeof candidate.builtAt !== 'number' || !Array.isArray(candidate.assets)) return false;\n return candidate.assets.every(\n (asset: unknown) =>\n typeof asset === 'object' &&\n asset !== null &&\n typeof (asset as BundleAsset).path === 'string' &&\n /^[0-9a-f]{64}$/u.test((asset as BundleAsset).sha256),\n );\n}\n\n/** The recorded digest for one asset, or undefined if the manifest does not cover it. */\nexport function digestFor(manifest: BundleManifest, path: string): string | undefined {\n return manifest.assets.find((asset) => asset.path === path)?.sha256;\n}\n"],"mappings":"AA6CA,SAAgB,EAAkB,EAAkC,CAIlE,IAAM,EAHS,CAAC,GAAG,EAAS,MAAM,CAAC,CAAC,MAAM,EAAM,IAC9C,EAAK,KAAO,EAAM,KAAO,GAAK,IAAK,KAAO,EAAM,KAE/B,CAAC,CAAC,IAAK,GAAU,GAAG,EAAM,KAAK,GAAG,EAAM,QAAQ,EACnE,MAAO,CAAC,OAAO,EAAS,OAAO,EAAG,OAAO,EAAS,OAAO,EAAG,GAAG,CAAK,CAAC,CAAC,KAAK;CAAI,CACjF,CAEA,SAAgB,EAAiB,EAAyC,CACxE,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAY,EAGlB,OAFI,EAAU,UAAA,GACV,OAAO,EAAU,SAAY,UAAY,CAAC,MAAM,QAAQ,EAAU,MAAM,EAAU,GAC/E,EAAU,OAAO,MACrB,GACC,OAAO,GAAU,YACjB,GACA,OAAQ,EAAsB,MAAS,UACvC,kBAAkB,KAAM,EAAsB,MAAM,CACxD,CACF,CAGA,SAAgB,EAAU,EAA0B,EAAkC,CACpF,OAAO,EAAS,OAAO,KAAM,GAAU,EAAM,OAAS,CAAI,CAAC,EAAE,MAC/D"}
|
|
@@ -0,0 +1,48 @@
|
|
|
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. Such an attacker can omit the
|
|
7
|
+
* verifier or substitute its trust key. Consumers need a separately trusted
|
|
8
|
+
* 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
|
+
*/
|
|
13
|
+
declare const BUNDLE_MANIFEST_ROUTE = "/bundle-manifest.json";
|
|
14
|
+
declare const BUNDLE_MANIFEST_VERSION = 1;
|
|
15
|
+
interface BundleAsset {
|
|
16
|
+
/** Path as the browser requests it, leading slash included. */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Lowercase hex SHA-256 of the bytes served at that path. */
|
|
19
|
+
sha256: string;
|
|
20
|
+
}
|
|
21
|
+
interface BundleManifest {
|
|
22
|
+
version: number;
|
|
23
|
+
/** Milliseconds, so a stale manifest is visible without another lookup. */
|
|
24
|
+
builtAt: number;
|
|
25
|
+
assets: BundleAsset[];
|
|
26
|
+
}
|
|
27
|
+
interface SignedBundleManifest {
|
|
28
|
+
manifest: BundleManifest;
|
|
29
|
+
/** Base64url ECDSA P-256 signature over the canonical serialization. */
|
|
30
|
+
signature: string;
|
|
31
|
+
/** Base64url SPKI of the key that signed it; trust must be established out of band. */
|
|
32
|
+
publicKey: string;
|
|
33
|
+
}
|
|
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
|
+
*/
|
|
42
|
+
declare function canonicalManifest(manifest: BundleManifest): string;
|
|
43
|
+
declare function isBundleManifest(value: unknown): value is BundleManifest;
|
|
44
|
+
/** The recorded digest for one asset, or undefined if the manifest does not cover it. */
|
|
45
|
+
declare function digestFor(manifest: BundleManifest, path: string): string | undefined;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, canonicalManifest, digestFor, isBundleManifest };
|
|
48
|
+
//# sourceMappingURL=bundleManifest.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundleManifest.d.cts","names":[],"sources":["../../src/types/bundleManifest.ts"],"mappings":";;;;;;;;;;;;cAYa;cACA;UAEI;;EAEf;;EAEA;;UAGe;EACf;;EAEA;EACA,QAAQ;;UAGO;EACf,UAAU;;EAEV;;EAEA;;;;;;;;;;iBAWc,kBAAkB,UAAU;iBAQ5B,iBAAiB,iBAAiB,SAAS;;iBAe3C,UAAU,UAAU,gBAAgB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
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. Such an attacker can omit the
|
|
7
|
+
* verifier or substitute its trust key. Consumers need a separately trusted
|
|
8
|
+
* 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
|
+
*/
|
|
13
|
+
declare const BUNDLE_MANIFEST_ROUTE = "/bundle-manifest.json";
|
|
14
|
+
declare const BUNDLE_MANIFEST_VERSION = 1;
|
|
15
|
+
interface BundleAsset {
|
|
16
|
+
/** Path as the browser requests it, leading slash included. */
|
|
17
|
+
path: string;
|
|
18
|
+
/** Lowercase hex SHA-256 of the bytes served at that path. */
|
|
19
|
+
sha256: string;
|
|
20
|
+
}
|
|
21
|
+
interface BundleManifest {
|
|
22
|
+
version: number;
|
|
23
|
+
/** Milliseconds, so a stale manifest is visible without another lookup. */
|
|
24
|
+
builtAt: number;
|
|
25
|
+
assets: BundleAsset[];
|
|
26
|
+
}
|
|
27
|
+
interface SignedBundleManifest {
|
|
28
|
+
manifest: BundleManifest;
|
|
29
|
+
/** Base64url ECDSA P-256 signature over the canonical serialization. */
|
|
30
|
+
signature: string;
|
|
31
|
+
/** Base64url SPKI of the key that signed it; trust must be established out of band. */
|
|
32
|
+
publicKey: string;
|
|
33
|
+
}
|
|
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
|
+
*/
|
|
42
|
+
declare function canonicalManifest(manifest: BundleManifest): string;
|
|
43
|
+
declare function isBundleManifest(value: unknown): value is BundleManifest;
|
|
44
|
+
/** The recorded digest for one asset, or undefined if the manifest does not cover it. */
|
|
45
|
+
declare function digestFor(manifest: BundleManifest, path: string): string | undefined;
|
|
46
|
+
//#endregion
|
|
47
|
+
export { BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, SignedBundleManifest, canonicalManifest, digestFor, isBundleManifest };
|
|
48
|
+
//# sourceMappingURL=bundleManifest.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundleManifest.d.mts","names":[],"sources":["../../src/types/bundleManifest.ts"],"mappings":";;;;;;;;;;;;cAYa;cACA;UAEI;;EAEf;;EAEA;;UAGe;EACf;;EAEA;EACA,QAAQ;;UAGO;EACf,UAAU;;EAEV;;EAEA;;;;;;;;;;iBAWc,kBAAkB,UAAU;iBAQ5B,iBAAiB,iBAAiB,SAAS;;iBAe3C,UAAU,UAAU,gBAAgB"}
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundleManifest.mjs","names":[],"sources":["../../src/types/bundleManifest.ts"],"sourcesContent":["/**\n * A signed description of cockpit assets for an independently trusted verifier.\n *\n * A manifest does not let a browser SPA defend itself against the server or TLS\n * edge that supplied its executable code. Such an attacker can omit the\n * verifier or substitute its trust key. Consumers need a separately trusted\n * bootstrap and key before these signatures become a delivery guarantee.\n *\n * Lives in types rather than services because any verifier must use the same\n * canonical encoding the server signs, byte for byte.\n */\n\nexport const BUNDLE_MANIFEST_ROUTE = '/bundle-manifest.json';\nexport const BUNDLE_MANIFEST_VERSION = 1;\n\nexport interface BundleAsset {\n /** Path as the browser requests it, leading slash included. */\n path: string;\n /** Lowercase hex SHA-256 of the bytes served at that path. */\n sha256: string;\n}\n\nexport interface BundleManifest {\n version: number;\n /** Milliseconds, so a stale manifest is visible without another lookup. */\n builtAt: number;\n assets: BundleAsset[];\n}\n\nexport interface SignedBundleManifest {\n manifest: BundleManifest;\n /** Base64url ECDSA P-256 signature over the canonical serialization. */\n signature: string;\n /** Base64url SPKI of the key that signed it; trust must be established out of band. */\n publicKey: string;\n}\n\n/**\n * The exact bytes that get signed.\n *\n * Hand-rolled rather than `JSON.stringify` of the whole object, because a\n * signature is only worth anything if both sides agree byte for byte on what\n * was signed. Key order in a JSON object is an implementation detail; a sorted\n * line-per-asset encoding is not.\n */\nexport function canonicalManifest(manifest: BundleManifest): string {\n const assets = [...manifest.assets].sort((left, right) =>\n left.path < right.path ? -1 : left.path > right.path ? 1 : 0,\n );\n const lines = assets.map((asset) => `${asset.path}\u0000${asset.sha256}`);\n return [String(manifest.version), String(manifest.builtAt), ...lines].join('\\n');\n}\n\nexport function isBundleManifest(value: unknown): value is BundleManifest {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<BundleManifest>;\n if (candidate.version !== BUNDLE_MANIFEST_VERSION) return false;\n if (typeof candidate.builtAt !== 'number' || !Array.isArray(candidate.assets)) return false;\n return candidate.assets.every(\n (asset: unknown) =>\n typeof asset === 'object' &&\n asset !== null &&\n typeof (asset as BundleAsset).path === 'string' &&\n /^[0-9a-f]{64}$/u.test((asset as BundleAsset).sha256),\n );\n}\n\n/** The recorded digest for one asset, or undefined if the manifest does not cover it. */\nexport function digestFor(manifest: BundleManifest, path: string): string | undefined {\n return manifest.assets.find((asset) => asset.path === path)?.sha256;\n}\n"],"mappings":"AAYA,MAAa,EAAwB,wBACxB,EAA0B,EAgCvC,SAAgB,EAAkB,EAAkC,CAIlE,IAAM,EAHS,CAAC,GAAG,EAAS,MAAM,CAAC,CAAC,MAAM,EAAM,IAC9C,EAAK,KAAO,EAAM,KAAO,GAAK,IAAK,KAAO,EAAM,KAE/B,CAAC,CAAC,IAAK,GAAU,GAAG,EAAM,KAAK,GAAG,EAAM,QAAQ,EACnE,MAAO,CAAC,OAAO,EAAS,OAAO,EAAG,OAAO,EAAS,OAAO,EAAG,GAAG,CAAK,CAAC,CAAC,KAAK;CAAI,CACjF,CAEA,SAAgB,EAAiB,EAAyC,CACxE,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAY,EAGlB,OAFI,EAAU,UAAA,GACV,OAAO,EAAU,SAAY,UAAY,CAAC,MAAM,QAAQ,EAAU,MAAM,EAAU,GAC/E,EAAU,OAAO,MACrB,GACC,OAAO,GAAU,YACjB,GACA,OAAQ,EAAsB,MAAS,UACvC,kBAAkB,KAAM,EAAsB,MAAM,CACxD,CACF,CAGA,SAAgB,EAAU,EAA0B,EAAkC,CACpF,OAAO,EAAS,OAAO,KAAM,GAAU,EAAM,OAAS,CAAI,CAAC,EAAE,MAC/D"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=2**32,t=`doompi/sealed/c2s/v1`,n=`doompi/sealed/s2c/v1`;function r(e){return e===`c2s`?t:n}function i(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.v===1&&typeof t.n==`string`&&typeof t.c==`string`}function a(e){switch(e){case`version`:return`The other end is speaking a different envelope version.`;case`malformed`:return`A sealed message was not shaped like one.`;case`replay`:return`A sealed message arrived out of order or twice.`;case`auth`:return`A sealed message failed its authentication tag, so it was altered in transit.`;case`exhausted`:return`This channel reached its message limit and must be re-established.`}}exports.CLIENT_TO_SERVER_INFO=t,exports.MAX_MESSAGES_PER_KEY=e,exports.NONCE_BYTES=12,exports.NONCE_COUNTER_BYTES=8,exports.NONCE_PREFIX_BYTES=4,exports.SEALED_BODY_HEADER=`x-doompi-sealed`,exports.SEALED_CLIENT_KEY_HEADER=`x-doompi-channel-key`,exports.SEALED_KEY_PARAM=`k`,exports.SEALED_VERSION=1,exports.SERVER_TO_CLIENT_INFO=n,exports.describeSealedFailure=a,exports.infoFor=r,exports.isSealedEnvelope=i;
|
|
2
|
+
//# sourceMappingURL=sealedChannel.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sealedChannel.cjs","names":[],"sources":["../../src/types/sealedChannel.ts"],"sourcesContent":["/**\n * The envelope that keeps the tunnel provider out of the payload.\n *\n * Cloudflare terminates TLS at its edge, so everything the cockpit carries\n * (prompts, file previews, model output) is plaintext to them. Sealing the\n * payload underneath their TLS leaves them a relay that can see timing and\n * sizes but not content.\n *\n * The key exchange is anchored on the QR rather than negotiated in band: the\n * host's ephemeral public key is printed on a screen the user is holding, so\n * there is no moment at which a relay could substitute its own. That is the\n * same job Telegram's emoji comparison does for a secret chat, except it\n * happens automatically instead of depending on two people comparing pictures.\n *\n * Declared here because the server seals with `node:crypto` and the browser\n * unseals with WebCrypto, and an envelope is only an envelope if both sides\n * read it identically.\n */\n\n/** Bumped when the envelope layout changes; a mismatch is refused, never guessed at. */\nexport const SEALED_VERSION = 1;\n\n/** Fragment key in the pairing URL carrying the host's ephemeral public key. */\nexport const SEALED_KEY_PARAM = 'k';\n\n/** AES-GCM standard nonce length. Longer is non-standard, shorter is unsafe. */\nexport const NONCE_BYTES = 12;\n/** Random per-channel prefix, so two channels never share a nonce space. */\nexport const NONCE_PREFIX_BYTES = 4;\n/** Monotonic per-direction counter filling the rest of the nonce. */\nexport const NONCE_COUNTER_BYTES = NONCE_BYTES - NONCE_PREFIX_BYTES;\n\n/**\n * Rekey well before the counter could wrap.\n *\n * A repeated nonce under one AES-GCM key is catastrophic, not merely weak: it\n * leaks the XOR of two plaintexts and the authentication subkey. The counter is\n * 64 bits, so this bound is nowhere near it; the point is that the bound exists\n * and is enforced rather than assumed.\n */\nexport const MAX_MESSAGES_PER_KEY = 2 ** 32;\n\n/**\n * HKDF info strings, one per direction.\n *\n * Separate keys per direction, not one shared key, so a message the server sent\n * can never be replayed back at it as though the client had sent it.\n */\nexport const CLIENT_TO_SERVER_INFO = 'doompi/sealed/c2s/v1';\nexport const SERVER_TO_CLIENT_INFO = 'doompi/sealed/s2c/v1';\n\nexport type SealedDirection = 'c2s' | 's2c';\n\nexport function infoFor(direction: SealedDirection): string {\n return direction === 'c2s' ? CLIENT_TO_SERVER_INFO : SERVER_TO_CLIENT_INFO;\n}\n\n/**\n * One sealed message, as it travels.\n *\n * Base64url rather than binary so the same envelope works on a JSON body and a\n * text WebSocket frame without a second encoding path to get wrong.\n */\nexport interface SealedEnvelope {\n v: number;\n /** Base64url, 4-byte prefix plus 8-byte counter. */\n n: string;\n /** Base64url ciphertext with the GCM tag appended. */\n c: string;\n}\n\nexport function isSealedEnvelope(value: unknown): value is SealedEnvelope {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<SealedEnvelope>;\n return candidate.v === SEALED_VERSION && typeof candidate.n === 'string' && typeof candidate.c === 'string';\n}\n\n/** Header naming the client's ephemeral public key during the handshake. */\nexport const SEALED_CLIENT_KEY_HEADER = 'x-doompi-channel-key';\n/** Header carrying a sealed request body's envelope on a non-JSON request. */\nexport const SEALED_BODY_HEADER = 'x-doompi-sealed';\n\nexport type SealedFailure = 'version' | 'malformed' | 'replay' | 'auth' | 'exhausted';\n\n/**\n * Why an unseal failed, in words a diagnostic can show.\n *\n * A decryption failure means a blank cockpit, so every one of these has to name\n * its cause rather than leaving a dead page and no explanation.\n */\nexport function describeSealedFailure(failure: SealedFailure): string {\n switch (failure) {\n case 'version':\n return 'The other end is speaking a different envelope version.';\n case 'malformed':\n return 'A sealed message was not shaped like one.';\n case 'replay':\n return 'A sealed message arrived out of order or twice.';\n case 'auth':\n return 'A sealed message failed its authentication tag, so it was altered in transit.';\n case 'exhausted':\n return 'This channel reached its message limit and must be re-established.';\n }\n}\n"],"mappings":"AAoBA,MAoBa,EAAuB,GAAK,GAQ5B,EAAwB,uBACxB,EAAwB,uBAIrC,SAAgB,EAAQ,EAAoC,CAC1D,OAAO,IAAc,MAAQ,EAAwB,CACvD,CAgBA,SAAgB,EAAiB,EAAyC,CACxE,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAY,EAClB,OAAO,EAAU,IAAA,GAAwB,OAAO,EAAU,GAAM,UAAY,OAAO,EAAU,GAAM,QACrG,CAeA,SAAgB,EAAsB,EAAgC,CACpE,OAAQ,EAAR,CACE,IAAK,UACH,MAAO,0DACT,IAAK,YACH,MAAO,4CACT,IAAK,SACH,MAAO,kDACT,IAAK,OACH,MAAO,gFACT,IAAK,YACH,MAAO,oEACX,CACF"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//#region src/types/sealedChannel.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The envelope that keeps the tunnel provider out of the payload.
|
|
4
|
+
*
|
|
5
|
+
* Cloudflare terminates TLS at its edge, so everything the cockpit carries
|
|
6
|
+
* (prompts, file previews, model output) is plaintext to them. Sealing the
|
|
7
|
+
* payload underneath their TLS leaves them a relay that can see timing and
|
|
8
|
+
* sizes but not content.
|
|
9
|
+
*
|
|
10
|
+
* The key exchange is anchored on the QR rather than negotiated in band: the
|
|
11
|
+
* host's ephemeral public key is printed on a screen the user is holding, so
|
|
12
|
+
* there is no moment at which a relay could substitute its own. That is the
|
|
13
|
+
* same job Telegram's emoji comparison does for a secret chat, except it
|
|
14
|
+
* happens automatically instead of depending on two people comparing pictures.
|
|
15
|
+
*
|
|
16
|
+
* Declared here because the server seals with `node:crypto` and the browser
|
|
17
|
+
* unseals with WebCrypto, and an envelope is only an envelope if both sides
|
|
18
|
+
* read it identically.
|
|
19
|
+
*/
|
|
20
|
+
/** Bumped when the envelope layout changes; a mismatch is refused, never guessed at. */
|
|
21
|
+
declare const SEALED_VERSION = 1;
|
|
22
|
+
/** Fragment key in the pairing URL carrying the host's ephemeral public key. */
|
|
23
|
+
declare const SEALED_KEY_PARAM = "k";
|
|
24
|
+
/** AES-GCM standard nonce length. Longer is non-standard, shorter is unsafe. */
|
|
25
|
+
declare const NONCE_BYTES = 12;
|
|
26
|
+
/** Random per-channel prefix, so two channels never share a nonce space. */
|
|
27
|
+
declare const NONCE_PREFIX_BYTES = 4;
|
|
28
|
+
/** Monotonic per-direction counter filling the rest of the nonce. */
|
|
29
|
+
declare const NONCE_COUNTER_BYTES: number;
|
|
30
|
+
/**
|
|
31
|
+
* Rekey well before the counter could wrap.
|
|
32
|
+
*
|
|
33
|
+
* A repeated nonce under one AES-GCM key is catastrophic, not merely weak: it
|
|
34
|
+
* leaks the XOR of two plaintexts and the authentication subkey. The counter is
|
|
35
|
+
* 64 bits, so this bound is nowhere near it; the point is that the bound exists
|
|
36
|
+
* and is enforced rather than assumed.
|
|
37
|
+
*/
|
|
38
|
+
declare const MAX_MESSAGES_PER_KEY: number;
|
|
39
|
+
/**
|
|
40
|
+
* HKDF info strings, one per direction.
|
|
41
|
+
*
|
|
42
|
+
* Separate keys per direction, not one shared key, so a message the server sent
|
|
43
|
+
* can never be replayed back at it as though the client had sent it.
|
|
44
|
+
*/
|
|
45
|
+
declare const CLIENT_TO_SERVER_INFO = "doompi/sealed/c2s/v1";
|
|
46
|
+
declare const SERVER_TO_CLIENT_INFO = "doompi/sealed/s2c/v1";
|
|
47
|
+
type SealedDirection = 'c2s' | 's2c';
|
|
48
|
+
declare function infoFor(direction: SealedDirection): string;
|
|
49
|
+
/**
|
|
50
|
+
* One sealed message, as it travels.
|
|
51
|
+
*
|
|
52
|
+
* Base64url rather than binary so the same envelope works on a JSON body and a
|
|
53
|
+
* text WebSocket frame without a second encoding path to get wrong.
|
|
54
|
+
*/
|
|
55
|
+
interface SealedEnvelope {
|
|
56
|
+
v: number;
|
|
57
|
+
/** Base64url, 4-byte prefix plus 8-byte counter. */
|
|
58
|
+
n: string;
|
|
59
|
+
/** Base64url ciphertext with the GCM tag appended. */
|
|
60
|
+
c: string;
|
|
61
|
+
}
|
|
62
|
+
declare function isSealedEnvelope(value: unknown): value is SealedEnvelope;
|
|
63
|
+
/** Header naming the client's ephemeral public key during the handshake. */
|
|
64
|
+
declare const SEALED_CLIENT_KEY_HEADER = "x-doompi-channel-key";
|
|
65
|
+
/** Header carrying a sealed request body's envelope on a non-JSON request. */
|
|
66
|
+
declare const SEALED_BODY_HEADER = "x-doompi-sealed";
|
|
67
|
+
type SealedFailure = 'version' | 'malformed' | 'replay' | 'auth' | 'exhausted';
|
|
68
|
+
/**
|
|
69
|
+
* Why an unseal failed, in words a diagnostic can show.
|
|
70
|
+
*
|
|
71
|
+
* A decryption failure means a blank cockpit, so every one of these has to name
|
|
72
|
+
* its cause rather than leaving a dead page and no explanation.
|
|
73
|
+
*/
|
|
74
|
+
declare function describeSealedFailure(failure: SealedFailure): string;
|
|
75
|
+
//#endregion
|
|
76
|
+
export { 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 };
|
|
77
|
+
//# sourceMappingURL=sealedChannel.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sealedChannel.d.cts","names":[],"sources":["../../src/types/sealedChannel.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;cAoBa;;cAGA;;cAGA;;cAEA;;cAEA;;;;;;;;;cAUA;;;;;;;cAQA;cACA;KAED;iBAEI,QAAQ,WAAW;;;;;;;UAUlB;EACf;;EAEA;;EAEA;;iBAGc,iBAAiB,iBAAiB,SAAS;;cAO9C;;cAEA;KAED;;;;;;;iBAQI,sBAAsB,SAAS"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//#region src/types/sealedChannel.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The envelope that keeps the tunnel provider out of the payload.
|
|
4
|
+
*
|
|
5
|
+
* Cloudflare terminates TLS at its edge, so everything the cockpit carries
|
|
6
|
+
* (prompts, file previews, model output) is plaintext to them. Sealing the
|
|
7
|
+
* payload underneath their TLS leaves them a relay that can see timing and
|
|
8
|
+
* sizes but not content.
|
|
9
|
+
*
|
|
10
|
+
* The key exchange is anchored on the QR rather than negotiated in band: the
|
|
11
|
+
* host's ephemeral public key is printed on a screen the user is holding, so
|
|
12
|
+
* there is no moment at which a relay could substitute its own. That is the
|
|
13
|
+
* same job Telegram's emoji comparison does for a secret chat, except it
|
|
14
|
+
* happens automatically instead of depending on two people comparing pictures.
|
|
15
|
+
*
|
|
16
|
+
* Declared here because the server seals with `node:crypto` and the browser
|
|
17
|
+
* unseals with WebCrypto, and an envelope is only an envelope if both sides
|
|
18
|
+
* read it identically.
|
|
19
|
+
*/
|
|
20
|
+
/** Bumped when the envelope layout changes; a mismatch is refused, never guessed at. */
|
|
21
|
+
declare const SEALED_VERSION = 1;
|
|
22
|
+
/** Fragment key in the pairing URL carrying the host's ephemeral public key. */
|
|
23
|
+
declare const SEALED_KEY_PARAM = "k";
|
|
24
|
+
/** AES-GCM standard nonce length. Longer is non-standard, shorter is unsafe. */
|
|
25
|
+
declare const NONCE_BYTES = 12;
|
|
26
|
+
/** Random per-channel prefix, so two channels never share a nonce space. */
|
|
27
|
+
declare const NONCE_PREFIX_BYTES = 4;
|
|
28
|
+
/** Monotonic per-direction counter filling the rest of the nonce. */
|
|
29
|
+
declare const NONCE_COUNTER_BYTES: number;
|
|
30
|
+
/**
|
|
31
|
+
* Rekey well before the counter could wrap.
|
|
32
|
+
*
|
|
33
|
+
* A repeated nonce under one AES-GCM key is catastrophic, not merely weak: it
|
|
34
|
+
* leaks the XOR of two plaintexts and the authentication subkey. The counter is
|
|
35
|
+
* 64 bits, so this bound is nowhere near it; the point is that the bound exists
|
|
36
|
+
* and is enforced rather than assumed.
|
|
37
|
+
*/
|
|
38
|
+
declare const MAX_MESSAGES_PER_KEY: number;
|
|
39
|
+
/**
|
|
40
|
+
* HKDF info strings, one per direction.
|
|
41
|
+
*
|
|
42
|
+
* Separate keys per direction, not one shared key, so a message the server sent
|
|
43
|
+
* can never be replayed back at it as though the client had sent it.
|
|
44
|
+
*/
|
|
45
|
+
declare const CLIENT_TO_SERVER_INFO = "doompi/sealed/c2s/v1";
|
|
46
|
+
declare const SERVER_TO_CLIENT_INFO = "doompi/sealed/s2c/v1";
|
|
47
|
+
type SealedDirection = 'c2s' | 's2c';
|
|
48
|
+
declare function infoFor(direction: SealedDirection): string;
|
|
49
|
+
/**
|
|
50
|
+
* One sealed message, as it travels.
|
|
51
|
+
*
|
|
52
|
+
* Base64url rather than binary so the same envelope works on a JSON body and a
|
|
53
|
+
* text WebSocket frame without a second encoding path to get wrong.
|
|
54
|
+
*/
|
|
55
|
+
interface SealedEnvelope {
|
|
56
|
+
v: number;
|
|
57
|
+
/** Base64url, 4-byte prefix plus 8-byte counter. */
|
|
58
|
+
n: string;
|
|
59
|
+
/** Base64url ciphertext with the GCM tag appended. */
|
|
60
|
+
c: string;
|
|
61
|
+
}
|
|
62
|
+
declare function isSealedEnvelope(value: unknown): value is SealedEnvelope;
|
|
63
|
+
/** Header naming the client's ephemeral public key during the handshake. */
|
|
64
|
+
declare const SEALED_CLIENT_KEY_HEADER = "x-doompi-channel-key";
|
|
65
|
+
/** Header carrying a sealed request body's envelope on a non-JSON request. */
|
|
66
|
+
declare const SEALED_BODY_HEADER = "x-doompi-sealed";
|
|
67
|
+
type SealedFailure = 'version' | 'malformed' | 'replay' | 'auth' | 'exhausted';
|
|
68
|
+
/**
|
|
69
|
+
* Why an unseal failed, in words a diagnostic can show.
|
|
70
|
+
*
|
|
71
|
+
* A decryption failure means a blank cockpit, so every one of these has to name
|
|
72
|
+
* its cause rather than leaving a dead page and no explanation.
|
|
73
|
+
*/
|
|
74
|
+
declare function describeSealedFailure(failure: SealedFailure): string;
|
|
75
|
+
//#endregion
|
|
76
|
+
export { 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 };
|
|
77
|
+
//# sourceMappingURL=sealedChannel.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sealedChannel.d.mts","names":[],"sources":["../../src/types/sealedChannel.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;cAoBa;;cAGA;;cAGA;;cAEA;;cAEA;;;;;;;;;cAUA;;;;;;;cAQA;cACA;KAED;iBAEI,QAAQ,WAAW;;;;;;;UAUlB;EACf;;EAEA;;EAEA;;iBAGc,iBAAiB,iBAAiB,SAAS;;cAO9C;;cAEA;KAED;;;;;;;iBAQI,sBAAsB,SAAS"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const e=1,t=`k`,n=12,r=4,i=8,a=2**32,o=`doompi/sealed/c2s/v1`,s=`doompi/sealed/s2c/v1`;function c(e){return e===`c2s`?o:s}function l(e){if(typeof e!=`object`||!e)return!1;let t=e;return t.v===1&&typeof t.n==`string`&&typeof t.c==`string`}const u=`x-doompi-channel-key`,d=`x-doompi-sealed`;function f(e){switch(e){case`version`:return`The other end is speaking a different envelope version.`;case`malformed`:return`A sealed message was not shaped like one.`;case`replay`:return`A sealed message arrived out of order or twice.`;case`auth`:return`A sealed message failed its authentication tag, so it was altered in transit.`;case`exhausted`:return`This channel reached its message limit and must be re-established.`}}export{o as CLIENT_TO_SERVER_INFO,a as MAX_MESSAGES_PER_KEY,n as NONCE_BYTES,i as NONCE_COUNTER_BYTES,r as NONCE_PREFIX_BYTES,d as SEALED_BODY_HEADER,u as SEALED_CLIENT_KEY_HEADER,t as SEALED_KEY_PARAM,e as SEALED_VERSION,s as SERVER_TO_CLIENT_INFO,f as describeSealedFailure,c as infoFor,l as isSealedEnvelope};
|
|
2
|
+
//# sourceMappingURL=sealedChannel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sealedChannel.mjs","names":[],"sources":["../../src/types/sealedChannel.ts"],"sourcesContent":["/**\n * The envelope that keeps the tunnel provider out of the payload.\n *\n * Cloudflare terminates TLS at its edge, so everything the cockpit carries\n * (prompts, file previews, model output) is plaintext to them. Sealing the\n * payload underneath their TLS leaves them a relay that can see timing and\n * sizes but not content.\n *\n * The key exchange is anchored on the QR rather than negotiated in band: the\n * host's ephemeral public key is printed on a screen the user is holding, so\n * there is no moment at which a relay could substitute its own. That is the\n * same job Telegram's emoji comparison does for a secret chat, except it\n * happens automatically instead of depending on two people comparing pictures.\n *\n * Declared here because the server seals with `node:crypto` and the browser\n * unseals with WebCrypto, and an envelope is only an envelope if both sides\n * read it identically.\n */\n\n/** Bumped when the envelope layout changes; a mismatch is refused, never guessed at. */\nexport const SEALED_VERSION = 1;\n\n/** Fragment key in the pairing URL carrying the host's ephemeral public key. */\nexport const SEALED_KEY_PARAM = 'k';\n\n/** AES-GCM standard nonce length. Longer is non-standard, shorter is unsafe. */\nexport const NONCE_BYTES = 12;\n/** Random per-channel prefix, so two channels never share a nonce space. */\nexport const NONCE_PREFIX_BYTES = 4;\n/** Monotonic per-direction counter filling the rest of the nonce. */\nexport const NONCE_COUNTER_BYTES = NONCE_BYTES - NONCE_PREFIX_BYTES;\n\n/**\n * Rekey well before the counter could wrap.\n *\n * A repeated nonce under one AES-GCM key is catastrophic, not merely weak: it\n * leaks the XOR of two plaintexts and the authentication subkey. The counter is\n * 64 bits, so this bound is nowhere near it; the point is that the bound exists\n * and is enforced rather than assumed.\n */\nexport const MAX_MESSAGES_PER_KEY = 2 ** 32;\n\n/**\n * HKDF info strings, one per direction.\n *\n * Separate keys per direction, not one shared key, so a message the server sent\n * can never be replayed back at it as though the client had sent it.\n */\nexport const CLIENT_TO_SERVER_INFO = 'doompi/sealed/c2s/v1';\nexport const SERVER_TO_CLIENT_INFO = 'doompi/sealed/s2c/v1';\n\nexport type SealedDirection = 'c2s' | 's2c';\n\nexport function infoFor(direction: SealedDirection): string {\n return direction === 'c2s' ? CLIENT_TO_SERVER_INFO : SERVER_TO_CLIENT_INFO;\n}\n\n/**\n * One sealed message, as it travels.\n *\n * Base64url rather than binary so the same envelope works on a JSON body and a\n * text WebSocket frame without a second encoding path to get wrong.\n */\nexport interface SealedEnvelope {\n v: number;\n /** Base64url, 4-byte prefix plus 8-byte counter. */\n n: string;\n /** Base64url ciphertext with the GCM tag appended. */\n c: string;\n}\n\nexport function isSealedEnvelope(value: unknown): value is SealedEnvelope {\n if (typeof value !== 'object' || value === null) return false;\n const candidate = value as Partial<SealedEnvelope>;\n return candidate.v === SEALED_VERSION && typeof candidate.n === 'string' && typeof candidate.c === 'string';\n}\n\n/** Header naming the client's ephemeral public key during the handshake. */\nexport const SEALED_CLIENT_KEY_HEADER = 'x-doompi-channel-key';\n/** Header carrying a sealed request body's envelope on a non-JSON request. */\nexport const SEALED_BODY_HEADER = 'x-doompi-sealed';\n\nexport type SealedFailure = 'version' | 'malformed' | 'replay' | 'auth' | 'exhausted';\n\n/**\n * Why an unseal failed, in words a diagnostic can show.\n *\n * A decryption failure means a blank cockpit, so every one of these has to name\n * its cause rather than leaving a dead page and no explanation.\n */\nexport function describeSealedFailure(failure: SealedFailure): string {\n switch (failure) {\n case 'version':\n return 'The other end is speaking a different envelope version.';\n case 'malformed':\n return 'A sealed message was not shaped like one.';\n case 'replay':\n return 'A sealed message arrived out of order or twice.';\n case 'auth':\n return 'A sealed message failed its authentication tag, so it was altered in transit.';\n case 'exhausted':\n return 'This channel reached its message limit and must be re-established.';\n }\n}\n"],"mappings":"AAoBA,MAAa,EAAiB,EAGjB,EAAmB,IAGnB,EAAc,GAEd,EAAqB,EAErB,EAAsB,EAUtB,EAAuB,GAAK,GAQ5B,EAAwB,uBACxB,EAAwB,uBAIrC,SAAgB,EAAQ,EAAoC,CAC1D,OAAO,IAAc,MAAQ,EAAwB,CACvD,CAgBA,SAAgB,EAAiB,EAAyC,CACxE,GAAI,OAAO,GAAU,WAAY,EAAgB,MAAO,GACxD,IAAM,EAAY,EAClB,OAAO,EAAU,IAAA,GAAwB,OAAO,EAAU,GAAM,UAAY,OAAO,EAAU,GAAM,QACrG,CAGA,MAAa,EAA2B,uBAE3B,EAAqB,kBAUlC,SAAgB,EAAsB,EAAgC,CACpE,OAAQ,EAAR,CACE,IAAK,UACH,MAAO,0DACT,IAAK,YACH,MAAO,4CACT,IAAK,SACH,MAAO,kDACT,IAAK,OACH,MAAO,gFACT,IAAK,YACH,MAAO,oEACX,CACF"}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @agimon-ai/doompi-web-security
|
|
2
|
+
|
|
3
|
+
Package-owned guidance for the DoomPi cockpit's shared security primitives.
|
|
4
|
+
|
|
5
|
+
## Resources
|
|
6
|
+
|
|
7
|
+
- [Package README](./README.md): Why this is a package rather than a file in the cockpit, what each
|
|
8
|
+
subpath carries, and the three properties of the sealed channel that are silent when wrong
|
|
9
|
+
(per-direction keys, nonce construction, and serialized ordering).
|