@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,70 @@
1
+ import type { TwinAuthStrategy } from './executor.js';
2
+ import { type TwinAction } from './actions.js';
3
+ import type { PerformAction } from './head.js';
4
+ import type { RemoteExecute } from './remote-execute.js';
5
+ import type { TwinResource } from './serve.js';
6
+ export type DeployPolicy = 'auto' | 'gated' | 'hold';
7
+ /** A twin's root: the vendor's real account. Key-free; the credential is sealed elsewhere. */
8
+ export type RootConfig = {
9
+ /** the vendor's API, or the one resource the account is (`https://api.github.com/repos/o/r`) */
10
+ url: string;
11
+ deploy: DeployPolicy;
12
+ /** the world's override of the pack's refresh posture: an interval, and the least time between on-demand refreshes */
13
+ refresh?: {
14
+ every?: string;
15
+ webhook?: boolean;
16
+ atMost?: string;
17
+ };
18
+ /** the one resource the account is, under `url` — `repos/acme/web` for GitHub — when the vendor's
19
+ * adapters need it named; the adapters see `url/scope` as their origin */
20
+ scope?: string;
21
+ };
22
+ export declare function rootPath(service: string, root?: string): string;
23
+ export declare function readRoot(service: string, root?: string): RootConfig | null;
24
+ export declare function writeRoot(service: string, config: RootConfig, root?: string): void;
25
+ export declare function clearRoot(service: string, root?: string): void;
26
+ /** The pack's half of the real state system. `perform` applies one entry against the vendor;
27
+ * `refresh` observes the vendor into the parent log (it folds through the kernel — a pack never
28
+ * writes a root log by hand); `ingest` folds one signed webhook. All over the kernel executor. */
29
+ export type StateSystemAdapters = {
30
+ perform?: PerformAction;
31
+ refresh?: (execute: RemoteExecute, opts: {
32
+ root?: string;
33
+ origin?: string;
34
+ credential?: string;
35
+ }) => Promise<unknown>;
36
+ ingest?: (request: Request, ctx: {
37
+ root?: string;
38
+ secret?: string;
39
+ }) => Response | Promise<Response>;
40
+ };
41
+ /** Register a twin's adapters (the mounting host does this from the pack's descriptor or its
42
+ * anchored exports). Replaces any earlier registration for the service. */
43
+ export declare function registerStateSystem(service: string, a: StateSystemAdapters): void;
44
+ export declare function stateSystemFor(service: string): StateSystemAdapters | undefined;
45
+ export declare function clearStateSystems(): void;
46
+ export declare function registerAuthStrategy(service: string, a: TwinAuthStrategy): void;
47
+ export declare function authStrategyFor(service: string): TwinAuthStrategy | undefined;
48
+ /** A check: deterministic, sees the entry and the tree, answers pass or fail with a reason. */
49
+ /** A check may answer at once or later: a hosted World runs its checks in an isolate of their own,
50
+ * reached over RPC. */
51
+ export type CheckResult = {
52
+ ok: true;
53
+ } | {
54
+ ok: false;
55
+ reason: string;
56
+ };
57
+ export type Check = {
58
+ name: string;
59
+ run: (entry: TwinAction, tree: TwinResource[]) => CheckResult | Promise<CheckResult>;
60
+ };
61
+ export type CheckVerdict = {
62
+ name: string;
63
+ reason: string;
64
+ } | null;
65
+ /** Run every check over one entry; the first failure is the verdict. */
66
+ export declare function runChecks(checks: Check[], entry: TwinAction, tree: TwinResource[]): Promise<CheckVerdict>;
67
+ /** The one check every world ships: no credential-shaped string leaves for a vendor. Looks at
68
+ * every string field of the entry (and its projection), so a token in a message body, a title or
69
+ * a description is caught wherever the vendor would have put it. */
70
+ export declare const NO_SECRETS_CHECK: Check;
@@ -0,0 +1,90 @@
1
+ // THE STATE SYSTEM (docs/concepts/the-model.md): what a write does at the
2
+ // head of a twin's log, and how the tree is kept current. Two implementations of one interface,
3
+ // bound per twin per world:
4
+ // simulated — apply appends the entry and the tree is the projection; refresh is nothing. That
5
+ // is every twin with no root, and every line of the write path as it stands.
6
+ // real — the twin has a ROOT: the vendor's real account. Apply performs the entry against
7
+ // the vendor with the sealed credential (the pack's perform adapter over the kernel
8
+ // executor) and lands the receipt on the parent log; refresh fills the parent log
9
+ // from the vendor (the pack's refresh adapter). Reads never egress.
10
+ // The binding is a file in the twin's state dir (`root.json`), written by `volter twin <vendor>
11
+ // root`, read here — no env var, no pack code. The adapters come from the pack's descriptor
12
+ // (protocol 2 `stateSystem`) or its anchored exports, registered by whoever mounts the twin.
13
+ // CHECKS run before any entry is performed: a check that fails lands the entry as `refused` with
14
+ // its reason and the deploy stops there. Checks are the world's files; the runtime loads them.
15
+ import { dirname, join } from 'node:path';
16
+ import { worldPaths } from "./storage.js";
17
+ import { getActiveWorldStore } from "./world-store.js";
18
+ export function rootPath(service, root) {
19
+ return join(dirname(worldPaths(service, root).events), 'root.json');
20
+ }
21
+ export function readRoot(service, root) {
22
+ const raw = getActiveWorldStore().read(rootPath(service, root));
23
+ return raw === null ? null : JSON.parse(raw);
24
+ }
25
+ export function writeRoot(service, config, root) {
26
+ const path = rootPath(service, root);
27
+ getActiveWorldStore().mkdir(dirname(path));
28
+ getActiveWorldStore().writeAtomic(path, `${JSON.stringify(config, null, 2)}\n`);
29
+ }
30
+ export function clearRoot(service, root) {
31
+ const path = rootPath(service, root);
32
+ if (getActiveWorldStore().exists(path))
33
+ getActiveWorldStore().remove(path);
34
+ }
35
+ const adapters = new Map();
36
+ /** Register a twin's adapters (the mounting host does this from the pack's descriptor or its
37
+ * anchored exports). Replaces any earlier registration for the service. */
38
+ export function registerStateSystem(service, a) { adapters.set(service, a); }
39
+ export function stateSystemFor(service) { return adapters.get(service); }
40
+ export function clearStateSystems() { adapters.clear(); authStrategies.clear(); }
41
+ /** HOW EACH VENDOR AUTHENTICATES, registered from the pack's descriptor by `registerPack` — the
42
+ * same shape as the adapters above, and it lives here rather than in the pack registry so the
43
+ * head can read it without importing one (there is no cycle to argue about). Absent means header
44
+ * replacement, which is what the executor has always done. */
45
+ const authStrategies = new Map();
46
+ export function registerAuthStrategy(service, a) { authStrategies.set(service, a); }
47
+ export function authStrategyFor(service) { return authStrategies.get(service); }
48
+ /** Run every check over one entry; the first failure is the verdict. */
49
+ export async function runChecks(checks, entry, tree) {
50
+ for (const check of checks) {
51
+ const result = await check.run(entry, tree);
52
+ if (!result.ok)
53
+ return { name: check.name, reason: result.reason };
54
+ }
55
+ return null;
56
+ }
57
+ /** The one check every world ships: no credential-shaped string leaves for a vendor. Looks at
58
+ * every string field of the entry (and its projection), so a token in a message body, a title or
59
+ * a description is caught wherever the vendor would have put it. */
60
+ export const NO_SECRETS_CHECK = {
61
+ name: 'no-secrets',
62
+ run(entry) {
63
+ const CREDENTIAL = /\b(?:sk|pk|rk)[_-](?:live|test|prod)[_-][A-Za-z0-9]{8,}|\bxox[abprs]-[A-Za-z0-9-]{10,}|\bgh[pousr]_[A-Za-z0-9]{20,}|\bAKIA[0-9A-Z]{16}\b|\bsk-[A-Za-z0-9]{20,}|\bsk-(?:proj|ant)-[A-Za-z0-9_-]{16,}|-----BEGIN [A-Z ]*PRIVATE KEY-----/;
64
+ const hit = (value, path) => {
65
+ if (typeof value === 'string')
66
+ return CREDENTIAL.test(value) ? path : null;
67
+ if (Array.isArray(value)) {
68
+ for (const [i, v] of value.entries()) {
69
+ const h = hit(v, `${path}[${i}]`);
70
+ if (h)
71
+ return h;
72
+ }
73
+ return null;
74
+ }
75
+ if (value && typeof value === 'object') {
76
+ for (const [k, v] of Object.entries(value)) {
77
+ const h = hit(v, path ? `${path}.${k}` : k);
78
+ if (h)
79
+ return h;
80
+ }
81
+ return null;
82
+ }
83
+ return null;
84
+ };
85
+ const where = hit(entry.fields, '') ?? hit(entry.projection, 'projection') ?? hit(entry.input, 'input');
86
+ return where === null ? { ok: true } : { ok: false, reason: `a credential-shaped string in ${where}` };
87
+ },
88
+ };
89
+ // DEPLOY and the deployable set live in head.ts (`performEntries`, `deployableEntries`): the same
90
+ // code performs a wire write at an `auto` head, a pushed changeset and `volter world deploy`.
@@ -0,0 +1,101 @@
1
+ import type { AppendEventResult, GenericWorldState, WorldPaths, WorldReducer, WorldServiceEvent } from './types.js';
2
+ /**
3
+ * Name of the per-project state directory holding world data
4
+ * (`<root>/<stateDir>/world/...`). Defaults to `.volter`; hosts that need a
5
+ * different directory set VOLTER_STATE_DIR. Every path in this package must
6
+ * go through worldStateRoot — never the literal.
7
+ */
8
+ export declare function stateDirName(): string;
9
+ export declare function worldStateRoot(root?: string): string;
10
+ export declare function observeWorldPaths(observer: (service: string, root: string | undefined) => void): void;
11
+ export declare function worldPaths(service: string, root?: string): WorldPaths;
12
+ /** Read + parse a whole-file JSON sidecar, citing the path on a parse failure (mirrors
13
+ * readJsonl's `${path}:...` error shape). Callers own existence semantics — this parses a
14
+ * file that is expected to exist; guard with existsSync first when absence is allowed. */
15
+ export declare function readJsonFile<T>(path: string): T;
16
+ /**
17
+ * Append `data` to `path`. When VOLTER_DURABLE=1, `fsyncSync` the file after the write so
18
+ * the appended record survives an OS crash / power loss — not just a process crash.
19
+ *
20
+ * Default (VOLTER_DURABLE unset) is OFF: `appendFileSync` is a completed write syscall, so a
21
+ * *process* crash after it returns still leaves the record on the log (the OS owns the page
22
+ * cache). The window this leaves open is a **kernel-panic / power loss** between the write
23
+ * landing in the page cache and the fs flushing it to stable storage — a torn or lost tail
24
+ * record. That crash window matters more once real pulled staging data lives in these files
25
+ * (purpose 2), so hosts that need durability opt in with VOLTER_DURABLE=1 at the cost of an
26
+ * fsync per append. See docs/contributing/architecture.md D1.
27
+ */
28
+ export declare function appendDurable(path: string, data: string): void;
29
+ /**
30
+ * Opt-in structured stderr logging for the audit trail (the July 2026 architecture review, row D3):
31
+ * one line per action-log append and per push-ledger row, so "who reviewed the change
32
+ * that caused this real write" is mechanically greppable from a single log stream via
33
+ * `correlationId`. Off by default — mirrors the VOLTER_DURABLE opt-in policy above: no
34
+ * always-on I/O, never on the default path. Set VOLTER_TWIN_LOG=1 to enable.
35
+ */
36
+ export declare function twinLog(kind: string, details: Record<string, unknown>): void;
37
+ /** Run `fn` holding an exclusive cross-process lock, via the active store. On the fs
38
+ * store this is the reclaimable file lock the event log uses; on an in-memory,
39
+ * single-instance store it is a synchronous pass-through. Used to make read-then-append
40
+ * critical sections atomic. Public so world-runtime can guard concurrent `upWorld`
41
+ * claims of one instance dir with the SAME lock semantics the event log uses (TWIN-36). */
42
+ export declare function withFileLock<T>(lockPath: string, fn: () => T): T;
43
+ /** The parent log as a protocol 1 pack reads it: every entry the branch inherited or landed, as
44
+ * observed rows (log.ts `toEvent`). A branched world sees its base world's logs to the branch
45
+ * position first. */
46
+ export declare function listEvents(service: string, root?: string): WorldServiceEvent[];
47
+ /** Exported so other modules that share a service's events log (e.g. egress.ts,
48
+ * TWIN-58) can serialize their own critical sections on the SAME lock appendEvent
49
+ * itself uses, instead of inventing a second, uncoordinated lockfile. */
50
+ export declare function eventsLockPath(paths: WorldPaths): string;
51
+ /**
52
+ * Serializes mutations that can change a service's projected state, regardless of whether the
53
+ * durable row lands in events.jsonl or actions.jsonl. The per-log locks still protect their own
54
+ * append/dedupe mechanics; this outer lock makes a projection-based decision linearizable against
55
+ * connector observations as well as local actions.
56
+ */
57
+ export declare function projectionLockPath(paths: WorldPaths): string;
58
+ /**
59
+ * The append body, assuming the caller already holds `eventsLockPath(paths)`. Split
60
+ * out so `commitQueuedEvents` can run a whole batch of appends (and the rebuild that
61
+ * follows them) under ONE lock acquisition instead of nesting a fresh `withFileLock`
62
+ * per row — `withFileLock` is not reentrant, so re-acquiring it from inside an
63
+ * already-held lock in the same process would just spin to its own timeout. Also
64
+ * exported for egress.ts (TWIN-58): performExternalWrite's ledger-check +
65
+ * intent-append span holds `eventsLockPath` itself, so it must append through this
66
+ * already-locked path rather than the public `appendEvent` (which would try to
67
+ * re-acquire the same lock and spin to its own timeout).
68
+ */
69
+ export declare function appendEvent(event: WorldServiceEvent, root?: string): AppendEventResult;
70
+ export declare function genericWorldReducer(state: GenericWorldState, event: WorldServiceEvent): GenericWorldState;
71
+ export declare function emptyGenericState(service: string): GenericWorldState;
72
+ export declare function rebuildState<State>(service: string, initialState: State, reducer: WorldReducer<State>, root?: string): State;
73
+ export declare function rebuildGenericState(service: string, root?: string): GenericWorldState;
74
+ export declare function loadState<T = unknown>(service: string, root?: string): T | null;
75
+ export declare function createEvent(input: Omit<WorldServiceEvent, 'schemaVersion' | 'observedAt'> & {
76
+ schemaVersion?: number;
77
+ observedAt?: string;
78
+ }): WorldServiceEvent;
79
+ export type ScrubResult = {
80
+ /** The directory scrub targeted (may not have existed). */
81
+ target: string;
82
+ /** Every file actually removed, path relative to `target`, in no particular order. */
83
+ removed: string[];
84
+ /** One-line human summary, safe to print as-is. */
85
+ message: string;
86
+ };
87
+ /** Delete one service's pulled data at rest: its event log, queued events,
88
+ * rebuilt state.json, and any resources/cursors/ingests sidecars — everything
89
+ * `worldPaths(service)` points at. Refuses (unless `force`) when the dir holds
90
+ * anything storage.ts didn't put there. */
91
+ export declare function scrubService(service: string, options?: {
92
+ root?: string;
93
+ force?: boolean;
94
+ }): ScrubResult;
95
+ /** Delete the ENTIRE world state dir (`<root>/<stateDir>/world`) — every
96
+ * service's pulled data at once. Refuses (unless `force`) when any entry in it
97
+ * isn't itself a recognizable per-service state dir. */
98
+ export declare function scrubWorld(options?: {
99
+ root?: string;
100
+ force?: boolean;
101
+ }): ScrubResult;
@@ -0,0 +1,337 @@
1
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
2
+ import { assertNotBeingRemoved, withAncestryLock, withStateRemoval } from "./ancestry.js";
3
+ import { appendParentEntry, toEntry } from "./log.js";
4
+ import { GenericWorldStateSchema, WorldServiceEventSchema, } from "./schemas.js";
5
+ import { getActiveWorldStore } from "./world-store.js";
6
+ import { parentEntries, toEvent } from "./log.js";
7
+ function nowIso() {
8
+ return new Date().toISOString();
9
+ }
10
+ function projectRoot(root) {
11
+ return resolve(root || process.env.PROJECT_ROOT || process.cwd());
12
+ }
13
+ /**
14
+ * Name of the per-project state directory holding world data
15
+ * (`<root>/<stateDir>/world/...`). Defaults to `.volter`; hosts that need a
16
+ * different directory set VOLTER_STATE_DIR. Every path in this package must
17
+ * go through worldStateRoot — never the literal.
18
+ */
19
+ export function stateDirName() {
20
+ const name = process.env.VOLTER_STATE_DIR || '.volter';
21
+ if (isAbsolute(name) || name.split(/[\\/]/).includes('..'))
22
+ throw new Error('VOLTER_STATE_DIR must be a relative directory without .. segments');
23
+ return name;
24
+ }
25
+ export function worldStateRoot(root) {
26
+ return join(projectRoot(root), stateDirName(), 'world');
27
+ }
28
+ function assertServiceName(service) {
29
+ if (!/^[A-Za-z0-9_-]+$/.test(service)) {
30
+ throw new Error(`Invalid world service name: ${service}`);
31
+ }
32
+ return service;
33
+ }
34
+ /**
35
+ * IDENTITY SEAM for the request journal (serve.ts). `worldPaths` is the single funnel every
36
+ * state read/write in every pack goes through, so it is also the one place a twin says, in its
37
+ * own words, WHO it is (`service`) and WHERE its state lives (`root`) — the two things an HTTP
38
+ * layer that only sees `Bun.serve({fetch})` cannot know. serve.ts installs an observer here so
39
+ * the journal it writes lands beside that twin's `actions.jsonl` instead of guessing a root.
40
+ * Nothing else may use this: it is a one-slot observability hook, never a control seam.
41
+ */
42
+ let worldPathsObserver;
43
+ export function observeWorldPaths(observer) {
44
+ worldPathsObserver = observer;
45
+ }
46
+ export function worldPaths(service, root) {
47
+ const safeService = assertServiceName(service);
48
+ worldPathsObserver?.(safeService, root);
49
+ const resolvedRoot = projectRoot(root);
50
+ const dir = join(worldStateRoot(root), safeService);
51
+ return {
52
+ root: resolvedRoot,
53
+ service: safeService,
54
+ dir,
55
+ events: join(dir, 'events.jsonl'),
56
+ state: join(dir, 'state.json'),
57
+ resources: join(dir, 'resources'),
58
+ cursors: join(dir, 'cursors'),
59
+ ingests: join(dir, 'ingests'),
60
+ eventQueue: join(dir, 'event-queue.jsonl'),
61
+ };
62
+ }
63
+ function readJsonl(path) {
64
+ const rows = [];
65
+ for (const [index, line] of getActiveWorldStore().readLines(path).entries()) {
66
+ if (!line.trim())
67
+ continue;
68
+ try {
69
+ rows.push(JSON.parse(line));
70
+ }
71
+ catch (error) {
72
+ throw new Error(`${path}:${index + 1}: invalid JSONL row: ${error.message}`);
73
+ }
74
+ }
75
+ return rows;
76
+ }
77
+ /** Read + parse a whole-file JSON sidecar, citing the path on a parse failure (mirrors
78
+ * readJsonl's `${path}:...` error shape). Callers own existence semantics — this parses a
79
+ * file that is expected to exist; guard with existsSync first when absence is allowed. */
80
+ export function readJsonFile(path) {
81
+ try {
82
+ const raw = getActiveWorldStore().read(path);
83
+ if (raw === null)
84
+ throw new Error('no such file');
85
+ return JSON.parse(raw);
86
+ }
87
+ catch (error) {
88
+ throw new Error(`${path}: invalid JSON: ${error.message}`);
89
+ }
90
+ }
91
+ /**
92
+ * Append `data` to `path`. When VOLTER_DURABLE=1, `fsyncSync` the file after the write so
93
+ * the appended record survives an OS crash / power loss — not just a process crash.
94
+ *
95
+ * Default (VOLTER_DURABLE unset) is OFF: `appendFileSync` is a completed write syscall, so a
96
+ * *process* crash after it returns still leaves the record on the log (the OS owns the page
97
+ * cache). The window this leaves open is a **kernel-panic / power loss** between the write
98
+ * landing in the page cache and the fs flushing it to stable storage — a torn or lost tail
99
+ * record. That crash window matters more once real pulled staging data lives in these files
100
+ * (purpose 2), so hosts that need durability opt in with VOLTER_DURABLE=1 at the cost of an
101
+ * fsync per append. See docs/contributing/architecture.md D1.
102
+ */
103
+ export function appendDurable(path, data) {
104
+ getActiveWorldStore().append(path, data);
105
+ }
106
+ /**
107
+ * Opt-in structured stderr logging for the audit trail (the July 2026 architecture review, row D3):
108
+ * one line per action-log append and per push-ledger row, so "who reviewed the change
109
+ * that caused this real write" is mechanically greppable from a single log stream via
110
+ * `correlationId`. Off by default — mirrors the VOLTER_DURABLE opt-in policy above: no
111
+ * always-on I/O, never on the default path. Set VOLTER_TWIN_LOG=1 to enable.
112
+ */
113
+ export function twinLog(kind, details) {
114
+ if (process.env.VOLTER_TWIN_LOG !== '1')
115
+ return;
116
+ console.error(`[twin:${kind}]`, JSON.stringify(details));
117
+ }
118
+ function appendJsonl(path, value) {
119
+ const store = getActiveWorldStore();
120
+ store.mkdir(dirname(path));
121
+ store.append(path, `${JSON.stringify(value)}\n`);
122
+ }
123
+ /** Run `fn` holding an exclusive cross-process lock, via the active store. On the fs
124
+ * store this is the reclaimable file lock the event log uses; on an in-memory,
125
+ * single-instance store it is a synchronous pass-through. Used to make read-then-append
126
+ * critical sections atomic. Public so world-runtime can guard concurrent `upWorld`
127
+ * claims of one instance dir with the SAME lock semantics the event log uses (TWIN-36). */
128
+ export function withFileLock(lockPath, fn) {
129
+ return withAncestryLock(() => { assertNotBeingRemoved(dirname(lockPath)); return getActiveWorldStore().withLock(lockPath, fn); });
130
+ }
131
+ function writeJsonAtomic(path, value) {
132
+ getActiveWorldStore().writeAtomic(path, `${JSON.stringify(value, null, 2)}\n`);
133
+ }
134
+ /** The parent log as a protocol 1 pack reads it: every entry the branch inherited or landed, as
135
+ * observed rows (log.ts `toEvent`). A branched world sees its base world's logs to the branch
136
+ * position first. */
137
+ export function listEvents(service, root) {
138
+ return parentEntries(service, root).map(toEvent);
139
+ }
140
+ function ensureEventDirs(paths) {
141
+ const store = getActiveWorldStore();
142
+ store.mkdir(paths.dir);
143
+ store.mkdir(paths.resources);
144
+ store.mkdir(paths.cursors);
145
+ store.mkdir(paths.ingests);
146
+ }
147
+ /** Exported so other modules that share a service's events log (e.g. egress.ts,
148
+ * TWIN-58) can serialize their own critical sections on the SAME lock appendEvent
149
+ * itself uses, instead of inventing a second, uncoordinated lockfile. */
150
+ export function eventsLockPath(paths) {
151
+ return join(paths.dir, 'events.jsonl.lock');
152
+ }
153
+ /**
154
+ * Serializes mutations that can change a service's projected state, regardless of whether the
155
+ * durable row lands in events.jsonl or actions.jsonl. The per-log locks still protect their own
156
+ * append/dedupe mechanics; this outer lock makes a projection-based decision linearizable against
157
+ * connector observations as well as local actions.
158
+ */
159
+ export function projectionLockPath(paths) {
160
+ return join(paths.dir, 'projection.lock');
161
+ }
162
+ /**
163
+ * The append body, assuming the caller already holds `eventsLockPath(paths)`. Split
164
+ * out so `commitQueuedEvents` can run a whole batch of appends (and the rebuild that
165
+ * follows them) under ONE lock acquisition instead of nesting a fresh `withFileLock`
166
+ * per row — `withFileLock` is not reentrant, so re-acquiring it from inside an
167
+ * already-held lock in the same process would just spin to its own timeout. Also
168
+ * exported for egress.ts (TWIN-58): performExternalWrite's ledger-check +
169
+ * intent-append span holds `eventsLockPath` itself, so it must append through this
170
+ * already-locked path rather than the public `appendEvent` (which would try to
171
+ * re-acquire the same lock and spin to its own timeout).
172
+ */
173
+ export function appendEvent(event, root) {
174
+ const parsed = WorldServiceEventSchema.parse(event);
175
+ const paths = worldPaths(parsed.service, root);
176
+ ensureEventDirs(paths);
177
+ const { appended } = appendParentEntry(toEntry(parsed), root);
178
+ return { event: parsed, appended };
179
+ }
180
+ export function genericWorldReducer(state, event) {
181
+ const key = `${event.subject.type}:${event.subject.id}`;
182
+ return GenericWorldStateSchema.parse({
183
+ ...state,
184
+ eventCount: state.eventCount + 1,
185
+ latestEventId: event.id,
186
+ subjects: {
187
+ ...state.subjects,
188
+ [key]: {
189
+ type: event.subject.type,
190
+ id: event.subject.id,
191
+ latestEventId: event.id,
192
+ latestType: event.type,
193
+ updatedAt: event.occurredAt || event.observedAt,
194
+ },
195
+ },
196
+ });
197
+ }
198
+ export function emptyGenericState(service) {
199
+ return {
200
+ version: 1,
201
+ service: assertServiceName(service),
202
+ rebuiltAt: nowIso(),
203
+ eventCount: 0,
204
+ subjects: {},
205
+ };
206
+ }
207
+ export function rebuildState(service, initialState, reducer, root) {
208
+ const paths = worldPaths(service, root);
209
+ const state = listEvents(service, root).reduce(reducer, initialState);
210
+ writeJsonAtomic(paths.state, state);
211
+ return state;
212
+ }
213
+ export function rebuildGenericState(service, root) {
214
+ return rebuildState(service, emptyGenericState(service), genericWorldReducer, root);
215
+ }
216
+ export function loadState(service, root) {
217
+ const paths = worldPaths(service, root);
218
+ if (!getActiveWorldStore().exists(paths.state))
219
+ return null;
220
+ return readJsonFile(paths.state);
221
+ }
222
+ export function createEvent(input) {
223
+ return WorldServiceEventSchema.parse({
224
+ schemaVersion: 1,
225
+ observedAt: nowIso(),
226
+ ...input,
227
+ });
228
+ }
229
+ /** Filenames/dirnames ANY control-plane src module ever writes inside a single
230
+ * service's state dir (see worldPaths) — plus the lockfiles withFileLock creates
231
+ * next to a log and the `.tmp`/`.stale.*` sidecars its atomic-write/reclaim paths
232
+ * use. storage.ts itself only accounts for events.jsonl/event-queue.jsonl/
233
+ * state.json/resources/cursors/ingests; the rest are written by other modules
234
+ * that share the same service dir (actions.ts, pushLedger.ts, plan.ts, lease.ts,
235
+ * refs.ts, fork.ts, queueLifecycle.ts, and connector-owned coordination) — this set MUST stay in sync with every
236
+ * one of them so scrubService never demands --force for an ordinary twin. */
237
+ const KNOWN_SERVICE_ENTRIES = new Set([
238
+ 'ancestry.json', 'history-format.json', 'views', 'origin.jsonl', 'origin-head.json',
239
+ 'events.jsonl',
240
+ 'events.jsonl.lock',
241
+ 'event-queue.jsonl',
242
+ 'event-queue.jsonl.lock',
243
+ 'state.json',
244
+ 'resources',
245
+ 'cursors',
246
+ 'ingests',
247
+ // actions.ts: the local transaction-commit log + its lock.
248
+ 'actions.jsonl',
249
+ 'actions.jsonl.lock',
250
+ // pushLedger.ts: the push-phase ledger for real-vendor replication.
251
+ 'push-ledger.jsonl',
252
+ // queueLifecycle.ts: the append-only status ledger over event-queue.jsonl rows.
253
+ 'event-queue-status.jsonl',
254
+ // fork.ts: fork metadata (base snapshot + divergence baseline) for a fork root.
255
+ 'fork-meta.json',
256
+ // plan.ts: one JSON file per apply plan, under plans/<planId>.json.
257
+ 'plans',
258
+ // lease.ts: one JSON file per apply lease, under leases/<leaseId>.json.
259
+ 'leases',
260
+ // refs.ts: remote/local checkpoint refs, under refs/remote/<provider>/<name>.json
261
+ // and refs/local/<forkId>.json.
262
+ 'refs',
263
+ // Vendor connectors may keep private, non-secret workflow leases and monotone observation clocks
264
+ // here when an async provider span must be serialized across processes.
265
+ 'connector-workflows',
266
+ ]);
267
+ function isKnownServiceEntry(name) {
268
+ // log.ts: the branch pointer and the checkpoint
269
+ if (name === 'branch.json' || name === 'checkpoints')
270
+ return true;
271
+ return KNOWN_SERVICE_ENTRIES.has(name) || name.endsWith('.tmp') || /\.stale\.\d+\.\d+$/.test(name);
272
+ }
273
+ /** A service dir "looks like" one of ours when every entry in it is something
274
+ * storage.ts is known to create. A directory that doesn't exist yet trivially
275
+ * looks fine (scrub will just no-op on it). */
276
+ function looksLikeServiceStateDir(dir) {
277
+ const store = getActiveWorldStore();
278
+ if (!store.exists(dir))
279
+ return true;
280
+ return store.list(dir).every((entry) => isKnownServiceEntry(entry));
281
+ }
282
+ /** The whole world dir "looks like" ours when every entry in it is itself a
283
+ * directory that looks like a service state dir (each service gets one subdir
284
+ * under worldStateRoot — see worldPaths). */
285
+ function looksLikeWorldStateDir(dir) {
286
+ const store = getActiveWorldStore();
287
+ if (!store.exists(dir))
288
+ return true;
289
+ return store.list(dir).every((entry) => {
290
+ const full = join(dir, entry);
291
+ return (store.stat(full)?.isDirectory ?? false) && looksLikeServiceStateDir(full);
292
+ });
293
+ }
294
+ /** Every file under `dir`, path relative to `dir`, depth-first. Used to report
295
+ * exactly what scrub is about to remove before it removes it. */
296
+ function listFilesRecursive(dir, base = dir) {
297
+ const store = getActiveWorldStore();
298
+ if (!store.exists(dir))
299
+ return [];
300
+ const out = [];
301
+ for (const name of store.list(dir)) {
302
+ const full = join(dir, name);
303
+ if (store.stat(full)?.isDirectory)
304
+ out.push(...listFilesRecursive(full, base));
305
+ else
306
+ out.push(full.slice(base.length + 1));
307
+ }
308
+ return out;
309
+ }
310
+ function scrubDir(dir, label, looksLike, options) {
311
+ const store = getActiveWorldStore();
312
+ if (!store.exists(dir)) {
313
+ return { target: dir, removed: [], message: `nothing to scrub: ${label} (${dir}) does not exist` };
314
+ }
315
+ if (!options.force && !looksLike(dir)) {
316
+ throw new Error(`world scrub: refusing to remove "${dir}" — it does not look like a world state directory ` +
317
+ `(unexpected contents). Pass --force (or { force: true }) to scrub it anyway.`);
318
+ }
319
+ const removed = listFilesRecursive(dir);
320
+ withStateRemoval(dir, () => store.remove(dir));
321
+ return { target: dir, removed, message: `scrubbed ${label}: removed ${removed.length} file(s) under ${dir}` };
322
+ }
323
+ /** Delete one service's pulled data at rest: its event log, queued events,
324
+ * rebuilt state.json, and any resources/cursors/ingests sidecars — everything
325
+ * `worldPaths(service)` points at. Refuses (unless `force`) when the dir holds
326
+ * anything storage.ts didn't put there. */
327
+ export function scrubService(service, options = {}) {
328
+ const paths = worldPaths(service, options.root);
329
+ return scrubDir(paths.dir, `service "${service}"`, looksLikeServiceStateDir, options);
330
+ }
331
+ /** Delete the ENTIRE world state dir (`<root>/<stateDir>/world`) — every
332
+ * service's pulled data at once. Refuses (unless `force`) when any entry in it
333
+ * isn't itself a recognizable per-service state dir. */
334
+ export function scrubWorld(options = {}) {
335
+ const dir = worldStateRoot(options.root);
336
+ return scrubDir(dir, 'entire world state dir', looksLikeWorldStateDir, options);
337
+ }
@@ -0,0 +1,64 @@
1
+ /** The header a host sets when it mounts a twin under a path (a served World's wire:
2
+ * `/<org>/<world>/<vendor>`), the reverse-proxy convention; with it the host sets
3
+ * `x-forwarded-host` and `x-forwarded-proto` to where the World is reached. */
4
+ export declare const TWIN_PREFIX_HEADER = "x-forwarded-prefix";
5
+ /** Where this twin is reached for this request: the request's origin plus the path a host mounted
6
+ * the twin under. A pack mints every live URL it hands back (an upload target, a file link, a
7
+ * callback) from this, never from the bare origin, so the URL works behind a served World. */
8
+ export declare function twinPublicBase(request: Request): string;
9
+ /** A twin's BYTE-STREAM door: a pack whose clients speak a TCP protocol (SMTP, the MySQL wire) serves
10
+ * it over whatever carries the bytes — a local listener's socket, or a WebSocket to a hosted World.
11
+ * The pack owns framing and protocol; the host owns the transport. `open` greets through the sink;
12
+ * `data` hands the connection bytes in arrival order; `settled` resolves when every byte handed so
13
+ * far has been answered; `close` is the peer going away. */
14
+ export type TwinStreamSink = {
15
+ write(bytes: Uint8Array): void;
16
+ end(): void;
17
+ };
18
+ export type TwinStreamConnection = {
19
+ data(chunk: Uint8Array): void;
20
+ settled(): Promise<void>;
21
+ close(): void;
22
+ };
23
+ export type TwinStream = (sink: TwinStreamSink, peer: string) => TwinStreamConnection;
24
+ export type TwinFetchHandlerResult = {
25
+ status: number;
26
+ body: unknown;
27
+ headers?: Record<string, string>;
28
+ };
29
+ /** The common handler request shape. Extra vendor fields ride `handlerOptions` (static,
30
+ * per instance) and `extras` (derived per request); handlers ignore keys they don't
31
+ * declare, so the union is safe to pass wholesale. */
32
+ export type TwinFetchHandlerRequest = {
33
+ method: string;
34
+ path: string;
35
+ body: string;
36
+ headers: Record<string, string>;
37
+ readOnly: boolean;
38
+ occurredAt: string;
39
+ root?: string;
40
+ } & Record<string, unknown>;
41
+ export interface TwinFetchAdapterConfig {
42
+ root?: string;
43
+ readOnly?: boolean;
44
+ /** The keyless GET /twin body — a value (stateful packs: statefulTwinManifest(...)) or
45
+ * a thunk (generative packs build theirs around the scenario engine). */
46
+ manifest: unknown | (() => unknown);
47
+ /** GET /twin/scenario body (thunk) — scenario-carrying packs only. */
48
+ scenarioStatus?: () => unknown;
49
+ /** THE STORE DOOR (R5c, mirror purity R3): named, deterministic projections over stored
50
+ * state, served at `GET /twin/store/<name>` and listed in the manifest as `stores`. A
51
+ * mirror reads twin state ONLY through this door — never by importing the handler or a
52
+ * `-twin-internal` module — so the vendor API lacking a listing endpoint (resend has no
53
+ * list-emails) no longer breeds a bespoke `/_twin/*` route per mirror. Keyed in the skin
54
+ * like every state read (only bare `GET /twin` is keyless). */
55
+ stores?: Record<string, () => unknown | Promise<unknown>>;
56
+ /** Per-request vendor extras (header threading: stripe-version, notion-version, …). */
57
+ extras?: (request: Request, url: URL) => Record<string, unknown>;
58
+ /** Static per-instance handler options (e.g. rateLimitPerSecond). */
59
+ handlerOptions?: Record<string, unknown>;
60
+ /** Extra response headers on every non-door reply (e.g. stripe's request-id, and
61
+ * postmark's `connection: close` Bun-socket workaround — inert under workerd). */
62
+ responseHeaders?: Record<string, string>;
63
+ }
64
+ export declare function createTwinFetchFromHandler(handler: (request: TwinFetchHandlerRequest) => TwinFetchHandlerResult | Promise<TwinFetchHandlerResult>, config: TwinFetchAdapterConfig): (request: Request) => Promise<Response>;