@agimon-ai/po-web-security 0.0.0-alpha.110 → 0.0.0-alpha.112
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 +1 -1
- package/dist/browser.d.mts +4 -2
- package/dist/browser.mjs +1 -1
- package/dist/services/bundleSigner/index.cjs +1 -1
- package/dist/services/bundleSigner/index.cjs.map +1 -1
- package/dist/services/bundleSigner/index.d.mts.map +1 -1
- package/dist/services/bundleSigner/index.mjs +1 -1
- package/dist/services/bundleSigner/index.mjs.map +1 -1
- package/dist/services/clientIdentity/index.cjs +2 -0
- package/dist/services/clientIdentity/index.cjs.map +1 -0
- package/dist/services/clientIdentity/index.d.mts +16 -0
- package/dist/services/clientIdentity/index.d.mts.map +1 -0
- package/dist/services/clientIdentity/index.mjs +2 -0
- package/dist/services/clientIdentity/index.mjs.map +1 -0
- package/dist/services/clientIdentity/type.d.mts +41 -0
- package/dist/services/clientIdentity/type.d.mts.map +1 -0
- package/dist/services/sealedTransport/index.cjs +1 -1
- package/dist/services/sealedTransport/index.cjs.map +1 -1
- package/dist/services/sealedTransport/index.d.mts +2 -0
- package/dist/services/sealedTransport/index.d.mts.map +1 -1
- package/dist/services/sealedTransport/index.mjs +1 -1
- package/dist/services/sealedTransport/index.mjs.map +1 -1
- package/package.json +1 -1
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/
|
|
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/clientIdentity/index.cjs"),i=require("./services/sealedTransport/index.cjs"),a=require("./types/federation.cjs");exports.ATTESTATION_TAG=e.ATTESTATION_TAG,exports.BUNDLE_MANIFEST_ROUTE=e.BUNDLE_MANIFEST_ROUTE,exports.BUNDLE_MANIFEST_VERSION=e.BUNDLE_MANIFEST_VERSION,exports.FEDERATION_FRESHNESS=a.FEDERATION_FRESHNESS,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=i.bindBrowserTransport,exports.bindClientMetadata=r.bindClientMetadata,exports.browserTransport=i.browserTransport,exports.browserTransportForHost=i.browserTransportForHost,exports.canonicalManifest=e.canonicalManifest,exports.channelFromSecret=n.channelFromSecret,exports.clientMetadata=r.clientMetadata,exports.connectSealedChannel=n.connectSealedChannel,exports.createBrowserClientIdentity=r.createBrowserClientIdentity,exports.createClientIdentity=r.createClientIdentity,exports.createSealedTransport=i.createSealedTransport,exports.digestFor=e.digestFor,exports.federationEnvelopeFields=a.federationEnvelopeFields,exports.isBundleManifest=e.isBundleManifest,exports.isSignedBundleManifest=e.isSignedBundleManifest,exports.poRequestInit=i.poRequestInit,exports.refreshClientDeviceName=r.refreshClientDeviceName,exports.updateClientDeviceName=r.updateClientDeviceName,exports.verifyAttestation=t.verifyAttestation,exports.verifyBundleAsset=t.verifyBundleAsset,exports.verifySignedBundleManifest=t.verifySignedBundleManifest;
|
package/dist/browser.d.mts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { 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, bindBrowserTransport, browserTransport, browserTransportForHost, createSealedTransport } from "./services/sealedTransport/index.mjs";
|
|
4
|
+
import { SealedTransport, bindBrowserTransport, browserTransport, browserTransportForHost, createSealedTransport, poRequestInit } from "./services/sealedTransport/index.mjs";
|
|
5
|
+
import { BrowserIdentityOptions, ClientIdentity, ClientIdentityOptions } from "./services/clientIdentity/type.mjs";
|
|
6
|
+
import { bindClientMetadata, clientMetadata, createBrowserClientIdentity, createClientIdentity, refreshClientDeviceName, updateClientDeviceName } from "./services/clientIdentity/index.mjs";
|
|
5
7
|
import { FEDERATION_FRESHNESS, federationEnvelopeFields } from "./types/federation.mjs";
|
|
6
|
-
export { ATTESTATION_TAG, BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BundleAsset, type BundleAssetVerificationFailure, type BundleAssetVerificationResult, type BundleManifest, type BundleProfile, type BundleVerificationFailure, type BundleVerificationResult, FEDERATION_FRESHNESS, 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, federationEnvelopeFields, isBundleManifest, isSignedBundleManifest, verifyAttestation, verifyBundleAsset, verifySignedBundleManifest };
|
|
8
|
+
export { ATTESTATION_TAG, BUNDLE_MANIFEST_ROUTE, BUNDLE_MANIFEST_VERSION, type BrowserIdentityOptions, type BundleAsset, type BundleAssetVerificationFailure, type BundleAssetVerificationResult, type BundleManifest, type BundleProfile, type BundleVerificationFailure, type BundleVerificationResult, type ClientIdentity, type ClientIdentityOptions, FEDERATION_FRESHNESS, 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, bindClientMetadata, browserTransport, browserTransportForHost, canonicalManifest, channelFromSecret, clientMetadata, connectSealedChannel, createBrowserClientIdentity, createClientIdentity, createSealedTransport, digestFor, federationEnvelopeFields, isBundleManifest, isSignedBundleManifest, poRequestInit, refreshClientDeviceName, updateClientDeviceName, verifyAttestation, verifyBundleAsset, verifySignedBundleManifest };
|
package/dist/browser.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{ATTESTATION_TAG as e,BUNDLE_MANIFEST_ROUTE as t,BUNDLE_MANIFEST_VERSION as n,NATIVE_COMPOSITION_MAX_BYTES as r,NATIVE_ENTRY_CONTENT_TYPE as i,NATIVE_ENTRY_PATH as a,WEB_ENTRY_PATH as o,assetFor as s,attestationBytes as c,canonicalManifest as l,digestFor as u,isBundleManifest as d,isSignedBundleManifest as f}from"./types/bundleManifest.mjs";import{verifyAttestation as p,verifyBundleAsset as m,verifySignedBundleManifest as h}from"./services/browserBundleVerifier/index.mjs";import{channelFromSecret as g,connectSealedChannel as _}from"./services/browserSealedChannel/index.mjs";import{
|
|
1
|
+
import{ATTESTATION_TAG as e,BUNDLE_MANIFEST_ROUTE as t,BUNDLE_MANIFEST_VERSION as n,NATIVE_COMPOSITION_MAX_BYTES as r,NATIVE_ENTRY_CONTENT_TYPE as i,NATIVE_ENTRY_PATH as a,WEB_ENTRY_PATH as o,assetFor as s,attestationBytes as c,canonicalManifest as l,digestFor as u,isBundleManifest as d,isSignedBundleManifest as f}from"./types/bundleManifest.mjs";import{verifyAttestation as p,verifyBundleAsset as m,verifySignedBundleManifest as h}from"./services/browserBundleVerifier/index.mjs";import{channelFromSecret as g,connectSealedChannel as _}from"./services/browserSealedChannel/index.mjs";import{bindClientMetadata as v,clientMetadata as y,createBrowserClientIdentity as b,createClientIdentity as x,refreshClientDeviceName as S,updateClientDeviceName as C}from"./services/clientIdentity/index.mjs";import{bindBrowserTransport as w,browserTransport as T,browserTransportForHost as E,createSealedTransport as D,poRequestInit as O}from"./services/sealedTransport/index.mjs";import{FEDERATION_FRESHNESS as k,federationEnvelopeFields as A}from"./types/federation.mjs";export{e as ATTESTATION_TAG,t as BUNDLE_MANIFEST_ROUTE,n as BUNDLE_MANIFEST_VERSION,k as FEDERATION_FRESHNESS,r as NATIVE_COMPOSITION_MAX_BYTES,i as NATIVE_ENTRY_CONTENT_TYPE,a as NATIVE_ENTRY_PATH,o as WEB_ENTRY_PATH,s as assetFor,c as attestationBytes,w as bindBrowserTransport,v as bindClientMetadata,T as browserTransport,E as browserTransportForHost,l as canonicalManifest,g as channelFromSecret,y as clientMetadata,_ as connectSealedChannel,b as createBrowserClientIdentity,x as createClientIdentity,D as createSealedTransport,u as digestFor,A as federationEnvelopeFields,d as isBundleManifest,f as isSignedBundleManifest,O as poRequestInit,S as refreshClientDeviceName,C as updateClientDeviceName,p as verifyAttestation,m as verifyBundleAsset,h as verifySignedBundleManifest};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../types/bundleManifest.cjs");let n=require("node:crypto"),r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);const
|
|
1
|
+
const e=require("../../_virtual/_rolldown/runtime.cjs"),t=require("../../types/bundleManifest.cjs");let n=require("node:crypto"),r=require("node:fs");r=e.__toESM(r,1);let i=require("node:path");i=e.__toESM(i,1);let a=require("node:sqlite");const o=`signing.json`,s=`signing.lock`,c=`sha256`,l=/* @__PURE__ */ new Set([`.map`]),u=[`web`,`native`],d={".css":`text/css`,".gif":`image/gif`,".html":`text/html`,".ico":`image/x-icon`,".jpeg":`image/jpeg`,".jpg":`image/jpeg`,".js":`text/javascript`,".json":`application/json`,".mjs":`text/javascript`,".png":`image/png`,".svg":`image/svg+xml`,".txt":`text/plain`,".wasm":`application/wasm`,".webp":`image/webp`,".woff":`font/woff`,".woff2":`font/woff2`};function f(e){r.default.mkdirSync(e,{recursive:!0,mode:448});let t=r.default.lstatSync(e);if(!t.isDirectory()||t.isSymbolicLink())throw Error(`bundle signing state is not a directory: ${e}`);r.default.chmodSync(e,448)}function p(e,t){f(e);let o=i.default.join(e,s);if(!r.default.existsSync(o)){let t=i.default.join(e,`.${s}.${process.pid}.${(0,n.randomBytes)(8).toString(`hex`)}`);r.default.closeSync(r.default.openSync(t,`wx`,384));try{let e=new a.DatabaseSync(t);try{e.exec(`PRAGMA user_version = 1`)}finally{e.close()}try{r.default.linkSync(t,o)}catch(e){if(e.code!==`EEXIST`)throw e}}finally{r.default.rmSync(t,{force:!0})}}if(!r.default.lstatSync(o).isFile())throw Error(`invalid bundle signing lock file: ${o}`);let c=r.default.openSync(o,r.default.constants.O_RDWR|r.default.constants.O_NOFOLLOW);try{if(!r.default.fstatSync(c).isFile())throw Error(`invalid bundle signing lock file: ${o}`);let e=Buffer.alloc(16);if(r.default.readSync(c,e,0,e.length,0),e.toString()!==`SQLite format 3\0`)throw Error(`legacy or invalid bundle signing state lock: ${o}; stop all Po servers and inspect the lock before manual recovery`);r.default.fchmodSync(c,384);let n=new a.DatabaseSync(o,{timeout:5e3});try{return n.exec(`BEGIN EXCLUSIVE`),t()}finally{n.close()}}finally{r.default.closeSync(c)}}function m(e,t){let a=i.default.join(e,o),s=i.default.join(e,`.${o}.${process.pid}.${(0,n.randomBytes)(8).toString(`hex`)}`),c=r.default.openSync(s,`wx`,384);try{r.default.writeFileSync(c,`${JSON.stringify(t,void 0,2)}\n`),r.default.fsyncSync(c)}finally{r.default.closeSync(c)}try{r.default.renameSync(s,a),r.default.chmodSync(a,384)}catch(e){throw r.default.rmSync(s,{force:!0}),e}}function h(e,t){if(typeof e!=`object`||!e)throw Error(`invalid bundle signing state: ${t}`);let n=e;if(typeof n.privateKey!=`string`||typeof n.publicKey!=`string`)throw Error(`invalid bundle signing state: ${t}`);let r=n.revision===void 0?0:n.revision;if(!Number.isSafeInteger(r)||r<0)throw Error(`invalid bundle signing revision: ${t}`);let i;try{i=x(n.privateKey)}catch{throw Error(`invalid bundle signing private key: ${t}`)}if(i!==n.publicKey)throw Error(`bundle signing keys do not match: ${t}`);return{privateKey:n.privateKey,publicKey:n.publicKey,revision:r}}function g(e){return p(e,()=>{let t=i.default.join(e,o);try{let n=r.default.lstatSync(t);if(!n.isFile()||n.isSymbolicLink())throw Error(`invalid bundle signing state file: ${t}`);let i=JSON.parse(r.default.readFileSync(t,`utf8`)),a=h(i,t);return r.default.chmodSync(t,384),i.revision===void 0&&m(e,a),a}catch(e){if(e.code!==`ENOENT`)throw e}let a=(0,n.generateKeyPairSync)(`ec`,{namedCurve:`prime256v1`,privateKeyEncoding:{type:`pkcs8`,format:`pem`},publicKeyEncoding:{type:`spki`,format:`der`}}),s={privateKey:a.privateKey,publicKey:Buffer.from(a.publicKey).toString(`base64url`),revision:0};return m(e,s),s})}function _(e,t){return p(e,()=>{let n=i.default.join(e,o),a=h(JSON.parse(r.default.readFileSync(n,`utf8`)),n);if(a.publicKey!==t)throw Error(`bundle signing key changed while the signer was active`);if(a.revision===2**53-1)throw Error(`bundle signing revision is exhausted`);let s={...a,revision:a.revision+1};return m(e,s),s.revision})}function v(e){return d[i.default.extname(e).toLowerCase()]??`application/octet-stream`}function y(e,t,a){let o=r.default.readdirSync(t,{withFileTypes:!0});for(let s of o){let o=i.default.join(t,s.name),c=r.default.lstatSync(o);if(c.isSymbolicLink())throw Error(`refusing to sign symbolic link: ${o}`);if(c.isDirectory()){y(e,o,a);continue}if(!c.isFile())throw Error(`refusing to omit unsupported asset: ${o}`);if(l.has(i.default.extname(s.name)))continue;let u=r.default.readFileSync(o);a.push({path:`/${i.default.relative(e,o).split(i.default.sep).map(e=>encodeURIComponent(e)).join(`/`)}`,sha256:(0,n.createHash)(`sha256`).update(u).digest(`hex`),byteLength:u.byteLength,contentType:v(o)})}}function b(e,a=()=>{}){let o=g(e),s=(0,n.createPrivateKey)(o.privateKey),l=/* @__PURE__ */ new Map,d=(e,t)=>`${t}:${i.default.resolve(e)}`,f=(i,a)=>{let l=r.default.lstatSync(i);if(!l.isDirectory()||l.isSymbolicLink())throw Error(`refusing to sign an unsafe asset root: ${i}`);let u=[];if(y(i,i,u),u.length===0)return;u.sort((e,t)=>e.path.localeCompare(t.path));let d={version:2,revision:_(e,o.publicKey),builtAt:Date.now(),assets:u};if(!t.isBundleManifest(d,a))throw Error(`refusing to sign an invalid ${a} bundle manifest`);return{manifest:d,signature:(0,n.createSign)(c).update(t.canonicalManifest(d)).sign({key:s,dsaEncoding:`ieee-p1363`}).toString(`base64url`),publicKey:o.publicKey}},p=(e,t=`web`)=>{let n=d(e,t);if(l.has(n))return l.get(n);let r=f(i.default.resolve(e),t);return l.set(n,r),r};return{publicKey:()=>o.publicKey,sign:p,refresh(e,t=`web`){return l.delete(d(e,t)),p(e,t)},invalidate(e){if(e===void 0)l.clear();else for(let t of u)l.delete(d(e,t))},attest(e,r){return(0,n.createSign)(c).update(t.attestationBytes(e,r)).sign({key:s,dsaEncoding:`ieee-p1363`}).toString(`base64url`)}}}function x(e){return Buffer.from((0,n.createPublicKey)(e).export({type:`spki`,format:`der`})).toString(`base64url`)}exports.createBundleSigner=b,exports.publicKeyOf=x;
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["fs","path","randomBytes","generateKeyPairSync","createHash","createPrivateKey","isBundleManifest","createSign","canonicalManifest","attestationBytes","createPublicKey","require_runtime","require_bundleManifest","node_crypto","node_fs","node_path","SIGNING_FILE","SIGN_ALGORITHM","SKIPPED","PROFILES","MIME_TYPES","ensureStateDirectory","stateDir","stat","withStateLock","lockPath","descriptor","attempt","error","operation","atomicWriteState","destination","temporary","state","parseState","value","keyPath","candidate","revision","derived","publicKeyOf","loadOrCreateState","parsed","pair","reserveRevision","expectedPublicKey","next","contentTypeFor","filePath","walk","entries","current","entry","full","root","into","bytes","segment","createBundleSigner","key","privateKey","cache","cacheKeyOf","profile","assetsDir","signFresh","rootStat","assets","left","right","manifest","sign","cacheKey","signed","tag","fields","privateKeyPem"],"sources":["../../../src/services/bundleSigner/index.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n randomBytes,\n} from 'node:crypto';\n// sync: Public createBundleSigner and BundleSigner.sign/refresh contracts must remain synchronous until phase 2.\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n attestationBytes,\n canonicalManifest,\n isBundleManifest,\n} from '../../types/bundleManifest';\n\nconst SIGNING_FILE = 'signing.json';\nconst LOCK_FILE = 'signing.lock';\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst SKIPPED = new Set(['.map']);\nconst PROFILES: readonly BundleProfile[] = ['web', 'native'];\nconst MIME_TYPES: Readonly<Record<string, string>> = {\n '.css': 'text/css',\n '.gif': 'image/gif',\n '.html': 'text/html',\n '.ico': 'image/x-icon',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.mjs': 'text/javascript',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n};\n\nexport interface BundleSigner {\n /** Base64url SPKI for an independently trusted verifier to pin. */\n publicKey(): string;\n /**\n * Hashes and signs once per asset directory and profile. Undefined when the bundle is empty. Throws\n * when the assets do not match the profile.\n */\n sign(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates and immediately rebuilds one cached manifest. */\n refresh(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates one cached directory under every profile, or every cached directory when omitted. */\n invalidate(assetsDir?: string): void;\n /**\n * Base64url IEEE P1363 signature over `attestationBytes(tag, fields)` with the bundle key. Throws on a\n * malformed tag.\n */\n attest(tag: string, fields: readonly string[]): string;\n}\n\ninterface StoredState {\n privateKey: string;\n publicKey: string;\n revision: number;\n}\n\nfunction ensureStateDirectory(stateDir: string): void {\n fs.mkdirSync(stateDir, { recursive: true, mode: DIRECTORY_MODE });\n const stat = fs.lstatSync(stateDir);\n if (!stat.isDirectory() || stat.isSymbolicLink())\n throw new Error(`bundle signing state is not a directory: ${stateDir}`);\n fs.chmodSync(stateDir, DIRECTORY_MODE);\n}\n\nfunction withStateLock<T>(stateDir: string, operation: () => T): T {\n ensureStateDirectory(stateDir);\n const lockPath = path.join(stateDir, LOCK_FILE);\n let descriptor: number | undefined;\n for (let attempt = 0; attempt < 500; attempt += 1) {\n try {\n descriptor = fs.openSync(lockPath, 'wx', FILE_MODE);\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);\n }\n }\n if (descriptor === undefined) throw new Error(`timed out waiting for bundle signing state lock: ${lockPath}`);\n try {\n return operation();\n } finally {\n fs.closeSync(descriptor);\n fs.rmSync(lockPath, { force: true });\n }\n}\n\nfunction atomicWriteState(stateDir: string, state: StoredState): void {\n const destination = path.join(stateDir, SIGNING_FILE);\n const temporary = path.join(stateDir, `.${SIGNING_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n const descriptor = fs.openSync(temporary, 'wx', FILE_MODE);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify(state, undefined, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n try {\n fs.renameSync(temporary, destination);\n fs.chmodSync(destination, FILE_MODE);\n } catch (error) {\n fs.rmSync(temporary, { force: true });\n throw error;\n }\n}\n\nfunction parseState(value: unknown, keyPath: string): StoredState {\n if (typeof value !== 'object' || value === null) throw new Error(`invalid bundle signing state: ${keyPath}`);\n const candidate = value as Partial<StoredState>;\n if (typeof candidate.privateKey !== 'string' || typeof candidate.publicKey !== 'string') {\n throw new Error(`invalid bundle signing state: ${keyPath}`);\n }\n const revision = candidate.revision === undefined ? 0 : candidate.revision;\n if (!Number.isSafeInteger(revision) || revision < 0) throw new Error(`invalid bundle signing revision: ${keyPath}`);\n let derived: string;\n try {\n derived = publicKeyOf(candidate.privateKey);\n } catch {\n throw new Error(`invalid bundle signing private key: ${keyPath}`);\n }\n if (derived !== candidate.publicKey) throw new Error(`bundle signing keys do not match: ${keyPath}`);\n return { privateKey: candidate.privateKey, publicKey: candidate.publicKey, revision };\n}\n\nfunction loadOrCreateState(stateDir: string): StoredState {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n try {\n const stat = fs.lstatSync(keyPath);\n if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`invalid bundle signing state file: ${keyPath}`);\n const parsed: unknown = JSON.parse(fs.readFileSync(keyPath, 'utf8'));\n const state = parseState(parsed, keyPath);\n fs.chmodSync(keyPath, FILE_MODE);\n if ((parsed as Partial<StoredState>).revision === undefined) atomicWriteState(stateDir, state);\n return state;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n\n const pair = generateKeyPairSync('ec', {\n namedCurve: CURVE,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' },\n publicKeyEncoding: { type: 'spki', format: 'der' },\n });\n const state: StoredState = {\n privateKey: pair.privateKey,\n publicKey: Buffer.from(pair.publicKey).toString('base64url'),\n revision: 0,\n };\n atomicWriteState(stateDir, state);\n return state;\n });\n}\n\nfunction reserveRevision(stateDir: string, expectedPublicKey: string): number {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n const state = parseState(JSON.parse(fs.readFileSync(keyPath, 'utf8')) as unknown, keyPath);\n if (state.publicKey !== expectedPublicKey)\n throw new Error('bundle signing key changed while the signer was active');\n if (state.revision === Number.MAX_SAFE_INTEGER) throw new Error('bundle signing revision is exhausted');\n const next = { ...state, revision: state.revision + 1 };\n atomicWriteState(stateDir, next);\n return next.revision;\n });\n}\n\nfunction contentTypeFor(filePath: string): string {\n return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/** Every servable regular file under the asset root, as browser paths. */\nfunction walk(root: string, current: string, into: BundleAsset[]): void {\n const entries = fs.readdirSync(current, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(current, entry.name);\n const stat = fs.lstatSync(full);\n if (stat.isSymbolicLink()) throw new Error(`refusing to sign symbolic link: ${full}`);\n if (stat.isDirectory()) {\n walk(root, full, into);\n continue;\n }\n if (!stat.isFile()) throw new Error(`refusing to omit unsupported asset: ${full}`);\n if (SKIPPED.has(path.extname(entry.name))) continue;\n const bytes = fs.readFileSync(full);\n into.push({\n path: `/${path\n .relative(root, full)\n .split(path.sep)\n .map((segment) => encodeURIComponent(segment))\n .join('/')}`,\n sha256: createHash('sha256').update(bytes).digest('hex'),\n byteLength: bytes.byteLength,\n contentType: contentTypeFor(full),\n });\n }\n}\n\nexport function createBundleSigner(stateDir: string, _onNotice: (message: string) => void = () => {}): BundleSigner {\n const key = loadOrCreateState(stateDir);\n const privateKey = createPrivateKey(key.privateKey);\n const cache = new Map<string, SignedBundleManifest | undefined>();\n const cacheKeyOf = (assetsDir: string, profile: BundleProfile): string => `${profile}:${path.resolve(assetsDir)}`;\n\n const signFresh = (assetsDir: string, profile: BundleProfile): SignedBundleManifest | undefined => {\n const rootStat = fs.lstatSync(assetsDir);\n if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {\n throw new Error(`refusing to sign an unsafe asset root: ${assetsDir}`);\n }\n const assets: BundleAsset[] = [];\n walk(assetsDir, assetsDir, assets);\n if (assets.length === 0) return undefined;\n assets.sort((left, right) => left.path.localeCompare(right.path));\n const manifest: BundleManifest = {\n version: BUNDLE_MANIFEST_VERSION,\n revision: reserveRevision(stateDir, key.publicKey),\n builtAt: Date.now(),\n assets,\n };\n if (!isBundleManifest(manifest, profile)) throw new Error(`refusing to sign an invalid ${profile} bundle manifest`);\n const signature = createSign(SIGN_ALGORITHM)\n .update(canonicalManifest(manifest))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n return { manifest, signature, publicKey: key.publicKey };\n };\n\n const sign = (assetsDir: string, profile: BundleProfile = 'web'): SignedBundleManifest | undefined => {\n const cacheKey = cacheKeyOf(assetsDir, profile);\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n const signed = signFresh(path.resolve(assetsDir), profile);\n cache.set(cacheKey, signed);\n return signed;\n };\n\n return {\n publicKey: () => key.publicKey,\n sign,\n refresh(assetsDir, profile = 'web') {\n cache.delete(cacheKeyOf(assetsDir, profile));\n return sign(assetsDir, profile);\n },\n invalidate(assetsDir) {\n if (assetsDir === undefined) {\n cache.clear();\n return;\n }\n for (const profile of PROFILES) cache.delete(cacheKeyOf(assetsDir, profile));\n },\n attest(tag, fields) {\n return createSign(SIGN_ALGORITHM)\n .update(attestationBytes(tag, fields))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n },\n };\n}\n\n/** Re-derives the public key from a stored private key. */\nexport function publicKeyOf(privateKeyPem: string): string {\n return Buffer.from(createPublicKey(privateKeyPem).export({ type: 'spki', format: 'der' }) as Buffer).toString(\n 'base64url',\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["fs","path","randomBytes","DatabaseSync","generateKeyPairSync","createHash","createPrivateKey","isBundleManifest","createSign","canonicalManifest","attestationBytes","createPublicKey","require_runtime","require_bundleManifest","node_crypto","node_fs","node_path","node_sqlite","SIGNING_FILE","LOCK_FILE","SIGN_ALGORITHM","SKIPPED","PROFILES","MIME_TYPES","ensureStateDirectory","stateDir","stat","withStateLock","lockPath","temporary","initial","error","descriptor","header","database","operation","atomicWriteState","destination","state","parseState","value","keyPath","candidate","revision","derived","publicKeyOf","loadOrCreateState","parsed","pair","reserveRevision","expectedPublicKey","next","contentTypeFor","filePath","walk","entries","current","entry","full","root","into","bytes","segment","createBundleSigner","key","privateKey","cache","cacheKeyOf","profile","assetsDir","signFresh","rootStat","assets","left","right","manifest","sign","cacheKey","signed","tag","fields","privateKeyPem"],"sources":["../../../src/services/bundleSigner/index.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n randomBytes,\n} from 'node:crypto';\n// sync: Public createBundleSigner and BundleSigner.sign/refresh contracts must remain synchronous until phase 2.\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n attestationBytes,\n canonicalManifest,\n isBundleManifest,\n} from '../../types/bundleManifest';\n\nconst SIGNING_FILE = 'signing.json';\nconst LOCK_FILE = 'signing.lock';\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst SKIPPED = new Set(['.map']);\nconst PROFILES: readonly BundleProfile[] = ['web', 'native'];\nconst MIME_TYPES: Readonly<Record<string, string>> = {\n '.css': 'text/css',\n '.gif': 'image/gif',\n '.html': 'text/html',\n '.ico': 'image/x-icon',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.mjs': 'text/javascript',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n};\n\nexport interface BundleSigner {\n /** Base64url SPKI for an independently trusted verifier to pin. */\n publicKey(): string;\n /**\n * Hashes and signs once per asset directory and profile. Undefined when the bundle is empty. Throws\n * when the assets do not match the profile.\n */\n sign(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates and immediately rebuilds one cached manifest. */\n refresh(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates one cached directory under every profile, or every cached directory when omitted. */\n invalidate(assetsDir?: string): void;\n /**\n * Base64url IEEE P1363 signature over `attestationBytes(tag, fields)` with the bundle key. Throws on a\n * malformed tag.\n */\n attest(tag: string, fields: readonly string[]): string;\n}\n\ninterface StoredState {\n privateKey: string;\n publicKey: string;\n revision: number;\n}\n\nfunction ensureStateDirectory(stateDir: string): void {\n fs.mkdirSync(stateDir, { recursive: true, mode: DIRECTORY_MODE });\n const stat = fs.lstatSync(stateDir);\n if (!stat.isDirectory() || stat.isSymbolicLink())\n throw new Error(`bundle signing state is not a directory: ${stateDir}`);\n fs.chmodSync(stateDir, DIRECTORY_MODE);\n}\n\nfunction withStateLock<T>(stateDir: string, operation: () => T): T {\n ensureStateDirectory(stateDir);\n const lockPath = path.join(stateDir, LOCK_FILE);\n if (!fs.existsSync(lockPath)) {\n const temporary = path.join(stateDir, `.${LOCK_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n fs.closeSync(fs.openSync(temporary, 'wx', FILE_MODE));\n try {\n const initial = new DatabaseSync(temporary);\n try {\n initial.exec('PRAGMA user_version = 1');\n } finally {\n initial.close();\n }\n // Publish a complete database, never an empty file another writer could mistake for a legacy lock.\n try {\n fs.linkSync(temporary, lockPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n }\n } finally {\n fs.rmSync(temporary, { force: true });\n }\n }\n if (!fs.lstatSync(lockPath).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const descriptor = fs.openSync(lockPath, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW);\n try {\n if (!fs.fstatSync(descriptor).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const header = Buffer.alloc(16);\n fs.readSync(descriptor, header, 0, header.length, 0);\n if (header.toString() !== 'SQLite format 3\\u0000') {\n // The old empty wx lock has no owner information. Never assume it is stale or unlink a live writer's lock.\n throw new Error(\n `legacy or invalid bundle signing state lock: ${lockPath}; stop all Po servers and inspect the lock before manual recovery`,\n );\n }\n fs.fchmodSync(descriptor, FILE_MODE);\n const database = new DatabaseSync(lockPath, { timeout: 5000 });\n try {\n // The kernel releases this lock even on SIGKILL. Keep its inode so concurrent writers share one lock.\n database.exec('BEGIN EXCLUSIVE');\n return operation();\n } finally {\n database.close();\n }\n } finally {\n fs.closeSync(descriptor);\n }\n}\n\nfunction atomicWriteState(stateDir: string, state: StoredState): void {\n const destination = path.join(stateDir, SIGNING_FILE);\n const temporary = path.join(stateDir, `.${SIGNING_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n const descriptor = fs.openSync(temporary, 'wx', FILE_MODE);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify(state, undefined, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n try {\n fs.renameSync(temporary, destination);\n fs.chmodSync(destination, FILE_MODE);\n } catch (error) {\n fs.rmSync(temporary, { force: true });\n throw error;\n }\n}\n\nfunction parseState(value: unknown, keyPath: string): StoredState {\n if (typeof value !== 'object' || value === null) throw new Error(`invalid bundle signing state: ${keyPath}`);\n const candidate = value as Partial<StoredState>;\n if (typeof candidate.privateKey !== 'string' || typeof candidate.publicKey !== 'string') {\n throw new Error(`invalid bundle signing state: ${keyPath}`);\n }\n const revision = candidate.revision === undefined ? 0 : candidate.revision;\n if (!Number.isSafeInteger(revision) || revision < 0) throw new Error(`invalid bundle signing revision: ${keyPath}`);\n let derived: string;\n try {\n derived = publicKeyOf(candidate.privateKey);\n } catch {\n throw new Error(`invalid bundle signing private key: ${keyPath}`);\n }\n if (derived !== candidate.publicKey) throw new Error(`bundle signing keys do not match: ${keyPath}`);\n return { privateKey: candidate.privateKey, publicKey: candidate.publicKey, revision };\n}\n\nfunction loadOrCreateState(stateDir: string): StoredState {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n try {\n const stat = fs.lstatSync(keyPath);\n if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`invalid bundle signing state file: ${keyPath}`);\n const parsed: unknown = JSON.parse(fs.readFileSync(keyPath, 'utf8'));\n const state = parseState(parsed, keyPath);\n fs.chmodSync(keyPath, FILE_MODE);\n if ((parsed as Partial<StoredState>).revision === undefined) atomicWriteState(stateDir, state);\n return state;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n\n const pair = generateKeyPairSync('ec', {\n namedCurve: CURVE,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' },\n publicKeyEncoding: { type: 'spki', format: 'der' },\n });\n const state: StoredState = {\n privateKey: pair.privateKey,\n publicKey: Buffer.from(pair.publicKey).toString('base64url'),\n revision: 0,\n };\n atomicWriteState(stateDir, state);\n return state;\n });\n}\n\nfunction reserveRevision(stateDir: string, expectedPublicKey: string): number {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n const state = parseState(JSON.parse(fs.readFileSync(keyPath, 'utf8')) as unknown, keyPath);\n if (state.publicKey !== expectedPublicKey)\n throw new Error('bundle signing key changed while the signer was active');\n if (state.revision === Number.MAX_SAFE_INTEGER) throw new Error('bundle signing revision is exhausted');\n const next = { ...state, revision: state.revision + 1 };\n atomicWriteState(stateDir, next);\n return next.revision;\n });\n}\n\nfunction contentTypeFor(filePath: string): string {\n return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/** Every servable regular file under the asset root, as browser paths. */\nfunction walk(root: string, current: string, into: BundleAsset[]): void {\n const entries = fs.readdirSync(current, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(current, entry.name);\n const stat = fs.lstatSync(full);\n if (stat.isSymbolicLink()) throw new Error(`refusing to sign symbolic link: ${full}`);\n if (stat.isDirectory()) {\n walk(root, full, into);\n continue;\n }\n if (!stat.isFile()) throw new Error(`refusing to omit unsupported asset: ${full}`);\n if (SKIPPED.has(path.extname(entry.name))) continue;\n const bytes = fs.readFileSync(full);\n into.push({\n path: `/${path\n .relative(root, full)\n .split(path.sep)\n .map((segment) => encodeURIComponent(segment))\n .join('/')}`,\n sha256: createHash('sha256').update(bytes).digest('hex'),\n byteLength: bytes.byteLength,\n contentType: contentTypeFor(full),\n });\n }\n}\n\nexport function createBundleSigner(stateDir: string, _onNotice: (message: string) => void = () => {}): BundleSigner {\n const key = loadOrCreateState(stateDir);\n const privateKey = createPrivateKey(key.privateKey);\n const cache = new Map<string, SignedBundleManifest | undefined>();\n const cacheKeyOf = (assetsDir: string, profile: BundleProfile): string => `${profile}:${path.resolve(assetsDir)}`;\n\n const signFresh = (assetsDir: string, profile: BundleProfile): SignedBundleManifest | undefined => {\n const rootStat = fs.lstatSync(assetsDir);\n if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {\n throw new Error(`refusing to sign an unsafe asset root: ${assetsDir}`);\n }\n const assets: BundleAsset[] = [];\n walk(assetsDir, assetsDir, assets);\n if (assets.length === 0) return undefined;\n assets.sort((left, right) => left.path.localeCompare(right.path));\n const manifest: BundleManifest = {\n version: BUNDLE_MANIFEST_VERSION,\n revision: reserveRevision(stateDir, key.publicKey),\n builtAt: Date.now(),\n assets,\n };\n if (!isBundleManifest(manifest, profile)) throw new Error(`refusing to sign an invalid ${profile} bundle manifest`);\n const signature = createSign(SIGN_ALGORITHM)\n .update(canonicalManifest(manifest))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n return { manifest, signature, publicKey: key.publicKey };\n };\n\n const sign = (assetsDir: string, profile: BundleProfile = 'web'): SignedBundleManifest | undefined => {\n const cacheKey = cacheKeyOf(assetsDir, profile);\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n const signed = signFresh(path.resolve(assetsDir), profile);\n cache.set(cacheKey, signed);\n return signed;\n };\n\n return {\n publicKey: () => key.publicKey,\n sign,\n refresh(assetsDir, profile = 'web') {\n cache.delete(cacheKeyOf(assetsDir, profile));\n return sign(assetsDir, profile);\n },\n invalidate(assetsDir) {\n if (assetsDir === undefined) {\n cache.clear();\n return;\n }\n for (const profile of PROFILES) cache.delete(cacheKeyOf(assetsDir, profile));\n },\n attest(tag, fields) {\n return createSign(SIGN_ALGORITHM)\n .update(attestationBytes(tag, fields))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n },\n };\n}\n\n/** Re-derives the public key from a stored private key. */\nexport function publicKeyOf(privateKeyPem: string): string {\n return Buffer.from(createPublicKey(privateKeyPem).export({ type: 'spki', format: 'der' }) as Buffer).toString(\n 'base64url',\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/bundleSigner/index.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/bundleSigner/index.ts"],"mappings":";;iBAmDiB;;EAEf;;;;;EAKA,KAAK,mBAAmB,UAAU,gBAAgB;;EAElD,QAAQ,mBAAmB,UAAU,gBAAgB;;EAErD,WAAW;;;;;EAKX,OAAO,aAAa;;wBAiLN,mBAAmB,kBAAkB,aAAY,2BAAsC;;wBA6DvF,YAAY"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{attestationBytes as e,canonicalManifest as t,isBundleManifest as n}from"../../types/bundleManifest.mjs";import{createHash as r,createPrivateKey as i,createPublicKey as a,createSign as o,generateKeyPairSync as s,randomBytes as c}from"node:crypto";import l from"node:fs";import u from"node:path";const
|
|
1
|
+
import{attestationBytes as e,canonicalManifest as t,isBundleManifest as n}from"../../types/bundleManifest.mjs";import{createHash as r,createPrivateKey as i,createPublicKey as a,createSign as o,generateKeyPairSync as s,randomBytes as c}from"node:crypto";import l from"node:fs";import u from"node:path";import{DatabaseSync as d}from"node:sqlite";const f=`signing.json`,p=`signing.lock`,m=`sha256`,h=/* @__PURE__ */ new Set([`.map`]),g=[`web`,`native`],_={".css":`text/css`,".gif":`image/gif`,".html":`text/html`,".ico":`image/x-icon`,".jpeg":`image/jpeg`,".jpg":`image/jpeg`,".js":`text/javascript`,".json":`application/json`,".mjs":`text/javascript`,".png":`image/png`,".svg":`image/svg+xml`,".txt":`text/plain`,".wasm":`application/wasm`,".webp":`image/webp`,".woff":`font/woff`,".woff2":`font/woff2`};function v(e){l.mkdirSync(e,{recursive:!0,mode:448});let t=l.lstatSync(e);if(!t.isDirectory()||t.isSymbolicLink())throw Error(`bundle signing state is not a directory: ${e}`);l.chmodSync(e,448)}function y(e,t){v(e);let n=u.join(e,p);if(!l.existsSync(n)){let t=u.join(e,`.${p}.${process.pid}.${c(8).toString(`hex`)}`);l.closeSync(l.openSync(t,`wx`,384));try{let e=new d(t);try{e.exec(`PRAGMA user_version = 1`)}finally{e.close()}try{l.linkSync(t,n)}catch(e){if(e.code!==`EEXIST`)throw e}}finally{l.rmSync(t,{force:!0})}}if(!l.lstatSync(n).isFile())throw Error(`invalid bundle signing lock file: ${n}`);let r=l.openSync(n,l.constants.O_RDWR|l.constants.O_NOFOLLOW);try{if(!l.fstatSync(r).isFile())throw Error(`invalid bundle signing lock file: ${n}`);let e=Buffer.alloc(16);if(l.readSync(r,e,0,e.length,0),e.toString()!==`SQLite format 3\0`)throw Error(`legacy or invalid bundle signing state lock: ${n}; stop all Po servers and inspect the lock before manual recovery`);l.fchmodSync(r,384);let i=new d(n,{timeout:5e3});try{return i.exec(`BEGIN EXCLUSIVE`),t()}finally{i.close()}}finally{l.closeSync(r)}}function b(e,t){let n=u.join(e,f),r=u.join(e,`.${f}.${process.pid}.${c(8).toString(`hex`)}`),i=l.openSync(r,`wx`,384);try{l.writeFileSync(i,`${JSON.stringify(t,void 0,2)}\n`),l.fsyncSync(i)}finally{l.closeSync(i)}try{l.renameSync(r,n),l.chmodSync(n,384)}catch(e){throw l.rmSync(r,{force:!0}),e}}function x(e,t){if(typeof e!=`object`||!e)throw Error(`invalid bundle signing state: ${t}`);let n=e;if(typeof n.privateKey!=`string`||typeof n.publicKey!=`string`)throw Error(`invalid bundle signing state: ${t}`);let r=n.revision===void 0?0:n.revision;if(!Number.isSafeInteger(r)||r<0)throw Error(`invalid bundle signing revision: ${t}`);let i;try{i=D(n.privateKey)}catch{throw Error(`invalid bundle signing private key: ${t}`)}if(i!==n.publicKey)throw Error(`bundle signing keys do not match: ${t}`);return{privateKey:n.privateKey,publicKey:n.publicKey,revision:r}}function S(e){return y(e,()=>{let t=u.join(e,f);try{let n=l.lstatSync(t);if(!n.isFile()||n.isSymbolicLink())throw Error(`invalid bundle signing state file: ${t}`);let r=JSON.parse(l.readFileSync(t,`utf8`)),i=x(r,t);return l.chmodSync(t,384),r.revision===void 0&&b(e,i),i}catch(e){if(e.code!==`ENOENT`)throw e}let n=s(`ec`,{namedCurve:`prime256v1`,privateKeyEncoding:{type:`pkcs8`,format:`pem`},publicKeyEncoding:{type:`spki`,format:`der`}}),r={privateKey:n.privateKey,publicKey:Buffer.from(n.publicKey).toString(`base64url`),revision:0};return b(e,r),r})}function C(e,t){return y(e,()=>{let n=u.join(e,f),r=x(JSON.parse(l.readFileSync(n,`utf8`)),n);if(r.publicKey!==t)throw Error(`bundle signing key changed while the signer was active`);if(r.revision===2**53-1)throw Error(`bundle signing revision is exhausted`);let i={...r,revision:r.revision+1};return b(e,i),i.revision})}function w(e){return _[u.extname(e).toLowerCase()]??`application/octet-stream`}function T(e,t,n){let i=l.readdirSync(t,{withFileTypes:!0});for(let a of i){let i=u.join(t,a.name),o=l.lstatSync(i);if(o.isSymbolicLink())throw Error(`refusing to sign symbolic link: ${i}`);if(o.isDirectory()){T(e,i,n);continue}if(!o.isFile())throw Error(`refusing to omit unsupported asset: ${i}`);if(h.has(u.extname(a.name)))continue;let s=l.readFileSync(i);n.push({path:`/${u.relative(e,i).split(u.sep).map(e=>encodeURIComponent(e)).join(`/`)}`,sha256:r(`sha256`).update(s).digest(`hex`),byteLength:s.byteLength,contentType:w(i)})}}function E(r,a=()=>{}){let s=S(r),c=i(s.privateKey),d=/* @__PURE__ */ new Map,f=(e,t)=>`${t}:${u.resolve(e)}`,p=(e,i)=>{let a=l.lstatSync(e);if(!a.isDirectory()||a.isSymbolicLink())throw Error(`refusing to sign an unsafe asset root: ${e}`);let u=[];if(T(e,e,u),u.length===0)return;u.sort((e,t)=>e.path.localeCompare(t.path));let d={version:2,revision:C(r,s.publicKey),builtAt:Date.now(),assets:u};if(!n(d,i))throw Error(`refusing to sign an invalid ${i} bundle manifest`);return{manifest:d,signature:o(m).update(t(d)).sign({key:c,dsaEncoding:`ieee-p1363`}).toString(`base64url`),publicKey:s.publicKey}},h=(e,t=`web`)=>{let n=f(e,t);if(d.has(n))return d.get(n);let r=p(u.resolve(e),t);return d.set(n,r),r};return{publicKey:()=>s.publicKey,sign:h,refresh(e,t=`web`){return d.delete(f(e,t)),h(e,t)},invalidate(e){if(e===void 0)d.clear();else for(let t of g)d.delete(f(e,t))},attest(t,n){return o(m).update(e(t,n)).sign({key:c,dsaEncoding:`ieee-p1363`}).toString(`base64url`)}}}function D(e){return Buffer.from(a(e).export({type:`spki`,format:`der`})).toString(`base64url`)}export{E as createBundleSigner,D as publicKeyOf};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["fs","path","SIGNING_FILE","SIGN_ALGORITHM","SKIPPED","PROFILES","MIME_TYPES","ensureStateDirectory","stateDir","stat","withStateLock","lockPath","descriptor","attempt","error","operation","atomicWriteState","destination","temporary","randomBytes","state","parseState","value","keyPath","candidate","revision","derived","publicKeyOf","loadOrCreateState","parsed","pair","generateKeyPairSync","reserveRevision","expectedPublicKey","next","contentTypeFor","filePath","walk","entries","current","entry","full","root","into","bytes","segment","createHash","createBundleSigner","key","privateKey","createPrivateKey","cache","cacheKeyOf","profile","assetsDir","signFresh","rootStat","assets","left","right","manifest","isBundleManifest","createSign","canonicalManifest","sign","cacheKey","signed","attestationBytes","tag","fields","createPublicKey","privateKeyPem"],"sources":["../../../src/services/bundleSigner/index.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n randomBytes,\n} from 'node:crypto';\n// sync: Public createBundleSigner and BundleSigner.sign/refresh contracts must remain synchronous until phase 2.\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n attestationBytes,\n canonicalManifest,\n isBundleManifest,\n} from '../../types/bundleManifest';\n\nconst SIGNING_FILE = 'signing.json';\nconst LOCK_FILE = 'signing.lock';\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst SKIPPED = new Set(['.map']);\nconst PROFILES: readonly BundleProfile[] = ['web', 'native'];\nconst MIME_TYPES: Readonly<Record<string, string>> = {\n '.css': 'text/css',\n '.gif': 'image/gif',\n '.html': 'text/html',\n '.ico': 'image/x-icon',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.mjs': 'text/javascript',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n};\n\nexport interface BundleSigner {\n /** Base64url SPKI for an independently trusted verifier to pin. */\n publicKey(): string;\n /**\n * Hashes and signs once per asset directory and profile. Undefined when the bundle is empty. Throws\n * when the assets do not match the profile.\n */\n sign(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates and immediately rebuilds one cached manifest. */\n refresh(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates one cached directory under every profile, or every cached directory when omitted. */\n invalidate(assetsDir?: string): void;\n /**\n * Base64url IEEE P1363 signature over `attestationBytes(tag, fields)` with the bundle key. Throws on a\n * malformed tag.\n */\n attest(tag: string, fields: readonly string[]): string;\n}\n\ninterface StoredState {\n privateKey: string;\n publicKey: string;\n revision: number;\n}\n\nfunction ensureStateDirectory(stateDir: string): void {\n fs.mkdirSync(stateDir, { recursive: true, mode: DIRECTORY_MODE });\n const stat = fs.lstatSync(stateDir);\n if (!stat.isDirectory() || stat.isSymbolicLink())\n throw new Error(`bundle signing state is not a directory: ${stateDir}`);\n fs.chmodSync(stateDir, DIRECTORY_MODE);\n}\n\nfunction withStateLock<T>(stateDir: string, operation: () => T): T {\n ensureStateDirectory(stateDir);\n const lockPath = path.join(stateDir, LOCK_FILE);\n let descriptor: number | undefined;\n for (let attempt = 0; attempt < 500; attempt += 1) {\n try {\n descriptor = fs.openSync(lockPath, 'wx', FILE_MODE);\n break;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);\n }\n }\n if (descriptor === undefined) throw new Error(`timed out waiting for bundle signing state lock: ${lockPath}`);\n try {\n return operation();\n } finally {\n fs.closeSync(descriptor);\n fs.rmSync(lockPath, { force: true });\n }\n}\n\nfunction atomicWriteState(stateDir: string, state: StoredState): void {\n const destination = path.join(stateDir, SIGNING_FILE);\n const temporary = path.join(stateDir, `.${SIGNING_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n const descriptor = fs.openSync(temporary, 'wx', FILE_MODE);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify(state, undefined, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n try {\n fs.renameSync(temporary, destination);\n fs.chmodSync(destination, FILE_MODE);\n } catch (error) {\n fs.rmSync(temporary, { force: true });\n throw error;\n }\n}\n\nfunction parseState(value: unknown, keyPath: string): StoredState {\n if (typeof value !== 'object' || value === null) throw new Error(`invalid bundle signing state: ${keyPath}`);\n const candidate = value as Partial<StoredState>;\n if (typeof candidate.privateKey !== 'string' || typeof candidate.publicKey !== 'string') {\n throw new Error(`invalid bundle signing state: ${keyPath}`);\n }\n const revision = candidate.revision === undefined ? 0 : candidate.revision;\n if (!Number.isSafeInteger(revision) || revision < 0) throw new Error(`invalid bundle signing revision: ${keyPath}`);\n let derived: string;\n try {\n derived = publicKeyOf(candidate.privateKey);\n } catch {\n throw new Error(`invalid bundle signing private key: ${keyPath}`);\n }\n if (derived !== candidate.publicKey) throw new Error(`bundle signing keys do not match: ${keyPath}`);\n return { privateKey: candidate.privateKey, publicKey: candidate.publicKey, revision };\n}\n\nfunction loadOrCreateState(stateDir: string): StoredState {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n try {\n const stat = fs.lstatSync(keyPath);\n if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`invalid bundle signing state file: ${keyPath}`);\n const parsed: unknown = JSON.parse(fs.readFileSync(keyPath, 'utf8'));\n const state = parseState(parsed, keyPath);\n fs.chmodSync(keyPath, FILE_MODE);\n if ((parsed as Partial<StoredState>).revision === undefined) atomicWriteState(stateDir, state);\n return state;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n\n const pair = generateKeyPairSync('ec', {\n namedCurve: CURVE,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' },\n publicKeyEncoding: { type: 'spki', format: 'der' },\n });\n const state: StoredState = {\n privateKey: pair.privateKey,\n publicKey: Buffer.from(pair.publicKey).toString('base64url'),\n revision: 0,\n };\n atomicWriteState(stateDir, state);\n return state;\n });\n}\n\nfunction reserveRevision(stateDir: string, expectedPublicKey: string): number {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n const state = parseState(JSON.parse(fs.readFileSync(keyPath, 'utf8')) as unknown, keyPath);\n if (state.publicKey !== expectedPublicKey)\n throw new Error('bundle signing key changed while the signer was active');\n if (state.revision === Number.MAX_SAFE_INTEGER) throw new Error('bundle signing revision is exhausted');\n const next = { ...state, revision: state.revision + 1 };\n atomicWriteState(stateDir, next);\n return next.revision;\n });\n}\n\nfunction contentTypeFor(filePath: string): string {\n return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/** Every servable regular file under the asset root, as browser paths. */\nfunction walk(root: string, current: string, into: BundleAsset[]): void {\n const entries = fs.readdirSync(current, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(current, entry.name);\n const stat = fs.lstatSync(full);\n if (stat.isSymbolicLink()) throw new Error(`refusing to sign symbolic link: ${full}`);\n if (stat.isDirectory()) {\n walk(root, full, into);\n continue;\n }\n if (!stat.isFile()) throw new Error(`refusing to omit unsupported asset: ${full}`);\n if (SKIPPED.has(path.extname(entry.name))) continue;\n const bytes = fs.readFileSync(full);\n into.push({\n path: `/${path\n .relative(root, full)\n .split(path.sep)\n .map((segment) => encodeURIComponent(segment))\n .join('/')}`,\n sha256: createHash('sha256').update(bytes).digest('hex'),\n byteLength: bytes.byteLength,\n contentType: contentTypeFor(full),\n });\n }\n}\n\nexport function createBundleSigner(stateDir: string, _onNotice: (message: string) => void = () => {}): BundleSigner {\n const key = loadOrCreateState(stateDir);\n const privateKey = createPrivateKey(key.privateKey);\n const cache = new Map<string, SignedBundleManifest | undefined>();\n const cacheKeyOf = (assetsDir: string, profile: BundleProfile): string => `${profile}:${path.resolve(assetsDir)}`;\n\n const signFresh = (assetsDir: string, profile: BundleProfile): SignedBundleManifest | undefined => {\n const rootStat = fs.lstatSync(assetsDir);\n if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {\n throw new Error(`refusing to sign an unsafe asset root: ${assetsDir}`);\n }\n const assets: BundleAsset[] = [];\n walk(assetsDir, assetsDir, assets);\n if (assets.length === 0) return undefined;\n assets.sort((left, right) => left.path.localeCompare(right.path));\n const manifest: BundleManifest = {\n version: BUNDLE_MANIFEST_VERSION,\n revision: reserveRevision(stateDir, key.publicKey),\n builtAt: Date.now(),\n assets,\n };\n if (!isBundleManifest(manifest, profile)) throw new Error(`refusing to sign an invalid ${profile} bundle manifest`);\n const signature = createSign(SIGN_ALGORITHM)\n .update(canonicalManifest(manifest))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n return { manifest, signature, publicKey: key.publicKey };\n };\n\n const sign = (assetsDir: string, profile: BundleProfile = 'web'): SignedBundleManifest | undefined => {\n const cacheKey = cacheKeyOf(assetsDir, profile);\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n const signed = signFresh(path.resolve(assetsDir), profile);\n cache.set(cacheKey, signed);\n return signed;\n };\n\n return {\n publicKey: () => key.publicKey,\n sign,\n refresh(assetsDir, profile = 'web') {\n cache.delete(cacheKeyOf(assetsDir, profile));\n return sign(assetsDir, profile);\n },\n invalidate(assetsDir) {\n if (assetsDir === undefined) {\n cache.clear();\n return;\n }\n for (const profile of PROFILES) cache.delete(cacheKeyOf(assetsDir, profile));\n },\n attest(tag, fields) {\n return createSign(SIGN_ALGORITHM)\n .update(attestationBytes(tag, fields))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n },\n };\n}\n\n/** Re-derives the public key from a stored private key. */\nexport function publicKeyOf(privateKeyPem: string): string {\n return Buffer.from(createPublicKey(privateKeyPem).export({ type: 'spki', format: 'der' }) as Buffer).toString(\n 'base64url',\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["fs","path","SIGNING_FILE","LOCK_FILE","SIGN_ALGORITHM","SKIPPED","PROFILES","MIME_TYPES","ensureStateDirectory","stateDir","stat","withStateLock","lockPath","temporary","randomBytes","initial","DatabaseSync","error","descriptor","header","database","operation","atomicWriteState","destination","state","parseState","value","keyPath","candidate","revision","derived","publicKeyOf","loadOrCreateState","parsed","pair","generateKeyPairSync","reserveRevision","expectedPublicKey","next","contentTypeFor","filePath","walk","entries","current","entry","full","root","into","bytes","segment","createHash","createBundleSigner","key","privateKey","createPrivateKey","cache","cacheKeyOf","profile","assetsDir","signFresh","rootStat","assets","left","right","manifest","isBundleManifest","createSign","canonicalManifest","sign","cacheKey","signed","attestationBytes","tag","fields","createPublicKey","privateKeyPem"],"sources":["../../../src/services/bundleSigner/index.ts"],"sourcesContent":["import {\n createHash,\n createPrivateKey,\n createPublicKey,\n createSign,\n generateKeyPairSync,\n randomBytes,\n} from 'node:crypto';\n// sync: Public createBundleSigner and BundleSigner.sign/refresh contracts must remain synchronous until phase 2.\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport {\n BUNDLE_MANIFEST_VERSION,\n type BundleAsset,\n type BundleManifest,\n type BundleProfile,\n type SignedBundleManifest,\n attestationBytes,\n canonicalManifest,\n isBundleManifest,\n} from '../../types/bundleManifest';\n\nconst SIGNING_FILE = 'signing.json';\nconst LOCK_FILE = 'signing.lock';\nconst FILE_MODE = 0o600;\nconst DIRECTORY_MODE = 0o700;\nconst CURVE = 'prime256v1';\nconst SIGN_ALGORITHM = 'sha256';\nconst SKIPPED = new Set(['.map']);\nconst PROFILES: readonly BundleProfile[] = ['web', 'native'];\nconst MIME_TYPES: Readonly<Record<string, string>> = {\n '.css': 'text/css',\n '.gif': 'image/gif',\n '.html': 'text/html',\n '.ico': 'image/x-icon',\n '.jpeg': 'image/jpeg',\n '.jpg': 'image/jpeg',\n '.js': 'text/javascript',\n '.json': 'application/json',\n '.mjs': 'text/javascript',\n '.png': 'image/png',\n '.svg': 'image/svg+xml',\n '.txt': 'text/plain',\n '.wasm': 'application/wasm',\n '.webp': 'image/webp',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n};\n\nexport interface BundleSigner {\n /** Base64url SPKI for an independently trusted verifier to pin. */\n publicKey(): string;\n /**\n * Hashes and signs once per asset directory and profile. Undefined when the bundle is empty. Throws\n * when the assets do not match the profile.\n */\n sign(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates and immediately rebuilds one cached manifest. */\n refresh(assetsDir: string, profile?: BundleProfile): SignedBundleManifest | undefined;\n /** Invalidates one cached directory under every profile, or every cached directory when omitted. */\n invalidate(assetsDir?: string): void;\n /**\n * Base64url IEEE P1363 signature over `attestationBytes(tag, fields)` with the bundle key. Throws on a\n * malformed tag.\n */\n attest(tag: string, fields: readonly string[]): string;\n}\n\ninterface StoredState {\n privateKey: string;\n publicKey: string;\n revision: number;\n}\n\nfunction ensureStateDirectory(stateDir: string): void {\n fs.mkdirSync(stateDir, { recursive: true, mode: DIRECTORY_MODE });\n const stat = fs.lstatSync(stateDir);\n if (!stat.isDirectory() || stat.isSymbolicLink())\n throw new Error(`bundle signing state is not a directory: ${stateDir}`);\n fs.chmodSync(stateDir, DIRECTORY_MODE);\n}\n\nfunction withStateLock<T>(stateDir: string, operation: () => T): T {\n ensureStateDirectory(stateDir);\n const lockPath = path.join(stateDir, LOCK_FILE);\n if (!fs.existsSync(lockPath)) {\n const temporary = path.join(stateDir, `.${LOCK_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n fs.closeSync(fs.openSync(temporary, 'wx', FILE_MODE));\n try {\n const initial = new DatabaseSync(temporary);\n try {\n initial.exec('PRAGMA user_version = 1');\n } finally {\n initial.close();\n }\n // Publish a complete database, never an empty file another writer could mistake for a legacy lock.\n try {\n fs.linkSync(temporary, lockPath);\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;\n }\n } finally {\n fs.rmSync(temporary, { force: true });\n }\n }\n if (!fs.lstatSync(lockPath).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const descriptor = fs.openSync(lockPath, fs.constants.O_RDWR | fs.constants.O_NOFOLLOW);\n try {\n if (!fs.fstatSync(descriptor).isFile()) throw new Error(`invalid bundle signing lock file: ${lockPath}`);\n const header = Buffer.alloc(16);\n fs.readSync(descriptor, header, 0, header.length, 0);\n if (header.toString() !== 'SQLite format 3\\u0000') {\n // The old empty wx lock has no owner information. Never assume it is stale or unlink a live writer's lock.\n throw new Error(\n `legacy or invalid bundle signing state lock: ${lockPath}; stop all Po servers and inspect the lock before manual recovery`,\n );\n }\n fs.fchmodSync(descriptor, FILE_MODE);\n const database = new DatabaseSync(lockPath, { timeout: 5000 });\n try {\n // The kernel releases this lock even on SIGKILL. Keep its inode so concurrent writers share one lock.\n database.exec('BEGIN EXCLUSIVE');\n return operation();\n } finally {\n database.close();\n }\n } finally {\n fs.closeSync(descriptor);\n }\n}\n\nfunction atomicWriteState(stateDir: string, state: StoredState): void {\n const destination = path.join(stateDir, SIGNING_FILE);\n const temporary = path.join(stateDir, `.${SIGNING_FILE}.${process.pid}.${randomBytes(8).toString('hex')}`);\n const descriptor = fs.openSync(temporary, 'wx', FILE_MODE);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify(state, undefined, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n try {\n fs.renameSync(temporary, destination);\n fs.chmodSync(destination, FILE_MODE);\n } catch (error) {\n fs.rmSync(temporary, { force: true });\n throw error;\n }\n}\n\nfunction parseState(value: unknown, keyPath: string): StoredState {\n if (typeof value !== 'object' || value === null) throw new Error(`invalid bundle signing state: ${keyPath}`);\n const candidate = value as Partial<StoredState>;\n if (typeof candidate.privateKey !== 'string' || typeof candidate.publicKey !== 'string') {\n throw new Error(`invalid bundle signing state: ${keyPath}`);\n }\n const revision = candidate.revision === undefined ? 0 : candidate.revision;\n if (!Number.isSafeInteger(revision) || revision < 0) throw new Error(`invalid bundle signing revision: ${keyPath}`);\n let derived: string;\n try {\n derived = publicKeyOf(candidate.privateKey);\n } catch {\n throw new Error(`invalid bundle signing private key: ${keyPath}`);\n }\n if (derived !== candidate.publicKey) throw new Error(`bundle signing keys do not match: ${keyPath}`);\n return { privateKey: candidate.privateKey, publicKey: candidate.publicKey, revision };\n}\n\nfunction loadOrCreateState(stateDir: string): StoredState {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n try {\n const stat = fs.lstatSync(keyPath);\n if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`invalid bundle signing state file: ${keyPath}`);\n const parsed: unknown = JSON.parse(fs.readFileSync(keyPath, 'utf8'));\n const state = parseState(parsed, keyPath);\n fs.chmodSync(keyPath, FILE_MODE);\n if ((parsed as Partial<StoredState>).revision === undefined) atomicWriteState(stateDir, state);\n return state;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;\n }\n\n const pair = generateKeyPairSync('ec', {\n namedCurve: CURVE,\n privateKeyEncoding: { type: 'pkcs8', format: 'pem' },\n publicKeyEncoding: { type: 'spki', format: 'der' },\n });\n const state: StoredState = {\n privateKey: pair.privateKey,\n publicKey: Buffer.from(pair.publicKey).toString('base64url'),\n revision: 0,\n };\n atomicWriteState(stateDir, state);\n return state;\n });\n}\n\nfunction reserveRevision(stateDir: string, expectedPublicKey: string): number {\n return withStateLock(stateDir, () => {\n const keyPath = path.join(stateDir, SIGNING_FILE);\n const state = parseState(JSON.parse(fs.readFileSync(keyPath, 'utf8')) as unknown, keyPath);\n if (state.publicKey !== expectedPublicKey)\n throw new Error('bundle signing key changed while the signer was active');\n if (state.revision === Number.MAX_SAFE_INTEGER) throw new Error('bundle signing revision is exhausted');\n const next = { ...state, revision: state.revision + 1 };\n atomicWriteState(stateDir, next);\n return next.revision;\n });\n}\n\nfunction contentTypeFor(filePath: string): string {\n return MIME_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';\n}\n\n/** Every servable regular file under the asset root, as browser paths. */\nfunction walk(root: string, current: string, into: BundleAsset[]): void {\n const entries = fs.readdirSync(current, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(current, entry.name);\n const stat = fs.lstatSync(full);\n if (stat.isSymbolicLink()) throw new Error(`refusing to sign symbolic link: ${full}`);\n if (stat.isDirectory()) {\n walk(root, full, into);\n continue;\n }\n if (!stat.isFile()) throw new Error(`refusing to omit unsupported asset: ${full}`);\n if (SKIPPED.has(path.extname(entry.name))) continue;\n const bytes = fs.readFileSync(full);\n into.push({\n path: `/${path\n .relative(root, full)\n .split(path.sep)\n .map((segment) => encodeURIComponent(segment))\n .join('/')}`,\n sha256: createHash('sha256').update(bytes).digest('hex'),\n byteLength: bytes.byteLength,\n contentType: contentTypeFor(full),\n });\n }\n}\n\nexport function createBundleSigner(stateDir: string, _onNotice: (message: string) => void = () => {}): BundleSigner {\n const key = loadOrCreateState(stateDir);\n const privateKey = createPrivateKey(key.privateKey);\n const cache = new Map<string, SignedBundleManifest | undefined>();\n const cacheKeyOf = (assetsDir: string, profile: BundleProfile): string => `${profile}:${path.resolve(assetsDir)}`;\n\n const signFresh = (assetsDir: string, profile: BundleProfile): SignedBundleManifest | undefined => {\n const rootStat = fs.lstatSync(assetsDir);\n if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) {\n throw new Error(`refusing to sign an unsafe asset root: ${assetsDir}`);\n }\n const assets: BundleAsset[] = [];\n walk(assetsDir, assetsDir, assets);\n if (assets.length === 0) return undefined;\n assets.sort((left, right) => left.path.localeCompare(right.path));\n const manifest: BundleManifest = {\n version: BUNDLE_MANIFEST_VERSION,\n revision: reserveRevision(stateDir, key.publicKey),\n builtAt: Date.now(),\n assets,\n };\n if (!isBundleManifest(manifest, profile)) throw new Error(`refusing to sign an invalid ${profile} bundle manifest`);\n const signature = createSign(SIGN_ALGORITHM)\n .update(canonicalManifest(manifest))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n return { manifest, signature, publicKey: key.publicKey };\n };\n\n const sign = (assetsDir: string, profile: BundleProfile = 'web'): SignedBundleManifest | undefined => {\n const cacheKey = cacheKeyOf(assetsDir, profile);\n if (cache.has(cacheKey)) return cache.get(cacheKey);\n const signed = signFresh(path.resolve(assetsDir), profile);\n cache.set(cacheKey, signed);\n return signed;\n };\n\n return {\n publicKey: () => key.publicKey,\n sign,\n refresh(assetsDir, profile = 'web') {\n cache.delete(cacheKeyOf(assetsDir, profile));\n return sign(assetsDir, profile);\n },\n invalidate(assetsDir) {\n if (assetsDir === undefined) {\n cache.clear();\n return;\n }\n for (const profile of PROFILES) cache.delete(cacheKeyOf(assetsDir, profile));\n },\n attest(tag, fields) {\n return createSign(SIGN_ALGORITHM)\n .update(attestationBytes(tag, fields))\n .sign({ key: privateKey, dsaEncoding: 'ieee-p1363' })\n .toString('base64url');\n },\n };\n}\n\n/** Re-derives the public key from a stored private key. */\nexport function publicKeyOf(privateKeyPem: string): string {\n return Buffer.from(createPublicKey(privateKeyPem).export({ type: 'spki', format: 'der' }) as Buffer).toString(\n 'base64url',\n );\n}\n"],"mappings":"wVAwBA,MAAME,EAAe,eACfC,EAAY,eAIZC,EAAiB,SACjBC,kBAAU,IAAI,IAAI,CAAC,MAAM,CAAC,EAC1BC,EAAqC,CAAC,MAAO,QAAQ,EACrDC,EAA+C,CACnD,OAAQ,WACR,OAAQ,YACR,QAAS,YACT,OAAQ,eACR,QAAS,aACT,OAAQ,aACR,MAAO,kBACP,QAAS,mBACT,OAAQ,kBACR,OAAQ,YACR,OAAQ,gBACR,OAAQ,aACR,QAAS,mBACT,QAAS,aACT,QAAS,YACT,SAAU,YACZ,EA2BA,SAASC,EAAqB,EAAwB,CACpDR,EAAG,UAAUS,EAAU,CAAE,UAAW,GAAM,KAAM,GAAe,CAAC,EAChE,IAAMC,EAAOV,EAAG,UAAUS,CAAQ,EAClC,GAAI,CAACC,EAAK,YAAY,GAAKA,EAAK,eAAe,EAC7C,MAAU,MAAM,4CAA4CD,GAAU,EACxET,EAAG,UAAUS,EAAU,GAAc,CACvC,CAEA,SAASE,EAAiB,EAAkB,EAAuB,CACjEH,EAAqBC,CAAQ,EAC7B,IAAMG,EAAWX,EAAK,KAAKQ,EAAUN,CAAS,EAC9C,GAAI,CAACH,EAAG,WAAWY,CAAQ,EAAG,CAC5B,IAAMC,EAAYZ,EAAK,KAAKQ,EAAU,IAAIN,EAAU,GAAG,QAAQ,IAAI,GAAGW,EAAY,CAAC,CAAC,CAAC,SAAS,KAAK,GAAG,EACtGd,EAAG,UAAUA,EAAG,SAASa,EAAW,KAAM,GAAS,CAAC,EACpD,GAAI,CACF,IAAME,EAAU,IAAIC,EAAaH,CAAS,EAC1C,GAAI,CACFE,EAAQ,KAAK,yBAAyB,CACxC,QAAU,CACRA,EAAQ,MAAM,CAChB,CAEA,GAAI,CACFf,EAAG,SAASa,EAAWD,CAAQ,CACjC,OAASK,EAAO,CACd,GAAKA,EAAgC,OAAS,SAAU,MAAMA,CAChE,CACF,QAAU,CACRjB,EAAG,OAAOa,EAAW,CAAE,MAAO,EAAK,CAAC,CACtC,CACF,CACA,GAAI,CAACb,EAAG,UAAUY,CAAQ,CAAC,CAAC,OAAO,EAAG,MAAU,MAAM,qCAAqCA,GAAU,EACrG,IAAMM,EAAalB,EAAG,SAASY,EAAUZ,EAAG,UAAU,OAASA,EAAG,UAAU,UAAU,EACtF,GAAI,CACF,GAAI,CAACA,EAAG,UAAUkB,CAAU,CAAC,CAAC,OAAO,EAAG,MAAU,MAAM,qCAAqCN,GAAU,EACvG,IAAMO,EAAS,OAAO,MAAM,EAAE,EAE9B,GADAnB,EAAG,SAASkB,EAAYC,EAAQ,EAAGA,EAAO,OAAQ,CAAC,EAC/CA,EAAO,SAAS,IAAM,oBAExB,MAAU,MACR,gDAAgDP,EAAS,kEAC3D,EAEFZ,EAAG,WAAWkB,EAAY,GAAS,EACnC,IAAME,EAAW,IAAIJ,EAAaJ,EAAU,CAAE,QAAS,GAAK,CAAC,EAC7D,GAAI,CAGF,OADAQ,EAAS,KAAK,iBAAiB,EACxBC,EAAU,CACnB,QAAU,CACRD,EAAS,MAAM,CACjB,CACF,QAAU,CACRpB,EAAG,UAAUkB,CAAU,CACzB,CACF,CAEA,SAASI,EAAiB,EAAkB,EAA0B,CACpE,IAAMC,EAActB,EAAK,KAAKQ,EAAUP,CAAY,EAC9CW,EAAYZ,EAAK,KAAKQ,EAAU,IAAIP,EAAa,GAAG,QAAQ,IAAI,GAAGY,EAAY,CAAC,CAAC,CAAC,SAAS,KAAK,GAAG,EACnGI,EAAalB,EAAG,SAASa,EAAW,KAAM,GAAS,EACzD,GAAI,CACFb,EAAG,cAAckB,EAAY,GAAG,KAAK,UAAUM,EAAO,IAAA,GAAW,CAAC,EAAE,GAAG,EACvExB,EAAG,UAAUkB,CAAU,CACzB,QAAU,CACRlB,EAAG,UAAUkB,CAAU,CACzB,CACA,GAAI,CACFlB,EAAG,WAAWa,EAAWU,CAAW,EACpCvB,EAAG,UAAUuB,EAAa,GAAS,CACrC,OAASN,EAAO,CAEd,MADAjB,EAAG,OAAOa,EAAW,CAAE,MAAO,EAAK,CAAC,EAC9BI,CACR,CACF,CAEA,SAASQ,EAAW,EAAgB,EAA8B,CAChE,GAAI,OAAOC,GAAU,WAAYA,EAAgB,MAAU,MAAM,iCAAiCC,GAAS,EAC3G,IAAMC,EAAYF,EAClB,GAAI,OAAOE,EAAU,YAAe,UAAY,OAAOA,EAAU,WAAc,SAC7E,MAAU,MAAM,iCAAiCD,GAAS,EAE5D,IAAME,EAAWD,EAAU,WAAa,IAAA,GAAY,EAAIA,EAAU,SAClE,GAAI,CAAC,OAAO,cAAcC,CAAQ,GAAKA,EAAW,EAAG,MAAU,MAAM,oCAAoCF,GAAS,EAClH,IAAIG,EACJ,GAAI,CACF,EAAUC,EAAYH,EAAU,UAAU,CAC5C,MAAQ,CACN,MAAU,MAAM,uCAAuCD,GAAS,CAClE,CACA,GAAIG,IAAYF,EAAU,UAAW,MAAU,MAAM,qCAAqCD,GAAS,EACnG,MAAO,CAAE,WAAYC,EAAU,WAAY,UAAWA,EAAU,UAAW,UAAS,CACtF,CAEA,SAASI,EAAkB,EAA+B,CACxD,OAAOrB,EAAcF,MAAgB,CACnC,IAAMkB,EAAU1B,EAAK,KAAKQ,EAAUP,CAAY,EAChD,GAAI,CACF,IAAMQ,EAAOV,EAAG,UAAU2B,CAAO,EACjC,GAAI,CAACjB,EAAK,OAAO,GAAKA,EAAK,eAAe,EAAG,MAAU,MAAM,sCAAsCiB,GAAS,EAC5G,IAAMM,EAAkB,KAAK,MAAMjC,EAAG,aAAa2B,EAAS,MAAM,CAAC,EAC7DH,EAAQC,EAAWQ,EAAQN,CAAO,EAGxC,OAFA3B,EAAG,UAAU2B,EAAS,GAAS,EAC1BM,EAAgC,WAAa,IAAA,IAAWX,EAAiBb,EAAUe,CAAK,EACtFA,CACT,OAASP,EAAO,CACd,GAAKA,EAAgC,OAAS,SAAU,MAAMA,CAChE,CAEA,IAAMiB,EAAOC,EAAoB,KAAM,CACrC,WAAY,aACZ,mBAAoB,CAAE,KAAM,QAAS,OAAQ,KAAM,EACnD,kBAAmB,CAAE,KAAM,OAAQ,OAAQ,KAAM,CACnD,CAAC,EACKX,EAAqB,CACzB,WAAYU,EAAK,WACjB,UAAW,OAAO,KAAKA,EAAK,SAAS,CAAC,CAAC,SAAS,WAAW,EAC3D,SAAU,CACZ,EAEA,OADAZ,EAAiBb,EAAUe,CAAK,EACzBA,CACT,CAAC,CACH,CAEA,SAASY,EAAgB,EAAkB,EAAmC,CAC5E,OAAOzB,EAAcF,MAAgB,CACnC,IAAMkB,EAAU1B,EAAK,KAAKQ,EAAUP,CAAY,EAC1CsB,EAAQC,EAAW,KAAK,MAAMzB,EAAG,aAAa2B,EAAS,MAAM,CAAC,EAAcA,CAAO,EACzF,GAAIH,EAAM,YAAca,EACtB,MAAU,MAAM,wDAAwD,EAC1E,GAAIb,EAAM,mBAAsC,MAAU,MAAM,sCAAsC,EACtG,IAAMc,EAAO,CAAE,GAAGd,EAAO,SAAUA,EAAM,SAAW,CAAE,EAEtD,OADAF,EAAiBb,EAAU6B,CAAI,EACxBA,EAAK,QACd,CAAC,CACH,CAEA,SAASC,EAAe,EAA0B,CAChD,OAAOhC,EAAWN,EAAK,QAAQuC,CAAQ,CAAC,CAAC,YAAY,IAAM,0BAC7D,CAGA,SAASC,EAAK,EAAc,EAAiB,EAA2B,CACtE,IAAMC,EAAU1C,EAAG,YAAY2C,EAAS,CAAE,cAAe,EAAK,CAAC,EAC/D,IAAK,IAAMC,KAASF,EAAS,CAC3B,IAAMG,EAAO5C,EAAK,KAAK0C,EAASC,EAAM,IAAI,EACpClC,EAAOV,EAAG,UAAU6C,CAAI,EAC9B,GAAInC,EAAK,eAAe,EAAG,MAAU,MAAM,mCAAmCmC,GAAM,EACpF,GAAInC,EAAK,YAAY,EAAG,CACtB+B,EAAKK,EAAMD,EAAME,CAAI,EACrB,QACF,CACA,GAAI,CAACrC,EAAK,OAAO,EAAG,MAAU,MAAM,uCAAuCmC,GAAM,EACjF,GAAIxC,EAAQ,IAAIJ,EAAK,QAAQ2C,EAAM,IAAI,CAAC,EAAG,SAC3C,IAAMI,EAAQhD,EAAG,aAAa6C,CAAI,EAClCE,EAAK,KAAK,CACR,KAAM,IAAI9C,EACP,SAAS6C,EAAMD,CAAI,CAAC,CACpB,MAAM5C,EAAK,GAAG,CAAC,CACf,IAAK,GAAY,mBAAmBgD,CAAO,CAAC,CAAC,CAC7C,KAAK,GAAG,IACX,OAAQC,EAAW,QAAQ,CAAC,CAAC,OAAOF,CAAK,CAAC,CAAC,OAAO,KAAK,EACvD,WAAYA,EAAM,WAClB,YAAaT,EAAeM,CAAI,CAClC,CAAC,CACH,CACF,CAEA,SAAgBM,EAAmB,EAAkB,MAA6C,CAAC,EAAiB,CAClH,IAAMC,EAAMpB,EAAkBvB,CAAQ,EAChC4C,EAAaC,EAAiBF,EAAI,UAAU,EAC5CG,kBAAQ,IAAI,IACZC,GAAc,EAAmB,IAAmC,GAAGC,EAAQ,GAAGxD,EAAK,QAAQyD,CAAS,IAExGC,GAAa,EAAmB,IAA6D,CACjG,IAAMC,EAAW5D,EAAG,UAAU0D,CAAS,EACvC,GAAI,CAACE,EAAS,YAAY,GAAKA,EAAS,eAAe,EACrD,MAAU,MAAM,0CAA0CF,GAAW,EAEvE,IAAMG,EAAwB,CAAC,EAE/B,GADApB,EAAKiB,EAAWA,EAAWG,CAAM,EAC7BA,EAAO,SAAW,EAAG,OACzBA,EAAO,MAAM,EAAM,IAAUC,EAAK,KAAK,cAAcC,EAAM,IAAI,CAAC,EAChE,IAAMC,EAA2B,CAC/B,QAAA,EACA,SAAU5B,EAAgB3B,EAAU2C,EAAI,SAAS,EACjD,QAAS,KAAK,IAAI,EAClB,QACF,EACA,GAAI,CAACa,EAAiBD,EAAUP,CAAO,EAAG,MAAU,MAAM,+BAA+BA,EAAQ,iBAAiB,EAKlH,MAAO,CAAE,WAAU,UAJDS,EAAW9D,CAAc,CAAC,CACzC,OAAO+D,EAAkBH,CAAQ,CAAC,CAAC,CACnC,KAAK,CAAE,IAAKX,EAAY,YAAa,YAAa,CAAC,CAAC,CACpD,SAAS,WACe,EAAG,UAAWD,EAAI,SAAU,CACzD,EAEMgB,GAAQ,EAAmB,EAAyB,QAA4C,CACpG,IAAMC,EAAWb,EAAWE,EAAWD,CAAO,EAC9C,GAAIF,EAAM,IAAIc,CAAQ,EAAG,OAAOd,EAAM,IAAIc,CAAQ,EAClD,IAAMC,EAASX,EAAU1D,EAAK,QAAQyD,CAAS,EAAGD,CAAO,EAEzD,OADAF,EAAM,IAAIc,EAAUC,CAAM,EACnBA,CACT,EAEA,MAAO,CACL,cAAiBlB,EAAI,UACrB,OACA,QAAQ,EAAW,EAAU,MAAO,CAElC,OADAG,EAAM,OAAOC,EAAWE,EAAWD,CAAO,CAAC,EACpCW,EAAKV,EAAWD,CAAO,CAChC,EACA,WAAW,EAAW,CACpB,GAAIC,IAAc,IAAA,GAChBH,EAAM,MAAM,OAGd,IAAK,IAAME,KAAWnD,EAAUiD,EAAM,OAAOC,EAAWE,EAAWD,CAAO,CAAC,CAC7E,EACA,OAAO,EAAK,EAAQ,CAClB,OAAOS,EAAW9D,CAAc,CAAC,CAC9B,OAAOmE,EAAiBC,EAAKC,CAAM,CAAC,CAAC,CACrC,KAAK,CAAE,IAAKpB,EAAY,YAAa,YAAa,CAAC,CAAC,CACpD,SAAS,WAAW,CACzB,CACF,CACF,CAGA,SAAgBtB,EAAY,EAA+B,CACzD,OAAO,OAAO,KAAK2C,EAAgBC,CAAa,CAAC,CAAC,OAAO,CAAE,KAAM,OAAQ,OAAQ,KAAM,CAAC,CAAW,CAAC,CAAC,SACnG,WACF,CACF"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e,t;function n(n,r){return e=n,t=r,()=>{e===n&&(e=void 0,t=void 0)}}async function r(e,n){if(!t)throw Error(`Client identity name updates are unavailable.`);await t(e,n)}function i(){return e?.()}async function a(e,t){let n=await t();if(n.status===404)return;if(!n.ok)throw Error(`Client device refresh failed (${n.status}).`);let r=await n.json();if(typeof r!=`object`||!r||!(`devices`in r)||!Array.isArray(r.devices))return;let i=r.devices.find(t=>typeof t==`object`&&!!t&&`deviceId`in t&&t.deviceId===e.current().deviceId);typeof i==`object`&&i&&`deviceName`in i&&`nameUpdatedAt`in i&&(typeof i.deviceName!=`string`||typeof i.nameUpdatedAt!=`number`||!Number.isSafeInteger(i.nameUpdatedAt)||i.nameUpdatedAt<0||await e.updateName(i.deviceName,i.nameUpdatedAt))}const o=`po.client.device.v1`,s=`po.client.instance.v1`;async function c(e){let t={...e.initial},n=!0,r=/* @__PURE__ */ new Set;try{await e.exclusive(async()=>{let n=e.parse(await e.read());if(n)t={...n,...t.instanceId?{instanceId:t.instanceId}:{}};else{let{instanceId:n,...r}=t;await e.write(r)}})}catch{n=!1}return{current:()=>({...t}),persistent:()=>n,subscribe(e){return r.add(e),()=>r.delete(e)},async updateName(i,a){let o=e.normalizeName(i);if(!o)throw Error(`Enter a device name from 1 to 64 characters.`);let s=a??Math.max(e.now(),t.nameUpdatedAt+1);if(!Number.isSafeInteger(s)||s<0)throw Error(`Invalid device name timestamp.`);if(s<t.nameUpdatedAt||s===t.nameUpdatedAt&&o===t.deviceName)return;let c=async()=>{let n=e.parse(await e.read());if(n&&n.deviceId===t.deviceId&&n.nameUpdatedAt>t.nameUpdatedAt&&(t={...t,deviceName:n.deviceName,nameUpdatedAt:n.nameUpdatedAt}),s<t.nameUpdatedAt||s===t.nameUpdatedAt&&o===t.deviceName)return;let r={...t,deviceName:o,nameUpdatedAt:s},{instanceId:i,...a}=r;await e.write(a),t=r};if(n)try{await e.exclusive(c)}catch{n=!1,t={...t,deviceName:o,nameUpdatedAt:s}}else t={...t,deviceName:o,nameUpdatedAt:s};for(let e of r)e()}}}async function l(e){let t=e.randomId(),n=!0;try{let n=e.sessionStorage.getItem(s);if(e.isToken(n)&&n&&await e.claimInstance(n))t=n;else if(!await e.claimInstance(t))throw Error(`The tab identity could not be claimed.`);e.sessionStorage.setItem(s,t)}catch{n=!1}let r=await c({...e,initial:{...e.initial,instanceId:t},read:async()=>{let t=e.localStorage.getItem(o);return t===null?void 0:JSON.parse(t)},write:async t=>e.localStorage.setItem(o,JSON.stringify(t)),exclusive:t=>e.exclusive(o,t)}),i=r.current(),a=/* @__PURE__ */ new Set,l=()=>{for(let e of a)e()},u=r.subscribe(()=>{i=r.current(),l()}),d=e.onStorage(()=>{try{let t=e.localStorage.getItem(o),n=t===null?void 0:e.parse(JSON.parse(t));n&&n.deviceId===i.deviceId&&(n.nameUpdatedAt>i.nameUpdatedAt||n.nameUpdatedAt===i.nameUpdatedAt&&n.deviceName!==i.deviceName)&&(i={...i,deviceName:n.deviceName,nameUpdatedAt:n.nameUpdatedAt},l())}catch{n=!1}});return{current:()=>({...i}),persistent:()=>n&&r.persistent(),async updateName(t,n){await r.updateName(t,n??Math.max(e.now(),i.nameUpdatedAt+1))},subscribe(e){return a.add(e),()=>a.delete(e)},dispose(){d(),u(),a.clear()}}}exports.bindClientMetadata=n,exports.clientMetadata=i,exports.createBrowserClientIdentity=l,exports.createClientIdentity=c,exports.refreshClientDeviceName=a,exports.updateClientDeviceName=r;
|
|
2
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["metadataProvider","nameUpdater","bindClientMetadata","provider","updateName","updateClientDeviceName","name","timestamp","clientMetadata","refreshClientDeviceName","response","request","value","own","device","identity","DEVICE_KEY","INSTANCE_KEY","createClientIdentity","current","options","persistent","listeners","saved","_instanceId","listener","updatedAt","update","next","createBrowserClientIdentity","instanceId","sessionPersistent","inherited","text","metadata","operation","observed","changed","unsubscribe","stopStorage"],"sources":["../../../src/services/clientIdentity/index.ts"],"sourcesContent":["import type { BrowserIdentityOptions, ClientIdentity, ClientIdentityOptions, IdentityRecord } from './type';\n\nlet metadataProvider: (() => string | undefined) | undefined;\nlet nameUpdater: ((name: unknown, timestamp: number) => Promise<void>) | undefined;\n\n/** The shell binds its current encoded metadata, keeping the codec out of this package. */\nexport function bindClientMetadata(\n provider: () => string | undefined,\n updateName?: (name: unknown, timestamp: number) => Promise<void>,\n): () => void {\n metadataProvider = provider;\n nameUpdater = updateName;\n return () => {\n if (metadataProvider === provider) {\n metadataProvider = undefined;\n nameUpdater = undefined;\n }\n };\n}\n\n/** Cache a canonical name through the owning shell, without importing its identity codec. */\nexport async function updateClientDeviceName(name: unknown, timestamp: number): Promise<void> {\n if (!nameUpdater) throw new Error('Client identity name updates are unavailable.');\n await nameUpdater(name, timestamp);\n}\n\nexport function clientMetadata(): string | undefined {\n return metadataProvider?.();\n}\n\n/** Registry names are display-only. Only a valid canonical record for this device is cached. */\nexport async function refreshClientDeviceName<T extends IdentityRecord>(\n identity: ClientIdentity<T>,\n request: () => Promise<Response>,\n): Promise<void> {\n const response = await request();\n if (response.status === 404) return;\n if (!response.ok) throw new Error(`Client device refresh failed (${response.status}).`);\n const value: unknown = await response.json();\n if (typeof value !== 'object' || value === null || !('devices' in value) || !Array.isArray(value.devices)) return;\n const own: unknown = value.devices.find(\n (device: unknown) =>\n typeof device === 'object' &&\n device !== null &&\n 'deviceId' in device &&\n device.deviceId === identity.current().deviceId,\n );\n if (typeof own !== 'object' || own === null || !('deviceName' in own) || !('nameUpdatedAt' in own)) return;\n if (\n typeof own.deviceName !== 'string' ||\n typeof own.nameUpdatedAt !== 'number' ||\n !Number.isSafeInteger(own.nameUpdatedAt) ||\n own.nameUpdatedAt < 0\n )\n return;\n await identity.updateName(own.deviceName, own.nameUpdatedAt);\n}\n\nconst DEVICE_KEY = 'po.client.device.v1';\nconst INSTANCE_KEY = 'po.client.instance.v1';\n\n/** Storage failures explicitly degrade to one in-memory identity, never an auth identity. */\nexport async function createClientIdentity<T extends IdentityRecord>(\n options: ClientIdentityOptions<T>,\n): Promise<ClientIdentity<T>> {\n let current = { ...options.initial };\n let persistent = true;\n const listeners = new Set<() => void>();\n try {\n await options.exclusive(async () => {\n const saved = options.parse(await options.read());\n if (saved) current = { ...saved, ...(current.instanceId ? { instanceId: current.instanceId } : {}) };\n else {\n const { instanceId: _instanceId, ...device } = current;\n await options.write(device as T);\n }\n });\n } catch {\n persistent = false;\n }\n return {\n current: () => ({ ...current }),\n persistent: () => persistent,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async updateName(value, timestamp) {\n const name = options.normalizeName(value);\n if (!name) throw new Error('Enter a device name from 1 to 64 characters.');\n const updatedAt = timestamp ?? Math.max(options.now(), current.nameUpdatedAt + 1);\n if (!Number.isSafeInteger(updatedAt) || updatedAt < 0) throw new Error('Invalid device name timestamp.');\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const update = async (): Promise<void> => {\n const saved = options.parse(await options.read());\n if (saved && saved.deviceId === current.deviceId && saved.nameUpdatedAt > current.nameUpdatedAt) {\n current = { ...current, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n }\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const next = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n const { instanceId: _instanceId, ...device } = next;\n await options.write(device as T);\n current = next;\n };\n if (persistent) {\n try {\n await options.exclusive(update);\n } catch {\n persistent = false;\n current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n }\n } else current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n for (const listener of listeners) listener();\n },\n };\n}\n\nexport async function createBrowserClientIdentity<T extends IdentityRecord>(\n options: BrowserIdentityOptions<T>,\n): Promise<ClientIdentity<T> & { dispose(): void }> {\n let instanceId = options.randomId();\n let sessionPersistent = true;\n try {\n const inherited = options.sessionStorage.getItem(INSTANCE_KEY);\n if (options.isToken(inherited) && inherited && (await options.claimInstance(inherited))) instanceId = inherited;\n else if (!(await options.claimInstance(instanceId))) throw new Error('The tab identity could not be claimed.');\n options.sessionStorage.setItem(INSTANCE_KEY, instanceId);\n } catch {\n sessionPersistent = false;\n }\n const identity = await createClientIdentity({\n ...options,\n initial: { ...options.initial, instanceId },\n read: async () => {\n const text = options.localStorage.getItem(DEVICE_KEY);\n return text === null ? undefined : (JSON.parse(text) as unknown);\n },\n write: async (metadata) => options.localStorage.setItem(DEVICE_KEY, JSON.stringify(metadata)),\n exclusive: (operation) => options.exclusive(DEVICE_KEY, operation),\n });\n let observed = identity.current();\n const listeners = new Set<() => void>();\n const changed = (): void => {\n for (const listener of listeners) listener();\n };\n const unsubscribe = identity.subscribe(() => {\n observed = identity.current();\n changed();\n });\n const stopStorage = options.onStorage(() => {\n try {\n const text = options.localStorage.getItem(DEVICE_KEY);\n const saved = text === null ? undefined : options.parse(JSON.parse(text));\n if (\n saved &&\n saved.deviceId === observed.deviceId &&\n (saved.nameUpdatedAt > observed.nameUpdatedAt ||\n (saved.nameUpdatedAt === observed.nameUpdatedAt && saved.deviceName !== observed.deviceName))\n ) {\n observed = { ...observed, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n changed();\n }\n } catch {\n sessionPersistent = false;\n }\n });\n return {\n current: () => ({ ...observed }),\n persistent: () => sessionPersistent && identity.persistent(),\n async updateName(name, timestamp) {\n // Merge storage notifications before assigning a local timestamp.\n await identity.updateName(name, timestamp ?? Math.max(options.now(), observed.nameUpdatedAt + 1));\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n dispose() {\n stopStorage();\n unsubscribe();\n listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BrowserIdentityOptions, ClientIdentity, ClientIdentityOptions, IdentityRecord } from "./type.mjs";
|
|
2
|
+
//#region src/services/clientIdentity/index.d.ts
|
|
3
|
+
/** The shell binds its current encoded metadata, keeping the codec out of this package. */
|
|
4
|
+
export declare function bindClientMetadata(provider: () => string | undefined, updateName?: (name: unknown, timestamp: number) => Promise<void>): () => void;
|
|
5
|
+
/** Cache a canonical name through the owning shell, without importing its identity codec. */
|
|
6
|
+
export declare function updateClientDeviceName(name: unknown, timestamp: number): Promise<void>;
|
|
7
|
+
export declare function clientMetadata(): string | undefined;
|
|
8
|
+
/** Registry names are display-only. Only a valid canonical record for this device is cached. */
|
|
9
|
+
export declare function refreshClientDeviceName<T extends IdentityRecord>(identity: ClientIdentity<T>, request: () => Promise<Response>): Promise<void>;
|
|
10
|
+
/** Storage failures explicitly degrade to one in-memory identity, never an auth identity. */
|
|
11
|
+
export declare function createClientIdentity<T extends IdentityRecord>(options: ClientIdentityOptions<T>): Promise<ClientIdentity<T>>;
|
|
12
|
+
export declare function createBrowserClientIdentity<T extends IdentityRecord>(options: BrowserIdentityOptions<T>): Promise<ClientIdentity<T> & {
|
|
13
|
+
dispose(): void;
|
|
14
|
+
}>;
|
|
15
|
+
//#endregion
|
|
16
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/clientIdentity/index.ts"],"mappings":";;;wBAMgB,mBACd,oCACA,cAAc,eAAe,sBAAsB;;wBAa/B,uBAAuB,eAAe,oBAAoB;wBAKhE;;wBAKM,wBAAwB,UAAU,gBACtD,UAAU,eAAe,IACzB,eAAe,QAAQ,YACtB;;wBA4BmB,qBAAqB,UAAU,gBACnD,SAAS,sBAAsB,KAC9B,QAAQ,eAAe;wBAuDJ,4BAA4B,UAAU,gBAC1D,SAAS,uBAAuB,KAC/B,QAAQ,eAAe;EAAO"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
let e,t;function n(n,r){return e=n,t=r,()=>{e===n&&(e=void 0,t=void 0)}}async function r(e,n){if(!t)throw Error(`Client identity name updates are unavailable.`);await t(e,n)}function i(){return e?.()}async function a(e,t){let n=await t();if(n.status===404)return;if(!n.ok)throw Error(`Client device refresh failed (${n.status}).`);let r=await n.json();if(typeof r!=`object`||!r||!(`devices`in r)||!Array.isArray(r.devices))return;let i=r.devices.find(t=>typeof t==`object`&&!!t&&`deviceId`in t&&t.deviceId===e.current().deviceId);typeof i==`object`&&i&&`deviceName`in i&&`nameUpdatedAt`in i&&(typeof i.deviceName!=`string`||typeof i.nameUpdatedAt!=`number`||!Number.isSafeInteger(i.nameUpdatedAt)||i.nameUpdatedAt<0||await e.updateName(i.deviceName,i.nameUpdatedAt))}const o=`po.client.device.v1`,s=`po.client.instance.v1`;async function c(e){let t={...e.initial},n=!0,r=/* @__PURE__ */ new Set;try{await e.exclusive(async()=>{let n=e.parse(await e.read());if(n)t={...n,...t.instanceId?{instanceId:t.instanceId}:{}};else{let{instanceId:n,...r}=t;await e.write(r)}})}catch{n=!1}return{current:()=>({...t}),persistent:()=>n,subscribe(e){return r.add(e),()=>r.delete(e)},async updateName(i,a){let o=e.normalizeName(i);if(!o)throw Error(`Enter a device name from 1 to 64 characters.`);let s=a??Math.max(e.now(),t.nameUpdatedAt+1);if(!Number.isSafeInteger(s)||s<0)throw Error(`Invalid device name timestamp.`);if(s<t.nameUpdatedAt||s===t.nameUpdatedAt&&o===t.deviceName)return;let c=async()=>{let n=e.parse(await e.read());if(n&&n.deviceId===t.deviceId&&n.nameUpdatedAt>t.nameUpdatedAt&&(t={...t,deviceName:n.deviceName,nameUpdatedAt:n.nameUpdatedAt}),s<t.nameUpdatedAt||s===t.nameUpdatedAt&&o===t.deviceName)return;let r={...t,deviceName:o,nameUpdatedAt:s},{instanceId:i,...a}=r;await e.write(a),t=r};if(n)try{await e.exclusive(c)}catch{n=!1,t={...t,deviceName:o,nameUpdatedAt:s}}else t={...t,deviceName:o,nameUpdatedAt:s};for(let e of r)e()}}}async function l(e){let t=e.randomId(),n=!0;try{let n=e.sessionStorage.getItem(s);if(e.isToken(n)&&n&&await e.claimInstance(n))t=n;else if(!await e.claimInstance(t))throw Error(`The tab identity could not be claimed.`);e.sessionStorage.setItem(s,t)}catch{n=!1}let r=await c({...e,initial:{...e.initial,instanceId:t},read:async()=>{let t=e.localStorage.getItem(o);return t===null?void 0:JSON.parse(t)},write:async t=>e.localStorage.setItem(o,JSON.stringify(t)),exclusive:t=>e.exclusive(o,t)}),i=r.current(),a=/* @__PURE__ */ new Set,l=()=>{for(let e of a)e()},u=r.subscribe(()=>{i=r.current(),l()}),d=e.onStorage(()=>{try{let t=e.localStorage.getItem(o),n=t===null?void 0:e.parse(JSON.parse(t));n&&n.deviceId===i.deviceId&&(n.nameUpdatedAt>i.nameUpdatedAt||n.nameUpdatedAt===i.nameUpdatedAt&&n.deviceName!==i.deviceName)&&(i={...i,deviceName:n.deviceName,nameUpdatedAt:n.nameUpdatedAt},l())}catch{n=!1}});return{current:()=>({...i}),persistent:()=>n&&r.persistent(),async updateName(t,n){await r.updateName(t,n??Math.max(e.now(),i.nameUpdatedAt+1))},subscribe(e){return a.add(e),()=>a.delete(e)},dispose(){d(),u(),a.clear()}}}export{n as bindClientMetadata,i as clientMetadata,l as createBrowserClientIdentity,c as createClientIdentity,a as refreshClientDeviceName,r as updateClientDeviceName};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["metadataProvider","nameUpdater","bindClientMetadata","provider","updateName","updateClientDeviceName","name","timestamp","clientMetadata","refreshClientDeviceName","response","request","value","own","device","identity","DEVICE_KEY","INSTANCE_KEY","createClientIdentity","current","options","persistent","listeners","saved","_instanceId","listener","updatedAt","update","next","createBrowserClientIdentity","instanceId","sessionPersistent","inherited","text","metadata","operation","observed","changed","unsubscribe","stopStorage"],"sources":["../../../src/services/clientIdentity/index.ts"],"sourcesContent":["import type { BrowserIdentityOptions, ClientIdentity, ClientIdentityOptions, IdentityRecord } from './type';\n\nlet metadataProvider: (() => string | undefined) | undefined;\nlet nameUpdater: ((name: unknown, timestamp: number) => Promise<void>) | undefined;\n\n/** The shell binds its current encoded metadata, keeping the codec out of this package. */\nexport function bindClientMetadata(\n provider: () => string | undefined,\n updateName?: (name: unknown, timestamp: number) => Promise<void>,\n): () => void {\n metadataProvider = provider;\n nameUpdater = updateName;\n return () => {\n if (metadataProvider === provider) {\n metadataProvider = undefined;\n nameUpdater = undefined;\n }\n };\n}\n\n/** Cache a canonical name through the owning shell, without importing its identity codec. */\nexport async function updateClientDeviceName(name: unknown, timestamp: number): Promise<void> {\n if (!nameUpdater) throw new Error('Client identity name updates are unavailable.');\n await nameUpdater(name, timestamp);\n}\n\nexport function clientMetadata(): string | undefined {\n return metadataProvider?.();\n}\n\n/** Registry names are display-only. Only a valid canonical record for this device is cached. */\nexport async function refreshClientDeviceName<T extends IdentityRecord>(\n identity: ClientIdentity<T>,\n request: () => Promise<Response>,\n): Promise<void> {\n const response = await request();\n if (response.status === 404) return;\n if (!response.ok) throw new Error(`Client device refresh failed (${response.status}).`);\n const value: unknown = await response.json();\n if (typeof value !== 'object' || value === null || !('devices' in value) || !Array.isArray(value.devices)) return;\n const own: unknown = value.devices.find(\n (device: unknown) =>\n typeof device === 'object' &&\n device !== null &&\n 'deviceId' in device &&\n device.deviceId === identity.current().deviceId,\n );\n if (typeof own !== 'object' || own === null || !('deviceName' in own) || !('nameUpdatedAt' in own)) return;\n if (\n typeof own.deviceName !== 'string' ||\n typeof own.nameUpdatedAt !== 'number' ||\n !Number.isSafeInteger(own.nameUpdatedAt) ||\n own.nameUpdatedAt < 0\n )\n return;\n await identity.updateName(own.deviceName, own.nameUpdatedAt);\n}\n\nconst DEVICE_KEY = 'po.client.device.v1';\nconst INSTANCE_KEY = 'po.client.instance.v1';\n\n/** Storage failures explicitly degrade to one in-memory identity, never an auth identity. */\nexport async function createClientIdentity<T extends IdentityRecord>(\n options: ClientIdentityOptions<T>,\n): Promise<ClientIdentity<T>> {\n let current = { ...options.initial };\n let persistent = true;\n const listeners = new Set<() => void>();\n try {\n await options.exclusive(async () => {\n const saved = options.parse(await options.read());\n if (saved) current = { ...saved, ...(current.instanceId ? { instanceId: current.instanceId } : {}) };\n else {\n const { instanceId: _instanceId, ...device } = current;\n await options.write(device as T);\n }\n });\n } catch {\n persistent = false;\n }\n return {\n current: () => ({ ...current }),\n persistent: () => persistent,\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n async updateName(value, timestamp) {\n const name = options.normalizeName(value);\n if (!name) throw new Error('Enter a device name from 1 to 64 characters.');\n const updatedAt = timestamp ?? Math.max(options.now(), current.nameUpdatedAt + 1);\n if (!Number.isSafeInteger(updatedAt) || updatedAt < 0) throw new Error('Invalid device name timestamp.');\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const update = async (): Promise<void> => {\n const saved = options.parse(await options.read());\n if (saved && saved.deviceId === current.deviceId && saved.nameUpdatedAt > current.nameUpdatedAt) {\n current = { ...current, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n }\n if (updatedAt < current.nameUpdatedAt || (updatedAt === current.nameUpdatedAt && name === current.deviceName))\n return;\n const next = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n const { instanceId: _instanceId, ...device } = next;\n await options.write(device as T);\n current = next;\n };\n if (persistent) {\n try {\n await options.exclusive(update);\n } catch {\n persistent = false;\n current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n }\n } else current = { ...current, deviceName: name, nameUpdatedAt: updatedAt };\n for (const listener of listeners) listener();\n },\n };\n}\n\nexport async function createBrowserClientIdentity<T extends IdentityRecord>(\n options: BrowserIdentityOptions<T>,\n): Promise<ClientIdentity<T> & { dispose(): void }> {\n let instanceId = options.randomId();\n let sessionPersistent = true;\n try {\n const inherited = options.sessionStorage.getItem(INSTANCE_KEY);\n if (options.isToken(inherited) && inherited && (await options.claimInstance(inherited))) instanceId = inherited;\n else if (!(await options.claimInstance(instanceId))) throw new Error('The tab identity could not be claimed.');\n options.sessionStorage.setItem(INSTANCE_KEY, instanceId);\n } catch {\n sessionPersistent = false;\n }\n const identity = await createClientIdentity({\n ...options,\n initial: { ...options.initial, instanceId },\n read: async () => {\n const text = options.localStorage.getItem(DEVICE_KEY);\n return text === null ? undefined : (JSON.parse(text) as unknown);\n },\n write: async (metadata) => options.localStorage.setItem(DEVICE_KEY, JSON.stringify(metadata)),\n exclusive: (operation) => options.exclusive(DEVICE_KEY, operation),\n });\n let observed = identity.current();\n const listeners = new Set<() => void>();\n const changed = (): void => {\n for (const listener of listeners) listener();\n };\n const unsubscribe = identity.subscribe(() => {\n observed = identity.current();\n changed();\n });\n const stopStorage = options.onStorage(() => {\n try {\n const text = options.localStorage.getItem(DEVICE_KEY);\n const saved = text === null ? undefined : options.parse(JSON.parse(text));\n if (\n saved &&\n saved.deviceId === observed.deviceId &&\n (saved.nameUpdatedAt > observed.nameUpdatedAt ||\n (saved.nameUpdatedAt === observed.nameUpdatedAt && saved.deviceName !== observed.deviceName))\n ) {\n observed = { ...observed, deviceName: saved.deviceName, nameUpdatedAt: saved.nameUpdatedAt };\n changed();\n }\n } catch {\n sessionPersistent = false;\n }\n });\n return {\n current: () => ({ ...observed }),\n persistent: () => sessionPersistent && identity.persistent(),\n async updateName(name, timestamp) {\n // Merge storage notifications before assigning a local timestamp.\n await identity.updateName(name, timestamp ?? Math.max(options.now(), observed.nameUpdatedAt + 1));\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n dispose() {\n stopStorage();\n unsubscribe();\n listeners.clear();\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
//#region src/services/clientIdentity/type.d.ts
|
|
2
|
+
export interface IdentityRecord {
|
|
3
|
+
deviceId: string;
|
|
4
|
+
instanceId?: string;
|
|
5
|
+
deviceName: string;
|
|
6
|
+
nameUpdatedAt: number;
|
|
7
|
+
}
|
|
8
|
+
export interface ClientIdentity<T extends IdentityRecord> {
|
|
9
|
+
current(): T;
|
|
10
|
+
persistent(): boolean;
|
|
11
|
+
updateName(name: unknown, nameUpdatedAt?: number): Promise<void>;
|
|
12
|
+
subscribe(listener: () => void): () => void;
|
|
13
|
+
}
|
|
14
|
+
export interface ClientIdentityOptions<T extends IdentityRecord> {
|
|
15
|
+
initial: T;
|
|
16
|
+
read(): Promise<unknown>;
|
|
17
|
+
write(metadata: T): Promise<void>;
|
|
18
|
+
parse(value: unknown): T | undefined;
|
|
19
|
+
normalizeName(value: unknown): string | undefined;
|
|
20
|
+
now(): number;
|
|
21
|
+
/** Serializes initialization and read/merge/write across all storage owners. */
|
|
22
|
+
exclusive<R>(operation: () => Promise<R>): Promise<R>;
|
|
23
|
+
}
|
|
24
|
+
export interface BrowserIdentityOptions<T extends IdentityRecord> extends Omit<ClientIdentityOptions<T>, 'read' | 'write' | 'exclusive'> {
|
|
25
|
+
localStorage: {
|
|
26
|
+
getItem(key: string): string | null;
|
|
27
|
+
setItem(key: string, value: string): void;
|
|
28
|
+
};
|
|
29
|
+
sessionStorage: {
|
|
30
|
+
getItem(key: string): string | null;
|
|
31
|
+
setItem(key: string, value: string): void;
|
|
32
|
+
};
|
|
33
|
+
randomId(): string;
|
|
34
|
+
isToken(value: unknown): boolean;
|
|
35
|
+
exclusive<R>(key: string, operation: () => Promise<R>): Promise<R>;
|
|
36
|
+
/** Holds an exclusive instance claim until the page exits. False means a cloned tab owns it. */
|
|
37
|
+
claimInstance(id: string): Promise<boolean>;
|
|
38
|
+
onStorage(listener: () => void): () => void;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//# sourceMappingURL=type.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type.d.mts","names":[],"sources":["../../../src/services/clientIdentity/type.ts"],"mappings":";iBAAiB;EACf;EACA;EACA;EACA;;iBAGe,eAAe,UAAU;EACxC,WAAW;EACX;EACA,WAAW,eAAe,yBAAyB;EACnD,UAAU;;iBAGK,sBAAsB,UAAU;EAC/C,SAAS;EACT,QAAQ;EACR,MAAM,UAAU,IAAI;EACpB,MAAM,iBAAiB;EACvB,cAAc;EACd;;EAEA,UAAU,GAAG,iBAAiB,QAAQ,KAAK,QAAQ;;iBAGpC,uBAAuB,UAAU,wBAAwB,KACxE,sBAAsB;EAGtB;IAAgB,QAAQ;IAA6B,QAAQ,aAAa;;EAC1E;IAAkB,QAAQ;IAA6B,QAAQ,aAAa;;EAC5E;EACA,QAAQ;EACR,UAAU,GAAG,aAAa,iBAAiB,QAAQ,KAAK,QAAQ;;EAEhE,cAAc,aAAa;EAC3B,UAAU"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
const e=require("../../types/sealedChannel.cjs"),t=require("../browserSealedChannel/index.cjs"),n=require("../serialQueue/index.cjs"),r=new
|
|
1
|
+
const e=require("../../types/sealedChannel.cjs"),t=require("../browserSealedChannel/index.cjs"),n=require("../clientIdentity/index.cjs"),r=require("../serialQueue/index.cjs");function i(e,t){if(/[\\\\\u0000-\u0020]/u.test(e)||e.startsWith(`//`))return t;let r=typeof location>`u`?void 0:location,i=r?.origin??`https://po.invalid`;if(!r&&!e.startsWith(`/`))return t;let a=new URL(e,i);if(a.origin!==i||a.username||a.password||!/^\/(?:api(?:\/|$)|hosts\/[^/]+\/api(?:\/|$))/u.test(a.pathname)||/\/api\/web-plugins(?:\/|$)/u.test(a.pathname)||a.pathname.endsWith(`/api/remote/request`))return t;let o=n.clientMetadata();if(!o)return t;let s=new Headers(t?.headers);return s.set(`x-po-client-metadata`,o),{...t,headers:s,redirect:`error`}}const a=new TextEncoder,o=new TextDecoder;function s(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t)}function c(e){let t=atob(e);return Uint8Array.from(t,e=>e.charCodeAt(0))}async function l(e){if(e!=null){if(typeof e==`string`)return a.encode(e);if(e instanceof URLSearchParams)return a.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 u(e){if(!e.startsWith(`/`)||e.startsWith(`//`)||e.includes(`\\`))throw Error(`A sealed HTTP request must use a root-relative URL.`);return e}function d(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 f(){let n,f,p,m=!1,h=r.createSerialQueue(),g=r.createSerialQueue(),_=r.createSerialQueue(),v,y=t=>{f=e.describeSealedFailure(t)},b=async e=>{if(n===void 0)return;let t=await n.seal(e);return t.ok?JSON.stringify(t.envelope):y(t.failure)},x=async e=>{if(n===void 0)return;let t;try{t=JSON.parse(e)}catch{return y(`malformed`)}let r=await n.open(t);return r.ok?r.plaintext:y(r.failure)};return{active:()=>n!==void 0,invalidate(){m=!0},onAuthorizationFailure(e){p=e},lastFailure:()=>f,async connect(e){let r=await t.connectSealedChannel(e);if(r===void 0)f=`This browser could not establish a sealed channel.`;else return n=r.channel,m=!1,f=void 0,r.clientPublicKey},relayRequestsThrough(e){v=e},reset(){m=!1,n=void 0,f=void 0},async sealText(e){if(m)throw Error(`Sign in to Po again.`);return n===void 0?e:await h.run(async()=>{let t=await b(a.encode(e));if(t===void 0)throw Error(f??`The active sealed channel could not encrypt a message.`);return t})},async openText(e){if(m)throw Error(`Sign in to Po again.`);return n===void 0?e:await g.run(async()=>{let t=await x(e);return t===void 0?void 0:o.decode(t)})},async sealBinary(e){if(m)throw Error(`Sign in to Po again.`);return n===void 0?e:await h.run(async()=>{let t=await b(e);if(t===void 0)throw Error(f??`The active sealed channel could not encrypt a message.`);return a.encode(t)})},async openBinary(e){if(m)throw Error(`Sign in to Po again.`);return n===void 0?e:await g.run(async()=>await x(o.decode(e)))},async fetch(e,t){if(t?.signal?.throwIfAborted(),m)throw Error(`Sign in to Po again.`);let r=v;if(n===void 0||r===void 0)return await fetch(e,i(e,t));let h=new AbortController,g=()=>h.abort(t?.signal?.reason),y=setTimeout(()=>h.abort(new DOMException(`The request timed out.`,`TimeoutError`)),3e4);t?.signal?.addEventListener(`abort`,g,{once:!0}),t?.signal?.aborted&&g();let S,C=new Promise((e,t)=>{S=()=>t(h.signal.reason),h.signal.addEventListener(`abort`,S,{once:!0}),h.signal.aborted&&S()}),w=n,T=()=>{if(h.signal.throwIfAborted(),m||n!==w)throw Error(`The sealed channel was replaced.`)};try{return T(),await Promise.race([_.run(()=>Promise.race([(async()=>{T();let n=await l(t?.body);T();let g={v:1,method:(t?.method??`GET`).toUpperCase(),target:u(e),headers:Array.from(new Headers(i(e,t)?.headers).entries()),...n===void 0?{}:{body:s(n)}},_=await b(a.encode(JSON.stringify(g)));if(T(),_===void 0)return new Response(JSON.stringify({error:f}),{status:502});let v=await fetch(r,{method:`POST`,signal:h.signal,credentials:`same-origin`,headers:{"content-type":`application/json`},body:_});if(T(),v.status===401||v.status===403)throw m=!0,f=`Sign in to Po again.`,p?.(),Error(f);let y=await v.text();T();let S=await x(y);if(T(),S===void 0)return new Response(JSON.stringify({error:f}),{status:502});let C;try{C=JSON.parse(o.decode(S))}catch{C=void 0}if(!d(C))return f=`The sealed HTTP response was malformed.`,new Response(JSON.stringify({error:f}),{status:502});let w=c(C.body);return new Response(C.status===204||C.status===205||C.status===304?null:w,{status:C.status,headers:C.headers})})(),C])),C])}finally{clearTimeout(y),t?.signal?.removeEventListener(`abort`,g),S&&h.signal.removeEventListener(`abort`,S)}}}}const p=/* @__PURE__ */ new Map;function m(e,t=``){return p.set(t,e),()=>{p.get(t)===e&&p.delete(t)}}function h(e){return{active:()=>p.get(e)?.active()??!1,async fetch(t,n){n?.signal?.throwIfAborted();let r=p.get(e);if(!r)throw Error(`The host transport is not connected.`);let i=g(t,e);return r.fetch(i.input,n)}}}function g(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 _={active:()=>p.get(``)?.active()??!1,async fetch(e,t){t?.signal?.throwIfAborted();let n=g(e);if(n.hostId!==void 0)return h(n.hostId).fetch(n.input,t);let r=p.get(``);return r?r.fetch(e,t):fetch(e,i(e,t))}};exports.bindBrowserTransport=m,exports.browserTransport=_,exports.browserTransportForHost=h,exports.createSealedTransport=f,exports.poRequestInit=i;
|
|
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","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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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["clientMetadata","createSerialQueue","describeSealedFailure","connectSealedChannel","require_sealedChannel","require_index","require_index$1","require_index$2","poRequestInit","input","init","pageLocation","origin","url","metadata","headers","encoder","decoder","encodeBase64","binary","byte","bytes","decodeBase64","value","character","bodyBytes","body","requestTarget","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","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 { clientMetadata } from '../clientIdentity';\nimport { createSerialQueue } from '../serialQueue';\n\n// This package also typechecks without DOM libraries for its native consumers.\ndeclare const location: { origin: string };\n\n/** Only same-origin Po API calls are eligible. Assets and arbitrary fetch targets stay untouched. */\nexport function poRequestInit(input: string, init?: RequestInit): RequestInit | undefined {\n // oxlint-disable-next-line no-control-regex -- Reject URL normalization ambiguities at admission.\n if (/[\\\\\\\\\\u0000-\\u0020]/u.test(input) || input.startsWith('//')) return init;\n const pageLocation = typeof location === 'undefined' ? undefined : location;\n const origin = pageLocation?.origin ?? 'https://po.invalid';\n if (!pageLocation && !input.startsWith('/')) return init;\n const url = new URL(input, origin);\n if (url.origin !== origin || url.username || url.password) return init;\n if (!/^\\/(?:api(?:\\/|$)|hosts\\/[^/]+\\/api(?:\\/|$))/u.test(url.pathname)) return init;\n if (/\\/api\\/web-plugins(?:\\/|$)/u.test(url.pathname) || url.pathname.endsWith('/api/remote/request')) return init;\n const metadata = clientMetadata();\n if (!metadata) return init;\n const headers = new Headers(init?.headers);\n headers.set('x-po-client-metadata', metadata);\n return { ...init, headers, redirect: 'error' };\n}\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('/') || input.startsWith('//') || input.includes('\\\\'))\n 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, poRequestInit(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(poRequestInit(input, 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, poRequestInit(input, init));\n },\n};\n"],"mappings":"+KASA,SAAgBQ,EAAc,EAAe,EAA6C,CAExF,GAAI,uBAAuB,KAAKC,CAAK,GAAKA,EAAM,WAAW,IAAI,EAAG,OAAOC,EACzE,IAAMC,EAAe,OAAO,SAAa,IAAc,IAAA,GAAY,SAC7DC,EAASD,GAAc,QAAU,qBACvC,GAAI,CAACA,GAAgB,CAACF,EAAM,WAAW,GAAG,EAAG,OAAOC,EACpD,IAAMG,EAAM,IAAI,IAAIJ,EAAOG,CAAM,EAGjC,GAFIC,EAAI,SAAWD,GAAUC,EAAI,UAAYA,EAAI,UAC7C,CAAC,gDAAgD,KAAKA,EAAI,QAAQ,GAClE,8BAA8B,KAAKA,EAAI,QAAQ,GAAKA,EAAI,SAAS,SAAS,qBAAqB,EAAG,OAAOH,EAC7G,IAAMI,EAAWd,EAAAA,eAAe,EAChC,GAAI,CAACc,EAAU,OAAOJ,EACtB,IAAMK,EAAU,IAAI,QAAQL,GAAM,OAAO,EAEzC,OADAK,EAAQ,IAAI,uBAAwBD,CAAQ,EACrC,CAAE,GAAGJ,EAAM,UAAS,SAAU,OAAQ,CAC/C,CAiBA,MAgBMM,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,CAAClB,EAAM,WAAW,GAAG,GAAKA,EAAM,WAAW,IAAI,GAAKA,EAAM,SAAS,IAAI,EACzE,MAAU,MAAM,qDAAqD,EACvE,OAAOA,CACT,CAEA,SAASmB,EAAqB,EAA6C,CACzE,GAAI,OAAOL,GAAU,WAAYA,EAAgB,MAAO,GACxD,IAAMM,EAAYN,EAClB,OACEM,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,EAAWpC,EAAAA,kBAAkB,EAC7BqC,EAAUrC,EAAAA,kBAAkB,EAC5BsC,EAAWtC,EAAAA,kBAAkB,EAC/BuC,EACEC,EAAQ,GAAqC,CAGjD,EAAUvC,EAAAA,sBAAsBwC,CAAM,CAExC,EAEMC,EAAY,KAAO,IAAmD,CAC1E,GAAIV,IAAY,IAAA,GAAW,OAC3B,IAAMW,EAAS,MAAMX,EAAQ,KAAKZ,CAAK,EAEvC,OADKuB,EAAO,GACL,KAAK,UAAUA,EAAO,QAAQ,EADdH,EAAKG,EAAO,OAAO,CAE5C,EAEMC,EAAY,KAAO,IAAkD,CACzE,GAAIZ,IAAY,IAAA,GAAW,OAC3B,IAAIa,EACJ,GAAI,CACF,EAAW,KAAK,MAAMC,CAAI,CAC5B,MAAQ,CACN,OAAON,EAAK,WAAW,CACzB,CACA,IAAMO,EAAS,MAAMf,EAAQ,KAAKa,CAAQ,EAE1C,OADKE,EAAO,GACLA,EAAO,UADSP,EAAKO,EAAO,OAAO,CAE5C,EAEA,MAAO,CACL,WAAcf,IAAY,IAAA,GAC1B,YAAa,CACX,EAAe,EACjB,EACA,uBAAuB,EAAU,CAC/B,EAAuBgB,CACzB,EACA,gBAAmBf,EAEnB,MAAM,QAAQ,EAAe,CAC3B,IAAMgB,EAAY,MAAM/C,EAAAA,qBAAqBgD,CAAa,EAC1D,GAAID,IAAc,IAAA,GAChB,EAAU,0DAMZ,MAHA,GAAUA,EAAU,QACpB,EAAe,GACf,EAAU,IAAA,GACHA,EAAU,eACnB,EAEA,qBAAqB,EAAM,CACzB,EAAYE,CACd,EAEA,OAAQ,CACN,EAAe,GACf,EAAU,IAAA,GACV,EAAU,IAAA,EACZ,EAEA,MAAM,SAAS,EAAM,CACnB,GAAIhB,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBc,EAC3B,MAAMV,EAAS,IAAI,SAAY,CACpC,IAAMO,EAAS,MAAMD,EAAU3B,EAAQ,OAAO+B,CAAI,CAAC,EACnD,GAAIH,IAAW,IAAA,GAAW,MAAU,MAAMV,GAAW,wDAAwD,EAC7G,OAAOU,CACT,CAAC,CACH,EAEA,MAAM,SAAS,EAAM,CACnB,GAAIR,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBc,EAC3B,MAAMT,EAAQ,IAAI,SAAY,CACnC,IAAMU,EAAS,MAAMH,EAAUE,CAAI,EACnC,OAAOC,IAAW,IAAA,GAAY,IAAA,GAAY/B,EAAQ,OAAO+B,CAAM,CACjE,CAAC,CACH,EAEA,MAAM,WAAW,EAAO,CACtB,GAAIZ,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBZ,EAC3B,MAAMgB,EAAS,IAAI,SAAY,CACpC,IAAMO,EAAS,MAAMD,EAAUtB,CAAK,EACpC,GAAIuB,IAAW,IAAA,GAAW,MAAU,MAAMV,GAAW,wDAAwD,EAC7G,OAAOlB,EAAQ,OAAO4B,CAAM,CAC9B,CAAC,CACH,EAEA,MAAM,WAAW,EAAO,CACtB,GAAIR,EAAc,MAAU,MAAM,sBAAsB,EAExD,OADIH,IAAY,IAAA,GAAkBZ,EAC3B,MAAMiB,EAAQ,IAAI,SAAY,MAAMO,EAAU5B,EAAQ,OAAOI,CAAK,CAAC,CAAC,CAC7E,EAEA,MAAM,MAAM,EAAO,EAAM,CAEvB,GADAX,GAAM,QAAQ,eAAe,EACzB0B,EAAc,MAAU,MAAM,sBAAsB,EACxD,IAAMiB,EAAUb,EAChB,GAAIP,IAAY,IAAA,IAAaoB,IAAY,IAAA,GAAW,OAAO,MAAM,MAAM5C,EAAOD,EAAcC,EAAOC,CAAI,CAAC,EACxG,IAAM4C,EAAa,IAAI,gBACjBC,MAAeD,EAAW,MAAM5C,GAAM,QAAQ,MAAM,EACpD8C,EAAW,eACTF,EAAW,MAAM,IAAI,aAAa,yBAA0B,cAAc,CAAC,EACjF,GACF,EACA5C,GAAM,QAAQ,iBAAiB,QAAS6C,EAAQ,CAAE,KAAM,EAAK,CAAC,EAC1D7C,GAAM,QAAQ,SAAS6C,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,EAAkB3B,EAClB4B,MAAc,CAElB,GADAP,EAAW,OAAO,eAAe,EAC7BlB,GAAgBH,IAAY2B,EAAiB,MAAU,MAAM,kCAAkC,CACrG,EACA,GAAI,CAEF,OADAC,EAAM,EACC,MAAM,QAAQ,KAAK,CACxBtB,EAAS,QACP,QAAQ,KAAK,EACV,SAAY,CACXsB,EAAM,EACN,IAAMxC,EAAQ,MAAMI,EAAUf,GAAM,IAAI,EACxCmD,EAAM,EACN,IAAMC,EAA6B,CACjC,EAAG,EACH,QAASpD,GAAM,QAAU,MAAA,CAAO,YAAY,EAC5C,OAAQiB,EAAclB,CAAK,EAC3B,QAAS,MAAM,KAAK,IAAI,QAAQD,EAAcC,EAAOC,CAAI,CAAC,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,EAC9E,GAAIW,IAAU,IAAA,GAAY,CAAC,EAAI,CAAE,KAAMH,EAAaG,CAAK,CAAE,CAC7D,EACMuB,EAAS,MAAMD,EAAU3B,EAAQ,OAAO,KAAK,UAAU8C,CAAO,CAAC,CAAC,EAEtE,GADAD,EAAM,EACFjB,IAAW,IAAA,GACb,OAAO,IAAI,SAAS,KAAK,UAAU,CAAE,MAAOV,CAAQ,CAAC,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEzE,IAAM6B,EAAgB,MAAM,MAAMV,EAAS,CACzC,OAAQ,OACR,OAAQC,EAAW,OACnB,YAAa,cACb,QAAS,CAAE,eAAgB,kBAAkB,EAC7C,KAAMV,CACR,CAAC,EAGD,GAFAiB,EAAM,EAEFE,EAAc,SAAW,KAAOA,EAAc,SAAW,IAI3D,KAHA,GAAe,GACf,EAAU,uBACV5B,IAAuB,EACb,MAAMD,CAAO,EAEzB,IAAM8B,EAAe,MAAMD,EAAc,KAAK,EAC9CF,EAAM,EACN,IAAMb,EAAS,MAAMH,EAAUmB,CAAY,EAE3C,GADAH,EAAM,EACFb,IAAW,IAAA,GACb,OAAO,IAAI,SAAS,KAAK,UAAU,CAAE,MAAOd,CAAQ,CAAC,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEzE,IAAI+B,EACJ,GAAI,CACF,EAAU,KAAK,MAAMhD,EAAQ,OAAO+B,CAAM,CAAC,CAC7C,MAAQ,CACN,EAAU,IAAA,EACZ,CACA,GAAI,CAACpB,EAAqBqC,CAAO,EAE/B,MADA,GAAU,0CACH,IAAI,SAAS,KAAK,UAAU,CAAE,MAAO/B,CAAQ,CAAC,EAAG,CAAE,OAAQ,GAAI,CAAC,EAEzE,IAAMR,EAAOJ,EAAa2C,EAAQ,IAAI,EACtC,OAAO,IAAI,SACTA,EAAQ,SAAW,KAAOA,EAAQ,SAAW,KAAOA,EAAQ,SAAW,IAAM,KAAOvC,EACpF,CACE,OAAQuC,EAAQ,OAChB,QAASA,EAAQ,OACnB,CACF,CACF,EAAA,CAAG,EACHP,CACF,CAAC,CACH,EACAA,CACF,CAAC,CACH,QAAU,CACR,aAAaF,CAAQ,EACrB9C,GAAM,QAAQ,oBAAoB,QAAS6C,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,CACvB1D,GAAM,QAAQ,eAAe,EAC7B,IAAM6D,EAAUL,EAAgB,IAAIE,CAAM,EAC1C,GAAI,CAACG,EAAS,MAAU,MAAM,sCAAsC,EACpE,IAAMC,EAAQC,EAAoBhE,EAAO2D,CAAM,EAC/C,OAAOG,EAAQ,MAAMC,EAAM,MAAO9D,CAAI,CACxC,CACF,CACF,CAGA,SAAS+D,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,IAAMhD,EAAY+C,EAAG,MAAM,EAAGE,CAAK,EAEnC,GAAI,CAAC,mEAAmE,KAAKjD,CAAS,EAAG,OAAOgD,EAChG,GAAIT,IAAW,IAAA,IAAavC,IAAcuC,EAAQ,MAAU,MAAM,oCAAoC,EAEtG,MADA,GAASvC,EACF,mBAAmB+C,EAAG,MAAME,EAAQ,CAAC,CAAC,CAC/C,EACMC,EAAUtE,EAAM,QAAQ,GAAG,EAE3BuE,GADOD,IAAY,GAAKtE,EAAQA,EAAM,MAAM,EAAGsE,CAAO,EAAA,CACxC,QAClB,0CACC,EAAQ,EAAgB,IAAeE,EAASN,EAAMC,CAAE,CAC3D,EACMM,EACJH,IAAY,GACR,GACAtE,EACG,MAAMsE,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,CACvBxD,GAAM,QAAQ,eAAe,EAC7B,IAAM8D,EAAQC,EAAoBhE,CAAK,EACvC,GAAI+D,EAAM,SAAW,IAAA,GAAW,OAAOF,EAAwBE,EAAM,MAAM,CAAC,CAAC,MAAMA,EAAM,MAAO9D,CAAI,EACpG,IAAM6D,EAAUL,EAAgB,IAAI,EAAY,EAChD,OAAOK,EAAUA,EAAQ,MAAM9D,EAAOC,CAAI,EAAI,MAAMD,EAAOD,EAAcC,EAAOC,CAAI,CAAC,CACvF,CACF"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
//#region src/services/sealedTransport/index.d.ts
|
|
2
|
+
/** Only same-origin Po API calls are eligible. Assets and arbitrary fetch targets stay untouched. */
|
|
3
|
+
export declare function poRequestInit(input: string, init?: RequestInit): RequestInit | undefined;
|
|
2
4
|
export interface SealedTransport {
|
|
3
5
|
/** False on loopback and before the handshake completes, where everything passes through. */
|
|
4
6
|
active(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/sealedTransport/index.ts"],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../../src/services/sealedTransport/index.ts"],"mappings":";;wBASgB,cAAc,eAAe,OAAO,cAAc;iBAmCjD;;EAEf;EACA,SAAS,eAAe;;EAExB,SAAS,eAAe;EACxB,WAAW,OAAO,aAAa,QAAQ;EACvC,WAAW,OAAO,aAAa,QAAQ;;EAEvC,MAAM,eAAe,OAAO,cAAc,QAAQ;EAClD;;wBA6Dc,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{
|
|
1
|
+
import{describeSealedFailure as e}from"../../types/sealedChannel.mjs";import{connectSealedChannel as t}from"../browserSealedChannel/index.mjs";import{clientMetadata as n}from"../clientIdentity/index.mjs";import{createSerialQueue as r}from"../serialQueue/index.mjs";function i(e,t){if(/[\\\\\u0000-\u0020]/u.test(e)||e.startsWith(`//`))return t;let r=typeof location>`u`?void 0:location,i=r?.origin??`https://po.invalid`;if(!r&&!e.startsWith(`/`))return t;let a=new URL(e,i);if(a.origin!==i||a.username||a.password||!/^\/(?:api(?:\/|$)|hosts\/[^/]+\/api(?:\/|$))/u.test(a.pathname)||/\/api\/web-plugins(?:\/|$)/u.test(a.pathname)||a.pathname.endsWith(`/api/remote/request`))return t;let o=n();if(!o)return t;let s=new Headers(t?.headers);return s.set(`x-po-client-metadata`,o),{...t,headers:s,redirect:`error`}}const a=new TextEncoder,o=new TextDecoder;function s(e){let t=``;for(let n of e)t+=String.fromCharCode(n);return btoa(t)}function c(e){let t=atob(e);return Uint8Array.from(t,e=>e.charCodeAt(0))}async function l(e){if(e!=null){if(typeof e==`string`)return a.encode(e);if(e instanceof URLSearchParams)return a.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 u(e){if(!e.startsWith(`/`)||e.startsWith(`//`)||e.includes(`\\`))throw Error(`A sealed HTTP request must use a root-relative URL.`);return e}function d(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 f(){let n,f,p,m=!1,h=r(),g=r(),_=r(),v,y=t=>{f=e(t)},b=async e=>{if(n===void 0)return;let t=await n.seal(e);return t.ok?JSON.stringify(t.envelope):y(t.failure)},x=async e=>{if(n===void 0)return;let t;try{t=JSON.parse(e)}catch{return y(`malformed`)}let r=await n.open(t);return r.ok?r.plaintext:y(r.failure)};return{active:()=>n!==void 0,invalidate(){m=!0},onAuthorizationFailure(e){p=e},lastFailure:()=>f,async connect(e){let r=await t(e);if(r===void 0)f=`This browser could not establish a sealed channel.`;else return n=r.channel,m=!1,f=void 0,r.clientPublicKey},relayRequestsThrough(e){v=e},reset(){m=!1,n=void 0,f=void 0},async sealText(e){if(m)throw Error(`Sign in to Po again.`);return n===void 0?e:await h.run(async()=>{let t=await b(a.encode(e));if(t===void 0)throw Error(f??`The active sealed channel could not encrypt a message.`);return t})},async openText(e){if(m)throw Error(`Sign in to Po again.`);return n===void 0?e:await g.run(async()=>{let t=await x(e);return t===void 0?void 0:o.decode(t)})},async sealBinary(e){if(m)throw Error(`Sign in to Po again.`);return n===void 0?e:await h.run(async()=>{let t=await b(e);if(t===void 0)throw Error(f??`The active sealed channel could not encrypt a message.`);return a.encode(t)})},async openBinary(e){if(m)throw Error(`Sign in to Po again.`);return n===void 0?e:await g.run(async()=>await x(o.decode(e)))},async fetch(e,t){if(t?.signal?.throwIfAborted(),m)throw Error(`Sign in to Po again.`);let r=v;if(n===void 0||r===void 0)return await fetch(e,i(e,t));let h=new AbortController,g=()=>h.abort(t?.signal?.reason),y=setTimeout(()=>h.abort(new DOMException(`The request timed out.`,`TimeoutError`)),3e4);t?.signal?.addEventListener(`abort`,g,{once:!0}),t?.signal?.aborted&&g();let S,C=new Promise((e,t)=>{S=()=>t(h.signal.reason),h.signal.addEventListener(`abort`,S,{once:!0}),h.signal.aborted&&S()}),w=n,T=()=>{if(h.signal.throwIfAborted(),m||n!==w)throw Error(`The sealed channel was replaced.`)};try{return T(),await Promise.race([_.run(()=>Promise.race([(async()=>{T();let n=await l(t?.body);T();let g={v:1,method:(t?.method??`GET`).toUpperCase(),target:u(e),headers:Array.from(new Headers(i(e,t)?.headers).entries()),...n===void 0?{}:{body:s(n)}},_=await b(a.encode(JSON.stringify(g)));if(T(),_===void 0)return new Response(JSON.stringify({error:f}),{status:502});let v=await fetch(r,{method:`POST`,signal:h.signal,credentials:`same-origin`,headers:{"content-type":`application/json`},body:_});if(T(),v.status===401||v.status===403)throw m=!0,f=`Sign in to Po again.`,p?.(),Error(f);let y=await v.text();T();let S=await x(y);if(T(),S===void 0)return new Response(JSON.stringify({error:f}),{status:502});let C;try{C=JSON.parse(o.decode(S))}catch{C=void 0}if(!d(C))return f=`The sealed HTTP response was malformed.`,new Response(JSON.stringify({error:f}),{status:502});let w=c(C.body);return new Response(C.status===204||C.status===205||C.status===304?null:w,{status:C.status,headers:C.headers})})(),C])),C])}finally{clearTimeout(y),t?.signal?.removeEventListener(`abort`,g),S&&h.signal.removeEventListener(`abort`,S)}}}}const p=/* @__PURE__ */ new Map;function m(e,t=``){return p.set(t,e),()=>{p.get(t)===e&&p.delete(t)}}function h(e){return{active:()=>p.get(e)?.active()??!1,async fetch(t,n){n?.signal?.throwIfAborted();let r=p.get(e);if(!r)throw Error(`The host transport is not connected.`);let i=g(t,e);return r.fetch(i.input,n)}}}function g(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 _={active:()=>p.get(``)?.active()??!1,async fetch(e,t){t?.signal?.throwIfAborted();let n=g(e);if(n.hostId!==void 0)return h(n.hostId).fetch(n.input,t);let r=p.get(``);return r?r.fetch(e,t):fetch(e,i(e,t))}};export{m as bindBrowserTransport,_ as browserTransport,h as browserTransportForHost,f as createSealedTransport,i as poRequestInit};
|
|
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","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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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["poRequestInit","input","init","pageLocation","origin","url","metadata","clientMetadata","headers","encoder","decoder","encodeBase64","binary","byte","bytes","decodeBase64","value","character","bodyBytes","body","requestTarget","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","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 { clientMetadata } from '../clientIdentity';\nimport { createSerialQueue } from '../serialQueue';\n\n// This package also typechecks without DOM libraries for its native consumers.\ndeclare const location: { origin: string };\n\n/** Only same-origin Po API calls are eligible. Assets and arbitrary fetch targets stay untouched. */\nexport function poRequestInit(input: string, init?: RequestInit): RequestInit | undefined {\n // oxlint-disable-next-line no-control-regex -- Reject URL normalization ambiguities at admission.\n if (/[\\\\\\\\\\u0000-\\u0020]/u.test(input) || input.startsWith('//')) return init;\n const pageLocation = typeof location === 'undefined' ? undefined : location;\n const origin = pageLocation?.origin ?? 'https://po.invalid';\n if (!pageLocation && !input.startsWith('/')) return init;\n const url = new URL(input, origin);\n if (url.origin !== origin || url.username || url.password) return init;\n if (!/^\\/(?:api(?:\\/|$)|hosts\\/[^/]+\\/api(?:\\/|$))/u.test(url.pathname)) return init;\n if (/\\/api\\/web-plugins(?:\\/|$)/u.test(url.pathname) || url.pathname.endsWith('/api/remote/request')) return init;\n const metadata = clientMetadata();\n if (!metadata) return init;\n const headers = new Headers(init?.headers);\n headers.set('x-po-client-metadata', metadata);\n return { ...init, headers, redirect: 'error' };\n}\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('/') || input.startsWith('//') || input.includes('\\\\'))\n 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, poRequestInit(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(poRequestInit(input, 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, poRequestInit(input, init));\n },\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/po-web-security",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.112",
|
|
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",
|