@crouter/plugin 0.3.377
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 +128 -0
- package/dist/approval.d.ts +5 -0
- package/dist/approval.js +1 -0
- package/dist/bundle.d.ts +36 -0
- package/dist/bundle.js +8 -0
- package/dist/compare.d.ts +9 -0
- package/dist/compare.js +1 -0
- package/dist/define.d.ts +205 -0
- package/dist/define.js +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.js +1 -0
- package/dist/fields.d.ts +141 -0
- package/dist/fields.js +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +17 -0
- package/dist/manifest.js +1 -0
- package/dist/params.d.ts +169 -0
- package/dist/params.js +1 -0
- package/dist/receipts.d.ts +39 -0
- package/dist/receipts.js +1 -0
- package/dist/serve.d.ts +78 -0
- package/dist/serve.js +3 -0
- package/dist/verify.d.ts +14 -0
- package/dist/verify.js +1 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @crouter/plugin — define crtr commands once in TypeScript and serve them over HTTP
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@crouter/plugin"><img alt="npm" src="https://img.shields.io/npm/v/@crouter/plugin?label=npm"></a>
|
|
7
|
+
<a href="https://github.com/crouton-labs/crouter/blob/main/LICENSE"><img alt="license" src="https://img.shields.io/badge/license-GPL--3.0-blue"></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
`@crouter/plugin` lets an application add commands to the `crtr` CLI, and so to every agent that runs it. You write one typed command tree. The package turns it into a Fetch handler, a `commands.json` manifest, and the archive that `crtr pkg plugin install --endpoint` reads. You do not write a manifest, a route table, or a `plugin.json`.
|
|
11
|
+
|
|
12
|
+
It is a server-side authoring kit, not a runtime: the handler is a standard Fetch `{ fetch }` default export that you deploy on whatever host serves one. It does not run agents; an agent calls your command the way it calls any other `crtr` command, and `crtr` makes an HTTP request to your endpoint. Plugins that need no server, such as local executables, are installed from a marketplace instead; see the [plugin docs](https://docs.crouter.ai/docs/plugin).
|
|
13
|
+
|
|
14
|
+
[Docs](https://docs.crouter.ai/docs/plugin) · [Commands](https://docs.crouter.ai/docs/plugin/commands) · [Deploying](https://docs.crouter.ai/docs/plugin/deploying) · [Official marketplace](https://github.com/crouton-labs/crouter-official-marketplace) · [crouter repository](https://github.com/crouton-labs/crouter)
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @crouter/plugin
|
|
20
|
+
```
|
|
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 && crtr sys setup`.
|
|
23
|
+
|
|
24
|
+
## Define a plugin
|
|
25
|
+
|
|
26
|
+
Create `src/crtr-plugin.ts`:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import {
|
|
30
|
+
createFetchHandler,
|
|
31
|
+
defineBranch,
|
|
32
|
+
defineLeaf,
|
|
33
|
+
definePlugin,
|
|
34
|
+
field,
|
|
35
|
+
LeafError,
|
|
36
|
+
param,
|
|
37
|
+
} from '@crouter/plugin';
|
|
38
|
+
|
|
39
|
+
export const plugin = definePlugin({
|
|
40
|
+
name: 'acme',
|
|
41
|
+
description: 'Manage Acme applications.',
|
|
42
|
+
whenToUse: 'you need to create or inspect an Acme application.',
|
|
43
|
+
summary: 'Acme application management',
|
|
44
|
+
rootEntry: {
|
|
45
|
+
concept: 'an Acme application',
|
|
46
|
+
description: 'Create and inspect applications.',
|
|
47
|
+
whenToUse: 'the task is about an Acme application.',
|
|
48
|
+
},
|
|
49
|
+
commands: {
|
|
50
|
+
app: defineBranch({
|
|
51
|
+
description: 'Create and inspect applications.',
|
|
52
|
+
whenToUse: 'you are working with an application.',
|
|
53
|
+
summary: 'application operations',
|
|
54
|
+
children: {
|
|
55
|
+
create: defineLeaf({
|
|
56
|
+
description: 'Create an Acme application.',
|
|
57
|
+
whenToUse: 'an application does not exist yet.',
|
|
58
|
+
summary: 'create an application',
|
|
59
|
+
params: {
|
|
60
|
+
name: param.positional('The application name.', { required: true }),
|
|
61
|
+
region: param.enum(['us-east', 'eu-west'], 'The region for the application.', { default: 'us-east' }),
|
|
62
|
+
},
|
|
63
|
+
output: {
|
|
64
|
+
appId: field.string('The created application id.'),
|
|
65
|
+
url: field.string('The application URL.'),
|
|
66
|
+
},
|
|
67
|
+
effects: ['Creates an Acme application.'],
|
|
68
|
+
handler(input) {
|
|
69
|
+
if (input.name === 'taken') {
|
|
70
|
+
throw new LeafError({
|
|
71
|
+
code: 'name_taken',
|
|
72
|
+
message: 'That application name is already in use.',
|
|
73
|
+
status: 409,
|
|
74
|
+
field: 'name',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
appId: `app_${input.name}`,
|
|
79
|
+
url: `https://${input.name}.${input.region ?? 'us-east'}.acme.example.com`,
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export default {
|
|
89
|
+
fetch: createFetchHandler(plugin, { token: process.env.ACME_CRTR_TOKEN }),
|
|
90
|
+
};
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The handler input is inferred from `params`: `name` is required and `region` is optional. Deploy the default export at the URL crtr reaches. The same URL handles `GET` archive requests and `POST` command requests.
|
|
94
|
+
|
|
95
|
+
## Install into crtr
|
|
96
|
+
|
|
97
|
+
Set the same bearer token on the machine that runs crtr, then install the handler's actual mount path:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
export ACME_CRTR_TOKEN='replace-with-a-secret'
|
|
101
|
+
crtr pkg plugin install --endpoint https://acme.example.com/crtr --name acme --auth-env ACME_CRTR_TOKEN
|
|
102
|
+
crtr acme app create my-app --region eu-west
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
`export` covers commands you type yourself. An agent's `crtr acme …` runs inside its broker, whose env comes from the profile env store, not from your shell — so for agents, store the token there too: `echo -n "$ACME_CRTR_TOKEN" | crtr profile env set <profile> --name ACME_CRTR_TOKEN`. Nodes launched under that profile from then on carry it.
|
|
106
|
+
|
|
107
|
+
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`.
|
|
108
|
+
|
|
109
|
+
## Metered provider tools
|
|
110
|
+
|
|
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
|
+
|
|
113
|
+
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
|
+
|
|
115
|
+
## Reference
|
|
116
|
+
|
|
117
|
+
The [plugin guide](https://docs.crouter.ai/docs/plugin) covers [commands](https://docs.crouter.ai/docs/plugin/commands), [parameters](https://docs.crouter.ai/docs/plugin/parameters), [output](https://docs.crouter.ai/docs/plugin/output), [errors and streaming](https://docs.crouter.ai/docs/plugin/errors), [deployment](https://docs.crouter.ai/docs/plugin/deploying), and [bundles and documents](https://docs.crouter.ai/docs/plugin/bundles-and-memory).
|
|
118
|
+
|
|
119
|
+
## Related
|
|
120
|
+
|
|
121
|
+
- [`@crouter/api`](../crouter-api): defines the command-manifest schema this package emits.
|
|
122
|
+
- [`@crouter/identity`](../crouter-identity): verifies the directory-minted tokens the handler checks under `auth`.
|
|
123
|
+
- [`@crouter/sdk`](../crouter-sdk): the application client; a plugin and an SDK app are often the same service.
|
|
124
|
+
- [Main repository](https://github.com/crouton-labs/crouter): the `crtr` CLI, the `crtrd` daemon, and [contributing guidelines](https://github.com/crouton-labs/crouter/blob/main/CONTRIBUTING.md).
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
[GPL-3.0-only](https://github.com/crouton-labs/crouter/blob/main/LICENSE).
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LeafErrorQuestion } from './errors.js';
|
|
2
|
+
/** The approval header is authoritative: the runtime chooses an option, not the provider kit. */
|
|
3
|
+
export declare function approvalFor(request: Request): {
|
|
4
|
+
require(question: LeafErrorQuestion): Promise<string>;
|
|
5
|
+
};
|
package/dist/approval.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var t=Object.defineProperty;var o=(r,e)=>t(r,"name",{value:e,configurable:!0});import{kitLeafError as p}from"./errors.js";function c(r){return{async require(e){const a=r.headers.get("crtr-approval");if(a!==null)return a;throw p({code:"approval_required",status:403,message:"This call requires approval.",question:e})}}}o(c,"approvalFor");export{c as approvalFor};
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type PluginDefinition } from './define.js';
|
|
2
|
+
/** Where the handler is served, which decides the route paths written into the manifest. */
|
|
3
|
+
export interface BuildBundleOptions {
|
|
4
|
+
/** Path prefix the handler is served under, `''` at the origin root and `/crtr`
|
|
5
|
+
* when mounted there. Every generated `rest.path` starts with it. */
|
|
6
|
+
readonly mountPath: string;
|
|
7
|
+
/** Written into the manifest verbatim when set; omitted otherwise. */
|
|
8
|
+
readonly baseUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
/** One file in the archive. */
|
|
11
|
+
export interface BuildBundleMemoryMember {
|
|
12
|
+
/** Path inside the archive, e.g. `memory/deploying.md`. */
|
|
13
|
+
readonly path: string;
|
|
14
|
+
/** File contents as text. */
|
|
15
|
+
readonly contents: string;
|
|
16
|
+
}
|
|
17
|
+
/** The archive and the documents that went into it. */
|
|
18
|
+
export interface BuildBundleResult {
|
|
19
|
+
/** The validated command manifest, as written to `commands.json`. */
|
|
20
|
+
readonly commandsJson: string;
|
|
21
|
+
/** The bundle descriptor, as written to `bundle.json`. */
|
|
22
|
+
readonly bundleJson: string;
|
|
23
|
+
/** One member per declared memory document, with frontmatter already written. */
|
|
24
|
+
readonly memory: BuildBundleMemoryMember[];
|
|
25
|
+
/** The uncompressed tar crtr installs. Byte-identical for identical input. */
|
|
26
|
+
readonly tar: Uint8Array;
|
|
27
|
+
/** SHA-256 of `tar`, quoted for use as an `ETag`. */
|
|
28
|
+
readonly etag: string;
|
|
29
|
+
}
|
|
30
|
+
/** Builds the install archive: `bundle.json`, `commands.json`, and one
|
|
31
|
+
* `memory/<name>.md` per declared memory doc, as an uncompressed tar. Runs
|
|
32
|
+
* the real manifest validator first — a manifest `crtr sys doctor` would
|
|
33
|
+
* reject never reaches the tar — and throws `ManifestInvalidError` from
|
|
34
|
+
* `buildCommandManifest` when it fails. The fetch handler calls this same
|
|
35
|
+
* function, so there is one build path, not two. */
|
|
36
|
+
export declare function buildBundle(plugin: PluginDefinition, options: BuildBundleOptions): Promise<BuildBundleResult>;
|
package/dist/bundle.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
var y=Object.defineProperty;var o=(n,t)=>y(n,"name",{value:t,configurable:!0});import{PluginDefinitionError as m}from"./define.js";import{buildCommandManifest as w}from"./manifest.js";const c=512,b=100,d=new TextEncoder;async function D(n,t){const e=w(n,{...t,mountPath:t.mountPath===""?"/":t.mountPath}),r=`${JSON.stringify(e,null,2)}
|
|
2
|
+
`,a=`${JSON.stringify({bundleVersion:1,...n.version!==void 0?{version:n.version}:{}})}
|
|
3
|
+
`,s=p(n.memory),f=[{path:"bundle.json",contents:a},{path:"commands.json",contents:r},...s],h=A(f),l=await B(h);return{commandsJson:r,bundleJson:a,memory:s,tar:h,etag:l}}o(D,"buildBundle");function p(n){const t=new Set;return n.map(e=>{const r=g(e);if(t.has(r))throw new m(`memory document "${e.name}" produces a duplicate tar member path ${r}`);return t.add(r),{path:r,contents:$(e)}})}o(p,"buildMemoryMembers");function g(n){if(n.name.includes("\0"))throw new m(`memory document "${n.name}" must not contain a NUL character`);const t=`memory/${n.name}.md`;if(d.encode(t).length>b)throw new m(`memory document "${n.name}" produces a tar member path longer than 100 bytes (${t}); shorten its name`);return t}o(g,"memoryMemberPath");function $(n){const t=[`kind: ${n.kind}`,`when-and-why-to-read: ${S(n.whenAndWhyToRead)}`];return n.unlisted===!0&&t.push("unlisted: true"),`---
|
|
4
|
+
${t.join(`
|
|
5
|
+
`)}
|
|
6
|
+
---
|
|
7
|
+
${n.body}`}o($,"renderMemoryDoc");function S(n){let t='"';for(const e of n)switch(e){case"\\":t+="\\\\";break;case'"':t+='\\"';break;case`
|
|
8
|
+
`:t+="\\n";break;case" ":t+="\\t";break;case"\r":t+="\\r";break;default:t+=e.charCodeAt(0)<=31||e==="\x7F"?`\\x${e.charCodeAt(0).toString(16).padStart(2,"0")}`:e}return`${t}"`}o(S,"yamlDoubleQuoted");function A(n){const t=[];for(const e of n){const r=d.encode(e.contents);t.push(k(e.path,r.length)),t.push(r);const a=x(r.length);a>0&&t.push(new Uint8Array(a))}return t.push(new Uint8Array(c)),t.push(new Uint8Array(c)),M(t)}o(A,"writeTar");function x(n){const t=n%c;return t===0?0:c-t}o(x,"padLength");function k(n,t){const e=new Uint8Array(c);return i(e,0,n),u(e,100,8,420),u(e,108,8,0),u(e,116,8,0),u(e,124,12,t),u(e,136,12,0),e.fill(32,148,156),e[156]=48,i(e,257,"ustar\0"),i(e,263,"00"),E(e,148,U(e)),e}o(k,"ustarHeader");function i(n,t,e){n.set(d.encode(e),t)}o(i,"writeAscii");function u(n,t,e,r){const a=e-1,s=r.toString(8).padStart(a,"0");if(s.length>a)throw new m(`tar header field at offset ${t} cannot hold value ${r} in ${a} octal digits`);i(n,t,s)}o(u,"writeOctalField");function E(n,t,e){i(n,t,e.toString(8).padStart(6,"0")),n[t+6]=0,n[t+7]=32}o(E,"writeChecksumField");function U(n){let t=0;for(const e of n)t+=e;return t}o(U,"sumBytes");function M(n){const t=n.reduce((a,s)=>a+s.length,0),e=new Uint8Array(t);let r=0;for(const a of n)e.set(a,r),r+=a.length;return e}o(M,"concatBytes");async function B(n){const t=new Uint8Array(await crypto.subtle.digest("SHA-256",n));return`"${Array.from(t,r=>r.toString(16).padStart(2,"0")).join("")}"`}o(B,"sha256Etag");export{D as buildBundle};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HttpPluginCommandManifest } from '@crouter/api/plugin-manifest';
|
|
2
|
+
/** A change to a command contract that requires a new provider major. */
|
|
3
|
+
export interface BreakingManifestChange {
|
|
4
|
+
/** Space-joined command path, including the plugin name. */
|
|
5
|
+
leaf: string;
|
|
6
|
+
rule: string;
|
|
7
|
+
}
|
|
8
|
+
/** Compare the command contracts of two archives for the same provider major. */
|
|
9
|
+
export declare function compareManifests(previous: HttpPluginCommandManifest, next: HttpPluginCommandManifest): BreakingManifestChange[];
|
package/dist/compare.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var j=Object.defineProperty;var a=(e,u)=>j(e,"name",{value:u,configurable:!0});function g(e,u){const r=f(e),n=f(u),t=[];for(const[s,i]of r){const o=n.get(s);if(!o){t.push({leaf:s,rule:"tool removed or renamed; bump the major"});continue}i.group!==o.group&&t.push({leaf:s,rule:"tool group changed; bump the major"});const p=new Map(o.params.map(m=>[m.name,m]));for(const m of i.params){const c=p.get(m.name);c?d(m)!==d(c)?t.push({leaf:s,rule:`parameter ${m.name} type changed; bump the major`}):!m.required&&c.required&&t.push({leaf:s,rule:`parameter ${m.name} became required; bump the major`}):t.push({leaf:s,rule:`parameter ${m.name} removed; bump the major`})}const l=new Set(i.params.map(m=>m.name));for(const m of o.params)!l.has(m.name)&&m.required&&t.push({leaf:s,rule:`required parameter ${m.name} added; bump the major`});h(i.output,o.output,s,t)}return t}a(g,"compareManifests");function f(e){const u=new Map;function r(n,t){const s=[...t,n.name];if(n.kind==="leaf")u.set(s.join(" "),n);else for(const i of n.children)r(i,s)}a(r,"visit");for(const n of e.mounts)r(n.node,n.parent);return u}a(f,"leaves");function d(e){return e.kind==="flag"?`${e.kind}:${e.type}:${!!e.repeatable}:${e.type==="enum"?JSON.stringify(e.choices):""}`:e.kind==="positional"?`${e.kind}:${e.type??"string"}:${!!e.repeatable}`:e.kind}a(d,"parameterType");function h(e,u,r,n,t=""){const s=new Map(u.map(o=>[o.name,o])),i=new Set(e.map(o=>o.name));for(const o of u)if(!i.has(o.name)&&b(o.type)==="file"){const p=t?`${t}.${o.name}`:o.name;n.push({leaf:r,rule:`file output field ${p} added; bump the major`})}for(const o of e){const p=t?`${t}.${o.name}`:o.name,l=s.get(o.name);if(!l){n.push({leaf:r,rule:`output field ${p} removed; bump the major`});continue}o.required&&!l.required&&n.push({leaf:r,rule:`output field ${p} became optional; bump the major`}),$(o,l,p,r,n)}}a(h,"compareFields");function b(e){return e.endsWith(" | null")?e.slice(0,-7):e}a(b,"baseType");function $(e,u,r,n,t){if(e.type!==u.type){t.push({leaf:n,rule:`output field ${r} type changed; bump the major`});return}e.values?.some(s=>!u.values?.includes(s))&&t.push({leaf:n,rule:`output field ${r} enum value removed; bump the major`}),e.children&&h(e.children,u.children??[],n,t,r),e.items&&u.items?$(e.items,u.items,`${r}[]`,n,t):!!e.items!=!!u.items&&t.push({leaf:n,rule:`output field ${r} type changed; bump the major`})}a($,"compareShape");export{g as compareManifests};
|
package/dist/define.d.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { OutField, Output } from './fields.js';
|
|
2
|
+
import type { LeafErrorQuestion } from './errors.js';
|
|
3
|
+
import type { Receipt } from './receipts.js';
|
|
4
|
+
import type { AnyParam, Input } from './params.js';
|
|
5
|
+
/** Thrown by `definePlugin` when a declaration is malformed, before anything is served. */
|
|
6
|
+
export declare class PluginDefinitionError extends Error {
|
|
7
|
+
constructor(message: string);
|
|
8
|
+
}
|
|
9
|
+
/** One level of the outbound token's `act` nesting: the actor, and the actor it acted for. */
|
|
10
|
+
export interface LeafCallerActor {
|
|
11
|
+
readonly sub: string;
|
|
12
|
+
readonly act?: LeafCallerActor;
|
|
13
|
+
}
|
|
14
|
+
/** Who called a leaf, from the verified outbound token and the request's correlation headers. */
|
|
15
|
+
export interface LeafCaller {
|
|
16
|
+
/** The user the call is made for, `user:<id>`. */
|
|
17
|
+
readonly sub: string;
|
|
18
|
+
/** The principal the grant was issued to: `app:<id>`, or the user for the person's own authority. */
|
|
19
|
+
readonly grantee: string;
|
|
20
|
+
/** The OAuth client id of the runtime that presented the token; it authorizes nothing. */
|
|
21
|
+
readonly clientId: string;
|
|
22
|
+
/** The `<provider>:<group>` strings the token carries. */
|
|
23
|
+
readonly scope: readonly string[];
|
|
24
|
+
/** The principal that initiated the work, for the provider's own audit or stricter rule. */
|
|
25
|
+
readonly initiatingApp: string;
|
|
26
|
+
/** The initiator chain, for audit only; the kit authorizes nothing on it. */
|
|
27
|
+
readonly act: LeafCallerActor;
|
|
28
|
+
/** The token's unique id, for the provider's audit. */
|
|
29
|
+
readonly jti: string;
|
|
30
|
+
/** The request's `Crtr-Request-Id`. */
|
|
31
|
+
readonly requestId: string;
|
|
32
|
+
/** The request's `Crtr-Run-Id`, when it carries one. */
|
|
33
|
+
readonly runId?: string;
|
|
34
|
+
}
|
|
35
|
+
/** The second argument to every leaf handler. */
|
|
36
|
+
export interface LeafContext {
|
|
37
|
+
/** The incoming HTTP request, for headers and other transport details. */
|
|
38
|
+
readonly request: Request;
|
|
39
|
+
/** The verified caller when the handler was created with `auth`; `undefined` under `token`. */
|
|
40
|
+
readonly caller?: LeafCaller;
|
|
41
|
+
/** Returns the runtime's approval choice or asks the runtime to obtain one. */
|
|
42
|
+
readonly approval: {
|
|
43
|
+
require(question: LeafErrorQuestion): Promise<string>;
|
|
44
|
+
};
|
|
45
|
+
/** Records the cost of this call once, including when the leaf later fails. */
|
|
46
|
+
readonly meter: (usage: {
|
|
47
|
+
units: number;
|
|
48
|
+
unit: string;
|
|
49
|
+
usdEquivalent?: number;
|
|
50
|
+
}) => Receipt;
|
|
51
|
+
}
|
|
52
|
+
/** One command an agent can invoke, as returned by `defineLeaf` or `defineStreamingLeaf`. */
|
|
53
|
+
export interface LeafNode {
|
|
54
|
+
readonly kind: 'leaf';
|
|
55
|
+
/** What the command is, written for an agent choosing between commands. */
|
|
56
|
+
readonly description: string;
|
|
57
|
+
/** When the command applies. */
|
|
58
|
+
readonly whenToUse: string;
|
|
59
|
+
/** The short outcome, shown in listings. */
|
|
60
|
+
readonly summary: string;
|
|
61
|
+
/** How prominently crtr lists the command. Defaults to `normal`. */
|
|
62
|
+
readonly tier?: 'normal' | 'common' | 'important';
|
|
63
|
+
/** Input parameters by camelCase key, each built with `param.*`. */
|
|
64
|
+
readonly params: Record<string, AnyParam>;
|
|
65
|
+
/** Result fields by key, each built with `field.*`. */
|
|
66
|
+
readonly output: Record<string, OutField<string, boolean>>;
|
|
67
|
+
/** What running the command does — mutations, billable resources, or read-only. */
|
|
68
|
+
readonly effects: readonly string[];
|
|
69
|
+
/** True when the handler returns an `AsyncIterable` rather than one object. */
|
|
70
|
+
readonly streaming: boolean;
|
|
71
|
+
/**
|
|
72
|
+
* The capability-provider tool group the command belongs to, matching `[A-Za-z0-9_-]{1,64}`.
|
|
73
|
+
* Required on every leaf of a `provider: true` plugin; a local install ignores it.
|
|
74
|
+
*/
|
|
75
|
+
readonly group?: string;
|
|
76
|
+
/**
|
|
77
|
+
* True when running the command has a side effect, so a repeated request must not run it
|
|
78
|
+
* twice. Never written into the manifest.
|
|
79
|
+
*/
|
|
80
|
+
readonly sideEffect?: boolean;
|
|
81
|
+
/** The handler, with decoded input. */
|
|
82
|
+
readonly run: (input: Record<string, unknown>, ctx: LeafContext) => Promise<unknown>;
|
|
83
|
+
}
|
|
84
|
+
/** A group of commands, as returned by `defineBranch`. A branch is never invoked itself. */
|
|
85
|
+
export interface BranchNode {
|
|
86
|
+
readonly kind: 'branch';
|
|
87
|
+
/** What the group covers, written for an agent choosing between commands. */
|
|
88
|
+
readonly description: string;
|
|
89
|
+
/** When commands in this group apply. */
|
|
90
|
+
readonly whenToUse: string;
|
|
91
|
+
/** The short outcome, shown in listings. */
|
|
92
|
+
readonly summary: string;
|
|
93
|
+
/** How prominently crtr lists the branch. Defaults to `normal`. */
|
|
94
|
+
readonly tier?: 'normal' | 'common' | 'important';
|
|
95
|
+
/** Nested branches and leaves by camelCase key. */
|
|
96
|
+
readonly children: Record<string, LeafNode | BranchNode>;
|
|
97
|
+
}
|
|
98
|
+
/** A memory document the plugin installs alongside its commands. */
|
|
99
|
+
export interface MemoryDoc {
|
|
100
|
+
/** Document name, also its path under `memory/` in the bundle. */
|
|
101
|
+
readonly name: string;
|
|
102
|
+
/** `knowledge` for how to do something, `preference` for how the user wants it done. */
|
|
103
|
+
readonly kind: 'knowledge' | 'preference';
|
|
104
|
+
/** The routing line telling an agent when reading this document pays off. */
|
|
105
|
+
readonly whenAndWhyToRead: string;
|
|
106
|
+
/** Markdown body, written without frontmatter — `buildBundle` writes that. */
|
|
107
|
+
readonly body: string;
|
|
108
|
+
/** Keep the document out of listings, reachable only by name. */
|
|
109
|
+
readonly unlisted?: boolean;
|
|
110
|
+
}
|
|
111
|
+
/** A validated, frozen plugin, as returned by `definePlugin`. */
|
|
112
|
+
export interface PluginDefinition {
|
|
113
|
+
/** The top-level crtr command name, lowercase kebab-case. */
|
|
114
|
+
readonly name: string;
|
|
115
|
+
/** What the plugin is, written for an agent choosing between commands. */
|
|
116
|
+
readonly description: string;
|
|
117
|
+
/** When the plugin applies. */
|
|
118
|
+
readonly whenToUse: string;
|
|
119
|
+
/** The short outcome, shown in listings. */
|
|
120
|
+
readonly summary: string;
|
|
121
|
+
/** The top-level command entry an agent reads before descending into it. */
|
|
122
|
+
readonly rootEntry: {
|
|
123
|
+
readonly concept: string;
|
|
124
|
+
readonly description: string;
|
|
125
|
+
readonly whenToUse: string;
|
|
126
|
+
};
|
|
127
|
+
/** Top-level branches and leaves by camelCase key. */
|
|
128
|
+
readonly commands: Record<string, LeafNode | BranchNode>;
|
|
129
|
+
/** Memory documents installed with the plugin; empty when none were declared. */
|
|
130
|
+
readonly memory: readonly MemoryDoc[];
|
|
131
|
+
/** Per-plugin timeout overrides crtr applies to requests. */
|
|
132
|
+
readonly timeouts?: {
|
|
133
|
+
readonly connectMs?: number;
|
|
134
|
+
readonly requestMs?: number;
|
|
135
|
+
readonly streamIdleMs?: number;
|
|
136
|
+
};
|
|
137
|
+
/** The plugin's integer major version; a plugin without one is treated as major 1. */
|
|
138
|
+
readonly version?: number;
|
|
139
|
+
/** True for a capability provider registered in the directory; every leaf then has a `group`. */
|
|
140
|
+
readonly provider?: boolean;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Declares one command whose handler returns a single object matching `output`.
|
|
144
|
+
* `params` may be omitted when the command takes no input.
|
|
145
|
+
*/
|
|
146
|
+
export declare function defineLeaf<P extends object = {}, const O extends object = {}>(spec: {
|
|
147
|
+
description: string;
|
|
148
|
+
whenToUse: string;
|
|
149
|
+
summary: string;
|
|
150
|
+
tier?: 'normal' | 'common' | 'important';
|
|
151
|
+
group?: string;
|
|
152
|
+
sideEffect?: boolean;
|
|
153
|
+
params?: P;
|
|
154
|
+
output: O;
|
|
155
|
+
effects: readonly string[];
|
|
156
|
+
handler: (input: Input<P>, ctx: LeafContext) => Output<O> | Promise<Output<O>>;
|
|
157
|
+
}): LeafNode;
|
|
158
|
+
/**
|
|
159
|
+
* Declares one command whose handler yields objects instead of returning one. The generated
|
|
160
|
+
* REST mapping is marked streaming; see the errors and streaming guide.
|
|
161
|
+
*/
|
|
162
|
+
export declare function defineStreamingLeaf<P extends object = {}, O extends object = {}>(spec: {
|
|
163
|
+
description: string;
|
|
164
|
+
whenToUse: string;
|
|
165
|
+
summary: string;
|
|
166
|
+
tier?: 'normal' | 'common' | 'important';
|
|
167
|
+
group?: string;
|
|
168
|
+
sideEffect?: boolean;
|
|
169
|
+
params?: P;
|
|
170
|
+
output: O;
|
|
171
|
+
effects: readonly string[];
|
|
172
|
+
handler: (input: Input<P>, ctx: LeafContext) => AsyncIterable<object>;
|
|
173
|
+
}): LeafNode;
|
|
174
|
+
/** Groups commands under one name. */
|
|
175
|
+
export declare function defineBranch(spec: Omit<BranchNode, 'kind'>): BranchNode;
|
|
176
|
+
/**
|
|
177
|
+
* Validates the whole declaration and returns a frozen plugin. Throws `PluginDefinitionError`
|
|
178
|
+
* on any malformed name, text, parameter, output field, or memory document, so a plugin that
|
|
179
|
+
* constructs successfully is one crtr can install.
|
|
180
|
+
*/
|
|
181
|
+
export declare function definePlugin(spec: {
|
|
182
|
+
name: string;
|
|
183
|
+
description: string;
|
|
184
|
+
whenToUse: string;
|
|
185
|
+
summary: string;
|
|
186
|
+
rootEntry: {
|
|
187
|
+
concept: string;
|
|
188
|
+
description: string;
|
|
189
|
+
whenToUse: string;
|
|
190
|
+
};
|
|
191
|
+
commands: Record<string, LeafNode | BranchNode>;
|
|
192
|
+
memory?: readonly MemoryDoc[];
|
|
193
|
+
timeouts?: {
|
|
194
|
+
connectMs?: number;
|
|
195
|
+
requestMs?: number;
|
|
196
|
+
streamIdleMs?: number;
|
|
197
|
+
};
|
|
198
|
+
version?: number;
|
|
199
|
+
provider?: boolean;
|
|
200
|
+
}): PluginDefinition;
|
|
201
|
+
/**
|
|
202
|
+
* The lowercase kebab-case name crtr exposes for a camelCase key. Throws
|
|
203
|
+
* `PluginDefinitionError` when the key does not convert.
|
|
204
|
+
*/
|
|
205
|
+
export declare function kebab(key: string): string;
|
package/dist/define.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var w=Object.defineProperty;var r=(e,t)=>w(e,"name",{value:t,configurable:!0});import{mentionsFile as j,outputBaseType as c}from"@crouter/api/command-manifest";class f extends Error{static{r(this,"PluginDefinitionError")}constructor(t){super(t),this.name="PluginDefinitionError"}}function C(e){return{kind:"leaf",description:e.description,whenToUse:e.whenToUse,summary:e.summary,...e.tier!==void 0?{tier:e.tier}:{},...e.group!==void 0?{group:e.group}:{},...e.sideEffect!==void 0?{sideEffect:e.sideEffect}:{},params:e.params??{},output:e.output,effects:e.effects,streaming:!1,run:r((t,i)=>Promise.resolve(e.handler(t,i)),"run")}}r(C,"defineLeaf");function I(e){return{kind:"leaf",description:e.description,whenToUse:e.whenToUse,summary:e.summary,...e.tier!==void 0?{tier:e.tier}:{},...e.group!==void 0?{group:e.group}:{},...e.sideEffect!==void 0?{sideEffect:e.sideEffect}:{},params:e.params??{},output:e.output,effects:e.effects,streaming:!0,run:r((t,i)=>Promise.resolve(e.handler(t,i)),"run")}}r(I,"defineStreamingLeaf");function R(e){return{kind:"branch",...e}}r(R,"defineBranch");function S(e){if(A(e.name,"plugin name"),a(e.description,"plugin description"),a(e.whenToUse,"plugin whenToUse"),a(e.summary,"plugin summary"),a(e.rootEntry?.concept,"plugin rootEntry.concept"),a(e.rootEntry?.description,"plugin rootEntry.description"),a(e.rootEntry?.whenToUse,"plugin rootEntry.whenToUse"),T(e.timeouts),e.version!==void 0&&(typeof e.version!="number"||!Number.isInteger(e.version)||e.version<1))throw new f("plugin version must be an integer of at least 1");if(e.provider!==void 0&&typeof e.provider!="boolean")throw new f("plugin provider must be a boolean");if(e.provider===!0&&e.version===void 0)throw new f("a provider plugin must declare its major version");for(const[t,i]of Object.entries(e.commands))p(i,[e.name,t]);if(e.provider===!0){const t=y(e.commands,[e.name]);t!==void 0&&n(t,"needs a group because the plugin is a provider")}for(const t of e.memory??[])E(t);return Object.freeze({name:e.name,description:e.description,whenToUse:e.whenToUse,summary:e.summary,rootEntry:Object.freeze({...e.rootEntry}),commands:b(e.commands),memory:Object.freeze((e.memory??[]).map(t=>Object.freeze({...t}))),...e.timeouts!==void 0?{timeouts:Object.freeze({...e.timeouts})}:{},...e.version!==void 0?{version:e.version}:{},...e.provider!==void 0?{provider:e.provider}:{}})}r(S,"definePlugin");function y(e,t){for(const[i,s]of Object.entries(e)){const o=[...t,i];if(s.kind==="leaf"){if(s.group===void 0)return o;continue}const u=y(s.children,o);if(u!==void 0)return u}}r(y,"firstLeafWithoutGroup");function b(e){return Object.freeze(Object.fromEntries(Object.entries(e).map(([t,i])=>[t,O(i)])))}r(b,"snapshotCommands");function O(e){return e.kind==="branch"?Object.freeze({kind:"branch",description:e.description,whenToUse:e.whenToUse,summary:e.summary,...e.tier!==void 0?{tier:e.tier}:{},children:b(e.children)}):Object.freeze({kind:"leaf",description:e.description,whenToUse:e.whenToUse,summary:e.summary,...e.tier!==void 0?{tier:e.tier}:{},params:k(e.params),output:g(e.output),effects:Object.freeze([...e.effects]),streaming:e.streaming,...e.group!==void 0?{group:e.group}:{},...e.sideEffect!==void 0?{sideEffect:e.sideEffect}:{},run:e.run})}r(O,"snapshotNode");function k(e){return Object.freeze(Object.fromEntries(Object.entries(e).map(([t,i])=>[t,$(i)])))}r(k,"snapshotParams");function $(e){return e.kind==="flag"&&e.type==="enum"?Object.freeze({...e,choices:Object.freeze([...e.choices])}):Object.freeze({...e})}r($,"snapshotParam");function g(e){return Object.freeze(Object.fromEntries(Object.entries(e).map(([t,i])=>[t,h(i)])))}r(g,"snapshotOutput");function h(e){return Object.freeze({...e,...e.children!==void 0?{children:g(e.children)}:{},...e.items!==void 0?{items:h(e.items)}:{},...e.values!==void 0?{values:Object.freeze([...e.values])}:{}})}r(h,"snapshotField");function p(e,t){(!d(e)||e.kind!=="branch"&&e.kind!=="leaf")&&n(t,"must be a branch or leaf defined by this package"),a(e.description,`${m(t)} description`),a(e.whenToUse,`${m(t)} whenToUse`),a(e.summary,`${m(t)} summary`),e.tier!==void 0&&e.tier!=="normal"&&e.tier!=="common"&&e.tier!=="important"&&n(t,"tier must be normal, common, or important");const i=t.at(-1);if(i===void 0&&n(t,"has no command name"),v(i,`${m(t)} command key`),e.kind==="branch"){d(e.children)||n(t,"branch children must be an object");for(const[o,u]of Object.entries(e.children))p(u,[...t,o]);return}e.group!==void 0&&(typeof e.group!="string"||!U.test(e.group))&&n(t,`group ${JSON.stringify(e.group)} must match [A-Za-z0-9_-]{1,64}`),e.sideEffect!==void 0&&typeof e.sideEffect!="boolean"&&n(t,"sideEffect must be a boolean"),d(e.params)||n(t,"leaf params must be an object");for(const[o,u]of Object.entries(e.params))v(o,`${m(t)} parameter key`),z(u,[...t,`parameter ${o}`]);Object.values(e.params).filter(o=>d(o)&&o.kind==="positional").length>1&&n(t,"a leaf may have at most one positional parameter"),d(e.output)||n(t,"leaf output must be an object");let s;for(const[o,u]of Object.entries(e.output))o.length===0&&n(t,"output field names must not be empty"),l(u,[...t,`output ${o}`],!0),c(u.type)==="file"&&(s!==void 0&&n(t,`declares file outputs ${s} and ${o}; a leaf may declare at most one file output`),s=o);(!Array.isArray(e.effects)||e.effects.length===0||!e.effects.every(o=>typeof o=="string"))&&n(t,"leaf effects must be a non-empty array of strings"),typeof e.streaming!="boolean"&&n(t,"leaf streaming must be a boolean"),typeof e.run!="function"&&n(t,"leaf handler must be a function")}r(p,"validateNode");function z(e,t){if((!d(e)||e.kind!=="flag"&&e.kind!=="positional"&&e.kind!=="stdin")&&n(t,"must be a parameter descriptor produced by param.*"),a(e.constraint,`${m(t)} constraint`),(typeof e.required!="boolean"||typeof e.repeatable!="boolean")&&n(t,"required and repeatable must be booleans"),e.kind==="stdin"){e.repeatable&&n(t,"stdin cannot be repeatable"),Object.hasOwn(e,"default")&&n(t,"stdin parameters cannot have a default");return}if(e.kind==="positional"){e.type!=="string"&&n(t,"positional parameters must have type string"),Object.hasOwn(e,"default")&&n(t,"positional parameters cannot have a default");return}if(e.type==="enum"?(!Array.isArray(e.choices)||e.choices.length===0||!e.choices.every(s=>typeof s=="string"))&&n(t,"enum parameters need at least one string choice"):e.type!=="string"&&e.type!=="int"&&e.type!=="bool"&&e.type!=="path"&&e.type!=="file"&&n(t,"flag parameters must have a supported type"),(e.type==="bool"||e.type==="path"||e.type==="file")&&e.repeatable&&n(t,`${e.type} parameters cannot be repeatable`),e.type==="path"&&e.encoding!=="text"&&e.encoding!=="base64"&&n(t,"path parameters must have text or base64 encoding"),e.type!=="path"&&e.encoding!==void 0&&n(t,"encoding is only valid on path parameters"),e.repeatable&&Object.hasOwn(e,"default")&&n(t,"repeatable parameters cannot have a default"),!Object.hasOwn(e,"default"))return;const i=e.default;e.type==="string"&&typeof i!="string"&&n(t,"string parameter defaults must be strings"),e.type==="int"&&(typeof i!="number"||!Number.isInteger(i))&&n(t,"int parameter defaults must be integers"),e.type==="bool"&&typeof i!="boolean"&&n(t,"bool parameter defaults must be booleans"),e.type==="enum"&&(typeof i!="string"||!Array.isArray(e.choices)||!e.choices.includes(i))&&n(t,"enum parameter defaults must be one of their choices"),(e.type==="path"||e.type==="file")&&n(t,`${e.type} parameters cannot have a default`)}r(z,"validateParam");function l(e,t,i){(!d(e)||typeof e.type!="string"||e.type.length===0||typeof e.constraint!="string"||typeof e.required!="boolean")&&n(t,"must be an output field descriptor produced by field.*");const s=c(e.type);if(e.kind!==void 0&&s!=="file"&&n(t,"is a parameter descriptor; declare output fields with field.*"),j(e.type)&&s!=="file"&&n(t,`has type ${e.type}; a file output is one file, declared file or file | null`),s==="file"&&!i&&n(t,"is a file, which is only valid as a top-level output field"),e.children!==void 0){s!=="object"&&n(t,"children are only valid on an object field"),d(e.children)||n(t,"children must be an object of output fields");for(const[o,u]of Object.entries(e.children))o.length===0&&n(t,"member names must not be empty"),l(u,[...t,`member ${o}`],!1)}e.items!==void 0&&(s!=="array"&&n(t,"items are only valid on an array field"),l(e.items,[...t,"items"],!1)),e.values!==void 0&&(s!=="enum"&&n(t,"values are only valid on an enum field"),(!Array.isArray(e.values)||e.values.length===0||!e.values.every(o=>typeof o=="string"&&o.length>0))&&n(t,"enum values must be a non-empty array of non-empty strings"),new Set(e.values).size!==e.values.length&&n(t,"enum values must be unique"))}r(l,"validateField");function E(e){if(!d(e)||typeof e.name!="string"||!x(e.name))throw new f("memory document name must be a safe relative path");if(e.kind!=="knowledge"&&e.kind!=="preference")throw new f(`memory document ${e.name} has an invalid kind`);if(a(e.whenAndWhyToRead,`memory document ${e.name} whenAndWhyToRead`),typeof e.body!="string")throw new f(`memory document ${e.name} body must be a string`);if(e.unlisted!==void 0&&typeof e.unlisted!="boolean")throw new f(`memory document ${e.name} unlisted must be a boolean`)}r(E,"validateMemoryDoc");function T(e){if(e!==void 0){if(!d(e))throw new f("timeouts must be an object");for(const t of["connectMs","requestMs","streamIdleMs"]){const i=e[t];if(i!==void 0&&(typeof i!="number"||!Number.isInteger(i)||i<=0))throw new f(`timeouts.${t} must be a positive integer`)}}}r(T,"assertTimeouts");const U=/^[A-Za-z0-9_-]{1,64}$/;function v(e,t){if(P(e).replace(/-([a-z])/g,(s,o)=>o.toUpperCase())!==e)throw new f(`${t} must round-trip between camelCase and kebab-case`)}r(v,"assertKey");function A(e,t){if(!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(e))throw new f(`${t} must be lowercase kebab-case`)}r(A,"assertKebabName");function P(e){const t=e.replace(/[A-Z]/g,i=>`-${i.toLowerCase()}`);if(!/^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(t))throw new f(`invalid key ${JSON.stringify(e)}; use camelCase keys that convert to lowercase kebab-case`);return t}r(P,"kebab");function a(e,t){if(typeof e!="string"||e.length===0)throw new f(`${t} must be a non-empty string`)}r(a,"assertText");function x(e){return e.length>0&&!e.startsWith("/")&&!e.includes("\\")&&e.split("/").every(t=>t.length>0&&t!=="."&&t!=="..")}r(x,"isSafeRelativePath");function d(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}r(d,"isRecord");function n(e,t){throw new f(`${m(e)} ${t}`)}r(n,"fail");function m(e){return e.join(" ")}r(m,"formatPath");export{f as PluginDefinitionError,R as defineBranch,C as defineLeaf,S as definePlugin,I as defineStreamingLeaf,P as kebab};
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** What an `approval_required` error asks the user. */
|
|
2
|
+
export interface LeafErrorQuestion {
|
|
3
|
+
readonly title: string;
|
|
4
|
+
readonly body: string;
|
|
5
|
+
/** Ways of approving the call as made; the runtime adds the `deny` option itself. */
|
|
6
|
+
readonly options: readonly {
|
|
7
|
+
readonly option_id: string;
|
|
8
|
+
readonly label: string;
|
|
9
|
+
}[];
|
|
10
|
+
}
|
|
11
|
+
/** What a `LeafError` reports. */
|
|
12
|
+
export interface LeafErrorOptions {
|
|
13
|
+
/** Lowercase snake_case, and not one of crtr's reserved codes. */
|
|
14
|
+
readonly code: string;
|
|
15
|
+
/** What went wrong, written for the agent that ran the command. */
|
|
16
|
+
readonly message: string;
|
|
17
|
+
/** HTTP status from 400 through 599. Defaults to 400. */
|
|
18
|
+
readonly status?: number;
|
|
19
|
+
/** The parameter at fault, by its kebab-case name. */
|
|
20
|
+
readonly field?: string;
|
|
21
|
+
/** The action that would resolve this. */
|
|
22
|
+
readonly next?: string;
|
|
23
|
+
/** The value that was rejected. */
|
|
24
|
+
readonly received?: unknown;
|
|
25
|
+
/** Tells crtr to refetch the archive and replay the command once. */
|
|
26
|
+
readonly manifestStale?: true;
|
|
27
|
+
/** The question an `approval_required` error asks the user, serialized in the error body. */
|
|
28
|
+
readonly question?: LeafErrorQuestion;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Throw this from a handler to report an expected application error. The Fetch handler turns
|
|
32
|
+
* it into the non-2xx error envelope crtr reads; anything else thrown becomes a 500
|
|
33
|
+
* `handler_failed`. The constructor throws when `code` is reserved or malformed, or when
|
|
34
|
+
* `status` is outside 400–599, and throws `PluginDefinitionError` for `unauthorized`,
|
|
35
|
+
* `scope_missing`, and `approval_required`, which only the kit's own helpers emit.
|
|
36
|
+
* `provider_refused` always has status 403.
|
|
37
|
+
*/
|
|
38
|
+
export declare class LeafError extends Error {
|
|
39
|
+
/** Lowercase snake_case error code; non-writable at runtime. */
|
|
40
|
+
readonly code: string;
|
|
41
|
+
/** HTTP status of the error response; 400 when none was given. */
|
|
42
|
+
readonly status: number;
|
|
43
|
+
/** The parameter at fault, by its kebab-case name. */
|
|
44
|
+
readonly field?: string;
|
|
45
|
+
/** The action that would resolve this. */
|
|
46
|
+
readonly next?: string;
|
|
47
|
+
/** The value that was rejected. */
|
|
48
|
+
readonly received?: unknown;
|
|
49
|
+
/** Tells crtr to refetch the archive and replay the command once. */
|
|
50
|
+
readonly manifestStale?: true;
|
|
51
|
+
/** The question an `approval_required` error asks the user. */
|
|
52
|
+
readonly question?: LeafErrorQuestion;
|
|
53
|
+
constructor(options: LeafErrorOptions);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Builds a `LeafError` with one of the codes only the kit emits (`unauthorized`,
|
|
57
|
+
* `scope_missing`, `approval_required`). Kit-internal: not exported from the package.
|
|
58
|
+
*/
|
|
59
|
+
export declare function kitLeafError(options: LeafErrorOptions): LeafError;
|
|
60
|
+
export declare function leafErrorResponse(error: LeafError): Response;
|
|
61
|
+
export declare function errorResponse(status: number, error: {
|
|
62
|
+
readonly code: string;
|
|
63
|
+
readonly message: string;
|
|
64
|
+
readonly field?: string;
|
|
65
|
+
readonly next?: string;
|
|
66
|
+
readonly received?: unknown;
|
|
67
|
+
readonly manifest_stale?: true;
|
|
68
|
+
readonly question?: LeafErrorQuestion;
|
|
69
|
+
}): Response;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var s=Object.defineProperty;var r=(t,e)=>s(t,"name",{value:e,configurable:!0});import{PluginDefinitionError as a}from"./define.js";const i=/^[a-z][a-z0-9]*(_[a-z0-9]+)*$/,d=new Set(["internal","unknown_path","command_collision","cli_protocol_error"]),u=new Set(["unauthorized","scope_missing","approval_required"]),f=403;class c extends Error{static{r(this,"LeafError")}status;field;next;received;manifestStale;question;constructor(e){if(!i.test(e.code)||d.has(e.code))throw new Error(`LeafError code ${JSON.stringify(e.code)} must be lowercase snake_case and must not be reserved`);if(u.has(e.code)&&!n)throw new a(`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");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"?f:e.status??400,this.field=e.field,this.next=e.next,this.received=e.received,this.manifestStale=e.manifestStale,this.question=e.question}}let n=!1;function h(t){n=!0;try{return new c(t)}finally{n=!1}}r(h,"kitLeafError");function m(t){return l(t.status,{code:t.code,message:t.message,...t.field!==void 0?{field:t.field}:{},...t.next!==void 0?{next:t.next}:{},...t.received!==void 0?{received:t.received}:{},...t.manifestStale===!0?{manifest_stale:!0}:{},...t.question!==void 0?{question:t.question}:{}})}r(m,"leafErrorResponse");function l(t,e){return new Response(JSON.stringify({error:e}),{status:t,headers:{"Content-Type":"application/json"}})}r(l,"errorResponse");export{c as LeafError,l as errorResponse,h as kitLeafError,m as leafErrorResponse};
|
package/dist/fields.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** One declared field of a leaf's result, as returned by a `field.*` builder. */
|
|
2
|
+
export interface OutField<T extends string, Req extends boolean> {
|
|
3
|
+
/** Manifest type string, e.g. `string`, `int`, or `string | null`. */
|
|
4
|
+
readonly type: T;
|
|
5
|
+
/** What the value means, written for the agent reading the result. */
|
|
6
|
+
readonly constraint: string;
|
|
7
|
+
/** True unless the field was declared with `{ required: false }`. */
|
|
8
|
+
readonly required: Req;
|
|
9
|
+
/** An object field's members by name, set by `field.object({ … })`. */
|
|
10
|
+
readonly children?: Readonly<Record<string, OutField<string, boolean>>>;
|
|
11
|
+
/** An array field's element shape, set by `field.array(field.…)`. */
|
|
12
|
+
readonly items?: OutField<string, boolean>;
|
|
13
|
+
/** An enum field's allowed values, set by `field.enum([…])`. */
|
|
14
|
+
readonly values?: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The shape a member or element must have. Constraining by shape instead of by
|
|
18
|
+
* `OutField<string, boolean>` keeps an inline member's `required` at its builder's default
|
|
19
|
+
* rather than widening it to `boolean`.
|
|
20
|
+
*/
|
|
21
|
+
type FieldShape = {
|
|
22
|
+
readonly type: string;
|
|
23
|
+
};
|
|
24
|
+
/** An object field with typed members, as returned by `field.object({ … })`. */
|
|
25
|
+
export type ObjectField<C extends Record<string, FieldShape>, Req extends boolean> = Omit<OutField<'object', Req>, 'children'> & {
|
|
26
|
+
readonly children: C;
|
|
27
|
+
};
|
|
28
|
+
/** An array field with a typed element, as returned by `field.array(field.…)`. */
|
|
29
|
+
export type ArrayField<I extends FieldShape, Req extends boolean> = Omit<OutField<'array', Req>, 'items'> & {
|
|
30
|
+
readonly items: I;
|
|
31
|
+
};
|
|
32
|
+
/** A field restricted to fixed values, as returned by `field.enum([…])`. */
|
|
33
|
+
export type EnumField<V extends string, Req extends boolean> = OutField<'enum', Req> & {
|
|
34
|
+
readonly values: readonly V[];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A file, as returned by `field.file`. The same descriptor declares a flat file parameter
|
|
38
|
+
* (placed in a leaf's `params`) or the leaf's one top-level file output (placed in `output`).
|
|
39
|
+
*/
|
|
40
|
+
export interface FileField<Req extends boolean> extends OutField<'file', Req> {
|
|
41
|
+
readonly kind: 'flag';
|
|
42
|
+
readonly repeatable: false;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* What a leaf returns in a file output: the upload it PUT its bytes to, named by the
|
|
46
|
+
* `Crtr-Upload-Id` request header, with the byte count and the hex SHA-256 digest. The runtime
|
|
47
|
+
* replaces it with the path of the fetched file before the caller sees the result.
|
|
48
|
+
*/
|
|
49
|
+
export interface FileOutputValue {
|
|
50
|
+
readonly upload_id: string;
|
|
51
|
+
readonly size: number;
|
|
52
|
+
readonly sha256: string;
|
|
53
|
+
}
|
|
54
|
+
type FieldValue<F> = F extends {
|
|
55
|
+
type: `${infer B} | null`;
|
|
56
|
+
} ? FieldValue<Omit<F, 'type'> & {
|
|
57
|
+
type: B;
|
|
58
|
+
}> | null : F extends {
|
|
59
|
+
type: 'enum';
|
|
60
|
+
values: readonly (infer V)[];
|
|
61
|
+
} ? V : F extends {
|
|
62
|
+
type: 'file';
|
|
63
|
+
} ? FileOutputValue : F extends {
|
|
64
|
+
type: 'object';
|
|
65
|
+
children: infer C;
|
|
66
|
+
} ? Output<C> : F extends {
|
|
67
|
+
type: 'object';
|
|
68
|
+
} ? Record<string, unknown> : F extends {
|
|
69
|
+
type: 'array';
|
|
70
|
+
items: infer I;
|
|
71
|
+
} ? readonly FieldValue<I>[] : F extends {
|
|
72
|
+
type: 'array';
|
|
73
|
+
} ? readonly unknown[] : F extends {
|
|
74
|
+
type: 'int' | 'number';
|
|
75
|
+
} ? number : F extends {
|
|
76
|
+
type: 'bool';
|
|
77
|
+
} ? boolean : F extends {
|
|
78
|
+
type: 'string' | 'path' | 'markdown';
|
|
79
|
+
} ? string : unknown;
|
|
80
|
+
/**
|
|
81
|
+
* The result type inferred from a leaf's output object: required fields present, the rest
|
|
82
|
+
* optional, and each value typed from its field's manifest type.
|
|
83
|
+
*/
|
|
84
|
+
export type Output<O> = {
|
|
85
|
+
[K in keyof O as O[K] extends {
|
|
86
|
+
required: true;
|
|
87
|
+
} ? K : never]: FieldValue<O[K]>;
|
|
88
|
+
} & {
|
|
89
|
+
[K in keyof O as O[K] extends {
|
|
90
|
+
required: true;
|
|
91
|
+
} ? never : K]?: FieldValue<O[K]>;
|
|
92
|
+
};
|
|
93
|
+
type FieldOptions<Req extends boolean> = {
|
|
94
|
+
required?: Req;
|
|
95
|
+
};
|
|
96
|
+
/** A field holding an object: pass its members to type them, or only a constraint for an untyped object. */
|
|
97
|
+
declare function object<const C extends Record<string, FieldShape>, Req extends boolean = true>(children: C, constraint?: string, opts?: FieldOptions<Req>): ObjectField<C, Req>;
|
|
98
|
+
declare function object<Req extends boolean = true>(constraint: string, opts?: FieldOptions<Req>): OutField<'object', Req>;
|
|
99
|
+
/**
|
|
100
|
+
* A field holding an array: pass the element's field to type it (its `required` is ignored),
|
|
101
|
+
* or only a constraint for an untyped array. A typed array's constraint defaults to its element's.
|
|
102
|
+
*/
|
|
103
|
+
declare function array<const I extends FieldShape, Req extends boolean = true>(items: I, constraint?: string, opts?: FieldOptions<Req>): ArrayField<I, Req>;
|
|
104
|
+
declare function array<Req extends boolean = true>(constraint: string, opts?: FieldOptions<Req>): OutField<'array', Req>;
|
|
105
|
+
/** The output-field builders. Fields are required unless `{ required: false }` is passed. */
|
|
106
|
+
export declare const field: {
|
|
107
|
+
/** A string field. */
|
|
108
|
+
string<Req extends boolean = true>(constraint: string, opts?: FieldOptions<Req>): OutField<"string", Req>;
|
|
109
|
+
/** An integer field. */
|
|
110
|
+
int<Req extends boolean = true>(constraint: string, opts?: FieldOptions<Req>): OutField<"int", Req>;
|
|
111
|
+
/** A field holding any number. */
|
|
112
|
+
number<Req extends boolean = true>(constraint: string, opts?: FieldOptions<Req>): OutField<"number", Req>;
|
|
113
|
+
/** A boolean field. */
|
|
114
|
+
bool<Req extends boolean = true>(constraint: string, opts?: FieldOptions<Req>): OutField<"bool", Req>;
|
|
115
|
+
object: typeof object;
|
|
116
|
+
array: typeof array;
|
|
117
|
+
/** A string field holding one of `values`, which the handler sees as literal types. */
|
|
118
|
+
enum<const V extends string, Req extends boolean = true>(values: readonly V[], constraint: string, opts?: FieldOptions<Req>): EnumField<V, Req>;
|
|
119
|
+
/**
|
|
120
|
+
* A file. In a leaf's `params` it is a flat file parameter: the agent passes a runtime path and
|
|
121
|
+
* the handler receives a signed `https` download link as a string. In a leaf's `output` it is
|
|
122
|
+
* the leaf's one file output, which must be top-level: the handler PUTs the bytes to the
|
|
123
|
+
* `Crtr-Upload-Link` request header and returns `{ upload_id, size, sha256 }`. Optional unless
|
|
124
|
+
* `{ required: true }` is passed, since a result may omit its file.
|
|
125
|
+
*/
|
|
126
|
+
file<Req extends boolean = false>(constraint: string, opts?: FieldOptions<Req>): FileField<Req>;
|
|
127
|
+
/** A string field crtr renders as markdown. */
|
|
128
|
+
markdown<Req extends boolean = true>(constraint: string, opts?: FieldOptions<Req>): OutField<"markdown", Req>;
|
|
129
|
+
/** A string field holding a file path. */
|
|
130
|
+
path<Req extends boolean = true>(constraint: string, opts?: FieldOptions<Req>): OutField<"path", Req>;
|
|
131
|
+
/**
|
|
132
|
+
* A field of any non-empty manifest type string. Its handler value is `unknown`, so prefer a
|
|
133
|
+
* named builder whenever the result type is known.
|
|
134
|
+
*/
|
|
135
|
+
of<const T extends string, Req extends boolean = true>(type: T, constraint: string, opts?: FieldOptions<Req>): OutField<T, Req>;
|
|
136
|
+
/** Allows `null` alongside the wrapped field's value, keeping its members, element, or values. Does not make the field optional. */
|
|
137
|
+
nullable<F extends OutField<string, boolean>>(inner: F): Omit<F, "type"> & {
|
|
138
|
+
readonly type: `${F["type"]} | null`;
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
export {};
|
package/dist/fields.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var i=Object.defineProperty;var u=(r,e)=>i(r,"name",{value:e,configurable:!0});function n(r,e,t={}){return{type:r,constraint:e,required:t.required??!0}}u(n,"defineField");function a(r,e,t){return typeof r=="string"?n("object",r,e??{}):{...n("object",e??"",t),children:r}}u(a,"object");function l(r,e,t){return typeof r=="string"?n("array",r,e??{}):{...n("array",e??r.constraint,t),items:r}}u(l,"array");const f={string(r,e={}){return n("string",r,e)},int(r,e={}){return n("int",r,e)},number(r,e={}){return n("number",r,e)},bool(r,e={}){return n("bool",r,e)},object:a,array:l,enum(r,e,t={}){return{...n("enum",e,t),values:r}},file(r,e={}){return{kind:"flag",type:"file",constraint:r,required:e.required??!1,repeatable:!1}},markdown(r,e={}){return n("markdown",r,e)},path(r,e={}){return n("path",r,e)},of(r,e,t={}){return n(r,e,t)},nullable(r){return{...r,type:`${r.type} | null`}}};export{f as field};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from './params.js';
|
|
2
|
+
export * from './fields.js';
|
|
3
|
+
export * from './define.js';
|
|
4
|
+
export * from './manifest.js';
|
|
5
|
+
export { compareManifests, type BreakingManifestChange } from './compare.js';
|
|
6
|
+
export * from './bundle.js';
|
|
7
|
+
export * from './serve.js';
|
|
8
|
+
export { createCallerVerifier, type CallerVerifier, type FetchHandlerAuth } from './verify.js';
|
|
9
|
+
export { postReceipt, ReceiptPostError, type Receipt, type ReceiptOptions, type ReceiptOutbox } from './receipts.js';
|
|
10
|
+
export { LeafError, type LeafErrorOptions, type LeafErrorQuestion } from './errors.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +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 c}from"./verify.js";import{postReceipt as l,ReceiptPostError as E}from"./receipts.js";import{LeafError as n}from"./errors.js";export{n as LeafError,E as ReceiptPostError,m as compareManifests,c as createCallerVerifier,l as postReceipt};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CommandManifestIssue } from '@crouter/api/command-manifest';
|
|
2
|
+
import type { HttpPluginCommandManifest } from '@crouter/api/plugin-manifest';
|
|
3
|
+
import { type PluginDefinition } from './define.js';
|
|
4
|
+
/** Thrown when the generated manifest fails the validator crtr itself applies at install. */
|
|
5
|
+
export declare class ManifestInvalidError extends Error {
|
|
6
|
+
/** Every validation issue found, not just the first. */
|
|
7
|
+
readonly issues: readonly CommandManifestIssue[];
|
|
8
|
+
constructor(issues: readonly CommandManifestIssue[]);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Generates and validates the command manifest crtr installs: one `POST` route per leaf,
|
|
12
|
+
* derived from the command tree. Throws `ManifestInvalidError` when validation finds issues.
|
|
13
|
+
*/
|
|
14
|
+
export declare function buildCommandManifest(plugin: PluginDefinition, options: {
|
|
15
|
+
mountPath: string;
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
}): HttpPluginCommandManifest;
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var w=Object.defineProperty;var s=(e,t)=>w(e,"name",{value:t,configurable:!0});import{validateCommandManifest as y}from"@crouter/api/command-manifest";import{kebab as d}from"./define.js";class f extends Error{static{s(this,"ManifestInvalidError")}issues;constructor(t){super(`Generated command manifest is invalid: ${t.map(n=>n.message).join("; ")}`),this.name="ManifestInvalidError",this.issues=t}}function E(e,t){const n=M(t.mountPath),i={schemaVersion:1,...e.version!==void 0?{version:e.version}:{},...t.baseUrl!==void 0?{baseUrl:t.baseUrl}:{},...e.timeouts!==void 0?{timeouts:e.timeouts}:{},mounts:[{parent:[],node:{...v(e.name,e.description,e.whenToUse,e.summary,void 0),rootEntry:e.rootEntry,children:Object.entries(e.commands).map(([c,m])=>h(c,m,[e.name],n))}}]},r=y(i,{transport:"http",reservedCoreNames:new Set});if(r.issues.length>0)throw new f(r.issues);return i}s(E,"buildCommandManifest");function h(e,t,n,i){const r=d(e),c=[...n,r];if(t.kind==="branch")return{...v(r,t.description,t.whenToUse,t.summary,t.tier),children:Object.entries(t.children).map(([o,a])=>h(o,a,c,i))};const m=Object.entries(t.params).map(([o,a])=>{const u=d(o);return a.kind==="stdin"?{kind:a.kind,name:u,required:a.required,constraint:a.constraint}:{name:u,...a}}),l=Object.fromEntries(m.map(o=>[o.name,{in:"body"}]));return{kind:"leaf",name:r,description:t.description,whenToUse:t.whenToUse,...t.tier!==void 0?{tier:t.tier}:{},summary:t.summary,...t.group!==void 0?{group:t.group}:{},params:m,output:p(t.output),effects:t.effects,rest:{method:"POST",path:`${i}/${c.join("/")}`,...t.streaming?{streaming:!0}:{},params:l}}}s(h,"toManifestNode");function p(e){return Object.entries(e).map(([t,n])=>({name:t,...b(n),required:n.required}))}s(p,"toManifestFields");function b(e){return{type:e.type,constraint:e.constraint,...e.children!==void 0?{children:p(e.children)}:{},...e.items!==void 0?{items:b(e.items)}:{},...e.values!==void 0?{values:[...e.values]}:{}}}s(b,"toManifestShape");function v(e,t,n,i,r){return{kind:"branch",name:e,description:t,whenToUse:n,...r!==void 0?{tier:r}:{},summary:i}}s(v,"branch");function M(e){if(e===""||e==="/")return"";if(!e.startsWith("/")||e.includes("?")||e.includes("#"))throw new f([{code:"command_rest_invalid",message:"mountPath must be an absolute pathname",received:e,expected:"an absolute pathname",next:"Pass the handler mount path, such as /crtr."}]);return e.replace(/\/+$/,"")}s(M,"normalizeMountPath");export{f as ManifestInvalidError,E as buildCommandManifest};
|
package/dist/params.d.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { type FileField } from './fields.js';
|
|
2
|
+
/** How `param.path` hands the caller's file to the handler. */
|
|
3
|
+
export type Encoding = 'text' | 'base64';
|
|
4
|
+
/** A string flag, as returned by `param.string`. */
|
|
5
|
+
export interface FlagStringParam<Req extends boolean, Rep extends boolean> {
|
|
6
|
+
readonly kind: 'flag';
|
|
7
|
+
readonly type: 'string';
|
|
8
|
+
/** What the value must be, written for the agent supplying it. */
|
|
9
|
+
readonly constraint: string;
|
|
10
|
+
/** True when the command cannot run without it. */
|
|
11
|
+
readonly required: Req;
|
|
12
|
+
/** True when the flag may be passed more than once, making the value an array. */
|
|
13
|
+
readonly repeatable: Rep;
|
|
14
|
+
/** Applied by crtr while parsing the command line, never by the server. */
|
|
15
|
+
readonly default?: string;
|
|
16
|
+
}
|
|
17
|
+
/** An integer flag, as returned by `param.int`. */
|
|
18
|
+
export interface FlagIntParam<Req extends boolean, Rep extends boolean> {
|
|
19
|
+
readonly kind: 'flag';
|
|
20
|
+
readonly type: 'int';
|
|
21
|
+
/** What the value must be, written for the agent supplying it. */
|
|
22
|
+
readonly constraint: string;
|
|
23
|
+
/** True when the command cannot run without it. */
|
|
24
|
+
readonly required: Req;
|
|
25
|
+
/** True when the flag may be passed more than once, making the value an array. */
|
|
26
|
+
readonly repeatable: Rep;
|
|
27
|
+
/** Applied by crtr while parsing the command line, never by the server. */
|
|
28
|
+
readonly default?: number;
|
|
29
|
+
}
|
|
30
|
+
/** A boolean flag, as returned by `param.bool`. Never repeatable. */
|
|
31
|
+
export interface FlagBoolParam<Req extends boolean> {
|
|
32
|
+
readonly kind: 'flag';
|
|
33
|
+
readonly type: 'bool';
|
|
34
|
+
/** What the value must be, written for the agent supplying it. */
|
|
35
|
+
readonly constraint: string;
|
|
36
|
+
/** True when the command cannot run without it. */
|
|
37
|
+
readonly required: Req;
|
|
38
|
+
readonly repeatable: false;
|
|
39
|
+
/** Applied by crtr while parsing the command line, never by the server. */
|
|
40
|
+
readonly default?: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** A flag restricted to a fixed set of values, as returned by `param.enum`. */
|
|
43
|
+
export interface FlagEnumParam<C extends string, Req extends boolean, Rep extends boolean> {
|
|
44
|
+
readonly kind: 'flag';
|
|
45
|
+
readonly type: 'enum';
|
|
46
|
+
/** The accepted values; the handler sees them as literal types. */
|
|
47
|
+
readonly choices: readonly C[];
|
|
48
|
+
/** What the value must be, written for the agent supplying it. */
|
|
49
|
+
readonly constraint: string;
|
|
50
|
+
/** True when the command cannot run without it. */
|
|
51
|
+
readonly required: Req;
|
|
52
|
+
/** True when the flag may be passed more than once, making the value an array. */
|
|
53
|
+
readonly repeatable: Rep;
|
|
54
|
+
/** Applied by crtr while parsing the command line, never by the server. */
|
|
55
|
+
readonly default?: C;
|
|
56
|
+
}
|
|
57
|
+
/** A flag carrying a local file's contents, as returned by `param.path`. Never repeatable. */
|
|
58
|
+
export interface FlagPathParam<Req extends boolean, Enc extends Encoding> {
|
|
59
|
+
readonly kind: 'flag';
|
|
60
|
+
readonly type: 'path';
|
|
61
|
+
/** `text` for a UTF-8 string, `base64` for the file bytes as a `Uint8Array`. */
|
|
62
|
+
readonly encoding: Enc;
|
|
63
|
+
/** What the file must contain, written for the agent supplying it. */
|
|
64
|
+
readonly constraint: string;
|
|
65
|
+
/** True when the command cannot run without it. */
|
|
66
|
+
readonly required: Req;
|
|
67
|
+
readonly repeatable: false;
|
|
68
|
+
}
|
|
69
|
+
/** The one positional argument a leaf may take, as returned by `param.positional`. */
|
|
70
|
+
export interface PositionalParam<Req extends boolean, Rep extends boolean> {
|
|
71
|
+
readonly kind: 'positional';
|
|
72
|
+
readonly type: 'string';
|
|
73
|
+
/** What the value must be, written for the agent supplying it. */
|
|
74
|
+
readonly constraint: string;
|
|
75
|
+
/** True when the command cannot run without it. */
|
|
76
|
+
readonly required: Req;
|
|
77
|
+
/** True when the argument may repeat, making the value an array. */
|
|
78
|
+
readonly repeatable: Rep;
|
|
79
|
+
}
|
|
80
|
+
/** The command's standard input, as returned by `param.stdin`. */
|
|
81
|
+
export interface StdinParam<Req extends boolean> {
|
|
82
|
+
readonly kind: 'stdin';
|
|
83
|
+
/** What the input must contain, written for the agent supplying it. */
|
|
84
|
+
readonly constraint: string;
|
|
85
|
+
/** True when the command cannot run without it. */
|
|
86
|
+
readonly required: Req;
|
|
87
|
+
readonly repeatable: false;
|
|
88
|
+
}
|
|
89
|
+
/** Any parameter descriptor a `param.*` builder returns. */
|
|
90
|
+
export type AnyParam = FlagStringParam<boolean, boolean> | FlagIntParam<boolean, boolean> | FlagBoolParam<boolean> | FlagEnumParam<string, boolean, boolean> | FlagPathParam<boolean, Encoding> | FileField<boolean> | PositionalParam<boolean, boolean> | StdinParam<boolean>;
|
|
91
|
+
type Scalar<P> = P extends {
|
|
92
|
+
type: 'enum';
|
|
93
|
+
choices: readonly (infer C)[];
|
|
94
|
+
} ? C : P extends {
|
|
95
|
+
type: 'int';
|
|
96
|
+
} ? number : P extends {
|
|
97
|
+
type: 'bool';
|
|
98
|
+
} ? boolean : P extends {
|
|
99
|
+
type: 'path';
|
|
100
|
+
encoding: 'base64';
|
|
101
|
+
} ? Uint8Array : string;
|
|
102
|
+
type Decoded<P> = P extends {
|
|
103
|
+
repeatable: true;
|
|
104
|
+
} ? Scalar<P>[] : Scalar<P>;
|
|
105
|
+
/**
|
|
106
|
+
* Handler input inferred from a leaf's parameter object: required parameters are present,
|
|
107
|
+
* everything else optional, repeatable parameters arrays, enum choices literal values.
|
|
108
|
+
*/
|
|
109
|
+
export type Input<P> = {
|
|
110
|
+
[K in keyof P as P[K] extends {
|
|
111
|
+
required: true;
|
|
112
|
+
} ? K : never]: Decoded<P[K]>;
|
|
113
|
+
} & {
|
|
114
|
+
[K in keyof P as P[K] extends {
|
|
115
|
+
required: true;
|
|
116
|
+
} ? never : K]?: Decoded<P[K]>;
|
|
117
|
+
};
|
|
118
|
+
/** The parameter builders. Build descriptors with these rather than by hand. */
|
|
119
|
+
export declare const param: {
|
|
120
|
+
/** A string flag. */
|
|
121
|
+
string<Req extends boolean = false, Rep extends boolean = false>(constraint: string, opts?: {
|
|
122
|
+
required?: Req;
|
|
123
|
+
repeatable?: Rep;
|
|
124
|
+
default?: string;
|
|
125
|
+
}): FlagStringParam<Req, Rep>;
|
|
126
|
+
/** A flag restricted to `choices`, which the handler sees as literal types. */
|
|
127
|
+
enum<const C extends string, Req extends boolean = false, Rep extends boolean = false>(choices: readonly C[], constraint: string, opts?: {
|
|
128
|
+
required?: Req;
|
|
129
|
+
repeatable?: Rep;
|
|
130
|
+
default?: NoInfer<C>;
|
|
131
|
+
}): FlagEnumParam<C, Req, Rep>;
|
|
132
|
+
/** An integer flag. */
|
|
133
|
+
int<Req extends boolean = false, Rep extends boolean = false>(constraint: string, opts?: {
|
|
134
|
+
required?: Req;
|
|
135
|
+
repeatable?: Rep;
|
|
136
|
+
default?: number;
|
|
137
|
+
}): FlagIntParam<Req, Rep>;
|
|
138
|
+
/** A boolean flag. Cannot be repeatable. */
|
|
139
|
+
bool<Req extends boolean = false>(constraint: string, opts?: {
|
|
140
|
+
required?: Req;
|
|
141
|
+
default?: boolean;
|
|
142
|
+
}): FlagBoolParam<Req>;
|
|
143
|
+
/**
|
|
144
|
+
* A flag whose value is the contents of a local file the crtr caller names, never the path
|
|
145
|
+
* string. `text` decodes UTF-8; `base64` yields a `Uint8Array`.
|
|
146
|
+
*/
|
|
147
|
+
path<Req extends boolean = false, Enc extends Encoding = "text">(constraint: string, opts?: {
|
|
148
|
+
required?: Req;
|
|
149
|
+
encoding?: Enc;
|
|
150
|
+
}): FlagPathParam<Req, Enc>;
|
|
151
|
+
/**
|
|
152
|
+
* A flat file parameter, the same descriptor as `field.file`. The agent passes a runtime path;
|
|
153
|
+
* a capability-provider call delivers it to the handler as a signed `https` download link
|
|
154
|
+
* string, and a local install sends the string the agent typed unchanged. Never repeatable.
|
|
155
|
+
*/
|
|
156
|
+
file: <Req extends boolean = false>(constraint: string, opts?: {
|
|
157
|
+
required?: Req | undefined;
|
|
158
|
+
}) => FileField<Req>;
|
|
159
|
+
/** The one positional argument a leaf may declare. */
|
|
160
|
+
positional<Req extends boolean = false, Rep extends boolean = false>(constraint: string, opts?: {
|
|
161
|
+
required?: Req;
|
|
162
|
+
repeatable?: Rep;
|
|
163
|
+
}): PositionalParam<Req, Rep>;
|
|
164
|
+
/** The command's standard input. Cannot be repeatable and cannot have a default. */
|
|
165
|
+
stdin<Req extends boolean = false>(constraint: string, opts?: {
|
|
166
|
+
required?: Req;
|
|
167
|
+
}): StdinParam<Req>;
|
|
168
|
+
};
|
|
169
|
+
export {};
|
package/dist/params.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{field as l}from"./fields.js";const i={string(r,e={}){return{kind:"flag",type:"string",constraint:r,required:e.required??!1,repeatable:e.repeatable??!1,...e.default!==void 0?{default:e.default}:{}}},enum(r,e,a={}){return{kind:"flag",type:"enum",choices:r,constraint:e,required:a.required??!1,repeatable:a.repeatable??!1,...a.default!==void 0?{default:a.default}:{}}},int(r,e={}){return{kind:"flag",type:"int",constraint:r,required:e.required??!1,repeatable:e.repeatable??!1,...e.default!==void 0?{default:e.default}:{}}},bool(r,e={}){return{kind:"flag",type:"bool",constraint:r,required:e.required??!1,repeatable:!1,...e.default!==void 0?{default:e.default}:{}}},path(r,e={}){return{kind:"flag",type:"path",constraint:r,required:e.required??!1,repeatable:!1,encoding:e.encoding??"text"}},file:l.file,positional(r,e={}){return{kind:"positional",type:"string",constraint:r,required:e.required??!1,repeatable:e.repeatable??!1}},stdin(r,e={}){return{kind:"stdin",constraint:r,required:e.required??!1,repeatable:!1}}};export{i as param};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The receipt persisted by the provider and sent to the directory ledger. */
|
|
2
|
+
export interface Receipt {
|
|
3
|
+
readonly receipt_id: string;
|
|
4
|
+
readonly provider: string;
|
|
5
|
+
readonly tool: string;
|
|
6
|
+
readonly sub: string;
|
|
7
|
+
readonly grantee: string;
|
|
8
|
+
readonly payer: string;
|
|
9
|
+
readonly units: number;
|
|
10
|
+
readonly unit: string;
|
|
11
|
+
readonly usd_equivalent: number;
|
|
12
|
+
readonly request_id: string;
|
|
13
|
+
readonly run_id?: string;
|
|
14
|
+
readonly time: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ReceiptOutbox {
|
|
17
|
+
save(receipt: Receipt): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface ReceiptOptions {
|
|
20
|
+
/** The provider's authenticated principal (e.g. `app:sms`), not its plugin name. */
|
|
21
|
+
readonly provider: string;
|
|
22
|
+
/** Prices keyed by tool path after the plugin name (e.g. `send`). */
|
|
23
|
+
readonly rates: Readonly<Record<string, {
|
|
24
|
+
readonly usdPerUnit: number;
|
|
25
|
+
}>>;
|
|
26
|
+
readonly outbox: ReceiptOutbox;
|
|
27
|
+
}
|
|
28
|
+
export declare class ReceiptPostError extends Error {
|
|
29
|
+
readonly permanent: boolean;
|
|
30
|
+
readonly status?: number | undefined;
|
|
31
|
+
constructor(message: string, permanent: boolean, status?: number | undefined);
|
|
32
|
+
}
|
|
33
|
+
/** Post one saved receipt; the provider owns durable retry and permanent-error reporting. */
|
|
34
|
+
export declare function postReceipt(receipt: Receipt, ledger: {
|
|
35
|
+
url: string;
|
|
36
|
+
clientId: string;
|
|
37
|
+
privateKey: string | JsonWebKey;
|
|
38
|
+
keyId?: string;
|
|
39
|
+
}): Promise<void>;
|
package/dist/receipts.js
ADDED
|
@@ -0,0 +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};
|
package/dist/serve.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Receipt, ReceiptOptions } from './receipts.js';
|
|
2
|
+
import { type LeafCaller, type PluginDefinition } from './define.js';
|
|
3
|
+
import { type FetchHandlerAuth } from './verify.js';
|
|
4
|
+
export type { FetchHandlerAuth } from './verify.js';
|
|
5
|
+
/** How the Fetch handler authenticates, advertises itself, and reports unhandled errors. */
|
|
6
|
+
export interface FetchHandlerOptions {
|
|
7
|
+
/** The static bearer token value crtr must send with every request. Exclusive with `auth`. */
|
|
8
|
+
readonly token?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Verify every command request's bearer as a directory-minted outbound token and check the
|
|
11
|
+
* called leaf's group against its `scope`; leaf handlers then get `ctx.caller`. The archive,
|
|
12
|
+
* `GET /versions`, stay public. Exclusive with `token`.
|
|
13
|
+
*/
|
|
14
|
+
readonly auth?: FetchHandlerAuth;
|
|
15
|
+
/**
|
|
16
|
+
* The public URL where the handler is served when request.url is internal. For a versioned
|
|
17
|
+
* handler this is the provider's endpoint, without any `/v<N>` segment.
|
|
18
|
+
*/
|
|
19
|
+
readonly baseUrl?: string;
|
|
20
|
+
/** Receives an unhandled leaf exception before a 500 response is returned. */
|
|
21
|
+
readonly onError?: (error: unknown, commandPath: readonly string[], receipt?: Receipt) => void;
|
|
22
|
+
readonly receipts?: ReceiptOptions;
|
|
23
|
+
readonly requests?: RequestStore;
|
|
24
|
+
/** One metadata-only event for each answered, recognized leaf POST; exceptions are ignored. */
|
|
25
|
+
readonly onAnswer?: (event: LeafAnswerEvent) => void;
|
|
26
|
+
}
|
|
27
|
+
/** `null` is used only by legacy static-token plugins without a verified caller. */
|
|
28
|
+
export type RequestIdentity = Readonly<{
|
|
29
|
+
sub: string;
|
|
30
|
+
grantee: string;
|
|
31
|
+
}> | null;
|
|
32
|
+
export interface LeafAnswerEvent {
|
|
33
|
+
readonly sub?: string;
|
|
34
|
+
readonly grantee?: string;
|
|
35
|
+
readonly initiatingApp?: string;
|
|
36
|
+
readonly act?: LeafCaller['act'];
|
|
37
|
+
readonly jti?: string;
|
|
38
|
+
readonly requestId?: string;
|
|
39
|
+
readonly runId?: string;
|
|
40
|
+
readonly tool: string;
|
|
41
|
+
readonly status: number;
|
|
42
|
+
readonly durationMs: number;
|
|
43
|
+
readonly receiptId?: string;
|
|
44
|
+
readonly replayed: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Persistence is owned by the provider, including an atomic answer + receipt outbox write. */
|
|
47
|
+
export interface RequestStore {
|
|
48
|
+
/** Persist the identity atomically with the request id. False means that id already exists. */
|
|
49
|
+
record(requestId: string, identity: RequestIdentity): Promise<boolean>;
|
|
50
|
+
store(requestId: string, answer: {
|
|
51
|
+
status: number;
|
|
52
|
+
body: string;
|
|
53
|
+
receipt?: Receipt;
|
|
54
|
+
}): Promise<void>;
|
|
55
|
+
read(requestId: string): Promise<{
|
|
56
|
+
identity: RequestIdentity;
|
|
57
|
+
recordedAt: Date | string | number;
|
|
58
|
+
answer: {
|
|
59
|
+
status: number;
|
|
60
|
+
body: string;
|
|
61
|
+
receipt?: Receipt;
|
|
62
|
+
} | null;
|
|
63
|
+
} | null>;
|
|
64
|
+
delete(requestId: string): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns the request handler that serves the whole plugin. `POST` on a command's route decodes
|
|
68
|
+
* the input, runs its handler, and writes the JSON result or the NDJSON stream; a `LeafError`
|
|
69
|
+
* becomes its error envelope and any other exception a 500 `handler_failed`.
|
|
70
|
+
*
|
|
71
|
+
* Given one definition without a `version`, `GET` returns the install archive at any path, and
|
|
72
|
+
* routes are served where the handler is mounted. Given a list of definitions, one per supported
|
|
73
|
+
* major, or one definition with a `version`, each major N is served under `<mount>/v<N>/`:
|
|
74
|
+
* `GET <mount>/versions` answers `{latest, supported}`, `GET <mount>/v<N>/` returns major N's
|
|
75
|
+
* archive, whose routes carry the `/v<N>` prefix. Archives carry an `ETag` and answer 304 on a
|
|
76
|
+
* matching `If-None-Match`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function createFetchHandler(plugins: PluginDefinition | readonly PluginDefinition[], options?: FetchHandlerOptions): (request: Request) => Promise<Response>;
|
package/dist/serve.js
ADDED
|
@@ -0,0 +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)}
|
|
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){O(e,d,t),l.error(d)}},async cancel(l){await r.return?.(l)}});return new Response(w,{headers:{"Content-Type":"application/x-ndjson"}})}s(ce,"streamResponse");function O(n,e,t,r){try{n?.(e,t,r)}catch{}}s(O,"reportUnhandledError");function R(n,e){return new A({code:"handler_output_invalid",message:`Handler returned an invalid value for ${n}.`,status:500,field:n,...e!==void 0?{received:e}:{}})}s(R,"outputInvalid");function q(n,e,t=!1,r){return new A({code:t?"invalid_argument":"invalid_input",message:n,...e!==void 0?{field:e}:{},...t?{received:r}:{}})}s(q,"invalidInput");function U(){return k(404,{code:"no_such_command",message:"No command matches this request."})}s(U,"noSuchCommand");function H(n){return n==="/"?"":n.replace(/\/+$/,"")}s(H,"mountPath");function ue(n){return n.replace(/-([a-z])/g,(e,t)=>t.toUpperCase())}s(ue,"camel");function G(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(G,"constantTimeEqual");function C(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)}s(C,"isRecord");function fe(n){return typeof n=="object"&&n!==null&&Symbol.asyncIterator in n}s(fe,"isAsyncIterable");export{be as createFetchHandler};
|
package/dist/verify.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LeafCaller } from './define.js';
|
|
2
|
+
import { LeafError } from './errors.js';
|
|
3
|
+
/** Verifies outbound tokens minted by the directory at `issuer` for the provider at `audience`. */
|
|
4
|
+
export interface FetchHandlerAuth {
|
|
5
|
+
/** The directory's issuer URL; the JWKS is fetched from `<issuer>/.well-known/jwks.json`. */
|
|
6
|
+
readonly issuer: string;
|
|
7
|
+
/** The provider's registered endpoint URL, exactly as registered; `aud` must equal it. */
|
|
8
|
+
readonly audience: string;
|
|
9
|
+
}
|
|
10
|
+
/** Turns a request's bearer into the verified caller, or throws the 401 `LeafError`. */
|
|
11
|
+
export type CallerVerifier = (request: Request) => Promise<Omit<LeafCaller, 'requestId' | 'runId'>>;
|
|
12
|
+
export declare function createCallerVerifier(auth: FetchHandlerAuth): CallerVerifier;
|
|
13
|
+
/** The 403 for a leaf whose `<provider>:<group>` the verified token does not carry. */
|
|
14
|
+
export declare function scopeMissing(scope: string): LeafError;
|
package/dist/verify.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var h=Object.defineProperty;var i=(e,r)=>h(e,"name",{value:r,configurable:!0});import{JwksCache as f,verifyOutboundToken as l}from"@crouter/identity";import{kitLeafError as s}from"./errors.js";function b(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");const{issuer:r,audience:c}=e,u=new f(r,(o,n)=>fetch(o,n),`${r.replace(/\/+$/,"")}/.well-known/jwks.json`);return async o=>{const n=/^Bearer ([^\s]+)$/.exec(o.headers.get("authorization")??"");if(n===null)throw a("The request carries no bearer token.");let t;try{t=await l({token:n[1],jwks:u,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(d=>d.length>0)),initiatingApp:t.initiating_app,act:t.act,jti:t.jti})}}i(b,"createCallerVerifier");function g(e){return s({code:"scope_missing",status:403,message:`The token does not carry ${e}, which this command needs.`,field:e,received:e})}i(g,"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>."})}i(a,"unauthorized");export{b as createCallerVerifier,g as scopeMissing};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@crouter/plugin",
|
|
3
|
+
"version": "0.3.377",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"!dist/**/__tests__/**",
|
|
16
|
+
"!dist/example/**",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"sideEffects": false,
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"prepack": "node ../../scripts/pack-minify.mjs prepack packages/crouter-plugin",
|
|
23
|
+
"postpack": "node ../../scripts/pack-minify.mjs postpack packages/crouter-plugin"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@crouter/api": "^0.3.377",
|
|
27
|
+
"@crouter/identity": "^0.3.377"
|
|
28
|
+
},
|
|
29
|
+
"license": "GPL-3.0-only",
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/crouton-labs/crouter.git",
|
|
36
|
+
"directory": "packages/crouter-plugin"
|
|
37
|
+
}
|
|
38
|
+
}
|