@north-light/plugin 0.3.456 → 0.3.458
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 +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/dist/node.d.ts +43 -0
- package/dist/node.js +1 -0
- package/dist/serve.js +3 -3
- package/dist/verify.d.ts +11 -1
- package/dist/verify.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -105,6 +105,23 @@ crtr acme app create my-app --region eu-west
|
|
|
105
105
|
|
|
106
106
|
Use the actual mount path in `--endpoint`. The generated archive declares command paths below that path, while crtr stores the origin for later command calls. Installing only `https://acme.example.com` would send calls to the origin root instead of `/crtr`.
|
|
107
107
|
|
|
108
|
+
## Serving as an app registered in the directory
|
|
109
|
+
|
|
110
|
+
An app that serves commands to people who connected it passes directory authentication instead of a static token:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { createFetchHandler, createNodeHandler } from '@north-light/plugin';
|
|
114
|
+
|
|
115
|
+
const handler = createFetchHandler(plugin, {
|
|
116
|
+
auth: { issuer: 'https://directory.example', audience: 'https://acme.example.com/crtr', providerId: CLIENT_ID },
|
|
117
|
+
requests,
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`providerId` is the app's client id (its OAuth client id), bare or as `app:<id>`. The directory mints scopes as `<provider id>:<group>`, so the kit requires `<providerId>:<group>` on each leaf. Without `providerId` it requires `<plugin name>:<group>`, which only a first-party provider whose name equals its id carries — every call from any other app answers 403 `scope_missing`, and the error's `next` names the id to set.
|
|
122
|
+
|
|
123
|
+
On Node, Express or Nest, mount the Fetch handler with `createNodeHandler(handler, { bodyLimit?, origin? })`. It turns the request into a Fetch `Request` (using `req.rawBody` when the framework buffered it, else the stream), refuses a body over `bodyLimit` (default 65536 bytes) with 413 before the handler runs, streams the response with its repeated headers and `set-cookie`, and answers 500 if the handler throws. In Nest, a controller method is `@All(['', '*path']) serve(@Req() req, @Res() res) { return node(req, res); }`; with `node:http`, `createServer((req, res) => void node(req, res))`. Pass `origin` when the public origin differs from the Host header.
|
|
124
|
+
|
|
108
125
|
## Metered provider tools
|
|
109
126
|
|
|
110
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.
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export * from './manifest.js';
|
|
|
5
5
|
export { compareManifests, type BreakingManifestChange } from './compare.js';
|
|
6
6
|
export * from './bundle.js';
|
|
7
7
|
export * from './serve.js';
|
|
8
|
+
export { createNodeHandler, type NodeHandlerOptions, type NodeLikeRequest, type NodeLikeResponse } from './node.js';
|
|
8
9
|
export { createCallerVerifier, type CallerVerifier, type FetchHandlerAuth } from './verify.js';
|
|
9
10
|
export { postReceipt, ReceiptPostError, type Receipt, type ReceiptOptions, type ReceiptOutbox } from './receipts.js';
|
|
10
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{createCallerVerifier as
|
|
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};
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
type Chunk = Uint8Array | string;
|
|
2
|
+
/**
|
|
3
|
+
* The part of a Node `IncomingMessage` — or an Express / Nest request — that `createNodeHandler`
|
|
4
|
+
* reads. Structural, so the kit needs neither `node:http` nor a framework's types.
|
|
5
|
+
*/
|
|
6
|
+
export interface NodeLikeRequest {
|
|
7
|
+
readonly method?: string;
|
|
8
|
+
readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
9
|
+
readonly url?: string;
|
|
10
|
+
/** Express sets this to the unmodified URL when a router has rewritten `url`; it wins over `url`. */
|
|
11
|
+
readonly originalUrl?: string;
|
|
12
|
+
/** The whole body when a framework already buffered it (Nest with `rawBody: true`). */
|
|
13
|
+
readonly rawBody?: Chunk;
|
|
14
|
+
/** The body stream, read only when `rawBody` is absent. */
|
|
15
|
+
readonly [Symbol.asyncIterator]?: () => AsyncIterator<Chunk>;
|
|
16
|
+
}
|
|
17
|
+
/** The part of a Node `ServerResponse` (or an Express response) that `createNodeHandler` writes. */
|
|
18
|
+
export interface NodeLikeResponse {
|
|
19
|
+
statusCode: number;
|
|
20
|
+
setHeader(name: string, value: string | string[]): unknown;
|
|
21
|
+
write(chunk: Uint8Array): boolean;
|
|
22
|
+
end(chunk?: Uint8Array | string): unknown;
|
|
23
|
+
once(event: 'drain' | 'close' | 'error', listener: () => void): unknown;
|
|
24
|
+
removeListener?(event: 'drain' | 'close' | 'error', listener: () => void): unknown;
|
|
25
|
+
/** True once the response has been fully flushed; a `close` before that is a client abort. */
|
|
26
|
+
readonly writableFinished?: boolean;
|
|
27
|
+
/** Tears the connection down; used when a response stream fails after its headers went out. */
|
|
28
|
+
destroy?(error?: Error): unknown;
|
|
29
|
+
}
|
|
30
|
+
export interface NodeHandlerOptions {
|
|
31
|
+
/** The largest request body accepted, in bytes. Defaults to 65536. */
|
|
32
|
+
readonly bodyLimit?: number;
|
|
33
|
+
/** The public origin the request URL is resolved against, e.g. `https://directory.example`. Defaults to `http://<Host header>`. */
|
|
34
|
+
readonly origin?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Mounts a `createFetchHandler` handler in Node's `http`, Express or Nest: the request becomes a
|
|
38
|
+
* Fetch `Request` and the `Response` is streamed back with its status, headers (repeated headers
|
|
39
|
+
* and `set-cookie` included) and body. A body over `bodyLimit` is refused 413 before the handler
|
|
40
|
+
* runs, and a handler that throws answers 500; neither leaves an unhandled rejection.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createNodeHandler(handler: (request: Request) => Promise<Response>, options?: NodeHandlerOptions): (req: NodeLikeRequest, res: NodeLikeResponse) => Promise<void>;
|
|
43
|
+
export {};
|
package/dist/node.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var y=Object.defineProperty;var u=(e,o)=>y(e,"name",{value:o,configurable:!0});import{errorResponse as f}from"./errors.js";const g=65536;function R(e,o={}){const s=o.bodyLimit??g;if(!Number.isInteger(s)||s<0)throw new Error("createNodeHandler bodyLimit must be a non-negative integer");const l=u(()=>f(413,{code:"invalid_request",message:`The request body is larger than ${s} bytes.`}),"tooLarge");return async(r,c)=>{const d=new AbortController;let i=!1;const t={};c.once("close",()=>{c.writableFinished!==!0&&(i=!0,d.abort(),t.current?.())});let n;try{const a=await b(r,o.origin,s,d.signal);n=a===null?l():await e(a)}catch(a){n=a instanceof h?f(400,{code:"invalid_request",message:a.message}):f(500,{code:"handler_failed",message:"The request failed."})}try{await E(c,n,r.method==="HEAD",()=>i,t)}catch(a){c.destroy?.(a instanceof Error?a:void 0)}}}u(R,"createNodeHandler");class h extends Error{static{u(this,"BadRequest")}}async function b(e,o,s,l){const r=new Headers;for(const[t,n]of Object.entries(e.headers))if(!(n===void 0||t.startsWith(":")))if(typeof n=="string")r.set(t,n);else for(const a of n)r.append(t,a);const c=e.method??"GET";let d;try{d=new URL(e.originalUrl??e.url??"/",o??`http://${r.get("host")??"localhost"}`)}catch{throw new h("The request URL is not valid.")}const i={method:c,headers:r,signal:l};if(c!=="GET"&&c!=="HEAD"){if(Number(r.get("content-length")??0)>s)return null;const n=e.rawBody!==void 0?w(e.rawBody):await m(e,s);if(n===null||n.length>s)return null;n.length>0&&(i.body=n)}return new Request(d,i)}u(b,"toRequest");async function m(e,o){const s=e[Symbol.asyncIterator];if(s===void 0)return new Uint8Array(0);const l=[];let r=0;const c=s.call(e);for(let t=await c.next();t.done!==!0;t=await c.next()){const n=w(t.value);if(r+=n.length,r>o)return null;l.push(n)}const d=new Uint8Array(r);let i=0;for(const t of l)d.set(t,i),i+=t.length;return d}u(m,"readBounded");function w(e){return typeof e=="string"?new TextEncoder().encode(e):e}u(w,"bytes");async function E(e,o,s,l,r){e.statusCode=o.status,o.headers.forEach((i,t)=>{t.toLowerCase()!=="set-cookie"&&e.setHeader(t,i)});const c=o.headers.getSetCookie();if(c.length>0&&e.setHeader("set-cookie",c),o.body===null||s){await o.body?.cancel().catch(()=>{}),e.end();return}const d=o.body.getReader();r.current=()=>{d.cancel().catch(()=>{})},l()&&r.current();try{for(;;){if(l()){await d.cancel();return}const{done:i,value:t}=await d.read();if(i)break;!e.write(t)&&!l()&&await new Promise(n=>{const a=u(()=>{e.removeListener?.("drain",a),e.removeListener?.("close",a),n()},"done");e.once("drain",a),e.once("close",a)})}}catch(i){throw await d.cancel().catch(()=>{}),i}l()||e.end()}u(E,"writeResponse");export{R as createNodeHandler};
|
package/dist/serve.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var V=Object.defineProperty;var s=(n,e)=>V(n,"name",{value:e,configurable:!0});import{approvalFor as Z}from"./approval.js";import{validateDeclaredResult as W}from"@north-light/api/command-manifest";import{errorResponse as j,LeafError as R,leafErrorResponse as J,storedAnswerHeaders as K}from"./errors.js";import{buildCommandManifest as Q}from"./manifest.js";import{buildBundle as X}from"./bundle.js";import{createCallerVerifier as Y,scopeMissing as ee}from"./verify.js";const B=/^v([1-9][0-9]*)$/;function ke(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 t=Array.isArray(n)||n.version!==void 0,r=re(Array.isArray(n)?n:[n],Array.isArray(n)),i=r[0].plugin.name;for(const d of r)for(const y of d.commands.values())if(y.leaf.sideEffect&&e.requests===void 0)throw new Error(`createFetchHandler needs requests for side-effect leaf ${y.path.join(" ")}`);const a=e.auth===void 0?void 0:Y(e.auth);if(a!==void 0){for(const d of r)for(const y of d.commands.values())if(y.scope===void 0)throw new Error(`createFetchHandler auth needs a group on every leaf; ${y.path.join(" ")} has none`)}const m=new Map,o=e.baseUrl===void 0?void 0:z(new URL(e.baseUrl).pathname),w=r[r.length-1];e.token===void 0&&e.auth===void 0&&console.warn(`crouter plugin ${i} is serving without bearer authentication`);const l=s(async(d,y,S)=>{const c=`${y.version}\0${S}`;let u=m.get(c);if(u===void 0){const $=await X(y.plugin,{mountPath:S,...e.baseUrl!==void 0?{baseUrl:e.baseUrl}:{}});u={tar:$.tar,etag:$.etag},m.set(c,u)}const p={ETag:u.etag};return d.headers.get("if-none-match")===u.etag?new Response(null,{status:304,headers:p}):new Response(new Uint8Array(u.tar).buffer,{headers:{...p,"Content-Type":"application/x-tar"}})},"archiveResponse");return async d=>{if(d.method!=="POST"&&e.token!==void 0&&!L(d.headers.get("authorization")??"",`Bearer ${e.token}`))return j(401,{code:"unauthorized",message:"A valid bearer token is required.",next:"Send Authorization: Bearer <token>."});const y=new URL(d.url).pathname;if(d.method==="GET"){if(!t)return l(d,w,o??z(y));const h=H(y),x=h[h.length-1],T=o??z(`/${h.slice(0,-1).join("/")}`);if(x==="versions")return Response.json({latest:w.version,supported:r.map(k=>k.version)});const _=x===void 0?null:B.exec(x),b=_===null?void 0:r.find(k=>k.version===Number(_[1]));return b===void 0?U():l(d,b,`${T}/v${b.version}`)}if(d.method!=="POST")return U();const S=performance.now();let c,u,p,$=!1,M=!1;const E=s(h=>{if(u!==void 0)try{e.onAnswer?.({...c===void 0?{}:{sub:c.sub,grantee:c.grantee,initiatingApp:c.initiatingApp,act:c.act,jti:c.jti,requestId:c.requestId,...c.runId===void 0?{}:{runId:c.runId}},...c===void 0&&g!==null&&g!==""?{requestId:g}:{},tool:u.path.slice(1).join(" "),status:h.status,durationMs:performance.now()-S,...p===void 0?{}:{receiptId:p.receipt_id},replayed:M})}catch{}return h},"answer");let g=d.headers.get("crtr-request-id");try{if(u=t?ae(r,y):oe(w.commands,y),e.token!==void 0&&!L(d.headers.get("authorization")??"",`Bearer ${e.token}`))return E(j(401,{code:"unauthorized",message:"A valid bearer token is required.",next:"Send Authorization: Bearer <token>."}));const h=a===void 0?void 0:await a(d);if(u===void 0)return U();if(h!==void 0&&!h.scope.includes(u.scope))throw c=Object.freeze({...h,requestId:g??"",...d.headers.get("crtr-run-id")?{runId:d.headers.get("crtr-run-id")}:{}}),ee(u.scope);if(h!==void 0){if(g===null||g==="")return E(j(400,{code:"invalid_request",message:"The request carries no Crtr-Request-Id header."}));const f=d.headers.get("crtr-run-id");c=Object.freeze({...h,requestId:g,...f!==null&&f!==""?{runId:f}:{}})}const x=await de(d,u.manifest,h!==void 0);if(u.leaf.sideEffect){if(g===null||g==="")return E(j(400,{code:"invalid_request",message:"The request carries no Crtr-Request-Id header."}));const f=c===void 0?null:{sub:c.sub,grantee:c.grantee};if(!await e.requests.record(g,f)){M=!0;const v=await te(e.requests,g,f,u.plugin.timeouts?.requestMs??3e4);if(v!==!0){const F=v.headers.get("Crtr-Receipt");if(F!==null)try{p=JSON.parse(F)}catch{}return E(v)}M=!1}$=!0}const T=u,_={request:d,...c!==void 0?{caller:c}:{},approval:Z(d),meter(f){if(c===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 A=T.path.slice(1).join(" "),v=f.usdEquivalent??(e.receipts.rates[A]===void 0?void 0:f.units*e.receipts.rates[A].usdPerUnit);if(v===void 0)throw new Error(`No receipt rate for ${A}`);return p={receipt_id:c.requestId,provider:e.receipts.provider,tool:A,sub:c.sub,grantee:c.grantee,payer:c.sub,units:f.units,unit:f.unit,usd_equivalent:v,request_id:c.requestId,...c.runId!==void 0?{run_id:c.runId}:{},time:new Date().toISOString()},p}};let b,k;try{const f=await u.leaf.run(x,_);b=u.leaf.streaming?await fe(f,e.onError,u.path):ue(f,u.manifest)}catch(f){f instanceof R?(b=J(f),k=f.headers):(q(e.onError,f,u.path),b=O(f))}if($&&b.status===403&&await ne(b))return await e.requests.delete(g),E(P(b,p));try{if($)await e.requests.store(g,{status:b.status,body:await b.clone().text(),...k!==void 0?{headers:{...k}}:{},...p!==void 0?{receipt:p}:{}});else if(p!==void 0){if(e.receipts===void 0)throw new Error(`No receipt outbox for ${T.path.join(" ")}`);await e.receipts.outbox.save(p)}}catch(f){return q(e.onError,f,u.path,p),E(O(f))}return E(P(b,p))}catch(h){return h instanceof R?E(J(h)):(q(e.onError,h,u?.path??[]),E(O(h)))}}}s(ke,"createFetchHandler");function O(n){return j(500,{code:"handler_failed",message:n instanceof Error?n.message:String(n)})}s(O,"handlerFailed");function P(n,e){if(e===void 0)return n;const t=new Headers(n.headers);return t.set("Crtr-Receipt",JSON.stringify(e)),new Response(n.body,{status:n.status,headers:t})}s(P,"responseWithReceipt");async function ne(n){try{const e=await n.clone().json();return N(e)&&N(e.error)&&e.error.code==="approval_required"}catch{return!1}}s(ne,"isApprovalRequired");async function te(n,e,t,r){for(;;){const i=await n.read(e);if(i===null){if(await n.record(e,t))return!0;continue}if(i.identity?.sub!==t?.sub||i.identity?.grantee!==t?.grantee)return j(403,{code:"provider_refused",message:"This request id belongs to a different caller."});if(i.answer!==null){const a=K(i.answer.headers);return a.set("Content-Type","application/json"),i.answer.receipt!==void 0&&a.set("Crtr-Receipt",JSON.stringify(i.answer.receipt)),new Response(i.answer.body,{status:i.answer.status,headers:a})}if(Date.now()>=new Date(i.recordedAt).getTime()+r)return O(new Error("The earlier request has no stored answer."));await new Promise(a=>setTimeout(a,Math.min(25,Math.max(1,new Date(i.recordedAt).getTime()+r-Date.now()))))}}s(te,"replay");function re(n,e){if(n.length===0)throw new Error("createFetchHandler needs at least one plugin definition");const t=n[0].name,r=new Map;for(const i of n){if(i.name!==t)throw new Error(`createFetchHandler serves one plugin; got ${t} and ${i.name}`);if(e&&i.version===void 0)throw new Error(`createFetchHandler needs an explicit version on every definition in an array; ${t} has none`);const a=i.version??1;if(r.has(a))throw new Error(`createFetchHandler got two definitions of ${t} major ${a}`);r.set(a,{version:a,plugin:i,commands:ie(i)})}return[...r.values()].sort((i,a)=>i.version-a.version)}s(re,"servedMajors");function ie(n){const t=Q(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 D(t.children,n.commands,[t.name],n,r),r}s(ie,"commandMap");function D(n,e,t,r,i){const a=Object.entries(e);if(n.length!==a.length)throw new Error("Generated plugin manifest does not match the command definition");for(const[m,[o,w]]of a.entries()){const l=n[m];if(l===void 0||l.kind!==w.kind)throw new Error(`Generated plugin manifest does not match command ${o}`);const d=[...t,l.name];if(l.kind==="branch"&&w.kind==="branch"){D(l.children,w.children,d,r,i);continue}if(l.kind==="leaf"&&w.kind==="leaf"){i.set(d.join("/"),{path:d,leaf:w,plugin:r,manifest:l,...w.group!==void 0?{scope:`${r.name}:${w.group}`}:{}});continue}throw new Error(`Generated plugin manifest does not match command ${o}`)}}s(D,"collectCommands");function oe(n,e){const t=H(e);for(let r=0;r<t.length;r+=1){const i=n.get(t.slice(r).join("/"));if(i!==void 0)return i}}s(oe,"findCommand");function ae(n,e){const t=H(e);for(let r=t.length-2;r>=0;r-=1){const i=B.exec(t[r]);if(i===null)continue;const m=n.find(o=>o.version===Number(i[1]))?.commands.get(t.slice(r+1).join("/"));if(m!==void 0)return m}}s(ae,"findVersionedCommand");function H(n){return n.split("/").filter(e=>e.length>0)}s(H,"pathSegments");async function de(n,e,t){const r=n.body===null?"":await n.text();let i;if(r.length===0)i={};else try{const o=JSON.parse(r);if(!N(o))throw new Error("not an object");i=o}catch{throw C("Request body must be a JSON object.",t?"body":void 0,t,r)}const a=new Map(e.params.map(o=>[o.name,o]));for(const o of Object.keys(i))if(!a.has(o))throw t?C(`Unknown parameter ${o}.`,o,!0,i[o]):new R({code:"unknown_parameter",message:`Unknown parameter ${o}.`,field:o});const m={};for(const o of e.params){if(!Object.hasOwn(i,o.name)){if(o.required)throw C(`Missing required parameter ${o.name}.`,o.name,t,null);continue}const w=i[o.name],l=se(w,o);if(l===void 0)throw C(`Invalid value for parameter ${o.name}.`,o.name,t,w);m[le(o.name)]=l}return m}s(de,"decodeInput");function se(n,e){if(e.kind!=="stdin"&&e.kind!=="context-file"&&e.repeatable){if(!Array.isArray(n))return;const t=n.map(r=>G(r,e));return t.some(r=>r===void 0)?void 0:t}return G(n,e)}s(se,"decodeParameter");function G(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"?ce(n):n}}s(G,"decodeScalar");function ce(n){if(!(!/^[A-Za-z0-9+/]*={0,2}$/.test(n)||n.length%4!==0))try{const e=atob(n);return Uint8Array.from(e,t=>t.charCodeAt(0))}catch{return}}s(ce,"decodeBase64");function ue(n,e){let t,r;try{t=JSON.stringify(n),r=JSON.parse(t)}catch{throw I("result")}if(!N(r))throw I("result");const i=W(e.output,r);if(i!==null)throw I(i.field,i.receivedType);for(const a of Object.keys(r))if(!e.output.some(m=>m.name===a))throw I(a);return new Response(t,{headers:{"Content-Type":"application/json"}})}s(ue,"resultResponse");async function fe(n,e,t){if(!he(n))throw I("result");const r=n[Symbol.asyncIterator](),i=new TextEncoder,a=await r.next();let m=a.done,o=m?void 0:i.encode(`${JSON.stringify(a.value)}
|
|
2
|
-
`);const
|
|
3
|
-
`))}catch(
|
|
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};
|
package/dist/verify.d.ts
CHANGED
|
@@ -6,9 +6,19 @@ export interface FetchHandlerAuth {
|
|
|
6
6
|
readonly issuer: string;
|
|
7
7
|
/** The provider's registered endpoint URL, exactly as registered; `aud` must equal it. */
|
|
8
8
|
readonly audience: string;
|
|
9
|
+
/**
|
|
10
|
+
* The provider's directory principal id — for a third-party app, its OAuth client id — bare
|
|
11
|
+
* (`k3x9…`) or as `app:k3x9…`. The directory mints scopes as `<providerId>:<group>`, so set this
|
|
12
|
+
* whenever the id differs from the plugin's `name`. When set, every leaf requires
|
|
13
|
+
* `<providerId>:<group>`; when absent, `<plugin name>:<group>` (first-party providers, whose
|
|
14
|
+
* name is their id).
|
|
15
|
+
*/
|
|
16
|
+
readonly providerId?: string;
|
|
9
17
|
}
|
|
18
|
+
/** Validates `auth.providerId` and returns it bare, or undefined when none is configured. */
|
|
19
|
+
export declare function normalizeProviderId(auth: FetchHandlerAuth): string | undefined;
|
|
10
20
|
/** Turns a request's bearer into the verified caller, or throws the 401 `LeafError`. */
|
|
11
21
|
export type CallerVerifier = (request: Request) => Promise<Omit<LeafCaller, 'requestId' | 'runId'>>;
|
|
12
22
|
export declare function createCallerVerifier(auth: FetchHandlerAuth): CallerVerifier;
|
|
13
23
|
/** The 403 for a leaf whose `<provider>:<group>` the verified token does not carry. */
|
|
14
|
-
export declare function scopeMissing(scope: string): LeafError;
|
|
24
|
+
export declare function scopeMissing(scope: string, heldByOtherProvider?: string): LeafError;
|
package/dist/verify.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
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};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.458",
|
|
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.
|
|
27
|
-
"@north-light/identity": "0.3.
|
|
26
|
+
"@north-light/api": "0.3.458",
|
|
27
|
+
"@north-light/identity": "0.3.458"
|
|
28
28
|
},
|
|
29
29
|
"license": "UNLICENSED",
|
|
30
30
|
"publishConfig": {
|