@volter/world-core 2.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 (180) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +29 -0
  3. package/app-route.cjs +154 -0
  4. package/app-route.d.cts +7 -0
  5. package/attach.cjs +80 -0
  6. package/dist/app-route.cjs +154 -0
  7. package/dist/app-route.d.cts +7 -0
  8. package/dist/attach.cjs +80 -0
  9. package/dist/generated/pack-facts.json +4306 -0
  10. package/dist/inject.cjs +1097 -0
  11. package/dist/network-policy.cjs +92 -0
  12. package/dist/network-policy.d.cts +10 -0
  13. package/dist/src/actions.d.ts +276 -0
  14. package/dist/src/actions.js +436 -0
  15. package/dist/src/ancestry.d.ts +22 -0
  16. package/dist/src/ancestry.js +238 -0
  17. package/dist/src/args.d.ts +3 -0
  18. package/dist/src/args.js +12 -0
  19. package/dist/src/blob-store.d.ts +55 -0
  20. package/dist/src/blob-store.js +186 -0
  21. package/dist/src/brand-tokens.d.ts +2 -0
  22. package/dist/src/brand-tokens.js +17 -0
  23. package/dist/src/changeset.d.ts +431 -0
  24. package/dist/src/changeset.js +0 -0
  25. package/dist/src/client-bundle.d.ts +1 -0
  26. package/dist/src/client-bundle.js +28 -0
  27. package/dist/src/credential.d.ts +38 -0
  28. package/dist/src/credential.js +114 -0
  29. package/dist/src/derived-core.d.ts +452 -0
  30. package/dist/src/derived-core.js +782 -0
  31. package/dist/src/derived.d.ts +84 -0
  32. package/dist/src/derived.js +122 -0
  33. package/dist/src/emit.d.ts +106 -0
  34. package/dist/src/emit.js +157 -0
  35. package/dist/src/executor.d.ts +120 -0
  36. package/dist/src/executor.js +387 -0
  37. package/dist/src/file-response.d.ts +3 -0
  38. package/dist/src/file-response.js +22 -0
  39. package/dist/src/fork.d.ts +26 -0
  40. package/dist/src/fork.js +68 -0
  41. package/dist/src/git/history.d.ts +36 -0
  42. package/dist/src/git/history.js +298 -0
  43. package/dist/src/git/index.d.ts +6 -0
  44. package/dist/src/git/index.js +6 -0
  45. package/dist/src/git/inflate.d.ts +11 -0
  46. package/dist/src/git/inflate.js +194 -0
  47. package/dist/src/git/objects.d.ts +64 -0
  48. package/dist/src/git/objects.js +161 -0
  49. package/dist/src/git/pack.d.ts +14 -0
  50. package/dist/src/git/pack.js +199 -0
  51. package/dist/src/git/refs.d.ts +19 -0
  52. package/dist/src/git/refs.js +35 -0
  53. package/dist/src/git/smart-http.d.ts +45 -0
  54. package/dist/src/git/smart-http.js +223 -0
  55. package/dist/src/hash.d.ts +38 -0
  56. package/dist/src/hash.js +48 -0
  57. package/dist/src/head.d.ts +140 -0
  58. package/dist/src/head.js +313 -0
  59. package/dist/src/history.d.ts +76 -0
  60. package/dist/src/history.js +322 -0
  61. package/dist/src/index.d.ts +73 -0
  62. package/dist/src/index.js +98 -0
  63. package/dist/src/lifecycle.d.ts +1 -0
  64. package/dist/src/lifecycle.js +8 -0
  65. package/dist/src/log.d.ts +254 -0
  66. package/dist/src/log.js +801 -0
  67. package/dist/src/mirror-shell.d.ts +2 -0
  68. package/dist/src/mirror-shell.js +13 -0
  69. package/dist/src/observe.d.ts +49 -0
  70. package/dist/src/observe.js +148 -0
  71. package/dist/src/pack-assets.d.ts +30 -0
  72. package/dist/src/pack-assets.js +88 -0
  73. package/dist/src/packRegistry.d.ts +374 -0
  74. package/dist/src/packRegistry.js +142 -0
  75. package/dist/src/placeholder-remote.d.ts +22 -0
  76. package/dist/src/placeholder-remote.js +86 -0
  77. package/dist/src/proxy.d.ts +25 -0
  78. package/dist/src/proxy.js +155 -0
  79. package/dist/src/rateBudget.d.ts +367 -0
  80. package/dist/src/rateBudget.js +925 -0
  81. package/dist/src/references.d.ts +18 -0
  82. package/dist/src/references.js +27 -0
  83. package/dist/src/remote-execute.d.ts +22 -0
  84. package/dist/src/remote-execute.js +1 -0
  85. package/dist/src/resource-blob.d.ts +10 -0
  86. package/dist/src/resource-blob.js +56 -0
  87. package/dist/src/scenario.d.ts +197 -0
  88. package/dist/src/scenario.js +425 -0
  89. package/dist/src/schemas.d.ts +78 -0
  90. package/dist/src/schemas.js +50 -0
  91. package/dist/src/serve-http.d.ts +48 -0
  92. package/dist/src/serve-http.js +340 -0
  93. package/dist/src/serve.d.ts +147 -0
  94. package/dist/src/serve.js +507 -0
  95. package/dist/src/shared-blob-index.d.ts +4 -0
  96. package/dist/src/shared-blob-index.js +126 -0
  97. package/dist/src/state-system.d.ts +70 -0
  98. package/dist/src/state-system.js +90 -0
  99. package/dist/src/storage.d.ts +101 -0
  100. package/dist/src/storage.js +337 -0
  101. package/dist/src/twin-fetch.d.ts +64 -0
  102. package/dist/src/twin-fetch.js +91 -0
  103. package/dist/src/types.d.ts +40 -0
  104. package/dist/src/types.js +1 -0
  105. package/dist/src/v1-removed.d.ts +159 -0
  106. package/dist/src/v1-removed.js +124 -0
  107. package/dist/src/volter-home.d.ts +5 -0
  108. package/dist/src/volter-home.js +10 -0
  109. package/dist/src/world-clock.d.ts +4 -0
  110. package/dist/src/world-clock.js +32 -0
  111. package/dist/src/world-env.d.ts +3 -0
  112. package/dist/src/world-env.js +22 -0
  113. package/dist/src/world-store-sql.d.ts +27 -0
  114. package/dist/src/world-store-sql.js +86 -0
  115. package/dist/src/world-store.d.ts +168 -0
  116. package/dist/src/world-store.js +475 -0
  117. package/dist/src/worldConfig.d.ts +9 -0
  118. package/dist/src/worldConfig.js +17 -0
  119. package/dist/stream-bridge.cjs +80 -0
  120. package/dist/vendor-hosts.cjs +200 -0
  121. package/generated/pack-facts.json +4306 -0
  122. package/inject.cjs +1097 -0
  123. package/network-policy.cjs +92 -0
  124. package/network-policy.d.cts +10 -0
  125. package/package.json +103 -0
  126. package/src/actions.ts +564 -0
  127. package/src/ancestry.ts +213 -0
  128. package/src/args.ts +14 -0
  129. package/src/blob-store.ts +185 -0
  130. package/src/brand-tokens.ts +17 -0
  131. package/src/changeset.ts +1032 -0
  132. package/src/client-bundle.ts +29 -0
  133. package/src/credential.ts +140 -0
  134. package/src/derived-core.ts +1004 -0
  135. package/src/derived.ts +176 -0
  136. package/src/emit.ts +242 -0
  137. package/src/executor.ts +431 -0
  138. package/src/file-response.ts +22 -0
  139. package/src/fork.ts +89 -0
  140. package/src/git/history.ts +177 -0
  141. package/src/git/index.ts +6 -0
  142. package/src/git/inflate.ts +125 -0
  143. package/src/git/objects.ts +110 -0
  144. package/src/git/pack.ts +105 -0
  145. package/src/git/refs.ts +25 -0
  146. package/src/git/smart-http.ts +149 -0
  147. package/src/hash.ts +66 -0
  148. package/src/head.ts +318 -0
  149. package/src/history.ts +246 -0
  150. package/src/index.ts +323 -0
  151. package/src/lifecycle.ts +8 -0
  152. package/src/log.ts +793 -0
  153. package/src/mirror-shell.ts +15 -0
  154. package/src/observe.ts +130 -0
  155. package/src/pack-assets.ts +81 -0
  156. package/src/packRegistry.ts +408 -0
  157. package/src/placeholder-remote.ts +81 -0
  158. package/src/proxy.ts +183 -0
  159. package/src/rateBudget.ts +1115 -0
  160. package/src/references.ts +46 -0
  161. package/src/remote-execute.ts +26 -0
  162. package/src/resource-blob.ts +57 -0
  163. package/src/scenario.ts +479 -0
  164. package/src/schemas.ts +56 -0
  165. package/src/serve-http.ts +299 -0
  166. package/src/serve.ts +618 -0
  167. package/src/shared-blob-index.ts +108 -0
  168. package/src/state-system.ts +115 -0
  169. package/src/storage.ts +407 -0
  170. package/src/twin-fetch.ts +147 -0
  171. package/src/types.ts +50 -0
  172. package/src/v1-removed.ts +172 -0
  173. package/src/volter-home.ts +11 -0
  174. package/src/world-clock.ts +33 -0
  175. package/src/world-env.ts +18 -0
  176. package/src/world-store-sql.ts +118 -0
  177. package/src/world-store.ts +572 -0
  178. package/src/worldConfig.ts +27 -0
  179. package/stream-bridge.cjs +80 -0
  180. package/vendor-hosts.cjs +200 -0
@@ -0,0 +1,149 @@
1
+ // SMART HTTP (gitprotocol-http, protocol v0): what `git clone|fetch|push` speak to a server. The
2
+ // twin answers the advertisement (`info/refs?service=`), upload-pack (wants, haves, done → NAK
3
+ // and a pack of the closure the client lacks) and receive-pack (ref commands, a pack, a
4
+ // report-status) with nothing but the object store, the refs and a push policy. Protocol v2 is
5
+ // not advertised; a client that asks for it falls back to v0 on this answer, as git specifies.
6
+ import { GitObjectStore, decodeCommit, decodeTag, decodeTree, type GitObjectType } from './objects.ts';
7
+ import { readPack, writePack } from './pack.ts';
8
+ import type { GitRefs } from './refs.ts';
9
+
10
+ const ENC = new TextEncoder(); const DEC = new TextDecoder();
11
+ export const ZERO_SHA = '0'.repeat(40);
12
+ export const FLUSH = new Uint8Array([0x30, 0x30, 0x30, 0x30]);
13
+ export function pktLine(s: string | Uint8Array): Uint8Array {
14
+ const body = typeof s === 'string' ? ENC.encode(s) : s; const len = (body.length + 4).toString(16).padStart(4, '0');
15
+ const out = new Uint8Array(body.length + 4); out.set(ENC.encode(len), 0); out.set(body, 4); return out;
16
+ }
17
+ /** Pkt-lines up to and including the first flush; `rest` is what follows (a pack, on push). */
18
+ export function readPktLines(bytes: Uint8Array): { lines: string[]; rest: Uint8Array } {
19
+ const lines: string[] = []; let at = 0;
20
+ while (at + 4 <= bytes.length) {
21
+ const len = parseInt(DEC.decode(bytes.subarray(at, at + 4)), 16);
22
+ if (Number.isNaN(len)) return { lines, rest: bytes.subarray(at) };
23
+ if (len === 0) return { lines, rest: bytes.subarray(at + 4) };
24
+ if (len < 4) { at += 4; continue; } // v2 delimiter/response-end packets: no payload
25
+ lines.push(DEC.decode(bytes.subarray(at + 4, at + len))); at += len;
26
+ }
27
+ return { lines, rest: bytes.subarray(at) };
28
+ }
29
+ /** Every pkt-line in the body across all flushes (stateless clients batch haves between flushes); `rest` follows the first flush. */
30
+ export function readAllPktLines(bytes: Uint8Array): { lines: string[]; rest: Uint8Array } {
31
+ const first = readPktLines(bytes); const lines = [...first.lines]; let cur = first.rest;
32
+ while (cur.length >= 4 && /^[0-9a-f]{4}$/.test(DEC.decode(cur.subarray(0, 4))) && !cur.subarray(0, 4).every((b, i) => b === 'PACK'.charCodeAt(i))) { const n = readPktLines(cur); lines.push(...n.lines); if (n.rest.length === cur.length) break; cur = n.rest; }
33
+ return { lines, rest: first.rest };
34
+ }
35
+ const SHA = /^[0-9a-f]{40}$/;
36
+ function cat(...parts: Uint8Array[]): Uint8Array { const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); let o = 0; for (const p of parts) { out.set(p, o); o += p.length; } return out; }
37
+
38
+ export type Service = 'git-upload-pack' | 'git-receive-pack';
39
+ const CAPS: Record<Service, string> = { 'git-upload-pack': 'ofs-delta no-progress agent=volter-twin', 'git-receive-pack': 'report-status delete-refs ofs-delta agent=volter-twin' };
40
+
41
+ /** `GET info/refs?service=<service>`: the ref advertisement, HEAD first as a symref. */
42
+ export function advertisement(service: Service, refs: GitRefs): Uint8Array {
43
+ const all = refs.list(); const head = refs.head(); const headSha = refs.get(head);
44
+ const caps = `${CAPS[service]} symref=HEAD:${head}`;
45
+ const lines: Uint8Array[] = [pktLine(`# service=${service}\n`), FLUSH];
46
+ if (headSha) lines.push(pktLine(`${headSha} HEAD\0${caps}\n`));
47
+ const rest = all;
48
+ if (!headSha && rest.length === 0) lines.push(pktLine(`${ZERO_SHA} capabilities^{}\0${caps}\n`));
49
+ let first = headSha === null;
50
+ for (const r of rest) { lines.push(pktLine(first ? `${r.sha} ${r.name}\0${caps}\n` : `${r.sha} ${r.name}\n`)); first = false; }
51
+ lines.push(FLUSH);
52
+ return cat(...lines);
53
+ }
54
+
55
+ /** Every object reachable from `roots`, stopping at `seen`. */
56
+ async function closure(store: GitObjectStore, roots: string[], seen: Set<string>): Promise<Array<{ type: GitObjectType; payload: Uint8Array; sha: string }>> {
57
+ const out: Array<{ type: GitObjectType; payload: Uint8Array; sha: string }> = []; const stack = [...roots];
58
+ while (stack.length) {
59
+ const sha = stack.pop()!; if (seen.has(sha)) continue; seen.add(sha);
60
+ const obj = await store.read(sha); if (!obj) continue;
61
+ out.push({ ...obj, sha });
62
+ if (obj.type === 'commit') { const c = decodeCommit(obj.payload); stack.push(c.tree, ...c.parents); }
63
+ else if (obj.type === 'tree') { for (const e of decodeTree(obj.payload)) if (e.mode !== '160000') stack.push(e.sha); }
64
+ else if (obj.type === 'tag') stack.push(decodeTag(obj.payload).object);
65
+ }
66
+ return out;
67
+ }
68
+ async function reachable(store: GitObjectStore, roots: string[]): Promise<Set<string>> { const seen = new Set<string>(); await closure(store, roots, seen); return seen; }
69
+
70
+ /** `POST git-upload-pack`: wants and haves in, NAK and (once `done`) the pack out. */
71
+ export async function uploadPack(body: Uint8Array, store: GitObjectStore): Promise<Uint8Array> {
72
+ const { lines } = readAllPktLines(body);
73
+ const wants = lines.filter((l) => l.startsWith('want ')).map((l) => l.slice(5, 45)).filter((s) => SHA.test(s));
74
+ const haves = lines.filter((l) => l.startsWith('have ')).map((l) => l.slice(5, 45)).filter((s) => SHA.test(s));
75
+ const done = lines.some((l) => l.trim() === 'done');
76
+ const known: string[] = []; for (const h of haves) if (await store.has(h)) known.push(h);
77
+ for (const w of wants) if (!(await store.has(w))) return pktLine(`ERR upload-pack: not our ref ${w}\n`);
78
+ if (!done) return pktLine('NAK\n');
79
+ const seen = await reachable(store, known); const objects = await closure(store, wants, seen);
80
+ return cat(pktLine('NAK\n'), await writePack(objects));
81
+ }
82
+
83
+ export type RefCommand = { old: string; new: string; name: string; /** false when `new` does not descend from `old` (a force push); true for creates, deletes and fast-forwards. */ fastForward: boolean };
84
+ /** Bounded ancestry walk: is `ancestor` reachable from `sha` through parents? */
85
+ export async function isAncestor(store: GitObjectStore, ancestor: string, sha: string, limit = 5000): Promise<boolean> {
86
+ const seen = new Set<string>(); const stack = [sha];
87
+ while (stack.length && seen.size < limit) { const s = stack.pop()!; if (s === ancestor) return true; if (seen.has(s)) continue; seen.add(s); const o = await store.read(s); if (o?.type === 'commit') stack.push(...decodeCommit(o.payload).parents); }
88
+ return false;
89
+ }
90
+ export type PushPolicy = (cmd: RefCommand) => string | null | Promise<string | null>;
91
+ /** `POST git-receive-pack`: commands and a pack in, refs updated under `policy`, report-status out. */
92
+ export async function receivePack(body: Uint8Array, store: GitObjectStore, refs: GitRefs, policy: PushPolicy = () => null): Promise<{ response: Uint8Array; applied: RefCommand[] }> {
93
+ const { lines, rest } = readPktLines(body);
94
+ const commands: RefCommand[] = lines.map((l) => l.split('\0')[0]!.trim()).filter(Boolean).map((l) => { const [o, n, name] = l.split(' '); return { old: o!, new: n!, name: name!, fastForward: true }; }).filter((c) => SHA.test(c.old) && SHA.test(c.new) && /^refs\/[^\s]+$/.test(c.name));
95
+ const report: Uint8Array[] = [];
96
+ let unpack = 'ok';
97
+ if (rest.length >= 12) { try { await readPack(rest, store); } catch (e) { unpack = (e as Error).message; } }
98
+ report.push(pktLine(`unpack ${unpack}\n`));
99
+ const applied: RefCommand[] = [];
100
+ const existing = refs.list().map((r) => r.name);
101
+ const verdicts: Array<{ cmd: RefCommand; reason: string | null }> = [];
102
+ for (const cmd of commands) {
103
+ const current = refs.get(cmd.name) ?? ZERO_SHA;
104
+ let reason: string | null = unpack === 'ok' ? null : 'unpacker error';
105
+ if (!reason && current !== cmd.old) reason = 'fetch first';
106
+ if (!reason && current === ZERO_SHA && existing.some((n) => n.startsWith(`${cmd.name}/`) || cmd.name.startsWith(`${n}/`))) reason = 'ref name conflict';
107
+ if (!reason && cmd.new !== ZERO_SHA && !(await store.has(cmd.new))) reason = 'missing necessary objects';
108
+ if (!reason && cmd.old !== ZERO_SHA && cmd.new !== ZERO_SHA) cmd.fastForward = await isAncestor(store, cmd.old, cmd.new);
109
+ if (!reason) reason = await policy(cmd);
110
+ verdicts.push({ cmd, reason });
111
+ }
112
+ // Apply under the refs lock, re-reading each old value: a racing push that won in between is
113
+ // told `fetch first` rather than silently overwritten.
114
+ refs.withLock(() => {
115
+ for (const v of verdicts) {
116
+ if (!v.reason && (refs.get(v.cmd.name) ?? ZERO_SHA) !== v.cmd.old) v.reason = 'fetch first';
117
+ if (v.reason) { report.push(pktLine(`ng ${v.cmd.name} ${v.reason}\n`)); continue; }
118
+ if (v.cmd.new === ZERO_SHA) refs.delete(v.cmd.name); else refs.set(v.cmd.name, v.cmd.new);
119
+ applied.push(v.cmd); report.push(pktLine(`ok ${v.cmd.name}\n`));
120
+ }
121
+ });
122
+ report.push(FLUSH);
123
+ return { response: cat(...report), applied };
124
+ }
125
+
126
+ export type GitRepo = { store: GitObjectStore; refs: GitRefs; policy?: PushPolicy; onPush?: (applied: RefCommand[]) => void | Promise<void> };
127
+ /** Route one request under a repo: returns null when the path is not a smart-HTTP endpoint. */
128
+ export async function serveSmartHttp(req: Request, repo: GitRepo, tailPath: string): Promise<Response | null> {
129
+ const url = new URL(req.url);
130
+ if (req.method === 'GET' && tailPath === 'info/refs') {
131
+ const service = url.searchParams.get('service') as Service | null;
132
+ if (service !== 'git-upload-pack' && service !== 'git-receive-pack') return new Response('smart HTTP only', { status: 403 });
133
+ return new Response(advertisement(service, repo.refs) as unknown as BodyInit, { headers: { 'content-type': `application/x-${service}-advertisement`, 'cache-control': 'no-cache' } });
134
+ }
135
+ if (req.method === 'POST' && (tailPath === 'git-upload-pack' || tailPath === 'git-receive-pack')) {
136
+ return serveSmartHttpPost(new Uint8Array(await req.arrayBuffer()), req.headers.get('content-encoding') ?? '', repo, tailPath);
137
+ }
138
+ return null;
139
+ }
140
+ /** The POST half of `serveSmartHttp` for a caller that already holds the body: a push pack is
141
+ * the size of the repository, so re-wrapping it in a Request only to read it back is a full copy. */
142
+ export async function serveSmartHttpPost(body: Uint8Array, contentEncoding: string, repo: GitRepo, tailPath: 'git-upload-pack' | 'git-receive-pack'): Promise<Response> {
143
+ if (contentEncoding.includes('gzip')) body = new Uint8Array(await new Response(new Response(body as unknown as BodyInit).body!.pipeThrough(new DecompressionStream('gzip'))).arrayBuffer());
144
+ const headers = { 'content-type': `application/x-${tailPath}-result`, 'cache-control': 'no-cache' };
145
+ if (tailPath === 'git-upload-pack') return new Response((await uploadPack(body, repo.store)) as unknown as BodyInit, { headers });
146
+ const { response, applied } = await receivePack(body, repo.store, repo.refs, repo.policy);
147
+ if (applied.length && repo.onPush) await repo.onPush(applied);
148
+ return new Response(response as unknown as BodyInit, { headers });
149
+ }
package/src/hash.ts ADDED
@@ -0,0 +1,66 @@
1
+ // HASHING AND THE DELTA READER — the one canonicalizer in this package, the content hash every
2
+ // id and every changeset hash is built on, and the reader of a v1 delta row's `after` values.
3
+ // (What else shadow.ts held — refs, bases, observed deltas as a row kind — left with v1.)
4
+ import { createHash } from 'node:crypto';
5
+ import type { WorldServiceEvent } from './types.ts';
6
+
7
+
8
+ export const DELTA_TYPE_SUFFIX = '.delta';
9
+
10
+ export type SubjectFields = Record<string, unknown>;
11
+
12
+ /**
13
+ * Maps a world event to the subject fields it observes, or null when the
14
+ * event says nothing about remote subject state (comments, egress records, …).
15
+ * Extractors are provider-specific; the shadow engine is not.
16
+ */
17
+ export type SubjectFieldExtractor = (event: WorldServiceEvent) => SubjectFields | null;
18
+
19
+ export type FieldChange = { before: unknown; after: unknown };
20
+
21
+ /** Deterministic JSON: object keys sorted, `undefined` entries dropped, arrays in order.
22
+ * The one canonicalizer in this package — `hashFieldValue` (action-id content hashing) and
23
+ * `changesetContentHash` (approval binds to exact bytes) must agree on what "same content"
24
+ * means, so they share this function rather than each rolling their own. */
25
+ export function canonicalJson(value: unknown): string {
26
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
27
+ if (value && typeof value === 'object') {
28
+ const entries = Object.entries(value as Record<string, unknown>)
29
+ .filter(([, item]) => item !== undefined)
30
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
31
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`);
32
+ return `{${entries.join(',')}}`;
33
+ }
34
+ return JSON.stringify(value) ?? 'null';
35
+ }
36
+
37
+ export function hashFieldValue(value: unknown): string {
38
+ return createHash('sha256').update(canonicalJson(value)).digest('hex').slice(0, 16);
39
+ }
40
+
41
+ export function subjectKey(subject: { type: string; id: string }): string {
42
+ return `${subject.type}:${subject.id}`;
43
+ }
44
+
45
+ /** Field-level diff of freshly observed subject fields against a shadow of them. */
46
+ export type SubjectShadow = { subject: { type: string; id: string }; fields: SubjectFields; fieldHashes: Record<string, string>; latestEventId: string; updatedAt: string };
47
+ export function diffSubjectFields(shadow: SubjectShadow | undefined, observed: SubjectFields): Record<string, FieldChange> {
48
+ const changed: Record<string, FieldChange> = {};
49
+ for (const [field, after] of Object.entries(observed)) {
50
+ if (after === undefined) continue;
51
+ const beforeHash = shadow?.fieldHashes[field];
52
+ if (beforeHash !== undefined && beforeHash === hashFieldValue(after)) continue;
53
+ changed[field] = { before: shadow?.fields[field], after };
54
+ }
55
+ return changed;
56
+ }
57
+
58
+ export function isDeltaEvent(event: WorldServiceEvent): boolean { return typeof event.type === 'string' && event.type.endsWith(DELTA_TYPE_SUFFIX); }
59
+
60
+ /** THE canonical delta reader: a v1 delta row's `changed.<field>.after` values. */
61
+ export function deltaAfterFields(event: WorldServiceEvent): SubjectFields {
62
+ const changed = (event.data as { changed?: Record<string, FieldChange> }).changed ?? {};
63
+ const fields: SubjectFields = {};
64
+ for (const [field, change] of Object.entries(changed)) fields[field] = change.after;
65
+ return fields;
66
+ }
package/src/head.ts ADDED
@@ -0,0 +1,318 @@
1
+ // THE HEAD — where a write is applied (company contract "The head", ruled 2026-09-06).
2
+ //
3
+ // Every write is an entry first. Then the twin's head says what happens to it: no root, or a root
4
+ // under `gated`/`hold`, and the entry waits (simulated); a root under `auto`, and the entry is
5
+ // PERFORMED NOW, in the process that serves the twin, before the app is answered — checks, then the
6
+ // pack's perform adapter over the kernel executor with the credential opened from its seal, then
7
+ // the landed copy with the receipt. The push door and `volter world deploy` perform through the
8
+ // same `performEntries`; there is no hook and no phase ledger. The receipt on the entry is the
9
+ // record: a `deployed` copy is never performed again, a `failed` one is retried.
10
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { basename, dirname, join, resolve } from 'node:path';
13
+ import { confirmAction, isTwinBookkeeping, resolveSubjectId, revertAction, type TwinAction } from './actions.ts';
14
+ import { openSealedCredential, sealCredential, type CredentialPayload, type SealedCredential } from './credential.ts';
15
+ import { buildRemoteExecute, validateRemoteOrigin, type CredentialCustody } from './executor.ts';
16
+ import { getPack } from './packRegistry.ts';
17
+ import type { RemoteExecute } from './remote-execute.ts';
18
+ import { aliasesFrom, branchEntries, parentEntries, readTree, type Entry, type Receipt } from './log.ts';
19
+ import { resolveReferences } from './references.ts';
20
+ import { authStrategyFor, NO_SECRETS_CHECK, readRoot, runChecks, stateSystemFor, type Check, type DeployPolicy, type RootConfig, type StateSystemAdapters } from './state-system.ts';
21
+ import { stateDirName, worldPaths } from './storage.ts';
22
+ import { worldNow } from './world-clock.ts';
23
+ import { getActiveWorldStore } from './world-store.ts';
24
+
25
+ // ── the pack's perform adapter ──────────────────────────────────────────────────────────────
26
+
27
+ /** What a perform answers: the id the vendor minted (the local id when it kept ours), its URL, and
28
+ * the vendor's answer as fields for the landed copy. */
29
+ export type PushOutcome = { externalId: string; url?: string; data?: Record<string, unknown> };
30
+ /** What a perform is handed beside the executor: a resolver from a local id to the vendor's (an
31
+ * entry authored against `twin-1` crosses against `REAL-42` once that subject's landing adopted it). */
32
+ /** `credential`: the sealed credential's keyed fingerprint (credential.ts — an HMAC under the user's key,
33
+ * never the secret and no offline oracle for it), so a pack keys its budget ledger per real credential:
34
+ * every World and branch performing with one vendor key then shares that key's one allowance. */
35
+ export type PerformContext = { resolve: (type: string, localId: string) => string; service?: string; root?: string; credential?: string };
36
+ export type PerformAction = (execute: RemoteExecute, action: TwinAction, ctx: PerformContext) => Promise<PushOutcome>;
37
+ export function performContext(service: string, root?: string, credential?: string): PerformContext {
38
+ return { resolve: (type, localId) => resolveSubjectId(service, type, localId, root), service, ...(root !== undefined ? { root } : {}), ...(credential !== undefined ? { credential } : {}) };
39
+ }
40
+ /** The keyed fingerprint of the root's sealed credential, or undefined when none is sealed. */
41
+ function rootCredentialFingerprint(root: BoundRoot): string | undefined {
42
+ const sealed = getActiveWorldStore().read(root.credential);
43
+ if (sealed === null) return undefined;
44
+ const fingerprint = (JSON.parse(sealed) as SealedCredential).fingerprint;
45
+ return typeof fingerprint === 'string' && fingerprint !== '' ? fingerprint : undefined;
46
+ }
47
+
48
+ // ── the user's key and the sealed credential ────────────────────────────────────────────────
49
+
50
+ /** The user's key-encryption key: `$XDG_CONFIG_HOME/volter/kek` (created on first use). */
51
+ export function userKekPath(): string {
52
+ const base = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME !== '' ? process.env.XDG_CONFIG_HOME : join(homedir(), '.config');
53
+ return join(base, 'volter', 'kek');
54
+ }
55
+ export function userKek(): string {
56
+ const path = userKekPath();
57
+ if (existsSync(path)) return readFileSync(path, 'utf8').trim();
58
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
59
+ const key = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64');
60
+ writeFileSync(path, `${key}\n`, { mode: 0o600 });
61
+ chmodSync(path, 0o600);
62
+ return key;
63
+ }
64
+
65
+ /** Where the key that wraps every sealed credential comes from. A local World uses the user's key
66
+ * file (`userKek`); a hosted World's host supplies its deployment's secret once at start. */
67
+ let sealingKeySource: (() => string) | null = null;
68
+ export function setSealingKeySource(source: (() => string) | null): void { sealingKeySource = source; }
69
+ export function sealingKey(): string { return sealingKeySource ? sealingKeySource() : userKek(); }
70
+
71
+ /** A root as a world materializes it: the config plus the path of the sealed credential. */
72
+ export type BoundRoot = RootConfig & { credential: string };
73
+
74
+ /** Open the credential a root names, under the user's key. */
75
+ export async function openRootCredential(root: BoundRoot, vendor: string = vendorOf(root)): Promise<CredentialPayload> {
76
+ const sealed = getActiveWorldStore().read(root.credential);
77
+ if (sealed === null) throw new Error(`no credential sealed for ${vendor} — \`printf '<token>' | volter twin ${vendor} credential\``);
78
+ return openSealedCredential(sealingKey(), JSON.parse(sealed) as SealedCredential, vendor);
79
+ }
80
+ /** Seal a credential the vendor rotated (an `exchange` with `rotate`) where the root's was, under the same key and
81
+ * slot. It is the same credential: its placedAt and fingerprint stay (a pack's budget keys its ledger by the
82
+ * fingerprint, and a new one per grant would start the ledger over), and rotatedAt says when. */
83
+ export async function resealRootCredential(root: BoundRoot, payload: CredentialPayload, vendor: string = vendorOf(root)): Promise<void> {
84
+ const store = getActiveWorldStore();
85
+ const held = store.read(root.credential);
86
+ const before = held === null ? null : (JSON.parse(held) as SealedCredential);
87
+ const now = new Date().toISOString();
88
+ const sealed = await sealCredential(sealingKey(), payload, before?.placedAt ?? now, vendor);
89
+ store.write(root.credential, `${JSON.stringify({ ...sealed, ...(before ? { fingerprint: before.fingerprint } : {}), rotatedAt: now }, null, 2)}\n`, { secret: true });
90
+ }
91
+ /** The root's custody for an exchange that rotates: the credential as sealed now, and the seal of a rotated one. */
92
+ export function rootCustody(root: BoundRoot, vendor: string = vendorOf(root)): CredentialCustody {
93
+ // the credential's identity: its path and its fingerprint, which a rotation keeps and a user's re-seal of another
94
+ // credential changes, so a token exchanged for one account never answers for the next
95
+ const held = getActiveWorldStore().read(root.credential);
96
+ const fingerprint = held === null ? '' : String((JSON.parse(held) as SealedCredential).fingerprint ?? '');
97
+ return { key: `${root.credential}#${fingerprint}`, open: () => openRootCredential(root, vendor), seal: (credential) => resealRootCredential(root, credential, vendor) };
98
+ }
99
+ /** The vendor a root belongs to: its credential is sealed under the vendor's name. */
100
+ export function vendorOf(root: BoundRoot): string { return basename(root.credential, '.json'); }
101
+ /** The world a root belongs to: the credential sits at `<world>/.volter/credentials/<vendor>.json`. */
102
+ export function worldRootOf(root: BoundRoot): string { return resolve(dirname(root.credential), '..', '..'); }
103
+
104
+ // ── the world's checks ──────────────────────────────────────────────────────────────────────
105
+
106
+ /** The checks a world runs before any entry is performed: the shipped no-secrets check, then every
107
+ * file under `.volter/checks/` exporting `{ name, run }`. */
108
+ export async function loadChecks(worldRoot: string): Promise<Check[]> {
109
+ const dir = join(resolve(worldRoot), stateDirName(), 'checks');
110
+ const out: Check[] = [NO_SECRETS_CHECK];
111
+ for (const file of getActiveWorldStore().list(dir).filter((f) => /\.(ts|js|mjs)$/.test(f)).sort()) out.push(await checkLoader(join(dir, file), file.replace(/\.(ts|js|mjs)$/, '')));
112
+ return out;
113
+ }
114
+
115
+ /** How a check file becomes a Check. Locally the file is imported; a hosted World's host supplies a
116
+ * loader that runs the check in an isolate of its own, with no network and nothing of the World's. */
117
+ export type CheckLoader = (path: string, name: string) => Promise<Check>;
118
+ const importCheck: CheckLoader = async (path, name) => {
119
+ const mod = (await import(path)) as { check?: Check; default?: Check };
120
+ const check = mod.check ?? mod.default;
121
+ if (!check || typeof check.run !== 'function') throw new Error(`${path} must export a check { name, run }`);
122
+ return { name: check.name ?? name, run: check.run };
123
+ };
124
+ let checkLoader: CheckLoader = importCheck;
125
+ export function setCheckLoader(loader: CheckLoader | null): void { checkLoader = loader ?? importCheck; }
126
+ /** One check file through the host's loader: what placing a check validates with. */
127
+ export function loadCheck(path: string, name: string): Promise<Check> { return checkLoader(path, name); }
128
+
129
+ // ── the head ────────────────────────────────────────────────────────────────────────────────
130
+
131
+ export type Head = { kind: 'simulated' } | { kind: 'real'; deploy: DeployPolicy; root: BoundRoot };
132
+
133
+ /** The root a state service under `root` is bound to — its own root.json, else a sibling state's
134
+ * (slack records under `chat`; the root was written under `slack`). */
135
+ export function boundRoot(service: string, root?: string): BoundRoot | null {
136
+ const own = readRoot(service, root) as BoundRoot | null;
137
+ if (own?.credential) return own;
138
+ const stateRoot = dirname(worldPaths(service, root).dir);
139
+ const store = getActiveWorldStore();
140
+ const states = store.list(stateRoot).filter((name) => store.stat(join(stateRoot, name))?.isDirectory);
141
+ for (const state of states) { if (state === service) continue; const r = readRoot(state, root) as BoundRoot | null; if (r?.credential) return r; }
142
+ return null;
143
+ }
144
+
145
+ /** What this twin's head does with a write. */
146
+ export function headOf(service: string, root?: string): Head {
147
+ const bound = boundRoot(service, root);
148
+ return bound ? { kind: 'real', deploy: bound.deploy, root: bound } : { kind: 'simulated' };
149
+ }
150
+
151
+ /** A check refused the write: the entry is in the log as refused; the pack answers in the vendor's error shape. */
152
+ export class RefusedWriteError extends Error {
153
+ constructor(readonly check: string, readonly reason: string, readonly actionId: string) { super(`${check}: ${reason}`); this.name = 'RefusedWriteError'; }
154
+ }
155
+ /** The head could not be bound for a write that needed it (no credential sealed, no adapter registered,
156
+ * no root): a server-side fault, answered as one. */
157
+ export class HeadError extends Error {
158
+ constructor(message: string) { super(message); this.name = 'HeadError'; }
159
+ }
160
+ /** The vendor refused or failed the write: the entry is in the log as failed, with the vendor's words. */
161
+ export class VendorWriteError extends Error {
162
+ constructor(message: string, readonly actionId: string) { super(message); this.name = 'VendorWriteError'; }
163
+ }
164
+
165
+ export type DeployReport = {
166
+ /** entries performed by this call */
167
+ pushed: number;
168
+ deployed: Array<{ actionId: string; externalId?: string; at: string; /** what the vendor answered, as the adapter returned it */ data?: unknown }>;
169
+ /** the first refusal; nothing after it was attempted */
170
+ refused?: { actionId: string; check: string; reason: string };
171
+ /** the first failure, with the vendor's words; nothing after it was attempted */
172
+ failed?: { actionId: string; error: string };
173
+ /** entries after the refusal or failure, not attempted */
174
+ skipped: string[];
175
+ /** entries whose landed copy already said `deployed` — not performed again */
176
+ replayed: string[];
177
+ };
178
+
179
+ /** The receipt a branch entry's latest landed copy carries, if any. */
180
+ function receiptsByEntry(parent: Entry[]): Map<string, Receipt> {
181
+ const out = new Map<string, Receipt>();
182
+ for (const e of parent) if (e.landsId && e.receipt) out.set(e.landsId, e.receipt);
183
+ return out;
184
+ }
185
+
186
+ /** The branch entries a root twin still has to perform: `set` rows, not reverted, whose landed copy
187
+ * is absent or says `failed`/`skipped` (a `deployed` or `refused` copy settles the entry). */
188
+ export function deployableEntries(service: string, root?: string): TwinAction[] {
189
+ const parent = parentEntries(service, root);
190
+ const receipts = receiptsByEntry(parent);
191
+ const branch = branchEntries(service, root);
192
+ const reverted = new Set(branch.filter((e) => e.op === 'revert' && e.revertsActionId).map((e) => e.revertsActionId!));
193
+ return branch.filter((e) => {
194
+ if (e.op !== 'set' || reverted.has(e.id) || isTwinBookkeeping(e as TwinAction)) return false;
195
+ const r = receipts.get(e.id);
196
+ return r === undefined || r.status === 'failed' || r.status === 'skipped';
197
+ }) as TwinAction[];
198
+ }
199
+
200
+ const queues = new Map<string, Promise<unknown>>(); // one performer per twin: wire writes perform in append order
201
+
202
+ /**
203
+ * PERFORM this twin's deployable entries against its root, in order, checks in front of every one,
204
+ * stopping at the first refusal or failure. Each outcome lands as the entry's copy with its receipt
205
+ * on the parent log. `actionIds` narrows to a changeset's (or one write's) entries.
206
+ */
207
+ export async function performEntries(opts: {
208
+ service: string;
209
+ root?: string;
210
+ actionIds?: string[];
211
+ at?: string;
212
+ checks?: Check[];
213
+ adapters?: StateSystemAdapters;
214
+ execute?: RemoteExecute;
215
+ }): Promise<DeployReport> {
216
+ const key = `${opts.root ?? ''}::${opts.service}`;
217
+ const prior = queues.get(key) ?? Promise.resolve();
218
+ const run = prior.then(() => performNow(opts), () => performNow(opts));
219
+ queues.set(key, run.catch(() => undefined));
220
+ return run;
221
+ }
222
+
223
+ async function performNow(opts: Parameters<typeof performEntries>[0]): Promise<DeployReport> {
224
+ const { service, root } = opts;
225
+ const head = headOf(service, root);
226
+ if (head.kind !== 'real') throw new Error(`the ${service} twin has no root — nothing to perform against`);
227
+ const vendor = vendorOf(head.root);
228
+ const adapters = opts.adapters ?? stateSystemFor(vendor) ?? stateSystemFor(service);
229
+ if (!adapters?.perform) throw new Error(`the ${vendor} twin has no perform adapter — a protocol 2 pack names one in its descriptor and registers itself (registerPack)`);
230
+ const execute = opts.execute ?? buildRemoteExecute(validateRemoteOrigin(head.root.url, { allowInsecureLoopback: true }), await openRootCredential(head.root, vendor), authStrategyFor(vendor), getPack(vendor)?.hosts, { custody: rootCustody(head.root, vendor) });
231
+ const checks = opts.checks ?? (await loadChecks(worldRootOf(head.root)));
232
+ const deployable = deployableEntries(service, root);
233
+ const entries = opts.actionIds === undefined ? deployable : opts.actionIds.map((id) => deployable.find((a) => a.id === id)).filter((a): a is TwinAction => a !== undefined);
234
+ const report: DeployReport = { pushed: 0, deployed: [], skipped: [], replayed: [] };
235
+ if (opts.actionIds) {
236
+ const settled = receiptsByEntry(parentEntries(service, root));
237
+ for (const id of opts.actionIds) if (settled.get(id)?.status === 'deployed') report.replayed.push(id);
238
+ }
239
+ let stopped = false;
240
+ for (const action of entries) {
241
+ if (stopped) { report.skipped.push(action.id); continue; }
242
+ const at = opts.at ?? worldNow();
243
+ const verdict = await runChecks(checks, action, readTree(service, root));
244
+ if (verdict) {
245
+ confirmAction({ service, actionId: action.id, subject: action.subject, fields: action.fields ?? {}, occurredAt: at, receipt: { status: 'refused', reason: `${verdict.name}: ${verdict.reason}` }, ...(root !== undefined ? { root } : {}) });
246
+ report.refused = { actionId: action.id, check: verdict.name, reason: verdict.reason };
247
+ stopped = true;
248
+ continue;
249
+ }
250
+ try {
251
+ // a declared reference to an adopted id is resolved before the pack sees the entry: the vendor is
252
+ // asked about #7, never the local #3 (docs/contributing/architecture.md#alias-aware-lookup-at-the-request-boundary)
253
+ const fields = resolveReferences(service, action.subject.type, action.fields ?? {}, aliasesFrom(parentEntries(service, root)));
254
+ const outcome = await adapters.perform(execute, fields === action.fields ? action : { ...action, fields }, performContext(service, root, rootCredentialFingerprint(head.root)));
255
+ // the vendor's answer, when the adapter returned one as fields, is what the landed copy holds
256
+ const answered = outcome.data !== null && typeof outcome.data === 'object' && !Array.isArray(outcome.data) ? (outcome.data as Record<string, unknown>) : {};
257
+ confirmAction({
258
+ service, actionId: action.id, subject: action.subject, fields: { ...fields, ...answered }, occurredAt: at,
259
+ ...(outcome.externalId ? { vendorSubjectId: outcome.externalId } : {}),
260
+ receipt: { status: 'deployed', ...(outcome.url ? { url: outcome.url } : {}) },
261
+ ...(root !== undefined ? { root } : {}),
262
+ });
263
+ report.pushed += 1;
264
+ report.deployed.push({ actionId: action.id, at, ...(outcome.externalId ? { externalId: outcome.externalId } : {}), ...(outcome.data !== undefined ? { data: outcome.data } : {}) });
265
+ } catch (error) {
266
+ // a write the pack itself refuses (it can never be sent: its subject never reached the vendor) is
267
+ // settled as refused, as a check's refusal is, so the next deploy moves on to the entry after it;
268
+ // anything else failed and stays deployable for a retry
269
+ if (error instanceof RefusedWriteError) {
270
+ confirmAction({ service, actionId: action.id, subject: action.subject, fields: action.fields ?? {}, occurredAt: at, receipt: { status: 'refused', reason: error.message }, ...(root !== undefined ? { root } : {}) });
271
+ report.refused = { actionId: action.id, check: error.check, reason: error.reason };
272
+ stopped = true;
273
+ continue;
274
+ }
275
+ const message = error instanceof Error ? error.message : String(error);
276
+ confirmAction({ service, actionId: action.id, subject: action.subject, fields: action.fields ?? {}, occurredAt: at, receipt: { status: 'failed', reason: message }, ...(root !== undefined ? { root } : {}) });
277
+ report.failed = { actionId: action.id, error: message };
278
+ stopped = true;
279
+ }
280
+ }
281
+ return report;
282
+ }
283
+
284
+ /**
285
+ * The write path's second half (serve.ts): a write just appended to a twin whose head is real and
286
+ * `auto` is performed now. Returns the vendor's id when it minted one, so the answer carries it.
287
+ * Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
288
+ */
289
+ export async function performAtHead(service: string, action: Pick<TwinAction, 'id'>, root?: string): Promise<{ performed: boolean; externalId?: string; data?: unknown }> {
290
+ const head = headOf(service, root);
291
+ if (head.kind !== 'real' || head.deploy !== 'auto') return { performed: false };
292
+ let report: DeployReport;
293
+ try { report = await performEntries({ service, actionIds: [action.id], ...(root !== undefined ? { root } : {}) }); }
294
+ catch (error) { throw error instanceof HeadError ? error : new HeadError(error instanceof Error ? error.message : String(error)); }
295
+ // under live use an entry the vendor did not take does not stay in the tree: the app was told no, the
296
+ // world says no — a revert lands (the receipt stays on the original, so the log shows what happened)
297
+ const undone = report.refused?.actionId === action.id || report.failed?.actionId === action.id;
298
+ if (undone) revertAction({ service, actionId: action.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
299
+ if (report.refused?.actionId === action.id) throw new RefusedWriteError(report.refused.check, report.refused.reason, action.id);
300
+ if (report.failed?.actionId === action.id) throw new VendorWriteError(report.failed.error, action.id);
301
+ const done = report.deployed.find((d) => d.actionId === action.id);
302
+ return { performed: done !== undefined, ...(done?.externalId ? { externalId: done.externalId } : {}), ...(done?.data !== undefined ? { data: done.data } : {}) };
303
+ }
304
+
305
+ /** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
306
+ * vendor's own error body (the pack's `shape`), so an app under live use sees what the vendor
307
+ * would have said. Anything else propagates. */
308
+ export function answerVendorErrors(fetch: (request: Request) => Promise<Response>, shape: (error: RefusedWriteError | VendorWriteError) => Response): (request: Request) => Promise<Response> {
309
+ return async (request: Request): Promise<Response> => {
310
+ try { return await fetch(request); } catch (error) {
311
+ if (error instanceof RefusedWriteError || error instanceof VendorWriteError) return shape(error);
312
+ // a head that could not be bound is a server error, answered as one (never a runtime's fallback
313
+ // page); anything else the twin threw propagates as it always did
314
+ if (error instanceof HeadError) return Response.json({ error: error.message }, { status: 500 });
315
+ throw error;
316
+ }
317
+ };
318
+ }