archal 0.9.8 → 0.9.9
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 +163 -93
- package/bin/archal.cjs +3 -3
- package/dist/index.cjs +82301 -0
- package/dist/index.d.cts +1 -0
- package/dist/seed/dynamic-generator.cjs +45640 -0
- package/dist/seed/dynamic-generator.d.cts +67 -0
- package/dist/vitest/chunk-RKYS44AS.js +2216 -0
- package/dist/vitest/chunk-YJICENME.js +1230 -0
- package/dist/vitest/chunk-YV6BH6DO.js +45974 -0
- package/dist/vitest/index.cjs +51963 -0
- package/dist/vitest/index.d.ts +398 -0
- package/dist/vitest/index.js +2669 -0
- package/dist/vitest/runtime/hosted-session-reaper.cjs +29349 -0
- package/dist/vitest/runtime/hosted-session-reaper.d.ts +2 -0
- package/dist/vitest/runtime/hosted-session-reaper.js +58 -0
- package/dist/vitest/runtime/setup-files.d.ts +2 -0
- package/dist/vitest/runtime/setup-files.js +27 -0
- package/dist/vitest/src-JGHX6UKK.js +94 -0
- package/package.json +15 -19
- package/twin-assets/discord/fidelity.json +113 -0
- package/twin-assets/discord/tools.json +1953 -0
- package/twin-assets/github/fidelity.json +13 -0
- package/twin-assets/github/tools.json +21818 -0
- package/twin-assets/google-workspace/fidelity.json +19 -0
- package/twin-assets/google-workspace/tools.json +10191 -0
- package/twin-assets/jira/fidelity.json +40 -0
- package/twin-assets/jira/tools.json +17387 -0
- package/twin-assets/linear/fidelity.json +18 -0
- package/twin-assets/linear/tools.json +6496 -0
- package/twin-assets/ramp/fidelity.json +22 -0
- package/twin-assets/ramp/tools.json +2610 -0
- package/twin-assets/slack/fidelity.json +20 -0
- package/twin-assets/slack/tools.json +7301 -0
- package/twin-assets/stripe/fidelity.json +22 -0
- package/twin-assets/stripe/tools.json +15284 -0
- package/twin-assets/supabase/fidelity.json +13 -0
- package/twin-assets/supabase/tools.json +2973 -0
- package/dist/vitest.d.ts +0 -1
- package/dist/vitest.js +0 -23
|
@@ -0,0 +1,2216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_HOSTED_API_BASE_URL,
|
|
3
|
+
DEFAULT_READY_TIMEOUT_MS,
|
|
4
|
+
DEFAULT_RENEW_INTERVAL_MS,
|
|
5
|
+
DEFAULT_SESSION_TTL_SECONDS,
|
|
6
|
+
HostedSessionClient,
|
|
7
|
+
MIN_READY_TIMEOUT_MS,
|
|
8
|
+
MIN_RENEW_INTERVAL_MS,
|
|
9
|
+
createHostedAuthLease,
|
|
10
|
+
decodeConfig,
|
|
11
|
+
fileExists,
|
|
12
|
+
getSessionIdFilePath,
|
|
13
|
+
isProcessAlive,
|
|
14
|
+
loadFileSeedsIntoTwins,
|
|
15
|
+
normalizeApiBaseUrl,
|
|
16
|
+
parsePositiveInteger,
|
|
17
|
+
redactSessionSnapshot,
|
|
18
|
+
requestedSeedsMatchSession,
|
|
19
|
+
sleep,
|
|
20
|
+
trimEnv
|
|
21
|
+
} from "./chunk-YV6BH6DO.js";
|
|
22
|
+
|
|
23
|
+
// src/runtime-module-resolution.ts
|
|
24
|
+
import { dirname, extname, resolve } from "path";
|
|
25
|
+
import { fileURLToPath } from "url";
|
|
26
|
+
function resolveRuntimeModuleExtension(currentExtension, buildExtension) {
|
|
27
|
+
if (currentExtension === ".ts" || currentExtension === ".mts" || currentExtension === ".cts") {
|
|
28
|
+
return currentExtension;
|
|
29
|
+
}
|
|
30
|
+
return buildExtension ?? currentExtension ?? ".js";
|
|
31
|
+
}
|
|
32
|
+
function resolveRuntimeModuleFromFile(currentModuleFile, relativePath, buildExtension) {
|
|
33
|
+
const currentExtension = extname(currentModuleFile) || ".js";
|
|
34
|
+
const resolvedExtension = resolveRuntimeModuleExtension(currentExtension, buildExtension);
|
|
35
|
+
return resolve(dirname(currentModuleFile), relativePath.replace(/\.js$/, resolvedExtension));
|
|
36
|
+
}
|
|
37
|
+
function resolveRuntimeModule(relativePath, buildExtension) {
|
|
38
|
+
const currentModuleFile = typeof __filename === "string" ? __filename : fileURLToPath(import.meta.url);
|
|
39
|
+
return resolveRuntimeModuleFromFile(currentModuleFile, relativePath, buildExtension);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/runtime/session-snapshot.ts
|
|
43
|
+
var ARCHAL_VITEST_CONFIG_ENV = "ARCHAL_VITEST_CONFIG";
|
|
44
|
+
function readArchalVitestConfigFromEnv() {
|
|
45
|
+
const encoded = process.env[ARCHAL_VITEST_CONFIG_ENV];
|
|
46
|
+
if (!encoded) {
|
|
47
|
+
throw new Error(`Missing ${ARCHAL_VITEST_CONFIG_ENV}. archalVitestProject() must set worker config before bootstrap.`);
|
|
48
|
+
}
|
|
49
|
+
return readArchalVitestConfig(encoded);
|
|
50
|
+
}
|
|
51
|
+
function readArchalVitestConfig(encoded) {
|
|
52
|
+
if (!encoded) {
|
|
53
|
+
throw new Error(`Missing ${ARCHAL_VITEST_CONFIG_ENV}. archalVitestProject() must set worker config before bootstrap.`);
|
|
54
|
+
}
|
|
55
|
+
return decodeConfig(encoded);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ../route-runtime-core/src/errors.ts
|
|
59
|
+
var RouteRuntimePolicyError = class extends Error {
|
|
60
|
+
code;
|
|
61
|
+
service;
|
|
62
|
+
url;
|
|
63
|
+
constructor(input) {
|
|
64
|
+
super(input.message);
|
|
65
|
+
this.name = "RouteRuntimePolicyError";
|
|
66
|
+
this.code = input.code;
|
|
67
|
+
this.service = input.service;
|
|
68
|
+
this.url = input.url;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// ../route-runtime-core/src/runtime.ts
|
|
73
|
+
import { createRequire } from "module";
|
|
74
|
+
|
|
75
|
+
// ../route-runtime-core/src/runtime-request.ts
|
|
76
|
+
var DEFAULT_HTTP_PROTOCOL = "http:";
|
|
77
|
+
var DEFAULT_HTTPS_PROTOCOL = "https:";
|
|
78
|
+
function normalizeProtocol(protocol, fallback) {
|
|
79
|
+
if (!protocol) return fallback;
|
|
80
|
+
return protocol.endsWith(":") ? protocol : `${protocol}:`;
|
|
81
|
+
}
|
|
82
|
+
function normalizeHost(value) {
|
|
83
|
+
if (value.includes("://")) {
|
|
84
|
+
const parsed = new URL(value);
|
|
85
|
+
return {
|
|
86
|
+
hostname: parsed.hostname,
|
|
87
|
+
port: parsed.port || void 0
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (value.startsWith("[")) {
|
|
91
|
+
const closingBracketIndex = value.indexOf("]");
|
|
92
|
+
if (closingBracketIndex !== -1) {
|
|
93
|
+
const hostname = value.slice(1, closingBracketIndex);
|
|
94
|
+
const port = value.slice(closingBracketIndex + 1).replace(/^:/, "") || void 0;
|
|
95
|
+
return { hostname, port };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const lastColonIndex = value.lastIndexOf(":");
|
|
99
|
+
if (lastColonIndex === -1 || value.indexOf(":") !== lastColonIndex) {
|
|
100
|
+
return { hostname: value };
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
hostname: value.slice(0, lastColonIndex),
|
|
104
|
+
port: value.slice(lastColonIndex + 1) || void 0
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function buildUrlFromOptions(options, defaultProtocol) {
|
|
108
|
+
const protocol = normalizeProtocol(
|
|
109
|
+
typeof options.protocol === "string" ? options.protocol : void 0,
|
|
110
|
+
defaultProtocol
|
|
111
|
+
);
|
|
112
|
+
const rawHost = typeof options.hostname === "string" ? options.hostname : typeof options.host === "string" ? normalizeHost(options.host).hostname : "localhost";
|
|
113
|
+
const rawPort = options.port == null ? typeof options.host === "string" ? normalizeHost(options.host).port : void 0 : String(options.port);
|
|
114
|
+
const base = `${protocol}//${rawHost}${rawPort ? `:${rawPort}` : ""}`;
|
|
115
|
+
const path = typeof options.path === "string" && options.path.length > 0 ? options.path : "/";
|
|
116
|
+
return new URL(path, base);
|
|
117
|
+
}
|
|
118
|
+
function applyRequestOptionsToUrl(requestUrl, options, defaultProtocol) {
|
|
119
|
+
const effectiveUrl = new URL(requestUrl.toString());
|
|
120
|
+
const protocol = normalizeProtocol(
|
|
121
|
+
typeof options.protocol === "string" ? options.protocol : void 0,
|
|
122
|
+
effectiveUrl.protocol || defaultProtocol
|
|
123
|
+
);
|
|
124
|
+
effectiveUrl.protocol = protocol;
|
|
125
|
+
const normalizedHost = typeof options.hostname === "string" ? normalizeHost(options.hostname) : typeof options.host === "string" ? normalizeHost(options.host) : null;
|
|
126
|
+
if (normalizedHost) {
|
|
127
|
+
effectiveUrl.hostname = normalizedHost.hostname;
|
|
128
|
+
effectiveUrl.port = normalizedHost.port ?? "";
|
|
129
|
+
}
|
|
130
|
+
if (options.port != null) {
|
|
131
|
+
effectiveUrl.port = String(options.port);
|
|
132
|
+
}
|
|
133
|
+
if (typeof options.path === "string" && options.path.length > 0) {
|
|
134
|
+
const pathUrl = new URL(options.path, `${effectiveUrl.protocol}//${effectiveUrl.host}`);
|
|
135
|
+
effectiveUrl.pathname = pathUrl.pathname;
|
|
136
|
+
effectiveUrl.search = pathUrl.search;
|
|
137
|
+
effectiveUrl.hash = pathUrl.hash;
|
|
138
|
+
}
|
|
139
|
+
return effectiveUrl;
|
|
140
|
+
}
|
|
141
|
+
function normalizeHeaders(headers) {
|
|
142
|
+
if (!headers || Array.isArray(headers)) {
|
|
143
|
+
return headers;
|
|
144
|
+
}
|
|
145
|
+
const copy = {};
|
|
146
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
147
|
+
if (value === void 0) continue;
|
|
148
|
+
copy[key] = value;
|
|
149
|
+
}
|
|
150
|
+
delete copy["host"];
|
|
151
|
+
delete copy["Host"];
|
|
152
|
+
return copy;
|
|
153
|
+
}
|
|
154
|
+
function applyRoutedHeaders(headers, service) {
|
|
155
|
+
const originalAuthorization = headers.get("authorization");
|
|
156
|
+
const routedRequestHeaders = typeof service.routedRequestHeaders === "function" ? service.routedRequestHeaders() : service.routedRequestHeaders;
|
|
157
|
+
for (const [name, value] of Object.entries(routedRequestHeaders ?? {})) {
|
|
158
|
+
headers.set(name, value);
|
|
159
|
+
}
|
|
160
|
+
if (!service.forwardedAuthorizationHeader) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (originalAuthorization) {
|
|
164
|
+
headers.set(service.forwardedAuthorizationHeader, originalAuthorization);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function buildPatchedHeaders(headers, service) {
|
|
168
|
+
const normalizedHeaders = new Headers(normalizeHeaders(headers));
|
|
169
|
+
normalizedHeaders.delete("x-archal-original-authorization");
|
|
170
|
+
normalizedHeaders.delete("x-archal-original-bearer-token");
|
|
171
|
+
if (service.forwardedAuthorizationHeader) {
|
|
172
|
+
normalizedHeaders.delete(service.forwardedAuthorizationHeader);
|
|
173
|
+
}
|
|
174
|
+
applyRoutedHeaders(normalizedHeaders, service);
|
|
175
|
+
const patchedHeaders = {};
|
|
176
|
+
for (const [name, value] of normalizedHeaders.entries()) {
|
|
177
|
+
patchedHeaders[name] = value;
|
|
178
|
+
}
|
|
179
|
+
return patchedHeaders;
|
|
180
|
+
}
|
|
181
|
+
function parseRequestArgs(defaultProtocol, args) {
|
|
182
|
+
let callback;
|
|
183
|
+
for (let index = args.length - 1; index >= 0; index -= 1) {
|
|
184
|
+
const candidate = args[index];
|
|
185
|
+
if (typeof candidate === "function") {
|
|
186
|
+
callback = candidate;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const first = args[0];
|
|
191
|
+
const second = args[1];
|
|
192
|
+
if (first instanceof URL || typeof first === "string") {
|
|
193
|
+
const baseRequestUrl = first instanceof URL ? new URL(first.toString()) : new URL(first, `${defaultProtocol}//localhost`);
|
|
194
|
+
const options = second && typeof second === "object" && !(second instanceof URL) ? { ...second } : {};
|
|
195
|
+
return {
|
|
196
|
+
requestUrl: applyRequestOptionsToUrl(baseRequestUrl, options, defaultProtocol),
|
|
197
|
+
options,
|
|
198
|
+
callback
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (first && typeof first === "object") {
|
|
202
|
+
const options = { ...first };
|
|
203
|
+
return {
|
|
204
|
+
requestUrl: buildUrlFromOptions(options, defaultProtocol),
|
|
205
|
+
options,
|
|
206
|
+
callback
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
requestUrl: new URL(`${defaultProtocol}//localhost/`),
|
|
211
|
+
options: {},
|
|
212
|
+
callback
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function buildPatchedArgs(rewrittenUrl, options, service, sourceProtocol, callback) {
|
|
216
|
+
const patchedOptions = {
|
|
217
|
+
...options,
|
|
218
|
+
protocol: rewrittenUrl.protocol,
|
|
219
|
+
hostname: rewrittenUrl.hostname,
|
|
220
|
+
port: rewrittenUrl.port ? Number(rewrittenUrl.port) : void 0,
|
|
221
|
+
path: `${rewrittenUrl.pathname}${rewrittenUrl.search}`,
|
|
222
|
+
headers: buildPatchedHeaders(options.headers, service)
|
|
223
|
+
};
|
|
224
|
+
delete patchedOptions.host;
|
|
225
|
+
if (sourceProtocol !== rewrittenUrl.protocol) {
|
|
226
|
+
delete patchedOptions.agent;
|
|
227
|
+
delete patchedOptions.createConnection;
|
|
228
|
+
}
|
|
229
|
+
if (callback) {
|
|
230
|
+
return [rewrittenUrl, patchedOptions, callback];
|
|
231
|
+
}
|
|
232
|
+
return [rewrittenUrl, patchedOptions];
|
|
233
|
+
}
|
|
234
|
+
function buildRoutedFetchRequest(request, targetUrl, service, init) {
|
|
235
|
+
const rewrittenRequest = new Request(targetUrl, request);
|
|
236
|
+
const headers = new Headers(rewrittenRequest.headers);
|
|
237
|
+
if (init?.headers) {
|
|
238
|
+
for (const [name, value] of new Headers(init.headers).entries()) {
|
|
239
|
+
headers.set(name, value);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
headers.delete("x-archal-original-authorization");
|
|
243
|
+
headers.delete("x-archal-original-bearer-token");
|
|
244
|
+
if (service.forwardedAuthorizationHeader) {
|
|
245
|
+
headers.delete(service.forwardedAuthorizationHeader);
|
|
246
|
+
}
|
|
247
|
+
applyRoutedHeaders(headers, service);
|
|
248
|
+
return new Request(rewrittenRequest, {
|
|
249
|
+
...init,
|
|
250
|
+
headers
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ../route-runtime-core/src/service-profiles.ts
|
|
255
|
+
function exactDomain(value) {
|
|
256
|
+
return { kind: "exact", value: value.toLowerCase() };
|
|
257
|
+
}
|
|
258
|
+
function suffixDomain(value) {
|
|
259
|
+
return { kind: "suffix", value: value.toLowerCase() };
|
|
260
|
+
}
|
|
261
|
+
function matchesDomainPattern(hostname, pattern) {
|
|
262
|
+
const normalizedHostname = hostname.toLowerCase();
|
|
263
|
+
if (pattern.kind === "exact") {
|
|
264
|
+
return normalizedHostname === pattern.value;
|
|
265
|
+
}
|
|
266
|
+
return normalizedHostname === pattern.value || normalizedHostname.endsWith(`.${pattern.value}`);
|
|
267
|
+
}
|
|
268
|
+
function classifyHostname(profile, hostname) {
|
|
269
|
+
if (profile.routeDomains.some((pattern) => matchesDomainPattern(hostname, pattern))) {
|
|
270
|
+
return "route";
|
|
271
|
+
}
|
|
272
|
+
if (profile.hardFailDomains.some((pattern) => matchesDomainPattern(hostname, pattern))) {
|
|
273
|
+
return "hard-fail";
|
|
274
|
+
}
|
|
275
|
+
if (profile.warningDomains.some((pattern) => matchesDomainPattern(hostname, pattern))) {
|
|
276
|
+
return "warn";
|
|
277
|
+
}
|
|
278
|
+
return "ignore";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ../route-runtime-core/src/runtime.ts
|
|
282
|
+
var require2 = createRequire(typeof __filename === "string" ? __filename : import.meta.url);
|
|
283
|
+
var httpModule = require2("node:http");
|
|
284
|
+
var httpsModule = require2("node:https");
|
|
285
|
+
function now() {
|
|
286
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
287
|
+
}
|
|
288
|
+
function normalizePathPrefix(pathPrefix) {
|
|
289
|
+
const withLeadingSlash = pathPrefix.startsWith("/") ? pathPrefix : `/${pathPrefix}`;
|
|
290
|
+
return withLeadingSlash.replace(/\/+$/, "") || "/";
|
|
291
|
+
}
|
|
292
|
+
function resolveRoutedPathname(sourceUrl, service) {
|
|
293
|
+
const sourcePathname = sourceUrl.pathname || "/";
|
|
294
|
+
if (!service.upstreamBasePath) {
|
|
295
|
+
return sourcePathname;
|
|
296
|
+
}
|
|
297
|
+
const normalizedUpstreamBasePath = normalizePathPrefix(service.upstreamBasePath);
|
|
298
|
+
if (normalizedUpstreamBasePath === "/") {
|
|
299
|
+
return sourcePathname;
|
|
300
|
+
}
|
|
301
|
+
if (sourcePathname === normalizedUpstreamBasePath) {
|
|
302
|
+
return "/";
|
|
303
|
+
}
|
|
304
|
+
if (sourcePathname.startsWith(`${normalizedUpstreamBasePath}/`)) {
|
|
305
|
+
return sourcePathname.slice(normalizedUpstreamBasePath.length) || "/";
|
|
306
|
+
}
|
|
307
|
+
return sourcePathname;
|
|
308
|
+
}
|
|
309
|
+
function mergeUrl(service, sourceUrl) {
|
|
310
|
+
const targetBaseUrl = new URL(service.baseUrl);
|
|
311
|
+
const targetPathname = targetBaseUrl.pathname.endsWith("/") ? targetBaseUrl.pathname.slice(0, -1) : targetBaseUrl.pathname;
|
|
312
|
+
targetBaseUrl.pathname = `${targetPathname}${resolveRoutedPathname(sourceUrl, service)}` || "/";
|
|
313
|
+
targetBaseUrl.search = sourceUrl.search;
|
|
314
|
+
targetBaseUrl.hash = sourceUrl.hash;
|
|
315
|
+
return targetBaseUrl;
|
|
316
|
+
}
|
|
317
|
+
function bridgeSecureConnectIfNeeded(request, sourceProtocol, targetProtocol) {
|
|
318
|
+
if (sourceProtocol !== DEFAULT_HTTPS_PROTOCOL || targetProtocol !== DEFAULT_HTTP_PROTOCOL) {
|
|
319
|
+
return request;
|
|
320
|
+
}
|
|
321
|
+
request.once("socket", (socket) => {
|
|
322
|
+
if (socket.connecting) {
|
|
323
|
+
socket.once("connect", () => {
|
|
324
|
+
socket.emit("secureConnect");
|
|
325
|
+
});
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
queueMicrotask(() => {
|
|
329
|
+
socket.emit("secureConnect");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
return request;
|
|
333
|
+
}
|
|
334
|
+
var NodeRouteRuntime = class {
|
|
335
|
+
profiles;
|
|
336
|
+
servicesByName;
|
|
337
|
+
events = [];
|
|
338
|
+
onEvent;
|
|
339
|
+
traceRecords = [];
|
|
340
|
+
traceEnabled;
|
|
341
|
+
onTrace;
|
|
342
|
+
started = false;
|
|
343
|
+
routingInstalled = false;
|
|
344
|
+
originalHttpRequest = httpModule.request;
|
|
345
|
+
originalHttpGet = httpModule.get;
|
|
346
|
+
originalHttpsRequest = httpsModule.request;
|
|
347
|
+
originalHttpsGet = httpsModule.get;
|
|
348
|
+
originalFetch = globalThis.fetch;
|
|
349
|
+
constructor(config) {
|
|
350
|
+
this.profiles = config.profiles;
|
|
351
|
+
this.servicesByName = new Map(config.services.map((service) => [service.name, service]));
|
|
352
|
+
this.onEvent = config.onEvent;
|
|
353
|
+
this.traceEnabled = config.trace?.enabled ?? false;
|
|
354
|
+
this.onTrace = config.trace?.onTrace;
|
|
355
|
+
}
|
|
356
|
+
async start() {
|
|
357
|
+
if (this.started) return;
|
|
358
|
+
this.started = true;
|
|
359
|
+
this.emit({ type: "runtime_started", timestamp: now() });
|
|
360
|
+
}
|
|
361
|
+
async reset() {
|
|
362
|
+
this.events.length = 0;
|
|
363
|
+
this.emit({ type: "runtime_reset", timestamp: now() });
|
|
364
|
+
}
|
|
365
|
+
async stop() {
|
|
366
|
+
this.uninstallRouting();
|
|
367
|
+
if (!this.started) return;
|
|
368
|
+
this.started = false;
|
|
369
|
+
this.emit({ type: "runtime_stopped", timestamp: now() });
|
|
370
|
+
}
|
|
371
|
+
installRouting() {
|
|
372
|
+
if (this.routingInstalled) return;
|
|
373
|
+
httpModule.request = ((...args) => this.dispatchRequest(this.originalHttpRequest, this.originalHttpsRequest, DEFAULT_HTTP_PROTOCOL, args));
|
|
374
|
+
httpModule.get = ((...args) => this.dispatchGet(this.originalHttpGet, this.originalHttpsGet, DEFAULT_HTTP_PROTOCOL, args));
|
|
375
|
+
httpsModule.request = ((...args) => this.dispatchRequest(this.originalHttpRequest, this.originalHttpsRequest, DEFAULT_HTTPS_PROTOCOL, args));
|
|
376
|
+
httpsModule.get = ((...args) => this.dispatchGet(this.originalHttpGet, this.originalHttpsGet, DEFAULT_HTTPS_PROTOCOL, args));
|
|
377
|
+
if (this.originalFetch) {
|
|
378
|
+
globalThis.fetch = (async (input, init) => {
|
|
379
|
+
const originalRequest = input instanceof Request ? input : void 0;
|
|
380
|
+
const sourceUrl = new URL(
|
|
381
|
+
originalRequest?.url ?? (input instanceof URL ? input.toString() : String(input))
|
|
382
|
+
);
|
|
383
|
+
const decision = this.evaluateRequest(sourceUrl);
|
|
384
|
+
const method = (originalRequest?.method ?? init?.method ?? "GET").toUpperCase();
|
|
385
|
+
if (decision.kind === "route") {
|
|
386
|
+
const service = this.servicesByName.get(decision.service);
|
|
387
|
+
if (!service) {
|
|
388
|
+
throw new Error(`Missing routed service config for ${decision.service}.`);
|
|
389
|
+
}
|
|
390
|
+
const targetUrl = decision.targetUrl.toString();
|
|
391
|
+
if (originalRequest) {
|
|
392
|
+
let response3;
|
|
393
|
+
try {
|
|
394
|
+
response3 = await this.originalFetch(
|
|
395
|
+
buildRoutedFetchRequest(originalRequest, decision.targetUrl, service, init)
|
|
396
|
+
);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
this.traceRequest({
|
|
399
|
+
method,
|
|
400
|
+
sourceUrl: sourceUrl.toString(),
|
|
401
|
+
manifestMatched: true,
|
|
402
|
+
service: decision.service,
|
|
403
|
+
target: "twin",
|
|
404
|
+
outcome: "failed",
|
|
405
|
+
reason: "network_error",
|
|
406
|
+
targetUrl,
|
|
407
|
+
error: error instanceof Error ? error.message : String(error)
|
|
408
|
+
});
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
this.traceRequest({
|
|
412
|
+
method,
|
|
413
|
+
sourceUrl: sourceUrl.toString(),
|
|
414
|
+
manifestMatched: true,
|
|
415
|
+
service: decision.service,
|
|
416
|
+
target: "twin",
|
|
417
|
+
outcome: "routed",
|
|
418
|
+
reason: "route",
|
|
419
|
+
targetUrl,
|
|
420
|
+
statusCode: response3.status,
|
|
421
|
+
statusText: response3.statusText
|
|
422
|
+
});
|
|
423
|
+
return response3;
|
|
424
|
+
}
|
|
425
|
+
const headers = new Headers(init?.headers);
|
|
426
|
+
applyRoutedHeaders(headers, service);
|
|
427
|
+
let response2;
|
|
428
|
+
try {
|
|
429
|
+
response2 = await this.originalFetch(new URL(targetUrl), {
|
|
430
|
+
...init,
|
|
431
|
+
headers
|
|
432
|
+
});
|
|
433
|
+
} catch (error) {
|
|
434
|
+
this.traceRequest({
|
|
435
|
+
method,
|
|
436
|
+
sourceUrl: sourceUrl.toString(),
|
|
437
|
+
manifestMatched: true,
|
|
438
|
+
service: decision.service,
|
|
439
|
+
target: "twin",
|
|
440
|
+
outcome: "failed",
|
|
441
|
+
reason: "network_error",
|
|
442
|
+
targetUrl,
|
|
443
|
+
error: error instanceof Error ? error.message : String(error)
|
|
444
|
+
});
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
this.traceRequest({
|
|
448
|
+
method,
|
|
449
|
+
sourceUrl: sourceUrl.toString(),
|
|
450
|
+
manifestMatched: true,
|
|
451
|
+
service: decision.service,
|
|
452
|
+
target: "twin",
|
|
453
|
+
outcome: "routed",
|
|
454
|
+
reason: "route",
|
|
455
|
+
targetUrl,
|
|
456
|
+
statusCode: response2.status,
|
|
457
|
+
statusText: response2.statusText
|
|
458
|
+
});
|
|
459
|
+
return response2;
|
|
460
|
+
}
|
|
461
|
+
if (decision.kind === "block") {
|
|
462
|
+
this.traceRequest({
|
|
463
|
+
method,
|
|
464
|
+
sourceUrl: sourceUrl.toString(),
|
|
465
|
+
manifestMatched: true,
|
|
466
|
+
service: decision.service,
|
|
467
|
+
target: "none",
|
|
468
|
+
outcome: "blocked",
|
|
469
|
+
reason: decision.code === "ARCHAL_UNDECLARED_SERVICE" ? "blocked_undeclared" : "blocked_escape"
|
|
470
|
+
});
|
|
471
|
+
throw new RouteRuntimePolicyError({
|
|
472
|
+
code: decision.code,
|
|
473
|
+
service: decision.service,
|
|
474
|
+
url: sourceUrl.toString(),
|
|
475
|
+
message: decision.message
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
let response;
|
|
479
|
+
try {
|
|
480
|
+
response = await this.originalFetch(input, init);
|
|
481
|
+
} catch (error) {
|
|
482
|
+
this.traceRequest({
|
|
483
|
+
method,
|
|
484
|
+
sourceUrl: sourceUrl.toString(),
|
|
485
|
+
manifestMatched: decision.kind === "warn",
|
|
486
|
+
service: decision.kind === "warn" ? decision.service : void 0,
|
|
487
|
+
target: "upstream",
|
|
488
|
+
outcome: "failed",
|
|
489
|
+
reason: "network_error",
|
|
490
|
+
error: error instanceof Error ? error.message : String(error)
|
|
491
|
+
});
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
this.traceRequest({
|
|
495
|
+
method,
|
|
496
|
+
sourceUrl: sourceUrl.toString(),
|
|
497
|
+
manifestMatched: decision.kind === "warn",
|
|
498
|
+
service: decision.kind === "warn" ? decision.service : void 0,
|
|
499
|
+
target: "upstream",
|
|
500
|
+
outcome: "bypassed",
|
|
501
|
+
reason: decision.kind === "warn" ? "warning" : "no_match",
|
|
502
|
+
statusCode: response.status,
|
|
503
|
+
statusText: response.statusText
|
|
504
|
+
});
|
|
505
|
+
return response;
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
this.routingInstalled = true;
|
|
509
|
+
this.emit({ type: "routing_installed", timestamp: now() });
|
|
510
|
+
}
|
|
511
|
+
uninstallRouting() {
|
|
512
|
+
if (!this.routingInstalled) return;
|
|
513
|
+
httpModule.request = this.originalHttpRequest;
|
|
514
|
+
httpModule.get = this.originalHttpGet;
|
|
515
|
+
httpsModule.request = this.originalHttpsRequest;
|
|
516
|
+
httpsModule.get = this.originalHttpsGet;
|
|
517
|
+
if (this.originalFetch) {
|
|
518
|
+
globalThis.fetch = this.originalFetch;
|
|
519
|
+
}
|
|
520
|
+
this.routingInstalled = false;
|
|
521
|
+
this.emit({ type: "routing_uninstalled", timestamp: now() });
|
|
522
|
+
}
|
|
523
|
+
getEvents() {
|
|
524
|
+
return [...this.events];
|
|
525
|
+
}
|
|
526
|
+
getTraceRecords() {
|
|
527
|
+
return [...this.traceRecords];
|
|
528
|
+
}
|
|
529
|
+
evaluateRequest(input) {
|
|
530
|
+
const sourceUrl = input instanceof URL ? input : new URL(input);
|
|
531
|
+
const hostname = sourceUrl.hostname.toLowerCase();
|
|
532
|
+
for (const profile of this.profiles) {
|
|
533
|
+
const classification = classifyHostname(profile, hostname);
|
|
534
|
+
if (classification === "ignore") {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const configuredService = this.servicesByName.get(profile.service);
|
|
538
|
+
const diagnosticsContext = {
|
|
539
|
+
service: profile.service,
|
|
540
|
+
hostname,
|
|
541
|
+
url: sourceUrl.toString(),
|
|
542
|
+
configuredBaseUrl: configuredService?.baseUrl
|
|
543
|
+
};
|
|
544
|
+
if (classification === "route") {
|
|
545
|
+
if (!configuredService) {
|
|
546
|
+
const message2 = profile.diagnostics.undeclared(diagnosticsContext);
|
|
547
|
+
this.emit({
|
|
548
|
+
type: "request_blocked",
|
|
549
|
+
timestamp: now(),
|
|
550
|
+
service: profile.service,
|
|
551
|
+
code: "ARCHAL_UNDECLARED_SERVICE",
|
|
552
|
+
url: sourceUrl.toString(),
|
|
553
|
+
message: message2
|
|
554
|
+
});
|
|
555
|
+
return {
|
|
556
|
+
kind: "block",
|
|
557
|
+
service: profile.service,
|
|
558
|
+
code: "ARCHAL_UNDECLARED_SERVICE",
|
|
559
|
+
message: message2
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const targetUrl = mergeUrl(configuredService, sourceUrl);
|
|
563
|
+
this.emit({
|
|
564
|
+
type: "request_routed",
|
|
565
|
+
timestamp: now(),
|
|
566
|
+
service: profile.service,
|
|
567
|
+
sourceUrl: sourceUrl.toString(),
|
|
568
|
+
targetUrl: targetUrl.toString()
|
|
569
|
+
});
|
|
570
|
+
return {
|
|
571
|
+
kind: "route",
|
|
572
|
+
service: profile.service,
|
|
573
|
+
targetUrl
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
if (classification === "hard-fail") {
|
|
577
|
+
const code = configuredService ? "ARCHAL_DECLARED_SERVICE_ESCAPE" : "ARCHAL_UNDECLARED_SERVICE";
|
|
578
|
+
const message2 = configuredService ? profile.diagnostics.declaredEscape(diagnosticsContext) : profile.diagnostics.undeclared(diagnosticsContext);
|
|
579
|
+
this.emit({
|
|
580
|
+
type: "request_blocked",
|
|
581
|
+
timestamp: now(),
|
|
582
|
+
service: profile.service,
|
|
583
|
+
code,
|
|
584
|
+
url: sourceUrl.toString(),
|
|
585
|
+
message: message2
|
|
586
|
+
});
|
|
587
|
+
return {
|
|
588
|
+
kind: "block",
|
|
589
|
+
service: profile.service,
|
|
590
|
+
code,
|
|
591
|
+
message: message2
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const message = profile.diagnostics.warning(diagnosticsContext);
|
|
595
|
+
this.emit({
|
|
596
|
+
type: "request_warning",
|
|
597
|
+
timestamp: now(),
|
|
598
|
+
service: profile.service,
|
|
599
|
+
url: sourceUrl.toString(),
|
|
600
|
+
message
|
|
601
|
+
});
|
|
602
|
+
return {
|
|
603
|
+
kind: "warn",
|
|
604
|
+
service: profile.service,
|
|
605
|
+
message
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
return { kind: "allow" };
|
|
609
|
+
}
|
|
610
|
+
dispatchRequest(originalHttpRequest, originalHttpsRequest, defaultProtocol, args) {
|
|
611
|
+
const { requestUrl, options, callback } = parseRequestArgs(defaultProtocol, args);
|
|
612
|
+
const decision = this.evaluateRequest(requestUrl);
|
|
613
|
+
const method = typeof options.method === "string" && options.method.trim().length > 0 ? options.method.trim().toUpperCase() : "GET";
|
|
614
|
+
if (decision.kind === "block") {
|
|
615
|
+
this.traceRequest({
|
|
616
|
+
method,
|
|
617
|
+
sourceUrl: requestUrl.toString(),
|
|
618
|
+
manifestMatched: true,
|
|
619
|
+
service: decision.service,
|
|
620
|
+
target: "none",
|
|
621
|
+
outcome: "blocked",
|
|
622
|
+
reason: decision.code === "ARCHAL_UNDECLARED_SERVICE" ? "blocked_undeclared" : "blocked_escape"
|
|
623
|
+
});
|
|
624
|
+
throw new RouteRuntimePolicyError({
|
|
625
|
+
code: decision.code,
|
|
626
|
+
service: decision.service,
|
|
627
|
+
url: requestUrl.toString(),
|
|
628
|
+
message: decision.message
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
if (decision.kind !== "route") {
|
|
632
|
+
const request = Reflect.apply(
|
|
633
|
+
defaultProtocol === DEFAULT_HTTPS_PROTOCOL ? originalHttpsRequest : originalHttpRequest,
|
|
634
|
+
defaultProtocol === DEFAULT_HTTPS_PROTOCOL ? httpsModule : httpModule,
|
|
635
|
+
args
|
|
636
|
+
);
|
|
637
|
+
return this.attachTraceListeners(request, {
|
|
638
|
+
method,
|
|
639
|
+
sourceUrl: requestUrl.toString(),
|
|
640
|
+
manifestMatched: decision.kind === "warn",
|
|
641
|
+
service: decision.kind === "warn" ? decision.service : void 0,
|
|
642
|
+
target: "upstream",
|
|
643
|
+
outcome: "bypassed",
|
|
644
|
+
reason: decision.kind === "warn" ? "warning" : "no_match"
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
const service = this.servicesByName.get(decision.service);
|
|
648
|
+
if (!service) {
|
|
649
|
+
throw new Error(`Missing routed service config for ${decision.service}.`);
|
|
650
|
+
}
|
|
651
|
+
const rewrittenArgs = buildPatchedArgs(
|
|
652
|
+
decision.targetUrl,
|
|
653
|
+
options,
|
|
654
|
+
service,
|
|
655
|
+
defaultProtocol,
|
|
656
|
+
callback
|
|
657
|
+
);
|
|
658
|
+
if (decision.targetUrl.protocol === DEFAULT_HTTP_PROTOCOL) {
|
|
659
|
+
const request = bridgeSecureConnectIfNeeded(
|
|
660
|
+
Reflect.apply(originalHttpRequest, httpModule, rewrittenArgs),
|
|
661
|
+
defaultProtocol,
|
|
662
|
+
decision.targetUrl.protocol
|
|
663
|
+
);
|
|
664
|
+
return this.attachTraceListeners(request, {
|
|
665
|
+
method,
|
|
666
|
+
sourceUrl: requestUrl.toString(),
|
|
667
|
+
manifestMatched: true,
|
|
668
|
+
service: decision.service,
|
|
669
|
+
target: "twin",
|
|
670
|
+
outcome: "routed",
|
|
671
|
+
reason: "route",
|
|
672
|
+
targetUrl: decision.targetUrl.toString()
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
return this.attachTraceListeners(
|
|
676
|
+
Reflect.apply(originalHttpsRequest, httpsModule, rewrittenArgs),
|
|
677
|
+
{
|
|
678
|
+
method,
|
|
679
|
+
sourceUrl: requestUrl.toString(),
|
|
680
|
+
manifestMatched: true,
|
|
681
|
+
service: decision.service,
|
|
682
|
+
target: "twin",
|
|
683
|
+
outcome: "routed",
|
|
684
|
+
reason: "route",
|
|
685
|
+
targetUrl: decision.targetUrl.toString()
|
|
686
|
+
}
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
dispatchGet(originalHttpGet, originalHttpsGet, defaultProtocol, args) {
|
|
690
|
+
const { requestUrl, options, callback } = parseRequestArgs(defaultProtocol, args);
|
|
691
|
+
const decision = this.evaluateRequest(requestUrl);
|
|
692
|
+
const method = typeof options.method === "string" && options.method.trim().length > 0 ? options.method.trim().toUpperCase() : "GET";
|
|
693
|
+
if (decision.kind === "block") {
|
|
694
|
+
this.traceRequest({
|
|
695
|
+
method,
|
|
696
|
+
sourceUrl: requestUrl.toString(),
|
|
697
|
+
manifestMatched: true,
|
|
698
|
+
service: decision.service,
|
|
699
|
+
target: "none",
|
|
700
|
+
outcome: "blocked",
|
|
701
|
+
reason: decision.code === "ARCHAL_UNDECLARED_SERVICE" ? "blocked_undeclared" : "blocked_escape"
|
|
702
|
+
});
|
|
703
|
+
throw new RouteRuntimePolicyError({
|
|
704
|
+
code: decision.code,
|
|
705
|
+
service: decision.service,
|
|
706
|
+
url: requestUrl.toString(),
|
|
707
|
+
message: decision.message
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
if (decision.kind !== "route") {
|
|
711
|
+
const request = Reflect.apply(
|
|
712
|
+
defaultProtocol === DEFAULT_HTTPS_PROTOCOL ? originalHttpsGet : originalHttpGet,
|
|
713
|
+
defaultProtocol === DEFAULT_HTTPS_PROTOCOL ? httpsModule : httpModule,
|
|
714
|
+
args
|
|
715
|
+
);
|
|
716
|
+
return this.attachTraceListeners(request, {
|
|
717
|
+
method,
|
|
718
|
+
sourceUrl: requestUrl.toString(),
|
|
719
|
+
manifestMatched: decision.kind === "warn",
|
|
720
|
+
service: decision.kind === "warn" ? decision.service : void 0,
|
|
721
|
+
target: "upstream",
|
|
722
|
+
outcome: "bypassed",
|
|
723
|
+
reason: decision.kind === "warn" ? "warning" : "no_match"
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
const service = this.servicesByName.get(decision.service);
|
|
727
|
+
if (!service) {
|
|
728
|
+
throw new Error(`Missing routed service config for ${decision.service}.`);
|
|
729
|
+
}
|
|
730
|
+
const rewrittenArgs = buildPatchedArgs(
|
|
731
|
+
decision.targetUrl,
|
|
732
|
+
options,
|
|
733
|
+
service,
|
|
734
|
+
defaultProtocol,
|
|
735
|
+
callback
|
|
736
|
+
);
|
|
737
|
+
if (decision.targetUrl.protocol === DEFAULT_HTTP_PROTOCOL) {
|
|
738
|
+
const request = bridgeSecureConnectIfNeeded(
|
|
739
|
+
Reflect.apply(originalHttpGet, httpModule, rewrittenArgs),
|
|
740
|
+
defaultProtocol,
|
|
741
|
+
decision.targetUrl.protocol
|
|
742
|
+
);
|
|
743
|
+
return this.attachTraceListeners(request, {
|
|
744
|
+
method,
|
|
745
|
+
sourceUrl: requestUrl.toString(),
|
|
746
|
+
manifestMatched: true,
|
|
747
|
+
service: decision.service,
|
|
748
|
+
target: "twin",
|
|
749
|
+
outcome: "routed",
|
|
750
|
+
reason: "route",
|
|
751
|
+
targetUrl: decision.targetUrl.toString()
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
return this.attachTraceListeners(
|
|
755
|
+
Reflect.apply(originalHttpsGet, httpsModule, rewrittenArgs),
|
|
756
|
+
{
|
|
757
|
+
method,
|
|
758
|
+
sourceUrl: requestUrl.toString(),
|
|
759
|
+
manifestMatched: true,
|
|
760
|
+
service: decision.service,
|
|
761
|
+
target: "twin",
|
|
762
|
+
outcome: "routed",
|
|
763
|
+
reason: "route",
|
|
764
|
+
targetUrl: decision.targetUrl.toString()
|
|
765
|
+
}
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
emit(event) {
|
|
769
|
+
this.events.push(event);
|
|
770
|
+
this.onEvent?.(event);
|
|
771
|
+
}
|
|
772
|
+
attachTraceListeners(request, record) {
|
|
773
|
+
if (!this.traceEnabled) {
|
|
774
|
+
return request;
|
|
775
|
+
}
|
|
776
|
+
let settled = false;
|
|
777
|
+
request.once("response", (response) => {
|
|
778
|
+
settled = true;
|
|
779
|
+
this.traceRequest({
|
|
780
|
+
...record,
|
|
781
|
+
statusCode: response.statusCode,
|
|
782
|
+
statusText: response.statusMessage
|
|
783
|
+
});
|
|
784
|
+
});
|
|
785
|
+
request.once("error", (error) => {
|
|
786
|
+
if (settled) {
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
this.traceRequest({
|
|
790
|
+
...record,
|
|
791
|
+
outcome: "failed",
|
|
792
|
+
reason: "network_error",
|
|
793
|
+
error: error.message
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
return request;
|
|
797
|
+
}
|
|
798
|
+
traceRequest(record) {
|
|
799
|
+
if (!this.traceEnabled) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const traceRecord = {
|
|
803
|
+
timestamp: now(),
|
|
804
|
+
...record,
|
|
805
|
+
method: record.method.toUpperCase()
|
|
806
|
+
};
|
|
807
|
+
this.traceRecords.push(traceRecord);
|
|
808
|
+
this.onTrace?.(traceRecord);
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// ../route-runtime-core/src/manifests/discord.ts
|
|
813
|
+
var discordRouteManifest = {
|
|
814
|
+
service: "discord",
|
|
815
|
+
manifestVersion: "2026-04-12.discord.v1",
|
|
816
|
+
baselineSeed: "small-server",
|
|
817
|
+
upstreamBasePath: "/api/v10",
|
|
818
|
+
routeDomains: [
|
|
819
|
+
exactDomain("discord.com"),
|
|
820
|
+
exactDomain("discordapp.com")
|
|
821
|
+
],
|
|
822
|
+
// Discord's CDN and gateway surfaces are outside the approved Vitest route
|
|
823
|
+
// contract. Leaving them warning-only would create obvious exfil and
|
|
824
|
+
// network-escape channels once a test declares Discord route mode.
|
|
825
|
+
hardFailDomains: [
|
|
826
|
+
exactDomain("cdn.discordapp.com"),
|
|
827
|
+
exactDomain("media.discordapp.net"),
|
|
828
|
+
exactDomain("gateway.discord.gg")
|
|
829
|
+
],
|
|
830
|
+
warningDomains: [
|
|
831
|
+
suffixDomain("discord.com"),
|
|
832
|
+
suffixDomain("discordapp.com"),
|
|
833
|
+
exactDomain("discord.gg")
|
|
834
|
+
],
|
|
835
|
+
diagnostics: {
|
|
836
|
+
undeclared: ({ url }) => `Blocked Discord traffic to ${url}. Declare discord in archalVitestProject({ services: { discord: { mode: "route" } } }) before calling the Discord REST API in Vitest.`,
|
|
837
|
+
declaredEscape: ({ url, configuredBaseUrl }) => `Blocked Discord escape to ${url}. Discord route mode is configured, but this domain is outside the approved Discord REST route surface. Configured twin base URL: ${configuredBaseUrl ?? "unknown"}.`,
|
|
838
|
+
warning: ({ url }) => `Observed adjacent Discord domain ${url}. Archal did not reroute this request because it is outside the approved Discord REST route surface.`
|
|
839
|
+
},
|
|
840
|
+
sdkIdentifiers: ["discord.js", "@discordjs/rest"],
|
|
841
|
+
conformanceSuiteId: "discord-hosted-route"
|
|
842
|
+
};
|
|
843
|
+
|
|
844
|
+
// ../route-runtime-core/src/manifests/github.ts
|
|
845
|
+
var githubRouteManifest = {
|
|
846
|
+
service: "github",
|
|
847
|
+
manifestVersion: "2026-04-02.github.v1",
|
|
848
|
+
baselineSeed: "small-project",
|
|
849
|
+
routeDomains: [
|
|
850
|
+
exactDomain("api.github.com"),
|
|
851
|
+
exactDomain("uploads.github.com")
|
|
852
|
+
],
|
|
853
|
+
// Block adjacent GitHub-owned domains that tests don't need. Integration
|
|
854
|
+
// tests for GitHub APIs should never hit raw.githubusercontent.com or
|
|
855
|
+
// gist.github.com — and leaving them as warning-only creates an
|
|
856
|
+
// exfiltration channel: a malicious dependency can POST stolen data to any
|
|
857
|
+
// GitHub-controlled endpoint (Gist creation, raw content retrieval with
|
|
858
|
+
// crafted paths, etc.) through a domain the developer has implicitly
|
|
859
|
+
// allow-listed by declaring `github: { mode: 'route' }`.
|
|
860
|
+
hardFailDomains: [
|
|
861
|
+
exactDomain("gist.github.com"),
|
|
862
|
+
exactDomain("raw.githubusercontent.com"),
|
|
863
|
+
exactDomain("objects.githubusercontent.com"),
|
|
864
|
+
exactDomain("codeload.github.com")
|
|
865
|
+
],
|
|
866
|
+
warningDomains: [
|
|
867
|
+
suffixDomain("github.com"),
|
|
868
|
+
suffixDomain("githubusercontent.com")
|
|
869
|
+
],
|
|
870
|
+
diagnostics: {
|
|
871
|
+
undeclared: ({ url }) => `Blocked GitHub traffic to ${url}. Declare github in archalVitestProject({ services: { github: { mode: "route" } } }) before using the official GitHub SDK.`,
|
|
872
|
+
declaredEscape: ({ url, configuredBaseUrl }) => `Blocked GitHub escape to ${url}. GitHub route mode is configured, but this domain is outside the primary routed GitHub surface. Configured twin base URL: ${configuredBaseUrl ?? "unknown"}.`,
|
|
873
|
+
warning: ({ url }) => `Observed adjacent GitHub domain ${url}. Archal did not reroute this request because it is outside the primary GitHub API route surface.`
|
|
874
|
+
},
|
|
875
|
+
sdkIdentifiers: ["@octokit/rest"],
|
|
876
|
+
conformanceSuiteId: "github-hosted-route"
|
|
877
|
+
};
|
|
878
|
+
|
|
879
|
+
// ../route-runtime-core/src/manifests/google-workspace.ts
|
|
880
|
+
var googleWorkspaceRouteManifest = {
|
|
881
|
+
service: "google-workspace",
|
|
882
|
+
manifestVersion: "2026-04-01.google-workspace.v1",
|
|
883
|
+
baselineSeed: "assistant-baseline",
|
|
884
|
+
routeDomains: [
|
|
885
|
+
exactDomain("gmail.googleapis.com"),
|
|
886
|
+
exactDomain("drive.googleapis.com"),
|
|
887
|
+
exactDomain("calendar.googleapis.com"),
|
|
888
|
+
exactDomain("people.googleapis.com"),
|
|
889
|
+
exactDomain("sheets.googleapis.com"),
|
|
890
|
+
exactDomain("oauth2.googleapis.com")
|
|
891
|
+
],
|
|
892
|
+
hardFailDomains: [],
|
|
893
|
+
warningDomains: [
|
|
894
|
+
suffixDomain("google.com"),
|
|
895
|
+
suffixDomain("googleusercontent.com")
|
|
896
|
+
],
|
|
897
|
+
diagnostics: {
|
|
898
|
+
undeclared: ({ url }) => `Blocked Google Workspace traffic to ${url}. Declare google-workspace in archalVitestProject({ services: { 'google-workspace': { mode: "route" } } }) before using the official Google client.`,
|
|
899
|
+
declaredEscape: ({ url, configuredBaseUrl }) => `Blocked Google Workspace escape to ${url}. Google Workspace route mode is configured, but this domain is outside the primary routed surface. Configured twin base URL: ${configuredBaseUrl ?? "unknown"}.`,
|
|
900
|
+
warning: ({ url }) => `Observed adjacent Google Workspace domain ${url}. Archal did not reroute this request because it is outside the primary Google Workspace API route surface.`
|
|
901
|
+
},
|
|
902
|
+
sdkIdentifiers: ["googleapis"],
|
|
903
|
+
conformanceSuiteId: "google-workspace-hosted-route"
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
// ../route-runtime-core/src/manifests/jira.ts
|
|
907
|
+
var jiraRouteManifest = {
|
|
908
|
+
service: "jira",
|
|
909
|
+
manifestVersion: "2026-04-01.jira.v1",
|
|
910
|
+
baselineSeed: "small-project",
|
|
911
|
+
routeDomains: [
|
|
912
|
+
suffixDomain("atlassian.net"),
|
|
913
|
+
exactDomain("api.atlassian.com")
|
|
914
|
+
],
|
|
915
|
+
hardFailDomains: [],
|
|
916
|
+
warningDomains: [
|
|
917
|
+
suffixDomain("atlassian.com"),
|
|
918
|
+
exactDomain("jira.com")
|
|
919
|
+
],
|
|
920
|
+
diagnostics: {
|
|
921
|
+
undeclared: ({ url }) => `Blocked Jira traffic to ${url}. Declare jira in archalVitestProject({ services: { jira: { mode: "route" } } }) before using the official Jira SDK.`,
|
|
922
|
+
declaredEscape: ({ url, configuredBaseUrl }) => `Blocked Jira escape to ${url}. Jira route mode is configured, but this domain is outside the primary routed Jira surface. Configured twin base URL: ${configuredBaseUrl ?? "unknown"}.`,
|
|
923
|
+
warning: ({ url }) => `Observed adjacent Jira domain ${url}. Archal did not reroute this request because it is outside the primary Jira route surface.`
|
|
924
|
+
},
|
|
925
|
+
sdkIdentifiers: ["jira.js"],
|
|
926
|
+
conformanceSuiteId: "jira-hosted-route"
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// ../route-runtime-core/src/manifests/linear.ts
|
|
930
|
+
var linearRouteManifest = {
|
|
931
|
+
service: "linear",
|
|
932
|
+
manifestVersion: "2026-04-19.linear.v1",
|
|
933
|
+
baselineSeed: "small-team",
|
|
934
|
+
routeDomains: [
|
|
935
|
+
exactDomain("api.linear.app")
|
|
936
|
+
],
|
|
937
|
+
hardFailDomains: [],
|
|
938
|
+
warningDomains: [
|
|
939
|
+
suffixDomain("linear.app")
|
|
940
|
+
],
|
|
941
|
+
diagnostics: {
|
|
942
|
+
undeclared: ({ url }) => `Blocked Linear traffic to ${url}. Declare linear in archalVitestProject({ services: { linear: { mode: "route" } } }) before using the official Linear SDK.`,
|
|
943
|
+
declaredEscape: ({ url, configuredBaseUrl }) => `Blocked Linear escape to ${url}. Linear route mode is configured, but this domain is outside the primary routed Linear API surface. Configured twin base URL: ${configuredBaseUrl ?? "unknown"}.`,
|
|
944
|
+
warning: ({ url }) => `Observed adjacent Linear domain ${url}. Archal did not reroute this request because it is outside the primary Linear API route surface.`
|
|
945
|
+
},
|
|
946
|
+
sdkIdentifiers: ["@linear/sdk"],
|
|
947
|
+
conformanceSuiteId: "linear-hosted-route"
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// ../route-runtime-core/src/manifests/ramp.ts
|
|
951
|
+
var rampRouteManifest = {
|
|
952
|
+
service: "ramp",
|
|
953
|
+
manifestVersion: "2026-04-19.ramp.v1",
|
|
954
|
+
baselineSeed: "default",
|
|
955
|
+
routeDomains: [
|
|
956
|
+
exactDomain("api.ramp.com")
|
|
957
|
+
],
|
|
958
|
+
hardFailDomains: [],
|
|
959
|
+
warningDomains: [
|
|
960
|
+
exactDomain("app.ramp.com"),
|
|
961
|
+
suffixDomain("ramp.com")
|
|
962
|
+
],
|
|
963
|
+
diagnostics: {
|
|
964
|
+
undeclared: ({ url }) => `Blocked Ramp traffic to ${url}. Declare ramp in archalVitestProject({ services: { ramp: { mode: "route" } } }) before using the official Ramp client.`,
|
|
965
|
+
declaredEscape: ({ url, configuredBaseUrl }) => `Blocked Ramp escape to ${url}. Ramp route mode is configured, but this domain is outside the primary routed Ramp API surface. Configured twin base URL: ${configuredBaseUrl ?? "unknown"}.`,
|
|
966
|
+
warning: ({ url }) => `Observed adjacent Ramp domain ${url}. Archal did not reroute this request because it is outside the primary Ramp API route surface.`
|
|
967
|
+
},
|
|
968
|
+
sdkIdentifiers: ["ramp"],
|
|
969
|
+
conformanceSuiteId: "ramp-hosted-route"
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
// ../route-runtime-core/src/manifests/slack.ts
|
|
973
|
+
var slackRouteManifest = {
|
|
974
|
+
service: "slack",
|
|
975
|
+
manifestVersion: "2026-04-02.slack.v1",
|
|
976
|
+
baselineSeed: "engineering-team",
|
|
977
|
+
routeDomains: [
|
|
978
|
+
exactDomain("slack.com")
|
|
979
|
+
],
|
|
980
|
+
// Block adjacent Slack-owned domains that tests don't need and that can be
|
|
981
|
+
// abused as data-exfiltration channels. hooks.slack.com and files.slack.com
|
|
982
|
+
// in particular are ideal POST targets for a malicious dependency trying to
|
|
983
|
+
// smuggle stolen tokens out through a domain the developer has already
|
|
984
|
+
// implicitly allow-listed.
|
|
985
|
+
hardFailDomains: [
|
|
986
|
+
exactDomain("hooks.slack.com"),
|
|
987
|
+
exactDomain("files.slack.com"),
|
|
988
|
+
exactDomain("edgeapi.slack.com"),
|
|
989
|
+
exactDomain("wss-primary.slack.com"),
|
|
990
|
+
exactDomain("wss-backup.slack.com")
|
|
991
|
+
],
|
|
992
|
+
warningDomains: [
|
|
993
|
+
suffixDomain("slack.com")
|
|
994
|
+
],
|
|
995
|
+
diagnostics: {
|
|
996
|
+
undeclared: ({ url }) => `Blocked Slack traffic to ${url}. Declare slack in archalVitestProject({ services: { slack: { mode: "route" } } }) before using the official Slack SDK.`,
|
|
997
|
+
declaredEscape: ({ url, configuredBaseUrl }) => `Blocked Slack escape to ${url}. Slack route mode is configured, but this domain is outside the primary routed Slack surface. Configured twin base URL: ${configuredBaseUrl ?? "unknown"}.`,
|
|
998
|
+
warning: ({ url }) => `Observed adjacent Slack domain ${url}. Archal did not reroute this request because it is outside the primary Slack API route surface.`
|
|
999
|
+
},
|
|
1000
|
+
sdkIdentifiers: ["@slack/web-api"],
|
|
1001
|
+
conformanceSuiteId: "slack-hosted-route"
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
// ../route-runtime-core/src/manifests/stripe.ts
|
|
1005
|
+
var stripeRouteManifest = {
|
|
1006
|
+
service: "stripe",
|
|
1007
|
+
manifestVersion: "2026-04-01.stripe.v1",
|
|
1008
|
+
baselineSeed: "small-business",
|
|
1009
|
+
routeDomains: [
|
|
1010
|
+
exactDomain("api.stripe.com")
|
|
1011
|
+
],
|
|
1012
|
+
hardFailDomains: [
|
|
1013
|
+
exactDomain("files.stripe.com"),
|
|
1014
|
+
exactDomain("uploads.stripe.com")
|
|
1015
|
+
],
|
|
1016
|
+
warningDomains: [
|
|
1017
|
+
exactDomain("billing.stripe.com"),
|
|
1018
|
+
exactDomain("buy.stripe.com"),
|
|
1019
|
+
exactDomain("checkout.stripe.com"),
|
|
1020
|
+
exactDomain("connect.stripe.com"),
|
|
1021
|
+
exactDomain("dashboard.stripe.com"),
|
|
1022
|
+
exactDomain("invoice.stripe.com"),
|
|
1023
|
+
exactDomain("pay.stripe.com"),
|
|
1024
|
+
exactDomain("verify.stripe.com"),
|
|
1025
|
+
suffixDomain("stripe.com")
|
|
1026
|
+
],
|
|
1027
|
+
diagnostics: {
|
|
1028
|
+
undeclared: ({ url }) => `Blocked Stripe traffic to ${url}. Declare stripe in archalVitestProject({ services: { stripe: { mode: "route" } } }) before using the official Stripe SDK.`,
|
|
1029
|
+
declaredEscape: ({ url, configuredBaseUrl }) => `Blocked Stripe escape to ${url}. Stripe route mode is configured, but this Stripe-owned domain is outside the current routed surface. Configured twin base URL: ${configuredBaseUrl ?? "unknown"}.`,
|
|
1030
|
+
warning: ({ url }) => `Observed adjacent Stripe domain ${url}. Archal did not reroute this request because it is outside the primary Stripe API route surface.`
|
|
1031
|
+
},
|
|
1032
|
+
sdkIdentifiers: ["stripe"],
|
|
1033
|
+
conformanceSuiteId: "stripe-hosted-route"
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
// ../route-runtime-core/src/manifests/supabase.ts
|
|
1037
|
+
var supabaseRouteManifest = {
|
|
1038
|
+
service: "supabase",
|
|
1039
|
+
manifestVersion: "2026-04-01.supabase.v1",
|
|
1040
|
+
baselineSeed: "small-project",
|
|
1041
|
+
routeDomains: [
|
|
1042
|
+
suffixDomain("supabase.co")
|
|
1043
|
+
],
|
|
1044
|
+
hardFailDomains: [],
|
|
1045
|
+
warningDomains: [
|
|
1046
|
+
suffixDomain("supabase.com")
|
|
1047
|
+
],
|
|
1048
|
+
diagnostics: {
|
|
1049
|
+
undeclared: ({ url }) => `Blocked Supabase traffic to ${url}. Declare supabase in archalVitestProject({ services: { supabase: { mode: "route" } } }) before using the official Supabase SDK.`,
|
|
1050
|
+
declaredEscape: ({ url, configuredBaseUrl }) => `Blocked Supabase escape to ${url}. Supabase route mode is configured, but this domain is outside the primary routed Supabase surface. Configured twin base URL: ${configuredBaseUrl ?? "unknown"}.`,
|
|
1051
|
+
warning: ({ url }) => `Observed adjacent Supabase domain ${url}. Archal did not reroute this request because it is outside the primary Supabase route surface.`
|
|
1052
|
+
},
|
|
1053
|
+
sdkIdentifiers: ["@supabase/supabase-js"],
|
|
1054
|
+
conformanceSuiteId: "supabase-hosted-route"
|
|
1055
|
+
};
|
|
1056
|
+
|
|
1057
|
+
// ../route-runtime-core/src/manifests.ts
|
|
1058
|
+
var SHARED_ROUTE_MANIFESTS = [
|
|
1059
|
+
discordRouteManifest,
|
|
1060
|
+
githubRouteManifest,
|
|
1061
|
+
googleWorkspaceRouteManifest,
|
|
1062
|
+
jiraRouteManifest,
|
|
1063
|
+
linearRouteManifest,
|
|
1064
|
+
rampRouteManifest,
|
|
1065
|
+
slackRouteManifest,
|
|
1066
|
+
stripeRouteManifest,
|
|
1067
|
+
supabaseRouteManifest
|
|
1068
|
+
];
|
|
1069
|
+
var SHARED_ROUTE_MANIFESTS_BY_SERVICE = new Map(
|
|
1070
|
+
SHARED_ROUTE_MANIFESTS.map((manifest) => [manifest.service, manifest])
|
|
1071
|
+
);
|
|
1072
|
+
function listSharedRouteManifests() {
|
|
1073
|
+
return [...SHARED_ROUTE_MANIFESTS];
|
|
1074
|
+
}
|
|
1075
|
+
function getSharedRouteManifest(service) {
|
|
1076
|
+
return SHARED_ROUTE_MANIFESTS_BY_SERVICE.get(service);
|
|
1077
|
+
}
|
|
1078
|
+
function buildServiceCompatibilityProfile(manifest) {
|
|
1079
|
+
return {
|
|
1080
|
+
service: manifest.service,
|
|
1081
|
+
routeDomains: manifest.routeDomains,
|
|
1082
|
+
hardFailDomains: manifest.hardFailDomains,
|
|
1083
|
+
warningDomains: manifest.warningDomains,
|
|
1084
|
+
diagnostics: manifest.diagnostics
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// ../route-runtime-core/src/service-profiles/stripe.ts
|
|
1089
|
+
var stripeCompatibilityProfile = buildServiceCompatibilityProfile(stripeRouteManifest);
|
|
1090
|
+
|
|
1091
|
+
// ../route-runtime-core/src/hosted-capabilities.ts
|
|
1092
|
+
import { createHash } from "crypto";
|
|
1093
|
+
|
|
1094
|
+
// src/runtime/service-profiles.ts
|
|
1095
|
+
function unsupportedServiceMessage(unsupportedServices) {
|
|
1096
|
+
const supportedServices = listSharedRouteManifests().map((manifest) => manifest.service).sort();
|
|
1097
|
+
return [
|
|
1098
|
+
`Unsupported route-mode service${unsupportedServices.length === 1 ? "" : "s"}: ${unsupportedServices.sort().join(", ")}.`,
|
|
1099
|
+
`Supported services in this Vitest adapter: ${supportedServices.join(", ")}.`
|
|
1100
|
+
].join(" ");
|
|
1101
|
+
}
|
|
1102
|
+
function assertSupportedArchalVitestServices(services) {
|
|
1103
|
+
const unsupportedServices = Object.entries(services).filter(([, serviceConfig]) => serviceConfig.mode === "route").map(([serviceName]) => serviceName).filter((serviceName) => !getSharedRouteManifest(serviceName));
|
|
1104
|
+
if (unsupportedServices.length === 0) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
throw new Error(unsupportedServiceMessage(unsupportedServices));
|
|
1108
|
+
}
|
|
1109
|
+
function resolveArchalVitestServiceProfiles(services) {
|
|
1110
|
+
const declaredRouteServices = new Set(
|
|
1111
|
+
Object.entries(services).filter(([, serviceConfig]) => serviceConfig.mode === "route").map(([serviceName]) => serviceName)
|
|
1112
|
+
);
|
|
1113
|
+
const profiles = listSharedRouteManifests().map((manifest) => ({
|
|
1114
|
+
serviceName: manifest.service,
|
|
1115
|
+
profile: buildServiceCompatibilityProfile(manifest)
|
|
1116
|
+
}));
|
|
1117
|
+
const declaredProfiles = profiles.filter(({ serviceName }) => declaredRouteServices.has(serviceName)).map(({ profile }) => profile);
|
|
1118
|
+
const undeclaredEnforcementProfiles = profiles.filter(({ serviceName }) => !declaredRouteServices.has(serviceName)).map(({ profile }) => profile);
|
|
1119
|
+
return [...declaredProfiles, ...undeclaredEnforcementProfiles];
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/runtime/hosted-session-provider.ts
|
|
1123
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
1124
|
+
import { spawn } from "child_process";
|
|
1125
|
+
import { promises as fs } from "fs";
|
|
1126
|
+
import { join, resolve as resolve2 } from "path";
|
|
1127
|
+
import { tmpdir } from "os";
|
|
1128
|
+
|
|
1129
|
+
// src/url-resolution.ts
|
|
1130
|
+
function readFirstConfiguredApiBaseUrl(envVars) {
|
|
1131
|
+
for (const envVar of envVars) {
|
|
1132
|
+
const configured = trimEnv(envVar);
|
|
1133
|
+
if (!configured) {
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
return normalizeApiBaseUrl(configured);
|
|
1137
|
+
}
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
function resolveArchalVitestApiBaseUrl(defaultBaseUrl) {
|
|
1141
|
+
return readFirstConfiguredApiBaseUrl([
|
|
1142
|
+
"ARCHAL_VITEST_API_URL",
|
|
1143
|
+
"ARCHAL_API_URL",
|
|
1144
|
+
"ARCHAL_API_BASE_URL",
|
|
1145
|
+
"ARCHAL_AUTH_URL",
|
|
1146
|
+
"ARCHAL_AUTH_BASE_URL"
|
|
1147
|
+
]) ?? defaultBaseUrl;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/runtime/hosted-session-provider.ts
|
|
1151
|
+
var COORDINATOR_LOCK_TIMEOUT_MS = 3e4;
|
|
1152
|
+
var COORDINATOR_STALE_LOCK_MS = COORDINATOR_LOCK_TIMEOUT_MS;
|
|
1153
|
+
var COORDINATOR_POLL_INTERVAL_MS = 50;
|
|
1154
|
+
var FORWARDED_AUTHORIZATION_HEADER = "x-archal-upstream-authorization";
|
|
1155
|
+
var WORKER_ID_HEADER = "x-archal-worker-id";
|
|
1156
|
+
function currentVitestWorkerId() {
|
|
1157
|
+
const raw = process.env["VITEST_WORKER_ID"];
|
|
1158
|
+
if (!raw || typeof raw !== "string") return void 0;
|
|
1159
|
+
const trimmed = raw.trim();
|
|
1160
|
+
if (trimmed.length === 0) return void 0;
|
|
1161
|
+
return `vitest-${trimmed}`;
|
|
1162
|
+
}
|
|
1163
|
+
function resolveWorkerId() {
|
|
1164
|
+
const vitestWorkerId = trimEnv("VITEST_WORKER_ID");
|
|
1165
|
+
return vitestWorkerId ? `vitest-worker-${vitestWorkerId}` : `pid-${process.pid}`;
|
|
1166
|
+
}
|
|
1167
|
+
function resolveRunnerPid(config) {
|
|
1168
|
+
return typeof config.runnerPid === "number" && Number.isInteger(config.runnerPid) && config.runnerPid > 0 ? config.runnerPid : process.pid;
|
|
1169
|
+
}
|
|
1170
|
+
function toSafeCoordinatorDirectoryName(sessionKey) {
|
|
1171
|
+
if (sessionKey !== "." && sessionKey !== ".." && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(sessionKey)) {
|
|
1172
|
+
return sessionKey;
|
|
1173
|
+
}
|
|
1174
|
+
const slug = sessionKey.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^[.-]+|[.-]+$/g, "").slice(0, 48);
|
|
1175
|
+
const hash = createHash2("sha256").update(sessionKey).digest("hex").slice(0, 12);
|
|
1176
|
+
return `${slug || "session"}-${hash}`;
|
|
1177
|
+
}
|
|
1178
|
+
function resolveCoordinatorPaths(config) {
|
|
1179
|
+
const baseDirectory = resolve2(
|
|
1180
|
+
trimEnv("ARCHAL_VITEST_COORDINATOR_DIR") ?? join(tmpdir(), "archal-vitest")
|
|
1181
|
+
);
|
|
1182
|
+
const sessionKey = config.sessionKey ?? `${config.projectName}-${randomUUID()}`;
|
|
1183
|
+
const directory = resolve2(baseDirectory, toSafeCoordinatorDirectoryName(sessionKey));
|
|
1184
|
+
return {
|
|
1185
|
+
directory,
|
|
1186
|
+
lockDirectory: join(directory, "lock"),
|
|
1187
|
+
statePath: join(directory, "state.json"),
|
|
1188
|
+
reaperPidPath: join(directory, "reaper.pid")
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
function shouldStopSessionOnRelease() {
|
|
1192
|
+
const explicitSetting = trimEnv("ARCHAL_VITEST_STOP_SESSION_ON_RELEASE");
|
|
1193
|
+
if (explicitSetting) {
|
|
1194
|
+
return !/^(0|false)$/i.test(explicitSetting);
|
|
1195
|
+
}
|
|
1196
|
+
return trimEnv("CI") !== void 0 || trimEnv("GITHUB_ACTIONS") !== void 0;
|
|
1197
|
+
}
|
|
1198
|
+
async function resolveHostedSessionEnvironment() {
|
|
1199
|
+
const apiBaseUrl = resolveArchalVitestApiBaseUrl(DEFAULT_HOSTED_API_BASE_URL);
|
|
1200
|
+
const auth = await createHostedAuthLease(VITEST_AUTH_LEASE_OPTIONS);
|
|
1201
|
+
const ttlFromMinutes = parsePositiveInteger(trimEnv("ARCHAL_SESSION_TTL_MINUTES"), 30, 1) * 60;
|
|
1202
|
+
const ttlSeconds = parsePositiveInteger(
|
|
1203
|
+
trimEnv("ARCHAL_VITEST_SESSION_TTL_SECONDS"),
|
|
1204
|
+
ttlFromMinutes,
|
|
1205
|
+
1
|
|
1206
|
+
);
|
|
1207
|
+
const readyTimeoutMs = Math.max(
|
|
1208
|
+
MIN_READY_TIMEOUT_MS,
|
|
1209
|
+
parsePositiveInteger(
|
|
1210
|
+
trimEnv("ARCHAL_VITEST_SESSION_READY_TIMEOUT_MS") ?? trimEnv("ARCHAL_SESSION_READY_TIMEOUT_MS"),
|
|
1211
|
+
DEFAULT_READY_TIMEOUT_MS,
|
|
1212
|
+
1
|
|
1213
|
+
)
|
|
1214
|
+
);
|
|
1215
|
+
const renewIntervalMs = Math.max(
|
|
1216
|
+
MIN_RENEW_INTERVAL_MS,
|
|
1217
|
+
parsePositiveInteger(
|
|
1218
|
+
trimEnv("ARCHAL_VITEST_SESSION_RENEW_INTERVAL_MS"),
|
|
1219
|
+
Math.min(Math.floor((ttlSeconds || DEFAULT_SESSION_TTL_SECONDS) * 500), DEFAULT_RENEW_INTERVAL_MS),
|
|
1220
|
+
1
|
|
1221
|
+
)
|
|
1222
|
+
);
|
|
1223
|
+
return {
|
|
1224
|
+
auth,
|
|
1225
|
+
apiBaseUrl,
|
|
1226
|
+
ttlSeconds: ttlSeconds || DEFAULT_SESSION_TTL_SECONDS,
|
|
1227
|
+
readyTimeoutMs,
|
|
1228
|
+
renewIntervalMs
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
async function withCoordinatorLock(paths, lockTimeoutMs, action) {
|
|
1232
|
+
await fs.mkdir(paths.directory, { recursive: true });
|
|
1233
|
+
const deadline = Date.now() + resolveCoordinatorLockWaitTimeoutMs(lockTimeoutMs);
|
|
1234
|
+
while (true) {
|
|
1235
|
+
try {
|
|
1236
|
+
await fs.mkdir(paths.lockDirectory);
|
|
1237
|
+
break;
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "";
|
|
1240
|
+
if (code !== "EEXIST") {
|
|
1241
|
+
throw error;
|
|
1242
|
+
}
|
|
1243
|
+
const stale = await fs.stat(paths.lockDirectory).then((stats) => Date.now() - stats.mtimeMs >= COORDINATOR_STALE_LOCK_MS).catch(() => false);
|
|
1244
|
+
if (stale) {
|
|
1245
|
+
await fs.rm(paths.lockDirectory, { recursive: true, force: true }).catch(() => {
|
|
1246
|
+
});
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (Date.now() >= deadline) {
|
|
1250
|
+
throw new Error(
|
|
1251
|
+
`Timed out waiting for Archal Vitest session coordinator lock in ${paths.directory}.`
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
await sleep(COORDINATOR_POLL_INTERVAL_MS);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
try {
|
|
1258
|
+
const keepalive = setInterval(() => {
|
|
1259
|
+
void fs.utimes(paths.lockDirectory, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
|
|
1260
|
+
}, 1e3);
|
|
1261
|
+
keepalive.unref();
|
|
1262
|
+
try {
|
|
1263
|
+
return await action();
|
|
1264
|
+
} finally {
|
|
1265
|
+
clearInterval(keepalive);
|
|
1266
|
+
}
|
|
1267
|
+
} finally {
|
|
1268
|
+
await fs.rm(paths.lockDirectory, { recursive: true, force: true });
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function resolveCoordinatorLockWaitTimeoutMs(readyTimeoutMs) {
|
|
1272
|
+
return Math.max(COORDINATOR_LOCK_TIMEOUT_MS, readyTimeoutMs);
|
|
1273
|
+
}
|
|
1274
|
+
async function readCoordinatorState(paths) {
|
|
1275
|
+
if (!await fileExists(paths.statePath)) {
|
|
1276
|
+
return { kind: "missing" };
|
|
1277
|
+
}
|
|
1278
|
+
const raw = await fs.readFile(paths.statePath, "utf8");
|
|
1279
|
+
try {
|
|
1280
|
+
const normalizedState = normalizeCoordinatorState(JSON.parse(raw));
|
|
1281
|
+
if (!normalizedState) {
|
|
1282
|
+
await fs.rm(paths.statePath, { force: true }).catch(() => void 0);
|
|
1283
|
+
return { kind: "corrupt" };
|
|
1284
|
+
}
|
|
1285
|
+
return {
|
|
1286
|
+
kind: "ready",
|
|
1287
|
+
state: normalizedState
|
|
1288
|
+
};
|
|
1289
|
+
} catch {
|
|
1290
|
+
await fs.rm(paths.statePath, { force: true }).catch(() => void 0);
|
|
1291
|
+
return { kind: "corrupt" };
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function normalizeCoordinatorState(input) {
|
|
1295
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
1296
|
+
return null;
|
|
1297
|
+
}
|
|
1298
|
+
const record = input;
|
|
1299
|
+
const session = normalizeHostedSessionSnapshot(record["session"]);
|
|
1300
|
+
if (!session) {
|
|
1301
|
+
return null;
|
|
1302
|
+
}
|
|
1303
|
+
const leases = normalizeWorkerLeases(record["leases"]);
|
|
1304
|
+
if (!leases) {
|
|
1305
|
+
return null;
|
|
1306
|
+
}
|
|
1307
|
+
return { session, leases };
|
|
1308
|
+
}
|
|
1309
|
+
function normalizeHostedSessionSnapshot(input) {
|
|
1310
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
const record = input;
|
|
1314
|
+
const sessionId = typeof record["sessionId"] === "string" ? record["sessionId"].trim() : "";
|
|
1315
|
+
const apiBaseUrls = normalizeStringRecord(record["apiBaseUrls"]);
|
|
1316
|
+
const resolvedRuntime = normalizeResolvedRuntime(record["resolvedRuntime"]);
|
|
1317
|
+
if (!sessionId || !apiBaseUrls || !resolvedRuntime) {
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
return {
|
|
1321
|
+
sessionId,
|
|
1322
|
+
apiBaseUrls,
|
|
1323
|
+
resolvedRuntime
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
function normalizeResolvedRuntime(input) {
|
|
1327
|
+
if (input === void 0 || input === null) {
|
|
1328
|
+
return null;
|
|
1329
|
+
}
|
|
1330
|
+
if (typeof input !== "object" || Array.isArray(input)) {
|
|
1331
|
+
return null;
|
|
1332
|
+
}
|
|
1333
|
+
const record = input;
|
|
1334
|
+
const resolvedServices = normalizeStringArray(record["resolvedServices"] ?? []);
|
|
1335
|
+
const resolvedSeeds = normalizeStringRecord(record["resolvedSeeds"] ?? {});
|
|
1336
|
+
const manifestVersions = normalizeStringRecord(record["manifestVersions"] ?? {});
|
|
1337
|
+
if (!resolvedServices || !resolvedSeeds || !manifestVersions) {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
const capabilityVersion = typeof record["capabilityVersion"] === "string" ? record["capabilityVersion"] : void 0;
|
|
1341
|
+
const runtimeVersion = typeof record["runtimeVersion"] === "string" ? record["runtimeVersion"] : void 0;
|
|
1342
|
+
if (resolvedServices.length === 0 && Object.keys(resolvedSeeds).length === 0 && Object.keys(manifestVersions).length === 0 && !capabilityVersion && !runtimeVersion) {
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
return {
|
|
1346
|
+
resolvedServices,
|
|
1347
|
+
resolvedSeeds,
|
|
1348
|
+
manifestVersions,
|
|
1349
|
+
...capabilityVersion ? { capabilityVersion } : {},
|
|
1350
|
+
...runtimeVersion ? { runtimeVersion } : {}
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
function normalizeWorkerLeases(input) {
|
|
1354
|
+
if (input === void 0) {
|
|
1355
|
+
return [];
|
|
1356
|
+
}
|
|
1357
|
+
if (!Array.isArray(input)) {
|
|
1358
|
+
return null;
|
|
1359
|
+
}
|
|
1360
|
+
return input.flatMap((lease) => {
|
|
1361
|
+
if (!lease || typeof lease !== "object" || Array.isArray(lease)) {
|
|
1362
|
+
return [];
|
|
1363
|
+
}
|
|
1364
|
+
const record = lease;
|
|
1365
|
+
const workerId = typeof record["workerId"] === "string" ? record["workerId"].trim() : "";
|
|
1366
|
+
const updatedAt = typeof record["updatedAt"] === "string" ? record["updatedAt"].trim() : "";
|
|
1367
|
+
const pid = typeof record["pid"] === "number" ? record["pid"] : Number.NaN;
|
|
1368
|
+
if (!workerId || !updatedAt || !Number.isInteger(pid) || pid <= 0) {
|
|
1369
|
+
return [];
|
|
1370
|
+
}
|
|
1371
|
+
return [{
|
|
1372
|
+
workerId,
|
|
1373
|
+
pid,
|
|
1374
|
+
updatedAt
|
|
1375
|
+
}];
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
function normalizeStringArray(input) {
|
|
1379
|
+
if (!Array.isArray(input)) {
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
const values = input.flatMap((value) => {
|
|
1383
|
+
if (typeof value !== "string") {
|
|
1384
|
+
return [];
|
|
1385
|
+
}
|
|
1386
|
+
const trimmed = value.trim();
|
|
1387
|
+
return trimmed ? [trimmed] : [];
|
|
1388
|
+
});
|
|
1389
|
+
return values.length === input.length ? values : null;
|
|
1390
|
+
}
|
|
1391
|
+
function normalizeStringRecord(input) {
|
|
1392
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
const record = input;
|
|
1396
|
+
const entries = Object.entries(record).flatMap(([key, value]) => {
|
|
1397
|
+
if (typeof value !== "string") {
|
|
1398
|
+
return [];
|
|
1399
|
+
}
|
|
1400
|
+
return [[key, value]];
|
|
1401
|
+
});
|
|
1402
|
+
return entries.length === Object.keys(record).length ? Object.fromEntries(entries) : null;
|
|
1403
|
+
}
|
|
1404
|
+
async function writeCoordinatorState(paths, state) {
|
|
1405
|
+
await fs.writeFile(paths.statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
1406
|
+
}
|
|
1407
|
+
async function clearCoordinatorState(paths) {
|
|
1408
|
+
await Promise.all([
|
|
1409
|
+
fs.rm(paths.statePath, { force: true }),
|
|
1410
|
+
fs.rm(paths.reaperPidPath, { force: true })
|
|
1411
|
+
]);
|
|
1412
|
+
}
|
|
1413
|
+
async function readRunnerReaperState(paths) {
|
|
1414
|
+
const raw = await fs.readFile(paths.reaperPidPath, "utf8").catch(() => null);
|
|
1415
|
+
if (!raw) {
|
|
1416
|
+
return null;
|
|
1417
|
+
}
|
|
1418
|
+
try {
|
|
1419
|
+
const parsed = JSON.parse(raw);
|
|
1420
|
+
if (typeof parsed === "number") {
|
|
1421
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
return {
|
|
1425
|
+
pid: parsed,
|
|
1426
|
+
sessionId: ""
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
if (!parsed || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.sessionId !== "string" || parsed.sessionId.trim().length === 0) {
|
|
1430
|
+
return null;
|
|
1431
|
+
}
|
|
1432
|
+
return {
|
|
1433
|
+
pid: parsed.pid,
|
|
1434
|
+
sessionId: parsed.sessionId
|
|
1435
|
+
};
|
|
1436
|
+
} catch {
|
|
1437
|
+
const pid = Number.parseInt(raw.trim(), 10);
|
|
1438
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
1439
|
+
return null;
|
|
1440
|
+
}
|
|
1441
|
+
return {
|
|
1442
|
+
pid,
|
|
1443
|
+
sessionId: ""
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
async function writeRunnerReaperState(paths, state) {
|
|
1448
|
+
await fs.writeFile(paths.reaperPidPath, JSON.stringify(state) + "\n", "utf8");
|
|
1449
|
+
}
|
|
1450
|
+
async function stopRunnerReaper(paths) {
|
|
1451
|
+
const reaperState = await readRunnerReaperState(paths);
|
|
1452
|
+
if (!reaperState) {
|
|
1453
|
+
await fs.rm(paths.reaperPidPath, { force: true }).catch(() => void 0);
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
if (isProcessAlive(reaperState.pid)) {
|
|
1457
|
+
try {
|
|
1458
|
+
process.kill(reaperState.pid, "SIGTERM");
|
|
1459
|
+
} catch {
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
await fs.rm(paths.reaperPidPath, { force: true }).catch(() => void 0);
|
|
1463
|
+
}
|
|
1464
|
+
function pruneLeases(leases) {
|
|
1465
|
+
return leases.filter((lease) => isProcessAlive(lease.pid));
|
|
1466
|
+
}
|
|
1467
|
+
function runnerIsAlive(runnerPid) {
|
|
1468
|
+
return isProcessAlive(runnerPid);
|
|
1469
|
+
}
|
|
1470
|
+
function resolveRequestedSeeds(config, serviceNames) {
|
|
1471
|
+
const fileSeedServices = new Set(Object.keys(config.fileSeedContents ?? {}));
|
|
1472
|
+
const requestedSeeds = Object.fromEntries(
|
|
1473
|
+
serviceNames.flatMap((serviceName) => {
|
|
1474
|
+
if (fileSeedServices.has(serviceName)) return [];
|
|
1475
|
+
const seed = config.services[serviceName]?.seed?.trim();
|
|
1476
|
+
return seed ? [[serviceName, seed]] : [];
|
|
1477
|
+
})
|
|
1478
|
+
);
|
|
1479
|
+
return Object.keys(requestedSeeds).length > 0 ? requestedSeeds : void 0;
|
|
1480
|
+
}
|
|
1481
|
+
function retainedSessionWithoutLeases(state) {
|
|
1482
|
+
if (!state?.session) {
|
|
1483
|
+
return null;
|
|
1484
|
+
}
|
|
1485
|
+
return pruneLeases(state.leases).length === 0 ? state.session : null;
|
|
1486
|
+
}
|
|
1487
|
+
function activeWorkerSession(state) {
|
|
1488
|
+
if (!state?.session) {
|
|
1489
|
+
return null;
|
|
1490
|
+
}
|
|
1491
|
+
const leases = pruneLeases(state.leases);
|
|
1492
|
+
if (leases.length === 0) {
|
|
1493
|
+
return null;
|
|
1494
|
+
}
|
|
1495
|
+
return {
|
|
1496
|
+
session: state.session,
|
|
1497
|
+
leases
|
|
1498
|
+
};
|
|
1499
|
+
}
|
|
1500
|
+
var CoordinatedHostedSessionManager = class {
|
|
1501
|
+
constructor(paths, client, environment, config) {
|
|
1502
|
+
this.paths = paths;
|
|
1503
|
+
this.client = client;
|
|
1504
|
+
this.environment = environment;
|
|
1505
|
+
this.config = config;
|
|
1506
|
+
}
|
|
1507
|
+
async acquire(serviceNames, requestedSeeds) {
|
|
1508
|
+
const workerId = resolveWorkerId();
|
|
1509
|
+
const runnerPid = resolveRunnerPid(this.config);
|
|
1510
|
+
return await withCoordinatorLock(this.paths, this.environment.readyTimeoutMs, async () => {
|
|
1511
|
+
const stateResult = await readCoordinatorState(this.paths);
|
|
1512
|
+
const existingState = stateResult.kind === "ready" ? stateResult.state : null;
|
|
1513
|
+
if (stateResult.kind === "corrupt") {
|
|
1514
|
+
const recoveredSession = await this.recoverCorruptCoordinatorState(
|
|
1515
|
+
serviceNames,
|
|
1516
|
+
requestedSeeds,
|
|
1517
|
+
workerId,
|
|
1518
|
+
runnerPid
|
|
1519
|
+
);
|
|
1520
|
+
if (recoveredSession) {
|
|
1521
|
+
return { session: recoveredSession, acquisition: "reused" };
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
const activeSession = activeWorkerSession(existingState);
|
|
1525
|
+
if (activeSession && requestedSeedsMatchSession(requestedSeeds, activeSession.session)) {
|
|
1526
|
+
const reusableSession = await this.tryReuseSession(
|
|
1527
|
+
activeSession.session,
|
|
1528
|
+
serviceNames
|
|
1529
|
+
);
|
|
1530
|
+
if (!reusableSession) {
|
|
1531
|
+
await this.client.stop(activeSession.session.sessionId).catch(() => void 0);
|
|
1532
|
+
await stopRunnerReaper(this.paths);
|
|
1533
|
+
await clearCoordinatorState(this.paths);
|
|
1534
|
+
} else {
|
|
1535
|
+
await this.ensureRunnerReaper(reusableSession.sessionId, runnerPid);
|
|
1536
|
+
await writeCoordinatorState(this.paths, {
|
|
1537
|
+
session: reusableSession,
|
|
1538
|
+
leases: this.upsertLease(activeSession.leases, workerId)
|
|
1539
|
+
});
|
|
1540
|
+
return { session: reusableSession, acquisition: "reused" };
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
if (activeSession && !requestedSeedsMatchSession(requestedSeeds, activeSession.session)) {
|
|
1544
|
+
await this.client.stop(activeSession.session.sessionId).catch(() => void 0);
|
|
1545
|
+
await stopRunnerReaper(this.paths);
|
|
1546
|
+
await clearCoordinatorState(this.paths);
|
|
1547
|
+
}
|
|
1548
|
+
const retainedSession = retainedSessionWithoutLeases(existingState);
|
|
1549
|
+
if (retainedSession) {
|
|
1550
|
+
if (!runnerIsAlive(runnerPid)) {
|
|
1551
|
+
await this.client.stop(retainedSession.sessionId).catch(() => void 0);
|
|
1552
|
+
await stopRunnerReaper(this.paths);
|
|
1553
|
+
await clearCoordinatorState(this.paths);
|
|
1554
|
+
} else if (!requestedSeedsMatchSession(requestedSeeds, retainedSession)) {
|
|
1555
|
+
await this.client.stop(retainedSession.sessionId).catch(() => void 0);
|
|
1556
|
+
await stopRunnerReaper(this.paths);
|
|
1557
|
+
await clearCoordinatorState(this.paths);
|
|
1558
|
+
} else {
|
|
1559
|
+
const reusableSession = await this.tryReuseSession(retainedSession, serviceNames);
|
|
1560
|
+
if (reusableSession) {
|
|
1561
|
+
await this.ensureRunnerReaper(reusableSession.sessionId, runnerPid);
|
|
1562
|
+
await writeCoordinatorState(this.paths, {
|
|
1563
|
+
session: reusableSession,
|
|
1564
|
+
leases: this.upsertLease([], workerId)
|
|
1565
|
+
});
|
|
1566
|
+
return { session: reusableSession, acquisition: "reused" };
|
|
1567
|
+
}
|
|
1568
|
+
await this.client.stop(retainedSession.sessionId).catch(() => void 0);
|
|
1569
|
+
await stopRunnerReaper(this.paths);
|
|
1570
|
+
await clearCoordinatorState(this.paths);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const startedSession = await this.client.startRouteSession(serviceNames, requestedSeeds, {
|
|
1574
|
+
source: "test"
|
|
1575
|
+
});
|
|
1576
|
+
await this.ensureRunnerReaper(startedSession.sessionId, runnerPid);
|
|
1577
|
+
await writeCoordinatorState(this.paths, {
|
|
1578
|
+
session: startedSession,
|
|
1579
|
+
leases: this.upsertLease([], workerId)
|
|
1580
|
+
});
|
|
1581
|
+
return { session: startedSession, acquisition: "fresh" };
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
async release() {
|
|
1585
|
+
const workerId = resolveWorkerId();
|
|
1586
|
+
const runnerPid = resolveRunnerPid(this.config);
|
|
1587
|
+
await withCoordinatorLock(this.paths, this.environment.readyTimeoutMs, async () => {
|
|
1588
|
+
const stateResult = await readCoordinatorState(this.paths);
|
|
1589
|
+
if (stateResult.kind !== "ready") {
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
const existingState = stateResult.state;
|
|
1593
|
+
const remainingLeases = pruneLeases(existingState.leases).filter((lease) => lease.workerId !== workerId);
|
|
1594
|
+
if (remainingLeases.length > 0) {
|
|
1595
|
+
await writeCoordinatorState(this.paths, {
|
|
1596
|
+
session: existingState.session,
|
|
1597
|
+
leases: remainingLeases
|
|
1598
|
+
});
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const shouldRetainSession = runnerIsAlive(runnerPid) && !shouldStopSessionOnRelease();
|
|
1602
|
+
if (shouldRetainSession) {
|
|
1603
|
+
await writeCoordinatorState(this.paths, {
|
|
1604
|
+
session: existingState.session,
|
|
1605
|
+
leases: []
|
|
1606
|
+
});
|
|
1607
|
+
return;
|
|
1608
|
+
}
|
|
1609
|
+
await this.client.stop(existingState.session.sessionId);
|
|
1610
|
+
await stopRunnerReaper(this.paths);
|
|
1611
|
+
await clearCoordinatorState(this.paths);
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
upsertLease(leases, workerId) {
|
|
1615
|
+
const nextLease = {
|
|
1616
|
+
workerId,
|
|
1617
|
+
pid: process.pid,
|
|
1618
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1619
|
+
};
|
|
1620
|
+
const withoutExisting = leases.filter((lease) => lease.workerId !== workerId);
|
|
1621
|
+
return [...withoutExisting, nextLease];
|
|
1622
|
+
}
|
|
1623
|
+
async recoverCorruptCoordinatorState(serviceNames, requestedSeeds, workerId, runnerPid) {
|
|
1624
|
+
const reaperState = await readRunnerReaperState(this.paths);
|
|
1625
|
+
if (!reaperState) {
|
|
1626
|
+
await clearCoordinatorState(this.paths);
|
|
1627
|
+
return null;
|
|
1628
|
+
}
|
|
1629
|
+
if (!reaperState.sessionId) {
|
|
1630
|
+
await stopRunnerReaper(this.paths);
|
|
1631
|
+
await clearCoordinatorState(this.paths);
|
|
1632
|
+
return null;
|
|
1633
|
+
}
|
|
1634
|
+
if (!runnerIsAlive(runnerPid)) {
|
|
1635
|
+
await this.client.stop(reaperState.sessionId).catch(() => void 0);
|
|
1636
|
+
await stopRunnerReaper(this.paths);
|
|
1637
|
+
await clearCoordinatorState(this.paths);
|
|
1638
|
+
return null;
|
|
1639
|
+
}
|
|
1640
|
+
const reusableSession = await this.client.reuseRouteSession(
|
|
1641
|
+
{
|
|
1642
|
+
sessionId: reaperState.sessionId,
|
|
1643
|
+
apiBaseUrls: {},
|
|
1644
|
+
resolvedRuntime: {
|
|
1645
|
+
resolvedServices: [],
|
|
1646
|
+
resolvedSeeds: {},
|
|
1647
|
+
manifestVersions: {}
|
|
1648
|
+
}
|
|
1649
|
+
},
|
|
1650
|
+
serviceNames
|
|
1651
|
+
);
|
|
1652
|
+
if (reusableSession && requestedSeedsMatchSession(requestedSeeds, reusableSession)) {
|
|
1653
|
+
await this.ensureRunnerReaper(reusableSession.sessionId, runnerPid);
|
|
1654
|
+
if (trimEnv("ARCHAL_VITEST_DISABLE_RUNNER_REAPER") === "1") {
|
|
1655
|
+
await fs.rm(this.paths.reaperPidPath, { force: true }).catch(() => void 0);
|
|
1656
|
+
}
|
|
1657
|
+
await writeCoordinatorState(this.paths, {
|
|
1658
|
+
session: reusableSession,
|
|
1659
|
+
leases: this.upsertLease([], workerId)
|
|
1660
|
+
});
|
|
1661
|
+
return reusableSession;
|
|
1662
|
+
}
|
|
1663
|
+
await this.client.stop(reaperState.sessionId).catch(() => void 0);
|
|
1664
|
+
await stopRunnerReaper(this.paths);
|
|
1665
|
+
await clearCoordinatorState(this.paths);
|
|
1666
|
+
return null;
|
|
1667
|
+
}
|
|
1668
|
+
async tryReuseSession(session, serviceNames) {
|
|
1669
|
+
try {
|
|
1670
|
+
return await this.client.reuseRouteSession(session, serviceNames);
|
|
1671
|
+
} catch {
|
|
1672
|
+
return null;
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
async ensureRunnerReaper(sessionId, runnerPid) {
|
|
1676
|
+
if (trimEnv("ARCHAL_VITEST_DISABLE_RUNNER_REAPER") === "1") {
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
const existingReaper = await readRunnerReaperState(this.paths);
|
|
1680
|
+
if (existingReaper && isProcessAlive(existingReaper.pid)) {
|
|
1681
|
+
if (existingReaper.sessionId === sessionId) {
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
await stopRunnerReaper(this.paths);
|
|
1685
|
+
}
|
|
1686
|
+
const reaperPath = resolveRuntimeModule("./runtime/hosted-session-reaper.js");
|
|
1687
|
+
const child = spawn(process.execPath, [reaperPath], {
|
|
1688
|
+
detached: true,
|
|
1689
|
+
stdio: "ignore",
|
|
1690
|
+
env: {
|
|
1691
|
+
...process.env,
|
|
1692
|
+
ARCHAL_VITEST_API_URL: this.environment.apiBaseUrl,
|
|
1693
|
+
ARCHAL_VITEST_REAPER_API_BASE_URL: this.environment.apiBaseUrl,
|
|
1694
|
+
ARCHAL_VITEST_REAPER_SESSION_ID: sessionId,
|
|
1695
|
+
ARCHAL_VITEST_REAPER_RUNNER_PID: String(runnerPid),
|
|
1696
|
+
ARCHAL_VITEST_REAPER_RENEW_INTERVAL_MS: String(this.environment.renewIntervalMs),
|
|
1697
|
+
ARCHAL_VITEST_REAPER_COORDINATOR_DIR: this.paths.directory,
|
|
1698
|
+
ARCHAL_VITEST_REAPER_LOCK_DIR: this.paths.lockDirectory,
|
|
1699
|
+
ARCHAL_VITEST_REAPER_STATE_PATH: this.paths.statePath,
|
|
1700
|
+
ARCHAL_VITEST_REAPER_PID_PATH: this.paths.reaperPidPath
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1703
|
+
child.unref();
|
|
1704
|
+
if (!child.pid) {
|
|
1705
|
+
throw new Error("Archal Vitest runner reaper failed to start.");
|
|
1706
|
+
}
|
|
1707
|
+
await writeRunnerReaperState(this.paths, { pid: child.pid, sessionId });
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
function buildNoSession() {
|
|
1711
|
+
return {
|
|
1712
|
+
sessionId: `archal-vitest-${randomUUID()}`,
|
|
1713
|
+
services: [],
|
|
1714
|
+
resolvedRuntime: {
|
|
1715
|
+
resolvedServices: [],
|
|
1716
|
+
resolvedSeeds: {},
|
|
1717
|
+
manifestVersions: {}
|
|
1718
|
+
},
|
|
1719
|
+
// No hosted session was provisioned (route-mode config had no services)
|
|
1720
|
+
// so there's no cold-start cost to report. Mark as `reused` so the
|
|
1721
|
+
// bootstrap's twin-startup log treats this as zero-cost and stays quiet.
|
|
1722
|
+
acquisition: "reused"
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
function buildRoutedServices(serviceNames, apiBaseUrls, environment) {
|
|
1726
|
+
return serviceNames.map((serviceName) => {
|
|
1727
|
+
const baseUrl = apiBaseUrls[serviceName];
|
|
1728
|
+
if (!baseUrl) {
|
|
1729
|
+
throw new Error(
|
|
1730
|
+
`Hosted Vitest session did not return apiBaseUrls.${serviceName}.`
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
const manifest = getSharedRouteManifest(serviceName);
|
|
1734
|
+
return {
|
|
1735
|
+
name: serviceName,
|
|
1736
|
+
mode: "route",
|
|
1737
|
+
baseUrl,
|
|
1738
|
+
upstreamBasePath: manifest?.upstreamBasePath,
|
|
1739
|
+
routedRequestHeaders: () => {
|
|
1740
|
+
const authorization = environment.auth.getAuthorizationHeader();
|
|
1741
|
+
if (!authorization) {
|
|
1742
|
+
throw new Error("Hosted Vitest routing auth is unavailable.");
|
|
1743
|
+
}
|
|
1744
|
+
const workerId = currentVitestWorkerId();
|
|
1745
|
+
const headers = { authorization };
|
|
1746
|
+
if (workerId) headers[WORKER_ID_HEADER] = workerId;
|
|
1747
|
+
return headers;
|
|
1748
|
+
},
|
|
1749
|
+
forwardedAuthorizationHeader: FORWARDED_AUTHORIZATION_HEADER
|
|
1750
|
+
};
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
async function startHostedArchalVitestSession(config) {
|
|
1754
|
+
assertSupportedArchalVitestServices(config.services);
|
|
1755
|
+
const routedServiceNames = Object.entries(config.services).filter(([, serviceConfig]) => serviceConfig.mode === "route").map(([serviceName]) => serviceName);
|
|
1756
|
+
if (routedServiceNames.length === 0) {
|
|
1757
|
+
return buildNoSession();
|
|
1758
|
+
}
|
|
1759
|
+
const requestedSeeds = resolveRequestedSeeds(config, routedServiceNames);
|
|
1760
|
+
const environment = await resolveHostedSessionEnvironment();
|
|
1761
|
+
const hostedClient = new HostedSessionClient(environment);
|
|
1762
|
+
const coordinator = new CoordinatedHostedSessionManager(
|
|
1763
|
+
resolveCoordinatorPaths(config),
|
|
1764
|
+
hostedClient,
|
|
1765
|
+
environment,
|
|
1766
|
+
config
|
|
1767
|
+
);
|
|
1768
|
+
try {
|
|
1769
|
+
const { session, acquisition } = await coordinator.acquire(routedServiceNames, requestedSeeds);
|
|
1770
|
+
return {
|
|
1771
|
+
sessionId: session.sessionId,
|
|
1772
|
+
services: buildRoutedServices(routedServiceNames, session.apiBaseUrls, environment),
|
|
1773
|
+
resolvedRuntime: session.resolvedRuntime,
|
|
1774
|
+
acquisition,
|
|
1775
|
+
stop: async () => {
|
|
1776
|
+
try {
|
|
1777
|
+
await coordinator.release();
|
|
1778
|
+
} finally {
|
|
1779
|
+
environment.auth.stop();
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
environment.auth.stop();
|
|
1785
|
+
throw error;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
var VITEST_AUTH_LEASE_OPTIONS = {
|
|
1789
|
+
apiUrlEnvVar: "ARCHAL_VITEST_API_URL"
|
|
1790
|
+
};
|
|
1791
|
+
|
|
1792
|
+
// src/runtime/bootstrap.ts
|
|
1793
|
+
var GLOBAL_RUNTIME_KEY = "__archalVitestRuntime";
|
|
1794
|
+
var GLOBAL_SESSION_KEY = "__archalVitestSession";
|
|
1795
|
+
var GLOBAL_CLEANUP_HANDLER_KEY = "__archalVitestRuntimeCleanupHandlerRegistered";
|
|
1796
|
+
var GLOBAL_BOOTSTRAP_PROMISE_KEY = "__archalVitestRuntimeBootstrapPromise";
|
|
1797
|
+
var GLOBAL_STOP_PROMISE_KEY = "__archalVitestRuntimeStopPromise";
|
|
1798
|
+
var ARCHAL_VITEST_ROUTE_TRACE_ENV = "ARCHAL_VITEST_ROUTE_TRACE";
|
|
1799
|
+
var activeFullSession;
|
|
1800
|
+
var baselineTwinStates = /* @__PURE__ */ new Map();
|
|
1801
|
+
var baselineCaptured = false;
|
|
1802
|
+
var sessionStartedAt;
|
|
1803
|
+
function emitUsageEstimate(services) {
|
|
1804
|
+
if (sessionStartedAt === void 0 || services.length === 0) return;
|
|
1805
|
+
if (process.env["ARCHAL_VITEST_QUIET_USAGE"] === "1") return;
|
|
1806
|
+
const durationSec = (Date.now() - sessionStartedAt) / 1e3;
|
|
1807
|
+
const twinMinutes = Math.max(1, Math.ceil(durationSec / 60)) * services.length;
|
|
1808
|
+
const twinList = services.map((s) => s.name).sort().join(", ");
|
|
1809
|
+
process.stderr.write(
|
|
1810
|
+
`\x1B[2m[archal] ~${twinMinutes} twin-minute${twinMinutes === 1 ? "" : "s"} for this run (${durationSec.toFixed(1)}s \xD7 ${services.length} twin${services.length === 1 ? "" : "s"}: ${twinList})\x1B[0m
|
|
1811
|
+
`
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
function twinStateUrl(serviceBaseUrl) {
|
|
1815
|
+
return serviceBaseUrl.replace(/\/api\/?$/, "/state");
|
|
1816
|
+
}
|
|
1817
|
+
function resolveRoutedHeaders(service) {
|
|
1818
|
+
try {
|
|
1819
|
+
const headers = service.routedRequestHeaders;
|
|
1820
|
+
if (typeof headers === "function") return headers();
|
|
1821
|
+
return headers ?? {};
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
activeFullSession = void 0;
|
|
1824
|
+
throw new Error(
|
|
1825
|
+
`Archal auth expired mid-test-run for twin "${service.name}". Re-run tests to refresh. (${err instanceof Error ? err.message : String(err)})`
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
var WORKER_ID_HEADER2 = "x-archal-worker-id";
|
|
1830
|
+
function resolveDefaultScopeHeaders(service) {
|
|
1831
|
+
const all = resolveRoutedHeaders(service);
|
|
1832
|
+
const { [WORKER_ID_HEADER2]: _discarded, ...rest } = all;
|
|
1833
|
+
return rest;
|
|
1834
|
+
}
|
|
1835
|
+
function twinRootUrl(serviceBaseUrl) {
|
|
1836
|
+
return serviceBaseUrl.replace(/\/api\/?$/, "");
|
|
1837
|
+
}
|
|
1838
|
+
async function fetchArchalTwin(serviceName, path, init) {
|
|
1839
|
+
const session = activeFullSession;
|
|
1840
|
+
if (!session) {
|
|
1841
|
+
throw new Error(
|
|
1842
|
+
"fetchArchalTwin() called before the Archal route runtime was installed. Ensure archalVitestProject() or withArchal() is wired into your vitest config, and that setup has completed."
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
const service = session.services.find((s) => s.name === serviceName);
|
|
1846
|
+
if (!service) {
|
|
1847
|
+
const available = session.services.map((s) => s.name).sort().join(", ") || "(none)";
|
|
1848
|
+
throw new Error(
|
|
1849
|
+
`No twin named "${serviceName}" in this session. Available: ${available}.`
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
const headers = resolveRoutedHeaders(service);
|
|
1853
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
1854
|
+
const url = `${twinRootUrl(service.baseUrl)}${suffix}`;
|
|
1855
|
+
const requestInit = {
|
|
1856
|
+
...init,
|
|
1857
|
+
headers: { ...headers, ...init?.headers ?? {} }
|
|
1858
|
+
};
|
|
1859
|
+
return fetch(url, requestInit);
|
|
1860
|
+
}
|
|
1861
|
+
async function fetchArchalTwinDefaultScope(serviceName, path, init) {
|
|
1862
|
+
const session = activeFullSession;
|
|
1863
|
+
if (!session) {
|
|
1864
|
+
throw new Error("fetchArchalTwinDefaultScope: no active session");
|
|
1865
|
+
}
|
|
1866
|
+
const service = session.services.find((s) => s.name === serviceName);
|
|
1867
|
+
if (!service) {
|
|
1868
|
+
throw new Error(`No twin "${serviceName}" in session`);
|
|
1869
|
+
}
|
|
1870
|
+
const headers = resolveDefaultScopeHeaders(service);
|
|
1871
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
1872
|
+
const url = `${twinRootUrl(service.baseUrl)}${suffix}`;
|
|
1873
|
+
return fetch(url, {
|
|
1874
|
+
...init,
|
|
1875
|
+
headers: { ...headers, ...init?.headers ?? {} }
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
async function captureBaselineTwinStates(session) {
|
|
1879
|
+
baselineTwinStates.clear();
|
|
1880
|
+
baselineCaptured = false;
|
|
1881
|
+
await Promise.all(session.services.map(async (service) => {
|
|
1882
|
+
try {
|
|
1883
|
+
const headers = resolveDefaultScopeHeaders(service);
|
|
1884
|
+
const res = await fetch(twinStateUrl(service.baseUrl), {
|
|
1885
|
+
method: "GET",
|
|
1886
|
+
headers: { ...headers, accept: "application/json" }
|
|
1887
|
+
});
|
|
1888
|
+
if (!res.ok) {
|
|
1889
|
+
process.stderr.write(
|
|
1890
|
+
`[archal] warning: failed to capture baseline for twin "${service.name}": ${res.status} ${res.statusText}
|
|
1891
|
+
`
|
|
1892
|
+
);
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
baselineTwinStates.set(service.name, await res.text());
|
|
1896
|
+
} catch (err) {
|
|
1897
|
+
process.stderr.write(
|
|
1898
|
+
`[archal] warning: failed to capture baseline for twin "${service.name}": ${err instanceof Error ? err.message : String(err)}
|
|
1899
|
+
`
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
}));
|
|
1903
|
+
baselineCaptured = true;
|
|
1904
|
+
}
|
|
1905
|
+
async function resetArchalTwins() {
|
|
1906
|
+
const session = activeFullSession;
|
|
1907
|
+
if (!session) {
|
|
1908
|
+
throw new Error(
|
|
1909
|
+
"resetArchalTwins() called before the Archal route runtime was installed. Ensure archalVitestProject() or withArchal() is wired into your vitest config, and that setup has completed."
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
if (!baselineCaptured) {
|
|
1913
|
+
throw new Error(
|
|
1914
|
+
"resetArchalTwins() called but baseline state was never captured. This usually means bootstrapArchalVitestRouting() has not completed."
|
|
1915
|
+
);
|
|
1916
|
+
}
|
|
1917
|
+
await Promise.all(session.services.map(async (service) => {
|
|
1918
|
+
const baseline = baselineTwinStates.get(service.name);
|
|
1919
|
+
if (baseline === void 0) {
|
|
1920
|
+
throw new Error(
|
|
1921
|
+
`No baseline state captured for twin "${service.name}". This usually means the twin was not reachable during setup.`
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
const stateUrl = twinStateUrl(service.baseUrl);
|
|
1925
|
+
const defaultHeaders = resolveDefaultScopeHeaders(service);
|
|
1926
|
+
const defaultRes = await fetch(stateUrl, {
|
|
1927
|
+
method: "PUT",
|
|
1928
|
+
headers: { ...defaultHeaders, "content-type": "application/json" },
|
|
1929
|
+
body: baseline
|
|
1930
|
+
});
|
|
1931
|
+
if (!defaultRes.ok) {
|
|
1932
|
+
throw new Error(
|
|
1933
|
+
`Failed to reset shared state for twin "${service.name}": ${defaultRes.status} ${defaultRes.statusText}`
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
const workerHeaders = resolveRoutedHeaders(service);
|
|
1937
|
+
const workerRes = await fetch(stateUrl, {
|
|
1938
|
+
method: "PUT",
|
|
1939
|
+
headers: { ...workerHeaders, "content-type": "application/json" },
|
|
1940
|
+
body: baseline
|
|
1941
|
+
});
|
|
1942
|
+
if (!workerRes.ok) {
|
|
1943
|
+
throw new Error(
|
|
1944
|
+
`Failed to reset per-worker state for twin "${service.name}": ${workerRes.status} ${workerRes.statusText}`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
try {
|
|
1948
|
+
const whRes = await fetch(`${twinRootUrl(service.baseUrl)}/webhooks/pending`, {
|
|
1949
|
+
method: "DELETE",
|
|
1950
|
+
headers: workerHeaders
|
|
1951
|
+
});
|
|
1952
|
+
if (!whRes.ok && whRes.status !== 404) {
|
|
1953
|
+
process.stderr.write(
|
|
1954
|
+
`[archal] warning: failed to drain webhook queue for "${service.name}": ${whRes.status} ${whRes.statusText}
|
|
1955
|
+
`
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
} catch {
|
|
1959
|
+
}
|
|
1960
|
+
}));
|
|
1961
|
+
}
|
|
1962
|
+
function emitManifestDriftWarning(session) {
|
|
1963
|
+
if (process.env["ARCHAL_VITEST_QUIET_DRIFT"] === "1") return;
|
|
1964
|
+
const serverVersions = session.resolvedRuntime?.manifestVersions ?? {};
|
|
1965
|
+
const drifts = [];
|
|
1966
|
+
for (const service of session.services) {
|
|
1967
|
+
const serverVersion = serverVersions[service.name];
|
|
1968
|
+
const local = getSharedRouteManifest(service.name);
|
|
1969
|
+
if (!serverVersion || !local) continue;
|
|
1970
|
+
if (local.manifestVersion !== serverVersion) {
|
|
1971
|
+
drifts.push({
|
|
1972
|
+
service: service.name,
|
|
1973
|
+
server: serverVersion,
|
|
1974
|
+
client: local.manifestVersion
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
if (drifts.length === 0) return;
|
|
1979
|
+
const lines = drifts.map(
|
|
1980
|
+
(d) => ` ${d.service}: client ${d.client} vs server ${d.server}`
|
|
1981
|
+
);
|
|
1982
|
+
process.stderr.write(
|
|
1983
|
+
"\x1B[33m[archal] manifest version drift \u2014 upgrade the `archal` package to match the hosted twins:\n" + lines.join("\n") + "\x1B[0m\n"
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
function emitProgressTicker(config) {
|
|
1987
|
+
if (process.env["ARCHAL_VITEST_QUIET_PROGRESS"] === "1") {
|
|
1988
|
+
return () => {
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
const serviceNames = Object.keys(config.services).sort();
|
|
1992
|
+
const services = serviceNames.join(", ");
|
|
1993
|
+
const twinWord = serviceNames.length === 1 ? "twin" : "twins";
|
|
1994
|
+
const start = Date.now();
|
|
1995
|
+
const ticker = setInterval(() => {
|
|
1996
|
+
const elapsed = Math.round((Date.now() - start) / 1e3);
|
|
1997
|
+
if (elapsed >= 60) {
|
|
1998
|
+
clearInterval(ticker);
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
process.stderr.write(
|
|
2002
|
+
`\x1B[2m[archal] provisioning ${services} ${twinWord}... ${elapsed}s\x1B[0m
|
|
2003
|
+
`
|
|
2004
|
+
);
|
|
2005
|
+
}, 5e3);
|
|
2006
|
+
ticker.unref();
|
|
2007
|
+
return () => clearInterval(ticker);
|
|
2008
|
+
}
|
|
2009
|
+
function isTruthyEnv(value) {
|
|
2010
|
+
if (!value) {
|
|
2011
|
+
return false;
|
|
2012
|
+
}
|
|
2013
|
+
switch (value.trim().toLowerCase()) {
|
|
2014
|
+
case "1":
|
|
2015
|
+
case "true":
|
|
2016
|
+
case "yes":
|
|
2017
|
+
case "on":
|
|
2018
|
+
return true;
|
|
2019
|
+
default:
|
|
2020
|
+
return false;
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
function formatTraceRecord(record) {
|
|
2024
|
+
const manifestStatus = record.manifestMatched ? `matched ${record.service ?? "manifest"}` : "no manifest match";
|
|
2025
|
+
const destination = record.target === "twin" ? `twin${record.targetUrl ? ` ${record.targetUrl}` : ""}` : record.target === "upstream" ? "real upstream" : "blocked";
|
|
2026
|
+
const status = record.statusCode === void 0 ? record.error ? `error=${record.error}` : "no status" : `${record.statusCode}${record.statusText ? ` ${record.statusText}` : ""}`;
|
|
2027
|
+
return `\x1B[2m[archal:route] ${record.method} ${record.sourceUrl} | ${manifestStatus} | ${record.outcome} -> ${destination} | ${status}\x1B[0m
|
|
2028
|
+
`;
|
|
2029
|
+
}
|
|
2030
|
+
async function startSession(config) {
|
|
2031
|
+
const stopTicker = emitProgressTicker(config);
|
|
2032
|
+
try {
|
|
2033
|
+
sessionStartedAt = Date.now();
|
|
2034
|
+
return await startHostedArchalVitestSession(config);
|
|
2035
|
+
} finally {
|
|
2036
|
+
stopTicker();
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
var DEFAULT_STARTUP_LOG_MIN_MS = 3e3;
|
|
2040
|
+
function resolveStartupLogThresholdMs() {
|
|
2041
|
+
const raw = process.env["ARCHAL_VITEST_STARTUP_LOG_MIN_MS"];
|
|
2042
|
+
if (!raw) return DEFAULT_STARTUP_LOG_MIN_MS;
|
|
2043
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
2044
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_STARTUP_LOG_MIN_MS;
|
|
2045
|
+
return parsed;
|
|
2046
|
+
}
|
|
2047
|
+
function emitTwinStartupSummary(phases) {
|
|
2048
|
+
if (process.env["ARCHAL_VITEST_QUIET_STARTUP"] === "1") return;
|
|
2049
|
+
const threshold = resolveStartupLogThresholdMs();
|
|
2050
|
+
if (phases.totalMs < threshold) return;
|
|
2051
|
+
if (phases.twinCount === 0) return;
|
|
2052
|
+
const twinLabel = phases.twinCount === 1 ? "twin" : "twins";
|
|
2053
|
+
const twinList = phases.twinNames.length > 0 ? ` [${phases.twinNames.slice().sort().join(", ")}]` : "";
|
|
2054
|
+
const mode = phases.acquisition === "unknown" ? "" : ` (${phases.acquisition})`;
|
|
2055
|
+
const parts = [];
|
|
2056
|
+
if (phases.acquireMs >= 10) parts.push(`acquire=${phases.acquireMs}ms`);
|
|
2057
|
+
if (phases.seedLoadMs >= 10) parts.push(`seed-load=${phases.seedLoadMs}ms`);
|
|
2058
|
+
if (phases.baselineMs >= 10) parts.push(`baseline=${phases.baselineMs}ms`);
|
|
2059
|
+
const breakdown = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
2060
|
+
process.stderr.write(
|
|
2061
|
+
`\x1B[2m[archal/vitest] twin-startup${mode}: ${phases.totalMs}ms for ${phases.twinCount} ${twinLabel}${twinList}${breakdown}\x1B[0m
|
|
2062
|
+
`
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
var pendingStartupPhases;
|
|
2066
|
+
async function buildRuntimeConfig(scope) {
|
|
2067
|
+
const config = readArchalVitestConfigFromEnv();
|
|
2068
|
+
let acquireMs = 0;
|
|
2069
|
+
let acquisition = "unknown";
|
|
2070
|
+
let session = activeFullSession;
|
|
2071
|
+
if (!session) {
|
|
2072
|
+
const acquireStart = Date.now();
|
|
2073
|
+
session = await startSession(config);
|
|
2074
|
+
acquireMs = Date.now() - acquireStart;
|
|
2075
|
+
acquisition = session.acquisition ?? "unknown";
|
|
2076
|
+
} else {
|
|
2077
|
+
acquisition = "reused";
|
|
2078
|
+
}
|
|
2079
|
+
activeFullSession = session;
|
|
2080
|
+
pendingStartupPhases = {
|
|
2081
|
+
acquireMs,
|
|
2082
|
+
seedLoadMs: 0,
|
|
2083
|
+
baselineMs: 0,
|
|
2084
|
+
totalMs: acquireMs,
|
|
2085
|
+
acquisition,
|
|
2086
|
+
twinCount: session.services.length,
|
|
2087
|
+
twinNames: session.services.map((s) => s.name)
|
|
2088
|
+
};
|
|
2089
|
+
scope[GLOBAL_SESSION_KEY] = redactSessionSnapshot(session);
|
|
2090
|
+
try {
|
|
2091
|
+
const { writeFileSync, mkdirSync } = await import("fs");
|
|
2092
|
+
const { dirname: dirname2 } = await import("path");
|
|
2093
|
+
const sessionIdPath = getSessionIdFilePath(config.sessionKey);
|
|
2094
|
+
mkdirSync(dirname2(sessionIdPath), { recursive: true });
|
|
2095
|
+
writeFileSync(sessionIdPath, session.sessionId, "utf-8");
|
|
2096
|
+
} catch {
|
|
2097
|
+
}
|
|
2098
|
+
return {
|
|
2099
|
+
profiles: resolveArchalVitestServiceProfiles(config.services),
|
|
2100
|
+
services: session.services,
|
|
2101
|
+
trace: isTruthyEnv(process.env[ARCHAL_VITEST_ROUTE_TRACE_ENV]) ? {
|
|
2102
|
+
enabled: true,
|
|
2103
|
+
onTrace: (record) => {
|
|
2104
|
+
process.stderr.write(formatTraceRecord(record));
|
|
2105
|
+
}
|
|
2106
|
+
} : void 0
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
async function stopArchalVitestRouting() {
|
|
2110
|
+
const scope = globalThis;
|
|
2111
|
+
if (scope[GLOBAL_BOOTSTRAP_PROMISE_KEY]) {
|
|
2112
|
+
await scope[GLOBAL_BOOTSTRAP_PROMISE_KEY].catch(() => {
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
if (scope[GLOBAL_STOP_PROMISE_KEY]) {
|
|
2116
|
+
await scope[GLOBAL_STOP_PROMISE_KEY];
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
scope[GLOBAL_STOP_PROMISE_KEY] = (async () => {
|
|
2120
|
+
const runtime = scope[GLOBAL_RUNTIME_KEY];
|
|
2121
|
+
const session = activeFullSession;
|
|
2122
|
+
const services = session?.services.map((s) => ({ name: s.name })) ?? [];
|
|
2123
|
+
delete scope[GLOBAL_RUNTIME_KEY];
|
|
2124
|
+
delete scope[GLOBAL_SESSION_KEY];
|
|
2125
|
+
emitUsageEstimate(services);
|
|
2126
|
+
sessionStartedAt = void 0;
|
|
2127
|
+
activeFullSession = void 0;
|
|
2128
|
+
baselineTwinStates.clear();
|
|
2129
|
+
baselineCaptured = false;
|
|
2130
|
+
await runtime?.stop();
|
|
2131
|
+
await session?.stop?.();
|
|
2132
|
+
})();
|
|
2133
|
+
try {
|
|
2134
|
+
await scope[GLOBAL_STOP_PROMISE_KEY];
|
|
2135
|
+
} finally {
|
|
2136
|
+
delete scope[GLOBAL_STOP_PROMISE_KEY];
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
async function bootstrapArchalVitestRouting() {
|
|
2140
|
+
const scope = globalThis;
|
|
2141
|
+
if (scope[GLOBAL_RUNTIME_KEY]) {
|
|
2142
|
+
return scope[GLOBAL_RUNTIME_KEY];
|
|
2143
|
+
}
|
|
2144
|
+
if (scope[GLOBAL_BOOTSTRAP_PROMISE_KEY]) {
|
|
2145
|
+
return await scope[GLOBAL_BOOTSTRAP_PROMISE_KEY];
|
|
2146
|
+
}
|
|
2147
|
+
scope[GLOBAL_BOOTSTRAP_PROMISE_KEY] = (async () => {
|
|
2148
|
+
const runtime = new NodeRouteRuntime(await buildRuntimeConfig(scope));
|
|
2149
|
+
await runtime.start();
|
|
2150
|
+
runtime.installRouting();
|
|
2151
|
+
scope[GLOBAL_RUNTIME_KEY] = runtime;
|
|
2152
|
+
const config = readArchalVitestConfigFromEnv();
|
|
2153
|
+
if (config.fileSeedContents && Object.keys(config.fileSeedContents).length > 0) {
|
|
2154
|
+
const seedStart = Date.now();
|
|
2155
|
+
await loadFileSeedsIntoTwins(config, fetchArchalTwinDefaultScope, {
|
|
2156
|
+
timeoutEnvVar: "ARCHAL_VITEST_SEED_LOAD_TIMEOUT_MS"
|
|
2157
|
+
});
|
|
2158
|
+
if (pendingStartupPhases) {
|
|
2159
|
+
pendingStartupPhases.seedLoadMs = Date.now() - seedStart;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
if (activeFullSession) {
|
|
2163
|
+
emitManifestDriftWarning(activeFullSession);
|
|
2164
|
+
const baselineStart = Date.now();
|
|
2165
|
+
await captureBaselineTwinStates(activeFullSession);
|
|
2166
|
+
if (pendingStartupPhases) {
|
|
2167
|
+
pendingStartupPhases.baselineMs = Date.now() - baselineStart;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
if (pendingStartupPhases) {
|
|
2171
|
+
pendingStartupPhases.totalMs = pendingStartupPhases.acquireMs + pendingStartupPhases.seedLoadMs + pendingStartupPhases.baselineMs;
|
|
2172
|
+
emitTwinStartupSummary(pendingStartupPhases);
|
|
2173
|
+
pendingStartupPhases = void 0;
|
|
2174
|
+
}
|
|
2175
|
+
if (!scope[GLOBAL_CLEANUP_HANDLER_KEY]) {
|
|
2176
|
+
const asyncCleanupAndExit = (exitCode) => {
|
|
2177
|
+
void stopArchalVitestRouting().finally(() => {
|
|
2178
|
+
process.exit(exitCode);
|
|
2179
|
+
});
|
|
2180
|
+
};
|
|
2181
|
+
process.once("beforeExit", () => {
|
|
2182
|
+
void stopArchalVitestRouting();
|
|
2183
|
+
});
|
|
2184
|
+
process.once("disconnect", () => {
|
|
2185
|
+
void stopArchalVitestRouting();
|
|
2186
|
+
});
|
|
2187
|
+
process.once("SIGINT", () => asyncCleanupAndExit(130));
|
|
2188
|
+
process.once("SIGTERM", () => asyncCleanupAndExit(143));
|
|
2189
|
+
scope[GLOBAL_CLEANUP_HANDLER_KEY] = true;
|
|
2190
|
+
}
|
|
2191
|
+
return runtime;
|
|
2192
|
+
})();
|
|
2193
|
+
try {
|
|
2194
|
+
return await scope[GLOBAL_BOOTSTRAP_PROMISE_KEY];
|
|
2195
|
+
} finally {
|
|
2196
|
+
delete scope[GLOBAL_BOOTSTRAP_PROMISE_KEY];
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
function getInstalledArchalVitestRuntime() {
|
|
2200
|
+
return globalThis[GLOBAL_RUNTIME_KEY];
|
|
2201
|
+
}
|
|
2202
|
+
function getInstalledArchalVitestSession() {
|
|
2203
|
+
return globalThis[GLOBAL_SESSION_KEY];
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
export {
|
|
2207
|
+
resolveRuntimeModule,
|
|
2208
|
+
ARCHAL_VITEST_CONFIG_ENV,
|
|
2209
|
+
readArchalVitestConfig,
|
|
2210
|
+
assertSupportedArchalVitestServices,
|
|
2211
|
+
fetchArchalTwin,
|
|
2212
|
+
resetArchalTwins,
|
|
2213
|
+
bootstrapArchalVitestRouting,
|
|
2214
|
+
getInstalledArchalVitestRuntime,
|
|
2215
|
+
getInstalledArchalVitestSession
|
|
2216
|
+
};
|