@vinaverse/server 0.0.0-stage → 0.1.0-alpha.2
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/LICENSE +202 -0
- package/README.md +165 -2
- package/dist/api.cjs +528 -0
- package/dist/api.cjs.map +1 -0
- package/dist/api.d.cts +453 -0
- package/dist/api.d.ts +453 -0
- package/dist/api.js +516 -0
- package/dist/api.js.map +1 -0
- package/dist/chunk-FEEJNQXV.js +64 -0
- package/dist/chunk-FEEJNQXV.js.map +1 -0
- package/dist/chunk-GTPMQKGX.js +277 -0
- package/dist/chunk-GTPMQKGX.js.map +1 -0
- package/dist/chunk-NPXO6SMZ.js +84 -0
- package/dist/chunk-NPXO6SMZ.js.map +1 -0
- package/dist/chunk-SDJFZPHJ.js +141 -0
- package/dist/chunk-SDJFZPHJ.js.map +1 -0
- package/dist/express.cjs +397 -0
- package/dist/express.cjs.map +1 -0
- package/dist/express.d.cts +43 -0
- package/dist/express.d.ts +43 -0
- package/dist/express.js +87 -0
- package/dist/express.js.map +1 -0
- package/dist/fastify.cjs +363 -0
- package/dist/fastify.cjs.map +1 -0
- package/dist/fastify.d.cts +62 -0
- package/dist/fastify.d.ts +62 -0
- package/dist/fastify.js +54 -0
- package/dist/fastify.js.map +1 -0
- package/dist/hono.cjs +421 -0
- package/dist/hono.cjs.map +1 -0
- package/dist/hono.d.cts +21 -0
- package/dist/hono.d.ts +21 -0
- package/dist/hono.js +34 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.cjs +1481 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +196 -0
- package/dist/index.d.ts +196 -0
- package/dist/index.js +920 -0
- package/dist/index.js.map +1 -0
- package/dist/lambda.cjs +405 -0
- package/dist/lambda.cjs.map +1 -0
- package/dist/lambda.d.cts +53 -0
- package/dist/lambda.d.ts +53 -0
- package/dist/lambda.js +86 -0
- package/dist/lambda.js.map +1 -0
- package/dist/nonces-scwljZwl.d.cts +92 -0
- package/dist/nonces-scwljZwl.d.ts +92 -0
- package/dist/provider-BqfTvF3g.d.ts +2016 -0
- package/dist/provider-Miaa9V7I.d.cts +2016 -0
- package/dist/stores.cjs +315 -0
- package/dist/stores.cjs.map +1 -0
- package/dist/stores.d.cts +52 -0
- package/dist/stores.d.ts +52 -0
- package/dist/stores.js +306 -0
- package/dist/stores.js.map +1 -0
- package/dist/testing.cjs +135 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +29 -0
- package/dist/testing.d.ts +29 -0
- package/dist/testing.js +49 -0
- package/dist/testing.js.map +1 -0
- package/dist/vinaverse-v1.d-BVc4uyUd.d.cts +3460 -0
- package/dist/vinaverse-v1.d-BVc4uyUd.d.ts +3460 -0
- package/package.json +225 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1481 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/webhooks/crypto.ts
|
|
4
|
+
var WebCryptoUnavailableError = class extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(
|
|
7
|
+
"Web Crypto (crypto.subtle) is not available in this runtime; @vinaverse/server needs Node 18+, an edge runtime or another runtime with Web Crypto"
|
|
8
|
+
);
|
|
9
|
+
this.name = "WebCryptoUnavailableError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var fromNode;
|
|
13
|
+
function webCrypto() {
|
|
14
|
+
const global = globalThis.crypto;
|
|
15
|
+
if (global?.subtle) return Promise.resolve(global);
|
|
16
|
+
fromNode ??= nodeWebCrypto();
|
|
17
|
+
return fromNode;
|
|
18
|
+
}
|
|
19
|
+
async function subtleCrypto() {
|
|
20
|
+
return (await webCrypto()).subtle;
|
|
21
|
+
}
|
|
22
|
+
async function nodeWebCrypto() {
|
|
23
|
+
if (typeof globalThis.process?.versions?.node !== "string") {
|
|
24
|
+
throw new WebCryptoUnavailableError();
|
|
25
|
+
}
|
|
26
|
+
const specifier = ["node", "crypto"].join(":");
|
|
27
|
+
const mod = await import(
|
|
28
|
+
/* webpackIgnore: true */
|
|
29
|
+
/* @vite-ignore */
|
|
30
|
+
specifier
|
|
31
|
+
);
|
|
32
|
+
if (!mod.webcrypto?.subtle) throw new WebCryptoUnavailableError();
|
|
33
|
+
return mod.webcrypto;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/internal/bytes.ts
|
|
37
|
+
var encoder = new TextEncoder();
|
|
38
|
+
function utf8(text) {
|
|
39
|
+
return encoder.encode(text);
|
|
40
|
+
}
|
|
41
|
+
function toBytes(body) {
|
|
42
|
+
if (typeof body === "string") return utf8(body);
|
|
43
|
+
if (body instanceof Uint8Array) return body;
|
|
44
|
+
return new Uint8Array(body);
|
|
45
|
+
}
|
|
46
|
+
function hexToBytes(hex) {
|
|
47
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
48
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
49
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
50
|
+
}
|
|
51
|
+
return bytes;
|
|
52
|
+
}
|
|
53
|
+
function bytesToHex(bytes) {
|
|
54
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
55
|
+
}
|
|
56
|
+
async function sha256Hex(data) {
|
|
57
|
+
const subtle = await subtleCrypto();
|
|
58
|
+
return bytesToHex(new Uint8Array(await subtle.digest("SHA-256", copy(data))));
|
|
59
|
+
}
|
|
60
|
+
async function hmacKey(secret, usage) {
|
|
61
|
+
const subtle = await subtleCrypto();
|
|
62
|
+
return subtle.importKey(
|
|
63
|
+
"raw",
|
|
64
|
+
utf8(secret),
|
|
65
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
66
|
+
false,
|
|
67
|
+
[usage]
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
function copy(data) {
|
|
71
|
+
const out = new Uint8Array(data.length);
|
|
72
|
+
out.set(data);
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function concat(...parts) {
|
|
76
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
77
|
+
let at = 0;
|
|
78
|
+
for (const p of parts) {
|
|
79
|
+
out.set(p, at);
|
|
80
|
+
at += p.length;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/webhooks/errors.ts
|
|
86
|
+
function webhookError(code, message) {
|
|
87
|
+
return { code, message };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/webhooks/events.ts
|
|
91
|
+
var WEBHOOK_EVENT_TYPES = [
|
|
92
|
+
"moderation.item.completed",
|
|
93
|
+
"moderation.job.completed",
|
|
94
|
+
"document_check.completed",
|
|
95
|
+
"session.completed",
|
|
96
|
+
"subject.erased",
|
|
97
|
+
"usage.threshold_reached",
|
|
98
|
+
"feature.state_changed",
|
|
99
|
+
"media.job.queued",
|
|
100
|
+
"media.job.progress",
|
|
101
|
+
"media.job.step.completed",
|
|
102
|
+
"media.job.completed",
|
|
103
|
+
"media.job.failed",
|
|
104
|
+
"media.job.cancelled",
|
|
105
|
+
"conversation.updated",
|
|
106
|
+
"conversation.completed",
|
|
107
|
+
"conversation.failed",
|
|
108
|
+
"conversation.expired"
|
|
109
|
+
];
|
|
110
|
+
var KNOWN = new Set(WEBHOOK_EVENT_TYPES);
|
|
111
|
+
var CONVERSATION_STATUSES = {
|
|
112
|
+
"conversation.updated": ["in_progress", "failed"],
|
|
113
|
+
"conversation.completed": ["completed"],
|
|
114
|
+
"conversation.failed": ["failed"],
|
|
115
|
+
"conversation.expired": ["expired"]
|
|
116
|
+
};
|
|
117
|
+
var ENVIRONMENTS = /* @__PURE__ */ new Set(["mock", "development", "production"]);
|
|
118
|
+
function isConversationEvent(event) {
|
|
119
|
+
return event.type.startsWith("conversation.");
|
|
120
|
+
}
|
|
121
|
+
function isObject(value) {
|
|
122
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
123
|
+
}
|
|
124
|
+
function readBody(text) {
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = JSON.parse(text);
|
|
128
|
+
} catch {
|
|
129
|
+
return webhookError("invalid_body", "the body is not JSON");
|
|
130
|
+
}
|
|
131
|
+
if (!isObject(parsed) || typeof parsed.id !== "string" || typeof parsed.type !== "string" || typeof parsed.created_at !== "string" || typeof parsed.environment !== "string" || !ENVIRONMENTS.has(parsed.environment) || !isObject(parsed.data)) {
|
|
132
|
+
return webhookError(
|
|
133
|
+
"invalid_body",
|
|
134
|
+
"the body is not a webhook envelope (id, type, created_at, environment, data)"
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const envelope = parsed;
|
|
138
|
+
if (parsed.data.test === true) return { kind: "test", envelope };
|
|
139
|
+
if (!KNOWN.has(parsed.type)) return { kind: "unrecognized", envelope };
|
|
140
|
+
const statuses = CONVERSATION_STATUSES[parsed.type];
|
|
141
|
+
if (statuses) {
|
|
142
|
+
const { conversation_id, sequence, status, flow } = parsed.data;
|
|
143
|
+
if (typeof conversation_id !== "string" || !Number.isSafeInteger(sequence) || sequence < 1 || typeof status !== "string" || !statuses.includes(status) || typeof flow !== "string") {
|
|
144
|
+
return webhookError(
|
|
145
|
+
"invalid_body",
|
|
146
|
+
`a ${parsed.type} event carries a guided flow's state: conversation_id, a positive integer sequence, a flow, and status ${statuses.join(" or ")}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return { kind: "event", event: parsed };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/webhooks/signature.ts
|
|
154
|
+
var SIGNATURE_HEADER = "Vinaverse-Signature";
|
|
155
|
+
var TOLERANCE_SECONDS = 300;
|
|
156
|
+
var DIGITS = /^[0-9]+$/;
|
|
157
|
+
var V1 = /^[0-9a-f]{64}$/;
|
|
158
|
+
function parseSignatureHeader(header) {
|
|
159
|
+
const parts = header.split(",").map((p) => p.trim()).filter((p) => p.includes("=")).map((p) => {
|
|
160
|
+
const at = p.indexOf("=");
|
|
161
|
+
return [p.slice(0, at), p.slice(at + 1)];
|
|
162
|
+
});
|
|
163
|
+
const timestamps = parts.filter(([k]) => k === "t").map(([, v]) => v);
|
|
164
|
+
const [t] = timestamps;
|
|
165
|
+
if (timestamps.length !== 1 || t === void 0 || !DIGITS.test(t)) {
|
|
166
|
+
return webhookError(
|
|
167
|
+
"malformed",
|
|
168
|
+
"the signature header needs exactly one timestamp, t=<unix seconds>"
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const signatures = parts.filter(([k, v]) => k === "v1" && V1.test(v)).map(([, v]) => hexToBytes(v));
|
|
172
|
+
if (signatures.length === 0) {
|
|
173
|
+
return webhookError(
|
|
174
|
+
"malformed",
|
|
175
|
+
"the signature header has no v1 signature"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return { timestamp: BigInt(t), signatures };
|
|
179
|
+
}
|
|
180
|
+
async function checkSignature(options) {
|
|
181
|
+
const parsed = parseSignatureHeader(options.header);
|
|
182
|
+
if ("code" in parsed) return parsed;
|
|
183
|
+
const now = BigInt(Math.floor(options.now.getTime() / 1e3));
|
|
184
|
+
const skew = now > parsed.timestamp ? now - parsed.timestamp : parsed.timestamp - now;
|
|
185
|
+
if (skew > BigInt(options.toleranceSeconds)) {
|
|
186
|
+
return webhookError(
|
|
187
|
+
"stale",
|
|
188
|
+
`the signature is ${skew} seconds from now; at most ${options.toleranceSeconds} are accepted`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const message = concat(utf8(`${parsed.timestamp}.`), options.body);
|
|
192
|
+
const subtle = await subtleCrypto();
|
|
193
|
+
for (const secret of options.secrets) {
|
|
194
|
+
const key = await hmacKey(secret, "verify");
|
|
195
|
+
for (const signature of parsed.signatures) {
|
|
196
|
+
if (await subtle.verify(
|
|
197
|
+
"HMAC",
|
|
198
|
+
key,
|
|
199
|
+
signature,
|
|
200
|
+
message
|
|
201
|
+
))
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return webhookError(
|
|
206
|
+
"mismatch",
|
|
207
|
+
"no signature matches the body with the signing secret: check the secret, and verify the raw body exactly as received"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/webhooks/verify.ts
|
|
212
|
+
async function verifyWebhook(options) {
|
|
213
|
+
const secrets = signingSecrets(options.secret);
|
|
214
|
+
const toleranceSeconds = checkTolerance(options.toleranceSeconds);
|
|
215
|
+
const header = singleHeader(options.header);
|
|
216
|
+
if (header === void 0) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
error: webhookError("missing", "no Vinaverse-Signature header")
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (header === null) {
|
|
223
|
+
return {
|
|
224
|
+
ok: false,
|
|
225
|
+
error: webhookError(
|
|
226
|
+
"malformed",
|
|
227
|
+
"more than one Vinaverse-Signature header"
|
|
228
|
+
)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const body = toBytes(options.rawBody);
|
|
232
|
+
const refused = await checkSignature({
|
|
233
|
+
body,
|
|
234
|
+
header,
|
|
235
|
+
secrets,
|
|
236
|
+
now: options.now ?? /* @__PURE__ */ new Date(),
|
|
237
|
+
toleranceSeconds
|
|
238
|
+
});
|
|
239
|
+
if (refused) return { ok: false, error: refused };
|
|
240
|
+
let text;
|
|
241
|
+
try {
|
|
242
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
243
|
+
} catch {
|
|
244
|
+
return {
|
|
245
|
+
ok: false,
|
|
246
|
+
error: webhookError("invalid_body", "the body is not UTF-8")
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const read = readBody(text);
|
|
250
|
+
return "code" in read ? { ok: false, error: read } : { ok: true, ...read };
|
|
251
|
+
}
|
|
252
|
+
function singleHeader(header) {
|
|
253
|
+
if (header === null || header === void 0) return void 0;
|
|
254
|
+
if (typeof header === "string") return header === "" ? void 0 : header;
|
|
255
|
+
if (header.length === 0) return void 0;
|
|
256
|
+
return header.length === 1 ? header[0] : null;
|
|
257
|
+
}
|
|
258
|
+
function signingSecrets(secret) {
|
|
259
|
+
const list = typeof secret === "string" ? [secret] : Array.isArray(secret) ? [...secret] : [];
|
|
260
|
+
if (list.length === 0 || list.some((s) => typeof s !== "string" || s === "")) {
|
|
261
|
+
throw new TypeError(
|
|
262
|
+
"the webhook signing secret is required (vv_whsec_\u2026), and none was given"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
return list;
|
|
266
|
+
}
|
|
267
|
+
function checkTolerance(seconds) {
|
|
268
|
+
if (seconds === void 0) return TOLERANCE_SECONDS;
|
|
269
|
+
if (!Number.isSafeInteger(seconds) || seconds < 0 || seconds > TOLERANCE_SECONDS) {
|
|
270
|
+
throw new TypeError(
|
|
271
|
+
`toleranceSeconds must be a whole number of seconds from 0 to ${TOLERANCE_SECONDS}, not ${seconds}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
return seconds;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/adapters/handler.ts
|
|
278
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
279
|
+
var RECEIVED = JSON.stringify({ received: true });
|
|
280
|
+
function tooLarge() {
|
|
281
|
+
return {
|
|
282
|
+
status: 413,
|
|
283
|
+
body: JSON.stringify({ error: { code: "body_too_large" } })
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function byteLength(body) {
|
|
287
|
+
return typeof body === "string" ? new TextEncoder().encode(body).length : body.byteLength;
|
|
288
|
+
}
|
|
289
|
+
async function handleWebhook(options, rawBody, header) {
|
|
290
|
+
if (byteLength(rawBody) > (options.maxBodyBytes ?? MAX_BODY_BYTES)) {
|
|
291
|
+
return tooLarge();
|
|
292
|
+
}
|
|
293
|
+
const result = await verifyWebhook({
|
|
294
|
+
rawBody,
|
|
295
|
+
header,
|
|
296
|
+
secret: options.secret,
|
|
297
|
+
...options.now ? { now: options.now() } : {},
|
|
298
|
+
...options.toleranceSeconds === void 0 ? {} : { toleranceSeconds: options.toleranceSeconds }
|
|
299
|
+
});
|
|
300
|
+
if (!result.ok) {
|
|
301
|
+
await options.onRejected?.(result.error);
|
|
302
|
+
return {
|
|
303
|
+
status: 400,
|
|
304
|
+
body: JSON.stringify({ error: { code: result.error.code } })
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (result.kind === "event") await options.onEvent(result.event);
|
|
308
|
+
else if (result.kind === "test") await options.onTest?.(result.envelope);
|
|
309
|
+
else await options.onUnrecognized?.(result.envelope);
|
|
310
|
+
return { status: 200, body: RECEIVED };
|
|
311
|
+
}
|
|
312
|
+
function checkHandlerOptions(options) {
|
|
313
|
+
signingSecrets(options.secret);
|
|
314
|
+
checkTolerance(options.toleranceSeconds);
|
|
315
|
+
if (typeof options.onEvent !== "function") {
|
|
316
|
+
throw new TypeError(
|
|
317
|
+
"onEvent is required: it receives every verified event"
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const max = options.maxBodyBytes;
|
|
321
|
+
if (max !== void 0 && (!Number.isSafeInteger(max) || max <= 0)) {
|
|
322
|
+
throw new TypeError(
|
|
323
|
+
`maxBodyBytes must be a positive whole number of bytes, not ${max}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/adapters/fetch.ts
|
|
329
|
+
function createFetchWebhookHandler(options) {
|
|
330
|
+
checkHandlerOptions(options);
|
|
331
|
+
const max = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
332
|
+
return async (request) => {
|
|
333
|
+
const body = await readCapped(request, max);
|
|
334
|
+
if (body === void 0) return toResponse(tooLarge());
|
|
335
|
+
return toResponse(
|
|
336
|
+
await handleWebhook(
|
|
337
|
+
options,
|
|
338
|
+
body,
|
|
339
|
+
request.headers.get("vinaverse-signature")
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function readCapped(request, max) {
|
|
345
|
+
const declared = Number(request.headers.get("content-length") ?? "0");
|
|
346
|
+
if (declared > max) return void 0;
|
|
347
|
+
if (!request.body) return new Uint8Array(0);
|
|
348
|
+
const reader = request.body.getReader();
|
|
349
|
+
const chunks = [];
|
|
350
|
+
let size = 0;
|
|
351
|
+
for (; ; ) {
|
|
352
|
+
const { done, value } = await reader.read();
|
|
353
|
+
if (done) break;
|
|
354
|
+
size += value.length;
|
|
355
|
+
if (size > max) {
|
|
356
|
+
await reader.cancel();
|
|
357
|
+
return void 0;
|
|
358
|
+
}
|
|
359
|
+
chunks.push(value);
|
|
360
|
+
}
|
|
361
|
+
const body = new Uint8Array(size);
|
|
362
|
+
let at = 0;
|
|
363
|
+
for (const chunk of chunks) {
|
|
364
|
+
body.set(chunk, at);
|
|
365
|
+
at += chunk.length;
|
|
366
|
+
}
|
|
367
|
+
return body;
|
|
368
|
+
}
|
|
369
|
+
function toResponse(response) {
|
|
370
|
+
return new Response(response.body, {
|
|
371
|
+
status: response.status,
|
|
372
|
+
headers: { "content-type": "application/json" }
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
function createFetchToolHandler(provider, options = {}) {
|
|
376
|
+
const max = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
377
|
+
return async (request) => {
|
|
378
|
+
const url = new URL(request.url);
|
|
379
|
+
const reads = request.method !== "GET" && request.method !== "HEAD";
|
|
380
|
+
const body = reads ? await readCapped(request, max) : null;
|
|
381
|
+
if (body === void 0) {
|
|
382
|
+
return new Response(
|
|
383
|
+
JSON.stringify({
|
|
384
|
+
status: 413,
|
|
385
|
+
code: "invalid_argument",
|
|
386
|
+
detail: "the request body is too large"
|
|
387
|
+
}),
|
|
388
|
+
{
|
|
389
|
+
status: 413,
|
|
390
|
+
headers: { "content-type": "application/problem+json" }
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
const response = await provider.handle({
|
|
395
|
+
method: request.method,
|
|
396
|
+
target: `${url.pathname}${url.search}`,
|
|
397
|
+
header: (name) => request.headers.get(name) ?? void 0,
|
|
398
|
+
body
|
|
399
|
+
});
|
|
400
|
+
return new Response(response.body, {
|
|
401
|
+
status: response.status,
|
|
402
|
+
headers: response.headers
|
|
403
|
+
});
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/contract.ts
|
|
408
|
+
var CONTRACT = {
|
|
409
|
+
api: "v1-draft.17",
|
|
410
|
+
commit: "fe4fb7d2945e1ced2bce7d44e85a4b28005e46df"
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// src/generated/tool-operations.ts
|
|
414
|
+
var TOOL_OPERATIONS = {
|
|
415
|
+
create_handoff: {
|
|
416
|
+
id: "create_handoff",
|
|
417
|
+
method: "POST",
|
|
418
|
+
path: "/handoffs",
|
|
419
|
+
effect: "write",
|
|
420
|
+
maxResultBytes: 16384,
|
|
421
|
+
successStatus: 201,
|
|
422
|
+
idempotencyKey: true,
|
|
423
|
+
body: true,
|
|
424
|
+
pathParams: {},
|
|
425
|
+
queryParams: {}
|
|
426
|
+
},
|
|
427
|
+
create_proposal: {
|
|
428
|
+
id: "create_proposal",
|
|
429
|
+
method: "POST",
|
|
430
|
+
path: "/proposals",
|
|
431
|
+
effect: "propose",
|
|
432
|
+
maxResultBytes: 16384,
|
|
433
|
+
successStatus: 201,
|
|
434
|
+
idempotencyKey: true,
|
|
435
|
+
body: true,
|
|
436
|
+
pathParams: {},
|
|
437
|
+
queryParams: {}
|
|
438
|
+
},
|
|
439
|
+
get_availability: {
|
|
440
|
+
id: "get_availability",
|
|
441
|
+
method: "GET",
|
|
442
|
+
path: "/availability",
|
|
443
|
+
effect: "read",
|
|
444
|
+
maxResultBytes: 16384,
|
|
445
|
+
successStatus: 200,
|
|
446
|
+
idempotencyKey: false,
|
|
447
|
+
body: false,
|
|
448
|
+
pathParams: {},
|
|
449
|
+
queryParams: {
|
|
450
|
+
professional_id: { type: "string", required: true },
|
|
451
|
+
service_id: { type: "string", required: true },
|
|
452
|
+
from: { type: "string", required: true },
|
|
453
|
+
to: { type: "string", required: true },
|
|
454
|
+
limit: { type: "integer", required: false }
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
get_flow_definition: {
|
|
458
|
+
id: "get_flow_definition",
|
|
459
|
+
method: "GET",
|
|
460
|
+
path: "/flows/{flow}/definition",
|
|
461
|
+
effect: "read",
|
|
462
|
+
maxResultBytes: 131072,
|
|
463
|
+
successStatus: 200,
|
|
464
|
+
idempotencyKey: false,
|
|
465
|
+
body: false,
|
|
466
|
+
pathParams: { flow: { type: "string", required: true } },
|
|
467
|
+
queryParams: { version: { type: "integer", required: false } }
|
|
468
|
+
},
|
|
469
|
+
get_my_booking: {
|
|
470
|
+
id: "get_my_booking",
|
|
471
|
+
method: "GET",
|
|
472
|
+
path: "/me/bookings/{id}",
|
|
473
|
+
effect: "read",
|
|
474
|
+
maxResultBytes: 16384,
|
|
475
|
+
successStatus: 200,
|
|
476
|
+
idempotencyKey: false,
|
|
477
|
+
body: false,
|
|
478
|
+
pathParams: { id: { type: "string", required: true } },
|
|
479
|
+
queryParams: {}
|
|
480
|
+
},
|
|
481
|
+
get_my_kit: {
|
|
482
|
+
id: "get_my_kit",
|
|
483
|
+
method: "GET",
|
|
484
|
+
path: "/me/kits/{id}",
|
|
485
|
+
effect: "read",
|
|
486
|
+
maxResultBytes: 16384,
|
|
487
|
+
successStatus: 200,
|
|
488
|
+
idempotencyKey: false,
|
|
489
|
+
body: false,
|
|
490
|
+
pathParams: { id: { type: "string", required: true } },
|
|
491
|
+
queryParams: {}
|
|
492
|
+
},
|
|
493
|
+
get_my_order: {
|
|
494
|
+
id: "get_my_order",
|
|
495
|
+
method: "GET",
|
|
496
|
+
path: "/me/orders/{id}",
|
|
497
|
+
effect: "read",
|
|
498
|
+
maxResultBytes: 16384,
|
|
499
|
+
successStatus: 200,
|
|
500
|
+
idempotencyKey: false,
|
|
501
|
+
body: false,
|
|
502
|
+
pathParams: { id: { type: "string", required: true } },
|
|
503
|
+
queryParams: {}
|
|
504
|
+
},
|
|
505
|
+
get_policy: {
|
|
506
|
+
id: "get_policy",
|
|
507
|
+
method: "GET",
|
|
508
|
+
path: "/policies/{topic}",
|
|
509
|
+
effect: "read",
|
|
510
|
+
maxResultBytes: 16384,
|
|
511
|
+
successStatus: 200,
|
|
512
|
+
idempotencyKey: false,
|
|
513
|
+
body: false,
|
|
514
|
+
pathParams: { topic: { type: "string", required: true } },
|
|
515
|
+
queryParams: {}
|
|
516
|
+
},
|
|
517
|
+
get_professional: {
|
|
518
|
+
id: "get_professional",
|
|
519
|
+
method: "GET",
|
|
520
|
+
path: "/professionals/{id}",
|
|
521
|
+
effect: "read",
|
|
522
|
+
maxResultBytes: 16384,
|
|
523
|
+
successStatus: 200,
|
|
524
|
+
idempotencyKey: false,
|
|
525
|
+
body: false,
|
|
526
|
+
pathParams: { id: { type: "string", required: true } },
|
|
527
|
+
queryParams: {}
|
|
528
|
+
},
|
|
529
|
+
get_quote: {
|
|
530
|
+
id: "get_quote",
|
|
531
|
+
method: "POST",
|
|
532
|
+
path: "/quotes",
|
|
533
|
+
effect: "read",
|
|
534
|
+
maxResultBytes: 16384,
|
|
535
|
+
successStatus: 200,
|
|
536
|
+
idempotencyKey: true,
|
|
537
|
+
body: true,
|
|
538
|
+
pathParams: {},
|
|
539
|
+
queryParams: {}
|
|
540
|
+
},
|
|
541
|
+
get_workspace: {
|
|
542
|
+
id: "get_workspace",
|
|
543
|
+
method: "GET",
|
|
544
|
+
path: "/workspaces/{id}",
|
|
545
|
+
effect: "read",
|
|
546
|
+
maxResultBytes: 16384,
|
|
547
|
+
successStatus: 200,
|
|
548
|
+
idempotencyKey: false,
|
|
549
|
+
body: false,
|
|
550
|
+
pathParams: { id: { type: "string", required: true } },
|
|
551
|
+
queryParams: {}
|
|
552
|
+
},
|
|
553
|
+
list_my_bookings: {
|
|
554
|
+
id: "list_my_bookings",
|
|
555
|
+
method: "GET",
|
|
556
|
+
path: "/me/bookings",
|
|
557
|
+
effect: "read",
|
|
558
|
+
maxResultBytes: 16384,
|
|
559
|
+
successStatus: 200,
|
|
560
|
+
idempotencyKey: false,
|
|
561
|
+
body: false,
|
|
562
|
+
pathParams: {},
|
|
563
|
+
queryParams: {
|
|
564
|
+
scope: { type: "string", required: false },
|
|
565
|
+
limit: { type: "integer", required: false }
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
list_my_kits: {
|
|
569
|
+
id: "list_my_kits",
|
|
570
|
+
method: "GET",
|
|
571
|
+
path: "/me/kits",
|
|
572
|
+
effect: "read",
|
|
573
|
+
maxResultBytes: 16384,
|
|
574
|
+
successStatus: 200,
|
|
575
|
+
idempotencyKey: false,
|
|
576
|
+
body: false,
|
|
577
|
+
pathParams: {},
|
|
578
|
+
queryParams: {
|
|
579
|
+
scope: { type: "string", required: false },
|
|
580
|
+
limit: { type: "integer", required: false }
|
|
581
|
+
}
|
|
582
|
+
},
|
|
583
|
+
list_my_orders: {
|
|
584
|
+
id: "list_my_orders",
|
|
585
|
+
method: "GET",
|
|
586
|
+
path: "/me/orders",
|
|
587
|
+
effect: "read",
|
|
588
|
+
maxResultBytes: 16384,
|
|
589
|
+
successStatus: 200,
|
|
590
|
+
idempotencyKey: false,
|
|
591
|
+
body: false,
|
|
592
|
+
pathParams: {},
|
|
593
|
+
queryParams: {
|
|
594
|
+
kit_id: { type: "string", required: false },
|
|
595
|
+
limit: { type: "integer", required: false }
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
list_services: {
|
|
599
|
+
id: "list_services",
|
|
600
|
+
method: "GET",
|
|
601
|
+
path: "/catalog/services",
|
|
602
|
+
effect: "read",
|
|
603
|
+
maxResultBytes: 16384,
|
|
604
|
+
successStatus: 200,
|
|
605
|
+
idempotencyKey: false,
|
|
606
|
+
body: false,
|
|
607
|
+
pathParams: {},
|
|
608
|
+
queryParams: {
|
|
609
|
+
q: { type: "string", required: true },
|
|
610
|
+
limit: { type: "integer", required: false }
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
resolve_place: {
|
|
614
|
+
id: "resolve_place",
|
|
615
|
+
method: "GET",
|
|
616
|
+
path: "/places/resolve",
|
|
617
|
+
effect: "read",
|
|
618
|
+
maxResultBytes: 16384,
|
|
619
|
+
successStatus: 200,
|
|
620
|
+
idempotencyKey: false,
|
|
621
|
+
body: false,
|
|
622
|
+
pathParams: {},
|
|
623
|
+
queryParams: {
|
|
624
|
+
text: { type: "string", required: true },
|
|
625
|
+
near_lat: { type: "number", required: false },
|
|
626
|
+
near_lng: { type: "number", required: false }
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
search_professionals: {
|
|
630
|
+
id: "search_professionals",
|
|
631
|
+
method: "GET",
|
|
632
|
+
path: "/search/professionals",
|
|
633
|
+
effect: "read",
|
|
634
|
+
maxResultBytes: 16384,
|
|
635
|
+
successStatus: 200,
|
|
636
|
+
idempotencyKey: false,
|
|
637
|
+
body: false,
|
|
638
|
+
pathParams: {},
|
|
639
|
+
queryParams: {
|
|
640
|
+
q: { type: "string", required: false },
|
|
641
|
+
category_id: { type: "string", required: false },
|
|
642
|
+
service_type_id: { type: "string", required: false },
|
|
643
|
+
lat: { type: "number", required: false },
|
|
644
|
+
lng: { type: "number", required: false },
|
|
645
|
+
radius_miles: { type: "integer", required: false },
|
|
646
|
+
date: { type: "string", required: false },
|
|
647
|
+
time_from: { type: "string", required: false },
|
|
648
|
+
time_to: { type: "string", required: false },
|
|
649
|
+
max_price: { type: "integer", required: false },
|
|
650
|
+
min_rating_ppm: { type: "integer", required: false },
|
|
651
|
+
sort: { type: "string", required: false },
|
|
652
|
+
limit: { type: "integer", required: false },
|
|
653
|
+
cursor: { type: "string", required: false }
|
|
654
|
+
}
|
|
655
|
+
},
|
|
656
|
+
search_workspaces: {
|
|
657
|
+
id: "search_workspaces",
|
|
658
|
+
method: "GET",
|
|
659
|
+
path: "/search/workspaces",
|
|
660
|
+
effect: "read",
|
|
661
|
+
maxResultBytes: 16384,
|
|
662
|
+
successStatus: 200,
|
|
663
|
+
idempotencyKey: false,
|
|
664
|
+
body: false,
|
|
665
|
+
pathParams: {},
|
|
666
|
+
queryParams: {
|
|
667
|
+
q: { type: "string", required: false },
|
|
668
|
+
space_category: { type: "string", required: false },
|
|
669
|
+
lat: { type: "number", required: false },
|
|
670
|
+
lng: { type: "number", required: false },
|
|
671
|
+
radius_miles: { type: "integer", required: false },
|
|
672
|
+
max_price: { type: "integer", required: false },
|
|
673
|
+
min_rating_ppm: { type: "integer", required: false },
|
|
674
|
+
sort: { type: "string", required: false },
|
|
675
|
+
limit: { type: "integer", required: false },
|
|
676
|
+
cursor: { type: "string", required: false }
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
submit_flow: {
|
|
680
|
+
id: "submit_flow",
|
|
681
|
+
method: "POST",
|
|
682
|
+
path: "/flows/{flow}/submissions",
|
|
683
|
+
effect: "write",
|
|
684
|
+
maxResultBytes: 16384,
|
|
685
|
+
successStatus: 200,
|
|
686
|
+
idempotencyKey: false,
|
|
687
|
+
body: true,
|
|
688
|
+
pathParams: { flow: { type: "string", required: true } },
|
|
689
|
+
queryParams: {}
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// src/tools/idempotency.ts
|
|
694
|
+
var MemoryIdempotencyStore = class {
|
|
695
|
+
#held = /* @__PURE__ */ new Map();
|
|
696
|
+
#now;
|
|
697
|
+
#sweepEvery;
|
|
698
|
+
#starts = 0;
|
|
699
|
+
constructor(options = {}) {
|
|
700
|
+
this.#now = options.now ?? (() => Date.now() / 1e3);
|
|
701
|
+
this.#sweepEvery = Math.max(1, options.sweepEvery ?? 256);
|
|
702
|
+
}
|
|
703
|
+
async start(key, fingerprint2, pendingSeconds, owner) {
|
|
704
|
+
const now = this.#now();
|
|
705
|
+
if (++this.#starts % this.#sweepEvery === 0) {
|
|
706
|
+
for (const [k, h] of this.#held) if (h.until < now) this.#held.delete(k);
|
|
707
|
+
}
|
|
708
|
+
const held = this.#live(key);
|
|
709
|
+
if (held) return held.record;
|
|
710
|
+
this.#held.set(key, {
|
|
711
|
+
record: { fingerprint: fingerprint2, state: "pending" },
|
|
712
|
+
owner,
|
|
713
|
+
until: now + pendingSeconds
|
|
714
|
+
});
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
async finish(key, record, keepSeconds, owner) {
|
|
718
|
+
const held = this.#live(key);
|
|
719
|
+
if (held && (held.record.state !== "pending" || held.owner !== owner)) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
this.#held.set(key, {
|
|
723
|
+
record: { ...record, state: "done" },
|
|
724
|
+
until: this.#now() + keepSeconds
|
|
725
|
+
});
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
async release(key, owner) {
|
|
729
|
+
const held = this.#live(key);
|
|
730
|
+
if (held?.record.state === "pending" && held.owner === owner) {
|
|
731
|
+
this.#held.delete(key);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/** Records held, expired ones included until a sweep drops them. */
|
|
735
|
+
get size() {
|
|
736
|
+
return this.#held.size;
|
|
737
|
+
}
|
|
738
|
+
#live(key) {
|
|
739
|
+
const held = this.#held.get(key);
|
|
740
|
+
return held && held.until >= this.#now() ? held : void 0;
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
function fingerprint(value) {
|
|
744
|
+
return sha256Hex(utf8(canonicalJson(value)));
|
|
745
|
+
}
|
|
746
|
+
function canonicalJson(value) {
|
|
747
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
748
|
+
if (value !== null && typeof value === "object") {
|
|
749
|
+
const entries = Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
750
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
|
|
751
|
+
}
|
|
752
|
+
return JSON.stringify(value) ?? "null";
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/tools/nonces.ts
|
|
756
|
+
var NONCE_KEEP_SECONDS = 360;
|
|
757
|
+
var MemoryNonceStore = class {
|
|
758
|
+
#seen = /* @__PURE__ */ new Map();
|
|
759
|
+
#now;
|
|
760
|
+
#sweepEvery;
|
|
761
|
+
#claims = 0;
|
|
762
|
+
constructor(options = {}) {
|
|
763
|
+
this.#now = options.now ?? (() => Math.floor(Date.now() / 1e3));
|
|
764
|
+
this.#sweepEvery = Math.max(1, options.sweepEvery ?? 256);
|
|
765
|
+
}
|
|
766
|
+
async claim(nonce, keepUntil) {
|
|
767
|
+
const now = this.#now();
|
|
768
|
+
if (++this.#claims % this.#sweepEvery === 0) {
|
|
769
|
+
for (const [n, until2] of this.#seen) {
|
|
770
|
+
if (until2 < now) this.#seen.delete(n);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
const until = this.#seen.get(nonce);
|
|
774
|
+
if (until !== void 0 && until >= now) return false;
|
|
775
|
+
this.#seen.set(nonce, keepUntil);
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
/** Nonces held, expired ones included until a sweep drops them. */
|
|
779
|
+
get size() {
|
|
780
|
+
return this.#seen.size;
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// src/tools/refusals.ts
|
|
785
|
+
var FLOW_REFUSAL_CODES = {
|
|
786
|
+
REQUIRED_MISSING: true,
|
|
787
|
+
UNKNOWN_VALUE: true,
|
|
788
|
+
TOO_LONG: true,
|
|
789
|
+
VERSION_NOT_ACCEPTED: true,
|
|
790
|
+
TOO_MANY: true,
|
|
791
|
+
TOO_FEW: true,
|
|
792
|
+
BAD_DATE: true,
|
|
793
|
+
UPLOAD_NOT_FOUND: true,
|
|
794
|
+
OUT_OF_RANGE: true
|
|
795
|
+
};
|
|
796
|
+
function isFlowRefusal(problem2) {
|
|
797
|
+
return problem2.status === 422 && Object.hasOwn(FLOW_REFUSAL_CODES, problem2.code) && (problem2.code === "VERSION_NOT_ACCEPTED" || !!problem2.question_id);
|
|
798
|
+
}
|
|
799
|
+
var STATUS = {
|
|
800
|
+
not_found: 404,
|
|
801
|
+
forbidden: 403,
|
|
802
|
+
invalid_argument: 422,
|
|
803
|
+
unavailable: 503,
|
|
804
|
+
token_expired: 401
|
|
805
|
+
};
|
|
806
|
+
var ToolRefusal = class _ToolRefusal extends Error {
|
|
807
|
+
problem;
|
|
808
|
+
constructor(problem2) {
|
|
809
|
+
super(problem2.detail ?? problem2.code);
|
|
810
|
+
this.name = "ToolRefusal";
|
|
811
|
+
this.problem = problem2;
|
|
812
|
+
}
|
|
813
|
+
get status() {
|
|
814
|
+
return this.problem.status;
|
|
815
|
+
}
|
|
816
|
+
/** The record does not exist, or the person may not see it (both are the same 404). */
|
|
817
|
+
static notFound(detail) {
|
|
818
|
+
return _ToolRefusal.#coded("not_found", detail);
|
|
819
|
+
}
|
|
820
|
+
static forbidden(detail) {
|
|
821
|
+
return _ToolRefusal.#coded("forbidden", detail);
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* A value the tenant does not accept. `field` is a JSON Pointer into the
|
|
825
|
+
* body (`/changes/address/postal_code`); `example` is a valid value the
|
|
826
|
+
* agent offers when it asks again.
|
|
827
|
+
*/
|
|
828
|
+
static invalidArgument(options = {}) {
|
|
829
|
+
return new _ToolRefusal({
|
|
830
|
+
status: 422,
|
|
831
|
+
code: "invalid_argument",
|
|
832
|
+
...clip("detail", options.detail, 300),
|
|
833
|
+
...clip("field", options.field, 200),
|
|
834
|
+
...clip("example", options.example, 200)
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
/** A temporary failure; a read is retried. */
|
|
838
|
+
static unavailable(detail) {
|
|
839
|
+
return _ToolRefusal.#coded("unavailable", detail);
|
|
840
|
+
}
|
|
841
|
+
/** The delegated token is not valid (expired, revoked, unknown): a 401, which ends the session's tool access. */
|
|
842
|
+
static tokenExpired(detail) {
|
|
843
|
+
return _ToolRefusal.#coded("token_expired", detail);
|
|
844
|
+
}
|
|
845
|
+
/** `get_flow_definition` with a `version` the tenant no longer accepts: `410 VERSION_GONE`. */
|
|
846
|
+
static versionGone(detail) {
|
|
847
|
+
return new _ToolRefusal({
|
|
848
|
+
status: 410,
|
|
849
|
+
code: "VERSION_GONE",
|
|
850
|
+
...clip("detail", detail, 300)
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* `submit_flow` refused: `422` with a flow code and the question to ask
|
|
855
|
+
* again (none only for `VERSION_NOT_ACCEPTED`). Stored like an acceptance,
|
|
856
|
+
* so the same submission gets the same refusal again.
|
|
857
|
+
*/
|
|
858
|
+
static flowRefused(code, questionId, detail) {
|
|
859
|
+
if (code !== "VERSION_NOT_ACCEPTED" && !questionId) {
|
|
860
|
+
throw new TypeError(`a ${code} refusal names the question to ask again`);
|
|
861
|
+
}
|
|
862
|
+
return new _ToolRefusal({
|
|
863
|
+
status: 422,
|
|
864
|
+
code,
|
|
865
|
+
...questionId ? { question_id: questionId } : {},
|
|
866
|
+
...clip("detail", detail, 300)
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
static #coded(code, detail) {
|
|
870
|
+
return new _ToolRefusal({
|
|
871
|
+
status: STATUS[code],
|
|
872
|
+
code,
|
|
873
|
+
...clip("detail", detail, 300)
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
function clip(key, value, max) {
|
|
878
|
+
if (value === void 0) return {};
|
|
879
|
+
return { [key]: [...value].slice(0, max).join("") };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// src/tools/signature.ts
|
|
883
|
+
var TOOL_TOLERANCE_SECONDS = 300;
|
|
884
|
+
var T = /^[0-9]{1,12}$/;
|
|
885
|
+
var NONCE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
886
|
+
var MAC = /^[0-9a-f]{64}$/;
|
|
887
|
+
function parseToolSignature(header) {
|
|
888
|
+
let t;
|
|
889
|
+
let n;
|
|
890
|
+
let seenT = 0;
|
|
891
|
+
let seenN = 0;
|
|
892
|
+
const v2 = [];
|
|
893
|
+
for (const part of header.split(",")) {
|
|
894
|
+
const trimmed = part.trim();
|
|
895
|
+
const at = trimmed.indexOf("=");
|
|
896
|
+
const key = at < 0 ? trimmed : trimmed.slice(0, at);
|
|
897
|
+
const value = at < 0 ? "" : trimmed.slice(at + 1);
|
|
898
|
+
if (key === "t") {
|
|
899
|
+
seenT++;
|
|
900
|
+
if (T.test(value)) t = Number(value);
|
|
901
|
+
} else if (key === "n") {
|
|
902
|
+
seenN++;
|
|
903
|
+
n = value;
|
|
904
|
+
} else if (key === "v2" && MAC.test(value)) {
|
|
905
|
+
v2.push(value);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return { t, n, v2, repeated: seenT > 1 || seenN > 1 };
|
|
909
|
+
}
|
|
910
|
+
async function toolSigningString(call) {
|
|
911
|
+
const bodyHash = call.body && call.body.length > 0 ? await sha256Hex(call.body) : "";
|
|
912
|
+
return utf8(
|
|
913
|
+
[
|
|
914
|
+
"v2",
|
|
915
|
+
String(call.t),
|
|
916
|
+
call.n,
|
|
917
|
+
call.method.toUpperCase(),
|
|
918
|
+
call.target,
|
|
919
|
+
await sha256Hex(utf8(call.token)),
|
|
920
|
+
bodyHash
|
|
921
|
+
].join("\n")
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
async function verifyToolCall(call, options) {
|
|
925
|
+
if (!call.header) {
|
|
926
|
+
return refusal("missing", "no Vinaverse-Signature header");
|
|
927
|
+
}
|
|
928
|
+
const { t, n, v2, repeated } = parseToolSignature(call.header);
|
|
929
|
+
if (repeated) {
|
|
930
|
+
return refusal(
|
|
931
|
+
"malformed",
|
|
932
|
+
"the signature repeats t or n: an ambiguous header"
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
if (t === void 0) {
|
|
936
|
+
return refusal(
|
|
937
|
+
"malformed",
|
|
938
|
+
"the signature has no timestamp t=<unix seconds>"
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
const now = Math.floor(options.now.getTime() / 1e3);
|
|
942
|
+
const tolerance = options.toleranceSeconds === void 0 ? TOOL_TOLERANCE_SECONDS : checkTolerance(options.toleranceSeconds);
|
|
943
|
+
if (Math.abs(now - t) > tolerance) {
|
|
944
|
+
return refusal(
|
|
945
|
+
"stale",
|
|
946
|
+
`the signature is ${Math.abs(now - t)} seconds from now; at most ${tolerance} are accepted`
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
if (v2.length === 0) {
|
|
950
|
+
return refusal(
|
|
951
|
+
"v2_required",
|
|
952
|
+
"the signature has no v2 (a v1-only header binds neither the token nor a nonce)"
|
|
953
|
+
);
|
|
954
|
+
}
|
|
955
|
+
if (n === void 0 || !NONCE.test(n)) {
|
|
956
|
+
return refusal(
|
|
957
|
+
"malformed",
|
|
958
|
+
"the signature has no valid nonce n (16 to 64 of A-Z a-z 0-9 - _)"
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
const message = await toolSigningString({ ...call, t, n });
|
|
962
|
+
const subtle = await subtleCrypto();
|
|
963
|
+
let matched = false;
|
|
964
|
+
for (const secret of options.secrets) {
|
|
965
|
+
const key = await hmacKey(secret, "verify");
|
|
966
|
+
for (const mac of v2) {
|
|
967
|
+
if (await subtle.verify("HMAC", key, hexToBytes(mac), message)) {
|
|
968
|
+
matched = true;
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (matched) break;
|
|
973
|
+
}
|
|
974
|
+
if (!matched) {
|
|
975
|
+
return refusal(
|
|
976
|
+
"mismatch",
|
|
977
|
+
"no v2 signature matches: check the tool signing secret, and verify the method, target, token and body exactly as received"
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
if (!await options.nonces.claim(n, Math.max(t, now) + NONCE_KEEP_SECONDS)) {
|
|
981
|
+
return refusal("replayed", "this nonce was already used: a replayed call");
|
|
982
|
+
}
|
|
983
|
+
return null;
|
|
984
|
+
}
|
|
985
|
+
function refusal(code, message) {
|
|
986
|
+
return { code, message };
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/tools/provider.ts
|
|
990
|
+
var ACCEPTED = { ok: true };
|
|
991
|
+
var KEEP_SECONDS = {
|
|
992
|
+
submit_flow: 31 * 86400,
|
|
993
|
+
// a conversation lives up to 30 days, and a submission is resent in later sessions
|
|
994
|
+
create_proposal: 8 * 86400,
|
|
995
|
+
// at least until the proposal expires (at most 7 days)
|
|
996
|
+
default: 86400,
|
|
997
|
+
pending: 60
|
|
998
|
+
};
|
|
999
|
+
var MAX_IDEMPOTENCY_KEY = 255;
|
|
1000
|
+
var MAX_REQUEST_BYTES = 1024 * 1024;
|
|
1001
|
+
var LeaseLostError = class extends Error {
|
|
1002
|
+
constructor(operation, leaseSeconds) {
|
|
1003
|
+
super(
|
|
1004
|
+
`${operation} ran longer than its ${leaseSeconds}-second idempotency lease and another call took the key: the answer was sent but not stored (raise leaseSeconds)`
|
|
1005
|
+
);
|
|
1006
|
+
this.name = "LeaseLostError";
|
|
1007
|
+
}
|
|
1008
|
+
};
|
|
1009
|
+
var ResultTooLargeError = class extends Error {
|
|
1010
|
+
constructor(operation, bytes, limit) {
|
|
1011
|
+
super(
|
|
1012
|
+
`${operation} returned ${bytes} bytes; the contract allows ${limit}: the runtime would refuse it`
|
|
1013
|
+
);
|
|
1014
|
+
this.name = "ResultTooLargeError";
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
function createToolProvider(options) {
|
|
1018
|
+
const secrets = typeof options.secret === "string" ? [options.secret] : [...options.secret];
|
|
1019
|
+
if (secrets.length === 0 || secrets.some((s) => typeof s !== "string" || !s)) {
|
|
1020
|
+
throw new TypeError("createToolProvider needs the tool signing secret");
|
|
1021
|
+
}
|
|
1022
|
+
checkTolerance(options.toleranceSeconds);
|
|
1023
|
+
const maxRequest = options.maxRequestBytes;
|
|
1024
|
+
if (maxRequest !== void 0 && (!Number.isSafeInteger(maxRequest) || maxRequest <= 0)) {
|
|
1025
|
+
throw new TypeError(
|
|
1026
|
+
`maxRequestBytes must be a positive whole number of bytes, not ${maxRequest}`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
const lease = options.leaseSeconds ?? KEEP_SECONDS.pending;
|
|
1030
|
+
if (!Number.isFinite(lease) || lease <= 0) {
|
|
1031
|
+
throw new TypeError(
|
|
1032
|
+
`leaseSeconds must be a positive number of seconds, not ${options.leaseSeconds}`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
if (typeof options.authenticate !== "function") {
|
|
1036
|
+
throw new TypeError(
|
|
1037
|
+
"createToolProvider needs authenticate: it resolves the delegated token"
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
const base = (options.basePath ?? "").replace(/\/+$/, "");
|
|
1041
|
+
const routes = Object.values(TOOL_OPERATIONS).map((o) => ({
|
|
1042
|
+
id: o.id,
|
|
1043
|
+
segments: o.path.split("/").filter(Boolean)
|
|
1044
|
+
}));
|
|
1045
|
+
async function handle(request) {
|
|
1046
|
+
const raw = request.body ?? new Uint8Array(0);
|
|
1047
|
+
const body = toBytes(raw);
|
|
1048
|
+
if (body.length > (options.maxRequestBytes ?? MAX_REQUEST_BYTES)) {
|
|
1049
|
+
return problem({
|
|
1050
|
+
status: 413,
|
|
1051
|
+
code: "invalid_argument",
|
|
1052
|
+
detail: "the request body is too large"
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
const token = bearer(request.header("authorization"));
|
|
1056
|
+
if (!token) {
|
|
1057
|
+
return problem({
|
|
1058
|
+
status: 401,
|
|
1059
|
+
code: "token_expired",
|
|
1060
|
+
detail: "no delegated token (Authorization: Bearer)"
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
const refused = await verifyToolCall(
|
|
1064
|
+
{
|
|
1065
|
+
method: request.method,
|
|
1066
|
+
target: request.target,
|
|
1067
|
+
token,
|
|
1068
|
+
body,
|
|
1069
|
+
header: request.header("vinaverse-signature")
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
secrets,
|
|
1073
|
+
nonces: options.nonces,
|
|
1074
|
+
now: options.now?.() ?? /* @__PURE__ */ new Date(),
|
|
1075
|
+
...options.toleranceSeconds === void 0 ? {} : { toleranceSeconds: options.toleranceSeconds }
|
|
1076
|
+
}
|
|
1077
|
+
);
|
|
1078
|
+
if (refused) {
|
|
1079
|
+
options.onRefused?.(refused.code, refused.message);
|
|
1080
|
+
return problem({
|
|
1081
|
+
status: 403,
|
|
1082
|
+
code: "forbidden",
|
|
1083
|
+
detail: "the signature was refused"
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
const matched = match(request.method, request.target);
|
|
1087
|
+
if (!matched)
|
|
1088
|
+
return problem({
|
|
1089
|
+
status: 404,
|
|
1090
|
+
code: "not_found",
|
|
1091
|
+
detail: "no such tool"
|
|
1092
|
+
});
|
|
1093
|
+
const { id, params, query } = matched;
|
|
1094
|
+
const operation = TOOL_OPERATIONS[id];
|
|
1095
|
+
const handler = options.handlers[id];
|
|
1096
|
+
if (!handler)
|
|
1097
|
+
return problem({
|
|
1098
|
+
status: 404,
|
|
1099
|
+
code: "not_found",
|
|
1100
|
+
detail: `${id} is not served here`
|
|
1101
|
+
});
|
|
1102
|
+
try {
|
|
1103
|
+
const principal = await options.authenticate(token, id);
|
|
1104
|
+
const typedQuery = readQuery(id, query);
|
|
1105
|
+
const parsed = operation.body ? parseBody(body) : void 0;
|
|
1106
|
+
const key = request.header("idempotency-key");
|
|
1107
|
+
if (operation.idempotencyKey && !key) {
|
|
1108
|
+
throw ToolRefusal.invalidArgument({
|
|
1109
|
+
detail: "the Idempotency-Key header is required"
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (key && key.length > MAX_IDEMPOTENCY_KEY) {
|
|
1113
|
+
throw ToolRefusal.invalidArgument({
|
|
1114
|
+
detail: `the Idempotency-Key is longer than ${MAX_IDEMPOTENCY_KEY} characters`
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
const call = {
|
|
1118
|
+
operation: id,
|
|
1119
|
+
principal,
|
|
1120
|
+
token,
|
|
1121
|
+
params,
|
|
1122
|
+
query: typedQuery,
|
|
1123
|
+
body: parsed,
|
|
1124
|
+
session: session(request),
|
|
1125
|
+
...key ? { idempotencyKey: key } : {}
|
|
1126
|
+
};
|
|
1127
|
+
const run = () => runHandler(id, handler, call);
|
|
1128
|
+
const scope = options.scope?.(principal, id);
|
|
1129
|
+
const keyOf = (...parts) => (scope === void 0 ? parts : [scope, ...parts]).map(encodeURIComponent).join(":");
|
|
1130
|
+
if (id === "submit_flow")
|
|
1131
|
+
return await submission(keyOf, params, parsed, run);
|
|
1132
|
+
if (id === "create_proposal" && key)
|
|
1133
|
+
return await proposal(keyOf(id, key), parsed, principal, run);
|
|
1134
|
+
if (operation.idempotencyKey && key) {
|
|
1135
|
+
return await keyed(
|
|
1136
|
+
id,
|
|
1137
|
+
keyOf(id, key),
|
|
1138
|
+
await fingerprint(parsed),
|
|
1139
|
+
KEEP_SECONDS.default,
|
|
1140
|
+
run,
|
|
1141
|
+
() => problem({
|
|
1142
|
+
status: 409,
|
|
1143
|
+
code: "IDEMPOTENCY_KEY_REUSED",
|
|
1144
|
+
detail: "this Idempotency-Key was used with another body"
|
|
1145
|
+
})
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
return await run();
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
return failure(error, id);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
async function runHandler(id, handler, call) {
|
|
1154
|
+
const result = await handler(call) ?? (id === "submit_flow" ? ACCEPTED : void 0);
|
|
1155
|
+
if (result === void 0) {
|
|
1156
|
+
throw new TypeError(`the ${id} handler returned nothing`);
|
|
1157
|
+
}
|
|
1158
|
+
const text = JSON.stringify(result);
|
|
1159
|
+
const limit = TOOL_OPERATIONS[id].maxResultBytes;
|
|
1160
|
+
const bytes = toBytes(text).length;
|
|
1161
|
+
if (bytes > limit) throw new ResultTooLargeError(id, bytes, limit);
|
|
1162
|
+
return json(TOOL_OPERATIONS[id].successStatus, text);
|
|
1163
|
+
}
|
|
1164
|
+
async function keyed(id, key, print, keepSeconds, run, conflict, replay, keeps = (r) => r.status === 422) {
|
|
1165
|
+
const store = options.idempotency;
|
|
1166
|
+
const owner = await ownerToken();
|
|
1167
|
+
const held = await store.start(key, print, lease, owner);
|
|
1168
|
+
if (held) {
|
|
1169
|
+
if (held.state === "pending") {
|
|
1170
|
+
return problem({
|
|
1171
|
+
status: 503,
|
|
1172
|
+
code: "unavailable",
|
|
1173
|
+
detail: "the same call is still running"
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
if (held.fingerprint !== print) return conflict(held);
|
|
1177
|
+
return replay ? replay(held) : stored(held);
|
|
1178
|
+
}
|
|
1179
|
+
let response;
|
|
1180
|
+
try {
|
|
1181
|
+
response = await run();
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
if (!(error instanceof ToolRefusal && keeps(error))) {
|
|
1184
|
+
await store.release(key, owner);
|
|
1185
|
+
throw error;
|
|
1186
|
+
}
|
|
1187
|
+
response = problem(error.problem);
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
const kept = await store.finish(
|
|
1191
|
+
key,
|
|
1192
|
+
{ fingerprint: print, status: response.status, body: response.body },
|
|
1193
|
+
keepSeconds,
|
|
1194
|
+
owner
|
|
1195
|
+
);
|
|
1196
|
+
if (!kept) options.onError?.(new LeaseLostError(id, lease), id);
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
options.onError?.(error, id);
|
|
1199
|
+
}
|
|
1200
|
+
return response;
|
|
1201
|
+
}
|
|
1202
|
+
async function submission(keyOf, params, body, run) {
|
|
1203
|
+
const submissionId = body?.submission_id;
|
|
1204
|
+
if (typeof submissionId !== "string" || submissionId === "") {
|
|
1205
|
+
throw ToolRefusal.invalidArgument({
|
|
1206
|
+
field: "/submission_id",
|
|
1207
|
+
detail: "a submission needs its submission_id"
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
const refused = async () => {
|
|
1211
|
+
try {
|
|
1212
|
+
return await run();
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
if (error instanceof ToolRefusal && error.status === 422 && !isFlowRefusal(error.problem)) {
|
|
1215
|
+
throw new TypeError(
|
|
1216
|
+
`submit_flow refused with ${error.problem.code}: a submission is refused only with ToolRefusal.flowRefused (a flow code and the question to ask again)`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
throw error;
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
return keyed(
|
|
1223
|
+
"submit_flow",
|
|
1224
|
+
keyOf("submit_flow", params.flow ?? "", submissionId),
|
|
1225
|
+
await fingerprint(body),
|
|
1226
|
+
KEEP_SECONDS.submit_flow,
|
|
1227
|
+
refused,
|
|
1228
|
+
(held) => {
|
|
1229
|
+
const was = held.body ? JSON.parse(held.body) : {};
|
|
1230
|
+
const storedResult = held.status === 200 ? { result: "accepted" } : {
|
|
1231
|
+
result: "refused",
|
|
1232
|
+
...was.code ? { code: was.code } : {},
|
|
1233
|
+
...was.question_id ? { question_id: was.question_id } : {}
|
|
1234
|
+
};
|
|
1235
|
+
return json(
|
|
1236
|
+
409,
|
|
1237
|
+
JSON.stringify({
|
|
1238
|
+
status: 409,
|
|
1239
|
+
code: "SUBMISSION_ID_REUSED",
|
|
1240
|
+
stored: storedResult
|
|
1241
|
+
}),
|
|
1242
|
+
"application/problem+json"
|
|
1243
|
+
);
|
|
1244
|
+
},
|
|
1245
|
+
void 0,
|
|
1246
|
+
(r) => isFlowRefusal(r.problem)
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
async function proposal(key, body, principal, run) {
|
|
1250
|
+
const { topic, changes } = body ?? {};
|
|
1251
|
+
const current = async (held) => {
|
|
1252
|
+
const storedProposal = JSON.parse(
|
|
1253
|
+
held.body ?? "null"
|
|
1254
|
+
);
|
|
1255
|
+
return options.currentProposal ? await options.currentProposal(storedProposal, principal) : storedProposal;
|
|
1256
|
+
};
|
|
1257
|
+
return keyed(
|
|
1258
|
+
"create_proposal",
|
|
1259
|
+
key,
|
|
1260
|
+
await fingerprint({ topic, changes }),
|
|
1261
|
+
KEEP_SECONDS.create_proposal,
|
|
1262
|
+
run,
|
|
1263
|
+
async (held) => held.status === 201 ? json(
|
|
1264
|
+
409,
|
|
1265
|
+
JSON.stringify({
|
|
1266
|
+
status: 409,
|
|
1267
|
+
code: "IDEMPOTENCY_KEY_REUSED",
|
|
1268
|
+
proposal: await current(held)
|
|
1269
|
+
}),
|
|
1270
|
+
"application/problem+json"
|
|
1271
|
+
) : stored(held),
|
|
1272
|
+
async (held) => held.status === 201 ? json(201, JSON.stringify(await current(held))) : stored(held)
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
function match(method, target) {
|
|
1276
|
+
const at = target.indexOf("?");
|
|
1277
|
+
const path = at < 0 ? target : target.slice(0, at);
|
|
1278
|
+
const query = new URLSearchParams(at < 0 ? "" : target.slice(at + 1));
|
|
1279
|
+
if (base && path !== base && !path.startsWith(`${base}/`)) return void 0;
|
|
1280
|
+
const segments = path.slice(base.length).split("/").filter(Boolean);
|
|
1281
|
+
for (const route of routes) {
|
|
1282
|
+
if (TOOL_OPERATIONS[route.id].method !== method.toUpperCase()) continue;
|
|
1283
|
+
if (route.segments.length !== segments.length) continue;
|
|
1284
|
+
const params = {};
|
|
1285
|
+
let ok = true;
|
|
1286
|
+
for (const [i, part] of route.segments.entries()) {
|
|
1287
|
+
const actual = segments[i];
|
|
1288
|
+
if (part.startsWith("{") && part.endsWith("}")) {
|
|
1289
|
+
try {
|
|
1290
|
+
params[part.slice(1, -1)] = decodeURIComponent(actual);
|
|
1291
|
+
} catch {
|
|
1292
|
+
ok = false;
|
|
1293
|
+
}
|
|
1294
|
+
} else if (part !== actual) {
|
|
1295
|
+
ok = false;
|
|
1296
|
+
}
|
|
1297
|
+
if (!ok) break;
|
|
1298
|
+
}
|
|
1299
|
+
if (ok) return { id: route.id, params, query };
|
|
1300
|
+
}
|
|
1301
|
+
return void 0;
|
|
1302
|
+
}
|
|
1303
|
+
function failure(error, id) {
|
|
1304
|
+
if (error instanceof ToolRefusal) return problem(error.problem);
|
|
1305
|
+
options.onError?.(error, id);
|
|
1306
|
+
return problem({
|
|
1307
|
+
status: 500,
|
|
1308
|
+
code: "unavailable",
|
|
1309
|
+
detail: "the tool failed"
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
return { handle };
|
|
1313
|
+
}
|
|
1314
|
+
function readQuery(id, query) {
|
|
1315
|
+
const declared = TOOL_OPERATIONS[id].queryParams;
|
|
1316
|
+
const out = {};
|
|
1317
|
+
for (const [name, spec] of Object.entries(declared)) {
|
|
1318
|
+
const value = query.get(name);
|
|
1319
|
+
if (value === null) {
|
|
1320
|
+
if (spec.required)
|
|
1321
|
+
throw ToolRefusal.invalidArgument({
|
|
1322
|
+
detail: `the query parameter ${name} is required`
|
|
1323
|
+
});
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
if (spec.type === "integer" || spec.type === "number") {
|
|
1327
|
+
const n = Number(value);
|
|
1328
|
+
if (value.trim() === "" || !Number.isFinite(n) || spec.type === "integer" && !Number.isInteger(n)) {
|
|
1329
|
+
throw ToolRefusal.invalidArgument({
|
|
1330
|
+
detail: `the query parameter ${name} must be ${spec.type === "integer" ? "an integer" : "a number"}`
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
out[name] = n;
|
|
1334
|
+
} else {
|
|
1335
|
+
out[name] = value;
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return out;
|
|
1339
|
+
}
|
|
1340
|
+
function parseBody(body) {
|
|
1341
|
+
if (body.length === 0)
|
|
1342
|
+
throw ToolRefusal.invalidArgument({
|
|
1343
|
+
detail: "the request needs a JSON body"
|
|
1344
|
+
});
|
|
1345
|
+
try {
|
|
1346
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body));
|
|
1347
|
+
} catch {
|
|
1348
|
+
throw ToolRefusal.invalidArgument({
|
|
1349
|
+
detail: "the request body is not JSON"
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function bearer(value) {
|
|
1354
|
+
const m = /^Bearer\s+(\S+)\s*$/i.exec(value ?? "");
|
|
1355
|
+
return m?.[1];
|
|
1356
|
+
}
|
|
1357
|
+
function session(request) {
|
|
1358
|
+
const pick = (name) => request.header(name);
|
|
1359
|
+
const entries = {
|
|
1360
|
+
id: pick("vinaverse-session") ?? pick("vinaverse-session-id"),
|
|
1361
|
+
conversationId: pick("vinaverse-conversation"),
|
|
1362
|
+
toolCallId: pick("vinaverse-tool-call-id"),
|
|
1363
|
+
requestId: pick("vinaverse-request-id")
|
|
1364
|
+
};
|
|
1365
|
+
return Object.fromEntries(
|
|
1366
|
+
Object.entries(entries).filter(([, v]) => v !== void 0)
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
async function ownerToken() {
|
|
1370
|
+
const bytes = (await webCrypto()).getRandomValues(new Uint8Array(16));
|
|
1371
|
+
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1372
|
+
}
|
|
1373
|
+
function json(status, body, type = "application/json") {
|
|
1374
|
+
return { status, headers: { "content-type": type }, body };
|
|
1375
|
+
}
|
|
1376
|
+
function problem(body) {
|
|
1377
|
+
return json(body.status, JSON.stringify(body), "application/problem+json");
|
|
1378
|
+
}
|
|
1379
|
+
function stored(held) {
|
|
1380
|
+
const status = held.status ?? 200;
|
|
1381
|
+
return json(
|
|
1382
|
+
status,
|
|
1383
|
+
held.body ?? "",
|
|
1384
|
+
status >= 400 ? "application/problem+json" : "application/json"
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// src/webhooks/sequence.ts
|
|
1389
|
+
function sequenceVerdict(held, incoming) {
|
|
1390
|
+
if (held && held.conversation_id !== incoming.conversation_id) {
|
|
1391
|
+
throw new TypeError(
|
|
1392
|
+
`the states belong to different conversations (${held.conversation_id}, ${incoming.conversation_id})`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
return held && incoming.sequence <= held.sequence ? "stale" : "apply";
|
|
1396
|
+
}
|
|
1397
|
+
function applyConversationState(record, incoming) {
|
|
1398
|
+
const action = sequenceVerdict(record?.state, incoming);
|
|
1399
|
+
const seen = record?.seen.includes(incoming.sequence) ? record.seen : [...record?.seen ?? [], incoming.sequence].sort((a, b) => a - b);
|
|
1400
|
+
if (action === "stale" && record)
|
|
1401
|
+
return { record: { ...record, seen }, action };
|
|
1402
|
+
if (incoming.answers_omitted === true) {
|
|
1403
|
+
const { answers_omitted: _omitted, ...rest } = incoming;
|
|
1404
|
+
const state = {
|
|
1405
|
+
...rest,
|
|
1406
|
+
answers: record?.state.answers ?? [],
|
|
1407
|
+
remarks: record?.state.remarks ?? {}
|
|
1408
|
+
};
|
|
1409
|
+
return { record: { state, seen, fullReadNeeded: true }, action };
|
|
1410
|
+
}
|
|
1411
|
+
return { record: { state: incoming, seen, fullReadNeeded: false }, action };
|
|
1412
|
+
}
|
|
1413
|
+
function missingSequences(record) {
|
|
1414
|
+
const [lowest] = record.seen;
|
|
1415
|
+
if (lowest === void 0) return [];
|
|
1416
|
+
const missing = [];
|
|
1417
|
+
for (let s = lowest + 1; s < record.state.sequence; s++) {
|
|
1418
|
+
if (!record.seen.includes(s)) missing.push(s);
|
|
1419
|
+
}
|
|
1420
|
+
return missing;
|
|
1421
|
+
}
|
|
1422
|
+
var ConversationReceiver = class {
|
|
1423
|
+
#ids = /* @__PURE__ */ new Set();
|
|
1424
|
+
#records = /* @__PURE__ */ new Map();
|
|
1425
|
+
/**
|
|
1426
|
+
* One verified delivery (an event, or a Studio test delivery's envelope):
|
|
1427
|
+
* what to do with it. The record for its conversation is then `record(id)`.
|
|
1428
|
+
*/
|
|
1429
|
+
receive(delivery) {
|
|
1430
|
+
if (this.#ids.has(delivery.id)) return "duplicate";
|
|
1431
|
+
this.#ids.add(delivery.id);
|
|
1432
|
+
const data = delivery.data;
|
|
1433
|
+
if (data?.test === true) return "skip_test";
|
|
1434
|
+
if (typeof data?.conversation_id !== "string") {
|
|
1435
|
+
throw new TypeError(`${delivery.id} carries no conversation state`);
|
|
1436
|
+
}
|
|
1437
|
+
const state = data;
|
|
1438
|
+
const { record, action } = applyConversationState(
|
|
1439
|
+
this.#records.get(state.conversation_id),
|
|
1440
|
+
state
|
|
1441
|
+
);
|
|
1442
|
+
this.#records.set(state.conversation_id, record);
|
|
1443
|
+
return action;
|
|
1444
|
+
}
|
|
1445
|
+
record(conversationId) {
|
|
1446
|
+
return this.#records.get(conversationId);
|
|
1447
|
+
}
|
|
1448
|
+
conversations() {
|
|
1449
|
+
return [...this.#records.keys()].sort();
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
|
|
1453
|
+
exports.CONTRACT = CONTRACT;
|
|
1454
|
+
exports.ConversationReceiver = ConversationReceiver;
|
|
1455
|
+
exports.KEEP_SECONDS = KEEP_SECONDS;
|
|
1456
|
+
exports.LeaseLostError = LeaseLostError;
|
|
1457
|
+
exports.MAX_REQUEST_BYTES = MAX_REQUEST_BYTES;
|
|
1458
|
+
exports.MemoryIdempotencyStore = MemoryIdempotencyStore;
|
|
1459
|
+
exports.MemoryNonceStore = MemoryNonceStore;
|
|
1460
|
+
exports.NONCE_KEEP_SECONDS = NONCE_KEEP_SECONDS;
|
|
1461
|
+
exports.ResultTooLargeError = ResultTooLargeError;
|
|
1462
|
+
exports.SIGNATURE_HEADER = SIGNATURE_HEADER;
|
|
1463
|
+
exports.TOLERANCE_SECONDS = TOLERANCE_SECONDS;
|
|
1464
|
+
exports.TOOL_OPERATIONS = TOOL_OPERATIONS;
|
|
1465
|
+
exports.ToolRefusal = ToolRefusal;
|
|
1466
|
+
exports.WEBHOOK_EVENT_TYPES = WEBHOOK_EVENT_TYPES;
|
|
1467
|
+
exports.WebCryptoUnavailableError = WebCryptoUnavailableError;
|
|
1468
|
+
exports.applyConversationState = applyConversationState;
|
|
1469
|
+
exports.canonicalJson = canonicalJson;
|
|
1470
|
+
exports.createFetchToolHandler = createFetchToolHandler;
|
|
1471
|
+
exports.createFetchWebhookHandler = createFetchWebhookHandler;
|
|
1472
|
+
exports.createToolProvider = createToolProvider;
|
|
1473
|
+
exports.fingerprint = fingerprint;
|
|
1474
|
+
exports.isConversationEvent = isConversationEvent;
|
|
1475
|
+
exports.missingSequences = missingSequences;
|
|
1476
|
+
exports.parseSignatureHeader = parseSignatureHeader;
|
|
1477
|
+
exports.sequenceVerdict = sequenceVerdict;
|
|
1478
|
+
exports.verifyToolCall = verifyToolCall;
|
|
1479
|
+
exports.verifyWebhook = verifyWebhook;
|
|
1480
|
+
//# sourceMappingURL=index.cjs.map
|
|
1481
|
+
//# sourceMappingURL=index.cjs.map
|