@crouter/plugin 0.3.383 → 0.3.385
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 +3 -1
- package/dist/errors.d.ts +25 -2
- package/dist/errors.js +1 -1
- package/dist/serve.d.ts +2 -0
- package/dist/serve.js +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ It is a server-side authoring kit, not a runtime: the handler is a standard Fetc
|
|
|
19
19
|
npm install @crouter/plugin
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
The package is ESM-only. Import it from an ES module or use dynamic `import()` from CommonJS. To try a plugin you also need the `crtr` CLI: `npm install -g @crouter/cli
|
|
22
|
+
The package is ESM-only. Import it from an ES module or use dynamic `import()` from CommonJS. To try a plugin you also need the `crtr` CLI: `npm install -g @crouter/cli`, then run `crtr` once to sign in to a model provider.
|
|
23
23
|
|
|
24
24
|
## Define a plugin
|
|
25
25
|
|
|
@@ -110,6 +110,8 @@ Use the actual mount path in `--endpoint`. The generated archive declares comman
|
|
|
110
110
|
|
|
111
111
|
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.
|
|
112
112
|
|
|
113
|
+
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.
|
|
114
|
+
|
|
113
115
|
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.
|
|
114
116
|
|
|
115
117
|
## Reference
|
package/dist/errors.d.ts
CHANGED
|
@@ -26,6 +26,19 @@ export interface LeafErrorOptions {
|
|
|
26
26
|
readonly manifestStale?: true;
|
|
27
27
|
/** The question an `approval_required` error asks the user, serialized in the error body. */
|
|
28
28
|
readonly question?: LeafErrorQuestion;
|
|
29
|
+
/**
|
|
30
|
+
* Seconds the caller should wait before retrying, sent as `Retry-After` — a non-negative
|
|
31
|
+
* integer, or a `Date` sent as an HTTP-date. Use it on a 429 or 503. Setting it together
|
|
32
|
+
* with a `Retry-After` entry in `headers` throws.
|
|
33
|
+
*/
|
|
34
|
+
readonly retryAfter?: number | Date;
|
|
35
|
+
/**
|
|
36
|
+
* Extra response headers for any other header a caller needs (`X-RateLimit-Reset`, …).
|
|
37
|
+
* Names are case-insensitive and must be unique ignoring case; `Content-Type`,
|
|
38
|
+
* `Content-Length`, `Content-Encoding`, `Transfer-Encoding` and `Crtr-Receipt` belong to
|
|
39
|
+
* the kit and throw. Values must be valid header values.
|
|
40
|
+
*/
|
|
41
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
29
42
|
}
|
|
30
43
|
/**
|
|
31
44
|
* Throw this from a handler to report an expected application error. The Fetch handler turns
|
|
@@ -33,7 +46,9 @@ export interface LeafErrorOptions {
|
|
|
33
46
|
* `handler_failed`. The constructor throws when `code` is reserved or malformed, or when
|
|
34
47
|
* `status` is outside 400–599, and throws `PluginDefinitionError` for `unauthorized`,
|
|
35
48
|
* `scope_missing`, and `approval_required`, which only the kit's own helpers emit.
|
|
36
|
-
* `provider_refused` always has status 403.
|
|
49
|
+
* `provider_refused` always has status 403. `retryAfter` and `headers` become response
|
|
50
|
+
* headers on the error response; for a side-effect leaf they are also stored with the answer
|
|
51
|
+
* (when the `RequestStore` keeps `answer.headers`) so a repeated request gets them again.
|
|
37
52
|
*/
|
|
38
53
|
export declare class LeafError extends Error {
|
|
39
54
|
/** Lowercase snake_case error code; non-writable at runtime. */
|
|
@@ -50,6 +65,8 @@ export declare class LeafError extends Error {
|
|
|
50
65
|
readonly manifestStale?: true;
|
|
51
66
|
/** The question an `approval_required` error asks the user. */
|
|
52
67
|
readonly question?: LeafErrorQuestion;
|
|
68
|
+
/** Response headers, lowercase names, with `retryAfter` folded in as `retry-after`; undefined when none were given. Non-writable at runtime. */
|
|
69
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
53
70
|
constructor(options: LeafErrorOptions);
|
|
54
71
|
}
|
|
55
72
|
/**
|
|
@@ -57,6 +74,12 @@ export declare class LeafError extends Error {
|
|
|
57
74
|
* `scope_missing`, `approval_required`). Kit-internal: not exported from the package.
|
|
58
75
|
*/
|
|
59
76
|
export declare function kitLeafError(options: LeafErrorOptions): LeafError;
|
|
77
|
+
/**
|
|
78
|
+
* Rebuilds the headers of a stored answer for a replay: entries the kit owns or that are not
|
|
79
|
+
* valid headers are dropped, so a damaged or hand-edited row still replays its stored status
|
|
80
|
+
* and body instead of failing the repeated request.
|
|
81
|
+
*/
|
|
82
|
+
export declare function storedAnswerHeaders(stored: unknown): Headers;
|
|
60
83
|
export declare function leafErrorResponse(error: LeafError): Response;
|
|
61
84
|
export declare function errorResponse(status: number, error: {
|
|
62
85
|
readonly code: string;
|
|
@@ -66,4 +89,4 @@ export declare function errorResponse(status: number, error: {
|
|
|
66
89
|
readonly received?: unknown;
|
|
67
90
|
readonly manifest_stale?: true;
|
|
68
91
|
readonly question?: LeafErrorQuestion;
|
|
69
|
-
}): Response;
|
|
92
|
+
}, extraHeaders?: Readonly<Record<string, string>>): Response;
|
package/dist/errors.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var f=Object.defineProperty;var n=(r,e)=>f(r,"name",{value:e,configurable:!0});import{PluginDefinitionError as d}from"./define.js";const c=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,u=new Set(["internal","unknown_path","command_collision","cli_protocol_error"]),l=new Set(["unauthorized","scope_missing","approval_required"]),h=403,o=new Set(["content-type","content-length","content-encoding","transfer-encoding","crtr-receipt"]);class E extends Error{static{n(this,"LeafError")}status;field;next;received;manifestStale;question;constructor(e){if(!c.test(e.code)||u.has(e.code))throw new Error(`LeafError code ${JSON.stringify(e.code)} must be lowercase snake_case and must not be reserved`);if(l.has(e.code)&&!i)throw new d(`LeafError code ${e.code} is emitted only by the kit's own helpers; a leaf cannot throw it`);if(e.question!==void 0&&e.code!=="approval_required")throw new Error(`LeafError question is only valid on approval_required, not ${e.code}`);if(e.status!==void 0&&(!Number.isInteger(e.status)||e.status<400||e.status>599))throw new Error("LeafError status must be an integer from 400 through 599");const t=w(e);super(e.message),this.name="LeafError",Object.defineProperty(this,"code",{value:e.code,enumerable:!0,writable:!1,configurable:!1}),this.status=e.code==="provider_refused"?h:e.status??400,this.field=e.field,this.next=e.next,this.received=e.received,this.manifestStale=e.manifestStale,this.question=e.question,Object.defineProperty(this,"headers",{value:t,enumerable:!0,writable:!1,configurable:!1})}}function w(r){const e=Object.create(null);for(const[t,a]of Object.entries(r.headers??{})){const s=t.toLowerCase();if(o.has(s))throw new Error(`LeafError headers cannot set ${t}; the kit owns it`);if(s in e)throw new Error(`LeafError headers names ${t} twice`);if(typeof a!="string")throw new Error(`LeafError header ${t} must be a string`);try{new Headers({[s]:a})}catch{throw new Error(`LeafError header ${JSON.stringify(t)} is not a valid header`)}e[s]=a}if(r.retryAfter!==void 0){if("retry-after"in e)throw new Error("LeafError sets Retry-After through both retryAfter and headers");const{retryAfter:t}=r;if(t instanceof Date){if(Number.isNaN(t.getTime()))throw new Error("LeafError retryAfter must be a valid Date");e["retry-after"]=t.toUTCString()}else if(typeof t=="number"&&Number.isSafeInteger(t)&&t>=0)e["retry-after"]=String(t);else throw new Error("LeafError retryAfter must be a non-negative integer number of seconds or a Date")}return Object.keys(e).length===0?void 0:Object.freeze(e)}n(w,"resolveHeaders");let i=!1;function b(r){i=!0;try{return new E(r)}finally{i=!1}}n(b,"kitLeafError");function _(r){const e=new Headers;if(typeof r!="object"||r===null)return e;for(const[t,a]of Object.entries(r))if(!(typeof a!="string"||o.has(t.toLowerCase())))try{e.set(t,a)}catch{}return e}n(_,"storedAnswerHeaders");function v(r){return m(r.status,{code:r.code,message:r.message,...r.field!==void 0?{field:r.field}:{},...r.next!==void 0?{next:r.next}:{},...r.received!==void 0?{received:r.received}:{},...r.manifestStale===!0?{manifest_stale:!0}:{},...r.question!==void 0?{question:r.question}:{}},r.headers)}n(v,"leafErrorResponse");function m(r,e,t){return new Response(JSON.stringify({error:e}),{status:r,headers:{...t,"Content-Type":"application/json"}})}n(m,"errorResponse");export{E as LeafError,m as errorResponse,b as kitLeafError,v as leafErrorResponse,_ as storedAnswerHeaders};
|
package/dist/serve.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ export interface RequestStore {
|
|
|
51
51
|
status: number;
|
|
52
52
|
body: string;
|
|
53
53
|
receipt?: Receipt;
|
|
54
|
+
headers?: Record<string, string>;
|
|
54
55
|
}): Promise<void>;
|
|
55
56
|
read(requestId: string): Promise<{
|
|
56
57
|
identity: RequestIdentity;
|
|
@@ -59,6 +60,7 @@ export interface RequestStore {
|
|
|
59
60
|
status: number;
|
|
60
61
|
body: string;
|
|
61
62
|
receipt?: Receipt;
|
|
63
|
+
headers?: Record<string, string>;
|
|
62
64
|
} | null;
|
|
63
65
|
} | null>;
|
|
64
66
|
delete(requestId: string): Promise<void>;
|
package/dist/serve.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var L=Object.defineProperty;var s=(n,e)=>L(n,"name",{value:e,configurable:!0});import{approvalFor as V}from"./approval.js";import{validateDeclaredResult as Z}from"@crouter/api/command-manifest";import{errorResponse as k,LeafError as A,leafErrorResponse as F}from"./errors.js";import{buildCommandManifest as W}from"./manifest.js";import{buildBundle as K}from"./bundle.js";import{createCallerVerifier as Q,scopeMissing as X}from"./verify.js";const J=/^v([1-9][0-9]*)$/;function be(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=ne(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:Q(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:H(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,I)=>{const c=`${y.version}\0${I}`;let f=m.get(c);if(f===void 0){const j=await K(y.plugin,{mountPath:I,...e.baseUrl!==void 0?{baseUrl:e.baseUrl}:{}});f={tar:j.tar,etag:j.etag},m.set(c,f)}const p={ETag:f.etag};return d.headers.get("if-none-match")===f.etag?new Response(null,{status:304,headers:p}):new Response(new Uint8Array(f.tar).buffer,{headers:{...p,"Content-Type":"application/x-tar"}})},"archiveResponse");return async d=>{if(d.method!=="POST"&&e.token!==void 0&&!G(d.headers.get("authorization")??"",`Bearer ${e.token}`))return k(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??H(y));const h=M(y),v=h[h.length-1],S=o??H(`/${h.slice(0,-1).join("/")}`);if(v==="versions")return Response.json({latest:w.version,supported:r.map(u=>u.version)});const T=v===void 0?null:J.exec(v),b=T===null?void 0:r.find(u=>u.version===Number(T[1]));return b===void 0?U():l(d,b,`${S}/v${b.version}`)}if(d.method!=="POST")return U();const I=performance.now();let c,f,p,j=!1,N=!1;const E=s(h=>{if(f!==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:f.path.slice(1).join(" "),status:h.status,durationMs:performance.now()-I,...p===void 0?{}:{receiptId:p.receipt_id},replayed:N})}catch{}return h},"answer");let g=d.headers.get("crtr-request-id");try{if(f=t?ie(r,y):re(w.commands,y),e.token!==void 0&&!G(d.headers.get("authorization")??"",`Bearer ${e.token}`))return E(k(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(f===void 0)return U();if(h!==void 0&&!h.scope.includes(f.scope))throw c=Object.freeze({...h,requestId:g??"",...d.headers.get("crtr-run-id")?{runId:d.headers.get("crtr-run-id")}:{}}),X(f.scope);if(h!==void 0){if(g===null||g==="")return E(k(400,{code:"invalid_request",message:"The request carries no Crtr-Request-Id header."}));const u=d.headers.get("crtr-run-id");c=Object.freeze({...h,requestId:g,...u!==null&&u!==""?{runId:u}:{}})}const v=await oe(d,f.manifest,h!==void 0);if(f.leaf.sideEffect){if(g===null||g==="")return E(k(400,{code:"invalid_request",message:"The request carries no Crtr-Request-Id header."}));const u=c===void 0?null:{sub:c.sub,grantee:c.grantee};if(!await e.requests.record(g,u)){N=!0;const $=await ee(e.requests,g,u,f.plugin.timeouts?.requestMs??3e4);if($!==!0){const z=$.headers.get("Crtr-Receipt");if(z!==null)try{p=JSON.parse(z)}catch{}return E($)}N=!1}j=!0}const S=f,T={request:d,...c!==void 0?{caller:c}:{},approval:V(d),meter(u){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 x=S.path.slice(1).join(" "),$=u.usdEquivalent??(e.receipts.rates[x]===void 0?void 0:u.units*e.receipts.rates[x].usdPerUnit);if($===void 0)throw new Error(`No receipt rate for ${x}`);return p={receipt_id:c.requestId,provider:e.receipts.provider,tool:x,sub:c.sub,grantee:c.grantee,payer:c.sub,units:u.units,unit:u.unit,usd_equivalent:$,request_id:c.requestId,...c.runId!==void 0?{run_id:c.runId}:{},time:new Date().toISOString()},p}};let b;try{const u=await f.leaf.run(v,T);b=f.leaf.streaming?await ce(u,e.onError,f.path):se(u,f.manifest)}catch(u){u instanceof A?b=F(u):(O(e.onError,u,f.path),b=_(u))}if(j&&b.status===403&&await Y(b))return await e.requests.delete(g),E(B(b,p));try{if(j)await e.requests.store(g,{status:b.status,body:await b.clone().text(),...p!==void 0?{receipt:p}:{}});else if(p!==void 0){if(e.receipts===void 0)throw new Error(`No receipt outbox for ${S.path.join(" ")}`);await e.receipts.outbox.save(p)}}catch(u){return O(e.onError,u,f.path,p),E(_(u))}return E(B(b,p))}catch(h){return h instanceof A?E(F(h)):(O(e.onError,h,f?.path??[]),E(_(h)))}}}s(be,"createFetchHandler");function _(n){return k(500,{code:"handler_failed",message:n instanceof Error?n.message:String(n)})}s(_,"handlerFailed");function B(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(B,"responseWithReceipt");async function Y(n){try{const e=await n.clone().json();return C(e)&&C(e.error)&&e.error.code==="approval_required"}catch{return!1}}s(Y,"isApprovalRequired");async function ee(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 k(403,{code:"provider_refused",message:"This request id belongs to a different caller."});if(i.answer!==null){const a=new Headers({"Content-Type":"application/json"});return 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 _(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(ee,"replay");function ne(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:te(i)})}return[...r.values()].sort((i,a)=>i.version-a.version)}s(ne,"servedMajors");function te(n){const t=W(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 P(t.children,n.commands,[t.name],n,r),r}s(te,"commandMap");function P(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"){P(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(P,"collectCommands");function re(n,e){const t=M(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(re,"findCommand");function ie(n,e){const t=M(e);for(let r=t.length-2;r>=0;r-=1){const i=J.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(ie,"findVersionedCommand");function M(n){return n.split("/").filter(e=>e.length>0)}s(M,"pathSegments");async function oe(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(!C(o))throw new Error("not an object");i=o}catch{throw q("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?q(`Unknown parameter ${o}.`,o,!0,i[o]):new A({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 q(`Missing required parameter ${o.name}.`,o.name,t,null);continue}const w=i[o.name],l=ae(w,o);if(l===void 0)throw q(`Invalid value for parameter ${o.name}.`,o.name,t,w);m[ue(o.name)]=l}return m}s(oe,"decodeInput");function ae(n,e){if(e.kind!=="stdin"&&e.kind!=="context-file"&&e.repeatable){if(!Array.isArray(n))return;const t=n.map(r=>D(r,e));return t.some(r=>r===void 0)?void 0:t}return D(n,e)}s(ae,"decodeParameter");function D(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"?de(n):n}}s(D,"decodeScalar");function de(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(de,"decodeBase64");function se(n,e){let t,r;try{t=JSON.stringify(n),r=JSON.parse(t)}catch{throw R("result")}if(!C(r))throw R("result");const i=Z(e.output,r);if(i!==null)throw R(i.field,i.receivedType);for(const a of Object.keys(r))if(!e.output.some(m=>m.name===a))throw R(a);return new Response(t,{headers:{"Content-Type":"application/json"}})}s(se,"resultResponse");async function ce(n,e,t){if(!fe(n))throw R("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)}
|
|
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"@crouter/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
2
|
`);const w=new ReadableStream({async pull(l){if(o!==void 0){l.enqueue(o),o=void 0;return}if(m){l.close();return}try{const d=await r.next();d.done?(m=!0,l.close()):l.enqueue(i.encode(`${JSON.stringify(d.value)}
|
|
3
|
-
`))}catch(d){
|
|
3
|
+
`))}catch(d){q(e,d,t),l.error(d)}},async cancel(l){await r.return?.(l)}});return new Response(w,{headers:{"Content-Type":"application/x-ndjson"}})}s(fe,"streamResponse");function q(n,e,t,r){try{n?.(e,t,r)}catch{}}s(q,"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}:{}})}s(I,"outputInvalid");function C(n,e,t=!1,r){return new R({code:t?"invalid_argument":"invalid_input",message:n,...e!==void 0?{field:e}:{},...t?{received:r}:{}})}s(C,"invalidInput");function U(){return j(404,{code:"no_such_command",message:"No command matches this request."})}s(U,"noSuchCommand");function z(n){return n==="/"?"":n.replace(/\/+$/,"")}s(z,"mountPath");function le(n){return n.replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}s(le,"camel");function L(n,e){const t=new TextEncoder().encode(n),r=new TextEncoder().encode(e);let i=t.length^r.length;for(let a=0;a<Math.min(t.length,r.length);a+=1)i|=t[a]^r[a];return i===0}s(L,"constantTimeEqual");function N(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}s(N,"isRecord");function he(n){return typeof n=="object"&&n!==null&&Symbol.asyncIterator in n}s(he,"isAsyncIterable");export{ke as createFetchHandler};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crouter/plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.385",
|
|
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
|
-
"@crouter/api": "^0.3.
|
|
27
|
-
"@crouter/identity": "^0.3.
|
|
26
|
+
"@crouter/api": "^0.3.385",
|
|
27
|
+
"@crouter/identity": "^0.3.385"
|
|
28
28
|
},
|
|
29
29
|
"license": "GPL-3.0-only",
|
|
30
30
|
"publishConfig": {
|