@volter/twin-standard 1.0.158 → 1.0.160

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 (100) 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/check-sources.js +2 -2
  10. package/dist/src/clients/harness.d.ts +25 -4
  11. package/dist/src/clients/harness.js +184 -22
  12. package/dist/src/clients/jira.js +11 -2
  13. package/dist/src/clients/resend.first-use.json +2 -2
  14. package/dist/src/clients/run.js +19 -14
  15. package/dist/src/conformance.d.ts +3 -4
  16. package/dist/src/conformance.js +396 -203
  17. package/dist/src/consumer.d.ts +22 -1
  18. package/dist/src/consumer.js +91 -26
  19. package/dist/src/create-pack.d.ts +1 -1
  20. package/dist/src/create-pack.js +13 -12
  21. package/dist/src/credential-issuer-import.d.ts +3 -0
  22. package/dist/src/credential-issuer-import.js +42 -0
  23. package/dist/src/derive.js +6 -3
  24. package/dist/src/gate.js +4 -0
  25. package/dist/src/index.d.ts +1 -1
  26. package/dist/src/index.js +1 -1
  27. package/dist/src/lanes.d.ts +2 -0
  28. package/dist/src/lanes.js +7 -0
  29. package/dist/src/pack-facts.d.ts +3 -0
  30. package/dist/src/pack-facts.js +36 -1
  31. package/dist/src/protocol-3.js +13 -8
  32. package/dist/src/spec-documents.d.ts +4 -0
  33. package/dist/src/spec-documents.js +110 -3
  34. package/dist/src/spec-ir-client.js +1 -1
  35. package/dist/src/spec-ir-commands.js +1 -1
  36. package/dist/src/spec-ir-lines.js +1 -1
  37. package/dist/src/spec-ir-proto.d.ts +1 -1
  38. package/dist/src/spec-patches.js +2 -2
  39. package/dist/src/tools/judge-brief.js +1 -2
  40. package/dist/src/tools/life-coverage-walk.js +38 -26
  41. package/dist/src/tools/life-coverage.js +2 -2
  42. package/dist/src/tools/packs-root.js +3 -3
  43. package/dist/src/tools/score-pack.js +7 -7
  44. package/dist/src/tools/vendor-examples.js +95 -85
  45. package/dist/src/verdict.js +1 -1
  46. package/dist/src/walk/behavior-journey.d.ts +22 -14
  47. package/dist/src/walk/behavior-journey.js +199 -133
  48. package/dist/src/walk/journey-kit.d.ts +5 -4
  49. package/dist/src/walk/journey-kit.js +6 -2
  50. package/dist/src/walk/replay.d.ts +23 -0
  51. package/dist/src/walk/replay.js +118 -0
  52. package/dist/src/walk/serve-pack.d.ts +2 -2
  53. package/dist/src/walk/serve-pack.js +44 -2
  54. package/dist/src/walk/shape.d.ts +26 -5
  55. package/dist/src/walk/shape.js +167 -57
  56. package/dist/src/walk/vendor-fetch.d.ts +7 -2
  57. package/dist/src/walk/vendor-fetch.js +13 -2
  58. package/dist/src/walk/walk-database.d.ts +9 -1
  59. package/dist/src/walk/walk-database.js +35 -13
  60. package/dist/src/walk/xml-view.d.ts +3 -2
  61. package/dist/src/walk/xml-view.js +5 -4
  62. package/package.json +6 -4
  63. package/src/assessment.ts +12 -8
  64. package/src/browser-assessment.ts +12 -5
  65. package/src/check-sources.ts +2 -2
  66. package/src/clients/harness.ts +142 -19
  67. package/src/clients/jira.ts +4 -2
  68. package/src/clients/resend.first-use.json +2 -2
  69. package/src/clients/run.ts +17 -15
  70. package/src/conformance.ts +317 -147
  71. package/src/consumer.ts +83 -21
  72. package/src/create-pack.ts +13 -12
  73. package/src/credential-issuer-import.ts +38 -0
  74. package/src/derive.ts +6 -4
  75. package/src/gate.ts +3 -0
  76. package/src/index.ts +1 -1
  77. package/src/lanes.ts +7 -0
  78. package/src/pack-facts.ts +25 -2
  79. package/src/protocol-3.ts +12 -8
  80. package/src/spec-documents.ts +92 -3
  81. package/src/spec-ir-client.ts +1 -1
  82. package/src/spec-ir-commands.ts +1 -1
  83. package/src/spec-ir-lines.ts +1 -1
  84. package/src/spec-ir-proto.ts +1 -1
  85. package/src/spec-patches.ts +2 -2
  86. package/src/tools/judge-brief.ts +1 -2
  87. package/src/tools/life-coverage-walk.ts +32 -22
  88. package/src/tools/life-coverage.ts +2 -2
  89. package/src/tools/packs-root.ts +3 -3
  90. package/src/tools/score-pack.ts +8 -8
  91. package/src/tools/vendor-examples.ts +92 -80
  92. package/src/verdict.ts +1 -1
  93. package/src/walk/behavior-journey.ts +158 -109
  94. package/src/walk/journey-kit.ts +9 -7
  95. package/src/walk/replay.ts +103 -0
  96. package/src/walk/serve-pack.ts +30 -5
  97. package/src/walk/shape.ts +157 -55
  98. package/src/walk/vendor-fetch.ts +16 -4
  99. package/src/walk/walk-database.ts +41 -15
  100. package/src/walk/xml-view.ts +5 -4
@@ -1,4 +1,4 @@
1
- import { hasOwnSurface } from './lanes.ts';
1
+ import { hasOwnSurface, unitsIn } from './lanes.ts';
2
2
  // CONFORMANCE — the deterministic half of the Protocol 3 standard that executes the pack: the contract it serves, run the
3
3
  // way an application calls it, and every rule a pack's author follows that a machine can decide (docs/contributing/
4
4
  // architecture.md, "Conformance"). One command, the same for a contributor, the catalog's admission and its regular
@@ -11,18 +11,26 @@ import { hasOwnSurface } from './lanes.ts';
11
11
  // judges (the life's plausibility) is the proof section's. Each check names the defect it was written against: one
12
12
  // that escaped review once fails here when reintroduced.
13
13
  import { spawn, spawnSync } from 'node:child_process';
14
- import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
15
15
  import { createRequire } from 'node:module';
16
16
  import { connect } from 'node:net';
17
17
  import { tmpdir } from 'node:os';
18
18
  import { basename, dirname, extname, join, matchesGlob, relative, resolve, sep } from 'node:path';
19
19
  // A path relative to another with '/' between its parts on every platform (Windows' relative() joins with '\').
20
20
  const rel = (from: string, to: string): string => relative(from, to).split(sep).join('/');
21
- import type { TwinStream } from '@volter/world-core';
21
+ import type { PackDescriptor, TwinStream } from '@volter/world-core';
22
+ import { hostRules } from '@volter/world-core/host-routing';
23
+ import { twinEnvStem } from '@volter/world-core/vendor-hosts';
24
+ import type { DerivedManifest, ErrorSpec, RemoteExecuteResponse, StateSystemAdapters, TwinAuthStrategy } from '@volter/world-core/runtime';
22
25
  import { citations, sourceHeld, type SourceRecord } from './check-sources.ts';
23
26
  import { fileURLToPath } from 'node:url';
24
- import { servedHttpFetch } from './clients/harness.ts';
27
+ import { parseEnv } from 'node:util';
28
+ import { openWebSocket, readHttpResponse, servedHttpFetch, worldProxy } from './clients/harness.ts';
25
29
  import { servePack as serve, type ServedPack as Served } from './walk/serve-pack.ts';
30
+ import { walkPackDirectory } from './walk/pack-directory.ts';
31
+ import { formBody, sigv4Headers } from './walk/journey-kit.ts';
32
+ import { consumerClientProof, consumerRecipe, runConsumerCommand, type ConsumerResult } from './consumer.ts';
33
+ import { declaredResponseStatuses } from './spec-documents.ts';
26
34
 
27
35
  export type ConformanceResult = { id: string; asks: string; failures: string[]; note?: string };
28
36
 
@@ -39,16 +47,32 @@ type Manifest = {
39
47
  const pascal = (s: string): string => s.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w[0]!.toUpperCase() + w.slice(1)).join('');
40
48
  const readJson = (p: string): unknown => (existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : undefined);
41
49
 
42
- /** The pack's units: the pack and each lane in it (a directory with its own src/manifest.ts and no package.json). */
43
- function unitsIn(pack: string): string[] {
44
- return [pack, ...readdirSync(pack).sort().map((n) => join(pack, n)).filter((d) => !existsSync(join(d, 'package.json')) && existsSync(join(d, 'src', 'manifest.ts')))];
50
+ /** Exercise the door's declared organization and deployment, as the World renderer does. */
51
+ function credentialDoorBody(door: NonNullable<PackDescriptor['credentialDoor']>): string {
52
+ return JSON.stringify({ ...door.body, ...(door.account ? { [door.account.field]: 'default' } : {}), ...(door.deployment ? { [door.deployment.field]: 'conformance' } : {}) });
45
53
  }
54
+
46
55
  const nameOf = (pack: string, unit: string): string => rel(resolve(pack, '..'), unit);
47
56
 
57
+ /** A credential door's native successful object and nonempty string outputs, through the served HTTP owner. */
58
+ async function credentialFields(server: Served, door: NonNullable<PackDescriptor['credentialDoor']>, host?: string): Promise<{ values: Record<string, unknown>; outputs: Record<string, string> }> {
59
+ await server.managedDatabaseUrl?.();
60
+ const port = server.inspectPort ?? server.port;
61
+ const answer = await readHttpResponse(servedHttpFetch(port), new Request(`http://${host ?? `127.0.0.1:${port}`}${door.path}`, {
62
+ method: 'POST', headers: { 'content-type': 'application/json', 'x-volter-world-activity': 'provisioning' }, body: credentialDoorBody(door),
63
+ }));
64
+ if (answer.bodyError) throw new Error(`credential door ${door.path}: ${answer.bodyError.message}`);
65
+ if (answer.status < 200 || answer.status >= 300) throw new Error(`credential door ${door.path} answered ${answer.status}`);
66
+ const { decodeResponse } = await import('./walk/shape.ts');
67
+ const values = decodeResponse(answer.text, undefined);
68
+ if (!values || typeof values !== 'object' || Array.isArray(values)) throw new Error(`credential door ${door.path} supplies no JSON object`);
69
+ const fields = values as Record<string, unknown>;
70
+ const outputs = Object.fromEntries(Object.entries(door.fill).flatMap(([name, field]) => typeof fields[field] === 'string' && fields[field] ? [[name, fields[field] as string]] : []));
71
+ return { values: fields, outputs };
72
+ }
73
+
48
74
  /** The files conformance reads from a unit: its manifest, its spec's record, its vendor's life and its decision table
49
- * (a lane's journeys are its own `journeys/` or else its vendor's). A release publishes each (published()), and a
50
- * pack's standing counts it in the form only when every unit holds them (twin-world's scripts/pack-standing.ts, by
51
- * unitFilesMissing): the one list of both. */
75
+ * (a lane's journeys are its own `journeys/` or else its vendor's). A release publishes each (published()). */
52
76
  export const UNIT_FILES = ['src/manifest.ts', 'spec/SOURCE.md', 'journeys/customer-life.json', 'journeys/decisions.json'] as const;
53
77
 
54
78
  /** The published package must carry every conformance input its units hold, including lanes. */
@@ -63,8 +87,6 @@ export function unpublishedUnitFiles(unit: string): string[] {
63
87
  }).map((f) => `${f} is absent from package.json files (conformance reads it)`);
64
88
  }
65
89
 
66
- /** Each of UNIT_FILES a unit lacks where conformance reads it: the manifest of every unit, the life at its vendor's root
67
- * (lifeServed), the spec's record and the decision table of a unit with a spec (decided()). */
68
90
  const trackedJourneySources = new Map<string, string[]>();
69
91
  /** Table ownership follows one index snapshot per checkout/run. */
70
92
  function hasJourneySource(unit: string): boolean {
@@ -97,17 +119,6 @@ function hasJourneySource(unit: string): boolean {
97
119
  return source.some(path => path.startsWith(prefix));
98
120
  }
99
121
 
100
- export function unitFilesMissing(unit: string): string[] {
101
- const vendor = existsSync(join(unit, 'package.json')) ? unit : dirname(unit);
102
- const spec = hasOwnSurface(unit);
103
- const at: Record<(typeof UNIT_FILES)[number], string | undefined> = {
104
- 'src/manifest.ts': unit,
105
- 'spec/SOURCE.md': spec ? unit : undefined,
106
- 'journeys/customer-life.json': vendor,
107
- 'journeys/decisions.json': spec ? (hasJourneySource(unit) ? unit : vendor) : undefined,
108
- };
109
- return UNIT_FILES.filter((f) => at[f] !== undefined && !existsSync(join(at[f]!, f))).map((f) => `${relative(unit, join(at[f]!, f))} is missing (conformance reads it)`);
110
- }
111
122
  /** A unit's fetch export: a pack's `create<Vendor>Fetch`, a lane's `create<Lane>LaneFetch`. */
112
123
  const fetchName = (dir: string): string => (existsSync(join(dir, 'package.json')) ? `create${pascal(basename(dir))}Fetch` : `create${pascal(basename(dir))}LaneFetch`);
113
124
  const manifestOf = async (unit: string): Promise<Manifest> => ((await import(join(unit, 'src', 'manifest.ts'))) as { manifest: Manifest }).manifest;
@@ -233,7 +244,7 @@ async function cited(pack: string): Promise<string[]> {
233
244
  const record = (readJson(join(unit, 'spec', 'sources.json')) ?? {}) as Record<string, SourceRecord>;
234
245
  for (const [url, quotes] of await citations(unit)) {
235
246
  const r = record[url];
236
- if (!r) { out.push(`${nameOf(pack, unit)}: ${url} is cited and not recorded (bun <twin-world>/scripts/check-sources.ts ${nameOf(pack, unit)})`); continue; }
247
+ if (!r) { out.push(`${nameOf(pack, unit)}: ${url} is cited and not recorded (twin-standard check-sources ${nameOf(pack, unit)})`); continue; }
237
248
  if (!sourceHeld(url, r)) out.push(`${nameOf(pack, unit)}: ${url} did not answer with verified evidence when checked`);
238
249
  for (const q of quotes) if (r.quotes?.[q] !== true) out.push(`${nameOf(pack, unit)}: "${q.slice(0, 80)}" is not on ${url}`);
239
250
  }
@@ -280,10 +291,11 @@ async function allowance(pack: string, runtime: Runtime): Promise<string[]> {
280
291
  }
281
292
 
282
293
  type Runtime = {
283
- getPack: (vendor: string) => { stateSystem?: { perform?: unknown; refresh?: unknown } } | undefined;
294
+ getPack: (vendor: string) => { stateSystem?: StateSystemAdapters } | undefined;
284
295
  DEFAULT_RATE_BUDGET: NonNullable<Manifest['rateBudget']>;
296
+ worldNow: () => string;
297
+ vendorError: (manifest: DerivedManifest, error: ErrorSpec) => Response;
285
298
  };
286
- const within = <T>(ms: number, p: Promise<T>): Promise<T | 'timeout'> => Promise.race([p, new Promise<'timeout'>((r) => setTimeout(() => r('timeout'), ms))]);
287
299
 
288
300
  /** Served by its server.ts as a World runs it (its boot id set), the pack answers HTTP and the boot probe, and every socket
289
301
  * its manifests declare takes an upgrade. Escaped twice: serveHttp never took the upgrade a pack's fetch carries, then
@@ -294,24 +306,23 @@ async function standalone(pack: string): Promise<string[]> {
294
306
  const sockets: Array<{ unit: string; path: string; host?: string }> = [];
295
307
  for (const unit of unitsIn(pack)) for (const s of (await manifestOf(unit)).sockets ?? []) sockets.push({ unit: nameOf(pack, unit), path: s.path, ...(s.host ? { host: s.host } : {}) });
296
308
  // served the way a World runs it: the boot id set, so the seam wraps the fetch with the World's boot probe
297
- const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env;
298
- const before = env.VOLTER_WORLD_BOOT_ID;
299
- env.VOLTER_WORLD_BOOT_ID = 'conformance';
300
- let server: Served;
301
- try { server = await serve(pack, {}); } finally { if (before === undefined) delete env.VOLTER_WORLD_BOOT_ID; else env.VOLTER_WORLD_BOOT_ID = before; }
309
+ const server = await serve(pack, {});
302
310
  try {
303
- const http = await within(5000, fetch(`http://127.0.0.1:${(server.inspectPort ?? server.port)}/`).then((r) => r.status, (e: Error) => e.message));
304
- if (http === 'timeout' || typeof http === 'string') out.push(`served as a World runs it, it answers no HTTP request (${http})`);
305
- const boot = await within(5000, fetch(`http://127.0.0.1:${(server.inspectPort ?? server.port)}/__volter/world-boot`).then((r) => r.json() as Promise<{ boot?: string }>, (): { boot?: string } => ({})));
306
- if (boot === 'timeout' || boot.boot !== 'conformance') out.push("served as a World runs it, it does not answer the World's boot probe (/__volter/world-boot)");
311
+ const http = servedHttpFetch(server.inspectPort ?? server.port);
312
+ const probe = (path: string) => readHttpResponse(http, new Request(`http://127.0.0.1:${server.inspectPort ?? server.port}${path}`, { signal: AbortSignal.timeout(5000) }));
313
+ try {
314
+ const answer = await probe('/');
315
+ if (answer.bodyError) out.push(`served as a World runs it, its HTTP response failed (${answer.bodyError.message})`);
316
+ } catch (error) { out.push(`served as a World runs it, it answers no HTTP request (${String(error)})`); }
317
+ try {
318
+ const boot = await probe('/__volter/world-boot');
319
+ if (boot.bodyError || (JSON.parse(boot.text) as { boot?: string }).boot !== 'conformance') throw new Error('missing boot identity');
320
+ } catch { out.push("served as a World runs it, it does not answer the World's boot probe (/__volter/world-boot)"); }
307
321
  for (const s of sockets) {
308
- const upgraded = await new Promise<string | undefined>((done) => {
309
- const ws = new WebSocket(`ws://127.0.0.1:${server.port}${s.path}`, { headers: s.host ? { host: s.host } : {} } as unknown as string[]);
310
- const timer = setTimeout(() => { ws.close(); done('no upgrade in 5 s'); }, 5000);
311
- ws.onopen = () => { clearTimeout(timer); ws.close(); done(undefined); };
312
- ws.onerror = () => { clearTimeout(timer); done('the upgrade was refused'); };
313
- });
314
- if (upgraded) out.push(`${s.unit}: the socket at ${s.host ?? ''}${s.path}: ${upgraded}`);
322
+ const ws = new WebSocket(`ws://127.0.0.1:${server.port}${s.path}`, { headers: s.host ? { host: s.host } : {} } as unknown as string[]);
323
+ const upgraded = await openWebSocket(ws);
324
+ if (upgraded) ws.close();
325
+ else out.push(`${s.unit}: the socket at ${s.host ?? ''}${s.path}: no upgrade in 5 s or the upgrade was refused`);
315
326
  }
316
327
  } finally { await server.stop(); }
317
328
  return out;
@@ -321,7 +332,6 @@ type SweptOperation = {
321
332
  id: string; method: string; path: string; basePath?: string; pathParams?: string[];
322
333
  query?: Array<{ name: string; type?: string; required?: boolean }>; body?: Array<{ name: string; required?: boolean }>;
323
334
  bodyEncoding?: string; headers?: Record<string, string | null>; discriminator?: Record<string, string | null>;
324
- responseMedia?: Record<string, string[]>; answers?: unknown;
325
335
  };
326
336
 
327
337
  /** The request an application's first call of an operation sends when it knows nothing yet: every path parameter, every
@@ -337,109 +347,292 @@ function sweepRequest(op: SweptOperation, basePath: string, host: string, creden
337
347
  const headers = new Headers(credential);
338
348
  for (const [name, value] of Object.entries(op.headers ?? {})) headers.set(name, value ?? fill(name));
339
349
  const method = op.method.toUpperCase();
340
- let body: string | undefined;
350
+ let body: BodyInit | undefined;
341
351
  if (op.bodyEncoding === 'form') { headers.set('content-type', 'application/x-www-form-urlencoded'); body = ''; }
342
352
  else if (op.bodyEncoding === 'xml') { headers.set('content-type', 'application/xml'); body = ''; }
343
- else if (op.bodyEncoding === 'json' || op.bodyEncoding === 'multipart') { headers.set('content-type', 'application/json'); body = '{}'; }
353
+ else if (op.bodyEncoding === 'json') { headers.set('content-type', 'application/json'); body = '{}'; }
354
+ else if (op.bodyEncoding === 'multipart') { const formed = formBody({ parts: [] }, {}); headers.set('content-type', formed.contentType); body = formed.bytes; }
344
355
  return new Request(url, { method, headers, ...(body !== undefined && !['GET', 'HEAD'].includes(method) ? { body } : {}) });
345
356
  }
346
357
 
358
+ /** Obtain authorization through the existing customer recipe, with any declared issuer on the same World as the API. */
359
+ async function consumerAuthorization(pack: string, scratch: string, root: string, server: Served, descriptor: PackDescriptor, outputs: Record<string, string>, claimsAuthorization: (hostname: string, pathname: string) => boolean, consumer?: ConsumerResult): Promise<{ headers: Record<string, string>; url: URL }> {
360
+ const vendor = (await manifestOf(pack)).vendor ?? basename(pack);
361
+ const issuer = descriptor.credentialIssuer;
362
+ const companion = issuer ? walkPackDirectory(dirname(pack), issuer, pack) : undefined;
363
+ if (issuer && !companion) throw new Error(`${vendor}: its declared credential issuer ${issuer} is not installed`);
364
+ const issuerDoor = companion ? ((await manifestOf(companion)) as { descriptor?: PackDescriptor }).descriptor?.credentialDoor : undefined;
365
+ if (companion && !issuerDoor) throw new Error(`${issuer}: its declared credential door is missing`);
366
+ const recipe = consumerRecipe(vendor, pack);
367
+ if (!consumer) throw new Error(`${vendor}: native authorization requires its prepared installed customer application`);
368
+ mkdirSync(scratch, { recursive: true });
369
+ const appDir = consumer.appDir;
370
+ for (const [path, contents] of Object.entries(recipe.files)) if (path !== 'package.json' && path !== '.gitignore' && readFileSync(join(appDir, path), 'utf8') !== contents) throw new Error(`${vendor}: its prepared customer file ${path} differs from its native first-use recipe`);
371
+ const proxies: Array<Awaited<ReturnType<typeof worldProxy>>> = [];
372
+ let servedIssuer: Served | undefined;
373
+ let granted: { headers: Record<string, string>; url: URL } | undefined;
374
+ const { forwardableHeaders } = await import('@volter/world-runtime');
375
+ try {
376
+ const env = { ...outputs };
377
+ if (companion && issuerDoor) {
378
+ servedIssuer = await serve(companion, { root });
379
+ Object.assign(env, (await credentialFields(servedIssuer, issuerDoor)).outputs);
380
+ }
381
+ const credentialNames = new Set([...Object.keys(descriptor.credentialDoor?.fill ?? {}), ...Object.keys(issuerDoor?.fill ?? {})]);
382
+ for (const name of Object.keys(parseEnv(recipe.files['.env.example']!))) if (credentialNames.has(name) && !env[name]) throw new Error(`${vendor}: its native credential doors supplied no ${name}`);
383
+ const selectedProxy = await worldProxy(server.inspectPort ?? server.port, (request, response) => {
384
+ const url = new URL(request.url);
385
+ if (response.status >= 200 && response.status < 300 && !url.pathname.startsWith('/_twin/') && claimsAuthorization(url.hostname, url.pathname)) {
386
+ const headers = forwardableHeaders(request.headers);
387
+ for (const name of [...headers.keys()]) if (/^(host|forwarded|x-forwarded-.*|x-volter-.*|x-twins-key|proxy-.*|via|content-.*|digest)$/i.test(name)) headers.delete(name);
388
+ granted = { headers: Object.fromEntries(headers), url };
389
+ }
390
+ });
391
+ proxies.push(selectedProxy);
392
+ const selected = [{ vendor, dir: pack, port: selectedProxy.port }];
393
+ if (issuer && companion && servedIssuer) {
394
+ const issuerProxy = await worldProxy(servedIssuer.inspectPort ?? servedIssuer.port);
395
+ proxies.push(issuerProxy);
396
+ selected.push({ vendor: issuer, dir: companion, port: issuerProxy.port });
397
+ }
398
+ const { compilePackFacts, renderPackFacts } = await import('./pack-facts.ts');
399
+ const facts = await compilePackFacts(selected.map(({ vendor, dir }) => ({ vendor, dir })));
400
+ if (facts.undeclared.length) throw new Error('Customer authorization requires declared pack descriptors');
401
+ const packFacts = join(scratch, 'pack-facts.json');
402
+ writeFileSync(packFacts, renderPackFacts(facts));
403
+ const instance = join(scratch, 'instance.json');
404
+ writeFileSync(instance, JSON.stringify({ packFacts: [packFacts] }));
405
+ const routes = Object.fromEntries(selected.flatMap(({ vendor, port }) => (facts.packs[vendor]?.hosts ?? []).map((rule) => [rule.key ?? vendor, `http://127.0.0.1:${port}`])));
406
+ const endpoints = Object.fromEntries(Object.entries(routes).map(([key, url]) => [`${twinEnvStem(key)}_TWIN_URL`, url]));
407
+ const require = createRequire(join(pack, 'package.json'));
408
+ const inject = require.resolve('@volter/world-core/inject');
409
+ const result = await runConsumerCommand(recipe.run, appDir, {
410
+ ...process.env, ...endpoints, ...env, VOLTER_WORLD_INSTANCE: instance,
411
+ TWIN_INJECT: Object.entries(routes).map(([name, url]) => `${name}=${url}`).join(','),
412
+ NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ''} --require ${JSON.stringify(inject)}`.trim(),
413
+ VOLTER_TWIN_STRICT_EGRESS: '1',
414
+ }, AbortSignal.timeout(10_000));
415
+ if (result.exitCode !== 0) throw new Error(`${vendor}: its native first-use command exited ${result.exitCode}`);
416
+ if (!granted) throw new Error(`${vendor}: its native first-use command made no successful authorized request to the selected API`);
417
+ return granted;
418
+ } finally {
419
+ const cleanup = await Promise.allSettled([...proxies.map((proxy) => proxy.stop()), ...(servedIssuer ? [Promise.resolve().then(() => servedIssuer!.stop())] : [])]);
420
+ const failed = cleanup.find((result) => result.status === 'rejected');
421
+ if (failed?.status === 'rejected') throw failed.reason;
422
+ }
423
+ }
424
+
347
425
  /** Every operation the pack serves, called over HTTP as an application calls it, against its server.ts served as a World
348
426
  * runs it: once plainly and once with a scenario loaded (an empty one, as a World that scripts nothing loads it). Each
349
427
  * answers within 10 s, never with a 500 the spec does not declare, never with a field of the twin's own (`_`
350
428
  * bookkeeping, the kernel's `updatedAt`) and, answered successfully, in the shape the spec gives the operation; and every
351
- * refresh list a fresh account can read (one with no parameter in its path) answers it successfully, so a World rooted
352
- * at this twin reads its resources back. Escaped before: the scenario hang (a served pack given a scenario answered
429
+ * refresh list a fresh account can read (one with no parameter in its path) succeeds or remains unread under the
430
+ * native 403 contract while another read succeeds, or matches the pack's own removal at the World clock.
431
+ * Escaped before: the scenario hang (a served pack given a scenario answered
353
432
  * nothing), leaked kernel fields in answers no life step read, and refresh scopes on operations the pack answered with
354
433
  * the gap. The life walks what a customer does; this calls what the pack declares it serves. */
355
- async function surfaceSwept(pack: string, scratch: string): Promise<{ failures: string[]; note: string }> {
434
+ async function surfaceSwept(pack: string, scratch: string, runtime: Runtime, consumer?: ConsumerResult): Promise<{ failures: string[]; note: string }> {
356
435
  const out: string[] = [];
357
- const descriptor = ((await manifestOf(pack)) as { descriptor?: { hosts?: Array<{ host?: string }>; credentialDoor?: { path: string; body: Record<string, unknown>; fill: Record<string, string> } } }).descriptor;
436
+ const descriptor = ((await manifestOf(pack)) as { descriptor?: PackDescriptor }).descriptor;
358
437
  const fetchOf = ((await import(join(pack, 'src', 'fetch.ts'))) as Record<string, unknown>)[fetchName(pack)] as (() => { owners?: () => Owners; ownersByUnit?: () => Record<string, Owners> }) | undefined;
359
438
  const root = fetchOf?.();
360
439
  const byUnit = root?.ownersByUnit?.() ?? { '': root?.owners?.() ?? {} };
361
- const { shapeChecker } = await import('./walk/shape.ts');
362
- const { leaked } = await import('./walk/behavior-journey.ts');
440
+ const { decodeResponse, responseProblems, shapeChecker, vendorFieldsOf } = await import('./walk/shape.ts');
363
441
  const shape = await shapeChecker(pack);
364
- type Unit = { name: string; host: string; basePath: string; ops: SweptOperation[]; refreshLists: Array<{ resource: string; id: string }>; auth?: { header: string; scheme: string } };
442
+ type Unit = { name: string; lane: string; hosts: NonNullable<PackDescriptor['hosts']>; basePath: string; manifest: DerivedManifest; ops: SweptOperation[]; responseStatuses: Map<string, Set<string>>; refreshLists: Array<{ resource: string; id: string }>; refreshReads: string[]; auth?: { header: string; scheme: string }; strategy?: TwinAuthStrategy; removedAt?: (operation: string, now: number) => ErrorSpec | undefined };
365
443
  const units: Unit[] = [];
366
- const vendorFields = new Set<string>();
444
+ const vendorFields = await vendorFieldsOf(pack);
367
445
  for (const unit of unitsIn(pack).filter(hasOwnSurface)) {
368
- const surface = readJson(join(unit, 'src', 'generated', 'surface.gen.json')) as { format?: string; basePath?: string; operations?: SweptOperation[]; resources?: Array<{ fields: Array<{ name: string }> }> } | undefined;
369
- for (const r of surface?.resources ?? []) for (const f of r.fields) vendorFields.add(f.name);
446
+ const surface = readJson(join(unit, 'src', 'generated', 'surface.gen.json')) as { format?: string; basePath?: string; operations?: SweptOperation[] } | undefined;
447
+ const m = (await manifestOf(unit)) as Manifest & { hosts?: PackDescriptor['hosts']; descriptor?: Omit<PackDescriptor, 'vendor'>; auth?: { header: string; scheme: string } };
370
448
  // a line protocol and a command table ride in no path of their own: the life's line steps and the examples read them
371
449
  if (!surface?.operations || ['command-reply', 'command-table'].includes(String(surface.format))) continue;
372
- const m = (await manifestOf(unit)) as Manifest & { hosts?: Array<{ host?: string }>; auth?: { header: string; scheme: string } };
373
- const host = m.hosts?.find((h) => h.host)?.host ?? descriptor?.hosts?.find((h) => h.host)?.host;
374
- if (!host) { out.push(`${nameOf(pack, unit)}: no host is named (its manifest's hosts or the descriptor's) for an application to call it at`); continue; }
450
+ const hosts = m.hosts ?? m.descriptor?.hosts ?? descriptor?.hosts ?? [];
451
+ if (!hosts.length) { out.push(`${nameOf(pack, unit)}: no host is claimed (its manifest's hosts or the descriptor's) for an application to call it at`); continue; }
375
452
  const owners = byUnit[unit === pack ? '' : basename(unit)] ?? {};
376
453
  const refreshLists = Object.entries(m.resources ?? {}).flatMap(([resource, decl]) => (decl.refresh?.list ? [{ resource, id: decl.refresh.list }] : []));
377
- units.push({ name: nameOf(pack, unit), host, basePath: surface.basePath ?? '', ops: surface.operations.filter((o) => owners[o.id] !== undefined && owners[o.id] !== 'gap'), refreshLists, ...(m.auth ? { auth: m.auth } : {}) });
454
+ const refreshReads = Object.values(m.resources ?? {}).flatMap((decl) => [decl.refresh?.list, decl.refresh?.get, decl.refresh?.detail].filter((id): id is string => !!id));
455
+ const declaredAuth = m.descriptor?.auth ?? descriptor?.auth;
456
+ const strategy = declaredAuth?.in === 'lanes' ? declaredAuth.lanes[unit === pack ? '' : basename(unit)] : declaredAuth;
457
+ const shared = join(unit, 'src/semantics/shared.ts');
458
+ const removedAt = existsSync(shared) ? ((await import(shared)) as { removedAt?: Unit['removedAt'] }).removedAt : undefined;
459
+ units.push({ name: nameOf(pack, unit), lane: unit === pack ? '' : basename(unit), hosts, basePath: surface.basePath ?? '', manifest: m as DerivedManifest, ops: surface.operations.filter((o) => owners[o.id] !== undefined && owners[o.id] !== 'gap'), responseStatuses: declaredResponseStatuses(join(unit, 'spec')), refreshLists, refreshReads, ...(m.auth ? { auth: m.auth } : {}), ...(strategy ? { strategy } : {}), ...(typeof removedAt === 'function' ? { removedAt } : {}) });
378
460
  }
379
461
  if (!units.length) return { failures: out, note: 'no unit serves an operation on a path of its own' };
380
- const tally = { calls: 0, ok: 0, refused: 0 };
381
- const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env;
462
+ const tally = { calls: 0, ok: 0, refused: 0, unread: 0, removed: 0 };
463
+ const replayedHeaders = new Set<string>();
382
464
  const scenarioPath = join(scratch, 'surface-scenario.json');
383
465
  writeFileSync(scenarioPath, '{"handlers":[]}');
384
466
  for (const mode of ['plain', 'scenario'] as const) {
385
- const before = env.VOLTER_WORLD_BOOT_ID;
386
- env.VOLTER_WORLD_BOOT_ID = 'conformance';
387
- let server: Served;
388
- try { server = await serve(pack, { root: join(scratch, `surface-${mode}`), ...(mode === 'scenario' ? { scenarioPath } : {}) }); } finally { if (before === undefined) delete env.VOLTER_WORLD_BOOT_ID; else env.VOLTER_WORLD_BOOT_ID = before; }
467
+ const root = join(scratch, `surface-${mode}`);
468
+ const server = await serve(pack, { root, ...(mode === 'scenario' ? { scenarioPath } : {}) });
389
469
  const http = servedHttpFetch(server.inspectPort ?? server.port);
390
- const send = async (request: Request): Promise<Response | 'timeout' | Error> => {
470
+ const send = async (request: Request): Promise<Awaited<ReturnType<typeof readHttpResponse>> | Error> => {
391
471
  await server.managedDatabaseUrl?.();
392
- const abort = new AbortController();
393
- try { return await within(10_000, http(request, { signal: abort.signal })); } catch (error) { return error as Error; } finally { abort.abort(); }
472
+ try { return await readHttpResponse(http, request); } catch (error) { return error instanceof Error ? error : new Error(String(error)); }
394
473
  };
395
474
  try {
475
+ const said = mode === 'scenario' ? ' with a scenario loaded' : '';
396
476
  // the account an application holds: the key the credential door issues, sent as the vendor reads its credential
397
477
  let issued: string | undefined;
478
+ let nativeHeaders: Record<string, string> | undefined;
479
+ let recipeAuthorized = false;
480
+ let nativeUrl: URL | undefined;
481
+ const outputs: Record<string, string> = {};
398
482
  if (descriptor?.credentialDoor) {
399
483
  const door = descriptor.credentialDoor;
400
- const answer = await send(new Request(`https://${descriptor.hosts?.find((h) => h.host)?.host ?? units[0]!.host}${door.path}`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(door.body) }));
401
- const fields = answer instanceof Response ? ((await answer.json().catch(() => ({}))) as Record<string, unknown>) : {};
402
- issued = Object.values(door.fill).map((f) => fields[f]).find((v): v is string => typeof v === 'string');
484
+ const fields = await credentialFields(server, door);
485
+ const values = fields.values;
486
+ Object.assign(outputs, fields.outputs);
487
+ issued = Object.values(door.fill).map((f) => values[f]).find((v): v is string => typeof v === 'string' && v.length > 0);
488
+ if (typeof values.authorization === 'string' && values.authorization) {
489
+ nativeHeaders = { authorization: values.authorization };
490
+ if (typeof values.url === 'string') {
491
+ try { nativeUrl = new URL(values.url); } catch { out.push(`${nameOf(pack, pack)}: credential door supplies an invalid native url`); continue; }
492
+ if (nativeUrl.protocol !== 'https:' || nativeUrl.username || nativeUrl.password) { out.push(`${nameOf(pack, pack)}: credential door supplies a native url without a vendor HTTPS origin`); continue; }
493
+ }
494
+ }
495
+ }
496
+ const headerUnits = units.filter((unit) => !unit.strategy);
497
+ const ambiguousHeaderCredential = !nativeHeaders && new Set(Object.values(outputs)).size > 1 && headerUnits.length > 0
498
+ && Object.keys(parseEnv(consumerRecipe((await manifestOf(pack)).vendor ?? basename(pack), pack).files['.env.example']!)).some((name) => outputs[name]);
499
+ if (descriptor && (descriptor.credentialIssuer || ambiguousHeaderCredential)) {
500
+ const headerClaims = headerUnits.map((unit) => hostRules(unit.hosts));
501
+ const otherClaims = units.filter((unit) => unit.strategy).map((unit) => hostRules(unit.hosts));
502
+ const claimsAuthorization = descriptor.credentialIssuer ? hostRules(descriptor.hosts ?? []).takes
503
+ : (host: string, path: string) => headerClaims.some((claims) => claims.takes(host, path)) && !otherClaims.some((claims) => claims.takes(host, path));
504
+ const granted = await consumerAuthorization(pack, join(scratch, `authorization-${mode}`), root, server, descriptor, outputs, claimsAuthorization, consumer);
505
+ nativeHeaders = granted.headers;
506
+ recipeAuthorized = true;
507
+ for (const name of Object.keys(nativeHeaders)) replayedHeaders.add(name);
508
+ nativeUrl = granted.url;
509
+ issued = undefined;
510
+ }
511
+ const endpoints = Object.values(outputs).flatMap((value) => {
512
+ try { const url = new URL(value); return url.protocol === 'https:' && !url.username && !url.password ? [url] : []; } catch { return []; }
513
+ });
514
+ const claimsNativeUrl = (u: Unit): boolean => {
515
+ return nativeUrl !== undefined && hostRules(u.hosts).takes(nativeUrl.hostname, nativeUrl.pathname);
516
+ };
517
+ if (nativeUrl && !units.some(claimsNativeUrl)) { out.push(`${units.map((u) => u.name).join(', ')}: credential door native url ${nativeUrl.origin} matches no selected unit's declared host rules${said}`); continue; }
518
+ const hostCandidates = (u: Unit): string[] => {
519
+ const claims = hostRules(u.hosts);
520
+ const remote = u.manifest.remoteHost;
521
+ const remoteHost = remote?.template.replace(/\{([^}]+)\}/g, (_match, name: string) => Object.values(remote.values?.[name] ?? {})[0] ?? 'conformance');
522
+ if (remoteHost && !claims.names(new URL(`https://${remoteHost}`).hostname)) throw new Error(`${u.name}: its remote host ${remoteHost} matches none of its declared host rules`);
523
+ return [...new Set([
524
+ ...(nativeUrl && claimsNativeUrl(u) ? [nativeUrl.host] : []),
525
+ ...endpoints.map((url) => url.host),
526
+ ...(remoteHost ? [remoteHost] : []),
527
+ ...u.hosts.flatMap((rule) => 'host' in rule && rule.exclude !== true ? [rule.host] : []),
528
+ ])];
529
+ };
530
+ const answers = new Map<Unit, Map<string, number>>(units.map((u) => [u, new Map<string, number>()]));
531
+ const removed = new Set<string>();
532
+ const callOperation = async (u: Unit, op: SweptOperation | undefined, request: Request): Promise<RemoteExecuteResponse | undefined> => {
533
+ const now = Date.parse(runtime.worldNow());
534
+ const signing = u.strategy?.in === 'signature' && u.strategy.algorithm === 'aws-sigv4' ? u.strategy : undefined;
535
+ if (signing) {
536
+ const keyPart = (suffix: string): string | undefined => {
537
+ const values = [...new Set(Object.entries(outputs).filter(([name]) => name === suffix || name.endsWith(`_${suffix}`)).map(([, value]) => value))];
538
+ return values.length === 1 && values[0] ? values[0] : undefined;
539
+ };
540
+ const accessKeyId = keyPart('ACCESS_KEY_ID');
541
+ const secretAccessKey = keyPart('SECRET_ACCESS_KEY');
542
+ if (!accessKeyId || !secretAccessKey) { out.push(`${u.name}: its SigV4 credential door supplies no unambiguous access-key-id and secret-access-key pair${said}`); return undefined; }
543
+ const url = new URL(request.url);
544
+ const headers = Object.fromEntries(request.headers);
545
+ const body = request.body === null ? undefined : await request.clone().text();
546
+ const scope = typeof signing.scope === 'function' ? signing.scope({ method: request.method, path: url.pathname + url.search, host: url.host, headers, ...(body === undefined ? {} : { body }) }) : signing.scope;
547
+ const signed = sigv4Headers({ accessKeyId, secretAccessKey, ...scope }, {}, request.method, request.url, headers, body, now);
548
+ request = new Request(request, { headers: { ...headers, ...signed } });
549
+ }
550
+ const where = `${u.name}: ${op?.id ?? 'GraphQL refresh'} (${request.method} ${new URL(request.url).pathname})`;
551
+ const graphqlBody = request.method === 'POST' && u.manifest.graphql?.paths.includes(new URL(request.url).pathname)
552
+ ? await request.clone().json().catch(() => undefined) : undefined;
553
+ const res = await send(request);
554
+ if (res instanceof Error) { out.push(`${where}: the connection failed${said} (${res.message})`); return undefined; }
555
+ if (op) answers.get(u)!.set(op.id, res.status);
556
+ if (mode === 'plain') { tally.calls++; if (res.status < 300) tally.ok++; else tally.refused++; }
557
+ if (res.bodyError) { out.push(`${where}: response body failed${said} (${res.bodyError.message})`); return undefined; }
558
+ const text = res.text;
559
+ const response = { status: res.status, headers: res.headers, body: text, bodyBytes: res.bodyBytes };
560
+ const removal = op ? u.removedAt?.(op.id, Math.floor(now / 1000)) : undefined;
561
+ if (removal) {
562
+ const expected = runtime.vendorError(u.manifest, removal);
563
+ if (res.status === expected.status && text === await expected.text()) removed.add(`${u.name}:${op!.id}`);
564
+ else out.push(`${where} does not match its declared native removal at ${new Date(now).toISOString()}${said}: expected ${removal.status} and its exact native body, received ${res.status}: ${text.slice(0, 160)}`);
565
+ }
566
+ const statuses = op ? u.responseStatuses.get(op.id) : undefined;
567
+ if (res.status === 500 && !statuses?.has('500') && !statuses?.has('5XX') && !statuses?.has('default')) out.push(`${where} answered 500${said}, which the spec does not declare: ${text.slice(0, 160)}`);
568
+ const type = res.headers['content-type'];
569
+ const judged = responseProblems(shape, vendorFields, { method: request.method, path: request.url, requestHeaders: Object.fromEntries(request.headers), graphqlBody,
570
+ status: res.status, mediaType: type, body: decodeResponse(text, type), text });
571
+ for (const key of judged.leaks) out.push(`${where} answers the twin's own field ${key}${said}`);
572
+ for (const detail of judged.shape) out.push(`${where}: ${detail}${said}`);
573
+ return response;
574
+ };
575
+ const refreshed = new Set<string>();
576
+ const nativeRefreshOwnsReads = recipeAuthorized && nativeHeaders !== undefined && units.some((unit) => unit.refreshReads.length > 0);
577
+ if (nativeRefreshOwnsReads) {
578
+ const vendor = (await manifestOf(pack)).vendor ?? basename(pack);
579
+ const refresh = runtime.getPack(vendor)?.stateSystem?.refresh;
580
+ if (!refresh) throw new Error(`${vendor}: its registered native refresh adapter is missing`);
581
+ try { await refresh(async (call) => {
582
+ const u = units.find((unit) => unit.lane === (call.lane ?? ''));
583
+ if (!u) throw new Error(`${vendor}: its native refresh names no selected unit`);
584
+ const url = hostCandidates(u).map((host) => new URL(call.path, `https://${host}`)).find((candidate) => hostRules(u.hosts).takes(candidate.hostname, candidate.pathname));
585
+ if (!url) throw new Error(`${u.name}: its native refresh request matches no declared host`);
586
+ const headers = { ...call.headers, ...(claimsNativeUrl(u) ? nativeHeaders : {}) };
587
+ const request = new Request(url, { method: call.method, headers, ...(call.body === undefined ? {} : { body: call.body as BodyInit }) });
588
+ const operation = shape?.operationOf(call.method, url.href, Object.fromEntries(request.headers));
589
+ const op = (operation?.lane ?? '') === u.lane ? u.ops.find((candidate) => candidate.id === operation?.op.id) : undefined;
590
+ const graphql = call.method.toUpperCase() === 'POST' && u.manifest.graphql?.paths.includes(url.pathname);
591
+ if (!op && !graphql) throw new Error(`${u.name}: its native refresh matches no served operation or declared GraphQL endpoint`);
592
+ if (op) refreshed.add(`${u.name}:${op.id}`);
593
+ const response = await callOperation(u, op, request);
594
+ if (!response) throw new Error(`${u.name}: its native refresh request failed`);
595
+ return response;
596
+ }, { root }); }
597
+ catch (error) {
598
+ const detail = error instanceof Error ? error.message : String(error);
599
+ const secrets = Object.values(nativeHeaders!).flatMap((value) => [value, value.split(/\s+/).slice(1).join(' ')]).filter(Boolean);
600
+ out.push(`${vendor}: its native refresh adapter failed${said}: ${secrets.reduce((message, secret) => message.replaceAll(secret, '[redacted]'), detail)}`);
601
+ }
403
602
  }
404
- const said = mode === 'scenario' ? ' with a scenario loaded' : '';
405
603
  for (const u of units) {
406
- // a unit whose manifest declares no `auth` checks its key in its own front: the key goes as most vendors read
407
- // one, a bearer token, and a refusal of it is no finding of the refresh below
604
+ // A front that checks its own key receives the door's token as a bearer.
408
605
  const auth = u.auth ?? { header: 'authorization', scheme: 'Bearer' };
409
- const credential: Record<string, string> = issued ? { [auth.header]: auth.scheme ? `${auth.scheme} ${issued}` : issued } : {};
410
- const answered = new Map<string, number>();
606
+ const native = nativeHeaders !== undefined && (!nativeUrl || claimsNativeUrl(u));
607
+ const credential: Record<string, string> = native ? nativeHeaders! : issued ? { [auth.header]: auth.scheme ? `${auth.scheme} ${issued}` : issued } : {};
608
+ const signing = u.strategy?.in === 'signature' && u.strategy.algorithm === 'aws-sigv4' ? u.strategy : undefined;
609
+ if (!signing && u.auth && !native && !issued) { out.push(`${u.name}: its credential door supplies no nonempty credential for ${auth.header}${said}`); continue; }
610
+ const claims = hostRules(u.hosts);
611
+ let hosts: string[];
612
+ try { hosts = hostCandidates(u); } catch (error) { out.push(`${(error as Error).message}${said}`); continue; }
411
613
  for (const op of u.ops) {
412
- const request = sweepRequest(op, u.basePath, u.host, credential);
413
- const where = `${u.name}: ${op.id} (${request.method} ${new URL(request.url).pathname})`;
414
- const res = await send(request);
415
- if (res === 'timeout') { out.push(`${where} answered nothing in 10 s${said}`); continue; }
416
- if (res instanceof Error) { out.push(`${where}: the connection failed${said} (${res.message})`); continue; }
417
- answered.set(op.id, res.status);
418
- if (mode === 'plain') { tally.calls++; if (res.status < 300) tally.ok++; else tally.refused++; }
419
- const text = await res.text();
420
- if (res.status === 500 && !op.responseMedia?.['500'] && !op.responseMedia?.['5XX']) { out.push(`${where} answered 500${said}, which the spec does not declare: ${text.slice(0, 160)}`); continue; }
421
- if (mode === 'scenario') continue;
422
- const type = res.headers.get('content-type') ?? '';
423
- let body: unknown;
424
- if (/json/.test(type)) { try { body = JSON.parse(text); } catch { body = undefined; } }
425
- for (const key of leaked(body, vendorFields, '', !op.answers || res.status >= 300)) out.push(`${where} answers the twin's own field ${key}`);
426
- if (res.status >= 200 && res.status < 300 && shape) {
427
- const hit = shape.operationOf(request.method, request.url, Object.fromEntries(request.headers));
428
- if (hit) for (const detail of shape.failures(hit.op, body, type, request.headers.get('accept') ?? undefined, res.status, { method: request.method, responseText: text })) out.push(`${where}: ${detail}`);
429
- }
430
- }
431
- if (mode === 'plain') for (const r of u.refreshLists) {
432
- const op = u.ops.find((o) => o.id === r.id);
433
- if (!op || op.path.includes('{') || (op.basePath ?? '').includes('{')) continue;
434
- const status = answered.get(r.id);
435
- if (status !== undefined && (status === 401 || status === 403) && (!u.auth || !issued)) continue;
436
- if (status !== undefined && (status < 200 || status >= 300)) out.push(`${u.name}: ${r.resource}'s refresh lists by ${r.id}, which answered a fresh account ${status}: a World rooted at this twin reads nothing back`);
614
+ if (refreshed.has(`${u.name}:${op.id}`)) continue;
615
+ const request = hosts.map((host) => sweepRequest(op, u.basePath, host, signing ? {} : credential)).find((candidate) => {
616
+ const url = new URL(candidate.url);
617
+ return claims.takes(url.hostname, url.pathname);
618
+ });
619
+ if (!request) { out.push(`${u.name}: ${op.id} has no concrete declared host or issued HTTPS endpoint matching its host rules${said}`); continue; }
620
+ await callOperation(u, op, request);
437
621
  }
438
622
  }
623
+ const readSomething = units.some((u) => u.refreshReads.some((id) => { const status = answers.get(u)?.get(id); return status !== undefined && status >= 200 && status < 300; }));
624
+ if (mode === 'plain' && !nativeRefreshOwnsReads) for (const u of units) for (const r of u.refreshLists) {
625
+ const op = u.ops.find((o) => o.id === r.id);
626
+ if (!op || op.path.includes('{') || (op.basePath ?? '').includes('{')) continue;
627
+ const status = answers.get(u)?.get(r.id);
628
+ if (removed.has(`${u.name}:${r.id}`)) { tally.removed++; continue; }
629
+ if (status === 403 && readSomething) { tally.unread++; continue; }
630
+ if (status !== undefined && (status < 200 || status >= 300)) out.push(`${u.name}: ${r.resource}'s refresh lists by ${r.id}, which answered a fresh account ${status}: a World rooted at this twin reads nothing back`);
631
+ }
439
632
  } finally { await server.stop(); }
440
633
  }
441
634
  // one defect repeated over a surface reads as one: the first 40 findings, and how many more
442
- return { failures: out.length > 40 ? [...out.slice(0, 40), `… ${out.length - 40} more`] : out, note: `${tally.calls} served operations called, twice each: ${tally.ok} answered successfully, ${tally.refused} refused, unscripted` };
635
+ return { failures: out.length > 40 ? [...out.slice(0, 40), `… ${out.length - 40} more`] : out, note: `${tally.calls} served operations called, twice each: ${tally.ok} answered successfully, ${tally.refused} refused, unscripted; ${tally.unread} refresh resources unread (403), ${tally.removed} match declared native removals${replayedHeaders.size ? `; replayed application headers (values redacted): ${[...replayedHeaders].sort().join(', ')}` : ''}` };
443
636
  }
444
637
 
445
638
  /** A line protocol's connection over TCP, in the walk's stream shape. A line drew its replies when bytes arrived and then
@@ -479,31 +672,22 @@ async function lifeServed(pack: string): Promise<string[]> {
479
672
  if (!existsSync(lifePath)) return ['journeys/customer-life.json is missing'];
480
673
  const { inMemoryWorld, usePacks, walk } = await import('./walk/behavior-journey.ts');
481
674
  usePacks(dirname(pack), { dependenciesFrom: pack });
482
- const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env;
483
675
  const served = async (root: string, options: { scenarioPath?: string }) => {
484
- const before = env.VOLTER_WORLD_BOOT_ID;
485
- env.VOLTER_WORLD_BOOT_ID = 'conformance';
486
- let server: Served;
487
- try { server = await serve(pack, { root, ...options }); } finally { if (before === undefined) delete env.VOLTER_WORLD_BOOT_ID; else env.VOLTER_WORLD_BOOT_ID = before; }
676
+ const server = await serve(pack, { root, ...options });
488
677
  const port = server.inspectPort ?? server.port;
489
678
  const http = servedHttpFetch(port);
490
679
  return {
491
680
  // the vendor's URL as the application called it, sent to the served pack: its host carried in the Host header
492
- fetch: Object.assign(async (request: Request): Promise<Response> => {
493
- await server.managedDatabaseUrl?.();
494
- const abort = new AbortController();
495
- try {
496
- const answer = await within(10_000, http(request, { signal: abort.signal }));
497
- return answer === 'timeout' ? new Response(`no answer in 10 s from the served pack (${request.method} ${new URL(request.url).pathname})`, { status: 599 }) : answer;
498
- } finally { abort.abort(); }
499
- }, server.managedDatabaseUrl ? { managedDatabaseUrl: server.managedDatabaseUrl } : {}),
681
+ fetch: Object.assign(http, {
682
+ managedDatabaseUrl: server.managedDatabaseUrl,
683
+ managedDatabaseClock: server.managedDatabaseClock,
684
+ managedDatabaseConnection: server.managedDatabaseConnection,
685
+ }),
500
686
  // a socket opened as an application opens it: a WebSocket to the served port, the vendor's host in its Host header
501
687
  socket: (url: string, headers: Record<string, string>) => new WebSocket(`ws://127.0.0.1:${port}${url}`, { headers } as unknown as string[]),
502
688
  // TCP to the native wire: a separate listener beside HTTP, or the primary protocol port.
503
689
  stream: lineConnection(server.wirePort ?? server.port),
504
- // bounded: Bun's server.stop(true) never settles once a WebSocket peer has closed under it (discord's life, Bun
505
- // 1.3.11: the walk ended and the stop spun at full CPU for 45 minutes), so the check waits 5 s and goes on
506
- stop: async () => { await within(5000, Promise.resolve(server.stop())); },
690
+ stop: () => server.stop(),
507
691
  };
508
692
  };
509
693
  const world = inMemoryWorld(`conform-${basename(pack)}`);
@@ -516,54 +700,40 @@ async function lifeServed(pack: string): Promise<string[]> {
516
700
 
517
701
  /** The pack driven by the vendor's own client (clients/<vendor>.ts): served as a World runs it, its credentials issued
518
702
  * by its credential door, the cases run in Node as an application runs the vendor's SDK (clients/run.ts), each the
519
- * vendor's documented behaviour. A vendor whose descriptor names npm clients and has no cases fails it, unless the
520
- * descriptor's clientCasesNone says why none is owed. */
521
- async function clientCases(pack: string, scratch: string): Promise<{ failures: string[]; note?: string }> {
703
+ * vendor's documented behaviour. Without a built-in module, the current installed workflow can supply that proof;
704
+ * otherwise a vendor naming npm clients needs cases or clientCasesNone saying why none is owed. */
705
+ async function clientCases(pack: string, scratch: string, consumer?: ConsumerResult): Promise<{ failures: string[]; note?: string }> {
522
706
  const vendor = (await manifestOf(pack)).vendor ?? basename(pack);
523
707
  const clients = join(dirname(fileURLToPath(import.meta.url)), 'clients');
524
708
  const extension = extname(fileURLToPath(import.meta.url));
525
709
  if (!existsSync(join(clients, `${vendor}${extension}`))) {
526
- // a vendor whose descriptor names npm clients (adoption.sdks) is held to them: no case is no proof the SDK works,
527
- // unless the descriptor says why none is owed (clientCasesNone: no official SDK, or none the demand's applications
528
- // use); one an application calls with no SDK of the vendor's (a raw HTTP catalog) has no client to drive
529
710
  const descriptor = ((await manifestOf(pack)) as { descriptor?: { adoption?: { sdks?: string[] }; clientCasesNone?: string } }).descriptor;
530
711
  const sdks = descriptor?.adoption?.sdks ?? [];
531
712
  if (descriptor?.clientCasesNone) return { failures: [], note: `${vendor} owes no client case: ${descriptor.clientCasesNone}` };
532
- return sdks.length
533
- ? { failures: [`${vendor} names npm clients (${sdks.join(', ')}) and has no client cases: add packages/twin-standard/src/clients/${vendor}.ts, the SDK driving the pack as an application does, or the descriptor's clientCasesNone saying why none is owed`] }
534
- : { failures: [], note: `${vendor} declares no SDK of its own` };
713
+ if (!sdks.length) return { failures: [], note: `${vendor} declares no SDK of its own` };
714
+ const proof = consumerClientProof(pack, vendor, sdks, consumer);
715
+ return { ...proof, failures: proof.failures.map((failure) => `${vendor} names npm clients (${sdks.join(', ')}) and has no built-in client cases: ${failure}`) };
535
716
  }
536
- const descriptor = ((await manifestOf(pack)) as { descriptor?: { hosts?: Array<{ host?: string; suffix?: string }>; hostsNone?: string; credentialDoor?: { path: string; body: Record<string, unknown>; fill: Record<string, string> } } }).descriptor;
717
+ const descriptor = ((await manifestOf(pack)) as { descriptor?: PackDescriptor }).descriptor;
537
718
  // the first host that names one: a descriptor may lead with a suffix (a region's hosts under one domain), which no
538
719
  // request addresses; the case's client reaches whatever host its issued credentials name
539
720
  // a raw protocol's pack (hostsNone: SMTP, whose relay is whatever host the operator names) is reached by its TCP
540
721
  // connection alone; its client names a stand-in host the connection never resolves
541
- const host = descriptor?.hosts?.find((h) => h.host)?.host ?? (descriptor?.hostsNone ? 'relay.world.test' : undefined);
722
+ const host = descriptor?.hosts?.flatMap((h) => 'host' in h && h.exclude !== true ? [h.host] : [])[0] ?? (descriptor?.hostsNone ? 'relay.world.test' : undefined);
542
723
  if (!host) return { failures: ['the descriptor names no host for its client to address'] };
543
724
  const scenarioPath = join(scratch, 'client-scenario.json');
544
725
  writeFileSync(scenarioPath, '{"handlers":[]}');
545
- const env = (globalThis as { process: { env: Record<string, string | undefined> } }).process.env;
546
- const before = env.VOLTER_WORLD_BOOT_ID;
547
- env.VOLTER_WORLD_BOOT_ID = 'conformance';
548
- let server: Served;
549
- try { server = await serve(pack, { root: join(scratch, 'client-world'), scenarioPath }); } finally { if (before === undefined) delete env.VOLTER_WORLD_BOOT_ID; else env.VOLTER_WORLD_BOOT_ID = before; }
726
+ const server = await serve(pack, { root: join(scratch, 'client-world'), scenarioPath });
550
727
  try {
551
728
  // the World's credentials: what the pack's credential door issues, by the variable a World sets each in
552
729
  const issued: Record<string, string> = {};
553
730
  const door = descriptor?.credentialDoor;
554
731
  if (door) {
555
- // a door that answers nothing is the pack's failure, never conform's hang (a pack served with a scenario that answered
556
- // nothing once held this request open for good)
557
- const abort = new AbortController();
558
- const asked = await within(10_000, fetch(`http://127.0.0.1:${server.port}${door.path}`, { method: 'POST', headers: { host, 'content-type': 'application/json' }, body: JSON.stringify(door.body), signal: abort.signal }).then((r) => r.json().catch(() => ({}))));
559
- abort.abort();
560
- if (asked === 'timeout') return { failures: [`the credential door (POST ${door.path}) answered nothing in 10 s, served with a scenario loaded`] };
561
- const answer = asked as Record<string, unknown>;
562
- for (const [name, field] of Object.entries(door.fill)) if (answer[field] !== undefined) issued[name] = String(answer[field]);
732
+ Object.assign(issued, (await credentialFields(server, door, host)).outputs);
563
733
  }
564
734
  const job = join(scratch, 'client-job.json');
565
- // a release carries its facts (generated/pack-facts.json, `twin-standard facts`); a source checkout does not, so its
566
- // facts are compiled here, as that command compiles them, for the client the case runs
735
+ // a pack carries its facts (generated/pack-facts.json, `twin-standard facts`), in source and release alike; one that
736
+ // has none yet has them compiled here, as that command compiles them, for the client the case runs
567
737
  let packFacts = join(pack, 'generated', 'pack-facts.json');
568
738
  if (!existsSync(packFacts)) {
569
739
  const { compilePackFacts, renderPackFacts } = await import('./pack-facts.ts');
@@ -624,7 +794,7 @@ function records(pack: string): string[] {
624
794
 
625
795
  /** Every check over the pack at `dir`, in order. */
626
796
  /** `onCheck` hears each check's id as it starts, so a caller can show where a long run stands. */
627
- export async function conformance(dir: string, options: { onCheck?: (id: string) => void } = {}): Promise<ConformanceResult[]> {
797
+ export async function conformance(dir: string, options: { onCheck?: (id: string) => void; consumer?: ConsumerResult } = {}): Promise<ConformanceResult[]> {
628
798
  const pack = resolve(dir);
629
799
  if (!existsSync(join(pack, 'package.json'))) throw new Error(`${dir} is no pack (no package.json): conformance takes a pack's directory`);
630
800
  // the kernel the pack itself resolves: one registry, one serve seam, the pack's own
@@ -647,11 +817,11 @@ export async function conformance(dir: string, options: { onCheck?: (id: string)
647
817
  await (async (): Promise<ConformanceResult> => {
648
818
  options.onCheck?.('client');
649
819
  const asks = "driven by the vendor's own client (its official SDK), served as a World runs it, the vendor's documented behaviour holds";
650
- try { const r = await clientCases(pack, scratch); return { id: 'client', asks, failures: r.failures, ...(r.note ? { note: r.note } : {}) }; } catch (error) { return { id: 'client', asks, failures: [`the check could not run: ${(error as Error).message.split('\n')[0]}`] }; }
820
+ try { const r = await clientCases(pack, scratch, options.consumer); return { id: 'client', asks, failures: r.failures, ...(r.note ? { note: r.note } : {}) }; } catch (error) { return { id: 'client', asks, failures: [`the check could not run: ${(error as Error).message.split('\n')[0]}`] }; }
651
821
  })(),
652
822
  await run('life', 'the life walked over HTTP against the pack served as a World runs it, with its scenario: every check held', () => lifeServed(pack)),
653
823
  await run('standalone', 'served by its server.ts as a World runs it, the pack answers HTTP and the boot probe, and each declared socket upgrades', () => standalone(pack)),
654
- await run('surface', 'every served operation called over HTTP as an application calls it, plainly and with a scenario loaded: answered within 10 s, no undeclared 500, no field of the twin\'s own, a success in the spec\'s shape, and every refresh list a fresh account reads answering', () => surfaceSwept(pack, scratch)),
824
+ await run('surface', 'every served operation called over HTTP as an application calls it, plainly and with a scenario loaded: answered within 10 s, no undeclared 500, no field of the twin\'s own, a success in the spec\'s shape, and every refresh list answering, remaining unread under the native 403 contract, or matching its declared removal at the World clock', () => surfaceSwept(pack, scratch, runtime, options.consumer)),
655
825
  await run('records', 'each demand row cites the line it was read at, the spec\'s record names a vendored file that is there, the life names its author, the package has its README', () => records(pack)),
656
826
  await run('form', 'every unit passes the form section of the grade (the gate): the layout, the handler and kernel contracts, the manifest as data, the rules an author\'s code holds', () => formOf(pack)),
657
827
  ];