@volter/twin-standard 1.0.31 → 1.0.33
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/README.md +9 -0
- package/dist/src/assessment.d.ts +49 -0
- package/dist/src/assessment.js +124 -0
- package/dist/src/browser-assessment.d.ts +6 -0
- package/dist/src/browser-assessment.js +69 -0
- package/dist/src/cli.js +49 -1
- package/dist/src/create-pack.d.ts +38 -0
- package/dist/src/create-pack.js +576 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/pack-facts.d.ts +117 -0
- package/dist/src/pack-facts.js +242 -0
- package/dist/src/walk/behavior-journey.d.ts +1 -0
- package/dist/src/walk/behavior-journey.js +22 -7
- package/dist/src/walk/vendor-fetch.js +1 -7
- package/package.json +7 -3
- package/src/assessment.ts +93 -0
- package/src/browser-assessment.ts +47 -0
- package/src/cli.ts +30 -1
- package/src/create-pack.ts +523 -0
- package/src/index.ts +5 -0
- package/src/pack-facts.ts +240 -0
- package/src/walk/behavior-journey.ts +21 -8
- package/src/walk/vendor-fetch.ts +1 -8
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// Published descriptor compiler. Pack locations are explicit; imports happen only when called by a publisher.
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { PACK_ARCHETYPES, PROTOCOL_VERSION, protocolStanding } from '@volter/world-core/runtime';
|
|
5
|
+
|
|
6
|
+
export type PackFacts = {
|
|
7
|
+
/** Explicit vendor-server backing; this entry declares no pack protocol or factory. */
|
|
8
|
+
serverBacking?: { command: string };
|
|
9
|
+
maxRequestBodySize?: number;
|
|
10
|
+
adoption?: { sdks?: string[]; pypi?: string[]; scopes?: string[]; envStems?: string[]; worldIds?: string[]; tools?: Array<{ package: string; usage: 'build' | 'deployment' }>; configFiles?: Array<{ file: string; usage: 'application' | 'build' | 'deployment'; when?: string }> };
|
|
11
|
+
hosts?: Array<{ host?: string; suffix?: string; hostPattern?: string; pathPattern?: string; demandPathPattern?: string | false; key?: string; exclude?: true }>;
|
|
12
|
+
hostsNone?: string;
|
|
13
|
+
hostsClaimed?: { door: string; note: string };
|
|
14
|
+
/** The subject types the twin projects (packRegistry `resources`) — invariant R2 at the resource
|
|
15
|
+
* level checks a replay creates every one of them. */
|
|
16
|
+
resources?: string[];
|
|
17
|
+
/** The wire transport class (packRegistry PackTransport) — the invariant matrix keys
|
|
18
|
+
* variants off it too (a raw-tcp twin has no workerd fetch path, so R12b is n/a). */
|
|
19
|
+
transport?: string;
|
|
20
|
+
/** Optional native frontend of the same state owner, selected explicitly on the pack CLI. */
|
|
21
|
+
/** The serve-family (packRegistry PackArchetype) — the axis the invariant matrix keys
|
|
22
|
+
* strictness off. Closed enum, REQUIRED: every pack declares one (the catalog was
|
|
23
|
+
* fully classified 2026-09-02; the scaffolder's A1 TODO makes a new pack's
|
|
24
|
+
* ratification unskippable because an undeclared archetype is RED here). */
|
|
25
|
+
archetype?: string;
|
|
26
|
+
/** R16: the protocol major the package targets and its standing against this kernel. */
|
|
27
|
+
protocol?: { declared?: string; major: number; standing: 'current' | 'deprecated' | 'refused' | 'assumed' };
|
|
28
|
+
/** R20: pack-shipped asset paths (relative to the pack dir) the serve path reads through the seam. */
|
|
29
|
+
assets?: string[];
|
|
30
|
+
/** R2: declared types no replay can create, each with its reason (adopted debt, legible). */
|
|
31
|
+
resourcesUnreachable?: Record<string, string>;
|
|
32
|
+
endpointEnv?: { name: string; templates?: Record<string, string> };
|
|
33
|
+
endpointEnvNone?: string;
|
|
34
|
+
/** The door that issues the application a credential the twin accepts (see TwinPack.credentialDoor). */
|
|
35
|
+
credentialDoor?: { path: string; body: Record<string, unknown>; fill: Record<string, string> };
|
|
36
|
+
credentialIssuer?: string;
|
|
37
|
+
/** The Prisma driver adapter this database twin is reached through (see TwinPack.prismaAdapter). */
|
|
38
|
+
prismaAdapter?: { adapter: string; export: string; urlEnv: string };
|
|
39
|
+
/** The managed-infrastructure kind this pack is the World's backing for (architecture.md, "The descriptor"). */
|
|
40
|
+
managedService?: { kind: 'redis' | 'mongodb'; scheme: string; note: string };
|
|
41
|
+
/** The World's managed database the runtime binds the twin to (see TwinPack.managedDatabase). */
|
|
42
|
+
managedDatabase?: { kind: 'postgres'; arg: string; note: string };
|
|
43
|
+
/** Listeners the twin opens beside its main port (see TwinPack.sidePorts). */
|
|
44
|
+
sidePorts?: Array<{ name: string; option: string; flag: string; path?: string; note: string }>;
|
|
45
|
+
/** DERIVED, never declared: the pack's `create<Name>TwinServer` factory export — the
|
|
46
|
+
* `({port,root,readOnly}) => {port,stop}` contract the colocated host mounts
|
|
47
|
+
* (world-runtime/src/host.ts). Read off the module's own exports because the name is not
|
|
48
|
+
* mechanically derivable from the vendor string (createUpstashRedisTwinServer,
|
|
49
|
+
* createGoogleOAuthTwinServer); the export itself is the single source of truth. Consumed
|
|
50
|
+
* by `volter-world init` to emit `colocate:` service entries (runtime contract R2a). */
|
|
51
|
+
serveExport?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type PackFactsFile = {
|
|
55
|
+
'//': string;
|
|
56
|
+
/** Vendor → facts from its current descriptor or explicit vendor-server backing. */
|
|
57
|
+
packs: Record<string, PackFacts>;
|
|
58
|
+
/** vendor packs whose index.ts exports no `pack` descriptor yet — the migration worklist. */
|
|
59
|
+
undeclared: string[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const sortObject = <T>(o: Record<string, T>): Record<string, T> =>
|
|
63
|
+
Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)));
|
|
64
|
+
|
|
65
|
+
/** Host rules are DATA with a closed grammar (packRegistry HostRule): exactly one selector, regex
|
|
66
|
+
* sources that compile, keys that are one pack's alone (and never another pack's vendor id),
|
|
67
|
+
* and an exclude only where the same key has something to exclude from. */
|
|
68
|
+
function validateHostRules(vendor: string, rules: NonNullable<PackFacts['hosts']>, vendors: string[], claimedKeys: Map<string, string>, worldIds: string[]): void {
|
|
69
|
+
const keys = new Set<string>();
|
|
70
|
+
for (const rule of rules) {
|
|
71
|
+
const selectors = ['host', 'suffix', 'hostPattern'].filter((k) => (rule as Record<string, unknown>)[k] !== undefined);
|
|
72
|
+
if (selectors.length !== 1) throw new Error(`pack-facts: ${vendor} host rule ${JSON.stringify(rule)} — exactly one of host | suffix | hostPattern`);
|
|
73
|
+
for (const k of ['hostPattern', 'pathPattern'] as const) {
|
|
74
|
+
if (rule[k] !== undefined) { try { new RegExp(rule[k]!); } catch (e) { throw new Error(`pack-facts: ${vendor} host rule ${k} ${JSON.stringify(rule[k])} does not compile: ${(e as Error).message}`); } }
|
|
75
|
+
}
|
|
76
|
+
if (rule.demandPathPattern !== undefined && rule.demandPathPattern !== false) {
|
|
77
|
+
if (typeof rule.demandPathPattern !== 'string') throw new Error(`pack-facts: ${vendor} demandPathPattern must be a regex source or false`);
|
|
78
|
+
try { new RegExp(rule.demandPathPattern); } catch (e) { throw new Error(`pack-facts: ${vendor} demandPathPattern does not compile: ${(e as Error).message}`); }
|
|
79
|
+
}
|
|
80
|
+
if (rule.exclude !== undefined && rule.exclude !== true) throw new Error(`pack-facts: ${vendor} host rule ${JSON.stringify(rule)} — exclude is \`true\` or absent, never a falsy flag`);
|
|
81
|
+
const key = rule.key ?? vendor;
|
|
82
|
+
// A foreign key is a WORLD identity too: `volter-world init` emits <KEY>_TWIN_URL only for the
|
|
83
|
+
// ids in adoption.worldIds (covers.ts injectorVendorKeysFor), so a key not listed there would
|
|
84
|
+
// route in the injector and never be wired by the world — and a pack claiming ONLY foreign
|
|
85
|
+
// keys would leave its own <VENDOR>_TWIN_URL inert while looking covered.
|
|
86
|
+
if (key !== vendor && !worldIds.includes(key)) throw new Error(`pack-facts: ${vendor} claims key "${key}" but does not list it in adoption.worldIds — the routing identity must also be a world identity`);
|
|
87
|
+
if (!/^[a-z0-9-]+$/.test(key)) throw new Error(`pack-facts: ${vendor} host rule key ${JSON.stringify(key)} — keys are [a-z0-9-]+ (the <KEY>_TWIN_URL stem)`);
|
|
88
|
+
if (key !== vendor && vendors.includes(key)) throw new Error(`pack-facts: ${vendor} claims key "${key}", which is another pack's vendor id — a pack's hosts are its own`);
|
|
89
|
+
const owner = claimedKeys.get(key);
|
|
90
|
+
if (owner !== undefined && owner !== vendor) throw new Error(`pack-facts: key "${key}" is claimed by both ${owner} and ${vendor} — a key belongs to one pack`);
|
|
91
|
+
claimedKeys.set(key, vendor);
|
|
92
|
+
keys.add(key);
|
|
93
|
+
}
|
|
94
|
+
for (const key of keys) {
|
|
95
|
+
if (rules.filter((r) => (r.key ?? vendor) === key).every((r) => r.exclude === true)) throw new Error(`pack-facts: ${vendor} key "${key}" has only exclude rules — nothing to exclude from`);
|
|
96
|
+
}
|
|
97
|
+
// A pack may answer ONLY under foreign keys (aws: the consolidated twin has no `aws` host and
|
|
98
|
+
// AWS_TWIN_URL is null by design) — the worldIds rule above is what makes that explicit.
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Compile descriptors at explicit package directories; no repository layout or publisher namespace is assumed. */
|
|
102
|
+
export async function compilePackFacts(dirs: Array<{ vendor: string; dir: string }>): Promise<PackFactsFile> {
|
|
103
|
+
const claimedKeys = new Map<string, string>();
|
|
104
|
+
const claimedTools = new Map<string, string>();
|
|
105
|
+
const packDirs = new Map(dirs.map((e) => [e.vendor, e.dir] as const));
|
|
106
|
+
const vendors = [...packDirs.keys()].sort();
|
|
107
|
+
const packs: Record<string, PackFacts> = {};
|
|
108
|
+
const undeclared: string[] = [];
|
|
109
|
+
for (const vendor of vendors) {
|
|
110
|
+
const entry = join(packDirs.get(vendor)!, 'src', 'index.ts');
|
|
111
|
+
if (!existsSync(entry)) { undeclared.push(vendor); continue; }
|
|
112
|
+
const mod = await import(entry);
|
|
113
|
+
const pack = mod.pack;
|
|
114
|
+
if (pack === undefined || typeof pack.vendor !== 'string') { undeclared.push(vendor); continue; }
|
|
115
|
+
if (pack.vendor !== vendor) {
|
|
116
|
+
throw new Error(`pack-facts: ${entry} declares vendor "${pack.vendor}" but lives in ${vendor}/ of the pack repository — the directory IS the identity; rename one.`);
|
|
117
|
+
}
|
|
118
|
+
if (pack.hosts !== undefined && pack.hostsNone !== undefined) {
|
|
119
|
+
throw new Error(`pack-facts: ${vendor} declares BOTH hosts and hostsNone — a ruling is one or the other.`);
|
|
120
|
+
}
|
|
121
|
+
if (pack.hosts !== undefined) validateHostRules(vendor, pack.hosts, vendors, claimedKeys, pack.adoption?.worldIds ?? []);
|
|
122
|
+
for (const tool of pack.adoption?.tools ?? []) {
|
|
123
|
+
if (!tool || typeof tool.package !== 'string' || tool.package === '' || (tool.usage !== 'build' && tool.usage !== 'deployment')) {
|
|
124
|
+
throw new Error(`pack-facts: ${vendor} adoption tool must be { package, usage: "build" | "deployment" }`);
|
|
125
|
+
}
|
|
126
|
+
const owner = claimedTools.get(tool.package);
|
|
127
|
+
if (owner !== undefined && owner !== vendor) throw new Error(`pack-facts: tool package "${tool.package}" is claimed by both ${owner} and ${vendor}`);
|
|
128
|
+
claimedTools.set(tool.package, vendor);
|
|
129
|
+
}
|
|
130
|
+
// the World reads its name and templates (init.ts, served-world.ts): a bare string sets nothing, silently
|
|
131
|
+
const ee = pack.endpointEnv as unknown;
|
|
132
|
+
if (ee !== undefined && (typeof ee !== 'object' || ee === null || typeof (ee as { name?: unknown }).name !== 'string' || typeof (ee as { note?: unknown }).note !== 'string')) {
|
|
133
|
+
throw new Error(`pack-facts: ${vendor}'s endpointEnv is ${JSON.stringify(ee)}; it is { name, templates?, note } (the variable the vendor's SDK reads, and why)`);
|
|
134
|
+
}
|
|
135
|
+
if (pack.endpointEnv !== undefined && pack.endpointEnvNone !== undefined) {
|
|
136
|
+
throw new Error(`pack-facts: ${vendor} declares BOTH endpointEnv and endpointEnvNone — a ruling is one or the other.`);
|
|
137
|
+
}
|
|
138
|
+
if (pack.archetype === undefined) {
|
|
139
|
+
throw new Error(`pack-facts: ${vendor} declares no archetype — every pack ratifies its serve-family (closed enum: ${PACK_ARCHETYPES.join(' | ')}; see packRegistry PackArchetype)`);
|
|
140
|
+
}
|
|
141
|
+
if (!(PACK_ARCHETYPES as readonly string[]).includes(pack.archetype)) {
|
|
142
|
+
throw new Error(`pack-facts: ${vendor} declares unknown archetype "${String(pack.archetype)}" (closed enum: ${PACK_ARCHETYPES.join(' | ')})`);
|
|
143
|
+
}
|
|
144
|
+
// raw-tcp consistency: the one raw-protocol pack today serves CRUD state over its wire.
|
|
145
|
+
if (pack.archetype === 'proxy' && pack.transport === 'raw-tcp') {
|
|
146
|
+
throw new Error(`pack-facts: ${vendor} — proxy archetype over raw-tcp transport is undeclared territory; rule it in the contract first.`);
|
|
147
|
+
}
|
|
148
|
+
const facts: PackFacts = {};
|
|
149
|
+
if (pack.adoption !== undefined) facts.adoption = pack.adoption;
|
|
150
|
+
if (pack.hosts !== undefined) facts.hosts = pack.hosts;
|
|
151
|
+
if (pack.hostsNone !== undefined) facts.hostsNone = pack.hostsNone;
|
|
152
|
+
if (pack.hostsClaimed !== undefined) {
|
|
153
|
+
if (!pack.hostsClaimed.door.startsWith('/') || !pack.hostsClaimed.note.trim()) throw new Error(`pack-facts: ${vendor} hostsClaimed needs a door path and a note`);
|
|
154
|
+
if (pack.hosts === undefined) throw new Error(`pack-facts: ${vendor} claims hosts at runtime but declares none of its own: its door is reached on its own host`);
|
|
155
|
+
facts.hostsClaimed = pack.hostsClaimed;
|
|
156
|
+
}
|
|
157
|
+
facts.archetype = pack.archetype;
|
|
158
|
+
// R16 — the protocol the package targets, and where it stands against this kernel: the kernel serves its own
|
|
159
|
+
// major and no other, so any other declaration, or none, is refused here.
|
|
160
|
+
{
|
|
161
|
+
const standing = protocolStanding(pack.protocol);
|
|
162
|
+
if (standing.standing === 'refused') throw new Error(`pack-facts: ${vendor} declares protocol ${JSON.stringify(pack.protocol)} — this kernel serves ${PROTOCOL_VERSION} and no other major (runtime contract R16)`);
|
|
163
|
+
facts.protocol = { ...(pack.protocol !== undefined ? { declared: pack.protocol } : {}), major: standing.major, standing: standing.standing };
|
|
164
|
+
}
|
|
165
|
+
facts.transport = pack.transport;
|
|
166
|
+
if (pack.maxRequestBodySize !== undefined) {
|
|
167
|
+
if (!Number.isSafeInteger(pack.maxRequestBodySize) || pack.maxRequestBodySize <= 0) throw new Error(`pack-facts: ${vendor} maxRequestBodySize must be positive integer bytes`);
|
|
168
|
+
facts.maxRequestBodySize = pack.maxRequestBodySize;
|
|
169
|
+
}
|
|
170
|
+
if (pack.assets !== undefined) facts.assets = pack.assets; // R20 — what the deploy step places behind the assets binding
|
|
171
|
+
if (pack.resourcesUnreachable !== undefined) facts.resourcesUnreachable = pack.resourcesUnreachable; // R2 adopted-as-debt, by name and reason
|
|
172
|
+
facts.resources = pack.resources; // the declared subject types — R2's resource-level claim
|
|
173
|
+
if (pack.endpointEnv !== undefined) facts.endpointEnv = pack.endpointEnv;
|
|
174
|
+
if (pack.endpointEnvNone !== undefined) facts.endpointEnvNone = pack.endpointEnvNone;
|
|
175
|
+
if (pack.credentialDoor !== undefined) facts.credentialDoor = pack.credentialDoor;
|
|
176
|
+
if (pack.credentialIssuer !== undefined) {
|
|
177
|
+
if (!/^[a-z][a-z0-9-]*$/.test(pack.credentialIssuer) || pack.credentialIssuer === vendor) throw new Error(`pack-facts: ${vendor} credentialIssuer must name another pack`);
|
|
178
|
+
facts.credentialIssuer = pack.credentialIssuer;
|
|
179
|
+
}
|
|
180
|
+
if (pack.prismaAdapter !== undefined) {
|
|
181
|
+
if (pack.endpointEnv?.templates?.[pack.prismaAdapter.urlEnv] === undefined && pack.credentialDoor?.fill[pack.prismaAdapter.urlEnv] === undefined) {
|
|
182
|
+
throw new Error(`pack-facts: ${vendor} declares prismaAdapter.urlEnv "${pack.prismaAdapter.urlEnv}" but neither its endpointEnv templates nor its credentialDoor fill such a variable — the adapter's URL is one of theirs.`);
|
|
183
|
+
}
|
|
184
|
+
facts.prismaAdapter = pack.prismaAdapter;
|
|
185
|
+
}
|
|
186
|
+
if (pack.managedDatabase !== undefined) {
|
|
187
|
+
const md = pack.managedDatabase as { kind?: unknown; arg?: unknown; note?: unknown };
|
|
188
|
+
if (md.kind !== 'postgres') throw new Error(`pack-facts: ${vendor} declares managedDatabase.kind "${String(md.kind)}"; the runtime binds only the World's managed postgres`);
|
|
189
|
+
if (typeof md.arg !== 'string' || !/^--[a-z][a-z-]*$/.test(md.arg)) throw new Error(`pack-facts: ${vendor} declares managedDatabase.arg "${String(md.arg)}"; it is the serve flag the URL follows (--name)`);
|
|
190
|
+
if (typeof md.note !== 'string' || md.note.trim() === '') throw new Error(`pack-facts: ${vendor} declares managedDatabase with no note`);
|
|
191
|
+
facts.managedDatabase = pack.managedDatabase;
|
|
192
|
+
}
|
|
193
|
+
if (pack.managedService !== undefined) {
|
|
194
|
+
const ms = pack.managedService as { kind?: unknown; scheme?: unknown; note?: unknown };
|
|
195
|
+
if (ms.kind !== 'redis' && ms.kind !== 'mongodb') throw new Error(`pack-facts: ${vendor} declares managedService.kind "${String(ms.kind)}"; the managed-infrastructure kinds a twin backs are redis and mongodb`);
|
|
196
|
+
if (typeof ms.scheme !== 'string' || !/^[a-z][a-z0-9+.-]*$/.test(ms.scheme)) throw new Error(`pack-facts: ${vendor} declares managedService.scheme "${String(ms.scheme)}"; it is the published URL's scheme`);
|
|
197
|
+
if (typeof ms.note !== 'string' || ms.note.trim() === '') throw new Error(`pack-facts: ${vendor} declares managedService with no note`);
|
|
198
|
+
if (pack.bin === undefined) throw new Error(`pack-facts: ${vendor} declares managedService with no bin; the managed infrastructure starts the pack by its bin`);
|
|
199
|
+
const other = Object.entries(packs).find(([, f]) => f.managedService?.kind === ms.kind);
|
|
200
|
+
if (other) throw new Error(`pack-facts: ${vendor} and ${other[0]} both declare managedService.kind "${ms.kind}"; one pack backs a kind`);
|
|
201
|
+
facts.managedService = pack.managedService;
|
|
202
|
+
}
|
|
203
|
+
if (pack.sidePorts !== undefined) {
|
|
204
|
+
for (const side of pack.sidePorts as Array<Record<string, unknown>>) {
|
|
205
|
+
if (typeof side.name !== 'string' || !/^[a-z][a-z0-9-]*$/.test(side.name)) throw new Error(`pack-facts: ${vendor} declares a side port named "${String(side.name)}"; a name is lowercase (inspect)`);
|
|
206
|
+
if (typeof side.option !== 'string' || !/^[a-z][A-Za-z0-9]*$/.test(side.option)) throw new Error(`pack-facts: ${vendor} side port ${side.name}: option "${String(side.option)}" is the factory's camelCase option`);
|
|
207
|
+
if (typeof side.flag !== 'string' || !/^--[a-z][a-z-]*$/.test(side.flag)) throw new Error(`pack-facts: ${vendor} side port ${side.name}: flag "${String(side.flag)}" is the serve flag (--name)`);
|
|
208
|
+
if (side.path !== undefined && (typeof side.path !== 'string' || !side.path.startsWith('/'))) throw new Error(`pack-facts: ${vendor} side port ${side.name}: path "${String(side.path)}" starts with /`);
|
|
209
|
+
if (typeof side.note !== 'string' || side.note.trim() === '') throw new Error(`pack-facts: ${vendor} side port ${side.name} has no note`);
|
|
210
|
+
}
|
|
211
|
+
facts.sidePorts = pack.sidePorts;
|
|
212
|
+
}
|
|
213
|
+
// The serve factory is DERIVED from the module's exports (see PackFacts.serveExport);
|
|
214
|
+
// a pack exporting SEVERAL factories must declare which one `cli.ts serve` boots — the
|
|
215
|
+
// exports alone cannot reveal that judgment (linear defaults to its DERIVED server).
|
|
216
|
+
const serveExports = Object.keys(mod).filter(
|
|
217
|
+
(k) => /^create\w*TwinServer$/.test(k) && typeof (mod as Record<string, unknown>)[k] === 'function',
|
|
218
|
+
);
|
|
219
|
+
if (pack.serveExport !== undefined) {
|
|
220
|
+
if (!serveExports.includes(pack.serveExport)) {
|
|
221
|
+
throw new Error(`pack-facts: ${vendor} declares serveExport "${pack.serveExport}" but its index exports no such create*TwinServer factory (found: ${serveExports.join(', ') || 'none'}).`);
|
|
222
|
+
}
|
|
223
|
+
facts.serveExport = pack.serveExport;
|
|
224
|
+
} else if (serveExports.length > 1) {
|
|
225
|
+
throw new Error(`pack-facts: ${vendor} exports ${serveExports.length} create*TwinServer factories (${serveExports.join(', ')}) — declare \`serveExport\` on its descriptor naming the one cli.ts serve boots.`);
|
|
226
|
+
} else if (serveExports.length === 1) {
|
|
227
|
+
facts.serveExport = serveExports[0]!;
|
|
228
|
+
}
|
|
229
|
+
if (Object.keys(facts).length > 0) packs[vendor] = facts;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
'//': 'GENERATED by scripts/pack-facts.ts — current pack descriptors and explicit world-runtime server backings; no prior-artifact entries are retained.',
|
|
233
|
+
packs: sortObject(packs),
|
|
234
|
+
undeclared,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function renderPackFacts(file: PackFactsFile): string {
|
|
239
|
+
return JSON.stringify(file, null, 2) + '\n';
|
|
240
|
+
}
|
|
@@ -12,10 +12,10 @@ import { attributionName } from '../verdict.ts';
|
|
|
12
12
|
import { redis } from '@volter/world-core';
|
|
13
13
|
import { gzipSync } from 'node:zlib';
|
|
14
14
|
import { parse, visit } from 'graphql';
|
|
15
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { databaseSession, type DatabaseSession, getActiveWorldStore, MemoryBlobStore, MemoryWorldStore, setActiveBlobStore, setActiveWorldStore, setDeliveryObserver, setInProcessTwins, type InProcessTwin, type TwinStream, type TwinStreamConnection } from '@volter/world-core/runtime';
|
|
18
|
-
import {
|
|
18
|
+
import { createHostTable, type HostFacts } from '@volter/world-core/host-routing';
|
|
19
19
|
import packFactsDocument from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
|
|
20
20
|
import { type Captured, type GitStep, type Journey, type JourneyStep, type LineStep, type SocketStep, type SqlStep, type RespStep, isResp, at, captureAt, echoFailures, expandFills, expectFailures, fill, fillJson, formBody, isGit, isLine, isSql, isSocket, isWait, jwtAuthorization, oauth1Authorization, presignedPath, sigv4Headers, waitMs } from './journey-kit.ts';
|
|
21
21
|
import { lanesOf, lineShape, shapeChecker, type LineConnection, type ShapeChecker } from './shape.ts';
|
|
@@ -30,7 +30,20 @@ function socketAt(open: string, host: string): { host: string; path: string } {
|
|
|
30
30
|
return { host: u.host, path: `${u.pathname}${u.search}` };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
const baselineFacts = packFactsDocument.packs as unknown as HostFacts;
|
|
34
|
+
let walkFacts: HostFacts = baselineFacts;
|
|
35
|
+
let walkHosts = createHostTable(walkFacts).hosts;
|
|
36
|
+
export function usePacks(dir: string): void {
|
|
37
|
+
packsRoot = dir;
|
|
38
|
+
const installed: Record<string, HostFacts[string]> = {};
|
|
39
|
+
for (const name of readdirSync(dir)) {
|
|
40
|
+
const file = join(dir, name, 'generated/pack-facts.json');
|
|
41
|
+
if (existsSync(file)) Object.assign(installed, JSON.parse(readFileSync(file, 'utf8')).packs);
|
|
42
|
+
}
|
|
43
|
+
walkFacts = { ...baselineFacts, ...installed };
|
|
44
|
+
walkHosts = createHostTable(walkFacts, Object.keys(installed)).hosts;
|
|
45
|
+
shapes.clear(); fieldsByVendor.clear();
|
|
46
|
+
}
|
|
34
47
|
const packsDir = (): string => { if (packsRoot === undefined) throw new Error('usePacks(<packs dir>) first'); return packsRoot; };
|
|
35
48
|
|
|
36
49
|
/** The walk's World: the twin's own code on its in-memory World and blob stores (the backends a serverless
|
|
@@ -78,14 +91,14 @@ const shapeOf = (vendor: string): Promise<ShapeChecker | undefined> => { if (!sh
|
|
|
78
91
|
* name, as vendor-hosts.cjs compiles them. */
|
|
79
92
|
function routingKeys(vendor: string): string[] {
|
|
80
93
|
const name = vendor.split('/')[0]!;
|
|
81
|
-
const hosts =
|
|
82
|
-
return [...new Set(hosts.map((rule) => rule.key ?? name))].filter((key) =>
|
|
94
|
+
const hosts = walkFacts[name]?.hosts ?? [];
|
|
95
|
+
return [...new Set(hosts.map((rule) => rule.key ?? name))].filter((key) => walkHosts[key] !== undefined);
|
|
83
96
|
}
|
|
84
97
|
/** Whether one of `keys`' host rules claims `url` (an absolute URL; a path is the life's own host). */
|
|
85
98
|
const claims = (keys: string[], url: string): boolean => {
|
|
86
99
|
if (!/^https?:\/\//.test(url) || !URL.canParse(url)) return false;
|
|
87
100
|
const u = new URL(url);
|
|
88
|
-
return keys.some((key) =>
|
|
101
|
+
return keys.some((key) => walkHosts[key]!(u.hostname, u.pathname));
|
|
89
102
|
};
|
|
90
103
|
|
|
91
104
|
type Answer = { status: number; body: unknown; text: string; headers: Record<string, string> };
|
|
@@ -155,7 +168,7 @@ function takeReplies(c: Connection): LineReply[] {
|
|
|
155
168
|
/** A pack served for a walk the way a World runs it (its server.ts on a port, the World's root and scenario its options):
|
|
156
169
|
* the walk's requests go to it over HTTP, the vendor's host in their Host header, instead of to the fetch in process;
|
|
157
170
|
* its sockets are WebSockets to that port, and a line connection (`stream`) is a TCP connection to its protocol port. */
|
|
158
|
-
export type ServedTwin = (root: string, options: { scenarioPath?: string }) => Promise<{ fetch: (request: Request) => Promise<Response>; stop: () => void; socket?: (url: string, headers: Record<string, string>) => WebSocket; stream?: TwinStream }>;
|
|
171
|
+
export type ServedTwin = (root: string, options: { scenarioPath?: string; application?: import('@volter/world-core').PackFetchOptions['application'] }) => Promise<{ fetch: (request: Request) => Promise<Response>; stop: () => void; socket?: (url: string, headers: Record<string, string>) => WebSocket; stream?: TwinStream }>;
|
|
159
172
|
|
|
160
173
|
export async function walk(journey: Journey, root: string, collect: Failure[], opts: { beats: boolean; verbose?: boolean; hooks?: StepHooks; captured?: Captured; served?: ServedTwin }): Promise<Captured> {
|
|
161
174
|
attributionName(journey.author);
|
|
@@ -206,7 +219,7 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
206
219
|
}
|
|
207
220
|
return { status: a!.status, ...(a!.headers ? { headers: a!.headers } : {}), ...(a!.bodyError === undefined ? {} : { bodyError: a!.bodyError }), body: typeof a!.body === 'string' ? a!.body : a!.body === undefined ? '' : JSON.stringify(a!.body) };
|
|
208
221
|
};
|
|
209
|
-
const served = opts.served ? await opts.served(root, scenarioPath ? { scenarioPath } : {}) : undefined;
|
|
222
|
+
const served = opts.served ? await opts.served(root, { ...(scenarioPath ? { scenarioPath } : {}), application }) : undefined;
|
|
210
223
|
const twin = served ? served.fetch : factory(root, { ...(scenarioPath ? { scenarioPath } : {}), application });
|
|
211
224
|
const shape = await shapeOf(journey.vendor);
|
|
212
225
|
const host = journey.host ?? 'twin.local';
|
package/src/walk/vendor-fetch.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// index. What the catalog's walkers drive (behavior journeys, the spec census probe, the invariants)
|
|
3
3
|
// since the worker render left with the v1 host.
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import packFacts from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
|
|
6
5
|
import { type PgliteHost, startPgliteHost } from './walk-database.ts';
|
|
7
6
|
import { type TwinStream, worldNow } from '@volter/world-core/runtime';
|
|
8
7
|
import type { PackFetchOptions } from '@volter/world-core';
|
|
@@ -20,7 +19,7 @@ export async function vendorFetch(twinDir: string, vendor: string): Promise<Vend
|
|
|
20
19
|
const name = Object.keys(mod).find((k) => /^create\w+?Fetch$/.test(k) && !/TwinFetch$/.test(k) && typeof mod[k] === 'function');
|
|
21
20
|
if (!name) return undefined;
|
|
22
21
|
const factory = mod[name] as (options: { root: string; database?: string; scenarioPath?: string; application?: ApplicationStandIn }) => ((request: Request) => Response | Promise<Response>) & Record<string, unknown>;
|
|
23
|
-
if (!
|
|
22
|
+
if (!(mod.pack as { managedDatabase?: unknown } | undefined)?.managedDatabase) return (root: string, options = {}) => factory({ root, ...options });
|
|
24
23
|
// A pack whose data plane is the World's managed Postgres is walked over one, as a World binds it: each World (each
|
|
25
24
|
// root) its own fresh database, started on its first request and stopped when the walk's process ends, its clock the
|
|
26
25
|
// walked World's (set before each request: the walk moves the World clock between steps).
|
|
@@ -41,12 +40,6 @@ export async function vendorFetch(twinDir: string, vendor: string): Promise<Vend
|
|
|
41
40
|
const hosts: PgliteHost[] = [];
|
|
42
41
|
process.once('exit', () => { for (const host of hosts) { try { process.kill(host.pid, 'SIGTERM'); } catch { /* ended */ } } });
|
|
43
42
|
|
|
44
|
-
/** Whether a pack declares the World's managed database (its descriptor's `managedDatabase`, in the pack facts: this
|
|
45
|
-
* checkout's kernel's, as the walk's other readers of them import them). */
|
|
46
|
-
function declaresManagedDatabase(vendor: string): boolean {
|
|
47
|
-
return (packFacts as { packs: Record<string, { managedDatabase?: unknown }> }).packs[vendor.split('/')[0]!]?.managedDatabase !== undefined;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
43
|
/** Named views of an answer: what a journey step's `view` asserts against. The kit's own: `jwt:<field>`, the payload of the
|
|
51
44
|
* JSON Web Token at that field of the answer, decoded as the application verifying it reads it (a session token's
|
|
52
45
|
* claims). A pack's own (`answerViews` in its index) beside them. */
|