@jigs.design/server 0.2.0 → 0.3.1

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/dist/server.js DELETED
@@ -1,786 +0,0 @@
1
- import { INTERNAL_MESSAGE, JigsCompileError, JigsError, errorData, fatalDiagnostics, isJigsError, statusOf } from "./errors.js";
2
- import { checkMarkdownElements, createChecker } from "./compile.js";
3
- import { belowPrefix, splitPragma, versionDiagnostics } from "./pragma.js";
4
- import { MAX_CONTENT, messagesSchema, runSession } from "./session.js";
5
- import { createReplyParser, repairBlock, streamChat } from "./chat.js";
6
- import { confirmMessage, createConfirmer, memoryConfirmations } from "./confirm.js";
7
- import { invalidationsOf } from "./invalidation.js";
8
- import { systemPromptParts } from "@jigs.design/prompt";
9
- import { isCallPolicy } from "@jigs.design/host";
10
- import { PROGRAM_FORMAT } from "@jigs.design/protocol";
11
- import { z } from "zod";
12
- import { frameDocument, frameSecurityPolicy } from "@jigs.design/runtime/frame";
13
- //#region src/server.ts
14
- const VERSION_HEADER = "jigs-version";
15
- const DEFAULT_MAX_BODY_BYTES = 1e6;
16
- const HEARTBEAT_MS = 15e3;
17
- /** A small document whose compiled code changes when the compiler's output does. */
18
- const COMPILER_PROBE = `{@body const [count, setCount] = useState(0)}
19
- # Probe
20
- <each of={[1, 2]} as="n">
21
- {n} {count}
22
- </each>
23
- <if test={count > 0}>
24
- **done**
25
- </if>`;
26
- /**
27
- * The body as text, or null once it passes `max` bytes. Reading stops there,
28
- * so an oversized body is never held in memory.
29
- */
30
- async function readLimited(request, max) {
31
- if (request.body === null) return "";
32
- const reader = request.body.getReader();
33
- const chunks = [];
34
- let size = 0;
35
- for (;;) {
36
- const chunk = await reader.read();
37
- if (chunk.done) break;
38
- size += chunk.value.byteLength;
39
- if (size > max) {
40
- await reader.cancel();
41
- return null;
42
- }
43
- chunks.push(chunk.value);
44
- }
45
- const bytes = new Uint8Array(size);
46
- let offset = 0;
47
- for (const chunk of chunks) {
48
- bytes.set(chunk, offset);
49
- offset += chunk.byteLength;
50
- }
51
- return new TextDecoder().decode(bytes);
52
- }
53
- /**
54
- * An origin such as `https://app.example.com`. Throws on anything else,
55
- * including an entry with no `http:` or `https:` scheme: `localhost:3000`
56
- * parses as a URL whose origin is `"null"`, the `Origin` that sandboxed
57
- * frames and `file:` pages send.
58
- */
59
- function originOf(entry) {
60
- let url = null;
61
- try {
62
- url = new URL(entry);
63
- } catch {}
64
- if (url === null || url.protocol !== "http:" && url.protocol !== "https:" || url.origin === "null") throw new Error(`"${entry}" is not an origin: write its scheme and host, such as https://app.example.com`);
65
- return url.origin;
66
- }
67
- const bodySchema = z.discriminatedUnion("type", [z.object({
68
- confirmation: z.string().max(1e3).optional(),
69
- input: z.json().optional(),
70
- kind: z.enum([
71
- "action",
72
- "mutation",
73
- "query"
74
- ], { error: "Send the procedure's kind as \"kind\": \"query\", \"mutation\" or \"action\"" }),
75
- name: z.string().max(200),
76
- type: z.literal("call")
77
- }), z.object({
78
- messages: z.json(),
79
- options: z.json().optional(),
80
- type: z.literal("session")
81
- })]);
82
- /** A call a policy refused. The procedure stays in the prompt. */
83
- const denial = (name) => ({ error: {
84
- code: "FORBIDDEN",
85
- message: `"${name}" is not allowed here`
86
- } });
87
- const TOKEN_REFUSALS = {
88
- expired: "The confirmation has expired. Try again.",
89
- invalid: "The confirmation is not valid for this call.",
90
- used: "The confirmation was already used."
91
- };
92
- /**
93
- * The server's confirmer, or null when it has no `secret`. Throws at startup
94
- * when a mutation's policy is "confirm" and the server cannot sign for it.
95
- */
96
- function confirmerFor(options) {
97
- if (options.secret === void 0) {
98
- const confirming = Object.entries(options.host.procedures).find(([, procedure]) => procedure.kind === "mutation" && procedure.policy === "confirm");
99
- if (confirming !== void 0) throw new Error(`createJigsServer needs a secret and confirmSubject: "${confirming[0]}" asks for confirmation, and its tokens are signed with the secret`);
100
- if (options.confirmSubject !== void 0) throw new Error("createJigsServer has confirmSubject but no secret");
101
- return null;
102
- }
103
- if (options.secret.length < 32) throw new Error(`createJigsServer's secret must be at least 32 characters`);
104
- if (options.confirmSubject === void 0) throw new Error("createJigsServer has a secret but no confirmSubject: a confirmation token must be bound to who it was issued to");
105
- const now = options.now ?? Date.now;
106
- return createConfirmer(options.secret, options.confirmations ?? memoryConfirmations(now), now);
107
- }
108
- /** FNV-1a, enough to tell versions apart; not a security boundary. */
109
- function fingerprint(text) {
110
- let hash = 2166136261;
111
- for (let index = 0; index < text.length; index++) {
112
- hash ^= text.charCodeAt(index);
113
- hash = Math.imul(hash, 16777619);
114
- }
115
- return (hash >>> 0).toString(36);
116
- }
117
- /** Everything about the host that changes what a source compiles to. */
118
- function hostFingerprint(host) {
119
- return JSON.stringify({
120
- catalog: host.components.version,
121
- components: host.components.components.map((component) => ({
122
- children: component.children,
123
- name: component.name,
124
- props: Object.entries(component.props).map(([name, definition]) => [
125
- name,
126
- definition.schema,
127
- definition.default ?? null
128
- ])
129
- })),
130
- imageHosts: host.imageHosts,
131
- procedures: Object.entries(host.procedures).map(([name, procedure]) => ({
132
- input: procedure.input === void 0 ? null : z.toJSONSchema(procedure.input),
133
- invalidates: procedure.kind === "mutation" ? procedure.invalidates : [],
134
- kind: procedure.kind,
135
- name,
136
- output: procedure.output === void 0 ? null : z.toJSONSchema(procedure.output),
137
- projection: procedure.kind === "action" || procedure.projection === void 0 ? null : z.toJSONSchema(procedure.projection.schema)
138
- })),
139
- recipes: host.recipes.map((entry) => [entry.name, entry.source])
140
- });
141
- }
142
- /**
143
- * What `jigs.chat` reads from the route besides the request. `blocks` and
144
- * `prompt` come from the browser, so they are checked like a session's
145
- * messages.
146
- */
147
- const chatInputSchema = z.object({
148
- blocks: z.array(z.object({
149
- id: z.string().max(200),
150
- source: z.string().max(MAX_CONTENT)
151
- })).max(200).optional(),
152
- prompt: z.string().max(MAX_CONTENT).optional()
153
- });
154
- /**
155
- * Runs a `safeParse`, with a value nested too deeply to walk reported as a
156
- * failure instead of a thrown `RangeError`.
157
- */
158
- function parseBounded(parse) {
159
- let parsed;
160
- try {
161
- parsed = parse();
162
- } catch {
163
- return {
164
- message: "The request body is nested too deeply.",
165
- success: false
166
- };
167
- }
168
- return parsed.success ? {
169
- data: parsed.data,
170
- success: true
171
- } : {
172
- message: issuesText(parsed.error),
173
- success: false
174
- };
175
- }
176
- function issuesText(error) {
177
- return error.issues.slice(0, 3).map((issue) => issue.path.length === 0 ? issue.message : `${issue.path.join(".")}: ${issue.message}`).join("; ");
178
- }
179
- /**
180
- * Types a `context` and `run` pair outside `createJigsServer`, so two
181
- * servers (one per platform) can share them. `TContext` comes from `context`
182
- * alone, as in `createJigsServer`.
183
- */
184
- function defineRun(procedures, handlers) {
185
- return handlers;
186
- }
187
- function createJigsServer(options) {
188
- const { host } = options;
189
- checkMarkdownElements(host.components);
190
- for (const [name, procedure] of Object.entries(host.procedures)) if (procedure.kind !== "action" && !Object.hasOwn(options.run, name)) throw new Error(`createJigsServer has no run function for "${name}"`);
191
- const confirmer = confirmerFor(options);
192
- const checker = createChecker(host);
193
- const promptOptions = (mode) => options.instructions === void 0 ? { mode } : {
194
- instructions: options.instructions,
195
- mode
196
- };
197
- const documentParts = systemPromptParts(host, promptOptions("document"));
198
- const chatParts = systemPromptParts(host, promptOptions("chat"));
199
- const system = `${documentParts.stable}\n\n${documentParts.variable}`;
200
- const chatSystem = `${chatParts.stable}\n\n${chatParts.variable}`;
201
- /** Tags that mark unfenced Jigs in a chat reply. */
202
- const chatTags = /* @__PURE__ */ new Set([...host.components.components.map((component) => component.name), ...host.recipes.map((entry) => entry.name)]);
203
- const maxRepairs = options.maxRepairs ?? 1;
204
- const programIntervalMs = options.programIntervalMs ?? 250;
205
- const designRepair = new Set(options.designRepair ?? []);
206
- const allowedOrigins = new Set((options.allowedOrigins ?? []).map((entry) => originOf(entry)));
207
- const ownOrigin = options.origin === void 0 ? null : originOf(options.origin);
208
- const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
209
- const frameHtml = frameDocument();
210
- const compilerProbe = checker.compile(COMPILER_PROBE).program.code;
211
- const version = fingerprint(JSON.stringify([
212
- PROGRAM_FORMAT,
213
- 0,
214
- frameHtml,
215
- compilerProbe,
216
- hostFingerprint(host)
217
- ]));
218
- const onError = (cause, info) => {
219
- try {
220
- options.onError?.(cause, info);
221
- } catch {}
222
- };
223
- const runs = options.run;
224
- /** Whether a mutation call may run, by the server's override or else the host. */
225
- const policyOf = (name, procedure, input, ctx) => {
226
- if (options.policy === void 0) return procedure.policy;
227
- try {
228
- const policyCall = {
229
- ctx,
230
- input,
231
- name
232
- };
233
- const decided = options.policy(policyCall);
234
- if (decided === void 0) return procedure.policy;
235
- if (!isCallPolicy(decided)) throw new Error(`The policy option returned ${String(decided)} for "${name}"; return "allow", "confirm", "deny" or undefined, synchronously`);
236
- return decided;
237
- } catch (error) {
238
- onError(error, {
239
- name,
240
- type: "call"
241
- });
242
- return "deny";
243
- }
244
- };
245
- /**
246
- * For a mutation whose policy is "confirm": without a token, a new one and
247
- * the host's message for the page; with one, null when it is valid for
248
- * this call and claimed now, else a refusal.
249
- */
250
- const confirmation = async (request, procedure, input, ctx) => {
251
- const { name } = request;
252
- const refuse = (cause) => {
253
- onError(cause, {
254
- name,
255
- type: "call"
256
- });
257
- return denial(name);
258
- };
259
- if (confirmer === null || options.confirmSubject === void 0) return refuse(/* @__PURE__ */ new Error(`The policy for "${name}" is "confirm", but createJigsServer has no secret and confirmSubject to sign a confirmation`));
260
- let binding;
261
- try {
262
- binding = {
263
- input,
264
- name,
265
- subject: options.confirmSubject(ctx)
266
- };
267
- } catch (error) {
268
- return refuse(error);
269
- }
270
- if (request.confirmation === void 0) {
271
- const fallback = `Allow "${name}" to run?`;
272
- let message;
273
- try {
274
- const written = procedure.confirm?.(input);
275
- message = written === void 0 ? fallback : confirmMessage(written);
276
- } catch (error) {
277
- return refuse(error);
278
- }
279
- if (message === "") message = fallback;
280
- const token = await confirmer.issue(binding);
281
- return { requiresAction: {
282
- message,
283
- token
284
- } };
285
- }
286
- let checked;
287
- try {
288
- checked = await confirmer.check(request.confirmation, binding);
289
- } catch (error) {
290
- return refuse(error);
291
- }
292
- if (checked === "ok") return null;
293
- return { error: {
294
- code: "FORBIDDEN",
295
- message: TOKEN_REFUSALS[checked]
296
- } };
297
- };
298
- const dispatch = async (request, context, trace) => {
299
- const procedure = Object.hasOwn(host.procedures, request.name) ? host.procedures[request.name] : void 0;
300
- const run = procedure?.kind !== "action" && Object.hasOwn(runs, request.name) ? runs[request.name] : void 0;
301
- if (procedure === void 0 || procedure.kind !== "action" && run === void 0) return { error: {
302
- code: "UNKNOWN_PROCEDURE",
303
- message: `"${request.name}" is not a query, mutation or action on this host`
304
- } };
305
- if (request.kind !== procedure.kind) return { error: {
306
- code: "BAD_INPUT",
307
- message: `"${request.name}" is ${procedure.kind === "action" ? "an" : "a"} ${procedure.kind} on this host, but the call says ${String(request.kind)}`
308
- } };
309
- let input;
310
- if (procedure.input !== void 0) {
311
- const parsed = procedure.input.safeParse(request.input);
312
- if (!parsed.success) return { error: {
313
- code: "BAD_INPUT",
314
- message: issuesText(parsed.error)
315
- } };
316
- input = parsed.data;
317
- }
318
- trace.input = input;
319
- if (procedure.kind === "mutation") {
320
- const policy = policyOf(request.name, procedure, input, context.ctx);
321
- const gated = policy === "deny" ? denial(request.name) : policy === "confirm" ? await confirmation(request, procedure, input, context.ctx) : null;
322
- if (gated !== null) {
323
- trace.gate = "requiresAction" in gated ? "requires-action" : "denied";
324
- return gated;
325
- }
326
- if (policy === "confirm") trace.gate = "confirmed";
327
- }
328
- if (procedure.kind === "action" || run === void 0) return {
329
- data: input ?? null,
330
- invalidate: []
331
- };
332
- let result;
333
- try {
334
- result = await run(input, context);
335
- } catch (error) {
336
- if (isJigsError(error)) return { error: errorData(error) };
337
- onError(error, {
338
- name: request.name,
339
- type: "call"
340
- });
341
- return { error: errorData(null) };
342
- }
343
- let data = null;
344
- let output;
345
- if (procedure.output !== void 0) {
346
- const parsed = procedure.output.safeParse(result);
347
- if (!parsed.success) {
348
- onError(parsed.error, {
349
- name: request.name,
350
- type: "call"
351
- });
352
- return { error: errorData(null) };
353
- }
354
- output = parsed.data;
355
- data = output;
356
- }
357
- if (procedure.projection !== void 0) {
358
- let projected;
359
- try {
360
- projected = procedure.projection.project(output);
361
- } catch (error) {
362
- onError(error, {
363
- name: request.name,
364
- type: "call"
365
- });
366
- return { error: errorData(null) };
367
- }
368
- const parsed = procedure.projection.schema.safeParse(projected);
369
- if (!parsed.success) {
370
- onError(parsed.error, {
371
- name: request.name,
372
- type: "call"
373
- });
374
- return { error: errorData(null) };
375
- }
376
- data = parsed.data;
377
- }
378
- if (procedure.kind === "query") return { data };
379
- const invalidate = invalidationsOf(procedure, input, output, (cause) => {
380
- onError(cause, {
381
- name: request.name,
382
- type: "call"
383
- });
384
- });
385
- return {
386
- data,
387
- invalidate
388
- };
389
- };
390
- const reportCall = (report) => {
391
- if (options.onCall === void 0) return;
392
- const failed = (cause) => {
393
- onError(cause, {
394
- name: report.name,
395
- type: "call"
396
- });
397
- };
398
- try {
399
- Promise.resolve(options.onCall(report)).catch(failed);
400
- } catch (error) {
401
- failed(error);
402
- }
403
- };
404
- const call = async (request, context) => {
405
- const started = performance.now();
406
- const trace = {
407
- gate: null,
408
- input: request.input
409
- };
410
- const outcome = await dispatch(request, context, trace);
411
- const report = {
412
- ctx: context.ctx,
413
- durationMs: performance.now() - started,
414
- input: trace.input,
415
- kind: request.kind,
416
- name: request.name,
417
- outcome: trace.gate ?? "success"
418
- };
419
- if ("error" in outcome) {
420
- report.error = outcome.error;
421
- report.outcome = trace.gate === "denied" ? "denied" : "error";
422
- }
423
- reportCall(report);
424
- return outcome;
425
- };
426
- const parseSession = (body) => {
427
- const messages = messagesSchema.safeParse(body.messages);
428
- if (!messages.success) return { error: {
429
- code: "BAD_INPUT",
430
- message: `messages: ${issuesText(messages.error)}`
431
- } };
432
- if (options.options === void 0) return {
433
- messages: messages.data,
434
- options: void 0
435
- };
436
- const parsed = options.options.safeParse(body.options ?? {});
437
- if (!parsed.success) return { error: {
438
- code: "BAD_INPUT",
439
- message: `options: ${issuesText(parsed.error)}`
440
- } };
441
- return {
442
- messages: messages.data,
443
- options: parsed.data
444
- };
445
- };
446
- /** A stored source: its pragma's versions are checked, then its body compiled. */
447
- const compileStored = (source, placement = "document") => {
448
- const stored = splitPragma(source);
449
- const compiled = checker.compile(stored.body);
450
- const prefix = source.slice(0, source.length - stored.body.length);
451
- return {
452
- advice: belowPrefix(compiled.advise(placement), prefix),
453
- diagnostics: [...versionDiagnostics(stored.versions, host.components.version), ...belowPrefix(compiled.diagnostics, prefix)],
454
- program: compiled.program
455
- };
456
- };
457
- /** The strict rule: an error or warning diagnostic throws. */
458
- const compileStrict = (subject, source, placement) => {
459
- const compiled = compileStored(source, placement);
460
- const fatal = fatalDiagnostics(compiled.diagnostics);
461
- if (fatal.length > 0) throw new JigsCompileError(subject, fatal);
462
- return compiled;
463
- };
464
- const specs = /* @__PURE__ */ new Map();
465
- for (const [name, source] of Object.entries(options.specs ?? {})) specs.set(name, compileStrict(`Spec "${name}"`, source).program);
466
- const spec = (name) => {
467
- const program = specs.get(name);
468
- if (program === void 0) throw new Error(`createJigsServer has no spec named "${name}"`);
469
- return program;
470
- };
471
- const compile = async (source, compileOptions) => compileOptions?.strict === false ? compileStored(source, compileOptions.placement) : compileStrict("The source", source, compileOptions?.placement);
472
- const sessionDeps = {
473
- checker,
474
- designRepair,
475
- host,
476
- kind: "session",
477
- maxRepairs,
478
- model: options.model,
479
- onError,
480
- onTurn: options.onTurn,
481
- programIntervalMs,
482
- system,
483
- systemPrefix: documentParts.stable
484
- };
485
- const serverOrigin = (request) => ownOrigin ?? new URL(request.url).origin;
486
- /**
487
- * Why a POST is refused before anything runs: an `Origin` that may not
488
- * call this server, or a body that is not JSON, which a cross-site form
489
- * can send without a preflight. Null when it may go on.
490
- */
491
- const refusal = (request) => {
492
- const origin = request.headers.get("origin");
493
- if (origin !== null && origin !== serverOrigin(request) && !allowedOrigins.has(origin)) return {
494
- code: "FORBIDDEN",
495
- message: "This origin may not call this server."
496
- };
497
- if (!(request.headers.get("content-type") ?? "").toLowerCase().startsWith("application/json")) return {
498
- code: "BAD_INPUT",
499
- message: "Send the request body as application/json."
500
- };
501
- return null;
502
- };
503
- const chat = async (request, chatOptions) => {
504
- const refused = refusal(request);
505
- if (refused !== null) throw new JigsError(refused.code, refused.message);
506
- let ctx;
507
- try {
508
- ctx = await options.context(request);
509
- } catch (error) {
510
- if (isJigsError(error)) throw error;
511
- onError(error, { type: "chat" });
512
- throw new JigsError("INTERNAL", INTERNAL_MESSAGE);
513
- }
514
- const input = parseBounded(() => chatInputSchema.safeParse({
515
- blocks: chatOptions?.blocks,
516
- prompt: chatOptions?.prompt
517
- }));
518
- if (!input.success) throw new JigsError("BAD_INPUT", input.message);
519
- let turnOptions;
520
- if (options.options === void 0) turnOptions = void 0;
521
- else {
522
- const schema = options.options;
523
- const parsed = parseBounded(() => schema.safeParse(chatOptions?.options ?? {}));
524
- if (!parsed.success) throw new JigsError("BAD_INPUT", `options: ${parsed.message}`);
525
- turnOptions = parsed.data;
526
- }
527
- const turnCall = {
528
- ctx,
529
- signal: chatOptions?.signal ?? new AbortController().signal
530
- };
531
- const repairRounds = Math.max(0, maxRepairs - 1);
532
- const parser = createReplyParser({
533
- catalogVersion: host.components.version,
534
- compile: (body) => checker.compile(body),
535
- designRepair,
536
- now: () => performance.now(),
537
- programIntervalMs,
538
- repairs: options.model !== void 0 && maxRepairs > 0,
539
- tags: chatTags
540
- }, input.data.blocks ?? []);
541
- const turnDeps = {
542
- call: turnCall,
543
- options: turnOptions,
544
- parser,
545
- prompt: input.data.prompt,
546
- session: {
547
- ...sessionDeps,
548
- kind: "chat-repair",
549
- maxRepairs: repairRounds
550
- }
551
- };
552
- const repair = (block) => repairBlock(turnDeps, block);
553
- return {
554
- end: parser.end,
555
- push: parser.push,
556
- repair,
557
- results: parser.results,
558
- stream: (reply) => streamChat({
559
- end: parser.end,
560
- push: parser.push,
561
- repair
562
- }, reply, chatOptions?.signal),
563
- system: chatSystem,
564
- systemPrefix: chatParts.stable
565
- };
566
- };
567
- async function* session(body, context) {
568
- const parsed = parseSession(body);
569
- if ("error" in parsed) {
570
- yield {
571
- error: parsed.error,
572
- type: "error"
573
- };
574
- return;
575
- }
576
- yield* runSession(sessionDeps, parsed.messages, parsed.options, context);
577
- }
578
- const tooLarge = (request) => failure(request, {
579
- code: "BAD_INPUT",
580
- message: `The request body is larger than ${maxBodyBytes} bytes.`
581
- });
582
- const corsHeaders = (request) => {
583
- const headers = new Headers({ [VERSION_HEADER]: version });
584
- const origin = request.headers.get("origin");
585
- if (origin !== null && allowedOrigins.has(origin)) {
586
- headers.set("access-control-allow-origin", origin);
587
- headers.set("access-control-allow-credentials", "true");
588
- headers.set("access-control-expose-headers", VERSION_HEADER);
589
- headers.append("vary", "Origin");
590
- }
591
- return headers;
592
- };
593
- const json = (request, status, value) => {
594
- const headers = corsHeaders(request);
595
- headers.set("content-type", "application/json");
596
- headers.set("cache-control", "no-store");
597
- return new Response(JSON.stringify(value), {
598
- headers,
599
- status
600
- });
601
- };
602
- const failure = (request, error) => json(request, statusOf(error.code), { error });
603
- const frame = (request) => {
604
- const ancestors = ["'self'", ...allowedOrigins].join(" ");
605
- const headers = request === void 0 ? new Headers() : corsHeaders(request);
606
- if (headers.has("access-control-allow-origin")) headers.set("access-control-expose-headers", `${VERSION_HEADER}, content-security-policy`);
607
- headers.set("cache-control", "no-cache");
608
- headers.set("content-security-policy", `${frameSecurityPolicy}; frame-ancestors ${ancestors}`);
609
- headers.set("content-type", "text/html; charset=utf-8");
610
- headers.set("etag", `"${version}"`);
611
- headers.set(VERSION_HEADER, version);
612
- headers.set("x-content-type-options", "nosniff");
613
- if (request?.headers.get("if-none-match") === `"${version}"`) return new Response(null, {
614
- headers,
615
- status: 304
616
- });
617
- return new Response(frameHtml, {
618
- headers,
619
- status: 200
620
- });
621
- };
622
- const contextFor = async (request, type) => {
623
- try {
624
- return { ctx: await options.context(request) };
625
- } catch (error) {
626
- if (isJigsError(error)) return { error: errorData(error) };
627
- onError(error, { type });
628
- return { error: {
629
- code: "INTERNAL",
630
- message: INTERNAL_MESSAGE
631
- } };
632
- }
633
- };
634
- const stream = (request, events, abort, cleanup) => {
635
- const encoder = new TextEncoder();
636
- const iterator = events[Symbol.asyncIterator]();
637
- let heartbeat;
638
- let open = true;
639
- const finish = () => {
640
- open = false;
641
- clearInterval(heartbeat);
642
- cleanup();
643
- };
644
- const body = new ReadableStream({
645
- async cancel() {
646
- finish();
647
- abort.abort();
648
- await iterator.return?.();
649
- },
650
- async pull(controller) {
651
- let next;
652
- try {
653
- next = await iterator.next();
654
- } catch (error) {
655
- onError(error, { type: "session" });
656
- finish();
657
- const event = {
658
- error: {
659
- code: "INTERNAL",
660
- message: INTERNAL_MESSAGE
661
- },
662
- type: "error"
663
- };
664
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\n\n`));
665
- controller.close();
666
- return;
667
- }
668
- if (next.done === true) {
669
- finish();
670
- controller.close();
671
- return;
672
- }
673
- controller.enqueue(encoder.encode(`data: ${JSON.stringify(next.value)}\n\n`));
674
- },
675
- start(controller) {
676
- heartbeat = setInterval(() => {
677
- if (!open) return;
678
- try {
679
- controller.enqueue(encoder.encode(": keep-alive\n\n"));
680
- } catch {
681
- finish();
682
- }
683
- }, HEARTBEAT_MS);
684
- }
685
- });
686
- const headers = corsHeaders(request);
687
- headers.set("content-type", "text/event-stream; charset=utf-8");
688
- headers.set("cache-control", "no-store");
689
- headers.set("x-accel-buffering", "no");
690
- return new Response(body, {
691
- headers,
692
- status: 200
693
- });
694
- };
695
- const handler = async (request) => {
696
- const origin = request.headers.get("origin");
697
- if (request.method === "OPTIONS") {
698
- const headers = corsHeaders(request);
699
- if (origin !== null && allowedOrigins.has(origin)) {
700
- headers.set("access-control-allow-methods", "GET, POST");
701
- headers.set("access-control-allow-headers", request.headers.get("access-control-request-headers") ?? "content-type");
702
- headers.set("access-control-max-age", "600");
703
- }
704
- return new Response(null, {
705
- headers,
706
- status: 204
707
- });
708
- }
709
- if (request.method === "GET") return frame(request);
710
- if (request.method !== "POST") {
711
- const headers = corsHeaders(request);
712
- headers.set("allow", "GET, POST, OPTIONS");
713
- return new Response(null, {
714
- headers,
715
- status: 405
716
- });
717
- }
718
- const refused = refusal(request);
719
- if (refused !== null) return failure(request, refused);
720
- if (Number(request.headers.get("content-length") ?? "0") > maxBodyBytes) return tooLarge(request);
721
- let text;
722
- try {
723
- text = await readLimited(request, maxBodyBytes);
724
- } catch {
725
- return failure(request, {
726
- code: "BAD_INPUT",
727
- message: "The request body could not be read."
728
- });
729
- }
730
- if (text === null) return tooLarge(request);
731
- let raw;
732
- try {
733
- raw = JSON.parse(text);
734
- } catch {
735
- return failure(request, {
736
- code: "BAD_INPUT",
737
- message: "The request body is not JSON."
738
- });
739
- }
740
- const body = parseBounded(() => bodySchema.safeParse(raw));
741
- if (!body.success) return failure(request, {
742
- code: "BAD_INPUT",
743
- message: body.message
744
- });
745
- const context = await contextFor(request, body.data.type === "call" ? "call" : "session");
746
- if ("error" in context) return failure(request, context.error);
747
- if (body.data.type === "call") {
748
- const callRequest = {
749
- input: body.data.input,
750
- kind: body.data.kind,
751
- name: body.data.name
752
- };
753
- if (body.data.confirmation !== void 0) callRequest.confirmation = body.data.confirmation;
754
- const outcome = await call(callRequest, {
755
- ctx: context.ctx,
756
- signal: request.signal
757
- });
758
- return json(request, "error" in outcome ? statusOf(outcome.error.code) : 200, outcome);
759
- }
760
- const parsed = parseSession(body.data);
761
- if ("error" in parsed) return failure(request, parsed.error);
762
- const abort = new AbortController();
763
- const forward = () => abort.abort();
764
- if (request.signal.aborted) abort.abort();
765
- else request.signal.addEventListener("abort", forward, { once: true });
766
- return stream(request, runSession(sessionDeps, parsed.messages, parsed.options, {
767
- ctx: context.ctx,
768
- signal: abort.signal
769
- }), abort, () => request.signal.removeEventListener("abort", forward));
770
- };
771
- return {
772
- call,
773
- chat,
774
- compile,
775
- frame,
776
- handler,
777
- maxBodyBytes,
778
- prompt: (mode) => mode === "chat" ? chatSystem : system,
779
- promptPrefix: (mode) => mode === "chat" ? chatParts.stable : documentParts.stable,
780
- session,
781
- spec,
782
- version
783
- };
784
- }
785
- //#endregion
786
- export { createJigsServer, defineRun };