@north-light/plugin 0.3.480 → 0.3.481

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/README.md CHANGED
@@ -126,6 +126,8 @@ On Node, Express or Nest, mount the Fetch handler with `createNodeHandler(handle
126
126
 
127
127
  With directory authentication, configure `createFetchHandler(plugin, { auth, receipts: { provider: 'app:acme', rates: { send: { usdPerUnit: 0.01 } }, outbox } })`. `provider` must be the provider's authenticated `app:<id>` principal, not the plugin name. Rate keys and emitted receipt `tool` values are command paths **after** the plugin name: `send` for `crtr acme send`, or `messages send` for `crtr acme messages send`. A handler calls `ctx.meter({ units, unit })` to emit a `Crtr-Receipt`; the provider persists and posts that same receipt to the directory ledger.
128
128
 
129
+ `ctx.caller.payer` is the principal charged for the call, `user:<id>` or `app:<id>`: the token's `payer` claim, or `sub` when the token has none. `ctx.meter` stamps it on the receipt. Before a priced call, `await ctx.checkSpend()` asks the directory (`POST <auth.issuer>/v1/billing/check`, sending the request's outbound token) whether that payer may spend; it reserves nothing. It needs `receipts.ledger: { clientId, privateKey, keyId? }`, the same credentials you pass to `postReceipt`, and throws a plain `Error` without them or without a verified caller. It resolves when the call may go ahead, throws `SpendRefusedError { code, message, next }` on a refusal (`payment_required` or `limit_reached`, with `next` the page that fixes it), and throws `ReceiptPostError` when the check cannot be completed. To refuse the call, rethrow it as `new LeafError({ code: e.code, message: e.message, status: 402, next: e.next })`.
130
+
129
131
  To answer a rate limit, throw a `LeafError` with `retryAfter` (seconds, or a `Date`): `throw new LeafError({ code: 'rate_limited', status: 429, message: 'Slow down.', retryAfter: 30 })` answers 429 with `Retry-After: 30`. `headers: { 'X-RateLimit-Reset': '…' }` sets any other response header; `Content-Type`, `Content-Length`, `Content-Encoding`, `Transfer-Encoding` and `Crtr-Receipt` belong to the kit. For a side-effect leaf the kit passes the headers to `requests.store` as `answer.headers` (lowercase names); a store that keeps and returns them makes a repeated request answer with the same headers.
130
132
 
131
133
  For side-effect leaves, provide `requests: { record, read, store, delete }`. `record(requestId, identity)` atomically inserts both the id and `{sub, grantee}` from the verified caller (or `null` for a legacy static-token plugin); `read(requestId)` returns `{identity, recordedAt, answer}`. The kit refuses a repeated id from a different caller rather than replaying another person's result. `store` saves the result and receipt transactionally with the outbox. For provider logging, `onAnswer(event)` receives one metadata-only event per answered leaf POST (principal-chain fields, request/run ids, relative tool, status, duration, receipt id, replay flag); it never receives bearer tokens, arguments, or result bodies, and a logging failure does not alter the answer. `createCallerVerifier(auth)` is exported for providers that verify requests outside the Fetch handler.
package/dist/define.d.ts CHANGED
@@ -17,6 +17,11 @@ export interface LeafCaller {
17
17
  readonly sub: string;
18
18
  /** The principal the grant was issued to: `app:<id>`, or the user for the person's own authority. */
19
19
  readonly grantee: string;
20
+ /**
21
+ * The principal charged for this call, `user:<id>` or `app:<id>`: the token's `payer` claim, or
22
+ * `sub` when the token has none (a directory that predates payers). Receipts from `ctx.meter` carry it.
23
+ */
24
+ readonly payer: string;
20
25
  /** The OAuth client id of the runtime that presented the token; it authorizes nothing. */
21
26
  readonly clientId: string;
22
27
  /** The `<provider>:<group>` strings the token carries. */
@@ -48,6 +53,13 @@ export interface LeafContext {
48
53
  unit: string;
49
54
  usdEquivalent?: number;
50
55
  }) => Receipt;
56
+ /**
57
+ * Asks the directory, before a priced call, whether `caller.payer` may spend. Resolves when it
58
+ * may and reserves nothing. Throws `SpendRefusedError` when the directory refuses (HTTP 402) and
59
+ * `ReceiptPostError` when the check cannot be completed. Throws an `Error` when the leaf has no
60
+ * verified caller or the handler has no `receipts` (with `receipts.ledger`).
61
+ */
62
+ readonly checkSpend: () => Promise<void>;
51
63
  }
52
64
  /** One command an agent can invoke, as returned by `defineLeaf` or `defineStreamingLeaf`. */
53
65
  export interface LeafNode {
package/dist/index.d.ts CHANGED
@@ -7,5 +7,5 @@ export * from './bundle.js';
7
7
  export * from './serve.js';
8
8
  export { createNodeHandler, type NodeHandlerOptions, type NodeLikeRequest, type NodeLikeResponse } from './node.js';
9
9
  export { createCallerVerifier, type CallerVerifier, type FetchHandlerAuth } from './verify.js';
10
- export { postReceipt, ReceiptPostError, type Receipt, type ReceiptOptions, type ReceiptOutbox } from './receipts.js';
10
+ export { postReceipt, ReceiptPostError, SpendRefusedError, type LedgerCredentials, type Receipt, type ReceiptOptions, type ReceiptOutbox } from './receipts.js';
11
11
  export { LeafError, type LeafErrorOptions, type LeafErrorQuestion } from './errors.js';
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export*from"./params.js";export*from"./fields.js";export*from"./define.js";export*from"./manifest.js";import{compareManifests as m}from"./compare.js";export*from"./bundle.js";export*from"./serve.js";import{createNodeHandler as i}from"./node.js";import{createCallerVerifier as l}from"./verify.js";import{postReceipt as n,ReceiptPostError as E}from"./receipts.js";import{LeafError as C}from"./errors.js";export{C as LeafError,E as ReceiptPostError,m as compareManifests,l as createCallerVerifier,i as createNodeHandler,n as postReceipt};
1
+ export*from"./params.js";export*from"./fields.js";export*from"./define.js";export*from"./manifest.js";import{compareManifests as m}from"./compare.js";export*from"./bundle.js";export*from"./serve.js";import{createNodeHandler as i}from"./node.js";import{createCallerVerifier as d}from"./verify.js";import{postReceipt as n,ReceiptPostError as E,SpendRefusedError as R}from"./receipts.js";import{LeafError as C}from"./errors.js";export{C as LeafError,E as ReceiptPostError,R as SpendRefusedError,m as compareManifests,d as createCallerVerifier,i as createNodeHandler,n as postReceipt};
@@ -16,6 +16,12 @@ export interface Receipt {
16
16
  export interface ReceiptOutbox {
17
17
  save(receipt: Receipt): Promise<void>;
18
18
  }
19
+ /** The provider's directory client credentials, the same ones that sign `postReceipt`'s assertion. */
20
+ export interface LedgerCredentials {
21
+ readonly clientId: string;
22
+ readonly privateKey: string | JsonWebKey;
23
+ readonly keyId?: string;
24
+ }
19
25
  export interface ReceiptOptions {
20
26
  /** The provider's authenticated principal (e.g. `app:sms`), not its plugin name. */
21
27
  readonly provider: string;
@@ -24,12 +30,27 @@ export interface ReceiptOptions {
24
30
  readonly usdPerUnit: number;
25
31
  }>>;
26
32
  readonly outbox: ReceiptOutbox;
33
+ /**
34
+ * The provider's directory client credentials. Needed only for `ctx.checkSpend()`, which asks
35
+ * `<auth.issuer>/v1/billing/check` with a client-credentials token signed by this key.
36
+ */
37
+ readonly ledger?: LedgerCredentials;
27
38
  }
28
39
  export declare class ReceiptPostError extends Error {
29
40
  readonly permanent: boolean;
30
41
  readonly status?: number | undefined;
31
42
  constructor(message: string, permanent: boolean, status?: number | undefined);
32
43
  }
44
+ /**
45
+ * Thrown by `ctx.checkSpend()` when the directory refuses a priced call (HTTP 402). `code` is the
46
+ * directory's (`payment_required` or `limit_reached`); `message` is written for the person, and
47
+ * `next` is the page that fixes it. Pass all three through unchanged, as a `LeafError` with status 402.
48
+ */
49
+ export declare class SpendRefusedError extends Error {
50
+ readonly code: string;
51
+ readonly next: string;
52
+ constructor(code: string, message: string, next: string);
53
+ }
33
54
  /** Post one saved receipt; the provider owns durable retry and permanent-error reporting. */
34
55
  export declare function postReceipt(receipt: Receipt, ledger: {
35
56
  url: string;
@@ -37,3 +58,9 @@ export declare function postReceipt(receipt: Receipt, ledger: {
37
58
  privateKey: string | JsonWebKey;
38
59
  keyId?: string;
39
60
  }): Promise<void>;
61
+ /**
62
+ * Asks the directory whether the caller whose outbound token this is may start a priced call.
63
+ * Resolves on 200, throws `SpendRefusedError` on 402 and `ReceiptPostError` on any other failure.
64
+ * The directory reserves nothing; the raw `token` goes to `<issuer>/v1/billing/check` and nowhere else.
65
+ */
66
+ export declare function requestSpendCheck(issuer: string, token: string, ledger: LedgerCredentials): Promise<void>;
package/dist/receipts.js CHANGED
@@ -1 +1 @@
1
- var A=Object.defineProperty;var p=(e,n)=>A(e,"name",{value:n,configurable:!0});class i extends Error{static{p(this,"ReceiptPostError")}permanent;status;constructor(n,t,o){super(n),this.permanent=t,this.status=o,this.name="ReceiptPostError"}}async function C(e,n){const t=new URL("/token",n.url),o=await b(n,t.href);let r;try{r=await fetch(t,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"client_credentials",client_id:n.clientId,client_assertion_type:"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",client_assertion:o})})}catch(s){throw new i(`Could not obtain a ledger token: ${String(s)}`,!1)}if(!r.ok)throw new i(`Ledger token request failed: HTTP ${r.status}`,S(r.status),r.status);let c;try{c=(await r.json()).access_token}catch{throw new i("Ledger token response was not JSON",!1)}if(typeof c!="string"||c.length===0)throw new i("Ledger token response has no access_token",!1);let a;try{a=await fetch(n.url,{method:"POST",headers:{Authorization:`Bearer ${c}`,"Content-Type":"application/json"},body:JSON.stringify(e)})}catch(s){throw new i(`Could not post receipt: ${String(s)}`,!1)}if(!a.ok)throw new i(`Ledger receipt request failed: HTTP ${a.status}`,S(a.status),a.status)}p(C,"postReceipt");async function b(e,n){const{privateKey:t}=e,o=typeof t=="string";let r=!o&&t.kty==="RSA"?"RS256":"ES256";if(!o&&t.kty!=="RSA"&&t.kty!=="EC")throw new i("Ledger private key must be an ES256 or RS256 PEM or JWK",!0);const c={name:"ECDSA",namedCurve:"P-256"},a={name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"};let s;try{if(o){const l=/^-----BEGIN PRIVATE KEY-----\s*([A-Za-z0-9+/=\s]+)-----END PRIVATE KEY-----\s*$/.exec(t);if(!l)throw new Error("Expected a PKCS#8 PEM private key");const u=Uint8Array.from(atob(l[1].replace(/\s/g,"")),E=>E.charCodeAt(0));try{s=await crypto.subtle.importKey("pkcs8",u,c,!1,["sign"])}catch{r="RS256",s=await crypto.subtle.importKey("pkcs8",u,a,!1,["sign"])}}else{if(t.alg!==void 0&&t.alg!==r)throw new Error(`JWK alg must be ${r}`);s=await crypto.subtle.importKey("jwk",t,r==="ES256"?c:a,!1,["sign"])}}catch(l){throw new i(`Invalid ledger private key: ${String(l)}`,!0)}const y=Math.floor(Date.now()/1e3),h=p(l=>f(new TextEncoder().encode(JSON.stringify(l))),"encode"),w=e.keyId??(o?void 0:t.kid),g=h({alg:r,typ:"JWT",...w?{kid:w}:{}}),k=h({iss:e.clientId,sub:e.clientId,aud:n,iat:y,exp:y+60,jti:crypto.randomUUID()}),d=`${g}.${k}`,m=await crypto.subtle.sign(r==="ES256"?{name:"ECDSA",hash:"SHA-256"}:"RSASSA-PKCS1-v1_5",s,new TextEncoder().encode(d));return`${d}.${f(new Uint8Array(m))}`}p(b,"clientAssertion");function f(e){let n="";for(const t of e)n+=String.fromCharCode(t);return btoa(n).replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_")}p(f,"base64url");function S(e){return e>=400&&e<500&&e!==408&&e!==429}p(S,"isPermanent");export{i as ReceiptPostError,C as postReceipt};
1
+ var b=Object.defineProperty;var c=(e,n)=>b(e,"name",{value:n,configurable:!0});class i extends Error{static{c(this,"ReceiptPostError")}permanent;status;constructor(n,t,r){super(n),this.permanent=t,this.status=r,this.name="ReceiptPostError"}}class T extends Error{static{c(this,"SpendRefusedError")}code;next;constructor(n,t,r){super(t),this.code=n,this.next=r,this.name="SpendRefusedError"}}async function $(e,n){const t=await g(n);let r;try{r=await fetch(n.url,{method:"POST",headers:{Authorization:`Bearer ${t}`,"Content-Type":"application/json"},body:JSON.stringify(e)})}catch(o){throw new i(`Could not post receipt: ${String(o)}`,!1)}if(!r.ok)throw new i(`Ledger receipt request failed: HTTP ${r.status}`,d(r.status),r.status)}c($,"postReceipt");async function g(e,n){const t=new URL("/token",e.url),r=await P(e,t.href);let o;try{o=await fetch(t,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"client_credentials",client_id:e.clientId,client_assertion_type:"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",client_assertion:r}),...n===void 0?{}:{signal:n}})}catch(a){throw new i(`Could not obtain a ledger token: ${String(a)}`,!1)}if(!o.ok)throw new i(`Ledger token request failed: HTTP ${o.status}`,d(o.status),o.status);let s;try{s=(await o.json()).access_token}catch{throw new i("Ledger token response was not JSON",!1)}if(typeof s!="string"||s.length===0)throw new i("Ledger token response has no access_token",!1);return s}c(g,"ledgerToken");async function x(e,n,t){const r=AbortSignal.timeout(1e4),o=await g({...t,url:e},r);let s;try{s=await fetch(`${e.replace(/\/+$/,"")}/v1/billing/check`,{method:"POST",headers:{Authorization:`Bearer ${o}`,"Content-Type":"application/json"},body:JSON.stringify({token:n}),signal:r})}catch(a){throw new i(`Could not reach the billing check: ${String(a)}`,!1)}if(!s.ok){if(s.status===402){let a;try{a=(await s.json()).error}catch{}throw typeof a?.code=="string"&&typeof a.message=="string"?new T(a.code,a.message,typeof a.details?.next=="string"?a.details.next:""):new i("Billing check answered 402 without a code and message",!0,402)}throw new i(`Billing check request failed: HTTP ${s.status}`,d(s.status),s.status)}}c(x,"requestSpendCheck");async function P(e,n){const{privateKey:t}=e,r=typeof t=="string";let o=!r&&t.kty==="RSA"?"RS256":"ES256";if(!r&&t.kty!=="RSA"&&t.kty!=="EC")throw new i("Ledger private key must be an ES256 or RS256 PEM or JWK",!0);const s={name:"ECDSA",namedCurve:"P-256"},a={name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"};let p;try{if(r){const l=/^-----BEGIN PRIVATE KEY-----\s*([A-Za-z0-9+/=\s]+)-----END PRIVATE KEY-----\s*$/.exec(t);if(!l)throw new Error("Expected a PKCS#8 PEM private key");const f=Uint8Array.from(atob(l[1].replace(/\s/g,"")),A=>A.charCodeAt(0));try{p=await crypto.subtle.importKey("pkcs8",f,s,!1,["sign"])}catch{o="RS256",p=await crypto.subtle.importKey("pkcs8",f,a,!1,["sign"])}}else{if(t.alg!==void 0&&t.alg!==o)throw new Error(`JWK alg must be ${o}`);p=await crypto.subtle.importKey("jwk",t,o==="ES256"?s:a,!1,["sign"])}}catch(l){throw new i(`Invalid ledger private key: ${String(l)}`,!0)}const h=Math.floor(Date.now()/1e3),u=c(l=>S(new TextEncoder().encode(JSON.stringify(l))),"encode"),w=e.keyId??(r?void 0:t.kid),k=u({alg:o,typ:"JWT",...w?{kid:w}:{}}),m=u({iss:e.clientId,sub:e.clientId,aud:n,iat:h,exp:h+60,jti:crypto.randomUUID()}),y=`${k}.${m}`,E=await crypto.subtle.sign(o==="ES256"?{name:"ECDSA",hash:"SHA-256"}:"RSASSA-PKCS1-v1_5",p,new TextEncoder().encode(y));return`${y}.${S(new Uint8Array(E))}`}c(P,"clientAssertion");function S(e){let n="";for(const t of e)n+=String.fromCharCode(t);return btoa(n).replace(/=/g,"").replace(/\+/g,"-").replace(/\//g,"_")}c(S,"base64url");function d(e){return e>=400&&e<500&&e!==408&&e!==429}c(d,"isPermanent");export{i as ReceiptPostError,T as SpendRefusedError,$ as postReceipt,x as requestSpendCheck};
package/dist/serve.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Receipt, ReceiptOptions } from './receipts.js';
1
+ import { type Receipt, type ReceiptOptions } from './receipts.js';
2
2
  import { type LeafCaller, type PluginDefinition } from './define.js';
3
3
  import { type FetchHandlerAuth } from './verify.js';
4
4
  export type { FetchHandlerAuth } from './verify.js';
package/dist/serve.js CHANGED
@@ -1,3 +1,3 @@
1
- var W=Object.defineProperty;var d=(n,e)=>W(n,"name",{value:e,configurable:!0});import{approvalFor as Z}from"./approval.js";import{validateDeclaredResult as K}from"@north-light/api/command-manifest";import{errorResponse as x,LeafError as I,leafErrorResponse as P,storedAnswerHeaders as Q}from"./errors.js";import{buildCommandManifest as X}from"./manifest.js";import{buildBundle as Y}from"./bundle.js";import{createCallerVerifier as ee,normalizeProviderId as ne,scopeMissing as te}from"./verify.js";const B=/^v([1-9][0-9]*)$/;function $e(n,e={}){if(e.token!==void 0&&e.auth!==void 0)throw new Error("createFetchHandler takes either token or auth, not both");if(e.token!==void 0&&e.receipts!==void 0)throw new Error("createFetchHandler cannot meter with a static token");if(e.receipts!==void 0&&!/^app:[A-Za-z0-9_-]{1,64}$/.test(e.receipts.provider))throw new Error("createFetchHandler receipts.provider must be an app principal (app:<id>)");const i=e.auth===void 0?void 0:ne(e.auth),t=Array.isArray(n)||n.version!==void 0,r=oe(Array.isArray(n)?n:[n],Array.isArray(n),i),o=r[0].plugin.name;for(const u of r)for(const g of u.commands.values())if(g.leaf.sideEffect&&e.requests===void 0)throw new Error(`createFetchHandler needs requests for side-effect leaf ${g.path.join(" ")}`);const f=e.auth===void 0?void 0:ee(e.auth);if(f!==void 0){for(const u of r)for(const g of u.commands.values())if(g.scope===void 0)throw new Error(`createFetchHandler auth needs a group on every leaf; ${g.path.join(" ")} has none`)}const a=new Map,E=e.baseUrl===void 0?void 0:F(new URL(e.baseUrl).pathname),h=r[r.length-1];e.token===void 0&&e.auth===void 0&&console.warn(`crouter plugin ${o} is serving without bearer authentication`);const w=d(async(u,g,T)=>{const s=`${g.version}\0${T}`;let l=a.get(s);if(l===void 0){const A=await Y(g.plugin,{mountPath:T,...e.baseUrl!==void 0?{baseUrl:e.baseUrl}:{}});l={tar:A.tar,etag:A.etag},a.set(s,l)}const p={ETag:l.etag};return u.headers.get("if-none-match")===l.etag?new Response(null,{status:304,headers:p}):new Response(new Uint8Array(l.tar).buffer,{headers:{...p,"Content-Type":"application/x-tar"}})},"archiveResponse");return async u=>{if(u.method!=="POST"&&e.token!==void 0&&!V(u.headers.get("authorization")??"",`Bearer ${e.token}`))return x(401,{code:"unauthorized",message:"A valid bearer token is required.",next:"Send Authorization: Bearer <token>."});const g=new URL(u.url).pathname;if(u.method==="GET"){if(!t)return w(u,h,E??F(g));const m=U(g),R=m[m.length-1],_=E??F(`/${m.slice(0,-1).join("/")}`);if(R==="versions")return Response.json({latest:h.version,supported:r.map($=>$.version)});const O=R===void 0?null:B.exec(R),b=O===null?void 0:r.find($=>$.version===Number(O[1]));return b===void 0?z():w(u,b,`${_}/v${b.version}`)}if(u.method!=="POST")return z();const T=performance.now();let s,l,p,A=!1,H=!1;const k=d(m=>{if(l!==void 0)try{e.onAnswer?.({...s===void 0?{}:{sub:s.sub,grantee:s.grantee,initiatingApp:s.initiatingApp,act:s.act,jti:s.jti,requestId:s.requestId,...s.runId===void 0?{}:{runId:s.runId}},...s===void 0&&y!==null&&y!==""?{requestId:y}:{},tool:l.path.slice(1).join(" "),status:m.status,durationMs:performance.now()-T,...p===void 0?{}:{receiptId:p.receipt_id},replayed:H})}catch{}return m},"answer");let y=u.headers.get("crtr-request-id");try{if(l=t?se(r,g):de(h.commands,g),e.token!==void 0&&!V(u.headers.get("authorization")??"",`Bearer ${e.token}`))return k(x(401,{code:"unauthorized",message:"A valid bearer token is required.",next:"Send Authorization: Bearer <token>."}));const m=f===void 0?void 0:await f(u);if(l===void 0)return z();if(m!==void 0&&!m.scope.includes(l.scope)){s=Object.freeze({...m,requestId:y??"",...u.headers.get("crtr-run-id")?{runId:u.headers.get("crtr-run-id")}:{}});const c=l.leaf.group,v=i===void 0&&c!==void 0?m.scope.find(j=>j.endsWith(`:${c}`)&&j.length>c.length+1)?.slice(0,-(c.length+1)):void 0;throw te(l.scope,v)}if(m!==void 0){if(y===null||y==="")return k(x(400,{code:"invalid_request",message:"The request carries no Crtr-Request-Id header."}));const c=u.headers.get("crtr-run-id");s=Object.freeze({...m,requestId:y,...c!==null&&c!==""?{runId:c}:{}})}const R=await ce(u,l.manifest,m!==void 0);if(l.leaf.sideEffect){if(y===null||y==="")return k(x(400,{code:"invalid_request",message:"The request carries no Crtr-Request-Id header."}));const c=s===void 0?null:{sub:s.sub,grantee:s.grantee};if(!await e.requests.record(y,c)){H=!0;const j=await ie(e.requests,y,c,l.plugin.timeouts?.requestMs??3e4);if(j!==!0){const J=j.headers.get("Crtr-Receipt");if(J!==null)try{p=JSON.parse(J)}catch{}return k(j)}H=!1}A=!0}const _=l,O={request:u,...s!==void 0?{caller:s}:{},approval:Z(u),meter(c){if(s===void 0)throw new Error("ctx.meter needs an authenticated caller");if(p!==void 0)throw new Error("ctx.meter may be called only once");if(e.receipts===void 0)throw new Error("ctx.meter needs receipts with a provider and outbox");const v=_.path.slice(1).join(" "),j=c.usdEquivalent??(e.receipts.rates[v]===void 0?void 0:c.units*e.receipts.rates[v].usdPerUnit);if(j===void 0)throw new Error(`No receipt rate for ${v}`);return p={receipt_id:s.requestId,provider:e.receipts.provider,tool:v,sub:s.sub,grantee:s.grantee,payer:s.sub,units:c.units,unit:c.unit,usd_equivalent:j,request_id:s.requestId,...s.runId!==void 0?{run_id:s.runId}:{},time:new Date().toISOString()},p}};let b,$;try{const c=await l.leaf.run(R,O);b=l.leaf.streaming?await he(c,e.onError,l.path):le(c,l.manifest)}catch(c){c instanceof I?(b=P(c),$=c.headers):(C(e.onError,c,l.path),b=q(c))}if(A&&b.status===403&&await re(b))return await e.requests.delete(y),k(D(b,p));try{if(A)await e.requests.store(y,{status:b.status,body:await b.clone().text(),...$!==void 0?{headers:{...$}}:{},...p!==void 0?{receipt:p}:{}});else if(p!==void 0){if(e.receipts===void 0)throw new Error(`No receipt outbox for ${_.path.join(" ")}`);await e.receipts.outbox.save(p)}}catch(c){return C(e.onError,c,l.path,p),k(q(c))}return k(D(b,p))}catch(m){return m instanceof I?k(P(m)):(C(e.onError,m,l?.path??[]),k(q(m)))}}}d($e,"createFetchHandler");function q(n){return x(500,{code:"handler_failed",message:n instanceof Error?n.message:String(n)})}d(q,"handlerFailed");function D(n,e){if(e===void 0)return n;const i=new Headers(n.headers);return i.set("Crtr-Receipt",JSON.stringify(e)),new Response(n.body,{status:n.status,headers:i})}d(D,"responseWithReceipt");async function re(n){try{const e=await n.clone().json();return M(e)&&M(e.error)&&e.error.code==="approval_required"}catch{return!1}}d(re,"isApprovalRequired");async function ie(n,e,i,t){for(;;){const r=await n.read(e);if(r===null){if(await n.record(e,i))return!0;continue}if(r.identity?.sub!==i?.sub||r.identity?.grantee!==i?.grantee)return x(403,{code:"provider_refused",message:"This request id belongs to a different caller."});if(r.answer!==null){const o=Q(r.answer.headers);return o.set("Content-Type","application/json"),r.answer.receipt!==void 0&&o.set("Crtr-Receipt",JSON.stringify(r.answer.receipt)),new Response(r.answer.body,{status:r.answer.status,headers:o})}if(Date.now()>=new Date(r.recordedAt).getTime()+t)return q(new Error("The earlier request has no stored answer."));await new Promise(o=>setTimeout(o,Math.min(25,Math.max(1,new Date(r.recordedAt).getTime()+t-Date.now()))))}}d(ie,"replay");function oe(n,e,i){if(n.length===0)throw new Error("createFetchHandler needs at least one plugin definition");const t=n[0].name,r=new Map;for(const o of n){if(o.name!==t)throw new Error(`createFetchHandler serves one plugin; got ${t} and ${o.name}`);if(e&&o.version===void 0)throw new Error(`createFetchHandler needs an explicit version on every definition in an array; ${t} has none`);const f=o.version??1;if(r.has(f))throw new Error(`createFetchHandler got two definitions of ${t} major ${f}`);r.set(f,{version:f,plugin:o,commands:ae(o,i)})}return[...r.values()].sort((o,f)=>o.version-f.version)}d(oe,"servedMajors");function ae(n,e){const t=X(n,{mountPath:""}).mounts[0]?.node;if(t===void 0||t.kind!=="branch")throw new Error("Generated plugin manifest has no root branch");const r=new Map;return G(t.children,n.commands,[t.name],n,r,e),r}d(ae,"commandMap");function G(n,e,i,t,r,o){const f=Object.entries(e);if(n.length!==f.length)throw new Error("Generated plugin manifest does not match the command definition");for(const[a,[E,h]]of f.entries()){const w=n[a];if(w===void 0||w.kind!==h.kind)throw new Error(`Generated plugin manifest does not match command ${E}`);const u=[...i,w.name];if(w.kind==="branch"&&h.kind==="branch"){G(w.children,h.children,u,t,r,o);continue}if(w.kind==="leaf"&&h.kind==="leaf"){r.set(u.join("/"),{path:u,leaf:h,plugin:t,manifest:w,...h.group!==void 0?{scope:`${o??t.name}:${h.group}`}:{}});continue}throw new Error(`Generated plugin manifest does not match command ${E}`)}}d(G,"collectCommands");function de(n,e){const i=U(e);for(let t=0;t<i.length;t+=1){const r=n.get(i.slice(t).join("/"));if(r!==void 0)return r}}d(de,"findCommand");function se(n,e){const i=U(e);for(let t=i.length-2;t>=0;t-=1){const r=B.exec(i[t]);if(r===null)continue;const f=n.find(a=>a.version===Number(r[1]))?.commands.get(i.slice(t+1).join("/"));if(f!==void 0)return f}}d(se,"findVersionedCommand");function U(n){return n.split("/").filter(e=>e.length>0)}d(U,"pathSegments");async function ce(n,e,i){const t=n.body===null?"":await n.text();let r;if(t.length===0)r={};else try{const a=JSON.parse(t);if(!M(a))throw new Error("not an object");r=a}catch{throw N("Request body must be a JSON object.",i?"body":void 0,i,t)}const o=new Map(e.params.map(a=>[a.name,a]));for(const a of Object.keys(r))if(!o.has(a))throw i?N(`Unknown parameter ${a}.`,a,!0,r[a]):new I({code:"unknown_parameter",message:`Unknown parameter ${a}.`,field:a});const f={};for(const a of e.params){if(!Object.hasOwn(r,a.name)){if(a.required)throw N(`Missing required parameter ${a.name}.`,a.name,i,null);continue}const E=r[a.name],h=ue(E,a);if(h===void 0)throw N(`Invalid value for parameter ${a.name}.`,a.name,i,E);f[me(a.name)]=h}return f}d(ce,"decodeInput");function ue(n,e){if(e.kind!=="stdin"&&e.kind!=="context-file"&&e.repeatable){if(!Array.isArray(n))return;const i=n.map(t=>L(t,e));return i.some(t=>t===void 0)?void 0:i}return L(n,e)}d(ue,"decodeParameter");function L(n,e){if(e.kind==="stdin"||e.kind==="positional")return typeof n=="string"?n:void 0;if(e.kind!=="context-file"){if(e.type==="string")return typeof n=="string"?n:void 0;if(e.type==="int")return typeof n=="number"&&Number.isInteger(n)?n:void 0;if(e.type==="bool")return typeof n=="boolean"?n:void 0;if(e.type==="enum")return typeof n=="string"&&Array.isArray(e.choices)&&e.choices.includes(n)?n:void 0;if(e.type==="file")return typeof n=="string"?n:void 0;if(e.type==="path")return typeof n!="string"?void 0:e.encoding==="base64"?fe(n):n}}d(L,"decodeScalar");function fe(n){if(!(!/^[A-Za-z0-9+/]*={0,2}$/.test(n)||n.length%4!==0))try{const e=atob(n);return Uint8Array.from(e,i=>i.charCodeAt(0))}catch{return}}d(fe,"decodeBase64");function le(n,e){let i,t;try{i=JSON.stringify(n),t=JSON.parse(i)}catch{throw S("result")}if(!M(t))throw S("result");const r=K(e.output,t);if(r!==null)throw S(r.field,r.receivedType);for(const o of Object.keys(t))if(!e.output.some(f=>f.name===o))throw S(o);return new Response(i,{headers:{"Content-Type":"application/json"}})}d(le,"resultResponse");async function he(n,e,i){if(!we(n))throw S("result");const t=n[Symbol.asyncIterator](),r=new TextEncoder,o=await t.next();let f=o.done,a=f?void 0:r.encode(`${JSON.stringify(o.value)}
2
- `);const E=new ReadableStream({async pull(h){if(a!==void 0){h.enqueue(a),a=void 0;return}if(f){h.close();return}try{const w=await t.next();w.done?(f=!0,h.close()):h.enqueue(r.encode(`${JSON.stringify(w.value)}
3
- `))}catch(w){C(e,w,i),h.error(w)}},async cancel(h){await t.return?.(h)}});return new Response(E,{headers:{"Content-Type":"application/x-ndjson"}})}d(he,"streamResponse");function C(n,e,i,t){try{n?.(e,i,t)}catch{}}d(C,"reportUnhandledError");function S(n,e){return new I({code:"handler_output_invalid",message:`Handler returned an invalid value for ${n}.`,status:500,field:n,...e!==void 0?{received:e}:{}})}d(S,"outputInvalid");function N(n,e,i=!1,t){return new I({code:i?"invalid_argument":"invalid_input",message:n,...e!==void 0?{field:e}:{},...i?{received:t}:{}})}d(N,"invalidInput");function z(){return x(404,{code:"no_such_command",message:"No command matches this request."})}d(z,"noSuchCommand");function F(n){return n==="/"?"":n.replace(/\/+$/,"")}d(F,"mountPath");function me(n){return n.replace(/-([a-z])/g,(e,i)=>i.toUpperCase())}d(me,"camel");function V(n,e){const i=new TextEncoder().encode(n),t=new TextEncoder().encode(e);let r=i.length^t.length;for(let o=0;o<Math.min(i.length,t.length);o+=1)r|=i[o]^t[o];return r===0}d(V,"constantTimeEqual");function M(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}d(M,"isRecord");function we(n){return typeof n=="object"&&n!==null&&Symbol.asyncIterator in n}d(we,"isAsyncIterable");export{$e as createFetchHandler};
1
+ var W=Object.defineProperty;var c=(n,e)=>W(n,"name",{value:e,configurable:!0});import{approvalFor as Z}from"./approval.js";import{requestSpendCheck as K}from"./receipts.js";import{validateDeclaredResult as Q}from"@north-light/api/command-manifest";import{errorResponse as $,LeafError as R,leafErrorResponse as B,storedAnswerHeaders as X}from"./errors.js";import{buildCommandManifest as Y}from"./manifest.js";import{buildBundle as ee}from"./bundle.js";import{createCallerVerifier as ne,normalizeProviderId as te,scopeMissing as re}from"./verify.js";const P=/^v([1-9][0-9]*)$/;function $e(n,e={}){if(e.token!==void 0&&e.auth!==void 0)throw new Error("createFetchHandler takes either token or auth, not both");if(e.token!==void 0&&e.receipts!==void 0)throw new Error("createFetchHandler cannot meter with a static token");if(e.receipts!==void 0&&!/^app:[A-Za-z0-9_-]{1,64}$/.test(e.receipts.provider))throw new Error("createFetchHandler receipts.provider must be an app principal (app:<id>)");const i=e.auth===void 0?void 0:te(e.auth),t=Array.isArray(n)||n.version!==void 0,r=de(Array.isArray(n)?n:[n],Array.isArray(n),i),o=r[0].plugin.name;for(const u of r)for(const g of u.commands.values())if(g.leaf.sideEffect&&e.requests===void 0)throw new Error(`createFetchHandler needs requests for side-effect leaf ${g.path.join(" ")}`);const f=e.auth===void 0?void 0:ne(e.auth);if(f!==void 0){for(const u of r)for(const g of u.commands.values())if(g.scope===void 0)throw new Error(`createFetchHandler auth needs a group on every leaf; ${g.path.join(" ")} has none`)}const d=new Map,E=e.baseUrl===void 0?void 0:F(new URL(e.baseUrl).pathname),h=r[r.length-1];e.token===void 0&&e.auth===void 0&&console.warn(`crouter plugin ${o} is serving without bearer authentication`);const w=c(async(u,g,T)=>{const s=`${g.version}\0${T}`;let l=d.get(s);if(l===void 0){const A=await ee(g.plugin,{mountPath:T,...e.baseUrl!==void 0?{baseUrl:e.baseUrl}:{}});l={tar:A.tar,etag:A.etag},d.set(s,l)}const p={ETag:l.etag};return u.headers.get("if-none-match")===l.etag?new Response(null,{status:304,headers:p}):new Response(new Uint8Array(l.tar).buffer,{headers:{...p,"Content-Type":"application/x-tar"}})},"archiveResponse");return async u=>{if(u.method!=="POST"&&e.token!==void 0&&!V(u.headers.get("authorization")??"",`Bearer ${e.token}`))return $(401,{code:"unauthorized",message:"A valid bearer token is required.",next:"Send Authorization: Bearer <token>."});const g=new URL(u.url).pathname;if(u.method==="GET"){if(!t)return w(u,h,E??F(g));const m=H(g),S=m[m.length-1],_=E??F(`/${m.slice(0,-1).join("/")}`);if(S==="versions")return Response.json({latest:h.version,supported:r.map(j=>j.version)});const q=S===void 0?null:P.exec(S),b=q===null?void 0:r.find(j=>j.version===Number(q[1]));return b===void 0?U():w(u,b,`${_}/v${b.version}`)}if(u.method!=="POST")return U();const T=performance.now();let s,l,p,A=!1,z=!1;const k=c(m=>{if(l!==void 0)try{e.onAnswer?.({...s===void 0?{}:{sub:s.sub,grantee:s.grantee,initiatingApp:s.initiatingApp,act:s.act,jti:s.jti,requestId:s.requestId,...s.runId===void 0?{}:{runId:s.runId}},...s===void 0&&y!==null&&y!==""?{requestId:y}:{},tool:l.path.slice(1).join(" "),status:m.status,durationMs:performance.now()-T,...p===void 0?{}:{receiptId:p.receipt_id},replayed:z})}catch{}return m},"answer");let y=u.headers.get("crtr-request-id");try{if(l=t?ce(r,g):se(h.commands,g),e.token!==void 0&&!V(u.headers.get("authorization")??"",`Bearer ${e.token}`))return k($(401,{code:"unauthorized",message:"A valid bearer token is required.",next:"Send Authorization: Bearer <token>."}));const m=f===void 0?void 0:await f(u);if(l===void 0)return U();if(m!==void 0&&!m.scope.includes(l.scope)){s=Object.freeze({...m,requestId:y??"",...u.headers.get("crtr-run-id")?{runId:u.headers.get("crtr-run-id")}:{}});const a=l.leaf.group,v=i===void 0&&a!==void 0?m.scope.find(x=>x.endsWith(`:${a}`)&&x.length>a.length+1)?.slice(0,-(a.length+1)):void 0;throw re(l.scope,v)}if(m!==void 0){if(y===null||y==="")return k($(400,{code:"invalid_request",message:"The request carries no Crtr-Request-Id header."}));const a=u.headers.get("crtr-run-id");s=Object.freeze({...m,requestId:y,...a!==null&&a!==""?{runId:a}:{}})}const S=await ue(u,l.manifest,m!==void 0);if(l.leaf.sideEffect){if(y===null||y==="")return k($(400,{code:"invalid_request",message:"The request carries no Crtr-Request-Id header."}));const a=s===void 0?null:{sub:s.sub,grantee:s.grantee};if(!await e.requests.record(y,a)){z=!0;const x=await oe(e.requests,y,a,l.plugin.timeouts?.requestMs??3e4);if(x!==!0){const J=x.headers.get("Crtr-Receipt");if(J!==null)try{p=JSON.parse(J)}catch{}return k(x)}z=!1}A=!0}const _=l,q={request:u,...s!==void 0?{caller:s}:{},approval:Z(u),meter(a){if(s===void 0)throw new Error("ctx.meter needs an authenticated caller");if(p!==void 0)throw new Error("ctx.meter may be called only once");if(e.receipts===void 0)throw new Error("ctx.meter needs receipts with a provider and outbox");const v=_.path.slice(1).join(" "),x=a.usdEquivalent??(e.receipts.rates[v]===void 0?void 0:a.units*e.receipts.rates[v].usdPerUnit);if(x===void 0)throw new Error(`No receipt rate for ${v}`);return p={receipt_id:s.requestId,provider:e.receipts.provider,tool:v,sub:s.sub,grantee:s.grantee,payer:s.payer,units:a.units,unit:a.unit,usd_equivalent:x,request_id:s.requestId,...s.runId!==void 0?{run_id:s.runId}:{},time:new Date().toISOString()},p},async checkSpend(){if(s===void 0)throw new Error("ctx.checkSpend needs an authenticated caller");if(e.receipts===void 0)throw new Error("ctx.checkSpend needs receipts with a provider and outbox");if(e.receipts.ledger===void 0)throw new Error("ctx.checkSpend needs receipts.ledger (the provider's directory client credentials)");const a=/^Bearer ([^\s]+)$/.exec(u.headers.get("authorization")??"")?.[1];if(a===void 0)throw new Error("ctx.checkSpend needs the request's bearer token");await K(e.auth.issuer,a,e.receipts.ledger)}};let b,j;try{const a=await l.leaf.run(S,q);b=l.leaf.streaming?await me(a,e.onError,l.path):he(a,l.manifest)}catch(a){a instanceof R?(b=B(a),j=a.headers):(O(e.onError,a,l.path),b=C(a))}if(A&&b.status===403&&await ie(b))return await e.requests.delete(y),k(D(b,p));try{if(A)await e.requests.store(y,{status:b.status,body:await b.clone().text(),...j!==void 0?{headers:{...j}}:{},...p!==void 0?{receipt:p}:{}});else if(p!==void 0){if(e.receipts===void 0)throw new Error(`No receipt outbox for ${_.path.join(" ")}`);await e.receipts.outbox.save(p)}}catch(a){return O(e.onError,a,l.path,p),k(C(a))}return k(D(b,p))}catch(m){return m instanceof R?k(B(m)):(O(e.onError,m,l?.path??[]),k(C(m)))}}}c($e,"createFetchHandler");function C(n){return $(500,{code:"handler_failed",message:n instanceof Error?n.message:String(n)})}c(C,"handlerFailed");function D(n,e){if(e===void 0)return n;const i=new Headers(n.headers);return i.set("Crtr-Receipt",JSON.stringify(e)),new Response(n.body,{status:n.status,headers:i})}c(D,"responseWithReceipt");async function ie(n){try{const e=await n.clone().json();return M(e)&&M(e.error)&&e.error.code==="approval_required"}catch{return!1}}c(ie,"isApprovalRequired");async function oe(n,e,i,t){for(;;){const r=await n.read(e);if(r===null){if(await n.record(e,i))return!0;continue}if(r.identity?.sub!==i?.sub||r.identity?.grantee!==i?.grantee)return $(403,{code:"provider_refused",message:"This request id belongs to a different caller."});if(r.answer!==null){const o=X(r.answer.headers);return o.set("Content-Type","application/json"),r.answer.receipt!==void 0&&o.set("Crtr-Receipt",JSON.stringify(r.answer.receipt)),new Response(r.answer.body,{status:r.answer.status,headers:o})}if(Date.now()>=new Date(r.recordedAt).getTime()+t)return C(new Error("The earlier request has no stored answer."));await new Promise(o=>setTimeout(o,Math.min(25,Math.max(1,new Date(r.recordedAt).getTime()+t-Date.now()))))}}c(oe,"replay");function de(n,e,i){if(n.length===0)throw new Error("createFetchHandler needs at least one plugin definition");const t=n[0].name,r=new Map;for(const o of n){if(o.name!==t)throw new Error(`createFetchHandler serves one plugin; got ${t} and ${o.name}`);if(e&&o.version===void 0)throw new Error(`createFetchHandler needs an explicit version on every definition in an array; ${t} has none`);const f=o.version??1;if(r.has(f))throw new Error(`createFetchHandler got two definitions of ${t} major ${f}`);r.set(f,{version:f,plugin:o,commands:ae(o,i)})}return[...r.values()].sort((o,f)=>o.version-f.version)}c(de,"servedMajors");function ae(n,e){const t=Y(n,{mountPath:""}).mounts[0]?.node;if(t===void 0||t.kind!=="branch")throw new Error("Generated plugin manifest has no root branch");const r=new Map;return G(t.children,n.commands,[t.name],n,r,e),r}c(ae,"commandMap");function G(n,e,i,t,r,o){const f=Object.entries(e);if(n.length!==f.length)throw new Error("Generated plugin manifest does not match the command definition");for(const[d,[E,h]]of f.entries()){const w=n[d];if(w===void 0||w.kind!==h.kind)throw new Error(`Generated plugin manifest does not match command ${E}`);const u=[...i,w.name];if(w.kind==="branch"&&h.kind==="branch"){G(w.children,h.children,u,t,r,o);continue}if(w.kind==="leaf"&&h.kind==="leaf"){r.set(u.join("/"),{path:u,leaf:h,plugin:t,manifest:w,...h.group!==void 0?{scope:`${o??t.name}:${h.group}`}:{}});continue}throw new Error(`Generated plugin manifest does not match command ${E}`)}}c(G,"collectCommands");function se(n,e){const i=H(e);for(let t=0;t<i.length;t+=1){const r=n.get(i.slice(t).join("/"));if(r!==void 0)return r}}c(se,"findCommand");function ce(n,e){const i=H(e);for(let t=i.length-2;t>=0;t-=1){const r=P.exec(i[t]);if(r===null)continue;const f=n.find(d=>d.version===Number(r[1]))?.commands.get(i.slice(t+1).join("/"));if(f!==void 0)return f}}c(ce,"findVersionedCommand");function H(n){return n.split("/").filter(e=>e.length>0)}c(H,"pathSegments");async function ue(n,e,i){const t=n.body===null?"":await n.text();let r;if(t.length===0)r={};else try{const d=JSON.parse(t);if(!M(d))throw new Error("not an object");r=d}catch{throw N("Request body must be a JSON object.",i?"body":void 0,i,t)}const o=new Map(e.params.map(d=>[d.name,d]));for(const d of Object.keys(r))if(!o.has(d))throw i?N(`Unknown parameter ${d}.`,d,!0,r[d]):new R({code:"unknown_parameter",message:`Unknown parameter ${d}.`,field:d});const f={};for(const d of e.params){if(!Object.hasOwn(r,d.name)){if(d.required)throw N(`Missing required parameter ${d.name}.`,d.name,i,null);continue}const E=r[d.name],h=fe(E,d);if(h===void 0)throw N(`Invalid value for parameter ${d.name}.`,d.name,i,E);f[we(d.name)]=h}return f}c(ue,"decodeInput");function fe(n,e){if(e.kind!=="stdin"&&e.kind!=="context-file"&&e.repeatable){if(!Array.isArray(n))return;const i=n.map(t=>L(t,e));return i.some(t=>t===void 0)?void 0:i}return L(n,e)}c(fe,"decodeParameter");function L(n,e){if(e.kind==="stdin"||e.kind==="positional")return typeof n=="string"?n:void 0;if(e.kind!=="context-file"){if(e.type==="string")return typeof n=="string"?n:void 0;if(e.type==="int")return typeof n=="number"&&Number.isInteger(n)?n:void 0;if(e.type==="bool")return typeof n=="boolean"?n:void 0;if(e.type==="enum")return typeof n=="string"&&Array.isArray(e.choices)&&e.choices.includes(n)?n:void 0;if(e.type==="file")return typeof n=="string"?n:void 0;if(e.type==="path")return typeof n!="string"?void 0:e.encoding==="base64"?le(n):n}}c(L,"decodeScalar");function le(n){if(!(!/^[A-Za-z0-9+/]*={0,2}$/.test(n)||n.length%4!==0))try{const e=atob(n);return Uint8Array.from(e,i=>i.charCodeAt(0))}catch{return}}c(le,"decodeBase64");function he(n,e){let i,t;try{i=JSON.stringify(n),t=JSON.parse(i)}catch{throw I("result")}if(!M(t))throw I("result");const r=Q(e.output,t);if(r!==null)throw I(r.field,r.receivedType);for(const o of Object.keys(t))if(!e.output.some(f=>f.name===o))throw I(o);return new Response(i,{headers:{"Content-Type":"application/json"}})}c(he,"resultResponse");async function me(n,e,i){if(!pe(n))throw I("result");const t=n[Symbol.asyncIterator](),r=new TextEncoder,o=await t.next();let f=o.done,d=f?void 0:r.encode(`${JSON.stringify(o.value)}
2
+ `);const E=new ReadableStream({async pull(h){if(d!==void 0){h.enqueue(d),d=void 0;return}if(f){h.close();return}try{const w=await t.next();w.done?(f=!0,h.close()):h.enqueue(r.encode(`${JSON.stringify(w.value)}
3
+ `))}catch(w){O(e,w,i),h.error(w)}},async cancel(h){await t.return?.(h)}});return new Response(E,{headers:{"Content-Type":"application/x-ndjson"}})}c(me,"streamResponse");function O(n,e,i,t){try{n?.(e,i,t)}catch{}}c(O,"reportUnhandledError");function I(n,e){return new R({code:"handler_output_invalid",message:`Handler returned an invalid value for ${n}.`,status:500,field:n,...e!==void 0?{received:e}:{}})}c(I,"outputInvalid");function N(n,e,i=!1,t){return new R({code:i?"invalid_argument":"invalid_input",message:n,...e!==void 0?{field:e}:{},...i?{received:t}:{}})}c(N,"invalidInput");function U(){return $(404,{code:"no_such_command",message:"No command matches this request."})}c(U,"noSuchCommand");function F(n){return n==="/"?"":n.replace(/\/+$/,"")}c(F,"mountPath");function we(n){return n.replace(/-([a-z])/g,(e,i)=>i.toUpperCase())}c(we,"camel");function V(n,e){const i=new TextEncoder().encode(n),t=new TextEncoder().encode(e);let r=i.length^t.length;for(let o=0;o<Math.min(i.length,t.length);o+=1)r|=i[o]^t[o];return r===0}c(V,"constantTimeEqual");function M(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}c(M,"isRecord");function pe(n){return typeof n=="object"&&n!==null&&Symbol.asyncIterator in n}c(pe,"isAsyncIterable");export{$e as createFetchHandler};
package/dist/verify.js CHANGED
@@ -1 +1 @@
1
- var p=Object.defineProperty;var n=(e,r)=>p(e,"name",{value:r,configurable:!0});import{JwksCache as f,verifyOutboundToken as l}from"@north-light/identity";import{kitLeafError as s}from"./errors.js";const h=/^[A-Za-z0-9_-]{1,64}$/;function w(e){if(e.providerId===void 0)return;const r=typeof e.providerId=="string"&&e.providerId.startsWith("app:")?e.providerId.slice(4):e.providerId;if(typeof r!="string"||!h.test(r))throw new Error("createFetchHandler auth.providerId must be a provider id (1-64 characters of A-Z a-z 0-9 _ -), bare or as app:<id>");return r}n(w,"normalizeProviderId");function k(e){if(typeof e!="object"||e===null)throw new Error("createFetchHandler auth must be {issuer, audience}");if(typeof e.issuer!="string"||e.issuer==="")throw new Error("createFetchHandler auth.issuer must be a non-empty string");if(typeof e.audience!="string"||e.audience==="")throw new Error("createFetchHandler auth.audience must be a non-empty string");w(e);const{issuer:r,audience:c}=e,d=new f(r,(o,i)=>fetch(o,i),`${r.replace(/\/+$/,"")}/.well-known/jwks.json`);return async o=>{const i=/^Bearer ([^\s]+)$/.exec(o.headers.get("authorization")??"");if(i===null)throw a("The request carries no bearer token.");let t;try{t=await l({token:i[1],jwks:d,issuer:r,audience:c})}catch{throw a("The bearer token is not a valid outbound token for this provider.")}return Object.freeze({sub:t.sub,grantee:t.grantee,clientId:t.client_id,scope:Object.freeze(t.scope.split(" ").filter(u=>u.length>0)),initiatingApp:t.initiating_app,act:t.act,jti:t.jti})}}n(k,"createCallerVerifier");function v(e,r){return s({code:"scope_missing",status:403,message:`The token does not carry ${e}, which this command needs.`,field:e,received:e,...r!==void 0?{next:`Set auth.providerId to your app's client id (${r}).`}:{}})}n(v,"scopeMissing");function a(e){return s({code:"unauthorized",status:401,message:e,next:"Send a current outbound token for this provider as Authorization: Bearer <token>."})}n(a,"unauthorized");export{k as createCallerVerifier,w as normalizeProviderId,v as scopeMissing};
1
+ var f=Object.defineProperty;var n=(e,r)=>f(e,"name",{value:r,configurable:!0});import{JwksCache as h,verifyOutboundToken as l}from"@north-light/identity";import{kitLeafError as d}from"./errors.js";const b=/^[A-Za-z0-9_-]{1,64}$/;function w(e){if(e.providerId===void 0)return;const r=typeof e.providerId=="string"&&e.providerId.startsWith("app:")?e.providerId.slice(4):e.providerId;if(typeof r!="string"||!b.test(r))throw new Error("createFetchHandler auth.providerId must be a provider id (1-64 characters of A-Z a-z 0-9 _ -), bare or as app:<id>");return r}n(w,"normalizeProviderId");function v(e){if(typeof e!="object"||e===null)throw new Error("createFetchHandler auth must be {issuer, audience}");if(typeof e.issuer!="string"||e.issuer==="")throw new Error("createFetchHandler auth.issuer must be a non-empty string");if(typeof e.audience!="string"||e.audience==="")throw new Error("createFetchHandler auth.audience must be a non-empty string");w(e);const{issuer:r,audience:c}=e,u=new h(r,(s,i)=>fetch(s,i),`${r.replace(/\/+$/,"")}/.well-known/jwks.json`);return async s=>{const i=/^Bearer ([^\s]+)$/.exec(s.headers.get("authorization")??"");if(i===null)throw a("The request carries no bearer token.");let t;try{t=await l({token:i[1],jwks:u,issuer:r,audience:c})}catch{throw a("The bearer token is not a valid outbound token for this provider.")}const o=t.payer;if(o!==void 0&&!/^(user|app):[A-Za-z0-9_-]{1,64}$/.test(String(o)))throw a("The bearer token is not a valid outbound token for this provider.");return Object.freeze({sub:t.sub,grantee:t.grantee,payer:o===void 0?t.sub:String(o),clientId:t.client_id,scope:Object.freeze(t.scope.split(" ").filter(p=>p.length>0)),initiatingApp:t.initiating_app,act:t.act,jti:t.jti})}}n(v,"createCallerVerifier");function y(e,r){return d({code:"scope_missing",status:403,message:`The token does not carry ${e}, which this command needs.`,field:e,received:e,...r!==void 0?{next:`Set auth.providerId to your app's client id (${r}).`}:{}})}n(y,"scopeMissing");function a(e){return d({code:"unauthorized",status:401,message:e,next:"Send a current outbound token for this provider as Authorization: Bearer <token>."})}n(a,"unauthorized");export{v as createCallerVerifier,w as normalizeProviderId,y as scopeMissing};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/plugin",
3
- "version": "0.3.480",
3
+ "version": "0.3.481",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -23,8 +23,8 @@
23
23
  "postpack": "node ../../scripts/pack-minify.mjs postpack packages/crouter-plugin"
24
24
  },
25
25
  "dependencies": {
26
- "@north-light/api": "0.3.480",
27
- "@north-light/identity": "0.3.480"
26
+ "@north-light/api": "0.3.481",
27
+ "@north-light/identity": "0.3.481"
28
28
  },
29
29
  "license": "UNLICENSED",
30
30
  "publishConfig": {