@robota-sdk/agent-transport 3.0.0-beta.81 → 3.0.0-beta.83
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/CHANGELOG.md +752 -0
- package/dist/browser/client.d.ts +2 -2
- package/dist/browser/client.js +1 -1
- package/dist/browser/index.d.ts +31 -11
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/observer-messages-Bx-xReUQ.js +2 -0
- package/dist/browser/observer-messages-Bx-xReUQ.js.map +1 -0
- package/dist/browser/{message-decoders-BB8n-H2X.d.ts → observer-messages-DC_8VHk9.d.ts} +104 -7
- package/dist/browser/observer-messages-DC_8VHk9.d.ts.map +1 -0
- package/dist/node/index.cjs +1 -1
- package/dist/node/index.d.cts +131 -14
- package/dist/node/index.d.cts.map +1 -1
- package/dist/node/index.d.ts +131 -14
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +1 -1
- package/dist/node/index.js.map +1 -1
- package/dist/node/node.cjs +1 -1
- package/dist/node/node.d.cts +177 -2
- package/dist/node/node.d.cts.map +1 -1
- package/dist/node/node.d.ts +178 -3
- package/dist/node/node.d.ts.map +1 -1
- package/dist/node/node.js +1 -1
- package/dist/node/node.js.map +1 -1
- package/package.json +13 -11
- package/src/__tests__/bearer-resource-gate.test.ts +120 -0
- package/src/__tests__/file-transfer.test.ts +281 -0
- package/src/__tests__/history-attached-budget.test.ts +216 -0
- package/src/__tests__/message-decoders.test.ts +201 -5
- package/src/__tests__/session-detail-loop-status.test.ts +169 -0
- package/src/__tests__/session-directory-messages.test.ts +192 -0
- package/src/__tests__/session-history-wire.test.ts +342 -0
- package/src/__tests__/session-message-handler-observe.test.ts +193 -0
- package/src/__tests__/session-message-handler.test.ts +82 -1
- package/src/__tests__/ws-broadcast-events.test.ts +10 -0
- package/src/client.ts +9 -1
- package/src/index.ts +14 -2
- package/src/loop-control-messages.ts +34 -0
- package/src/message-decoders.ts +82 -4
- package/src/node/bearer-resource-gate.ts +291 -0
- package/src/node/file-transfer.ts +530 -0
- package/src/node/index.ts +30 -0
- package/src/observer-messages.ts +36 -0
- package/src/open-prompts.ts +76 -0
- package/src/outbound-delivery.ts +11 -2
- package/src/protocol-session.ts +7 -1
- package/src/session-directory-messages.ts +85 -0
- package/src/session-events.ts +93 -18
- package/src/session-message-handler.ts +53 -8
- package/src/session-query-messages.ts +105 -4
- package/src/wire-messages.ts +105 -7
- package/dist/browser/message-decoders-BB8n-H2X.d.ts.map +0 -1
- package/dist/browser/message-decoders-CDmDoZPR.js +0 -2
- package/dist/browser/message-decoders-CDmDoZPR.js.map +0 -1
package/dist/node/node.cjs
CHANGED
|
@@ -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)}exports.bearerCredential=o,exports.createAccessTokenVerifier=m,exports.credentialMatches=a,exports.mintTransportToken=r,exports.resolveAdmission=i,exports.sealHandoffRecord=s,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;
|
package/dist/node/node.d.cts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { IInteractiveSessionRecord } from "@robota-sdk/agent-interface-session";
|
|
2
2
|
import { IAccessTokenVerifier, IAccessTokenVerifierConfig, ITransportAdmission, ITransportAdmissionConfig } from "@robota-sdk/agent-interface-transport";
|
|
3
|
-
import { IHandoffIntegrity } from "@robota-sdk/agent-interface-session-mobility";
|
|
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,5 +106,179 @@ interface IAccessTokenVerifierDeps {
|
|
|
105
106
|
*/
|
|
106
107
|
declare function createAccessTokenVerifier(config: IAccessTokenVerifierConfig, deps?: IAccessTokenVerifierDeps): IAccessTokenVerifier;
|
|
107
108
|
//#endregion
|
|
108
|
-
|
|
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
|
|
176
|
+
//#region src/node/file-transfer.d.ts
|
|
177
|
+
/** Why a transfer did not end with the file kept. */
|
|
178
|
+
type TFileTransferRefusal =
|
|
179
|
+
/** The receiving operator said no, could not be asked, or the connection does not carry files. */
|
|
180
|
+
'declined' |
|
|
181
|
+
/** The file is over the size limit. */
|
|
182
|
+
'too-large' |
|
|
183
|
+
/** The name is empty, or would leave the receiving directory. */
|
|
184
|
+
'bad-name' |
|
|
185
|
+
/** A file of that name was already received; nothing is overwritten. */
|
|
186
|
+
'exists' |
|
|
187
|
+
/** The receiving directory is not one this user owns outright, e.g. a symbolic link. */
|
|
188
|
+
'unsafe-path' |
|
|
189
|
+
/** The content did not match the offered size or hash. The partial file is discarded. */
|
|
190
|
+
'integrity' |
|
|
191
|
+
/** A frame broke the protocol. */
|
|
192
|
+
'protocol' |
|
|
193
|
+
/** The channel ended first. */
|
|
194
|
+
'closed' |
|
|
195
|
+
/** The other side went quiet. */
|
|
196
|
+
'timeout' |
|
|
197
|
+
/** The receiver could not store the file. */
|
|
198
|
+
'unavailable';
|
|
199
|
+
/**
|
|
200
|
+
* The default size limit. A transfer does not resume, so a dropped connection costs the whole file
|
|
201
|
+
* again; a received file also sits on the receiver's disk until its operator removes it. The limit
|
|
202
|
+
* keeps both costs small while covering what is sent between one's own sessions: sources, logs,
|
|
203
|
+
* screenshots, a small archive.
|
|
204
|
+
*/
|
|
205
|
+
declare const DEFAULT_MAX_FILE_BYTES: number;
|
|
206
|
+
/**
|
|
207
|
+
* Bytes of content per chunk. Small enough that a frame, base64 and envelope included, stays well
|
|
208
|
+
* inside the message size every channel it runs on carries.
|
|
209
|
+
*/
|
|
210
|
+
declare const FILE_CHUNK_BYTES: number;
|
|
211
|
+
/** Chunks the sender may have unconfirmed. */
|
|
212
|
+
declare const FILE_CREDIT_WINDOW = 16;
|
|
213
|
+
/** Where content comes from. `read` yields the content once, in order, in pieces of any size. */
|
|
214
|
+
interface IFileSource {
|
|
215
|
+
readonly size: number;
|
|
216
|
+
read(): AsyncIterable<Uint8Array>;
|
|
217
|
+
}
|
|
218
|
+
/** Where received content goes until it is verified; nothing is kept unless `commit` runs. */
|
|
219
|
+
interface IFileSink {
|
|
220
|
+
write(chunk: Uint8Array): Promise<void>;
|
|
221
|
+
/** Keep the verified file. Resolves with where it is. */
|
|
222
|
+
commit(): Promise<string>;
|
|
223
|
+
/** Drop what was written. */
|
|
224
|
+
discard(): Promise<void>;
|
|
225
|
+
}
|
|
226
|
+
/** The receiver's decision on an offer: somewhere to put it, or why not. */
|
|
227
|
+
type TFileAdmission = {
|
|
228
|
+
readonly sink: IFileSink;
|
|
229
|
+
} | {
|
|
230
|
+
readonly refused: TFileTransferRefusal;
|
|
231
|
+
readonly detail?: string;
|
|
232
|
+
};
|
|
233
|
+
type TFileSendOutcome = {
|
|
234
|
+
readonly ok: true;
|
|
235
|
+
} | {
|
|
236
|
+
readonly ok: false;
|
|
237
|
+
readonly reason: TFileTransferRefusal;
|
|
238
|
+
readonly detail?: string;
|
|
239
|
+
};
|
|
240
|
+
type TFileReceiveOutcome = {
|
|
241
|
+
readonly ok: true;
|
|
242
|
+
readonly offer: IFileOffer;
|
|
243
|
+
readonly location: string;
|
|
244
|
+
} | {
|
|
245
|
+
readonly ok: false;
|
|
246
|
+
readonly reason: TFileTransferRefusal;
|
|
247
|
+
readonly detail?: string;
|
|
248
|
+
readonly offer?: IFileOffer;
|
|
249
|
+
};
|
|
250
|
+
interface ISendFileOptions {
|
|
251
|
+
readonly channel: IFileFrameChannel;
|
|
252
|
+
readonly offer: IFileOffer;
|
|
253
|
+
readonly source: IFileSource;
|
|
254
|
+
readonly chunkBytes?: number;
|
|
255
|
+
readonly window?: number;
|
|
256
|
+
readonly idleMs?: number;
|
|
257
|
+
/** How long the receiving operator may take to answer. */
|
|
258
|
+
readonly decisionMs?: number;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Offer a file and, once accepted, send it. Resolves when the receiver confirms it kept the file, or
|
|
262
|
+
* with why it did not. Closes the channel either way.
|
|
263
|
+
*/
|
|
264
|
+
declare function sendFileOverChannel(options: ISendFileOptions): Promise<TFileSendOutcome>;
|
|
265
|
+
interface IReceiveFileOptions {
|
|
266
|
+
readonly channel: IFileFrameChannel;
|
|
267
|
+
/** Offers above this are refused without asking anyone. */
|
|
268
|
+
readonly maxBytes?: number;
|
|
269
|
+
/**
|
|
270
|
+
* Decide on a well-formed offer within the size limit: ask whoever must approve it, and open
|
|
271
|
+
* somewhere to put it. `signal` aborts when the sender goes away.
|
|
272
|
+
*/
|
|
273
|
+
readonly admit: (offer: IFileOffer, signal: AbortSignal) => Promise<TFileAdmission>;
|
|
274
|
+
readonly window?: number;
|
|
275
|
+
readonly idleMs?: number;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Receive one file: the channel's first frame is the offer. Resolves with where the verified file was
|
|
279
|
+
* kept, or with why nothing was. Closes the channel either way.
|
|
280
|
+
*/
|
|
281
|
+
declare function receiveFileOverChannel(options: IReceiveFileOptions): Promise<TFileReceiveOutcome>;
|
|
282
|
+
//#endregion
|
|
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 };
|
|
109
284
|
//# sourceMappingURL=node.d.cts.map
|
package/dist/node/node.d.cts.map
CHANGED
|
@@ -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"],"mappings":"
|
|
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"}
|
package/dist/node/node.d.ts
CHANGED
|
@@ -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
|
-
import { IHandoffIntegrity } from "@robota-sdk/agent-interface-session-mobility";
|
|
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,5 +106,179 @@ interface IAccessTokenVerifierDeps {
|
|
|
105
106
|
*/
|
|
106
107
|
declare function createAccessTokenVerifier(config: IAccessTokenVerifierConfig, deps?: IAccessTokenVerifierDeps): IAccessTokenVerifier;
|
|
107
108
|
//#endregion
|
|
108
|
-
|
|
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
|
|
176
|
+
//#region src/node/file-transfer.d.ts
|
|
177
|
+
/** Why a transfer did not end with the file kept. */
|
|
178
|
+
type TFileTransferRefusal =
|
|
179
|
+
/** The receiving operator said no, could not be asked, or the connection does not carry files. */
|
|
180
|
+
'declined' |
|
|
181
|
+
/** The file is over the size limit. */
|
|
182
|
+
'too-large' |
|
|
183
|
+
/** The name is empty, or would leave the receiving directory. */
|
|
184
|
+
'bad-name' |
|
|
185
|
+
/** A file of that name was already received; nothing is overwritten. */
|
|
186
|
+
'exists' |
|
|
187
|
+
/** The receiving directory is not one this user owns outright, e.g. a symbolic link. */
|
|
188
|
+
'unsafe-path' |
|
|
189
|
+
/** The content did not match the offered size or hash. The partial file is discarded. */
|
|
190
|
+
'integrity' |
|
|
191
|
+
/** A frame broke the protocol. */
|
|
192
|
+
'protocol' |
|
|
193
|
+
/** The channel ended first. */
|
|
194
|
+
'closed' |
|
|
195
|
+
/** The other side went quiet. */
|
|
196
|
+
'timeout' |
|
|
197
|
+
/** The receiver could not store the file. */
|
|
198
|
+
'unavailable';
|
|
199
|
+
/**
|
|
200
|
+
* The default size limit. A transfer does not resume, so a dropped connection costs the whole file
|
|
201
|
+
* again; a received file also sits on the receiver's disk until its operator removes it. The limit
|
|
202
|
+
* keeps both costs small while covering what is sent between one's own sessions: sources, logs,
|
|
203
|
+
* screenshots, a small archive.
|
|
204
|
+
*/
|
|
205
|
+
declare const DEFAULT_MAX_FILE_BYTES: number;
|
|
206
|
+
/**
|
|
207
|
+
* Bytes of content per chunk. Small enough that a frame, base64 and envelope included, stays well
|
|
208
|
+
* inside the message size every channel it runs on carries.
|
|
209
|
+
*/
|
|
210
|
+
declare const FILE_CHUNK_BYTES: number;
|
|
211
|
+
/** Chunks the sender may have unconfirmed. */
|
|
212
|
+
declare const FILE_CREDIT_WINDOW = 16;
|
|
213
|
+
/** Where content comes from. `read` yields the content once, in order, in pieces of any size. */
|
|
214
|
+
interface IFileSource {
|
|
215
|
+
readonly size: number;
|
|
216
|
+
read(): AsyncIterable<Uint8Array>;
|
|
217
|
+
}
|
|
218
|
+
/** Where received content goes until it is verified; nothing is kept unless `commit` runs. */
|
|
219
|
+
interface IFileSink {
|
|
220
|
+
write(chunk: Uint8Array): Promise<void>;
|
|
221
|
+
/** Keep the verified file. Resolves with where it is. */
|
|
222
|
+
commit(): Promise<string>;
|
|
223
|
+
/** Drop what was written. */
|
|
224
|
+
discard(): Promise<void>;
|
|
225
|
+
}
|
|
226
|
+
/** The receiver's decision on an offer: somewhere to put it, or why not. */
|
|
227
|
+
type TFileAdmission = {
|
|
228
|
+
readonly sink: IFileSink;
|
|
229
|
+
} | {
|
|
230
|
+
readonly refused: TFileTransferRefusal;
|
|
231
|
+
readonly detail?: string;
|
|
232
|
+
};
|
|
233
|
+
type TFileSendOutcome = {
|
|
234
|
+
readonly ok: true;
|
|
235
|
+
} | {
|
|
236
|
+
readonly ok: false;
|
|
237
|
+
readonly reason: TFileTransferRefusal;
|
|
238
|
+
readonly detail?: string;
|
|
239
|
+
};
|
|
240
|
+
type TFileReceiveOutcome = {
|
|
241
|
+
readonly ok: true;
|
|
242
|
+
readonly offer: IFileOffer;
|
|
243
|
+
readonly location: string;
|
|
244
|
+
} | {
|
|
245
|
+
readonly ok: false;
|
|
246
|
+
readonly reason: TFileTransferRefusal;
|
|
247
|
+
readonly detail?: string;
|
|
248
|
+
readonly offer?: IFileOffer;
|
|
249
|
+
};
|
|
250
|
+
interface ISendFileOptions {
|
|
251
|
+
readonly channel: IFileFrameChannel;
|
|
252
|
+
readonly offer: IFileOffer;
|
|
253
|
+
readonly source: IFileSource;
|
|
254
|
+
readonly chunkBytes?: number;
|
|
255
|
+
readonly window?: number;
|
|
256
|
+
readonly idleMs?: number;
|
|
257
|
+
/** How long the receiving operator may take to answer. */
|
|
258
|
+
readonly decisionMs?: number;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Offer a file and, once accepted, send it. Resolves when the receiver confirms it kept the file, or
|
|
262
|
+
* with why it did not. Closes the channel either way.
|
|
263
|
+
*/
|
|
264
|
+
declare function sendFileOverChannel(options: ISendFileOptions): Promise<TFileSendOutcome>;
|
|
265
|
+
interface IReceiveFileOptions {
|
|
266
|
+
readonly channel: IFileFrameChannel;
|
|
267
|
+
/** Offers above this are refused without asking anyone. */
|
|
268
|
+
readonly maxBytes?: number;
|
|
269
|
+
/**
|
|
270
|
+
* Decide on a well-formed offer within the size limit: ask whoever must approve it, and open
|
|
271
|
+
* somewhere to put it. `signal` aborts when the sender goes away.
|
|
272
|
+
*/
|
|
273
|
+
readonly admit: (offer: IFileOffer, signal: AbortSignal) => Promise<TFileAdmission>;
|
|
274
|
+
readonly window?: number;
|
|
275
|
+
readonly idleMs?: number;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Receive one file: the channel's first frame is the offer. Resolves with where the verified file was
|
|
279
|
+
* kept, or with why nothing was. Closes the channel either way.
|
|
280
|
+
*/
|
|
281
|
+
declare function receiveFileOverChannel(options: IReceiveFileOptions): Promise<TFileReceiveOutcome>;
|
|
282
|
+
//#endregion
|
|
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 };
|
|
109
284
|
//# sourceMappingURL=node.d.ts.map
|
package/dist/node/node.d.ts.map
CHANGED
|
@@ -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"],"mappings":"
|
|
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)}export{u as bearerCredential,v as createAccessTokenVerifier,l as credentialMatches,s as mintTransportToken,c as resolveAdmission,d as sealHandoffRecord,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
|