@robota-sdk/agent-transport 3.0.0-beta.82 → 3.0.0-beta.84

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +795 -0
  2. package/README.md +80 -118
  3. package/dist/browser/client.d.ts +2 -2
  4. package/dist/browser/client.js +1 -1
  5. package/dist/browser/index.d.ts +87 -13
  6. package/dist/browser/index.d.ts.map +1 -1
  7. package/dist/browser/index.js +1 -1
  8. package/dist/browser/index.js.map +1 -1
  9. package/dist/browser/observer-messages-CEeisRaF.js +2 -0
  10. package/dist/browser/observer-messages-CEeisRaF.js.map +1 -0
  11. package/dist/browser/observer-messages-Dnff2Tdb.d.ts +471 -0
  12. package/dist/browser/observer-messages-Dnff2Tdb.d.ts.map +1 -0
  13. package/dist/node/index.cjs +1 -1
  14. package/dist/node/index.d.cts +308 -16
  15. package/dist/node/index.d.cts.map +1 -1
  16. package/dist/node/index.d.ts +308 -16
  17. package/dist/node/index.d.ts.map +1 -1
  18. package/dist/node/index.js +1 -1
  19. package/dist/node/index.js.map +1 -1
  20. package/dist/node/node.cjs +1 -1
  21. package/dist/node/node.d.cts +69 -1
  22. package/dist/node/node.d.cts.map +1 -1
  23. package/dist/node/node.d.ts +70 -2
  24. package/dist/node/node.d.ts.map +1 -1
  25. package/dist/node/node.js +1 -1
  26. package/dist/node/node.js.map +1 -1
  27. package/package.json +17 -14
  28. package/src/__tests__/bearer-resource-gate.test.ts +120 -0
  29. package/src/__tests__/history-attached-budget.test.ts +216 -0
  30. package/src/__tests__/message-decoders.test.ts +496 -6
  31. package/src/__tests__/project-read-messages.test.ts +118 -0
  32. package/src/__tests__/session-detail-loop-status.test.ts +169 -0
  33. package/src/__tests__/session-directory-messages.test.ts +322 -0
  34. package/src/__tests__/session-error-classification.test.ts +113 -0
  35. package/src/__tests__/session-history-wire.test.ts +342 -0
  36. package/src/__tests__/session-message-handler-observe.test.ts +217 -0
  37. package/src/__tests__/session-message-handler.test.ts +139 -4
  38. package/src/__tests__/settings-messages.test.ts +193 -0
  39. package/src/__tests__/ws-broadcast-events.test.ts +10 -0
  40. package/src/client.ts +9 -1
  41. package/src/index.ts +18 -2
  42. package/src/loop-control-messages.ts +34 -0
  43. package/src/message-decoders.ts +238 -6
  44. package/src/node/bearer-resource-gate.ts +291 -0
  45. package/src/node/index.ts +13 -0
  46. package/src/observer-messages.ts +47 -0
  47. package/src/open-prompts.ts +76 -0
  48. package/src/outbound-delivery.ts +11 -2
  49. package/src/project-read-messages.ts +74 -0
  50. package/src/protocol-session.ts +23 -1
  51. package/src/session-directory-messages.ts +162 -0
  52. package/src/session-error-classification.ts +91 -0
  53. package/src/session-events.ts +100 -19
  54. package/src/session-message-handler.ts +110 -15
  55. package/src/session-query-messages.ts +123 -5
  56. package/src/session-resume-bridge.ts +8 -0
  57. package/src/settings-messages.ts +103 -0
  58. package/src/wire-messages.ts +238 -11
  59. package/dist/browser/message-decoders-BB8n-H2X.d.ts +0 -252
  60. package/dist/browser/message-decoders-BB8n-H2X.d.ts.map +0 -1
  61. package/dist/browser/message-decoders-CDmDoZPR.js +0 -2
  62. package/dist/browser/message-decoders-CDmDoZPR.js.map +0 -1
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("node:crypto"),t=require("jose"),n=require("@robota-sdk/agent-core/node");function r(){let t=(0,e.randomBytes)(32).toString(`hex`);if(t.length!==64)throw Error(`transport admission: could not mint a credential of the required length`);return t}function i(e={}){let t=`open`in e&&e.open===!0,n=e.token!==void 0&&e.token!==null&&e.token!==``;if(n&&t)throw Error("transport admission: a token and `open: true` are contradictory — one requires a credential and the other requires none. Asking for both means one of the two beliefs behind the call is wrong; say which is intended.");if(n)return{token:e.token};if(e.token===null||t){let t=e.openReason?.trim();if(!t)throw Error("transport admission: an open transport requires `openReason`. An open transport is a decision, and a decision nobody wrote down is indistinguishable from an oversight.");return{token:null,openReason:t}}return{token:r()}}function a(t,n){if(n===void 0)return!1;let r=Buffer.from(t,`utf8`),i=Buffer.from(n,`utf8`);return r.length===i.length&&(0,e.timingSafeEqual)(r,i)}function o(e){if(!e||e.slice(0,6).toLowerCase()!==`bearer`)return;let t=e.slice(6);if(t===``||t[0]!==` `&&t[0]!==` `)return;let n=t.replace(/^[ \t]+/,``);return n===``?void 0:n}function s(t){let n=JSON.stringify(t),r=Buffer.from(n,`utf8`);return{serialized:n,integrity:{digest:(0,e.createHash)(`sha256`).update(r).digest(`base64url`),byteLength:r.byteLength}}}function c(t,n){let r=Buffer.from(t,`utf8`);return r.byteLength===n.byteLength?(0,e.createHash)(`sha256`).update(r).digest(`base64url`)===n.digest?{intact:!0}:{intact:!1,failure:`digest-mismatch`}:{intact:!1,failure:`truncated`,expectedBytes:n.byteLength,actualBytes:r.byteLength}}const l=10*6e4,u=`at+jwt`,d=[`RS256`,`ES256`,`EdDSA`],f={RS256:{kty:`RSA`},ES256:{kty:`EC`,crv:`P-256`},EdDSA:{kty:`OKP`,crv:`Ed25519`}};var p=class{refusal;constructor(e){this.refusal=e}};function m(e,r={}){let i=h(e),a=r.now??Date.now,o=r.monotonicNow??(()=>performance.now()),s={fetch:r.fetch,lookup:r.lookup},c=i.hostname.replace(/^\[|\]$/g,``).toLowerCase(),d={keys:void 0,jwksUri:void 0,fetchedAt:void 0,lastAttemptAt:void 0,inflight:void 0},f=new Set(e.allowedSubjects??[]),m=new Set(e.allowedClients??[]);async function S(e){if(new URL(e).protocol!==`https:`)return;let t=await(0,n.fetchWithEgressPolicy)(e,{headers:{accept:`application/json`},timeoutMs:1e4,maxResponseBytes:65536},{allowedHosts:[c],maxRedirects:0},s);if(!(!t.ok||t.status!==200))return JSON.parse(new TextDecoder().decode(t.body))}async function C(){for(let t of g(i)){let n=await S(t).catch(()=>void 0);if(!x(n))continue;if(n.issuer!==e.issuer)return;let r=n.jwks_uri;return typeof r!=`string`||!URL.canParse(r)?void 0:new URL(r).protocol===`https:`?r:void 0}}function w(){return d.fetchedAt===void 0?1/0:o()-d.fetchedAt}async function T(){d.lastAttemptAt=o();try{if(w()>=l&&(d.jwksUri=void 0),d.jwksUri??=await C(),d.jwksUri===void 0)return;let e=_(await S(d.jwksUri));if(e===void 0)return;d.keys=e,d.fetchedAt=o()}catch{}}async function E(){if(d.inflight!==void 0)return d.inflight;if(!(d.lastAttemptAt!==void 0&&o()-d.lastAttemptAt<3e4))return d.inflight=T().finally(()=>{d.inflight=void 0}),d.inflight}async function D(){return w()>=l&&await E(),w()>=36e5&&(d.keys=void 0,d.jwksUri=void 0,d.fetchedAt=void 0),d.keys}async function O(e,t){let n=await D();if(n===void 0)throw new p(`keys-unavailable`);if(e===void 0){if(n.length>1)throw new p(`ambiguous-key`);let e=n[0];if(e===void 0)throw new p(`unknown-key`);if(!v(e,t))throw new p(`key-mismatch`);return e}let r=n.filter(t=>t.kid===e);if(r.length===0&&(await E(),n=d.keys??[],r=n.filter(t=>t.kid===e)),r.length===0)throw new p(`unknown-key`);let i=r.find(e=>v(e,t));if(i===void 0)throw new p(`key-mismatch`);return i}async function k(n){if(typeof n!=`string`||n.length===0)throw new p(`malformed`);if(n.length>8192)throw new p(`oversize`);let r;try{r=(0,t.decodeProtectedHeader)(n)}catch{throw new p(`malformed`)}if(typeof r.typ!=`string`||b(r.typ)!==u)throw new p(`wrong-type`);let i=e.algorithms.find(e=>e===r.alg);if(i===void 0)throw new p(`unsupported-algorithm`);if(r.kid!==void 0&&typeof r.kid!=`string`)throw new p(`malformed`);let o=await O(r.kid,i),s;try{s=await(0,t.importJWK)(o,i)}catch{throw new p(`key-mismatch`)}let c;try{({payload:c}=await(0,t.jwtVerify)(n,s,{issuer:e.issuer,audience:e.resource,algorithms:[i],typ:u,clockTolerance:60,currentDate:new Date(a()),requiredClaims:[`exp`,`sub`,`client_id`]}))}catch(e){throw new p(y(e))}let l=typeof c.scope==`string`?c.scope.split(` `):[];if(!e.requiredScopes.every(e=>l.includes(e)))throw new p(`missing-scope`);let d=c.sub,h=c.client_id;if(!(typeof d==`string`&&f.has(d)||typeof h==`string`&&m.has(h)))throw new p(`principal-not-allowed`);return{admitted:!0}}return{async verify(e){try{return await k(e)}catch(e){return{admitted:!1,refusal:e instanceof p?e.refusal:`malformed`}}}}}function h(e){if(!URL.canParse(e.issuer))throw Error(`access-token verifier: issuer is not a URL`);let t=new URL(e.issuer);if(t.protocol!==`https:`||t.search!==``||t.hash!==``)throw Error(`access-token verifier: issuer must be an https URL with no query or fragment`);if(!URL.canParse(e.resource))throw Error(`access-token verifier: resource is not a URL`);if(e.algorithms.length===0||!e.algorithms.every(e=>d.includes(e)))throw Error(`access-token verifier: algorithms must be a non-empty subset of RS256, ES256, EdDSA`);if(e.requiredScopes.length===0)throw Error(`access-token verifier: at least one required scope is needed`);if((e.allowedSubjects?.length??0)+(e.allowedClients?.length??0)===0)throw Error(`access-token verifier: name at least one allowed subject or client — this host is single-tenant, and issuer + audience + scope alone admit anyone the issuer serves`);return t}function g(e){let t=e.pathname===`/`?``:e.pathname.replace(/\/$/,``);return[`${e.origin}/.well-known/oauth-authorization-server${t}`,`${e.origin}${t}/.well-known/openid-configuration`]}function _(e){if(!(!x(e)||!Array.isArray(e.keys)||e.keys.length>32))return e.keys.filter(e=>x(e)&&typeof e.kty==`string`)}function v(e,t){let n=f[t];return e.kty!==n.kty||n.crv!==void 0&&e.crv!==n.crv||e.alg!==void 0&&e.alg!==t||e.use!==void 0&&e.use!==`sig`||e.key_ops!==void 0&&!e.key_ops.includes(`verify`)?!1:e.d===void 0}function y(e){let t=x(e)?e.code:void 0,n=x(e)?e.claim:void 0;switch(t){case`ERR_JWT_EXPIRED`:return`expired`;case`ERR_JWS_SIGNATURE_VERIFICATION_FAILED`:return`bad-signature`;case`ERR_JOSE_ALG_NOT_ALLOWED`:return`unsupported-algorithm`;case`ERR_JWT_CLAIM_VALIDATION_FAILED`:return n===`iss`?`wrong-issuer`:n===`aud`?`wrong-audience`:n===`nbf`?`not-yet-valid`:n===`typ`?`wrong-type`:`malformed`;default:return`malformed`}}function b(e){let t=e.toLowerCase();return t.startsWith(`application/`)?t.slice(12):t}function x(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}const S=/^[0-9a-f]{64}$/,C=/^[A-Za-z0-9+/]*={0,2}$/,w=3e4,T=new Set([`declined`,`too-large`,`bad-name`,`exists`,`unsafe-path`,`integrity`,`protocol`,`closed`,`timeout`,`unavailable`]),E=[[0,31],[127,159],[8203,8207],[8232,8238],[8294,8297],[65279,65279]];function D(e){let t=``;for(let n of e){let e=n.codePointAt(0)??0;t+=E.some(([t,n])=>e>=t&&e<=n)?` `:n}return t}function O(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=0}function k(e){if(e.length>22360)return;let t;try{t=JSON.parse(e)}catch{return}if(typeof t!=`object`||!t)return;let n=t;switch(n.t){case`file-offer`:{let{transferId:e,name:t,size:r,sha256:i}=n;return typeof e!=`string`||e.length===0||e.length>128||typeof t!=`string`||t.length>1024||!O(r)||typeof i!=`string`||!S.test(i)?void 0:{t:`file-offer`,transferId:e,name:t,size:r,sha256:i}}case`file-accept`:return{t:`file-accept`};case`file-refuse`:{let e=n.reason,t=n.detail;return typeof e!=`string`||!T.has(e)?void 0:{t:`file-refuse`,reason:e,...typeof t==`string`?{detail:D(t).slice(0,500)}:{}}}case`file-chunk`:{let{seq:e,data:t}=n;return!O(e)||typeof t!=`string`||t===``||!C.test(t)?void 0:{t:`file-chunk`,seq:e,data:t}}case`file-credit`:return O(n.upTo)?{t:`file-credit`,upTo:n.upTo}:void 0;case`file-end`:return O(n.chunks)?{t:`file-end`,chunks:n.chunks}:void 0;case`file-done`:return{t:`file-done`};default:return}}var A=class extends Error{reason;detail;constructor(e,t){super(t??e),this.reason=e,this.detail=t}},j=class{capacity;queue=[];waiter;failure;stops=[];constructor(e,t){this.capacity=t,this.stops.push(e.onFrame(e=>{let t=k(e);if(t===void 0){this.fail(new A(`protocol`,`a frame could not be read`));return}if(this.waiter!==void 0){let{resolve:e}=this.waiter;this.waiter=void 0,e(t);return}if(this.queue.length>=this.capacity){this.fail(new A(`protocol`,`the peer sent past its credit`));return}this.queue.push(t)}),e.onClose(()=>this.fail(new A(`closed`,`the channel closed`))))}fail(e){if(this.failure!==void 0)return;this.failure=e,e.reason===`protocol`&&(this.queue.length=0);let t=this.waiter;this.waiter=void 0,t?.reject(e)}next(e){let t=this.queue.shift();return t===void 0?this.failure===void 0?new Promise((t,n)=>{let r=setTimeout(()=>{this.waiter=void 0,n(new A(`timeout`,`nothing arrived within ${e}ms`))},e);r.unref?.(),this.waiter={resolve:e=>{clearTimeout(r),t(e)},reject:e=>{clearTimeout(r),n(e)}}}):Promise.reject(this.failure):Promise.resolve(t)}get ended(){return this.failure!==void 0}stop(){for(let e of this.stops.splice(0))e()}};function M(e,t){try{e.send(JSON.stringify(t))}catch(e){throw new A(`closed`,e instanceof Error?e.message:String(e))}}function N(e){return e instanceof A?e:new A(`unavailable`,e instanceof Error?e.message:String(e))}async function*P(e,t){let n=Buffer.alloc(0);for await(let r of e)for(n=n.length===0?Buffer.from(r):Buffer.concat([n,r]);n.length>=t;)yield n.subarray(0,t),n=n.subarray(t);n.length>0&&(yield n)}async function F(e){let{channel:t,offer:n,source:r}=e,i=e.chunkBytes??16384,a=e.window??16,o=e.idleMs??w,s=new j(t,a+2);try{if(r.size!==n.size)throw new A(`integrity`,`the source is not the size offered`);M(t,{t:`file-offer`,...n});let c=await s.next(e.decisionMs??3e5);if(c.t===`file-refuse`)throw new A(c.reason,c.detail);if(c.t!==`file-accept`)throw new A(`protocol`,`expected an answer`);let l=0,u=0,d=0,f=async()=>{let e=await s.next(o);if(e.t===`file-refuse`)throw new A(e.reason,e.detail);if(e.t!==`file-credit`||e.upTo<=u||e.upTo>l)throw new A(`protocol`,`expected credit`);u=e.upTo};for await(let e of P(r.read(),i)){if(d+=e.length,d>n.size)throw new A(`integrity`,`the source grew while sending`);for(;l-u>=a;)await f();M(t,{t:`file-chunk`,seq:l,data:e.toString(`base64`)}),l+=1}if(d!==n.size)throw new A(`integrity`,`the source shrank while sending`);for(M(t,{t:`file-end`,chunks:l});;){let e=await s.next(o);if(e.t===`file-done`)return{ok:!0};if(e.t===`file-refuse`)throw new A(e.reason,e.detail);if(e.t!==`file-credit`||e.upTo<=u||e.upTo>l)throw new A(`protocol`,`expected credit or completion`);u=e.upTo}}catch(e){let t=N(e);return{ok:!1,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail}}}finally{s.stop(),t.close()}}async function I(t){let{channel:n}=t,r=t.maxBytes??33554432,i=t.window??16,a=t.idleMs??w,o=new j(n,i+1),s=new AbortController,c=n.onClose(()=>s.abort()),l,u,d=!0;try{let i=await o.next(a);if(i.t!==`file-offer`)throw new A(`protocol`,`expected an offer`);if(l={transferId:i.transferId,name:i.name,size:i.size,sha256:i.sha256},l.size>r)throw new A(`too-large`,`${l.size} bytes is over the ${r}-byte limit`);let c=await t.admit(l,s.signal);if(`refused`in c)throw new A(c.refused,c.detail);if(u=c.sink,o.ended)throw new A(`closed`,`the sender left before the answer`);M(n,{t:`file-accept`});let d=(0,e.createHash)(`sha256`),f=0,p=0;for(;;){let e=await o.next(a);if(e.t===`file-end`){if(e.chunks!==f||p!==l.size)throw new A(`integrity`,`the content is not the size offered`);if(d.digest(`hex`)!==l.sha256)throw new A(`integrity`,`the content does not match the offered sha256`);let t=await u.commit();return u=void 0,M(n,{t:`file-done`}),{ok:!0,offer:l,location:t}}if(e.t!==`file-chunk`||e.seq!==f)throw new A(`protocol`,`expected the next chunk`);let t=Buffer.from(e.data,`base64`);if(p+=t.length,p>l.size)throw new A(`integrity`,`more content than offered`);d.update(t),await u.write(t),f+=1,M(n,{t:`file-credit`,upTo:f})}}catch(e){let t=N(e);if(d=t.reason!==`protocol`,u!==void 0&&await u.discard().catch(()=>void 0),t.reason!==`closed`&&t.reason!==`timeout`)try{M(n,{t:`file-refuse`,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail}})}catch{}return{ok:!1,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail},...l===void 0?{}:{offer:l}}}finally{d&&await L(s.signal,5e3),c(),o.stop(),n.close()}}function L(e,t){return e.aborted?Promise.resolve():new Promise(n=>{let r=setTimeout(i,t);r.unref?.(),e.addEventListener(`abort`,i,{once:!0});function i(){clearTimeout(r),e.removeEventListener(`abort`,i),n()}})}exports.DEFAULT_MAX_FILE_BYTES=33554432,exports.FILE_CHUNK_BYTES=16384,exports.FILE_CREDIT_WINDOW=16,exports.bearerCredential=o,exports.createAccessTokenVerifier=m,exports.credentialMatches=a,exports.mintTransportToken=r,exports.receiveFileOverChannel=I,exports.resolveAdmission=i,exports.sealHandoffRecord=s,exports.sendFileOverChannel=F,exports.verifyHandoffPayload=c;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("node:crypto"),t=require("jose"),n=require("@robota-sdk/agent-core/node"),r=require("node:net");function i(){let t=(0,e.randomBytes)(32).toString(`hex`);if(t.length!==64)throw Error(`transport admission: could not mint a credential of the required length`);return t}function a(e={}){let t=`open`in e&&e.open===!0,n=e.token!==void 0&&e.token!==null&&e.token!==``;if(n&&t)throw Error("transport admission: a token and `open: true` are contradictory — one requires a credential and the other requires none. Asking for both means one of the two beliefs behind the call is wrong; say which is intended.");if(n)return{token:e.token};if(e.token===null||t){let t=e.openReason?.trim();if(!t)throw Error("transport admission: an open transport requires `openReason`. An open transport is a decision, and a decision nobody wrote down is indistinguishable from an oversight.");return{token:null,openReason:t}}return{token:i()}}function o(t,n){if(n===void 0)return!1;let r=Buffer.from(t,`utf8`),i=Buffer.from(n,`utf8`);return r.length===i.length&&(0,e.timingSafeEqual)(r,i)}function s(e){if(!e||e.slice(0,6).toLowerCase()!==`bearer`)return;let t=e.slice(6);if(t===``||t[0]!==` `&&t[0]!==` `)return;let n=t.replace(/^[ \t]+/,``);return n===``?void 0:n}function c(t){let n=JSON.stringify(t),r=Buffer.from(n,`utf8`);return{serialized:n,integrity:{digest:(0,e.createHash)(`sha256`).update(r).digest(`base64url`),byteLength:r.byteLength}}}function l(t,n){let r=Buffer.from(t,`utf8`);return r.byteLength===n.byteLength?(0,e.createHash)(`sha256`).update(r).digest(`base64url`)===n.digest?{intact:!0}:{intact:!1,failure:`digest-mismatch`}:{intact:!1,failure:`truncated`,expectedBytes:n.byteLength,actualBytes:r.byteLength}}const u=10*6e4,d=`at+jwt`,f=[`RS256`,`ES256`,`EdDSA`],p={RS256:{kty:`RSA`},ES256:{kty:`EC`,crv:`P-256`},EdDSA:{kty:`OKP`,crv:`Ed25519`}};var m=class{refusal;constructor(e){this.refusal=e}};function h(e,r={}){let i=g(e),a=r.now??Date.now,o=r.monotonicNow??(()=>performance.now()),s={fetch:r.fetch,lookup:r.lookup},c=i.hostname.replace(/^\[|\]$/g,``).toLowerCase(),l={keys:void 0,jwksUri:void 0,fetchedAt:void 0,lastAttemptAt:void 0,inflight:void 0},f=new Set(e.allowedSubjects??[]),p=new Set(e.allowedClients??[]);async function h(e){if(new URL(e).protocol!==`https:`)return;let t=await(0,n.fetchWithEgressPolicy)(e,{headers:{accept:`application/json`},timeoutMs:1e4,maxResponseBytes:65536},{allowedHosts:[c],maxRedirects:0},s);if(!(!t.ok||t.status!==200))return JSON.parse(new TextDecoder().decode(t.body))}async function C(){for(let t of _(i)){let n=await h(t).catch(()=>void 0);if(!S(n))continue;if(n.issuer!==e.issuer)return;let r=n.jwks_uri;return typeof r!=`string`||!URL.canParse(r)?void 0:new URL(r).protocol===`https:`?r:void 0}}function w(){return l.fetchedAt===void 0?1/0:o()-l.fetchedAt}async function T(){l.lastAttemptAt=o();try{if(w()>=u&&(l.jwksUri=void 0),l.jwksUri??=await C(),l.jwksUri===void 0)return;let e=v(await h(l.jwksUri));if(e===void 0)return;l.keys=e,l.fetchedAt=o()}catch{}}async function E(){if(l.inflight!==void 0)return l.inflight;if(!(l.lastAttemptAt!==void 0&&o()-l.lastAttemptAt<3e4))return l.inflight=T().finally(()=>{l.inflight=void 0}),l.inflight}async function D(){return w()>=u&&await E(),w()>=36e5&&(l.keys=void 0,l.jwksUri=void 0,l.fetchedAt=void 0),l.keys}async function O(e,t){let n=await D();if(n===void 0)throw new m(`keys-unavailable`);if(e===void 0){if(n.length>1)throw new m(`ambiguous-key`);let e=n[0];if(e===void 0)throw new m(`unknown-key`);if(!y(e,t))throw new m(`key-mismatch`);return e}let r=n.filter(t=>t.kid===e);if(r.length===0&&(await E(),n=l.keys??[],r=n.filter(t=>t.kid===e)),r.length===0)throw new m(`unknown-key`);let i=r.find(e=>y(e,t));if(i===void 0)throw new m(`key-mismatch`);return i}async function k(n){if(typeof n!=`string`||n.length===0)throw new m(`malformed`);if(n.length>8192)throw new m(`oversize`);let r;try{r=(0,t.decodeProtectedHeader)(n)}catch{throw new m(`malformed`)}if(typeof r.typ!=`string`||x(r.typ)!==d)throw new m(`wrong-type`);let i=e.algorithms.find(e=>e===r.alg);if(i===void 0)throw new m(`unsupported-algorithm`);if(r.kid!==void 0&&typeof r.kid!=`string`)throw new m(`malformed`);let o=await O(r.kid,i),s;try{s=await(0,t.importJWK)(o,i)}catch{throw new m(`key-mismatch`)}let c;try{({payload:c}=await(0,t.jwtVerify)(n,s,{issuer:e.issuer,audience:e.resource,algorithms:[i],typ:d,clockTolerance:60,currentDate:new Date(a()),requiredClaims:[`exp`,`sub`,`client_id`]}))}catch(e){throw new m(b(e))}let l=typeof c.scope==`string`?c.scope.split(` `):[];if(!e.requiredScopes.every(e=>l.includes(e)))throw new m(`missing-scope`);let u=c.sub,h=c.client_id;if(!(typeof u==`string`&&f.has(u)||typeof h==`string`&&p.has(h)))throw new m(`principal-not-allowed`);return{admitted:!0}}return{async verify(e){try{return await k(e)}catch(e){return{admitted:!1,refusal:e instanceof m?e.refusal:`malformed`}}}}}function g(e){if(!URL.canParse(e.issuer))throw Error(`access-token verifier: issuer is not a URL`);let t=new URL(e.issuer);if(t.protocol!==`https:`||t.search!==``||t.hash!==``)throw Error(`access-token verifier: issuer must be an https URL with no query or fragment`);if(!URL.canParse(e.resource))throw Error(`access-token verifier: resource is not a URL`);if(e.algorithms.length===0||!e.algorithms.every(e=>f.includes(e)))throw Error(`access-token verifier: algorithms must be a non-empty subset of RS256, ES256, EdDSA`);if(e.requiredScopes.length===0)throw Error(`access-token verifier: at least one required scope is needed`);if((e.allowedSubjects?.length??0)+(e.allowedClients?.length??0)===0)throw Error(`access-token verifier: name at least one allowed subject or client — this host is single-tenant, and issuer + audience + scope alone admit anyone the issuer serves`);return t}function _(e){let t=e.pathname===`/`?``:e.pathname.replace(/\/$/,``);return[`${e.origin}/.well-known/oauth-authorization-server${t}`,`${e.origin}${t}/.well-known/openid-configuration`]}function v(e){if(!(!S(e)||!Array.isArray(e.keys)||e.keys.length>32))return e.keys.filter(e=>S(e)&&typeof e.kty==`string`)}function y(e,t){let n=p[t];return e.kty!==n.kty||n.crv!==void 0&&e.crv!==n.crv||e.alg!==void 0&&e.alg!==t||e.use!==void 0&&e.use!==`sig`||e.key_ops!==void 0&&!e.key_ops.includes(`verify`)?!1:e.d===void 0}function b(e){let t=S(e)?e.code:void 0,n=S(e)?e.claim:void 0;switch(t){case`ERR_JWT_EXPIRED`:return`expired`;case`ERR_JWS_SIGNATURE_VERIFICATION_FAILED`:return`bad-signature`;case`ERR_JOSE_ALG_NOT_ALLOWED`:return`unsupported-algorithm`;case`ERR_JWT_CLAIM_VALIDATION_FAILED`:return n===`iss`?`wrong-issuer`:n===`aud`?`wrong-audience`:n===`nbf`?`not-yet-valid`:n===`typ`?`wrong-type`:`malformed`;default:return`malformed`}}function x(e){let t=e.toLowerCase();return t.startsWith(`application/`)?t.slice(12):t}function S(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}const C=6e4,w=4096,T=/^[\x21\x23-\x5b\x5d-\x7e]+$/,E=new r.BlockList;E.addSubnet(`127.0.0.0`,8,`ipv4`),E.addAddress(`::1`,`ipv6`);const D=new r.BlockList;D.addSubnet(`10.0.0.0`,8,`ipv4`),D.addSubnet(`172.16.0.0`,12,`ipv4`),D.addSubnet(`192.168.0.0`,16,`ipv4`),D.addSubnet(`169.254.0.0`,16,`ipv4`),D.addSubnet(`100.64.0.0`,10,`ipv4`),D.addSubnet(`fc00::`,7,`ipv6`),D.addSubnet(`fe80::`,10,`ipv6`);function O(e){if(e===void 0)return;let t=e.trim().toLowerCase(),n=t.startsWith(`::ffff:`)?t.slice(7):void 0;return n!==void 0&&(0,r.isIP)(n)===4?n:(0,r.isIP)(t)===0?void 0:t}function k(e){if(e===void 0)return`unknown`;let t=(0,r.isIP)(e)===4?`ipv4`:`ipv6`;return E.check(e,t)?`loopback`:D.check(e,t)?`private`:`public`}function A(e,t){let n;try{n=new URL(e)}catch{throw Error(`${t} public URL must be an absolute https URL`)}if(n.protocol!==`https:`)throw Error(`${t} public URL must use https`);if(n.username!==``||n.password!==``||n.search!==``||n.hash!==``)throw Error(`${t} public URL must not carry credentials, a query or a fragment`);return n}function j(e){let t=new Map;return n=>{let r=e(),i=t.get(n);if(i===void 0||r-i.startedAt>=C){if(t.delete(n),t.size>=w){for(let[e,n]of t)r-n.startedAt>=C&&t.delete(e);let e=t.keys().next();t.size>=w&&e.done!==!0&&t.delete(e.value)}i={count:0,startedAt:r},t.set(n,i)}i.count+=1;let a=C-(r-i.startedAt);return{throttled:i.count>20,retryAfterSeconds:Math.max(1,Math.ceil(a/1e3))}}}function M(e){let t=A(e.publicUrl,e.label),n=new Set;for(let t of e.trustedProxies??[]){let r=O(t);if(r===void 0)throw Error(`${e.label} trusted proxy must be a literal IP address`);n.add(r)}let r=new Set([t.host]);t.port===``&&r.add(`${t.hostname}:443`);let i=j(e.now??(()=>performance.now()));function a(e){let t=O(e.socket.remoteAddress);if(t===void 0||!n.has(t))return t;let r=e.headers[`x-forwarded-for`],i=(Array.isArray(r)?r.join(`,`):r??``).split(`,`);for(let e=i.length-1;e>=0;--e){let r=O(i[e]);if(r===void 0)return t;if(!n.has(r))return r}return t}return{url:t,checkNames(e,n){let i=e.headers.host?.toLowerCase(),a=e.headers.origin;return i===void 0||!r.has(i)||a!==void 0&&a!==t.origin?(n.writeHead(403).end(),!1):!0},fail(e){let t=a(e);return{remote:k(t),...i(t??`unknown`)}},remote(e){return k(a(e))}}}function N(e){let t=A(e.resource,e.label),n;try{n=new URL(e.issuer)}catch{throw Error(`${e.label} authorization issuer must be an absolute https URL`)}if(n.protocol!==`https:`)throw Error(`${e.label} authorization issuer must use https`);if(e.scopes.length===0||!e.scopes.every(e=>T.test(e)))throw Error(`${e.label} authorization needs at least one valid scope`);let r=`/.well-known/oauth-protected-resource${t.pathname===`/`?``:t.pathname}`,i=`${t.origin}${r}`,a=e.scopes.join(` `);return{wellKnownPath:r,metadata:JSON.stringify({resource:e.resource,authorization_servers:[e.issuer],scopes_supported:[...e.scopes],bearer_methods_supported:[`header`]}),challenges:{missing:`Bearer resource_metadata="${i}"`,invalid:`Bearer error="invalid_token", resource_metadata="${i}"`,scope:`Bearer error="insufficient_scope", scope="${a}", resource_metadata="${i}"`}}}function P(e,t,n){if(e.method!==`GET`){t.writeHead(405,{Allow:`GET`}).end();return}t.writeHead(200,{"Content-Type":`application/json`}).end(n.metadata)}function F(e,t,n,r){if(r.throttled)e.writeHead(429,{"Retry-After":String(r.retryAfterSeconds)}).end();else if(n===`missing-scope`)e.writeHead(403,{"WWW-Authenticate":t.challenges.scope}).end();else{let r=n===`missing-token`?t.challenges.missing:t.challenges.invalid;e.writeHead(401,{"WWW-Authenticate":r}).end()}}const I=/^[0-9a-f]{64}$/,L=/^[A-Za-z0-9+/]*={0,2}$/,R=3e4,z=new Set([`declined`,`too-large`,`bad-name`,`exists`,`unsafe-path`,`integrity`,`protocol`,`closed`,`timeout`,`unavailable`]),B=[[0,31],[127,159],[8203,8207],[8232,8238],[8294,8297],[65279,65279]];function V(e){let t=``;for(let n of e){let e=n.codePointAt(0)??0;t+=B.some(([t,n])=>e>=t&&e<=n)?` `:n}return t}function H(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=0}function U(e){if(e.length>22360)return;let t;try{t=JSON.parse(e)}catch{return}if(typeof t!=`object`||!t)return;let n=t;switch(n.t){case`file-offer`:{let{transferId:e,name:t,size:r,sha256:i}=n;return typeof e!=`string`||e.length===0||e.length>128||typeof t!=`string`||t.length>1024||!H(r)||typeof i!=`string`||!I.test(i)?void 0:{t:`file-offer`,transferId:e,name:t,size:r,sha256:i}}case`file-accept`:return{t:`file-accept`};case`file-refuse`:{let e=n.reason,t=n.detail;return typeof e!=`string`||!z.has(e)?void 0:{t:`file-refuse`,reason:e,...typeof t==`string`?{detail:V(t).slice(0,500)}:{}}}case`file-chunk`:{let{seq:e,data:t}=n;return!H(e)||typeof t!=`string`||t===``||!L.test(t)?void 0:{t:`file-chunk`,seq:e,data:t}}case`file-credit`:return H(n.upTo)?{t:`file-credit`,upTo:n.upTo}:void 0;case`file-end`:return H(n.chunks)?{t:`file-end`,chunks:n.chunks}:void 0;case`file-done`:return{t:`file-done`};default:return}}var W=class extends Error{reason;detail;constructor(e,t){super(t??e),this.reason=e,this.detail=t}},G=class{capacity;queue=[];waiter;failure;stops=[];constructor(e,t){this.capacity=t,this.stops.push(e.onFrame(e=>{let t=U(e);if(t===void 0){this.fail(new W(`protocol`,`a frame could not be read`));return}if(this.waiter!==void 0){let{resolve:e}=this.waiter;this.waiter=void 0,e(t);return}if(this.queue.length>=this.capacity){this.fail(new W(`protocol`,`the peer sent past its credit`));return}this.queue.push(t)}),e.onClose(()=>this.fail(new W(`closed`,`the channel closed`))))}fail(e){if(this.failure!==void 0)return;this.failure=e,e.reason===`protocol`&&(this.queue.length=0);let t=this.waiter;this.waiter=void 0,t?.reject(e)}next(e){let t=this.queue.shift();return t===void 0?this.failure===void 0?new Promise((t,n)=>{let r=setTimeout(()=>{this.waiter=void 0,n(new W(`timeout`,`nothing arrived within ${e}ms`))},e);r.unref?.(),this.waiter={resolve:e=>{clearTimeout(r),t(e)},reject:e=>{clearTimeout(r),n(e)}}}):Promise.reject(this.failure):Promise.resolve(t)}get ended(){return this.failure!==void 0}stop(){for(let e of this.stops.splice(0))e()}};function K(e,t){try{e.send(JSON.stringify(t))}catch(e){throw new W(`closed`,e instanceof Error?e.message:String(e))}}function q(e){return e instanceof W?e:new W(`unavailable`,e instanceof Error?e.message:String(e))}async function*J(e,t){let n=Buffer.alloc(0);for await(let r of e)for(n=n.length===0?Buffer.from(r):Buffer.concat([n,r]);n.length>=t;)yield n.subarray(0,t),n=n.subarray(t);n.length>0&&(yield n)}async function Y(e){let{channel:t,offer:n,source:r}=e,i=e.chunkBytes??16384,a=e.window??16,o=e.idleMs??R,s=new G(t,a+2);try{if(r.size!==n.size)throw new W(`integrity`,`the source is not the size offered`);K(t,{t:`file-offer`,...n});let c=await s.next(e.decisionMs??3e5);if(c.t===`file-refuse`)throw new W(c.reason,c.detail);if(c.t!==`file-accept`)throw new W(`protocol`,`expected an answer`);let l=0,u=0,d=0,f=async()=>{let e=await s.next(o);if(e.t===`file-refuse`)throw new W(e.reason,e.detail);if(e.t!==`file-credit`||e.upTo<=u||e.upTo>l)throw new W(`protocol`,`expected credit`);u=e.upTo};for await(let e of J(r.read(),i)){if(d+=e.length,d>n.size)throw new W(`integrity`,`the source grew while sending`);for(;l-u>=a;)await f();K(t,{t:`file-chunk`,seq:l,data:e.toString(`base64`)}),l+=1}if(d!==n.size)throw new W(`integrity`,`the source shrank while sending`);for(K(t,{t:`file-end`,chunks:l});;){let e=await s.next(o);if(e.t===`file-done`)return{ok:!0};if(e.t===`file-refuse`)throw new W(e.reason,e.detail);if(e.t!==`file-credit`||e.upTo<=u||e.upTo>l)throw new W(`protocol`,`expected credit or completion`);u=e.upTo}}catch(e){let t=q(e);return{ok:!1,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail}}}finally{s.stop(),t.close()}}async function X(t){let{channel:n}=t,r=t.maxBytes??33554432,i=t.window??16,a=t.idleMs??R,o=new G(n,i+1),s=new AbortController,c=n.onClose(()=>s.abort()),l,u,d=!0;try{let i=await o.next(a);if(i.t!==`file-offer`)throw new W(`protocol`,`expected an offer`);if(l={transferId:i.transferId,name:i.name,size:i.size,sha256:i.sha256},l.size>r)throw new W(`too-large`,`${l.size} bytes is over the ${r}-byte limit`);let c=await t.admit(l,s.signal);if(`refused`in c)throw new W(c.refused,c.detail);if(u=c.sink,o.ended)throw new W(`closed`,`the sender left before the answer`);K(n,{t:`file-accept`});let d=(0,e.createHash)(`sha256`),f=0,p=0;for(;;){let e=await o.next(a);if(e.t===`file-end`){if(e.chunks!==f||p!==l.size)throw new W(`integrity`,`the content is not the size offered`);if(d.digest(`hex`)!==l.sha256)throw new W(`integrity`,`the content does not match the offered sha256`);let t=await u.commit();return u=void 0,K(n,{t:`file-done`}),{ok:!0,offer:l,location:t}}if(e.t!==`file-chunk`||e.seq!==f)throw new W(`protocol`,`expected the next chunk`);let t=Buffer.from(e.data,`base64`);if(p+=t.length,p>l.size)throw new W(`integrity`,`more content than offered`);d.update(t),await u.write(t),f+=1,K(n,{t:`file-credit`,upTo:f})}}catch(e){let t=q(e);if(d=t.reason!==`protocol`,u!==void 0&&await u.discard().catch(()=>void 0),t.reason!==`closed`&&t.reason!==`timeout`)try{K(n,{t:`file-refuse`,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail}})}catch{}return{ok:!1,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail},...l===void 0?{}:{offer:l}}}finally{d&&await Z(s.signal,5e3),c(),o.stop(),n.close()}}function Z(e,t){return e.aborted?Promise.resolve():new Promise(n=>{let r=setTimeout(i,t);r.unref?.(),e.addEventListener(`abort`,i,{once:!0});function i(){clearTimeout(r),e.removeEventListener(`abort`,i),n()}})}exports.DEFAULT_MAX_FILE_BYTES=33554432,exports.FILE_CHUNK_BYTES=16384,exports.FILE_CREDIT_WINDOW=16,exports.bearerCredential=s,exports.createAccessTokenVerifier=h,exports.createBearerResourceServer=M,exports.credentialMatches=o,exports.describeProtectedResource=N,exports.mintTransportToken=i,exports.parsePublicHttpsUrl=A,exports.receiveFileOverChannel=X,exports.refuseBearerToken=F,exports.resolveAdmission=a,exports.sealHandoffRecord=c,exports.sendFileOverChannel=Y,exports.serveProtectedResourceMetadata=P,exports.verifyHandoffPayload=l;
@@ -2,6 +2,7 @@ import { IInteractiveSessionRecord } from "@robota-sdk/agent-interface-session";
2
2
  import { IAccessTokenVerifier, IAccessTokenVerifierConfig, ITransportAdmission, ITransportAdmissionConfig } from "@robota-sdk/agent-interface-transport";
3
3
  import { IFileFrameChannel, IFileOffer, IHandoffIntegrity } from "@robota-sdk/agent-interface-session-mobility";
4
4
  import { IEgressDeps } from "@robota-sdk/agent-core/node";
5
+ import { IncomingMessage, ServerResponse } from "node:http";
5
6
  //#region src/node/admission.d.ts
6
7
  /**
7
8
  * Mint a per-launch credential.
@@ -105,6 +106,73 @@ interface IAccessTokenVerifierDeps {
105
106
  */
106
107
  declare function createAccessTokenVerifier(config: IAccessTokenVerifierConfig, deps?: IAccessTokenVerifierDeps): IAccessTokenVerifier;
107
108
  //#endregion
109
+ //#region src/node/bearer-resource-gate.d.ts
110
+ /** A coarse class of a peer's address; the address itself is never reported. */
111
+ type TRemoteAddressClass = 'loopback' | 'private' | 'public' | 'unknown';
112
+ /** Where a refused request came from, and whether its address is over the failure budget. */
113
+ interface IBearerFailure {
114
+ readonly remote: TRemoteAddressClass;
115
+ readonly throttled: boolean;
116
+ readonly retryAfterSeconds: number;
117
+ }
118
+ /** The public-facing identity of one resource server and its per-address failure budget. */
119
+ interface IBearerResourceServer {
120
+ /** The parsed public URL; its path is the resource's base path. */
121
+ readonly url: URL;
122
+ /**
123
+ * Host and Origin must name the public URL. On a mismatch the 403 is written and false returned;
124
+ * nothing is counted, since no credential was judged.
125
+ */
126
+ checkNames(req: IncomingMessage, res: ServerResponse): boolean;
127
+ /** Count one failure against the request's peer. */
128
+ fail(req: IncomingMessage): IBearerFailure;
129
+ /** The request's peer class without counting anything. */
130
+ remote(req: IncomingMessage): TRemoteAddressClass;
131
+ }
132
+ /** The challenges and metadata of one protected resource (one audience). */
133
+ interface IProtectedResource {
134
+ /** The RFC 9728 path clients fetch the metadata from. */
135
+ readonly wellKnownPath: string;
136
+ readonly metadata: string;
137
+ readonly challenges: {
138
+ readonly missing: string;
139
+ readonly invalid: string;
140
+ readonly scope: string;
141
+ };
142
+ }
143
+ /** Parse a resource server's public URL: plain `https`, no credentials, query or fragment. */
144
+ declare function parsePublicHttpsUrl(publicUrl: string, label: string): URL;
145
+ /**
146
+ * Build the public identity of a resource server. Throws when the public URL is not plain `https`,
147
+ * or a trusted proxy is not a literal address. `label` names the carrier in those messages.
148
+ */
149
+ declare function createBearerResourceServer(options: {
150
+ readonly publicUrl: string;
151
+ /** Proxy addresses whose `X-Forwarded-For` is believed. Absent: the header is ignored. */
152
+ readonly trustedProxies?: readonly string[];
153
+ readonly label: string;
154
+ readonly now?: () => number;
155
+ }): IBearerResourceServer;
156
+ /**
157
+ * Describe one protected resource: where its RFC 9728 metadata lives and the challenges that point
158
+ * there. Throws for a non-`https` issuer, or no scope or a scope a challenge cannot quote.
159
+ */
160
+ declare function describeProtectedResource(options: {
161
+ /** The resource identifier (`aud`), an `https` URL. */
162
+ readonly resource: string;
163
+ readonly issuer: string;
164
+ readonly scopes: readonly string[];
165
+ readonly label: string;
166
+ }): IProtectedResource;
167
+ /** Answer a metadata request: GET only, the JSON document, nothing else. */
168
+ declare function serveProtectedResourceMetadata(req: IncomingMessage, res: ServerResponse, resource: IProtectedResource): void;
169
+ /**
170
+ * Answer a refused bearer token after its failure was counted: 429 once the peer is over budget,
171
+ * 403 `insufficient_scope` for a missing scope, otherwise 401 with the missing-token or
172
+ * `invalid_token` challenge. Always an empty body.
173
+ */
174
+ declare function refuseBearerToken(res: ServerResponse, resource: IProtectedResource, refusal: 'missing-token' | 'missing-scope' | 'invalid-token', failure: IBearerFailure): void;
175
+ //#endregion
108
176
  //#region src/node/file-transfer.d.ts
109
177
  /** Why a transfer did not end with the file kept. */
110
178
  type TFileTransferRefusal =
@@ -212,5 +280,5 @@ interface IReceiveFileOptions {
212
280
  */
213
281
  declare function receiveFileOverChannel(options: IReceiveFileOptions): Promise<TFileReceiveOutcome>;
214
282
  //#endregion
215
- export { DEFAULT_MAX_FILE_BYTES, FILE_CHUNK_BYTES, FILE_CREDIT_WINDOW, type IAccessTokenVerifierDeps, type IFileSink, type IFileSource, type IIntegrityVerdict, type IReceiveFileOptions, type ISendFileOptions, type TFileAdmission, type TFileReceiveOutcome, type TFileSendOutcome, type TFileTransferRefusal, type TIntegrityFailure, bearerCredential, createAccessTokenVerifier, credentialMatches, mintTransportToken, receiveFileOverChannel, resolveAdmission, sealHandoffRecord, sendFileOverChannel, verifyHandoffPayload };
283
+ export { DEFAULT_MAX_FILE_BYTES, FILE_CHUNK_BYTES, FILE_CREDIT_WINDOW, type IAccessTokenVerifierDeps, type IBearerFailure, type IBearerResourceServer, type IFileSink, type IFileSource, type IIntegrityVerdict, type IProtectedResource, type IReceiveFileOptions, type ISendFileOptions, type TFileAdmission, type TFileReceiveOutcome, type TFileSendOutcome, type TFileTransferRefusal, type TIntegrityFailure, type TRemoteAddressClass, bearerCredential, createAccessTokenVerifier, createBearerResourceServer, credentialMatches, describeProtectedResource, mintTransportToken, parsePublicHttpsUrl, receiveFileOverChannel, refuseBearerToken, resolveAdmission, sealHandoffRecord, sendFileOverChannel, serveProtectedResourceMetadata, verifyHandoffPayload };
216
284
  //# sourceMappingURL=node.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.cts","names":[],"sources":["../../src/node/admission.ts","../../src/node/handoff-manifest.ts","../../src/node/access-token-verifier.ts","../../src/node/file-transfer.ts"],"mappings":";;;;;;;;;;;iBA4BgB;;;;;;;;;;;;;;;;;;;;;;;;iBA+BA,iBACd,SAAQ,4BAA4B,sBACnC;;;;;;;;;;;;;;;;;;;iBAoDa,kBAAkB,kBAAkB;iBA8BpC,iBAAiB;;;;;;;;;;;iBCpHjB,kBAAkB,QAAQ;EACxC;EACA,WAAW;;;KAcD;UAEK;WACN;WACA,UAAU;;WAEV;WACA;;;;;;;;;iBAUK,qBACd,oBACA,WAAW,oBACV;;;;UCCc;WACN,QAAQ;WACR,SAAS;;WAET;;WAEA;;;;;;;;;iBAwBK,0BACd,QAAQ,4BACR,OAAM,2BACL;;;;KCnES;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4BC;;;;;cAMA;;cAEA;;UAcI;WACN;EACT,QAAQ,cAAc;;;UAIP;EACf,MAAM,OAAO,aAAa;;EAE1B,UAAU;;EAEV,WAAW;;;KAID;WACG,MAAM;;WACN,SAAS;WAA+B;;KAE3C;WACG;;WACA;WAAoB,QAAQ;WAA+B;;KAE9D;WACG;WAAmB,OAAO;WAAqB;;WAE/C;WACA,QAAQ;WACR;WACA,QAAQ;;UA4NN;WACN,SAAS;WACT,OAAO;WACP,QAAQ;WACR;WACA;WACA;;WAEA;;;;;;iBAOW,oBAAoB,SAAS,mBAAmB,QAAQ;UA2D7D;WACN,SAAS;;WAET;;;;;WAKA,QAAQ,OAAO,YAAY,QAAQ,gBAAgB,QAAQ;WAC3D;WACA;;;;;;iBAOW,uBACpB,SAAS,sBACR,QAAQ"}
1
+ {"version":3,"file":"node.d.cts","names":[],"sources":["../../src/node/admission.ts","../../src/node/handoff-manifest.ts","../../src/node/access-token-verifier.ts","../../src/node/bearer-resource-gate.ts","../../src/node/file-transfer.ts"],"mappings":";;;;;;;;;;;;iBA4BgB;;;;;;;;;;;;;;;;;;;;;;;;iBA+BA,iBACd,SAAQ,4BAA4B,sBACnC;;;;;;;;;;;;;;;;;;;iBAoDa,kBAAkB,kBAAkB;iBA8BpC,iBAAiB;;;;;;;;;;;iBCpHjB,kBAAkB,QAAQ;EACxC;EACA,WAAW;;;KAcD;UAEK;WACN;WACA,UAAU;;WAEV;WACA;;;;;;;;;iBAUK,qBACd,oBACA,WAAW,oBACV;;;;UCCc;WACN,QAAQ;WACR,SAAS;;WAET;;WAEA;;;;;;;;;iBAwBK,0BACd,QAAQ,4BACR,OAAM,2BACL;;;;KC/DS;;UAGK;WACN,QAAQ;WACR;WACA;;;UAIM;;WAEN,KAAK;;;;;EAKd,WAAW,KAAK,iBAAiB,KAAK;;EAEtC,KAAK,KAAK,kBAAkB;;EAE5B,OAAO,KAAK,kBAAkB;;;UAIf;;WAEN;WACA;WACA;aACE;aACA;aACA;;;;iBAkCG,oBAAoB,mBAAmB,gBAAgB;;;;;iBAoDvD,2BAA2B;WAChC;;WAEA;WACA;WACA;IACP;;;;;iBA0DY,0BAA0B;;WAE/B;WACA;WACA;WACA;IACP;;iBAoCY,+BACd,KAAK,iBACL,KAAK,gBACL,UAAU;;;;;;iBAcI,kBACd,KAAK,gBACL,UAAU,oBACV,8DACA,SAAS;;;;KCzPC;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4BC;;;;;cAMA;;cAEA;;UAcI;WACN;EACT,QAAQ,cAAc;;;UAIP;EACf,MAAM,OAAO,aAAa;;EAE1B,UAAU;;EAEV,WAAW;;;KAID;WACG,MAAM;;WACN,SAAS;WAA+B;;KAE3C;WACG;;WACA;WAAoB,QAAQ;WAA+B;;KAE9D;WACG;WAAmB,OAAO;WAAqB;;WAE/C;WACA,QAAQ;WACR;WACA,QAAQ;;UA4NN;WACN,SAAS;WACT,OAAO;WACP,QAAQ;WACR;WACA;WACA;;WAEA;;;;;;iBAOW,oBAAoB,SAAS,mBAAmB,QAAQ;UA2D7D;WACN,SAAS;;WAET;;;;;WAKA,QAAQ,OAAO,YAAY,QAAQ,gBAAgB,QAAQ;WAC3D;WACA;;;;;;iBAOW,uBACpB,SAAS,sBACR,QAAQ"}
@@ -1,7 +1,8 @@
1
- import { IEgressDeps } from "@robota-sdk/agent-core/node";
2
1
  import { IInteractiveSessionRecord } from "@robota-sdk/agent-interface-session";
2
+ import { IEgressDeps } from "@robota-sdk/agent-core/node";
3
3
  import { IAccessTokenVerifier, IAccessTokenVerifierConfig, ITransportAdmission, ITransportAdmissionConfig } from "@robota-sdk/agent-interface-transport";
4
4
  import { IFileFrameChannel, IFileOffer, IHandoffIntegrity } from "@robota-sdk/agent-interface-session-mobility";
5
+ import { IncomingMessage, ServerResponse } from "node:http";
5
6
  //#region src/node/admission.d.ts
6
7
  /**
7
8
  * Mint a per-launch credential.
@@ -105,6 +106,73 @@ interface IAccessTokenVerifierDeps {
105
106
  */
106
107
  declare function createAccessTokenVerifier(config: IAccessTokenVerifierConfig, deps?: IAccessTokenVerifierDeps): IAccessTokenVerifier;
107
108
  //#endregion
109
+ //#region src/node/bearer-resource-gate.d.ts
110
+ /** A coarse class of a peer's address; the address itself is never reported. */
111
+ type TRemoteAddressClass = 'loopback' | 'private' | 'public' | 'unknown';
112
+ /** Where a refused request came from, and whether its address is over the failure budget. */
113
+ interface IBearerFailure {
114
+ readonly remote: TRemoteAddressClass;
115
+ readonly throttled: boolean;
116
+ readonly retryAfterSeconds: number;
117
+ }
118
+ /** The public-facing identity of one resource server and its per-address failure budget. */
119
+ interface IBearerResourceServer {
120
+ /** The parsed public URL; its path is the resource's base path. */
121
+ readonly url: URL;
122
+ /**
123
+ * Host and Origin must name the public URL. On a mismatch the 403 is written and false returned;
124
+ * nothing is counted, since no credential was judged.
125
+ */
126
+ checkNames(req: IncomingMessage, res: ServerResponse): boolean;
127
+ /** Count one failure against the request's peer. */
128
+ fail(req: IncomingMessage): IBearerFailure;
129
+ /** The request's peer class without counting anything. */
130
+ remote(req: IncomingMessage): TRemoteAddressClass;
131
+ }
132
+ /** The challenges and metadata of one protected resource (one audience). */
133
+ interface IProtectedResource {
134
+ /** The RFC 9728 path clients fetch the metadata from. */
135
+ readonly wellKnownPath: string;
136
+ readonly metadata: string;
137
+ readonly challenges: {
138
+ readonly missing: string;
139
+ readonly invalid: string;
140
+ readonly scope: string;
141
+ };
142
+ }
143
+ /** Parse a resource server's public URL: plain `https`, no credentials, query or fragment. */
144
+ declare function parsePublicHttpsUrl(publicUrl: string, label: string): URL;
145
+ /**
146
+ * Build the public identity of a resource server. Throws when the public URL is not plain `https`,
147
+ * or a trusted proxy is not a literal address. `label` names the carrier in those messages.
148
+ */
149
+ declare function createBearerResourceServer(options: {
150
+ readonly publicUrl: string;
151
+ /** Proxy addresses whose `X-Forwarded-For` is believed. Absent: the header is ignored. */
152
+ readonly trustedProxies?: readonly string[];
153
+ readonly label: string;
154
+ readonly now?: () => number;
155
+ }): IBearerResourceServer;
156
+ /**
157
+ * Describe one protected resource: where its RFC 9728 metadata lives and the challenges that point
158
+ * there. Throws for a non-`https` issuer, or no scope or a scope a challenge cannot quote.
159
+ */
160
+ declare function describeProtectedResource(options: {
161
+ /** The resource identifier (`aud`), an `https` URL. */
162
+ readonly resource: string;
163
+ readonly issuer: string;
164
+ readonly scopes: readonly string[];
165
+ readonly label: string;
166
+ }): IProtectedResource;
167
+ /** Answer a metadata request: GET only, the JSON document, nothing else. */
168
+ declare function serveProtectedResourceMetadata(req: IncomingMessage, res: ServerResponse, resource: IProtectedResource): void;
169
+ /**
170
+ * Answer a refused bearer token after its failure was counted: 429 once the peer is over budget,
171
+ * 403 `insufficient_scope` for a missing scope, otherwise 401 with the missing-token or
172
+ * `invalid_token` challenge. Always an empty body.
173
+ */
174
+ declare function refuseBearerToken(res: ServerResponse, resource: IProtectedResource, refusal: 'missing-token' | 'missing-scope' | 'invalid-token', failure: IBearerFailure): void;
175
+ //#endregion
108
176
  //#region src/node/file-transfer.d.ts
109
177
  /** Why a transfer did not end with the file kept. */
110
178
  type TFileTransferRefusal =
@@ -212,5 +280,5 @@ interface IReceiveFileOptions {
212
280
  */
213
281
  declare function receiveFileOverChannel(options: IReceiveFileOptions): Promise<TFileReceiveOutcome>;
214
282
  //#endregion
215
- export { DEFAULT_MAX_FILE_BYTES, FILE_CHUNK_BYTES, FILE_CREDIT_WINDOW, type IAccessTokenVerifierDeps, type IFileSink, type IFileSource, type IIntegrityVerdict, type IReceiveFileOptions, type ISendFileOptions, type TFileAdmission, type TFileReceiveOutcome, type TFileSendOutcome, type TFileTransferRefusal, type TIntegrityFailure, bearerCredential, createAccessTokenVerifier, credentialMatches, mintTransportToken, receiveFileOverChannel, resolveAdmission, sealHandoffRecord, sendFileOverChannel, verifyHandoffPayload };
283
+ export { DEFAULT_MAX_FILE_BYTES, FILE_CHUNK_BYTES, FILE_CREDIT_WINDOW, type IAccessTokenVerifierDeps, type IBearerFailure, type IBearerResourceServer, type IFileSink, type IFileSource, type IIntegrityVerdict, type IProtectedResource, type IReceiveFileOptions, type ISendFileOptions, type TFileAdmission, type TFileReceiveOutcome, type TFileSendOutcome, type TFileTransferRefusal, type TIntegrityFailure, type TRemoteAddressClass, bearerCredential, createAccessTokenVerifier, createBearerResourceServer, credentialMatches, describeProtectedResource, mintTransportToken, parsePublicHttpsUrl, receiveFileOverChannel, refuseBearerToken, resolveAdmission, sealHandoffRecord, sendFileOverChannel, serveProtectedResourceMetadata, verifyHandoffPayload };
216
284
  //# sourceMappingURL=node.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"node.d.ts","names":[],"sources":["../../src/node/admission.ts","../../src/node/handoff-manifest.ts","../../src/node/access-token-verifier.ts","../../src/node/file-transfer.ts"],"mappings":";;;;;;;;;;;iBA4BgB;;;;;;;;;;;;;;;;;;;;;;;;iBA+BA,iBACd,SAAQ,4BAA4B,sBACnC;;;;;;;;;;;;;;;;;;;iBAoDa,kBAAkB,kBAAkB;iBA8BpC,iBAAiB;;;;;;;;;;;iBCpHjB,kBAAkB,QAAQ;EACxC;EACA,WAAW;;;KAcD;UAEK;WACN;WACA,UAAU;;WAEV;WACA;;;;;;;;;iBAUK,qBACd,oBACA,WAAW,oBACV;;;;UCCc;WACN,QAAQ;WACR,SAAS;;WAET;;WAEA;;;;;;;;;iBAwBK,0BACd,QAAQ,4BACR,OAAM,2BACL;;;;KCnES;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4BC;;;;;cAMA;;cAEA;;UAcI;WACN;EACT,QAAQ,cAAc;;;UAIP;EACf,MAAM,OAAO,aAAa;;EAE1B,UAAU;;EAEV,WAAW;;;KAID;WACG,MAAM;;WACN,SAAS;WAA+B;;KAE3C;WACG;;WACA;WAAoB,QAAQ;WAA+B;;KAE9D;WACG;WAAmB,OAAO;WAAqB;;WAE/C;WACA,QAAQ;WACR;WACA,QAAQ;;UA4NN;WACN,SAAS;WACT,OAAO;WACP,QAAQ;WACR;WACA;WACA;;WAEA;;;;;;iBAOW,oBAAoB,SAAS,mBAAmB,QAAQ;UA2D7D;WACN,SAAS;;WAET;;;;;WAKA,QAAQ,OAAO,YAAY,QAAQ,gBAAgB,QAAQ;WAC3D;WACA;;;;;;iBAOW,uBACpB,SAAS,sBACR,QAAQ"}
1
+ {"version":3,"file":"node.d.ts","names":[],"sources":["../../src/node/admission.ts","../../src/node/handoff-manifest.ts","../../src/node/access-token-verifier.ts","../../src/node/bearer-resource-gate.ts","../../src/node/file-transfer.ts"],"mappings":";;;;;;;;;;;;iBA4BgB;;;;;;;;;;;;;;;;;;;;;;;;iBA+BA,iBACd,SAAQ,4BAA4B,sBACnC;;;;;;;;;;;;;;;;;;;iBAoDa,kBAAkB,kBAAkB;iBA8BpC,iBAAiB;;;;;;;;;;;iBCpHjB,kBAAkB,QAAQ;EACxC;EACA,WAAW;;;KAcD;UAEK;WACN;WACA,UAAU;;WAEV;WACA;;;;;;;;;iBAUK,qBACd,oBACA,WAAW,oBACV;;;;UCCc;WACN,QAAQ;WACR,SAAS;;WAET;;WAEA;;;;;;;;;iBAwBK,0BACd,QAAQ,4BACR,OAAM,2BACL;;;;KC/DS;;UAGK;WACN,QAAQ;WACR;WACA;;;UAIM;;WAEN,KAAK;;;;;EAKd,WAAW,KAAK,iBAAiB,KAAK;;EAEtC,KAAK,KAAK,kBAAkB;;EAE5B,OAAO,KAAK,kBAAkB;;;UAIf;;WAEN;WACA;WACA;aACE;aACA;aACA;;;;iBAkCG,oBAAoB,mBAAmB,gBAAgB;;;;;iBAoDvD,2BAA2B;WAChC;;WAEA;WACA;WACA;IACP;;;;;iBA0DY,0BAA0B;;WAE/B;WACA;WACA;WACA;IACP;;iBAoCY,+BACd,KAAK,iBACL,KAAK,gBACL,UAAU;;;;;;iBAcI,kBACd,KAAK,gBACL,UAAU,oBACV,8DACA,SAAS;;;;KCzPC;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4BC;;;;;cAMA;;cAEA;;UAcI;WACN;EACT,QAAQ,cAAc;;;UAIP;EACf,MAAM,OAAO,aAAa;;EAE1B,UAAU;;EAEV,WAAW;;;KAID;WACG,MAAM;;WACN,SAAS;WAA+B;;KAE3C;WACG;;WACA;WAAoB,QAAQ;WAA+B;;KAE9D;WACG;WAAmB,OAAO;WAAqB;;WAE/C;WACA,QAAQ;WACR;WACA,QAAQ;;UA4NN;WACN,SAAS;WACT,OAAO;WACP,QAAQ;WACR;WACA;WACA;;WAEA;;;;;;iBAOW,oBAAoB,SAAS,mBAAmB,QAAQ;UA2D7D;WACN,SAAS;;WAET;;;;;WAKA,QAAQ,OAAO,YAAY,QAAQ,gBAAgB,QAAQ;WAC3D;WACA;;;;;;iBAOW,uBACpB,SAAS,sBACR,QAAQ"}
package/dist/node/node.js CHANGED
@@ -1,2 +1,2 @@
1
- import{createHash as e,randomBytes as t,timingSafeEqual as n}from"node:crypto";import{decodeProtectedHeader as r,importJWK as i,jwtVerify as a}from"jose";import{fetchWithEgressPolicy as o}from"@robota-sdk/agent-core/node";function s(){let e=t(32).toString(`hex`);if(e.length!==64)throw Error(`transport admission: could not mint a credential of the required length`);return e}function c(e={}){let t=`open`in e&&e.open===!0,n=e.token!==void 0&&e.token!==null&&e.token!==``;if(n&&t)throw Error("transport admission: a token and `open: true` are contradictory — one requires a credential and the other requires none. Asking for both means one of the two beliefs behind the call is wrong; say which is intended.");if(n)return{token:e.token};if(e.token===null||t){let t=e.openReason?.trim();if(!t)throw Error("transport admission: an open transport requires `openReason`. An open transport is a decision, and a decision nobody wrote down is indistinguishable from an oversight.");return{token:null,openReason:t}}return{token:s()}}function l(e,t){if(t===void 0)return!1;let r=Buffer.from(e,`utf8`),i=Buffer.from(t,`utf8`);return r.length===i.length&&n(r,i)}function u(e){if(!e||e.slice(0,6).toLowerCase()!==`bearer`)return;let t=e.slice(6);if(t===``||t[0]!==` `&&t[0]!==` `)return;let n=t.replace(/^[ \t]+/,``);return n===``?void 0:n}function d(t){let n=JSON.stringify(t),r=Buffer.from(n,`utf8`);return{serialized:n,integrity:{digest:e(`sha256`).update(r).digest(`base64url`),byteLength:r.byteLength}}}function f(t,n){let r=Buffer.from(t,`utf8`);return r.byteLength===n.byteLength?e(`sha256`).update(r).digest(`base64url`)===n.digest?{intact:!0}:{intact:!1,failure:`digest-mismatch`}:{intact:!1,failure:`truncated`,expectedBytes:n.byteLength,actualBytes:r.byteLength}}const p=10*6e4,m=`at+jwt`,h=[`RS256`,`ES256`,`EdDSA`],g={RS256:{kty:`RSA`},ES256:{kty:`EC`,crv:`P-256`},EdDSA:{kty:`OKP`,crv:`Ed25519`}};var _=class{refusal;constructor(e){this.refusal=e}};function v(e,t={}){let n=y(e),s=t.now??Date.now,c=t.monotonicNow??(()=>performance.now()),l={fetch:t.fetch,lookup:t.lookup},u=n.hostname.replace(/^\[|\]$/g,``).toLowerCase(),d={keys:void 0,jwksUri:void 0,fetchedAt:void 0,lastAttemptAt:void 0,inflight:void 0},f=new Set(e.allowedSubjects??[]),h=new Set(e.allowedClients??[]);async function g(e){if(new URL(e).protocol!==`https:`)return;let t=await o(e,{headers:{accept:`application/json`},timeoutMs:1e4,maxResponseBytes:65536},{allowedHosts:[u],maxRedirects:0},l);if(!(!t.ok||t.status!==200))return JSON.parse(new TextDecoder().decode(t.body))}async function v(){for(let t of b(n)){let n=await g(t).catch(()=>void 0);if(!T(n))continue;if(n.issuer!==e.issuer)return;let r=n.jwks_uri;return typeof r!=`string`||!URL.canParse(r)?void 0:new URL(r).protocol===`https:`?r:void 0}}function E(){return d.fetchedAt===void 0?1/0:c()-d.fetchedAt}async function D(){d.lastAttemptAt=c();try{if(E()>=p&&(d.jwksUri=void 0),d.jwksUri??=await v(),d.jwksUri===void 0)return;let e=x(await g(d.jwksUri));if(e===void 0)return;d.keys=e,d.fetchedAt=c()}catch{}}async function O(){if(d.inflight!==void 0)return d.inflight;if(!(d.lastAttemptAt!==void 0&&c()-d.lastAttemptAt<3e4))return d.inflight=D().finally(()=>{d.inflight=void 0}),d.inflight}async function k(){return E()>=p&&await O(),E()>=36e5&&(d.keys=void 0,d.jwksUri=void 0,d.fetchedAt=void 0),d.keys}async function A(e,t){let n=await k();if(n===void 0)throw new _(`keys-unavailable`);if(e===void 0){if(n.length>1)throw new _(`ambiguous-key`);let e=n[0];if(e===void 0)throw new _(`unknown-key`);if(!S(e,t))throw new _(`key-mismatch`);return e}let r=n.filter(t=>t.kid===e);if(r.length===0&&(await O(),n=d.keys??[],r=n.filter(t=>t.kid===e)),r.length===0)throw new _(`unknown-key`);let i=r.find(e=>S(e,t));if(i===void 0)throw new _(`key-mismatch`);return i}async function j(t){if(typeof t!=`string`||t.length===0)throw new _(`malformed`);if(t.length>8192)throw new _(`oversize`);let n;try{n=r(t)}catch{throw new _(`malformed`)}if(typeof n.typ!=`string`||w(n.typ)!==m)throw new _(`wrong-type`);let o=e.algorithms.find(e=>e===n.alg);if(o===void 0)throw new _(`unsupported-algorithm`);if(n.kid!==void 0&&typeof n.kid!=`string`)throw new _(`malformed`);let c=await A(n.kid,o),l;try{l=await i(c,o)}catch{throw new _(`key-mismatch`)}let u;try{({payload:u}=await a(t,l,{issuer:e.issuer,audience:e.resource,algorithms:[o],typ:m,clockTolerance:60,currentDate:new Date(s()),requiredClaims:[`exp`,`sub`,`client_id`]}))}catch(e){throw new _(C(e))}let d=typeof u.scope==`string`?u.scope.split(` `):[];if(!e.requiredScopes.every(e=>d.includes(e)))throw new _(`missing-scope`);let p=u.sub,g=u.client_id;if(!(typeof p==`string`&&f.has(p)||typeof g==`string`&&h.has(g)))throw new _(`principal-not-allowed`);return{admitted:!0}}return{async verify(e){try{return await j(e)}catch(e){return{admitted:!1,refusal:e instanceof _?e.refusal:`malformed`}}}}}function y(e){if(!URL.canParse(e.issuer))throw Error(`access-token verifier: issuer is not a URL`);let t=new URL(e.issuer);if(t.protocol!==`https:`||t.search!==``||t.hash!==``)throw Error(`access-token verifier: issuer must be an https URL with no query or fragment`);if(!URL.canParse(e.resource))throw Error(`access-token verifier: resource is not a URL`);if(e.algorithms.length===0||!e.algorithms.every(e=>h.includes(e)))throw Error(`access-token verifier: algorithms must be a non-empty subset of RS256, ES256, EdDSA`);if(e.requiredScopes.length===0)throw Error(`access-token verifier: at least one required scope is needed`);if((e.allowedSubjects?.length??0)+(e.allowedClients?.length??0)===0)throw Error(`access-token verifier: name at least one allowed subject or client — this host is single-tenant, and issuer + audience + scope alone admit anyone the issuer serves`);return t}function b(e){let t=e.pathname===`/`?``:e.pathname.replace(/\/$/,``);return[`${e.origin}/.well-known/oauth-authorization-server${t}`,`${e.origin}${t}/.well-known/openid-configuration`]}function x(e){if(!(!T(e)||!Array.isArray(e.keys)||e.keys.length>32))return e.keys.filter(e=>T(e)&&typeof e.kty==`string`)}function S(e,t){let n=g[t];return e.kty!==n.kty||n.crv!==void 0&&e.crv!==n.crv||e.alg!==void 0&&e.alg!==t||e.use!==void 0&&e.use!==`sig`||e.key_ops!==void 0&&!e.key_ops.includes(`verify`)?!1:e.d===void 0}function C(e){let t=T(e)?e.code:void 0,n=T(e)?e.claim:void 0;switch(t){case`ERR_JWT_EXPIRED`:return`expired`;case`ERR_JWS_SIGNATURE_VERIFICATION_FAILED`:return`bad-signature`;case`ERR_JOSE_ALG_NOT_ALLOWED`:return`unsupported-algorithm`;case`ERR_JWT_CLAIM_VALIDATION_FAILED`:return n===`iss`?`wrong-issuer`:n===`aud`?`wrong-audience`:n===`nbf`?`not-yet-valid`:n===`typ`?`wrong-type`:`malformed`;default:return`malformed`}}function w(e){let t=e.toLowerCase();return t.startsWith(`application/`)?t.slice(12):t}function T(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}const E=32*1024*1024,D=16*1024,O=16,k=/^[0-9a-f]{64}$/,A=/^[A-Za-z0-9+/]*={0,2}$/,j=3e4,M=new Set([`declined`,`too-large`,`bad-name`,`exists`,`unsafe-path`,`integrity`,`protocol`,`closed`,`timeout`,`unavailable`]),N=[[0,31],[127,159],[8203,8207],[8232,8238],[8294,8297],[65279,65279]];function P(e){let t=``;for(let n of e){let e=n.codePointAt(0)??0;t+=N.some(([t,n])=>e>=t&&e<=n)?` `:n}return t}function F(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=0}function I(e){if(e.length>22360)return;let t;try{t=JSON.parse(e)}catch{return}if(typeof t!=`object`||!t)return;let n=t;switch(n.t){case`file-offer`:{let{transferId:e,name:t,size:r,sha256:i}=n;return typeof e!=`string`||e.length===0||e.length>128||typeof t!=`string`||t.length>1024||!F(r)||typeof i!=`string`||!k.test(i)?void 0:{t:`file-offer`,transferId:e,name:t,size:r,sha256:i}}case`file-accept`:return{t:`file-accept`};case`file-refuse`:{let e=n.reason,t=n.detail;return typeof e!=`string`||!M.has(e)?void 0:{t:`file-refuse`,reason:e,...typeof t==`string`?{detail:P(t).slice(0,500)}:{}}}case`file-chunk`:{let{seq:e,data:t}=n;return!F(e)||typeof t!=`string`||t===``||!A.test(t)?void 0:{t:`file-chunk`,seq:e,data:t}}case`file-credit`:return F(n.upTo)?{t:`file-credit`,upTo:n.upTo}:void 0;case`file-end`:return F(n.chunks)?{t:`file-end`,chunks:n.chunks}:void 0;case`file-done`:return{t:`file-done`};default:return}}var L=class extends Error{reason;detail;constructor(e,t){super(t??e),this.reason=e,this.detail=t}},R=class{capacity;queue=[];waiter;failure;stops=[];constructor(e,t){this.capacity=t,this.stops.push(e.onFrame(e=>{let t=I(e);if(t===void 0){this.fail(new L(`protocol`,`a frame could not be read`));return}if(this.waiter!==void 0){let{resolve:e}=this.waiter;this.waiter=void 0,e(t);return}if(this.queue.length>=this.capacity){this.fail(new L(`protocol`,`the peer sent past its credit`));return}this.queue.push(t)}),e.onClose(()=>this.fail(new L(`closed`,`the channel closed`))))}fail(e){if(this.failure!==void 0)return;this.failure=e,e.reason===`protocol`&&(this.queue.length=0);let t=this.waiter;this.waiter=void 0,t?.reject(e)}next(e){let t=this.queue.shift();return t===void 0?this.failure===void 0?new Promise((t,n)=>{let r=setTimeout(()=>{this.waiter=void 0,n(new L(`timeout`,`nothing arrived within ${e}ms`))},e);r.unref?.(),this.waiter={resolve:e=>{clearTimeout(r),t(e)},reject:e=>{clearTimeout(r),n(e)}}}):Promise.reject(this.failure):Promise.resolve(t)}get ended(){return this.failure!==void 0}stop(){for(let e of this.stops.splice(0))e()}};function z(e,t){try{e.send(JSON.stringify(t))}catch(e){throw new L(`closed`,e instanceof Error?e.message:String(e))}}function B(e){return e instanceof L?e:new L(`unavailable`,e instanceof Error?e.message:String(e))}async function*V(e,t){let n=Buffer.alloc(0);for await(let r of e)for(n=n.length===0?Buffer.from(r):Buffer.concat([n,r]);n.length>=t;)yield n.subarray(0,t),n=n.subarray(t);n.length>0&&(yield n)}async function H(e){let{channel:t,offer:n,source:r}=e,i=e.chunkBytes??16384,a=e.window??16,o=e.idleMs??j,s=new R(t,a+2);try{if(r.size!==n.size)throw new L(`integrity`,`the source is not the size offered`);z(t,{t:`file-offer`,...n});let c=await s.next(e.decisionMs??3e5);if(c.t===`file-refuse`)throw new L(c.reason,c.detail);if(c.t!==`file-accept`)throw new L(`protocol`,`expected an answer`);let l=0,u=0,d=0,f=async()=>{let e=await s.next(o);if(e.t===`file-refuse`)throw new L(e.reason,e.detail);if(e.t!==`file-credit`||e.upTo<=u||e.upTo>l)throw new L(`protocol`,`expected credit`);u=e.upTo};for await(let e of V(r.read(),i)){if(d+=e.length,d>n.size)throw new L(`integrity`,`the source grew while sending`);for(;l-u>=a;)await f();z(t,{t:`file-chunk`,seq:l,data:e.toString(`base64`)}),l+=1}if(d!==n.size)throw new L(`integrity`,`the source shrank while sending`);for(z(t,{t:`file-end`,chunks:l});;){let e=await s.next(o);if(e.t===`file-done`)return{ok:!0};if(e.t===`file-refuse`)throw new L(e.reason,e.detail);if(e.t!==`file-credit`||e.upTo<=u||e.upTo>l)throw new L(`protocol`,`expected credit or completion`);u=e.upTo}}catch(e){let t=B(e);return{ok:!1,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail}}}finally{s.stop(),t.close()}}async function U(t){let{channel:n}=t,r=t.maxBytes??33554432,i=t.window??16,a=t.idleMs??j,o=new R(n,i+1),s=new AbortController,c=n.onClose(()=>s.abort()),l,u,d=!0;try{let i=await o.next(a);if(i.t!==`file-offer`)throw new L(`protocol`,`expected an offer`);if(l={transferId:i.transferId,name:i.name,size:i.size,sha256:i.sha256},l.size>r)throw new L(`too-large`,`${l.size} bytes is over the ${r}-byte limit`);let c=await t.admit(l,s.signal);if(`refused`in c)throw new L(c.refused,c.detail);if(u=c.sink,o.ended)throw new L(`closed`,`the sender left before the answer`);z(n,{t:`file-accept`});let d=e(`sha256`),f=0,p=0;for(;;){let e=await o.next(a);if(e.t===`file-end`){if(e.chunks!==f||p!==l.size)throw new L(`integrity`,`the content is not the size offered`);if(d.digest(`hex`)!==l.sha256)throw new L(`integrity`,`the content does not match the offered sha256`);let t=await u.commit();return u=void 0,z(n,{t:`file-done`}),{ok:!0,offer:l,location:t}}if(e.t!==`file-chunk`||e.seq!==f)throw new L(`protocol`,`expected the next chunk`);let t=Buffer.from(e.data,`base64`);if(p+=t.length,p>l.size)throw new L(`integrity`,`more content than offered`);d.update(t),await u.write(t),f+=1,z(n,{t:`file-credit`,upTo:f})}}catch(e){let t=B(e);if(d=t.reason!==`protocol`,u!==void 0&&await u.discard().catch(()=>void 0),t.reason!==`closed`&&t.reason!==`timeout`)try{z(n,{t:`file-refuse`,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail}})}catch{}return{ok:!1,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail},...l===void 0?{}:{offer:l}}}finally{d&&await W(s.signal,5e3),c(),o.stop(),n.close()}}function W(e,t){return e.aborted?Promise.resolve():new Promise(n=>{let r=setTimeout(i,t);r.unref?.(),e.addEventListener(`abort`,i,{once:!0});function i(){clearTimeout(r),e.removeEventListener(`abort`,i),n()}})}export{E as DEFAULT_MAX_FILE_BYTES,D as FILE_CHUNK_BYTES,O as FILE_CREDIT_WINDOW,u as bearerCredential,v as createAccessTokenVerifier,l as credentialMatches,s as mintTransportToken,U as receiveFileOverChannel,c as resolveAdmission,d as sealHandoffRecord,H as sendFileOverChannel,f as verifyHandoffPayload};
1
+ import{createHash as e,randomBytes as t,timingSafeEqual as n}from"node:crypto";import{decodeProtectedHeader as r,importJWK as i,jwtVerify as a}from"jose";import{fetchWithEgressPolicy as o}from"@robota-sdk/agent-core/node";import{BlockList as s,isIP as c}from"node:net";function l(){let e=t(32).toString(`hex`);if(e.length!==64)throw Error(`transport admission: could not mint a credential of the required length`);return e}function u(e={}){let t=`open`in e&&e.open===!0,n=e.token!==void 0&&e.token!==null&&e.token!==``;if(n&&t)throw Error("transport admission: a token and `open: true` are contradictory — one requires a credential and the other requires none. Asking for both means one of the two beliefs behind the call is wrong; say which is intended.");if(n)return{token:e.token};if(e.token===null||t){let t=e.openReason?.trim();if(!t)throw Error("transport admission: an open transport requires `openReason`. An open transport is a decision, and a decision nobody wrote down is indistinguishable from an oversight.");return{token:null,openReason:t}}return{token:l()}}function d(e,t){if(t===void 0)return!1;let r=Buffer.from(e,`utf8`),i=Buffer.from(t,`utf8`);return r.length===i.length&&n(r,i)}function f(e){if(!e||e.slice(0,6).toLowerCase()!==`bearer`)return;let t=e.slice(6);if(t===``||t[0]!==` `&&t[0]!==` `)return;let n=t.replace(/^[ \t]+/,``);return n===``?void 0:n}function p(t){let n=JSON.stringify(t),r=Buffer.from(n,`utf8`);return{serialized:n,integrity:{digest:e(`sha256`).update(r).digest(`base64url`),byteLength:r.byteLength}}}function m(t,n){let r=Buffer.from(t,`utf8`);return r.byteLength===n.byteLength?e(`sha256`).update(r).digest(`base64url`)===n.digest?{intact:!0}:{intact:!1,failure:`digest-mismatch`}:{intact:!1,failure:`truncated`,expectedBytes:n.byteLength,actualBytes:r.byteLength}}const h=10*6e4,g=`at+jwt`,_=[`RS256`,`ES256`,`EdDSA`],v={RS256:{kty:`RSA`},ES256:{kty:`EC`,crv:`P-256`},EdDSA:{kty:`OKP`,crv:`Ed25519`}};var y=class{refusal;constructor(e){this.refusal=e}};function b(e,t={}){let n=ee(e),s=t.now??Date.now,c=t.monotonicNow??(()=>performance.now()),l={fetch:t.fetch,lookup:t.lookup},u=n.hostname.replace(/^\[|\]$/g,``).toLowerCase(),d={keys:void 0,jwksUri:void 0,fetchedAt:void 0,lastAttemptAt:void 0,inflight:void 0},f=new Set(e.allowedSubjects??[]),p=new Set(e.allowedClients??[]);async function m(e){if(new URL(e).protocol!==`https:`)return;let t=await o(e,{headers:{accept:`application/json`},timeoutMs:1e4,maxResponseBytes:65536},{allowedHosts:[u],maxRedirects:0},l);if(!(!t.ok||t.status!==200))return JSON.parse(new TextDecoder().decode(t.body))}async function _(){for(let t of x(n)){let n=await m(t).catch(()=>void 0);if(!E(n))continue;if(n.issuer!==e.issuer)return;let r=n.jwks_uri;return typeof r!=`string`||!URL.canParse(r)?void 0:new URL(r).protocol===`https:`?r:void 0}}function v(){return d.fetchedAt===void 0?1/0:c()-d.fetchedAt}async function b(){d.lastAttemptAt=c();try{if(v()>=h&&(d.jwksUri=void 0),d.jwksUri??=await _(),d.jwksUri===void 0)return;let e=S(await m(d.jwksUri));if(e===void 0)return;d.keys=e,d.fetchedAt=c()}catch{}}async function D(){if(d.inflight!==void 0)return d.inflight;if(!(d.lastAttemptAt!==void 0&&c()-d.lastAttemptAt<3e4))return d.inflight=b().finally(()=>{d.inflight=void 0}),d.inflight}async function O(){return v()>=h&&await D(),v()>=36e5&&(d.keys=void 0,d.jwksUri=void 0,d.fetchedAt=void 0),d.keys}async function k(e,t){let n=await O();if(n===void 0)throw new y(`keys-unavailable`);if(e===void 0){if(n.length>1)throw new y(`ambiguous-key`);let e=n[0];if(e===void 0)throw new y(`unknown-key`);if(!C(e,t))throw new y(`key-mismatch`);return e}let r=n.filter(t=>t.kid===e);if(r.length===0&&(await D(),n=d.keys??[],r=n.filter(t=>t.kid===e)),r.length===0)throw new y(`unknown-key`);let i=r.find(e=>C(e,t));if(i===void 0)throw new y(`key-mismatch`);return i}async function A(t){if(typeof t!=`string`||t.length===0)throw new y(`malformed`);if(t.length>8192)throw new y(`oversize`);let n;try{n=r(t)}catch{throw new y(`malformed`)}if(typeof n.typ!=`string`||T(n.typ)!==g)throw new y(`wrong-type`);let o=e.algorithms.find(e=>e===n.alg);if(o===void 0)throw new y(`unsupported-algorithm`);if(n.kid!==void 0&&typeof n.kid!=`string`)throw new y(`malformed`);let c=await k(n.kid,o),l;try{l=await i(c,o)}catch{throw new y(`key-mismatch`)}let u;try{({payload:u}=await a(t,l,{issuer:e.issuer,audience:e.resource,algorithms:[o],typ:g,clockTolerance:60,currentDate:new Date(s()),requiredClaims:[`exp`,`sub`,`client_id`]}))}catch(e){throw new y(w(e))}let d=typeof u.scope==`string`?u.scope.split(` `):[];if(!e.requiredScopes.every(e=>d.includes(e)))throw new y(`missing-scope`);let m=u.sub,h=u.client_id;if(!(typeof m==`string`&&f.has(m)||typeof h==`string`&&p.has(h)))throw new y(`principal-not-allowed`);return{admitted:!0}}return{async verify(e){try{return await A(e)}catch(e){return{admitted:!1,refusal:e instanceof y?e.refusal:`malformed`}}}}}function ee(e){if(!URL.canParse(e.issuer))throw Error(`access-token verifier: issuer is not a URL`);let t=new URL(e.issuer);if(t.protocol!==`https:`||t.search!==``||t.hash!==``)throw Error(`access-token verifier: issuer must be an https URL with no query or fragment`);if(!URL.canParse(e.resource))throw Error(`access-token verifier: resource is not a URL`);if(e.algorithms.length===0||!e.algorithms.every(e=>_.includes(e)))throw Error(`access-token verifier: algorithms must be a non-empty subset of RS256, ES256, EdDSA`);if(e.requiredScopes.length===0)throw Error(`access-token verifier: at least one required scope is needed`);if((e.allowedSubjects?.length??0)+(e.allowedClients?.length??0)===0)throw Error(`access-token verifier: name at least one allowed subject or client — this host is single-tenant, and issuer + audience + scope alone admit anyone the issuer serves`);return t}function x(e){let t=e.pathname===`/`?``:e.pathname.replace(/\/$/,``);return[`${e.origin}/.well-known/oauth-authorization-server${t}`,`${e.origin}${t}/.well-known/openid-configuration`]}function S(e){if(!(!E(e)||!Array.isArray(e.keys)||e.keys.length>32))return e.keys.filter(e=>E(e)&&typeof e.kty==`string`)}function C(e,t){let n=v[t];return e.kty!==n.kty||n.crv!==void 0&&e.crv!==n.crv||e.alg!==void 0&&e.alg!==t||e.use!==void 0&&e.use!==`sig`||e.key_ops!==void 0&&!e.key_ops.includes(`verify`)?!1:e.d===void 0}function w(e){let t=E(e)?e.code:void 0,n=E(e)?e.claim:void 0;switch(t){case`ERR_JWT_EXPIRED`:return`expired`;case`ERR_JWS_SIGNATURE_VERIFICATION_FAILED`:return`bad-signature`;case`ERR_JOSE_ALG_NOT_ALLOWED`:return`unsupported-algorithm`;case`ERR_JWT_CLAIM_VALIDATION_FAILED`:return n===`iss`?`wrong-issuer`:n===`aud`?`wrong-audience`:n===`nbf`?`not-yet-valid`:n===`typ`?`wrong-type`:`malformed`;default:return`malformed`}}function T(e){let t=e.toLowerCase();return t.startsWith(`application/`)?t.slice(12):t}function E(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}const D=6e4,O=4096,k=/^[\x21\x23-\x5b\x5d-\x7e]+$/,A=new s;A.addSubnet(`127.0.0.0`,8,`ipv4`),A.addAddress(`::1`,`ipv6`);const j=new s;j.addSubnet(`10.0.0.0`,8,`ipv4`),j.addSubnet(`172.16.0.0`,12,`ipv4`),j.addSubnet(`192.168.0.0`,16,`ipv4`),j.addSubnet(`169.254.0.0`,16,`ipv4`),j.addSubnet(`100.64.0.0`,10,`ipv4`),j.addSubnet(`fc00::`,7,`ipv6`),j.addSubnet(`fe80::`,10,`ipv6`);function M(e){if(e===void 0)return;let t=e.trim().toLowerCase(),n=t.startsWith(`::ffff:`)?t.slice(7):void 0;return n!==void 0&&c(n)===4?n:c(t)===0?void 0:t}function N(e){if(e===void 0)return`unknown`;let t=c(e)===4?`ipv4`:`ipv6`;return A.check(e,t)?`loopback`:j.check(e,t)?`private`:`public`}function P(e,t){let n;try{n=new URL(e)}catch{throw Error(`${t} public URL must be an absolute https URL`)}if(n.protocol!==`https:`)throw Error(`${t} public URL must use https`);if(n.username!==``||n.password!==``||n.search!==``||n.hash!==``)throw Error(`${t} public URL must not carry credentials, a query or a fragment`);return n}function F(e){let t=new Map;return n=>{let r=e(),i=t.get(n);if(i===void 0||r-i.startedAt>=D){if(t.delete(n),t.size>=O){for(let[e,n]of t)r-n.startedAt>=D&&t.delete(e);let e=t.keys().next();t.size>=O&&e.done!==!0&&t.delete(e.value)}i={count:0,startedAt:r},t.set(n,i)}i.count+=1;let a=D-(r-i.startedAt);return{throttled:i.count>20,retryAfterSeconds:Math.max(1,Math.ceil(a/1e3))}}}function I(e){let t=P(e.publicUrl,e.label),n=new Set;for(let t of e.trustedProxies??[]){let r=M(t);if(r===void 0)throw Error(`${e.label} trusted proxy must be a literal IP address`);n.add(r)}let r=new Set([t.host]);t.port===``&&r.add(`${t.hostname}:443`);let i=F(e.now??(()=>performance.now()));function a(e){let t=M(e.socket.remoteAddress);if(t===void 0||!n.has(t))return t;let r=e.headers[`x-forwarded-for`],i=(Array.isArray(r)?r.join(`,`):r??``).split(`,`);for(let e=i.length-1;e>=0;--e){let r=M(i[e]);if(r===void 0)return t;if(!n.has(r))return r}return t}return{url:t,checkNames(e,n){let i=e.headers.host?.toLowerCase(),a=e.headers.origin;return i===void 0||!r.has(i)||a!==void 0&&a!==t.origin?(n.writeHead(403).end(),!1):!0},fail(e){let t=a(e);return{remote:N(t),...i(t??`unknown`)}},remote(e){return N(a(e))}}}function L(e){let t=P(e.resource,e.label),n;try{n=new URL(e.issuer)}catch{throw Error(`${e.label} authorization issuer must be an absolute https URL`)}if(n.protocol!==`https:`)throw Error(`${e.label} authorization issuer must use https`);if(e.scopes.length===0||!e.scopes.every(e=>k.test(e)))throw Error(`${e.label} authorization needs at least one valid scope`);let r=`/.well-known/oauth-protected-resource${t.pathname===`/`?``:t.pathname}`,i=`${t.origin}${r}`,a=e.scopes.join(` `);return{wellKnownPath:r,metadata:JSON.stringify({resource:e.resource,authorization_servers:[e.issuer],scopes_supported:[...e.scopes],bearer_methods_supported:[`header`]}),challenges:{missing:`Bearer resource_metadata="${i}"`,invalid:`Bearer error="invalid_token", resource_metadata="${i}"`,scope:`Bearer error="insufficient_scope", scope="${a}", resource_metadata="${i}"`}}}function R(e,t,n){if(e.method!==`GET`){t.writeHead(405,{Allow:`GET`}).end();return}t.writeHead(200,{"Content-Type":`application/json`}).end(n.metadata)}function z(e,t,n,r){if(r.throttled)e.writeHead(429,{"Retry-After":String(r.retryAfterSeconds)}).end();else if(n===`missing-scope`)e.writeHead(403,{"WWW-Authenticate":t.challenges.scope}).end();else{let r=n===`missing-token`?t.challenges.missing:t.challenges.invalid;e.writeHead(401,{"WWW-Authenticate":r}).end()}}const B=32*1024*1024,V=16*1024,H=16,U=/^[0-9a-f]{64}$/,W=/^[A-Za-z0-9+/]*={0,2}$/,G=3e4,K=new Set([`declined`,`too-large`,`bad-name`,`exists`,`unsafe-path`,`integrity`,`protocol`,`closed`,`timeout`,`unavailable`]),q=[[0,31],[127,159],[8203,8207],[8232,8238],[8294,8297],[65279,65279]];function J(e){let t=``;for(let n of e){let e=n.codePointAt(0)??0;t+=q.some(([t,n])=>e>=t&&e<=n)?` `:n}return t}function Y(e){return typeof e==`number`&&Number.isSafeInteger(e)&&e>=0}function te(e){if(e.length>22360)return;let t;try{t=JSON.parse(e)}catch{return}if(typeof t!=`object`||!t)return;let n=t;switch(n.t){case`file-offer`:{let{transferId:e,name:t,size:r,sha256:i}=n;return typeof e!=`string`||e.length===0||e.length>128||typeof t!=`string`||t.length>1024||!Y(r)||typeof i!=`string`||!U.test(i)?void 0:{t:`file-offer`,transferId:e,name:t,size:r,sha256:i}}case`file-accept`:return{t:`file-accept`};case`file-refuse`:{let e=n.reason,t=n.detail;return typeof e!=`string`||!K.has(e)?void 0:{t:`file-refuse`,reason:e,...typeof t==`string`?{detail:J(t).slice(0,500)}:{}}}case`file-chunk`:{let{seq:e,data:t}=n;return!Y(e)||typeof t!=`string`||t===``||!W.test(t)?void 0:{t:`file-chunk`,seq:e,data:t}}case`file-credit`:return Y(n.upTo)?{t:`file-credit`,upTo:n.upTo}:void 0;case`file-end`:return Y(n.chunks)?{t:`file-end`,chunks:n.chunks}:void 0;case`file-done`:return{t:`file-done`};default:return}}var X=class extends Error{reason;detail;constructor(e,t){super(t??e),this.reason=e,this.detail=t}},Z=class{capacity;queue=[];waiter;failure;stops=[];constructor(e,t){this.capacity=t,this.stops.push(e.onFrame(e=>{let t=te(e);if(t===void 0){this.fail(new X(`protocol`,`a frame could not be read`));return}if(this.waiter!==void 0){let{resolve:e}=this.waiter;this.waiter=void 0,e(t);return}if(this.queue.length>=this.capacity){this.fail(new X(`protocol`,`the peer sent past its credit`));return}this.queue.push(t)}),e.onClose(()=>this.fail(new X(`closed`,`the channel closed`))))}fail(e){if(this.failure!==void 0)return;this.failure=e,e.reason===`protocol`&&(this.queue.length=0);let t=this.waiter;this.waiter=void 0,t?.reject(e)}next(e){let t=this.queue.shift();return t===void 0?this.failure===void 0?new Promise((t,n)=>{let r=setTimeout(()=>{this.waiter=void 0,n(new X(`timeout`,`nothing arrived within ${e}ms`))},e);r.unref?.(),this.waiter={resolve:e=>{clearTimeout(r),t(e)},reject:e=>{clearTimeout(r),n(e)}}}):Promise.reject(this.failure):Promise.resolve(t)}get ended(){return this.failure!==void 0}stop(){for(let e of this.stops.splice(0))e()}};function Q(e,t){try{e.send(JSON.stringify(t))}catch(e){throw new X(`closed`,e instanceof Error?e.message:String(e))}}function $(e){return e instanceof X?e:new X(`unavailable`,e instanceof Error?e.message:String(e))}async function*ne(e,t){let n=Buffer.alloc(0);for await(let r of e)for(n=n.length===0?Buffer.from(r):Buffer.concat([n,r]);n.length>=t;)yield n.subarray(0,t),n=n.subarray(t);n.length>0&&(yield n)}async function re(e){let{channel:t,offer:n,source:r}=e,i=e.chunkBytes??16384,a=e.window??16,o=e.idleMs??G,s=new Z(t,a+2);try{if(r.size!==n.size)throw new X(`integrity`,`the source is not the size offered`);Q(t,{t:`file-offer`,...n});let c=await s.next(e.decisionMs??3e5);if(c.t===`file-refuse`)throw new X(c.reason,c.detail);if(c.t!==`file-accept`)throw new X(`protocol`,`expected an answer`);let l=0,u=0,d=0,f=async()=>{let e=await s.next(o);if(e.t===`file-refuse`)throw new X(e.reason,e.detail);if(e.t!==`file-credit`||e.upTo<=u||e.upTo>l)throw new X(`protocol`,`expected credit`);u=e.upTo};for await(let e of ne(r.read(),i)){if(d+=e.length,d>n.size)throw new X(`integrity`,`the source grew while sending`);for(;l-u>=a;)await f();Q(t,{t:`file-chunk`,seq:l,data:e.toString(`base64`)}),l+=1}if(d!==n.size)throw new X(`integrity`,`the source shrank while sending`);for(Q(t,{t:`file-end`,chunks:l});;){let e=await s.next(o);if(e.t===`file-done`)return{ok:!0};if(e.t===`file-refuse`)throw new X(e.reason,e.detail);if(e.t!==`file-credit`||e.upTo<=u||e.upTo>l)throw new X(`protocol`,`expected credit or completion`);u=e.upTo}}catch(e){let t=$(e);return{ok:!1,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail}}}finally{s.stop(),t.close()}}async function ie(t){let{channel:n}=t,r=t.maxBytes??33554432,i=t.window??16,a=t.idleMs??G,o=new Z(n,i+1),s=new AbortController,c=n.onClose(()=>s.abort()),l,u,d=!0;try{let i=await o.next(a);if(i.t!==`file-offer`)throw new X(`protocol`,`expected an offer`);if(l={transferId:i.transferId,name:i.name,size:i.size,sha256:i.sha256},l.size>r)throw new X(`too-large`,`${l.size} bytes is over the ${r}-byte limit`);let c=await t.admit(l,s.signal);if(`refused`in c)throw new X(c.refused,c.detail);if(u=c.sink,o.ended)throw new X(`closed`,`the sender left before the answer`);Q(n,{t:`file-accept`});let d=e(`sha256`),f=0,p=0;for(;;){let e=await o.next(a);if(e.t===`file-end`){if(e.chunks!==f||p!==l.size)throw new X(`integrity`,`the content is not the size offered`);if(d.digest(`hex`)!==l.sha256)throw new X(`integrity`,`the content does not match the offered sha256`);let t=await u.commit();return u=void 0,Q(n,{t:`file-done`}),{ok:!0,offer:l,location:t}}if(e.t!==`file-chunk`||e.seq!==f)throw new X(`protocol`,`expected the next chunk`);let t=Buffer.from(e.data,`base64`);if(p+=t.length,p>l.size)throw new X(`integrity`,`more content than offered`);d.update(t),await u.write(t),f+=1,Q(n,{t:`file-credit`,upTo:f})}}catch(e){let t=$(e);if(d=t.reason!==`protocol`,u!==void 0&&await u.discard().catch(()=>void 0),t.reason!==`closed`&&t.reason!==`timeout`)try{Q(n,{t:`file-refuse`,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail}})}catch{}return{ok:!1,reason:t.reason,...t.detail===void 0?{}:{detail:t.detail},...l===void 0?{}:{offer:l}}}finally{d&&await ae(s.signal,5e3),c(),o.stop(),n.close()}}function ae(e,t){return e.aborted?Promise.resolve():new Promise(n=>{let r=setTimeout(i,t);r.unref?.(),e.addEventListener(`abort`,i,{once:!0});function i(){clearTimeout(r),e.removeEventListener(`abort`,i),n()}})}export{B as DEFAULT_MAX_FILE_BYTES,V as FILE_CHUNK_BYTES,H as FILE_CREDIT_WINDOW,f as bearerCredential,b as createAccessTokenVerifier,I as createBearerResourceServer,d as credentialMatches,L as describeProtectedResource,l as mintTransportToken,P as parsePublicHttpsUrl,ie as receiveFileOverChannel,z as refuseBearerToken,u as resolveAdmission,p as sealHandoffRecord,re as sendFileOverChannel,R as serveProtectedResourceMetadata,m as verifyHandoffPayload};
2
2
  //# sourceMappingURL=node.js.map