@volter/twin-standard 1.0.158 → 1.0.159

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 (69) hide show
  1. package/README.md +3 -2
  2. package/client-sdks/package.json +1 -0
  3. package/client-sdks/pnpm-lock.yaml +210 -0
  4. package/dist/client-sdks/package.json +1 -0
  5. package/dist/client-sdks/pnpm-lock.yaml +210 -0
  6. package/dist/src/assessment.d.ts +13 -3
  7. package/dist/src/assessment.js +10 -6
  8. package/dist/src/browser-assessment.js +16 -13
  9. package/dist/src/clients/harness.d.ts +25 -4
  10. package/dist/src/clients/harness.js +184 -22
  11. package/dist/src/clients/jira.js +11 -2
  12. package/dist/src/clients/run.js +19 -14
  13. package/dist/src/conformance.d.ts +2 -0
  14. package/dist/src/conformance.js +392 -184
  15. package/dist/src/consumer.d.ts +22 -1
  16. package/dist/src/consumer.js +91 -26
  17. package/dist/src/credential-issuer-import.d.ts +3 -0
  18. package/dist/src/credential-issuer-import.js +42 -0
  19. package/dist/src/derive.js +3 -0
  20. package/dist/src/gate.js +4 -0
  21. package/dist/src/lanes.d.ts +2 -0
  22. package/dist/src/lanes.js +7 -0
  23. package/dist/src/protocol-3.js +8 -3
  24. package/dist/src/spec-documents.d.ts +4 -0
  25. package/dist/src/spec-documents.js +110 -3
  26. package/dist/src/tools/life-coverage-walk.js +38 -26
  27. package/dist/src/tools/score-pack.js +6 -5
  28. package/dist/src/tools/vendor-examples.js +91 -80
  29. package/dist/src/walk/behavior-journey.d.ts +22 -14
  30. package/dist/src/walk/behavior-journey.js +182 -133
  31. package/dist/src/walk/journey-kit.d.ts +3 -2
  32. package/dist/src/walk/journey-kit.js +6 -2
  33. package/dist/src/walk/replay.d.ts +23 -0
  34. package/dist/src/walk/replay.js +118 -0
  35. package/dist/src/walk/serve-pack.d.ts +2 -2
  36. package/dist/src/walk/serve-pack.js +38 -2
  37. package/dist/src/walk/shape.d.ts +25 -4
  38. package/dist/src/walk/shape.js +166 -56
  39. package/dist/src/walk/vendor-fetch.d.ts +7 -2
  40. package/dist/src/walk/vendor-fetch.js +13 -2
  41. package/dist/src/walk/walk-database.d.ts +9 -1
  42. package/dist/src/walk/walk-database.js +35 -13
  43. package/dist/src/walk/xml-view.d.ts +3 -2
  44. package/dist/src/walk/xml-view.js +5 -4
  45. package/package.json +3 -3
  46. package/src/assessment.ts +12 -8
  47. package/src/browser-assessment.ts +12 -5
  48. package/src/clients/harness.ts +142 -19
  49. package/src/clients/jira.ts +4 -2
  50. package/src/clients/run.ts +17 -15
  51. package/src/conformance.ts +313 -128
  52. package/src/consumer.ts +83 -21
  53. package/src/credential-issuer-import.ts +38 -0
  54. package/src/derive.ts +3 -1
  55. package/src/gate.ts +3 -0
  56. package/src/lanes.ts +7 -0
  57. package/src/protocol-3.ts +7 -3
  58. package/src/spec-documents.ts +92 -3
  59. package/src/tools/life-coverage-walk.ts +32 -22
  60. package/src/tools/score-pack.ts +7 -6
  61. package/src/tools/vendor-examples.ts +88 -75
  62. package/src/walk/behavior-journey.ts +145 -109
  63. package/src/walk/journey-kit.ts +7 -5
  64. package/src/walk/replay.ts +103 -0
  65. package/src/walk/serve-pack.ts +25 -5
  66. package/src/walk/shape.ts +156 -54
  67. package/src/walk/vendor-fetch.ts +16 -4
  68. package/src/walk/walk-database.ts +41 -15
  69. package/src/walk/xml-view.ts +5 -4
@@ -1,17 +1,26 @@
1
1
  // The standard serves a pack with its declared backing, as the runtime binds it in a World.
2
2
  import { join } from 'node:path';
3
3
  import type { TwinPack } from '@volter/world-core/runtime';
4
- import { startTwinDatabase, type TwinDatabase } from './walk-database.ts';
4
+ import { startTwinDatabase, type ManagedDatabaseWalk, type TwinDatabase } from './walk-database.ts';
5
5
 
6
6
  export type ServedPack = {
7
7
  port: number;
8
8
  inspectPort?: number;
9
9
  wirePort?: number;
10
10
  stop: () => void | Promise<void>;
11
- managedDatabaseUrl?: () => Promise<string>;
12
- };
11
+ } & Omit<ManagedDatabaseWalk, 'stopDatabase'>;
13
12
 
14
13
  export async function servePack(pack: string, options: { root?: string; scenarioPath?: string }): Promise<ServedPack> {
14
+ const previousBoot = process.env.VOLTER_WORLD_BOOT_ID;
15
+ process.env.VOLTER_WORLD_BOOT_ID = 'conformance';
16
+ try { return await serve(pack, options); }
17
+ finally {
18
+ if (previousBoot === undefined) delete process.env.VOLTER_WORLD_BOOT_ID;
19
+ else process.env.VOLTER_WORLD_BOOT_ID = previousBoot;
20
+ }
21
+ }
22
+
23
+ async function serve(pack: string, options: { root?: string; scenarioPath?: string }): Promise<ServedPack> {
15
24
  const mod = await import(join(pack, 'src', 'server.ts')) as Record<string, unknown>;
16
25
  const create = Object.entries(mod).find(([name, value]) => /^create\w+TwinServer$/.test(name) && typeof value === 'function')?.[1] as ((options: Record<string, unknown>) => Promise<ServedPack>) | undefined;
17
26
  if (!create) throw new Error('src/server.ts exports no create<Vendor>TwinServer');
@@ -20,13 +29,24 @@ export async function servePack(pack: string, options: { root?: string; scenario
20
29
  if (descriptor?.managedDatabase?.kind === 'postgres') database = await startTwinDatabase(pack, descriptor);
21
30
  let server: ServedPack;
22
31
  try { server = await create({ port: 0, ...options, ...(database ? { database: database.url } : {}) }); }
23
- catch (error) { await database?.stop(); throw error; }
32
+ catch (error) {
33
+ try { await database?.stop(); }
34
+ catch (cleanup) { throw new AggregateError([error, cleanup], 'Pack startup and backing retirement failed'); }
35
+ throw error;
36
+ }
24
37
  if (!database) return server;
25
38
  const backing = database;
26
39
  let stopping: Promise<void> | undefined;
27
40
  return {
28
41
  ...server,
29
42
  managedDatabaseUrl: async () => backing.url,
30
- stop: () => stopping ??= (async () => { await server.stop(); await backing.stop(); })(),
43
+ managedDatabaseClock: descriptor!.managedDatabase!.clock,
44
+ managedDatabaseConnection: backing.connection,
45
+ stop: () => stopping ??= (async () => {
46
+ const failures: unknown[] = [];
47
+ try { await server.stop(); } catch (error) { failures.push(error); }
48
+ try { await backing.stop(); } catch (error) { failures.push(error); }
49
+ if (failures.length) throw new AggregateError(failures, 'Pack retirement failed');
50
+ })(),
31
51
  };
32
52
  }
package/src/walk/shape.ts CHANGED
@@ -2,57 +2,156 @@
2
2
  // candidate only the values the spec lists (docs/contributing/architecture.md, "Protocol 3",
3
3
  // State and The report), and every field the spec requires. The walk (behavior-journey.ts) runs it on every successful step of a derived
4
4
  // pack, so a walk an author iterates with fails where the score would; score-pack reads it from there.
5
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
6
- import { join } from 'node:path';
7
- import { compileSurface, matchOperation } from '@volter/world-core/runtime';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { basename, join } from 'node:path';
7
+ import { compileSurface, hostOf, laneSelector, matchOperation, slashesMerged, unitRequestRouter, worldDoorMatcher } from '@volter/world-core/runtime';
8
+ import type { DerivedManifest } from '@volter/world-core';
9
+ import { parseExactJson } from '@volter/world-core/exact-json';
10
+ import { hostRules } from '@volter/world-core/host-routing';
8
11
  import { binaryMedia } from '../spec-ir.ts';
12
+ import { schemaFieldNames } from '../spec-documents.ts';
13
+ import { unitsIn } from '../lanes.ts';
14
+ import { xmlMembers } from './xml-view.ts';
15
+ import { parse, visit } from 'graphql';
9
16
 
10
17
  export type ShapeOperation = { id: string; method: string; path: string; class: string; resource?: string; successStatus?: number; responseMedia?: Record<string, string[]>; defaultResponseMedia?: Record<string, string>; answers?: { resource: string; list: boolean; key?: string; extends?: string[]; alternatives?: string[] } };
11
18
  type Resource = { name: string; fields: Array<{ name: string; required?: boolean; enum?: string[] }> };
12
19
  /** Whether a resource is one of the objects `said` names (a union's `object` lists every alternative's). */
13
20
  const isObject = (r: Resource, said: unknown): boolean => typeof said === 'string' && (r.fields.find((f) => f.name === 'object')?.enum ?? []).includes(said);
14
21
  type Surface = { version: string; basePath?: string; operations: ShapeOperation[]; resources: Resource[] };
22
+ type FieldSurface = { resources?: Array<{ fields: Array<{ name: string } | string> }> };
23
+
24
+ /** Field names owned by the vendor's resource schemas and native error templates. */
25
+ function vendorFieldNames(surface: FieldSurface | undefined, ...errors: unknown[]): Set<string> {
26
+ const fields = new Set((surface?.resources ?? []).flatMap((resource) => resource.fields.map((field) => typeof field === 'string' ? field : field.name)));
27
+ const visit = (value: unknown): void => {
28
+ if (Array.isArray(value)) { for (const item of value) visit(item); }
29
+ else if (value && typeof value === 'object') for (const [name, item] of Object.entries(value)) { fields.add(name); visit(item); }
30
+ };
31
+ for (const error of errors) visit(error);
32
+ return fields;
33
+ }
34
+
35
+ /** Every selected unit's vendor fields, including nested spec schemas, GraphQL and native error wires. */
36
+ export async function vendorFieldsOf(pkg: string): Promise<Set<string>> {
37
+ const fields = new Set<string>();
38
+ for (const unit of unitsIn(pkg)) {
39
+ const manifestPath = join(unit, 'src', 'manifest.ts');
40
+ const { manifest } = existsSync(manifestPath) ? await import(manifestPath) as { manifest?: { error?: unknown; auth?: { wires?: Array<{ error: unknown }> } } } : {};
41
+ for (const name of ['surface.gen.json', 'graphql.gen.json']) {
42
+ const path = join(unit, 'src', 'generated', name);
43
+ if (existsSync(path)) for (const field of vendorFieldNames(JSON.parse(readFileSync(path, 'utf8')) as FieldSurface)) fields.add(field);
44
+ }
45
+ for (const field of schemaFieldNames(join(unit, 'spec'))) fields.add(field);
46
+ for (const field of vendorFieldNames(undefined, manifest?.error, ...(manifest?.auth?.wires ?? []).map((wire) => wire.error))) fields.add(field);
47
+ }
48
+ return fields;
49
+ }
50
+
51
+ /** Decode a vendor answer without rounding wire integers; XML reads as its root's members. */
52
+ export function decodeResponse(text: string, mediaType: string | undefined): unknown {
53
+ let body: unknown;
54
+ try { body = parseExactJson(text); } catch { body = text; }
55
+ return typeof body === 'string' && /xml/.test(mediaType ?? '') ? xmlMembers(text) ?? text : body;
56
+ }
57
+
58
+ /** Private stored fields, except names the vendor owns, in any answer, event or frame. */
59
+ export function leaked(v: unknown, vendorFields: Set<string>, at = '', unshaped = false): string[] {
60
+ if (Array.isArray(v)) return v.flatMap((x, i) => leaked(x, vendorFields, `${at}[${i}]`, unshaped)).slice(0, 5);
61
+ if (!v || typeof v !== 'object') return [];
62
+ return Object.entries(v).flatMap(([k, x]) => ((k.startsWith('_') || (unshaped && k === 'updatedAt')) && !vendorFields.has(k) ? [`${at}${at ? '.' : ''}${k}`] : leaked(x, vendorFields, `${at}${at ? '.' : ''}${k}`, unshaped))).slice(0, 5);
63
+ }
64
+
65
+ /** Vendor schema fields plus the response keys this GraphQL request selects, including aliases. */
66
+ function responseFieldNames(schemaFields: ReadonlySet<string>, graphqlBody: unknown): Set<string> {
67
+ const fields = new Set(schemaFields);
68
+ if (graphqlBody && typeof graphqlBody === 'object' && typeof (graphqlBody as { query?: unknown }).query === 'string') {
69
+ try { visit(parse((graphqlBody as { query: string }).query), { Field: (field) => { fields.add(field.alias?.value ?? field.name.value); } }); } catch { /* malformed documents have no selected response keys */ }
70
+ }
71
+ return fields;
72
+ }
15
73
 
16
74
  export type ShapeChecker = {
17
- /** The spec operation a request matches, if any. An RPC wire answers a method whatever the HTTP method. */
18
- operationOf: (method: string, path: string, headers?: Record<string, string>) => { op: ShapeOperation; params: Record<string, string> } | undefined;
75
+ /** The spec operation the routed request matches, if any. */
76
+ operationOf: (method: string, path: string, headers?: Record<string, string>) => { op: ShapeOperation; params: Record<string, string>; lane?: string } | undefined;
19
77
  /** A request's URL as the unit routes it (a virtual-hosted S3 request's bucket moved from its host into its path). */
20
- pathOf?: (path: string) => string;
78
+ pathOf: (method: string, path: string, headers?: Record<string, string>) => string;
21
79
  /** What in an answer the spec does not give the operation's resource (or its view). */
22
80
  failures: (op: ShapeOperation, body: unknown, mediaType?: string, accept?: string, status?: number, exchange?: { method: string; responseText: string }) => string[];
23
81
  /** Whether a request pins an API version before the spec's, whose answers the spec does not describe (a Stripe
24
82
  * mobile SDK's `Stripe-Version: 2024-06-20`), by the version header the manifest declares. */
25
- pinsOlder: (headers: Record<string, string> | undefined) => boolean;
83
+ pinsOlder: (headers: Record<string, string> | undefined, op: ShapeOperation) => boolean;
26
84
  };
27
85
 
86
+ type UnitChecker = Omit<ShapeChecker, 'pathOf'> & { serves: (method: string, path: string) => boolean };
87
+ type UnitResponseOwner = { pathOf: ShapeChecker['pathOf']; checker?: UnitChecker };
88
+
89
+ /** The same response rules apply to a life's requests and the unscripted surface sweep. */
90
+ export function responseProblems(checker: ShapeChecker | undefined, schemaFields: Set<string>, exchange: {
91
+ method: string; path: string; requestHeaders: Record<string, string>; graphqlBody: unknown;
92
+ status: number; mediaType: string | undefined; body: unknown; text: string;
93
+ }): { shape: string[]; leaks: string[] } {
94
+ const hit = checker?.operationOf(exchange.method, exchange.path, exchange.requestHeaders);
95
+ const shaped = exchange.status >= 200 && exchange.status < 300 && hit !== undefined && !checker!.pinsOlder(exchange.requestHeaders, hit.op);
96
+ return {
97
+ shape: shaped ? checker!.failures(hit.op, exchange.body, exchange.mediaType, new Headers(exchange.requestHeaders).get('accept') ?? undefined, exchange.status, { method: exchange.method, responseText: exchange.text }) : [],
98
+ leaks: leaked(exchange.body, responseFieldNames(schemaFields, exchange.graphqlBody), '', !shaped || !hit?.op.answers),
99
+ };
100
+ }
101
+
28
102
  /** The directories under a pack that are lanes: a derived unit with its own generated surface
29
103
  * (`../twin-packs-p3/aws/secretsmanager/`, `../twin-packs-p3/planetscale/api/`). */
30
104
  export function lanesOf(pkg: string): string[] {
31
- if (!existsSync(pkg)) return [];
32
- return readdirSync(pkg, { withFileTypes: true })
33
- .filter((e) => e.isDirectory() && !['src', 'spec', 'journeys', 'node_modules', 'scripts', 'test-fixtures'].includes(e.name) && existsSync(join(pkg, e.name, 'src', 'generated', 'surface.gen.json')))
34
- .map((e) => join(pkg, e.name));
105
+ return unitsIn(pkg).filter((unit) => unit !== pkg && existsSync(join(unit, 'src', 'generated', 'surface.gen.json')));
35
106
  }
36
107
 
37
108
  /** A derived pack's shape checker, or undefined for a pack with no generated surface. A pack's walk reaches its lanes'
38
109
  * operations through the pack's router (a psdb life creates its database's passwords through the management API), so
39
- * a request is matched against the pack's own surface, then each lane's, and judged by the surface it matched. */
110
+ * the core gateway selects the owning lane before its surface is matched and its answer judged. */
40
111
  export async function shapeChecker(pkg: string): Promise<ShapeChecker | undefined> {
41
- const units = (await Promise.all([pkg, ...lanesOf(pkg)].map((dir) => unitChecker(dir)))).filter((c): c is ShapeChecker => !!c);
42
- if (!units.length) return undefined;
43
- if (units.length === 1) return units[0];
44
- const owner = new WeakMap<ShapeOperation, ShapeChecker>();
112
+ const units = await Promise.all(unitsIn(pkg).map(async (dir) => {
113
+ const path = join(dir, 'src', 'manifest.ts');
114
+ const { manifest } = existsSync(path) ? await import(path) as { manifest?: Partial<DerivedManifest> } : {};
115
+ return { dir, manifest, response: unitResponseOwner(dir, manifest), door: worldDoorMatcher(manifest?.doors ?? []) };
116
+ }));
117
+ if (!units.some((unit) => unit.response?.checker)) return undefined;
118
+ const root = units[0]!;
119
+ const arrival = (method: string, path: string, headers: Record<string, string> | undefined): Request => {
120
+ const request = new Request(new URL(path, 'http://twin.local'), { method, headers });
121
+ return root.manifest?.mergeSlashes ? slashesMerged(request) : request;
122
+ };
123
+ const selected = root.manifest?.lanes ? laneSelector(root.manifest.lanes, Object.fromEntries(units.slice(1).map((unit) => [basename(unit.dir), {
124
+ manifest: unit.manifest ?? {}, fetch: { serves: unit.response?.checker?.serves,
125
+ contentHosts: (unit.manifest?.screens ?? []).filter((screen) => screen.kind === 'content' && screen.status === 'done').flatMap((screen) => [screen.host, ...(screen.hosts ?? [])]).filter((host) => !/[{<]/.test(host)).map((host) => host.toLowerCase()) },
126
+ }])), root.response?.checker?.serves) : undefined;
127
+ const owner = new WeakMap<ShapeOperation, UnitChecker>();
128
+ const selectedRequest = (method: string, path: string, headers: Record<string, string> | undefined) => {
129
+ const request = arrival(method, path, headers);
130
+ const source = root.manifest?.mergeSlashes ? request.url : path;
131
+ const rootDoor = root.door(request.method, new URL(request.url).pathname);
132
+ const name = rootDoor ? undefined : selected?.(request);
133
+ const candidates = rootDoor ? [root] : selected ? [name === undefined ? root : units.find((unit) => basename(unit.dir) === name)] : units;
134
+ for (const unit of candidates) {
135
+ if (!unit) continue;
136
+ const routed = unit.response?.pathOf(method, source, headers) ?? request.url;
137
+ if (rootDoor || unit.door(method, new URL(routed).pathname)) return { path: routed };
138
+ const checker = unit.response?.checker;
139
+ const hit = checker?.operationOf(method, source, headers);
140
+ if (hit) return { path: routed, checker, hit: unit === root ? hit : { ...hit, lane: basename(unit.dir) } };
141
+ }
142
+ return { path: candidates[0]?.response?.pathOf(method, source, headers) ?? request.url };
143
+ };
144
+ const operationOf: ShapeChecker['operationOf'] = (method, path, headers) => {
145
+ const selected = selectedRequest(method, path, headers);
146
+ if (!selected.hit) return undefined;
147
+ owner.set(selected.hit.op, selected.checker!);
148
+ return selected.hit;
149
+ };
45
150
  return {
46
- pinsOlder: (headers) => units[0]!.pinsOlder(headers),
47
- pathOf: (path) => units.find((u) => u.operationOf('GET', path) || u.operationOf('PUT', path))?.pathOf?.(path) ?? path,
48
- operationOf: (method, path, headers) => {
49
- for (const u of units) {
50
- const hit = u.operationOf(method, path, headers);
51
- if (hit) { owner.set(hit.op, u); return hit; }
52
- }
53
- return undefined;
54
- },
55
- failures: (op, body, mediaType, accept, status, exchange) => (owner.get(op) ?? units[0]!).failures(op, body, mediaType, accept, status, exchange),
151
+ pinsOlder: (headers, op) => owner.get(op)!.pinsOlder(headers, op),
152
+ pathOf: (method, path, headers) => selectedRequest(method, path, headers).path,
153
+ operationOf,
154
+ failures: (op, body, mediaType, accept, status, exchange) => owner.get(op)!.failures(op, body, mediaType, accept, status, exchange),
56
155
  };
57
156
  }
58
157
 
@@ -71,48 +170,49 @@ function acceptsMedia(accept: string | undefined, media: string, defaultMedia?:
71
170
  return (matches[0]?.q ?? 0) > 0;
72
171
  }
73
172
 
74
- async function unitChecker(pkg: string): Promise<ShapeChecker | undefined> {
173
+ function unitResponseOwner(pkg: string, manifest: Partial<DerivedManifest> | undefined): UnitResponseOwner | undefined {
75
174
  const surfacePath = join(pkg, 'src', 'generated', 'surface.gen.json');
76
175
  if (!existsSync(surfacePath)) return undefined;
77
- const surface = JSON.parse(readFileSync(surfacePath, 'utf8')) as Surface;
78
- // a line protocol's surface is its commands' replies (lineShape); a command table's commands ride in a request's body
79
- // (Upstash's REST envelope), which a check by method and path cannot see: its replies are not shape-checked yet, and
80
- // its examples assert what each answers
81
- if (['command-reply', 'command-table'].includes(String((surface as { format?: string }).format))) return undefined;
82
- const manifestModule = existsSync(join(pkg, 'src', 'manifest.ts')) ? ((await import(join(pkg, 'src', 'manifest.ts'))) as { manifest?: { doors?: ReadonlyArray<{ method: string; path: string }>; worldFields?: Record<string, Record<string, { type: 'array' | 'object' | 'string' | 'number' | 'boolean'; why: string }>>; version?: { header: string }; hosts?: ReadonlyArray<{ host?: string; suffix?: string }>; spanning?: ReadonlyArray<string>; pathFromHost?: string } }) : {};
83
- // a unit whose host can name part of what its path names (S3's virtual-hosted bucket) is routed as the kernel routes it: a host whose label is part of the path (the manifest's pathFromHost, S3's virtual-hosted
84
- // addressing) puts it in front of the path
85
- const hostPath = manifestModule.manifest?.pathFromHost ? new RegExp(manifestModule.manifest.pathFromHost, 'i') : undefined;
86
- const pathStyle = (u: string): string => {
87
- const url = new URL(u);
88
- const label = hostPath?.exec(url.hostname)?.[1];
89
- return label ? `${url.protocol}//${url.host}/${label}${url.pathname === '/' ? '' : url.pathname}${url.search}` : u;
90
- };
176
+ const source = JSON.parse(readFileSync(surfacePath, 'utf8')) as Surface;
177
+ const surface = { ...source, operations: source.operations ?? [] };
91
178
  // a lane is routed by host ("Other wires: lanes"): one whose manifest names the hosts it serves is judged on those
92
179
  // alone, so a page or door of its pack on another host (a dashboard, a World door) is never read as one of its operations
93
- const hosts = manifestModule.manifest?.hosts;
94
- const served = (hostname: string): boolean => !hosts || hosts.some((h) => (h.host ? hostname === h.host : !!h.suffix && hostname.endsWith(h.suffix)));
95
- const versionHeader = manifestModule.manifest?.version?.header.toLowerCase();
180
+ const hosts = manifest?.hosts;
181
+ const claims = hostRules(hosts);
182
+ const versionHeader = manifest?.version?.header.toLowerCase();
96
183
  const specDate = /^\d{4}-\d{2}-\d{2}/.exec(String(surface.version))?.[0];
97
184
  // the path parameters whose values hold slashes (a git ref, a file path): the surface's own `spanning`, and the
98
185
  // manifest's where the spec cannot say
99
- const spans = [...((surface as { spanning?: string[] }).spanning ?? []), ...(manifestModule.manifest?.spanning ?? [])];
100
- const routes = compileSurface({ ...surface, ...(spans.length ? { spanning: spans } : {}) });
186
+ const spans = [...((surface as { spanning?: string[] }).spanning ?? []), ...(manifest?.spanning ?? [])];
187
+ const effectiveSurface = { ...surface, ...(spans.length ? { spanning: spans } : {}) };
188
+ const routes = compileSurface(effectiveSurface);
189
+ const routed = unitRequestRouter(manifest ?? {}, effectiveSurface, routes);
190
+ const door = worldDoorMatcher(manifest?.doors ?? []);
191
+ const requestAt = (method: string, path: string, headers: Record<string, string> | undefined): Request => {
192
+ const request = new Request(new URL(path, 'http://twin.local'), { method, headers });
193
+ return routed(manifest?.mergeSlashes ? slashesMerged(request) : request);
194
+ };
195
+ const pathOf: ShapeChecker['pathOf'] = (method, path, headers) => requestAt(method, path, headers).url;
196
+ // Command surfaces retain routing ownership; their replies are checked through their native protocol.
197
+ if (['command-reply', 'command-table'].includes(String((surface as { format?: string }).format))) return { pathOf };
101
198
  const resources = new Map(surface.resources.map((r) => [r.name, r]));
102
- return {
199
+ const checker: UnitChecker = {
200
+ serves: (method, path) => Boolean(matchOperation(routes, method, path)),
103
201
  pinsOlder: (headers) => {
104
202
  if (!versionHeader || !specDate || !headers) return false;
105
203
  const pinned = Object.entries(headers).find(([k]) => k.toLowerCase() === versionHeader)?.[1];
106
204
  return typeof pinned === 'string' && pinned.slice(0, 10) < specDate;
107
205
  },
108
- pathOf: (path) => (/^https?:\/\//.test(path) ? pathStyle(path) : path),
109
206
  operationOf: (method, path, headers) => {
110
- const url = new URL(/^https?:\/\//.test(path) ? pathStyle(path) : path, 'http://twin.local');
111
- if (/^https?:\/\//.test(path) && !served(new URL(path).hostname)) return undefined;
207
+ const request = requestAt(method, path, headers);
208
+ const url = new URL(request.url);
209
+ if (/^https?:\/\//.test(path)) {
210
+ const original = new URL(path);
211
+ if (hosts && !claims.takes(hostOf(request), original.pathname)) return undefined;
212
+ }
112
213
  // a World door the manifest declares is that door, never the vendor's operation at a path it would also match
113
- if (manifestModule.manifest?.doors?.some((door) => door.method.toUpperCase() === method.toUpperCase() && door.path === url.pathname)) return undefined;
114
- const h = new Headers(headers ?? {});
115
- const hit = matchOperation(routes, method, url.pathname, url.searchParams, h) ?? [...routes.keys()].map((m) => matchOperation(routes, m, url.pathname, url.searchParams, h)).find(Boolean);
214
+ if (door(request.method, url.pathname)) return undefined;
215
+ const hit = matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers);
116
216
  return hit ? { op: hit.operation as ShapeOperation, params: hit.params } : undefined;
117
217
  },
118
218
  failures: (op, body, mediaType, accept, status = op.successStatus ?? 200, exchange) => {
@@ -128,6 +228,7 @@ async function unitChecker(pkg: string): Promise<ShapeChecker | undefined> {
128
228
  if (binary && declared.some((value) => value.toLowerCase() === media) && acceptsMedia(accept, media, defaultMedia)) return [];
129
229
  const primary = op.answers ? resources.get(op.answers.resource) : undefined;
130
230
  if (!primary) return [];
231
+ if (exchange && /xml/.test(media ?? '')) body = xmlMembers(exchange.responseText, primary.fields.map((field) => field.name)) ?? body;
131
232
  if (!body || typeof body !== 'object') return binary ? [`the answer is not the operation's ${primary.name} resource`] : [];
132
233
  // an answer that may be one of several resources (a card or a bank account) is judged as the one it says it is
133
234
  const alternatives = [primary, ...(op.answers!.alternatives ?? []).map((n) => resources.get(n)).filter((x): x is Resource => !!x)];
@@ -144,7 +245,7 @@ async function unitChecker(pkg: string): Promise<ShapeChecker | undefined> {
144
245
  const known = new Map<string, { enum?: string[] }>([...r.fields.map((f) => [f.name, f] as const), ...(op.answers!.extends ?? []).map((name) => [name, {}] as const)]);
145
246
  for (const [k, v] of Object.entries(item as Record<string, unknown>)) {
146
247
  const f = known.get(k);
147
- const local = manifestModule.manifest?.worldFields?.[r.name]?.[k];
248
+ const local = manifest?.worldFields?.[r.name]?.[k];
148
249
  if (local) {
149
250
  if (!k.startsWith('twin_') || !local.why?.trim() || f) found.push(`${r.name}.${k}: a World field must have a twin_ name and reason and must not replace a vendor field`);
150
251
  const type = Array.isArray(v) ? 'array' : v === null ? 'null' : typeof v;
@@ -170,6 +271,7 @@ async function unitChecker(pkg: string): Promise<ShapeChecker | undefined> {
170
271
  return [...new Set(out)];
171
272
  },
172
273
  };
274
+ return { pathOf, checker };
173
275
  }
174
276
 
175
277
  // ── a line protocol's replies ─────────────────────────────────────────────────────────────────
@@ -2,7 +2,7 @@
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 { type TwinDatabase, startTwinDatabase } from './walk-database.ts';
5
+ import { type ManagedDatabaseWalk, type TwinDatabase, startTwinDatabase } from './walk-database.ts';
6
6
  import { type TwinStream, type TwinPack } from '@volter/world-core/runtime';
7
7
  import type { PackFetchOptions } from '@volter/world-core';
8
8
  import { walkPackDirectory } from './pack-directory.ts';
@@ -11,11 +11,16 @@ type ApplicationStandIn = NonNullable<PackFetchOptions['application']>;
11
11
 
12
12
  /** A pack's fetch over a World's root; a walk whose life names a scenario loads it as the World's (`scenarioPath`), and a
13
13
  * life's application answers stand in for the application the vendor asks (`application`, this fetch's alone). */
14
- export type VendorFetchFactory = (root: string, options?: { scenarioPath?: string; application?: ApplicationStandIn }) => (request: Request) => Response | Promise<Response>;
14
+ export type VendorFetchFactory = ((root: string, options?: { scenarioPath?: string; application?: ApplicationStandIn }) => ((request: Request) => Response | Promise<Response>) & ManagedDatabaseWalk) & { managedDatabaseDoor?: string };
15
15
 
16
16
  export async function vendorFetch(twinDir: string, vendor: string, dependenciesFrom = twinDir): Promise<VendorFetchFactory | undefined> {
17
17
  const directory = walkPackDirectory(twinDir, vendor, dependenciesFrom);
18
18
  if (!directory) return undefined;
19
+ return packFetch(directory);
20
+ }
21
+
22
+ /** The selected pack's same fetch and native backing for in-process and browser transports. */
23
+ export async function packFetch(directory: string): Promise<VendorFetchFactory | undefined> {
19
24
  let mod: Record<string, unknown>;
20
25
  try { mod = (await import(join(directory, 'src', 'index.ts'))) as Record<string, unknown>; } catch { return undefined; }
21
26
  // a pack exports create<Name>Fetch (its generated fetch.ts)
@@ -24,9 +29,10 @@ export async function vendorFetch(twinDir: string, vendor: string, dependenciesF
24
29
  const factory = mod[name] as (options: { root: string; database?: string; scenarioPath?: string; application?: ApplicationStandIn }) => ((request: Request) => Response | Promise<Response>) & Record<string, unknown>;
25
30
  const descriptor = mod.pack as TwinPack | undefined;
26
31
  if (!descriptor?.managedDatabase) return (root: string, options = {}) => factory({ root, ...options });
32
+ const managedDatabaseClock = descriptor.managedDatabase.clock;
27
33
  // Each walked twin owns a fresh SQL product through the ordinary selected-twin lifecycle.
28
34
  // Native SQL uses the descriptor's explicit wall clock; kernel World time is not claimed for it.
29
- return (root: string, options = {}) => {
35
+ const backed: VendorFetchFactory = (root, options = {}) => {
30
36
  const bare = factory({ root, ...options });
31
37
  let bound: Promise<(request: Request) => Response | Promise<Response>> | undefined;
32
38
  let host: TwinDatabase | undefined;
@@ -34,10 +40,16 @@ export async function vendorFetch(twinDir: string, vendor: string, dependenciesF
34
40
  const served = (request: Request) => bind().then((fetch) => { return fetch(request); });
35
41
  // The twin's SQL product, for what an application sends over its own wire (journey-kit.ts).
36
42
  const managedDatabaseUrl = () => bind().then(() => { return host!.url; });
43
+ const managedDatabaseConnection: TwinDatabase['connection'] = async (...args) => {
44
+ await bind();
45
+ return host!.connection(...args);
46
+ };
37
47
  // The walk's end retires the selected twin's private engine through its World.
38
48
  const stopDatabase = async () => { if (bound === undefined) return; await bound.catch(() => undefined); await host?.stop(); };
39
- return Object.assign(served, bare, { managedDatabaseUrl, stopDatabase });
49
+ return Object.assign(served, bare, { managedDatabaseUrl, managedDatabaseClock, managedDatabaseConnection, stopDatabase });
40
50
  };
51
+ backed.managedDatabaseDoor = descriptor.credentialDoor?.path;
52
+ return backed;
41
53
  }
42
54
 
43
55
 
@@ -3,7 +3,8 @@
3
3
  import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
5
  import { dirname, join } from 'node:path';
6
- import type { TwinPack } from '@volter/world-core/runtime';
6
+ import { worldDoorMatcher, type TwinPack } from '@volter/world-core/runtime';
7
+ import type { ReplayStep } from './replay.ts';
7
8
 
8
9
  type Runtime = typeof import('@volter/world-runtime');
9
10
  let loaded: Promise<Runtime> | undefined;
@@ -13,13 +14,24 @@ const worldRuntime = () => loaded ??= (async () => {
13
14
  return import('@volter/world-runtime');
14
15
  })();
15
16
 
16
- export type TwinDatabase = { url: string; stop: () => Promise<void> };
17
+ export type TwinDatabase = {
18
+ url: string;
19
+ connection: (method: string, path: string, status: number, body: unknown) => Promise<ReplayStep['connection']>;
20
+ stop: () => Promise<void>;
21
+ };
22
+
23
+ export type ManagedDatabaseWalk = {
24
+ managedDatabaseUrl?: () => Promise<string>;
25
+ managedDatabaseClock?: 'wall';
26
+ managedDatabaseConnection?: TwinDatabase['connection'];
27
+ stopDatabase?: () => Promise<void>;
28
+ };
17
29
 
18
30
  export async function startTwinDatabase(pack: string, descriptor: TwinPack): Promise<TwinDatabase> {
19
31
  if (!process.env.VOLTER_WORLD) throw new Error('A managed SQL walk requires its enclosing World');
20
32
  const definition = descriptor.managedDatabase, door = descriptor.credentialDoor;
21
33
  if (!definition || !door || !definition.credentialField) throw new Error('The selected twin must declare its SQL credential door field');
22
- const { runWorld, worldConfigDocument } = await worldRuntime();
34
+ const { runWorld, worldConfigDocument, verifyPrivateTwinDatabaseConnection } = await worldRuntime();
23
35
  // Keep the disposable World on the pack's own volume and package-resolution ancestry.
24
36
  const root = mkdtempSync(join(dirname(pack), '.walk-database-'));
25
37
  const manifest = JSON.parse(readFileSync(join(pack, 'package.json'), 'utf8')) as { name: string };
@@ -30,10 +42,10 @@ export async function startTwinDatabase(pack: string, descriptor: TwinPack): Pro
30
42
  const readyFile = join(root, 'ready.json'), releaseFile = join(root, 'release');
31
43
  // Ask the selected twin's actual product door. The runtime alone allocates and owns its engine.
32
44
  const hold = `
33
- import { existsSync, readFileSync, renameSync, watch, writeFileSync } from 'node:fs';
34
- import { dirname } from 'node:path';
45
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
46
+ import { join } from 'node:path';
35
47
  const [ready, release, vendor, path, body, field] = process.argv.slice(1);
36
- if (!process.env.VOLTER_WORLD_INSTANCE) throw new Error('Missing held World instance');
48
+ if (!process.env.VOLTER_WORLD_INSTANCE || !process.env.VOLTER_WORLD_DATA) throw new Error('Missing held World instance');
37
49
  const instance = JSON.parse(readFileSync(process.env.VOLTER_WORLD_INSTANCE, 'utf8'));
38
50
  const service = instance.services[vendor];
39
51
  if (!service?.url) throw new Error('Selected twin has no product endpoint');
@@ -42,8 +54,8 @@ export async function startTwinDatabase(pack: string, descriptor: TwinPack): Pro
42
54
  const result = await response.json();
43
55
  if (typeof result[field] !== 'string' || !result[field]) throw new Error('SQL credential door omitted its declared field');
44
56
  const done = () => { if (existsSync(release)) process.exit(0); };
45
- watch(dirname(release), done);
46
- writeFileSync(ready + '.tmp', JSON.stringify({ url: result[field] }), {mode:0o600});
57
+ setInterval(done, 100);
58
+ writeFileSync(ready + '.tmp', JSON.stringify({ url: result[field], databaseRoot: join(process.env.VOLTER_WORLD_DATA, vendor) }), {mode:0o600});
47
59
  renameSync(ready + '.tmp', ready);
48
60
  done();`;
49
61
  let finished: { result?: Awaited<ReturnType<Runtime['runWorld']>>; error?: unknown; failed: boolean } | undefined;
@@ -61,13 +73,27 @@ export async function startTwinDatabase(pack: string, descriptor: TwinPack): Pro
61
73
  }
62
74
  await new Promise(resolve => setTimeout(resolve, 100));
63
75
  }
64
- const ready = JSON.parse(readFileSync(readyFile, 'utf8')) as { url: string };
76
+ const ready = JSON.parse(readFileSync(readyFile, 'utf8')) as { url: string; databaseRoot: string };
65
77
  let stopping: Promise<void> | undefined;
66
- return { url: ready.url,
67
- stop: () => stopping ??= (async () => {
68
- writeFileSync(releaseFile, 'released\n');
69
- const result = await task;
70
- if (result.exitCode !== 0) throw new Error(`Twin SQL World consumer exited ${result.exitCode}; evidence: ${root}`);
71
- })(),
78
+ const stop = () => stopping ??= (async () => {
79
+ writeFileSync(releaseFile, 'released\n');
80
+ const result = await task;
81
+ if (result.exitCode !== 0) throw new Error(`Twin SQL World consumer exited ${result.exitCode}; evidence: ${root}`);
82
+ })();
83
+ const verify = (value: unknown) => verifyPrivateTwinDatabaseConnection(descriptor.vendor, ready.databaseRoot, value);
84
+ try { await verify(ready.url); }
85
+ catch (error) {
86
+ try { await stop(); }
87
+ catch (cleanup) { throw new AggregateError([error, cleanup], 'Native connection verification and World retirement failed'); }
88
+ throw error;
89
+ }
90
+ const matches = worldDoorMatcher([{ method: 'POST', path: door.path }]);
91
+ return { url: ready.url, stop,
92
+ connection: async (method, path, status, body) => {
93
+ if (status < 200 || status >= 300 || !matches(method, path)) return undefined;
94
+ const value = body && typeof body === 'object' ? (body as Record<string, unknown>)[definition.credentialField] : undefined;
95
+ await verify(value);
96
+ return { kind: definition.kind, field: definition.credentialField, authenticated: true };
97
+ },
72
98
  };
73
99
  }
@@ -3,15 +3,16 @@
3
3
  // text. What a restXml output structure's members are on the wire, so a journey asserts and captures them (`Contents.0.Key`)
4
4
  // and SHAPE checks them as it checks a JSON answer's. Attributes and namespaces are not members and are dropped. XML
5
5
  // cannot say a lone element is a list: one `<Contents>` reads as an object, two as a list. A root with no children is
6
- // its one member, by its name.
6
+ // its one member, by its name. When a resource schema is supplied, an empty root is a member only if the schema names it.
7
7
 
8
8
  const ENTITIES: Record<string, string> = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
9
9
  const decode = (s: string): string => s.replace(/&(#x[0-9a-f]+|#\d+|\w+);/gi, (m, e: string) => (e[0] === '#' ? String.fromCodePoint(e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : Number(e.slice(1))) : ENTITIES[e] ?? m));
10
10
 
11
11
  type Node = { name: string; children: Node[]; text: string };
12
12
 
13
- /** The root element's members, or undefined when the text is not one XML document. */
14
- export function xmlMembers(text: string): Record<string, unknown> | undefined {
13
+ /** The root element's members, or undefined when the text is not one XML document.
14
+ * A schema distinguishes an empty structure from an empty scalar member such as LocationConstraint. */
15
+ export function xmlMembers(text: string, rootFields?: ReadonlyArray<string>): Record<string, unknown> | undefined {
15
16
  const src = text.replace(/<\?[\s\S]*?\?>/g, '').replace(/<!--[\s\S]*?-->/g, '').replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_, c: string) => c.replace(/&/g, '&amp;').replace(/</g, '&lt;'));
16
17
  const stack: Node[] = [{ name: '', children: [], text: '' }];
17
18
  const tag = /<(\/?)([\w:.-]+)[^>]*?(\/?)>|([^<]+)/g;
@@ -39,6 +40,6 @@ export function xmlMembers(text: string): Record<string, unknown> | undefined {
39
40
  return out;
40
41
  };
41
42
  // a document whose root is a leaf (S3's `<LocationConstraint>auto</LocationConstraint>`) is that one member
42
- if (!root.children.length) return { [root.name]: value(root) };
43
+ if (!root.children.length) return !root.text.trim() && rootFields && !rootFields.includes(root.name) ? {} : { [root.name]: value(root) };
43
44
  return value(root) as Record<string, unknown>;
44
45
  }