@nylorun/agents 0.13.0-beta → 0.16.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +139 -0
- package/README.md +220 -235
- package/dist/access.d.ts +6 -97
- package/dist/access.js +1 -103
- package/dist/ag-ui/handler.d.ts +3 -6
- package/dist/ag-ui/handler.js +2 -2
- package/dist/artifacts.d.ts +1 -1
- package/dist/files.d.ts +31 -0
- package/dist/files.js +60 -0
- package/dist/http.d.ts +6 -13
- package/dist/http.js +4 -16
- package/dist/index.d.ts +8 -16
- package/dist/index.js +4 -7
- package/dist/plugins/load.d.ts +3 -2
- package/dist/plugins/load.js +40 -100
- package/dist/plugins/plugin.d.ts +6 -1
- package/dist/plugins/plugin.js +20 -7
- package/dist/reads.d.ts +17 -0
- package/dist/reads.js +17 -0
- package/dist/sandboxes.d.ts +29 -0
- package/dist/sandboxes.js +10 -1
- package/dist/session-client.d.ts +94 -112
- package/dist/session-client.js +80 -107
- package/dist/skills/index.d.ts +1 -1
- package/dist/skills/load.d.ts +14 -3
- package/dist/skills/load.js +50 -32
- package/dist/skills/skills.d.ts +6 -4
- package/dist/skills/skills.js +17 -10
- package/dist/sse.js +1 -1
- package/dist/studio-embed.d.ts +1 -1
- package/dist/studio-embed.js +1 -1
- package/package.json +3 -7
- package/dist/action-handler.d.ts +0 -63
- package/dist/action-handler.js +0 -279
- package/dist/browser.d.ts +0 -104
- package/dist/browser.js +0 -281
- package/dist/delivery-token.d.ts +0 -62
- package/dist/delivery-token.js +0 -159
- package/dist/execute-action.d.ts +0 -16
- package/dist/execute-action.js +0 -288
- package/dist/plugins/launch.d.ts +0 -17
- package/dist/plugins/launch.js +0 -71
- package/dist/sandbox/client.d.ts +0 -63
- package/dist/sandbox/client.js +0 -29
- package/dist/sandbox/index.d.ts +0 -2
- package/dist/sandbox/index.js +0 -1
- package/dist/served-definitions.d.ts +0 -20
- package/dist/served-definitions.js +0 -100
- package/dist/token-endpoint.d.ts +0 -23
- package/dist/token-endpoint.js +0 -30
package/dist/browser.js
DELETED
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `@nylorun/agents/browser`: a Runtime client for browsers and apps that hold no Tenant key
|
|
3
|
-
* (Host features `subject-tokens` and `browser-access`). The page ships a publishable key; the
|
|
4
|
-
* app server's token route returns a subject token for the signed-in person; the client keeps
|
|
5
|
-
* it in memory, fetches a new one a minute before it expires or when the Runtime answers
|
|
6
|
-
* `401 token_expired`, and never asks for two at once.
|
|
7
|
-
*
|
|
8
|
-
* No Node-only module is imported here or below.
|
|
9
|
-
*/
|
|
10
|
-
import { PROTOCOL_HEADER, PROTOCOL_VERSION, PUBLISHABLE_KEY_HEADER, } from "@nylorun/core/compatibility";
|
|
11
|
-
import { AgentsClient } from "./session-client.js";
|
|
12
|
-
/** Fetch a new token this long before the current one expires. */
|
|
13
|
-
const REFRESH_EARLY_MS = 60_000;
|
|
14
|
-
/** The token's claims, read without verifying (the Runtime verifies). */
|
|
15
|
-
export function readClaims(token) {
|
|
16
|
-
const payload = token.split(".")[1];
|
|
17
|
-
if (!payload)
|
|
18
|
-
return {};
|
|
19
|
-
try {
|
|
20
|
-
const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
|
|
21
|
-
const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
|
|
22
|
-
const json = new TextDecoder().decode(Uint8Array.from(atob(padded), (c) => c.charCodeAt(0)));
|
|
23
|
-
return JSON.parse(json);
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
/** A token kept in memory, fetched on demand, one fetch at a time. */
|
|
30
|
-
export class CachedToken {
|
|
31
|
-
fetchToken;
|
|
32
|
-
now;
|
|
33
|
-
current;
|
|
34
|
-
pending;
|
|
35
|
-
constructor(fetchToken, now = Date.now) {
|
|
36
|
-
this.fetchToken = fetchToken;
|
|
37
|
-
this.now = now;
|
|
38
|
-
}
|
|
39
|
-
get(signal) {
|
|
40
|
-
if (this.current && this.current.expiresAt - REFRESH_EARLY_MS > this.now())
|
|
41
|
-
return Promise.resolve(this.current.token);
|
|
42
|
-
this.pending ??= this.fetchToken(signal)
|
|
43
|
-
.then((reply) => {
|
|
44
|
-
const token = typeof reply === "string" ? reply : reply.token;
|
|
45
|
-
if (typeof token !== "string" || token === "")
|
|
46
|
-
throw new Error("The token callback returned no token");
|
|
47
|
-
const expiresAt = typeof reply !== "string" && reply.expiresAt
|
|
48
|
-
? Date.parse(reply.expiresAt)
|
|
49
|
-
: (readClaims(token).exp ?? 0) * 1000;
|
|
50
|
-
this.current = { token, expiresAt };
|
|
51
|
-
return token;
|
|
52
|
-
})
|
|
53
|
-
.finally(() => {
|
|
54
|
-
this.pending = undefined;
|
|
55
|
-
});
|
|
56
|
-
return this.pending;
|
|
57
|
-
}
|
|
58
|
-
invalidate() {
|
|
59
|
-
this.current = undefined;
|
|
60
|
-
}
|
|
61
|
-
/** The subject of the current token, fetching one if needed. */
|
|
62
|
-
async subject(signal) {
|
|
63
|
-
const sub = readClaims(await this.get(signal)).sub;
|
|
64
|
-
if (!sub)
|
|
65
|
-
throw new Error("The subject token names no subject");
|
|
66
|
-
return sub;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/** Reattaching a run the Runtime ended for its token, at most this many times in a row. */
|
|
70
|
-
const MAX_REATTACH = 5;
|
|
71
|
-
function parseFrame(text) {
|
|
72
|
-
let id;
|
|
73
|
-
let data;
|
|
74
|
-
for (const line of text.split("\n")) {
|
|
75
|
-
if (line.startsWith("id: "))
|
|
76
|
-
id = line.slice(4);
|
|
77
|
-
else if (line.startsWith("data: "))
|
|
78
|
-
data = line.slice(6);
|
|
79
|
-
}
|
|
80
|
-
let event;
|
|
81
|
-
try {
|
|
82
|
-
event = data === undefined ? undefined : JSON.parse(data);
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
85
|
-
event = undefined;
|
|
86
|
-
}
|
|
87
|
-
return { text, ...(id ? { id } : {}), ...(event ? { event } : {}) };
|
|
88
|
-
}
|
|
89
|
-
/** The frames of an SSE body, as they arrive. */
|
|
90
|
-
async function* frames(body) {
|
|
91
|
-
const reader = body.getReader();
|
|
92
|
-
const decoder = new TextDecoder();
|
|
93
|
-
let buffer = "";
|
|
94
|
-
try {
|
|
95
|
-
for (;;) {
|
|
96
|
-
const { value, done } = await reader.read();
|
|
97
|
-
if (done)
|
|
98
|
-
break;
|
|
99
|
-
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, "\n");
|
|
100
|
-
let end;
|
|
101
|
-
while ((end = buffer.indexOf("\n\n")) >= 0) {
|
|
102
|
-
const text = buffer.slice(0, end);
|
|
103
|
-
buffer = buffer.slice(end + 2);
|
|
104
|
-
if (text.trim() !== "")
|
|
105
|
-
yield parseFrame(text);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
finally {
|
|
110
|
-
await reader.cancel().catch(() => { });
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
/** The signed-in person's view of the Runtime: their sessions and vaults, the agents they may use. */
|
|
114
|
-
export class BrowserClient {
|
|
115
|
-
/** The underlying Tenant API client, for anything this class does not wrap. */
|
|
116
|
-
client;
|
|
117
|
-
url;
|
|
118
|
-
publishableKey;
|
|
119
|
-
tokens;
|
|
120
|
-
fetcher;
|
|
121
|
-
constructor(options) {
|
|
122
|
-
this.url = options.url.replace(/\/$/, "");
|
|
123
|
-
this.publishableKey = options.publishableKey;
|
|
124
|
-
this.tokens = new CachedToken((signal) => options.token(signal));
|
|
125
|
-
const base = options.fetch ?? globalThis.fetch;
|
|
126
|
-
// Called as a method, a page's window.fetch would lose its receiver.
|
|
127
|
-
this.fetcher = options.fetch ? base : (input, init) => base(input, init);
|
|
128
|
-
this.client = new AgentsClient({
|
|
129
|
-
url: this.url,
|
|
130
|
-
publishableKey: options.publishableKey,
|
|
131
|
-
token: this.tokens,
|
|
132
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
/** The person the tokens are for. */
|
|
136
|
-
subject(signal) {
|
|
137
|
-
return this.tokens.subject(signal);
|
|
138
|
-
}
|
|
139
|
-
/** A current token, e.g. for a request this client does not make itself. */
|
|
140
|
-
token(signal) {
|
|
141
|
-
return this.tokens.get(signal);
|
|
142
|
-
}
|
|
143
|
-
/** The agents the person may use: id, name and description. */
|
|
144
|
-
listAgents(signal) {
|
|
145
|
-
return this.client.listAgents({ ...(signal ? { signal } : {}) });
|
|
146
|
-
}
|
|
147
|
-
listSessions(agentId, signal) {
|
|
148
|
-
return this.client.listSessions({
|
|
149
|
-
...(agentId ? { agentId } : {}),
|
|
150
|
-
...(signal ? { signal } : {}),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
session(sessionId) {
|
|
154
|
-
return this.client.session(sessionId);
|
|
155
|
-
}
|
|
156
|
-
/** The person's file artifacts, in their own sessions (protocol 6). */
|
|
157
|
-
get artifacts() {
|
|
158
|
-
return this.client.artifacts;
|
|
159
|
-
}
|
|
160
|
-
/** A session of the person's own; the owner is always the token's subject. */
|
|
161
|
-
async createSession(options) {
|
|
162
|
-
return this.client.createSession({
|
|
163
|
-
...options,
|
|
164
|
-
ownerUserId: await this.subject(),
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
async createVault(options) {
|
|
168
|
-
return this.client.createVault({ ...options, ownerUserId: await this.subject() });
|
|
169
|
-
}
|
|
170
|
-
async listVaults(signal) {
|
|
171
|
-
return this.client.listVaults(await this.subject(signal), signal);
|
|
172
|
-
}
|
|
173
|
-
/** `fetch` with this client's credentials, retrying once after `401 token_expired`. */
|
|
174
|
-
async send(input, init = {}, retried = false) {
|
|
175
|
-
const headers = new Headers(init.headers);
|
|
176
|
-
headers.set("authorization", `Bearer ${await this.tokens.get(init.signal ?? undefined)}`);
|
|
177
|
-
headers.set(PUBLISHABLE_KEY_HEADER, this.publishableKey);
|
|
178
|
-
headers.set(PROTOCOL_HEADER, String(PROTOCOL_VERSION));
|
|
179
|
-
const response = await this.fetcher(input, { ...init, headers });
|
|
180
|
-
if (response.status !== 401 || retried)
|
|
181
|
-
return response;
|
|
182
|
-
const body = (await response.clone().json().catch(() => undefined));
|
|
183
|
-
if (body?.code !== "token_expired")
|
|
184
|
-
return response;
|
|
185
|
-
await response.body?.cancel().catch(() => { });
|
|
186
|
-
this.tokens.invalidate();
|
|
187
|
-
return this.send(input, init, true);
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* For `@ag-ui/client`'s `HttpAgent`: `new HttpAgent({ url, fetch })`. The `fetch` adds the
|
|
191
|
-
* publishable key and a current token, and when the Runtime ends a run's stream because the
|
|
192
|
-
* token expired or was revoked, it reattaches with a new token from the last event, so the
|
|
193
|
-
* agent sees one run.
|
|
194
|
-
*/
|
|
195
|
-
agUi(agentId) {
|
|
196
|
-
const url = `${this.url}/v1/ag-ui/agents/${encodeURIComponent(agentId)}`;
|
|
197
|
-
const fetchRun = async (input, init = {}) => {
|
|
198
|
-
const target = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
199
|
-
const response = await this.send(target, init);
|
|
200
|
-
const isStream = response.headers.get("content-type")?.includes("text/event-stream");
|
|
201
|
-
const method = (init.method ?? "GET").toUpperCase();
|
|
202
|
-
if (!response.ok || !isStream || !response.body || method !== "POST" || target !== url)
|
|
203
|
-
return response;
|
|
204
|
-
let run = {};
|
|
205
|
-
try {
|
|
206
|
-
run = JSON.parse(String(init.body ?? "{}"));
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
/* not a RunAgentInput: pass the stream through as it is */
|
|
210
|
-
}
|
|
211
|
-
if (!run.threadId)
|
|
212
|
-
return response;
|
|
213
|
-
return new Response(this.stitch(response.body, url, run.threadId, run.runId, init.signal ?? undefined), {
|
|
214
|
-
status: response.status,
|
|
215
|
-
headers: response.headers,
|
|
216
|
-
});
|
|
217
|
-
};
|
|
218
|
-
return { url, fetch: fetchRun };
|
|
219
|
-
}
|
|
220
|
-
/** The thread's messages, for `HttpAgent`'s `initialMessages`. */
|
|
221
|
-
async agUiHistory(agentId, threadId, signal) {
|
|
222
|
-
const response = await this.send(`${this.url}/v1/ag-ui/agents/${encodeURIComponent(agentId)}/threads/${encodeURIComponent(threadId)}/messages`, { method: "GET", ...(signal ? { signal } : {}) });
|
|
223
|
-
if (!response.ok)
|
|
224
|
-
throw new Error(`Runtime HTTP ${response.status}`);
|
|
225
|
-
return (await response.json());
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Passes a run's frames through; on `nylorun.stream_closed` before the run finished, fetches
|
|
229
|
-
* the rest from the last event id and continues without repeating `RUN_STARTED`.
|
|
230
|
-
*/
|
|
231
|
-
stitch(body, url, threadId, runId, signal) {
|
|
232
|
-
const encoder = new TextEncoder();
|
|
233
|
-
const self = this;
|
|
234
|
-
return new ReadableStream({
|
|
235
|
-
async start(controller) {
|
|
236
|
-
let lastId;
|
|
237
|
-
let source = body;
|
|
238
|
-
let reattaching = false;
|
|
239
|
-
try {
|
|
240
|
-
for (let attempt = 0; source && attempt <= MAX_REATTACH; attempt += 1) {
|
|
241
|
-
let closed = false;
|
|
242
|
-
for await (const frame of frames(source)) {
|
|
243
|
-
if (frame.event?.type === "CUSTOM" && frame.event.name === "nylorun.stream_closed") {
|
|
244
|
-
if (frame.id)
|
|
245
|
-
lastId = frame.id;
|
|
246
|
-
closed = true;
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
// The reattached stream starts its run again; the agent already has it.
|
|
250
|
-
if (reattaching && frame.event?.type === "RUN_STARTED")
|
|
251
|
-
continue;
|
|
252
|
-
if (frame.id)
|
|
253
|
-
lastId = frame.id;
|
|
254
|
-
controller.enqueue(encoder.encode(`${frame.text}\n\n`));
|
|
255
|
-
}
|
|
256
|
-
source = null;
|
|
257
|
-
if (!closed || !lastId || signal?.aborted)
|
|
258
|
-
break;
|
|
259
|
-
reattaching = true;
|
|
260
|
-
const query = runId ? `?runId=${encodeURIComponent(runId)}` : "";
|
|
261
|
-
const next = await self.send(`${url}/threads/${encodeURIComponent(threadId)}/events${query}`, {
|
|
262
|
-
method: "GET",
|
|
263
|
-
headers: { accept: "text/event-stream", "last-event-id": lastId },
|
|
264
|
-
...(signal ? { signal } : {}),
|
|
265
|
-
});
|
|
266
|
-
// 204: the run ended with nothing after the last event; there is no more to send.
|
|
267
|
-
if (next.ok && next.status !== 204 && next.body)
|
|
268
|
-
source = next.body;
|
|
269
|
-
}
|
|
270
|
-
controller.close();
|
|
271
|
-
}
|
|
272
|
-
catch (error) {
|
|
273
|
-
controller.error(error);
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
export function createBrowserClient(options) {
|
|
280
|
-
return new BrowserClient(options);
|
|
281
|
-
}
|
package/dist/delivery-token.d.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/** The claims of a verified delivery token. */
|
|
2
|
-
export interface DeliveryClaims {
|
|
3
|
-
readonly iss: string;
|
|
4
|
-
readonly aud: string;
|
|
5
|
-
/** The Action id, or `ping`. */
|
|
6
|
-
readonly sub: string;
|
|
7
|
-
/** The agent or workflow the Action belongs to. */
|
|
8
|
-
readonly agt: string;
|
|
9
|
-
/** The delivery's generation; `0` for a ping. */
|
|
10
|
-
readonly gen: number;
|
|
11
|
-
/** base64url SHA-256 of the request body. */
|
|
12
|
-
readonly bdy: string;
|
|
13
|
-
readonly iat: number;
|
|
14
|
-
readonly exp: number;
|
|
15
|
-
readonly jti: string;
|
|
16
|
-
}
|
|
17
|
-
/** Why a delivery was refused; `code` is sent back to the Runtime. */
|
|
18
|
-
export declare class DeliveryVerificationError extends Error {
|
|
19
|
-
readonly code: "signature_missing" | "signature_invalid" | "key_unknown" | "token_expired";
|
|
20
|
-
readonly name = "DeliveryVerificationError";
|
|
21
|
-
constructor(code: "signature_missing" | "signature_invalid" | "key_unknown" | "token_expired", message: string);
|
|
22
|
-
}
|
|
23
|
-
/** Finds the public key for a `kid`, or `undefined` when the Tenant has none. */
|
|
24
|
-
export type KeyLookup = (kid: string) => Promise<CryptoKey | undefined>;
|
|
25
|
-
/**
|
|
26
|
-
* Verifies `token` against `body`. The keys are the installation's, so a valid signature already
|
|
27
|
-
* binds the issuer to its one Tenant: any `urn:nylorun:tenant:<id>` issuer is accepted (and
|
|
28
|
-
* returned in `iss`) unless `tenantId` names the one expected.
|
|
29
|
-
*/
|
|
30
|
-
export declare function verifyDeliveryToken(token: string | null, options: {
|
|
31
|
-
keys: KeyLookup;
|
|
32
|
-
/** When set, `iss` must name this Tenant. */
|
|
33
|
-
tenantId?: string;
|
|
34
|
-
body: Uint8Array;
|
|
35
|
-
/** When set, `aud` must equal it: the URL this endpoint was registered with. */
|
|
36
|
-
audience?: string;
|
|
37
|
-
now?: number;
|
|
38
|
-
}): Promise<DeliveryClaims>;
|
|
39
|
-
/** base64url SHA-256 of `body`: the token's `bdy` claim. */
|
|
40
|
-
export declare function bodyHash(body: Uint8Array): Promise<string>;
|
|
41
|
-
/**
|
|
42
|
-
* The Tenant's public keys, fetched from `GET /v1/access/jwks` and cached by `kid`. An
|
|
43
|
-
* unknown `kid` refetches once, at most every `refetchMs`, so forged tokens cannot make the
|
|
44
|
-
* endpoint call the Runtime on every request. Concurrent lookups share one in-flight fetch.
|
|
45
|
-
*/
|
|
46
|
-
export declare class JwksCache {
|
|
47
|
-
private readonly source;
|
|
48
|
-
private readonly refetchMs;
|
|
49
|
-
private readonly keys;
|
|
50
|
-
private lastFetch;
|
|
51
|
-
private pending;
|
|
52
|
-
constructor(source: {
|
|
53
|
-
url: string;
|
|
54
|
-
key?: string;
|
|
55
|
-
fetch?: typeof fetch;
|
|
56
|
-
} | {
|
|
57
|
-
keys: readonly JsonWebKey[];
|
|
58
|
-
}, refetchMs?: number);
|
|
59
|
-
readonly lookup: KeyLookup;
|
|
60
|
-
private load;
|
|
61
|
-
private fetchKeys;
|
|
62
|
-
}
|
package/dist/delivery-token.js
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Verifies the delivery token on a request the Runtime sends to an Action endpoint: an ES256
|
|
3
|
-
* JWT signed with the Tenant's current signing key (design: Action endpoints §8.1). WebCrypto
|
|
4
|
-
* only, so the SDK takes no JWT dependency: a JWS ES256 signature is the raw `r || s` pair
|
|
5
|
-
* WebCrypto's ECDSA verifies.
|
|
6
|
-
*/
|
|
7
|
-
import { PROTOCOL_HEADER, PROTOCOL_VERSION, isTenantId } from "@nylorun/core/compatibility";
|
|
8
|
-
import { DELIVERY_TOKEN_TYPE, subjectTokenIssuer } from "@nylorun/core/contracts";
|
|
9
|
-
/** The issuer prefix of a Tenant's tokens: `urn:nylorun:tenant:<tenantId>`. */
|
|
10
|
-
const ISSUER_PREFIX = subjectTokenIssuer("");
|
|
11
|
-
/** Why a delivery was refused; `code` is sent back to the Runtime. */
|
|
12
|
-
export class DeliveryVerificationError extends Error {
|
|
13
|
-
code;
|
|
14
|
-
name = "DeliveryVerificationError";
|
|
15
|
-
constructor(code, message) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.code = code;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/** Clock skew tolerated on `iat` and `exp`, in seconds. */
|
|
21
|
-
const SKEW_SECONDS = 30;
|
|
22
|
-
const FORBIDDEN_HEADERS = ["jku", "jwk", "x5u", "x5c", "x5t", "x5t#S256", "crit"];
|
|
23
|
-
/** The largest token accepted: well above a real one, well below a request limit. */
|
|
24
|
-
const MAX_TOKEN_LENGTH = 4096;
|
|
25
|
-
/**
|
|
26
|
-
* Verifies `token` against `body`. The keys are the installation's, so a valid signature already
|
|
27
|
-
* binds the issuer to its one Tenant: any `urn:nylorun:tenant:<id>` issuer is accepted (and
|
|
28
|
-
* returned in `iss`) unless `tenantId` names the one expected.
|
|
29
|
-
*/
|
|
30
|
-
export async function verifyDeliveryToken(token, options) {
|
|
31
|
-
if (!token)
|
|
32
|
-
throw new DeliveryVerificationError("signature_missing", "No delivery token");
|
|
33
|
-
const parts = token.length <= MAX_TOKEN_LENGTH ? token.split(".") : [];
|
|
34
|
-
if (parts.length !== 3)
|
|
35
|
-
throw invalid("Not a compact JWS");
|
|
36
|
-
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
37
|
-
const header = json(encodedHeader);
|
|
38
|
-
if (header.alg !== "ES256" ||
|
|
39
|
-
header.typ !== DELIVERY_TOKEN_TYPE ||
|
|
40
|
-
typeof header.kid !== "string" ||
|
|
41
|
-
FORBIDDEN_HEADERS.some((name) => name in header))
|
|
42
|
-
throw invalid("Unexpected token header");
|
|
43
|
-
const key = await options.keys(header.kid);
|
|
44
|
-
if (!key)
|
|
45
|
-
throw new DeliveryVerificationError("key_unknown", `Unknown signing key ${header.kid}`);
|
|
46
|
-
const signed = await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, key, bytes(base64url(encodedSignature)), new TextEncoder().encode(`${encodedHeader}.${encodedPayload}`));
|
|
47
|
-
if (!signed)
|
|
48
|
-
throw invalid("Bad signature");
|
|
49
|
-
const claims = json(encodedPayload);
|
|
50
|
-
if (typeof claims.iss !== "string" ||
|
|
51
|
-
typeof claims.aud !== "string" ||
|
|
52
|
-
typeof claims.sub !== "string" ||
|
|
53
|
-
typeof claims.agt !== "string" ||
|
|
54
|
-
!Number.isInteger(claims.gen) ||
|
|
55
|
-
typeof claims.bdy !== "string" ||
|
|
56
|
-
typeof claims.iat !== "number" ||
|
|
57
|
-
typeof claims.exp !== "number" ||
|
|
58
|
-
typeof claims.jti !== "string")
|
|
59
|
-
throw invalid("Missing claims");
|
|
60
|
-
if (!claims.iss.startsWith(ISSUER_PREFIX) ||
|
|
61
|
-
!isTenantId(claims.iss.slice(ISSUER_PREFIX.length)))
|
|
62
|
-
throw invalid("The token was not issued by a Tenant");
|
|
63
|
-
if (options.tenantId !== undefined && claims.iss !== subjectTokenIssuer(options.tenantId))
|
|
64
|
-
throw invalid("The token is for another Tenant");
|
|
65
|
-
if (options.audience !== undefined && claims.aud !== options.audience)
|
|
66
|
-
throw invalid(`The token is for ${claims.aud}, not ${options.audience}`);
|
|
67
|
-
const now = (options.now ?? Date.now()) / 1000;
|
|
68
|
-
if (claims.exp < now - SKEW_SECONDS || claims.iat > now + SKEW_SECONDS)
|
|
69
|
-
throw new DeliveryVerificationError("token_expired", "The delivery token has expired");
|
|
70
|
-
if (claims.bdy !== (await bodyHash(options.body)))
|
|
71
|
-
throw invalid("The body does not match the token");
|
|
72
|
-
return claims;
|
|
73
|
-
}
|
|
74
|
-
/** base64url SHA-256 of `body`: the token's `bdy` claim. */
|
|
75
|
-
export async function bodyHash(body) {
|
|
76
|
-
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes(body)));
|
|
77
|
-
return Buffer.from(digest).toString("base64url");
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* The Tenant's public keys, fetched from `GET /v1/access/jwks` and cached by `kid`. An
|
|
81
|
-
* unknown `kid` refetches once, at most every `refetchMs`, so forged tokens cannot make the
|
|
82
|
-
* endpoint call the Runtime on every request. Concurrent lookups share one in-flight fetch.
|
|
83
|
-
*/
|
|
84
|
-
export class JwksCache {
|
|
85
|
-
source;
|
|
86
|
-
refetchMs;
|
|
87
|
-
keys = new Map();
|
|
88
|
-
lastFetch = 0;
|
|
89
|
-
pending;
|
|
90
|
-
constructor(source, refetchMs = 5000) {
|
|
91
|
-
this.source = source;
|
|
92
|
-
this.refetchMs = refetchMs;
|
|
93
|
-
}
|
|
94
|
-
lookup = async (kid) => {
|
|
95
|
-
const known = this.keys.get(kid);
|
|
96
|
-
if (known)
|
|
97
|
-
return known;
|
|
98
|
-
// Wait for an in-flight fetch: stamping the cooldown at the start of `load` used
|
|
99
|
-
// to make a concurrent ping (register + smoke, or tsx watch restart) answer
|
|
100
|
-
// `key_unknown` before the keys arrived.
|
|
101
|
-
if (this.pending) {
|
|
102
|
-
await this.pending;
|
|
103
|
-
return this.keys.get(kid);
|
|
104
|
-
}
|
|
105
|
-
if (Date.now() - this.lastFetch < this.refetchMs && this.lastFetch > 0)
|
|
106
|
-
return undefined;
|
|
107
|
-
await (this.pending ??= this.load().finally(() => (this.pending = undefined)));
|
|
108
|
-
return this.keys.get(kid);
|
|
109
|
-
};
|
|
110
|
-
async load() {
|
|
111
|
-
const keys = "keys" in this.source ? this.source.keys : await this.fetchKeys(this.source);
|
|
112
|
-
this.lastFetch = Date.now();
|
|
113
|
-
for (const jwk of keys) {
|
|
114
|
-
const kid = jwk.kid;
|
|
115
|
-
if (typeof kid !== "string" || jwk.kty !== "EC" || jwk.crv !== "P-256")
|
|
116
|
-
continue;
|
|
117
|
-
if (this.keys.has(kid))
|
|
118
|
-
continue;
|
|
119
|
-
this.keys.set(kid, await crypto.subtle.importKey("jwk", { kty: jwk.kty, crv: jwk.crv, x: jwk.x, y: jwk.y }, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
async fetchKeys(source) {
|
|
123
|
-
const headers = {
|
|
124
|
-
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
125
|
-
};
|
|
126
|
-
if (source.key)
|
|
127
|
-
headers.Authorization = `Bearer ${source.key}`;
|
|
128
|
-
const response = await (source.fetch ?? globalThis.fetch)(`${source.url.replace(/\/$/, "")}/v1/access/jwks`, { headers, redirect: "error", signal: AbortSignal.timeout(10_000) });
|
|
129
|
-
if (!response.ok) {
|
|
130
|
-
await response.body?.cancel();
|
|
131
|
-
throw new Error(`The Runtime answered ${response.status} for its public keys (GET /v1/access/jwks)`);
|
|
132
|
-
}
|
|
133
|
-
const body = (await response.json());
|
|
134
|
-
return Array.isArray(body.keys) ? body.keys : [];
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function invalid(message) {
|
|
138
|
-
return new DeliveryVerificationError("signature_invalid", message);
|
|
139
|
-
}
|
|
140
|
-
function base64url(value) {
|
|
141
|
-
return new Uint8Array(Buffer.from(value, "base64url"));
|
|
142
|
-
}
|
|
143
|
-
/** A copy backed by a plain `ArrayBuffer`, which WebCrypto's types require. */
|
|
144
|
-
function bytes(value) {
|
|
145
|
-
const copy = new Uint8Array(value.byteLength);
|
|
146
|
-
copy.set(value);
|
|
147
|
-
return copy;
|
|
148
|
-
}
|
|
149
|
-
function json(segment) {
|
|
150
|
-
try {
|
|
151
|
-
const value = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
|
|
152
|
-
if (value && typeof value === "object" && !Array.isArray(value))
|
|
153
|
-
return value;
|
|
154
|
-
}
|
|
155
|
-
catch {
|
|
156
|
-
/* fall through */
|
|
157
|
-
}
|
|
158
|
-
throw invalid("Malformed token");
|
|
159
|
-
}
|
package/dist/execute-action.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { type BuiltAgent, type BuiltWorkflow } from "@nylorun/core/define";
|
|
2
|
-
import type { Action, ActionOutcome } from "@nylorun/core/contracts";
|
|
3
|
-
import type { ActionSandbox } from "./sandbox/client.js";
|
|
4
|
-
export type ExecutableDefinition = BuiltAgent | BuiltWorkflow;
|
|
5
|
-
export type ExecuteActionOptions = {
|
|
6
|
-
/** Claim-scoped sandbox client; omitted when the session has no sandbox. */
|
|
7
|
-
readonly sandbox?: ActionSandbox;
|
|
8
|
-
};
|
|
9
|
-
/** Executes code only after a host-issued claim. No engine dependency. */
|
|
10
|
-
export declare function executeAction(action: Action, root: ExecutableDefinition, signal: AbortSignal, options?: ExecuteActionOptions): Promise<ActionOutcome>;
|
|
11
|
-
/**
|
|
12
|
-
* True when `action` runs a tool marked `background` (`ToolDefinition.background`): an Action
|
|
13
|
-
* endpoint answers its delivery at once and posts the outcome later. False for anything it
|
|
14
|
-
* cannot find; `executeAction` reports those.
|
|
15
|
-
*/
|
|
16
|
-
export declare function runsInBackground(action: Action, root: ExecutableDefinition): boolean;
|