@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,313 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // THE HEAD — where a write is applied (company contract "The head", ruled 2026-09-06).
10
+ //
11
+ // Every write is an entry first. Then the twin's head says what happens to it: no root, or a root
12
+ // under `gated`/`hold`, and the entry waits (simulated); a root under `auto`, and the entry is
13
+ // PERFORMED NOW, in the process that serves the twin, before the app is answered — checks, then the
14
+ // pack's perform adapter over the kernel executor with the credential opened from its seal, then
15
+ // the landed copy with the receipt. The push door and `volter world deploy` perform through the
16
+ // same `performEntries`; there is no hook and no phase ledger. The receipt on the entry is the
17
+ // record: a `deployed` copy is never performed again, a `failed` one is retried.
18
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
19
+ import { homedir } from 'node:os';
20
+ import { basename, dirname, join, resolve } from 'node:path';
21
+ import { confirmAction, isTwinBookkeeping, resolveSubjectId, revertAction } from "./actions.js";
22
+ import { openSealedCredential, sealCredential } from "./credential.js";
23
+ import { buildRemoteExecute, validateRemoteOrigin } from "./executor.js";
24
+ import { getPack } from "./packRegistry.js";
25
+ import { aliasesFrom, branchEntries, parentEntries, readTree } from "./log.js";
26
+ import { resolveReferences } from "./references.js";
27
+ import { authStrategyFor, NO_SECRETS_CHECK, readRoot, runChecks, stateSystemFor } from "./state-system.js";
28
+ import { stateDirName, worldPaths } from "./storage.js";
29
+ import { worldNow } from "./world-clock.js";
30
+ import { getActiveWorldStore } from "./world-store.js";
31
+ export function performContext(service, root, credential) {
32
+ return { resolve: (type, localId) => resolveSubjectId(service, type, localId, root), service, ...(root !== undefined ? { root } : {}), ...(credential !== undefined ? { credential } : {}) };
33
+ }
34
+ /** The keyed fingerprint of the root's sealed credential, or undefined when none is sealed. */
35
+ function rootCredentialFingerprint(root) {
36
+ const sealed = getActiveWorldStore().read(root.credential);
37
+ if (sealed === null)
38
+ return undefined;
39
+ const fingerprint = JSON.parse(sealed).fingerprint;
40
+ return typeof fingerprint === 'string' && fingerprint !== '' ? fingerprint : undefined;
41
+ }
42
+ // ── the user's key and the sealed credential ────────────────────────────────────────────────
43
+ /** The user's key-encryption key: `$XDG_CONFIG_HOME/volter/kek` (created on first use). */
44
+ export function userKekPath() {
45
+ const base = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME !== '' ? process.env.XDG_CONFIG_HOME : join(homedir(), '.config');
46
+ return join(base, 'volter', 'kek');
47
+ }
48
+ export function userKek() {
49
+ const path = userKekPath();
50
+ if (existsSync(path))
51
+ return readFileSync(path, 'utf8').trim();
52
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
53
+ const key = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64');
54
+ writeFileSync(path, `${key}\n`, { mode: 0o600 });
55
+ chmodSync(path, 0o600);
56
+ return key;
57
+ }
58
+ /** Where the key that wraps every sealed credential comes from. A local World uses the user's key
59
+ * file (`userKek`); a hosted World's host supplies its deployment's secret once at start. */
60
+ let sealingKeySource = null;
61
+ export function setSealingKeySource(source) { sealingKeySource = source; }
62
+ export function sealingKey() { return sealingKeySource ? sealingKeySource() : userKek(); }
63
+ /** Open the credential a root names, under the user's key. */
64
+ export async function openRootCredential(root, vendor = vendorOf(root)) {
65
+ const sealed = getActiveWorldStore().read(root.credential);
66
+ if (sealed === null)
67
+ throw new Error(`no credential sealed for ${vendor} — \`printf '<token>' | volter twin ${vendor} credential\``);
68
+ return openSealedCredential(sealingKey(), JSON.parse(sealed), vendor);
69
+ }
70
+ /** Seal a credential the vendor rotated (an `exchange` with `rotate`) where the root's was, under the same key and
71
+ * slot. It is the same credential: its placedAt and fingerprint stay (a pack's budget keys its ledger by the
72
+ * fingerprint, and a new one per grant would start the ledger over), and rotatedAt says when. */
73
+ export async function resealRootCredential(root, payload, vendor = vendorOf(root)) {
74
+ const store = getActiveWorldStore();
75
+ const held = store.read(root.credential);
76
+ const before = held === null ? null : JSON.parse(held);
77
+ const now = new Date().toISOString();
78
+ const sealed = await sealCredential(sealingKey(), payload, before?.placedAt ?? now, vendor);
79
+ store.write(root.credential, `${JSON.stringify({ ...sealed, ...(before ? { fingerprint: before.fingerprint } : {}), rotatedAt: now }, null, 2)}\n`, { secret: true });
80
+ }
81
+ /** The root's custody for an exchange that rotates: the credential as sealed now, and the seal of a rotated one. */
82
+ export function rootCustody(root, vendor = vendorOf(root)) {
83
+ // the credential's identity: its path and its fingerprint, which a rotation keeps and a user's re-seal of another
84
+ // credential changes, so a token exchanged for one account never answers for the next
85
+ const held = getActiveWorldStore().read(root.credential);
86
+ const fingerprint = held === null ? '' : String(JSON.parse(held).fingerprint ?? '');
87
+ return { key: `${root.credential}#${fingerprint}`, open: () => openRootCredential(root, vendor), seal: (credential) => resealRootCredential(root, credential, vendor) };
88
+ }
89
+ /** The vendor a root belongs to: its credential is sealed under the vendor's name. */
90
+ export function vendorOf(root) { return basename(root.credential, '.json'); }
91
+ /** The world a root belongs to: the credential sits at `<world>/.volter/credentials/<vendor>.json`. */
92
+ export function worldRootOf(root) { return resolve(dirname(root.credential), '..', '..'); }
93
+ // ── the world's checks ──────────────────────────────────────────────────────────────────────
94
+ /** The checks a world runs before any entry is performed: the shipped no-secrets check, then every
95
+ * file under `.volter/checks/` exporting `{ name, run }`. */
96
+ export async function loadChecks(worldRoot) {
97
+ const dir = join(resolve(worldRoot), stateDirName(), 'checks');
98
+ const out = [NO_SECRETS_CHECK];
99
+ for (const file of getActiveWorldStore().list(dir).filter((f) => /\.(ts|js|mjs)$/.test(f)).sort())
100
+ out.push(await checkLoader(join(dir, file), file.replace(/\.(ts|js|mjs)$/, '')));
101
+ return out;
102
+ }
103
+ const importCheck = async (path, name) => {
104
+ const mod = (await import(__rewriteRelativeImportExtension(path)));
105
+ const check = mod.check ?? mod.default;
106
+ if (!check || typeof check.run !== 'function')
107
+ throw new Error(`${path} must export a check { name, run }`);
108
+ return { name: check.name ?? name, run: check.run };
109
+ };
110
+ let checkLoader = importCheck;
111
+ export function setCheckLoader(loader) { checkLoader = loader ?? importCheck; }
112
+ /** One check file through the host's loader: what placing a check validates with. */
113
+ export function loadCheck(path, name) { return checkLoader(path, name); }
114
+ /** The root a state service under `root` is bound to — its own root.json, else a sibling state's
115
+ * (slack records under `chat`; the root was written under `slack`). */
116
+ export function boundRoot(service, root) {
117
+ const own = readRoot(service, root);
118
+ if (own?.credential)
119
+ return own;
120
+ const stateRoot = dirname(worldPaths(service, root).dir);
121
+ const store = getActiveWorldStore();
122
+ const states = store.list(stateRoot).filter((name) => store.stat(join(stateRoot, name))?.isDirectory);
123
+ for (const state of states) {
124
+ if (state === service)
125
+ continue;
126
+ const r = readRoot(state, root);
127
+ if (r?.credential)
128
+ return r;
129
+ }
130
+ return null;
131
+ }
132
+ /** What this twin's head does with a write. */
133
+ export function headOf(service, root) {
134
+ const bound = boundRoot(service, root);
135
+ return bound ? { kind: 'real', deploy: bound.deploy, root: bound } : { kind: 'simulated' };
136
+ }
137
+ /** A check refused the write: the entry is in the log as refused; the pack answers in the vendor's error shape. */
138
+ export class RefusedWriteError extends Error {
139
+ check;
140
+ reason;
141
+ actionId;
142
+ constructor(check, reason, actionId) {
143
+ super(`${check}: ${reason}`);
144
+ this.check = check;
145
+ this.reason = reason;
146
+ this.actionId = actionId;
147
+ this.name = 'RefusedWriteError';
148
+ }
149
+ }
150
+ /** The head could not be bound for a write that needed it (no credential sealed, no adapter registered,
151
+ * no root): a server-side fault, answered as one. */
152
+ export class HeadError extends Error {
153
+ constructor(message) { super(message); this.name = 'HeadError'; }
154
+ }
155
+ /** The vendor refused or failed the write: the entry is in the log as failed, with the vendor's words. */
156
+ export class VendorWriteError extends Error {
157
+ actionId;
158
+ constructor(message, actionId) {
159
+ super(message);
160
+ this.actionId = actionId;
161
+ this.name = 'VendorWriteError';
162
+ }
163
+ }
164
+ /** The receipt a branch entry's latest landed copy carries, if any. */
165
+ function receiptsByEntry(parent) {
166
+ const out = new Map();
167
+ for (const e of parent)
168
+ if (e.landsId && e.receipt)
169
+ out.set(e.landsId, e.receipt);
170
+ return out;
171
+ }
172
+ /** The branch entries a root twin still has to perform: `set` rows, not reverted, whose landed copy
173
+ * is absent or says `failed`/`skipped` (a `deployed` or `refused` copy settles the entry). */
174
+ export function deployableEntries(service, root) {
175
+ const parent = parentEntries(service, root);
176
+ const receipts = receiptsByEntry(parent);
177
+ const branch = branchEntries(service, root);
178
+ const reverted = new Set(branch.filter((e) => e.op === 'revert' && e.revertsActionId).map((e) => e.revertsActionId));
179
+ return branch.filter((e) => {
180
+ if (e.op !== 'set' || reverted.has(e.id) || isTwinBookkeeping(e))
181
+ return false;
182
+ const r = receipts.get(e.id);
183
+ return r === undefined || r.status === 'failed' || r.status === 'skipped';
184
+ });
185
+ }
186
+ const queues = new Map(); // one performer per twin: wire writes perform in append order
187
+ /**
188
+ * PERFORM this twin's deployable entries against its root, in order, checks in front of every one,
189
+ * stopping at the first refusal or failure. Each outcome lands as the entry's copy with its receipt
190
+ * on the parent log. `actionIds` narrows to a changeset's (or one write's) entries.
191
+ */
192
+ export async function performEntries(opts) {
193
+ const key = `${opts.root ?? ''}::${opts.service}`;
194
+ const prior = queues.get(key) ?? Promise.resolve();
195
+ const run = prior.then(() => performNow(opts), () => performNow(opts));
196
+ queues.set(key, run.catch(() => undefined));
197
+ return run;
198
+ }
199
+ async function performNow(opts) {
200
+ const { service, root } = opts;
201
+ const head = headOf(service, root);
202
+ if (head.kind !== 'real')
203
+ throw new Error(`the ${service} twin has no root — nothing to perform against`);
204
+ const vendor = vendorOf(head.root);
205
+ const adapters = opts.adapters ?? stateSystemFor(vendor) ?? stateSystemFor(service);
206
+ if (!adapters?.perform)
207
+ throw new Error(`the ${vendor} twin has no perform adapter — a protocol 2 pack names one in its descriptor and registers itself (registerPack)`);
208
+ 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) });
209
+ const checks = opts.checks ?? (await loadChecks(worldRootOf(head.root)));
210
+ const deployable = deployableEntries(service, root);
211
+ const entries = opts.actionIds === undefined ? deployable : opts.actionIds.map((id) => deployable.find((a) => a.id === id)).filter((a) => a !== undefined);
212
+ const report = { pushed: 0, deployed: [], skipped: [], replayed: [] };
213
+ if (opts.actionIds) {
214
+ const settled = receiptsByEntry(parentEntries(service, root));
215
+ for (const id of opts.actionIds)
216
+ if (settled.get(id)?.status === 'deployed')
217
+ report.replayed.push(id);
218
+ }
219
+ let stopped = false;
220
+ for (const action of entries) {
221
+ if (stopped) {
222
+ report.skipped.push(action.id);
223
+ continue;
224
+ }
225
+ const at = opts.at ?? worldNow();
226
+ const verdict = await runChecks(checks, action, readTree(service, root));
227
+ if (verdict) {
228
+ confirmAction({ service, actionId: action.id, subject: action.subject, fields: action.fields ?? {}, occurredAt: at, receipt: { status: 'refused', reason: `${verdict.name}: ${verdict.reason}` }, ...(root !== undefined ? { root } : {}) });
229
+ report.refused = { actionId: action.id, check: verdict.name, reason: verdict.reason };
230
+ stopped = true;
231
+ continue;
232
+ }
233
+ try {
234
+ // a declared reference to an adopted id is resolved before the pack sees the entry: the vendor is
235
+ // asked about #7, never the local #3 (docs/contributing/architecture.md#alias-aware-lookup-at-the-request-boundary)
236
+ const fields = resolveReferences(service, action.subject.type, action.fields ?? {}, aliasesFrom(parentEntries(service, root)));
237
+ const outcome = await adapters.perform(execute, fields === action.fields ? action : { ...action, fields }, performContext(service, root, rootCredentialFingerprint(head.root)));
238
+ // the vendor's answer, when the adapter returned one as fields, is what the landed copy holds
239
+ const answered = outcome.data !== null && typeof outcome.data === 'object' && !Array.isArray(outcome.data) ? outcome.data : {};
240
+ confirmAction({
241
+ service, actionId: action.id, subject: action.subject, fields: { ...fields, ...answered }, occurredAt: at,
242
+ ...(outcome.externalId ? { vendorSubjectId: outcome.externalId } : {}),
243
+ receipt: { status: 'deployed', ...(outcome.url ? { url: outcome.url } : {}) },
244
+ ...(root !== undefined ? { root } : {}),
245
+ });
246
+ report.pushed += 1;
247
+ report.deployed.push({ actionId: action.id, at, ...(outcome.externalId ? { externalId: outcome.externalId } : {}), ...(outcome.data !== undefined ? { data: outcome.data } : {}) });
248
+ }
249
+ catch (error) {
250
+ // a write the pack itself refuses (it can never be sent: its subject never reached the vendor) is
251
+ // settled as refused, as a check's refusal is, so the next deploy moves on to the entry after it;
252
+ // anything else failed and stays deployable for a retry
253
+ if (error instanceof RefusedWriteError) {
254
+ confirmAction({ service, actionId: action.id, subject: action.subject, fields: action.fields ?? {}, occurredAt: at, receipt: { status: 'refused', reason: error.message }, ...(root !== undefined ? { root } : {}) });
255
+ report.refused = { actionId: action.id, check: error.check, reason: error.reason };
256
+ stopped = true;
257
+ continue;
258
+ }
259
+ const message = error instanceof Error ? error.message : String(error);
260
+ confirmAction({ service, actionId: action.id, subject: action.subject, fields: action.fields ?? {}, occurredAt: at, receipt: { status: 'failed', reason: message }, ...(root !== undefined ? { root } : {}) });
261
+ report.failed = { actionId: action.id, error: message };
262
+ stopped = true;
263
+ }
264
+ }
265
+ return report;
266
+ }
267
+ /**
268
+ * The write path's second half (serve.ts): a write just appended to a twin whose head is real and
269
+ * `auto` is performed now. Returns the vendor's id when it minted one, so the answer carries it.
270
+ * Raises `RefusedWriteError` / `VendorWriteError` for the pack to answer in the vendor's shape.
271
+ */
272
+ export async function performAtHead(service, action, root) {
273
+ const head = headOf(service, root);
274
+ if (head.kind !== 'real' || head.deploy !== 'auto')
275
+ return { performed: false };
276
+ let report;
277
+ try {
278
+ report = await performEntries({ service, actionIds: [action.id], ...(root !== undefined ? { root } : {}) });
279
+ }
280
+ catch (error) {
281
+ throw error instanceof HeadError ? error : new HeadError(error instanceof Error ? error.message : String(error));
282
+ }
283
+ // under live use an entry the vendor did not take does not stay in the tree: the app was told no, the
284
+ // world says no — a revert lands (the receipt stays on the original, so the log shows what happened)
285
+ const undone = report.refused?.actionId === action.id || report.failed?.actionId === action.id;
286
+ if (undone)
287
+ revertAction({ service, actionId: action.id, occurredAt: worldNow(), ...(root !== undefined ? { root } : {}) });
288
+ if (report.refused?.actionId === action.id)
289
+ throw new RefusedWriteError(report.refused.check, report.refused.reason, action.id);
290
+ if (report.failed?.actionId === action.id)
291
+ throw new VendorWriteError(report.failed.error, action.id);
292
+ const done = report.deployed.find((d) => d.actionId === action.id);
293
+ return { performed: done !== undefined, ...(done?.externalId ? { externalId: done.externalId } : {}), ...(done?.data !== undefined ? { data: done.data } : {}) };
294
+ }
295
+ /** Wrap a pack's fetch: a `RefusedWriteError` or `VendorWriteError` the head raised becomes the
296
+ * vendor's own error body (the pack's `shape`), so an app under live use sees what the vendor
297
+ * would have said. Anything else propagates. */
298
+ export function answerVendorErrors(fetch, shape) {
299
+ return async (request) => {
300
+ try {
301
+ return await fetch(request);
302
+ }
303
+ catch (error) {
304
+ if (error instanceof RefusedWriteError || error instanceof VendorWriteError)
305
+ return shape(error);
306
+ // a head that could not be bound is a server error, answered as one (never a runtime's fallback
307
+ // page); anything else the twin threw propagates as it always did
308
+ if (error instanceof HeadError)
309
+ return Response.json({ error: error.message }, { status: 500 });
310
+ throw error;
311
+ }
312
+ };
313
+ }
@@ -0,0 +1,76 @@
1
+ import { type Entry } from './log.js';
2
+ export type HistoryLayout = {
3
+ base?: HistoryLayout;
4
+ parent: number;
5
+ branch: number;
6
+ };
7
+ type Segment = {
8
+ directory: string;
9
+ generation: string;
10
+ file: 'events.jsonl' | 'actions.jsonl' | 'origin.jsonl';
11
+ start: number;
12
+ count: number;
13
+ digest: string;
14
+ };
15
+ export type HistoryOrigin = {
16
+ at: string;
17
+ directory: string;
18
+ generation: string;
19
+ view: string;
20
+ position: number;
21
+ remoteView?: string;
22
+ depth: number;
23
+ incomplete?: boolean;
24
+ };
25
+ export type HistoryView = {
26
+ version: 1;
27
+ owner: {
28
+ directory: string;
29
+ generation: string;
30
+ };
31
+ layout: HistoryLayout;
32
+ segments: Segment[];
33
+ origin?: HistoryOrigin;
34
+ };
35
+ export type HistoryReference = {
36
+ view: string;
37
+ position: number;
38
+ };
39
+ export declare function validateHistoryOrigin(origin: HistoryOrigin, layout?: HistoryLayout): void;
40
+ export declare const historyEntryTime: (entry: Entry) => number;
41
+ export declare const historyDigest: (value: unknown) => string;
42
+ export declare function historyLength(layout: HistoryLayout, depth?: number): number;
43
+ export declare function readHistoryView(directory: string, id: string): HistoryView;
44
+ export declare function historyEntries(view: HistoryView): Entry[];
45
+ export declare function historyPrefix(view: HistoryView, at: number): HistoryView;
46
+ export declare function saveHistoryView(directory: string, view: HistoryView): string;
47
+ /** What an inherited history's content stands on, as file facts: every segment file the view names
48
+ * (and the view document itself) and each named directory's ancestry record (its generation). A read cache keyed by this sees an
49
+ * ancestor's retained history truncated, replaced or its directory re-created, which the full read
50
+ * refuses (`historyEntries`), instead of serving a tree folded from the history it replaced. */
51
+ export declare function inheritedHistoryKey(service: string, root?: string): string;
52
+ export declare function inheritedHistory(service: string, root?: string): HistoryView | undefined;
53
+ /** Capture under the same coordinator as append and complete observation batches. */
54
+ export declare function captureHistory(service: string, root?: string): {
55
+ view: string;
56
+ position: number;
57
+ descriptor: HistoryView;
58
+ };
59
+ export declare function originHead(service: string, root?: string): {
60
+ view: string;
61
+ remoteView: string;
62
+ } | null;
63
+ /** A complete remote view shares cached rows by content, never by mutable offsets or entry ID. */
64
+ export declare function publishOriginHistory(service: string, root: string, remoteView: string, layout: HistoryLayout, entries: Entry[]): {
65
+ view: string;
66
+ appended: number;
67
+ };
68
+ /** A changeset binds the complete inherited view, including this root's observations. */
69
+ export declare function captureParentHistory(service: string, root?: string): HistoryReference;
70
+ /** Compare evaluated views, so removals, reordered rows and layout changes count as drift. */
71
+ export declare function historyChanges(service: string, before: HistoryView, after: HistoryView): Entry[];
72
+ /** A timestamp can select across layers; represent it as a view, never a misleading offset. */
73
+ export declare function historyAtInstant(service: string, instant: string, root?: string, selectedView?: string): HistoryReference;
74
+ /** Replace only the tracked origin layer; inherited local layers keep their exact projection order. */
75
+ export declare function replaceHistoryOrigin(previous: HistoryView, origin: HistoryOrigin, next: HistoryOrigin): HistoryView;
76
+ export {};