@volter/world-core 2.0.37 → 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.
Files changed (205) hide show
  1. package/README.md +4 -5
  2. package/app-route.cjs +12 -6
  3. package/app-route.d.cts +1 -1
  4. package/dist/app-route.cjs +12 -6
  5. package/dist/app-route.d.cts +1 -1
  6. package/dist/generated/pack-facts.json +1410 -3069
  7. package/dist/inject.cjs +64 -9
  8. package/dist/pack-facts.cjs +44 -0
  9. package/dist/src/actions.d.ts +3 -3
  10. package/dist/src/actions.js +22 -16
  11. package/dist/src/ancestry.d.ts +14 -2
  12. package/dist/src/ancestry.js +92 -2
  13. package/dist/src/anthropic-wire.d.ts +39 -0
  14. package/dist/src/anthropic-wire.js +136 -0
  15. package/dist/src/bytes.d.ts +7 -0
  16. package/dist/src/bytes.js +35 -0
  17. package/dist/src/changeset.d.ts +1 -1
  18. package/dist/src/changeset.js +0 -0
  19. package/dist/src/clickhouse/index.d.ts +3 -0
  20. package/dist/src/clickhouse/index.js +6 -0
  21. package/dist/src/clickhouse/sql.d.ts +233 -0
  22. package/dist/src/clickhouse/sql.js +4329 -0
  23. package/dist/src/clickhouse/types.d.ts +18 -0
  24. package/dist/src/clickhouse/types.js +47 -0
  25. package/dist/src/clickhouse/values.d.ts +146 -0
  26. package/dist/src/clickhouse/values.js +858 -0
  27. package/dist/src/client-bundle.js +2 -3
  28. package/dist/src/cors.d.ts +15 -0
  29. package/dist/src/cors.js +31 -0
  30. package/dist/src/derived-core.d.ts +487 -24
  31. package/dist/src/derived-core.js +788 -144
  32. package/dist/src/derived-real.d.ts +13 -0
  33. package/dist/src/derived-real.js +518 -0
  34. package/dist/src/derived.d.ts +35 -1
  35. package/dist/src/derived.js +61 -9
  36. package/dist/src/emit.js +1 -2
  37. package/dist/src/events.d.ts +206 -0
  38. package/dist/src/events.js +341 -0
  39. package/dist/src/executor.d.ts +3 -0
  40. package/dist/src/executor.js +19 -2
  41. package/dist/src/file-response.d.ts +6 -0
  42. package/dist/src/file-response.js +30 -0
  43. package/dist/src/fork.js +3 -2
  44. package/dist/src/git/history.d.ts +7 -0
  45. package/dist/src/git/history.js +24 -0
  46. package/dist/src/git/index.d.ts +1 -0
  47. package/dist/src/git/index.js +1 -0
  48. package/dist/src/git/lfs.d.ts +28 -0
  49. package/dist/src/git/lfs.js +66 -0
  50. package/dist/src/git/objects.js +3 -8
  51. package/dist/src/git/smart-http.d.ts +3 -1
  52. package/dist/src/git/smart-http.js +67 -6
  53. package/dist/src/graphql-wire.d.ts +29 -0
  54. package/dist/src/graphql-wire.js +101 -0
  55. package/dist/src/grpc-wire.d.ts +67 -0
  56. package/dist/src/grpc-wire.js +170 -0
  57. package/dist/src/h2.d.ts +40 -0
  58. package/dist/src/h2.js +656 -0
  59. package/dist/src/head.d.ts +32 -3
  60. package/dist/src/head.js +161 -40
  61. package/dist/src/history.d.ts +1 -1
  62. package/dist/src/history.js +6 -6
  63. package/dist/src/hpack.json +1 -0
  64. package/dist/src/index.d.ts +64 -75
  65. package/dist/src/index.js +58 -101
  66. package/dist/src/log.js +28 -19
  67. package/dist/src/machines.d.ts +50 -0
  68. package/dist/src/machines.js +151 -0
  69. package/dist/src/managed-database.d.ts +86 -0
  70. package/dist/src/managed-database.js +283 -0
  71. package/dist/src/multipart.d.ts +11 -0
  72. package/dist/src/multipart.js +51 -0
  73. package/dist/src/observe.d.ts +15 -5
  74. package/dist/src/observe.js +23 -9
  75. package/dist/src/openai-wire.d.ts +108 -0
  76. package/dist/src/openai-wire.js +337 -0
  77. package/dist/src/pack-assets.d.ts +3 -4
  78. package/dist/src/pack-assets.js +15 -10
  79. package/dist/src/pack-fetch.d.ts +77 -0
  80. package/dist/src/pack-fetch.js +449 -0
  81. package/dist/src/pack-paths.d.ts +12 -0
  82. package/dist/src/pack-paths.js +86 -0
  83. package/dist/src/packRegistry.d.ts +69 -162
  84. package/dist/src/packRegistry.js +55 -20
  85. package/dist/src/people.d.ts +13 -0
  86. package/dist/src/people.js +18 -0
  87. package/dist/src/placeholder-image.d.ts +5 -0
  88. package/dist/src/placeholder-image.js +114 -0
  89. package/dist/src/protobuf.d.ts +28 -0
  90. package/dist/src/protobuf.js +332 -0
  91. package/dist/src/redis/engine.js +1 -1
  92. package/dist/src/request-scope.d.ts +1 -1
  93. package/dist/src/request-scope.js +6 -4
  94. package/dist/src/resource-blob.d.ts +5 -0
  95. package/dist/src/resource-blob.js +11 -0
  96. package/dist/src/runtime.d.ts +85 -0
  97. package/dist/src/runtime.js +104 -0
  98. package/dist/src/s3/wire.d.ts +60 -0
  99. package/dist/src/s3/wire.js +157 -0
  100. package/dist/src/scenario.d.ts +3 -0
  101. package/dist/src/scenario.js +2 -0
  102. package/dist/src/schema-sample.d.ts +1 -0
  103. package/dist/src/schema-sample.js +21 -0
  104. package/dist/src/sealed-box.d.ts +14 -0
  105. package/dist/src/sealed-box.js +225 -0
  106. package/dist/src/serve-http.d.ts +14 -0
  107. package/dist/src/serve-http.js +27 -3
  108. package/dist/src/serve.d.ts +6 -0
  109. package/dist/src/serve.js +69 -14
  110. package/dist/src/signing.d.ts +135 -0
  111. package/dist/src/signing.js +222 -0
  112. package/dist/src/sigv4.d.ts +48 -0
  113. package/dist/src/sigv4.js +167 -0
  114. package/dist/src/smtp.d.ts +16 -0
  115. package/dist/src/smtp.js +72 -0
  116. package/dist/src/sockets.d.ts +51 -0
  117. package/dist/src/sockets.js +90 -0
  118. package/dist/src/state-system.d.ts +1 -0
  119. package/dist/src/state-system.js +1 -1
  120. package/dist/src/storage.d.ts +1 -1
  121. package/dist/src/storage.js +3 -3
  122. package/dist/src/trace-context.js +1 -1
  123. package/dist/src/twin-fetch.d.ts +0 -7
  124. package/dist/src/twin-fetch.js +0 -14
  125. package/dist/src/vendor-call.d.ts +6 -0
  126. package/dist/src/vendor-call.js +41 -0
  127. package/dist/src/world-store.js +1 -1
  128. package/dist/vendor-hosts.cjs +36 -125
  129. package/dist/vendor-hosts.d.cts +8 -0
  130. package/generated/pack-facts.json +1410 -3069
  131. package/inject.cjs +64 -9
  132. package/pack-facts.cjs +44 -0
  133. package/package.json +17 -3
  134. package/src/actions.ts +23 -16
  135. package/src/ancestry.ts +74 -2
  136. package/src/anthropic-wire.ts +137 -0
  137. package/src/bytes.ts +42 -0
  138. package/src/changeset.ts +5 -5
  139. package/src/clickhouse/index.ts +6 -0
  140. package/src/clickhouse/sql.ts +3059 -0
  141. package/src/clickhouse/types.ts +44 -0
  142. package/src/clickhouse/values.ts +697 -0
  143. package/src/client-bundle.ts +2 -3
  144. package/src/cors.ts +34 -0
  145. package/src/derived-core.ts +1013 -146
  146. package/src/derived-real.ts +434 -0
  147. package/src/derived.ts +73 -3
  148. package/src/emit.ts +1 -2
  149. package/src/events.ts +449 -0
  150. package/src/executor.ts +24 -2
  151. package/src/file-response.ts +27 -0
  152. package/src/fork.ts +3 -2
  153. package/src/git/history.ts +19 -0
  154. package/src/git/index.ts +1 -0
  155. package/src/git/lfs.ts +67 -0
  156. package/src/git/objects.ts +3 -5
  157. package/src/git/smart-http.ts +56 -6
  158. package/src/graphql-wire.ts +106 -0
  159. package/src/grpc-wire.ts +159 -0
  160. package/src/h2.ts +627 -0
  161. package/src/head.ts +132 -41
  162. package/src/history.ts +6 -6
  163. package/src/hpack.json +1 -0
  164. package/src/index.ts +82 -329
  165. package/src/log.ts +27 -18
  166. package/src/machines.ts +151 -0
  167. package/src/managed-database.ts +299 -0
  168. package/src/multipart.ts +51 -0
  169. package/src/observe.ts +31 -15
  170. package/src/openai-wire.ts +371 -0
  171. package/src/pack-assets.ts +15 -11
  172. package/src/pack-fetch.ts +458 -0
  173. package/src/pack-paths.ts +72 -0
  174. package/src/packRegistry.ts +79 -167
  175. package/src/people.ts +31 -0
  176. package/src/placeholder-image.ts +88 -0
  177. package/src/protobuf.ts +251 -0
  178. package/src/redis/engine.ts +1 -1
  179. package/src/request-scope.ts +8 -4
  180. package/src/resource-blob.ts +13 -0
  181. package/src/runtime.ts +344 -0
  182. package/src/s3/wire.ts +172 -0
  183. package/src/scenario.ts +4 -0
  184. package/src/schema-sample.ts +24 -0
  185. package/src/sealed-box.ts +182 -0
  186. package/src/serve-http.ts +31 -3
  187. package/src/serve.ts +58 -14
  188. package/src/signing.ts +231 -0
  189. package/src/sigv4.ts +158 -0
  190. package/src/smtp.ts +76 -0
  191. package/src/sockets.ts +140 -0
  192. package/src/state-system.ts +2 -2
  193. package/src/storage.ts +3 -3
  194. package/src/trace-context.ts +1 -1
  195. package/src/twin-fetch.ts +0 -20
  196. package/src/vendor-call.ts +41 -0
  197. package/src/world-store.ts +1 -1
  198. package/vendor-hosts.cjs +36 -125
  199. package/vendor-hosts.d.cts +8 -0
  200. package/dist/src/mirror-shell.d.ts +0 -2
  201. package/dist/src/mirror-shell.js +0 -13
  202. package/dist/src/v1-removed.d.ts +0 -159
  203. package/dist/src/v1-removed.js +0 -124
  204. package/src/mirror-shell.ts +0 -15
  205. package/src/v1-removed.ts +0 -172
@@ -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
+ }>;
@@ -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 mirror-UI client bundles through the kernel's
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 = new Request(`${protocol}://${req.headers.host ?? 'localhost'}${req.url ?? '/'}`, { headers: Object.fromEntries(Object.entries(req.headers).filter((entry) => typeof entry[1] === 'string')) });
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;
@@ -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: `datadog` under `/tmp/world/data/datadog`. It is cached per server, so only
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 (`packages/twin/<vendor>/…` or `@volter/twin-<vendor>`) and the process's `--root`
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, and this is load-bearing rather than style. Mirror-UI packs import their own
252
- * server module into the BROWSER bundle and rely on Bun tree-shaking the server-only half away
253
- * (see any `*-mirror-ui.ts` header). A module-scope `new AsyncLocalStorage()` is a side effect,
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 = /(?:packages[\\/]twin[\\/]|@volter[\\/]twin-)([A-Za-z0-9_-]+)/g;
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 (a mirror UI carrying the kernel): nothing to wrap, nothing to touch
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 caller = request.headers.get(CALLER_HEADER) ?? undefined;
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/adding-a-twin.md
505
- // #5-build-on-the-shared-kernel--dont-reinvent, "A local write is an occurrence".
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 };