@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/README.md +19 -13
- package/dist/chat.d.ts +2 -2
- package/dist/chat.js +46 -34
- package/dist/compile-cache.js +60 -0
- package/dist/compile.d.ts +1 -1
- package/dist/compile.js +182 -40
- package/dist/{server.d.ts → core.d.ts} +7 -3
- package/dist/core.js +722 -0
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +3 -3
- package/dist/events.js +129 -0
- package/dist/handler.d.ts +4 -0
- package/dist/handler.js +344 -0
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -2
- package/dist/inputs.js +16 -1
- package/dist/model-server.js +155 -0
- package/dist/session.d.ts +1 -2
- package/dist/session.js +116 -52
- package/dist/types.d.ts +292 -55
- package/package.json +6 -6
- package/dist/server.js +0 -786
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 };
|