@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
@@ -0,0 +1,118 @@
1
+ /** PostgreSQL's ISO timestamp text, retaining its six fractional digits and timezone offset. */
2
+ function timestamp(value, oid) {
3
+ if (value === 'infinity')
4
+ return 10n ** 30n;
5
+ if (value === '-infinity')
6
+ return -(10n ** 30n);
7
+ if (typeof value !== 'string')
8
+ return undefined;
9
+ const m = /^(\d{4,6})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?(Z|([+-])(\d{2})(?::?(\d{2}))?(?::?(\d{2}))?)?( BC)?$/.exec(value);
10
+ if (!m || (oid === 1184) !== (m[8] !== undefined))
11
+ return undefined;
12
+ const [year, month, day, hour, minute, second] = m.slice(1, 7).map(Number);
13
+ if (!year || !month || month > 12 || !day || hour > 23 || minute > 59 || second > 59)
14
+ return undefined;
15
+ const astronomicalYear = m[13] ? 1 - year : year;
16
+ const leap = astronomicalYear % 4 === 0 && (astronomicalYear % 100 !== 0 || astronomicalYear % 400 === 0);
17
+ if (day > [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1])
18
+ return undefined;
19
+ const zoneHour = Number(m[10] ?? 0), zoneMinute = Number(m[11] ?? 0), zoneSecond = Number(m[12] ?? 0);
20
+ if (zoneHour > 15 || zoneMinute > 59 || zoneSecond > 59)
21
+ return undefined;
22
+ const adjustedYear = astronomicalYear - (month <= 2 ? 1 : 0);
23
+ const era = Math.floor(adjustedYear / 400), yearOfEra = adjustedYear - era * 400;
24
+ const dayOfYear = Math.floor((153 * (month + (month > 2 ? -3 : 9)) + 2) / 5) + day - 1;
25
+ const days = era * 146097 + yearOfEra * 365 + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100) + dayOfYear - 719468;
26
+ const offset = (zoneHour * 3600 + zoneMinute * 60 + zoneSecond) * (m[9] === '-' ? -1 : 1);
27
+ return BigInt(days) * 86400000000n + BigInt(hour * 3600 + minute * 60 + second - offset) * 1000000n + BigInt((m[7] ?? '').padEnd(6, '0'));
28
+ }
29
+ /** A journey's declared native timestamp columns are present, valid and nondecreasing down the returned rows. */
30
+ export function timestampOrderFailures(body, timeFields, columns) {
31
+ if (!columns.length)
32
+ return [];
33
+ const rows = body && typeof body === 'object' ? body.rows : undefined;
34
+ if (!Array.isArray(rows))
35
+ return ['timestamp ordering requires SQL result rows'];
36
+ const failures = [];
37
+ for (const column of columns) {
38
+ const oid = timeFields[column];
39
+ if (oid !== 1114 && oid !== 1184) {
40
+ failures.push(`${column} is not a native timestamp column`);
41
+ continue;
42
+ }
43
+ let previous;
44
+ for (const [i, row] of rows.entries()) {
45
+ const at = timestamp(row && typeof row === 'object' ? row[column] : undefined, oid);
46
+ if (at === undefined)
47
+ failures.push(`rows.${i}.${column} is not a non-null native timestamp`);
48
+ else if (previous !== undefined && at < previous)
49
+ failures.push(`rows.${i}.${column} precedes the previous row's timestamp`);
50
+ previous = at;
51
+ }
52
+ }
53
+ return failures;
54
+ }
55
+ /** Compare received traces by their selected engine declarations, retaining every undeclared byte. */
56
+ export function compareReplays(first, second) {
57
+ const diagnostics = [];
58
+ if (!first.length)
59
+ diagnostics.push('the journey answered no steps');
60
+ if (first.length !== second.length)
61
+ diagnostics.push(`the replays answered ${first.length} and ${second.length} steps`);
62
+ const times = new Map();
63
+ for (let i = 0; i < Math.min(first.length, second.length); i++) {
64
+ const a = first[i], b = second[i];
65
+ const where = `step ${i + 1} (${a.name})`;
66
+ if (a.name !== b.name || a.path !== b.path || a.status !== b.status || JSON.stringify(a.sql) !== JSON.stringify(b.sql) || JSON.stringify(a.connection) !== JSON.stringify(b.connection)) {
67
+ diagnostics.push(`${where}: request, status or native engine provenance differs`);
68
+ continue;
69
+ }
70
+ const fields = a.sql?.clock === 'wall' ? a.sql.timeFields : {};
71
+ const same = (left, right, path) => {
72
+ if (path.length === 1 && a.connection?.kind === 'postgres' && a.connection.authenticated === true && path[0] === a.connection.field)
73
+ return typeof left === 'string' && typeof right === 'string';
74
+ const oid = path.length === 3 && path[0] === 'rows' ? fields[path[2]] : undefined;
75
+ if ((oid === 1114 || oid === 1184) && left !== null && right !== null) {
76
+ const x = timestamp(left, oid), y = timestamp(right, oid);
77
+ if (x === undefined || y === undefined) {
78
+ diagnostics.push(`${where}: ${path.join('.')} is not a valid native timestamp`);
79
+ return false;
80
+ }
81
+ const values = times.get(oid) ?? [];
82
+ values.push({ first: x, second: y, where: `${where}: ${path.join('.')}` });
83
+ times.set(oid, values);
84
+ return true;
85
+ }
86
+ if (Array.isArray(left) || Array.isArray(right))
87
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, n) => same(value, right[n], [...path, String(n)]));
88
+ if (left && right && typeof left === 'object' && typeof right === 'object') {
89
+ const keys = Object.keys(left);
90
+ return JSON.stringify(keys) === JSON.stringify(Object.keys(right)) && keys.every(key => same(left[key], right[key], [...path, key]));
91
+ }
92
+ return JSON.stringify(left) === JSON.stringify(right);
93
+ };
94
+ if (!same(a.body, b.body, []))
95
+ diagnostics.push(`${where}: response bytes differ`);
96
+ }
97
+ // Equal timestamps form one group: ties may split or coincide, while a strict order cannot reverse.
98
+ for (const values of times.values()) {
99
+ values.sort((a, b) => a.first < b.first ? -1 : a.first > b.first ? 1 : 0);
100
+ let greatest;
101
+ for (let i = 0; i < values.length;) {
102
+ let end = i + 1;
103
+ while (end < values.length && values[end].first === values[i].first)
104
+ end++;
105
+ let next = greatest;
106
+ for (let n = i; n < end; n++) {
107
+ const at = values[n];
108
+ if (greatest && at.second < greatest.value)
109
+ diagnostics.push(`${at.where}: timestamp order reverses relative to ${greatest.where}`);
110
+ if (!next || at.second > next.value)
111
+ next = { value: at.second, where: at.where };
112
+ }
113
+ greatest = next;
114
+ i = end;
115
+ }
116
+ }
117
+ return { equal: diagnostics.length === 0, diagnostics };
118
+ }
@@ -1,10 +1,10 @@
1
+ import { type ManagedDatabaseWalk } from './walk-database.js';
1
2
  export type ServedPack = {
2
3
  port: number;
3
4
  inspectPort?: number;
4
5
  wirePort?: number;
5
6
  stop: () => void | Promise<void>;
6
- managedDatabaseUrl?: () => Promise<string>;
7
- };
7
+ } & Omit<ManagedDatabaseWalk, 'stopDatabase'>;
8
8
  export declare function servePack(pack: string, options: {
9
9
  root?: string;
10
10
  scenarioPath?: string;
@@ -10,6 +10,19 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
10
10
  import { join } from 'node:path';
11
11
  import { startTwinDatabase } from "./walk-database.js";
12
12
  export async function servePack(pack, options) {
13
+ const previousBoot = process.env.VOLTER_WORLD_BOOT_ID;
14
+ process.env.VOLTER_WORLD_BOOT_ID = 'conformance';
15
+ try {
16
+ return await serve(pack, options);
17
+ }
18
+ finally {
19
+ if (previousBoot === undefined)
20
+ delete process.env.VOLTER_WORLD_BOOT_ID;
21
+ else
22
+ process.env.VOLTER_WORLD_BOOT_ID = previousBoot;
23
+ }
24
+ }
25
+ async function serve(pack, options) {
13
26
  const mod = await import(__rewriteRelativeImportExtension(join(pack, 'src', 'server.ts')));
14
27
  const create = Object.entries(mod).find(([name, value]) => /^create\w+TwinServer$/.test(name) && typeof value === 'function')?.[1];
15
28
  if (!create)
@@ -23,7 +36,12 @@ export async function servePack(pack, options) {
23
36
  server = await create({ port: 0, ...options, ...(database ? { database: database.url } : {}) });
24
37
  }
25
38
  catch (error) {
26
- await database?.stop();
39
+ try {
40
+ await database?.stop();
41
+ }
42
+ catch (cleanup) {
43
+ throw new AggregateError([error, cleanup], 'Pack startup and backing retirement failed');
44
+ }
27
45
  throw error;
28
46
  }
29
47
  if (!database)
@@ -33,6 +51,24 @@ export async function servePack(pack, options) {
33
51
  return {
34
52
  ...server,
35
53
  managedDatabaseUrl: async () => backing.url,
36
- stop: () => stopping ??= (async () => { await server.stop(); await backing.stop(); })(),
54
+ managedDatabaseClock: descriptor.managedDatabase.clock,
55
+ managedDatabaseConnection: backing.connection,
56
+ stop: () => stopping ??= (async () => {
57
+ const failures = [];
58
+ try {
59
+ await server.stop();
60
+ }
61
+ catch (error) {
62
+ failures.push(error);
63
+ }
64
+ try {
65
+ await backing.stop();
66
+ }
67
+ catch (error) {
68
+ failures.push(error);
69
+ }
70
+ if (failures.length)
71
+ throw new AggregateError(failures, 'Pack retirement failed');
72
+ })(),
37
73
  };
38
74
  }
@@ -15,14 +15,21 @@ export type ShapeOperation = {
15
15
  alternatives?: string[];
16
16
  };
17
17
  };
18
+ /** Every selected unit's vendor fields, including nested spec schemas, GraphQL and native error wires. */
19
+ export declare function vendorFieldsOf(pkg: string): Promise<Set<string>>;
20
+ /** Decode a vendor answer without rounding wire integers; XML reads as its root's members. */
21
+ export declare function decodeResponse(text: string, mediaType: string | undefined): unknown;
22
+ /** Private stored fields, except names the vendor owns, in any answer, event or frame. */
23
+ export declare function leaked(v: unknown, vendorFields: Set<string>, at?: string, unshaped?: boolean): string[];
18
24
  export type ShapeChecker = {
19
- /** The spec operation a request matches, if any. An RPC wire answers a method whatever the HTTP method. */
25
+ /** The spec operation the routed request matches, if any. */
20
26
  operationOf: (method: string, path: string, headers?: Record<string, string>) => {
21
27
  op: ShapeOperation;
22
28
  params: Record<string, string>;
29
+ lane?: string;
23
30
  } | undefined;
24
31
  /** A request's URL as the unit routes it (a virtual-hosted S3 request's bucket moved from its host into its path). */
25
- pathOf?: (path: string) => string;
32
+ pathOf: (method: string, path: string, headers?: Record<string, string>) => string;
26
33
  /** What in an answer the spec does not give the operation's resource (or its view). */
27
34
  failures: (op: ShapeOperation, body: unknown, mediaType?: string, accept?: string, status?: number, exchange?: {
28
35
  method: string;
@@ -30,14 +37,28 @@ export type ShapeChecker = {
30
37
  }) => string[];
31
38
  /** Whether a request pins an API version before the spec's, whose answers the spec does not describe (a Stripe
32
39
  * mobile SDK's `Stripe-Version: 2024-06-20`), by the version header the manifest declares. */
33
- pinsOlder: (headers: Record<string, string> | undefined) => boolean;
40
+ pinsOlder: (headers: Record<string, string> | undefined, op: ShapeOperation) => boolean;
41
+ };
42
+ /** The same response rules apply to a life's requests and the unscripted surface sweep. */
43
+ export declare function responseProblems(checker: ShapeChecker | undefined, schemaFields: Set<string>, exchange: {
44
+ method: string;
45
+ path: string;
46
+ requestHeaders: Record<string, string>;
47
+ graphqlBody: unknown;
48
+ status: number;
49
+ mediaType: string | undefined;
50
+ body: unknown;
51
+ text: string;
52
+ }): {
53
+ shape: string[];
54
+ leaks: string[];
34
55
  };
35
56
  /** The directories under a pack that are lanes: a derived unit with its own generated surface
36
57
  * (`../twin-packs-p3/aws/secretsmanager/`, `../twin-packs-p3/planetscale/api/`). */
37
58
  export declare function lanesOf(pkg: string): string[];
38
59
  /** A derived pack's shape checker, or undefined for a pack with no generated surface. A pack's walk reaches its lanes'
39
60
  * operations through the pack's router (a psdb life creates its database's passwords through the management API), so
40
- * a request is matched against the pack's own surface, then each lane's, and judged by the surface it matched. */
61
+ * the core gateway selects the owning lane before its surface is matched and its answer judged. */
41
62
  export declare function shapeChecker(pkg: string): Promise<ShapeChecker | undefined>;
42
63
  /** Where one connection stands for the check: the command whose intermediate reply the next lines answer. */
43
64
  export type LineConnection = {
@@ -10,45 +10,151 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
10
10
  // candidate only the values the spec lists (docs/contributing/architecture.md, "Protocol 3",
11
11
  // State and The report), and every field the spec requires. The walk (behavior-journey.ts) runs it on every successful step of a derived
12
12
  // pack, so a walk an author iterates with fails where the score would; score-pack reads it from there.
13
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
14
- import { join } from 'node:path';
15
- import { compileSurface, matchOperation } from '@volter/world-core/runtime';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { basename, join } from 'node:path';
15
+ import { compileSurface, hostOf, laneSelector, matchOperation, slashesMerged, unitRequestRouter, worldDoorMatcher } from '@volter/world-core/runtime';
16
+ import { parseExactJson } from '@volter/world-core/exact-json';
17
+ import { hostRules } from '@volter/world-core/host-routing';
16
18
  import { binaryMedia } from "../spec-ir.js";
19
+ import { schemaFieldNames } from "../spec-documents.js";
20
+ import { unitsIn } from "../lanes.js";
21
+ import { xmlMembers } from "./xml-view.js";
22
+ import { parse, visit } from 'graphql';
17
23
  /** Whether a resource is one of the objects `said` names (a union's `object` lists every alternative's). */
18
24
  const isObject = (r, said) => typeof said === 'string' && (r.fields.find((f) => f.name === 'object')?.enum ?? []).includes(said);
25
+ /** Field names owned by the vendor's resource schemas and native error templates. */
26
+ function vendorFieldNames(surface, ...errors) {
27
+ const fields = new Set((surface?.resources ?? []).flatMap((resource) => resource.fields.map((field) => typeof field === 'string' ? field : field.name)));
28
+ const visit = (value) => {
29
+ if (Array.isArray(value)) {
30
+ for (const item of value)
31
+ visit(item);
32
+ }
33
+ else if (value && typeof value === 'object')
34
+ for (const [name, item] of Object.entries(value)) {
35
+ fields.add(name);
36
+ visit(item);
37
+ }
38
+ };
39
+ for (const error of errors)
40
+ visit(error);
41
+ return fields;
42
+ }
43
+ /** Every selected unit's vendor fields, including nested spec schemas, GraphQL and native error wires. */
44
+ export async function vendorFieldsOf(pkg) {
45
+ const fields = new Set();
46
+ for (const unit of unitsIn(pkg)) {
47
+ const manifestPath = join(unit, 'src', 'manifest.ts');
48
+ const { manifest } = existsSync(manifestPath) ? await import(__rewriteRelativeImportExtension(manifestPath)) : {};
49
+ for (const name of ['surface.gen.json', 'graphql.gen.json']) {
50
+ const path = join(unit, 'src', 'generated', name);
51
+ if (existsSync(path))
52
+ for (const field of vendorFieldNames(JSON.parse(readFileSync(path, 'utf8'))))
53
+ fields.add(field);
54
+ }
55
+ for (const field of schemaFieldNames(join(unit, 'spec')))
56
+ fields.add(field);
57
+ for (const field of vendorFieldNames(undefined, manifest?.error, ...(manifest?.auth?.wires ?? []).map((wire) => wire.error)))
58
+ fields.add(field);
59
+ }
60
+ return fields;
61
+ }
62
+ /** Decode a vendor answer without rounding wire integers; XML reads as its root's members. */
63
+ export function decodeResponse(text, mediaType) {
64
+ let body;
65
+ try {
66
+ body = parseExactJson(text);
67
+ }
68
+ catch {
69
+ body = text;
70
+ }
71
+ return typeof body === 'string' && /xml/.test(mediaType ?? '') ? xmlMembers(text) ?? text : body;
72
+ }
73
+ /** Private stored fields, except names the vendor owns, in any answer, event or frame. */
74
+ export function leaked(v, vendorFields, at = '', unshaped = false) {
75
+ if (Array.isArray(v))
76
+ return v.flatMap((x, i) => leaked(x, vendorFields, `${at}[${i}]`, unshaped)).slice(0, 5);
77
+ if (!v || typeof v !== 'object')
78
+ return [];
79
+ 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);
80
+ }
81
+ /** Vendor schema fields plus the response keys this GraphQL request selects, including aliases. */
82
+ function responseFieldNames(schemaFields, graphqlBody) {
83
+ const fields = new Set(schemaFields);
84
+ if (graphqlBody && typeof graphqlBody === 'object' && typeof graphqlBody.query === 'string') {
85
+ try {
86
+ visit(parse(graphqlBody.query), { Field: (field) => { fields.add(field.alias?.value ?? field.name.value); } });
87
+ }
88
+ catch { /* malformed documents have no selected response keys */ }
89
+ }
90
+ return fields;
91
+ }
92
+ /** The same response rules apply to a life's requests and the unscripted surface sweep. */
93
+ export function responseProblems(checker, schemaFields, exchange) {
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
+ }
19
101
  /** The directories under a pack that are lanes: a derived unit with its own generated surface
20
102
  * (`../twin-packs-p3/aws/secretsmanager/`, `../twin-packs-p3/planetscale/api/`). */
21
103
  export function lanesOf(pkg) {
22
- if (!existsSync(pkg))
23
- return [];
24
- return readdirSync(pkg, { withFileTypes: true })
25
- .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')))
26
- .map((e) => join(pkg, e.name));
104
+ return unitsIn(pkg).filter((unit) => unit !== pkg && existsSync(join(unit, 'src', 'generated', 'surface.gen.json')));
27
105
  }
28
106
  /** A derived pack's shape checker, or undefined for a pack with no generated surface. A pack's walk reaches its lanes'
29
107
  * operations through the pack's router (a psdb life creates its database's passwords through the management API), so
30
- * a request is matched against the pack's own surface, then each lane's, and judged by the surface it matched. */
108
+ * the core gateway selects the owning lane before its surface is matched and its answer judged. */
31
109
  export async function shapeChecker(pkg) {
32
- const units = (await Promise.all([pkg, ...lanesOf(pkg)].map((dir) => unitChecker(dir)))).filter((c) => !!c);
33
- if (!units.length)
110
+ const units = await Promise.all(unitsIn(pkg).map(async (dir) => {
111
+ const path = join(dir, 'src', 'manifest.ts');
112
+ const { manifest } = existsSync(path) ? await import(__rewriteRelativeImportExtension(path)) : {};
113
+ return { dir, manifest, response: unitResponseOwner(dir, manifest), door: worldDoorMatcher(manifest?.doors ?? []) };
114
+ }));
115
+ if (!units.some((unit) => unit.response?.checker))
34
116
  return undefined;
35
- if (units.length === 1)
36
- return units[0];
117
+ const root = units[0];
118
+ const arrival = (method, path, headers) => {
119
+ const request = new Request(new URL(path, 'http://twin.local'), { method, headers });
120
+ return root.manifest?.mergeSlashes ? slashesMerged(request) : request;
121
+ };
122
+ const selected = root.manifest?.lanes ? laneSelector(root.manifest.lanes, Object.fromEntries(units.slice(1).map((unit) => [basename(unit.dir), {
123
+ manifest: unit.manifest ?? {}, fetch: { serves: unit.response?.checker?.serves,
124
+ 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()) },
125
+ }])), root.response?.checker?.serves) : undefined;
37
126
  const owner = new WeakMap();
38
- return {
39
- pinsOlder: (headers) => units[0].pinsOlder(headers),
40
- pathOf: (path) => units.find((u) => u.operationOf('GET', path) || u.operationOf('PUT', path))?.pathOf?.(path) ?? path,
41
- operationOf: (method, path, headers) => {
42
- for (const u of units) {
43
- const hit = u.operationOf(method, path, headers);
44
- if (hit) {
45
- owner.set(hit.op, u);
46
- return hit;
47
- }
48
- }
127
+ const selectedRequest = (method, path, headers) => {
128
+ const request = arrival(method, path, headers);
129
+ const source = root.manifest?.mergeSlashes ? request.url : path;
130
+ const rootDoor = root.door(request.method, new URL(request.url).pathname);
131
+ const name = rootDoor ? undefined : selected?.(request);
132
+ const candidates = rootDoor ? [root] : selected ? [name === undefined ? root : units.find((unit) => basename(unit.dir) === name)] : units;
133
+ for (const unit of candidates) {
134
+ if (!unit)
135
+ continue;
136
+ const routed = unit.response?.pathOf(method, source, headers) ?? request.url;
137
+ if (rootDoor || unit.door(method, new URL(routed).pathname))
138
+ return { path: routed };
139
+ const checker = unit.response?.checker;
140
+ const hit = checker?.operationOf(method, source, headers);
141
+ if (hit)
142
+ return { path: routed, checker, hit: unit === root ? hit : { ...hit, lane: basename(unit.dir) } };
143
+ }
144
+ return { path: candidates[0]?.response?.pathOf(method, source, headers) ?? request.url };
145
+ };
146
+ const operationOf = (method, path, headers) => {
147
+ const selected = selectedRequest(method, path, headers);
148
+ if (!selected.hit)
49
149
  return undefined;
50
- },
51
- failures: (op, body, mediaType, accept, status, exchange) => (owner.get(op) ?? units[0]).failures(op, body, mediaType, accept, status, exchange),
150
+ owner.set(selected.hit.op, selected.checker);
151
+ return selected.hit;
152
+ };
153
+ return {
154
+ pinsOlder: (headers, op) => owner.get(op).pinsOlder(headers, op),
155
+ pathOf: (method, path, headers) => selectedRequest(method, path, headers).path,
156
+ operationOf,
157
+ failures: (op, body, mediaType, accept, status, exchange) => owner.get(op).failures(op, body, mediaType, accept, status, exchange),
52
158
  };
53
159
  }
54
160
  /** Accept's most specific media range decides its quality; an explicit q=0 overrides a wildcard. */
@@ -67,53 +173,54 @@ function acceptsMedia(accept, media, defaultMedia) {
67
173
  }).sort((a, b) => b.specificity - a.specificity || b.q - a.q);
68
174
  return (matches[0]?.q ?? 0) > 0;
69
175
  }
70
- async function unitChecker(pkg) {
176
+ function unitResponseOwner(pkg, manifest) {
71
177
  const surfacePath = join(pkg, 'src', 'generated', 'surface.gen.json');
72
178
  if (!existsSync(surfacePath))
73
179
  return undefined;
74
- const surface = JSON.parse(readFileSync(surfacePath, 'utf8'));
75
- // a line protocol's surface is its commands' replies (lineShape); a command table's commands ride in a request's body
76
- // (Upstash's REST envelope), which a check by method and path cannot see: its replies are not shape-checked yet, and
77
- // its examples assert what each answers
78
- if (['command-reply', 'command-table'].includes(String(surface.format)))
79
- return undefined;
80
- const manifestModule = existsSync(join(pkg, 'src', 'manifest.ts')) ? (await import(__rewriteRelativeImportExtension(join(pkg, 'src', 'manifest.ts')))) : {};
81
- // 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
82
- // addressing) puts it in front of the path
83
- const hostPath = manifestModule.manifest?.pathFromHost ? new RegExp(manifestModule.manifest.pathFromHost, 'i') : undefined;
84
- const pathStyle = (u) => {
85
- const url = new URL(u);
86
- const label = hostPath?.exec(url.hostname)?.[1];
87
- return label ? `${url.protocol}//${url.host}/${label}${url.pathname === '/' ? '' : url.pathname}${url.search}` : u;
88
- };
180
+ const source = JSON.parse(readFileSync(surfacePath, 'utf8'));
181
+ const surface = { ...source, operations: source.operations ?? [] };
89
182
  // a lane is routed by host ("Other wires: lanes"): one whose manifest names the hosts it serves is judged on those
90
183
  // 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
91
- const hosts = manifestModule.manifest?.hosts;
92
- const served = (hostname) => !hosts || hosts.some((h) => (h.host ? hostname === h.host : !!h.suffix && hostname.endsWith(h.suffix)));
93
- const versionHeader = manifestModule.manifest?.version?.header.toLowerCase();
184
+ const hosts = manifest?.hosts;
185
+ const claims = hostRules(hosts);
186
+ const versionHeader = manifest?.version?.header.toLowerCase();
94
187
  const specDate = /^\d{4}-\d{2}-\d{2}/.exec(String(surface.version))?.[0];
95
188
  // the path parameters whose values hold slashes (a git ref, a file path): the surface's own `spanning`, and the
96
189
  // manifest's where the spec cannot say
97
- const spans = [...(surface.spanning ?? []), ...(manifestModule.manifest?.spanning ?? [])];
98
- const routes = compileSurface({ ...surface, ...(spans.length ? { spanning: spans } : {}) });
190
+ const spans = [...(surface.spanning ?? []), ...(manifest?.spanning ?? [])];
191
+ const effectiveSurface = { ...surface, ...(spans.length ? { spanning: spans } : {}) };
192
+ const routes = compileSurface(effectiveSurface);
193
+ const routed = unitRequestRouter(manifest ?? {}, effectiveSurface, routes);
194
+ const door = worldDoorMatcher(manifest?.doors ?? []);
195
+ const requestAt = (method, path, headers) => {
196
+ const request = new Request(new URL(path, 'http://twin.local'), { method, headers });
197
+ return routed(manifest?.mergeSlashes ? slashesMerged(request) : request);
198
+ };
199
+ const pathOf = (method, path, headers) => requestAt(method, path, headers).url;
200
+ // Command surfaces retain routing ownership; their replies are checked through their native protocol.
201
+ if (['command-reply', 'command-table'].includes(String(surface.format)))
202
+ return { pathOf };
99
203
  const resources = new Map(surface.resources.map((r) => [r.name, r]));
100
- return {
204
+ const checker = {
205
+ serves: (method, path) => Boolean(matchOperation(routes, method, path)),
101
206
  pinsOlder: (headers) => {
102
207
  if (!versionHeader || !specDate || !headers)
103
208
  return false;
104
209
  const pinned = Object.entries(headers).find(([k]) => k.toLowerCase() === versionHeader)?.[1];
105
210
  return typeof pinned === 'string' && pinned.slice(0, 10) < specDate;
106
211
  },
107
- pathOf: (path) => (/^https?:\/\//.test(path) ? pathStyle(path) : path),
108
212
  operationOf: (method, path, headers) => {
109
- const url = new URL(/^https?:\/\//.test(path) ? pathStyle(path) : path, 'http://twin.local');
110
- if (/^https?:\/\//.test(path) && !served(new URL(path).hostname))
111
- return undefined;
213
+ const request = requestAt(method, path, headers);
214
+ const url = new URL(request.url);
215
+ if (/^https?:\/\//.test(path)) {
216
+ const original = new URL(path);
217
+ if (hosts && !claims.takes(hostOf(request), original.pathname))
218
+ return undefined;
219
+ }
112
220
  // 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))
221
+ if (door(request.method, url.pathname))
114
222
  return undefined;
115
- const h = new Headers(headers ?? {});
116
- const hit = matchOperation(routes, method, url.pathname, url.searchParams, h) ?? [...routes.keys()].map((m) => matchOperation(routes, m, url.pathname, url.searchParams, h)).find(Boolean);
223
+ const hit = matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers);
117
224
  return hit ? { op: hit.operation, params: hit.params } : undefined;
118
225
  },
119
226
  failures: (op, body, mediaType, accept, status = op.successStatus ?? 200, exchange) => {
@@ -131,6 +238,8 @@ async function unitChecker(pkg) {
131
238
  const primary = op.answers ? resources.get(op.answers.resource) : undefined;
132
239
  if (!primary)
133
240
  return [];
241
+ if (exchange && /xml/.test(media ?? ''))
242
+ body = xmlMembers(exchange.responseText, primary.fields.map((field) => field.name)) ?? body;
134
243
  if (!body || typeof body !== 'object')
135
244
  return binary ? [`the answer is not the operation's ${primary.name} resource`] : [];
136
245
  // an answer that may be one of several resources (a card or a bank account) is judged as the one it says it is
@@ -149,7 +258,7 @@ async function unitChecker(pkg) {
149
258
  const known = new Map([...r.fields.map((f) => [f.name, f]), ...(op.answers.extends ?? []).map((name) => [name, {}])]);
150
259
  for (const [k, v] of Object.entries(item)) {
151
260
  const f = known.get(k);
152
- const local = manifestModule.manifest?.worldFields?.[r.name]?.[k];
261
+ const local = manifest?.worldFields?.[r.name]?.[k];
153
262
  if (local) {
154
263
  if (!k.startsWith('twin_') || !local.why?.trim() || f)
155
264
  found.push(`${r.name}.${k}: a World field must have a twin_ name and reason and must not replace a vendor field`);
@@ -181,6 +290,7 @@ async function unitChecker(pkg) {
181
290
  return [...new Set(out)];
182
291
  },
183
292
  };
293
+ return { pathOf, checker };
184
294
  }
185
295
  /** The SHAPE of a line protocol (scripts/derive-pack.ts from an RFC's command-reply table): each reply a
186
296
  * code the spec allows the command it answers, or one any command may answer. A line after an
@@ -1,13 +1,18 @@
1
+ import { type ManagedDatabaseWalk } from './walk-database.js';
1
2
  import { type TwinStream } from '@volter/world-core/runtime';
2
3
  import type { PackFetchOptions } from '@volter/world-core';
3
4
  type ApplicationStandIn = NonNullable<PackFetchOptions['application']>;
4
5
  /** 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
5
6
  * life's application answers stand in for the application the vendor asks (`application`, this fetch's alone). */
6
- export type VendorFetchFactory = (root: string, options?: {
7
+ export type VendorFetchFactory = ((root: string, options?: {
7
8
  scenarioPath?: string;
8
9
  application?: ApplicationStandIn;
9
- }) => (request: Request) => Response | Promise<Response>;
10
+ }) => ((request: Request) => Response | Promise<Response>) & ManagedDatabaseWalk) & {
11
+ managedDatabaseDoor?: string;
12
+ };
10
13
  export declare function vendorFetch(twinDir: string, vendor: string, dependenciesFrom?: string): Promise<VendorFetchFactory | undefined>;
14
+ /** The selected pack's same fetch and native backing for in-process and browser transports. */
15
+ export declare function packFetch(directory: string): Promise<VendorFetchFactory | undefined>;
11
16
  /** Named views of an answer: what a journey step's `view` asserts against. The kit's own: `jwt:<field>`, the payload of the
12
17
  * JSON Web Token at that field of the answer, decoded as the application verifying it reads it (a session token's
13
18
  * claims). A pack's own (`answerViews` in its index) beside them. */
@@ -16,6 +16,10 @@ export async function vendorFetch(twinDir, vendor, dependenciesFrom = twinDir) {
16
16
  const directory = walkPackDirectory(twinDir, vendor, dependenciesFrom);
17
17
  if (!directory)
18
18
  return undefined;
19
+ return packFetch(directory);
20
+ }
21
+ /** The selected pack's same fetch and native backing for in-process and browser transports. */
22
+ export async function packFetch(directory) {
19
23
  let mod;
20
24
  try {
21
25
  mod = (await import(__rewriteRelativeImportExtension(join(directory, 'src', 'index.ts'))));
@@ -31,9 +35,10 @@ export async function vendorFetch(twinDir, vendor, dependenciesFrom = twinDir) {
31
35
  const descriptor = mod.pack;
32
36
  if (!descriptor?.managedDatabase)
33
37
  return (root, options = {}) => factory({ root, ...options });
38
+ const managedDatabaseClock = descriptor.managedDatabase.clock;
34
39
  // Each walked twin owns a fresh SQL product through the ordinary selected-twin lifecycle.
35
40
  // Native SQL uses the descriptor's explicit wall clock; kernel World time is not claimed for it.
36
- return (root, options = {}) => {
41
+ const backed = (root, options = {}) => {
37
42
  const bare = factory({ root, ...options });
38
43
  let bound;
39
44
  let host;
@@ -41,11 +46,17 @@ export async function vendorFetch(twinDir, vendor, dependenciesFrom = twinDir) {
41
46
  const served = (request) => bind().then((fetch) => { return fetch(request); });
42
47
  // The twin's SQL product, for what an application sends over its own wire (journey-kit.ts).
43
48
  const managedDatabaseUrl = () => bind().then(() => { return host.url; });
49
+ const managedDatabaseConnection = async (...args) => {
50
+ await bind();
51
+ return host.connection(...args);
52
+ };
44
53
  // The walk's end retires the selected twin's private engine through its World.
45
54
  const stopDatabase = async () => { if (bound === undefined)
46
55
  return; await bound.catch(() => undefined); await host?.stop(); };
47
- return Object.assign(served, bare, { managedDatabaseUrl, stopDatabase });
56
+ return Object.assign(served, bare, { managedDatabaseUrl, managedDatabaseClock, managedDatabaseConnection, stopDatabase });
48
57
  };
58
+ backed.managedDatabaseDoor = descriptor.credentialDoor?.path;
59
+ return backed;
49
60
  }
50
61
  const jwtView = (field) => (body) => {
51
62
  const token = body?.[field];
@@ -1,6 +1,14 @@
1
- import type { TwinPack } from '@volter/world-core/runtime';
1
+ import { type TwinPack } from '@volter/world-core/runtime';
2
+ import type { ReplayStep } from './replay.js';
2
3
  export type TwinDatabase = {
3
4
  url: string;
5
+ connection: (method: string, path: string, status: number, body: unknown) => Promise<ReplayStep['connection']>;
4
6
  stop: () => Promise<void>;
5
7
  };
8
+ export type ManagedDatabaseWalk = {
9
+ managedDatabaseUrl?: () => Promise<string>;
10
+ managedDatabaseClock?: 'wall';
11
+ managedDatabaseConnection?: TwinDatabase['connection'];
12
+ stopDatabase?: () => Promise<void>;
13
+ };
6
14
  export declare function startTwinDatabase(pack: string, descriptor: TwinPack): Promise<TwinDatabase>;