@volter/world-core 2.0.36 → 3.0.0
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 +4 -5
- package/app-route.cjs +12 -6
- package/app-route.d.cts +1 -1
- package/dist/app-route.cjs +12 -6
- package/dist/app-route.d.cts +1 -1
- package/dist/generated/pack-facts.json +1410 -3069
- package/dist/inject.cjs +64 -9
- package/dist/pack-facts.cjs +44 -0
- package/dist/src/actions.d.ts +3 -3
- package/dist/src/actions.js +22 -16
- package/dist/src/ancestry.d.ts +14 -2
- package/dist/src/ancestry.js +92 -2
- package/dist/src/anthropic-wire.d.ts +39 -0
- package/dist/src/anthropic-wire.js +136 -0
- package/dist/src/bytes.d.ts +7 -0
- package/dist/src/bytes.js +35 -0
- package/dist/src/changeset.d.ts +1 -1
- package/dist/src/changeset.js +0 -0
- package/dist/src/clickhouse/index.d.ts +3 -0
- package/dist/src/clickhouse/index.js +6 -0
- package/dist/src/clickhouse/sql.d.ts +233 -0
- package/dist/src/clickhouse/sql.js +4329 -0
- package/dist/src/clickhouse/types.d.ts +18 -0
- package/dist/src/clickhouse/types.js +47 -0
- package/dist/src/clickhouse/values.d.ts +146 -0
- package/dist/src/clickhouse/values.js +858 -0
- package/dist/src/client-bundle.js +2 -3
- package/dist/src/cors.d.ts +15 -0
- package/dist/src/cors.js +31 -0
- package/dist/src/derived-core.d.ts +487 -24
- package/dist/src/derived-core.js +788 -144
- package/dist/src/derived-real.d.ts +13 -0
- package/dist/src/derived-real.js +518 -0
- package/dist/src/derived.d.ts +35 -1
- package/dist/src/derived.js +61 -9
- package/dist/src/emit.js +1 -2
- package/dist/src/events.d.ts +206 -0
- package/dist/src/events.js +341 -0
- package/dist/src/executor.d.ts +3 -0
- package/dist/src/executor.js +19 -2
- package/dist/src/file-response.d.ts +6 -0
- package/dist/src/file-response.js +30 -0
- package/dist/src/fork.js +3 -2
- package/dist/src/git/history.d.ts +7 -0
- package/dist/src/git/history.js +24 -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 +28 -0
- package/dist/src/git/lfs.js +66 -0
- package/dist/src/git/objects.js +3 -8
- package/dist/src/git/smart-http.d.ts +3 -1
- package/dist/src/git/smart-http.js +67 -6
- package/dist/src/graphql-wire.d.ts +29 -0
- package/dist/src/graphql-wire.js +101 -0
- package/dist/src/grpc-wire.d.ts +67 -0
- package/dist/src/grpc-wire.js +170 -0
- package/dist/src/h2.d.ts +40 -0
- package/dist/src/h2.js +656 -0
- package/dist/src/head.d.ts +32 -3
- package/dist/src/head.js +161 -40
- package/dist/src/history.d.ts +1 -1
- package/dist/src/history.js +6 -6
- package/dist/src/hpack.json +1 -0
- package/dist/src/index.d.ts +64 -75
- package/dist/src/index.js +58 -101
- package/dist/src/log.js +28 -19
- package/dist/src/machines.d.ts +50 -0
- package/dist/src/machines.js +151 -0
- package/dist/src/managed-database.d.ts +86 -0
- package/dist/src/managed-database.js +283 -0
- package/dist/src/multipart.d.ts +11 -0
- package/dist/src/multipart.js +51 -0
- package/dist/src/observe.d.ts +15 -5
- package/dist/src/observe.js +23 -9
- package/dist/src/openai-wire.d.ts +108 -0
- package/dist/src/openai-wire.js +337 -0
- package/dist/src/pack-assets.d.ts +3 -4
- package/dist/src/pack-assets.js +15 -10
- package/dist/src/pack-fetch.d.ts +77 -0
- package/dist/src/pack-fetch.js +449 -0
- package/dist/src/pack-paths.d.ts +12 -0
- package/dist/src/pack-paths.js +86 -0
- package/dist/src/packRegistry.d.ts +69 -162
- package/dist/src/packRegistry.js +55 -20
- package/dist/src/people.d.ts +13 -0
- package/dist/src/people.js +18 -0
- package/dist/src/placeholder-image.d.ts +5 -0
- package/dist/src/placeholder-image.js +114 -0
- package/dist/src/protobuf.d.ts +28 -0
- package/dist/src/protobuf.js +332 -0
- package/dist/src/redis/engine.js +1 -1
- package/dist/src/request-scope.d.ts +1 -1
- package/dist/src/request-scope.js +6 -4
- package/dist/src/resource-blob.d.ts +5 -0
- package/dist/src/resource-blob.js +11 -0
- package/dist/src/runtime.d.ts +85 -0
- package/dist/src/runtime.js +104 -0
- package/dist/src/s3/wire.d.ts +60 -0
- package/dist/src/s3/wire.js +157 -0
- package/dist/src/scenario.d.ts +3 -0
- package/dist/src/scenario.js +2 -0
- package/dist/src/schema-sample.d.ts +1 -0
- package/dist/src/schema-sample.js +21 -0
- package/dist/src/sealed-box.d.ts +14 -0
- package/dist/src/sealed-box.js +225 -0
- package/dist/src/serve-http.d.ts +14 -0
- package/dist/src/serve-http.js +27 -3
- package/dist/src/serve.d.ts +6 -0
- package/dist/src/serve.js +69 -14
- package/dist/src/signing.d.ts +135 -0
- package/dist/src/signing.js +222 -0
- package/dist/src/sigv4.d.ts +48 -0
- package/dist/src/sigv4.js +167 -0
- package/dist/src/smtp.d.ts +16 -0
- package/dist/src/smtp.js +72 -0
- package/dist/src/sockets.d.ts +51 -0
- package/dist/src/sockets.js +90 -0
- package/dist/src/state-system.d.ts +1 -0
- package/dist/src/state-system.js +1 -1
- package/dist/src/storage.d.ts +1 -1
- package/dist/src/storage.js +3 -3
- package/dist/src/trace-context.js +1 -1
- package/dist/src/twin-fetch.d.ts +0 -7
- package/dist/src/twin-fetch.js +0 -14
- package/dist/src/vendor-call.d.ts +6 -0
- package/dist/src/vendor-call.js +41 -0
- package/dist/src/world-store.js +1 -1
- package/dist/vendor-hosts.cjs +36 -125
- package/dist/vendor-hosts.d.cts +8 -0
- package/generated/pack-facts.json +1410 -3069
- package/inject.cjs +64 -9
- package/pack-facts.cjs +44 -0
- package/package.json +17 -3
- package/src/actions.ts +23 -16
- package/src/ancestry.ts +74 -2
- package/src/anthropic-wire.ts +137 -0
- package/src/bytes.ts +42 -0
- package/src/changeset.ts +5 -5
- package/src/clickhouse/index.ts +6 -0
- package/src/clickhouse/sql.ts +3059 -0
- package/src/clickhouse/types.ts +44 -0
- package/src/clickhouse/values.ts +697 -0
- package/src/client-bundle.ts +2 -3
- package/src/cors.ts +34 -0
- package/src/derived-core.ts +1013 -146
- package/src/derived-real.ts +434 -0
- package/src/derived.ts +73 -3
- package/src/emit.ts +1 -2
- package/src/events.ts +449 -0
- package/src/executor.ts +24 -2
- package/src/file-response.ts +27 -0
- package/src/fork.ts +3 -2
- package/src/git/history.ts +19 -0
- package/src/git/index.ts +1 -0
- package/src/git/lfs.ts +67 -0
- package/src/git/objects.ts +3 -5
- package/src/git/smart-http.ts +56 -6
- package/src/graphql-wire.ts +106 -0
- package/src/grpc-wire.ts +159 -0
- package/src/h2.ts +627 -0
- package/src/head.ts +132 -41
- package/src/history.ts +6 -6
- package/src/hpack.json +1 -0
- package/src/index.ts +82 -329
- package/src/log.ts +27 -18
- package/src/machines.ts +151 -0
- package/src/managed-database.ts +299 -0
- package/src/multipart.ts +51 -0
- package/src/observe.ts +31 -15
- package/src/openai-wire.ts +371 -0
- package/src/pack-assets.ts +15 -11
- package/src/pack-fetch.ts +458 -0
- package/src/pack-paths.ts +72 -0
- package/src/packRegistry.ts +79 -167
- package/src/people.ts +31 -0
- package/src/placeholder-image.ts +88 -0
- package/src/protobuf.ts +251 -0
- package/src/redis/engine.ts +1 -1
- package/src/request-scope.ts +8 -4
- package/src/resource-blob.ts +13 -0
- package/src/runtime.ts +344 -0
- package/src/s3/wire.ts +172 -0
- package/src/scenario.ts +4 -0
- package/src/schema-sample.ts +24 -0
- package/src/sealed-box.ts +182 -0
- package/src/serve-http.ts +31 -3
- package/src/serve.ts +58 -14
- package/src/signing.ts +231 -0
- package/src/sigv4.ts +158 -0
- package/src/smtp.ts +76 -0
- package/src/sockets.ts +140 -0
- package/src/state-system.ts +2 -2
- package/src/storage.ts +3 -3
- package/src/trace-context.ts +1 -1
- package/src/twin-fetch.ts +0 -20
- package/src/vendor-call.ts +41 -0
- package/src/world-store.ts +1 -1
- package/vendor-hosts.cjs +36 -125
- package/vendor-hosts.d.cts +8 -0
- package/dist/src/mirror-shell.d.ts +0 -2
- package/dist/src/mirror-shell.js +0 -13
- package/dist/src/v1-removed.d.ts +0 -159
- package/dist/src/v1-removed.js +0 -124
- package/src/mirror-shell.ts +0 -15
- package/src/v1-removed.ts +0 -172
package/dist/src/serve-http.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TwinStream } from './twin-fetch.js';
|
|
1
2
|
export type HttpHandler = (request: Request) => Response | Promise<Response>;
|
|
2
3
|
export type WebSocketPeer = {
|
|
3
4
|
send: (data: string | Uint8Array) => unknown;
|
|
@@ -5,6 +6,8 @@ export type WebSocketPeer = {
|
|
|
5
6
|
};
|
|
6
7
|
export type WebSocketUpgrade = {
|
|
7
8
|
accepts: (request: Request) => boolean;
|
|
9
|
+
/** The subprotocol the server selects for this request, answered in `Sec-WebSocket-Protocol`; none when undefined. */
|
|
10
|
+
protocol?: (request: Request) => string | undefined;
|
|
8
11
|
open: (peer: WebSocketPeer, request: Request) => void;
|
|
9
12
|
message: (peer: WebSocketPeer, data: string | Uint8Array) => void | Promise<void>;
|
|
10
13
|
close: (peer: WebSocketPeer) => void;
|
|
@@ -46,3 +49,14 @@ export declare function serveHttp(options: ServeHttpOptions): Promise<HttpServer
|
|
|
46
49
|
* module that also rides into a browser bundle reaches `node:crypto` with. A bare `require()`
|
|
47
50
|
* is Bun's alone; Node's ESM has none (found 2026-09-07 by the pages running under node). */
|
|
48
51
|
export declare function nodeBuiltin<T = unknown>(name: string): T;
|
|
52
|
+
/** A raw-TCP twin's listener (the seam's byte-stream half): its `create<Name>TwinStream` served on a TCP port, each
|
|
53
|
+
* socket one connection, on loopback unless the caller names a hostname. Runtime-neutral, as `serveHttp` is: Node's
|
|
54
|
+
* `net` reached at call time, never imported. */
|
|
55
|
+
export declare function serveStream(options: {
|
|
56
|
+
port?: number;
|
|
57
|
+
hostname?: string;
|
|
58
|
+
stream: TwinStream;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
port: number;
|
|
61
|
+
stop: () => void;
|
|
62
|
+
}>;
|
package/dist/src/serve-http.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// asynchronous on Node — Bun's synchronous bind is the special case.
|
|
6
6
|
//
|
|
7
7
|
// Nothing here runs at module scope, and `node:http` is reached through `process.getBuiltinModule`
|
|
8
|
-
// rather than a static import: this module rides into
|
|
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
10
|
/** A decorator the request journal installs: every server made through the seam serves through it. */
|
|
11
11
|
let decorate;
|
|
@@ -16,6 +16,11 @@ export const WORLD_BOOT_PATH = '/__volter/world-boot';
|
|
|
16
16
|
export async function serveHttp(options) {
|
|
17
17
|
// Inside a World every server made through the seam answers which boot it belongs to, so the World can tell its own
|
|
18
18
|
// twin from a leftover World's that holds the same port. Outside a World nothing changes.
|
|
19
|
+
// a pack's fetch carries the upgrade its sockets take (pack-fetch.ts), read before anything wraps the fetch: served on
|
|
20
|
+
// its own or in a World, its sockets answer
|
|
21
|
+
const carried = options.upgrade ?? options.fetch.upgrade;
|
|
22
|
+
if (carried)
|
|
23
|
+
options = { ...options, upgrade: carried };
|
|
19
24
|
const worldBoot = globalThis.process?.env?.VOLTER_WORLD_BOOT_ID;
|
|
20
25
|
if (worldBoot) {
|
|
21
26
|
const served = options.fetch;
|
|
@@ -32,6 +37,8 @@ export async function serveHttp(options) {
|
|
|
32
37
|
const server = bun.serve({ ...options, hostname, port: options.port ?? 0,
|
|
33
38
|
...(upgrade ? {
|
|
34
39
|
fetch: (request, server) => {
|
|
40
|
+
// Bun answers the first subprotocol the client offers by itself; a header set here would be a second one, which a
|
|
41
|
+
// client refuses (measured, Bun 1.2.20: Node's client reads `realtime`; with the header added, a duplicate)
|
|
35
42
|
if (request.headers.get('upgrade')?.toLowerCase() === 'websocket' && upgrade.accepts(request) && server.upgrade(request, { data: { request } }))
|
|
36
43
|
return undefined;
|
|
37
44
|
return options.fetch(request);
|
|
@@ -52,6 +59,21 @@ export async function serveHttp(options) {
|
|
|
52
59
|
* module that also rides into a browser bundle reaches `node:crypto` with. A bare `require()`
|
|
53
60
|
* is Bun's alone; Node's ESM has none (found 2026-09-07 by the pages running under node). */
|
|
54
61
|
export function nodeBuiltin(name) { return builtin(name); }
|
|
62
|
+
/** A raw-TCP twin's listener (the seam's byte-stream half): its `create<Name>TwinStream` served on a TCP port, each
|
|
63
|
+
* socket one connection, on loopback unless the caller names a hostname. Runtime-neutral, as `serveHttp` is: Node's
|
|
64
|
+
* `net` reached at call time, never imported. */
|
|
65
|
+
export async function serveStream(options) {
|
|
66
|
+
const net = builtin('node:net');
|
|
67
|
+
const server = net.createServer((socket) => {
|
|
68
|
+
const conn = options.stream({ write: (bytes) => { socket.write(bytes); }, end: () => { socket.end(); } }, socket.remoteAddress ?? '127.0.0.1');
|
|
69
|
+
socket.on('data', (chunk) => conn.data(new Uint8Array(chunk)));
|
|
70
|
+
socket.on('close', () => conn.close());
|
|
71
|
+
socket.on('error', () => conn.close());
|
|
72
|
+
});
|
|
73
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(options.port ?? 0, options.hostname ?? '127.0.0.1', () => resolve()); });
|
|
74
|
+
const address = server.address();
|
|
75
|
+
return { port: typeof address === 'object' && address ? address.port : options.port ?? 0, stop: () => { server.close(); } };
|
|
76
|
+
}
|
|
55
77
|
function builtin(name) {
|
|
56
78
|
const get = process.getBuiltinModule;
|
|
57
79
|
if (typeof get !== 'function')
|
|
@@ -306,10 +328,12 @@ async function serveOnNode(options) {
|
|
|
306
328
|
let socketServer;
|
|
307
329
|
if (options.upgrade) {
|
|
308
330
|
const { WebSocketServer } = await import('ws');
|
|
309
|
-
socketServer = new WebSocketServer({ noServer: true });
|
|
310
331
|
const upgrade = options.upgrade;
|
|
332
|
+
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
|
+
// the subprotocol the socket selects, answered as Bun's branch answers it
|
|
334
|
+
socketServer = new WebSocketServer({ noServer: true, handleProtocols: (_offered, req) => upgrade.protocol?.(requestOf(req)) ?? false });
|
|
311
335
|
server.on('upgrade', (req, socket, head) => {
|
|
312
|
-
const request =
|
|
336
|
+
const request = requestOf(req);
|
|
313
337
|
if (!upgrade.accepts(request)) {
|
|
314
338
|
socket.end('HTTP/1.1 404 Not Found\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
|
|
315
339
|
return;
|
package/dist/src/serve.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export type TwinCredentialShape = {
|
|
|
19
19
|
* `token:read`). The World replaces any a caller sent; the serve seam records it in the twin's request journal and
|
|
20
20
|
* removes it before the twin's handler. A twin reached on its own port, not through a World, records what it was sent. */
|
|
21
21
|
export declare const CALLER_HEADER = "x-volter-caller";
|
|
22
|
+
/** The caller as it travels in CALLER_HEADER: percent-encoded, since a key's name is a person's words (`preview pr-8 ·
|
|
23
|
+
* org token ci`) and a header carries bytes only. `callerFromHeader` reads it back; a value it cannot decode stands. */
|
|
24
|
+
export declare const callerForHeader: (caller: string) => string;
|
|
25
|
+
export declare const callerFromHeader: (value: string) => string;
|
|
22
26
|
export type TwinRequestJournalEntry = {
|
|
23
27
|
at?: string;
|
|
24
28
|
method: string;
|
|
@@ -157,6 +161,8 @@ export type AtomicTwinWriteDecision<T> = {
|
|
|
157
161
|
value: T;
|
|
158
162
|
write: TwinWriteInput;
|
|
159
163
|
};
|
|
164
|
+
/** A recorded request as it was sent: each string the log keeps as a blob, read back. */
|
|
165
|
+
export declare function recordedInput(input: unknown): Promise<unknown>;
|
|
160
166
|
/**
|
|
161
167
|
* Decide a state-dependent local write and append it under the same cross-process action lock.
|
|
162
168
|
* Use this when acceptance or the new fields depend on current projected state; a caller-side
|
package/dist/src/serve.js
CHANGED
|
@@ -18,11 +18,21 @@ import { hashFieldValue } from "./hash.js";
|
|
|
18
18
|
import { appendActionIfAbsent, appendActionOccurrence, decideAndAppendAction, projectResources } from "./actions.js";
|
|
19
19
|
import { observeWorldPaths, worldPaths } from "./storage.js";
|
|
20
20
|
import { getActiveWorldStore } from "./world-store.js";
|
|
21
|
+
import { getActiveBlobStore } from "./blob-store.js";
|
|
21
22
|
import { parseTraceparent } from "./trace-context.js";
|
|
22
23
|
/** The header a World puts on a request it forwards to a twin: who made it (`key:<name>`, `person:<who>`, `token`,
|
|
23
24
|
* `token:read`). The World replaces any a caller sent; the serve seam records it in the twin's request journal and
|
|
24
25
|
* removes it before the twin's handler. A twin reached on its own port, not through a World, records what it was sent. */
|
|
25
26
|
export const CALLER_HEADER = 'x-volter-caller';
|
|
27
|
+
/** The caller as it travels in CALLER_HEADER: percent-encoded, since a key's name is a person's words (`preview pr-8 ·
|
|
28
|
+
* org token ci`) and a header carries bytes only. `callerFromHeader` reads it back; a value it cannot decode stands. */
|
|
29
|
+
export const callerForHeader = (caller) => encodeURIComponent(caller);
|
|
30
|
+
export const callerFromHeader = (value) => { try {
|
|
31
|
+
return decodeURIComponent(value);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return value;
|
|
35
|
+
} };
|
|
26
36
|
/** The journal's closed segments, oldest first: `requests.jsonl.<n>` beside the live file. */
|
|
27
37
|
function journalSegments(path) {
|
|
28
38
|
const base = path.slice(path.lastIndexOf('/') + 1);
|
|
@@ -239,21 +249,18 @@ export function journalTwinRequest(service, entry, root) {
|
|
|
239
249
|
// factory's closure. They are recoverable without touching a single pack, because handling a
|
|
240
250
|
// request makes the pack call the kernel, and every kernel state path funnels through
|
|
241
251
|
// `worldPaths(service, root)` (storage.ts). Observing the FIRST such call a server makes is the
|
|
242
|
-
// twin naming itself: `
|
|
252
|
+
// twin naming itself: `deepgram` under `/tmp/world/data/deepgram`. It is cached per server, so only
|
|
243
253
|
// the first request pays for it. A server whose first request touches no state falls back to the
|
|
244
|
-
// call site (
|
|
254
|
+
// call site (a catalog checkout or `@volter/twin-<vendor>`) and the process's `--root`
|
|
245
255
|
// argument, which is how the world runtime launches every twin.
|
|
246
256
|
export const TWIN_JOURNAL_IDENTITY = Symbol.for('volter.twin.requestJournal.identity');
|
|
247
257
|
const JOURNAL_SHIM_INSTALLED = Symbol.for('volter.twin.requestJournal.installed');
|
|
248
258
|
/** One capture slot per in-flight request, so two twins co-located in ONE process (the shared
|
|
249
259
|
* host) can never read each other's identity off a racing sibling's first request.
|
|
250
260
|
*
|
|
251
|
-
* LAZY ON PURPOSE
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
* so it survives tree-shaking and lands in the client — where `node:async_hooks` does not
|
|
255
|
-
* resolve, `new` throws at bundle evaluation, and the React app never mounts. That took out
|
|
256
|
-
* every mirror UI in the estate at once. Nothing in this module may run at module scope. */
|
|
261
|
+
* LAZY ON PURPOSE: kernel modules ride into browser bundles (kernel imports are free of side effects), and a module-scope
|
|
262
|
+
* `new AsyncLocalStorage()` survives tree-shaking into a client, where `node:async_hooks` does not resolve and `new`
|
|
263
|
+
* throws at bundle evaluation. Nothing in this module may run at module scope. */
|
|
257
264
|
let identityCapture;
|
|
258
265
|
function identitySlot() {
|
|
259
266
|
identityCapture ??= new AsyncLocalStorage();
|
|
@@ -279,7 +286,7 @@ const INFRA_PACKAGES = new Set(['world-runtime', 'world-tooling', 'world-attach'
|
|
|
279
286
|
export function vendorFromStack(stack) {
|
|
280
287
|
if (!stack)
|
|
281
288
|
return undefined;
|
|
282
|
-
const pattern = /(?:
|
|
289
|
+
const pattern = /(?:twin-packs-p[23][\\/]|@volter[\\/]twin-)([A-Za-z0-9_-]+)/g;
|
|
283
290
|
for (const match of stack.matchAll(pattern)) {
|
|
284
291
|
const name = match[1];
|
|
285
292
|
if (!INFRA_PACKAGES.has(name))
|
|
@@ -298,7 +305,7 @@ export function vendorFromStack(stack) {
|
|
|
298
305
|
* service is an argument rather than a fact about the pack.
|
|
299
306
|
*/
|
|
300
307
|
export function installTwinRequestJournal() {
|
|
301
|
-
// a browser bundle
|
|
308
|
+
// a browser bundle carrying the kernel: nothing to wrap, nothing to touch
|
|
302
309
|
if (typeof window !== 'undefined' || typeof document !== 'undefined')
|
|
303
310
|
return false;
|
|
304
311
|
const bun = globalThis.Bun;
|
|
@@ -338,7 +345,8 @@ function journalingFetch(config, callSiteVendor) {
|
|
|
338
345
|
const inner = config.fetch;
|
|
339
346
|
const journaling = async function (request, server) {
|
|
340
347
|
// who made it is read here and goes no further: no twin's handler (a tunnel's, a proxy's) hands it on
|
|
341
|
-
const
|
|
348
|
+
const sent = request.headers.get(CALLER_HEADER);
|
|
349
|
+
const caller = sent === null ? undefined : callerFromHeader(sent);
|
|
342
350
|
if (caller !== undefined) {
|
|
343
351
|
const headers = new Headers(request.headers);
|
|
344
352
|
headers.delete(CALLER_HEADER);
|
|
@@ -404,6 +412,53 @@ installTwinRequestJournal();
|
|
|
404
412
|
export function twinResources(service, root) {
|
|
405
413
|
return projectResources(service, root);
|
|
406
414
|
}
|
|
415
|
+
/** The largest string a write's recorded request keeps in the log itself. */
|
|
416
|
+
const INPUT_INLINE_MAX = 64 * 1024;
|
|
417
|
+
/**
|
|
418
|
+
* A write's recorded request, with each string longer than INPUT_INLINE_MAX (an attached file's base64: an npm
|
|
419
|
+
* publish's tarball) kept once as a content-addressed blob under the service's state and named in its place as
|
|
420
|
+
* `{ $blob, bytes }`. The request stays whole and deterministic, and a body several writes record is kept once, never
|
|
421
|
+
* copied into the log (docs/contributing/architecture.md, "The log keeps a large request once"). `recordedInput` reads
|
|
422
|
+
* it back whole.
|
|
423
|
+
*/
|
|
424
|
+
async function offloadInput(service, input, root) {
|
|
425
|
+
if (typeof input === 'string') {
|
|
426
|
+
if (input.length <= INPUT_INLINE_MAX)
|
|
427
|
+
return input;
|
|
428
|
+
// hashed as it is (UTF-8), and encoded only when its blob is new: each write of a request records the same body
|
|
429
|
+
const key = join(worldPaths(service, root).dir, 'inputs', createHash('sha256').update(input, 'utf8').digest('hex'));
|
|
430
|
+
const blobs = getActiveBlobStore();
|
|
431
|
+
if (!(await blobs.exists(key)))
|
|
432
|
+
await blobs.put(key, Buffer.from(input, 'utf8'));
|
|
433
|
+
return { $blob: key, bytes: Buffer.byteLength(input, 'utf8') };
|
|
434
|
+
}
|
|
435
|
+
if (Array.isArray(input))
|
|
436
|
+
return Promise.all(input.map((v) => offloadInput(service, v, root)));
|
|
437
|
+
if (input && typeof input === 'object') {
|
|
438
|
+
const out = {};
|
|
439
|
+
for (const [k, v] of Object.entries(input))
|
|
440
|
+
out[k] = await offloadInput(service, v, root);
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
return input;
|
|
444
|
+
}
|
|
445
|
+
/** A recorded request as it was sent: each string the log keeps as a blob, read back. */
|
|
446
|
+
export async function recordedInput(input) {
|
|
447
|
+
if (Array.isArray(input))
|
|
448
|
+
return Promise.all(input.map(recordedInput));
|
|
449
|
+
if (input && typeof input === 'object') {
|
|
450
|
+
const ref = input;
|
|
451
|
+
if (typeof ref.$blob === 'string' && typeof ref.bytes === 'number' && Object.keys(input).length === 2) {
|
|
452
|
+
const bytes = await getActiveBlobStore().get(ref.$blob);
|
|
453
|
+
return bytes ? new TextDecoder().decode(bytes) : undefined;
|
|
454
|
+
}
|
|
455
|
+
const out = {};
|
|
456
|
+
for (const [k, v] of Object.entries(input))
|
|
457
|
+
out[k] = await recordedInput(v);
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
return input;
|
|
461
|
+
}
|
|
407
462
|
function actionForTwinWrite(service, write) {
|
|
408
463
|
const occurredAt = write.occurredAt ?? new Date().toISOString();
|
|
409
464
|
// Idempotency: dedup a RE-ISSUED IDENTICAL write only. The key includes a hash of the write
|
|
@@ -493,7 +548,7 @@ export function resolveTwinRead(service, pathname, opts = {}) {
|
|
|
493
548
|
// provably untouched (no network I/O, no egress). Pushing the action to the real
|
|
494
549
|
// vendor is a separate, explicit step that records egress + confirms the action.
|
|
495
550
|
export async function applyTwinWrite(service, write, root) {
|
|
496
|
-
const action = actionForTwinWrite(service, write);
|
|
551
|
+
const action = actionForTwinWrite(service, write.input === undefined ? write : { ...write, input: await offloadInput(service, write.input, root) });
|
|
497
552
|
// A LOCAL VENDOR WRITE IS AN OCCURRENCE. Two calls are two actions, even byte-identical in the
|
|
498
553
|
// same instant, and the ordinal in the action id says which occurrence this is. The kernel used
|
|
499
554
|
// to derive identity from (content + occurredAt millisecond) and drop a repeat as `replayed`,
|
|
@@ -501,8 +556,8 @@ export async function applyTwinWrite(service, write, root) {
|
|
|
501
556
|
// under a pinned world clock, where every write in a world shares one instant. That default cost
|
|
502
557
|
// github, slack and jira live defects while the recipe told each pack to defend itself with a
|
|
503
558
|
// per-write ordinal and a fifth of the catalog opted out via `uniqueness`. A default every pack
|
|
504
|
-
// must remember to defend against is not a default. See docs/contributing/
|
|
505
|
-
//
|
|
559
|
+
// must remember to defend against is not a default. See docs/contributing/architecture.md,
|
|
560
|
+
// "The state kernel", "A local write is an occurrence".
|
|
506
561
|
//
|
|
507
562
|
// AT-MOST-ONCE is opt in, by KEY: `idempotencyKey` makes the id a function of the caller's own
|
|
508
563
|
// request identity, so a re-issue collapses onto the first and answers `replayed`. Content can
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { sealedBoxKeyPair, sealedBoxOpen } from './sealed-box.js';
|
|
2
|
+
type Row = Record<string, unknown>;
|
|
3
|
+
export type Jwk = {
|
|
4
|
+
kty: 'RSA';
|
|
5
|
+
use: 'sig';
|
|
6
|
+
alg: 'RS256';
|
|
7
|
+
kid: string;
|
|
8
|
+
n: string;
|
|
9
|
+
e: string;
|
|
10
|
+
};
|
|
11
|
+
export type Jwks = {
|
|
12
|
+
keys: Jwk[];
|
|
13
|
+
};
|
|
14
|
+
/** A signing key: an RSA private key as a PEM (its public half, given or derived, is what a JWKS serves and names by
|
|
15
|
+
* `kid`), or a shared secret. Only deterministic algorithms: an ECDSA signature is random, so a World signs none. */
|
|
16
|
+
export type SigningKey = {
|
|
17
|
+
alg: 'RS256' | 'RS384' | 'RS512';
|
|
18
|
+
privatePem: string;
|
|
19
|
+
publicPem?: string;
|
|
20
|
+
} | {
|
|
21
|
+
alg: 'HS256' | 'HS384' | 'HS512';
|
|
22
|
+
secret: string;
|
|
23
|
+
};
|
|
24
|
+
/** The algorithms `jwtSign` signs with. */
|
|
25
|
+
export declare const SIGNING_ALGORITHMS: ReadonlyArray<SigningKey['alg']>;
|
|
26
|
+
/** Whether a token holds: its signature and its `exp`/`nbf` at `now` (seconds), with the payload when it does. */
|
|
27
|
+
export type JwtVerdict = {
|
|
28
|
+
valid: boolean;
|
|
29
|
+
payload?: Row;
|
|
30
|
+
reason?: 'malformed' | 'unexpected_alg' | 'no_matching_key' | 'bad_signature' | 'expired' | 'not_yet_valid';
|
|
31
|
+
};
|
|
32
|
+
/** 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
|
+
export declare function keyId(publicPem: string, prefix?: string): string;
|
|
34
|
+
/** 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. RS256 tokens carry the key's `kid`. */
|
|
36
|
+
export declare function jwtSign(claims: Row, key: SigningKey, opts: {
|
|
37
|
+
now: number;
|
|
38
|
+
expiresInSeconds?: number;
|
|
39
|
+
kidPrefix?: string;
|
|
40
|
+
}): string;
|
|
41
|
+
/** A JWT's header and payload, unverified. */
|
|
42
|
+
export declare function jwtDecode(token: string): {
|
|
43
|
+
header: Row;
|
|
44
|
+
payload: Row;
|
|
45
|
+
};
|
|
46
|
+
/** A token checked: an RS256 one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
|
|
47
|
+
* then its `exp`/`nbf` at `now` (seconds). */
|
|
48
|
+
export declare function jwtVerify(token: string, key: Jwks | {
|
|
49
|
+
alg: 'HS256' | 'HS384' | 'HS512';
|
|
50
|
+
secret: string;
|
|
51
|
+
}, opts: {
|
|
52
|
+
now: number;
|
|
53
|
+
}): JwtVerdict;
|
|
54
|
+
/** The JWKS that verifies every token a public key's pair signs. */
|
|
55
|
+
export declare function jwks(publicPem: string, kidPrefix?: string): Jwks;
|
|
56
|
+
/** An HMAC of a value, hex or base64 (a webhook's signature, a signed id): SHA-256 unless the vendor names another
|
|
57
|
+
* (OAuth 1.0a's HMAC-SHA1). The secret is its text, or its bytes where the vendor keys with a decoded secret (Svix's
|
|
58
|
+
* `whsec_` base64). */
|
|
59
|
+
export declare const hmac: (secret: string | Uint8Array, value: string | Uint8Array, encoding?: "hex" | "base64" | "base64url", algorithm?: "sha1" | "sha256" | "sha512") => string;
|
|
60
|
+
/** A SHA-256, hex (a key stored by its hash, a deterministic id). */
|
|
61
|
+
export declare const sha256: (value: string) => string;
|
|
62
|
+
/** A digest of a value in the algorithm and encoding a vendor names (GoTrue's SHA-224 token hashes, a PKCE challenge's
|
|
63
|
+
* base64url SHA-256). */
|
|
64
|
+
export declare function digest(algorithm: 'md5' | 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512', value: string | Uint8Array, encoding?: 'hex' | 'base64' | 'base64url'): string;
|
|
65
|
+
/** RFC 3986 percent-encoding as RFC 5849 §3.6 uses it (unreserved: A-Z a-z 0-9 - . _ ~). */
|
|
66
|
+
export declare const oauth1Encode: (s: string) => string;
|
|
67
|
+
/** The `Authorization: OAuth k="v", …` header's parameters, decoded, in order; undefined when it is not one. */
|
|
68
|
+
export declare function oauth1Header(value: string | null | undefined): Array<[string, string]> | undefined;
|
|
69
|
+
/** The signature base string (RFC 5849 §3.4.1): the method, the base URL (no query) and the parameters signed, each
|
|
70
|
+
* percent-encoded and sorted by name and value. */
|
|
71
|
+
export declare function oauth1BaseString(method: string, baseUrl: string, pairs: ReadonlyArray<readonly [string, string]>): string;
|
|
72
|
+
/** The HMAC-SHA1 signature (RFC 5849 §3.4.2) over a base string, keyed by the consumer secret and the token secret. */
|
|
73
|
+
export declare const oauth1Signature: (base: string, consumerSecret: string, tokenSecret?: string) => string;
|
|
74
|
+
/** A digest of a value as its bytes (a key or an id drawn from a hash's bytes, a deterministic embedding). */
|
|
75
|
+
export declare function digestBytes(algorithm: 'md5' | 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512', value: string | Uint8Array): Uint8Array;
|
|
76
|
+
/** An MD5, hex (an object's ETag, as S3 and storage APIs give it); never a secret's protection. */
|
|
77
|
+
export declare const md5: (value: string | Uint8Array) => string;
|
|
78
|
+
/** A version-4-shaped UUID derived from `seed`: the same seed gives the same id every run (R9). */
|
|
79
|
+
export declare function uuidFrom(seed: string): string;
|
|
80
|
+
/** `count` lowercase letters derived from `seed` (a 20-letter project ref, a slug's suffix), the same every run. */
|
|
81
|
+
export declare function lettersFrom(seed: string, count?: number): string;
|
|
82
|
+
/** Two strings compared in constant time. */
|
|
83
|
+
export declare function equalSecrets(a: string, b: string): boolean;
|
|
84
|
+
/** Bytes signed with a private key (RSA with SHA-256, or Ed25519), in the encoding asked: a certificate's signature,
|
|
85
|
+
* a transparency log's checkpoint. */
|
|
86
|
+
export declare function signWith(privatePem: string, data: string | Uint8Array, encoding?: 'base64' | 'hex' | 'base64url'): string;
|
|
87
|
+
/** Whether a signature holds over bytes under a public key or a certificate's (PEM, or a certificate's DER): any
|
|
88
|
+
* scheme a vendor uses (ECDSA, RSA, Ed25519), since verifying draws nothing. The digest is SHA-256 unless named. */
|
|
89
|
+
export declare function verifyWith(publicKey: string | Uint8Array, data: string | Uint8Array, signature: string | Uint8Array, opts?: {
|
|
90
|
+
digest?: 'sha256' | 'sha384' | 'sha512';
|
|
91
|
+
encoding?: 'base64' | 'hex';
|
|
92
|
+
}): boolean;
|
|
93
|
+
/** A public key's PEM and its DER (SubjectPublicKeyInfo), from a public or private key's PEM or a certificate. */
|
|
94
|
+
export declare function publicKeyOf(key: string): {
|
|
95
|
+
pem: string;
|
|
96
|
+
der: Uint8Array;
|
|
97
|
+
};
|
|
98
|
+
/** An X.509 certificate read (PEM or DER): its bytes, names, validity and key, and whether an issuer's key signed it;
|
|
99
|
+
* undefined when it is not one. */
|
|
100
|
+
export declare function certificateOf(cert: string | Uint8Array): {
|
|
101
|
+
der: Uint8Array;
|
|
102
|
+
subject: string;
|
|
103
|
+
issuer: string;
|
|
104
|
+
subjectAltName: string | undefined;
|
|
105
|
+
notBefore: string;
|
|
106
|
+
notAfter: string;
|
|
107
|
+
publicKey: {
|
|
108
|
+
pem: string;
|
|
109
|
+
der: Uint8Array;
|
|
110
|
+
};
|
|
111
|
+
issuedBy(issuer: string | Uint8Array): boolean;
|
|
112
|
+
} | undefined;
|
|
113
|
+
/** What a handler signs and hashes with: `ctx.crypto`. */
|
|
114
|
+
export declare const handlerCrypto: {
|
|
115
|
+
readonly jwtSign: typeof jwtSign;
|
|
116
|
+
readonly jwtVerify: typeof jwtVerify;
|
|
117
|
+
readonly jwtDecode: typeof jwtDecode;
|
|
118
|
+
readonly jwks: typeof jwks;
|
|
119
|
+
readonly keyId: typeof keyId;
|
|
120
|
+
readonly hmac: (secret: string | Uint8Array, value: string | Uint8Array, encoding?: "hex" | "base64" | "base64url", algorithm?: "sha1" | "sha256" | "sha512") => string;
|
|
121
|
+
readonly sha256: (value: string) => string;
|
|
122
|
+
readonly md5: (value: string | Uint8Array) => string;
|
|
123
|
+
readonly digest: typeof digest;
|
|
124
|
+
readonly uuidFrom: typeof uuidFrom;
|
|
125
|
+
readonly lettersFrom: typeof lettersFrom;
|
|
126
|
+
readonly equalSecrets: typeof equalSecrets;
|
|
127
|
+
readonly sealedBoxKeyPair: typeof sealedBoxKeyPair;
|
|
128
|
+
readonly sealedBoxOpen: typeof sealedBoxOpen;
|
|
129
|
+
readonly signWith: typeof signWith;
|
|
130
|
+
readonly verifyWith: typeof verifyWith;
|
|
131
|
+
readonly publicKeyOf: typeof publicKeyOf;
|
|
132
|
+
readonly certificateOf: typeof certificateOf;
|
|
133
|
+
};
|
|
134
|
+
export type HandlerCrypto = typeof handlerCrypto;
|
|
135
|
+
export {};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// SIGNING ON THE CONTEXT (docs/contributing/architecture.md, "What an author writes, and how": the tokens-and-keys row)
|
|
2
|
+
// — 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
|
+
// 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
|
+
// and every value a signature carries comes from the call.
|
|
6
|
+
// A namespace, never named imports: a browser client bundles the kernel, and the browser's `node:crypto` polyfill has no
|
|
7
|
+
// createPublicKey or timingSafeEqual; named, the bundle fails to build. They are reached only when a signature is made.
|
|
8
|
+
import * as nodeCrypto from 'node:crypto';
|
|
9
|
+
import { sealedBoxKeyPair, sealedBoxOpen } from "./sealed-box.js";
|
|
10
|
+
/** The algorithms `jwtSign` signs with. */
|
|
11
|
+
export const SIGNING_ALGORITHMS = ['RS256', 'RS384', 'RS512', 'HS256', 'HS384', 'HS512'];
|
|
12
|
+
const digestOf = (alg) => `sha${alg.slice(2)}`;
|
|
13
|
+
const base64url = (input) => Buffer.from(input).toString('base64').replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
14
|
+
const fromBase64url = (s) => Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
15
|
+
const jsonPart = (value) => base64url(JSON.stringify(value));
|
|
16
|
+
/** 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). */
|
|
17
|
+
export function keyId(publicPem, prefix = 'ins_') {
|
|
18
|
+
const der = nodeCrypto.createPublicKey(publicPem).export({ type: 'spki', format: 'der' });
|
|
19
|
+
return `${prefix}${nodeCrypto.createHash('sha256').update(der).digest('hex').slice(0, 16)}`;
|
|
20
|
+
}
|
|
21
|
+
/** 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. RS256 tokens carry the key's `kid`. */
|
|
23
|
+
export function jwtSign(claims, key, opts) {
|
|
24
|
+
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' };
|
|
27
|
+
const payload = { iat: opts.now, exp: opts.now + (opts.expiresInSeconds ?? 60), nbf: opts.now, ...claims };
|
|
28
|
+
const input = `${jsonPart(header)}.${jsonPart(payload)}`;
|
|
29
|
+
if ('secret' in key)
|
|
30
|
+
return `${input}.${base64url(nodeCrypto.createHmac(digestOf(key.alg), key.secret).update(input).digest())}`;
|
|
31
|
+
const signer = nodeCrypto.createSign(`RSA-SHA${key.alg.slice(2)}`);
|
|
32
|
+
signer.update(input);
|
|
33
|
+
signer.end();
|
|
34
|
+
return `${input}.${base64url(signer.sign(key.privatePem))}`;
|
|
35
|
+
}
|
|
36
|
+
/** A JWT's header and payload, unverified. */
|
|
37
|
+
export function jwtDecode(token) {
|
|
38
|
+
const parts = token.split('.');
|
|
39
|
+
if (parts.length !== 3)
|
|
40
|
+
throw new Error('malformed jwt');
|
|
41
|
+
const part = (s) => JSON.parse(fromBase64url(s).toString('utf8'));
|
|
42
|
+
return { header: part(parts[0]), payload: part(parts[1]) };
|
|
43
|
+
}
|
|
44
|
+
function timely(payload, now) {
|
|
45
|
+
if (typeof payload.exp === 'number' && now >= payload.exp)
|
|
46
|
+
return { valid: false, reason: 'expired' };
|
|
47
|
+
if (typeof payload.nbf === 'number' && now < payload.nbf)
|
|
48
|
+
return { valid: false, reason: 'not_yet_valid' };
|
|
49
|
+
return { valid: true, payload };
|
|
50
|
+
}
|
|
51
|
+
/** A token checked: an RS256 one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
|
|
52
|
+
* then its `exp`/`nbf` at `now` (seconds). */
|
|
53
|
+
export function jwtVerify(token, key, opts) {
|
|
54
|
+
let decoded;
|
|
55
|
+
try {
|
|
56
|
+
decoded = jwtDecode(token);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { valid: false, reason: 'malformed' };
|
|
60
|
+
}
|
|
61
|
+
const [h, p, s] = token.split('.');
|
|
62
|
+
if ('secret' in key) {
|
|
63
|
+
if (decoded.header.alg !== key.alg)
|
|
64
|
+
return { valid: false, reason: 'unexpected_alg' };
|
|
65
|
+
const expected = Buffer.from(base64url(nodeCrypto.createHmac(digestOf(key.alg), key.secret).update(`${h}.${p}`).digest()), 'utf8');
|
|
66
|
+
const given = Buffer.from(s, 'utf8');
|
|
67
|
+
if (given.length !== expected.length || !nodeCrypto.timingSafeEqual(given, expected))
|
|
68
|
+
return { valid: false, reason: 'bad_signature' };
|
|
69
|
+
return timely(decoded.payload, opts.now);
|
|
70
|
+
}
|
|
71
|
+
if (!['RS256', 'RS384', 'RS512'].includes(String(decoded.header.alg)))
|
|
72
|
+
return { valid: false, reason: 'unexpected_alg' };
|
|
73
|
+
// 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
|
+
const jwk = decoded.header.kid !== undefined ? key.keys.find((k) => k.kid === decoded.header.kid) : key.keys.length === 1 ? key.keys[0] : undefined;
|
|
75
|
+
if (!jwk)
|
|
76
|
+
return { valid: false, reason: 'no_matching_key' };
|
|
77
|
+
const verifier = nodeCrypto.createVerify(`RSA-SHA${String(decoded.header.alg).slice(2)}`);
|
|
78
|
+
verifier.update(`${h}.${p}`);
|
|
79
|
+
verifier.end();
|
|
80
|
+
const publicKey = nodeCrypto.createPublicKey({ key: { kty: 'RSA', n: jwk.n, e: jwk.e }, format: 'jwk' });
|
|
81
|
+
if (!verifier.verify(publicKey, fromBase64url(s)))
|
|
82
|
+
return { valid: false, reason: 'bad_signature' };
|
|
83
|
+
return timely(decoded.payload, opts.now);
|
|
84
|
+
}
|
|
85
|
+
/** The JWKS that verifies every token a public key's pair signs. */
|
|
86
|
+
export function jwks(publicPem, kidPrefix) {
|
|
87
|
+
const jwk = nodeCrypto.createPublicKey(publicPem).export({ format: 'jwk' });
|
|
88
|
+
return { keys: [{ kty: 'RSA', use: 'sig', alg: 'RS256', kid: keyId(publicPem, kidPrefix), n: String(jwk.n), e: String(jwk.e) }] };
|
|
89
|
+
}
|
|
90
|
+
/** An HMAC of a value, hex or base64 (a webhook's signature, a signed id): SHA-256 unless the vendor names another
|
|
91
|
+
* (OAuth 1.0a's HMAC-SHA1). The secret is its text, or its bytes where the vendor keys with a decoded secret (Svix's
|
|
92
|
+
* `whsec_` base64). */
|
|
93
|
+
export const hmac = (secret, value, encoding = 'hex', algorithm = 'sha256') => {
|
|
94
|
+
const bytes = nodeCrypto.createHmac(algorithm, secret).update(value).digest();
|
|
95
|
+
return encoding === 'base64url' ? base64url(bytes) : bytes.toString(encoding);
|
|
96
|
+
};
|
|
97
|
+
/** A SHA-256, hex (a key stored by its hash, a deterministic id). */
|
|
98
|
+
export const sha256 = (value) => nodeCrypto.createHash('sha256').update(value).digest('hex');
|
|
99
|
+
/** A digest of a value in the algorithm and encoding a vendor names (GoTrue's SHA-224 token hashes, a PKCE challenge's
|
|
100
|
+
* base64url SHA-256). */
|
|
101
|
+
export function digest(algorithm, value, encoding = 'hex') {
|
|
102
|
+
const bytes = nodeCrypto.createHash(algorithm).update(value).digest();
|
|
103
|
+
return encoding === 'base64url' ? base64url(bytes) : bytes.toString(encoding);
|
|
104
|
+
}
|
|
105
|
+
// ── OAuth 1.0a (RFC 5849): the signature a request carries, which the vendor checks against the secrets it holds ──────
|
|
106
|
+
/** RFC 3986 percent-encoding as RFC 5849 §3.6 uses it (unreserved: A-Z a-z 0-9 - . _ ~). */
|
|
107
|
+
export const oauth1Encode = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
108
|
+
/** The `Authorization: OAuth k="v", …` header's parameters, decoded, in order; undefined when it is not one. */
|
|
109
|
+
export function oauth1Header(value) {
|
|
110
|
+
const m = value ? /^OAuth\s+(.*)$/is.exec(value.trim()) : null;
|
|
111
|
+
if (!m)
|
|
112
|
+
return undefined;
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const part of m[1].split(',')) {
|
|
115
|
+
const kv = /^\s*([^=\s]+)\s*=\s*"([^"]*)"\s*$/.exec(part);
|
|
116
|
+
if (!kv) {
|
|
117
|
+
if (part.trim() === '')
|
|
118
|
+
continue;
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
out.push([decodeURIComponent(kv[1]), decodeURIComponent(kv[2])]);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/** The signature base string (RFC 5849 §3.4.1): the method, the base URL (no query) and the parameters signed, each
|
|
131
|
+
* percent-encoded and sorted by name and value. */
|
|
132
|
+
export function oauth1BaseString(method, baseUrl, pairs) {
|
|
133
|
+
const encoded = pairs.map(([k, v]) => [oauth1Encode(k), oauth1Encode(v)]).sort((a, b) => (a[0] === b[0] ? (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0) : a[0] < b[0] ? -1 : 1));
|
|
134
|
+
return `${method.toUpperCase()}&${oauth1Encode(baseUrl)}&${oauth1Encode(encoded.map(([k, v]) => `${k}=${v}`).join('&'))}`;
|
|
135
|
+
}
|
|
136
|
+
/** The HMAC-SHA1 signature (RFC 5849 §3.4.2) over a base string, keyed by the consumer secret and the token secret. */
|
|
137
|
+
export const oauth1Signature = (base, consumerSecret, tokenSecret = '') => hmac(`${oauth1Encode(consumerSecret)}&${oauth1Encode(tokenSecret)}`, base, 'base64', 'sha1');
|
|
138
|
+
/** A digest of a value as its bytes (a key or an id drawn from a hash's bytes, a deterministic embedding). */
|
|
139
|
+
export function digestBytes(algorithm, value) {
|
|
140
|
+
return new Uint8Array(nodeCrypto.createHash(algorithm).update(value).digest());
|
|
141
|
+
}
|
|
142
|
+
/** An MD5, hex (an object's ETag, as S3 and storage APIs give it); never a secret's protection. */
|
|
143
|
+
export const md5 = (value) => nodeCrypto.createHash('md5').update(value).digest('hex');
|
|
144
|
+
/** A version-4-shaped UUID derived from `seed`: the same seed gives the same id every run (R9). */
|
|
145
|
+
export function uuidFrom(seed) {
|
|
146
|
+
const h = sha256(seed);
|
|
147
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${'89ab'[Number.parseInt(h[16], 16) % 4]}${h.slice(17, 20)}-${h.slice(20, 32)}`;
|
|
148
|
+
}
|
|
149
|
+
/** `count` lowercase letters derived from `seed` (a 20-letter project ref, a slug's suffix), the same every run. */
|
|
150
|
+
export function lettersFrom(seed, count = 20) {
|
|
151
|
+
let out = '';
|
|
152
|
+
for (let round = 0; out.length < count; round += 1) {
|
|
153
|
+
for (const byte of sha256(`${seed}:${round}`).match(/../g)) {
|
|
154
|
+
if (out.length === count)
|
|
155
|
+
break;
|
|
156
|
+
out += String.fromCharCode(97 + (Number.parseInt(byte, 16) % 26));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
/** Two strings compared in constant time. */
|
|
162
|
+
export function equalSecrets(a, b) {
|
|
163
|
+
const left = Buffer.from(a, 'utf8');
|
|
164
|
+
const right = Buffer.from(b, 'utf8');
|
|
165
|
+
return left.length === right.length && nodeCrypto.timingSafeEqual(left, right);
|
|
166
|
+
}
|
|
167
|
+
/** A key's type, from its PEM: the schemes the kernel signs with are the deterministic ones. */
|
|
168
|
+
function signingKey(privatePem) {
|
|
169
|
+
const key = nodeCrypto.createPrivateKey(privatePem);
|
|
170
|
+
// ECDSA draws a random nonce for every signature, so the same write signs differently on every run: a World signs
|
|
171
|
+
// with RSA (PKCS #1 v1.5) or Ed25519, whose signatures are functions of the key and the bytes
|
|
172
|
+
if (key.asymmetricKeyType !== 'rsa' && key.asymmetricKeyType !== 'ed25519')
|
|
173
|
+
throw new Error(`signWith: a ${key.asymmetricKeyType} key signs randomly; a World signs with an RSA or Ed25519 key`);
|
|
174
|
+
return key;
|
|
175
|
+
}
|
|
176
|
+
/** Bytes signed with a private key (RSA with SHA-256, or Ed25519), in the encoding asked: a certificate's signature,
|
|
177
|
+
* a transparency log's checkpoint. */
|
|
178
|
+
export function signWith(privatePem, data, encoding = 'base64') {
|
|
179
|
+
const key = signingKey(privatePem);
|
|
180
|
+
const bytes = nodeCrypto.sign(key.asymmetricKeyType === 'ed25519' ? null : 'sha256', typeof data === 'string' ? Buffer.from(data) : data, key);
|
|
181
|
+
return encoding === 'base64url' ? base64url(bytes) : bytes.toString(encoding);
|
|
182
|
+
}
|
|
183
|
+
/** Whether a signature holds over bytes under a public key or a certificate's (PEM, or a certificate's DER): any
|
|
184
|
+
* scheme a vendor uses (ECDSA, RSA, Ed25519), since verifying draws nothing. The digest is SHA-256 unless named. */
|
|
185
|
+
export function verifyWith(publicKey, data, signature, opts = {}) {
|
|
186
|
+
try {
|
|
187
|
+
const key = typeof publicKey !== 'string' || publicKey.includes('CERTIFICATE') ? new nodeCrypto.X509Certificate(typeof publicKey === 'string' ? publicKey : Buffer.from(publicKey)).publicKey : nodeCrypto.createPublicKey(publicKey);
|
|
188
|
+
const sig = typeof signature === 'string' ? Buffer.from(signature, opts.encoding ?? 'base64') : signature;
|
|
189
|
+
return nodeCrypto.verify(key.asymmetricKeyType === 'ed25519' ? null : (opts.digest ?? 'sha256'), typeof data === 'string' ? Buffer.from(data) : data, key, sig);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** A public key's PEM and its DER (SubjectPublicKeyInfo), from a public or private key's PEM or a certificate. */
|
|
196
|
+
export function publicKeyOf(key) {
|
|
197
|
+
const k = key.includes('CERTIFICATE') ? new nodeCrypto.X509Certificate(key).publicKey : nodeCrypto.createPublicKey(key);
|
|
198
|
+
return { pem: String(k.export({ type: 'spki', format: 'pem' })), der: new Uint8Array(k.export({ type: 'spki', format: 'der' })) };
|
|
199
|
+
}
|
|
200
|
+
/** An X.509 certificate read (PEM or DER): its bytes, names, validity and key, and whether an issuer's key signed it;
|
|
201
|
+
* undefined when it is not one. */
|
|
202
|
+
export function certificateOf(cert) {
|
|
203
|
+
try {
|
|
204
|
+
const c = new nodeCrypto.X509Certificate(typeof cert === 'string' ? cert : Buffer.from(cert));
|
|
205
|
+
return {
|
|
206
|
+
der: new Uint8Array(c.raw), subject: c.subject, issuer: c.issuer, subjectAltName: c.subjectAltName,
|
|
207
|
+
notBefore: new Date(c.validFrom).toISOString(), notAfter: new Date(c.validTo).toISOString(),
|
|
208
|
+
publicKey: publicKeyOf(c.toString()),
|
|
209
|
+
issuedBy: (issuer) => { try {
|
|
210
|
+
return c.verify(new nodeCrypto.X509Certificate(typeof issuer === 'string' ? issuer : Buffer.from(issuer)).publicKey);
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return false;
|
|
214
|
+
} },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/** 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 };
|