@volter/twin-standard 1.0.35 → 1.0.37
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.
- package/client-sdks/bun.lock +437 -0
- package/client-sdks/package.json +16 -0
- package/dist/client-sdks/bun.lock +437 -0
- package/dist/client-sdks/package.json +16 -0
- package/dist/src/assessment.js +1 -1
- package/dist/src/browser-assessment.d.ts +6 -0
- package/dist/src/browser-assessment.js +38 -6
- package/dist/src/browser-verification.d.ts +19 -0
- package/dist/src/browser-verification.js +86 -0
- package/dist/src/cli.js +2 -1
- package/dist/src/clients/sentry.js +2 -1
- package/dist/src/create-pack.js +2 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/package-file.d.ts +2 -0
- package/dist/src/package-file.js +11 -0
- package/dist/src/walk/behavior-journey.d.ts +1 -1
- package/dist/src/walk/behavior-journey.js +1 -1
- package/package.json +7 -2
- package/src/assessment.ts +1 -1
- package/src/browser-assessment.ts +27 -5
- package/src/browser-verification.ts +74 -0
- package/src/cli.ts +2 -1
- package/src/clients/sentry.ts +2 -1
- package/src/create-pack.ts +2 -1
- package/src/index.ts +1 -0
- package/src/package-file.ts +11 -0
- package/src/walk/behavior-journey.ts +2 -2
|
@@ -2,5 +2,11 @@ import type { ServedTwin } from './walk/behavior-journey.js';
|
|
|
2
2
|
export declare function chromiumServed(packDir: string, host: string): Promise<{
|
|
3
3
|
served: ServedTwin;
|
|
4
4
|
version: string;
|
|
5
|
+
clock: {
|
|
6
|
+
native: string;
|
|
7
|
+
nativePrecisionMs: number;
|
|
8
|
+
javascriptPrecisionMs: number;
|
|
9
|
+
monotonic: string;
|
|
10
|
+
};
|
|
5
11
|
stop: () => Promise<void>;
|
|
6
12
|
}>;
|
|
@@ -8,16 +8,39 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
};
|
|
9
9
|
// Browser transport for the existing journey engine. A container supplies network isolation in catalog execution.
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { worldNow } from '@volter/world-core/runtime';
|
|
11
14
|
export async function chromiumServed(packDir, host) {
|
|
12
15
|
const { chromium } = await import('playwright');
|
|
13
|
-
const browser = await chromium.launch({ args: ['--no-sandbox'] });
|
|
14
16
|
const mod = await import(__rewriteRelativeImportExtension(join(packDir, 'src/index.ts')));
|
|
15
17
|
const factory = Object.entries(mod).find(([name, value]) => /^create\w+Fetch$/.test(name) && !/TwinFetch$/.test(name) && typeof value === 'function')?.[1];
|
|
16
|
-
if (!factory)
|
|
17
|
-
await browser.close();
|
|
18
|
+
if (!factory)
|
|
18
19
|
throw new Error('Browser assessment needs the pack HTTP fetch factory');
|
|
20
|
+
const library = ['/usr/lib/x86_64-linux-gnu/faketime/libfaketimeMT.so.1', '/usr/lib/aarch64-linux-gnu/faketime/libfaketimeMT.so.1'].find(existsSync);
|
|
21
|
+
if (process.platform !== 'linux' || !library)
|
|
22
|
+
throw new Error('Deterministic browser assessment needs Linux libfaketime installed for native cookie time');
|
|
23
|
+
const clockDir = mkdtempSync(join(tmpdir(), 'twin-browser-clock-'));
|
|
24
|
+
const clockFile = join(clockDir, 'clock');
|
|
25
|
+
const syncClock = () => {
|
|
26
|
+
const instant = new Date(worldNow());
|
|
27
|
+
// libfaketime's absolute format stops wall time, at HTTP-date (one-second) precision.
|
|
28
|
+
writeFileSync(clockFile, `${instant.toISOString().slice(0, 19).replace('T', ' ')}\n`);
|
|
29
|
+
return instant;
|
|
30
|
+
};
|
|
31
|
+
syncClock();
|
|
32
|
+
const env = Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined && !entry[0].startsWith('FAKETIME')));
|
|
33
|
+
Object.assign(env, { LD_PRELOAD: library, TZ: 'UTC', FAKETIME_TIMESTAMP_FILE: clockFile, FAKETIME_NO_CACHE: '1', FAKETIME_DISABLE_SHM: '1', FAKETIME_DONT_FAKE_MONOTONIC: '1', NO_FAKE_STAT: '1' });
|
|
34
|
+
let browser;
|
|
35
|
+
try {
|
|
36
|
+
browser = await chromium.launch({ args: ['--no-sandbox'], env });
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
rmSync(clockDir, { recursive: true, force: true });
|
|
40
|
+
throw error;
|
|
19
41
|
}
|
|
20
42
|
const served = async (root, options) => {
|
|
43
|
+
syncClock();
|
|
21
44
|
const fetchPack = factory({ root, ...options });
|
|
22
45
|
const context = await browser.newContext({ serviceWorkers: 'block', locale: 'en-US', timezoneId: 'UTC' });
|
|
23
46
|
const page = await context.newPage();
|
|
@@ -35,6 +58,9 @@ export async function chromiumServed(packDir, host) {
|
|
|
35
58
|
try {
|
|
36
59
|
const response = await fetchPack(new Request(request.url(), { method: request.method(), headers: await request.allHeaders(), ...(request.postDataBuffer() ? { body: new Uint8Array(request.postDataBuffer()).buffer } : {}) }));
|
|
37
60
|
const headers = Object.fromEntries(response.headers);
|
|
61
|
+
// Playwright otherwise adds CORS permission to cross-origin fulfillments. Absence means denial on the vendor wire.
|
|
62
|
+
if (!Object.hasOwn(headers, 'access-control-allow-origin'))
|
|
63
|
+
headers['access-control-allow-origin'] = '';
|
|
38
64
|
const cookies = response.headers.getSetCookie();
|
|
39
65
|
if (cookies.length)
|
|
40
66
|
headers['set-cookie'] = cookies.join('\n');
|
|
@@ -45,9 +71,10 @@ export async function chromiumServed(packDir, host) {
|
|
|
45
71
|
}
|
|
46
72
|
});
|
|
47
73
|
await page.goto(harness);
|
|
48
|
-
await page.clock.setFixedTime(
|
|
74
|
+
await page.clock.setFixedTime(syncClock());
|
|
49
75
|
return {
|
|
50
76
|
fetch: async (request) => {
|
|
77
|
+
await page.clock.setFixedTime(syncClock());
|
|
51
78
|
const body = request.body ? Buffer.from(await request.arrayBuffer()).toString('base64') : null;
|
|
52
79
|
const result = await page.evaluate(async ({ url, method, headers, body }) => {
|
|
53
80
|
const bytes = body === null ? undefined : Uint8Array.from(atob(body), (char) => char.charCodeAt(0));
|
|
@@ -62,8 +89,13 @@ export async function chromiumServed(packDir, host) {
|
|
|
62
89
|
throw new Error('Chromium returned an opaque response; this journey needs browser-specific navigation assertions');
|
|
63
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 });
|
|
64
91
|
},
|
|
65
|
-
stop: () =>
|
|
92
|
+
stop: () => context.close(),
|
|
66
93
|
};
|
|
67
94
|
};
|
|
68
|
-
return { served, version: browser.version(), stop: () =>
|
|
95
|
+
return { served, version: browser.version(), clock: { native: 'World time through libfaketime', nativePrecisionMs: 1000, javascriptPrecisionMs: 1, monotonic: 'elapsed machine time' }, stop: async () => { try {
|
|
96
|
+
await browser.close();
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
rmSync(clockDir, { recursive: true, force: true });
|
|
100
|
+
} } };
|
|
69
101
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare function verifyBrowserTransport(pack: string): Promise<{
|
|
2
|
+
purpose: string;
|
|
3
|
+
passed: boolean;
|
|
4
|
+
chromium: string;
|
|
5
|
+
replays: number;
|
|
6
|
+
exercisedOperations: number;
|
|
7
|
+
declaredOperations: number;
|
|
8
|
+
cookieJar: boolean;
|
|
9
|
+
cookieExpiry: string[];
|
|
10
|
+
clock: {
|
|
11
|
+
native: string;
|
|
12
|
+
nativePrecisionMs: number;
|
|
13
|
+
javascriptPrecisionMs: number;
|
|
14
|
+
monotonic: string;
|
|
15
|
+
};
|
|
16
|
+
binaryBytes: number;
|
|
17
|
+
corsEnforced: boolean;
|
|
18
|
+
domCoverage: null;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Regression of the standard's own browser transport. This fixture is not a pack or an admission verdict.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { getActiveWorldStore, observeDerivedDispatch } from '@volter/world-core/runtime';
|
|
8
|
+
import { inMemoryWorld } from "./walk/behavior-journey.js";
|
|
9
|
+
import { chromiumServed } from "./browser-assessment.js";
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
export async function verifyBrowserTransport(pack) {
|
|
12
|
+
assert.ok(process.env.VOLTER_WORLD, 'browser transport verification must run through a World');
|
|
13
|
+
assert.ok(!existsSync(pack), 'browser fixture directory must be new and task-owned');
|
|
14
|
+
mkdirSync(join(pack, 'src'), { recursive: true });
|
|
15
|
+
writeFileSync(join(pack, 'src/index.ts'), `
|
|
16
|
+
import { createDerivedFetch } from ${JSON.stringify(pathToFileURL(require.resolve('@volter/world-core')).href)};
|
|
17
|
+
export function createFixtureFetch() {
|
|
18
|
+
return createDerivedFetch({ surface: { version: '1', operations: [
|
|
19
|
+
{ id: 'login', method: 'post', path: '/login', class: 'action' },
|
|
20
|
+
{ id: 'identity', method: 'get', path: '/identity', class: 'get' },
|
|
21
|
+
{ id: 'binary', method: 'get', path: '/binary', class: 'get' },
|
|
22
|
+
{ id: 'head', method: 'head', path: '/head', class: 'get' },
|
|
23
|
+
{ id: 'gap', method: 'get', path: '/gap', class: 'get' },
|
|
24
|
+
{ id: 'unused', method: 'get', path: '/unused', class: 'get' },
|
|
25
|
+
] }, handlers: {
|
|
26
|
+
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']] }),
|
|
27
|
+
identity: async ({ request }) => {
|
|
28
|
+
const session = request.headers.get('cookie')?.includes('session=fixture') || false;
|
|
29
|
+
const second = request.headers.get('cookie')?.includes('second=fixture') || false;
|
|
30
|
+
return Response.json({ authenticated: session && second, session, second });
|
|
31
|
+
},
|
|
32
|
+
binary: async () => new Response(new Uint8Array(100000).fill(197), { headers: { 'content-type': 'application/octet-stream' } }),
|
|
33
|
+
head: async () => new Response(null, { status: 204 }),
|
|
34
|
+
}, gap: async () => Response.json({ error: 'unsupported' }, { status: 404 }) });
|
|
35
|
+
}
|
|
36
|
+
`);
|
|
37
|
+
const world = inMemoryWorld('browser-transport-verification');
|
|
38
|
+
const setClock = (instant) => getActiveWorldStore().write(process.env.TWIN_WORLD_CLOCK_FILE, instant);
|
|
39
|
+
const browser = await chromiumServed(pack, 'browser-fixture.test').catch((error) => { world.restore(); throw error; });
|
|
40
|
+
const seen = new Set();
|
|
41
|
+
const unsubscribe = observeDerivedDispatch(({ operation }) => seen.add(operation.id));
|
|
42
|
+
const replays = [];
|
|
43
|
+
try {
|
|
44
|
+
for (let run = 0; run < 2; run++) {
|
|
45
|
+
setClock('2026-01-01T00:00:00.000Z');
|
|
46
|
+
const served = await browser.served(world.root(), {});
|
|
47
|
+
try {
|
|
48
|
+
const fetch = (path, method = 'GET') => served.fetch(new Request(`https://browser-fixture.test${path}`, { method }));
|
|
49
|
+
assert.equal((await (await fetch('/identity')).json()).authenticated, false, 'fresh contexts must not inherit cookies');
|
|
50
|
+
assert.equal((await fetch('/login', 'POST')).status, 204);
|
|
51
|
+
const identity = await (await fetch('/identity')).json();
|
|
52
|
+
assert.equal(identity.authenticated, true, 'the browser cookie jar must reach the twin');
|
|
53
|
+
setClock('2026-01-01T00:59:59.000Z');
|
|
54
|
+
assert.deepEqual(await (await fetch('/identity')).json(), identity, 'both cookie types must remain before their World-time expiry');
|
|
55
|
+
setClock('2026-01-01T01:00:01.000Z');
|
|
56
|
+
const expired = await (await fetch('/identity')).json();
|
|
57
|
+
assert.deepEqual(expired, { authenticated: false, session: false, second: false }, 'native Expires and Max-Age must follow World time');
|
|
58
|
+
const binary = new Uint8Array(await (await fetch('/binary')).arrayBuffer());
|
|
59
|
+
assert.equal(binary.byteLength, 100000);
|
|
60
|
+
assert.ok(binary.every((byte) => byte === 197));
|
|
61
|
+
const head = await fetch('/head', 'HEAD');
|
|
62
|
+
assert.equal(head.status, 204);
|
|
63
|
+
assert.equal(await head.text(), '');
|
|
64
|
+
assert.equal((await fetch('/gap')).status, 404);
|
|
65
|
+
assert.equal((await fetch('/unknown')).status, 404);
|
|
66
|
+
await assert.rejects(() => served.fetch(new Request('https://another-origin.test/identity')), /Failed to fetch/);
|
|
67
|
+
replays.push({ identity, expired, binaryBytes: binary.byteLength, headStatus: head.status, gapStatus: 404 });
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await served.stop();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
assert.deepEqual(replays[0], replays[1]);
|
|
74
|
+
assert.deepEqual([...seen].sort(), ['binary', 'gap', 'head', 'identity', 'login']);
|
|
75
|
+
return { purpose: 'browser-transport-fixture', passed: true, chromium: browser.version, replays: 2, exercisedOperations: seen.size, declaredOperations: 6, cookieJar: true, cookieExpiry: ['Expires', 'Max-Age'], clock: browser.clock, binaryBytes: 100000, corsEnforced: true, domCoverage: null };
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
unsubscribe();
|
|
79
|
+
try {
|
|
80
|
+
await browser.stop();
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
world.restore();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/src/cli.js
CHANGED
|
@@ -27,6 +27,7 @@ import { relative, resolve } from 'node:path';
|
|
|
27
27
|
import { unitsOf, p3Gate } from "./gate.js";
|
|
28
28
|
import { grade, STANDARDS } from "./grade.js";
|
|
29
29
|
import { withCheckoutFiles } from "./protocol-3.js";
|
|
30
|
+
import { standardPackageFile } from "./package-file.js";
|
|
30
31
|
import { gatePublished } from "./published.js";
|
|
31
32
|
const [command, ...args] = process.argv.slice(2);
|
|
32
33
|
const usage = () => {
|
|
@@ -34,7 +35,7 @@ const usage = () => {
|
|
|
34
35
|
process.exit(2);
|
|
35
36
|
};
|
|
36
37
|
if (command === '--version') {
|
|
37
|
-
const pkg = JSON.parse(readFileSync(
|
|
38
|
+
const pkg = JSON.parse(readFileSync(standardPackageFile('package.json'), 'utf8'));
|
|
38
39
|
console.log(`@volter/twin-standard ${pkg.version} · ${STANDARDS.map((s) => `${s.id}@${s.version}`).join(' ')}`);
|
|
39
40
|
}
|
|
40
41
|
else if (command === 'gate') {
|
|
@@ -13,9 +13,10 @@ import { existsSync } from 'node:fs';
|
|
|
13
13
|
import { request as httpsRequest } from 'node:https';
|
|
14
14
|
import { createRequire } from 'node:module';
|
|
15
15
|
import { pathToFileURL } from 'node:url';
|
|
16
|
+
import { standardPackageFile } from "../package-file.js";
|
|
16
17
|
// The pins live in ../../client-sdks with their own lockfile, installed here on first use: some carry advisories with no
|
|
17
18
|
// fix in the line an application pins, so the repository's install (and a release, .github/release-scan.mjs) never does.
|
|
18
|
-
const SDKS =
|
|
19
|
+
const SDKS = standardPackageFile('client-sdks/');
|
|
19
20
|
async function sdkOf(name) {
|
|
20
21
|
if (!existsSync(new URL('node_modules/', SDKS))) {
|
|
21
22
|
const install = spawnSync('bun', ['install', '--frozen-lockfile', '--ignore-scripts'], { cwd: SDKS, stdio: 'inherit' });
|
package/dist/src/create-pack.js
CHANGED
|
@@ -33,6 +33,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
33
33
|
import { parseExactJson, exactInteger } from '@volter/world-core/exact-json';
|
|
34
34
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
35
35
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
36
|
+
import { standardPackageFile } from "./package-file.js";
|
|
36
37
|
import { gunzipSync } from 'node:zlib';
|
|
37
38
|
import { parse as parseYaml } from 'yaml';
|
|
38
39
|
import { coreFor } from '@volter/world-core';
|
|
@@ -476,7 +477,7 @@ export async function createPack(args) {
|
|
|
476
477
|
// the package's licence, the platform's (Apache-2.0), as every published pack carries it
|
|
477
478
|
const licence = join(dir, 'LICENSE');
|
|
478
479
|
if (!existsSync(licence)) {
|
|
479
|
-
writeFileSync(licence, readFileSync(
|
|
480
|
+
writeFileSync(licence, readFileSync(standardPackageFile('LICENSE')));
|
|
480
481
|
console.log('create-pack: wrote LICENSE');
|
|
481
482
|
}
|
|
482
483
|
// the handler map each fetch imports, empty until the families are written (--index again then)
|
package/dist/src/index.d.ts
CHANGED
|
@@ -10,3 +10,4 @@ export type { Criterion, CriterionResult, GradeUnit, ScoreReport, Standard } fro
|
|
|
10
10
|
export { compilePackFacts, renderPackFacts, type PackFacts, type PackFactsFile } from './pack-facts.js';
|
|
11
11
|
export { assessPack, type AssessmentOptions } from './assessment.js';
|
|
12
12
|
export { createPack } from './create-pack.js';
|
|
13
|
+
export { verifyBrowserTransport } from './browser-verification.js';
|
package/dist/src/index.js
CHANGED
|
@@ -14,3 +14,4 @@ export { unitAt } from "./unit.js";
|
|
|
14
14
|
export { compilePackFacts, renderPackFacts } from "./pack-facts.js";
|
|
15
15
|
export { assessPack } from "./assessment.js";
|
|
16
16
|
export { createPack } from "./create-pack.js";
|
|
17
|
+
export { verifyBrowserTransport } from "./browser-verification.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
/** Package-owned data beside src/, or beside dist/ when the released entrypoints are compiled. */
|
|
3
|
+
export function standardPackageFile(name) {
|
|
4
|
+
for (const parent of ['../', '../../']) {
|
|
5
|
+
const root = new URL(parent, import.meta.url);
|
|
6
|
+
const manifest = new URL('package.json', root);
|
|
7
|
+
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, 'utf8')).name === '@volter/twin-standard')
|
|
8
|
+
return new URL(name, root);
|
|
9
|
+
}
|
|
10
|
+
throw new Error('Cannot locate the twin-standard package manifest');
|
|
11
|
+
}
|
|
@@ -34,7 +34,7 @@ export type ServedTwin = (root: string, options: {
|
|
|
34
34
|
application?: import('@volter/world-core').PackFetchOptions['application'];
|
|
35
35
|
}) => Promise<{
|
|
36
36
|
fetch: (request: Request) => Promise<Response>;
|
|
37
|
-
stop: () => void
|
|
37
|
+
stop: () => void | Promise<void>;
|
|
38
38
|
socket?: (url: string, headers: Record<string, string>) => WebSocket;
|
|
39
39
|
stream?: TwinStream;
|
|
40
40
|
}>;
|
|
@@ -1094,7 +1094,7 @@ export async function walk(journey, root, collect, opts) {
|
|
|
1094
1094
|
await Promise.allSettled([...pending.values()].map((request) => request.answer));
|
|
1095
1095
|
setDeliveryObserver(previousObserver);
|
|
1096
1096
|
setInProcessTwins(previousTwins);
|
|
1097
|
-
served?.stop();
|
|
1097
|
+
await served?.stop();
|
|
1098
1098
|
if (sqlSession)
|
|
1099
1099
|
await sqlSession.then((session) => session.end(), () => { });
|
|
1100
1100
|
// a World's managed database ends with its walk (vendor-fetch.ts), so the next World declares it at the same port
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-standard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.37",
|
|
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",
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"src",
|
|
20
20
|
"README.md",
|
|
21
21
|
"LICENSE",
|
|
22
|
+
"client-sdks/package.json",
|
|
23
|
+
"client-sdks/bun.lock",
|
|
22
24
|
"!**/*.test.ts",
|
|
23
25
|
"dist"
|
|
24
26
|
],
|
|
@@ -27,6 +29,9 @@
|
|
|
27
29
|
"url": "git+https://github.com/volter-ai/twin-world.git",
|
|
28
30
|
"directory": "packages/twin-standard"
|
|
29
31
|
},
|
|
32
|
+
"assessmentClientSdks": [
|
|
33
|
+
"client-sdks"
|
|
34
|
+
],
|
|
30
35
|
"homepage": "https://github.com/volter-ai/twin-world/tree/main/packages/twin-standard#readme",
|
|
31
36
|
"type": "module",
|
|
32
37
|
"exports": {
|
|
@@ -70,7 +75,7 @@
|
|
|
70
75
|
"yaml": "2.9.0"
|
|
71
76
|
},
|
|
72
77
|
"peerDependencies": {
|
|
73
|
-
"@volter/world-core": "3.0.
|
|
78
|
+
"@volter/world-core": "3.0.45",
|
|
74
79
|
"@volter/world-runtime": "*",
|
|
75
80
|
"playwright": "^1.63.0"
|
|
76
81
|
},
|
package/src/assessment.ts
CHANGED
|
@@ -84,7 +84,7 @@ export async function assessPack(packDir: string, options: AssessmentOptions = {
|
|
|
84
84
|
const transport = await chromiumServed(packDir, life.host ?? 'twin.local');
|
|
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:
|
|
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 };
|
|
88
88
|
} finally { await transport.stop(); }
|
|
89
89
|
}
|
|
90
90
|
const conform = options.conformance === false ? null : await conformance(packDir);
|
|
@@ -1,13 +1,32 @@
|
|
|
1
1
|
// Browser transport for the existing journey engine. A container supplies network isolation in catalog execution.
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { worldNow } from '@volter/world-core/runtime';
|
|
3
6
|
import type { ServedTwin } from './walk/behavior-journey.ts';
|
|
4
7
|
export async function chromiumServed(packDir: string, host: string) {
|
|
5
8
|
const { chromium } = await import('playwright');
|
|
6
|
-
const browser = await chromium.launch({ args: ['--no-sandbox'] });
|
|
7
9
|
const mod = await import(join(packDir, 'src/index.ts')) as Record<string, unknown>;
|
|
8
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;
|
|
9
|
-
if (!factory)
|
|
11
|
+
if (!factory) throw new Error('Browser assessment needs the pack HTTP fetch factory');
|
|
12
|
+
const library = ['/usr/lib/x86_64-linux-gnu/faketime/libfaketimeMT.so.1', '/usr/lib/aarch64-linux-gnu/faketime/libfaketimeMT.so.1'].find(existsSync);
|
|
13
|
+
if (process.platform !== 'linux' || !library) throw new Error('Deterministic browser assessment needs Linux libfaketime installed for native cookie time');
|
|
14
|
+
const clockDir = mkdtempSync(join(tmpdir(), 'twin-browser-clock-'));
|
|
15
|
+
const clockFile = join(clockDir, 'clock');
|
|
16
|
+
const syncClock = () => {
|
|
17
|
+
const instant = new Date(worldNow());
|
|
18
|
+
// libfaketime's absolute format stops wall time, at HTTP-date (one-second) precision.
|
|
19
|
+
writeFileSync(clockFile, `${instant.toISOString().slice(0, 19).replace('T', ' ')}\n`);
|
|
20
|
+
return instant;
|
|
21
|
+
};
|
|
22
|
+
syncClock();
|
|
23
|
+
const env: Record<string, string> = Object.fromEntries(Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined && !entry[0].startsWith('FAKETIME')));
|
|
24
|
+
Object.assign(env, { LD_PRELOAD: library, TZ: 'UTC', FAKETIME_TIMESTAMP_FILE: clockFile, FAKETIME_NO_CACHE: '1', FAKETIME_DISABLE_SHM: '1', FAKETIME_DONT_FAKE_MONOTONIC: '1', NO_FAKE_STAT: '1' });
|
|
25
|
+
let browser;
|
|
26
|
+
try { browser = await chromium.launch({ args: ['--no-sandbox'], env }); }
|
|
27
|
+
catch (error) { rmSync(clockDir, { recursive: true, force: true }); throw error; }
|
|
10
28
|
const served: ServedTwin = async (root, options) => {
|
|
29
|
+
syncClock();
|
|
11
30
|
const fetchPack = factory({ root, ...options });
|
|
12
31
|
const context = await browser.newContext({ serviceWorkers: 'block', locale: 'en-US', timezoneId: 'UTC' });
|
|
13
32
|
const page = await context.newPage();
|
|
@@ -19,15 +38,18 @@ export async function chromiumServed(packDir: string, host: string) {
|
|
|
19
38
|
try {
|
|
20
39
|
const response = await fetchPack(new Request(request.url(), { method: request.method(), headers: await request.allHeaders(), ...(request.postDataBuffer() ? { body: new Uint8Array(request.postDataBuffer()!).buffer } : {}) }));
|
|
21
40
|
const headers = Object.fromEntries(response.headers);
|
|
41
|
+
// Playwright otherwise adds CORS permission to cross-origin fulfillments. Absence means denial on the vendor wire.
|
|
42
|
+
if (!Object.hasOwn(headers, 'access-control-allow-origin')) headers['access-control-allow-origin'] = '';
|
|
22
43
|
const cookies = response.headers.getSetCookie();
|
|
23
44
|
if (cookies.length) headers['set-cookie'] = cookies.join('\n');
|
|
24
45
|
await route.fulfill({ status: response.status, headers, body: Buffer.from(await response.arrayBuffer()) });
|
|
25
46
|
} catch { await route.abort('failed'); }
|
|
26
47
|
});
|
|
27
48
|
await page.goto(harness);
|
|
28
|
-
await page.clock.setFixedTime(
|
|
49
|
+
await page.clock.setFixedTime(syncClock());
|
|
29
50
|
return {
|
|
30
51
|
fetch: async (request: Request) => {
|
|
52
|
+
await page.clock.setFixedTime(syncClock());
|
|
31
53
|
const body = request.body ? Buffer.from(await request.arrayBuffer()).toString('base64') : null;
|
|
32
54
|
const result = await page.evaluate(async ({ url, method, headers, body }) => {
|
|
33
55
|
const bytes = body === null ? undefined : Uint8Array.from(atob(body), (char) => char.charCodeAt(0));
|
|
@@ -40,8 +62,8 @@ export async function chromiumServed(packDir: string, host: string) {
|
|
|
40
62
|
if (!result.status) throw new Error('Chromium returned an opaque response; this journey needs browser-specific navigation assertions');
|
|
41
63
|
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 });
|
|
42
64
|
},
|
|
43
|
-
stop: () =>
|
|
65
|
+
stop: () => context.close(),
|
|
44
66
|
};
|
|
45
67
|
};
|
|
46
|
-
return { served, version: browser.version(), stop: () => browser.close() };
|
|
68
|
+
return { served, version: browser.version(), clock: { native: 'World time through libfaketime', nativePrecisionMs: 1000, javascriptPrecisionMs: 1, monotonic: 'elapsed machine time' }, stop: async () => { try { await browser.close(); } finally { rmSync(clockDir, { recursive: true, force: true }); } } };
|
|
47
69
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Regression of the standard's own browser transport. This fixture is not a pack or an admission verdict.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
7
|
+
import { getActiveWorldStore, observeDerivedDispatch } from '@volter/world-core/runtime';
|
|
8
|
+
import { inMemoryWorld } from './walk/behavior-journey.ts';
|
|
9
|
+
import { chromiumServed } from './browser-assessment.ts';
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
export async function verifyBrowserTransport(pack: string) {
|
|
12
|
+
assert.ok(process.env.VOLTER_WORLD, 'browser transport verification must run through a World');
|
|
13
|
+
assert.ok(!existsSync(pack), 'browser fixture directory must be new and task-owned');
|
|
14
|
+
|
|
15
|
+
mkdirSync(join(pack, 'src'), { recursive: true });
|
|
16
|
+
writeFileSync(join(pack, 'src/index.ts'), `
|
|
17
|
+
import { createDerivedFetch } from ${JSON.stringify(pathToFileURL(require.resolve('@volter/world-core')).href)};
|
|
18
|
+
export function createFixtureFetch() {
|
|
19
|
+
return createDerivedFetch({ surface: { version: '1', operations: [
|
|
20
|
+
{ id: 'login', method: 'post', path: '/login', class: 'action' },
|
|
21
|
+
{ id: 'identity', method: 'get', path: '/identity', class: 'get' },
|
|
22
|
+
{ id: 'binary', method: 'get', path: '/binary', class: 'get' },
|
|
23
|
+
{ id: 'head', method: 'head', path: '/head', class: 'get' },
|
|
24
|
+
{ id: 'gap', method: 'get', path: '/gap', class: 'get' },
|
|
25
|
+
{ id: 'unused', method: 'get', path: '/unused', class: 'get' },
|
|
26
|
+
] }, handlers: {
|
|
27
|
+
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']] }),
|
|
28
|
+
identity: async ({ request }) => {
|
|
29
|
+
const session = request.headers.get('cookie')?.includes('session=fixture') || false;
|
|
30
|
+
const second = request.headers.get('cookie')?.includes('second=fixture') || false;
|
|
31
|
+
return Response.json({ authenticated: session && second, session, second });
|
|
32
|
+
},
|
|
33
|
+
binary: async () => new Response(new Uint8Array(100000).fill(197), { headers: { 'content-type': 'application/octet-stream' } }),
|
|
34
|
+
head: async () => new Response(null, { status: 204 }),
|
|
35
|
+
}, gap: async () => Response.json({ error: 'unsupported' }, { status: 404 }) });
|
|
36
|
+
}
|
|
37
|
+
`);
|
|
38
|
+
const world = inMemoryWorld('browser-transport-verification');
|
|
39
|
+
const setClock = (instant: string) => getActiveWorldStore().write(process.env.TWIN_WORLD_CLOCK_FILE!, instant);
|
|
40
|
+
const browser = await chromiumServed(pack, 'browser-fixture.test').catch((error) => { world.restore(); throw error; });
|
|
41
|
+
const seen = new Set<string>();
|
|
42
|
+
const unsubscribe = observeDerivedDispatch(({ operation }) => seen.add(operation.id));
|
|
43
|
+
const replays = [];
|
|
44
|
+
try {
|
|
45
|
+
for (let run = 0; run < 2; run++) {
|
|
46
|
+
setClock('2026-01-01T00:00:00.000Z');
|
|
47
|
+
const served = await browser.served(world.root(), {});
|
|
48
|
+
try {
|
|
49
|
+
const fetch = (path: string, method = 'GET') => served.fetch(new Request(`https://browser-fixture.test${path}`, { method }));
|
|
50
|
+
assert.equal((await (await fetch('/identity')).json()).authenticated, false, 'fresh contexts must not inherit cookies');
|
|
51
|
+
assert.equal((await fetch('/login', 'POST')).status, 204);
|
|
52
|
+
const identity = await (await fetch('/identity')).json();
|
|
53
|
+
assert.equal(identity.authenticated, true, 'the browser cookie jar must reach the twin');
|
|
54
|
+
setClock('2026-01-01T00:59:59.000Z');
|
|
55
|
+
assert.deepEqual(await (await fetch('/identity')).json(), identity, 'both cookie types must remain before their World-time expiry');
|
|
56
|
+
setClock('2026-01-01T01:00:01.000Z');
|
|
57
|
+
const expired = await (await fetch('/identity')).json();
|
|
58
|
+
assert.deepEqual(expired, { authenticated: false, session: false, second: false }, 'native Expires and Max-Age must follow World time');
|
|
59
|
+
const binary = new Uint8Array(await (await fetch('/binary')).arrayBuffer());
|
|
60
|
+
assert.equal(binary.byteLength, 100000);
|
|
61
|
+
assert.ok(binary.every((byte) => byte === 197));
|
|
62
|
+
const head = await fetch('/head', 'HEAD');
|
|
63
|
+
assert.equal(head.status, 204); assert.equal(await head.text(), '');
|
|
64
|
+
assert.equal((await fetch('/gap')).status, 404);
|
|
65
|
+
assert.equal((await fetch('/unknown')).status, 404);
|
|
66
|
+
await assert.rejects(() => served.fetch(new Request('https://another-origin.test/identity')), /Failed to fetch/);
|
|
67
|
+
replays.push({ identity, expired, binaryBytes: binary.byteLength, headStatus: head.status, gapStatus: 404 });
|
|
68
|
+
} finally { await served.stop(); }
|
|
69
|
+
}
|
|
70
|
+
assert.deepEqual(replays[0], replays[1]);
|
|
71
|
+
assert.deepEqual([...seen].sort(), ['binary', 'gap', 'head', 'identity', 'login']);
|
|
72
|
+
return { purpose: 'browser-transport-fixture', passed: true, chromium: browser.version, replays: 2, exercisedOperations: seen.size, declaredOperations: 6, cookieJar: true, cookieExpiry: ['Expires', 'Max-Age'], clock: browser.clock, binaryBytes: 100000, corsEnforced: true, domCoverage: null };
|
|
73
|
+
} finally { unsubscribe(); try { await browser.stop(); } finally { world.restore(); } }
|
|
74
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { relative, resolve } from 'node:path';
|
|
|
19
19
|
import { unitsOf, p3Gate } from './gate.ts';
|
|
20
20
|
import { grade, STANDARDS } from './grade.ts';
|
|
21
21
|
import { withCheckoutFiles } from './protocol-3.ts';
|
|
22
|
+
import { standardPackageFile } from './package-file.ts';
|
|
22
23
|
import { gatePublished } from './published.ts';
|
|
23
24
|
|
|
24
25
|
const [command, ...args] = process.argv.slice(2);
|
|
@@ -28,7 +29,7 @@ const usage = (): never => {
|
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
if (command === '--version') {
|
|
31
|
-
const pkg = JSON.parse(readFileSync(
|
|
32
|
+
const pkg = JSON.parse(readFileSync(standardPackageFile('package.json'), 'utf8')) as { version: string };
|
|
32
33
|
console.log(`@volter/twin-standard ${pkg.version} · ${STANDARDS.map((s) => `${s.id}@${s.version}`).join(' ')}`);
|
|
33
34
|
} else if (command === 'gate') {
|
|
34
35
|
const [repoArg, ...only] = args;
|
package/src/clients/sentry.ts
CHANGED
|
@@ -6,10 +6,11 @@ import { request as httpsRequest } from 'node:https';
|
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
8
|
import type { ClientCase, ClientContext } from './harness.ts';
|
|
9
|
+
import { standardPackageFile } from '../package-file.ts';
|
|
9
10
|
|
|
10
11
|
// The pins live in ../../client-sdks with their own lockfile, installed here on first use: some carry advisories with no
|
|
11
12
|
// fix in the line an application pins, so the repository's install (and a release, .github/release-scan.mjs) never does.
|
|
12
|
-
const SDKS =
|
|
13
|
+
const SDKS = standardPackageFile('client-sdks/');
|
|
13
14
|
async function sdkOf(name: string): Promise<any> {
|
|
14
15
|
if (!existsSync(new URL('node_modules/', SDKS))) {
|
|
15
16
|
const install = spawnSync('bun', ['install', '--frozen-lockfile', '--ignore-scripts'], { cwd: SDKS, stdio: 'inherit' });
|
package/src/create-pack.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
import { parseExactJson, exactInteger } from '@volter/world-core/exact-json';
|
|
26
26
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
27
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
28
|
+
import { standardPackageFile } from './package-file.ts';
|
|
28
29
|
import { gunzipSync } from 'node:zlib';
|
|
29
30
|
import { parse as parseYaml } from 'yaml';
|
|
30
31
|
import { coreFor, type DerivedManifest, type DerivedOperation } from '@volter/world-core';
|
|
@@ -457,7 +458,7 @@ export async function createPack(args: string[]): Promise<void> {
|
|
|
457
458
|
}
|
|
458
459
|
// the package's licence, the platform's (Apache-2.0), as every published pack carries it
|
|
459
460
|
const licence = join(dir, 'LICENSE');
|
|
460
|
-
if (!existsSync(licence)) { writeFileSync(licence, readFileSync(
|
|
461
|
+
if (!existsSync(licence)) { writeFileSync(licence, readFileSync(standardPackageFile('LICENSE'))); console.log('create-pack: wrote LICENSE'); }
|
|
461
462
|
// the handler map each fetch imports, empty until the families are written (--index again then)
|
|
462
463
|
for (const at of lanesArg.length ? [...lanesArg.map((l) => join(dir, l)), dir] : [dir]) {
|
|
463
464
|
if (at === dir && lanesArg.length) { const f = join(dir, 'src', 'fetch.ts'); if (!existsSync(f)) { writeFileSync(f, packFetchFile(dir, fetchName(dir))); console.log('create-pack: wrote src/fetch.ts'); } continue; }
|
package/src/index.ts
CHANGED
|
@@ -17,3 +17,4 @@ export { compilePackFacts, renderPackFacts, type PackFacts, type PackFactsFile }
|
|
|
17
17
|
export { assessPack, type AssessmentOptions } from './assessment.ts';
|
|
18
18
|
|
|
19
19
|
export { createPack } from './create-pack.ts';
|
|
20
|
+
export { verifyBrowserTransport } from './browser-verification.ts';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
/** Package-owned data beside src/, or beside dist/ when the released entrypoints are compiled. */
|
|
4
|
+
export function standardPackageFile(name: 'package.json' | 'LICENSE' | 'client-sdks/'): URL {
|
|
5
|
+
for (const parent of ['../', '../../']) {
|
|
6
|
+
const root = new URL(parent, import.meta.url);
|
|
7
|
+
const manifest = new URL('package.json', root);
|
|
8
|
+
if (existsSync(manifest) && JSON.parse(readFileSync(manifest, 'utf8')).name === '@volter/twin-standard') return new URL(name, root);
|
|
9
|
+
}
|
|
10
|
+
throw new Error('Cannot locate the twin-standard package manifest');
|
|
11
|
+
}
|
|
@@ -168,7 +168,7 @@ function takeReplies(c: Connection): LineReply[] {
|
|
|
168
168
|
/** A pack served for a walk the way a World runs it (its server.ts on a port, the World's root and scenario its options):
|
|
169
169
|
* the walk's requests go to it over HTTP, the vendor's host in their Host header, instead of to the fetch in process;
|
|
170
170
|
* its sockets are WebSockets to that port, and a line connection (`stream`) is a TCP connection to its protocol port. */
|
|
171
|
-
export type ServedTwin = (root: string, options: { scenarioPath?: string; application?: import('@volter/world-core').PackFetchOptions['application'] }) => Promise<{ fetch: (request: Request) => Promise<Response>; stop: () => void
|
|
171
|
+
export type ServedTwin = (root: string, options: { scenarioPath?: string; application?: import('@volter/world-core').PackFetchOptions['application'] }) => Promise<{ fetch: (request: Request) => Promise<Response>; stop: () => void | Promise<void>; socket?: (url: string, headers: Record<string, string>) => WebSocket; stream?: TwinStream }>;
|
|
172
172
|
|
|
173
173
|
export async function walk(journey: Journey, root: string, collect: Failure[], opts: { beats: boolean; verbose?: boolean; hooks?: StepHooks; captured?: Captured; served?: ServedTwin }): Promise<Captured> {
|
|
174
174
|
attributionName(journey.author);
|
|
@@ -769,7 +769,7 @@ export async function walk(journey: Journey, root: string, collect: Failure[], o
|
|
|
769
769
|
await Promise.allSettled([...pending.values()].map((request) => request.answer));
|
|
770
770
|
setDeliveryObserver(previousObserver);
|
|
771
771
|
setInProcessTwins(previousTwins);
|
|
772
|
-
served?.stop();
|
|
772
|
+
await served?.stop();
|
|
773
773
|
if (sqlSession) await sqlSession.then((session) => session.end(), () => {});
|
|
774
774
|
// a World's managed database ends with its walk (vendor-fetch.ts), so the next World declares it at the same port
|
|
775
775
|
await (twin as { stopDatabase?: () => Promise<void> }).stopDatabase?.();
|