@volter/twin-standard 1.0.30 → 1.0.31

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 (149) hide show
  1. package/dist/src/archive-source.d.ts +13 -0
  2. package/dist/src/archive-source.js +176 -0
  3. package/dist/src/check-sources.d.ts +19 -2
  4. package/dist/src/check-sources.js +95 -37
  5. package/dist/src/cli.js +52 -9
  6. package/dist/src/clients/ai-gateway.d.ts +1 -0
  7. package/dist/src/clients/ai-gateway.js +3 -0
  8. package/dist/src/clients/anthropic-messages.d.ts +10 -0
  9. package/dist/src/clients/anthropic-messages.js +52 -0
  10. package/dist/src/clients/anthropic.d.ts +1 -0
  11. package/dist/src/clients/anthropic.js +3 -0
  12. package/dist/src/clients/aws.d.ts +2 -0
  13. package/dist/src/clients/aws.js +137 -0
  14. package/dist/src/clients/cerebras.d.ts +1 -0
  15. package/dist/src/clients/cerebras.js +3 -0
  16. package/dist/src/clients/clerk.d.ts +2 -0
  17. package/dist/src/clients/clerk.js +99 -0
  18. package/dist/src/clients/cloudflare.d.ts +2 -0
  19. package/dist/src/clients/cloudflare.js +86 -0
  20. package/dist/src/clients/elevenlabs.d.ts +2 -0
  21. package/dist/src/clients/elevenlabs.js +39 -0
  22. package/dist/src/clients/gemini.d.ts +2 -0
  23. package/dist/src/clients/gemini.js +92 -0
  24. package/dist/src/clients/github.d.ts +2 -0
  25. package/dist/src/clients/github.js +71 -0
  26. package/dist/src/clients/groq.d.ts +1 -0
  27. package/dist/src/clients/groq.js +3 -0
  28. package/dist/src/clients/harness.d.ts +36 -0
  29. package/dist/src/clients/harness.js +74 -0
  30. package/dist/src/clients/merge.d.ts +1 -0
  31. package/dist/src/clients/merge.js +8 -0
  32. package/dist/src/clients/openai-compatible.d.ts +9 -0
  33. package/dist/src/clients/openai-compatible.js +51 -0
  34. package/dist/src/clients/openai.d.ts +2 -0
  35. package/dist/src/clients/openai.js +65 -0
  36. package/dist/src/clients/openrouter.d.ts +1 -0
  37. package/dist/src/clients/openrouter.js +3 -0
  38. package/dist/src/clients/perplexity.d.ts +1 -0
  39. package/dist/src/clients/perplexity.js +3 -0
  40. package/dist/src/clients/planetscale.d.ts +2 -0
  41. package/dist/src/clients/planetscale.js +109 -0
  42. package/dist/src/clients/posthog.d.ts +2 -0
  43. package/dist/src/clients/posthog.js +145 -0
  44. package/dist/src/clients/run.d.ts +1 -0
  45. package/dist/src/clients/run.js +25 -0
  46. package/dist/src/clients/sentry.d.ts +2 -0
  47. package/dist/src/clients/sentry.js +167 -0
  48. package/dist/src/clients/slack.d.ts +2 -0
  49. package/dist/src/clients/slack.js +107 -0
  50. package/dist/src/clients/stripe.d.ts +2 -0
  51. package/dist/src/clients/stripe.js +161 -0
  52. package/dist/src/clients/tinybird.d.ts +2 -0
  53. package/dist/src/clients/tinybird.js +71 -0
  54. package/dist/src/clients/tunnel.d.ts +2 -0
  55. package/dist/src/clients/tunnel.js +99 -0
  56. package/dist/src/clients/upstash.d.ts +2 -0
  57. package/dist/src/clients/upstash.js +143 -0
  58. package/dist/src/clients/volteridentity.d.ts +2 -0
  59. package/dist/src/clients/volteridentity.js +128 -0
  60. package/dist/src/clients/xai.d.ts +2 -0
  61. package/dist/src/clients/xai.js +23 -0
  62. package/dist/src/conformance.d.ts +33 -0
  63. package/dist/src/conformance.js +520 -0
  64. package/dist/src/derive.js +113 -22
  65. package/dist/src/gate.js +22 -16
  66. package/dist/src/index.d.ts +5 -3
  67. package/dist/src/index.js +5 -3
  68. package/dist/src/lanes.d.ts +2 -0
  69. package/dist/src/lanes.js +7 -0
  70. package/dist/src/p3-rules.js +0 -2
  71. package/dist/src/protocol-3.d.ts +1 -0
  72. package/dist/src/protocol-3.js +76 -30
  73. package/dist/src/published.d.ts +6 -0
  74. package/dist/src/published.js +65 -11
  75. package/dist/src/spec-documents.js +2 -1
  76. package/dist/src/spec-ir-client.d.ts +5 -1
  77. package/dist/src/spec-ir-client.js +12 -2
  78. package/dist/src/spec-ir.d.ts +7 -2
  79. package/dist/src/spec-ir.js +42 -10
  80. package/dist/src/verdict.d.ts +14 -0
  81. package/dist/src/verdict.js +30 -0
  82. package/dist/src/walk/behavior-journey.d.ts +47 -0
  83. package/dist/src/walk/behavior-journey.js +1138 -0
  84. package/dist/src/walk/git-client.d.ts +26 -0
  85. package/dist/src/walk/git-client.js +139 -0
  86. package/dist/src/walk/journey-kit.d.ts +445 -0
  87. package/dist/src/walk/journey-kit.js +743 -0
  88. package/dist/src/walk/shape.d.ts +54 -0
  89. package/dist/src/walk/shape.js +210 -0
  90. package/dist/src/walk/sse-example.d.ts +2 -0
  91. package/dist/src/walk/sse-example.js +32 -0
  92. package/dist/src/walk/vendor-fetch.d.ts +20 -0
  93. package/dist/src/walk/vendor-fetch.js +92 -0
  94. package/dist/src/walk/walk-database.d.ts +12 -0
  95. package/dist/src/walk/walk-database.js +96 -0
  96. package/dist/src/walk/xml-view.d.ts +2 -0
  97. package/dist/src/walk/xml-view.js +55 -0
  98. package/package.json +39 -3
  99. package/src/archive-source.ts +141 -0
  100. package/src/check-sources.ts +83 -33
  101. package/src/cli.ts +40 -8
  102. package/src/clients/ai-gateway.ts +4 -0
  103. package/src/clients/anthropic-messages.ts +60 -0
  104. package/src/clients/anthropic.ts +4 -0
  105. package/src/clients/aws.ts +119 -0
  106. package/src/clients/cerebras.ts +4 -0
  107. package/src/clients/clerk.ts +97 -0
  108. package/src/clients/cloudflare.ts +80 -0
  109. package/src/clients/elevenlabs.ts +38 -0
  110. package/src/clients/gemini.ts +89 -0
  111. package/src/clients/github.ts +67 -0
  112. package/src/clients/groq.ts +4 -0
  113. package/src/clients/harness.ts +101 -0
  114. package/src/clients/merge.ts +9 -0
  115. package/src/clients/openai-compatible.ts +59 -0
  116. package/src/clients/openai.ts +66 -0
  117. package/src/clients/openrouter.ts +4 -0
  118. package/src/clients/perplexity.ts +4 -0
  119. package/src/clients/planetscale.ts +98 -0
  120. package/src/clients/posthog.ts +132 -0
  121. package/src/clients/run.ts +18 -0
  122. package/src/clients/sentry.ts +157 -0
  123. package/src/clients/slack.ts +98 -0
  124. package/src/clients/stripe.ts +129 -0
  125. package/src/clients/tinybird.ts +69 -0
  126. package/src/clients/tunnel.ts +79 -0
  127. package/src/clients/upstash.ts +137 -0
  128. package/src/clients/volteridentity.ts +108 -0
  129. package/src/clients/xai.ts +25 -0
  130. package/src/conformance.ts +446 -0
  131. package/src/derive.ts +87 -16
  132. package/src/gate.ts +20 -14
  133. package/src/index.ts +5 -3
  134. package/src/lanes.ts +7 -0
  135. package/src/p3-rules.ts +0 -2
  136. package/src/protocol-3.ts +71 -27
  137. package/src/published.ts +59 -11
  138. package/src/spec-documents.ts +2 -1
  139. package/src/spec-ir-client.ts +15 -4
  140. package/src/spec-ir.ts +43 -5
  141. package/src/verdict.ts +28 -0
  142. package/src/walk/behavior-journey.ts +806 -0
  143. package/src/walk/git-client.ts +138 -0
  144. package/src/walk/journey-kit.ts +858 -0
  145. package/src/walk/shape.ts +208 -0
  146. package/src/walk/sse-example.ts +25 -0
  147. package/src/walk/vendor-fetch.ts +80 -0
  148. package/src/walk/walk-database.ts +97 -0
  149. package/src/walk/xml-view.ts +44 -0
@@ -0,0 +1,54 @@
1
+ export type ShapeOperation = {
2
+ id: string;
3
+ method: string;
4
+ path: string;
5
+ class: string;
6
+ resource?: string;
7
+ successStatus?: number;
8
+ responseMedia?: Record<string, string[]>;
9
+ defaultResponseMedia?: Record<string, string>;
10
+ answers?: {
11
+ resource: string;
12
+ list: boolean;
13
+ key?: string;
14
+ extends?: string[];
15
+ alternatives?: string[];
16
+ };
17
+ };
18
+ export type ShapeChecker = {
19
+ /** The spec operation a request matches, if any. An RPC wire answers a method whatever the HTTP method. */
20
+ operationOf: (method: string, path: string, headers?: Record<string, string>) => {
21
+ op: ShapeOperation;
22
+ params: Record<string, string>;
23
+ } | undefined;
24
+ /** 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;
26
+ /** What in an answer the spec does not give the operation's resource (or its view). */
27
+ failures: (op: ShapeOperation, body: unknown, mediaType?: string, accept?: string, status?: number, exchange?: {
28
+ method: string;
29
+ responseText: string;
30
+ }) => string[];
31
+ /** Whether a request pins an API version before the spec's, whose answers the spec does not describe (a Stripe
32
+ * mobile SDK's `Stripe-Version: 2024-06-20`), by the version header the manifest declares. */
33
+ pinsOlder: (headers: Record<string, string> | undefined) => boolean;
34
+ };
35
+ /** The directories under a pack that are lanes: a derived unit with its own generated surface
36
+ * (`../twin-packs-p3/aws/secretsmanager/`, `../twin-packs-p3/planetscale/api/`). */
37
+ export declare function lanesOf(pkg: string): string[];
38
+ /** A derived pack's shape checker, or undefined for a pack with no generated surface. A pack's walk reaches its lanes'
39
+ * 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. */
41
+ export declare function shapeChecker(pkg: string): Promise<ShapeChecker | undefined>;
42
+ /** Where one connection stands for the check: the command whose intermediate reply the next lines answer. */
43
+ export type LineConnection = {
44
+ pending?: string;
45
+ };
46
+ export type LineShape = {
47
+ /** What is wrong with the replies one line drew (null: the connection's opening), as the spec's
48
+ * command-reply table reads them; moves `conn` past an intermediate reply. */
49
+ check: (conn: LineConnection, line: string | null, codes: number[]) => string[];
50
+ };
51
+ /** The SHAPE of a line protocol (scripts/derive-pack.ts from an RFC's command-reply table): each reply a
52
+ * code the spec allows the command it answers, or one any command may answer. A line after an
53
+ * intermediate reply (DATA's payload, a SASL response) is answered as that command's `then`. */
54
+ export declare function lineShape(pkg: string): LineShape | undefined;
@@ -0,0 +1,210 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // SHAPE — an answer carries only the fields the spec gives what the operation answers, and a state
10
+ // candidate only the values the spec lists (docs/contributing/architecture.md, "Protocol 3",
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
+ // 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';
16
+ import { binaryMedia } from "../spec-ir.js";
17
+ /** Whether a resource is one of the objects `said` names (a union's `object` lists every alternative's). */
18
+ const isObject = (r, said) => typeof said === 'string' && (r.fields.find((f) => f.name === 'object')?.enum ?? []).includes(said);
19
+ /** The directories under a pack that are lanes: a derived unit with its own generated surface
20
+ * (`../twin-packs-p3/aws/secretsmanager/`, `../twin-packs-p3/planetscale/api/`). */
21
+ 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));
27
+ }
28
+ /** A derived pack's shape checker, or undefined for a pack with no generated surface. A pack's walk reaches its lanes'
29
+ * 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. */
31
+ 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)
34
+ return undefined;
35
+ if (units.length === 1)
36
+ return units[0];
37
+ 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
+ }
49
+ return undefined;
50
+ },
51
+ failures: (op, body, mediaType, accept, status, exchange) => (owner.get(op) ?? units[0]).failures(op, body, mediaType, accept, status, exchange),
52
+ };
53
+ }
54
+ /** Accept's most specific media range decides its quality; an explicit q=0 overrides a wildcard. */
55
+ function acceptsMedia(accept, media, defaultMedia) {
56
+ if (accept === undefined)
57
+ return media === defaultMedia?.toLowerCase();
58
+ const [type] = media.split('/');
59
+ const matches = accept.toLowerCase().split(',').flatMap((part) => {
60
+ const [range, ...parameters] = part.trim().split(';').map((value) => value.trim());
61
+ const specificity = range === media ? 2 : range === `${type}/*` ? 1 : range === '*/*' ? 0 : -1;
62
+ if (specificity < 0)
63
+ return [];
64
+ const quality = parameters.find((value) => value.startsWith('q='));
65
+ const q = quality === undefined ? 1 : Number(quality.slice(2));
66
+ return [{ specificity, q: Number.isFinite(q) && q >= 0 && q <= 1 ? q : 0 }];
67
+ }).sort((a, b) => b.specificity - a.specificity || b.q - a.q);
68
+ return (matches[0]?.q ?? 0) > 0;
69
+ }
70
+ async function unitChecker(pkg) {
71
+ const surfacePath = join(pkg, 'src', 'generated', 'surface.gen.json');
72
+ if (!existsSync(surfacePath))
73
+ 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
+ };
89
+ // a lane is routed by host ("Other wires: lanes"): one whose manifest names the hosts it serves is judged on those
90
+ // 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();
94
+ const specDate = /^\d{4}-\d{2}-\d{2}/.exec(String(surface.version))?.[0];
95
+ // the path parameters whose values hold slashes (a git ref, a file path): the surface's own `spanning`, and the
96
+ // manifest's where the spec cannot say
97
+ const spans = [...(surface.spanning ?? []), ...(manifestModule.manifest?.spanning ?? [])];
98
+ const routes = compileSurface({ ...surface, ...(spans.length ? { spanning: spans } : {}) });
99
+ const resources = new Map(surface.resources.map((r) => [r.name, r]));
100
+ return {
101
+ pinsOlder: (headers) => {
102
+ if (!versionHeader || !specDate || !headers)
103
+ return false;
104
+ const pinned = Object.entries(headers).find(([k]) => k.toLowerCase() === versionHeader)?.[1];
105
+ return typeof pinned === 'string' && pinned.slice(0, 10) < specDate;
106
+ },
107
+ pathOf: (path) => (/^https?:\/\//.test(path) ? pathStyle(path) : path),
108
+ 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;
112
+ // 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))
114
+ 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);
117
+ return hit ? { op: hit.operation, params: hit.params } : undefined;
118
+ },
119
+ failures: (op, body, mediaType, accept, status = op.successStatus ?? 200, exchange) => {
120
+ // HEAD's headers describe GET's representation, while HTTP forbids response content.
121
+ if ((exchange?.method ?? op.method).toUpperCase() === 'HEAD') {
122
+ const empty = exchange ? exchange.responseText.length === 0 : body === undefined || body === null || body === '';
123
+ return empty ? [] : ['a HEAD response carries content, which HTTP forbids'];
124
+ }
125
+ const media = mediaType?.split(';')[0]?.trim().toLowerCase();
126
+ const binary = media !== undefined && binaryMedia(media);
127
+ const declared = op.responseMedia?.[String(status)] ?? op.responseMedia?.[`${Math.floor(status / 100)}XX`] ?? op.responseMedia?.default ?? [];
128
+ const defaultMedia = op.defaultResponseMedia?.[String(status)] ?? op.defaultResponseMedia?.[`${Math.floor(status / 100)}XX`] ?? op.defaultResponseMedia?.default;
129
+ if (binary && declared.some((value) => value.toLowerCase() === media) && acceptsMedia(accept, media, defaultMedia))
130
+ return [];
131
+ const primary = op.answers ? resources.get(op.answers.resource) : undefined;
132
+ if (!primary)
133
+ return [];
134
+ if (!body || typeof body !== 'object')
135
+ return binary ? [`the answer is not the operation's ${primary.name} resource`] : [];
136
+ // an answer that may be one of several resources (a card or a bank account) is judged as the one it says it is
137
+ const alternatives = [primary, ...(op.answers.alternatives ?? []).map((n) => resources.get(n)).filter((x) => !!x)];
138
+ const holder = op.answers.key ? body[op.answers.key] : body;
139
+ const items = op.answers.list ? (Array.isArray(holder) ? holder : (holder?.data ?? [])) : [holder];
140
+ const out = [];
141
+ for (const item of items) {
142
+ if (!item || typeof item !== 'object')
143
+ continue;
144
+ const said = item.object;
145
+ // a deleted object is judged as the deleted form of what it says it is
146
+ const gone = item.deleted === true;
147
+ const judged = (r) => {
148
+ const found = [];
149
+ const known = new Map([...r.fields.map((f) => [f.name, f]), ...(op.answers.extends ?? []).map((name) => [name, {}])]);
150
+ for (const [k, v] of Object.entries(item)) {
151
+ const f = known.get(k);
152
+ const local = manifestModule.manifest?.worldFields?.[r.name]?.[k];
153
+ if (local) {
154
+ if (!k.startsWith('twin_') || !local.why?.trim() || f)
155
+ found.push(`${r.name}.${k}: a World field must have a twin_ name and reason and must not replace a vendor field`);
156
+ const type = Array.isArray(v) ? 'array' : v === null ? 'null' : typeof v;
157
+ if (type !== local.type)
158
+ found.push(`${r.name}.${k}: World field is ${type}, expected ${local.type}`);
159
+ continue;
160
+ }
161
+ if (!f)
162
+ found.push(`${r.name} answers \`${k}\`, which the spec does not give it`);
163
+ else if (f.enum && typeof v === 'string' && !f.enum.includes(v))
164
+ found.push(`${r.name}.${k} is \`${v}\`, not one of the spec's ${f.enum.join(', ')}`);
165
+ }
166
+ for (const f of r.fields)
167
+ if (f.required && !(f.name in item))
168
+ found.push(`${r.name} leaves out \`${f.name}\`, which the spec requires`);
169
+ return found;
170
+ };
171
+ const named = alternatives.find((a) => isObject(a, said) && gone === a.fields.some((f) => f.name === 'deleted'))
172
+ ?? alternatives.find((a) => isObject(a, said));
173
+ // an item that names no `object` is judged as the alternative its own values say it is: one whose every enum field
174
+ // the item carries holds that value (a key's `kty`), the one it fits best where several do
175
+ const consistent = (a) => a.fields.every((f) => !f.enum || typeof item[f.name] !== 'string' || f.enum.includes(item[f.name]));
176
+ const r = named ?? (alternatives.length > 1
177
+ ? [...alternatives].filter(consistent).sort((a, b) => judged(a).length - judged(b).length)[0] ?? primary
178
+ : primary);
179
+ out.push(...judged(r));
180
+ }
181
+ return [...new Set(out)];
182
+ },
183
+ };
184
+ }
185
+ /** The SHAPE of a line protocol (scripts/derive-pack.ts from an RFC's command-reply table): each reply a
186
+ * code the spec allows the command it answers, or one any command may answer. A line after an
187
+ * intermediate reply (DATA's payload, a SASL response) is answered as that command's `then`. */
188
+ export function lineShape(pkg) {
189
+ const surfacePath = join(pkg, 'src', 'generated', 'surface.gen.json');
190
+ if (!existsSync(surfacePath))
191
+ return undefined;
192
+ const surface = JSON.parse(readFileSync(surfacePath, 'utf8'));
193
+ if (surface.format !== 'command-reply')
194
+ return undefined;
195
+ const commands = new Map(surface.commands.map((c) => [c.id, c]));
196
+ return {
197
+ check: (conn, line, codes) => {
198
+ const pending = conn.pending ? commands.get(conn.pending) : undefined;
199
+ const verb = line === null ? 'CONNECTION ESTABLISHMENT' : line.trim().split(/\s/)[0].toUpperCase();
200
+ const command = pending ?? commands.get(verb);
201
+ const replies = pending?.then ?? command;
202
+ const allowed = new Set([...surface.anyCommand, ...(replies ? [...replies.intermediate, ...replies.success, ...replies.error] : [])]);
203
+ const out = codes.filter((c) => !allowed.has(c)).map((c) => `${pending ? `the lines after ${pending.id}` : verb} answered ${c}, which the spec does not give it`);
204
+ const last = codes.at(-1);
205
+ if (last !== undefined)
206
+ conn.pending = command && replies?.intermediate.includes(last) ? command.id : undefined;
207
+ return out;
208
+ },
209
+ };
210
+ }
@@ -0,0 +1,2 @@
1
+ /** Match a published SDK-decoded SSE sample to a real HTTP frame; preserve non-SSE answers and mismatches. */
2
+ export declare function sseExampleAnswer(example: unknown, answer: unknown, matches: (frame: unknown) => boolean): unknown;
@@ -0,0 +1,32 @@
1
+ import { parseExactJson } from '@volter/world-core/exact-json';
2
+ /** Match a published SDK-decoded SSE sample to a real HTTP frame; preserve non-SSE answers and mismatches. */
3
+ export function sseExampleAnswer(example, answer, matches) {
4
+ if (typeof answer !== 'string' || !example || typeof example !== 'object')
5
+ return answer;
6
+ const wrapped = 'data' in example;
7
+ if (!wrapped && !('type' in example) && !('object' in example))
8
+ return answer;
9
+ const sample = (wrapped ? example : { data: example });
10
+ const frames = [];
11
+ for (const block of answer.replace(/\r\n/g, '\n').split('\n\n')) {
12
+ const lines = block.split('\n');
13
+ const data = lines.filter((s) => s.startsWith('data:')).map((s) => s.slice(5).replace(/^ /, '')).join('\n');
14
+ if (!data || data === '[DONE]')
15
+ continue;
16
+ const event = lines.find((s) => s.startsWith('event:'))?.slice(6).trim();
17
+ try {
18
+ frames.push({ data: parseExactJson(data), ...(event ? { event } : {}) });
19
+ }
20
+ catch { /* not a decoded JSON frame */ }
21
+ }
22
+ const relevant = frames.filter((f) => {
23
+ if (sample.event && f.event !== sample.event)
24
+ return false;
25
+ const want = sample.data;
26
+ const got = f.data;
27
+ return !want || typeof want !== 'object' || !got || typeof got !== 'object'
28
+ || (!want.type || want.type === got.type) && (!want.object || want.object === got.object);
29
+ });
30
+ const candidates = relevant.map((frame) => wrapped ? frame : frame.data);
31
+ return candidates.find(matches) ?? candidates[0] ?? answer;
32
+ }
@@ -0,0 +1,20 @@
1
+ import { type TwinStream } from '@volter/world-core/runtime';
2
+ import type { PackFetchOptions } from '@volter/world-core';
3
+ type ApplicationStandIn = NonNullable<PackFetchOptions['application']>;
4
+ /** 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
+ * 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
+ scenarioPath?: string;
8
+ application?: ApplicationStandIn;
9
+ }) => (request: Request) => Response | Promise<Response>;
10
+ export declare function vendorFetch(twinDir: string, vendor: string): Promise<VendorFetchFactory | undefined>;
11
+ /** Named views of an answer: what a journey step's `view` asserts against. The kit's own: `jwt:<field>`, the payload of the
12
+ * JSON Web Token at that field of the answer, decoded as the application verifying it reads it (a session token's
13
+ * claims). A pack's own (`answerViews` in its index) beside them. */
14
+ export type AnswerViews = Record<string, (body: unknown) => unknown>;
15
+ export declare function vendorAnswerViews(twinDir: string, vendor: string): Promise<AnswerViews>;
16
+ /** A vendor's byte-stream wire (a line protocol), in process: the pack's `create<Name>TwinStream`, what a
17
+ * hosted World hands a WebSocket's bytes. The walk drives it for a journey's line steps. */
18
+ export type VendorStreamFactory = (root: string, options?: Record<string, unknown>) => TwinStream;
19
+ export declare function vendorStream(twinDir: string, vendor: string): Promise<VendorStreamFactory | undefined>;
20
+ export {};
@@ -0,0 +1,92 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // A vendor's wire, in process: the pack's own fetch factory (`create<Name>Fetch`), found on its
10
+ // index. What the catalog's walkers drive (behavior journeys, the spec census probe, the invariants)
11
+ // since the worker render left with the v1 host.
12
+ import { join } from 'node:path';
13
+ import packFacts from '@volter/world-core/generated/pack-facts.json' with { type: 'json' };
14
+ import { startPgliteHost } from "./walk-database.js";
15
+ import { worldNow } from '@volter/world-core/runtime';
16
+ export async function vendorFetch(twinDir, vendor) {
17
+ let mod;
18
+ try {
19
+ mod = (await import(__rewriteRelativeImportExtension(join(twinDir, vendor, 'src', 'index.ts'))));
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ // a pack exports create<Name>Fetch (its generated fetch.ts)
25
+ const name = Object.keys(mod).find((k) => /^create\w+?Fetch$/.test(k) && !/TwinFetch$/.test(k) && typeof mod[k] === 'function');
26
+ if (!name)
27
+ return undefined;
28
+ const factory = mod[name];
29
+ if (!declaresManagedDatabase(vendor))
30
+ return (root, options = {}) => factory({ root, ...options });
31
+ // A pack whose data plane is the World's managed Postgres is walked over one, as a World binds it: each World (each
32
+ // root) its own fresh database, started on its first request and stopped when the walk's process ends, its clock the
33
+ // walked World's (set before each request: the walk moves the World clock between steps).
34
+ return (root, options = {}) => {
35
+ const bare = factory({ root, ...options });
36
+ let bound;
37
+ let host;
38
+ const bind = () => (bound ??= startPgliteHost().then((started) => { hosts.push(started); host = started; return factory({ root, database: started.url, ...options }); }));
39
+ const served = (request) => bind().then((fetch) => { host.clock(worldNow()); return fetch(request); });
40
+ // the World's database itself, for what an application sends it over its own wire (a SQL step: journey-kit.ts)
41
+ const managedDatabaseUrl = () => bind().then(() => { host.clock(worldNow()); return host.url; });
42
+ // the walk's end stops its World's database, so the next World walked declares it at the same port
43
+ const stopDatabase = async () => { if (bound === undefined)
44
+ return; await bound.catch(() => undefined); await host?.stop(); };
45
+ return Object.assign(served, bare, { managedDatabaseUrl, stopDatabase });
46
+ };
47
+ }
48
+ const hosts = [];
49
+ process.once('exit', () => { for (const host of hosts) {
50
+ try {
51
+ process.kill(host.pid, 'SIGTERM');
52
+ }
53
+ catch { /* ended */ }
54
+ } });
55
+ /** Whether a pack declares the World's managed database (its descriptor's `managedDatabase`, in the pack facts: this
56
+ * checkout's kernel's, as the walk's other readers of them import them). */
57
+ function declaresManagedDatabase(vendor) {
58
+ return packFacts.packs[vendor.split('/')[0]]?.managedDatabase !== undefined;
59
+ }
60
+ const jwtView = (field) => (body) => {
61
+ const token = body?.[field];
62
+ if (typeof token !== 'string')
63
+ return null;
64
+ try {
65
+ return JSON.parse(Buffer.from(token.split('.')[1] ?? '', 'base64url').toString('utf8'));
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ };
71
+ export async function vendorAnswerViews(twinDir, vendor) {
72
+ let own = {};
73
+ try {
74
+ own = (await import(__rewriteRelativeImportExtension(join(twinDir, vendor, 'src', 'index.ts')))).answerViews ?? {};
75
+ }
76
+ catch { /* none of its own */ }
77
+ return new Proxy(own, { get: (views, name) => (typeof name === 'string' && name.startsWith('jwt:') ? jwtView(name.slice(4)) : views[name]) });
78
+ }
79
+ export async function vendorStream(twinDir, vendor) {
80
+ let mod;
81
+ try {
82
+ mod = (await import(__rewriteRelativeImportExtension(join(twinDir, vendor, 'src', 'index.ts'))));
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ const name = Object.keys(mod).find((k) => /^create\w+TwinStream$/.test(k) && typeof mod[k] === 'function');
88
+ if (!name)
89
+ return undefined;
90
+ const factory = mod[name];
91
+ return (root, options = {}) => factory({ ...options, root });
92
+ }
@@ -0,0 +1,12 @@
1
+ /** `clock(at)` sets the World clock the database reads (pglite-host.mjs: the World's time), frozen at the instant `at`. */
2
+ export type PgliteHost = {
3
+ url: string;
4
+ port: number;
5
+ pid: number;
6
+ clock: (at: string) => void;
7
+ stop: () => Promise<void>;
8
+ };
9
+ /** Start a PGlite host under node (PGlite's WASM init does not complete under bun) and wait until it serves. Its clock
10
+ * is a file of the host's own, which the walk sets to the walked World's clock before each request (`clock`); its
11
+ * port is the same for every walked World. Database randomness is not pinned. */
12
+ export declare function startPgliteHost(): Promise<PgliteHost>;
@@ -0,0 +1,96 @@
1
+ // The walker's managed database: the World's own containerless Postgres (world-runtime's pglite-host.mjs, the PGlite host a World
2
+ // without a container runtime serves its postgres service with), started on a free loopback port in a temp dir and
3
+ // stopped by its own PID, for a pack that declares `managedDatabase` (packages/twin-standard/src/walk/vendor-fetch.ts). Not on the serve path.
4
+ import { spawn } from 'node:child_process';
5
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { createServer, connect } from 'node:net';
7
+ import { tmpdir } from 'node:os';
8
+ import { createRequire } from 'node:module';
9
+ import { join } from 'node:path';
10
+ const HOST_SCRIPT = createRequire(import.meta.url).resolve('@volter/world-runtime/src/pglite-host.mjs');
11
+ async function freePort() {
12
+ return new Promise((done, fail) => {
13
+ const server = createServer();
14
+ server.once('error', fail);
15
+ server.listen(0, '127.0.0.1', () => {
16
+ const port = server.address().port;
17
+ server.close(() => done(port));
18
+ });
19
+ });
20
+ }
21
+ function answers(port) {
22
+ return new Promise((done) => {
23
+ const socket = connect(port, '127.0.0.1');
24
+ socket.once('connect', () => { socket.destroy(); done(true); });
25
+ socket.once('error', () => done(false));
26
+ });
27
+ }
28
+ // The port a walked World declares its postgres service at, as a World's infrastructure file declares one: the same for
29
+ // every World this process walks, so two fresh Worlds hand an application the same database URL (R9), each World's
30
+ // host on it once the one before has stopped (a walk stops its World's host when it ends). A World walked while another
31
+ // still holds it gets a port of its own.
32
+ let declaredPort;
33
+ let declaredHeld = false;
34
+ async function released(port) {
35
+ for (const deadline = Date.now() + 5000; Date.now() < deadline; await new Promise((r) => setTimeout(r, 100)))
36
+ if (!(await answers(port)))
37
+ return true;
38
+ return false;
39
+ }
40
+ /** Start a PGlite host under node (PGlite's WASM init does not complete under bun) and wait until it serves. Its clock
41
+ * is a file of the host's own, which the walk sets to the walked World's clock before each request (`clock`); its
42
+ * port is the same for every walked World. Database randomness is not pinned. */
43
+ export async function startPgliteHost() {
44
+ const dir = mkdtempSync(join(tmpdir(), 'walk-database-'));
45
+ const clockFile = join(dir, 'world-clock');
46
+ const declared = declaredPort !== undefined && !declaredHeld && (await released(declaredPort));
47
+ const port = declared ? declaredPort : await freePort();
48
+ declaredPort ??= port;
49
+ const holdsDeclared = port === declaredPort;
50
+ if (holdsDeclared)
51
+ declaredHeld = true;
52
+ // The host runs under a small guard that ends it (and removes its data) when this process's end closes the guard's
53
+ // stdin: a test runner or a manifest run that exits without its teardown leaves no PGlite host behind.
54
+ const guard = `
55
+ import { spawn } from 'node:child_process'; import { rmSync } from 'node:fs';
56
+ const [script, port, data, dir] = process.argv.slice(1);
57
+ const host = spawn(process.execPath, [script, '--port', port, '--data', data], { stdio: ['ignore', 'ignore', 'inherit'] });
58
+ const end = () => { try { host.kill('SIGTERM'); } catch {} setTimeout(() => { try { rmSync(dir, { recursive: true, force: true }); } catch {} process.exit(0); }, 500); };
59
+ process.stdin.on('end', end); process.stdin.on('close', end); process.stdin.resume();
60
+ process.on('SIGTERM', end);
61
+ host.on('exit', (code) => { try { rmSync(dir, { recursive: true, force: true }); } catch {} process.exit(code ?? 0); });`;
62
+ const child = spawn('node', ['--input-type=module', '-e', guard, HOST_SCRIPT, String(port), join(dir, 'pgdata'), dir], { stdio: ['pipe', 'ignore', 'pipe'], env: { ...process.env, TWIN_WORLD_CLOCK_FILE: clockFile } });
63
+ let stderr = '';
64
+ child.stderr?.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
65
+ // the host never keeps the walk's process alive: its end closes the guard's stdin, which ends the host (a walk that
66
+ // finished otherwise waited on these pipes forever, and grade-pack on its 600 s timeout)
67
+ child.unref();
68
+ child.stdin?.unref?.();
69
+ child.stderr?.unref?.();
70
+ const deadline = Date.now() + 60_000;
71
+ while (!(await answers(port))) {
72
+ if (child.exitCode !== null)
73
+ throw new Error(`pglite-host exited ${child.exitCode}: ${stderr}`);
74
+ if (Date.now() > deadline) {
75
+ child.kill('SIGTERM');
76
+ throw new Error(`pglite-host did not listen within 60s: ${stderr}`);
77
+ }
78
+ await new Promise((r) => setTimeout(r, 200));
79
+ }
80
+ return {
81
+ url: `postgresql://postgres:postgres@127.0.0.1:${port}/postgres`,
82
+ port,
83
+ pid: child.pid,
84
+ clock: (at) => writeFileSync(clockFile, `${at}\n`),
85
+ stop: async () => {
86
+ if (child.exitCode === null && child.pid !== undefined) {
87
+ const ended = new Promise((r) => child.once('exit', r));
88
+ process.kill(child.pid, 'SIGTERM');
89
+ await Promise.race([ended, new Promise((r) => setTimeout(r, 5000))]);
90
+ }
91
+ rmSync(dir, { recursive: true, force: true });
92
+ if (holdsDeclared)
93
+ declaredHeld = false;
94
+ },
95
+ };
96
+ }
@@ -0,0 +1,2 @@
1
+ /** The root element's members, or undefined when the text is not one XML document. */
2
+ export declare function xmlMembers(text: string): Record<string, unknown> | undefined;
@@ -0,0 +1,55 @@
1
+ // An XML answer as the members of its root element (S3's `<ListBucketResult><Name>…</Name><Contents>…</Contents>…`,
2
+ // its `<Error><Code>…`): each child element by its name, a repeated one as a list, an element with no children as its
3
+ // text. What a restXml output structure's members are on the wire, so a journey asserts and captures them (`Contents.0.Key`)
4
+ // and SHAPE checks them as it checks a JSON answer's. Attributes and namespaces are not members and are dropped. XML
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.
7
+ const ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
8
+ const decode = (s) => s.replace(/&(#x[0-9a-f]+|#\d+|\w+);/gi, (m, e) => (e[0] === '#' ? String.fromCodePoint(e[1] === 'x' || e[1] === 'X' ? parseInt(e.slice(2), 16) : Number(e.slice(1))) : ENTITIES[e] ?? m));
9
+ /** The root element's members, or undefined when the text is not one XML document. */
10
+ export function xmlMembers(text) {
11
+ const src = text.replace(/<\?[\s\S]*?\?>/g, '').replace(/<!--[\s\S]*?-->/g, '').replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, (_, c) => c.replace(/&/g, '&amp;').replace(/</g, '&lt;'));
12
+ const stack = [{ name: '', children: [], text: '' }];
13
+ const tag = /<(\/?)([\w:.-]+)[^>]*?(\/?)>|([^<]+)/g;
14
+ for (let m = tag.exec(src); m; m = tag.exec(src)) {
15
+ const top = stack.at(-1);
16
+ if (m[4] !== undefined) {
17
+ top.text += m[4];
18
+ continue;
19
+ }
20
+ const name = m[2].replace(/^[\w.-]+:/, '');
21
+ if (m[1]) {
22
+ if (stack.length < 2 || top.name !== name)
23
+ return undefined;
24
+ stack.pop();
25
+ continue;
26
+ }
27
+ const node = { name, children: [], text: '' };
28
+ top.children.push(node);
29
+ if (!m[3])
30
+ stack.push(node);
31
+ }
32
+ const root = stack.length === 1 && stack[0].children.length === 1 ? stack[0].children[0] : undefined;
33
+ if (!root)
34
+ return undefined;
35
+ // an element with no children is its text; a name that repeats among siblings is a list
36
+ const value = (n) => {
37
+ if (!n.children.length)
38
+ return decode(n.text.trim());
39
+ const count = new Map();
40
+ for (const c of n.children)
41
+ count.set(c.name, (count.get(c.name) ?? 0) + 1);
42
+ const out = {};
43
+ for (const c of n.children) {
44
+ if ((count.get(c.name) ?? 0) > 1)
45
+ (out[c.name] ??= []).push(value(c));
46
+ else
47
+ out[c.name] = value(c);
48
+ }
49
+ return out;
50
+ };
51
+ // a document whose root is a leaf (S3's `<LocationConstraint>auto</LocationConstraint>`) is that one member
52
+ if (!root.children.length)
53
+ return { [root.name]: value(root) };
54
+ return value(root);
55
+ }