@agimon-ai/po-web-security 0.0.0-alpha.37 → 0.0.0-alpha.38

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/dist/browser.cjs CHANGED
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types/bundleManifest.cjs"),t=require("./services/browserBundleVerifier/index.cjs"),n=require("./services/browserSealedChannel/index.cjs"),r=require("./services/sealedTransport/index.cjs");exports.APP_ASSET_MAX_BYTES=e.APP_ASSET_MAX_BYTES,exports.APP_DESCRIPTOR_PATH=e.APP_DESCRIPTOR_PATH,exports.APP_PART_PREFIX=e.APP_PART_PREFIX,exports.ATTESTATION_TAG=e.ATTESTATION_TAG,exports.BUNDLE_MANIFEST_ROUTE=e.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=e.BUNDLE_MANIFEST_VERSION,exports.NATIVE_COMPOSITION_MAX_BYTES=e.NATIVE_COMPOSITION_MAX_BYTES,exports.NATIVE_ENTRY_CONTENT_TYPE=e.NATIVE_ENTRY_CONTENT_TYPE,exports.NATIVE_ENTRY_PATH=e.NATIVE_ENTRY_PATH,exports.WEB_ENTRY_PATH=e.WEB_ENTRY_PATH,exports.assetFor=e.assetFor,exports.attestationBytes=e.attestationBytes,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.verifyAttestation=t.verifyAttestation,exports.verifyBundleAsset=t.verifyBundleAsset,exports.verifySignedBundleManifest=t.verifySignedBundleManifest;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./types/bundleManifest.cjs"),t=require("./services/browserBundleVerifier/index.cjs"),n=require("./services/browserSealedChannel/index.cjs"),r=require("./services/sealedTransport/index.cjs");exports.APP_ASSET_MAX_BYTES=e.APP_ASSET_MAX_BYTES,exports.APP_DESCRIPTOR_PATH=e.APP_DESCRIPTOR_PATH,exports.APP_PART_PREFIX=e.APP_PART_PREFIX,exports.ATTESTATION_TAG=e.ATTESTATION_TAG,exports.BUNDLE_MANIFEST_ROUTE=e.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=e.BUNDLE_MANIFEST_VERSION,exports.NATIVE_COMPOSITION_MAX_BYTES=e.NATIVE_COMPOSITION_MAX_BYTES,exports.NATIVE_ENTRY_CONTENT_TYPE=e.NATIVE_ENTRY_CONTENT_TYPE,exports.NATIVE_ENTRY_PATH=e.NATIVE_ENTRY_PATH,exports.WEB_ENTRY_PATH=e.WEB_ENTRY_PATH,exports.assetFor=e.assetFor,exports.attestationBytes=e.attestationBytes,exports.bindBrowserTransport=r.bindBrowserTransport,exports.browserTransport=r.browserTransport,exports.browserTransportForHost=r.browserTransportForHost,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.verifyAttestation=t.verifyAttestation,exports.verifyBundleAsset=t.verifyBundleAsset,exports.verifySignedBundleManifest=t.verifySignedBundleManifest;
@@ -1,5 +1,5 @@
1
1
  import { APP_ASSET_MAX_BYTES, APP_DESCRIPTOR_PATH, APP_PART_PREFIX, ATTESTATION_TAG, BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, BundleAsset, BundleManifest, BundleProfile, NATIVE_COMPOSITION_MAX_BYTES, NATIVE_ENTRY_CONTENT_TYPE, NATIVE_ENTRY_PATH, SignedBundleManifest, WEB_ENTRY_PATH, assetFor, attestationBytes, canonicalManifest, digestFor, isBundleManifest, isSignedBundleManifest } from "./types/bundleManifest.mjs";
2
2
  import { BundleAssetVerificationFailure, BundleAssetVerificationResult, BundleVerificationFailure, BundleVerificationResult, verifyAttestation, verifyBundleAsset, verifySignedBundleManifest } from "./services/browserBundleVerifier/index.mjs";
3
3
  import { OpenResult, SealResult, SealedChannel, channelFromSecret, connectSealedChannel } from "./services/browserSealedChannel/index.mjs";
4
- import { SealedTransport, createSealedTransport, sealedTransport } from "./services/sealedTransport/index.mjs";
5
- export { APP_ASSET_MAX_BYTES, APP_DESCRIPTOR_PATH, APP_PART_PREFIX, ATTESTATION_TAG, BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleAssetVerificationFailure, type BundleAssetVerificationResult, type BundleManifest, type BundleProfile, type BundleVerificationFailure, type BundleVerificationResult, NATIVE_COMPOSITION_MAX_BYTES, NATIVE_ENTRY_CONTENT_TYPE, NATIVE_ENTRY_PATH, type OpenResult, type SealResult, type SealedChannel, type SealedTransport, type SignedBundleManifest, WEB_ENTRY_PATH, assetFor, attestationBytes, canonicalManifest, channelFromSecret, connectSealedChannel, createSealedTransport, digestFor, isBundleManifest, isSignedBundleManifest, sealedTransport, verifyAttestation, verifyBundleAsset, verifySignedBundleManifest };
4
+ import { SealedTransport, bindBrowserTransport, browserTransport, browserTransportForHost, createSealedTransport } from "./services/sealedTransport/index.mjs";
5
+ export { APP_ASSET_MAX_BYTES, APP_DESCRIPTOR_PATH, APP_PART_PREFIX, ATTESTATION_TAG, BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleAssetVerificationFailure, type BundleAssetVerificationResult, type BundleManifest, type BundleProfile, type BundleVerificationFailure, type BundleVerificationResult, NATIVE_COMPOSITION_MAX_BYTES, NATIVE_ENTRY_CONTENT_TYPE, NATIVE_ENTRY_PATH, type OpenResult, type SealResult, type SealedChannel, type SealedTransport, type SignedBundleManifest, WEB_ENTRY_PATH, assetFor, attestationBytes, bindBrowserTransport, browserTransport, browserTransportForHost, canonicalManifest, channelFromSecret, connectSealedChannel, createSealedTransport, digestFor, isBundleManifest, isSignedBundleManifest, verifyAttestation, verifyBundleAsset, verifySignedBundleManifest };
package/dist/browser.mjs CHANGED
@@ -1 +1 @@
1
- import{APP_ASSET_MAX_BYTES as e,APP_DESCRIPTOR_PATH as t,APP_PART_PREFIX as n,ATTESTATION_TAG as r,BUNDLE_MANIFEST_ROUTE as i,BUNDLE_MANIFEST_VERSION as a,NATIVE_COMPOSITION_MAX_BYTES as o,NATIVE_ENTRY_CONTENT_TYPE as s,NATIVE_ENTRY_PATH as c,WEB_ENTRY_PATH as l,assetFor as u,attestationBytes as d,canonicalManifest as f,digestFor as p,isBundleManifest as m,isSignedBundleManifest as h}from"./types/bundleManifest.mjs";import{verifyAttestation as g,verifyBundleAsset as _,verifySignedBundleManifest as v}from"./services/browserBundleVerifier/index.mjs";import{channelFromSecret as y,connectSealedChannel as b}from"./services/browserSealedChannel/index.mjs";import{createSealedTransport as x,sealedTransport as S}from"./services/sealedTransport/index.mjs";export{e as APP_ASSET_MAX_BYTES,t as APP_DESCRIPTOR_PATH,n as APP_PART_PREFIX,r as ATTESTATION_TAG,i as BUNDLE_MANIFEST_ROUTE,a as BUNDLE_MANIFEST_VERSION,o as NATIVE_COMPOSITION_MAX_BYTES,s as NATIVE_ENTRY_CONTENT_TYPE,c as NATIVE_ENTRY_PATH,l as WEB_ENTRY_PATH,u as assetFor,d as attestationBytes,f as canonicalManifest,y as channelFromSecret,b as connectSealedChannel,x as createSealedTransport,p as digestFor,m as isBundleManifest,h as isSignedBundleManifest,S as sealedTransport,g as verifyAttestation,_ as verifyBundleAsset,v as verifySignedBundleManifest};
1
+ import{APP_ASSET_MAX_BYTES as e,APP_DESCRIPTOR_PATH as t,APP_PART_PREFIX as n,ATTESTATION_TAG as r,BUNDLE_MANIFEST_ROUTE as i,BUNDLE_MANIFEST_VERSION as a,NATIVE_COMPOSITION_MAX_BYTES as o,NATIVE_ENTRY_CONTENT_TYPE as s,NATIVE_ENTRY_PATH as c,WEB_ENTRY_PATH as l,assetFor as u,attestationBytes as d,canonicalManifest as f,digestFor as p,isBundleManifest as m,isSignedBundleManifest as h}from"./types/bundleManifest.mjs";import{verifyAttestation as g,verifyBundleAsset as _,verifySignedBundleManifest as v}from"./services/browserBundleVerifier/index.mjs";import{channelFromSecret as y,connectSealedChannel as b}from"./services/browserSealedChannel/index.mjs";import{bindBrowserTransport as x,browserTransport as S,browserTransportForHost as C,createSealedTransport as w}from"./services/sealedTransport/index.mjs";export{e as APP_ASSET_MAX_BYTES,t as APP_DESCRIPTOR_PATH,n as APP_PART_PREFIX,r as ATTESTATION_TAG,i as BUNDLE_MANIFEST_ROUTE,a as BUNDLE_MANIFEST_VERSION,o as NATIVE_COMPOSITION_MAX_BYTES,s as NATIVE_ENTRY_CONTENT_TYPE,c as NATIVE_ENTRY_PATH,l as WEB_ENTRY_PATH,u as assetFor,d as attestationBytes,x as bindBrowserTransport,S as browserTransport,C as browserTransportForHost,f as canonicalManifest,y as channelFromSecret,b as connectSealedChannel,w as createSealedTransport,p as digestFor,m as isBundleManifest,h as isSignedBundleManifest,g as verifyAttestation,_ as verifyBundleAsset,v as verifySignedBundleManifest};
@@ -0,0 +1 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./services/nodeSealedChannel/index.cjs");exports.createClientHandshake=e.createClientHandshake,exports.createHostHandshake=e.createHostHandshake;
@@ -0,0 +1,2 @@
1
+ import { ClientHandshake, HostHandshake, OpenResult, SealResult, SealedChannel, createClientHandshake, createHostHandshake } from "./services/nodeSealedChannel/index.mjs";
2
+ export { type ClientHandshake, type HostHandshake, type OpenResult, type SealResult, type SealedChannel, createClientHandshake, createHostHandshake };
@@ -0,0 +1 @@
1
+ export{createClientHandshake,createHostHandshake}from"./services/nodeSealedChannel/index.mjs";
@@ -1,2 +1,2 @@
1
- const e=require("../../types/sealedChannel.cjs"),t=require("../browserSealedChannel/index.cjs"),n=require("../serialQueue/index.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,p=!1,m=n.createSerialQueue(),h=n.createSerialQueue(),g=n.createSerialQueue(),_,v=t=>{d=e.describeSealedFailure(t)},y=async e=>{if(u===void 0)return;let t=await u.seal(e);return t.ok?JSON.stringify(t.envelope):v(t.failure)},b=async e=>{if(u===void 0)return;let t;try{t=JSON.parse(e)}catch{return v(`malformed`)}let n=await u.open(t);return n.ok?n.plaintext:v(n.failure)};return{active:()=>u!==void 0,invalidate(){p=!0},onAuthorizationFailure(e){f=e},lastFailure:()=>d,async connect(e){let n=await t.connectSealedChannel(e);if(n===void 0)d=`This browser could not establish a sealed channel.`;else return u=n.channel,p=!1,d=void 0,n.clientPublicKey},relayRequestsThrough(e){_=e},reset(){p=!1,u=void 0,d=void 0},async sealText(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await m.run(async()=>{let t=await y(r.encode(e));if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return t})},async openText(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await h.run(async()=>{let t=await b(e);return t===void 0?void 0:i.decode(t)})},async sealBinary(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await m.run(async()=>{let t=await y(e);if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return r.encode(t)})},async openBinary(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await h.run(async()=>await b(i.decode(e)))},async fetch(e,t){if(t?.signal?.throwIfAborted(),p)throw Error(`Sign in to Po again.`);let n=_;if(u===void 0||n===void 0)return await fetch(e,t);let m=new AbortController,h=()=>m.abort(t?.signal?.reason),v=setTimeout(()=>m.abort(new DOMException(`The request timed out.`,`TimeoutError`)),3e4);t?.signal?.addEventListener(`abort`,h,{once:!0}),t?.signal?.aborted&&h();let x,S=new Promise((e,t)=>{x=()=>t(m.signal.reason),m.signal.addEventListener(`abort`,x,{once:!0}),m.signal.aborted&&x()}),C=u,w=()=>{if(m.signal.throwIfAborted(),p||u!==C)throw Error(`The sealed channel was replaced.`)};try{return w(),await Promise.race([g.run(()=>Promise.race([(async()=>{w();let u=await s(t?.body);w();let h={v:1,method:(t?.method??`GET`).toUpperCase(),target:c(e),headers:Array.from(new Headers(t?.headers).entries()),...u===void 0?{}:{body:a(u)}},g=await y(r.encode(JSON.stringify(h)));if(w(),g===void 0)return new Response(JSON.stringify({error:d}),{status:502});let _=await fetch(n,{method:`POST`,signal:m.signal,credentials:`same-origin`,headers:{"content-type":`application/json`},body:g});if(w(),_.status===401||_.status===403)throw p=!0,d=`Sign in to Po again.`,f?.(),Error(d);let v=await _.text();w();let x=await b(v);if(w(),x===void 0)return new Response(JSON.stringify({error:d}),{status:502});let S;try{S=JSON.parse(i.decode(x))}catch{S=void 0}if(!l(S))return d=`The sealed HTTP response was malformed.`,new Response(JSON.stringify({error:d}),{status:502});let C=o(S.body);return new Response(S.status===204||S.status===205||S.status===304?null:C,{status:S.status,headers:S.headers})})(),S])),S])}finally{clearTimeout(v),t?.signal?.removeEventListener(`abort`,h),x&&m.signal.removeEventListener(`abort`,x)}}}}const d=u();exports.createSealedTransport=u,exports.sealedTransport=d;
1
+ const e=require("../../types/sealedChannel.cjs"),t=require("../browserSealedChannel/index.cjs"),n=require("../serialQueue/index.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,p=!1,m=n.createSerialQueue(),h=n.createSerialQueue(),g=n.createSerialQueue(),_,v=t=>{d=e.describeSealedFailure(t)},y=async e=>{if(u===void 0)return;let t=await u.seal(e);return t.ok?JSON.stringify(t.envelope):v(t.failure)},b=async e=>{if(u===void 0)return;let t;try{t=JSON.parse(e)}catch{return v(`malformed`)}let n=await u.open(t);return n.ok?n.plaintext:v(n.failure)};return{active:()=>u!==void 0,invalidate(){p=!0},onAuthorizationFailure(e){f=e},lastFailure:()=>d,async connect(e){let n=await t.connectSealedChannel(e);if(n===void 0)d=`This browser could not establish a sealed channel.`;else return u=n.channel,p=!1,d=void 0,n.clientPublicKey},relayRequestsThrough(e){_=e},reset(){p=!1,u=void 0,d=void 0},async sealText(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await m.run(async()=>{let t=await y(r.encode(e));if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return t})},async openText(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await h.run(async()=>{let t=await b(e);return t===void 0?void 0:i.decode(t)})},async sealBinary(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await m.run(async()=>{let t=await y(e);if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return r.encode(t)})},async openBinary(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await h.run(async()=>await b(i.decode(e)))},async fetch(e,t){if(t?.signal?.throwIfAborted(),p)throw Error(`Sign in to Po again.`);let n=_;if(u===void 0||n===void 0)return await fetch(e,t);let m=new AbortController,h=()=>m.abort(t?.signal?.reason),v=setTimeout(()=>m.abort(new DOMException(`The request timed out.`,`TimeoutError`)),3e4);t?.signal?.addEventListener(`abort`,h,{once:!0}),t?.signal?.aborted&&h();let x,S=new Promise((e,t)=>{x=()=>t(m.signal.reason),m.signal.addEventListener(`abort`,x,{once:!0}),m.signal.aborted&&x()}),C=u,w=()=>{if(m.signal.throwIfAborted(),p||u!==C)throw Error(`The sealed channel was replaced.`)};try{return w(),await Promise.race([g.run(()=>Promise.race([(async()=>{w();let u=await s(t?.body);w();let h={v:1,method:(t?.method??`GET`).toUpperCase(),target:c(e),headers:Array.from(new Headers(t?.headers).entries()),...u===void 0?{}:{body:a(u)}},g=await y(r.encode(JSON.stringify(h)));if(w(),g===void 0)return new Response(JSON.stringify({error:d}),{status:502});let _=await fetch(n,{method:`POST`,signal:m.signal,credentials:`same-origin`,headers:{"content-type":`application/json`},body:g});if(w(),_.status===401||_.status===403)throw p=!0,d=`Sign in to Po again.`,f?.(),Error(d);let v=await _.text();w();let x=await b(v);if(w(),x===void 0)return new Response(JSON.stringify({error:d}),{status:502});let S;try{S=JSON.parse(i.decode(x))}catch{S=void 0}if(!l(S))return d=`The sealed HTTP response was malformed.`,new Response(JSON.stringify({error:d}),{status:502});let C=o(S.body);return new Response(S.status===204||S.status===205||S.status===304?null:C,{status:S.status,headers:S.headers})})(),S])),S])}finally{clearTimeout(v),t?.signal?.removeEventListener(`abort`,h),x&&m.signal.removeEventListener(`abort`,x)}}}}const d=/* @__PURE__ */ new Map;function f(e,t=``){return d.set(t,e),()=>{d.get(t)===e&&d.delete(t)}}function p(e){return{active:()=>d.get(e)?.active()??!1,async fetch(t,n){n?.signal?.throwIfAborted();let r=d.get(e);if(!r)throw Error(`The host transport is not connected.`);let i=m(t,e);return r.fetch(i.input,n)}}}function m(e,t){let n=t,r=e=>{let t=decodeURIComponent(e),r=t.indexOf(`/`);if(r<1)return e;let i=t.slice(0,r);if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(i))return e;if(n!==void 0&&i!==n)throw Error(`The request spans different hosts.`);return n=i,encodeURIComponent(t.slice(r+1))},i=e.indexOf(`?`),a=(i===-1?e:e.slice(0,i)).replace(/(\/(?:workspaces|sessions)\/)([^/]+)/gu,(e,t,n)=>t+r(n)),o=i===-1?``:e.slice(i).replace(/([?&]session=)([^&]*)/gu,(e,t,n)=>t+r(n));return{hostId:n,input:a+o}}const h={active:()=>d.get(``)?.active()??!1,async fetch(e,t){t?.signal?.throwIfAborted();let n=m(e);if(n.hostId!==void 0)return p(n.hostId).fetch(n.input,t);let r=d.get(``);return r?r.fetch(e,t):fetch(e,t)}};exports.bindBrowserTransport=f,exports.browserTransport=h,exports.browserTransportForHost=p,exports.createSealedTransport=u;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["createSerialQueue","describeSealedFailure","connectSealedChannel","require_sealedChannel","require_index","require_index$1","encoder","decoder","encodeBase64","binary","byte","bytes","decodeBase64","value","character","bodyBytes","body","requestTarget","input","isSealedHttpResponse","candidate","header","part","createSealedTransport","channel","failure","authorizationFailure","unauthorized","outbound","inbound","exchange","relayPath","note","reason","sealBytes","sealed","openBytes","envelope","text","opened","listener","connected","hostPublicKey","path","init","gateway","controller","cancel","deadline","rejectAbort","aborted","reject","expectedChannel","check","request","relayResponse","responseText","decoded","sealedTransport"],"sources":["../../../src/services/sealedTransport/index.ts"],"sourcesContent":["import { describeSealedFailure, type SealedFailure } from '../../types/sealedChannel';\nimport { connectSealedChannel, type SealedChannel } from '../browserSealedChannel';\nimport { createSerialQueue } from '../serialQueue';\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';\nconst FETCH_TIMEOUT_MS = 30_000;\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 onAuthorizationFailure: (listener: () => void) => void;\n invalidate: () => void;\n} {\n let channel: SealedChannel | undefined;\n let failure: string | undefined;\n let authorizationFailure: (() => void) | undefined;\n let unauthorized = false;\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 invalidate() {\n unauthorized = true;\n },\n onAuthorizationFailure(listener) {\n authorizationFailure = listener;\n },\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 unauthorized = false;\n failure = undefined;\n return connected.clientPublicKey;\n },\n\n relayRequestsThrough(path) {\n relayPath = path;\n },\n\n reset() {\n unauthorized = false;\n channel = undefined;\n failure = undefined;\n },\n\n async sealText(text) {\n if (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 init?.signal?.throwIfAborted();\n if (unauthorized) throw new Error('Sign in to Po again.');\n const gateway = relayPath;\n if (channel === undefined || gateway === undefined) return await fetch(input, init);\n const controller = new AbortController();\n const cancel = () => controller.abort(init?.signal?.reason);\n const deadline = setTimeout(\n () => controller.abort(new DOMException('The request timed out.', 'TimeoutError')),\n FETCH_TIMEOUT_MS,\n );\n init?.signal?.addEventListener('abort', cancel, { once: true });\n if (init?.signal?.aborted) cancel();\n let rejectAbort: (() => void) | undefined;\n const aborted = new Promise<never>((_resolve, reject) => {\n rejectAbort = () => reject(controller.signal.reason);\n controller.signal.addEventListener('abort', rejectAbort, { once: true });\n if (controller.signal.aborted) rejectAbort();\n });\n const expectedChannel = channel;\n const check = () => {\n controller.signal.throwIfAborted();\n if (unauthorized || channel !== expectedChannel) throw new Error('The sealed channel was replaced.');\n };\n try {\n check();\n return await Promise.race([\n exchange.run(() =>\n Promise.race([\n (async () => {\n check();\n const bytes = await bodyBytes(init?.body);\n check();\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 check();\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 signal: controller.signal,\n credentials: 'same-origin',\n headers: { 'content-type': JSON_CONTENT_TYPE },\n body: sealed,\n });\n check();\n // These are unsealed gateway refusals, not the inner application's authorization response.\n if (relayResponse.status === 401 || relayResponse.status === 403) {\n unauthorized = true;\n failure = 'Sign in to Po again.';\n authorizationFailure?.();\n throw new Error(failure);\n }\n const responseText = await relayResponse.text();\n check();\n const opened = await openBytes(responseText);\n check();\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(\n decoded.status === 204 || decoded.status === 205 || decoded.status === 304 ? null : body,\n {\n status: decoded.status,\n headers: decoded.headers,\n },\n );\n })(),\n aborted,\n ]),\n ),\n aborted,\n ]);\n } finally {\n clearTimeout(deadline);\n init?.signal?.removeEventListener('abort', cancel);\n if (rejectAbort) controller.signal.removeEventListener('abort', rejectAbort);\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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"}
1
+ {"version":3,"file":"index.cjs","names":["createSerialQueue","describeSealedFailure","connectSealedChannel","require_sealedChannel","require_index","require_index$1","encoder","decoder","encodeBase64","binary","byte","bytes","decodeBase64","value","character","bodyBytes","body","requestTarget","input","isSealedHttpResponse","candidate","header","part","createSealedTransport","channel","failure","authorizationFailure","unauthorized","outbound","inbound","exchange","relayPath","note","reason","sealBytes","sealed","openBytes","envelope","text","opened","listener","connected","hostPublicKey","path","init","gateway","controller","cancel","deadline","rejectAbort","aborted","reject","expectedChannel","check","request","relayResponse","responseText","decoded","browserBindings","bindBrowserTransport","hostId","transport","browserTransportForHost","binding","route","browserRequestRoute","expectedHost","local","id","encoded","slash","queryAt","routed","prefix","query","browserTransport"],"sources":["../../../src/services/sealedTransport/index.ts"],"sourcesContent":["import { describeSealedFailure, type SealedFailure } from '../../types/sealedChannel';\nimport { connectSealedChannel, type SealedChannel } from '../browserSealedChannel';\nimport { createSerialQueue } from '../serialQueue';\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';\nconst FETCH_TIMEOUT_MS = 30_000;\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 onAuthorizationFailure: (listener: () => void) => void;\n invalidate: () => void;\n} {\n let channel: SealedChannel | undefined;\n let failure: string | undefined;\n let authorizationFailure: (() => void) | undefined;\n let unauthorized = false;\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 invalidate() {\n unauthorized = true;\n },\n onAuthorizationFailure(listener) {\n authorizationFailure = listener;\n },\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 unauthorized = false;\n failure = undefined;\n return connected.clientPublicKey;\n },\n\n relayRequestsThrough(path) {\n relayPath = path;\n },\n\n reset() {\n unauthorized = false;\n channel = undefined;\n failure = undefined;\n },\n\n async sealText(text) {\n if (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 init?.signal?.throwIfAborted();\n if (unauthorized) throw new Error('Sign in to Po again.');\n const gateway = relayPath;\n if (channel === undefined || gateway === undefined) return await fetch(input, init);\n const controller = new AbortController();\n const cancel = () => controller.abort(init?.signal?.reason);\n const deadline = setTimeout(\n () => controller.abort(new DOMException('The request timed out.', 'TimeoutError')),\n FETCH_TIMEOUT_MS,\n );\n init?.signal?.addEventListener('abort', cancel, { once: true });\n if (init?.signal?.aborted) cancel();\n let rejectAbort: (() => void) | undefined;\n const aborted = new Promise<never>((_resolve, reject) => {\n rejectAbort = () => reject(controller.signal.reason);\n controller.signal.addEventListener('abort', rejectAbort, { once: true });\n if (controller.signal.aborted) rejectAbort();\n });\n const expectedChannel = channel;\n const check = () => {\n controller.signal.throwIfAborted();\n if (unauthorized || channel !== expectedChannel) throw new Error('The sealed channel was replaced.');\n };\n try {\n check();\n return await Promise.race([\n exchange.run(() =>\n Promise.race([\n (async () => {\n check();\n const bytes = await bodyBytes(init?.body);\n check();\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 check();\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 signal: controller.signal,\n credentials: 'same-origin',\n headers: { 'content-type': JSON_CONTENT_TYPE },\n body: sealed,\n });\n check();\n // These are unsealed gateway refusals, not the inner application's authorization response.\n if (relayResponse.status === 401 || relayResponse.status === 403) {\n unauthorized = true;\n failure = 'Sign in to Po again.';\n authorizationFailure?.();\n throw new Error(failure);\n }\n const responseText = await relayResponse.text();\n check();\n const opened = await openBytes(responseText);\n check();\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(\n decoded.status === 204 || decoded.status === 205 || decoded.status === 304 ? null : body,\n {\n status: decoded.status,\n headers: decoded.headers,\n },\n );\n })(),\n aborted,\n ]),\n ),\n aborted,\n ]);\n } finally {\n clearTimeout(deadline);\n init?.signal?.removeEventListener('abort', cancel);\n if (rejectAbort) controller.signal.removeEventListener('abort', rejectAbort);\n }\n },\n };\n}\n\n/** Only the request capability is shared. Each host owns its cryptographic transport. */\ntype BrowserTransport = Pick<SealedTransport, 'fetch' | 'active'>;\nconst browserBindings = new Map<string, BrowserTransport>();\nconst DEFAULT_HOST = '';\n\n/** An identity-safe disposer cannot unbind a newer connection. */\nexport function bindBrowserTransport(transport: BrowserTransport, hostId = DEFAULT_HOST): () => void {\n browserBindings.set(hostId, transport);\n return () => {\n if (browserBindings.get(hostId) === transport) browserBindings.delete(hostId);\n };\n}\n\n/** Selects a host explicitly for global requests, never by whichever session is focused. */\nexport function browserTransportForHost(hostId: string): BrowserTransport {\n return {\n active: () => browserBindings.get(hostId)?.active() ?? false,\n async fetch(input, init) {\n init?.signal?.throwIfAborted();\n const binding = browserBindings.get(hostId);\n if (!binding) throw new Error('The host transport is not connected.');\n const route = browserRequestRoute(input, hostId);\n return binding.fetch(route.input, init);\n },\n };\n}\n\n/** Encoded session and workspace identities are runtime addresses, not wire values. */\nfunction browserRequestRoute(input: string, expectedHost?: string): { hostId?: string; input: string } {\n let hostId = expectedHost;\n const local = (encoded: string): string => {\n const id = decodeURIComponent(encoded);\n const slash = id.indexOf('/');\n if (slash < 1) return encoded;\n const candidate = id.slice(0, slash);\n // A local ID containing a slash is not a host address.\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(candidate)) return encoded;\n if (hostId !== undefined && candidate !== hostId) throw new Error('The request spans different hosts.');\n hostId = candidate;\n return encodeURIComponent(id.slice(slash + 1));\n };\n const queryAt = input.indexOf('?');\n const path = queryAt === -1 ? input : input.slice(0, queryAt);\n const routed = path.replace(\n /(\\/(?:workspaces|sessions)\\/)([^/]+)/gu,\n (_match, prefix: string, id: string) => prefix + local(id),\n );\n const query =\n queryAt === -1\n ? ''\n : input\n .slice(queryAt)\n .replace(/([?&]session=)([^&]*)/gu, (_match, prefix: string, id: string) => prefix + local(id));\n return { hostId, input: routed + query };\n}\n\n/** Legacy unqualified URLs use only the explicitly bound default host. */\nexport const browserTransport: BrowserTransport = {\n active: () => browserBindings.get(DEFAULT_HOST)?.active() ?? false,\n async fetch(input, init) {\n init?.signal?.throwIfAborted();\n const route = browserRequestRoute(input);\n if (route.hostId !== undefined) return browserTransportForHost(route.hostId).fetch(route.input, init);\n const binding = browserBindings.get(DEFAULT_HOST);\n return binding ? binding.fetch(input, init) : fetch(input, init);\n },\n};\n"],"mappings":"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"}
@@ -21,26 +21,14 @@ export declare function createSealedTransport(): SealedTransport & {
21
21
  onAuthorizationFailure: (listener: () => void) => void;
22
22
  invalidate: () => void;
23
23
  };
24
- /**
25
- * The page's one sealed channel.
26
- *
27
- * A module singleton rather than an instance per caller, because the cockpit
28
- * and every plugin are bundled into the same page and all of them must share
29
- * one pair of nonce counters. Two transports over one handshake would each
30
- * start counting at zero and the receiver would reject the second as a replay.
31
- *
32
- * Inactive until a handshake completes, and inactive forever on the loopback
33
- * listener, so `sealedTransport.fetch` is a plain `fetch` there.
34
- */
35
- export declare const sealedTransport: SealedTransport & {
36
- /** Completes the handshake against the host key from the QR. */
37
- connect: (hostPublicKey: string) => Promise<string | undefined>;
38
- /** Drops the channel, so everything falls back to pass-through. */
39
- reset: () => void;
40
- /** Selects the same-origin endpoint that carries sealed HTTP exchanges. */
41
- relayRequestsThrough: (path: string) => void;
42
- onAuthorizationFailure: (listener: () => void) => void;
43
- invalidate: () => void;
44
- };
24
+ /** Only the request capability is shared. Each host owns its cryptographic transport. */
25
+ type BrowserTransport = Pick<SealedTransport, 'fetch' | 'active'>;
26
+ /** An identity-safe disposer cannot unbind a newer connection. */
27
+ export declare function bindBrowserTransport(transport: BrowserTransport, hostId?: string): () => void;
28
+ /** Selects a host explicitly for global requests, never by whichever session is focused. */
29
+ export declare function browserTransportForHost(hostId: string): BrowserTransport;
30
+ /** Legacy unqualified URLs use only the explicitly bound default host. */
31
+ export declare const browserTransport: BrowserTransport;
45
32
  //#endregion
33
+ export {};
46
34
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/sealedTransport/index.ts"],"mappings":";iBAsBiB;;EAEf;EACA,SAAS,eAAe;;EAExB,SAAS,eAAe;EACxB,WAAW,OAAO,aAAa,QAAQ;EACvC,WAAW,OAAO,aAAa,QAAQ;;EAEvC,MAAM,eAAe,OAAO,cAAc,QAAQ;EAClD;;wBA4Dc,yBAAyB;;EAEvC,UAAU,0BAA0B;;EAEpC;;EAEA,uBAAuB;EACvB,yBAAyB;EACzB;;;;;;;;;;;;;qBAuNW,iBAAe;;EA7NjB,UAAC,0BAA0B;;EAE7B;;EAEe,uBAAC;EACC,yBAAC;EACb"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/sealedTransport/index.ts"],"mappings":";iBAsBiB;;EAEf;EACA,SAAS,eAAe;;EAExB,SAAS,eAAe;EACxB,WAAW,OAAO,aAAa,QAAQ;EACvC,WAAW,OAAO,aAAa,QAAQ;;EAEvC,MAAM,eAAe,OAAO,cAAc,QAAQ;EAClD;;wBA4Dc,yBAAyB;;EAEvC,UAAU,0BAA0B;;EAEpC;;EAEA,uBAAuB;EACvB,yBAAyB;EACzB;;;KA6MG,mBAAmB,KAAK;;wBAKb,qBAAqB,WAAW,kBAAkB;;wBAQlD,wBAAwB,iBAAiB;;qBA2C5C,kBAAkB"}
@@ -1,2 +1,2 @@
1
- import{describeSealedFailure as e}from"../../types/sealedChannel.mjs";import{connectSealedChannel as t}from"../browserSealedChannel/index.mjs";import{createSerialQueue as n}from"../serialQueue/index.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,p=!1,m=n(),h=n(),g=n(),_,v=t=>{d=e(t)},y=async e=>{if(u===void 0)return;let t=await u.seal(e);return t.ok?JSON.stringify(t.envelope):v(t.failure)},b=async e=>{if(u===void 0)return;let t;try{t=JSON.parse(e)}catch{return v(`malformed`)}let n=await u.open(t);return n.ok?n.plaintext:v(n.failure)};return{active:()=>u!==void 0,invalidate(){p=!0},onAuthorizationFailure(e){f=e},lastFailure:()=>d,async connect(e){let n=await t(e);if(n===void 0)d=`This browser could not establish a sealed channel.`;else return u=n.channel,p=!1,d=void 0,n.clientPublicKey},relayRequestsThrough(e){_=e},reset(){p=!1,u=void 0,d=void 0},async sealText(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await m.run(async()=>{let t=await y(r.encode(e));if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return t})},async openText(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await h.run(async()=>{let t=await b(e);return t===void 0?void 0:i.decode(t)})},async sealBinary(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await m.run(async()=>{let t=await y(e);if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return r.encode(t)})},async openBinary(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await h.run(async()=>await b(i.decode(e)))},async fetch(e,t){if(t?.signal?.throwIfAborted(),p)throw Error(`Sign in to Po again.`);let n=_;if(u===void 0||n===void 0)return await fetch(e,t);let m=new AbortController,h=()=>m.abort(t?.signal?.reason),v=setTimeout(()=>m.abort(new DOMException(`The request timed out.`,`TimeoutError`)),3e4);t?.signal?.addEventListener(`abort`,h,{once:!0}),t?.signal?.aborted&&h();let x,S=new Promise((e,t)=>{x=()=>t(m.signal.reason),m.signal.addEventListener(`abort`,x,{once:!0}),m.signal.aborted&&x()}),C=u,w=()=>{if(m.signal.throwIfAborted(),p||u!==C)throw Error(`The sealed channel was replaced.`)};try{return w(),await Promise.race([g.run(()=>Promise.race([(async()=>{w();let u=await s(t?.body);w();let h={v:1,method:(t?.method??`GET`).toUpperCase(),target:c(e),headers:Array.from(new Headers(t?.headers).entries()),...u===void 0?{}:{body:a(u)}},g=await y(r.encode(JSON.stringify(h)));if(w(),g===void 0)return new Response(JSON.stringify({error:d}),{status:502});let _=await fetch(n,{method:`POST`,signal:m.signal,credentials:`same-origin`,headers:{"content-type":`application/json`},body:g});if(w(),_.status===401||_.status===403)throw p=!0,d=`Sign in to Po again.`,f?.(),Error(d);let v=await _.text();w();let x=await b(v);if(w(),x===void 0)return new Response(JSON.stringify({error:d}),{status:502});let S;try{S=JSON.parse(i.decode(x))}catch{S=void 0}if(!l(S))return d=`The sealed HTTP response was malformed.`,new Response(JSON.stringify({error:d}),{status:502});let C=o(S.body);return new Response(S.status===204||S.status===205||S.status===304?null:C,{status:S.status,headers:S.headers})})(),S])),S])}finally{clearTimeout(v),t?.signal?.removeEventListener(`abort`,h),x&&m.signal.removeEventListener(`abort`,x)}}}}const d=u();export{u as createSealedTransport,d as sealedTransport};
1
+ import{describeSealedFailure as e}from"../../types/sealedChannel.mjs";import{connectSealedChannel as t}from"../browserSealedChannel/index.mjs";import{createSerialQueue as n}from"../serialQueue/index.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,p=!1,m=n(),h=n(),g=n(),_,v=t=>{d=e(t)},y=async e=>{if(u===void 0)return;let t=await u.seal(e);return t.ok?JSON.stringify(t.envelope):v(t.failure)},b=async e=>{if(u===void 0)return;let t;try{t=JSON.parse(e)}catch{return v(`malformed`)}let n=await u.open(t);return n.ok?n.plaintext:v(n.failure)};return{active:()=>u!==void 0,invalidate(){p=!0},onAuthorizationFailure(e){f=e},lastFailure:()=>d,async connect(e){let n=await t(e);if(n===void 0)d=`This browser could not establish a sealed channel.`;else return u=n.channel,p=!1,d=void 0,n.clientPublicKey},relayRequestsThrough(e){_=e},reset(){p=!1,u=void 0,d=void 0},async sealText(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await m.run(async()=>{let t=await y(r.encode(e));if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return t})},async openText(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await h.run(async()=>{let t=await b(e);return t===void 0?void 0:i.decode(t)})},async sealBinary(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await m.run(async()=>{let t=await y(e);if(t===void 0)throw Error(d??`The active sealed channel could not encrypt a message.`);return r.encode(t)})},async openBinary(e){if(p)throw Error(`Sign in to Po again.`);return u===void 0?e:await h.run(async()=>await b(i.decode(e)))},async fetch(e,t){if(t?.signal?.throwIfAborted(),p)throw Error(`Sign in to Po again.`);let n=_;if(u===void 0||n===void 0)return await fetch(e,t);let m=new AbortController,h=()=>m.abort(t?.signal?.reason),v=setTimeout(()=>m.abort(new DOMException(`The request timed out.`,`TimeoutError`)),3e4);t?.signal?.addEventListener(`abort`,h,{once:!0}),t?.signal?.aborted&&h();let x,S=new Promise((e,t)=>{x=()=>t(m.signal.reason),m.signal.addEventListener(`abort`,x,{once:!0}),m.signal.aborted&&x()}),C=u,w=()=>{if(m.signal.throwIfAborted(),p||u!==C)throw Error(`The sealed channel was replaced.`)};try{return w(),await Promise.race([g.run(()=>Promise.race([(async()=>{w();let u=await s(t?.body);w();let h={v:1,method:(t?.method??`GET`).toUpperCase(),target:c(e),headers:Array.from(new Headers(t?.headers).entries()),...u===void 0?{}:{body:a(u)}},g=await y(r.encode(JSON.stringify(h)));if(w(),g===void 0)return new Response(JSON.stringify({error:d}),{status:502});let _=await fetch(n,{method:`POST`,signal:m.signal,credentials:`same-origin`,headers:{"content-type":`application/json`},body:g});if(w(),_.status===401||_.status===403)throw p=!0,d=`Sign in to Po again.`,f?.(),Error(d);let v=await _.text();w();let x=await b(v);if(w(),x===void 0)return new Response(JSON.stringify({error:d}),{status:502});let S;try{S=JSON.parse(i.decode(x))}catch{S=void 0}if(!l(S))return d=`The sealed HTTP response was malformed.`,new Response(JSON.stringify({error:d}),{status:502});let C=o(S.body);return new Response(S.status===204||S.status===205||S.status===304?null:C,{status:S.status,headers:S.headers})})(),S])),S])}finally{clearTimeout(v),t?.signal?.removeEventListener(`abort`,h),x&&m.signal.removeEventListener(`abort`,x)}}}}const d=/* @__PURE__ */ new Map;function f(e,t=``){return d.set(t,e),()=>{d.get(t)===e&&d.delete(t)}}function p(e){return{active:()=>d.get(e)?.active()??!1,async fetch(t,n){n?.signal?.throwIfAborted();let r=d.get(e);if(!r)throw Error(`The host transport is not connected.`);let i=m(t,e);return r.fetch(i.input,n)}}}function m(e,t){let n=t,r=e=>{let t=decodeURIComponent(e),r=t.indexOf(`/`);if(r<1)return e;let i=t.slice(0,r);if(!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(i))return e;if(n!==void 0&&i!==n)throw Error(`The request spans different hosts.`);return n=i,encodeURIComponent(t.slice(r+1))},i=e.indexOf(`?`),a=(i===-1?e:e.slice(0,i)).replace(/(\/(?:workspaces|sessions)\/)([^/]+)/gu,(e,t,n)=>t+r(n)),o=i===-1?``:e.slice(i).replace(/([?&]session=)([^&]*)/gu,(e,t,n)=>t+r(n));return{hostId:n,input:a+o}}const h={active:()=>d.get(``)?.active()??!1,async fetch(e,t){t?.signal?.throwIfAborted();let n=m(e);if(n.hostId!==void 0)return p(n.hostId).fetch(n.input,t);let r=d.get(``);return r?r.fetch(e,t):fetch(e,t)}};export{f as bindBrowserTransport,h as browserTransport,p as browserTransportForHost,u as createSealedTransport};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["encoder","decoder","encodeBase64","binary","byte","bytes","decodeBase64","value","character","bodyBytes","body","requestTarget","input","isSealedHttpResponse","candidate","header","part","createSealedTransport","channel","failure","authorizationFailure","unauthorized","outbound","createSerialQueue","inbound","exchange","relayPath","note","describeSealedFailure","reason","sealBytes","sealed","openBytes","envelope","text","opened","listener","connected","connectSealedChannel","hostPublicKey","path","init","gateway","controller","cancel","deadline","rejectAbort","aborted","reject","expectedChannel","check","request","relayResponse","responseText","decoded","sealedTransport"],"sources":["../../../src/services/sealedTransport/index.ts"],"sourcesContent":["import { describeSealedFailure, type SealedFailure } from '../../types/sealedChannel';\nimport { connectSealedChannel, type SealedChannel } from '../browserSealedChannel';\nimport { createSerialQueue } from '../serialQueue';\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';\nconst FETCH_TIMEOUT_MS = 30_000;\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 onAuthorizationFailure: (listener: () => void) => void;\n invalidate: () => void;\n} {\n let channel: SealedChannel | undefined;\n let failure: string | undefined;\n let authorizationFailure: (() => void) | undefined;\n let unauthorized = false;\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 invalidate() {\n unauthorized = true;\n },\n onAuthorizationFailure(listener) {\n authorizationFailure = listener;\n },\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 unauthorized = false;\n failure = undefined;\n return connected.clientPublicKey;\n },\n\n relayRequestsThrough(path) {\n relayPath = path;\n },\n\n reset() {\n unauthorized = false;\n channel = undefined;\n failure = undefined;\n },\n\n async sealText(text) {\n if (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 init?.signal?.throwIfAborted();\n if (unauthorized) throw new Error('Sign in to Po again.');\n const gateway = relayPath;\n if (channel === undefined || gateway === undefined) return await fetch(input, init);\n const controller = new AbortController();\n const cancel = () => controller.abort(init?.signal?.reason);\n const deadline = setTimeout(\n () => controller.abort(new DOMException('The request timed out.', 'TimeoutError')),\n FETCH_TIMEOUT_MS,\n );\n init?.signal?.addEventListener('abort', cancel, { once: true });\n if (init?.signal?.aborted) cancel();\n let rejectAbort: (() => void) | undefined;\n const aborted = new Promise<never>((_resolve, reject) => {\n rejectAbort = () => reject(controller.signal.reason);\n controller.signal.addEventListener('abort', rejectAbort, { once: true });\n if (controller.signal.aborted) rejectAbort();\n });\n const expectedChannel = channel;\n const check = () => {\n controller.signal.throwIfAborted();\n if (unauthorized || channel !== expectedChannel) throw new Error('The sealed channel was replaced.');\n };\n try {\n check();\n return await Promise.race([\n exchange.run(() =>\n Promise.race([\n (async () => {\n check();\n const bytes = await bodyBytes(init?.body);\n check();\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 check();\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 signal: controller.signal,\n credentials: 'same-origin',\n headers: { 'content-type': JSON_CONTENT_TYPE },\n body: sealed,\n });\n check();\n // These are unsealed gateway refusals, not the inner application's authorization response.\n if (relayResponse.status === 401 || relayResponse.status === 403) {\n unauthorized = true;\n failure = 'Sign in to Po again.';\n authorizationFailure?.();\n throw new Error(failure);\n }\n const responseText = await relayResponse.text();\n check();\n const opened = await openBytes(responseText);\n check();\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(\n decoded.status === 204 || decoded.status === 205 || decoded.status === 304 ? null : body,\n {\n status: decoded.status,\n headers: decoded.headers,\n },\n );\n })(),\n aborted,\n ]),\n ),\n aborted,\n ]);\n } finally {\n clearTimeout(deadline);\n init?.signal?.removeEventListener('abort', cancel);\n if (rejectAbort) controller.signal.removeEventListener('abort', rejectAbort);\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":"4MAmBA,MAgBMA,EAAU,IAAI,YACdC,EAAU,IAAI,YAiBpB,SAASC,EAAa,EAA2B,CAC/C,IAAIC,EAAS,GACb,IAAK,IAAMC,KAAQC,EAAO,GAAU,OAAO,aAAaD,CAAI,EAC5D,OAAO,KAAKD,CAAM,CACpB,CAEA,SAASG,EAAa,EAA2B,CAC/C,IAAMH,EAAS,KAAKI,CAAK,EACzB,OAAO,WAAW,KAAKJ,EAAS,GAAcK,EAAU,WAAW,CAAC,CAAC,CACvE,CAEA,eAAeC,EAAU,EAA4D,CACnF,GAAIC,GAA+B,KACnC,IAAI,OAAOA,GAAS,SAAU,OAAOV,EAAQ,OAAOU,CAAI,EACxD,GAAIA,aAAgB,gBAAiB,OAAOV,EAAQ,OAAOU,EAAK,SAAS,CAAC,EAC1E,GAAIA,aAAgB,KAAM,OAAO,IAAI,WAAW,MAAMA,EAAK,YAAY,CAAC,EACxE,GAAIA,aAAgB,YAAa,OAAO,IAAI,WAAWA,CAAI,EAC3D,GAAI,YAAY,OAAOA,CAAI,EAAG,OAAO,IAAI,WAAWA,EAAK,OAAQA,EAAK,WAAYA,EAAK,UAAU,EACjG,MAAU,MAAM,oEAAoE,CAL5B,CAM1D,CAEA,SAASC,EAAc,EAAuB,CAC5C,GAAI,CAACC,EAAM,WAAW,GAAG,EAAG,MAAU,MAAM,qDAAqD,EACjG,OAAOA,CACT,CAEA,SAASC,EAAqB,EAA6C,CACzE,GAAI,OAAON,GAAU,WAAYA,EAAgB,MAAO,GACxD,IAAMO,EAAYP,EAClB,OACEO,EAAU,IAAM,GAChB,OAAOA,EAAU,QAAW,UAC5B,MAAM,QAAQA,EAAU,OAAO,GAC/BA,EAAU,QAAQ,MACf,GAAW,MAAM,QAAQC,CAAM,GAAKA,EAAO,SAAW,GAAKA,EAAO,MAAO,GAAS,OAAOC,GAAS,QAAQ,CAC7G,GACA,OAAOF,EAAU,MAAS,QAE9B,CACA,SAAgBG,GASd,CACA,IAAIC,EACAC,EACAC,EACAC,EAAe,GACbC,EAAWC,EAAkB,EAC7BC,EAAUD,EAAkB,EAC5BE,EAAWF,EAAkB,EAC/BG,EACEC,EAAQ,GAAqC,CAGjD,EAAUC,EAAsBC,CAAM,CAExC,EAEMC,EAAY,KAAO,IAAmD,CAC1E,GAAIZ,IAAY,IAAA,GAAW,OAC3B,IAAMa,EAAS,MAAMb,EAAQ,KAAKb,CAAK,EAEvC,OADK0B,EAAO,GACL,KAAK,UAAUA,EAAO,QAAQ,EADdJ,EAAKI,EAAO,OAAO,CAE5C,EAEMC,EAAY,KAAO,IAAkD,CACzE,GAAId,IAAY,IAAA,GAAW,OAC3B,IAAIe,EACJ,GAAI,CACF,EAAW,KAAK,MAAMC,CAAI,CAC5B,MAAQ,CACN,OAAOP,EAAK,WAAW,CACzB,CACA,IAAMQ,EAAS,MAAMjB,EAAQ,KAAKe,CAAQ,EAE1C,OADKE,EAAO,GACLA,EAAO,UADSR,EAAKQ,EAAO,OAAO,CAE5C,EAEA,MAAO,CACL,WAAcjB,IAAY,IAAA,GAC1B,YAAa,CACX,EAAe,EACjB,EACA,uBAAuB,EAAU,CAC/B,EAAuBkB,CACzB,EACA,gBAAmBjB,EAEnB,MAAM,QAAQ,EAAe,CAC3B,IAAMkB,EAAY,MAAMC,EAAqBC,CAAa,EAC1D,GAAIF,IAAc,IAAA,GAChB,EAAU,0DAMZ,MAHA,GAAUA,EAAU,QACpB,EAAe,GACf,EAAU,IAAA,GACHA,EAAU,eACnB,EAEA,qBAAqB,EAAM,CACzB,EAAYG,CACd,EAEA,OAAQ,CACN,EAAe,GACf,EAAU,IAAA,GACV,EAAU,IAAA,EACZ,EAEA,MAAM,SAAS,EAAM,CACnB,GAAInB,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBgB,EAC3B,MAAMZ,EAAS,IAAI,SAAY,CACpC,IAAMS,EAAS,MAAMD,EAAU9B,EAAQ,OAAOkC,CAAI,CAAC,EACnD,GAAIH,IAAW,IAAA,GAAW,MAAU,MAAMZ,GAAW,wDAAwD,EAC7G,OAAOY,CACT,CAAC,CACH,EAEA,MAAM,SAAS,EAAM,CACnB,GAAIV,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBgB,EAC3B,MAAMV,EAAQ,IAAI,SAAY,CACnC,IAAMW,EAAS,MAAMH,EAAUE,CAAI,EACnC,OAAOC,IAAW,IAAA,GAAY,IAAA,GAAYlC,EAAQ,OAAOkC,CAAM,CACjE,CAAC,CACH,EAEA,MAAM,WAAW,EAAO,CACtB,GAAId,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBb,EAC3B,MAAMiB,EAAS,IAAI,SAAY,CACpC,IAAMS,EAAS,MAAMD,EAAUzB,CAAK,EACpC,GAAI0B,IAAW,IAAA,GAAW,MAAU,MAAMZ,GAAW,wDAAwD,EAC7G,OAAOnB,EAAQ,OAAO+B,CAAM,CAC9B,CAAC,CACH,EAEA,MAAM,WAAW,EAAO,CACtB,GAAIV,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBb,EAC3B,MAAMmB,EAAQ,IAAI,SAAY,MAAMQ,EAAU/B,EAAQ,OAAOI,CAAK,CAAC,CAAC,CAC7E,EAEA,MAAM,MAAM,EAAO,EAAM,CAEvB,GADAoC,GAAM,QAAQ,eAAe,EACzBpB,EAAc,MAAU,MAAM,sBAAsB,EACxD,IAAMqB,EAAUhB,EAChB,GAAIR,IAAY,IAAA,IAAawB,IAAY,IAAA,GAAW,OAAO,MAAM,MAAM9B,EAAO6B,CAAI,EAClF,IAAME,EAAa,IAAI,gBACjBC,MAAeD,EAAW,MAAMF,GAAM,QAAQ,MAAM,EACpDI,EAAW,eACTF,EAAW,MAAM,IAAI,aAAa,yBAA0B,cAAc,CAAC,EACjF,GACF,EACAF,GAAM,QAAQ,iBAAiB,QAASG,EAAQ,CAAE,KAAM,EAAK,CAAC,EAC1DH,GAAM,QAAQ,SAASG,EAAO,EAClC,IAAIE,EACEC,EAAU,IAAI,SAAgB,EAAU,IAAW,CACvD,MAAoBC,EAAOL,EAAW,OAAO,MAAM,EACnDA,EAAW,OAAO,iBAAiB,QAASG,EAAa,CAAE,KAAM,EAAK,CAAC,EACnEH,EAAW,OAAO,SAASG,EAAY,CAC7C,CAAC,EACKG,EAAkB/B,EAClBgC,MAAc,CAElB,GADAP,EAAW,OAAO,eAAe,EAC7BtB,GAAgBH,IAAY+B,EAAiB,MAAU,MAAM,kCAAkC,CACrG,EACA,GAAI,CAEF,OADAC,EAAM,EACC,MAAM,QAAQ,KAAK,CACxBzB,EAAS,QACP,QAAQ,KAAK,EACV,SAAY,CACXyB,EAAM,EACN,IAAM7C,EAAQ,MAAMI,EAAUgC,GAAM,IAAI,EACxCS,EAAM,EACN,IAAMC,EAA6B,CACjC,EAAG,EACH,QAASV,GAAM,QAAU,MAAA,CAAO,YAAY,EAC5C,OAAQ9B,EAAcC,CAAK,EAC3B,QAAS,MAAM,KAAK,IAAI,QAAQ6B,GAAM,OAAO,CAAC,CAAC,QAAQ,CAAC,EACxD,GAAIpC,IAAU,IAAA,GAAY,CAAC,EAAI,CAAE,KAAMH,EAAaG,CAAK,CAAE,CAC7D,EACM0B,EAAS,MAAMD,EAAU9B,EAAQ,OAAO,KAAK,UAAUmD,CAAO,CAAC,CAAC,EAEtE,GADAD,EAAM,EACFnB,IAAW,IAAA,GACb,OAAO,IAAI,SAAS,KAAK,UAAU,CAAE,MAAOZ,CAAQ,CAAC,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEzE,IAAMiC,EAAgB,MAAM,MAAMV,EAAS,CACzC,OAAQ,OACR,OAAQC,EAAW,OACnB,YAAa,cACb,QAAS,CAAE,eAAgB,kBAAkB,EAC7C,KAAMZ,CACR,CAAC,EAGD,GAFAmB,EAAM,EAEFE,EAAc,SAAW,KAAOA,EAAc,SAAW,IAI3D,KAHA,GAAe,GACf,EAAU,uBACVhC,IAAuB,EACb,MAAMD,CAAO,EAEzB,IAAMkC,EAAe,MAAMD,EAAc,KAAK,EAC9CF,EAAM,EACN,IAAMf,EAAS,MAAMH,EAAUqB,CAAY,EAE3C,GADAH,EAAM,EACFf,IAAW,IAAA,GACb,OAAO,IAAI,SAAS,KAAK,UAAU,CAAE,MAAOhB,CAAQ,CAAC,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEzE,IAAImC,EACJ,GAAI,CACF,EAAU,KAAK,MAAMrD,EAAQ,OAAOkC,CAAM,CAAC,CAC7C,MAAQ,CACN,EAAU,IAAA,EACZ,CACA,GAAI,CAACtB,EAAqByC,CAAO,EAE/B,MADA,GAAU,0CACH,IAAI,SAAS,KAAK,UAAU,CAAE,MAAOnC,CAAQ,CAAC,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEzE,IAAMT,EAAOJ,EAAagD,EAAQ,IAAI,EACtC,OAAO,IAAI,SACTA,EAAQ,SAAW,KAAOA,EAAQ,SAAW,KAAOA,EAAQ,SAAW,IAAM,KAAO5C,EACpF,CACE,OAAQ4C,EAAQ,OAChB,QAASA,EAAQ,OACnB,CACF,CACF,EAAA,CAAG,EACHP,CACF,CAAC,CACH,EACAA,CACF,CAAC,CACH,QAAU,CACR,aAAaF,CAAQ,EACrBJ,GAAM,QAAQ,oBAAoB,QAASG,CAAM,EAC7CE,GAAaH,EAAW,OAAO,oBAAoB,QAASG,CAAW,CAC7E,CACF,CACF,CACF,CAaA,MAAaS,EAAkBtC,EAAsB"}
1
+ {"version":3,"file":"index.mjs","names":["encoder","decoder","encodeBase64","binary","byte","bytes","decodeBase64","value","character","bodyBytes","body","requestTarget","input","isSealedHttpResponse","candidate","header","part","createSealedTransport","channel","failure","authorizationFailure","unauthorized","outbound","createSerialQueue","inbound","exchange","relayPath","note","describeSealedFailure","reason","sealBytes","sealed","openBytes","envelope","text","opened","listener","connected","connectSealedChannel","hostPublicKey","path","init","gateway","controller","cancel","deadline","rejectAbort","aborted","reject","expectedChannel","check","request","relayResponse","responseText","decoded","browserBindings","bindBrowserTransport","hostId","transport","browserTransportForHost","binding","route","browserRequestRoute","expectedHost","local","id","encoded","slash","queryAt","routed","prefix","query","browserTransport"],"sources":["../../../src/services/sealedTransport/index.ts"],"sourcesContent":["import { describeSealedFailure, type SealedFailure } from '../../types/sealedChannel';\nimport { connectSealedChannel, type SealedChannel } from '../browserSealedChannel';\nimport { createSerialQueue } from '../serialQueue';\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';\nconst FETCH_TIMEOUT_MS = 30_000;\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 onAuthorizationFailure: (listener: () => void) => void;\n invalidate: () => void;\n} {\n let channel: SealedChannel | undefined;\n let failure: string | undefined;\n let authorizationFailure: (() => void) | undefined;\n let unauthorized = false;\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 invalidate() {\n unauthorized = true;\n },\n onAuthorizationFailure(listener) {\n authorizationFailure = listener;\n },\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 unauthorized = false;\n failure = undefined;\n return connected.clientPublicKey;\n },\n\n relayRequestsThrough(path) {\n relayPath = path;\n },\n\n reset() {\n unauthorized = false;\n channel = undefined;\n failure = undefined;\n },\n\n async sealText(text) {\n if (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 (unauthorized) throw new Error('Sign in to Po again.');\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 init?.signal?.throwIfAborted();\n if (unauthorized) throw new Error('Sign in to Po again.');\n const gateway = relayPath;\n if (channel === undefined || gateway === undefined) return await fetch(input, init);\n const controller = new AbortController();\n const cancel = () => controller.abort(init?.signal?.reason);\n const deadline = setTimeout(\n () => controller.abort(new DOMException('The request timed out.', 'TimeoutError')),\n FETCH_TIMEOUT_MS,\n );\n init?.signal?.addEventListener('abort', cancel, { once: true });\n if (init?.signal?.aborted) cancel();\n let rejectAbort: (() => void) | undefined;\n const aborted = new Promise<never>((_resolve, reject) => {\n rejectAbort = () => reject(controller.signal.reason);\n controller.signal.addEventListener('abort', rejectAbort, { once: true });\n if (controller.signal.aborted) rejectAbort();\n });\n const expectedChannel = channel;\n const check = () => {\n controller.signal.throwIfAborted();\n if (unauthorized || channel !== expectedChannel) throw new Error('The sealed channel was replaced.');\n };\n try {\n check();\n return await Promise.race([\n exchange.run(() =>\n Promise.race([\n (async () => {\n check();\n const bytes = await bodyBytes(init?.body);\n check();\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 check();\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 signal: controller.signal,\n credentials: 'same-origin',\n headers: { 'content-type': JSON_CONTENT_TYPE },\n body: sealed,\n });\n check();\n // These are unsealed gateway refusals, not the inner application's authorization response.\n if (relayResponse.status === 401 || relayResponse.status === 403) {\n unauthorized = true;\n failure = 'Sign in to Po again.';\n authorizationFailure?.();\n throw new Error(failure);\n }\n const responseText = await relayResponse.text();\n check();\n const opened = await openBytes(responseText);\n check();\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(\n decoded.status === 204 || decoded.status === 205 || decoded.status === 304 ? null : body,\n {\n status: decoded.status,\n headers: decoded.headers,\n },\n );\n })(),\n aborted,\n ]),\n ),\n aborted,\n ]);\n } finally {\n clearTimeout(deadline);\n init?.signal?.removeEventListener('abort', cancel);\n if (rejectAbort) controller.signal.removeEventListener('abort', rejectAbort);\n }\n },\n };\n}\n\n/** Only the request capability is shared. Each host owns its cryptographic transport. */\ntype BrowserTransport = Pick<SealedTransport, 'fetch' | 'active'>;\nconst browserBindings = new Map<string, BrowserTransport>();\nconst DEFAULT_HOST = '';\n\n/** An identity-safe disposer cannot unbind a newer connection. */\nexport function bindBrowserTransport(transport: BrowserTransport, hostId = DEFAULT_HOST): () => void {\n browserBindings.set(hostId, transport);\n return () => {\n if (browserBindings.get(hostId) === transport) browserBindings.delete(hostId);\n };\n}\n\n/** Selects a host explicitly for global requests, never by whichever session is focused. */\nexport function browserTransportForHost(hostId: string): BrowserTransport {\n return {\n active: () => browserBindings.get(hostId)?.active() ?? false,\n async fetch(input, init) {\n init?.signal?.throwIfAborted();\n const binding = browserBindings.get(hostId);\n if (!binding) throw new Error('The host transport is not connected.');\n const route = browserRequestRoute(input, hostId);\n return binding.fetch(route.input, init);\n },\n };\n}\n\n/** Encoded session and workspace identities are runtime addresses, not wire values. */\nfunction browserRequestRoute(input: string, expectedHost?: string): { hostId?: string; input: string } {\n let hostId = expectedHost;\n const local = (encoded: string): string => {\n const id = decodeURIComponent(encoded);\n const slash = id.indexOf('/');\n if (slash < 1) return encoded;\n const candidate = id.slice(0, slash);\n // A local ID containing a slash is not a host address.\n if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(candidate)) return encoded;\n if (hostId !== undefined && candidate !== hostId) throw new Error('The request spans different hosts.');\n hostId = candidate;\n return encodeURIComponent(id.slice(slash + 1));\n };\n const queryAt = input.indexOf('?');\n const path = queryAt === -1 ? input : input.slice(0, queryAt);\n const routed = path.replace(\n /(\\/(?:workspaces|sessions)\\/)([^/]+)/gu,\n (_match, prefix: string, id: string) => prefix + local(id),\n );\n const query =\n queryAt === -1\n ? ''\n : input\n .slice(queryAt)\n .replace(/([?&]session=)([^&]*)/gu, (_match, prefix: string, id: string) => prefix + local(id));\n return { hostId, input: routed + query };\n}\n\n/** Legacy unqualified URLs use only the explicitly bound default host. */\nexport const browserTransport: BrowserTransport = {\n active: () => browserBindings.get(DEFAULT_HOST)?.active() ?? false,\n async fetch(input, init) {\n init?.signal?.throwIfAborted();\n const route = browserRequestRoute(input);\n if (route.hostId !== undefined) return browserTransportForHost(route.hostId).fetch(route.input, init);\n const binding = browserBindings.get(DEFAULT_HOST);\n return binding ? binding.fetch(input, init) : fetch(input, init);\n },\n};\n"],"mappings":"4MAmBA,MAgBMA,EAAU,IAAI,YACdC,EAAU,IAAI,YAiBpB,SAASC,EAAa,EAA2B,CAC/C,IAAIC,EAAS,GACb,IAAK,IAAMC,KAAQC,EAAO,GAAU,OAAO,aAAaD,CAAI,EAC5D,OAAO,KAAKD,CAAM,CACpB,CAEA,SAASG,EAAa,EAA2B,CAC/C,IAAMH,EAAS,KAAKI,CAAK,EACzB,OAAO,WAAW,KAAKJ,EAAS,GAAcK,EAAU,WAAW,CAAC,CAAC,CACvE,CAEA,eAAeC,EAAU,EAA4D,CACnF,GAAIC,GAA+B,KACnC,IAAI,OAAOA,GAAS,SAAU,OAAOV,EAAQ,OAAOU,CAAI,EACxD,GAAIA,aAAgB,gBAAiB,OAAOV,EAAQ,OAAOU,EAAK,SAAS,CAAC,EAC1E,GAAIA,aAAgB,KAAM,OAAO,IAAI,WAAW,MAAMA,EAAK,YAAY,CAAC,EACxE,GAAIA,aAAgB,YAAa,OAAO,IAAI,WAAWA,CAAI,EAC3D,GAAI,YAAY,OAAOA,CAAI,EAAG,OAAO,IAAI,WAAWA,EAAK,OAAQA,EAAK,WAAYA,EAAK,UAAU,EACjG,MAAU,MAAM,oEAAoE,CAL5B,CAM1D,CAEA,SAASC,EAAc,EAAuB,CAC5C,GAAI,CAACC,EAAM,WAAW,GAAG,EAAG,MAAU,MAAM,qDAAqD,EACjG,OAAOA,CACT,CAEA,SAASC,EAAqB,EAA6C,CACzE,GAAI,OAAON,GAAU,WAAYA,EAAgB,MAAO,GACxD,IAAMO,EAAYP,EAClB,OACEO,EAAU,IAAM,GAChB,OAAOA,EAAU,QAAW,UAC5B,MAAM,QAAQA,EAAU,OAAO,GAC/BA,EAAU,QAAQ,MACf,GAAW,MAAM,QAAQC,CAAM,GAAKA,EAAO,SAAW,GAAKA,EAAO,MAAO,GAAS,OAAOC,GAAS,QAAQ,CAC7G,GACA,OAAOF,EAAU,MAAS,QAE9B,CACA,SAAgBG,GASd,CACA,IAAIC,EACAC,EACAC,EACAC,EAAe,GACbC,EAAWC,EAAkB,EAC7BC,EAAUD,EAAkB,EAC5BE,EAAWF,EAAkB,EAC/BG,EACEC,EAAQ,GAAqC,CAGjD,EAAUC,EAAsBC,CAAM,CAExC,EAEMC,EAAY,KAAO,IAAmD,CAC1E,GAAIZ,IAAY,IAAA,GAAW,OAC3B,IAAMa,EAAS,MAAMb,EAAQ,KAAKb,CAAK,EAEvC,OADK0B,EAAO,GACL,KAAK,UAAUA,EAAO,QAAQ,EADdJ,EAAKI,EAAO,OAAO,CAE5C,EAEMC,EAAY,KAAO,IAAkD,CACzE,GAAId,IAAY,IAAA,GAAW,OAC3B,IAAIe,EACJ,GAAI,CACF,EAAW,KAAK,MAAMC,CAAI,CAC5B,MAAQ,CACN,OAAOP,EAAK,WAAW,CACzB,CACA,IAAMQ,EAAS,MAAMjB,EAAQ,KAAKe,CAAQ,EAE1C,OADKE,EAAO,GACLA,EAAO,UADSR,EAAKQ,EAAO,OAAO,CAE5C,EAEA,MAAO,CACL,WAAcjB,IAAY,IAAA,GAC1B,YAAa,CACX,EAAe,EACjB,EACA,uBAAuB,EAAU,CAC/B,EAAuBkB,CACzB,EACA,gBAAmBjB,EAEnB,MAAM,QAAQ,EAAe,CAC3B,IAAMkB,EAAY,MAAMC,EAAqBC,CAAa,EAC1D,GAAIF,IAAc,IAAA,GAChB,EAAU,0DAMZ,MAHA,GAAUA,EAAU,QACpB,EAAe,GACf,EAAU,IAAA,GACHA,EAAU,eACnB,EAEA,qBAAqB,EAAM,CACzB,EAAYG,CACd,EAEA,OAAQ,CACN,EAAe,GACf,EAAU,IAAA,GACV,EAAU,IAAA,EACZ,EAEA,MAAM,SAAS,EAAM,CACnB,GAAInB,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBgB,EAC3B,MAAMZ,EAAS,IAAI,SAAY,CACpC,IAAMS,EAAS,MAAMD,EAAU9B,EAAQ,OAAOkC,CAAI,CAAC,EACnD,GAAIH,IAAW,IAAA,GAAW,MAAU,MAAMZ,GAAW,wDAAwD,EAC7G,OAAOY,CACT,CAAC,CACH,EAEA,MAAM,SAAS,EAAM,CACnB,GAAIV,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBgB,EAC3B,MAAMV,EAAQ,IAAI,SAAY,CACnC,IAAMW,EAAS,MAAMH,EAAUE,CAAI,EACnC,OAAOC,IAAW,IAAA,GAAY,IAAA,GAAYlC,EAAQ,OAAOkC,CAAM,CACjE,CAAC,CACH,EAEA,MAAM,WAAW,EAAO,CACtB,GAAId,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBb,EAC3B,MAAMiB,EAAS,IAAI,SAAY,CACpC,IAAMS,EAAS,MAAMD,EAAUzB,CAAK,EACpC,GAAI0B,IAAW,IAAA,GAAW,MAAU,MAAMZ,GAAW,wDAAwD,EAC7G,OAAOnB,EAAQ,OAAO+B,CAAM,CAC9B,CAAC,CACH,EAEA,MAAM,WAAW,EAAO,CACtB,GAAIV,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBb,EAC3B,MAAMmB,EAAQ,IAAI,SAAY,MAAMQ,EAAU/B,EAAQ,OAAOI,CAAK,CAAC,CAAC,CAC7E,EAEA,MAAM,MAAM,EAAO,EAAM,CAEvB,GADAoC,GAAM,QAAQ,eAAe,EACzBpB,EAAc,MAAU,MAAM,sBAAsB,EACxD,IAAMqB,EAAUhB,EAChB,GAAIR,IAAY,IAAA,IAAawB,IAAY,IAAA,GAAW,OAAO,MAAM,MAAM9B,EAAO6B,CAAI,EAClF,IAAME,EAAa,IAAI,gBACjBC,MAAeD,EAAW,MAAMF,GAAM,QAAQ,MAAM,EACpDI,EAAW,eACTF,EAAW,MAAM,IAAI,aAAa,yBAA0B,cAAc,CAAC,EACjF,GACF,EACAF,GAAM,QAAQ,iBAAiB,QAASG,EAAQ,CAAE,KAAM,EAAK,CAAC,EAC1DH,GAAM,QAAQ,SAASG,EAAO,EAClC,IAAIE,EACEC,EAAU,IAAI,SAAgB,EAAU,IAAW,CACvD,MAAoBC,EAAOL,EAAW,OAAO,MAAM,EACnDA,EAAW,OAAO,iBAAiB,QAASG,EAAa,CAAE,KAAM,EAAK,CAAC,EACnEH,EAAW,OAAO,SAASG,EAAY,CAC7C,CAAC,EACKG,EAAkB/B,EAClBgC,MAAc,CAElB,GADAP,EAAW,OAAO,eAAe,EAC7BtB,GAAgBH,IAAY+B,EAAiB,MAAU,MAAM,kCAAkC,CACrG,EACA,GAAI,CAEF,OADAC,EAAM,EACC,MAAM,QAAQ,KAAK,CACxBzB,EAAS,QACP,QAAQ,KAAK,EACV,SAAY,CACXyB,EAAM,EACN,IAAM7C,EAAQ,MAAMI,EAAUgC,GAAM,IAAI,EACxCS,EAAM,EACN,IAAMC,EAA6B,CACjC,EAAG,EACH,QAASV,GAAM,QAAU,MAAA,CAAO,YAAY,EAC5C,OAAQ9B,EAAcC,CAAK,EAC3B,QAAS,MAAM,KAAK,IAAI,QAAQ6B,GAAM,OAAO,CAAC,CAAC,QAAQ,CAAC,EACxD,GAAIpC,IAAU,IAAA,GAAY,CAAC,EAAI,CAAE,KAAMH,EAAaG,CAAK,CAAE,CAC7D,EACM0B,EAAS,MAAMD,EAAU9B,EAAQ,OAAO,KAAK,UAAUmD,CAAO,CAAC,CAAC,EAEtE,GADAD,EAAM,EACFnB,IAAW,IAAA,GACb,OAAO,IAAI,SAAS,KAAK,UAAU,CAAE,MAAOZ,CAAQ,CAAC,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEzE,IAAMiC,EAAgB,MAAM,MAAMV,EAAS,CACzC,OAAQ,OACR,OAAQC,EAAW,OACnB,YAAa,cACb,QAAS,CAAE,eAAgB,kBAAkB,EAC7C,KAAMZ,CACR,CAAC,EAGD,GAFAmB,EAAM,EAEFE,EAAc,SAAW,KAAOA,EAAc,SAAW,IAI3D,KAHA,GAAe,GACf,EAAU,uBACVhC,IAAuB,EACb,MAAMD,CAAO,EAEzB,IAAMkC,EAAe,MAAMD,EAAc,KAAK,EAC9CF,EAAM,EACN,IAAMf,EAAS,MAAMH,EAAUqB,CAAY,EAE3C,GADAH,EAAM,EACFf,IAAW,IAAA,GACb,OAAO,IAAI,SAAS,KAAK,UAAU,CAAE,MAAOhB,CAAQ,CAAC,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEzE,IAAImC,EACJ,GAAI,CACF,EAAU,KAAK,MAAMrD,EAAQ,OAAOkC,CAAM,CAAC,CAC7C,MAAQ,CACN,EAAU,IAAA,EACZ,CACA,GAAI,CAACtB,EAAqByC,CAAO,EAE/B,MADA,GAAU,0CACH,IAAI,SAAS,KAAK,UAAU,CAAE,MAAOnC,CAAQ,CAAC,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEzE,IAAMT,EAAOJ,EAAagD,EAAQ,IAAI,EACtC,OAAO,IAAI,SACTA,EAAQ,SAAW,KAAOA,EAAQ,SAAW,KAAOA,EAAQ,SAAW,IAAM,KAAO5C,EACpF,CACE,OAAQ4C,EAAQ,OAChB,QAASA,EAAQ,OACnB,CACF,CACF,EAAA,CAAG,EACHP,CACF,CAAC,CACH,EACAA,CACF,CAAC,CACH,QAAU,CACR,aAAaF,CAAQ,EACrBJ,GAAM,QAAQ,oBAAoB,QAASG,CAAM,EAC7CE,GAAaH,EAAW,OAAO,oBAAoB,QAASG,CAAW,CAC7E,CACF,CACF,CACF,CAIA,MAAMS,kBAAkB,IAAI,IAI5B,SAAgBC,EAAqB,EAA6B,EAAS,GAA0B,CAEnG,OADAD,EAAgB,IAAIE,EAAQC,CAAS,MACxB,CACPH,EAAgB,IAAIE,CAAM,IAAMC,GAAWH,EAAgB,OAAOE,CAAM,CAC9E,CACF,CAGA,SAAgBE,EAAwB,EAAkC,CACxE,MAAO,CACL,WAAcJ,EAAgB,IAAIE,CAAM,CAAC,EAAE,OAAO,GAAK,GACvD,MAAM,MAAM,EAAO,EAAM,CACvBhB,GAAM,QAAQ,eAAe,EAC7B,IAAMmB,EAAUL,EAAgB,IAAIE,CAAM,EAC1C,GAAI,CAACG,EAAS,MAAU,MAAM,sCAAsC,EACpE,IAAMC,EAAQC,EAAoBlD,EAAO6C,CAAM,EAC/C,OAAOG,EAAQ,MAAMC,EAAM,MAAOpB,CAAI,CACxC,CACF,CACF,CAGA,SAASqB,EAAoB,EAAe,EAA2D,CACrG,IAAIL,EAASM,EACPC,EAAS,GAA4B,CACzC,IAAMC,EAAK,mBAAmBC,CAAO,EAC/BC,EAAQF,EAAG,QAAQ,GAAG,EAC5B,GAAIE,EAAQ,EAAG,OAAOD,EACtB,IAAMpD,EAAYmD,EAAG,MAAM,EAAGE,CAAK,EAEnC,GAAI,CAAC,mEAAmE,KAAKrD,CAAS,EAAG,OAAOoD,EAChG,GAAIT,IAAW,IAAA,IAAa3C,IAAc2C,EAAQ,MAAU,MAAM,oCAAoC,EAEtG,MADA,GAAS3C,EACF,mBAAmBmD,EAAG,MAAME,EAAQ,CAAC,CAAC,CAC/C,EACMC,EAAUxD,EAAM,QAAQ,GAAG,EAE3ByD,GADOD,IAAY,GAAKxD,EAAQA,EAAM,MAAM,EAAGwD,CAAO,EAAA,CACxC,QAClB,0CACC,EAAQ,EAAgB,IAAeE,EAASN,EAAMC,CAAE,CAC3D,EACMM,EACJH,IAAY,GACR,GACAxD,EACG,MAAMwD,CAAO,CAAC,CACd,QAAQ,2BAA4B,EAAQ,EAAgB,IAAeE,EAASN,EAAMC,CAAE,CAAC,EACtG,MAAO,CAAE,SAAQ,MAAOI,EAASE,CAAM,CACzC,CAGA,MAAaC,EAAqC,CAChD,WAAcjB,EAAgB,IAAI,EAAY,CAAC,EAAE,OAAO,GAAK,GAC7D,MAAM,MAAM,EAAO,EAAM,CACvBd,GAAM,QAAQ,eAAe,EAC7B,IAAMoB,EAAQC,EAAoBlD,CAAK,EACvC,GAAIiD,EAAM,SAAW,IAAA,GAAW,OAAOF,EAAwBE,EAAM,MAAM,CAAC,CAAC,MAAMA,EAAM,MAAOpB,CAAI,EACpG,IAAMmB,EAAUL,EAAgB,IAAI,EAAY,EAChD,OAAOK,EAAUA,EAAQ,MAAMhD,EAAO6B,CAAI,EAAI,MAAM7B,EAAO6B,CAAI,CACjE,CACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/po-web-security",
3
- "version": "0.0.0-alpha.37",
3
+ "version": "0.0.0-alpha.38",
4
4
  "description": "Shared security primitives for the Po web cockpit: sealed channels and signed bundle manifests for independently trusted verifiers.",
5
5
  "keywords": [
6
6
  "ai",
@@ -46,6 +46,11 @@
46
46
  "import": "./dist/browser.mjs",
47
47
  "require": "./dist/browser.cjs"
48
48
  },
49
+ "./nodeSealedChannel": {
50
+ "types": "./dist/nodeSealedChannel.d.mts",
51
+ "import": "./dist/nodeSealedChannel.mjs",
52
+ "require": "./dist/nodeSealedChannel.cjs"
53
+ },
49
54
  "./node": {
50
55
  "types": "./dist/node.d.mts",
51
56
  "import": "./dist/node.mjs",