@volter/twin-standard 1.0.63 → 1.0.64

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.
@@ -109,10 +109,10 @@ export async function assessPack(packDir, options = {}) {
109
109
  const httpOnly = life.steps.every((step) => 'method' in step || 'wait' in step);
110
110
  if (options.browser && httpOnly) {
111
111
  const { chromiumServed } = await import("./browser-assessment.js");
112
- const transport = await chromiumServed(packDir, life.host ?? 'twin.local');
112
+ const transport = await chromiumServed(packDir, life.host ?? 'twin.local', 'http-journey');
113
113
  try {
114
114
  const a = await replay(transport.served), b = await replay(transport.served);
115
- browser = { measured: true, target: 'Chromium fetch, cookie jar and HTTP response behavior; no DOM interaction coverage', version: transport.version, answered: a.trace.length, failures: [...a.failures, ...b.failures], replay: { equal: a.trace.length > 0 && json(a.trace) === json(b.trace), firstSha256: hash(a.trace), secondSha256: hash(b.trace), runs: 2 }, operationCoverage: coverageOf(a), domCoverage: null, clock: transport.clock };
115
+ browser = { measured: true, target: 'HTTP customer journey replay through Chromium at each request origin; authored HTTP headers and observed wire responses', responseObservation: 'Transport observes status, headers and body, including redirects and Set-Cookie; these are not JavaScript-visible response claims', applicationOriginCorsCoverage: null, cookieJarCoverage: null, version: transport.version, answered: a.trace.length, failures: [...a.failures, ...b.failures], replay: { equal: a.trace.length > 0 && json(a.trace) === json(b.trace), firstSha256: hash(a.trace), secondSha256: hash(b.trace), runs: 2 }, operationCoverage: coverageOf(a), domCoverage: null, clock: transport.clock };
116
116
  }
117
117
  finally {
118
118
  await transport.stop();
@@ -1,5 +1,5 @@
1
1
  import type { ServedTwin } from './walk/behavior-journey.js';
2
- export declare function chromiumServed(packDir: string, host: string): Promise<{
2
+ export declare function chromiumServed(packDir: string, host: string, mode?: 'browser-fetch' | 'http-journey'): Promise<{
3
3
  served: ServedTwin;
4
4
  version: string;
5
5
  clock: {
@@ -11,7 +11,7 @@ import { join } from 'node:path';
11
11
  import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
12
12
  import { tmpdir } from 'node:os';
13
13
  import { worldNow } from '@volter/world-core/runtime';
14
- export async function chromiumServed(packDir, host) {
14
+ export async function chromiumServed(packDir, host, mode = 'browser-fetch') {
15
15
  const { chromium } = await import('playwright');
16
16
  const mod = await import(__rewriteRelativeImportExtension(join(packDir, 'src/index.ts')));
17
17
  const factory = Object.entries(mod).find(([name, value]) => /^create\w+Fetch$/.test(name) && !/TwinFetch$/.test(name) && typeof value === 'function')?.[1];
@@ -43,11 +43,12 @@ export async function chromiumServed(packDir, host) {
43
43
  syncClock();
44
44
  const fetchPack = factory({ root, ...options });
45
45
  const context = await browser.newContext({ serviceWorkers: 'block', locale: 'en-US', timezoneId: 'UTC' });
46
- const page = await context.newPage();
47
- const harness = `https://${host}/__catalog_assessment__`;
46
+ const pages = [];
47
+ const pending = new Map();
48
+ const harnesses = new Set();
48
49
  await context.route('**/*', async (route) => {
49
50
  const request = route.request();
50
- if (request.url() === harness) {
51
+ if (request.isNavigationRequest() && harnesses.has(request.url())) {
51
52
  await route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><title>Catalog assessment</title>' });
52
53
  return;
53
54
  }
@@ -55,8 +56,12 @@ export async function chromiumServed(packDir, host) {
55
56
  await route.abort('blockedbyclient');
56
57
  return;
57
58
  }
59
+ const observation = pending.get(request.frame().page());
58
60
  try {
59
- const response = await fetchPack(new Request(request.url(), { method: request.method(), headers: await request.allHeaders(), ...(request.postDataBuffer() ? { body: new Uint8Array(request.postDataBuffer()).buffer } : {}) }));
61
+ if (mode === 'http-journey' && !observation)
62
+ throw new Error('HTTP journey request has no authored input');
63
+ // HTTP transcripts explicitly name actors and headers; strict browser checks use the native wire instead.
64
+ const response = await fetchPack(new Request(request.url(), { method: request.method(), headers: mode === 'http-journey' ? observation.request.headers : await request.allHeaders(), ...(request.postDataBuffer() ? { body: new Uint8Array(request.postDataBuffer()).buffer } : {}) }));
60
65
  const headers = Object.fromEntries(response.headers);
61
66
  // Playwright otherwise adds CORS permission to cross-origin fulfillments. Absence means denial on the vendor wire.
62
67
  if (!Object.hasOwn(headers, 'access-control-allow-origin'))
@@ -64,31 +69,65 @@ export async function chromiumServed(packDir, host) {
64
69
  const cookies = response.headers.getSetCookie();
65
70
  if (cookies.length)
66
71
  headers['set-cookie'] = cookies.join('\n');
67
- await route.fulfill({ status: response.status, headers, body: Buffer.from(await response.arrayBuffer()) });
72
+ const bytes = Buffer.from(await response.arrayBuffer());
73
+ if (observation)
74
+ observation.response = new Response(request.method() === 'HEAD' || [204, 205, 304].includes(response.status) ? null : bytes, { status: response.status, headers: response.headers });
75
+ await route.fulfill({ status: response.status, headers, body: bytes });
68
76
  }
69
- catch {
77
+ catch (error) {
78
+ if (observation)
79
+ observation.error = error;
70
80
  await route.abort('failed');
71
81
  }
72
82
  });
73
- await page.goto(harness);
74
- await page.clock.setFixedTime(syncClock());
83
+ const acquire = async (origin) => {
84
+ const available = pages.find((entry) => entry.origin === origin && !entry.busy);
85
+ if (available) {
86
+ available.busy = true;
87
+ return available;
88
+ }
89
+ const page = await context.newPage();
90
+ const entry = { page, origin, busy: true };
91
+ pages.push(entry);
92
+ const harness = `${origin}/__catalog_assessment__`;
93
+ harnesses.add(harness);
94
+ await page.goto(harness);
95
+ return entry;
96
+ };
75
97
  return {
76
98
  scheme: 'https',
77
99
  fetch: async (request, options) => {
78
- await page.clock.setFixedTime(syncClock());
79
- const body = request.body ? Buffer.from(await request.arrayBuffer()).toString('base64') : null;
80
- const result = await page.evaluate(async ({ url, method, headers, body, credentials }) => {
81
- const bytes = body === null ? undefined : Uint8Array.from(atob(body), (char) => char.charCodeAt(0));
82
- const response = await fetch(url, { method, headers, body: bytes, redirect: 'manual', credentials });
83
- const data = new Uint8Array(await response.arrayBuffer());
84
- let text = '';
85
- for (let i = 0; i < data.length; i += 32768)
86
- text += String.fromCharCode(...data.subarray(i, i + 32768));
87
- return { status: response.status, headers: [...response.headers], body: btoa(text) };
88
- }, { url: request.url, method: request.method, headers: Object.fromEntries(request.headers), body, credentials: options?.credentials ?? 'same-origin' });
89
- if (!result.status)
90
- throw new Error('Chromium returned an opaque response; this journey needs browser-specific navigation assertions');
91
- return new Response([101, 204, 205, 304].includes(result.status) || request.method === 'HEAD' ? null : Buffer.from(result.body, 'base64'), { status: result.status, headers: result.headers });
100
+ const entry = await acquire(mode === 'http-journey' ? new URL(request.url).origin : `https://${host}`);
101
+ const { page } = entry;
102
+ const observation = { request };
103
+ pending.set(page, observation);
104
+ try {
105
+ await page.clock.setFixedTime(syncClock());
106
+ const body = request.body ? Buffer.from(await request.arrayBuffer()).toString('base64') : null;
107
+ const result = await page.evaluate(async ({ url, method, headers, body, credentials }) => {
108
+ const bytes = body === null ? undefined : Uint8Array.from(atob(body), (char) => char.charCodeAt(0));
109
+ const response = await fetch(url, { method, headers, body: bytes, redirect: 'manual', credentials });
110
+ const data = new Uint8Array(await response.arrayBuffer());
111
+ let text = '';
112
+ for (let i = 0; i < data.length; i += 32768)
113
+ text += String.fromCharCode(...data.subarray(i, i + 32768));
114
+ return { status: response.status, headers: [...response.headers], body: btoa(text) };
115
+ }, { url: request.url, method: request.method, headers: Object.fromEntries(request.headers), body, credentials: options?.credentials ?? 'same-origin' });
116
+ if (mode === 'http-journey') {
117
+ if (observation.error)
118
+ throw observation.error;
119
+ if (!observation.response)
120
+ throw new Error('Chromium request produced no observed HTTP response');
121
+ return observation.response;
122
+ }
123
+ if (!result.status)
124
+ throw new Error('Chromium returned an opaque response; this journey needs browser-specific navigation assertions');
125
+ return new Response([101, 204, 205, 304].includes(result.status) || request.method === 'HEAD' ? null : Buffer.from(result.body, 'base64'), { status: result.status, headers: result.headers });
126
+ }
127
+ finally {
128
+ pending.delete(page);
129
+ entry.busy = false;
130
+ }
92
131
  },
93
132
  stop: () => context.close(),
94
133
  };
@@ -16,5 +16,12 @@ export declare function verifyBrowserTransport(pack: string): Promise<{
16
16
  binaryBytes: number;
17
17
  corsEnforced: boolean;
18
18
  credentialModes: boolean;
19
+ httpJourney: {
20
+ perRequestOrigin: boolean;
21
+ authoredActorHeaders: boolean;
22
+ redirectCaptures: boolean;
23
+ setCookieCaptures: boolean;
24
+ dispatchExactlyOnce: boolean;
25
+ };
19
26
  domCoverage: null;
20
27
  }>;
@@ -26,9 +26,12 @@ export async function verifyBrowserTransport(pack) {
26
26
  { id: 'unused', method: 'get', path: '/unused', class: 'get' },
27
27
  { id: 'publicCrossOrigin', method: 'get', path: '/public', class: 'get' },
28
28
  { id: 'credentialedCrossOrigin', method: 'get', path: '/credentialed', class: 'get' },
29
+ { id: 'redirect', method: 'post', path: '/redirect', class: 'action' },
29
30
  ] }, handlers: {
30
31
  login: async () => new Response(null, { status: 204, headers: [['set-cookie', 'session=fixture; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=3600'], ['set-cookie', 'second=fixture; Path=/; Secure; SameSite=Strict; Expires=Thu, 01 Jan 2026 01:00:00 GMT']] }),
31
32
  identity: async ({ request }) => {
33
+ const actor = request.headers.get('cookie')?.match(/actor=([^;]+)/)?.[1];
34
+ if (actor) return Response.json({ actor, origin: request.headers.get('origin') });
32
35
  const session = request.headers.get('cookie')?.includes('session=fixture') || false;
33
36
  const second = request.headers.get('cookie')?.includes('second=fixture') || false;
34
37
  return Response.json({ authenticated: session && second, session, second });
@@ -37,6 +40,7 @@ export async function verifyBrowserTransport(pack) {
37
40
  head: async () => new Response(null, { status: 204 }),
38
41
  publicCrossOrigin: async () => Response.json({ public: true }, { headers: { 'access-control-allow-origin': '*' } }),
39
42
  credentialedCrossOrigin: async () => Response.json({ credentialed: true }, { headers: { 'access-control-allow-origin': 'https://browser-fixture.test', 'access-control-allow-credentials': 'true' } }),
43
+ redirect: async () => new Response(null, { status: 302, headers: [['location', 'https://console-fixture.test/identity'], ['set-cookie', 'actor=second; Path=/; Secure; HttpOnly'], ['set-cookie', 'redirect=fixture; Path=/; Secure']] }),
40
44
  }, gap: async () => Response.json({ error: 'unsupported' }, { status: 404 }) });
41
45
  }
42
46
  `);
@@ -46,6 +50,9 @@ export async function verifyBrowserTransport(pack) {
46
50
  const seen = new Set();
47
51
  const unsubscribe = observeDerivedDispatch(({ operation }) => seen.add(operation.id));
48
52
  const replays = [];
53
+ let redirects = 0;
54
+ const stopRedirects = observeDerivedDispatch(({ operation }) => { if (operation.id === 'redirect')
55
+ redirects++; });
49
56
  try {
50
57
  for (let run = 0; run < 2; run++) {
51
58
  progress(`replay-${run + 1}:start`);
@@ -79,6 +86,7 @@ export async function verifyBrowserTransport(pack) {
79
86
  await assert.rejects(() => served.fetch(new Request('https://another-origin.test/public'), { credentials: 'include' }), /Failed to fetch/, 'credentialed fetch refuses wildcard permission');
80
87
  progress(`replay-${run + 1}:credentialed-wildcard-denied`);
81
88
  assert.deepEqual(await (await served.fetch(new Request('https://another-origin.test/credentialed'), { credentials: 'include' })).json(), { credentialed: true }, 'credentialed fetch requires matching origin and credential permission');
89
+ await assert.rejects(() => fetch('/redirect', 'POST'), /opaque response/, 'strict Fetch must not expose HTTP redirect captures');
82
90
  progress(`replay-${run + 1}:complete`);
83
91
  replays.push({ identity, expired, binaryBytes: binary.byteLength, headStatus: head.status, gapStatus: 404 });
84
92
  }
@@ -87,10 +95,42 @@ export async function verifyBrowserTransport(pack) {
87
95
  }
88
96
  }
89
97
  assert.deepEqual(replays[0], replays[1]);
90
- assert.deepEqual([...seen].sort(), ['binary', 'credentialedCrossOrigin', 'gap', 'head', 'identity', 'login', 'publicCrossOrigin']);
91
- return { purpose: 'browser-transport-fixture', passed: true, chromium: browser.version, replays: 2, exercisedOperations: seen.size, declaredOperations: 8, cookieJar: true, cookieExpiry: ['Expires', 'Max-Age'], clock: browser.clock, binaryBytes: 100000, corsEnforced: true, credentialModes: true, domCoverage: null };
98
+ setClock('2026-01-01T00:00:00.000Z');
99
+ progress('http-journey:launch');
100
+ const http = await chromiumServed(pack, 'browser-fixture.test', 'http-journey');
101
+ progress('http-journey:launched');
102
+ try {
103
+ for (let run = 0; run < 2; run++) {
104
+ progress(`http-journey-${run + 1}:start`);
105
+ setClock('2026-01-01T00:00:00.000Z');
106
+ const served = await http.served(world.root(), {});
107
+ try {
108
+ const redirect = await served.fetch(new Request('https://api-fixture.test/redirect', { method: 'POST' }));
109
+ progress(`http-journey-${run + 1}:redirect`);
110
+ assert.equal(redirect.status, 302);
111
+ assert.equal(redirect.headers.get('location'), 'https://console-fixture.test/identity');
112
+ assert.equal(redirect.headers.getSetCookie().length, 2, 'HTTP observations retain all Set-Cookie headers');
113
+ for (const actor of ['first', 'second', 'first']) {
114
+ const response = await served.fetch(new Request('https://console-fixture.test/identity', { headers: { cookie: `actor=${actor}`, origin: 'https://authored-app.test' } }));
115
+ assert.deepEqual(await response.json(), { actor, origin: 'https://authored-app.test' }, 'HTTP transcripts retain authored actor and origin headers');
116
+ }
117
+ assert.equal((await served.fetch(new Request('https://third-fixture.test/unknown'))).status, 404, 'changing hosts does not erase real HTTP gaps');
118
+ progress(`http-journey-${run + 1}:complete`);
119
+ }
120
+ finally {
121
+ await served.stop();
122
+ }
123
+ }
124
+ }
125
+ finally {
126
+ await http.stop();
127
+ }
128
+ assert.equal(redirects, 4, 'each strict or HTTP redirect request executes exactly once');
129
+ assert.deepEqual([...seen].sort(), ['binary', 'credentialedCrossOrigin', 'gap', 'head', 'identity', 'login', 'publicCrossOrigin', 'redirect']);
130
+ return { purpose: 'browser-transport-fixture', passed: true, chromium: browser.version, replays: 2, exercisedOperations: seen.size, declaredOperations: 9, cookieJar: true, cookieExpiry: ['Expires', 'Max-Age'], clock: browser.clock, binaryBytes: 100000, corsEnforced: true, credentialModes: true, httpJourney: { perRequestOrigin: true, authoredActorHeaders: true, redirectCaptures: true, setCookieCaptures: true, dispatchExactlyOnce: true }, domCoverage: null };
92
131
  }
93
132
  finally {
133
+ stopRedirects();
94
134
  unsubscribe();
95
135
  try {
96
136
  await browser.stop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/twin-standard",
3
- "version": "1.0.63",
3
+ "version": "1.0.64",
4
4
  "description": "The Protocol 3 standard for Volter twin packs: the criteria a pack is graded by, the grader, the gate a pack repository and the twin catalog run on every change and submission, and the spec derivation the grade checks against. Pin a version: every place a pack is judged judges by that standard.",
5
5
  "keywords": [
6
6
  "twin",
@@ -75,7 +75,7 @@
75
75
  "yaml": "2.9.0"
76
76
  },
77
77
  "peerDependencies": {
78
- "@volter/world-core": "3.0.71",
78
+ "@volter/world-core": "3.0.72",
79
79
  "@volter/world-runtime": "*",
80
80
  "playwright": "^1.63.0"
81
81
  },
package/src/assessment.ts CHANGED
@@ -81,10 +81,10 @@ export async function assessPack(packDir: string, options: AssessmentOptions = {
81
81
  const httpOnly = life.steps.every((step) => 'method' in step || 'wait' in step);
82
82
  if (options.browser && httpOnly) {
83
83
  const { chromiumServed } = await import('./browser-assessment.ts');
84
- const transport = await chromiumServed(packDir, life.host ?? 'twin.local');
84
+ const transport = await chromiumServed(packDir, life.host ?? 'twin.local', 'http-journey');
85
85
  try {
86
86
  const a = await replay(transport.served), b = await replay(transport.served);
87
- browser = { measured: true, target: 'Chromium fetch, cookie jar and HTTP response behavior; no DOM interaction coverage', version: transport.version, answered: a.trace.length, failures: [...a.failures, ...b.failures], replay: { equal: a.trace.length > 0 && json(a.trace) === json(b.trace), firstSha256: hash(a.trace), secondSha256: hash(b.trace), runs: 2 }, operationCoverage: coverageOf(a), domCoverage: null, clock: transport.clock };
87
+ browser = { measured: true, target: 'HTTP customer journey replay through Chromium at each request origin; authored HTTP headers and observed wire responses', responseObservation: 'Transport observes status, headers and body, including redirects and Set-Cookie; these are not JavaScript-visible response claims', applicationOriginCorsCoverage: null, cookieJarCoverage: null, version: transport.version, answered: a.trace.length, failures: [...a.failures, ...b.failures], replay: { equal: a.trace.length > 0 && json(a.trace) === json(b.trace), firstSha256: hash(a.trace), secondSha256: hash(b.trace), runs: 2 }, operationCoverage: coverageOf(a), domCoverage: null, clock: transport.clock };
88
88
  } finally { await transport.stop(); }
89
89
  }
90
90
  const conform = options.conformance === false ? null : await conformance(packDir);
@@ -4,7 +4,7 @@ import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { worldNow } from '@volter/world-core/runtime';
6
6
  import type { ServedTwin } from './walk/behavior-journey.ts';
7
- export async function chromiumServed(packDir: string, host: string) {
7
+ export async function chromiumServed(packDir: string, host: string, mode: 'browser-fetch' | 'http-journey' = 'browser-fetch') {
8
8
  const { chromium } = await import('playwright');
9
9
  const mod = await import(join(packDir, 'src/index.ts')) as Record<string, unknown>;
10
10
  const factory = Object.entries(mod).find(([name, value]) => /^create\w+Fetch$/.test(name) && !/TwinFetch$/.test(name) && typeof value === 'function')?.[1] as ((options: import('@volter/world-core').PackFetchOptions) => (request: Request) => Promise<Response>) | undefined;
@@ -29,39 +29,66 @@ export async function chromiumServed(packDir: string, host: string) {
29
29
  syncClock();
30
30
  const fetchPack = factory({ root, ...options });
31
31
  const context = await browser.newContext({ serviceWorkers: 'block', locale: 'en-US', timezoneId: 'UTC' });
32
- const page = await context.newPage();
33
- const harness = `https://${host}/__catalog_assessment__`;
32
+ type Page = Awaited<ReturnType<typeof context.newPage>>;
33
+ const pages: Array<{ page: Page; origin: string; busy: boolean }> = [];
34
+ const pending = new Map<Page, { request: Request; response?: Response; error?: unknown }>();
35
+ const harnesses = new Set<string>();
34
36
  await context.route('**/*', async (route) => {
35
37
  const request = route.request();
36
- if (request.url() === harness) { await route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><title>Catalog assessment</title>' }); return; }
38
+ if (request.isNavigationRequest() && harnesses.has(request.url())) { await route.fulfill({ status: 200, contentType: 'text/html', body: '<!doctype html><title>Catalog assessment</title>' }); return; }
37
39
  if (!['xhr', 'fetch'].includes(request.resourceType())) { await route.abort('blockedbyclient'); return; }
40
+ const observation = pending.get(request.frame().page());
38
41
  try {
39
- const response = await fetchPack(new Request(request.url(), { method: request.method(), headers: await request.allHeaders(), ...(request.postDataBuffer() ? { body: new Uint8Array(request.postDataBuffer()!).buffer } : {}) }));
42
+ if (mode === 'http-journey' && !observation) throw new Error('HTTP journey request has no authored input');
43
+ // HTTP transcripts explicitly name actors and headers; strict browser checks use the native wire instead.
44
+ const response = await fetchPack(new Request(request.url(), { method: request.method(), headers: mode === 'http-journey' ? observation!.request.headers : await request.allHeaders(), ...(request.postDataBuffer() ? { body: new Uint8Array(request.postDataBuffer()!).buffer } : {}) }));
40
45
  const headers = Object.fromEntries(response.headers);
41
46
  // Playwright otherwise adds CORS permission to cross-origin fulfillments. Absence means denial on the vendor wire.
42
47
  if (!Object.hasOwn(headers, 'access-control-allow-origin')) headers['access-control-allow-origin'] = '';
43
48
  const cookies = response.headers.getSetCookie();
44
49
  if (cookies.length) headers['set-cookie'] = cookies.join('\n');
45
- await route.fulfill({ status: response.status, headers, body: Buffer.from(await response.arrayBuffer()) });
46
- } catch { await route.abort('failed'); }
50
+ const bytes = Buffer.from(await response.arrayBuffer());
51
+ if (observation) observation.response = new Response(request.method() === 'HEAD' || [204, 205, 304].includes(response.status) ? null : bytes, { status: response.status, headers: response.headers });
52
+ await route.fulfill({ status: response.status, headers, body: bytes });
53
+ } catch (error) { if (observation) observation.error = error; await route.abort('failed'); }
47
54
  });
48
- await page.goto(harness);
49
- await page.clock.setFixedTime(syncClock());
55
+ const acquire = async (origin: string) => {
56
+ const available = pages.find((entry) => entry.origin === origin && !entry.busy);
57
+ if (available) { available.busy = true; return available; }
58
+ const page = await context.newPage();
59
+ const entry = { page, origin, busy: true };
60
+ pages.push(entry);
61
+ const harness = `${origin}/__catalog_assessment__`;
62
+ harnesses.add(harness);
63
+ await page.goto(harness);
64
+ return entry;
65
+ };
50
66
  return {
51
67
  scheme: 'https',
52
68
  fetch: async (request: Request, options?: { credentials?: RequestCredentials }) => {
53
- await page.clock.setFixedTime(syncClock());
54
- const body = request.body ? Buffer.from(await request.arrayBuffer()).toString('base64') : null;
55
- const result = await page.evaluate(async ({ url, method, headers, body, credentials }) => {
56
- const bytes = body === null ? undefined : Uint8Array.from(atob(body), (char) => char.charCodeAt(0));
57
- const response = await fetch(url, { method, headers, body: bytes, redirect: 'manual', credentials });
58
- const data = new Uint8Array(await response.arrayBuffer());
59
- let text = '';
60
- for (let i = 0; i < data.length; i += 32768) text += String.fromCharCode(...data.subarray(i, i + 32768));
61
- return { status: response.status, headers: [...response.headers], body: btoa(text) };
62
- }, { url: request.url, method: request.method, headers: Object.fromEntries(request.headers), body, credentials: options?.credentials ?? 'same-origin' });
63
- if (!result.status) throw new Error('Chromium returned an opaque response; this journey needs browser-specific navigation assertions');
64
- return new Response([101, 204, 205, 304].includes(result.status) || request.method === 'HEAD' ? null : Buffer.from(result.body, 'base64'), { status: result.status, headers: result.headers });
69
+ const entry = await acquire(mode === 'http-journey' ? new URL(request.url).origin : `https://${host}`);
70
+ const { page } = entry;
71
+ const observation = { request } as { request: Request; response?: Response; error?: unknown };
72
+ pending.set(page, observation);
73
+ try {
74
+ await page.clock.setFixedTime(syncClock());
75
+ const body = request.body ? Buffer.from(await request.arrayBuffer()).toString('base64') : null;
76
+ const result = await page.evaluate(async ({ url, method, headers, body, credentials }) => {
77
+ const bytes = body === null ? undefined : Uint8Array.from(atob(body), (char) => char.charCodeAt(0));
78
+ const response = await fetch(url, { method, headers, body: bytes, redirect: 'manual', credentials });
79
+ const data = new Uint8Array(await response.arrayBuffer());
80
+ let text = '';
81
+ for (let i = 0; i < data.length; i += 32768) text += String.fromCharCode(...data.subarray(i, i + 32768));
82
+ return { status: response.status, headers: [...response.headers], body: btoa(text) };
83
+ }, { url: request.url, method: request.method, headers: Object.fromEntries(request.headers), body, credentials: options?.credentials ?? 'same-origin' });
84
+ if (mode === 'http-journey') {
85
+ if (observation.error) throw observation.error;
86
+ if (!observation.response) throw new Error('Chromium request produced no observed HTTP response');
87
+ return observation.response;
88
+ }
89
+ if (!result.status) throw new Error('Chromium returned an opaque response; this journey needs browser-specific navigation assertions');
90
+ return new Response([101, 204, 205, 304].includes(result.status) || request.method === 'HEAD' ? null : Buffer.from(result.body, 'base64'), { status: result.status, headers: result.headers });
91
+ } finally { pending.delete(page); entry.busy = false; }
65
92
  },
66
93
  stop: () => context.close(),
67
94
  };
@@ -27,9 +27,12 @@ export async function verifyBrowserTransport(pack: string) {
27
27
  { id: 'unused', method: 'get', path: '/unused', class: 'get' },
28
28
  { id: 'publicCrossOrigin', method: 'get', path: '/public', class: 'get' },
29
29
  { id: 'credentialedCrossOrigin', method: 'get', path: '/credentialed', class: 'get' },
30
+ { id: 'redirect', method: 'post', path: '/redirect', class: 'action' },
30
31
  ] }, handlers: {
31
32
  login: async () => new Response(null, { status: 204, headers: [['set-cookie', 'session=fixture; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=3600'], ['set-cookie', 'second=fixture; Path=/; Secure; SameSite=Strict; Expires=Thu, 01 Jan 2026 01:00:00 GMT']] }),
32
33
  identity: async ({ request }) => {
34
+ const actor = request.headers.get('cookie')?.match(/actor=([^;]+)/)?.[1];
35
+ if (actor) return Response.json({ actor, origin: request.headers.get('origin') });
33
36
  const session = request.headers.get('cookie')?.includes('session=fixture') || false;
34
37
  const second = request.headers.get('cookie')?.includes('second=fixture') || false;
35
38
  return Response.json({ authenticated: session && second, session, second });
@@ -38,6 +41,7 @@ export async function verifyBrowserTransport(pack: string) {
38
41
  head: async () => new Response(null, { status: 204 }),
39
42
  publicCrossOrigin: async () => Response.json({ public: true }, { headers: { 'access-control-allow-origin': '*' } }),
40
43
  credentialedCrossOrigin: async () => Response.json({ credentialed: true }, { headers: { 'access-control-allow-origin': 'https://browser-fixture.test', 'access-control-allow-credentials': 'true' } }),
44
+ redirect: async () => new Response(null, { status: 302, headers: [['location', 'https://console-fixture.test/identity'], ['set-cookie', 'actor=second; Path=/; Secure; HttpOnly'], ['set-cookie', 'redirect=fixture; Path=/; Secure']] }),
41
45
  }, gap: async () => Response.json({ error: 'unsupported' }, { status: 404 }) });
42
46
  }
43
47
  `);
@@ -47,6 +51,8 @@ export async function verifyBrowserTransport(pack: string) {
47
51
  const seen = new Set<string>();
48
52
  const unsubscribe = observeDerivedDispatch(({ operation }) => seen.add(operation.id));
49
53
  const replays = [];
54
+ let redirects = 0;
55
+ const stopRedirects = observeDerivedDispatch(({ operation }) => { if (operation.id === 'redirect') redirects++; });
50
56
  try {
51
57
  for (let run = 0; run < 2; run++) {
52
58
  progress(`replay-${run + 1}:start`);
@@ -79,12 +85,38 @@ export async function verifyBrowserTransport(pack: string) {
79
85
  await assert.rejects(() => served.fetch(new Request('https://another-origin.test/public'), { credentials: 'include' }), /Failed to fetch/, 'credentialed fetch refuses wildcard permission');
80
86
  progress(`replay-${run + 1}:credentialed-wildcard-denied`);
81
87
  assert.deepEqual(await (await served.fetch(new Request('https://another-origin.test/credentialed'), { credentials: 'include' })).json(), { credentialed: true }, 'credentialed fetch requires matching origin and credential permission');
88
+ await assert.rejects(() => fetch('/redirect', 'POST'), /opaque response/, 'strict Fetch must not expose HTTP redirect captures');
82
89
  progress(`replay-${run + 1}:complete`);
83
90
  replays.push({ identity, expired, binaryBytes: binary.byteLength, headStatus: head.status, gapStatus: 404 });
84
91
  } finally { await served.stop(); }
85
92
  }
86
93
  assert.deepEqual(replays[0], replays[1]);
87
- assert.deepEqual([...seen].sort(), ['binary', 'credentialedCrossOrigin', 'gap', 'head', 'identity', 'login', 'publicCrossOrigin']);
88
- return { purpose: 'browser-transport-fixture', passed: true, chromium: browser.version, replays: 2, exercisedOperations: seen.size, declaredOperations: 8, cookieJar: true, cookieExpiry: ['Expires', 'Max-Age'], clock: browser.clock, binaryBytes: 100000, corsEnforced: true, credentialModes: true, domCoverage: null };
89
- } finally { unsubscribe(); try { await browser.stop(); } finally { world.restore(); } }
94
+ setClock('2026-01-01T00:00:00.000Z');
95
+ progress('http-journey:launch');
96
+ const http = await chromiumServed(pack, 'browser-fixture.test', 'http-journey');
97
+ progress('http-journey:launched');
98
+ try {
99
+ for (let run = 0; run < 2; run++) {
100
+ progress(`http-journey-${run + 1}:start`);
101
+ setClock('2026-01-01T00:00:00.000Z');
102
+ const served = await http.served(world.root(), {});
103
+ try {
104
+ const redirect = await served.fetch(new Request('https://api-fixture.test/redirect', { method: 'POST' }));
105
+ progress(`http-journey-${run + 1}:redirect`);
106
+ assert.equal(redirect.status, 302);
107
+ assert.equal(redirect.headers.get('location'), 'https://console-fixture.test/identity');
108
+ assert.equal(redirect.headers.getSetCookie().length, 2, 'HTTP observations retain all Set-Cookie headers');
109
+ for (const actor of ['first', 'second', 'first']) {
110
+ const response = await served.fetch(new Request('https://console-fixture.test/identity', { headers: { cookie: `actor=${actor}`, origin: 'https://authored-app.test' } }));
111
+ assert.deepEqual(await response.json(), { actor, origin: 'https://authored-app.test' }, 'HTTP transcripts retain authored actor and origin headers');
112
+ }
113
+ assert.equal((await served.fetch(new Request('https://third-fixture.test/unknown'))).status, 404, 'changing hosts does not erase real HTTP gaps');
114
+ progress(`http-journey-${run + 1}:complete`);
115
+ } finally { await served.stop(); }
116
+ }
117
+ } finally { await http.stop(); }
118
+ assert.equal(redirects, 4, 'each strict or HTTP redirect request executes exactly once');
119
+ assert.deepEqual([...seen].sort(), ['binary', 'credentialedCrossOrigin', 'gap', 'head', 'identity', 'login', 'publicCrossOrigin', 'redirect']);
120
+ return { purpose: 'browser-transport-fixture', passed: true, chromium: browser.version, replays: 2, exercisedOperations: seen.size, declaredOperations: 9, cookieJar: true, cookieExpiry: ['Expires', 'Max-Age'], clock: browser.clock, binaryBytes: 100000, corsEnforced: true, credentialModes: true, httpJourney: { perRequestOrigin: true, authoredActorHeaders: true, redirectCaptures: true, setCookieCaptures: true, dispatchExactlyOnce: true }, domCoverage: null };
121
+ } finally { stopRedirects(); unsubscribe(); try { await browser.stop(); } finally { world.restore(); } }
90
122
  }