@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.
- package/LICENSE +202 -0
- package/README.md +29 -0
- package/app-route.cjs +154 -0
- package/app-route.d.cts +7 -0
- package/attach.cjs +80 -0
- package/dist/app-route.cjs +154 -0
- package/dist/app-route.d.cts +7 -0
- package/dist/attach.cjs +80 -0
- package/dist/generated/pack-facts.json +4306 -0
- package/dist/inject.cjs +1097 -0
- package/dist/network-policy.cjs +92 -0
- package/dist/network-policy.d.cts +10 -0
- package/dist/src/actions.d.ts +276 -0
- package/dist/src/actions.js +436 -0
- package/dist/src/ancestry.d.ts +22 -0
- package/dist/src/ancestry.js +238 -0
- package/dist/src/args.d.ts +3 -0
- package/dist/src/args.js +12 -0
- package/dist/src/blob-store.d.ts +55 -0
- package/dist/src/blob-store.js +186 -0
- package/dist/src/brand-tokens.d.ts +2 -0
- package/dist/src/brand-tokens.js +17 -0
- package/dist/src/changeset.d.ts +431 -0
- package/dist/src/changeset.js +0 -0
- package/dist/src/client-bundle.d.ts +1 -0
- package/dist/src/client-bundle.js +28 -0
- package/dist/src/credential.d.ts +38 -0
- package/dist/src/credential.js +114 -0
- package/dist/src/derived-core.d.ts +452 -0
- package/dist/src/derived-core.js +782 -0
- package/dist/src/derived.d.ts +84 -0
- package/dist/src/derived.js +122 -0
- package/dist/src/emit.d.ts +106 -0
- package/dist/src/emit.js +157 -0
- package/dist/src/executor.d.ts +120 -0
- package/dist/src/executor.js +387 -0
- package/dist/src/file-response.d.ts +3 -0
- package/dist/src/file-response.js +22 -0
- package/dist/src/fork.d.ts +26 -0
- package/dist/src/fork.js +68 -0
- package/dist/src/git/history.d.ts +36 -0
- package/dist/src/git/history.js +298 -0
- package/dist/src/git/index.d.ts +6 -0
- package/dist/src/git/index.js +6 -0
- package/dist/src/git/inflate.d.ts +11 -0
- package/dist/src/git/inflate.js +194 -0
- package/dist/src/git/objects.d.ts +64 -0
- package/dist/src/git/objects.js +161 -0
- package/dist/src/git/pack.d.ts +14 -0
- package/dist/src/git/pack.js +199 -0
- package/dist/src/git/refs.d.ts +19 -0
- package/dist/src/git/refs.js +35 -0
- package/dist/src/git/smart-http.d.ts +45 -0
- package/dist/src/git/smart-http.js +223 -0
- package/dist/src/hash.d.ts +38 -0
- package/dist/src/hash.js +48 -0
- package/dist/src/head.d.ts +140 -0
- package/dist/src/head.js +313 -0
- package/dist/src/history.d.ts +76 -0
- package/dist/src/history.js +322 -0
- package/dist/src/index.d.ts +73 -0
- package/dist/src/index.js +98 -0
- package/dist/src/lifecycle.d.ts +1 -0
- package/dist/src/lifecycle.js +8 -0
- package/dist/src/log.d.ts +254 -0
- package/dist/src/log.js +801 -0
- package/dist/src/mirror-shell.d.ts +2 -0
- package/dist/src/mirror-shell.js +13 -0
- package/dist/src/observe.d.ts +49 -0
- package/dist/src/observe.js +148 -0
- package/dist/src/pack-assets.d.ts +30 -0
- package/dist/src/pack-assets.js +88 -0
- package/dist/src/packRegistry.d.ts +374 -0
- package/dist/src/packRegistry.js +142 -0
- package/dist/src/placeholder-remote.d.ts +22 -0
- package/dist/src/placeholder-remote.js +86 -0
- package/dist/src/proxy.d.ts +25 -0
- package/dist/src/proxy.js +155 -0
- package/dist/src/rateBudget.d.ts +367 -0
- package/dist/src/rateBudget.js +925 -0
- package/dist/src/references.d.ts +18 -0
- package/dist/src/references.js +27 -0
- package/dist/src/remote-execute.d.ts +22 -0
- package/dist/src/remote-execute.js +1 -0
- package/dist/src/resource-blob.d.ts +10 -0
- package/dist/src/resource-blob.js +56 -0
- package/dist/src/scenario.d.ts +197 -0
- package/dist/src/scenario.js +425 -0
- package/dist/src/schemas.d.ts +78 -0
- package/dist/src/schemas.js +50 -0
- package/dist/src/serve-http.d.ts +48 -0
- package/dist/src/serve-http.js +340 -0
- package/dist/src/serve.d.ts +147 -0
- package/dist/src/serve.js +507 -0
- package/dist/src/shared-blob-index.d.ts +4 -0
- package/dist/src/shared-blob-index.js +126 -0
- package/dist/src/state-system.d.ts +70 -0
- package/dist/src/state-system.js +90 -0
- package/dist/src/storage.d.ts +101 -0
- package/dist/src/storage.js +337 -0
- package/dist/src/twin-fetch.d.ts +64 -0
- package/dist/src/twin-fetch.js +91 -0
- package/dist/src/types.d.ts +40 -0
- package/dist/src/types.js +1 -0
- package/dist/src/v1-removed.d.ts +159 -0
- package/dist/src/v1-removed.js +124 -0
- package/dist/src/volter-home.d.ts +5 -0
- package/dist/src/volter-home.js +10 -0
- package/dist/src/world-clock.d.ts +4 -0
- package/dist/src/world-clock.js +32 -0
- package/dist/src/world-env.d.ts +3 -0
- package/dist/src/world-env.js +22 -0
- package/dist/src/world-store-sql.d.ts +27 -0
- package/dist/src/world-store-sql.js +86 -0
- package/dist/src/world-store.d.ts +168 -0
- package/dist/src/world-store.js +475 -0
- package/dist/src/worldConfig.d.ts +9 -0
- package/dist/src/worldConfig.js +17 -0
- package/dist/stream-bridge.cjs +80 -0
- package/dist/vendor-hosts.cjs +200 -0
- package/generated/pack-facts.json +4306 -0
- package/inject.cjs +1097 -0
- package/network-policy.cjs +92 -0
- package/network-policy.d.cts +10 -0
- package/package.json +103 -0
- package/src/actions.ts +564 -0
- package/src/ancestry.ts +213 -0
- package/src/args.ts +14 -0
- package/src/blob-store.ts +185 -0
- package/src/brand-tokens.ts +17 -0
- package/src/changeset.ts +1032 -0
- package/src/client-bundle.ts +29 -0
- package/src/credential.ts +140 -0
- package/src/derived-core.ts +1004 -0
- package/src/derived.ts +176 -0
- package/src/emit.ts +242 -0
- package/src/executor.ts +431 -0
- package/src/file-response.ts +22 -0
- package/src/fork.ts +89 -0
- package/src/git/history.ts +177 -0
- package/src/git/index.ts +6 -0
- package/src/git/inflate.ts +125 -0
- package/src/git/objects.ts +110 -0
- package/src/git/pack.ts +105 -0
- package/src/git/refs.ts +25 -0
- package/src/git/smart-http.ts +149 -0
- package/src/hash.ts +66 -0
- package/src/head.ts +318 -0
- package/src/history.ts +246 -0
- package/src/index.ts +323 -0
- package/src/lifecycle.ts +8 -0
- package/src/log.ts +793 -0
- package/src/mirror-shell.ts +15 -0
- package/src/observe.ts +130 -0
- package/src/pack-assets.ts +81 -0
- package/src/packRegistry.ts +408 -0
- package/src/placeholder-remote.ts +81 -0
- package/src/proxy.ts +183 -0
- package/src/rateBudget.ts +1115 -0
- package/src/references.ts +46 -0
- package/src/remote-execute.ts +26 -0
- package/src/resource-blob.ts +57 -0
- package/src/scenario.ts +479 -0
- package/src/schemas.ts +56 -0
- package/src/serve-http.ts +299 -0
- package/src/serve.ts +618 -0
- package/src/shared-blob-index.ts +108 -0
- package/src/state-system.ts +115 -0
- package/src/storage.ts +407 -0
- package/src/twin-fetch.ts +147 -0
- package/src/types.ts +50 -0
- package/src/v1-removed.ts +172 -0
- package/src/volter-home.ts +11 -0
- package/src/world-clock.ts +33 -0
- package/src/world-env.ts +18 -0
- package/src/world-store-sql.ts +118 -0
- package/src/world-store.ts +572 -0
- package/src/worldConfig.ts +27 -0
- package/stream-bridge.cjs +80 -0
- package/vendor-hosts.cjs +200 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// A pack's mirror shell as a World serves it, at /<org>/<world>/<vendor>/mirror/ (the served World's
|
|
2
|
+
// mount and the hosted supervisor's): its <base> becomes the twin's place under the World, so the
|
|
3
|
+
// mirror's reads go to the World's wire; its assets stay under mirror/; and its own `#/` routes stay on
|
|
4
|
+
// the shell. A mirror links its routes as href="#/…", which the browser resolves against <base> — the
|
|
5
|
+
// twin's API path, not the shell — so a capture-phase handler moves the hash on the page instead.
|
|
6
|
+
|
|
7
|
+
const HASH_LINKS = `<script>document.addEventListener('click',function(e){var a=e.target&&e.target.closest&&e.target.closest('a[href^="#"]');if(!a||e.defaultPrevented||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)return;e.preventDefault();location.hash=a.getAttribute('href');},true);</script>`;
|
|
8
|
+
|
|
9
|
+
/** The shell's HTML mounted at `base` (the twin's place, ending in '/'). */
|
|
10
|
+
export function mirrorShellUnder(html: string, base: string): string {
|
|
11
|
+
const mounted = html
|
|
12
|
+
.replace(/<base href="[^"]*">/, `<base href="${base}">`)
|
|
13
|
+
.replace(/(href|src)="assets\//g, '$1="mirror/assets/');
|
|
14
|
+
return mounted.includes('</head>') ? mounted.replace('</head>', `${HASH_LINKS}</head>`) : `${HASH_LINKS}${mounted}`;
|
|
15
|
+
}
|
package/src/observe.ts
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// OBSERVE — the kernel's fold for the second difference between simulated and real: how the tree
|
|
2
|
+
// is kept current (company contract "Refresh is the kernel's fold", ruled 2026-09-06).
|
|
3
|
+
//
|
|
4
|
+
// A pack's refresh and ingest code hand the kernel RESOURCES, never rows. The kernel diffs each
|
|
5
|
+
// against the upstream view and appends to the ROOT's log (the parent log — the vendor's history as
|
|
6
|
+
// observed) only what changed: one entry per changed subject, carrying the changed fields; an
|
|
7
|
+
// unchanged resource appends nothing; a listing the adapter declares complete tombstones what it
|
|
8
|
+
// no longer holds. A refresh runs inside a scope that collects the adapter's observations and folds
|
|
9
|
+
// them as one batch; an ingest observes directly, one resource at a time.
|
|
10
|
+
import { withAncestryLock } from './ancestry.ts';
|
|
11
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
12
|
+
import { appendParentEntry, readParentTreeMap, type Entry, type Tree } from './log.ts';
|
|
13
|
+
import { hashFieldValue, type SubjectFields } from './hash.ts';
|
|
14
|
+
import { worldNow } from './world-clock.ts';
|
|
15
|
+
|
|
16
|
+
export type ObservedResource = { type: string; id: string; fields: SubjectFields; deleted?: boolean };
|
|
17
|
+
export type ObserveReport = { observed: number; appended: number; unchanged: number; removed: number };
|
|
18
|
+
|
|
19
|
+
export type Observation = { service: string; root?: string } & (
|
|
20
|
+
| { resource: ObservedResource }
|
|
21
|
+
| { resources: ObservedResource[]; complete?: string[] }
|
|
22
|
+
);
|
|
23
|
+
// ONE sink per process, whichever copy of the kernel a pack resolved: a pack installed beside a
|
|
24
|
+
// world carries its own `@volter/world-core`, and its observations must reach the scope the runtime
|
|
25
|
+
// opened. A module-level instance would be one per copy; a process-global symbol is one per process.
|
|
26
|
+
const SINK = Symbol.for('volter.observe.sink');
|
|
27
|
+
// created on first use, never at import: a mirror's CLIENT bundle (target browser) carries this module
|
|
28
|
+
// through the pack's shared helpers, and a browser has no AsyncLocalStorage — constructing one at load
|
|
29
|
+
// would kill the whole client ("not a constructor") for a scope the browser never opens
|
|
30
|
+
const sink = (): AsyncLocalStorage<Observation[]> => ((globalThis as Record<symbol, unknown>)[SINK] ??= new AsyncLocalStorage<Observation[]>()) as AsyncLocalStorage<Observation[]>;
|
|
31
|
+
|
|
32
|
+
/** Run `fn` with every `observeResource` inside it COLLECTED instead of folded; the caller folds the
|
|
33
|
+
* batch (`observeResources`) once `fn` returns. The kernel's refresh does this around an adapter. */
|
|
34
|
+
export async function collectObservations<T>(fn: () => Promise<T>): Promise<{ value: T; observations: Observation[] }> {
|
|
35
|
+
const observations: Observation[] = [];
|
|
36
|
+
const value = await sink().run(observations, fn);
|
|
37
|
+
return { value, observations };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Fold a collected batch: one `observeResources` per service, `complete` applied to each. */
|
|
41
|
+
export function foldObservations(observations: Observation[], opts: { root?: string; at?: string; complete?: string[]; batch?: string } = {}): ObserveReport {
|
|
42
|
+
const total: ObserveReport = { observed: 0, appended: 0, unchanged: 0, removed: 0 };
|
|
43
|
+
const groups = new Map<string, { service: string; root?: string; resources: ObservedResource[]; complete: Set<string> }>();
|
|
44
|
+
for (const o of observations) {
|
|
45
|
+
const root = opts.root ?? o.root;
|
|
46
|
+
const key = JSON.stringify([o.service, root]);
|
|
47
|
+
let group = groups.get(key);
|
|
48
|
+
if (!group) { group = { service: o.service, root, resources: [], complete: new Set(opts.complete) }; groups.set(key, group); }
|
|
49
|
+
if ('resource' in o) group.resources.push(o.resource);
|
|
50
|
+
else for (const resource of o.resources) group.resources.push(resource);
|
|
51
|
+
if ('resources' in o) for (const type of o.complete ?? []) group.complete.add(type);
|
|
52
|
+
}
|
|
53
|
+
for (const { service, root, resources, complete } of groups.values()) {
|
|
54
|
+
const r = foldResources(service, resources, { ...opts, root, complete: [...complete] });
|
|
55
|
+
total.observed += r.observed; total.appended += r.appended; total.unchanged += r.unchanged; total.removed += r.removed;
|
|
56
|
+
}
|
|
57
|
+
return total;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** One observed resource: collected when a refresh scope is open, else folded onto the root's log now. */
|
|
61
|
+
export function observeResource(service: string, resource: ObservedResource, opts: { root?: string; at?: string } = {}): ObserveReport {
|
|
62
|
+
const collecting = typeof AsyncLocalStorage === 'function' ? sink().getStore() : undefined;
|
|
63
|
+
if (collecting) { collecting.push({ service, resource, ...(opts.root !== undefined ? { root: opts.root } : {}) }); return { observed: 1, appended: 0, unchanged: 0, removed: 0 }; }
|
|
64
|
+
return observeResources(service, [resource], opts);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function stable(value: unknown): string {
|
|
68
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'undefined';
|
|
69
|
+
if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
|
|
70
|
+
const o = value as Record<string, unknown>;
|
|
71
|
+
return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${stable(o[k])}`).join(',')}}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The fold: diff each resource against upstream state, append what changed to the root's log. `complete`
|
|
75
|
+
* names the types this batch listed in full — a subject of such a type the batch does not hold is
|
|
76
|
+
* tombstoned. Entry ids are content-addressed, so the same observation folds once. */
|
|
77
|
+
export function observeResources(service: string, resources: ObservedResource[], opts: { root?: string; at?: string; complete?: string[]; batch?: string } = {}): ObserveReport {
|
|
78
|
+
const collecting = typeof AsyncLocalStorage === 'function' ? sink().getStore() : undefined;
|
|
79
|
+
if (collecting) {
|
|
80
|
+
collecting.push({ service, resources, ...(opts.root !== undefined ? { root: opts.root } : {}), ...(opts.complete ? { complete: opts.complete } : {}) });
|
|
81
|
+
return { observed: resources.length, appended: 0, unchanged: 0, removed: 0 };
|
|
82
|
+
}
|
|
83
|
+
return foldResources(service, resources, opts);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function foldResources(service: string, resources: ObservedResource[], opts: { root?: string; at?: string; complete?: string[]; batch?: string }): ObserveReport {
|
|
87
|
+
return withAncestryLock(() => {
|
|
88
|
+
const at = opts.at ?? worldNow();
|
|
89
|
+
// one look is one batch: every entry it appends carries the same id, and no position may split it
|
|
90
|
+
const batch = opts.batch ?? `obs:${service}:${at}`;
|
|
91
|
+
const tree: Tree = readParentTreeMap(service, opts.root);
|
|
92
|
+
const report: ObserveReport = { observed: resources.length, appended: 0, unchanged: 0, removed: 0 };
|
|
93
|
+
const seen = new Set<string>();
|
|
94
|
+
const append = (subject: { type: string; id: string }, fields: SubjectFields): boolean => {
|
|
95
|
+
const entry: Entry = {
|
|
96
|
+
id: `observed:${service}:${subject.type}:${subject.id}:${hashFieldValue({ subject, fields, at })}`,
|
|
97
|
+
service, op: 'set', operation: 'observed', subject, occurredAt: at, actor: { kind: 'system', id: 'vendor' }, fields, batch,
|
|
98
|
+
};
|
|
99
|
+
const { appended } = appendParentEntry(entry, opts.root);
|
|
100
|
+
const key = `${subject.type}:${subject.id}`;
|
|
101
|
+
const existing = tree.get(key) ?? { type: subject.type, id: subject.id, updatedAt: at, fields: {} };
|
|
102
|
+
const merged: SubjectFields = { ...existing.fields, ...fields };
|
|
103
|
+
if (existing.fields.deleted === true && fields.deleted !== true) delete merged.deleted;
|
|
104
|
+
tree.set(key, { ...existing, updatedAt: at, fields: merged });
|
|
105
|
+
return appended;
|
|
106
|
+
};
|
|
107
|
+
for (const r of resources) {
|
|
108
|
+
const key = `${r.type}:${r.id}`;
|
|
109
|
+
seen.add(key);
|
|
110
|
+
const existing = tree.get(key);
|
|
111
|
+
if (r.deleted) {
|
|
112
|
+
if (existing && existing.fields.deleted !== true) { if (append({ type: r.type, id: r.id }, { deleted: true })) report.removed += 1; }
|
|
113
|
+
else report.unchanged += 1;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const changed: SubjectFields = {};
|
|
117
|
+
for (const [k, v] of Object.entries(r.fields)) if (v !== undefined && (existing === undefined || stable(existing.fields[k]) !== stable(v))) changed[k] = v;
|
|
118
|
+
if (Object.keys(changed).length === 0 && existing !== undefined && existing.fields.deleted !== true) { report.unchanged += 1; continue; }
|
|
119
|
+
if (existing?.fields.deleted === true && Object.keys(changed).length === 0) Object.assign(changed, r.fields); // resurrected as it stands
|
|
120
|
+
if (append({ type: r.type, id: r.id }, changed)) report.appended += 1; else report.unchanged += 1;
|
|
121
|
+
}
|
|
122
|
+
for (const type of opts.complete ?? []) {
|
|
123
|
+
for (const s of tree.values()) {
|
|
124
|
+
if (s.type !== type || seen.has(`${s.type}:${s.id}`) || s.fields.deleted === true) continue;
|
|
125
|
+
if (append({ type: s.type, id: s.id }, { deleted: true })) report.removed += 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return report;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// THE PACK-ASSET SEAM (runtime contract R20). A pack may ship bytes it serves byte-for-byte —
|
|
2
|
+
// clerk's vendored clerk-js dist (136 files, 14.5 MB), a mirror shell — that are neither
|
|
3
|
+
// committed text (size) nor world-state blobs (R11's seam is keyed under a world's state root
|
|
4
|
+
// and filled by requests). They are PACK assets: read-only, versioned with the package, the
|
|
5
|
+
// same on every world. This seam is how a pack reaches them without touching a host: the
|
|
6
|
+
// pack declares them on its descriptor (`assets`), reads them through `getActivePackAssets()`,
|
|
7
|
+
// and the SHELL binds the store — the Bun shell reads the pack's own tree, workerd the
|
|
8
|
+
// static-assets binding the mirror shells already deploy through. A pack that reads its tree
|
|
9
|
+
// with Bun.file or import.meta is host-bound (R12b); one that reads through here is not.
|
|
10
|
+
// No node builtins at module scope: `@volter/world-core`'s index re-exports this module and every mirror
|
|
11
|
+
// client bundle (browser target) pulls the index in — a static `node:fs`/`node:url` import here
|
|
12
|
+
// broke every mirror bundle the day the seam landed. The local store reads through Bun's file
|
|
13
|
+
// API at call time; the worker store never touches a filesystem.
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
16
|
+
export interface PackAssetStore {
|
|
17
|
+
/** The asset at `relPath` inside the pack `vendor`'s directory (e.g. `client/clerk.browser.js`),
|
|
18
|
+
* as a Response carrying the bytes and a content type — or null when the pack ships no such
|
|
19
|
+
* file. `relPath` is confined to the pack's own tree: a traversal never resolves. */
|
|
20
|
+
fetch(vendor: string, relPath: string): Promise<Response | null>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CONTENT_TYPES: Record<string, string> = {
|
|
24
|
+
'.js': 'application/javascript; charset=utf-8', '.mjs': 'application/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8',
|
|
25
|
+
'.html': 'text/html; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8',
|
|
26
|
+
'.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.woff': 'font/woff', '.txt': 'text/plain; charset=utf-8',
|
|
27
|
+
};
|
|
28
|
+
export function assetContentType(relPath: string): string {
|
|
29
|
+
const dot = relPath.lastIndexOf('.');
|
|
30
|
+
return (dot >= 0 ? CONTENT_TYPES[relPath.slice(dot)] : undefined) ?? 'application/octet-stream';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A relative path confined to a pack's tree: no absolute, no `..`, no empty segment. */
|
|
34
|
+
export function confinedAssetPath(relPath: string): string | null {
|
|
35
|
+
if (relPath.startsWith('/') || relPath.includes('\\') || relPath.includes('\0')) return null;
|
|
36
|
+
const segs = relPath.split('/');
|
|
37
|
+
if (segs.some((s) => s === '' || s === '.' || s === '..')) return null;
|
|
38
|
+
return segs.join('/');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The Bun shell's store: the pack's own directory under the catalog (`packages/twin/<vendor>`).
|
|
42
|
+
* `twinDir` ends with a slash. */
|
|
43
|
+
export class LocalPackAssets implements PackAssetStore {
|
|
44
|
+
constructor(private readonly twinDir: string) {}
|
|
45
|
+
async fetch(vendor: string, relPath: string): Promise<Response | null> {
|
|
46
|
+
const safe = confinedAssetPath(relPath);
|
|
47
|
+
if (safe === null || !/^[a-z][a-z0-9-]*$/.test(vendor)) return null;
|
|
48
|
+
const file = `${this.twinDir}${vendor}/${safe}`; // confined: no `..`, no absolute, no empty segment (above)
|
|
49
|
+
if (!existsSync(file) || !statSync(file).isFile()) return null;
|
|
50
|
+
return new Response(readFileSync(file), { status: 200, headers: { 'content-type': assetContentType(safe) } });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The workerd shell's store: the static-assets binding, where the deploy step places each
|
|
55
|
+
* pack's declared assets under `/{vendor}/pack/<relPath>` beside the mirror shells. */
|
|
56
|
+
export class BindingPackAssets implements PackAssetStore {
|
|
57
|
+
constructor(private readonly assets: { fetch(request: Request): Promise<Response> }) {}
|
|
58
|
+
async fetch(vendor: string, relPath: string): Promise<Response | null> {
|
|
59
|
+
const safe = confinedAssetPath(relPath);
|
|
60
|
+
if (safe === null) return null;
|
|
61
|
+
const res = await this.assets.fetch(new Request(`https://pack-assets/${vendor}/pack/${safe}`));
|
|
62
|
+
if (res.status !== 200) return null;
|
|
63
|
+
return new Response(res.body, { status: 200, headers: { 'content-type': assetContentType(safe) } });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let active: PackAssetStore | undefined;
|
|
68
|
+
/** The bound store — the Bun shell's local tree unless a shell bound another. */
|
|
69
|
+
export function getActivePackAssets(): PackAssetStore {
|
|
70
|
+
if (active === undefined) active = new LocalPackAssets(new URL('../../twin/', import.meta.url).pathname);
|
|
71
|
+
return active;
|
|
72
|
+
}
|
|
73
|
+
export function setActivePackAssets(store: PackAssetStore): PackAssetStore | undefined {
|
|
74
|
+
const previous = active; active = store; return previous;
|
|
75
|
+
}
|
|
76
|
+
/** `getActivePackAssets().fetch(...)` with the pack's declared fallback: the first path that exists. */
|
|
77
|
+
export async function packAsset(vendor: string, ...candidates: string[]): Promise<Response | null> {
|
|
78
|
+
const store = getActivePackAssets();
|
|
79
|
+
for (const c of candidates) { const r = await store.fetch(vendor, c); if (r !== null) return r; }
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
// Pack registry (the twins architecture notes → "Optional shared
|
|
2
|
+
// libraries"; #1). A lightweight, dependency-free way for vendor twins to *declare*
|
|
3
|
+
// themselves so tooling can discover them — "add a vendor" gets cheaper. A pack
|
|
4
|
+
// EXPORTS a `TwinPack` descriptor (no import side-effects); a consumer that imports
|
|
5
|
+
// the packs it wants registers them and queries the registry. The kernel never
|
|
6
|
+
// imports packs (no dependency inversion) — discovery is the consumer's choice.
|
|
7
|
+
//
|
|
8
|
+
// This is an optional convenience, NOT a kernel that defines the twin: a pack can
|
|
9
|
+
// ignore the registry entirely. Pure + deterministic.
|
|
10
|
+
import { declareRateBudget, type RateBudgetDeclaration } from './rateBudget.ts';
|
|
11
|
+
import type { TwinAuthStrategy } from './executor.ts';
|
|
12
|
+
import { registerReferences, type ReferenceDeclaration } from './references.ts';
|
|
13
|
+
import type { TwinEmitter } from './emit.ts';
|
|
14
|
+
import { registerAuthStrategy, registerStateSystem, type StateSystemAdapters } from './state-system.ts';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* How a pack's clients ADDRESS it. The first three are HTTP API styles; `raw-tcp` is the
|
|
18
|
+
* RAW-PROTOCOL class — a pack whose clients speak a line protocol directly over a TCP socket
|
|
19
|
+
* rather than HTTP, so none of the HTTP machinery applies: no `browserRouting`, and no entry in
|
|
20
|
+
* the injector's `VENDOR_HOSTS` host map is possible (the injector patches http/fetch and never
|
|
21
|
+
* sees the traffic). Such a pack is wired into a world through app-read host/port env instead,
|
|
22
|
+
* and this value is what tells a reader that "no injector entry" is STRUCTURAL rather than a
|
|
23
|
+
* missing wiring point. `packages/twin/smtp` is the first.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately the transport CLASS, not the protocol name: naming the wire protocol would put a
|
|
26
|
+
* vendor id in the kernel the moment a pack is named after its protocol, which is exactly what
|
|
27
|
+
* `scripts/architecture.test.ts`'s "kernel does not branch on vendor identity" forbids. The
|
|
28
|
+
* specific protocol belongs on the pack's own `specSource`/`description`.
|
|
29
|
+
*/
|
|
30
|
+
export type PackTransport = 'rest' | 'graphql' | 'web-api' | 'raw-tcp';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The pack's SERVE-FAMILY — the axis the invariant matrix keys strictness off, DECLARED
|
|
34
|
+
* because it is the pack's own claim about what kind of thing it is (a heuristic over file
|
|
35
|
+
* shapes would be the loose-scan disease). Orthogonal facts stay derived: transport is its
|
|
36
|
+
* own field, a mirror is mirrorMutations in gate.ts, webhooks are the events module.
|
|
37
|
+
* 'crud' — stateful resource CRUD behind the vendor's API (the default family;
|
|
38
|
+
* includes ingestion→grouping packs — the ingest door is a trait).
|
|
39
|
+
* 'generative' — model-shaped surface serving deterministic stubs/scenarios; "cannot
|
|
40
|
+
* run the model" is the invariant, never a gap.
|
|
41
|
+
* 'signed-protocol' — the wire is a signature/format protocol (SigV4 + XML/AWS-JSON …):
|
|
42
|
+
* request authentication IS the fidelity surface.
|
|
43
|
+
* 'engine-control' — a control plane driving a REAL execution engine behind an injected
|
|
44
|
+
* seam (fly's runtime, github's git plane, supabase's external stack).
|
|
45
|
+
* 'proxy' — the vendor itself is a forwarding plane (tunnel).
|
|
46
|
+
*/
|
|
47
|
+
/** THE PLATFORM PROTOCOL VERSION (runtime contract R16): `major.minor`. The major names the
|
|
48
|
+
* descriptor shape, the factory signature, the doors and the adapter contract a package is
|
|
49
|
+
* written against; a change that removes or alters a required shape bumps it, an additive
|
|
50
|
+
* change bumps minor. A package declares the major it targets (`protocol`); the kernel serves
|
|
51
|
+
* its own major, the previous one under a deprecation warning for one cycle, and refuses two
|
|
52
|
+
* back. Undeclared reads as the previous major (deprecated) once there is one. */
|
|
53
|
+
export const PROTOCOL_VERSION = '2.0';
|
|
54
|
+
export const PROTOCOL_MAJOR = Number(PROTOCOL_VERSION.split('.')[0]);
|
|
55
|
+
/** Where a package's declared protocol stands against this kernel. */
|
|
56
|
+
export function protocolStanding(declared: string | undefined): { major: number; standing: 'current' | 'deprecated' | 'refused' | 'assumed' } {
|
|
57
|
+
// undeclared: written before a pack could say — the previous major once there is one, and
|
|
58
|
+
// deprecated like any pack on it; the current major only while it is the first
|
|
59
|
+
if (declared === undefined) return PROTOCOL_MAJOR > 1 ? { major: PROTOCOL_MAJOR - 1, standing: 'deprecated' } : { major: PROTOCOL_MAJOR, standing: 'assumed' };
|
|
60
|
+
const major = Number(String(declared).split('.')[0]);
|
|
61
|
+
if (!Number.isInteger(major) || major < 1) return { major: Number.NaN, standing: 'refused' };
|
|
62
|
+
if (major === PROTOCOL_MAJOR) return { major, standing: 'current' };
|
|
63
|
+
if (major === PROTOCOL_MAJOR - 1) return { major, standing: 'deprecated' };
|
|
64
|
+
return { major, standing: 'refused' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const PACK_ARCHETYPES = ['crud', 'generative', 'signed-protocol', 'engine-control', 'proxy'] as const;
|
|
68
|
+
export type PackArchetype = (typeof PACK_ARCHETYPES)[number];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* How often this vendor's REAL API may be pulled — a property of the VENDOR's rate limits, so it
|
|
72
|
+
* belongs to the pack that knows them, not to every world that might sync one (same argument as
|
|
73
|
+
* `browserRouting`: vendor knowledge in the descriptor, kernel stays vendor-agnostic).
|
|
74
|
+
*
|
|
75
|
+
* - `'continuous'` — limits are generous and well-documented, so a scheduled/shadow world may sync
|
|
76
|
+
* this vendor on a cadence (slack, github: thousands/hour, per-method tiers, clear headers).
|
|
77
|
+
* - `'on-demand'` — limits are tight, opaque, cost-based, or punish bursts with long lockouts, so a
|
|
78
|
+
* real pull happens ONLY when a human/agent explicitly asks for one. Never on a schedule, never
|
|
79
|
+
* as a side effect of booting a world. Figma is the cautionary case: undocumented cost-based
|
|
80
|
+
* limits, and a burst of renders cost this project a ~4.5-DAY token lockout (2026-07-25).
|
|
81
|
+
*
|
|
82
|
+
* DEFAULT IS `'on-demand'` when omitted — deliberately the safe direction. A pack that hasn't
|
|
83
|
+
* thought about its limits must not be assumed schedulable; being wrong that way costs a slower
|
|
84
|
+
* sync, while being wrong the other way costs a lockout.
|
|
85
|
+
*/
|
|
86
|
+
export type PullPosture = 'continuous' | 'on-demand';
|
|
87
|
+
|
|
88
|
+
/** The posture to assume for a pack that doesn't declare one. Conservative on purpose. */
|
|
89
|
+
export const DEFAULT_PULL_POSTURE: PullPosture = 'on-demand';
|
|
90
|
+
|
|
91
|
+
/** A pack's effective posture — its declaration, or the conservative default. */
|
|
92
|
+
export function pullPosture(pack: Pick<TwinPack, 'pullPosture'>): PullPosture {
|
|
93
|
+
return pack.pullPosture ?? DEFAULT_PULL_POSTURE;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* What may name a vendor at a pull seam: the pack descriptor itself (the pack knows its own
|
|
98
|
+
* posture — no registration needed) or a vendor id resolved through the registry. The id form is
|
|
99
|
+
* the convenient one; it is also the one that FAILS CLOSED, because an id nothing registered
|
|
100
|
+
* resolves to a posture-less descriptor, i.e. the conservative default.
|
|
101
|
+
*/
|
|
102
|
+
export type PullVendor = string | Pick<TwinPack, 'vendor' | 'pullPosture'>;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Why a pull is happening — the distinction the posture rule actually turns on.
|
|
106
|
+
*
|
|
107
|
+
* - `'scheduled'` — a cadence, a poll loop, a shadow tick, or any pull that happens as a side
|
|
108
|
+
* effect of something else (booting a world, a cron). Allowed for `'continuous'` vendors ONLY.
|
|
109
|
+
* - `'explicit'` — a human/agent asked for THIS pull, now. Sanctioned for any vendor; it is the
|
|
110
|
+
* only way an on-demand vendor is ever supposed to be pulled.
|
|
111
|
+
*
|
|
112
|
+
* Omitted ⇒ `'scheduled'`, matching the posture default: an unlabeled pull is treated as the
|
|
113
|
+
* dangerous kind, so labeling is what unlocks the vendor, never silence.
|
|
114
|
+
*/
|
|
115
|
+
export type PullTrigger = 'scheduled' | 'explicit';
|
|
116
|
+
|
|
117
|
+
/** The trigger to assume when a caller does not say. Conservative on purpose. */
|
|
118
|
+
export const DEFAULT_PULL_TRIGGER: PullTrigger = 'scheduled';
|
|
119
|
+
|
|
120
|
+
/** Resolve a `PullVendor` to the descriptor whose posture governs it. Unregistered ⇒ no posture. */
|
|
121
|
+
export function resolvePullVendor(vendor: PullVendor): Pick<TwinPack, 'vendor' | 'pullPosture'> {
|
|
122
|
+
if (typeof vendor !== 'string') return vendor;
|
|
123
|
+
return getPack(vendor) ?? { vendor };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Guard a SCHEDULED/continuous sync. Throws for a vendor that may only be pulled on demand.
|
|
128
|
+
*
|
|
129
|
+
* This is the mechanical form of the rule — worlds differ in what they may pull:
|
|
130
|
+
* • sealed (hermetic OA cycle worlds) pull NOTHING; they seed synthetic state over loopback and
|
|
131
|
+
* an egress guard blocks the internet, so posture is irrelevant there;
|
|
132
|
+
* • shadow/scheduled worlds sync real state on a cadence — `'continuous'` vendors ONLY;
|
|
133
|
+
* • on-demand pulls are explicit, operator-invoked, and may target any vendor.
|
|
134
|
+
* A scheduled world that quietly includes an on-demand vendor is how a lockout happens, so it
|
|
135
|
+
* fails loudly here instead.
|
|
136
|
+
*/
|
|
137
|
+
export function assertContinuousPullAllowed(vendor: PullVendor): void {
|
|
138
|
+
const pack = resolvePullVendor(vendor);
|
|
139
|
+
if (pullPosture(pack) === 'continuous') return;
|
|
140
|
+
const undeclared = pack.pullPosture === undefined;
|
|
141
|
+
throw new Error(
|
|
142
|
+
`pull posture: "${pack.vendor}" is on-demand — it must NOT be pulled on a schedule or as a side ` +
|
|
143
|
+
`effect of booting a world. Its rate limits are tight/opaque enough that a burst risks a long ` +
|
|
144
|
+
`lockout. Pull it explicitly when someone asks, through the pack's guarded connector; a sealed ` +
|
|
145
|
+
`world should seed synthetic state locally instead. If this vendor's limits are genuinely ` +
|
|
146
|
+
`generous, declare pullPosture:'continuous' on its TwinPack and say why.` +
|
|
147
|
+
(undeclared
|
|
148
|
+
? ` (Nothing declared a posture for "${pack.vendor}" — either no pack is registered under ` +
|
|
149
|
+
`that id (registerPack) or its TwinPack omits pullPosture. Silence is not permission: the ` +
|
|
150
|
+
`conservative default applied. If this pull is a one-off someone asked for, say so at the ` +
|
|
151
|
+
`call site — pass the explicit trigger instead of putting it on a cadence.)`
|
|
152
|
+
: ''),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The ONE sanctioned way to pull a vendor on a cadence. Asserts the posture BEFORE `pull` runs, so
|
|
158
|
+
* a refused vendor makes ZERO vendor requests — the guard has to sit in front of the network call,
|
|
159
|
+
* not behind it (guarding the fold, e.g. `syncPull`, would be theatre: the request already went out).
|
|
160
|
+
*
|
|
161
|
+
* Wrap any scheduler/cron/loop's pull in this and the rule enforces itself:
|
|
162
|
+
* `await pullOnSchedule(pack, () => syncSlackFromReal(client, opts))`
|
|
163
|
+
* An explicit, human-asked pull does NOT go through here — it calls the connector directly, which is
|
|
164
|
+
* exactly the sanctioned path for an on-demand vendor.
|
|
165
|
+
*/
|
|
166
|
+
export async function pullOnSchedule<T>(vendor: PullVendor, pull: () => Promise<T>): Promise<T> {
|
|
167
|
+
assertContinuousPullAllowed(vendor);
|
|
168
|
+
return pull();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* One injector host rule — DATA the committed inject.cjs compiles, never a function. Exactly one
|
|
173
|
+
* selector: `host` (exact), `suffix` (endsWith), or `hostPattern` (a RegExp SOURCE over the
|
|
174
|
+
* hostname — regional families like `^s3[.-][a-z0-9-]+\.amazonaws\.com$`). `pathPattern` (a
|
|
175
|
+
* RegExp source over the pathname) splits a host shared between packs. `key` names the routing
|
|
176
|
+
* identity the rule belongs to — the `<KEY>_TWIN_URL` env stem — when it is not the pack's own
|
|
177
|
+
* vendor id (aws's consolidated twin answers under s3 / dynamodb / … ; gemini serves the
|
|
178
|
+
* `googleauth` token exchange); a key belongs to ONE pack. `exclude: true` carves a host out of
|
|
179
|
+
* the key's includes (`.upstash.io` minus `*-vector.upstash.io`): a key matches when any include
|
|
180
|
+
* matches and no exclude does. Validated by scripts/pack-facts.ts: keys are `[a-z0-9-]+`, a
|
|
181
|
+
* foreign key is never another pack's vendor id, belongs to one pack, and must appear in the
|
|
182
|
+
* pack's `adoption.worldIds` (the world wires `<KEY>_TWIN_URL` from there); a pack must claim its
|
|
183
|
+
* own vendor id too; no key is exclude-only; `exclude` is `true` or absent.
|
|
184
|
+
*/
|
|
185
|
+
export type HostRule = ({ host: string } | { suffix: string } | { hostPattern: string }) & {
|
|
186
|
+
pathPattern?: string;
|
|
187
|
+
key?: string;
|
|
188
|
+
exclude?: true;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export type RoundTripWrite = { method: string; path: string; body?: unknown; headers?: Record<string, string> };
|
|
192
|
+
|
|
193
|
+
export type TwinPack = {
|
|
194
|
+
/** vendor id / service, e.g. 'stripe'. */
|
|
195
|
+
vendor: string;
|
|
196
|
+
transport: PackTransport;
|
|
197
|
+
/** Optional native frontend of the same state owner, selected explicitly on the pack CLI. */
|
|
198
|
+
nativeTransport?: { protocol: string; flag: string; upstreamEnv: string };
|
|
199
|
+
/** The serve-family (see PackArchetype). Every pack declares one. */
|
|
200
|
+
archetype?: PackArchetype;
|
|
201
|
+
/** The platform protocol MAJOR this package targets (R16), e.g. '1'. Undeclared reads as the
|
|
202
|
+
* previous major (deprecated), or as the current major (recorded as assumed) while it is the first;
|
|
203
|
+
* the scaffolder declares it. */
|
|
204
|
+
protocol?: string;
|
|
205
|
+
/** Pack-shipped assets (R20): files or directories under the pack dir the serve path reads through
|
|
206
|
+
* the pack-asset seam (`getActivePackAssets`), never off the host. Declared so the deploy step
|
|
207
|
+
* can place them behind the workerd assets binding. Paths relative to the pack dir. */
|
|
208
|
+
assets?: string[];
|
|
209
|
+
/** R2, resource level: declared subject types the twin genuinely serves but no replay can create —
|
|
210
|
+
* born only of the vendor's own catalog, scheduler, account or a pull. Each names WHY. The cell
|
|
211
|
+
* stays debt (adopted, never faked), but legibly: the runner separates these from open gaps. */
|
|
212
|
+
resourcesUnreachable?: Record<string, string>;
|
|
213
|
+
/** subject types the twin serves, e.g. ['customer','charge','payment_intent']. */
|
|
214
|
+
resources: string[];
|
|
215
|
+
/** the `world-<vendor>` operator bin, if any. */
|
|
216
|
+
bin?: string;
|
|
217
|
+
/** conformance field map { object: { field: type } } — vendored or derived from the spec. */
|
|
218
|
+
conformanceFields?: Record<string, Record<string, string>>;
|
|
219
|
+
/** where the exact surface came from (a spec path) — provenance for re-derivation. */
|
|
220
|
+
specSource?: string;
|
|
221
|
+
/** one-line human description. */
|
|
222
|
+
description?: string;
|
|
223
|
+
/** How this vendor's BROWSER SDK addresses its API — used by the zero-edit dev proxy so
|
|
224
|
+
* the kernel proxy stays vendor-agnostic (it forwards/rewrites by these values, never by a
|
|
225
|
+
* hardcoded vendor table). `apiPathPrefix`: the same-origin path the browser SDK calls
|
|
226
|
+
* (e.g. Stripe.js → '/v1/'). `loaderHost`: the absolute API host to strip from the loaded
|
|
227
|
+
* SDK so its calls become same-origin (e.g. 'https://api.stripe.com'). Omit for vendors
|
|
228
|
+
* with no browser SDK. */
|
|
229
|
+
browserRouting?: { apiPathPrefix: string; loaderHost?: string };
|
|
230
|
+
/** How often this vendor's REAL API may be pulled. Omitted ⇒ `'on-demand'` (see `PullPosture`).
|
|
231
|
+
* Declare `'continuous'` only for a vendor whose published limits genuinely tolerate a
|
|
232
|
+
* scheduled sync, and say why in `pullPostureReason`. */
|
|
233
|
+
pullPosture?: PullPosture;
|
|
234
|
+
/** Why this posture — the limits that justify it. Required in review for `'continuous'`, since
|
|
235
|
+
* that is the claim that can cost a lockout if it's wrong. */
|
|
236
|
+
pullPostureReason?: string;
|
|
237
|
+
/**
|
|
238
|
+
* The vendor's CLIENT-SIDE RATE BUDGET — the ceiling, window and per-endpoint weights the pack's
|
|
239
|
+
* guarded connector enforces before a live call goes out. Vendor knowledge as DATA, exactly like
|
|
240
|
+
* `browserRouting`: the mechanism is the kernel's (`rateBudget.ts`), the numbers are the pack's.
|
|
241
|
+
*
|
|
242
|
+
* `registerPack` forwards this to `declareRateBudget`, so registering a pack arms its budget.
|
|
243
|
+
* A pack that omits it is NOT unlimited — any budget built for that vendor falls back to
|
|
244
|
+
* `DEFAULT_RATE_BUDGET` (see its docstring). `pullPosture` says "do not SCHEDULE this vendor";
|
|
245
|
+
* this says "and here is the ceiling on an EXPLICIT pull". They are complementary, not
|
|
246
|
+
* substitutes — the posture guards cadence seams, the budget guards the call itself.
|
|
247
|
+
*/
|
|
248
|
+
rateBudget?: RateBudgetDeclaration;
|
|
249
|
+
/**
|
|
250
|
+
* The pack's DELIVER support (`emit` — see emit.ts): how to synthesize this vendor's
|
|
251
|
+
* signed event/webhook deliveries from current twin state. Vendor knowledge on the
|
|
252
|
+
* descriptor, like `browserRouting`; the kernel engine (`emitTwinEvent`) is generic.
|
|
253
|
+
* The operator surface is the pack's own bin (`world-<vendor> emit`); a consumer that
|
|
254
|
+
* registered the pack can also drive it via `volter-twin emit <vendor>`.
|
|
255
|
+
*/
|
|
256
|
+
emitter?: TwinEmitter;
|
|
257
|
+
/**
|
|
258
|
+
* SERVE FACTORY OVERRIDE — normally DERIVED, declared only under ambiguity. The colocated
|
|
259
|
+
* host (world-runtime/src/host.ts) mounts a pack by its `create<Name>TwinServer` factory
|
|
260
|
+
* export (`({port,root,readOnly}) => {port,stop}`); `scripts/pack-facts.ts` reads that
|
|
261
|
+
* export's name off the module, so a pack with exactly ONE such export declares nothing.
|
|
262
|
+
* A pack exporting SEVERAL factories declares here which one `cli.ts serve` would have
|
|
263
|
+
* booted — the judgment the exports alone cannot reveal (linear serves its DERIVED server
|
|
264
|
+
* by default; the hand-written one is behind a flag). Must name a function export of the
|
|
265
|
+
* pack's index module matching /^create\w*TwinServer$/.
|
|
266
|
+
*/
|
|
267
|
+
serveExport?: string;
|
|
268
|
+
/**
|
|
269
|
+
* ADOPTION — how application repos betray that they talk to this vendor, so
|
|
270
|
+
* `volter-world covers`/`init` can attribute the signal to this pack. Vendor knowledge as
|
|
271
|
+
* DATA, same doctrine as `browserRouting`/`rateBudget`: the detector mechanism lives in
|
|
272
|
+
* world-runtime; the names live here. Absorbs the central `SDK_TWINS` / `SDK_SCOPE_VENDORS` /
|
|
273
|
+
* `ENV_STEM_VENDORS` / `VENDOR_WORLD_IDS` maps (the one wiring point NO gate enforced —
|
|
274
|
+
* the class that let `@planetscale/database` escape, twin#255).
|
|
275
|
+
*/
|
|
276
|
+
adoption?: {
|
|
277
|
+
/** every official npm client of the API surface this pack models, e.g. ['stripe']. */
|
|
278
|
+
sdks?: string[];
|
|
279
|
+
/** PyPI distribution names (PEP 503 normalized: lowercase, `-`) the vendor's Python SDKs
|
|
280
|
+
* ship under — the Python half of adoption discovery and coverage (`covers`). */
|
|
281
|
+
pypi?: string[];
|
|
282
|
+
/** npm scope prefixes whose members all belong to this vendor, e.g. ['@upstash/']. */
|
|
283
|
+
scopes?: string[];
|
|
284
|
+
/** credential-env-var stems, e.g. ['STRIPE'] for STRIPE_SECRET_KEY et al. */
|
|
285
|
+
envStems?: string[];
|
|
286
|
+
/** additional world service ids this vendor answers to (the VENDOR_WORLD_IDS case). */
|
|
287
|
+
worldIds?: string[];
|
|
288
|
+
/** Vendor-facing tool packages whose calls belong to supporting workflow rather than the
|
|
289
|
+
* application itself. The use is saved and selected by World discovery policy. */
|
|
290
|
+
tools?: Array<{ package: string; usage: 'build' | 'deployment' }>;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* INTERCEPTION — the vendor hosts whose traffic the injector must route to this twin,
|
|
294
|
+
* as serializable data (the committed `inject.cjs` table is GENERATED from these — it must
|
|
295
|
+
* stay dependency-free preloaded CJS, so it consumes compiled output, never imports packs).
|
|
296
|
+
* Exactly one of `hosts` or `hostsNone` per pack once migration completes: silence is not a
|
|
297
|
+
* ruling. `pathPattern` (a RegExp source string, applied to the URL pathname) splits shared
|
|
298
|
+
* hosts (the youtube/googleauth case). Absorbed `VENDOR_HOSTS`'s per-pack keys + the retired NO_INJECTOR_ENTRY
|
|
299
|
+
* allowlist (hostsNone IS the ruling now).
|
|
300
|
+
*/
|
|
301
|
+
hosts?: HostRule[];
|
|
302
|
+
/** Why this pack deliberately has NO injector entry (explicit-endpoint wiring only). */
|
|
303
|
+
hostsNone?: string;
|
|
304
|
+
/**
|
|
305
|
+
* HOSTS A TWIN CLAIMS WHILE IT RUNS — names a person makes answer to this vendor (a custom domain connected to a
|
|
306
|
+
* bucket), which no descriptor can list. The twin lists the ones it answers now at its own `door` (GET, answering
|
|
307
|
+
* `{ "hosts": [...] }`), and the World routes them to it as DNS would the vendor's; a host a descriptor's `hosts`
|
|
308
|
+
* names is never taken from another pack this way.
|
|
309
|
+
*/
|
|
310
|
+
hostsClaimed?: { door: string; note: string };
|
|
311
|
+
/**
|
|
312
|
+
* WORLD WIRING — the env var the vendor's own SDK documents for overriding its base URL,
|
|
313
|
+
* which `volter-world init` injects pointing at the twin. `endpointEnvNone` declares the
|
|
314
|
+
* deliberate absence WITH its reason (inventing a var the app never reads would make
|
|
315
|
+
* `covers` report coverage while traffic still reaches the real vendor — the exact lie the
|
|
316
|
+
* proof exists to catch). Absorbs init.ts's `APP_READ_ENDPOINT_ENV` map, where these
|
|
317
|
+
* reasons lived as comments. Exactly one of the two once migration completes.
|
|
318
|
+
*/
|
|
319
|
+
endpointEnv?: { name: string; templates?: Record<string, string>; note: string };
|
|
320
|
+
/** Why this pack deliberately injects no endpoint env (see `endpointEnv`). */
|
|
321
|
+
endpointEnvNone?: string;
|
|
322
|
+
/**
|
|
323
|
+
* PRISMA — the driver adapter a Prisma client reaches this database twin through, as DATA.
|
|
324
|
+
* Prisma's client-engine build (the one a tab runs, and the one its edge client uses) refuses
|
|
325
|
+
* to construct without a driver adapter; an application that constructs `new PrismaClient()`
|
|
326
|
+
* against the vendor's URL gets this adapter from ITS OWN dependencies, constructed on the
|
|
327
|
+
* URL in `urlEnv` (the endpoint template's variable), by the injector. The adapter package is
|
|
328
|
+
* never installed by the World: absent from the application, the client is left as it was.
|
|
329
|
+
*/
|
|
330
|
+
prismaAdapter?: { adapter: string; export: string; urlEnv: string };
|
|
331
|
+
/** PROTOCOL 2 — the pack's half of the REAL state system (state-system.ts): perform one entry
|
|
332
|
+
* against the vendor, refresh the parent log from it, ingest one signed webhook. All over the
|
|
333
|
+
* kernel executor; the pack never holds a credential. Registered with the pack. */
|
|
334
|
+
stateSystem?: StateSystemAdapters;
|
|
335
|
+
/** PROTOCOL 2 — how a root is kept current, the pack's way: `every` schedules a pull in a served
|
|
336
|
+
* world (the root's `refresh.every` overrides), `webhook` says the vendor pushes to the ingest
|
|
337
|
+
* door, `onDemand.atMost` is the least time between `volter twin <v> refresh` runs (a throttle for
|
|
338
|
+
* a rate-limited vendor). Absorbs `pullPosture`. */
|
|
339
|
+
refresh?: { every?: string; webhook?: boolean; onDemand?: { atMost: string } };
|
|
340
|
+
/** PROTOCOL 2 — a minimal write on the vendor's wire the kernel can send blind (or a short
|
|
341
|
+
* sequence whose LAST write creates something new every time): the branch round-trip
|
|
342
|
+
* (docs/contributing/architecture.md#evidence) sends it, checkpoints, branches, sends it again on the branch, and
|
|
343
|
+
* proves the tree contract with no pack code. */
|
|
344
|
+
roundTrip?: RoundTripWrite | RoundTripWrite[];
|
|
345
|
+
/** PROTOCOL 2 — which fields of which subject types hold another subject's id (docs/contributing/architecture.md
|
|
346
|
+
* #alias-aware-lookup-at-the-request-boundary): the kernel resolves them through an adopted vendor id, in the tree and at the perform. */
|
|
347
|
+
references?: ReferenceDeclaration[];
|
|
348
|
+
/** PROTOCOL 2 — with `references`: a write on the wire that references the `roundTrip` write's subject;
|
|
349
|
+
* `{{field}}` in its path or body is the referenced subject's field. The round-trip gate sends it
|
|
350
|
+
* after simulating the parent's adoption and checks the reference followed. */
|
|
351
|
+
referenceTrip?: RoundTripWrite;
|
|
352
|
+
/** PROTOCOL 2 — shape parity (roadmap 4c): the origin the parity gate hands the refresh adapter when it
|
|
353
|
+
* refreshes the twin from itself (`http://twin` + whatever path the adapter reads its scope from), and
|
|
354
|
+
* `'held'` once the write handler and the refresh adapter store the same shape (the gate then asserts). */
|
|
355
|
+
parityOrigin?: string;
|
|
356
|
+
shapeParity?: 'held';
|
|
357
|
+
/** PROTOCOL 2 — a refresh that cannot be exercised for effect when the parity gate refreshes the twin
|
|
358
|
+
* from itself, and the refusal it answers with instead: `refusal` is text its thrown error carries,
|
|
359
|
+
* `reason` why (an identity pull the twin's own wire refuses without a consent-minted token; a pulled
|
|
360
|
+
* id inside the twin's reserved local namespace). The gate runs the refresh and passes it only on
|
|
361
|
+
* that refusal; any other error fails. */
|
|
362
|
+
refreshRefusal?: { refusal: string; reason: string };
|
|
363
|
+
/** PROTOCOL 2 — HOW THIS VENDOR AUTHENTICATES, so the kernel executor can apply the sealed
|
|
364
|
+
* credential the way the vendor actually reads it (docs/concepts/the-model.md#the-rules, rule 5). Absent means header
|
|
365
|
+
* replacement, which is what `credential.headers` has always done — declare this ONLY for a
|
|
366
|
+
* vendor that reads its key from the query string or wants a signature computed per request. */
|
|
367
|
+
auth?: TwinAuthStrategy;
|
|
368
|
+
/** PROTOCOL 2 — the ENGINE beside the tree, when the pack's state has a second half that is not a
|
|
369
|
+
* projection (a git plane, S3 bytes, a SQL engine): the module that owns every write outside the
|
|
370
|
+
* world store. The tree references engine objects by hash; the gate keeps such writes there. */
|
|
371
|
+
engine?: { module: string; note?: string };
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const registry = new Map<string, TwinPack>();
|
|
375
|
+
|
|
376
|
+
/** Register (or replace) a pack descriptor. Returns it.
|
|
377
|
+
*
|
|
378
|
+
* Side effect, deliberately: a `rateBudget` declaration on the descriptor is ARMED here, so
|
|
379
|
+
* registering a pack is enough to give its vendor the ceiling it declared. `declareRateBudget`
|
|
380
|
+
* is idempotent for an identical declaration and REFUSES a widening one, so re-registering is
|
|
381
|
+
* safe and "register a fatter pack descriptor to buy a bigger budget" is not a move. */
|
|
382
|
+
export function registerPack(pack: TwinPack): TwinPack {
|
|
383
|
+
if (!/^[a-z0-9-]+$/.test(pack.vendor)) throw new Error(`invalid pack vendor id: ${pack.vendor}`);
|
|
384
|
+
if (pack.rateBudget) declareRateBudget(pack.vendor, pack.rateBudget);
|
|
385
|
+
if (pack.stateSystem) registerStateSystem(pack.vendor, pack.stateSystem);
|
|
386
|
+
if (pack.auth) registerAuthStrategy(pack.vendor, pack.auth);
|
|
387
|
+
if (pack.references) registerReferences(pack.vendor, pack.references);
|
|
388
|
+
registry.set(pack.vendor, pack);
|
|
389
|
+
return pack;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export function getPack(vendor: string): TwinPack | undefined {
|
|
393
|
+
return registry.get(vendor);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** All registered packs, sorted by vendor (deterministic). */
|
|
397
|
+
export function listPacks(): TwinPack[] {
|
|
398
|
+
return [...registry.values()].sort((a, b) => a.vendor.localeCompare(b.vendor));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function hasPack(vendor: string): boolean {
|
|
402
|
+
return registry.has(vendor);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Clear the registry (tests). */
|
|
406
|
+
export function clearRegistry(): void {
|
|
407
|
+
registry.clear();
|
|
408
|
+
}
|