@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/core.js ADDED
@@ -0,0 +1,722 @@
1
+ import { INTERNAL_MESSAGE, JigsCompileError, JigsError, errorData, fatalDiagnostics, isJigsError } from "./errors.js";
2
+ import { belowPrefix, splitPragma, versionDiagnostics } from "./pragma.js";
3
+ import { checkMarkdownElements, createChecker } from "./compile.js";
4
+ import { VERSION_HEADER, createHandler, createHttp, issuesText, originOf } from "./handler.js";
5
+ import { MAX_CONTENT, asksModel, parseSessionInput, restoreTurn } from "./session.js";
6
+ import { confirmMessage, createConfirmer, memoryConfirmations } from "./confirm.js";
7
+ import { processCompileCache, sha256 } from "./compile-cache.js";
8
+ import { eventStream } from "./events.js";
9
+ import { invalidationsOf } from "./invalidation.js";
10
+ import { createModelServer } from "./model-server.js";
11
+ import { systemPromptParts } from "@jigs.design/prompt";
12
+ import { isCallPolicy } from "@jigs.design/host";
13
+ import { PROGRAM_FORMAT } from "@jigs.design/protocol";
14
+ import { z } from "zod";
15
+ import { frameDocument, frameSecurityPolicy } from "@jigs.design/runtime/frame";
16
+ //#region src/core.ts
17
+ const DEFAULT_MAX_BODY_BYTES = 1e6;
18
+ /** A small document whose compiled code changes when the compiler's output does. */
19
+ const COMPILER_PROBE = `{@body const [count, setCount] = useState(0)}
20
+ # Probe
21
+ <each of={[1, 2]} as="n">
22
+ {n} {count}
23
+ </each>
24
+ <if test={count > 0}>
25
+ **done**
26
+ </if>`;
27
+ /** What `resolvePage` must return, checked, since a plain-JavaScript one may not. */
28
+ const savedPageSchema = z.object({ source: z.string() });
29
+ /** The strict rule: an error or warning diagnostic throws. */
30
+ function strictly(subject, compiled) {
31
+ const fatal = fatalDiagnostics(compiled.diagnostics);
32
+ if (fatal.length > 0) throw new JigsCompileError(subject, fatal);
33
+ return compiled;
34
+ }
35
+ /** A document page, shipped or saved, takes no props. */
36
+ function noProps(name, props) {
37
+ if (Object.keys(props).length > 0) throw new JigsError("BAD_INPUT", `"${name}" is a document page, which takes no props`);
38
+ }
39
+ /** An `AbortError`, as `fetch` and other APIs reject with on an aborted signal. */
40
+ function isAbortError(cause) {
41
+ return cause instanceof Error && cause.name === "AbortError";
42
+ }
43
+ /** A call a policy refused. The procedure stays in the prompt. */
44
+ const denial = (name) => ({ error: {
45
+ code: "FORBIDDEN",
46
+ message: `"${name}" is not allowed here`
47
+ } });
48
+ const TOKEN_REFUSALS = {
49
+ expired: "The confirmation has expired. Try again.",
50
+ invalid: "The confirmation is not valid for this call.",
51
+ used: "The confirmation was already used."
52
+ };
53
+ /**
54
+ * The server's confirmer, or null when it has no `secret`. Throws at startup
55
+ * when a mutation's policy is "confirm" and the server cannot sign for it.
56
+ */
57
+ function confirmerFor(options) {
58
+ if (options.secret === void 0) {
59
+ const confirming = Object.entries(options.host.procedures).find(([, procedure]) => procedure.kind === "mutation" && procedure.policy === "confirm");
60
+ 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`);
61
+ if (options.confirmSubject !== void 0) throw new Error("createJigsServer has confirmSubject but no secret");
62
+ return null;
63
+ }
64
+ if (options.secret.length < 32) throw new Error(`createJigsServer's secret must be at least 32 characters`);
65
+ 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");
66
+ const now = options.now ?? Date.now;
67
+ return createConfirmer(options.secret, options.confirmations ?? memoryConfirmations(now), now);
68
+ }
69
+ /** FNV-1a, enough to tell versions apart; not a security boundary. */
70
+ function fingerprint(text) {
71
+ let hash = 2166136261;
72
+ for (let index = 0; index < text.length; index++) {
73
+ hash ^= text.charCodeAt(index);
74
+ hash = Math.imul(hash, 16777619);
75
+ }
76
+ return (hash >>> 0).toString(36);
77
+ }
78
+ /** Everything about the host that changes what a source compiles to. */
79
+ function hostFingerprint(host) {
80
+ return JSON.stringify({
81
+ catalog: host.components.version,
82
+ components: host.components.components.map((component) => ({
83
+ children: component.children,
84
+ name: component.name,
85
+ props: Object.entries(component.props).map(([name, definition]) => [
86
+ name,
87
+ definition.schema,
88
+ definition.default ?? null
89
+ ])
90
+ })),
91
+ imageHosts: host.imageHosts,
92
+ procedures: Object.entries(host.procedures).map(([name, procedure]) => ({
93
+ input: procedure.input === void 0 ? null : z.toJSONSchema(procedure.input),
94
+ invalidates: procedure.kind === "mutation" ? procedure.invalidates : [],
95
+ kind: procedure.kind,
96
+ name,
97
+ output: procedure.output === void 0 ? null : z.toJSONSchema(procedure.output),
98
+ projection: procedure.kind === "action" || procedure.kind === "data" || procedure.projection === void 0 ? null : z.toJSONSchema(procedure.projection.schema)
99
+ })),
100
+ jigs: host.jigs.map((entry) => [
101
+ entry.name,
102
+ splitPragma(entry.source).body,
103
+ entry.kind,
104
+ entry.page,
105
+ entry.tag
106
+ ])
107
+ });
108
+ }
109
+ /**
110
+ * Types a `context` and `run` pair outside `createJigsServer`, so two
111
+ * servers (one per platform) can share them. `TContext` comes from `context`
112
+ * alone, as in `createJigsServer`.
113
+ */
114
+ function defineRun(procedures, handlers) {
115
+ return handlers;
116
+ }
117
+ function createJigsServer(options) {
118
+ const { host } = options;
119
+ checkMarkdownElements(host.components);
120
+ for (const [name, procedure] of Object.entries(host.procedures)) if (procedure.kind !== "action" && procedure.kind !== "data" && !Object.hasOwn(options.run, name)) throw new Error(`createJigsServer has no run function for "${name}"`);
121
+ const confirmer = confirmerFor(options);
122
+ const checker = createChecker(host);
123
+ const promptOptions = (mode) => options.instructions === void 0 ? { mode } : {
124
+ instructions: options.instructions,
125
+ mode
126
+ };
127
+ const documentParts = systemPromptParts(host, promptOptions("document"));
128
+ const chatParts = systemPromptParts(host, promptOptions("chat"));
129
+ const system = `${documentParts.stable}\n\n${documentParts.variable}`;
130
+ const chatSystem = `${chatParts.stable}\n\n${chatParts.variable}`;
131
+ const allowedOrigins = new Set((options.allowedOrigins ?? []).map((entry) => originOf(entry)));
132
+ const ownOrigin = options.origin === void 0 ? null : originOf(options.origin);
133
+ const maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
134
+ const frameHtml = frameDocument();
135
+ const compilerProbe = checker.compile(COMPILER_PROBE).program.code;
136
+ const fingerprintInput = JSON.stringify([
137
+ PROGRAM_FORMAT,
138
+ 0,
139
+ frameHtml,
140
+ compilerProbe,
141
+ hostFingerprint(host)
142
+ ]);
143
+ const version = fingerprint(fingerprintInput);
144
+ const namespace = sha256(fingerprintInput);
145
+ const onError = (cause, info) => {
146
+ try {
147
+ options.onError?.(cause, info);
148
+ } catch {}
149
+ };
150
+ const runs = options.run;
151
+ /** Whether a mutation call may run, by the server's override or else the host. */
152
+ const policyOf = (name, procedure, input, ctx) => {
153
+ if (options.policy === void 0) return procedure.policy;
154
+ try {
155
+ const policyCall = {
156
+ ctx,
157
+ input,
158
+ name
159
+ };
160
+ const decided = options.policy(policyCall);
161
+ if (decided === void 0) return procedure.policy;
162
+ if (!isCallPolicy(decided)) throw new Error(`The policy option returned ${String(decided)} for "${name}"; return "allow", "confirm", "deny" or undefined, synchronously`);
163
+ return decided;
164
+ } catch (error) {
165
+ onError(error, {
166
+ name,
167
+ type: "call"
168
+ });
169
+ return "deny";
170
+ }
171
+ };
172
+ /**
173
+ * For a mutation whose policy is "confirm": without a token, a new one and
174
+ * the host's message for the page; with one, null when it is valid for
175
+ * this call and claimed now, else a refusal.
176
+ */
177
+ const confirmation = async (request, procedure, input, ctx) => {
178
+ const { name } = request;
179
+ const refuse = (cause) => {
180
+ onError(cause, {
181
+ name,
182
+ type: "call"
183
+ });
184
+ return denial(name);
185
+ };
186
+ 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`));
187
+ let binding;
188
+ try {
189
+ binding = {
190
+ input,
191
+ name,
192
+ subject: options.confirmSubject(ctx)
193
+ };
194
+ } catch (error) {
195
+ return refuse(error);
196
+ }
197
+ if (request.confirmation === void 0) {
198
+ const fallback = `Allow "${name}" to run?`;
199
+ let message;
200
+ try {
201
+ const written = procedure.confirm?.(input);
202
+ message = written === void 0 ? fallback : confirmMessage(written);
203
+ } catch (error) {
204
+ return refuse(error);
205
+ }
206
+ if (message === "") message = fallback;
207
+ const token = await confirmer.issue(binding);
208
+ return { requiresAction: {
209
+ message,
210
+ token
211
+ } };
212
+ }
213
+ let checked;
214
+ try {
215
+ checked = await confirmer.check(request.confirmation, binding);
216
+ } catch (error) {
217
+ return refuse(error);
218
+ }
219
+ if (checked === "ok") return null;
220
+ return { error: {
221
+ code: "FORBIDDEN",
222
+ message: TOKEN_REFUSALS[checked]
223
+ } };
224
+ };
225
+ const dispatch = async (request, context, trace) => {
226
+ const procedure = Object.hasOwn(host.procedures, request.name) ? host.procedures[request.name] : void 0;
227
+ const run = procedure?.kind !== "action" && procedure?.kind !== "data" && Object.hasOwn(runs, request.name) ? runs[request.name] : void 0;
228
+ if (procedure?.kind === "data") return { error: {
229
+ code: "BAD_INPUT",
230
+ message: `"${request.name}" is provided data: it is supplied by the page, not fetched`
231
+ } };
232
+ if (procedure === void 0 || procedure.kind !== "action" && run === void 0) return { error: {
233
+ code: "UNKNOWN_PROCEDURE",
234
+ message: `"${request.name}" is not a query, mutation or action on this host`
235
+ } };
236
+ if (request.kind !== procedure.kind) return { error: {
237
+ code: "BAD_INPUT",
238
+ message: `"${request.name}" is ${procedure.kind === "action" ? "an" : "a"} ${procedure.kind} on this host, but the call says ${String(request.kind)}`
239
+ } };
240
+ let input;
241
+ if (procedure.input !== void 0) {
242
+ const parsed = procedure.input.safeParse(request.input);
243
+ if (!parsed.success) return { error: {
244
+ code: "BAD_INPUT",
245
+ message: issuesText(parsed.error)
246
+ } };
247
+ input = parsed.data;
248
+ }
249
+ trace.input = input;
250
+ if (procedure.kind === "mutation") {
251
+ const policy = policyOf(request.name, procedure, input, context.ctx);
252
+ const gated = policy === "deny" ? denial(request.name) : policy === "confirm" ? await confirmation(request, procedure, input, context.ctx) : null;
253
+ if (gated !== null) {
254
+ trace.gate = "requiresAction" in gated ? "requires-action" : "denied";
255
+ return gated;
256
+ }
257
+ if (policy === "confirm") trace.gate = "confirmed";
258
+ }
259
+ if (procedure.kind === "action" || run === void 0) return {
260
+ data: input ?? null,
261
+ invalidate: []
262
+ };
263
+ let result;
264
+ try {
265
+ result = await run(input, context);
266
+ } catch (error) {
267
+ if (context.signal.aborted || isAbortError(error)) {
268
+ trace.aborted = true;
269
+ return { error: errorData(null) };
270
+ }
271
+ if (isJigsError(error)) return { error: errorData(error) };
272
+ onError(error, {
273
+ name: request.name,
274
+ type: "call"
275
+ });
276
+ return { error: errorData(null) };
277
+ }
278
+ let data = null;
279
+ let output;
280
+ if (procedure.output !== void 0) {
281
+ const parsed = procedure.output.safeParse(result);
282
+ if (!parsed.success) {
283
+ onError(parsed.error, {
284
+ name: request.name,
285
+ type: "call"
286
+ });
287
+ return { error: errorData(null) };
288
+ }
289
+ output = parsed.data;
290
+ data = output;
291
+ }
292
+ if (procedure.projection !== void 0) {
293
+ let projected;
294
+ try {
295
+ projected = procedure.projection.project(output);
296
+ } catch (error) {
297
+ onError(error, {
298
+ name: request.name,
299
+ type: "call"
300
+ });
301
+ return { error: errorData(null) };
302
+ }
303
+ const parsed = procedure.projection.schema.safeParse(projected);
304
+ if (!parsed.success) {
305
+ onError(parsed.error, {
306
+ name: request.name,
307
+ type: "call"
308
+ });
309
+ return { error: errorData(null) };
310
+ }
311
+ data = parsed.data;
312
+ }
313
+ if (procedure.kind === "query") return { data };
314
+ const invalidate = invalidationsOf(procedure, input, output, (cause) => {
315
+ onError(cause, {
316
+ name: request.name,
317
+ type: "call"
318
+ });
319
+ });
320
+ return {
321
+ data,
322
+ invalidate
323
+ };
324
+ };
325
+ const reportCall = (report) => {
326
+ if (options.onCall === void 0) return;
327
+ const failed = (cause) => {
328
+ onError(cause, {
329
+ name: report.name,
330
+ type: report.kind === "page" ? "page" : "call"
331
+ });
332
+ };
333
+ try {
334
+ Promise.resolve(options.onCall(report)).catch(failed);
335
+ } catch (error) {
336
+ failed(error);
337
+ }
338
+ };
339
+ const call = async (request, context) => {
340
+ const started = performance.now();
341
+ const trace = {
342
+ aborted: false,
343
+ gate: null,
344
+ input: request.input
345
+ };
346
+ const outcome = await dispatch(request, context, trace);
347
+ const report = {
348
+ ctx: context.ctx,
349
+ durationMs: performance.now() - started,
350
+ input: trace.input,
351
+ kind: request.kind,
352
+ name: request.name,
353
+ outcome: trace.gate ?? "success"
354
+ };
355
+ if (trace.aborted) report.outcome = "aborted";
356
+ else if ("error" in outcome) {
357
+ report.error = outcome.error;
358
+ report.outcome = trace.gate === "denied" ? "denied" : "error";
359
+ }
360
+ reportCall(report);
361
+ return outcome;
362
+ };
363
+ /** A stored source: its pragma's versions are checked, then its body compiled. */
364
+ const compileStored = (source, placement = "document") => {
365
+ const stored = splitPragma(source);
366
+ const compiled = checker.compile(stored.body);
367
+ const prefix = source.slice(0, source.length - stored.body.length);
368
+ return {
369
+ advice: belowPrefix(compiled.advise(placement), prefix),
370
+ diagnostics: [...versionDiagnostics(stored.versions, host.components.version), ...belowPrefix(compiled.diagnostics, prefix)],
371
+ program: compiled.program
372
+ };
373
+ };
374
+ /**
375
+ * `compile` through the process's cache, keyed by what this server
376
+ * compiles against, the placement (or "page", for a define page's body,
377
+ * which compiles against every define jig) and the body's hash. Only
378
+ * final artifacts come here: a reply's partial sources stay on `checker`.
379
+ */
380
+ const cached = async (placement, body, compile) => {
381
+ const key = `${await namespace}.${placement}.${await sha256(body)}`;
382
+ const hit = processCompileCache.get(key);
383
+ if (hit !== void 0) return hit;
384
+ const compiled = compile();
385
+ processCompileCache.set(key, compiled);
386
+ return compiled;
387
+ };
388
+ const compileCached = (source, placement = "document") => cached(placement, source, () => compileStored(source, placement));
389
+ const jigsByName = new Map(host.jigs.map((entry) => [entry.name, entry]));
390
+ /** A define page's one-tag body compiled; it throws as `pageBody` does. */
391
+ const compileDefine = (name, props) => {
392
+ const compiled = checker.page(name, props);
393
+ return {
394
+ advice: compiled.advise("document"),
395
+ diagnostics: compiled.diagnostics,
396
+ program: compiled.program
397
+ };
398
+ };
399
+ const startupPages = /* @__PURE__ */ new Map();
400
+ for (const entry of host.jigs) {
401
+ if (!entry.page) continue;
402
+ const subject = `Jig "${entry.name}"`;
403
+ if (entry.kind === "document") startupPages.set(entry.name, {
404
+ ...strictly(subject, compileStored(entry.source)),
405
+ origin: "shipped",
406
+ source: entry.source
407
+ });
408
+ else if (!checker.needsProps(entry.name)) startupPages.set(entry.name, {
409
+ ...strictly(subject, compileDefine(entry.name, {})),
410
+ origin: "shipped",
411
+ source: checker.pageBody(entry.name, {})
412
+ });
413
+ }
414
+ /** A page the host lists, compiled strictly. */
415
+ const shippedPage = async (entry, props) => {
416
+ const ready = startupPages.get(entry.name);
417
+ if (entry.kind === "document") noProps(entry.name, props);
418
+ if (ready !== void 0 && Object.keys(props).length === 0) return ready;
419
+ const body = checker.pageBody(entry.name, props);
420
+ const compiled = await cached("page", body, () => compileDefine(entry.name, props));
421
+ return {
422
+ ...strictly(`Page "${entry.name}"`, compiled),
423
+ origin: "shipped",
424
+ source: body
425
+ };
426
+ };
427
+ const internal = (cause, name) => {
428
+ onError(cause, {
429
+ name,
430
+ type: "page"
431
+ });
432
+ return new JigsError("INTERNAL", INTERNAL_MESSAGE);
433
+ };
434
+ /** The source `resolvePage` finds for `call.ctx`, checked; null when it finds none. */
435
+ const savedSource = async (name, props, lookup) => {
436
+ if (options.resolvePage === void 0) return null;
437
+ let found;
438
+ try {
439
+ found = await options.resolvePage(name, lookup.signal === void 0 ? { ctx: lookup.ctx } : {
440
+ ctx: lookup.ctx,
441
+ signal: lookup.signal
442
+ });
443
+ } catch (error) {
444
+ if (isJigsError(error)) throw error;
445
+ throw internal(error, name);
446
+ }
447
+ if (found === null) return null;
448
+ const checked = savedPageSchema.safeParse(found);
449
+ if (!checked.success) throw internal(/* @__PURE__ */ new Error(`resolvePage returned ${String(found)} for "${name}"; return { source } or null`), name);
450
+ const source = checked.data.source;
451
+ if (source.length > 2e5) throw internal(/* @__PURE__ */ new Error(`The source resolvePage returned for "${name}" is longer than ${MAX_CONTENT} characters`), name);
452
+ noProps(name, props);
453
+ return source;
454
+ };
455
+ /** A page `resolvePage` finds for `call.ctx`, compiled leniently unless `strict`. */
456
+ const savedPage = async (name, props, lookup) => {
457
+ const source = await savedSource(name, props, lookup);
458
+ if (source === null) return null;
459
+ const compiled = await compileCached(source);
460
+ return {
461
+ ...lookup.strict === true ? strictly(`Page "${name}"`, compiled) : compiled,
462
+ origin: "saved",
463
+ source
464
+ };
465
+ };
466
+ const resolve = async (name, props = {}, lookup) => {
467
+ const entry = jigsByName.get(name);
468
+ if (entry?.page === true) return shippedPage(entry, props);
469
+ if (lookup === void 0 || !("ctx" in lookup)) throw new Error(`createJigsServer has no page named "${name}"`);
470
+ return entry === void 0 ? savedPage(name, props, lookup) : null;
471
+ };
472
+ const page = resolve;
473
+ const compile = async (source, compileOptions) => {
474
+ const compiled = await compileCached(source, compileOptions?.placement);
475
+ return compileOptions?.strict === false ? compiled : strictly("The source", compiled);
476
+ };
477
+ /** Whether `pagePolicy` lets this `ctx` read a shipped page; it fails closed. */
478
+ const pageAllowed = (ctx, name, props) => {
479
+ if (options.pagePolicy === void 0) return true;
480
+ try {
481
+ const decided = options.pagePolicy({
482
+ ctx,
483
+ name,
484
+ props
485
+ });
486
+ if (decided !== "allow" && decided !== "deny") throw new Error(`pagePolicy returned ${String(decided)} for "${name}"; return "allow" or "deny", synchronously`);
487
+ return decided === "allow";
488
+ } catch (error) {
489
+ onError(error, {
490
+ name,
491
+ type: "page"
492
+ });
493
+ return false;
494
+ }
495
+ };
496
+ /**
497
+ * A page request from the wire, after `context`: `pagePolicy` for a
498
+ * shipped page, then `page` with `ctx`. A denied page and one no list
499
+ * has answer alike, so a denial does not say the page exists.
500
+ */
501
+ const pageRequest = async (request, context) => {
502
+ const started = performance.now();
503
+ const { name, props } = request;
504
+ const notFound = {
505
+ code: "NOT_FOUND",
506
+ message: `There is no page named "${name}".`
507
+ };
508
+ let outcome;
509
+ let denied = false;
510
+ const shippedName = name;
511
+ if (jigsByName.get(name)?.page === true && !pageAllowed(context.ctx, shippedName, props)) {
512
+ denied = true;
513
+ outcome = { error: notFound };
514
+ } else try {
515
+ const resolved = await page(name, props, context);
516
+ outcome = resolved === null ? { error: notFound } : {
517
+ diagnostics: resolved.diagnostics,
518
+ program: resolved.program,
519
+ version
520
+ };
521
+ } catch (error) {
522
+ if (!isJigsError(error)) onError(error, {
523
+ name,
524
+ type: "page"
525
+ });
526
+ outcome = { error: errorData(isJigsError(error) ? error : null) };
527
+ }
528
+ const report = {
529
+ ctx: context.ctx,
530
+ durationMs: performance.now() - started,
531
+ input: props,
532
+ kind: "page",
533
+ name,
534
+ outcome: "success"
535
+ };
536
+ if ("error" in outcome) {
537
+ report.error = outcome.error;
538
+ report.outcome = denied ? "denied" : "error";
539
+ }
540
+ reportCall(report);
541
+ return outcome;
542
+ };
543
+ /**
544
+ * The source a session opens for a page by name, or null when this
545
+ * `ctx` has no such page: a shipped page `pagePolicy` allows, as a
546
+ * self-contained document for a define page, or a saved one. A tag that
547
+ * is not a page is not one a session may open.
548
+ */
549
+ const pageSource = async (name, props, context) => {
550
+ const entry = jigsByName.get(name);
551
+ if (entry === void 0) return savedSource(name, props, context);
552
+ const shippedName = name;
553
+ if (!entry.page || !pageAllowed(context.ctx, shippedName, props)) return null;
554
+ if (entry.kind === "document") {
555
+ noProps(name, props);
556
+ return entry.source;
557
+ }
558
+ return checker.pageDocument(name, props);
559
+ };
560
+ const prepareSession = async (body, context) => {
561
+ const input = parseSessionInput(body);
562
+ if ("error" in input) return input;
563
+ const document = input.document;
564
+ if (document === null || "source" in document) return {
565
+ document: document?.source ?? null,
566
+ messages: input.messages
567
+ };
568
+ let source;
569
+ try {
570
+ source = await pageSource(document.name, document.props ?? {}, context);
571
+ } catch (error) {
572
+ if (!isJigsError(error)) onError(error, {
573
+ name: document.name,
574
+ type: "page"
575
+ });
576
+ return { error: errorData(isJigsError(error) ? error : null) };
577
+ }
578
+ if (source === null) return { error: {
579
+ code: "NOT_FOUND",
580
+ message: `There is no page named "${document.name}".`
581
+ } };
582
+ return {
583
+ document: source,
584
+ messages: input.messages
585
+ };
586
+ };
587
+ const http = createHttp({
588
+ allowedOrigins,
589
+ ownOrigin,
590
+ version
591
+ });
592
+ const frame = (request) => {
593
+ const ancestors = ["'self'", ...allowedOrigins].join(" ");
594
+ const headers = request === void 0 ? new Headers() : http.corsHeaders(request);
595
+ if (headers.has("access-control-allow-origin")) headers.set("access-control-expose-headers", `${VERSION_HEADER}, content-security-policy`);
596
+ headers.set("cache-control", "no-cache");
597
+ headers.append("vary", "Accept");
598
+ headers.set("content-security-policy", `${frameSecurityPolicy}; frame-ancestors ${ancestors}`);
599
+ headers.set("content-type", "text/html; charset=utf-8");
600
+ headers.set("etag", `"${version}"`);
601
+ headers.set(VERSION_HEADER, version);
602
+ headers.set("x-content-type-options", "nosniff");
603
+ if (request?.headers.get("if-none-match") === `"${version}"`) return new Response(null, {
604
+ headers,
605
+ status: 304
606
+ });
607
+ return new Response(frameHtml, {
608
+ headers,
609
+ status: 200
610
+ });
611
+ };
612
+ const contextFor = async (request, type) => {
613
+ try {
614
+ return { ctx: await options.context(request) };
615
+ } catch (error) {
616
+ if (isJigsError(error)) return { error: errorData(error) };
617
+ onError(error, { type });
618
+ return { error: {
619
+ code: "INTERNAL",
620
+ message: INTERNAL_MESSAGE
621
+ } };
622
+ }
623
+ };
624
+ /**
625
+ * A session turn with no model: a document that asks nothing is
626
+ * compiled again; a turn that would ask the model is not found here.
627
+ */
628
+ const restoreOnly = async (body, context) => {
629
+ const prepared = await prepareSession(body, context);
630
+ if ("error" in prepared) return prepared;
631
+ const document = prepared.document;
632
+ if (document === null || asksModel(prepared.messages)) return { error: {
633
+ code: "NOT_FOUND",
634
+ message: "This server has no model, so it cannot answer a turn that asks one."
635
+ } };
636
+ return { async *turn() {
637
+ yield* restoreTurn(checker, host.components.version, document, true);
638
+ } };
639
+ };
640
+ const hostEvents = options.events;
641
+ /**
642
+ * The push stream's answer before `ctx` is needed: a foreign `Origin` is
643
+ * refused, and a server without `events` says so with `204`. Null when
644
+ * the stream may open.
645
+ */
646
+ const eventsRefusal = (request) => {
647
+ const foreign = http.foreignOrigin(request);
648
+ if (foreign !== null) return http.failure(request, foreign);
649
+ if (hostEvents !== void 0) return null;
650
+ const headers = http.corsHeaders(request);
651
+ headers.append("vary", "Accept");
652
+ return new Response(null, {
653
+ headers,
654
+ status: 204
655
+ });
656
+ };
657
+ const events = async (request, context) => {
658
+ const refused = eventsRefusal(request);
659
+ if (refused !== null || hostEvents === void 0) return refused ?? new Response(null, { status: 204 });
660
+ return eventStream({
661
+ events: hostEvents,
662
+ http,
663
+ onError
664
+ }, request, context);
665
+ };
666
+ /** The handler's route for a `GET` that accepts `text/event-stream`. */
667
+ const eventsRoute = async (request) => {
668
+ const refused = eventsRefusal(request);
669
+ if (refused !== null) return refused;
670
+ const context = await contextFor(request, "events");
671
+ if ("error" in context) return http.failure(request, context.error);
672
+ return events(request, {
673
+ ctx: context.ctx,
674
+ signal: request.signal
675
+ });
676
+ };
677
+ const handlerDeps = {
678
+ allowedOrigins,
679
+ call,
680
+ contextFor,
681
+ events: eventsRoute,
682
+ frame,
683
+ http,
684
+ maxBodyBytes,
685
+ onError,
686
+ page: pageRequest
687
+ };
688
+ const data = (name, value) => {
689
+ const slot = Object.hasOwn(host.procedures, name) ? host.procedures[name] : void 0;
690
+ if (slot?.kind !== "data") throw new JigsError("BAD_INPUT", `"${name}" is not provided data on this host`);
691
+ const parsed = slot.output.safeParse(value);
692
+ if (!parsed.success) throw new JigsError("BAD_INPUT", `"${name}": ${issuesText(parsed.error)}`);
693
+ return parsed.data;
694
+ };
695
+ const core = {
696
+ call,
697
+ compile,
698
+ data,
699
+ events,
700
+ frame,
701
+ handler: createHandler({
702
+ ...handlerDeps,
703
+ session: restoreOnly
704
+ }),
705
+ maxBodyBytes,
706
+ page,
707
+ prompt: (mode) => mode === "chat" ? chatSystem : system,
708
+ promptPrefix: (mode) => mode === "chat" ? chatParts.stable : documentParts.stable,
709
+ version,
710
+ withModel: (modelOptions) => createModelServer({
711
+ checker,
712
+ context: options.context,
713
+ handlerDeps,
714
+ host,
715
+ onError,
716
+ prepareSession
717
+ }, core, modelOptions)
718
+ };
719
+ return core;
720
+ }
721
+ //#endregion
722
+ export { createJigsServer, defineRun };