@volter/world-core 3.0.38 → 3.0.39
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/app-route.cjs +26 -25
- package/app-route.d.cts +1 -0
- package/dist/app-route.cjs +26 -25
- package/dist/app-route.d.cts +1 -0
- package/dist/generated/pack-facts.json +409 -352
- package/dist/inject.cjs +474 -95
- package/dist/network-policy.cjs +108 -13
- package/dist/network-policy.d.cts +8 -3
- package/dist/src/actions.js +2 -1
- package/dist/src/anthropic-wire.d.ts +2 -0
- package/dist/src/anthropic-wire.js +5 -0
- package/dist/src/attestation.d.ts +6 -0
- package/dist/src/attestation.js +27 -0
- package/dist/src/bytes.d.ts +3 -0
- package/dist/src/bytes.js +17 -0
- package/dist/src/changeset.js +2 -1
- package/dist/src/clickhouse/sql.js +73 -14
- package/dist/src/derived-core.d.ts +210 -32
- package/dist/src/derived-core.js +510 -244
- package/dist/src/derived-real.d.ts +1 -1
- package/dist/src/derived-real.js +704 -93
- package/dist/src/derived.d.ts +18 -2
- package/dist/src/derived.js +21 -2
- package/dist/src/events.d.ts +28 -45
- package/dist/src/events.js +40 -58
- package/dist/src/exact-json.d.ts +10 -0
- package/dist/src/exact-json.js +98 -0
- package/dist/src/executor.d.ts +17 -0
- package/dist/src/executor.js +91 -30
- package/dist/src/file-response.js +1 -1
- package/dist/src/form.d.ts +8 -0
- package/dist/src/form.js +76 -0
- package/dist/src/git/format.d.ts +16 -0
- package/dist/src/git/format.js +110 -0
- package/dist/src/git/index.d.ts +1 -0
- package/dist/src/git/index.js +1 -0
- package/dist/src/git/lfs.d.ts +1 -1
- package/dist/src/git/lfs.js +4 -3
- package/dist/src/graphql-wire.d.ts +6 -0
- package/dist/src/graphql-wire.js +6 -3
- package/dist/src/grpc-wire.js +2 -1
- package/dist/src/head.d.ts +1 -0
- package/dist/src/head.js +15 -4
- package/dist/src/held-reads.d.ts +6 -0
- package/dist/src/held-reads.js +60 -0
- package/dist/src/history.js +5 -4
- package/dist/src/host-port.d.ts +5 -0
- package/dist/src/host-port.js +14 -0
- package/dist/src/index.d.ts +11 -3
- package/dist/src/index.js +9 -4
- package/dist/src/log.js +12 -7
- package/dist/src/machines.d.ts +81 -5
- package/dist/src/machines.js +125 -9
- package/dist/src/managed-database.d.ts +15 -11
- package/dist/src/managed-database.js +38 -37
- package/dist/src/multipart.d.ts +21 -3
- package/dist/src/multipart.js +112 -38
- package/dist/src/observe.d.ts +3 -0
- package/dist/src/observe.js +6 -2
- package/dist/src/openai-wire.d.ts +16 -0
- package/dist/src/openai-wire.js +43 -12
- package/dist/src/pack-fetch.d.ts +4 -6
- package/dist/src/pack-fetch.js +189 -57
- package/dist/src/packRegistry.d.ts +7 -1
- package/dist/src/packRegistry.js +20 -3
- package/dist/src/private-transport.d.ts +1 -0
- package/dist/src/private-transport.js +11 -0
- package/dist/src/protobuf.js +10 -4
- package/dist/src/rateBudget.d.ts +8 -1
- package/dist/src/rateBudget.js +1 -0
- package/dist/src/redis/engine.d.ts +14 -5
- package/dist/src/redis/engine.js +271 -72
- package/dist/src/redis/frames.d.ts +17 -0
- package/dist/src/redis/frames.js +123 -0
- package/dist/src/redis/index.d.ts +6 -0
- package/dist/src/redis/index.js +6 -0
- package/dist/src/redis/json.d.ts +2 -0
- package/dist/src/redis/json.js +14 -0
- package/dist/src/redis/lua-libs.d.ts +4 -0
- package/dist/src/redis/lua-libs.js +173 -0
- package/dist/src/redis/lua.d.ts +1 -0
- package/dist/src/redis/lua.js +36 -0
- package/dist/src/redis/member-storage.d.ts +5 -0
- package/dist/src/redis/member-storage.js +75 -0
- package/dist/src/redis/resp2.d.ts +4 -0
- package/dist/src/redis/resp2.js +29 -0
- package/dist/src/redis/stream.d.ts +14 -0
- package/dist/src/redis/stream.js +335 -0
- package/dist/src/remote-execute.d.ts +2 -2
- package/dist/src/request-body.d.ts +11 -0
- package/dist/src/request-body.js +11 -0
- package/dist/src/runtime.d.ts +6 -5
- package/dist/src/runtime.js +5 -5
- package/dist/src/s3/wire.d.ts +4 -4
- package/dist/src/s3/wire.js +5 -3
- package/dist/src/scenario.d.ts +22 -0
- package/dist/src/scenario.js +35 -31
- package/dist/src/serve-http.d.ts +2 -1
- package/dist/src/serve-http.js +10 -1
- package/dist/src/serve.js +7 -4
- package/dist/src/signing.d.ts +23 -5
- package/dist/src/signing.js +74 -11
- package/dist/src/sigv4.d.ts +3 -1
- package/dist/src/sigv4.js +4 -2
- package/dist/src/smtp.js +2 -1
- package/dist/src/sockets.js +8 -3
- package/dist/src/state-system.d.ts +3 -0
- package/dist/src/storage.js +5 -2
- package/dist/src/twin-fetch.d.ts +3 -1
- package/dist/src/twin-fetch.js +3 -2
- package/dist/src/vendor-call.d.ts +10 -3
- package/dist/src/vendor-call.js +58 -14
- package/dist/test-fixtures/attestation.SOURCE.md +1 -0
- package/dist/test-fixtures/attestation.json +50 -0
- package/generated/pack-facts.json +409 -352
- package/inject.cjs +474 -95
- package/network-policy.cjs +108 -13
- package/network-policy.d.cts +8 -3
- package/package.json +11 -4
- package/src/actions.ts +2 -1
- package/src/anthropic-wire.ts +6 -0
- package/src/attestation.ts +23 -0
- package/src/bytes.ts +21 -0
- package/src/changeset.ts +2 -1
- package/src/clickhouse/sql.ts +50 -16
- package/src/derived-core.ts +573 -249
- package/src/derived-real.ts +552 -42
- package/src/derived.ts +23 -4
- package/src/events.ts +54 -85
- package/src/exact-json.ts +71 -0
- package/src/executor.ts +86 -27
- package/src/file-response.ts +1 -1
- package/src/form.ts +66 -0
- package/src/git/format.ts +89 -0
- package/src/git/index.ts +1 -0
- package/src/git/lfs.ts +5 -4
- package/src/graphql-wire.ts +8 -3
- package/src/grpc-wire.ts +2 -1
- package/src/head.ts +15 -5
- package/src/held-reads.ts +54 -0
- package/src/history.ts +5 -4
- package/src/host-port.ts +9 -0
- package/src/index.ts +10 -4
- package/src/log.ts +11 -7
- package/src/machines.ts +135 -13
- package/src/managed-database.ts +48 -40
- package/src/multipart.ts +110 -35
- package/src/observe.ts +6 -3
- package/src/openai-wire.ts +45 -14
- package/src/pack-fetch.ts +173 -50
- package/src/packRegistry.ts +27 -4
- package/src/private-transport.ts +8 -0
- package/src/protobuf.ts +8 -4
- package/src/rateBudget.ts +6 -1
- package/src/redis/engine.ts +168 -63
- package/src/redis/frames.ts +53 -0
- package/src/redis/index.ts +6 -0
- package/src/redis/json.ts +13 -0
- package/src/redis/lua-libs.ts +62 -0
- package/src/redis/lua.ts +20 -0
- package/src/redis/member-storage.ts +58 -0
- package/src/redis/resp2.ts +25 -0
- package/src/redis/stream.ts +102 -0
- package/src/remote-execute.ts +2 -2
- package/src/request-body.ts +12 -0
- package/src/runtime.ts +6 -5
- package/src/s3/wire.ts +8 -5
- package/src/scenario.ts +34 -13
- package/src/serve-http.ts +11 -2
- package/src/serve.ts +4 -3
- package/src/signing.ts +71 -17
- package/src/sigv4.ts +5 -3
- package/src/smtp.ts +3 -2
- package/src/sockets.ts +8 -3
- package/src/state-system.ts +3 -0
- package/src/storage.ts +5 -2
- package/src/twin-fetch.ts +4 -3
- package/src/vendor-call.ts +59 -12
- package/test-fixtures/attestation.json +50 -0
package/dist/src/scenario.js
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// vocabulary). See company-repo BRIEFS/TWIN-PROGRAMMING-MODEL.md (LOCKED, 2026-08-27).
|
|
3
|
-
//
|
|
4
|
-
// A HANDLER is an MSW-shaped data rule: { on, respond, once?, scope?, phase?, advancePhase? }.
|
|
5
|
-
// Handlers are evaluated IN ORDER; the FIRST handler whose `on` conditions ALL hold fires.
|
|
6
|
-
// No match → the caller serves its labeled deterministic stub and records the MISS (with the
|
|
7
|
-
// request's extracted features — the authoring signal). The handler FILE in the world dir is
|
|
8
|
-
// the only write surface; `engine.use(...)` exists for in-process tests only (LIFO over the
|
|
9
|
-
// baseline, removable). There are NO runtime write doors — a running world is never mutated.
|
|
10
|
-
//
|
|
11
|
-
// The GRAMMAR (structure, ordering, once/scope/phase, strict validation, extractors,
|
|
12
|
-
// placeholders, miss records) is this module's and identical for every vendor. The
|
|
13
|
-
// VOCABULARY (which `on` keys exist and how each matches; what `respond` may contain; which
|
|
14
|
-
// routes are stateful and therefore refuse success-shaped handlers) is the pack's, declared
|
|
15
|
-
// through a PackScenarioAdapter. Determinism: the engine is a pure state machine — same
|
|
16
|
-
// handler list + same request sequence → same decisions, byte for byte.
|
|
17
|
-
//
|
|
18
|
-
// STRICT EVERYWHERE (the gemini discipline): unknown top-level keys, unknown `on` keys,
|
|
19
|
-
// unknown placeholder names, malformed extractors — all THROW with the valid vocabulary in
|
|
20
|
-
// the message. A typo must fail loudly at load, never silently mis-match at serve.
|
|
1
|
+
import { exactInteger, compareExactNumbers, isExactIntegerValue } from "./exact-json.js";
|
|
21
2
|
export class ScenarioError extends Error {
|
|
22
3
|
}
|
|
23
4
|
// `$comment` is allowed (and ignored) at document and handler level — JSON has no comments
|
|
@@ -112,22 +93,38 @@ function parseHandler(raw, index, adapter, extractors) {
|
|
|
112
93
|
if (h.fault !== undefined) {
|
|
113
94
|
const f = h.fault;
|
|
114
95
|
if (typeof f !== "object" || f === null || Array.isArray(f))
|
|
115
|
-
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault is an object { kind: "slow" | "status" | "drop", ... }`);
|
|
116
|
-
const kinds = ["slow", "status", "drop"];
|
|
96
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault is an object { kind: "slow" | "status" | "drop" | "generation", ... }`);
|
|
97
|
+
const kinds = ["slow", "status", "drop", "generation"];
|
|
117
98
|
if (!kinds.includes(f.kind))
|
|
118
99
|
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.kind is one of ${kinds.join(", ")}, got ${JSON.stringify(f.kind)}`);
|
|
119
|
-
const keysFor = { slow: ["kind", "ms"], status: ["kind", "status", "retryAfterSeconds", "message"], drop: ["kind", "holdMs"] };
|
|
100
|
+
const keysFor = { slow: ["kind", "ms"], status: ["kind", "status", "retryAfterSeconds", "message", "metadata", "errorType"], generation: ["kind", "status", "message", "errorType", "metadata", "partialText", "event"], drop: ["kind", "holdMs"] };
|
|
120
101
|
for (const key of Object.keys(f))
|
|
121
102
|
if (!keysFor[f.kind].includes(key))
|
|
122
103
|
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.${key} is not a key of a ${String(f.kind)} fault (valid: ${keysFor[f.kind].join(", ")})`);
|
|
123
104
|
if (f.kind === "slow" && !(Number.isInteger(f.ms) && f.ms >= 0 && f.ms <= 600_000))
|
|
124
105
|
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.ms is an integer 0..600000 (milliseconds)`);
|
|
125
|
-
if (f.kind === "status" && !(Number.isInteger(f.status) && f.status >= 400 && f.status <= 599))
|
|
106
|
+
if ((f.kind === "status" || f.kind === "generation") && !(Number.isInteger(f.status) && f.status >= 400 && f.status <= 599))
|
|
126
107
|
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.status is an integer 400..599 — a fault is a refusal or a failure, never a success`);
|
|
127
108
|
if (f.kind === "status" && f.retryAfterSeconds !== undefined && !(Number.isInteger(f.retryAfterSeconds) && f.retryAfterSeconds >= 0))
|
|
128
109
|
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.retryAfterSeconds is a non-negative integer`);
|
|
129
110
|
if (f.kind === "status" && f.message !== undefined && typeof f.message !== "string")
|
|
130
111
|
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.message is a string`);
|
|
112
|
+
if ((f.kind === "status" || f.kind === "generation") && f.metadata !== undefined && (typeof f.metadata !== "object" || f.metadata === null || Array.isArray(f.metadata)))
|
|
113
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.metadata is an object`);
|
|
114
|
+
if (f.kind === "status" && f.errorType !== undefined && typeof f.errorType !== "string")
|
|
115
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.errorType is a string`);
|
|
116
|
+
if (f.kind === "generation") {
|
|
117
|
+
if (typeof f.message !== "string" || typeof f.errorType !== "string" || !f.errorType)
|
|
118
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: generation fault requires message and nonempty errorType`);
|
|
119
|
+
for (const key of ["partialText", "event"])
|
|
120
|
+
if (f[key] !== undefined && typeof f[key] !== "string")
|
|
121
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.${key} is a string`);
|
|
122
|
+
if (!adapter.validateGenerationFault)
|
|
123
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: generation faults require a compute-only adapter`);
|
|
124
|
+
const refusal = adapter.validateGenerationFault(f, h);
|
|
125
|
+
if (refusal)
|
|
126
|
+
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: ${refusal}`);
|
|
127
|
+
}
|
|
131
128
|
if (f.kind === "drop" && f.holdMs !== undefined && !(Number.isInteger(f.holdMs) && f.holdMs >= 0))
|
|
132
129
|
throw new ScenarioError(`${adapter.vendor} scenario: ${at}: fault.holdMs is a non-negative integer`);
|
|
133
130
|
if (f.kind !== "slow" && "respond" in h)
|
|
@@ -185,10 +182,11 @@ function applyTransform(value, transform, vendor, where) {
|
|
|
185
182
|
if (!transform)
|
|
186
183
|
return value;
|
|
187
184
|
const [op, rawN] = transform.slice(1).split(":");
|
|
188
|
-
if (typeof value !== "number")
|
|
185
|
+
if (typeof value !== "number" && !(typeof value === "string" && isExactIntegerValue(value)))
|
|
189
186
|
throw new ScenarioError(`${vendor} scenario: ${where} applies |${op}:${rawN} to a non-numeric extractor value ${JSON.stringify(value)}`);
|
|
190
|
-
const n = Number(rawN);
|
|
191
|
-
|
|
187
|
+
const n = /^-?\d+$/.test(rawN) ? exactInteger(rawN) : Number(rawN);
|
|
188
|
+
const cmp = compareExactNumbers(value, n);
|
|
189
|
+
return op === "min" ? (cmp <= 0 ? value : n) : (cmp >= 0 ? value : n);
|
|
192
190
|
}
|
|
193
191
|
const MISS_KEEP = 20;
|
|
194
192
|
/** The engine: pure state machine over registered handlers. One instance per twin server. */
|
|
@@ -322,7 +320,7 @@ export class ScenarioEngine {
|
|
|
322
320
|
throw new ScenarioError(`${this.adapter.vendor} scenario: placeholder "{{${name}}}": pattern did not match the request (the handler demanded a value the request never stated)`);
|
|
323
321
|
const captured = m[1] ?? m[0];
|
|
324
322
|
if (tp.as === "number") {
|
|
325
|
-
const n = Number(captured);
|
|
323
|
+
const n = /^-?\d+$/.test(captured) ? exactInteger(captured) : Number(captured);
|
|
326
324
|
if (Number.isNaN(n))
|
|
327
325
|
throw new ScenarioError(`${this.adapter.vendor} scenario: placeholder "{{${name}}}": captured "${captured}" is not a number`);
|
|
328
326
|
value = n;
|
|
@@ -375,6 +373,8 @@ function waitForScenarioFault(ms, signal) {
|
|
|
375
373
|
}
|
|
376
374
|
export async function scenarioFaultResult(fault, render, signal) {
|
|
377
375
|
throwIfScenarioAborted(signal);
|
|
376
|
+
if (fault.kind === "generation")
|
|
377
|
+
return null; // compute handler renders the admitted generation failure
|
|
378
378
|
if (fault.kind === "slow") {
|
|
379
379
|
await waitForScenarioFault(fault.ms, signal);
|
|
380
380
|
throwIfScenarioAborted(signal);
|
|
@@ -393,18 +393,22 @@ export async function scenarioFaultResult(fault, render, signal) {
|
|
|
393
393
|
return { status: fault.status, headers, body: rendered?.body ?? { error: { message: fault.message ?? `twin fault: ${fault.status}`, type: "twin_fault" } } };
|
|
394
394
|
}
|
|
395
395
|
export function statefulTwinManifest(input) {
|
|
396
|
+
// the World's doors the pack declares (what the vendor's own pages do that its API does not: issuing an account's key)
|
|
397
|
+
const doors = input.doors ?? [];
|
|
396
398
|
return {
|
|
397
399
|
twin: true,
|
|
398
400
|
vendor: input.vendor,
|
|
399
401
|
twinOf: input.twinOf,
|
|
400
402
|
program: {
|
|
401
|
-
state:
|
|
403
|
+
state: doors.length
|
|
404
|
+
? `Stateful: it stores ${input.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here; what the vendor does on its own pages (not its API) is a door of the World, listed under doors. There is no fixture language.`
|
|
405
|
+
: `Stateful: it stores ${input.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here — there is no fixture language and no write door besides the vendor's.`,
|
|
402
406
|
identity: input.identity ?? 'Authenticate as the vendor does; the twin accepts any non-sentinel credential.',
|
|
403
407
|
time: 'Writes are stamped from the WORLD CLOCK (volter-world clock <world> set/advance) — deterministic history is clock-set, seed, clock-advance.',
|
|
404
|
-
behavior: 'This twin is state, not scripting — answers are functions of what you seeded. Judgment/fault scripting lives on the scripted vendor twins (their GET /twin explains).',
|
|
408
|
+
behavior: input.behavior ?? 'This twin is state, not scripting — answers are functions of what you seeded. Judgment/fault scripting lives on the scripted vendor twins (their GET /twin explains).',
|
|
405
409
|
...(input.notes ? { notes: input.notes } : {}),
|
|
406
410
|
},
|
|
407
|
-
doors: { manifest: 'GET /twin' },
|
|
411
|
+
doors: { manifest: 'GET /twin', ...Object.fromEntries(doors.map((d) => [`${d.method} ${d.path}`, d.note ?? 'a door of the World'])) },
|
|
408
412
|
};
|
|
409
413
|
}
|
|
410
414
|
/** The GET /twin manifest — the discovery door's body. Education ships INSIDE the twin:
|
package/dist/src/serve-http.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TwinStream } from './twin-fetch.js';
|
|
2
|
+
export declare function connectWebSocket(url: string, headers: Record<string, string>, protocols?: string[]): Promise<WebSocket>;
|
|
2
3
|
export type HttpHandler = (request: Request) => Response | Promise<Response>;
|
|
3
4
|
export type WebSocketPeer = {
|
|
4
5
|
send: (data: string | Uint8Array) => unknown;
|
|
@@ -28,7 +29,7 @@ export type ServeHttpOptions = {
|
|
|
28
29
|
fetch: HttpHandler;
|
|
29
30
|
/** seconds a connection may idle (Bun honors it; Node's keep-alive timeout is set from it) */
|
|
30
31
|
idleTimeout?: number;
|
|
31
|
-
/**
|
|
32
|
+
/** Explicit transport limit in bytes; absent, Bun retains its 128 MiB default. */
|
|
32
33
|
maxRequestBodySize?: number;
|
|
33
34
|
/** what a thrown handler answers with; absent, a 500 carrying the error (as Bun.serve) */
|
|
34
35
|
error?: (error: Error) => Response | Promise<Response>;
|
package/dist/src/serve-http.js
CHANGED
|
@@ -7,6 +7,12 @@
|
|
|
7
7
|
// Nothing here runs at module scope, and `node:http` is reached through `process.getBuiltinModule`
|
|
8
8
|
// rather than a static import: this module rides into browser client bundles through the kernel's
|
|
9
9
|
// entrypoint, where a static `node:` import is the 2026-09-06 class of break.
|
|
10
|
+
/** One socket library at the host seam, shared by inbound upgrades and authenticated outbound relays. */
|
|
11
|
+
const socketLibrary = () => import('ws');
|
|
12
|
+
export async function connectWebSocket(url, headers, protocols = []) {
|
|
13
|
+
const { WebSocket: Client } = await socketLibrary();
|
|
14
|
+
return new Client(url, protocols, { headers });
|
|
15
|
+
}
|
|
10
16
|
/** A decorator the request journal installs: every server made through the seam serves through it. */
|
|
11
17
|
let decorate;
|
|
12
18
|
export function setServeDecorator(fn) { decorate = fn; }
|
|
@@ -18,6 +24,9 @@ export async function serveHttp(options) {
|
|
|
18
24
|
// twin from a leftover World's that holds the same port. Outside a World nothing changes.
|
|
19
25
|
// a pack's fetch carries the upgrade its sockets take (pack-fetch.ts), read before anything wraps the fetch: served on
|
|
20
26
|
// its own or in a World, its sockets answer
|
|
27
|
+
const declaredCap = options.fetch.maxRequestBodySize;
|
|
28
|
+
if (options.maxRequestBodySize === undefined && declaredCap !== undefined)
|
|
29
|
+
options = { ...options, maxRequestBodySize: declaredCap };
|
|
21
30
|
const carried = options.upgrade ?? options.fetch.upgrade;
|
|
22
31
|
if (carried)
|
|
23
32
|
options = { ...options, upgrade: carried };
|
|
@@ -327,7 +336,7 @@ async function serveOnNode(options) {
|
|
|
327
336
|
});
|
|
328
337
|
let socketServer;
|
|
329
338
|
if (options.upgrade) {
|
|
330
|
-
const { WebSocketServer } = await
|
|
339
|
+
const { WebSocketServer } = await socketLibrary();
|
|
331
340
|
const upgrade = options.upgrade;
|
|
332
341
|
const requestOf = (req) => new Request(`${protocol}://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, { headers: Object.fromEntries(Object.entries(req.headers).filter((entry) => typeof entry[1] === 'string')) });
|
|
333
342
|
// the subprotocol the socket selects, answered as Bun's branch answers it
|
package/dist/src/serve.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseExactJson } from "./exact-json.js";
|
|
1
2
|
// Twin serve layer (the twins architecture notes, increment 1): answer vendor-shaped
|
|
2
3
|
// READS from the local event-sourced state — the "local twin" read path. An app
|
|
3
4
|
// or agent points at this instead of the real vendor; it responds from local
|
|
@@ -56,7 +57,7 @@ export function readTwinRequestJournal(service, root, options = {}) {
|
|
|
56
57
|
if (!line)
|
|
57
58
|
continue;
|
|
58
59
|
try {
|
|
59
|
-
rows.push(
|
|
60
|
+
rows.push(parseExactJson(line));
|
|
60
61
|
}
|
|
61
62
|
catch { /* a torn line */ }
|
|
62
63
|
}
|
|
@@ -107,7 +108,7 @@ export function readTwinRequestJournalFrom(service, root, cursor, options = {})
|
|
|
107
108
|
if (!line)
|
|
108
109
|
continue;
|
|
109
110
|
try {
|
|
110
|
-
entries.push(
|
|
111
|
+
entries.push(parseExactJson(line));
|
|
111
112
|
}
|
|
112
113
|
catch { /* a torn line */ }
|
|
113
114
|
}
|
|
@@ -363,8 +364,10 @@ function journalingFetch(config, callSiteVendor) {
|
|
|
363
364
|
const response = (await identitySlot().run(slot, () => inner.call(this, request, server)));
|
|
364
365
|
if (response instanceof Response)
|
|
365
366
|
status = response.status;
|
|
366
|
-
else if (response === undefined)
|
|
367
|
-
|
|
367
|
+
else if (response === undefined) {
|
|
368
|
+
status = 101;
|
|
369
|
+
return response;
|
|
370
|
+
} // accepted WebSocket upgrade
|
|
368
371
|
return response;
|
|
369
372
|
}
|
|
370
373
|
finally {
|
package/dist/src/signing.d.ts
CHANGED
|
@@ -7,14 +7,22 @@ export type Jwk = {
|
|
|
7
7
|
kid: string;
|
|
8
8
|
n: string;
|
|
9
9
|
e: string;
|
|
10
|
+
} | {
|
|
11
|
+
kty: 'OKP';
|
|
12
|
+
use: 'sig';
|
|
13
|
+
alg: 'EdDSA';
|
|
14
|
+
crv: 'Ed25519';
|
|
15
|
+
kid: string;
|
|
16
|
+
x: string;
|
|
10
17
|
};
|
|
11
18
|
export type Jwks = {
|
|
12
19
|
keys: Jwk[];
|
|
13
20
|
};
|
|
14
|
-
/** A signing key: an RSA private key as a PEM (its public half, given or derived, is what a JWKS serves and
|
|
15
|
-
* `kid`), or a shared secret. Only deterministic algorithms: an ECDSA signature is random, so a World signs
|
|
21
|
+
/** A signing key: an RSA or Ed25519 private key as a PEM (its public half, given or derived, is what a JWKS serves and
|
|
22
|
+
* names by `kid`), or a shared secret. Only deterministic algorithms: an ECDSA signature is random, so a World signs
|
|
23
|
+
* none; Ed25519's is deterministic (RFC 8032), Better Auth's default (`EdDSA`). */
|
|
16
24
|
export type SigningKey = {
|
|
17
|
-
alg: 'RS256' | 'RS384' | 'RS512';
|
|
25
|
+
alg: 'RS256' | 'RS384' | 'RS512' | 'EdDSA';
|
|
18
26
|
privatePem: string;
|
|
19
27
|
publicPem?: string;
|
|
20
28
|
} | {
|
|
@@ -32,18 +40,19 @@ export type JwtVerdict = {
|
|
|
32
40
|
/** A public key's id: a prefix and the first 16 hex of SHA-256 over its DER (what its JWKS and every token name). */
|
|
33
41
|
export declare function keyId(publicPem: string, prefix?: string): string;
|
|
34
42
|
/** A JWT: `iat` and `nbf` are `now` (seconds, the World's), `exp` `now` plus the lifetime (60 s when none is given),
|
|
35
|
-
* unless the claims name their own.
|
|
43
|
+
* unless the claims name their own. A token of a key pair carries the key's `kid`. */
|
|
36
44
|
export declare function jwtSign(claims: Row, key: SigningKey, opts: {
|
|
37
45
|
now: number;
|
|
38
46
|
expiresInSeconds?: number;
|
|
39
47
|
kidPrefix?: string;
|
|
48
|
+
typ?: string;
|
|
40
49
|
}): string;
|
|
41
50
|
/** A JWT's header and payload, unverified. */
|
|
42
51
|
export declare function jwtDecode(token: string): {
|
|
43
52
|
header: Row;
|
|
44
53
|
payload: Row;
|
|
45
54
|
};
|
|
46
|
-
/** A token checked: an RS256 one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
|
|
55
|
+
/** A token checked: an RS256 or EdDSA one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
|
|
47
56
|
* then its `exp`/`nbf` at `now` (seconds). */
|
|
48
57
|
export declare function jwtVerify(token: string, key: Jwks | {
|
|
49
58
|
alg: 'HS256' | 'HS384' | 'HS512';
|
|
@@ -79,6 +88,9 @@ export declare const md5: (value: string | Uint8Array) => string;
|
|
|
79
88
|
export declare function uuidFrom(seed: string): string;
|
|
80
89
|
/** `count` lowercase letters derived from `seed` (a 20-letter project ref, a slug's suffix), the same every run. */
|
|
81
90
|
export declare function lettersFrom(seed: string, count?: number): string;
|
|
91
|
+
/** `count` base62 characters (0-9A-Za-z) derived from `seed`, the same every run: the opaque tail of an id a vendor
|
|
92
|
+
* writes in base62 (OpenAI's `call_unLAR8MvFNptuiZK6K6HCy5k`, 24 after its prefix). */
|
|
93
|
+
export declare function base62From(seed: string, count?: number): string;
|
|
82
94
|
/** Two strings compared in constant time. */
|
|
83
95
|
export declare function equalSecrets(a: string, b: string): boolean;
|
|
84
96
|
/** Bytes signed with a private key (RSA with SHA-256, or Ed25519), in the encoding asked: a certificate's signature,
|
|
@@ -123,6 +135,7 @@ export declare const handlerCrypto: {
|
|
|
123
135
|
readonly digest: typeof digest;
|
|
124
136
|
readonly uuidFrom: typeof uuidFrom;
|
|
125
137
|
readonly lettersFrom: typeof lettersFrom;
|
|
138
|
+
readonly base62From: typeof base62From;
|
|
126
139
|
readonly equalSecrets: typeof equalSecrets;
|
|
127
140
|
readonly sealedBoxKeyPair: typeof sealedBoxKeyPair;
|
|
128
141
|
readonly sealedBoxOpen: typeof sealedBoxOpen;
|
|
@@ -132,4 +145,9 @@ export declare const handlerCrypto: {
|
|
|
132
145
|
readonly certificateOf: typeof certificateOf;
|
|
133
146
|
};
|
|
134
147
|
export type HandlerCrypto = typeof handlerCrypto;
|
|
148
|
+
/** A [KSUID](https://github.com/segmentio/ksuid) as 27 base62 characters (0-9A-Za-z, most significant first): `seconds`
|
|
149
|
+
* past KSUID's epoch (1400000000) in four bytes, then `n` in four, then twelve bytes derived from `seed`, so KSUIDs
|
|
150
|
+
* made in one second sort by `n`, at `width` characters (27 unless a vendor's id is longer or shorter). Deterministic:
|
|
151
|
+
* the same arguments give the same id. */
|
|
152
|
+
export declare function ksuidFrom(seconds: number, n: number, seed: string, width?: number): string;
|
|
135
153
|
export {};
|
package/dist/src/signing.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { parseExactJson, compareExactNumbers } from "./exact-json.js";
|
|
1
2
|
// SIGNING ON THE CONTEXT (docs/contributing/architecture.md, "What an author writes, and how": the tokens-and-keys row)
|
|
2
3
|
// — one deterministic implementation of what vendors sign with, given to handlers as `ctx.crypto`, so no pack reaches
|
|
3
|
-
// `node:crypto` itself: JWTs (RS256 with a pack's instance key, HS256 with a shared secret), their verification, a
|
|
4
|
+
// `node:crypto` itself: JWTs (RS256 or EdDSA with a pack's instance key, HS256 with a shared secret), their verification, a
|
|
4
5
|
// JWKS for a public key, an HMAC and a SHA-256. Nothing here is random: a key is the pack's data (a PEM it declares),
|
|
5
6
|
// and every value a signature carries comes from the call.
|
|
6
7
|
// A namespace, never named imports: a browser client bundles the kernel, and the browser's `node:crypto` polyfill has no
|
|
@@ -8,7 +9,7 @@
|
|
|
8
9
|
import * as nodeCrypto from 'node:crypto';
|
|
9
10
|
import { sealedBoxKeyPair, sealedBoxOpen } from "./sealed-box.js";
|
|
10
11
|
/** The algorithms `jwtSign` signs with. */
|
|
11
|
-
export const SIGNING_ALGORITHMS = ['RS256', 'RS384', 'RS512', 'HS256', 'HS384', 'HS512'];
|
|
12
|
+
export const SIGNING_ALGORITHMS = ['RS256', 'RS384', 'RS512', 'EdDSA', 'HS256', 'HS384', 'HS512'];
|
|
12
13
|
const digestOf = (alg) => `sha${alg.slice(2)}`;
|
|
13
14
|
const base64url = (input) => Buffer.from(input).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
14
15
|
const fromBase64url = (s) => Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
@@ -19,15 +20,24 @@ export function keyId(publicPem, prefix = 'ins_') {
|
|
|
19
20
|
return `${prefix}${nodeCrypto.createHash('sha256').update(der).digest('hex').slice(0, 16)}`;
|
|
20
21
|
}
|
|
21
22
|
/** A JWT: `iat` and `nbf` are `now` (seconds, the World's), `exp` `now` plus the lifetime (60 s when none is given),
|
|
22
|
-
* unless the claims name their own.
|
|
23
|
+
* unless the claims name their own. A token of a key pair carries the key's `kid`. */
|
|
23
24
|
export function jwtSign(claims, key, opts) {
|
|
25
|
+
// a key of another kind than its algorithm's is refused by name (an RSA key under EdDSA would fail inside the signer)
|
|
26
|
+
if ('privatePem' in key) {
|
|
27
|
+
const kind = nodeCrypto.createPrivateKey(key.privatePem).asymmetricKeyType;
|
|
28
|
+
const wants = key.alg === 'EdDSA' ? 'ed25519' : 'rsa';
|
|
29
|
+
if (kind !== wants)
|
|
30
|
+
throw new Error(`jwtSign: ${key.alg} signs with an ${wants === 'rsa' ? 'RSA' : 'Ed25519'} key, and this key is ${kind ?? 'of no known type'}`);
|
|
31
|
+
}
|
|
24
32
|
const header = 'privatePem' in key
|
|
25
|
-
? { alg: key.alg, typ: 'JWT', kid: keyId(key.publicPem ?? nodeCrypto.createPublicKey(key.privatePem).export({ type: 'spki', format: 'pem' }).toString(), opts.kidPrefix) }
|
|
26
|
-
: { alg: key.alg, typ: 'JWT' };
|
|
33
|
+
? { alg: key.alg, typ: opts.typ ?? 'JWT', kid: keyId(key.publicPem ?? nodeCrypto.createPublicKey(key.privatePem).export({ type: 'spki', format: 'pem' }).toString(), opts.kidPrefix) }
|
|
34
|
+
: { alg: key.alg, typ: opts.typ ?? 'JWT' };
|
|
27
35
|
const payload = { iat: opts.now, exp: opts.now + (opts.expiresInSeconds ?? 60), nbf: opts.now, ...claims };
|
|
28
36
|
const input = `${jsonPart(header)}.${jsonPart(payload)}`;
|
|
29
37
|
if ('secret' in key)
|
|
30
38
|
return `${input}.${base64url(nodeCrypto.createHmac(digestOf(key.alg), key.secret).update(input).digest())}`;
|
|
39
|
+
if (key.alg === 'EdDSA')
|
|
40
|
+
return `${input}.${base64url(nodeCrypto.sign(null, Buffer.from(input), key.privatePem))}`;
|
|
31
41
|
const signer = nodeCrypto.createSign(`RSA-SHA${key.alg.slice(2)}`);
|
|
32
42
|
signer.update(input);
|
|
33
43
|
signer.end();
|
|
@@ -38,17 +48,17 @@ export function jwtDecode(token) {
|
|
|
38
48
|
const parts = token.split('.');
|
|
39
49
|
if (parts.length !== 3)
|
|
40
50
|
throw new Error('malformed jwt');
|
|
41
|
-
const part = (s) =>
|
|
51
|
+
const part = (s) => parseExactJson(fromBase64url(s).toString('utf8'));
|
|
42
52
|
return { header: part(parts[0]), payload: part(parts[1]) };
|
|
43
53
|
}
|
|
44
54
|
function timely(payload, now) {
|
|
45
|
-
if (typeof payload.exp === 'number' && now
|
|
55
|
+
if ((typeof payload.exp === 'number' || typeof payload.exp === 'string' && /^-?\d+$/.test(payload.exp)) && compareExactNumbers(now, payload.exp) >= 0)
|
|
46
56
|
return { valid: false, reason: 'expired' };
|
|
47
|
-
if (typeof payload.nbf === 'number' && now
|
|
57
|
+
if ((typeof payload.nbf === 'number' || typeof payload.nbf === 'string' && /^-?\d+$/.test(payload.nbf)) && compareExactNumbers(now, payload.nbf) < 0)
|
|
48
58
|
return { valid: false, reason: 'not_yet_valid' };
|
|
49
59
|
return { valid: true, payload };
|
|
50
60
|
}
|
|
51
|
-
/** A token checked: an RS256 one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
|
|
61
|
+
/** A token checked: an RS256 or EdDSA one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
|
|
52
62
|
* then its `exp`/`nbf` at `now` (seconds). */
|
|
53
63
|
export function jwtVerify(token, key, opts) {
|
|
54
64
|
let decoded;
|
|
@@ -68,12 +78,20 @@ export function jwtVerify(token, key, opts) {
|
|
|
68
78
|
return { valid: false, reason: 'bad_signature' };
|
|
69
79
|
return timely(decoded.payload, opts.now);
|
|
70
80
|
}
|
|
71
|
-
if (!['RS256', 'RS384', 'RS512'].includes(String(decoded.header.alg)))
|
|
81
|
+
if (!['RS256', 'RS384', 'RS512', 'EdDSA'].includes(String(decoded.header.alg)))
|
|
72
82
|
return { valid: false, reason: 'unexpected_alg' };
|
|
73
83
|
// a token naming a key the set does not hold matches none; one naming no key is tried against the set's only key
|
|
74
84
|
const jwk = decoded.header.kid !== undefined ? key.keys.find((k) => k.kid === decoded.header.kid) : key.keys.length === 1 ? key.keys[0] : undefined;
|
|
75
85
|
if (!jwk)
|
|
76
86
|
return { valid: false, reason: 'no_matching_key' };
|
|
87
|
+
if ((jwk.kty === 'OKP') !== (decoded.header.alg === 'EdDSA'))
|
|
88
|
+
return { valid: false, reason: 'unexpected_alg' };
|
|
89
|
+
if (jwk.kty === 'OKP') {
|
|
90
|
+
const edKey = nodeCrypto.createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: jwk.x }, format: 'jwk' });
|
|
91
|
+
if (!nodeCrypto.verify(null, Buffer.from(`${h}.${p}`), edKey, fromBase64url(s)))
|
|
92
|
+
return { valid: false, reason: 'bad_signature' };
|
|
93
|
+
return timely(decoded.payload, opts.now);
|
|
94
|
+
}
|
|
77
95
|
const verifier = nodeCrypto.createVerify(`RSA-SHA${String(decoded.header.alg).slice(2)}`);
|
|
78
96
|
verifier.update(`${h}.${p}`);
|
|
79
97
|
verifier.end();
|
|
@@ -85,6 +103,8 @@ export function jwtVerify(token, key, opts) {
|
|
|
85
103
|
/** The JWKS that verifies every token a public key's pair signs. */
|
|
86
104
|
export function jwks(publicPem, kidPrefix) {
|
|
87
105
|
const jwk = nodeCrypto.createPublicKey(publicPem).export({ format: 'jwk' });
|
|
106
|
+
if (jwk.kty === 'OKP')
|
|
107
|
+
return { keys: [{ kty: 'OKP', use: 'sig', alg: 'EdDSA', crv: 'Ed25519', kid: keyId(publicPem, kidPrefix), x: String(jwk.x) }] };
|
|
88
108
|
return { keys: [{ kty: 'RSA', use: 'sig', alg: 'RS256', kid: keyId(publicPem, kidPrefix), n: String(jwk.n), e: String(jwk.e) }] };
|
|
89
109
|
}
|
|
90
110
|
/** An HMAC of a value, hex or base64 (a webhook's signature, a signed id): SHA-256 unless the vendor names another
|
|
@@ -158,6 +178,23 @@ export function lettersFrom(seed, count = 20) {
|
|
|
158
178
|
}
|
|
159
179
|
return out;
|
|
160
180
|
}
|
|
181
|
+
/** `count` base62 characters (0-9A-Za-z) derived from `seed`, the same every run: the opaque tail of an id a vendor
|
|
182
|
+
* writes in base62 (OpenAI's `call_unLAR8MvFNptuiZK6K6HCy5k`, 24 after its prefix). */
|
|
183
|
+
export function base62From(seed, count = 24) {
|
|
184
|
+
const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
185
|
+
let out = '';
|
|
186
|
+
for (let round = 0; out.length < count; round += 1) {
|
|
187
|
+
// each byte below 248 (4 x 62) maps evenly onto the alphabet; the rest are skipped
|
|
188
|
+
for (const byte of sha256(`${seed}:${round}`).match(/../g)) {
|
|
189
|
+
if (out.length === count)
|
|
190
|
+
break;
|
|
191
|
+
const b = Number.parseInt(byte, 16);
|
|
192
|
+
if (b < 248)
|
|
193
|
+
out += ALPHABET[b % 62];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
161
198
|
/** Two strings compared in constant time. */
|
|
162
199
|
export function equalSecrets(a, b) {
|
|
163
200
|
const left = Buffer.from(a, 'utf8');
|
|
@@ -219,4 +256,30 @@ export function certificateOf(cert) {
|
|
|
219
256
|
}
|
|
220
257
|
}
|
|
221
258
|
/** What a handler signs and hashes with: `ctx.crypto`. */
|
|
222
|
-
export const handlerCrypto = { jwtSign, jwtVerify, jwtDecode, jwks, keyId, hmac, sha256, md5, digest, uuidFrom, lettersFrom, equalSecrets, sealedBoxKeyPair, sealedBoxOpen, signWith, verifyWith, publicKeyOf, certificateOf };
|
|
259
|
+
export const handlerCrypto = { jwtSign, jwtVerify, jwtDecode, jwks, keyId, hmac, sha256, md5, digest, uuidFrom, lettersFrom, base62From, equalSecrets, sealedBoxKeyPair, sealedBoxOpen, signWith, verifyWith, publicKeyOf, certificateOf };
|
|
260
|
+
/** A [KSUID](https://github.com/segmentio/ksuid) as 27 base62 characters (0-9A-Za-z, most significant first): `seconds`
|
|
261
|
+
* past KSUID's epoch (1400000000) in four bytes, then `n` in four, then twelve bytes derived from `seed`, so KSUIDs
|
|
262
|
+
* made in one second sort by `n`, at `width` characters (27 unless a vendor's id is longer or shorter). Deterministic:
|
|
263
|
+
* the same arguments give the same id. */
|
|
264
|
+
export function ksuidFrom(seconds, n, seed, width = 27) {
|
|
265
|
+
const bytes = new Uint8Array(20);
|
|
266
|
+
const view = new DataView(bytes.buffer);
|
|
267
|
+
view.setUint32(0, Math.max(0, seconds - 1_400_000_000) >>> 0);
|
|
268
|
+
view.setUint32(4, n >>> 0);
|
|
269
|
+
const tail = sha256(seed);
|
|
270
|
+
for (let i = 0; i < 12; i++)
|
|
271
|
+
bytes[8 + i] = parseInt(tail.slice(i * 2, i * 2 + 2), 16);
|
|
272
|
+
const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
273
|
+
let value = 0n;
|
|
274
|
+
for (const b of bytes)
|
|
275
|
+
value = (value << 8n) | BigInt(b);
|
|
276
|
+
let out = '';
|
|
277
|
+
while (value > 0n) {
|
|
278
|
+
out = ALPHABET[Number(value % 62n)] + out;
|
|
279
|
+
value /= 62n;
|
|
280
|
+
}
|
|
281
|
+
const full = out.padStart(27, '0');
|
|
282
|
+
// another width than KSUID's 27 (a vendor's id of 24 or 29 characters): its first `width` characters, which keep the
|
|
283
|
+
// instant and the mint count (and so the order) from 11 on, or more base62 characters derived from the seed after them
|
|
284
|
+
return width <= 27 ? full.slice(0, width) : `${full}${base62From(`${seed}:ksuid`, width - 27)}`;
|
|
285
|
+
}
|
package/dist/src/sigv4.d.ts
CHANGED
|
@@ -37,7 +37,8 @@ export declare function signSigV4(input: {
|
|
|
37
37
|
/**
|
|
38
38
|
* A presigned URL signed again with a key: its date, expiry, region, service and signed headers are the URL's own
|
|
39
39
|
* (X-Amz-Date, X-Amz-Expires, X-Amz-Credential's scope, X-Amz-SignedHeaders, `host` when it names none), its key and
|
|
40
|
-
* signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it.
|
|
40
|
+
* signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it. A
|
|
41
|
+
* signed header other than `host` (a Content-Type the URL binds) is signed with the value the request sends (`headers`).
|
|
41
42
|
*/
|
|
42
43
|
export declare function presignSigV4(input: {
|
|
43
44
|
method: string;
|
|
@@ -45,4 +46,5 @@ export declare function presignSigV4(input: {
|
|
|
45
46
|
keyId: string;
|
|
46
47
|
secret: string;
|
|
47
48
|
host?: string;
|
|
49
|
+
headers?: Record<string, string>;
|
|
48
50
|
}): string;
|
package/dist/src/sigv4.js
CHANGED
|
@@ -136,7 +136,8 @@ export function signSigV4(input) {
|
|
|
136
136
|
/**
|
|
137
137
|
* A presigned URL signed again with a key: its date, expiry, region, service and signed headers are the URL's own
|
|
138
138
|
* (X-Amz-Date, X-Amz-Expires, X-Amz-Credential's scope, X-Amz-SignedHeaders, `host` when it names none), its key and
|
|
139
|
-
* signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it.
|
|
139
|
+
* signature this key's. What an SDK's getSignedUrl makes, for a walk that wrote the URL as its client printed it. A
|
|
140
|
+
* signed header other than `host` (a Content-Type the URL binds) is signed with the value the request sends (`headers`).
|
|
140
141
|
*/
|
|
141
142
|
export function presignSigV4(input) {
|
|
142
143
|
const url = new URL(input.url);
|
|
@@ -156,7 +157,8 @@ export function presignSigV4(input) {
|
|
|
156
157
|
.map(([k, v]) => `${k}=${v}`)
|
|
157
158
|
.join('&');
|
|
158
159
|
const names = (q.get('X-Amz-SignedHeaders') ?? 'host').toLowerCase().split(';').filter(Boolean);
|
|
159
|
-
const
|
|
160
|
+
const sent = new Headers(input.headers ?? {});
|
|
161
|
+
const canonicalHeaders = names.map((n) => `${n}:${(n === 'host' ? (input.host ?? url.host) : sent.get(n) ?? '').trim().replace(/\s+/g, ' ')}\n`).join('');
|
|
160
162
|
const canonicalRequest = [input.method.toUpperCase(), canonicalUri, canonicalQuery, canonicalHeaders, names.join(';'), 'UNSIGNED-PAYLOAD'].join('\n');
|
|
161
163
|
const stringToSign = ['AWS4-HMAC-SHA256', amzDate, `${dateStamp}/${region}/${service}/aws4_request`, sha256Hex(canonicalRequest)].join('\n');
|
|
162
164
|
let key = hmac(`AWS4${input.secret}`, dateStamp);
|
package/dist/src/smtp.js
CHANGED
|
@@ -40,7 +40,8 @@ export async function sendMail(route, mail, headers = {}) {
|
|
|
40
40
|
import('node:net'),
|
|
41
41
|
import('../network-policy.cjs'),
|
|
42
42
|
]);
|
|
43
|
-
|
|
43
|
+
// sending mail is a write: an external relay needs a `writes` grant
|
|
44
|
+
const refusal = worldEgressRefusal(`http://${route.host}:${route.port}/`, process.env, 'SMTP');
|
|
44
45
|
if (refusal !== null)
|
|
45
46
|
throw new Error(refusal);
|
|
46
47
|
const socket = connect(route.port, route.host);
|
package/dist/src/sockets.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const sessions = new Map();
|
|
2
|
-
|
|
2
|
+
/** A World's next session number, per World (its service and root): a session's id is the World's, never the
|
|
3
|
+
* process's, so a life replayed on a fresh World numbers its sessions as it did the first time. */
|
|
4
|
+
const nextSession = new Map();
|
|
3
5
|
const keyOf = (service, root) => `${service}\0${root ?? ''}`;
|
|
4
6
|
/** The open sessions of one socket of the vendor in a World (a handler reaches them as `ctx.sockets(id)`). */
|
|
5
7
|
export function openSessions(service, root, socket) {
|
|
@@ -40,7 +42,7 @@ export function socketUpgrade(service, root, decls, engines, context, hostOf, pa
|
|
|
40
42
|
const engine = engines[decl.id];
|
|
41
43
|
const pending = new Map();
|
|
42
44
|
const session = {
|
|
43
|
-
id: nextSession
|
|
45
|
+
id: (() => { const n = (nextSession.get(key) ?? 0) + 1; nextSession.set(key, n); return n; })(), socket: decl.id, state: {},
|
|
44
46
|
send: (frame) => { peer.send(typeof frame === 'string' || frame instanceof Uint8Array ? frame : JSON.stringify(frame)); },
|
|
45
47
|
close: (code, reason) => { peer.close(code, reason); },
|
|
46
48
|
expect: (key, ms) => new Promise((resolve, reject) => {
|
|
@@ -83,8 +85,11 @@ export function socketUpgrade(service, root, decls, engines, context, hostOf, pa
|
|
|
83
85
|
o.pending.delete(k);
|
|
84
86
|
}
|
|
85
87
|
// the end is written after what the session is still doing, over its own context
|
|
88
|
+
// the session is gone, so a failure has no frame to go in: it is reported, never swallowed
|
|
86
89
|
if (o.engine.close)
|
|
87
|
-
o.queue = o.queue.then(async () => o.engine.close(o.session, await o.context())).catch(() => {
|
|
90
|
+
o.queue = o.queue.then(async () => o.engine.close(o.session, await o.context())).catch((error) => {
|
|
91
|
+
console.error(`${service}: the ${o.session.socket} socket's end was not written: ${String(error?.message ?? error)}`);
|
|
92
|
+
});
|
|
88
93
|
},
|
|
89
94
|
};
|
|
90
95
|
}
|
|
@@ -38,6 +38,9 @@ export type StateSystemAdapters = {
|
|
|
38
38
|
root?: string;
|
|
39
39
|
secret?: string;
|
|
40
40
|
}) => Response | Promise<Response>;
|
|
41
|
+
/** Why a recorded write is the World's own and sent nowhere, or undefined when perform sends it: a unit of the pack that
|
|
42
|
+
* declares `vendorBacked.none` beside lanes that are vendor-backed (PlanetScale's SQL wire beside its API lane). */
|
|
43
|
+
unsent?: (input: Record<string, unknown> | undefined) => string | undefined;
|
|
41
44
|
};
|
|
42
45
|
/** Register a twin's adapters (the mounting host does this from the pack's descriptor or its
|
|
43
46
|
* anchored exports). Replaces any earlier registration for the service. */
|
package/dist/src/storage.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseExactJson } from "./exact-json.js";
|
|
1
2
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
2
3
|
import { withStateLock, withStateRemoval } from "./ancestry.js";
|
|
3
4
|
import { appendParentEntry, toEntry } from "./log.js";
|
|
@@ -121,7 +122,7 @@ function readJsonl(path) {
|
|
|
121
122
|
if (!line.trim())
|
|
122
123
|
continue;
|
|
123
124
|
try {
|
|
124
|
-
rows.push(
|
|
125
|
+
rows.push(parseExactJson(line));
|
|
125
126
|
}
|
|
126
127
|
catch (error) {
|
|
127
128
|
throw new Error(`${path}:${index + 1}: invalid JSONL row: ${error.message}`);
|
|
@@ -137,7 +138,7 @@ export function readJsonFile(path) {
|
|
|
137
138
|
const raw = getActiveWorldStore().read(path);
|
|
138
139
|
if (raw === null)
|
|
139
140
|
throw new Error('no such file');
|
|
140
|
-
return
|
|
141
|
+
return parseExactJson(raw);
|
|
141
142
|
}
|
|
142
143
|
catch (error) {
|
|
143
144
|
throw new Error(`${path}: invalid JSON: ${error.message}`);
|
|
@@ -292,6 +293,8 @@ export function createEvent(input) {
|
|
|
292
293
|
* one of them so scrubService never demands --force for an ordinary twin. */
|
|
293
294
|
const KNOWN_SERVICE_ENTRIES = new Set([
|
|
294
295
|
'ancestry.json', 'history-format.json', 'views', 'origin.jsonl', 'origin-head.json',
|
|
296
|
+
'answer-count',
|
|
297
|
+
'answer-count.lock',
|
|
295
298
|
'events.jsonl',
|
|
296
299
|
'events.jsonl.lock',
|
|
297
300
|
'event-queue.jsonl',
|
package/dist/src/twin-fetch.d.ts
CHANGED
|
@@ -50,12 +50,14 @@ export type TwinStream = (sink: TwinStreamSink, peer: string) => TwinStreamConne
|
|
|
50
50
|
export declare const TWIN_REQUEST_SCOPES: readonly string[];
|
|
51
51
|
/**
|
|
52
52
|
* THE READ SCOPE for a pack that writes its own fetch (the derived packs, the byte-wire packs): a
|
|
53
|
-
* request carrying the read-only marker runs with the kernel's
|
|
53
|
+
* request carrying the read-only marker, or an instance mounted read-only, runs with the kernel's
|
|
54
|
+
* write seam refusing its writes, and a
|
|
54
55
|
* refused write answers `refuse` (the vendor's own read-only error; a generic 405 without one) —
|
|
55
56
|
* whatever the handler made of the refusal. `GET /twin` advertises `requestScopes`. Everything else
|
|
56
57
|
* passes through untouched; the wrapped fetch keeps its own properties (a derived fetch's `owners`).
|
|
57
58
|
*/
|
|
58
59
|
export declare function withRequestScopes<F extends (request: Request) => Promise<Response>>(fetch: F, opts?: {
|
|
60
|
+
readOnly?: boolean;
|
|
59
61
|
refuse?: (request: Request, error: ReadOnlyRequestError) => Response | Promise<Response>;
|
|
60
62
|
}): F;
|
|
61
63
|
export type TwinFetchHandlerResult = {
|
package/dist/src/twin-fetch.js
CHANGED
|
@@ -130,7 +130,8 @@ function readOnlyRefusal(error) {
|
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
132
|
* THE READ SCOPE for a pack that writes its own fetch (the derived packs, the byte-wire packs): a
|
|
133
|
-
* request carrying the read-only marker runs with the kernel's
|
|
133
|
+
* request carrying the read-only marker, or an instance mounted read-only, runs with the kernel's
|
|
134
|
+
* write seam refusing its writes, and a
|
|
134
135
|
* refused write answers `refuse` (the vendor's own read-only error; a generic 405 without one) —
|
|
135
136
|
* whatever the handler made of the refusal. `GET /twin` advertises `requestScopes`. Everything else
|
|
136
137
|
* passes through untouched; the wrapped fetch keeps its own properties (a derived fetch's `owners`).
|
|
@@ -149,7 +150,7 @@ export function withRequestScopes(fetch, opts = {}) {
|
|
|
149
150
|
// the request's W3C traceparent follows every entry the fetch writes, as through the kernel's own adapters
|
|
150
151
|
// (a custom fetch never entered it, so the timeline's trace filter missed its entries)
|
|
151
152
|
return runWithRequestTrace(request, async () => {
|
|
152
|
-
if (!isReadOnlyRequest(request))
|
|
153
|
+
if (!opts.readOnly && !isReadOnlyRequest(request))
|
|
153
154
|
return fetch(request);
|
|
154
155
|
const out = await runAsReadOnlyRequest(() => fetch(request));
|
|
155
156
|
if (!out.refused)
|
|
@@ -1,6 +1,13 @@
|
|
|
1
|
+
import type { ApplicationStandIn } from './events.js';
|
|
1
2
|
/** A call to a vendor the World runs no twin of. */
|
|
2
3
|
export declare class VendorUnreachableError extends Error {
|
|
3
|
-
constructor(message: string);
|
|
4
|
+
constructor(message: string, options?: ErrorOptions);
|
|
4
5
|
}
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
6
|
+
/** A twin answered in this process: its fetch, as a World's server hands it a request. */
|
|
7
|
+
export type InProcessTwin = (request: Request) => Response | Promise<Response>;
|
|
8
|
+
/** The twins this process answers itself, by routing key (a key of VENDOR_HOSTS: a journey walk's companions, over the
|
|
9
|
+
* walk's own World), consulted before the instance's; returns those set before, which the caller puts back. */
|
|
10
|
+
export declare function setInProcessTwins(twins: Readonly<Record<string, InProcessTwin>>): Readonly<Record<string, InProcessTwin>>;
|
|
11
|
+
/** A vendor URL (or the World's application), including redirects, always answered inside the World; `standIn` is the
|
|
12
|
+
* calling fetch's application stand-in (PackFetchOptions.application), consulted at every hop. */
|
|
13
|
+
export declare function vendorFetch(url: string | URL, init?: RequestInit, standIn?: ApplicationStandIn): Promise<Response>;
|