@volter/twin-standard 1.0.73 → 1.0.75
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.
|
@@ -32,5 +32,8 @@ export type ClientCase = {
|
|
|
32
32
|
* proxy forwards a caller of the vendor's https host (twin-fetch.ts, twinPublicBase: the links and socket URLs the pack
|
|
33
33
|
* answers are then the vendor's secure ones). Answers the proxy's port. */
|
|
34
34
|
export declare function worldProxy(served: number): Promise<number>;
|
|
35
|
+
/** HTTP to a served pack, with the vendor Host and manual redirects. Each call owns a fresh connection;
|
|
36
|
+
* its caller's abort signal retires that request, including an unfinished response body. */
|
|
37
|
+
export declare function servedHttpFetch(port: number): typeof fetch;
|
|
35
38
|
/** The context for a pack reached at `port` of this machine (the World's proxy in front of it). */
|
|
36
39
|
export declare function clientContext(port: number, host: string, env: Record<string, string>, scenarioPath: string): ClientContext;
|
|
@@ -37,37 +37,60 @@ export async function worldProxy(served) {
|
|
|
37
37
|
server.unref();
|
|
38
38
|
return server.address().port;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
/** HTTP to a served pack, with the vendor Host and manual redirects. Each call owns a fresh connection;
|
|
41
|
+
* its caller's abort signal retires that request, including an unfinished response body. */
|
|
42
|
+
export function servedHttpFetch(port) {
|
|
43
|
+
return (async (input, init) => {
|
|
43
44
|
const request = new Request(input, init);
|
|
44
45
|
const url = new URL(request.url);
|
|
45
46
|
const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : Buffer.from(await request.arrayBuffer());
|
|
46
47
|
const headers = Object.fromEntries(request.headers);
|
|
47
48
|
headers.host = url.host;
|
|
49
|
+
const signal = init?.signal ?? (input instanceof Request ? input.signal : request.signal);
|
|
48
50
|
return new Promise((resolve, reject) => {
|
|
49
|
-
const
|
|
51
|
+
const cleanup = () => signal.removeEventListener('abort', abort);
|
|
52
|
+
const failed = (error) => { cleanup(); reject(error); };
|
|
53
|
+
const sent = httpRequest({ host: '127.0.0.1', port, method: request.method, path: `${url.pathname}${url.search}`, headers, agent: false }, (res) => {
|
|
50
54
|
const chunks = [];
|
|
51
55
|
res.on('data', (c) => chunks.push(c));
|
|
52
56
|
res.on('end', () => {
|
|
57
|
+
cleanup();
|
|
53
58
|
const answer = new Headers();
|
|
54
59
|
for (const [k, v] of Object.entries(res.headers))
|
|
55
60
|
if (v !== undefined)
|
|
56
61
|
for (const one of Array.isArray(v) ? v : [v])
|
|
57
62
|
answer.append(k, one);
|
|
58
|
-
|
|
63
|
+
try {
|
|
64
|
+
resolve(new Response(request.method === 'HEAD' || res.statusCode === 204 || res.statusCode === 205 || res.statusCode === 304 ? null : Buffer.concat(chunks), { status: res.statusCode ?? 0, headers: answer }));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
reject(error);
|
|
68
|
+
}
|
|
59
69
|
});
|
|
60
|
-
res.on('error',
|
|
70
|
+
res.on('error', failed);
|
|
61
71
|
});
|
|
62
|
-
|
|
72
|
+
const abort = () => {
|
|
73
|
+
const error = new Error('served HTTP request aborted');
|
|
74
|
+
sent.destroy(error);
|
|
75
|
+
failed(error);
|
|
76
|
+
};
|
|
77
|
+
sent.on('error', failed);
|
|
78
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
79
|
+
if (signal.aborted) {
|
|
80
|
+
abort();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
63
83
|
sent.end(body);
|
|
64
84
|
});
|
|
65
85
|
});
|
|
86
|
+
}
|
|
87
|
+
/** The context for a pack reached at `port` of this machine (the World's proxy in front of it). */
|
|
88
|
+
export function clientContext(port, host, env, scenarioPath) {
|
|
66
89
|
const agent = new (class extends HttpsAgent {
|
|
67
90
|
createConnection() { return connect(port, '127.0.0.1'); }
|
|
68
91
|
})({ keepAlive: false });
|
|
69
92
|
return {
|
|
70
|
-
host, env, fetch:
|
|
93
|
+
host, env, fetch: servedHttpFetch(port), agent,
|
|
71
94
|
connect: () => connect(port, '127.0.0.1'),
|
|
72
95
|
scenario: (doc) => writeFileSync(scenarioPath, JSON.stringify(doc)),
|
|
73
96
|
};
|
package/dist/src/conformance.js
CHANGED
|
@@ -26,6 +26,7 @@ import { tmpdir } from 'node:os';
|
|
|
26
26
|
import { basename, dirname, extname, join, matchesGlob, relative, resolve } from 'node:path';
|
|
27
27
|
import { citations, sourceHeld } from "./check-sources.js";
|
|
28
28
|
import { fileURLToPath } from 'node:url';
|
|
29
|
+
import { servedHttpFetch } from "./clients/harness.js";
|
|
29
30
|
const pascal = (s) => s.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join('');
|
|
30
31
|
const readJson = (p) => (existsSync(p) ? JSON.parse(readFileSync(p, 'utf8')) : undefined);
|
|
31
32
|
/** The pack's units: the pack and each lane in it (a directory with its own src/manifest.ts and no package.json). */
|
|
@@ -379,15 +380,18 @@ async function lifeServed(pack) {
|
|
|
379
380
|
env.VOLTER_WORLD_BOOT_ID = before;
|
|
380
381
|
}
|
|
381
382
|
const port = server.inspectPort ?? server.port;
|
|
383
|
+
const http = servedHttpFetch(port);
|
|
382
384
|
return {
|
|
383
385
|
// the vendor's URL as the application called it, sent to the served pack: its host carried in the Host header
|
|
384
386
|
fetch: async (request) => {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
387
|
+
const abort = new AbortController();
|
|
388
|
+
try {
|
|
389
|
+
const answer = await within(10_000, http(request, { signal: abort.signal }));
|
|
390
|
+
return answer === 'timeout' ? new Response(`no answer in 10 s from the served pack (${request.method} ${new URL(request.url).pathname})`, { status: 599 }) : answer;
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
abort.abort();
|
|
394
|
+
}
|
|
391
395
|
},
|
|
392
396
|
// a socket opened as an application opens it: a WebSocket to the served port, the vendor's host in its Host header
|
|
393
397
|
socket: (url, headers) => new WebSocket(`ws://127.0.0.1:${port}${url}`, { headers }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/twin-standard",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.75",
|
|
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.
|
|
78
|
+
"@volter/world-core": "3.0.83",
|
|
79
79
|
"@volter/world-runtime": "*",
|
|
80
80
|
"playwright": "^1.63.0"
|
|
81
81
|
},
|
package/src/clients/harness.ts
CHANGED
|
@@ -67,34 +67,51 @@ export async function worldProxy(served: number): Promise<number> {
|
|
|
67
67
|
return (server.address() as AddressInfo).port;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/**
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
/** HTTP to a served pack, with the vendor Host and manual redirects. Each call owns a fresh connection;
|
|
71
|
+
* its caller's abort signal retires that request, including an unfinished response body. */
|
|
72
|
+
export function servedHttpFetch(port: number): typeof fetch {
|
|
73
|
+
return (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
73
74
|
const request = new Request(input, init);
|
|
74
75
|
const url = new URL(request.url);
|
|
75
76
|
const body = request.method === 'GET' || request.method === 'HEAD' ? undefined : Buffer.from(await request.arrayBuffer());
|
|
76
77
|
const headers: Record<string, string> = Object.fromEntries(request.headers);
|
|
77
78
|
headers.host = url.host;
|
|
79
|
+
const signal = init?.signal ?? (input instanceof Request ? input.signal : request.signal);
|
|
78
80
|
return new Promise<Response>((resolve, reject) => {
|
|
79
|
-
const
|
|
81
|
+
const cleanup = () => signal.removeEventListener('abort', abort);
|
|
82
|
+
const failed = (error: Error) => { cleanup(); reject(error); };
|
|
83
|
+
const sent = httpRequest({ host: '127.0.0.1', port, method: request.method, path: `${url.pathname}${url.search}`, headers, agent: false }, (res) => {
|
|
80
84
|
const chunks: Buffer[] = [];
|
|
81
85
|
res.on('data', (c: Buffer) => chunks.push(c));
|
|
82
86
|
res.on('end', () => {
|
|
87
|
+
cleanup();
|
|
83
88
|
const answer = new Headers();
|
|
84
89
|
for (const [k, v] of Object.entries(res.headers)) if (v !== undefined) for (const one of Array.isArray(v) ? v : [v]) answer.append(k, one);
|
|
85
|
-
resolve(new Response(res.statusCode === 204 || res.statusCode === 304 ? null : Buffer.concat(chunks), { status: res.statusCode ?? 0, headers: answer }));
|
|
90
|
+
try { resolve(new Response(request.method === 'HEAD' || res.statusCode === 204 || res.statusCode === 205 || res.statusCode === 304 ? null : Buffer.concat(chunks), { status: res.statusCode ?? 0, headers: answer })); }
|
|
91
|
+
catch (error) { reject(error); }
|
|
86
92
|
});
|
|
87
|
-
res.on('error',
|
|
93
|
+
res.on('error', failed);
|
|
88
94
|
});
|
|
89
|
-
|
|
95
|
+
const abort = () => {
|
|
96
|
+
const error = new Error('served HTTP request aborted');
|
|
97
|
+
sent.destroy(error);
|
|
98
|
+
failed(error);
|
|
99
|
+
};
|
|
100
|
+
sent.on('error', failed);
|
|
101
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
102
|
+
if (signal.aborted) { abort(); return; }
|
|
90
103
|
sent.end(body);
|
|
91
104
|
});
|
|
92
105
|
}) as typeof fetch;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The context for a pack reached at `port` of this machine (the World's proxy in front of it). */
|
|
109
|
+
export function clientContext(port: number, host: string, env: Record<string, string>, scenarioPath: string): ClientContext {
|
|
93
110
|
const agent = new (class extends HttpsAgent {
|
|
94
111
|
createConnection(): Socket { return connect(port, '127.0.0.1'); }
|
|
95
112
|
})({ keepAlive: false });
|
|
96
113
|
return {
|
|
97
|
-
host, env, fetch:
|
|
114
|
+
host, env, fetch: servedHttpFetch(port), agent,
|
|
98
115
|
connect: () => connect(port, '127.0.0.1'),
|
|
99
116
|
scenario: (doc) => writeFileSync(scenarioPath, JSON.stringify(doc)),
|
|
100
117
|
};
|
package/src/conformance.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { basename, dirname, extname, join, matchesGlob, relative, resolve } from
|
|
|
19
19
|
import type { TwinStream } from '@volter/world-core';
|
|
20
20
|
import { citations, sourceHeld, type SourceRecord } from './check-sources.ts';
|
|
21
21
|
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { servedHttpFetch } from './clients/harness.ts';
|
|
22
23
|
|
|
23
24
|
export type ConformanceResult = { id: string; asks: string; failures: string[]; note?: string };
|
|
24
25
|
|
|
@@ -334,15 +335,15 @@ async function lifeServed(pack: string): Promise<string[]> {
|
|
|
334
335
|
let server: Served;
|
|
335
336
|
try { server = await serve(pack, { root, ...options }); } finally { if (before === undefined) delete env.VOLTER_WORLD_BOOT_ID; else env.VOLTER_WORLD_BOOT_ID = before; }
|
|
336
337
|
const port = server.inspectPort ?? server.port;
|
|
338
|
+
const http = servedHttpFetch(port);
|
|
337
339
|
return {
|
|
338
340
|
// the vendor's URL as the application called it, sent to the served pack: its host carried in the Host header
|
|
339
341
|
fetch: async (request: Request): Promise<Response> => {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
return answer === 'timeout' ? new Response(`no answer in 10 s from the served pack (${request.method} ${url.pathname})`, { status: 599 }) : answer;
|
|
342
|
+
const abort = new AbortController();
|
|
343
|
+
try {
|
|
344
|
+
const answer = await within(10_000, http(request, { signal: abort.signal }));
|
|
345
|
+
return answer === 'timeout' ? new Response(`no answer in 10 s from the served pack (${request.method} ${new URL(request.url).pathname})`, { status: 599 }) : answer;
|
|
346
|
+
} finally { abort.abort(); }
|
|
346
347
|
},
|
|
347
348
|
// a socket opened as an application opens it: a WebSocket to the served port, the vendor's host in its Host header
|
|
348
349
|
socket: (url: string, headers: Record<string, string>) => new WebSocket(`ws://127.0.0.1:${port}${url}`, { headers } as unknown as string[]),
|