@volter/twin-standard 1.0.157 → 1.0.159
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/README.md +3 -2
- package/client-sdks/import.mjs +2 -0
- package/client-sdks/package.json +4 -0
- package/client-sdks/pnpm-lock.yaml +609 -0
- package/dist/client-sdks/import.mjs +2 -0
- package/dist/client-sdks/package.json +4 -0
- package/dist/client-sdks/pnpm-lock.yaml +609 -0
- package/dist/src/assessment.d.ts +13 -3
- package/dist/src/assessment.js +10 -6
- package/dist/src/browser-assessment.js +16 -13
- package/dist/src/clients/harness.d.ts +25 -4
- package/dist/src/clients/harness.js +184 -22
- package/dist/src/clients/jira.js +11 -2
- package/dist/src/clients/run.js +19 -14
- package/dist/src/clients/tinybird.js +15 -6
- package/dist/src/clients/vercel.d.ts +2 -0
- package/dist/src/clients/vercel.js +73 -0
- package/dist/src/conformance.d.ts +2 -0
- package/dist/src/conformance.js +392 -184
- package/dist/src/consumer.d.ts +22 -1
- package/dist/src/consumer.js +91 -26
- package/dist/src/credential-issuer-import.d.ts +3 -0
- package/dist/src/credential-issuer-import.js +42 -0
- package/dist/src/derive.js +3 -0
- package/dist/src/gate.js +4 -0
- package/dist/src/lanes.d.ts +2 -0
- package/dist/src/lanes.js +7 -0
- package/dist/src/protocol-3.js +8 -3
- package/dist/src/spec-documents.d.ts +4 -0
- package/dist/src/spec-documents.js +110 -3
- package/dist/src/tools/life-coverage-walk.js +38 -26
- package/dist/src/tools/score-pack.js +6 -5
- package/dist/src/tools/vendor-examples.js +91 -80
- package/dist/src/walk/behavior-journey.d.ts +22 -14
- package/dist/src/walk/behavior-journey.js +182 -133
- package/dist/src/walk/journey-kit.d.ts +3 -2
- package/dist/src/walk/journey-kit.js +6 -2
- package/dist/src/walk/replay.d.ts +23 -0
- package/dist/src/walk/replay.js +118 -0
- package/dist/src/walk/serve-pack.d.ts +2 -2
- package/dist/src/walk/serve-pack.js +38 -2
- package/dist/src/walk/shape.d.ts +25 -4
- package/dist/src/walk/shape.js +166 -56
- package/dist/src/walk/vendor-fetch.d.ts +7 -2
- package/dist/src/walk/vendor-fetch.js +13 -2
- package/dist/src/walk/walk-database.d.ts +9 -1
- package/dist/src/walk/walk-database.js +35 -13
- package/dist/src/walk/xml-view.d.ts +3 -2
- package/dist/src/walk/xml-view.js +5 -4
- package/package.json +4 -3
- package/src/assessment.ts +12 -8
- package/src/browser-assessment.ts +12 -5
- package/src/clients/harness.ts +142 -19
- package/src/clients/jira.ts +4 -2
- package/src/clients/run.ts +17 -15
- package/src/clients/tinybird.ts +8 -6
- package/src/clients/vercel.ts +67 -0
- package/src/conformance.ts +313 -128
- package/src/consumer.ts +83 -21
- package/src/credential-issuer-import.ts +38 -0
- package/src/derive.ts +3 -1
- package/src/gate.ts +3 -0
- package/src/lanes.ts +7 -0
- package/src/protocol-3.ts +7 -3
- package/src/spec-documents.ts +92 -3
- package/src/tools/life-coverage-walk.ts +32 -22
- package/src/tools/score-pack.ts +7 -6
- package/src/tools/vendor-examples.ts +88 -75
- package/src/walk/behavior-journey.ts +145 -109
- package/src/walk/journey-kit.ts +7 -5
- package/src/walk/replay.ts +103 -0
- package/src/walk/serve-pack.ts +25 -5
- package/src/walk/shape.ts +156 -54
- package/src/walk/vendor-fetch.ts +16 -4
- package/src/walk/walk-database.ts +41 -15
- package/src/walk/xml-view.ts +5 -4
package/dist/src/assessment.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export declare function assessPack(packDir: string, options?: AssessmentOptions)
|
|
|
23
23
|
}[];
|
|
24
24
|
replay: {
|
|
25
25
|
equal: boolean;
|
|
26
|
+
diagnostics: string[];
|
|
26
27
|
runs: number;
|
|
27
28
|
firstSha256: string;
|
|
28
29
|
secondSha256: string;
|
|
@@ -53,7 +54,7 @@ export declare function assessPack(packDir: string, options?: AssessmentOptions)
|
|
|
53
54
|
};
|
|
54
55
|
at: string;
|
|
55
56
|
retentionNotApplicable?: string | undefined;
|
|
56
|
-
measured:
|
|
57
|
+
measured: true;
|
|
57
58
|
passed: boolean;
|
|
58
59
|
scope: string;
|
|
59
60
|
package: any;
|
|
@@ -74,6 +75,15 @@ export declare function assessPack(packDir: string, options?: AssessmentOptions)
|
|
|
74
75
|
package: string;
|
|
75
76
|
version: string;
|
|
76
77
|
};
|
|
78
|
+
peers: {
|
|
79
|
+
package: string;
|
|
80
|
+
version: string;
|
|
81
|
+
}[];
|
|
82
|
+
sdks: {
|
|
83
|
+
package: string;
|
|
84
|
+
version: string;
|
|
85
|
+
entry: string;
|
|
86
|
+
}[];
|
|
77
87
|
dependencyLockSha256: string;
|
|
78
88
|
appLocal: boolean;
|
|
79
89
|
};
|
|
@@ -103,8 +113,8 @@ export declare function assessPack(packDir: string, options?: AssessmentOptions)
|
|
|
103
113
|
mode: string;
|
|
104
114
|
};
|
|
105
115
|
} | {
|
|
106
|
-
measured:
|
|
107
|
-
passed:
|
|
116
|
+
measured: false;
|
|
117
|
+
passed: false;
|
|
108
118
|
failures: string[];
|
|
109
119
|
};
|
|
110
120
|
integrity?: string | undefined;
|
package/dist/src/assessment.js
CHANGED
|
@@ -16,6 +16,7 @@ import { conformance } from "./conformance.js";
|
|
|
16
16
|
import { inMemoryWorld, walk, withPinnedWorldDraws, usePacks } from "./walk/behavior-journey.js";
|
|
17
17
|
import { compilePackFacts } from "./pack-facts.js";
|
|
18
18
|
import { expandFills } from "./walk/journey-kit.js";
|
|
19
|
+
import { compareReplays } from "./walk/replay.js";
|
|
19
20
|
import { runConsumerWorkflow } from "./consumer.js";
|
|
20
21
|
const canonical = (value) => value && typeof value === 'object' ? Array.isArray(value) ? value.map(canonical) : Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, canonical(item)])) : value;
|
|
21
22
|
const json = (value) => JSON.stringify(canonical(value));
|
|
@@ -73,7 +74,8 @@ export async function assessPack(packDir, options = {}) {
|
|
|
73
74
|
const coverageOf = (result) => !instrumented || result.ambiguous ? null : { total: declared.length, exercised: result.exercised.length, percent: 100 * result.exercised.length / declared.length, operations: result.exercised, missing: declared.filter((id) => !result.exercised.includes(id)) };
|
|
74
75
|
async function replay(served) {
|
|
75
76
|
const world = inMemoryWorld('catalog-replay');
|
|
76
|
-
const failures = []
|
|
77
|
+
const failures = [];
|
|
78
|
+
const trace = [];
|
|
77
79
|
const exercised = new Set();
|
|
78
80
|
const signatures = new WeakMap();
|
|
79
81
|
let ambiguous = false;
|
|
@@ -94,7 +96,7 @@ export async function assessPack(packDir, options = {}) {
|
|
|
94
96
|
exercised.add(id);
|
|
95
97
|
});
|
|
96
98
|
try {
|
|
97
|
-
await withPinnedWorldDraws(() => walk(structuredClone(life), world.root(), failures, { beats: true, ...(served ? { served } : {}), hooks: { after: (step, path, answer) => trace.push({ name: step.name ?? '', path, status: answer.status, body: answer.body }) } }));
|
|
99
|
+
await withPinnedWorldDraws(() => walk(structuredClone(life), world.root(), failures, { beats: true, ...(served ? { served } : {}), hooks: { after: (step, path, answer) => trace.push({ name: step.name ?? '', path, status: answer.status, body: answer.body, ...(answer.sql ? { sql: answer.sql } : {}), ...(answer.connection ? { connection: answer.connection } : {}) }) } }));
|
|
98
100
|
return { failures, trace, exercised: [...exercised].sort(), ambiguous };
|
|
99
101
|
}
|
|
100
102
|
finally {
|
|
@@ -103,9 +105,10 @@ export async function assessPack(packDir, options = {}) {
|
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
107
|
const first = await replay(), second = await replay();
|
|
106
|
-
const
|
|
108
|
+
const comparison = compareReplays(first.trace, second.trace);
|
|
109
|
+
const replayEqual = comparison.equal;
|
|
107
110
|
const coverage = coverageOf(first);
|
|
108
|
-
const quick = { scope: 'packaged-customer-journey; in-process', steps: life.steps.length, answered: first.trace.length, failures: [...first.failures, ...second.failures], replay: { equal: replayEqual, runs: 2, firstSha256: hash(first.trace), secondSha256: hash(second.trace) }, operationCoverage: coverage, servedOperations, surface: owners ? { total: declared.length, served: servedOperations.length, gap: declared.length - servedOperations.length } : null, codeCoverage: null, stateTransitionCoverage: null };
|
|
111
|
+
const quick = { scope: 'packaged-customer-journey; in-process', steps: life.steps.length, answered: first.trace.length, failures: [...first.failures, ...second.failures], replay: { equal: replayEqual, diagnostics: comparison.diagnostics, runs: 2, firstSha256: hash(first.trace), secondSha256: hash(second.trace) }, operationCoverage: coverage, servedOperations, surface: owners ? { total: declared.length, served: servedOperations.length, gap: declared.length - servedOperations.length } : null, codeCoverage: null, stateTransitionCoverage: null };
|
|
109
112
|
let browser = { measured: false, reason: options.browser ? 'Journey has a wire Chromium fetch cannot exercise' : 'Browser target not requested' };
|
|
110
113
|
const httpOnly = life.steps.every((step) => 'method' in step || 'wait' in step);
|
|
111
114
|
if (options.browser && httpOnly) {
|
|
@@ -113,13 +116,13 @@ export async function assessPack(packDir, options = {}) {
|
|
|
113
116
|
const transport = await chromiumServed(packDir, life.host ?? 'twin.local', 'http-journey');
|
|
114
117
|
try {
|
|
115
118
|
const a = await replay(transport.served), b = await replay(transport.served);
|
|
116
|
-
|
|
119
|
+
const compared = compareReplays(a.trace, b.trace);
|
|
120
|
+
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: compared.equal, diagnostics: compared.diagnostics, firstSha256: hash(a.trace), secondSha256: hash(b.trace), runs: 2 }, operationCoverage: coverageOf(a), domCoverage: null, clock: transport.clock };
|
|
117
121
|
}
|
|
118
122
|
finally {
|
|
119
123
|
await transport.stop();
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
|
-
const conform = options.conformance === false ? null : await conformance(packDir);
|
|
123
126
|
let consumer;
|
|
124
127
|
try {
|
|
125
128
|
consumer = options.consumer ? await runConsumerWorkflow(packDir, options.consumer) : { measured: false, passed: false, failures: ['Installed customer workflow was not prepared'] };
|
|
@@ -127,6 +130,7 @@ export async function assessPack(packDir, options = {}) {
|
|
|
127
130
|
catch (error) {
|
|
128
131
|
consumer = { measured: false, passed: false, failures: [error.message] };
|
|
129
132
|
}
|
|
133
|
+
const conform = options.conformance === false ? null : await conformance(packDir, consumer.measured ? { consumer } : {});
|
|
130
134
|
const ready = gates.length > 0 && gates.every((gate) => !gate.failures.length) && !quick.failures.length && replayEqual && conform !== null && conform.length > 0 && conform.every((result) => !result.failures.length) && consumer.measured && consumer.passed && (!options.browser || !httpOnly || browser.measured === true && browser.failures.length === 0 && browser.replay.equal);
|
|
131
135
|
// the artifact assessed, by the integrity its prepared consumer verified the installed candidate against (consumer.ts)
|
|
132
136
|
return { schemaVersion: 1, vendor, package: manifest.name, version: manifest.version, ...(consumer.integrity ? { integrity: consumer.integrity } : {}), ready, gates: gates.map((gate) => ({ ...gate, unit: gate.unit === packDir ? vendor : `${vendor}/${gate.unit.slice(packDir.length + 1)}` })), quick, browser, conformance: conform, consumer };
|
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
-
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
-
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
-
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
-
});
|
|
6
|
-
}
|
|
7
|
-
return path;
|
|
8
|
-
};
|
|
9
1
|
// Browser transport for the existing journey engine. A container supplies network isolation in catalog execution.
|
|
10
2
|
import { join } from 'node:path';
|
|
11
3
|
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
12
4
|
import { tmpdir } from 'node:os';
|
|
13
5
|
import { worldNow } from '@volter/world-core/runtime';
|
|
6
|
+
import { packFetch } from "./walk/vendor-fetch.js";
|
|
14
7
|
export async function chromiumServed(packDir, host, mode = 'browser-fetch') {
|
|
15
8
|
const { chromium } = await import('playwright');
|
|
16
|
-
const
|
|
17
|
-
const factory = Object.entries(mod).find(([name, value]) => /^create\w+Fetch$/.test(name) && !/TwinFetch$/.test(name) && typeof value === 'function')?.[1];
|
|
9
|
+
const factory = await packFetch(packDir);
|
|
18
10
|
if (!factory)
|
|
19
11
|
throw new Error('Browser assessment needs the pack HTTP fetch factory');
|
|
20
12
|
const library = ['/usr/lib/x86_64-linux-gnu/faketime/libfaketimeMT.so.1', '/usr/lib/aarch64-linux-gnu/faketime/libfaketimeMT.so.1'].find(existsSync);
|
|
@@ -41,7 +33,7 @@ export async function chromiumServed(packDir, host, mode = 'browser-fetch') {
|
|
|
41
33
|
}
|
|
42
34
|
const served = async (root, options) => {
|
|
43
35
|
syncClock();
|
|
44
|
-
const fetchPack = factory(
|
|
36
|
+
const fetchPack = factory(root, options);
|
|
45
37
|
const context = await browser.newContext({ serviceWorkers: 'block', locale: 'en-US', timezoneId: 'UTC' });
|
|
46
38
|
const pages = [];
|
|
47
39
|
const pending = new Map();
|
|
@@ -96,7 +88,7 @@ export async function chromiumServed(packDir, host, mode = 'browser-fetch') {
|
|
|
96
88
|
};
|
|
97
89
|
return {
|
|
98
90
|
scheme: 'https',
|
|
99
|
-
fetch: async (request, options) => {
|
|
91
|
+
fetch: Object.assign(async (request, options) => {
|
|
100
92
|
const entry = await acquire(mode === 'http-journey' ? new URL(request.url).origin : `https://${host}`);
|
|
101
93
|
const { page } = entry;
|
|
102
94
|
const observation = { request };
|
|
@@ -128,8 +120,19 @@ export async function chromiumServed(packDir, host, mode = 'browser-fetch') {
|
|
|
128
120
|
pending.delete(page);
|
|
129
121
|
entry.busy = false;
|
|
130
122
|
}
|
|
123
|
+
}, {
|
|
124
|
+
managedDatabaseUrl: fetchPack.managedDatabaseUrl,
|
|
125
|
+
managedDatabaseClock: fetchPack.managedDatabaseClock,
|
|
126
|
+
managedDatabaseConnection: fetchPack.managedDatabaseConnection,
|
|
127
|
+
}),
|
|
128
|
+
stop: async () => {
|
|
129
|
+
try {
|
|
130
|
+
await context.close();
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
await fetchPack.stopDatabase?.();
|
|
134
|
+
}
|
|
131
135
|
},
|
|
132
|
-
stop: () => context.close(),
|
|
133
136
|
};
|
|
134
137
|
};
|
|
135
138
|
return { served, version: browser.version(), clock: { native: 'World time through libfaketime', nativePrecisionMs: 1000, javascriptPrecisionMs: 1, monotonic: 'elapsed machine time' }, stop: async () => { try {
|
|
@@ -30,10 +30,31 @@ export type ClientCase = {
|
|
|
30
30
|
/** The World's proxy in front of a pack served on `served`: it ends the caller's TLS (here, none was begun) and forwards
|
|
31
31
|
* each request and socket upgrade to the pack with `x-forwarded-proto: https`, the vendor's Host unchanged, as a World's
|
|
32
32
|
* proxy forwards a caller of the vendor's https host (twin-fetch.ts, twinPublicBase: the links and socket URLs the pack
|
|
33
|
-
* answers are then the vendor's secure ones).
|
|
34
|
-
export declare function worldProxy(served: number
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
* answers are then the vendor's secure ones). Its stop settles every connection it opened. */
|
|
34
|
+
export declare function worldProxy(served: number, observe?: (request: {
|
|
35
|
+
url: string;
|
|
36
|
+
headers: Headers;
|
|
37
|
+
}, answer: {
|
|
38
|
+
status: number;
|
|
39
|
+
}) => void): Promise<{
|
|
40
|
+
port: number;
|
|
41
|
+
stop: () => Promise<void>;
|
|
42
|
+
}>;
|
|
43
|
+
/** A served socket either opens within its five-second handshake or closes under the same owner. */
|
|
44
|
+
export declare function openWebSocket(socket: WebSocket): Promise<boolean>;
|
|
45
|
+
/** One request and its complete body share the check's ten-second lifetime, including caller cancellation. */
|
|
46
|
+
export declare function readHttpResponse(fetcher: (request: Request) => Promise<Response>, request: Request): Promise<{
|
|
47
|
+
status: number;
|
|
48
|
+
headers: Record<string, string>;
|
|
49
|
+
text: string;
|
|
50
|
+
bodyBytes: Uint8Array;
|
|
51
|
+
bodyError?: {
|
|
52
|
+
message: string;
|
|
53
|
+
cancelled: boolean;
|
|
54
|
+
};
|
|
55
|
+
}>;
|
|
56
|
+
/** HTTP to a served pack, with the vendor Host and manual redirects. Headers resolve before the streamed body;
|
|
57
|
+
* its caller's abort signal owns the fresh connection until the body ends or is cancelled. */
|
|
37
58
|
export declare function servedHttpFetch(port: number): typeof fetch;
|
|
38
59
|
/** The context for a pack reached at `port` of this machine (the World's proxy in front of it). */
|
|
39
60
|
/** `raw`: a raw protocol's served port (SMTP's listener), which a connection reaches directly, as a World passes a raw TCP
|
|
@@ -11,16 +11,38 @@ import { writeFileSync } from 'node:fs';
|
|
|
11
11
|
/** The World's proxy in front of a pack served on `served`: it ends the caller's TLS (here, none was begun) and forwards
|
|
12
12
|
* each request and socket upgrade to the pack with `x-forwarded-proto: https`, the vendor's Host unchanged, as a World's
|
|
13
13
|
* proxy forwards a caller of the vendor's https host (twin-fetch.ts, twinPublicBase: the links and socket URLs the pack
|
|
14
|
-
* answers are then the vendor's secure ones).
|
|
15
|
-
export async function worldProxy(served) {
|
|
14
|
+
* answers are then the vendor's secure ones). Its stop settles every connection it opened. */
|
|
15
|
+
export async function worldProxy(served, observe) {
|
|
16
|
+
const sockets = new Set();
|
|
17
|
+
const requests = new Set();
|
|
18
|
+
const track = (socket) => { if (sockets.has(socket))
|
|
19
|
+
return; sockets.add(socket); socket.once('close', () => sockets.delete(socket)); };
|
|
16
20
|
const server = createServer((req, res) => {
|
|
17
21
|
const forward = httpRequest({ host: '127.0.0.1', port: served, method: req.method, path: req.url, headers: { ...req.headers, 'x-forwarded-proto': 'https' } }, (answer) => {
|
|
22
|
+
if (observe) {
|
|
23
|
+
answer.on('end', () => {
|
|
24
|
+
const headers = new Headers();
|
|
25
|
+
for (const [name, value] of Object.entries(req.headers))
|
|
26
|
+
if (value !== undefined)
|
|
27
|
+
for (const one of Array.isArray(value) ? value : [value])
|
|
28
|
+
headers.append(name, one);
|
|
29
|
+
observe({ url: `https://${headers.get('x-volter-twin-original-host') ?? headers.get('host')}${req.url}`, headers }, { status: answer.statusCode ?? 502 });
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
answer.on('error', () => res.destroy());
|
|
18
33
|
res.writeHead(answer.statusCode ?? 502, answer.headers);
|
|
19
34
|
answer.pipe(res);
|
|
20
35
|
});
|
|
21
|
-
|
|
36
|
+
requests.add(forward);
|
|
37
|
+
forward.once('close', () => requests.delete(forward));
|
|
38
|
+
forward.on('error', () => { if (!res.headersSent)
|
|
39
|
+
res.writeHead(502); res.end(); });
|
|
40
|
+
forward.on('socket', track);
|
|
41
|
+
req.on('aborted', () => forward.destroy());
|
|
42
|
+
res.on('close', () => forward.destroy());
|
|
22
43
|
req.pipe(forward);
|
|
23
44
|
});
|
|
45
|
+
server.on('connection', track);
|
|
24
46
|
server.on('upgrade', (req, socket, head) => {
|
|
25
47
|
const upstream = connect(served, '127.0.0.1', () => {
|
|
26
48
|
const lines = [`${req.method} ${req.url} HTTP/1.1`, ...Object.entries({ ...req.headers, 'x-forwarded-proto': 'https' }).flatMap(([k, v]) => (Array.isArray(v) ? v : [v]).map((x) => `${k}: ${x}`)), '', ''];
|
|
@@ -30,15 +52,115 @@ export async function worldProxy(served) {
|
|
|
30
52
|
upstream.pipe(socket);
|
|
31
53
|
socket.pipe(upstream);
|
|
32
54
|
});
|
|
55
|
+
track(upstream);
|
|
33
56
|
upstream.on('error', () => socket.destroy());
|
|
34
57
|
socket.on('error', () => upstream.destroy());
|
|
58
|
+
socket.on('close', () => upstream.destroy());
|
|
59
|
+
});
|
|
60
|
+
await new Promise((done, reject) => {
|
|
61
|
+
server.once('error', reject);
|
|
62
|
+
server.listen(0, '127.0.0.1', () => { server.off('error', reject); done(); });
|
|
35
63
|
});
|
|
36
|
-
await new Promise((r) => server.listen(0, '127.0.0.1', () => r()));
|
|
37
64
|
server.unref();
|
|
38
|
-
|
|
65
|
+
let stopping;
|
|
66
|
+
return {
|
|
67
|
+
port: server.address().port,
|
|
68
|
+
stop: () => stopping ??= (async () => {
|
|
69
|
+
const closed = new Promise((done, reject) => server.close((error) => error ? reject(error) : done()));
|
|
70
|
+
const pending = [...requests].map((request) => new Promise((done) => { request.once('close', () => done()); request.destroy(); }));
|
|
71
|
+
const connections = [...sockets].map((socket) => new Promise((done) => { socket.once('close', () => done()); socket.destroy(); }));
|
|
72
|
+
await Promise.all([closed, ...pending, ...connections]);
|
|
73
|
+
})(),
|
|
74
|
+
};
|
|
39
75
|
}
|
|
40
|
-
/**
|
|
41
|
-
|
|
76
|
+
/** A served socket either opens within its five-second handshake or closes under the same owner. */
|
|
77
|
+
export function openWebSocket(socket) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
let settled = false;
|
|
80
|
+
const finish = (opened) => {
|
|
81
|
+
if (settled)
|
|
82
|
+
return;
|
|
83
|
+
settled = true;
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
socket.removeEventListener('open', openedSocket);
|
|
86
|
+
socket.removeEventListener('error', failed);
|
|
87
|
+
socket.removeEventListener('close', failed);
|
|
88
|
+
try {
|
|
89
|
+
if (!opened)
|
|
90
|
+
socket.close();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
reject(error);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
resolve(opened);
|
|
97
|
+
};
|
|
98
|
+
const openedSocket = () => finish(true);
|
|
99
|
+
const failed = () => finish(false);
|
|
100
|
+
const timer = setTimeout(failed, 5000);
|
|
101
|
+
socket.addEventListener('open', openedSocket, { once: true });
|
|
102
|
+
socket.addEventListener('error', failed, { once: true });
|
|
103
|
+
socket.addEventListener('close', failed, { once: true });
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** One request and its complete body share the check's ten-second lifetime, including caller cancellation. */
|
|
107
|
+
export async function readHttpResponse(fetcher, request) {
|
|
108
|
+
request.signal.throwIfAborted();
|
|
109
|
+
const abort = new AbortController();
|
|
110
|
+
const signal = AbortSignal.any([request.signal, abort.signal]);
|
|
111
|
+
const timer = setTimeout(() => abort.abort(new DOMException('request exceeded 10 s', 'TimeoutError')), 10_000);
|
|
112
|
+
let cancel;
|
|
113
|
+
const interrupted = new Promise((_, reject) => {
|
|
114
|
+
cancel = () => reject(signal.reason);
|
|
115
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
116
|
+
if (signal.aborted)
|
|
117
|
+
cancel();
|
|
118
|
+
});
|
|
119
|
+
let reader;
|
|
120
|
+
try {
|
|
121
|
+
const arriving = fetcher(new Request(request, { signal })).then((response) => {
|
|
122
|
+
if (signal.aborted)
|
|
123
|
+
void response.body?.cancel(signal.reason).catch(() => undefined);
|
|
124
|
+
return response;
|
|
125
|
+
});
|
|
126
|
+
const response = await Promise.race([arriving, interrupted]);
|
|
127
|
+
reader = response.body?.getReader();
|
|
128
|
+
const chunks = [];
|
|
129
|
+
let size = 0;
|
|
130
|
+
let bodyError;
|
|
131
|
+
try {
|
|
132
|
+
if (reader)
|
|
133
|
+
for (;;) {
|
|
134
|
+
const chunk = await Promise.race([reader.read(), interrupted]);
|
|
135
|
+
if (chunk.done)
|
|
136
|
+
break;
|
|
137
|
+
chunks.push(chunk.value);
|
|
138
|
+
size += chunk.value.byteLength;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
bodyError = { message: String(error), cancelled: signal.aborted || error instanceof Error && ['AbortError', 'TimeoutError'].includes(error.name) };
|
|
143
|
+
void reader?.cancel(error).catch(() => undefined);
|
|
144
|
+
}
|
|
145
|
+
const bodyBytes = new Uint8Array(size);
|
|
146
|
+
let offset = 0;
|
|
147
|
+
for (const chunk of chunks) {
|
|
148
|
+
bodyBytes.set(chunk, offset);
|
|
149
|
+
offset += chunk.byteLength;
|
|
150
|
+
}
|
|
151
|
+
const cookies = response.headers.getSetCookie();
|
|
152
|
+
return { status: response.status, headers: { ...Object.fromEntries(response.headers), ...(cookies.length ? { 'set-cookie': cookies.join(', ') } : {}) },
|
|
153
|
+
text: new TextDecoder().decode(bodyBytes), bodyBytes, ...(bodyError ? { bodyError } : {}) };
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
if (cancel)
|
|
158
|
+
signal.removeEventListener('abort', cancel);
|
|
159
|
+
reader?.releaseLock();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/** HTTP to a served pack, with the vendor Host and manual redirects. Headers resolve before the streamed body;
|
|
163
|
+
* its caller's abort signal owns the fresh connection until the body ends or is cancelled. */
|
|
42
164
|
export function servedHttpFetch(port) {
|
|
43
165
|
return (async (input, init) => {
|
|
44
166
|
const request = new Request(input, init);
|
|
@@ -49,28 +171,68 @@ export function servedHttpFetch(port) {
|
|
|
49
171
|
const signal = init?.signal ?? (input instanceof Request ? input.signal : request.signal);
|
|
50
172
|
return new Promise((resolve, reject) => {
|
|
51
173
|
const cleanup = () => signal.removeEventListener('abort', abort);
|
|
52
|
-
|
|
174
|
+
let responseFailed;
|
|
175
|
+
const failed = (error) => { cleanup(); if (responseFailed)
|
|
176
|
+
responseFailed(error);
|
|
177
|
+
else
|
|
178
|
+
reject(error); };
|
|
53
179
|
const sent = httpRequest({ host: '127.0.0.1', port, method: request.method, path: `${url.pathname}${url.search}`, headers, agent: false }, (res) => {
|
|
180
|
+
res.pause();
|
|
181
|
+
const answer = new Headers();
|
|
182
|
+
for (const [k, v] of Object.entries(res.headers))
|
|
183
|
+
if (v !== undefined)
|
|
184
|
+
for (const one of Array.isArray(v) ? v : [v])
|
|
185
|
+
answer.append(k, one);
|
|
54
186
|
const chunks = [];
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
187
|
+
let controller;
|
|
188
|
+
let ended = false, cancelled = false, bodyError;
|
|
189
|
+
const pump = () => {
|
|
190
|
+
if (cancelled)
|
|
191
|
+
return;
|
|
192
|
+
while (chunks.length && (controller.desiredSize ?? 0) > 0)
|
|
193
|
+
controller.enqueue(chunks.shift());
|
|
194
|
+
if (chunks.length || (controller.desiredSize ?? 0) <= 0) {
|
|
195
|
+
res.pause();
|
|
196
|
+
return;
|
|
65
197
|
}
|
|
66
|
-
|
|
67
|
-
|
|
198
|
+
// Do not error until the reader has taken queued bytes: an errored web stream discards its queue.
|
|
199
|
+
if (bodyError) {
|
|
200
|
+
cancelled = true;
|
|
201
|
+
controller.error(bodyError);
|
|
68
202
|
}
|
|
203
|
+
else if (ended) {
|
|
204
|
+
cancelled = true;
|
|
205
|
+
controller.close();
|
|
206
|
+
}
|
|
207
|
+
else
|
|
208
|
+
res.resume();
|
|
209
|
+
};
|
|
210
|
+
const body = new ReadableStream({
|
|
211
|
+
start(value) { controller = value; },
|
|
212
|
+
pull() { pump(); },
|
|
213
|
+
cancel() { cancelled = true; chunks.length = 0; cleanup(); res.destroy(); sent.destroy(); },
|
|
69
214
|
});
|
|
70
|
-
|
|
215
|
+
responseFailed = (error) => { bodyError ??= error; cleanup(); pump(); };
|
|
216
|
+
res.on('data', (chunk) => { chunks.push(chunk); pump(); });
|
|
217
|
+
res.on('end', () => { ended = true; cleanup(); pump(); });
|
|
218
|
+
res.on('error', responseFailed);
|
|
219
|
+
res.on('close', () => { if (!res.complete && !cancelled)
|
|
220
|
+
responseFailed(new Error('HTTP response closed before completion')); });
|
|
221
|
+
const noBody = request.method === 'HEAD' || res.statusCode === 204 || res.statusCode === 205 || res.statusCode === 304;
|
|
222
|
+
try {
|
|
223
|
+
resolve(new Response(noBody ? null : body, { status: res.statusCode ?? 0, headers: answer }));
|
|
224
|
+
if (noBody)
|
|
225
|
+
res.resume();
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
cancelled = true;
|
|
229
|
+
cleanup();
|
|
230
|
+
res.destroy();
|
|
231
|
+
reject(error);
|
|
232
|
+
}
|
|
71
233
|
});
|
|
72
234
|
const abort = () => {
|
|
73
|
-
const error = new Error('served HTTP request aborted');
|
|
235
|
+
const error = signal.reason instanceof Error ? signal.reason : new Error('served HTTP request aborted');
|
|
74
236
|
sent.destroy(error);
|
|
75
237
|
failed(error);
|
|
76
238
|
};
|
package/dist/src/clients/jira.js
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { standardPackageFile } from "../package-file.js";
|
|
10
|
+
const { importSdk } = await import(__rewriteRelativeImportExtension(new URL('import.mjs', standardPackageFile('client-sdks/')).href));
|
|
11
|
+
const { Version3Client, HttpException } = await importSdk('jira.js');
|
|
1
12
|
const README = 'https://www.npmjs.com/package/jira.js/v/5.4.0';
|
|
2
13
|
const CREATE = 'https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-post';
|
|
3
14
|
const ADF = 'https://developer.atlassian.com/cloud/jira/platform/apis/document/structure/';
|
|
@@ -12,7 +23,6 @@ async function site(c, slug, apiToken) {
|
|
|
12
23
|
const email = `admin@${slug}.example`;
|
|
13
24
|
const made = await door('/_twin/sites', { name: slug, admin: { name: 'Admin', email, password: 'sdk-case-2026' } });
|
|
14
25
|
const token = apiToken ?? (await door('/_twin/api-token', { site: slug, email, password: 'sdk-case-2026' })).apiToken;
|
|
15
|
-
const { Version3Client } = await import('jira.js');
|
|
16
26
|
const jira = new Version3Client({ host: `https://${slug}.atlassian.net`, authentication: { basic: { email, apiToken: token } }, baseRequestConfig: { httpsAgent: c.agent } });
|
|
17
27
|
return { jira, admin: made.admin };
|
|
18
28
|
}
|
|
@@ -55,7 +65,6 @@ export const cases = [
|
|
|
55
65
|
asks: 'a request with an API token the account never made is refused, and the client throws its HttpException carrying the status',
|
|
56
66
|
source: `${README} "\`HttpException\`: Server responded with an error (includes parsed error details)"`,
|
|
57
67
|
async run(c) {
|
|
58
|
-
const { HttpException } = await import('jira.js');
|
|
59
68
|
const { jira } = await site(c, 'sdk-refused', 'ATATT-not-a-token-this-account-made');
|
|
60
69
|
try {
|
|
61
70
|
await jira.projects.searchProjects({ maxResults: 50 });
|
package/dist/src/clients/run.js
CHANGED
|
@@ -20,19 +20,24 @@ if (!Array.isArray(cases) || cases.length === 0)
|
|
|
20
20
|
throw new Error(`${job.vendor} client module declares no cases; there is no SDK execution evidence`);
|
|
21
21
|
// every connection a case makes goes through the World's proxy, as an application's does in a World
|
|
22
22
|
const proxy = await worldProxy(job.port);
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
require(
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
23
|
+
try {
|
|
24
|
+
// Bind only this artifact's declared vendor hosts to the assessed server. The enclosing World's
|
|
25
|
+
// injector stays active, and untwinned destinations keep its refusal policy.
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
require('@volter/world-core/pack-facts').extendPackFacts([job.packFacts]);
|
|
28
|
+
const facts = JSON.parse(readFileSync(job.packFacts, 'utf8')).packs[job.vendor];
|
|
29
|
+
const keys = [...new Set((facts.hosts ?? []).map((rule) => rule.key ?? job.vendor))];
|
|
30
|
+
require('@volter/world-core/inject').install(Object.fromEntries(keys.map((key) => [key, `http://127.0.0.1:${proxy.port}`])));
|
|
31
|
+
const context = clientContext(proxy.port, job.host, job.env, job.scenarioPath, job.raw ? job.port : undefined);
|
|
32
|
+
for (const c of cases) {
|
|
33
|
+
const failures = await Promise.race([
|
|
34
|
+
c.run(context).catch((e) => [`the case threw: ${e.message}`]),
|
|
35
|
+
new Promise((r) => setTimeout(() => r(['no outcome in 30 s']), 30_000)),
|
|
36
|
+
]);
|
|
37
|
+
process.stdout.write(`${JSON.stringify({ id: c.id, asks: c.asks, failures })}\n`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
await proxy.stop();
|
|
37
42
|
}
|
|
38
43
|
process.exit(0);
|
|
@@ -1,6 +1,16 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
import { standardPackageFile } from "../package-file.js";
|
|
10
|
+
const { importSdk } = await import(__rewriteRelativeImportExtension(new URL('import.mjs', standardPackageFile('client-sdks/')).href));
|
|
11
|
+
const { createTinybirdApi, TinybirdApiError } = await importSdk('@tinybirdco/sdk/api');
|
|
1
12
|
const SDK = 'https://unpkg.com/@tinybirdco/sdk@0.0.84/src/api/api.ts';
|
|
2
|
-
|
|
3
|
-
const { createTinybirdApi } = await import('@tinybirdco/sdk/api');
|
|
13
|
+
function client(c, fetch = c.fetch) {
|
|
4
14
|
return createTinybirdApi({ baseUrl: `https://${c.host}`, token: c.env.TINYBIRD_API_KEY ?? c.env.TINYBIRD_TOKEN ?? '', fetch });
|
|
5
15
|
}
|
|
6
16
|
export const cases = [
|
|
@@ -9,8 +19,7 @@ export const cases = [
|
|
|
9
19
|
asks: 'a request token override is used instead of the Workspace token, and its refusal retains the status and JSON error in TinybirdApiError',
|
|
10
20
|
source: `${SDK} "Error thrown by TinybirdApi when a response is not OK"`,
|
|
11
21
|
async run(c) {
|
|
12
|
-
const
|
|
13
|
-
const api = await client(c);
|
|
22
|
+
const api = client(c);
|
|
14
23
|
try {
|
|
15
24
|
await api.ingest('client_case_events', { label: 'refused' }, { token: 'throwaway-invalid-token' });
|
|
16
25
|
return ['the invalid request token was accepted'];
|
|
@@ -36,7 +45,7 @@ export const cases = [
|
|
|
36
45
|
async run(c) {
|
|
37
46
|
let calls = 0;
|
|
38
47
|
const routed = ((input, init) => { calls++; return c.fetch(input, init); });
|
|
39
|
-
const api =
|
|
48
|
+
const api = client(c, routed);
|
|
40
49
|
try {
|
|
41
50
|
await api.ingest('client_case_events', { timestamp: new Date('2024-01-15T10:30:00Z') });
|
|
42
51
|
return ['a Date object was accepted for ingestion'];
|
|
@@ -60,7 +69,7 @@ export const cases = [
|
|
|
60
69
|
asks: 'an object query parameter becomes JSON and the Endpoint uses its value rather than the object string representation',
|
|
61
70
|
source: `${SDK} "Plain objects (e.g. p.json() params) become JSON strings; Dates are rejected;"`,
|
|
62
71
|
async run(c) {
|
|
63
|
-
const api =
|
|
72
|
+
const api = client(c);
|
|
64
73
|
await api.requestJson('/v0/datasources', { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ name: 'client_json_rows', schema: 'name String' }).toString() });
|
|
65
74
|
await api.ingestBatch('client_json_rows', [{ name: 'selected' }, { name: 'excluded' }], { wait: true });
|
|
66
75
|
await api.requestJson('/v0/pipes', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: 'client_json_filter', nodes: [{ name: 'endpoint', type: 'endpoint', sql: "%\nSELECT name FROM client_json_rows WHERE name = {{JSON(filters, '{\"name\":\"selected\"}').get('name')}}" }] }) });
|