@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,223 @@
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 { decodeCommit, decodeTag, decodeTree } from "./objects.js";
7
+ import { readPack, writePack } from "./pack.js";
8
+ const ENC = new TextEncoder();
9
+ const DEC = new TextDecoder();
10
+ export const ZERO_SHA = '0'.repeat(40);
11
+ export const FLUSH = new Uint8Array([0x30, 0x30, 0x30, 0x30]);
12
+ export function pktLine(s) {
13
+ const body = typeof s === 'string' ? ENC.encode(s) : s;
14
+ const len = (body.length + 4).toString(16).padStart(4, '0');
15
+ const out = new Uint8Array(body.length + 4);
16
+ out.set(ENC.encode(len), 0);
17
+ out.set(body, 4);
18
+ return out;
19
+ }
20
+ /** Pkt-lines up to and including the first flush; `rest` is what follows (a pack, on push). */
21
+ export function readPktLines(bytes) {
22
+ const lines = [];
23
+ let at = 0;
24
+ while (at + 4 <= bytes.length) {
25
+ const len = parseInt(DEC.decode(bytes.subarray(at, at + 4)), 16);
26
+ if (Number.isNaN(len))
27
+ return { lines, rest: bytes.subarray(at) };
28
+ if (len === 0)
29
+ return { lines, rest: bytes.subarray(at + 4) };
30
+ if (len < 4) {
31
+ at += 4;
32
+ continue;
33
+ } // v2 delimiter/response-end packets: no payload
34
+ lines.push(DEC.decode(bytes.subarray(at + 4, at + len)));
35
+ at += len;
36
+ }
37
+ return { lines, rest: bytes.subarray(at) };
38
+ }
39
+ /** Every pkt-line in the body across all flushes (stateless clients batch haves between flushes); `rest` follows the first flush. */
40
+ export function readAllPktLines(bytes) {
41
+ const first = readPktLines(bytes);
42
+ const lines = [...first.lines];
43
+ let cur = first.rest;
44
+ 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))) {
45
+ const n = readPktLines(cur);
46
+ lines.push(...n.lines);
47
+ if (n.rest.length === cur.length)
48
+ break;
49
+ cur = n.rest;
50
+ }
51
+ return { lines, rest: first.rest };
52
+ }
53
+ const SHA = /^[0-9a-f]{40}$/;
54
+ function cat(...parts) { const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0)); let o = 0; for (const p of parts) {
55
+ out.set(p, o);
56
+ o += p.length;
57
+ } return out; }
58
+ const CAPS = { 'git-upload-pack': 'ofs-delta no-progress agent=volter-twin', 'git-receive-pack': 'report-status delete-refs ofs-delta agent=volter-twin' };
59
+ /** `GET info/refs?service=<service>`: the ref advertisement, HEAD first as a symref. */
60
+ export function advertisement(service, refs) {
61
+ const all = refs.list();
62
+ const head = refs.head();
63
+ const headSha = refs.get(head);
64
+ const caps = `${CAPS[service]} symref=HEAD:${head}`;
65
+ const lines = [pktLine(`# service=${service}\n`), FLUSH];
66
+ if (headSha)
67
+ lines.push(pktLine(`${headSha} HEAD\0${caps}\n`));
68
+ const rest = all;
69
+ if (!headSha && rest.length === 0)
70
+ lines.push(pktLine(`${ZERO_SHA} capabilities^{}\0${caps}\n`));
71
+ let first = headSha === null;
72
+ for (const r of rest) {
73
+ lines.push(pktLine(first ? `${r.sha} ${r.name}\0${caps}\n` : `${r.sha} ${r.name}\n`));
74
+ first = false;
75
+ }
76
+ lines.push(FLUSH);
77
+ return cat(...lines);
78
+ }
79
+ /** Every object reachable from `roots`, stopping at `seen`. */
80
+ async function closure(store, roots, seen) {
81
+ const out = [];
82
+ const stack = [...roots];
83
+ while (stack.length) {
84
+ const sha = stack.pop();
85
+ if (seen.has(sha))
86
+ continue;
87
+ seen.add(sha);
88
+ const obj = await store.read(sha);
89
+ if (!obj)
90
+ continue;
91
+ out.push({ ...obj, sha });
92
+ if (obj.type === 'commit') {
93
+ const c = decodeCommit(obj.payload);
94
+ stack.push(c.tree, ...c.parents);
95
+ }
96
+ else if (obj.type === 'tree') {
97
+ for (const e of decodeTree(obj.payload))
98
+ if (e.mode !== '160000')
99
+ stack.push(e.sha);
100
+ }
101
+ else if (obj.type === 'tag')
102
+ stack.push(decodeTag(obj.payload).object);
103
+ }
104
+ return out;
105
+ }
106
+ async function reachable(store, roots) { const seen = new Set(); await closure(store, roots, seen); return seen; }
107
+ /** `POST git-upload-pack`: wants and haves in, NAK and (once `done`) the pack out. */
108
+ export async function uploadPack(body, store) {
109
+ const { lines } = readAllPktLines(body);
110
+ const wants = lines.filter((l) => l.startsWith('want ')).map((l) => l.slice(5, 45)).filter((s) => SHA.test(s));
111
+ const haves = lines.filter((l) => l.startsWith('have ')).map((l) => l.slice(5, 45)).filter((s) => SHA.test(s));
112
+ const done = lines.some((l) => l.trim() === 'done');
113
+ const known = [];
114
+ for (const h of haves)
115
+ if (await store.has(h))
116
+ known.push(h);
117
+ for (const w of wants)
118
+ if (!(await store.has(w)))
119
+ return pktLine(`ERR upload-pack: not our ref ${w}\n`);
120
+ if (!done)
121
+ return pktLine('NAK\n');
122
+ const seen = await reachable(store, known);
123
+ const objects = await closure(store, wants, seen);
124
+ return cat(pktLine('NAK\n'), await writePack(objects));
125
+ }
126
+ /** Bounded ancestry walk: is `ancestor` reachable from `sha` through parents? */
127
+ export async function isAncestor(store, ancestor, sha, limit = 5000) {
128
+ const seen = new Set();
129
+ const stack = [sha];
130
+ while (stack.length && seen.size < limit) {
131
+ const s = stack.pop();
132
+ if (s === ancestor)
133
+ return true;
134
+ if (seen.has(s))
135
+ continue;
136
+ seen.add(s);
137
+ const o = await store.read(s);
138
+ if (o?.type === 'commit')
139
+ stack.push(...decodeCommit(o.payload).parents);
140
+ }
141
+ return false;
142
+ }
143
+ /** `POST git-receive-pack`: commands and a pack in, refs updated under `policy`, report-status out. */
144
+ export async function receivePack(body, store, refs, policy = () => null) {
145
+ const { lines, rest } = readPktLines(body);
146
+ const commands = 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));
147
+ const report = [];
148
+ let unpack = 'ok';
149
+ if (rest.length >= 12) {
150
+ try {
151
+ await readPack(rest, store);
152
+ }
153
+ catch (e) {
154
+ unpack = e.message;
155
+ }
156
+ }
157
+ report.push(pktLine(`unpack ${unpack}\n`));
158
+ const applied = [];
159
+ const existing = refs.list().map((r) => r.name);
160
+ const verdicts = [];
161
+ for (const cmd of commands) {
162
+ const current = refs.get(cmd.name) ?? ZERO_SHA;
163
+ let reason = unpack === 'ok' ? null : 'unpacker error';
164
+ if (!reason && current !== cmd.old)
165
+ reason = 'fetch first';
166
+ if (!reason && current === ZERO_SHA && existing.some((n) => n.startsWith(`${cmd.name}/`) || cmd.name.startsWith(`${n}/`)))
167
+ reason = 'ref name conflict';
168
+ if (!reason && cmd.new !== ZERO_SHA && !(await store.has(cmd.new)))
169
+ reason = 'missing necessary objects';
170
+ if (!reason && cmd.old !== ZERO_SHA && cmd.new !== ZERO_SHA)
171
+ cmd.fastForward = await isAncestor(store, cmd.old, cmd.new);
172
+ if (!reason)
173
+ reason = await policy(cmd);
174
+ verdicts.push({ cmd, reason });
175
+ }
176
+ // Apply under the refs lock, re-reading each old value: a racing push that won in between is
177
+ // told `fetch first` rather than silently overwritten.
178
+ refs.withLock(() => {
179
+ for (const v of verdicts) {
180
+ if (!v.reason && (refs.get(v.cmd.name) ?? ZERO_SHA) !== v.cmd.old)
181
+ v.reason = 'fetch first';
182
+ if (v.reason) {
183
+ report.push(pktLine(`ng ${v.cmd.name} ${v.reason}\n`));
184
+ continue;
185
+ }
186
+ if (v.cmd.new === ZERO_SHA)
187
+ refs.delete(v.cmd.name);
188
+ else
189
+ refs.set(v.cmd.name, v.cmd.new);
190
+ applied.push(v.cmd);
191
+ report.push(pktLine(`ok ${v.cmd.name}\n`));
192
+ }
193
+ });
194
+ report.push(FLUSH);
195
+ return { response: cat(...report), applied };
196
+ }
197
+ /** Route one request under a repo: returns null when the path is not a smart-HTTP endpoint. */
198
+ export async function serveSmartHttp(req, repo, tailPath) {
199
+ const url = new URL(req.url);
200
+ if (req.method === 'GET' && tailPath === 'info/refs') {
201
+ const service = url.searchParams.get('service');
202
+ if (service !== 'git-upload-pack' && service !== 'git-receive-pack')
203
+ return new Response('smart HTTP only', { status: 403 });
204
+ return new Response(advertisement(service, repo.refs), { headers: { 'content-type': `application/x-${service}-advertisement`, 'cache-control': 'no-cache' } });
205
+ }
206
+ if (req.method === 'POST' && (tailPath === 'git-upload-pack' || tailPath === 'git-receive-pack')) {
207
+ return serveSmartHttpPost(new Uint8Array(await req.arrayBuffer()), req.headers.get('content-encoding') ?? '', repo, tailPath);
208
+ }
209
+ return null;
210
+ }
211
+ /** The POST half of `serveSmartHttp` for a caller that already holds the body: a push pack is
212
+ * the size of the repository, so re-wrapping it in a Request only to read it back is a full copy. */
213
+ export async function serveSmartHttpPost(body, contentEncoding, repo, tailPath) {
214
+ if (contentEncoding.includes('gzip'))
215
+ body = new Uint8Array(await new Response(new Response(body).body.pipeThrough(new DecompressionStream('gzip'))).arrayBuffer());
216
+ const headers = { 'content-type': `application/x-${tailPath}-result`, 'cache-control': 'no-cache' };
217
+ if (tailPath === 'git-upload-pack')
218
+ return new Response((await uploadPack(body, repo.store)), { headers });
219
+ const { response, applied } = await receivePack(body, repo.store, repo.refs, repo.policy);
220
+ if (applied.length && repo.onPush)
221
+ await repo.onPush(applied);
222
+ return new Response(response, { headers });
223
+ }
@@ -0,0 +1,38 @@
1
+ import type { WorldServiceEvent } from './types.js';
2
+ export declare const DELTA_TYPE_SUFFIX = ".delta";
3
+ export type SubjectFields = Record<string, unknown>;
4
+ /**
5
+ * Maps a world event to the subject fields it observes, or null when the
6
+ * event says nothing about remote subject state (comments, egress records, …).
7
+ * Extractors are provider-specific; the shadow engine is not.
8
+ */
9
+ export type SubjectFieldExtractor = (event: WorldServiceEvent) => SubjectFields | null;
10
+ export type FieldChange = {
11
+ before: unknown;
12
+ after: unknown;
13
+ };
14
+ /** Deterministic JSON: object keys sorted, `undefined` entries dropped, arrays in order.
15
+ * The one canonicalizer in this package — `hashFieldValue` (action-id content hashing) and
16
+ * `changesetContentHash` (approval binds to exact bytes) must agree on what "same content"
17
+ * means, so they share this function rather than each rolling their own. */
18
+ export declare function canonicalJson(value: unknown): string;
19
+ export declare function hashFieldValue(value: unknown): string;
20
+ export declare function subjectKey(subject: {
21
+ type: string;
22
+ id: string;
23
+ }): string;
24
+ /** Field-level diff of freshly observed subject fields against a shadow of them. */
25
+ export type SubjectShadow = {
26
+ subject: {
27
+ type: string;
28
+ id: string;
29
+ };
30
+ fields: SubjectFields;
31
+ fieldHashes: Record<string, string>;
32
+ latestEventId: string;
33
+ updatedAt: string;
34
+ };
35
+ export declare function diffSubjectFields(shadow: SubjectShadow | undefined, observed: SubjectFields): Record<string, FieldChange>;
36
+ export declare function isDeltaEvent(event: WorldServiceEvent): boolean;
37
+ /** THE canonical delta reader: a v1 delta row's `changed.<field>.after` values. */
38
+ export declare function deltaAfterFields(event: WorldServiceEvent): SubjectFields;
@@ -0,0 +1,48 @@
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
+ export const DELTA_TYPE_SUFFIX = '.delta';
6
+ /** Deterministic JSON: object keys sorted, `undefined` entries dropped, arrays in order.
7
+ * The one canonicalizer in this package — `hashFieldValue` (action-id content hashing) and
8
+ * `changesetContentHash` (approval binds to exact bytes) must agree on what "same content"
9
+ * means, so they share this function rather than each rolling their own. */
10
+ export function canonicalJson(value) {
11
+ if (Array.isArray(value))
12
+ return `[${value.map(canonicalJson).join(',')}]`;
13
+ if (value && typeof value === 'object') {
14
+ const entries = Object.entries(value)
15
+ .filter(([, item]) => item !== undefined)
16
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
17
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`);
18
+ return `{${entries.join(',')}}`;
19
+ }
20
+ return JSON.stringify(value) ?? 'null';
21
+ }
22
+ export function hashFieldValue(value) {
23
+ return createHash('sha256').update(canonicalJson(value)).digest('hex').slice(0, 16);
24
+ }
25
+ export function subjectKey(subject) {
26
+ return `${subject.type}:${subject.id}`;
27
+ }
28
+ export function diffSubjectFields(shadow, observed) {
29
+ const changed = {};
30
+ for (const [field, after] of Object.entries(observed)) {
31
+ if (after === undefined)
32
+ continue;
33
+ const beforeHash = shadow?.fieldHashes[field];
34
+ if (beforeHash !== undefined && beforeHash === hashFieldValue(after))
35
+ continue;
36
+ changed[field] = { before: shadow?.fields[field], after };
37
+ }
38
+ return changed;
39
+ }
40
+ export function isDeltaEvent(event) { return typeof event.type === 'string' && event.type.endsWith(DELTA_TYPE_SUFFIX); }
41
+ /** THE canonical delta reader: a v1 delta row's `changed.<field>.after` values. */
42
+ export function deltaAfterFields(event) {
43
+ const changed = event.data.changed ?? {};
44
+ const fields = {};
45
+ for (const [field, change] of Object.entries(changed))
46
+ fields[field] = change.after;
47
+ return fields;
48
+ }
@@ -0,0 +1,140 @@
1
+ import { type TwinAction } from './actions.js';
2
+ import { type CredentialPayload } from './credential.js';
3
+ import { type CredentialCustody } from './executor.js';
4
+ import type { RemoteExecute } from './remote-execute.js';
5
+ import { type Check, type DeployPolicy, type RootConfig, type StateSystemAdapters } from './state-system.js';
6
+ /** What a perform answers: the id the vendor minted (the local id when it kept ours), its URL, and
7
+ * the vendor's answer as fields for the landed copy. */
8
+ export type PushOutcome = {
9
+ externalId: string;
10
+ url?: string;
11
+ data?: Record<string, unknown>;
12
+ };
13
+ /** What a perform is handed beside the executor: a resolver from a local id to the vendor's (an
14
+ * entry authored against `twin-1` crosses against `REAL-42` once that subject's landing adopted it). */
15
+ /** `credential`: the sealed credential's keyed fingerprint (credential.ts — an HMAC under the user's key,
16
+ * never the secret and no offline oracle for it), so a pack keys its budget ledger per real credential:
17
+ * every World and branch performing with one vendor key then shares that key's one allowance. */
18
+ export type PerformContext = {
19
+ resolve: (type: string, localId: string) => string;
20
+ service?: string;
21
+ root?: string;
22
+ credential?: string;
23
+ };
24
+ export type PerformAction = (execute: RemoteExecute, action: TwinAction, ctx: PerformContext) => Promise<PushOutcome>;
25
+ export declare function performContext(service: string, root?: string, credential?: string): PerformContext;
26
+ /** The user's key-encryption key: `$XDG_CONFIG_HOME/volter/kek` (created on first use). */
27
+ export declare function userKekPath(): string;
28
+ export declare function userKek(): string;
29
+ export declare function setSealingKeySource(source: (() => string) | null): void;
30
+ export declare function sealingKey(): string;
31
+ /** A root as a world materializes it: the config plus the path of the sealed credential. */
32
+ export type BoundRoot = RootConfig & {
33
+ credential: string;
34
+ };
35
+ /** Open the credential a root names, under the user's key. */
36
+ export declare function openRootCredential(root: BoundRoot, vendor?: string): Promise<CredentialPayload>;
37
+ /** Seal a credential the vendor rotated (an `exchange` with `rotate`) where the root's was, under the same key and
38
+ * slot. It is the same credential: its placedAt and fingerprint stay (a pack's budget keys its ledger by the
39
+ * fingerprint, and a new one per grant would start the ledger over), and rotatedAt says when. */
40
+ export declare function resealRootCredential(root: BoundRoot, payload: CredentialPayload, vendor?: string): Promise<void>;
41
+ /** The root's custody for an exchange that rotates: the credential as sealed now, and the seal of a rotated one. */
42
+ export declare function rootCustody(root: BoundRoot, vendor?: string): CredentialCustody;
43
+ /** The vendor a root belongs to: its credential is sealed under the vendor's name. */
44
+ export declare function vendorOf(root: BoundRoot): string;
45
+ /** The world a root belongs to: the credential sits at `<world>/.volter/credentials/<vendor>.json`. */
46
+ export declare function worldRootOf(root: BoundRoot): string;
47
+ /** The checks a world runs before any entry is performed: the shipped no-secrets check, then every
48
+ * file under `.volter/checks/` exporting `{ name, run }`. */
49
+ export declare function loadChecks(worldRoot: string): Promise<Check[]>;
50
+ /** How a check file becomes a Check. Locally the file is imported; a hosted World's host supplies a
51
+ * loader that runs the check in an isolate of its own, with no network and nothing of the World's. */
52
+ export type CheckLoader = (path: string, name: string) => Promise<Check>;
53
+ export declare function setCheckLoader(loader: CheckLoader | null): void;
54
+ /** One check file through the host's loader: what placing a check validates with. */
55
+ export declare function loadCheck(path: string, name: string): Promise<Check>;
56
+ export type Head = {
57
+ kind: 'simulated';
58
+ } | {
59
+ kind: 'real';
60
+ deploy: DeployPolicy;
61
+ root: BoundRoot;
62
+ };
63
+ /** The root a state service under `root` is bound to — its own root.json, else a sibling state's
64
+ * (slack records under `chat`; the root was written under `slack`). */
65
+ export declare function boundRoot(service: string, root?: string): BoundRoot | null;
66
+ /** What this twin's head does with a write. */
67
+ export declare function headOf(service: string, root?: string): Head;
68
+ /** A check refused the write: the entry is in the log as refused; the pack answers in the vendor's error shape. */
69
+ export declare class RefusedWriteError extends Error {
70
+ readonly check: string;
71
+ readonly reason: string;
72
+ readonly actionId: string;
73
+ constructor(check: string, reason: string, actionId: string);
74
+ }
75
+ /** The head could not be bound for a write that needed it (no credential sealed, no adapter registered,
76
+ * no root): a server-side fault, answered as one. */
77
+ export declare class HeadError extends Error {
78
+ constructor(message: string);
79
+ }
80
+ /** The vendor refused or failed the write: the entry is in the log as failed, with the vendor's words. */
81
+ export declare class VendorWriteError extends Error {
82
+ readonly actionId: string;
83
+ constructor(message: string, actionId: string);
84
+ }
85
+ export type DeployReport = {
86
+ /** entries performed by this call */
87
+ pushed: number;
88
+ deployed: Array<{
89
+ actionId: string;
90
+ externalId?: string;
91
+ at: string; /** what the vendor answered, as the adapter returned it */
92
+ data?: unknown;
93
+ }>;
94
+ /** the first refusal; nothing after it was attempted */
95
+ refused?: {
96
+ actionId: string;
97
+ check: string;
98
+ reason: string;
99
+ };
100
+ /** the first failure, with the vendor's words; nothing after it was attempted */
101
+ failed?: {
102
+ actionId: string;
103
+ error: string;
104
+ };
105
+ /** entries after the refusal or failure, not attempted */
106
+ skipped: string[];
107
+ /** entries whose landed copy already said `deployed` — not performed again */
108
+ replayed: string[];
109
+ };
110
+ /** The branch entries a root twin still has to perform: `set` rows, not reverted, whose landed copy
111
+ * is absent or says `failed`/`skipped` (a `deployed` or `refused` copy settles the entry). */
112
+ export declare function deployableEntries(service: string, root?: string): TwinAction[];
113
+ /**
114
+ * PERFORM this twin's deployable entries against its root, in order, checks in front of every one,
115
+ * stopping at the first refusal or failure. Each outcome lands as the entry's copy with its receipt
116
+ * on the parent log. `actionIds` narrows to a changeset's (or one write's) entries.
117
+ */
118
+ export declare function performEntries(opts: {
119
+ service: string;
120
+ root?: string;
121
+ actionIds?: string[];
122
+ at?: string;
123
+ checks?: Check[];
124
+ adapters?: StateSystemAdapters;
125
+ execute?: RemoteExecute;
126
+ }): Promise<DeployReport>;
127
+ /**
128
+ * The write path's second half (serve.ts): a write just appended to a twin whose head is real and
129
+ * `auto` is performed now. Returns the vendor's id when it minted one, so the answer carries it.
130
+ * Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
131
+ */
132
+ export declare function performAtHead(service: string, action: Pick<TwinAction, 'id'>, root?: string): Promise<{
133
+ performed: boolean;
134
+ externalId?: string;
135
+ data?: unknown;
136
+ }>;
137
+ /** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
138
+ * vendor's own error body (the pack's `shape`), so an app under live use sees what the vendor
139
+ * would have said. Anything else propagates. */
140
+ export declare function answerVendorErrors(fetch: (request: Request) => Promise<Response>, shape: (error: RefusedWriteError | VendorWriteError) => Response): (request: Request) => Promise<Response>;