archal 0.9.19 → 0.9.20
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 +9 -1
- package/agents/github-octokit/.archal.json +8 -0
- package/agents/github-octokit/Dockerfile +8 -0
- package/agents/github-octokit/README.md +113 -0
- package/agents/github-octokit/agent.mjs +54 -0
- package/agents/github-octokit/package.json +9 -0
- package/agents/github-octokit/scenarios/test-repo-access.md +27 -0
- package/agents/google-workspace-local-tools/Dockerfile +6 -0
- package/agents/google-workspace-local-tools/README.md +58 -0
- package/agents/google-workspace-local-tools/agent.mjs +196 -0
- package/agents/google-workspace-local-tools/archal-harness.json +7 -0
- package/agents/google-workspace-local-tools/run-input.yaml +16 -0
- package/agents/google-workspace-local-tools/scenario.md +29 -0
- package/agents/hermes/.archal.json +8 -0
- package/agents/hermes/Dockerfile +46 -0
- package/agents/hermes/README.md +87 -0
- package/agents/hermes/SOUL.md +27 -0
- package/agents/hermes/config.yaml +34 -0
- package/agents/hermes/drive.mjs +113 -0
- package/agents/hermes/scenarios/stripe-customers-read-only.md +32 -0
- package/agents/openclaw/.archal.json +8 -0
- package/agents/openclaw/Dockerfile +96 -0
- package/agents/openclaw/README.md +120 -0
- package/agents/openclaw/drive.mjs +311 -0
- package/agents/openclaw/package.json +9 -0
- package/agents/openclaw/scenarios/github-issue-triage-read-only.md +44 -0
- package/agents/openclaw/workspace/AGENTS.md +23 -0
- package/agents/openclaw/workspace/IDENTITY.md +8 -0
- package/agents/openclaw/workspace/SOUL.md +14 -0
- package/agents/openclaw/workspace/TOOLS.md +35 -0
- package/agents/pagination-test/README.md +24 -0
- package/agents/pagination-test/scenario.md +24 -0
- package/agents/replay-capsule-harness/README.md +29 -0
- package/agents/replay-capsule-harness/observability-install-offline-e2e.mts +1517 -0
- package/agents/replay-capsule-harness/replay-capsule-e2e.mjs +104 -0
- package/clone-assets/apify/tools.json +256 -22
- package/clone-assets/calcom/tools.json +510 -0
- package/clone-assets/clickup/tools.json +1258 -0
- package/clone-assets/customerio/tools.json +386 -0
- package/clone-assets/datadog/tools.json +734 -0
- package/clone-assets/github/tools.json +306 -25
- package/clone-assets/gitlab/tools.json +999 -0
- package/clone-assets/google-workspace/tools.json +18 -6
- package/clone-assets/hubspot/tools.json +1406 -0
- package/clone-assets/jira/fidelity.json +1 -1
- package/clone-assets/jira/tools.json +266 -543
- package/clone-assets/linear/tools.json +238 -40
- package/clone-assets/ownerrez/tools.json +548 -0
- package/clone-assets/pricelabs/tools.json +343 -0
- package/clone-assets/sentry/tools.json +745 -0
- package/clone-assets/slack/tools.json +1 -2
- package/clone-assets/stripe/tools.json +185 -46
- package/clone-assets/supabase/tools.json +437 -0
- package/clone-assets/unipile/tools.json +408 -0
- package/clone-assets/webflow/tools.json +415 -0
- package/dist/autoloop-worker-types-BEb_E44z.d.cts +196 -0
- package/dist/cli.cjs +150299 -87430
- package/dist/commands/autoloop-hosted-worker.cjs +43942 -0
- package/dist/commands/autoloop-hosted-worker.d.cts +143 -0
- package/dist/commands/autoloop-pr-verification.cjs +4227 -0
- package/dist/commands/autoloop-pr-verification.d.cts +17 -0
- package/dist/{vitest/chunk-L36NXAU6.js → commands/autoloop-result-parser.cjs} +16445 -18852
- package/dist/commands/autoloop-result-parser.d.cts +39 -0
- package/dist/commands/autoloop-worker.cjs +36163 -0
- package/dist/commands/autoloop-worker.d.cts +97 -0
- package/dist/harness.cjs +1 -0
- package/dist/index.cjs +1 -1
- package/dist/replay.cjs +49624 -0
- package/dist/replay.d.cts +4625 -0
- package/dist/scenarios.cjs +80343 -0
- package/dist/scenarios.d.cts +562 -0
- package/dist/vitest/chunk-6CBYFCFK.js +4667 -0
- package/dist/vitest/chunk-ARVS45PP.js +2764 -0
- package/dist/vitest/index.cjs +6011 -75261
- package/dist/vitest/index.d.ts +7 -6
- package/dist/vitest/index.js +8 -8
- package/dist/vitest/runtime/hosted-session-reaper.cjs +792 -34359
- package/dist/vitest/runtime/hosted-session-reaper.js +1 -1
- package/dist/vitest/runtime/setup-files.js +2 -2
- package/package.json +8 -3
- package/skills/archal-agent/SKILL.md +87 -0
- package/skills/{attach → autoloop}/SKILL.md +94 -120
- package/skills/autoloop/references/hosted-sources.md +62 -0
- package/skills/autoloop/references/trace-schema-mapping.md +73 -0
- package/skills/eval/SKILL.md +35 -1
- package/skills/install-agent/SKILL.md +221 -0
- package/skills/onboard/SKILL.md +73 -5
- package/skills/scenario/SKILL.md +19 -4
- package/skills/seed/SKILL.md +237 -0
- package/dist/seed/dynamic-generator.cjs +0 -45687
- package/dist/seed/dynamic-generator.d.cts +0 -106
- package/dist/vitest/chunk-WZ7SA4CK.js +0 -47369
|
@@ -0,0 +1,4667 @@
|
|
|
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
|
+
loadFileSeedsIntoClones,
|
|
15
|
+
normalizeApiBaseUrl,
|
|
16
|
+
parsePositiveInteger,
|
|
17
|
+
redactSessionSnapshot,
|
|
18
|
+
requestedSeedsMatchSession,
|
|
19
|
+
sleep,
|
|
20
|
+
trimEnv
|
|
21
|
+
} from "./chunk-ARVS45PP.js";
|
|
22
|
+
|
|
23
|
+
// src/runtime-module-resolution.ts
|
|
24
|
+
import { dirname, extname, isAbsolute, 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" && isAbsolute(__filename) ? __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("Network request failed");
|
|
65
|
+
this.name = "TypeError";
|
|
66
|
+
Object.defineProperties(this, {
|
|
67
|
+
code: { value: input.code, enumerable: false },
|
|
68
|
+
service: { value: input.service, enumerable: false },
|
|
69
|
+
url: { value: input.url, enumerable: false },
|
|
70
|
+
policyMessage: { value: input.message, enumerable: false }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ../route-runtime-core/src/runtime.ts
|
|
76
|
+
import { createRequire } from "module";
|
|
77
|
+
import { isAbsolute as isAbsolute2 } from "path";
|
|
78
|
+
|
|
79
|
+
// ../../clones/core/src/errors.ts
|
|
80
|
+
function errorMessage(error) {
|
|
81
|
+
return error instanceof Error ? error.message : String(error);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ../route-runtime-core/src/runtime-request.ts
|
|
85
|
+
var DEFAULT_HTTP_PROTOCOL = "http:";
|
|
86
|
+
var DEFAULT_HTTPS_PROTOCOL = "https:";
|
|
87
|
+
function normalizeProtocol(protocol, fallback) {
|
|
88
|
+
if (!protocol) return fallback;
|
|
89
|
+
return protocol.endsWith(":") ? protocol : `${protocol}:`;
|
|
90
|
+
}
|
|
91
|
+
function normalizeHost(value) {
|
|
92
|
+
if (value.includes("://")) {
|
|
93
|
+
const parsed = new URL(value);
|
|
94
|
+
return {
|
|
95
|
+
hostname: parsed.hostname,
|
|
96
|
+
port: parsed.port || void 0
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (value.startsWith("[")) {
|
|
100
|
+
const closingBracketIndex = value.indexOf("]");
|
|
101
|
+
if (closingBracketIndex !== -1) {
|
|
102
|
+
const hostname = value.slice(1, closingBracketIndex);
|
|
103
|
+
const port = value.slice(closingBracketIndex + 1).replace(/^:/, "") || void 0;
|
|
104
|
+
return { hostname, port };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const lastColonIndex = value.lastIndexOf(":");
|
|
108
|
+
if (lastColonIndex === -1 || value.indexOf(":") !== lastColonIndex) {
|
|
109
|
+
return { hostname: value };
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
hostname: value.slice(0, lastColonIndex),
|
|
113
|
+
port: value.slice(lastColonIndex + 1) || void 0
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function buildUrlFromOptions(options, defaultProtocol) {
|
|
117
|
+
const protocol = normalizeProtocol(
|
|
118
|
+
typeof options.protocol === "string" ? options.protocol : void 0,
|
|
119
|
+
defaultProtocol
|
|
120
|
+
);
|
|
121
|
+
const rawHost = typeof options.hostname === "string" ? options.hostname : typeof options.host === "string" ? normalizeHost(options.host).hostname : "localhost";
|
|
122
|
+
const rawPort = options.port == null ? typeof options.host === "string" ? normalizeHost(options.host).port : void 0 : String(options.port);
|
|
123
|
+
const base = `${protocol}//${rawHost}${rawPort ? `:${rawPort}` : ""}`;
|
|
124
|
+
const path = typeof options.path === "string" && options.path.length > 0 ? options.path : "/";
|
|
125
|
+
return new URL(path, base);
|
|
126
|
+
}
|
|
127
|
+
function applyRequestOptionsToUrl(requestUrl, options, defaultProtocol) {
|
|
128
|
+
const effectiveUrl = new URL(requestUrl.toString());
|
|
129
|
+
const protocol = normalizeProtocol(
|
|
130
|
+
typeof options.protocol === "string" ? options.protocol : void 0,
|
|
131
|
+
effectiveUrl.protocol || defaultProtocol
|
|
132
|
+
);
|
|
133
|
+
effectiveUrl.protocol = protocol;
|
|
134
|
+
const normalizedHost = typeof options.hostname === "string" ? normalizeHost(options.hostname) : typeof options.host === "string" ? normalizeHost(options.host) : null;
|
|
135
|
+
if (normalizedHost) {
|
|
136
|
+
effectiveUrl.hostname = normalizedHost.hostname;
|
|
137
|
+
effectiveUrl.port = normalizedHost.port ?? "";
|
|
138
|
+
}
|
|
139
|
+
if (options.port != null) {
|
|
140
|
+
effectiveUrl.port = String(options.port);
|
|
141
|
+
}
|
|
142
|
+
if (typeof options.path === "string" && options.path.length > 0) {
|
|
143
|
+
const pathUrl = new URL(options.path, `${effectiveUrl.protocol}//${effectiveUrl.host}`);
|
|
144
|
+
effectiveUrl.pathname = pathUrl.pathname;
|
|
145
|
+
effectiveUrl.search = pathUrl.search;
|
|
146
|
+
effectiveUrl.hash = pathUrl.hash;
|
|
147
|
+
}
|
|
148
|
+
return effectiveUrl;
|
|
149
|
+
}
|
|
150
|
+
function normalizeHeaders(headers) {
|
|
151
|
+
if (!headers || Array.isArray(headers)) {
|
|
152
|
+
return headers;
|
|
153
|
+
}
|
|
154
|
+
const copy = {};
|
|
155
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
156
|
+
if (value === void 0) continue;
|
|
157
|
+
copy[key] = value;
|
|
158
|
+
}
|
|
159
|
+
delete copy["host"];
|
|
160
|
+
delete copy["Host"];
|
|
161
|
+
return copy;
|
|
162
|
+
}
|
|
163
|
+
var ROUTE_CONTROL_AUTHORIZATION_HEADER = "x-route-authorization";
|
|
164
|
+
var ROUTE_SERVICE_ORIGIN_HEADER = "x-route-service-origin";
|
|
165
|
+
var ROUTED_CONTROL_HEADERS = /* @__PURE__ */ new Set([
|
|
166
|
+
ROUTE_CONTROL_AUTHORIZATION_HEADER,
|
|
167
|
+
ROUTE_SERVICE_ORIGIN_HEADER,
|
|
168
|
+
"proxy-authorization"
|
|
169
|
+
]);
|
|
170
|
+
function isArchalInternalHeaderName(name) {
|
|
171
|
+
const normalized = name.toLowerCase();
|
|
172
|
+
return normalized === "x-archal" || normalized.startsWith("x-archal-");
|
|
173
|
+
}
|
|
174
|
+
function sanitizeRoutedHeaders(headers, service) {
|
|
175
|
+
const originalAuthorization = headers.get("authorization");
|
|
176
|
+
for (const header of [...headers.keys()]) {
|
|
177
|
+
const normalized = header.toLowerCase();
|
|
178
|
+
if (isArchalInternalHeaderName(normalized) || ROUTED_CONTROL_HEADERS.has(normalized)) {
|
|
179
|
+
headers.delete(header);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (service.forwardedAuthorizationHeader) {
|
|
183
|
+
headers.delete(service.forwardedAuthorizationHeader);
|
|
184
|
+
}
|
|
185
|
+
return originalAuthorization;
|
|
186
|
+
}
|
|
187
|
+
function applyRoutedHeaders(headers, service) {
|
|
188
|
+
const originalAuthorization = sanitizeRoutedHeaders(headers, service);
|
|
189
|
+
const routedRequestHeaders = typeof service.routedRequestHeaders === "function" ? service.routedRequestHeaders() : service.routedRequestHeaders;
|
|
190
|
+
for (const [name, value] of Object.entries(routedRequestHeaders ?? {})) {
|
|
191
|
+
headers.set(name, value);
|
|
192
|
+
}
|
|
193
|
+
if (!service.forwardedAuthorizationHeader) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (originalAuthorization) {
|
|
197
|
+
headers.set(service.forwardedAuthorizationHeader, originalAuthorization);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function buildPatchedHeaders(headers, service) {
|
|
201
|
+
const normalizedHeaders = new Headers(normalizeHeaders(headers));
|
|
202
|
+
applyRoutedHeaders(normalizedHeaders, service);
|
|
203
|
+
const patchedHeaders = {};
|
|
204
|
+
for (const [name, value] of normalizedHeaders.entries()) {
|
|
205
|
+
patchedHeaders[name] = value;
|
|
206
|
+
}
|
|
207
|
+
return patchedHeaders;
|
|
208
|
+
}
|
|
209
|
+
function parseRequestArgs(defaultProtocol, args) {
|
|
210
|
+
let callback;
|
|
211
|
+
for (let index = args.length - 1; index >= 0; index -= 1) {
|
|
212
|
+
const candidate = args[index];
|
|
213
|
+
if (typeof candidate === "function") {
|
|
214
|
+
callback = candidate;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
const first = args[0];
|
|
219
|
+
const second = args[1];
|
|
220
|
+
if (first instanceof URL || typeof first === "string") {
|
|
221
|
+
const baseRequestUrl = first instanceof URL ? new URL(first.toString()) : new URL(first, `${defaultProtocol}//localhost`);
|
|
222
|
+
const options = second && typeof second === "object" && !(second instanceof URL) ? { ...second } : {};
|
|
223
|
+
return {
|
|
224
|
+
requestUrl: applyRequestOptionsToUrl(baseRequestUrl, options, defaultProtocol),
|
|
225
|
+
options,
|
|
226
|
+
callback
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (first && typeof first === "object") {
|
|
230
|
+
const options = { ...first };
|
|
231
|
+
return {
|
|
232
|
+
requestUrl: buildUrlFromOptions(options, defaultProtocol),
|
|
233
|
+
options,
|
|
234
|
+
callback
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
requestUrl: new URL(`${defaultProtocol}//localhost/`),
|
|
239
|
+
options: {},
|
|
240
|
+
callback
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function buildPatchedArgs(rewrittenUrl, options, service, sourceProtocol, callback) {
|
|
244
|
+
const patchedOptions = {
|
|
245
|
+
...options,
|
|
246
|
+
protocol: rewrittenUrl.protocol,
|
|
247
|
+
hostname: rewrittenUrl.hostname,
|
|
248
|
+
port: rewrittenUrl.port ? Number(rewrittenUrl.port) : void 0,
|
|
249
|
+
path: `${rewrittenUrl.pathname}${rewrittenUrl.search}`,
|
|
250
|
+
headers: buildPatchedHeaders(options.headers, service)
|
|
251
|
+
};
|
|
252
|
+
delete patchedOptions.host;
|
|
253
|
+
if (sourceProtocol !== rewrittenUrl.protocol) {
|
|
254
|
+
delete patchedOptions.agent;
|
|
255
|
+
delete patchedOptions.createConnection;
|
|
256
|
+
}
|
|
257
|
+
if (callback) {
|
|
258
|
+
return [rewrittenUrl, patchedOptions, callback];
|
|
259
|
+
}
|
|
260
|
+
return [rewrittenUrl, patchedOptions];
|
|
261
|
+
}
|
|
262
|
+
function buildRoutedFetchRequest(request, targetUrl, service, init) {
|
|
263
|
+
const rewrittenRequest = new Request(targetUrl, request);
|
|
264
|
+
const headers = new Headers(rewrittenRequest.headers);
|
|
265
|
+
if (init?.headers) {
|
|
266
|
+
for (const [name, value] of new Headers(init.headers).entries()) {
|
|
267
|
+
headers.set(name, value);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
applyRoutedHeaders(headers, service);
|
|
271
|
+
return new Request(rewrittenRequest, {
|
|
272
|
+
...init,
|
|
273
|
+
headers
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ../route-runtime-core/src/service-profiles.ts
|
|
278
|
+
function exactDomain(value) {
|
|
279
|
+
return { kind: "exact", value: value.toLowerCase() };
|
|
280
|
+
}
|
|
281
|
+
function suffixDomain(value) {
|
|
282
|
+
return { kind: "suffix", value: value.toLowerCase() };
|
|
283
|
+
}
|
|
284
|
+
function matchesDomainPattern(hostname, pattern) {
|
|
285
|
+
const normalizedHostname = hostname.toLowerCase();
|
|
286
|
+
if (pattern.kind === "exact") {
|
|
287
|
+
return normalizedHostname === pattern.value;
|
|
288
|
+
}
|
|
289
|
+
return normalizedHostname === pattern.value || normalizedHostname.endsWith(`.${pattern.value}`);
|
|
290
|
+
}
|
|
291
|
+
function classifyHostname(profile, hostname) {
|
|
292
|
+
if (profile.routeDomains.some((pattern) => matchesDomainPattern(hostname, pattern))) {
|
|
293
|
+
return "route";
|
|
294
|
+
}
|
|
295
|
+
if (profile.hardFailDomains.some((pattern) => matchesDomainPattern(hostname, pattern))) {
|
|
296
|
+
return "hard-fail";
|
|
297
|
+
}
|
|
298
|
+
if (profile.warningDomains.some((pattern) => matchesDomainPattern(hostname, pattern))) {
|
|
299
|
+
return "warn";
|
|
300
|
+
}
|
|
301
|
+
return "ignore";
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ../route-runtime-core/src/manifests/discord.ts
|
|
305
|
+
var discordRouteManifest = {
|
|
306
|
+
service: "discord",
|
|
307
|
+
manifestVersion: "2026-04-12.discord.v1",
|
|
308
|
+
baselineSeed: "small-server",
|
|
309
|
+
upstreamBasePath: "/api/v10",
|
|
310
|
+
routeDomains: [
|
|
311
|
+
exactDomain("discord.com"),
|
|
312
|
+
exactDomain("discordapp.com")
|
|
313
|
+
],
|
|
314
|
+
// Discord's CDN and gateway surfaces are outside the approved Vitest route
|
|
315
|
+
// contract. Leaving them warning-only would create obvious exfil and
|
|
316
|
+
// network-escape channels once a test declares Discord route mode.
|
|
317
|
+
hardFailDomains: [
|
|
318
|
+
exactDomain("cdn.discordapp.com"),
|
|
319
|
+
exactDomain("media.discordapp.net"),
|
|
320
|
+
exactDomain("gateway.discord.gg")
|
|
321
|
+
],
|
|
322
|
+
warningDomains: [
|
|
323
|
+
suffixDomain("discord.com"),
|
|
324
|
+
suffixDomain("discordapp.com"),
|
|
325
|
+
exactDomain("discord.gg")
|
|
326
|
+
],
|
|
327
|
+
diagnostics: {
|
|
328
|
+
undeclared: ({ url }) => `Blocked Discord traffic to ${url}. Declare discord in archalVitestProject({ services: { discord: { mode: "route" } } }) before calling the Discord REST API in Vitest.`,
|
|
329
|
+
declaredEscape: ({ url }) => `Blocked Discord escape to ${url}. Discord route mode is configured, but this domain is outside the approved Discord REST route surface.`,
|
|
330
|
+
warning: ({ url }) => `Observed adjacent Discord domain ${url}. Archal did not reroute this request because it is outside the approved Discord REST route surface.`
|
|
331
|
+
},
|
|
332
|
+
sdkIdentifiers: ["discord.js", "@discordjs/rest"],
|
|
333
|
+
conformanceSuiteId: "discord-hosted-route"
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// ../route-runtime-core/src/manifests/datadog.ts
|
|
337
|
+
var datadogRouteManifest = {
|
|
338
|
+
service: "datadog",
|
|
339
|
+
manifestVersion: "2026-06-07.datadog.v1",
|
|
340
|
+
baselineSeed: "empty",
|
|
341
|
+
routeDomains: [exactDomain("api.datadoghq.com")],
|
|
342
|
+
hardFailDomains: [],
|
|
343
|
+
warningDomains: [suffixDomain("datadoghq.com")],
|
|
344
|
+
diagnostics: {
|
|
345
|
+
undeclared: ({ url }) => `Blocked Datadog traffic to ${url}. Declare datadog before calling the Datadog API.`,
|
|
346
|
+
declaredEscape: ({ url }) => `Blocked Datadog escape to ${url}. Datadog route mode is configured, but this domain is outside the routed Datadog API surface.`,
|
|
347
|
+
warning: ({ url }) => `Observed adjacent Datadog domain ${url}. Archal did not reroute this request because it is outside the routed Datadog API surface.`
|
|
348
|
+
},
|
|
349
|
+
sdkIdentifiers: ["@datadog/datadog-api-client", "datadog-api-client"],
|
|
350
|
+
conformanceSuiteId: "datadog-hosted-route"
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
// ../route-runtime-core/src/manifests/github.ts
|
|
354
|
+
function rewriteGitHubRawContentPath(sourceUrl) {
|
|
355
|
+
if (sourceUrl.hostname.toLowerCase() !== "raw.githubusercontent.com") {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const [owner, repo, branch, ...pathSegments] = sourceUrl.pathname.split("/").filter(Boolean);
|
|
359
|
+
if (!owner || !repo || !branch || pathSegments.length === 0) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
const searchParams = new URLSearchParams(sourceUrl.search);
|
|
363
|
+
searchParams.set("ref", decodeURIComponent(branch));
|
|
364
|
+
return {
|
|
365
|
+
pathname: `/raw/${owner}/${repo}/${branch}/${pathSegments.join("/")}`,
|
|
366
|
+
search: `?${searchParams.toString()}`
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
var githubRouteManifest = {
|
|
370
|
+
service: "github",
|
|
371
|
+
manifestVersion: "2026-04-02.github.v1",
|
|
372
|
+
baselineSeed: "small-project",
|
|
373
|
+
routeDomains: [
|
|
374
|
+
exactDomain("api.github.com"),
|
|
375
|
+
exactDomain("uploads.github.com"),
|
|
376
|
+
exactDomain("github.com"),
|
|
377
|
+
exactDomain("raw.githubusercontent.com")
|
|
378
|
+
],
|
|
379
|
+
// Block adjacent GitHub-owned domains that tests don't need. Keep raw
|
|
380
|
+
// content routed because GitHub's contents API returns download_url values
|
|
381
|
+
// on raw.githubusercontent.com, and normal clients often follow them.
|
|
382
|
+
hardFailDomains: [
|
|
383
|
+
exactDomain("gist.github.com"),
|
|
384
|
+
exactDomain("objects.githubusercontent.com"),
|
|
385
|
+
exactDomain("codeload.github.com")
|
|
386
|
+
],
|
|
387
|
+
routePathRewrite: rewriteGitHubRawContentPath,
|
|
388
|
+
warningDomains: [
|
|
389
|
+
suffixDomain("github.com"),
|
|
390
|
+
suffixDomain("githubusercontent.com")
|
|
391
|
+
],
|
|
392
|
+
diagnostics: {
|
|
393
|
+
undeclared: ({ url }) => `Blocked GitHub traffic to ${url}. Declare github in archalVitestProject({ services: { github: { mode: "route" } } }) before using the official GitHub SDK.`,
|
|
394
|
+
declaredEscape: ({ url }) => `Blocked GitHub escape to ${url}. GitHub route mode is configured, but this domain is outside the primary routed GitHub surface.`,
|
|
395
|
+
warning: ({ url }) => `Observed adjacent GitHub domain ${url}. Archal did not reroute this request because it is outside the primary GitHub API route surface.`
|
|
396
|
+
},
|
|
397
|
+
sdkIdentifiers: ["@octokit/rest"],
|
|
398
|
+
conformanceSuiteId: "github-hosted-route"
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
// ../route-runtime-core/src/manifests/gitlab.ts
|
|
402
|
+
var gitlabRouteManifest = {
|
|
403
|
+
service: "gitlab",
|
|
404
|
+
manifestVersion: "2026-06-08.gitlab.v1",
|
|
405
|
+
baselineSeed: "empty",
|
|
406
|
+
routeDomains: [
|
|
407
|
+
// GitLab SaaS serves its REST API from the gitlab.com apex (/api/v4).
|
|
408
|
+
exactDomain("gitlab.com")
|
|
409
|
+
],
|
|
410
|
+
hardFailDomains: [],
|
|
411
|
+
warningDomains: [
|
|
412
|
+
// docs.gitlab.com / about.gitlab.com are marketing/docs hosts, not the API
|
|
413
|
+
// surface — warn rather than reroute so a stray call stays observable.
|
|
414
|
+
suffixDomain("gitlab.com")
|
|
415
|
+
],
|
|
416
|
+
diagnostics: {
|
|
417
|
+
undeclared: ({ url }) => `Blocked GitLab traffic to ${url}. Declare gitlab in archalVitestProject({ services: { gitlab: { mode: "route" } } }) before calling the GitLab API.`,
|
|
418
|
+
declaredEscape: ({ url }) => `Blocked GitLab escape to ${url}. GitLab route mode is configured, but this domain is outside the routed GitLab API surface.`,
|
|
419
|
+
warning: ({ url }) => `Observed adjacent GitLab domain ${url}. Archal did not reroute this request because it is outside the routed GitLab API surface.`
|
|
420
|
+
},
|
|
421
|
+
sdkIdentifiers: ["@gitbeaker/rest"],
|
|
422
|
+
conformanceSuiteId: "gitlab-hosted-route"
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// ../route-runtime-core/src/manifests/google-workspace.ts
|
|
426
|
+
var googleWorkspaceRouteManifest = {
|
|
427
|
+
service: "google-workspace",
|
|
428
|
+
manifestVersion: "2026-04-01.google-workspace.v1",
|
|
429
|
+
baselineSeed: "assistant-baseline",
|
|
430
|
+
routeDomains: [
|
|
431
|
+
exactDomain("gmail.googleapis.com"),
|
|
432
|
+
exactDomain("drive.googleapis.com"),
|
|
433
|
+
exactDomain("calendar.googleapis.com"),
|
|
434
|
+
exactDomain("people.googleapis.com"),
|
|
435
|
+
exactDomain("sheets.googleapis.com"),
|
|
436
|
+
exactDomain("oauth2.googleapis.com"),
|
|
437
|
+
exactDomain("www.googleapis.com")
|
|
438
|
+
],
|
|
439
|
+
hardFailDomains: [],
|
|
440
|
+
warningDomains: [
|
|
441
|
+
suffixDomain("google.com"),
|
|
442
|
+
suffixDomain("googleusercontent.com")
|
|
443
|
+
],
|
|
444
|
+
diagnostics: {
|
|
445
|
+
undeclared: ({ url }) => `Blocked Google Workspace traffic to ${url}. Declare google-workspace in archalVitestProject({ services: { 'google-workspace': { mode: "route" } } }) before using the official Google client.`,
|
|
446
|
+
declaredEscape: ({ url }) => `Blocked Google Workspace escape to ${url}. Google Workspace route mode is configured, but this domain is outside the primary routed surface.`,
|
|
447
|
+
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.`
|
|
448
|
+
},
|
|
449
|
+
sdkIdentifiers: ["googleapis"],
|
|
450
|
+
conformanceSuiteId: "google-workspace-hosted-route"
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// ../route-runtime-core/src/manifests/apify.ts
|
|
454
|
+
var apifyRouteManifest = {
|
|
455
|
+
service: "apify",
|
|
456
|
+
manifestVersion: "2026-05-14.apify.v1",
|
|
457
|
+
baselineSeed: "empty",
|
|
458
|
+
routeDomains: [
|
|
459
|
+
exactDomain("api.apify.com"),
|
|
460
|
+
exactDomain("apify.com")
|
|
461
|
+
],
|
|
462
|
+
hardFailDomains: [],
|
|
463
|
+
warningDomains: [
|
|
464
|
+
suffixDomain("apify.com")
|
|
465
|
+
],
|
|
466
|
+
diagnostics: {
|
|
467
|
+
undeclared: ({ url }) => `Blocked Apify traffic to ${url}. Declare apify before using the official Apify client.`,
|
|
468
|
+
declaredEscape: ({ url }) => `Blocked Apify escape to ${url}. Apify route mode is configured, but this domain is outside the primary routed Apify API surface.`,
|
|
469
|
+
warning: ({ url }) => `Observed adjacent Apify domain ${url}. Archal did not reroute this request because it is outside the primary Apify API route surface.`
|
|
470
|
+
},
|
|
471
|
+
sdkIdentifiers: ["apify-client", "apify"],
|
|
472
|
+
conformanceSuiteId: "apify-hosted-route"
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
// ../route-runtime-core/src/manifests/calcom.ts
|
|
476
|
+
var calcomRouteManifest = {
|
|
477
|
+
service: "calcom",
|
|
478
|
+
manifestVersion: "2026-06-08.calcom.v1",
|
|
479
|
+
baselineSeed: "empty",
|
|
480
|
+
routeDomains: [
|
|
481
|
+
exactDomain("api.cal.com")
|
|
482
|
+
],
|
|
483
|
+
hardFailDomains: [],
|
|
484
|
+
warningDomains: [
|
|
485
|
+
// The bare cal.com is the marketing/app host, not the API surface — warn
|
|
486
|
+
// rather than reroute so a stray call is observable but not silently served.
|
|
487
|
+
suffixDomain("cal.com")
|
|
488
|
+
],
|
|
489
|
+
diagnostics: {
|
|
490
|
+
undeclared: ({ url }) => `Blocked Cal.com traffic to ${url}. Declare calcom in archalVitestProject({ services: { calcom: { mode: "route" } } }) before calling the Cal.com API.`,
|
|
491
|
+
declaredEscape: ({ url }) => `Blocked Cal.com escape to ${url}. Cal.com route mode is configured, but this domain is outside the primary routed Cal.com API surface.`,
|
|
492
|
+
warning: ({ url }) => `Observed adjacent Cal.com domain ${url}. Archal did not reroute this request because it is outside the primary Cal.com API route surface.`
|
|
493
|
+
},
|
|
494
|
+
sdkIdentifiers: ["@calcom/api"],
|
|
495
|
+
conformanceSuiteId: "calcom-hosted-route"
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// ../route-runtime-core/src/manifests/clickup.ts
|
|
499
|
+
var clickupRouteManifest = {
|
|
500
|
+
service: "clickup",
|
|
501
|
+
manifestVersion: "2026-06-08.clickup.v1",
|
|
502
|
+
baselineSeed: "empty",
|
|
503
|
+
routeDomains: [
|
|
504
|
+
exactDomain("api.clickup.com")
|
|
505
|
+
],
|
|
506
|
+
hardFailDomains: [],
|
|
507
|
+
warningDomains: [
|
|
508
|
+
// app.clickup.com is the web app host, not the API surface — warn rather
|
|
509
|
+
// than reroute so a stray call is observable but not silently served.
|
|
510
|
+
suffixDomain("clickup.com")
|
|
511
|
+
],
|
|
512
|
+
diagnostics: {
|
|
513
|
+
undeclared: ({ url }) => `Blocked ClickUp traffic to ${url}. Declare clickup in archalVitestProject({ services: { clickup: { mode: "route" } } }) before calling the ClickUp API.`,
|
|
514
|
+
declaredEscape: ({ url }) => `Blocked ClickUp escape to ${url}. ClickUp route mode is configured, but this domain is outside the routed ClickUp API surface.`,
|
|
515
|
+
warning: ({ url }) => `Observed adjacent ClickUp domain ${url}. Archal did not reroute this request because it is outside the routed ClickUp API surface.`
|
|
516
|
+
},
|
|
517
|
+
sdkIdentifiers: ["clickup"],
|
|
518
|
+
conformanceSuiteId: "clickup-hosted-route"
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// ../route-runtime-core/src/manifests/customerio.ts
|
|
522
|
+
var customerioRouteManifest = {
|
|
523
|
+
service: "customerio",
|
|
524
|
+
manifestVersion: "2026-06-08.customerio.v1",
|
|
525
|
+
baselineSeed: "empty",
|
|
526
|
+
routeDomains: [
|
|
527
|
+
exactDomain("api.customer.io")
|
|
528
|
+
],
|
|
529
|
+
hardFailDomains: [],
|
|
530
|
+
warningDomains: [
|
|
531
|
+
// The bare customer.io is the marketing site and track.customer.io is the
|
|
532
|
+
// separate CDP/track ingest surface — neither is the App API this clone
|
|
533
|
+
// models. Warn rather than reroute so a stray call is observable.
|
|
534
|
+
suffixDomain("customer.io")
|
|
535
|
+
],
|
|
536
|
+
diagnostics: {
|
|
537
|
+
undeclared: ({ url }) => `Blocked Customer.io traffic to ${url}. Declare customerio before calling the Customer.io API.`,
|
|
538
|
+
declaredEscape: ({ url }) => `Blocked Customer.io escape to ${url}. Customer.io route mode is configured, but this domain is outside the routed Customer.io App API surface.`,
|
|
539
|
+
warning: ({ url }) => `Observed adjacent Customer.io domain ${url}. Archal did not reroute this request because it is outside the routed Customer.io App API surface.`
|
|
540
|
+
},
|
|
541
|
+
sdkIdentifiers: ["customerio-node"],
|
|
542
|
+
conformanceSuiteId: "customerio-hosted-route"
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
// ../route-runtime-core/src/manifests/hubspot.ts
|
|
546
|
+
var hubspotRouteManifest = {
|
|
547
|
+
service: "hubspot",
|
|
548
|
+
manifestVersion: "2026-06-10.hubspot.v1",
|
|
549
|
+
baselineSeed: "empty",
|
|
550
|
+
routeDomains: [
|
|
551
|
+
exactDomain("api.hubapi.com")
|
|
552
|
+
],
|
|
553
|
+
hardFailDomains: [],
|
|
554
|
+
warningDomains: [
|
|
555
|
+
// Non-API hubapi.com hosts (forms.hubapi.com, track ingest) and the
|
|
556
|
+
// hubspot.com web UI (app.hubspot.com — twin payload URLs point at
|
|
557
|
+
// app-na2.hubspot.com) are adjacent surfaces, not the CRM API this clone
|
|
558
|
+
// models. Warn rather than reroute so a stray call is observable.
|
|
559
|
+
suffixDomain("hubapi.com"),
|
|
560
|
+
suffixDomain("hubspot.com")
|
|
561
|
+
],
|
|
562
|
+
diagnostics: {
|
|
563
|
+
undeclared: ({ url }) => `Blocked HubSpot traffic to ${url}. Declare hubspot before calling the HubSpot API.`,
|
|
564
|
+
declaredEscape: ({ url }) => `Blocked HubSpot escape to ${url}. HubSpot route mode is configured, but this domain is outside the routed HubSpot CRM API surface.`,
|
|
565
|
+
warning: ({ url }) => `Observed adjacent HubSpot domain ${url}. Archal did not reroute this request because it is outside the routed HubSpot CRM API surface.`
|
|
566
|
+
},
|
|
567
|
+
sdkIdentifiers: ["@hubspot/api-client"],
|
|
568
|
+
conformanceSuiteId: "hubspot-hosted-route"
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
// ../route-runtime-core/src/manifests/jira.ts
|
|
572
|
+
var jiraRouteManifest = {
|
|
573
|
+
service: "jira",
|
|
574
|
+
manifestVersion: "2026-04-01.jira.v1",
|
|
575
|
+
baselineSeed: "small-project",
|
|
576
|
+
upstreamBasePathAliases: ["/ex/jira/:cloudId"],
|
|
577
|
+
routeDomains: [
|
|
578
|
+
suffixDomain("atlassian.net"),
|
|
579
|
+
exactDomain("api.atlassian.com"),
|
|
580
|
+
exactDomain("jira.atlassian.com")
|
|
581
|
+
],
|
|
582
|
+
hardFailDomains: [],
|
|
583
|
+
warningDomains: [
|
|
584
|
+
suffixDomain("atlassian.com"),
|
|
585
|
+
exactDomain("jira.com")
|
|
586
|
+
],
|
|
587
|
+
diagnostics: {
|
|
588
|
+
undeclared: ({ url }) => `Blocked Jira traffic to ${url}. Declare jira in archalVitestProject({ services: { jira: { mode: "route" } } }) before using the official Jira SDK.`,
|
|
589
|
+
declaredEscape: ({ url }) => `Blocked Jira escape to ${url}. Jira route mode is configured, but this domain is outside the primary routed Jira surface.`,
|
|
590
|
+
warning: ({ url }) => `Observed adjacent Jira domain ${url}. Archal did not reroute this request because it is outside the primary Jira route surface.`
|
|
591
|
+
},
|
|
592
|
+
sdkIdentifiers: ["jira.js"],
|
|
593
|
+
conformanceSuiteId: "jira-hosted-route"
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// ../route-runtime-core/src/manifests/linear.ts
|
|
597
|
+
var linearRouteManifest = {
|
|
598
|
+
service: "linear",
|
|
599
|
+
manifestVersion: "2026-04-19.linear.v1",
|
|
600
|
+
baselineSeed: "small-team",
|
|
601
|
+
routeDomains: [
|
|
602
|
+
exactDomain("api.linear.app")
|
|
603
|
+
],
|
|
604
|
+
hardFailDomains: [],
|
|
605
|
+
warningDomains: [
|
|
606
|
+
suffixDomain("linear.app")
|
|
607
|
+
],
|
|
608
|
+
diagnostics: {
|
|
609
|
+
undeclared: ({ url }) => `Blocked Linear traffic to ${url}. Declare linear in archalVitestProject({ services: { linear: { mode: "route" } } }) before using the official Linear SDK.`,
|
|
610
|
+
declaredEscape: ({ url }) => `Blocked Linear escape to ${url}. Linear route mode is configured, but this domain is outside the primary routed Linear API surface.`,
|
|
611
|
+
warning: ({ url }) => `Observed adjacent Linear domain ${url}. Archal did not reroute this request because it is outside the primary Linear API route surface.`
|
|
612
|
+
},
|
|
613
|
+
sdkIdentifiers: ["@linear/sdk"],
|
|
614
|
+
conformanceSuiteId: "linear-hosted-route"
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// ../route-runtime-core/src/manifests/ownerrez.ts
|
|
618
|
+
var ownerrezRouteManifest = {
|
|
619
|
+
service: "ownerrez",
|
|
620
|
+
manifestVersion: "2026-06-07.ownerrez.v1",
|
|
621
|
+
baselineSeed: "empty",
|
|
622
|
+
routeDomains: [
|
|
623
|
+
exactDomain("api.ownerrez.com"),
|
|
624
|
+
exactDomain("ownerrez.com")
|
|
625
|
+
],
|
|
626
|
+
hardFailDomains: [],
|
|
627
|
+
warningDomains: [
|
|
628
|
+
suffixDomain("ownerrez.com")
|
|
629
|
+
],
|
|
630
|
+
diagnostics: {
|
|
631
|
+
undeclared: ({ url }) => `Blocked OwnerRez traffic to ${url}. Declare ownerrez before calling the OwnerRez API.`,
|
|
632
|
+
declaredEscape: ({ url }) => `Blocked OwnerRez escape to ${url}. OwnerRez route mode is configured, but this domain is outside the routed OwnerRez API surface.`,
|
|
633
|
+
warning: ({ url }) => `Observed adjacent OwnerRez domain ${url}. Archal did not reroute this request because it is outside the routed OwnerRez API surface.`
|
|
634
|
+
},
|
|
635
|
+
sdkIdentifiers: ["ownerrez"],
|
|
636
|
+
conformanceSuiteId: "ownerrez-hosted-route"
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
// ../route-runtime-core/src/manifests/pricelabs.ts
|
|
640
|
+
var pricelabsRouteManifest = {
|
|
641
|
+
service: "pricelabs",
|
|
642
|
+
manifestVersion: "2026-06-07.pricelabs.v1",
|
|
643
|
+
baselineSeed: "empty",
|
|
644
|
+
routeDomains: [
|
|
645
|
+
exactDomain("api.pricelabs.co"),
|
|
646
|
+
exactDomain("pricelabs.co")
|
|
647
|
+
],
|
|
648
|
+
hardFailDomains: [],
|
|
649
|
+
warningDomains: [
|
|
650
|
+
suffixDomain("pricelabs.co")
|
|
651
|
+
],
|
|
652
|
+
diagnostics: {
|
|
653
|
+
undeclared: ({ url }) => `Blocked PriceLabs traffic to ${url}. Declare pricelabs before calling the PriceLabs API.`,
|
|
654
|
+
declaredEscape: ({ url }) => `Blocked PriceLabs escape to ${url}. PriceLabs route mode is configured, but this domain is outside the routed PriceLabs API surface.`,
|
|
655
|
+
warning: ({ url }) => `Observed adjacent PriceLabs domain ${url}. Archal did not reroute this request because it is outside the routed PriceLabs API surface.`
|
|
656
|
+
},
|
|
657
|
+
sdkIdentifiers: ["pricelabs"],
|
|
658
|
+
conformanceSuiteId: "pricelabs-hosted-route"
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// ../route-runtime-core/src/manifests/ramp.ts
|
|
662
|
+
var rampRouteManifest = {
|
|
663
|
+
service: "ramp",
|
|
664
|
+
manifestVersion: "2026-04-19.ramp.v1",
|
|
665
|
+
baselineSeed: "default",
|
|
666
|
+
routeDomains: [
|
|
667
|
+
exactDomain("api.ramp.com"),
|
|
668
|
+
exactDomain("app.ramp.com")
|
|
669
|
+
],
|
|
670
|
+
hardFailDomains: [],
|
|
671
|
+
warningDomains: [
|
|
672
|
+
suffixDomain("ramp.com")
|
|
673
|
+
],
|
|
674
|
+
diagnostics: {
|
|
675
|
+
undeclared: ({ url }) => `Blocked Ramp traffic to ${url}. Declare ramp in archalVitestProject({ services: { ramp: { mode: "route" } } }) before using the official Ramp client.`,
|
|
676
|
+
declaredEscape: ({ url }) => `Blocked Ramp escape to ${url}. Ramp route mode is configured, but this domain is outside the primary routed Ramp API surface.`,
|
|
677
|
+
warning: ({ url }) => `Observed adjacent Ramp domain ${url}. Archal did not reroute this request because it is outside the primary Ramp API route surface.`
|
|
678
|
+
},
|
|
679
|
+
sdkIdentifiers: ["ramp"],
|
|
680
|
+
conformanceSuiteId: "ramp-hosted-route"
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// ../route-runtime-core/src/manifests/sendgrid.ts
|
|
684
|
+
var sendgridRouteManifest = {
|
|
685
|
+
service: "sendgrid",
|
|
686
|
+
manifestVersion: "2026-06-08.sendgrid.v1",
|
|
687
|
+
baselineSeed: "empty",
|
|
688
|
+
routeDomains: [
|
|
689
|
+
exactDomain("api.sendgrid.com")
|
|
690
|
+
],
|
|
691
|
+
hardFailDomains: [],
|
|
692
|
+
warningDomains: [
|
|
693
|
+
// The bare sendgrid.com is the marketing site, not the API surface — warn
|
|
694
|
+
// rather than reroute so a stray call is observable but not silently served.
|
|
695
|
+
suffixDomain("sendgrid.com")
|
|
696
|
+
],
|
|
697
|
+
diagnostics: {
|
|
698
|
+
undeclared: ({ url }) => `Blocked SendGrid traffic to ${url}. Declare sendgrid before calling the SendGrid API.`,
|
|
699
|
+
declaredEscape: ({ url }) => `Blocked SendGrid escape to ${url}. SendGrid route mode is configured, but this domain is outside the routed SendGrid API surface.`,
|
|
700
|
+
warning: ({ url }) => `Observed adjacent SendGrid domain ${url}. Archal did not reroute this request because it is outside the routed SendGrid API surface.`
|
|
701
|
+
},
|
|
702
|
+
sdkIdentifiers: ["sendgrid", "@sendgrid/mail", "@sendgrid/client"],
|
|
703
|
+
conformanceSuiteId: "sendgrid-hosted-route"
|
|
704
|
+
};
|
|
705
|
+
|
|
706
|
+
// ../route-runtime-core/src/manifests/sentry.ts
|
|
707
|
+
var sentryRouteManifest = {
|
|
708
|
+
service: "sentry",
|
|
709
|
+
manifestVersion: "2026-06-08.sentry.v1",
|
|
710
|
+
baselineSeed: "empty",
|
|
711
|
+
routeDomains: [
|
|
712
|
+
exactDomain("sentry.io")
|
|
713
|
+
],
|
|
714
|
+
hardFailDomains: [],
|
|
715
|
+
warningDomains: [
|
|
716
|
+
// *.sentry.io subdomains (docs/blog/app/regional) sit adjacent to the routed
|
|
717
|
+
// sentry.io API apex — warn rather than reroute. The exact apex above wins
|
|
718
|
+
// for sentry.io itself, so only subdomains fall through to this warning.
|
|
719
|
+
suffixDomain("sentry.io")
|
|
720
|
+
],
|
|
721
|
+
diagnostics: {
|
|
722
|
+
undeclared: ({ url }) => `Blocked Sentry traffic to ${url}. Declare sentry in archalVitestProject({ services: { sentry: { mode: "route" } } }) before calling the Sentry API.`,
|
|
723
|
+
declaredEscape: ({ url }) => `Blocked Sentry escape to ${url}. Sentry route mode is configured, but this domain is outside the primary routed Sentry API surface.`,
|
|
724
|
+
warning: ({ url }) => `Observed adjacent Sentry domain ${url}. Archal did not reroute this request because it is outside the primary Sentry API route surface.`
|
|
725
|
+
},
|
|
726
|
+
sdkIdentifiers: ["@sentry/node", "@sentry/browser", "@sentry/core"],
|
|
727
|
+
conformanceSuiteId: "sentry-hosted-route"
|
|
728
|
+
};
|
|
729
|
+
|
|
730
|
+
// ../route-runtime-core/src/manifests/slack.ts
|
|
731
|
+
var slackRouteManifest = {
|
|
732
|
+
service: "slack",
|
|
733
|
+
manifestVersion: "2026-04-02.slack.v1",
|
|
734
|
+
baselineSeed: "engineering-team",
|
|
735
|
+
routeDomains: [
|
|
736
|
+
exactDomain("slack.com"),
|
|
737
|
+
exactDomain("api.slack.com")
|
|
738
|
+
],
|
|
739
|
+
upstreamBasePath: "/api",
|
|
740
|
+
// Block adjacent Slack-owned domains that tests don't need and that can be
|
|
741
|
+
// abused as data-exfiltration channels. hooks.slack.com and files.slack.com
|
|
742
|
+
// in particular are ideal POST targets for a malicious dependency trying to
|
|
743
|
+
// smuggle stolen tokens out through a domain the developer has already
|
|
744
|
+
// implicitly allow-listed.
|
|
745
|
+
hardFailDomains: [
|
|
746
|
+
exactDomain("hooks.slack.com"),
|
|
747
|
+
exactDomain("files.slack.com"),
|
|
748
|
+
exactDomain("edgeapi.slack.com"),
|
|
749
|
+
exactDomain("wss-primary.slack.com"),
|
|
750
|
+
exactDomain("wss-backup.slack.com")
|
|
751
|
+
],
|
|
752
|
+
warningDomains: [
|
|
753
|
+
suffixDomain("slack.com")
|
|
754
|
+
],
|
|
755
|
+
diagnostics: {
|
|
756
|
+
undeclared: ({ url }) => `Blocked Slack traffic to ${url}. Declare slack in archalVitestProject({ services: { slack: { mode: "route" } } }) before using the official Slack SDK.`,
|
|
757
|
+
declaredEscape: ({ url }) => `Blocked Slack escape to ${url}. Slack route mode is configured, but this domain is outside the primary routed Slack surface.`,
|
|
758
|
+
warning: ({ url }) => `Observed adjacent Slack domain ${url}. Archal did not reroute this request because it is outside the primary Slack API route surface.`
|
|
759
|
+
},
|
|
760
|
+
sdkIdentifiers: ["@slack/web-api"],
|
|
761
|
+
conformanceSuiteId: "slack-hosted-route"
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
// ../route-runtime-core/src/manifests/stripe.ts
|
|
765
|
+
var stripeRuntimeResourcePolicy = [
|
|
766
|
+
{
|
|
767
|
+
resource: "customers",
|
|
768
|
+
commands: ["customers.create", "customers.update"],
|
|
769
|
+
events: ["customer.created", "customer.updated"]
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
resource: "payment_intents",
|
|
773
|
+
commands: ["paymentIntents.create", "paymentIntents.confirm"],
|
|
774
|
+
events: ["payment_intent.succeeded"]
|
|
775
|
+
},
|
|
776
|
+
{ resource: "charges", events: ["charge.refunded"] },
|
|
777
|
+
{ resource: "refunds", commands: ["refunds.create"] },
|
|
778
|
+
{ resource: "products" },
|
|
779
|
+
{ resource: "prices" },
|
|
780
|
+
{
|
|
781
|
+
resource: "subscriptions",
|
|
782
|
+
commands: ["subscriptions.create"],
|
|
783
|
+
events: ["customer.subscription.created", "customer.subscription.updated"]
|
|
784
|
+
},
|
|
785
|
+
{
|
|
786
|
+
resource: "invoices",
|
|
787
|
+
commands: ["invoices.create", "invoices.finalizeInvoice"],
|
|
788
|
+
events: ["invoice.finalized"]
|
|
789
|
+
},
|
|
790
|
+
{ resource: "webhook_endpoints", commands: ["webhookEndpoints.create"] },
|
|
791
|
+
{ resource: "events" }
|
|
792
|
+
];
|
|
793
|
+
function deriveStripeRuntimeSurface(policy) {
|
|
794
|
+
return {
|
|
795
|
+
resources: policy.map((entry) => entry.resource),
|
|
796
|
+
commands: policy.flatMap((entry) => entry.commands ?? []),
|
|
797
|
+
events: policy.flatMap((entry) => entry.events ?? [])
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
var stripeRuntimeSurface = deriveStripeRuntimeSurface(stripeRuntimeResourcePolicy);
|
|
801
|
+
var stripeRouteManifest = {
|
|
802
|
+
service: "stripe",
|
|
803
|
+
manifestVersion: "2026-04-01.stripe.v1",
|
|
804
|
+
baselineSeed: "small-business",
|
|
805
|
+
routeDomains: [
|
|
806
|
+
exactDomain("api.stripe.com")
|
|
807
|
+
],
|
|
808
|
+
hardFailDomains: [
|
|
809
|
+
exactDomain("files.stripe.com"),
|
|
810
|
+
exactDomain("uploads.stripe.com")
|
|
811
|
+
],
|
|
812
|
+
warningDomains: [
|
|
813
|
+
exactDomain("billing.stripe.com"),
|
|
814
|
+
exactDomain("buy.stripe.com"),
|
|
815
|
+
exactDomain("checkout.stripe.com"),
|
|
816
|
+
exactDomain("connect.stripe.com"),
|
|
817
|
+
exactDomain("dashboard.stripe.com"),
|
|
818
|
+
exactDomain("invoice.stripe.com"),
|
|
819
|
+
exactDomain("pay.stripe.com"),
|
|
820
|
+
exactDomain("verify.stripe.com"),
|
|
821
|
+
suffixDomain("stripe.com")
|
|
822
|
+
],
|
|
823
|
+
diagnostics: {
|
|
824
|
+
undeclared: ({ url }) => `Blocked Stripe traffic to ${url}. Declare stripe in archalVitestProject({ services: { stripe: { mode: "route" } } }) before using the official Stripe SDK.`,
|
|
825
|
+
declaredEscape: ({ url }) => `Blocked Stripe escape to ${url}. Stripe route mode is configured, but this Stripe-owned domain is outside the current routed surface.`,
|
|
826
|
+
warning: ({ url }) => `Observed adjacent Stripe domain ${url}. Archal did not reroute this request because it is outside the primary Stripe API route surface.`
|
|
827
|
+
},
|
|
828
|
+
runtimeSurface: stripeRuntimeSurface,
|
|
829
|
+
sdkIdentifiers: ["stripe"],
|
|
830
|
+
conformanceSuiteId: "stripe-hosted-route"
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
// ../route-runtime-core/src/manifests/supabase.ts
|
|
834
|
+
var supabaseRouteManifest = {
|
|
835
|
+
service: "supabase",
|
|
836
|
+
manifestVersion: "2026-04-01.supabase.v1",
|
|
837
|
+
baselineSeed: "small-project",
|
|
838
|
+
routeDomains: [
|
|
839
|
+
suffixDomain("supabase.co"),
|
|
840
|
+
exactDomain("api.supabase.com")
|
|
841
|
+
],
|
|
842
|
+
hardFailDomains: [],
|
|
843
|
+
warningDomains: [
|
|
844
|
+
suffixDomain("supabase.com")
|
|
845
|
+
],
|
|
846
|
+
diagnostics: {
|
|
847
|
+
undeclared: ({ url }) => `Blocked Supabase traffic to ${url}. Declare supabase in archalVitestProject({ services: { supabase: { mode: "route" } } }) before using the official Supabase SDK.`,
|
|
848
|
+
declaredEscape: ({ url }) => `Blocked Supabase escape to ${url}. Supabase route mode is configured, but this domain is outside the primary routed Supabase surface.`,
|
|
849
|
+
warning: ({ url }) => `Observed adjacent Supabase domain ${url}. Archal did not reroute this request because it is outside the primary Supabase route surface.`
|
|
850
|
+
},
|
|
851
|
+
sdkIdentifiers: ["@supabase/supabase-js"],
|
|
852
|
+
conformanceSuiteId: "supabase-hosted-route"
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// ../route-runtime-core/src/manifests/tavily.ts
|
|
856
|
+
var tavilyRouteManifest = {
|
|
857
|
+
service: "tavily",
|
|
858
|
+
manifestVersion: "2026-05-14.tavily.v1",
|
|
859
|
+
baselineSeed: "empty",
|
|
860
|
+
routeDomains: [
|
|
861
|
+
exactDomain("api.tavily.com"),
|
|
862
|
+
exactDomain("tavily.com")
|
|
863
|
+
],
|
|
864
|
+
hardFailDomains: [],
|
|
865
|
+
warningDomains: [
|
|
866
|
+
suffixDomain("tavily.com")
|
|
867
|
+
],
|
|
868
|
+
diagnostics: {
|
|
869
|
+
undeclared: ({ url }) => `Blocked Tavily traffic to ${url}. Declare tavily before using the official Tavily client.`,
|
|
870
|
+
declaredEscape: ({ url }) => `Blocked Tavily escape to ${url}. Tavily route mode is configured, but this domain is outside the primary routed Tavily API surface.`,
|
|
871
|
+
warning: ({ url }) => `Observed adjacent Tavily domain ${url}. Archal did not reroute this request because it is outside the primary Tavily API route surface.`
|
|
872
|
+
},
|
|
873
|
+
sdkIdentifiers: ["@tavily/core", "tavily"],
|
|
874
|
+
conformanceSuiteId: "tavily-hosted-route"
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
// ../route-runtime-core/src/manifests/unipile.ts
|
|
878
|
+
var unipileRouteManifest = {
|
|
879
|
+
service: "unipile",
|
|
880
|
+
manifestVersion: "2026-06-05.unipile.v1",
|
|
881
|
+
baselineSeed: "empty",
|
|
882
|
+
routeDomains: [
|
|
883
|
+
exactDomain("api.unipile.com"),
|
|
884
|
+
suffixDomain("unipile.com")
|
|
885
|
+
],
|
|
886
|
+
hardFailDomains: [],
|
|
887
|
+
warningDomains: [],
|
|
888
|
+
diagnostics: {
|
|
889
|
+
undeclared: ({ url }) => `Blocked Unipile traffic to ${url}. Declare unipile before calling the Unipile API.`,
|
|
890
|
+
declaredEscape: ({ url }) => `Blocked Unipile escape to ${url}. Unipile route mode is configured, but this domain is outside the routed Unipile API surface.`,
|
|
891
|
+
warning: ({ url }) => `Observed adjacent Unipile domain ${url}. Archal did not reroute this request because it is outside the routed Unipile API surface.`
|
|
892
|
+
},
|
|
893
|
+
sdkIdentifiers: ["unipile"],
|
|
894
|
+
conformanceSuiteId: "unipile-hosted-route"
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// ../route-runtime-core/src/manifests/webflow.ts
|
|
898
|
+
var webflowRouteManifest = {
|
|
899
|
+
service: "webflow",
|
|
900
|
+
manifestVersion: "2026-06-07.webflow.v1",
|
|
901
|
+
baselineSeed: "empty",
|
|
902
|
+
routeDomains: [
|
|
903
|
+
exactDomain("api.webflow.com"),
|
|
904
|
+
exactDomain("webflow.com")
|
|
905
|
+
],
|
|
906
|
+
hardFailDomains: [],
|
|
907
|
+
warningDomains: [
|
|
908
|
+
suffixDomain("webflow.com")
|
|
909
|
+
],
|
|
910
|
+
diagnostics: {
|
|
911
|
+
undeclared: ({ url }) => `Blocked Webflow traffic to ${url}. Declare webflow before calling the Webflow API.`,
|
|
912
|
+
declaredEscape: ({ url }) => `Blocked Webflow escape to ${url}. Webflow route mode is configured, but this domain is outside the routed Webflow API surface.`,
|
|
913
|
+
warning: ({ url }) => `Observed adjacent Webflow domain ${url}. Archal did not reroute this request because it is outside the routed Webflow API surface.`
|
|
914
|
+
},
|
|
915
|
+
sdkIdentifiers: ["webflow-api", "webflow"],
|
|
916
|
+
conformanceSuiteId: "webflow-hosted-route"
|
|
917
|
+
};
|
|
918
|
+
|
|
919
|
+
// ../route-runtime-core/src/manifests.ts
|
|
920
|
+
var SHARED_ROUTE_MANIFESTS = [
|
|
921
|
+
discordRouteManifest,
|
|
922
|
+
datadogRouteManifest,
|
|
923
|
+
githubRouteManifest,
|
|
924
|
+
gitlabRouteManifest,
|
|
925
|
+
googleWorkspaceRouteManifest,
|
|
926
|
+
apifyRouteManifest,
|
|
927
|
+
calcomRouteManifest,
|
|
928
|
+
clickupRouteManifest,
|
|
929
|
+
customerioRouteManifest,
|
|
930
|
+
hubspotRouteManifest,
|
|
931
|
+
jiraRouteManifest,
|
|
932
|
+
linearRouteManifest,
|
|
933
|
+
ownerrezRouteManifest,
|
|
934
|
+
pricelabsRouteManifest,
|
|
935
|
+
rampRouteManifest,
|
|
936
|
+
sendgridRouteManifest,
|
|
937
|
+
sentryRouteManifest,
|
|
938
|
+
slackRouteManifest,
|
|
939
|
+
stripeRouteManifest,
|
|
940
|
+
supabaseRouteManifest,
|
|
941
|
+
tavilyRouteManifest,
|
|
942
|
+
unipileRouteManifest,
|
|
943
|
+
webflowRouteManifest
|
|
944
|
+
];
|
|
945
|
+
var SHARED_ROUTE_MANIFESTS_BY_SERVICE = new Map(
|
|
946
|
+
SHARED_ROUTE_MANIFESTS.map((manifest) => [manifest.service, manifest])
|
|
947
|
+
);
|
|
948
|
+
function listSharedRouteManifests() {
|
|
949
|
+
return [...SHARED_ROUTE_MANIFESTS];
|
|
950
|
+
}
|
|
951
|
+
function getSharedRouteManifest(service) {
|
|
952
|
+
return SHARED_ROUTE_MANIFESTS_BY_SERVICE.get(service);
|
|
953
|
+
}
|
|
954
|
+
function buildServiceCompatibilityProfile(manifest) {
|
|
955
|
+
return {
|
|
956
|
+
service: manifest.service,
|
|
957
|
+
upstreamBasePath: manifest.upstreamBasePath,
|
|
958
|
+
upstreamBasePathAliases: manifest.upstreamBasePathAliases,
|
|
959
|
+
routePathRewrite: manifest.routePathRewrite,
|
|
960
|
+
routeDomains: manifest.routeDomains,
|
|
961
|
+
hardFailDomains: manifest.hardFailDomains,
|
|
962
|
+
warningDomains: manifest.warningDomains,
|
|
963
|
+
diagnostics: manifest.diagnostics
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ../route-runtime-core/src/runtime.ts
|
|
968
|
+
var require2 = createRequire(
|
|
969
|
+
typeof __filename === "string" && isAbsolute2(__filename) ? __filename : import.meta.url
|
|
970
|
+
);
|
|
971
|
+
var httpModule = require2("node:http");
|
|
972
|
+
var httpsModule = require2("node:https");
|
|
973
|
+
var SHARED_SERVICE_PROFILES = listSharedRouteManifests().map(buildServiceCompatibilityProfile);
|
|
974
|
+
var CLIENT_VISIBLE_BLOCKED_REQUEST_MESSAGE = "Network request failed";
|
|
975
|
+
function createClientVisibleNetworkError() {
|
|
976
|
+
return new TypeError(CLIENT_VISIBLE_BLOCKED_REQUEST_MESSAGE);
|
|
977
|
+
}
|
|
978
|
+
function sanitizeClientVisibleNetworkError(error) {
|
|
979
|
+
const visibleError = error;
|
|
980
|
+
for (const key of Reflect.ownKeys(error)) {
|
|
981
|
+
delete visibleError[key];
|
|
982
|
+
}
|
|
983
|
+
error.name = "TypeError";
|
|
984
|
+
error.message = CLIENT_VISIBLE_BLOCKED_REQUEST_MESSAGE;
|
|
985
|
+
error.stack = `${error.name}: ${error.message}`;
|
|
986
|
+
}
|
|
987
|
+
function now() {
|
|
988
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
989
|
+
}
|
|
990
|
+
function normalizePathPrefix(pathPrefix) {
|
|
991
|
+
const withLeadingSlash = pathPrefix.startsWith("/") ? pathPrefix : `/${pathPrefix}`;
|
|
992
|
+
return withLeadingSlash.replace(/\/+$/, "") || "/";
|
|
993
|
+
}
|
|
994
|
+
function matchPathPrefix(pathPrefix, pathname) {
|
|
995
|
+
const normalizedPrefix = normalizePathPrefix(pathPrefix);
|
|
996
|
+
const normalizedPathname = pathname.startsWith("/") ? pathname : `/${pathname}`;
|
|
997
|
+
if (normalizedPrefix === "/") {
|
|
998
|
+
return normalizedPathname;
|
|
999
|
+
}
|
|
1000
|
+
const prefixSegments = normalizedPrefix.split("/").filter(Boolean);
|
|
1001
|
+
const pathnameSegments = normalizedPathname.split("/").filter(Boolean);
|
|
1002
|
+
if (prefixSegments.length > pathnameSegments.length) {
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
for (let index = 0; index < prefixSegments.length; index += 1) {
|
|
1006
|
+
const expected = prefixSegments[index];
|
|
1007
|
+
if (expected.startsWith(":")) {
|
|
1008
|
+
continue;
|
|
1009
|
+
}
|
|
1010
|
+
if (expected !== pathnameSegments[index]) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
const remainingSegments = pathnameSegments.slice(prefixSegments.length);
|
|
1015
|
+
return remainingSegments.length === 0 ? "/" : `/${remainingSegments.join("/")}`;
|
|
1016
|
+
}
|
|
1017
|
+
function resolveRoutedPathname(sourceUrl, service, profile, rewritten) {
|
|
1018
|
+
if (rewritten) {
|
|
1019
|
+
return rewritten.pathname;
|
|
1020
|
+
}
|
|
1021
|
+
const sourcePathname = sourceUrl.pathname || "/";
|
|
1022
|
+
const pathPrefixes = [
|
|
1023
|
+
service.upstreamBasePath,
|
|
1024
|
+
profile.upstreamBasePath,
|
|
1025
|
+
...profile.upstreamBasePathAliases ?? []
|
|
1026
|
+
].filter((prefix) => typeof prefix === "string" && prefix.length > 0);
|
|
1027
|
+
for (const pathPrefix of pathPrefixes) {
|
|
1028
|
+
const normalizedPathname = matchPathPrefix(pathPrefix, sourcePathname);
|
|
1029
|
+
if (normalizedPathname) {
|
|
1030
|
+
return normalizedPathname;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return sourcePathname;
|
|
1034
|
+
}
|
|
1035
|
+
function mergeUrl(service, sourceUrl, profile) {
|
|
1036
|
+
const rewritten = profile.routePathRewrite?.(sourceUrl);
|
|
1037
|
+
const targetBaseUrl = new URL(service.baseUrl);
|
|
1038
|
+
const targetPathname = targetBaseUrl.pathname.endsWith("/") ? targetBaseUrl.pathname.slice(0, -1) : targetBaseUrl.pathname;
|
|
1039
|
+
targetBaseUrl.pathname = `${targetPathname}${resolveRoutedPathname(sourceUrl, service, profile, rewritten)}` || "/";
|
|
1040
|
+
targetBaseUrl.search = rewritten?.search ?? sourceUrl.search;
|
|
1041
|
+
targetBaseUrl.hash = sourceUrl.hash;
|
|
1042
|
+
return targetBaseUrl;
|
|
1043
|
+
}
|
|
1044
|
+
function bridgeSecureConnectIfNeeded(request, sourceProtocol, targetProtocol) {
|
|
1045
|
+
if (sourceProtocol !== DEFAULT_HTTPS_PROTOCOL || targetProtocol !== DEFAULT_HTTP_PROTOCOL) {
|
|
1046
|
+
return request;
|
|
1047
|
+
}
|
|
1048
|
+
request.once("socket", (socket) => {
|
|
1049
|
+
if (socket.connecting) {
|
|
1050
|
+
socket.once("connect", () => {
|
|
1051
|
+
socket.emit("secureConnect");
|
|
1052
|
+
});
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
queueMicrotask(() => {
|
|
1056
|
+
socket.emit("secureConnect");
|
|
1057
|
+
});
|
|
1058
|
+
});
|
|
1059
|
+
return request;
|
|
1060
|
+
}
|
|
1061
|
+
function blockedWarningCode(decision, servicesByName) {
|
|
1062
|
+
return servicesByName.has(decision.service) ? "ARCHAL_DECLARED_SERVICE_ESCAPE" : "ARCHAL_UNDECLARED_SERVICE";
|
|
1063
|
+
}
|
|
1064
|
+
var CLIENT_VISIBLE_INTERNAL_RESPONSE_HEADERS = /* @__PURE__ */ new Set([
|
|
1065
|
+
"proxy-authorization",
|
|
1066
|
+
"x-route-authorization",
|
|
1067
|
+
"x-route-service-origin",
|
|
1068
|
+
"x-service-routing-scope"
|
|
1069
|
+
]);
|
|
1070
|
+
function isClientVisibleInternalHeaderName(name) {
|
|
1071
|
+
const normalized = name.toLowerCase();
|
|
1072
|
+
return normalized === "x-archal" || normalized.startsWith("x-archal-") || normalized.startsWith("x-route-") || CLIENT_VISIBLE_INTERNAL_RESPONSE_HEADERS.has(normalized);
|
|
1073
|
+
}
|
|
1074
|
+
function sanitizedFetchResponse(response) {
|
|
1075
|
+
const headers = new Headers(response.headers);
|
|
1076
|
+
let changed = false;
|
|
1077
|
+
for (const header of [...headers.keys()]) {
|
|
1078
|
+
if (!isClientVisibleInternalHeaderName(header)) {
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
headers.delete(header);
|
|
1082
|
+
changed = true;
|
|
1083
|
+
}
|
|
1084
|
+
if (!changed) {
|
|
1085
|
+
return response;
|
|
1086
|
+
}
|
|
1087
|
+
return new Response(response.body, {
|
|
1088
|
+
status: response.status,
|
|
1089
|
+
statusText: response.statusText,
|
|
1090
|
+
headers
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1093
|
+
function sanitizedRawHeaders(rawHeaders) {
|
|
1094
|
+
const result = [];
|
|
1095
|
+
for (let index = 0; index < rawHeaders.length; index += 2) {
|
|
1096
|
+
const name = rawHeaders[index];
|
|
1097
|
+
const value = rawHeaders[index + 1];
|
|
1098
|
+
if (!name || isClientVisibleInternalHeaderName(name)) {
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
result.push(name);
|
|
1102
|
+
if (value !== void 0) {
|
|
1103
|
+
result.push(value);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return result;
|
|
1107
|
+
}
|
|
1108
|
+
function sanitizeIncomingResponseHeaders(response) {
|
|
1109
|
+
for (const header of Object.keys(response.headers)) {
|
|
1110
|
+
if (isClientVisibleInternalHeaderName(header)) {
|
|
1111
|
+
delete response.headers[header];
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
if (Array.isArray(response.rawHeaders)) {
|
|
1115
|
+
response.rawHeaders = sanitizedRawHeaders(response.rawHeaders);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
function wrapRoutedResponseCallback(callback) {
|
|
1119
|
+
if (!callback) {
|
|
1120
|
+
return void 0;
|
|
1121
|
+
}
|
|
1122
|
+
return (response) => {
|
|
1123
|
+
sanitizeIncomingResponseHeaders(response);
|
|
1124
|
+
callback(response);
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
var NodeRouteRuntime = class {
|
|
1128
|
+
profiles;
|
|
1129
|
+
servicesByName;
|
|
1130
|
+
events = [];
|
|
1131
|
+
onEvent;
|
|
1132
|
+
traceRecords = [];
|
|
1133
|
+
traceEnabled;
|
|
1134
|
+
onTrace;
|
|
1135
|
+
started = false;
|
|
1136
|
+
routingInstalled = false;
|
|
1137
|
+
originalHttpRequest = httpModule.request;
|
|
1138
|
+
originalHttpGet = httpModule.get;
|
|
1139
|
+
originalHttpsRequest = httpsModule.request;
|
|
1140
|
+
originalHttpsGet = httpsModule.get;
|
|
1141
|
+
originalFetch = globalThis.fetch;
|
|
1142
|
+
constructor(config) {
|
|
1143
|
+
this.profiles = config.profiles;
|
|
1144
|
+
this.servicesByName = new Map(config.services.map((service) => [service.name, service]));
|
|
1145
|
+
this.onEvent = config.onEvent;
|
|
1146
|
+
this.traceEnabled = config.trace?.enabled ?? false;
|
|
1147
|
+
this.onTrace = config.trace?.onTrace;
|
|
1148
|
+
}
|
|
1149
|
+
async start() {
|
|
1150
|
+
if (this.started) return;
|
|
1151
|
+
this.started = true;
|
|
1152
|
+
this.emit({ type: "runtime_started", timestamp: now() });
|
|
1153
|
+
}
|
|
1154
|
+
async reset() {
|
|
1155
|
+
this.events.length = 0;
|
|
1156
|
+
this.emit({ type: "runtime_reset", timestamp: now() });
|
|
1157
|
+
}
|
|
1158
|
+
async stop() {
|
|
1159
|
+
this.uninstallRouting();
|
|
1160
|
+
if (!this.started) return;
|
|
1161
|
+
this.started = false;
|
|
1162
|
+
this.emit({ type: "runtime_stopped", timestamp: now() });
|
|
1163
|
+
}
|
|
1164
|
+
installRouting() {
|
|
1165
|
+
if (this.routingInstalled) return;
|
|
1166
|
+
httpModule.request = ((...args) => this.dispatchRequest(this.originalHttpRequest, this.originalHttpsRequest, DEFAULT_HTTP_PROTOCOL, args));
|
|
1167
|
+
httpModule.get = ((...args) => this.dispatchGet(this.originalHttpGet, this.originalHttpsGet, DEFAULT_HTTP_PROTOCOL, args));
|
|
1168
|
+
httpsModule.request = ((...args) => this.dispatchRequest(this.originalHttpRequest, this.originalHttpsRequest, DEFAULT_HTTPS_PROTOCOL, args));
|
|
1169
|
+
httpsModule.get = ((...args) => this.dispatchGet(this.originalHttpGet, this.originalHttpsGet, DEFAULT_HTTPS_PROTOCOL, args));
|
|
1170
|
+
if (this.originalFetch) {
|
|
1171
|
+
globalThis.fetch = (async (input, init) => {
|
|
1172
|
+
const originalRequest = input instanceof Request ? input : void 0;
|
|
1173
|
+
const sourceUrl = new URL(
|
|
1174
|
+
originalRequest?.url ?? (input instanceof URL ? input.toString() : String(input))
|
|
1175
|
+
);
|
|
1176
|
+
const decision = this.evaluateRequest(sourceUrl);
|
|
1177
|
+
const method = (originalRequest?.method ?? init?.method ?? "GET").toUpperCase();
|
|
1178
|
+
if (decision.kind === "route") {
|
|
1179
|
+
const service = this.servicesByName.get(decision.service);
|
|
1180
|
+
if (!service) {
|
|
1181
|
+
throw new Error(`Missing routed service config for ${decision.service}.`);
|
|
1182
|
+
}
|
|
1183
|
+
const targetUrl = decision.targetUrl.toString();
|
|
1184
|
+
if (originalRequest) {
|
|
1185
|
+
let response3;
|
|
1186
|
+
try {
|
|
1187
|
+
response3 = await this.originalFetch(
|
|
1188
|
+
buildRoutedFetchRequest(originalRequest, decision.targetUrl, service, init)
|
|
1189
|
+
);
|
|
1190
|
+
} catch {
|
|
1191
|
+
this.traceRequest({
|
|
1192
|
+
method,
|
|
1193
|
+
sourceUrl: sourceUrl.toString(),
|
|
1194
|
+
manifestMatched: true,
|
|
1195
|
+
service: decision.service,
|
|
1196
|
+
target: "twin",
|
|
1197
|
+
outcome: "failed",
|
|
1198
|
+
reason: "network_error",
|
|
1199
|
+
targetUrl,
|
|
1200
|
+
error: CLIENT_VISIBLE_BLOCKED_REQUEST_MESSAGE
|
|
1201
|
+
});
|
|
1202
|
+
throw createClientVisibleNetworkError();
|
|
1203
|
+
}
|
|
1204
|
+
this.traceRequest({
|
|
1205
|
+
method,
|
|
1206
|
+
sourceUrl: sourceUrl.toString(),
|
|
1207
|
+
manifestMatched: true,
|
|
1208
|
+
service: decision.service,
|
|
1209
|
+
target: "twin",
|
|
1210
|
+
outcome: "routed",
|
|
1211
|
+
reason: "route",
|
|
1212
|
+
targetUrl,
|
|
1213
|
+
statusCode: response3.status,
|
|
1214
|
+
statusText: response3.statusText
|
|
1215
|
+
});
|
|
1216
|
+
return sanitizedFetchResponse(response3);
|
|
1217
|
+
}
|
|
1218
|
+
const headers = new Headers(init?.headers);
|
|
1219
|
+
applyRoutedHeaders(headers, service);
|
|
1220
|
+
let response2;
|
|
1221
|
+
try {
|
|
1222
|
+
response2 = await this.originalFetch(new URL(targetUrl), {
|
|
1223
|
+
...init,
|
|
1224
|
+
headers
|
|
1225
|
+
});
|
|
1226
|
+
} catch {
|
|
1227
|
+
this.traceRequest({
|
|
1228
|
+
method,
|
|
1229
|
+
sourceUrl: sourceUrl.toString(),
|
|
1230
|
+
manifestMatched: true,
|
|
1231
|
+
service: decision.service,
|
|
1232
|
+
target: "twin",
|
|
1233
|
+
outcome: "failed",
|
|
1234
|
+
reason: "network_error",
|
|
1235
|
+
targetUrl,
|
|
1236
|
+
error: CLIENT_VISIBLE_BLOCKED_REQUEST_MESSAGE
|
|
1237
|
+
});
|
|
1238
|
+
throw createClientVisibleNetworkError();
|
|
1239
|
+
}
|
|
1240
|
+
this.traceRequest({
|
|
1241
|
+
method,
|
|
1242
|
+
sourceUrl: sourceUrl.toString(),
|
|
1243
|
+
manifestMatched: true,
|
|
1244
|
+
service: decision.service,
|
|
1245
|
+
target: "twin",
|
|
1246
|
+
outcome: "routed",
|
|
1247
|
+
reason: "route",
|
|
1248
|
+
targetUrl,
|
|
1249
|
+
statusCode: response2.status,
|
|
1250
|
+
statusText: response2.statusText
|
|
1251
|
+
});
|
|
1252
|
+
return sanitizedFetchResponse(response2);
|
|
1253
|
+
}
|
|
1254
|
+
if (decision.kind === "block") {
|
|
1255
|
+
this.traceRequest({
|
|
1256
|
+
method,
|
|
1257
|
+
sourceUrl: sourceUrl.toString(),
|
|
1258
|
+
manifestMatched: true,
|
|
1259
|
+
service: decision.service,
|
|
1260
|
+
target: "none",
|
|
1261
|
+
outcome: "blocked",
|
|
1262
|
+
reason: decision.code === "ARCHAL_UNDECLARED_SERVICE" ? "blocked_undeclared" : "blocked_escape"
|
|
1263
|
+
});
|
|
1264
|
+
throw new RouteRuntimePolicyError({
|
|
1265
|
+
code: decision.code,
|
|
1266
|
+
service: decision.service,
|
|
1267
|
+
url: sourceUrl.toString(),
|
|
1268
|
+
message: decision.message
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
if (decision.kind === "warn") {
|
|
1272
|
+
const code = blockedWarningCode(decision, this.servicesByName);
|
|
1273
|
+
this.traceRequest({
|
|
1274
|
+
method,
|
|
1275
|
+
sourceUrl: sourceUrl.toString(),
|
|
1276
|
+
manifestMatched: true,
|
|
1277
|
+
service: decision.service,
|
|
1278
|
+
target: "none",
|
|
1279
|
+
outcome: "blocked",
|
|
1280
|
+
reason: code === "ARCHAL_UNDECLARED_SERVICE" ? "blocked_undeclared" : "blocked_escape"
|
|
1281
|
+
});
|
|
1282
|
+
throw new RouteRuntimePolicyError({
|
|
1283
|
+
code,
|
|
1284
|
+
service: decision.service,
|
|
1285
|
+
url: sourceUrl.toString(),
|
|
1286
|
+
message: decision.message
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
let response;
|
|
1290
|
+
try {
|
|
1291
|
+
response = await this.originalFetch(input, init);
|
|
1292
|
+
} catch (error) {
|
|
1293
|
+
this.traceRequest({
|
|
1294
|
+
method,
|
|
1295
|
+
sourceUrl: sourceUrl.toString(),
|
|
1296
|
+
manifestMatched: false,
|
|
1297
|
+
target: "upstream",
|
|
1298
|
+
outcome: "failed",
|
|
1299
|
+
reason: "network_error",
|
|
1300
|
+
error: errorMessage(error)
|
|
1301
|
+
});
|
|
1302
|
+
throw error;
|
|
1303
|
+
}
|
|
1304
|
+
this.traceRequest({
|
|
1305
|
+
method,
|
|
1306
|
+
sourceUrl: sourceUrl.toString(),
|
|
1307
|
+
manifestMatched: false,
|
|
1308
|
+
target: "upstream",
|
|
1309
|
+
outcome: "bypassed",
|
|
1310
|
+
reason: "no_match",
|
|
1311
|
+
statusCode: response.status,
|
|
1312
|
+
statusText: response.statusText
|
|
1313
|
+
});
|
|
1314
|
+
return response;
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
this.routingInstalled = true;
|
|
1318
|
+
this.emit({ type: "routing_installed", timestamp: now() });
|
|
1319
|
+
}
|
|
1320
|
+
uninstallRouting() {
|
|
1321
|
+
if (!this.routingInstalled) return;
|
|
1322
|
+
httpModule.request = this.originalHttpRequest;
|
|
1323
|
+
httpModule.get = this.originalHttpGet;
|
|
1324
|
+
httpsModule.request = this.originalHttpsRequest;
|
|
1325
|
+
httpsModule.get = this.originalHttpsGet;
|
|
1326
|
+
if (this.originalFetch) {
|
|
1327
|
+
globalThis.fetch = this.originalFetch;
|
|
1328
|
+
}
|
|
1329
|
+
this.routingInstalled = false;
|
|
1330
|
+
this.emit({ type: "routing_uninstalled", timestamp: now() });
|
|
1331
|
+
}
|
|
1332
|
+
getEvents() {
|
|
1333
|
+
return [...this.events];
|
|
1334
|
+
}
|
|
1335
|
+
getTraceRecords() {
|
|
1336
|
+
return [...this.traceRecords];
|
|
1337
|
+
}
|
|
1338
|
+
evaluateRequest(input) {
|
|
1339
|
+
const sourceUrl = input instanceof URL ? input : new URL(input);
|
|
1340
|
+
const hostname = sourceUrl.hostname.toLowerCase();
|
|
1341
|
+
for (const profile of this.profiles) {
|
|
1342
|
+
const classification = classifyHostname(profile, hostname);
|
|
1343
|
+
if (classification === "ignore") {
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
1346
|
+
const configuredService = this.servicesByName.get(profile.service);
|
|
1347
|
+
const diagnosticsContext = {
|
|
1348
|
+
service: profile.service,
|
|
1349
|
+
hostname,
|
|
1350
|
+
url: sourceUrl.toString(),
|
|
1351
|
+
configuredBaseUrl: configuredService?.baseUrl
|
|
1352
|
+
};
|
|
1353
|
+
if (classification === "route") {
|
|
1354
|
+
if (!configuredService) {
|
|
1355
|
+
const message2 = profile.diagnostics.undeclared(diagnosticsContext);
|
|
1356
|
+
this.emit({
|
|
1357
|
+
type: "request_blocked",
|
|
1358
|
+
timestamp: now(),
|
|
1359
|
+
service: profile.service,
|
|
1360
|
+
code: "ARCHAL_UNDECLARED_SERVICE",
|
|
1361
|
+
url: sourceUrl.toString(),
|
|
1362
|
+
message: message2
|
|
1363
|
+
});
|
|
1364
|
+
return {
|
|
1365
|
+
kind: "block",
|
|
1366
|
+
service: profile.service,
|
|
1367
|
+
code: "ARCHAL_UNDECLARED_SERVICE",
|
|
1368
|
+
message: message2
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
const targetUrl = mergeUrl(configuredService, sourceUrl, profile);
|
|
1372
|
+
this.emit({
|
|
1373
|
+
type: "request_routed",
|
|
1374
|
+
timestamp: now(),
|
|
1375
|
+
service: profile.service,
|
|
1376
|
+
sourceUrl: sourceUrl.toString(),
|
|
1377
|
+
targetUrl: targetUrl.toString()
|
|
1378
|
+
});
|
|
1379
|
+
return {
|
|
1380
|
+
kind: "route",
|
|
1381
|
+
service: profile.service,
|
|
1382
|
+
targetUrl
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
if (classification === "hard-fail") {
|
|
1386
|
+
const code = configuredService ? "ARCHAL_DECLARED_SERVICE_ESCAPE" : "ARCHAL_UNDECLARED_SERVICE";
|
|
1387
|
+
const message2 = configuredService ? profile.diagnostics.declaredEscape(diagnosticsContext) : profile.diagnostics.undeclared(diagnosticsContext);
|
|
1388
|
+
this.emit({
|
|
1389
|
+
type: "request_blocked",
|
|
1390
|
+
timestamp: now(),
|
|
1391
|
+
service: profile.service,
|
|
1392
|
+
code,
|
|
1393
|
+
url: sourceUrl.toString(),
|
|
1394
|
+
message: message2
|
|
1395
|
+
});
|
|
1396
|
+
return {
|
|
1397
|
+
kind: "block",
|
|
1398
|
+
service: profile.service,
|
|
1399
|
+
code,
|
|
1400
|
+
message: message2
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
const message = profile.diagnostics.warning(diagnosticsContext);
|
|
1404
|
+
this.emit({
|
|
1405
|
+
type: "request_warning",
|
|
1406
|
+
timestamp: now(),
|
|
1407
|
+
service: profile.service,
|
|
1408
|
+
url: sourceUrl.toString(),
|
|
1409
|
+
message
|
|
1410
|
+
});
|
|
1411
|
+
return {
|
|
1412
|
+
kind: "warn",
|
|
1413
|
+
service: profile.service,
|
|
1414
|
+
message
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
for (const profile of SHARED_SERVICE_PROFILES) {
|
|
1418
|
+
const classification = classifyHostname(profile, hostname);
|
|
1419
|
+
if (classification === "ignore") {
|
|
1420
|
+
continue;
|
|
1421
|
+
}
|
|
1422
|
+
const configuredService = this.servicesByName.get(profile.service);
|
|
1423
|
+
const diagnosticsContext = {
|
|
1424
|
+
service: profile.service,
|
|
1425
|
+
hostname,
|
|
1426
|
+
url: sourceUrl.toString(),
|
|
1427
|
+
configuredBaseUrl: configuredService?.baseUrl
|
|
1428
|
+
};
|
|
1429
|
+
const code = configuredService ? "ARCHAL_DECLARED_SERVICE_ESCAPE" : "ARCHAL_UNDECLARED_SERVICE";
|
|
1430
|
+
const message = configuredService ? profile.diagnostics.declaredEscape(diagnosticsContext) : profile.diagnostics.undeclared(diagnosticsContext);
|
|
1431
|
+
this.emit({
|
|
1432
|
+
type: "request_blocked",
|
|
1433
|
+
timestamp: now(),
|
|
1434
|
+
service: profile.service,
|
|
1435
|
+
code,
|
|
1436
|
+
url: sourceUrl.toString(),
|
|
1437
|
+
message
|
|
1438
|
+
});
|
|
1439
|
+
return {
|
|
1440
|
+
kind: "block",
|
|
1441
|
+
service: profile.service,
|
|
1442
|
+
code,
|
|
1443
|
+
message
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
return { kind: "allow" };
|
|
1447
|
+
}
|
|
1448
|
+
dispatchRequest(originalHttpRequest, originalHttpsRequest, defaultProtocol, args) {
|
|
1449
|
+
const { requestUrl, options, callback } = parseRequestArgs(defaultProtocol, args);
|
|
1450
|
+
const decision = this.evaluateRequest(requestUrl);
|
|
1451
|
+
const method = typeof options.method === "string" && options.method.trim().length > 0 ? options.method.trim().toUpperCase() : "GET";
|
|
1452
|
+
if (decision.kind === "block") {
|
|
1453
|
+
this.traceRequest({
|
|
1454
|
+
method,
|
|
1455
|
+
sourceUrl: requestUrl.toString(),
|
|
1456
|
+
manifestMatched: true,
|
|
1457
|
+
service: decision.service,
|
|
1458
|
+
target: "none",
|
|
1459
|
+
outcome: "blocked",
|
|
1460
|
+
reason: decision.code === "ARCHAL_UNDECLARED_SERVICE" ? "blocked_undeclared" : "blocked_escape"
|
|
1461
|
+
});
|
|
1462
|
+
throw new RouteRuntimePolicyError({
|
|
1463
|
+
code: decision.code,
|
|
1464
|
+
service: decision.service,
|
|
1465
|
+
url: requestUrl.toString(),
|
|
1466
|
+
message: decision.message
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
if (decision.kind === "warn") {
|
|
1470
|
+
const code = blockedWarningCode(decision, this.servicesByName);
|
|
1471
|
+
this.traceRequest({
|
|
1472
|
+
method,
|
|
1473
|
+
sourceUrl: requestUrl.toString(),
|
|
1474
|
+
manifestMatched: true,
|
|
1475
|
+
service: decision.service,
|
|
1476
|
+
target: "none",
|
|
1477
|
+
outcome: "blocked",
|
|
1478
|
+
reason: code === "ARCHAL_UNDECLARED_SERVICE" ? "blocked_undeclared" : "blocked_escape"
|
|
1479
|
+
});
|
|
1480
|
+
throw new RouteRuntimePolicyError({
|
|
1481
|
+
code,
|
|
1482
|
+
service: decision.service,
|
|
1483
|
+
url: requestUrl.toString(),
|
|
1484
|
+
message: decision.message
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
if (decision.kind !== "route") {
|
|
1488
|
+
const request = Reflect.apply(
|
|
1489
|
+
defaultProtocol === DEFAULT_HTTPS_PROTOCOL ? originalHttpsRequest : originalHttpRequest,
|
|
1490
|
+
defaultProtocol === DEFAULT_HTTPS_PROTOCOL ? httpsModule : httpModule,
|
|
1491
|
+
args
|
|
1492
|
+
);
|
|
1493
|
+
return this.addTraceListeners(request, {
|
|
1494
|
+
method,
|
|
1495
|
+
sourceUrl: requestUrl.toString(),
|
|
1496
|
+
manifestMatched: false,
|
|
1497
|
+
target: "upstream",
|
|
1498
|
+
outcome: "bypassed",
|
|
1499
|
+
reason: "no_match"
|
|
1500
|
+
});
|
|
1501
|
+
}
|
|
1502
|
+
const service = this.servicesByName.get(decision.service);
|
|
1503
|
+
if (!service) {
|
|
1504
|
+
throw new Error(`Missing routed service config for ${decision.service}.`);
|
|
1505
|
+
}
|
|
1506
|
+
const rewrittenArgs = buildPatchedArgs(
|
|
1507
|
+
decision.targetUrl,
|
|
1508
|
+
options,
|
|
1509
|
+
service,
|
|
1510
|
+
defaultProtocol,
|
|
1511
|
+
wrapRoutedResponseCallback(callback)
|
|
1512
|
+
);
|
|
1513
|
+
if (decision.targetUrl.protocol === DEFAULT_HTTP_PROTOCOL) {
|
|
1514
|
+
const request = bridgeSecureConnectIfNeeded(
|
|
1515
|
+
Reflect.apply(originalHttpRequest, httpModule, rewrittenArgs),
|
|
1516
|
+
defaultProtocol,
|
|
1517
|
+
decision.targetUrl.protocol
|
|
1518
|
+
);
|
|
1519
|
+
return this.addTraceListeners(request, {
|
|
1520
|
+
method,
|
|
1521
|
+
sourceUrl: requestUrl.toString(),
|
|
1522
|
+
manifestMatched: true,
|
|
1523
|
+
service: decision.service,
|
|
1524
|
+
target: "twin",
|
|
1525
|
+
outcome: "routed",
|
|
1526
|
+
reason: "route",
|
|
1527
|
+
targetUrl: decision.targetUrl.toString()
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
return this.addTraceListeners(
|
|
1531
|
+
Reflect.apply(originalHttpsRequest, httpsModule, rewrittenArgs),
|
|
1532
|
+
{
|
|
1533
|
+
method,
|
|
1534
|
+
sourceUrl: requestUrl.toString(),
|
|
1535
|
+
manifestMatched: true,
|
|
1536
|
+
service: decision.service,
|
|
1537
|
+
target: "twin",
|
|
1538
|
+
outcome: "routed",
|
|
1539
|
+
reason: "route",
|
|
1540
|
+
targetUrl: decision.targetUrl.toString()
|
|
1541
|
+
}
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
dispatchGet(originalHttpGet, originalHttpsGet, defaultProtocol, args) {
|
|
1545
|
+
const { requestUrl, options, callback } = parseRequestArgs(defaultProtocol, args);
|
|
1546
|
+
const decision = this.evaluateRequest(requestUrl);
|
|
1547
|
+
const method = typeof options.method === "string" && options.method.trim().length > 0 ? options.method.trim().toUpperCase() : "GET";
|
|
1548
|
+
if (decision.kind === "block") {
|
|
1549
|
+
this.traceRequest({
|
|
1550
|
+
method,
|
|
1551
|
+
sourceUrl: requestUrl.toString(),
|
|
1552
|
+
manifestMatched: true,
|
|
1553
|
+
service: decision.service,
|
|
1554
|
+
target: "none",
|
|
1555
|
+
outcome: "blocked",
|
|
1556
|
+
reason: decision.code === "ARCHAL_UNDECLARED_SERVICE" ? "blocked_undeclared" : "blocked_escape"
|
|
1557
|
+
});
|
|
1558
|
+
throw new RouteRuntimePolicyError({
|
|
1559
|
+
code: decision.code,
|
|
1560
|
+
service: decision.service,
|
|
1561
|
+
url: requestUrl.toString(),
|
|
1562
|
+
message: decision.message
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
if (decision.kind === "warn") {
|
|
1566
|
+
const code = blockedWarningCode(decision, this.servicesByName);
|
|
1567
|
+
this.traceRequest({
|
|
1568
|
+
method,
|
|
1569
|
+
sourceUrl: requestUrl.toString(),
|
|
1570
|
+
manifestMatched: true,
|
|
1571
|
+
service: decision.service,
|
|
1572
|
+
target: "none",
|
|
1573
|
+
outcome: "blocked",
|
|
1574
|
+
reason: code === "ARCHAL_UNDECLARED_SERVICE" ? "blocked_undeclared" : "blocked_escape"
|
|
1575
|
+
});
|
|
1576
|
+
throw new RouteRuntimePolicyError({
|
|
1577
|
+
code,
|
|
1578
|
+
service: decision.service,
|
|
1579
|
+
url: requestUrl.toString(),
|
|
1580
|
+
message: decision.message
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
if (decision.kind !== "route") {
|
|
1584
|
+
const request = Reflect.apply(
|
|
1585
|
+
defaultProtocol === DEFAULT_HTTPS_PROTOCOL ? originalHttpsGet : originalHttpGet,
|
|
1586
|
+
defaultProtocol === DEFAULT_HTTPS_PROTOCOL ? httpsModule : httpModule,
|
|
1587
|
+
args
|
|
1588
|
+
);
|
|
1589
|
+
return this.addTraceListeners(request, {
|
|
1590
|
+
method,
|
|
1591
|
+
sourceUrl: requestUrl.toString(),
|
|
1592
|
+
manifestMatched: false,
|
|
1593
|
+
target: "upstream",
|
|
1594
|
+
outcome: "bypassed",
|
|
1595
|
+
reason: "no_match"
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
const service = this.servicesByName.get(decision.service);
|
|
1599
|
+
if (!service) {
|
|
1600
|
+
throw new Error(`Missing routed service config for ${decision.service}.`);
|
|
1601
|
+
}
|
|
1602
|
+
const rewrittenArgs = buildPatchedArgs(
|
|
1603
|
+
decision.targetUrl,
|
|
1604
|
+
options,
|
|
1605
|
+
service,
|
|
1606
|
+
defaultProtocol,
|
|
1607
|
+
wrapRoutedResponseCallback(callback)
|
|
1608
|
+
);
|
|
1609
|
+
if (decision.targetUrl.protocol === DEFAULT_HTTP_PROTOCOL) {
|
|
1610
|
+
const request = bridgeSecureConnectIfNeeded(
|
|
1611
|
+
Reflect.apply(originalHttpGet, httpModule, rewrittenArgs),
|
|
1612
|
+
defaultProtocol,
|
|
1613
|
+
decision.targetUrl.protocol
|
|
1614
|
+
);
|
|
1615
|
+
return this.addTraceListeners(request, {
|
|
1616
|
+
method,
|
|
1617
|
+
sourceUrl: requestUrl.toString(),
|
|
1618
|
+
manifestMatched: true,
|
|
1619
|
+
service: decision.service,
|
|
1620
|
+
target: "twin",
|
|
1621
|
+
outcome: "routed",
|
|
1622
|
+
reason: "route",
|
|
1623
|
+
targetUrl: decision.targetUrl.toString()
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
return this.addTraceListeners(
|
|
1627
|
+
Reflect.apply(originalHttpsGet, httpsModule, rewrittenArgs),
|
|
1628
|
+
{
|
|
1629
|
+
method,
|
|
1630
|
+
sourceUrl: requestUrl.toString(),
|
|
1631
|
+
manifestMatched: true,
|
|
1632
|
+
service: decision.service,
|
|
1633
|
+
target: "twin",
|
|
1634
|
+
outcome: "routed",
|
|
1635
|
+
reason: "route",
|
|
1636
|
+
targetUrl: decision.targetUrl.toString()
|
|
1637
|
+
}
|
|
1638
|
+
);
|
|
1639
|
+
}
|
|
1640
|
+
emit(event) {
|
|
1641
|
+
this.events.push(event);
|
|
1642
|
+
this.onEvent?.(event);
|
|
1643
|
+
}
|
|
1644
|
+
addTraceListeners(request, record) {
|
|
1645
|
+
if (record.target === "twin") {
|
|
1646
|
+
request.once("response", sanitizeIncomingResponseHeaders);
|
|
1647
|
+
}
|
|
1648
|
+
let settled = false;
|
|
1649
|
+
if (this.traceEnabled) {
|
|
1650
|
+
request.once("response", (response) => {
|
|
1651
|
+
settled = true;
|
|
1652
|
+
this.traceRequest({
|
|
1653
|
+
...record,
|
|
1654
|
+
statusCode: response.statusCode,
|
|
1655
|
+
statusText: response.statusMessage
|
|
1656
|
+
});
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
request.once("error", (error) => {
|
|
1660
|
+
if (this.traceEnabled && !settled) {
|
|
1661
|
+
this.traceRequest({
|
|
1662
|
+
...record,
|
|
1663
|
+
outcome: "failed",
|
|
1664
|
+
reason: "network_error",
|
|
1665
|
+
error: record.target === "twin" ? CLIENT_VISIBLE_BLOCKED_REQUEST_MESSAGE : error.message
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
if (record.target === "twin") {
|
|
1669
|
+
sanitizeClientVisibleNetworkError(error);
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
return request;
|
|
1673
|
+
}
|
|
1674
|
+
traceRequest(record) {
|
|
1675
|
+
if (!this.traceEnabled) {
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
const traceRecord = {
|
|
1679
|
+
timestamp: now(),
|
|
1680
|
+
...record,
|
|
1681
|
+
method: record.method.toUpperCase()
|
|
1682
|
+
};
|
|
1683
|
+
this.traceRecords.push(traceRecord);
|
|
1684
|
+
this.onTrace?.(traceRecord);
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
// ../route-runtime-core/src/routing-policy.ts
|
|
1689
|
+
import { isIP } from "net";
|
|
1690
|
+
var ROUTE_RUNTIME_PROXY_ROUTE_KINDS = [
|
|
1691
|
+
"api",
|
|
1692
|
+
"mcp",
|
|
1693
|
+
"rest",
|
|
1694
|
+
"state",
|
|
1695
|
+
"trace",
|
|
1696
|
+
"tools",
|
|
1697
|
+
"health",
|
|
1698
|
+
"events",
|
|
1699
|
+
"webhooks",
|
|
1700
|
+
"graphql"
|
|
1701
|
+
];
|
|
1702
|
+
var ROUTE_RUNTIME_PUBLIC_ROUTE_KINDS = [
|
|
1703
|
+
"api",
|
|
1704
|
+
"mcp",
|
|
1705
|
+
"rest",
|
|
1706
|
+
"graphql"
|
|
1707
|
+
];
|
|
1708
|
+
var ROUTE_RUNTIME_CONTROL_ROUTE_KINDS = [
|
|
1709
|
+
"state",
|
|
1710
|
+
"trace",
|
|
1711
|
+
"tools",
|
|
1712
|
+
"health",
|
|
1713
|
+
"events",
|
|
1714
|
+
"webhooks"
|
|
1715
|
+
];
|
|
1716
|
+
var ROUTE_RUNTIME_PROXY_ROUTE_KIND_SET = new Set(ROUTE_RUNTIME_PROXY_ROUTE_KINDS);
|
|
1717
|
+
var ROUTE_RUNTIME_PUBLIC_ROUTE_KIND_SET = new Set(ROUTE_RUNTIME_PUBLIC_ROUTE_KINDS);
|
|
1718
|
+
var ROUTE_RUNTIME_CONTROL_ROUTE_KIND_SET = new Set(ROUTE_RUNTIME_CONTROL_ROUTE_KINDS);
|
|
1719
|
+
var ROUTE_RUNTIME_DISPATCHING_ROUTE_KINDS = new Set(ROUTE_RUNTIME_PUBLIC_ROUTE_KINDS);
|
|
1720
|
+
|
|
1721
|
+
// ../route-runtime-core/src/service-profiles/stripe.ts
|
|
1722
|
+
var stripeCompatibilityProfile = buildServiceCompatibilityProfile(stripeRouteManifest);
|
|
1723
|
+
|
|
1724
|
+
// ../../clones/core/dist/chunk-FBW6QFHJ.js
|
|
1725
|
+
var manifest_default = [
|
|
1726
|
+
{
|
|
1727
|
+
name: "apify",
|
|
1728
|
+
package: "@archal/clone-apify",
|
|
1729
|
+
path: "clones/apify",
|
|
1730
|
+
stage: "preview",
|
|
1731
|
+
display: {
|
|
1732
|
+
icon: "AP",
|
|
1733
|
+
name: "Apify",
|
|
1734
|
+
description: "Actors, runs, datasets, key-value stores, and request queues.",
|
|
1735
|
+
toolCount: 59
|
|
1736
|
+
},
|
|
1737
|
+
transport: "rest",
|
|
1738
|
+
hostedRuntime: {
|
|
1739
|
+
enabled: true,
|
|
1740
|
+
requiresDist: true,
|
|
1741
|
+
requiresEmptySeed: true
|
|
1742
|
+
},
|
|
1743
|
+
cli: {
|
|
1744
|
+
bundleAssets: true,
|
|
1745
|
+
bundleToolSnapshot: true
|
|
1746
|
+
},
|
|
1747
|
+
routeProxy: {
|
|
1748
|
+
enabled: true,
|
|
1749
|
+
domains: [
|
|
1750
|
+
{
|
|
1751
|
+
domain: "api.apify.com"
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
domain: "apify.com"
|
|
1755
|
+
}
|
|
1756
|
+
]
|
|
1757
|
+
},
|
|
1758
|
+
seeds: {
|
|
1759
|
+
generatedCatalog: true,
|
|
1760
|
+
default: "empty"
|
|
1761
|
+
},
|
|
1762
|
+
smoke: {
|
|
1763
|
+
production: "clone-surface"
|
|
1764
|
+
},
|
|
1765
|
+
architecture: {
|
|
1766
|
+
class: "domain-simulator",
|
|
1767
|
+
summary: "Storage/admin state uses StateEngine, but REST behavior still depends on Apify-specific upstream envelopes, fixture fallbacks, and stateful storage projections owned by upstream.ts.",
|
|
1768
|
+
domainPrimitives: [
|
|
1769
|
+
"Apify upstream error and pagination envelopes",
|
|
1770
|
+
"dataset, key-value store, and request-queue stateful projections",
|
|
1771
|
+
"fixture-backed fallback semantics for uncovered REST paths"
|
|
1772
|
+
],
|
|
1773
|
+
behaviorOwner: "src/upstream.ts"
|
|
1774
|
+
}
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
name: "box",
|
|
1778
|
+
package: "@archal/clone-box",
|
|
1779
|
+
path: "clones/box",
|
|
1780
|
+
stage: "internal",
|
|
1781
|
+
transport: "rest",
|
|
1782
|
+
hostedRuntime: {
|
|
1783
|
+
enabled: true,
|
|
1784
|
+
requiresDist: true,
|
|
1785
|
+
requiresEmptySeed: true
|
|
1786
|
+
},
|
|
1787
|
+
cli: {
|
|
1788
|
+
bundleAssets: false,
|
|
1789
|
+
bundleToolSnapshot: false
|
|
1790
|
+
},
|
|
1791
|
+
routeProxy: {
|
|
1792
|
+
enabled: false,
|
|
1793
|
+
domains: []
|
|
1794
|
+
},
|
|
1795
|
+
seeds: {
|
|
1796
|
+
generatedCatalog: false,
|
|
1797
|
+
default: "empty"
|
|
1798
|
+
},
|
|
1799
|
+
smoke: {
|
|
1800
|
+
production: "none"
|
|
1801
|
+
},
|
|
1802
|
+
architecture: {
|
|
1803
|
+
class: "replay-shell",
|
|
1804
|
+
summary: "Current internal Box package is a bootstrap scaffold with no registered tools or REST behavior; promote to standard-core only after file/folder/admin handlers mutate StateEngine and sequence invariants land.",
|
|
1805
|
+
domainPrimitives: [
|
|
1806
|
+
"file and folder hierarchy state",
|
|
1807
|
+
"shared-link and collaboration state",
|
|
1808
|
+
"future change-stream behavior"
|
|
1809
|
+
],
|
|
1810
|
+
promotionEvidence: [
|
|
1811
|
+
"clones/box/SPEC.md tracks file/folder/admin handlers mutating StateEngine",
|
|
1812
|
+
"clones/box/SPEC.md tracks create/read/update/delete/reset sequence invariants"
|
|
1813
|
+
]
|
|
1814
|
+
}
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
name: "calcom",
|
|
1818
|
+
package: "@archal/clone-calcom",
|
|
1819
|
+
path: "clones/calcom",
|
|
1820
|
+
stage: "preview",
|
|
1821
|
+
display: {
|
|
1822
|
+
icon: "CC",
|
|
1823
|
+
name: "Cal.com",
|
|
1824
|
+
description: "Event types, schedules, availability slots, bookings, calendars, and webhooks.",
|
|
1825
|
+
toolCount: 18
|
|
1826
|
+
},
|
|
1827
|
+
transport: "both",
|
|
1828
|
+
hostedRuntime: {
|
|
1829
|
+
enabled: true,
|
|
1830
|
+
requiresDist: true,
|
|
1831
|
+
requiresEmptySeed: true
|
|
1832
|
+
},
|
|
1833
|
+
cli: {
|
|
1834
|
+
bundleAssets: true,
|
|
1835
|
+
bundleToolSnapshot: true
|
|
1836
|
+
},
|
|
1837
|
+
routeProxy: {
|
|
1838
|
+
enabled: true,
|
|
1839
|
+
domains: [
|
|
1840
|
+
{
|
|
1841
|
+
domain: "api.cal.com"
|
|
1842
|
+
}
|
|
1843
|
+
]
|
|
1844
|
+
},
|
|
1845
|
+
seeds: {
|
|
1846
|
+
generatedCatalog: true,
|
|
1847
|
+
default: "empty"
|
|
1848
|
+
},
|
|
1849
|
+
smoke: {
|
|
1850
|
+
production: "health-and-seed"
|
|
1851
|
+
},
|
|
1852
|
+
architecture: {
|
|
1853
|
+
class: "domain-simulator",
|
|
1854
|
+
summary: "Cal.com relies on availability computation, booking lifecycle, calendar projection, and webhook lifecycle semantics beyond generic CRUD.",
|
|
1855
|
+
domainPrimitives: [
|
|
1856
|
+
"schedule and event-type availability projection",
|
|
1857
|
+
"booking create, cancel, and reschedule lifecycle",
|
|
1858
|
+
"host-wide busy interval and slot computation",
|
|
1859
|
+
"webhook summary/detail lifecycle projection"
|
|
1860
|
+
],
|
|
1861
|
+
behaviorOwner: "src/state/relationships.ts"
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
name: "clickup",
|
|
1866
|
+
package: "@archal/clone-clickup",
|
|
1867
|
+
path: "clones/clickup",
|
|
1868
|
+
stage: "preview",
|
|
1869
|
+
display: {
|
|
1870
|
+
icon: "CU",
|
|
1871
|
+
name: "ClickUp",
|
|
1872
|
+
description: "Teams, spaces, folders, lists, tasks, comments, tags, time tracking, and checklists.",
|
|
1873
|
+
toolCount: 40
|
|
1874
|
+
},
|
|
1875
|
+
transport: "both",
|
|
1876
|
+
hostedRuntime: {
|
|
1877
|
+
enabled: true,
|
|
1878
|
+
requiresDist: true,
|
|
1879
|
+
requiresEmptySeed: true
|
|
1880
|
+
},
|
|
1881
|
+
cli: {
|
|
1882
|
+
bundleAssets: true,
|
|
1883
|
+
bundleToolSnapshot: true
|
|
1884
|
+
},
|
|
1885
|
+
routeProxy: {
|
|
1886
|
+
enabled: true,
|
|
1887
|
+
domains: [
|
|
1888
|
+
{
|
|
1889
|
+
domain: "api.clickup.com"
|
|
1890
|
+
}
|
|
1891
|
+
]
|
|
1892
|
+
},
|
|
1893
|
+
seeds: {
|
|
1894
|
+
generatedCatalog: true,
|
|
1895
|
+
default: "empty"
|
|
1896
|
+
},
|
|
1897
|
+
smoke: {
|
|
1898
|
+
production: "health-and-seed"
|
|
1899
|
+
},
|
|
1900
|
+
architecture: {
|
|
1901
|
+
class: "standard-core",
|
|
1902
|
+
summary: "Current task/project/comment workflows are modeled as mutable records plus relationships."
|
|
1903
|
+
}
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
name: "customerio",
|
|
1907
|
+
package: "@archal/clone-customerio",
|
|
1908
|
+
path: "clones/customerio",
|
|
1909
|
+
stage: "preview",
|
|
1910
|
+
display: {
|
|
1911
|
+
icon: "CI",
|
|
1912
|
+
name: "Customer.io",
|
|
1913
|
+
description: "Messaging automation across campaigns, broadcasts, transactional email, segments, customers, and message delivery.",
|
|
1914
|
+
toolCount: 22
|
|
1915
|
+
},
|
|
1916
|
+
transport: "rest",
|
|
1917
|
+
hostedRuntime: {
|
|
1918
|
+
enabled: true,
|
|
1919
|
+
requiresDist: true,
|
|
1920
|
+
requiresEmptySeed: true
|
|
1921
|
+
},
|
|
1922
|
+
cli: {
|
|
1923
|
+
bundleAssets: true,
|
|
1924
|
+
bundleToolSnapshot: true
|
|
1925
|
+
},
|
|
1926
|
+
routeProxy: {
|
|
1927
|
+
enabled: true,
|
|
1928
|
+
domains: [
|
|
1929
|
+
{
|
|
1930
|
+
domain: "api.customer.io"
|
|
1931
|
+
}
|
|
1932
|
+
]
|
|
1933
|
+
},
|
|
1934
|
+
seeds: {
|
|
1935
|
+
generatedCatalog: true,
|
|
1936
|
+
default: "empty"
|
|
1937
|
+
},
|
|
1938
|
+
smoke: {
|
|
1939
|
+
production: "clone-surface"
|
|
1940
|
+
},
|
|
1941
|
+
architecture: {
|
|
1942
|
+
class: "standard-core",
|
|
1943
|
+
summary: "Current preview slice models campaigns, broadcasts, segments, customers, triggers, and message delivery as deterministic StateEngine records; Track API, webhook delivery, and real audience projection are not claimed."
|
|
1944
|
+
}
|
|
1945
|
+
},
|
|
1946
|
+
{
|
|
1947
|
+
name: "datadog",
|
|
1948
|
+
package: "@archal/clone-datadog",
|
|
1949
|
+
path: "clones/datadog",
|
|
1950
|
+
stage: "preview",
|
|
1951
|
+
display: {
|
|
1952
|
+
icon: "DD",
|
|
1953
|
+
name: "Datadog",
|
|
1954
|
+
description: "Observability REST API \u2014 metrics, logs, monitors, dashboards, SLOs, incidents, teams, users, and service definitions.",
|
|
1955
|
+
toolCount: 63
|
|
1956
|
+
},
|
|
1957
|
+
transport: "rest",
|
|
1958
|
+
hostedRuntime: {
|
|
1959
|
+
enabled: true,
|
|
1960
|
+
requiresDist: true,
|
|
1961
|
+
requiresEmptySeed: true
|
|
1962
|
+
},
|
|
1963
|
+
cli: {
|
|
1964
|
+
bundleAssets: true,
|
|
1965
|
+
bundleToolSnapshot: true
|
|
1966
|
+
},
|
|
1967
|
+
routeProxy: {
|
|
1968
|
+
enabled: true,
|
|
1969
|
+
domains: [
|
|
1970
|
+
{
|
|
1971
|
+
domain: "api.datadoghq.com"
|
|
1972
|
+
}
|
|
1973
|
+
]
|
|
1974
|
+
},
|
|
1975
|
+
seeds: {
|
|
1976
|
+
generatedCatalog: true,
|
|
1977
|
+
default: "empty"
|
|
1978
|
+
},
|
|
1979
|
+
smoke: {
|
|
1980
|
+
production: "clone-surface"
|
|
1981
|
+
},
|
|
1982
|
+
architecture: {
|
|
1983
|
+
class: "domain-simulator",
|
|
1984
|
+
summary: "Observability behavior depends on time-windowed queries and monitor state, so REST routes must stay thin over DatadogSimulator.",
|
|
1985
|
+
domainPrimitives: [
|
|
1986
|
+
"time-series query windows",
|
|
1987
|
+
"log query filtering",
|
|
1988
|
+
"monitor evaluation and alert state"
|
|
1989
|
+
],
|
|
1990
|
+
behaviorOwner: "src/domain/simulator.ts"
|
|
1991
|
+
}
|
|
1992
|
+
},
|
|
1993
|
+
{
|
|
1994
|
+
name: "discord",
|
|
1995
|
+
package: "@archal/clone-discord",
|
|
1996
|
+
path: "clones/discord",
|
|
1997
|
+
stage: "preview",
|
|
1998
|
+
display: {
|
|
1999
|
+
icon: "DC",
|
|
2000
|
+
name: "Discord",
|
|
2001
|
+
description: "Guilds, channels, messages, webhooks, threads, commands, and interaction responses.",
|
|
2002
|
+
toolCount: 67
|
|
2003
|
+
},
|
|
2004
|
+
transport: "both",
|
|
2005
|
+
hostedRuntime: {
|
|
2006
|
+
enabled: true,
|
|
2007
|
+
requiresDist: true,
|
|
2008
|
+
requiresEmptySeed: true
|
|
2009
|
+
},
|
|
2010
|
+
cli: {
|
|
2011
|
+
bundleAssets: true,
|
|
2012
|
+
bundleToolSnapshot: true
|
|
2013
|
+
},
|
|
2014
|
+
routeProxy: {
|
|
2015
|
+
enabled: true,
|
|
2016
|
+
domains: [
|
|
2017
|
+
{
|
|
2018
|
+
domain: "discord.com"
|
|
2019
|
+
},
|
|
2020
|
+
{
|
|
2021
|
+
domain: "discordapp.com"
|
|
2022
|
+
}
|
|
2023
|
+
]
|
|
2024
|
+
},
|
|
2025
|
+
seeds: {
|
|
2026
|
+
generatedCatalog: true,
|
|
2027
|
+
default: "small-server"
|
|
2028
|
+
},
|
|
2029
|
+
smoke: {
|
|
2030
|
+
production: "health-and-seed"
|
|
2031
|
+
},
|
|
2032
|
+
architecture: {
|
|
2033
|
+
class: "domain-simulator",
|
|
2034
|
+
summary: "Chat correctness depends on channel/message/thread ordering, membership visibility, and event delivery.",
|
|
2035
|
+
domainPrimitives: [
|
|
2036
|
+
"channel membership",
|
|
2037
|
+
"message timeline ordering",
|
|
2038
|
+
"thread and event delivery semantics"
|
|
2039
|
+
],
|
|
2040
|
+
behaviorOwner: "src/domain/discord-simulator.ts"
|
|
2041
|
+
},
|
|
2042
|
+
inferenceKeywords: [
|
|
2043
|
+
"discord",
|
|
2044
|
+
"guild",
|
|
2045
|
+
"text channel",
|
|
2046
|
+
"thread",
|
|
2047
|
+
"webhook",
|
|
2048
|
+
"slash command"
|
|
2049
|
+
]
|
|
2050
|
+
},
|
|
2051
|
+
{
|
|
2052
|
+
name: "dropbox",
|
|
2053
|
+
package: "@archal/clone-dropbox",
|
|
2054
|
+
path: "clones/dropbox",
|
|
2055
|
+
stage: "internal",
|
|
2056
|
+
transport: "mcp",
|
|
2057
|
+
hostedRuntime: {
|
|
2058
|
+
enabled: true,
|
|
2059
|
+
requiresDist: true,
|
|
2060
|
+
requiresEmptySeed: true
|
|
2061
|
+
},
|
|
2062
|
+
cli: {
|
|
2063
|
+
bundleAssets: false,
|
|
2064
|
+
bundleToolSnapshot: false
|
|
2065
|
+
},
|
|
2066
|
+
routeProxy: {
|
|
2067
|
+
enabled: false,
|
|
2068
|
+
domains: []
|
|
2069
|
+
},
|
|
2070
|
+
seeds: {
|
|
2071
|
+
generatedCatalog: false,
|
|
2072
|
+
default: "empty"
|
|
2073
|
+
},
|
|
2074
|
+
smoke: {
|
|
2075
|
+
production: "none"
|
|
2076
|
+
},
|
|
2077
|
+
architecture: {
|
|
2078
|
+
class: "domain-simulator",
|
|
2079
|
+
summary: "A faithful Dropbox surface depends on file tree, revision, sharing, and delta/list-folder cursor semantics.",
|
|
2080
|
+
domainPrimitives: [
|
|
2081
|
+
"file tree and revisions",
|
|
2082
|
+
"sharing permissions",
|
|
2083
|
+
"delta/list-folder cursors"
|
|
2084
|
+
],
|
|
2085
|
+
behaviorOwner: "src/domain/files.ts"
|
|
2086
|
+
}
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
name: "figma",
|
|
2090
|
+
package: "@archal/clone-figma",
|
|
2091
|
+
path: "clones/figma",
|
|
2092
|
+
stage: "internal",
|
|
2093
|
+
transport: "mcp",
|
|
2094
|
+
hostedRuntime: {
|
|
2095
|
+
enabled: true,
|
|
2096
|
+
requiresDist: true,
|
|
2097
|
+
requiresEmptySeed: true
|
|
2098
|
+
},
|
|
2099
|
+
cli: {
|
|
2100
|
+
bundleAssets: false,
|
|
2101
|
+
bundleToolSnapshot: false
|
|
2102
|
+
},
|
|
2103
|
+
routeProxy: {
|
|
2104
|
+
enabled: false,
|
|
2105
|
+
domains: []
|
|
2106
|
+
},
|
|
2107
|
+
seeds: {
|
|
2108
|
+
generatedCatalog: false,
|
|
2109
|
+
default: "empty"
|
|
2110
|
+
},
|
|
2111
|
+
smoke: {
|
|
2112
|
+
production: "none"
|
|
2113
|
+
},
|
|
2114
|
+
architecture: {
|
|
2115
|
+
class: "standard-core",
|
|
2116
|
+
summary: "Current internal Figma slice is treated as deterministic file/project records; live collaboration semantics are not claimed."
|
|
2117
|
+
}
|
|
2118
|
+
},
|
|
2119
|
+
{
|
|
2120
|
+
name: "firecrawl",
|
|
2121
|
+
package: "@archal/clone-firecrawl",
|
|
2122
|
+
path: "clones/firecrawl",
|
|
2123
|
+
stage: "internal",
|
|
2124
|
+
display: {
|
|
2125
|
+
icon: "FC",
|
|
2126
|
+
name: "Firecrawl",
|
|
2127
|
+
description: "Scraping, crawling, mapping, search, and extraction.",
|
|
2128
|
+
toolCount: 6
|
|
2129
|
+
},
|
|
2130
|
+
transport: "rest",
|
|
2131
|
+
hostedRuntime: {
|
|
2132
|
+
enabled: true,
|
|
2133
|
+
requiresDist: true,
|
|
2134
|
+
requiresEmptySeed: true
|
|
2135
|
+
},
|
|
2136
|
+
cli: {
|
|
2137
|
+
bundleAssets: false,
|
|
2138
|
+
bundleToolSnapshot: false
|
|
2139
|
+
},
|
|
2140
|
+
routeProxy: {
|
|
2141
|
+
enabled: false,
|
|
2142
|
+
domains: []
|
|
2143
|
+
},
|
|
2144
|
+
seeds: {
|
|
2145
|
+
generatedCatalog: false,
|
|
2146
|
+
default: "empty"
|
|
2147
|
+
},
|
|
2148
|
+
smoke: {
|
|
2149
|
+
production: "none"
|
|
2150
|
+
},
|
|
2151
|
+
architecture: {
|
|
2152
|
+
class: "replay-shell",
|
|
2153
|
+
summary: "Current Firecrawl surface is recorded-response replay with request-key matching; crawling engine behavior is not modeled yet.",
|
|
2154
|
+
domainPrimitives: [
|
|
2155
|
+
"recording-backed scrape/search/map response replay",
|
|
2156
|
+
"request key canonicalization",
|
|
2157
|
+
"future crawl/job lifecycle simulator"
|
|
2158
|
+
],
|
|
2159
|
+
promotionEvidence: [
|
|
2160
|
+
"clones/firecrawl/SPEC.md tracks crawl/job lifecycle state model",
|
|
2161
|
+
"clones/firecrawl/SPEC.md tracks runtime recording-backed handlers replaced by state-derived crawl/job responses",
|
|
2162
|
+
"clones/firecrawl/SPEC.md tracks status/progress/replay determinism invariants"
|
|
2163
|
+
]
|
|
2164
|
+
}
|
|
2165
|
+
},
|
|
2166
|
+
{
|
|
2167
|
+
name: "freshdesk",
|
|
2168
|
+
package: "@archal/clone-freshdesk",
|
|
2169
|
+
path: "clones/freshdesk",
|
|
2170
|
+
stage: "internal",
|
|
2171
|
+
transport: "rest",
|
|
2172
|
+
hostedRuntime: {
|
|
2173
|
+
enabled: true,
|
|
2174
|
+
requiresDist: true,
|
|
2175
|
+
requiresEmptySeed: true
|
|
2176
|
+
},
|
|
2177
|
+
cli: {
|
|
2178
|
+
bundleAssets: false,
|
|
2179
|
+
bundleToolSnapshot: false
|
|
2180
|
+
},
|
|
2181
|
+
routeProxy: {
|
|
2182
|
+
enabled: false,
|
|
2183
|
+
domains: []
|
|
2184
|
+
},
|
|
2185
|
+
seeds: {
|
|
2186
|
+
generatedCatalog: false,
|
|
2187
|
+
default: "empty"
|
|
2188
|
+
},
|
|
2189
|
+
smoke: {
|
|
2190
|
+
production: "none"
|
|
2191
|
+
},
|
|
2192
|
+
architecture: {
|
|
2193
|
+
class: "replay-shell",
|
|
2194
|
+
summary: "Current internal Freshdesk package is a bootstrap scaffold with no registered tools or REST behavior; promote to standard-core only after ticket/contact/conversation handlers mutate StateEngine and sequence invariants land.",
|
|
2195
|
+
domainPrimitives: [
|
|
2196
|
+
"ticket status transitions",
|
|
2197
|
+
"contact and company records",
|
|
2198
|
+
"conversation and note history"
|
|
2199
|
+
],
|
|
2200
|
+
promotionEvidence: [
|
|
2201
|
+
"clones/freshdesk/SPEC.md tracks runtime recording-backed handlers replaced by state-derived ticket/contact/conversation handlers",
|
|
2202
|
+
"clones/freshdesk/SPEC.md tracks ticket/contact/conversation handlers mutating StateEngine",
|
|
2203
|
+
"clones/freshdesk/SPEC.md tracks comment/status/read-after-write/reset sequence invariants"
|
|
2204
|
+
]
|
|
2205
|
+
}
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
name: "github",
|
|
2209
|
+
package: "@archal/clone-github",
|
|
2210
|
+
path: "clones/github",
|
|
2211
|
+
stage: "public",
|
|
2212
|
+
display: {
|
|
2213
|
+
icon: "GH",
|
|
2214
|
+
name: "GitHub",
|
|
2215
|
+
description: "Repos, issues, pull requests, branches, and commits.",
|
|
2216
|
+
toolCount: 26
|
|
2217
|
+
},
|
|
2218
|
+
transport: "both",
|
|
2219
|
+
hostedRuntime: {
|
|
2220
|
+
enabled: true,
|
|
2221
|
+
requiresDist: true,
|
|
2222
|
+
requiresEmptySeed: true
|
|
2223
|
+
},
|
|
2224
|
+
cli: {
|
|
2225
|
+
bundleAssets: true,
|
|
2226
|
+
bundleToolSnapshot: true
|
|
2227
|
+
},
|
|
2228
|
+
routeProxy: {
|
|
2229
|
+
enabled: true,
|
|
2230
|
+
domains: [
|
|
2231
|
+
{
|
|
2232
|
+
domain: "api.github.com"
|
|
2233
|
+
},
|
|
2234
|
+
{
|
|
2235
|
+
domain: "uploads.github.com"
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
domain: "github.com"
|
|
2239
|
+
},
|
|
2240
|
+
{
|
|
2241
|
+
domain: "raw.githubusercontent.com"
|
|
2242
|
+
}
|
|
2243
|
+
]
|
|
2244
|
+
},
|
|
2245
|
+
seeds: {
|
|
2246
|
+
generatedCatalog: true,
|
|
2247
|
+
default: "small-project"
|
|
2248
|
+
},
|
|
2249
|
+
smoke: {
|
|
2250
|
+
production: "health-and-seed"
|
|
2251
|
+
},
|
|
2252
|
+
architecture: {
|
|
2253
|
+
class: "domain-simulator",
|
|
2254
|
+
summary: "GitHub relies on repository, issue, pull-request, review, workflow, permission, and webhook semantics beyond generic CRUD.",
|
|
2255
|
+
domainPrimitives: [
|
|
2256
|
+
"repository branch, commit, tree, and diff projection",
|
|
2257
|
+
"issue and pull-request lifecycle projection",
|
|
2258
|
+
"review, comment, and status/check aggregation",
|
|
2259
|
+
"permission-scoped reads and webhook event delivery"
|
|
2260
|
+
],
|
|
2261
|
+
behaviorOwner: "src/state/transition-rules.ts"
|
|
2262
|
+
},
|
|
2263
|
+
inferenceKeywords: [
|
|
2264
|
+
"github",
|
|
2265
|
+
"repository",
|
|
2266
|
+
"pull request",
|
|
2267
|
+
"issue",
|
|
2268
|
+
"create_issue",
|
|
2269
|
+
"create_pull_request",
|
|
2270
|
+
"merge_pull_request"
|
|
2271
|
+
]
|
|
2272
|
+
},
|
|
2273
|
+
{
|
|
2274
|
+
name: "gitlab",
|
|
2275
|
+
package: "@archal/clone-gitlab",
|
|
2276
|
+
path: "clones/gitlab",
|
|
2277
|
+
stage: "preview",
|
|
2278
|
+
display: {
|
|
2279
|
+
icon: "GL",
|
|
2280
|
+
name: "GitLab",
|
|
2281
|
+
description: "Projects, branches, commits, issues, merge requests, pipelines, labels, milestones, releases, and webhooks.",
|
|
2282
|
+
toolCount: 46
|
|
2283
|
+
},
|
|
2284
|
+
transport: "both",
|
|
2285
|
+
hostedRuntime: {
|
|
2286
|
+
enabled: true,
|
|
2287
|
+
requiresDist: true,
|
|
2288
|
+
requiresEmptySeed: true
|
|
2289
|
+
},
|
|
2290
|
+
cli: {
|
|
2291
|
+
bundleAssets: true,
|
|
2292
|
+
bundleToolSnapshot: true
|
|
2293
|
+
},
|
|
2294
|
+
routeProxy: {
|
|
2295
|
+
enabled: true,
|
|
2296
|
+
domains: [
|
|
2297
|
+
{
|
|
2298
|
+
domain: "gitlab.com"
|
|
2299
|
+
}
|
|
2300
|
+
]
|
|
2301
|
+
},
|
|
2302
|
+
seeds: {
|
|
2303
|
+
generatedCatalog: true,
|
|
2304
|
+
default: "empty"
|
|
2305
|
+
},
|
|
2306
|
+
smoke: {
|
|
2307
|
+
production: "health-and-seed"
|
|
2308
|
+
},
|
|
2309
|
+
architecture: {
|
|
2310
|
+
class: "domain-simulator",
|
|
2311
|
+
summary: "Current GitLab workflows combine StateEngine records with GitLab-specific repository, issue, merge-request, note, webhook, and event-delivery semantics.",
|
|
2312
|
+
domainPrimitives: [
|
|
2313
|
+
"issue and merge-request state transitions",
|
|
2314
|
+
"note/comment ordering and count projection",
|
|
2315
|
+
"repository branch, commit, merge, and diff projection",
|
|
2316
|
+
"GitLab webhook subscription, payload, and header emission"
|
|
2317
|
+
],
|
|
2318
|
+
behaviorOwner: "src/state/webhook-delivery-config.ts"
|
|
2319
|
+
}
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
name: "google-workspace",
|
|
2323
|
+
package: "@archal/clone-google-workspace",
|
|
2324
|
+
path: "clones/google-workspace",
|
|
2325
|
+
stage: "preview",
|
|
2326
|
+
display: {
|
|
2327
|
+
icon: "GW",
|
|
2328
|
+
name: "Google Workspace",
|
|
2329
|
+
description: "Gmail, Calendar, Drive, Sheets, and Contacts.",
|
|
2330
|
+
toolCount: 249
|
|
2331
|
+
},
|
|
2332
|
+
transport: "both",
|
|
2333
|
+
hostedRuntime: {
|
|
2334
|
+
enabled: true,
|
|
2335
|
+
requiresDist: true,
|
|
2336
|
+
requiresEmptySeed: true
|
|
2337
|
+
},
|
|
2338
|
+
cli: {
|
|
2339
|
+
bundleAssets: true,
|
|
2340
|
+
bundleToolSnapshot: true
|
|
2341
|
+
},
|
|
2342
|
+
routeProxy: {
|
|
2343
|
+
enabled: true,
|
|
2344
|
+
domains: [
|
|
2345
|
+
{
|
|
2346
|
+
domain: "gmail.googleapis.com"
|
|
2347
|
+
},
|
|
2348
|
+
{
|
|
2349
|
+
domain: "drive.googleapis.com"
|
|
2350
|
+
},
|
|
2351
|
+
{
|
|
2352
|
+
domain: "calendar.googleapis.com"
|
|
2353
|
+
},
|
|
2354
|
+
{
|
|
2355
|
+
domain: "people.googleapis.com"
|
|
2356
|
+
},
|
|
2357
|
+
{
|
|
2358
|
+
domain: "sheets.googleapis.com"
|
|
2359
|
+
},
|
|
2360
|
+
{
|
|
2361
|
+
domain: "oauth2.googleapis.com"
|
|
2362
|
+
},
|
|
2363
|
+
{
|
|
2364
|
+
domain: "www.googleapis.com"
|
|
2365
|
+
}
|
|
2366
|
+
]
|
|
2367
|
+
},
|
|
2368
|
+
seeds: {
|
|
2369
|
+
generatedCatalog: true,
|
|
2370
|
+
default: "assistant-baseline"
|
|
2371
|
+
},
|
|
2372
|
+
smoke: {
|
|
2373
|
+
production: "health-and-seed"
|
|
2374
|
+
},
|
|
2375
|
+
architecture: {
|
|
2376
|
+
class: "domain-simulator",
|
|
2377
|
+
summary: "Gmail, Drive, Calendar, and watches rely on histories, change feeds, recurrence, sync tokens, and permission projection.",
|
|
2378
|
+
domainPrimitives: [
|
|
2379
|
+
"Gmail thread/history engine",
|
|
2380
|
+
"Drive changes and permissions",
|
|
2381
|
+
"Calendar recurrence and watch sync"
|
|
2382
|
+
],
|
|
2383
|
+
behaviorOwner: "src/domain/google-workspace-simulator.ts"
|
|
2384
|
+
},
|
|
2385
|
+
inferenceKeywords: [
|
|
2386
|
+
"google workspace",
|
|
2387
|
+
"gmail",
|
|
2388
|
+
"google calendar",
|
|
2389
|
+
"calendar event",
|
|
2390
|
+
"inbox",
|
|
2391
|
+
"email draft",
|
|
2392
|
+
"google drive",
|
|
2393
|
+
"google sheet",
|
|
2394
|
+
"google sheets",
|
|
2395
|
+
"contacts"
|
|
2396
|
+
]
|
|
2397
|
+
},
|
|
2398
|
+
{
|
|
2399
|
+
name: "hubspot",
|
|
2400
|
+
package: "@archal/clone-hubspot",
|
|
2401
|
+
path: "clones/hubspot",
|
|
2402
|
+
stage: "preview",
|
|
2403
|
+
display: {
|
|
2404
|
+
icon: "HS",
|
|
2405
|
+
name: "HubSpot",
|
|
2406
|
+
description: "CRM contacts, companies, deals, tickets, and engagements.",
|
|
2407
|
+
toolCount: 41
|
|
2408
|
+
},
|
|
2409
|
+
transport: "rest",
|
|
2410
|
+
hostedRuntime: {
|
|
2411
|
+
enabled: true,
|
|
2412
|
+
requiresDist: true,
|
|
2413
|
+
requiresEmptySeed: true
|
|
2414
|
+
},
|
|
2415
|
+
cli: {
|
|
2416
|
+
bundleAssets: true,
|
|
2417
|
+
bundleToolSnapshot: true
|
|
2418
|
+
},
|
|
2419
|
+
routeProxy: {
|
|
2420
|
+
enabled: true,
|
|
2421
|
+
domains: [
|
|
2422
|
+
{
|
|
2423
|
+
domain: "api.hubapi.com"
|
|
2424
|
+
}
|
|
2425
|
+
]
|
|
2426
|
+
},
|
|
2427
|
+
seeds: {
|
|
2428
|
+
generatedCatalog: true,
|
|
2429
|
+
default: "empty"
|
|
2430
|
+
},
|
|
2431
|
+
smoke: {
|
|
2432
|
+
production: "clone-surface"
|
|
2433
|
+
},
|
|
2434
|
+
architecture: {
|
|
2435
|
+
class: "replay-shell",
|
|
2436
|
+
summary: "HubSpot has a stateful CRM overlay, but the exported REST/MCP surface still includes broad runtime recording replay.",
|
|
2437
|
+
domainPrimitives: [
|
|
2438
|
+
"CRM object state and associations",
|
|
2439
|
+
"Pipeline and property schema state",
|
|
2440
|
+
"Recording-backed REST/MCP aliases awaiting stateful lift"
|
|
2441
|
+
],
|
|
2442
|
+
promotionEvidence: [
|
|
2443
|
+
"clones/hubspot/SPEC.md tracks runtime recording reads removed from exported handlers",
|
|
2444
|
+
"clones/hubspot/SPEC.md tracks CRM alias and association sequence invariants"
|
|
2445
|
+
]
|
|
2446
|
+
}
|
|
2447
|
+
},
|
|
2448
|
+
{
|
|
2449
|
+
name: "jira",
|
|
2450
|
+
package: "@archal/clone-jira",
|
|
2451
|
+
path: "clones/jira",
|
|
2452
|
+
stage: "preview",
|
|
2453
|
+
display: {
|
|
2454
|
+
icon: "JR",
|
|
2455
|
+
name: "Jira",
|
|
2456
|
+
description: "Issues, projects, boards, sprints, and versions.",
|
|
2457
|
+
toolCount: 49
|
|
2458
|
+
},
|
|
2459
|
+
transport: "both",
|
|
2460
|
+
hostedRuntime: {
|
|
2461
|
+
enabled: true,
|
|
2462
|
+
requiresDist: true,
|
|
2463
|
+
requiresEmptySeed: true
|
|
2464
|
+
},
|
|
2465
|
+
cli: {
|
|
2466
|
+
bundleAssets: true,
|
|
2467
|
+
bundleToolSnapshot: true
|
|
2468
|
+
},
|
|
2469
|
+
routeProxy: {
|
|
2470
|
+
enabled: true,
|
|
2471
|
+
domains: [
|
|
2472
|
+
{
|
|
2473
|
+
domain: "atlassian.net",
|
|
2474
|
+
matchSubdomains: true
|
|
2475
|
+
},
|
|
2476
|
+
{
|
|
2477
|
+
domain: "api.atlassian.com"
|
|
2478
|
+
},
|
|
2479
|
+
{
|
|
2480
|
+
domain: "jira.atlassian.com"
|
|
2481
|
+
}
|
|
2482
|
+
]
|
|
2483
|
+
},
|
|
2484
|
+
seeds: {
|
|
2485
|
+
generatedCatalog: true,
|
|
2486
|
+
default: "small-project"
|
|
2487
|
+
},
|
|
2488
|
+
smoke: {
|
|
2489
|
+
production: "health-and-seed"
|
|
2490
|
+
},
|
|
2491
|
+
architecture: {
|
|
2492
|
+
class: "domain-simulator",
|
|
2493
|
+
summary: "Jira relies on issue workflow, JQL/search, sprint/version timelines, permission projection, history, and webhook semantics beyond generic CRUD.",
|
|
2494
|
+
domainPrimitives: [
|
|
2495
|
+
"issue workflow transitions and mutation history",
|
|
2496
|
+
"JQL/search and issue field projection",
|
|
2497
|
+
"sprint, board, version, and SLA time-based transitions",
|
|
2498
|
+
"permission-scoped reads and Jira webhook event delivery"
|
|
2499
|
+
],
|
|
2500
|
+
behaviorOwner: "src/state/transition-rules.ts"
|
|
2501
|
+
},
|
|
2502
|
+
inferenceKeywords: [
|
|
2503
|
+
"jira",
|
|
2504
|
+
"jira sprint",
|
|
2505
|
+
"jira epic",
|
|
2506
|
+
"jira board",
|
|
2507
|
+
"sprint",
|
|
2508
|
+
"epic"
|
|
2509
|
+
]
|
|
2510
|
+
},
|
|
2511
|
+
{
|
|
2512
|
+
name: "linear",
|
|
2513
|
+
package: "@archal/clone-linear",
|
|
2514
|
+
path: "clones/linear",
|
|
2515
|
+
stage: "public",
|
|
2516
|
+
display: {
|
|
2517
|
+
icon: "LN",
|
|
2518
|
+
name: "Linear",
|
|
2519
|
+
description: "Issues, projects, teams, cycles, and workflows.",
|
|
2520
|
+
toolCount: 50
|
|
2521
|
+
},
|
|
2522
|
+
transport: "both",
|
|
2523
|
+
hostedRuntime: {
|
|
2524
|
+
enabled: true,
|
|
2525
|
+
requiresDist: true,
|
|
2526
|
+
requiresEmptySeed: true
|
|
2527
|
+
},
|
|
2528
|
+
cli: {
|
|
2529
|
+
bundleAssets: true,
|
|
2530
|
+
bundleToolSnapshot: true
|
|
2531
|
+
},
|
|
2532
|
+
routeProxy: {
|
|
2533
|
+
enabled: true,
|
|
2534
|
+
domains: [
|
|
2535
|
+
{
|
|
2536
|
+
domain: "api.linear.app"
|
|
2537
|
+
}
|
|
2538
|
+
]
|
|
2539
|
+
},
|
|
2540
|
+
seeds: {
|
|
2541
|
+
generatedCatalog: true,
|
|
2542
|
+
default: "small-team"
|
|
2543
|
+
},
|
|
2544
|
+
smoke: {
|
|
2545
|
+
production: "health-and-seed"
|
|
2546
|
+
},
|
|
2547
|
+
architecture: {
|
|
2548
|
+
class: "domain-simulator",
|
|
2549
|
+
summary: "Current Linear workflows combine deterministic records with Linear-specific GraphQL dispatch, validation, issue workflow transitions, comments, cycles, and derived projections.",
|
|
2550
|
+
domainPrimitives: [
|
|
2551
|
+
"GraphQL operation dispatch",
|
|
2552
|
+
"issue workflow transitions",
|
|
2553
|
+
"comment and history projection",
|
|
2554
|
+
"cycle and project progress projection"
|
|
2555
|
+
],
|
|
2556
|
+
behaviorOwner: "src/rest/graphql-dispatch.ts"
|
|
2557
|
+
},
|
|
2558
|
+
inferenceKeywords: [
|
|
2559
|
+
"linear",
|
|
2560
|
+
"linear ticket",
|
|
2561
|
+
"linear project",
|
|
2562
|
+
"linear cycle",
|
|
2563
|
+
"cycle"
|
|
2564
|
+
]
|
|
2565
|
+
},
|
|
2566
|
+
{
|
|
2567
|
+
name: "mailchimp",
|
|
2568
|
+
package: "@archal/clone-mailchimp",
|
|
2569
|
+
path: "clones/mailchimp",
|
|
2570
|
+
stage: "internal",
|
|
2571
|
+
transport: "mcp",
|
|
2572
|
+
hostedRuntime: {
|
|
2573
|
+
enabled: true,
|
|
2574
|
+
requiresDist: true,
|
|
2575
|
+
requiresEmptySeed: true
|
|
2576
|
+
},
|
|
2577
|
+
cli: {
|
|
2578
|
+
bundleAssets: false,
|
|
2579
|
+
bundleToolSnapshot: false
|
|
2580
|
+
},
|
|
2581
|
+
routeProxy: {
|
|
2582
|
+
enabled: false,
|
|
2583
|
+
domains: []
|
|
2584
|
+
},
|
|
2585
|
+
seeds: {
|
|
2586
|
+
generatedCatalog: false,
|
|
2587
|
+
default: "empty"
|
|
2588
|
+
},
|
|
2589
|
+
smoke: {
|
|
2590
|
+
production: "none"
|
|
2591
|
+
},
|
|
2592
|
+
architecture: {
|
|
2593
|
+
class: "standard-core",
|
|
2594
|
+
summary: "Current internal Mailchimp slice models lists, members, merge fields, segments, campaign content, and campaign status as deterministic StateEngine records; send delivery, reports, and real audience activity history are not claimed."
|
|
2595
|
+
}
|
|
2596
|
+
},
|
|
2597
|
+
{
|
|
2598
|
+
name: "ownerrez",
|
|
2599
|
+
package: "@archal/clone-ownerrez",
|
|
2600
|
+
path: "clones/ownerrez",
|
|
2601
|
+
stage: "preview",
|
|
2602
|
+
display: {
|
|
2603
|
+
icon: "OR",
|
|
2604
|
+
name: "OwnerRez",
|
|
2605
|
+
description: "Vacation-rental PMS \u2014 properties, bookings, guests, quotes, financial reads, tags, fields, and webhook subscriptions.",
|
|
2606
|
+
toolCount: 25
|
|
2607
|
+
},
|
|
2608
|
+
transport: "rest",
|
|
2609
|
+
hostedRuntime: {
|
|
2610
|
+
enabled: true,
|
|
2611
|
+
requiresDist: true,
|
|
2612
|
+
requiresEmptySeed: true
|
|
2613
|
+
},
|
|
2614
|
+
cli: {
|
|
2615
|
+
bundleAssets: true,
|
|
2616
|
+
bundleToolSnapshot: true
|
|
2617
|
+
},
|
|
2618
|
+
routeProxy: {
|
|
2619
|
+
enabled: true,
|
|
2620
|
+
domains: [
|
|
2621
|
+
{
|
|
2622
|
+
domain: "api.ownerrez.com"
|
|
2623
|
+
},
|
|
2624
|
+
{
|
|
2625
|
+
domain: "ownerrez.com"
|
|
2626
|
+
}
|
|
2627
|
+
]
|
|
2628
|
+
},
|
|
2629
|
+
seeds: {
|
|
2630
|
+
generatedCatalog: true,
|
|
2631
|
+
default: "empty"
|
|
2632
|
+
},
|
|
2633
|
+
smoke: {
|
|
2634
|
+
production: "clone-surface"
|
|
2635
|
+
},
|
|
2636
|
+
architecture: {
|
|
2637
|
+
class: "replay-shell",
|
|
2638
|
+
summary: "OwnerRez is primarily recording-replay backed with focused stateful overlays; it should not be treated as a general PMS simulator.",
|
|
2639
|
+
domainPrimitives: [
|
|
2640
|
+
"recording replay cursor",
|
|
2641
|
+
"tag and guest stateful overlays",
|
|
2642
|
+
"booking/payment read projections"
|
|
2643
|
+
],
|
|
2644
|
+
promotionEvidence: [
|
|
2645
|
+
"clones/ownerrez/SPEC.md tracks reservation or payment lifecycle state model",
|
|
2646
|
+
"clones/ownerrez/SPEC.md tracks runtime recording-backed handlers replaced for the promoted reservation/payment slice",
|
|
2647
|
+
"clones/ownerrez/SPEC.md tracks availability/payment invariants"
|
|
2648
|
+
]
|
|
2649
|
+
}
|
|
2650
|
+
},
|
|
2651
|
+
{
|
|
2652
|
+
name: "posthog",
|
|
2653
|
+
package: "@archal/clone-posthog",
|
|
2654
|
+
path: "clones/posthog",
|
|
2655
|
+
stage: "internal",
|
|
2656
|
+
transport: "rest",
|
|
2657
|
+
hostedRuntime: {
|
|
2658
|
+
enabled: true,
|
|
2659
|
+
requiresDist: true,
|
|
2660
|
+
requiresEmptySeed: true
|
|
2661
|
+
},
|
|
2662
|
+
cli: {
|
|
2663
|
+
bundleAssets: false,
|
|
2664
|
+
bundleToolSnapshot: false
|
|
2665
|
+
},
|
|
2666
|
+
routeProxy: {
|
|
2667
|
+
enabled: false,
|
|
2668
|
+
domains: []
|
|
2669
|
+
},
|
|
2670
|
+
seeds: {
|
|
2671
|
+
generatedCatalog: false,
|
|
2672
|
+
default: "empty"
|
|
2673
|
+
},
|
|
2674
|
+
smoke: {
|
|
2675
|
+
production: "none"
|
|
2676
|
+
},
|
|
2677
|
+
architecture: {
|
|
2678
|
+
class: "redesign-before-expansion",
|
|
2679
|
+
summary: "PostHog currently models admin resources and frozen insight/dashboard projections; event ingestion, query execution, and feature flag evaluation need a simulator contract before expansion.",
|
|
2680
|
+
domainPrimitives: ["event ingestion", "query execution", "feature flag evaluation"],
|
|
2681
|
+
redesignContract: "docs/internal/dev/clones/redesign-contracts/posthog.md"
|
|
2682
|
+
}
|
|
2683
|
+
},
|
|
2684
|
+
{
|
|
2685
|
+
name: "pricelabs",
|
|
2686
|
+
package: "@archal/clone-pricelabs",
|
|
2687
|
+
path: "clones/pricelabs",
|
|
2688
|
+
stage: "preview",
|
|
2689
|
+
display: {
|
|
2690
|
+
icon: "PL",
|
|
2691
|
+
name: "PriceLabs",
|
|
2692
|
+
description: "Dynamic pricing \u2014 listings, overrides, neighborhood data, rate plans, and reservations.",
|
|
2693
|
+
toolCount: 11
|
|
2694
|
+
},
|
|
2695
|
+
transport: "rest",
|
|
2696
|
+
hostedRuntime: {
|
|
2697
|
+
enabled: true,
|
|
2698
|
+
requiresDist: true,
|
|
2699
|
+
requiresEmptySeed: true
|
|
2700
|
+
},
|
|
2701
|
+
cli: {
|
|
2702
|
+
bundleAssets: true,
|
|
2703
|
+
bundleToolSnapshot: true
|
|
2704
|
+
},
|
|
2705
|
+
routeProxy: {
|
|
2706
|
+
enabled: true,
|
|
2707
|
+
domains: [
|
|
2708
|
+
{
|
|
2709
|
+
domain: "api.pricelabs.co"
|
|
2710
|
+
},
|
|
2711
|
+
{
|
|
2712
|
+
domain: "pricelabs.co"
|
|
2713
|
+
}
|
|
2714
|
+
]
|
|
2715
|
+
},
|
|
2716
|
+
seeds: {
|
|
2717
|
+
generatedCatalog: true,
|
|
2718
|
+
default: "empty"
|
|
2719
|
+
},
|
|
2720
|
+
smoke: {
|
|
2721
|
+
production: "clone-surface"
|
|
2722
|
+
},
|
|
2723
|
+
architecture: {
|
|
2724
|
+
class: "replay-shell",
|
|
2725
|
+
summary: "PriceLabs remains a replay-shell-plus-overlay surface until pricing/listing/reservation state is fully modeled.",
|
|
2726
|
+
domainPrimitives: [
|
|
2727
|
+
"recording replay cursor",
|
|
2728
|
+
"listing settings overlay",
|
|
2729
|
+
"pricing and reservation projections"
|
|
2730
|
+
],
|
|
2731
|
+
promotionEvidence: [
|
|
2732
|
+
"clones/pricelabs/SPEC.md tracks pricing calendar or reservation lifecycle state model",
|
|
2733
|
+
"clones/pricelabs/SPEC.md tracks runtime recording-backed handlers replaced for the promoted pricing/reservation slice",
|
|
2734
|
+
"clones/pricelabs/SPEC.md tracks occupancy/rate/replay determinism invariants"
|
|
2735
|
+
]
|
|
2736
|
+
}
|
|
2737
|
+
},
|
|
2738
|
+
{
|
|
2739
|
+
name: "quickbooks",
|
|
2740
|
+
package: "@archal/clone-quickbooks",
|
|
2741
|
+
path: "clones/quickbooks",
|
|
2742
|
+
stage: "internal",
|
|
2743
|
+
transport: "rest",
|
|
2744
|
+
hostedRuntime: {
|
|
2745
|
+
enabled: true,
|
|
2746
|
+
requiresDist: true,
|
|
2747
|
+
requiresEmptySeed: true
|
|
2748
|
+
},
|
|
2749
|
+
cli: {
|
|
2750
|
+
bundleAssets: false,
|
|
2751
|
+
bundleToolSnapshot: false
|
|
2752
|
+
},
|
|
2753
|
+
routeProxy: {
|
|
2754
|
+
enabled: false,
|
|
2755
|
+
domains: []
|
|
2756
|
+
},
|
|
2757
|
+
seeds: {
|
|
2758
|
+
generatedCatalog: false,
|
|
2759
|
+
default: "empty"
|
|
2760
|
+
},
|
|
2761
|
+
smoke: {
|
|
2762
|
+
production: "none"
|
|
2763
|
+
},
|
|
2764
|
+
architecture: {
|
|
2765
|
+
class: "replay-shell",
|
|
2766
|
+
summary: "QuickBooks serves a recording-backed REST/MCP surface; it cannot safely expand as CRUD because accounting correctness depends on ledgers, balances, and immutable audit history.",
|
|
2767
|
+
domainPrimitives: [
|
|
2768
|
+
"ledger entries",
|
|
2769
|
+
"balance reconciliation",
|
|
2770
|
+
"invoice/payment audit history",
|
|
2771
|
+
"recording-backed REST/MCP surface awaiting stateful lift"
|
|
2772
|
+
],
|
|
2773
|
+
promotionEvidence: [
|
|
2774
|
+
"clones/quickbooks/SPEC.md tracks runtime recording reads removed from exported handlers",
|
|
2775
|
+
"clones/quickbooks/SPEC.md tracks ledger -> balance and payment/refund -> reversal sequence invariants"
|
|
2776
|
+
]
|
|
2777
|
+
}
|
|
2778
|
+
},
|
|
2779
|
+
{
|
|
2780
|
+
name: "ramp",
|
|
2781
|
+
package: "@archal/clone-ramp",
|
|
2782
|
+
path: "clones/ramp",
|
|
2783
|
+
stage: "preview",
|
|
2784
|
+
display: {
|
|
2785
|
+
icon: "RP",
|
|
2786
|
+
name: "Ramp",
|
|
2787
|
+
description: "Cards, funds, expenses, reimbursements, bills, and travel.",
|
|
2788
|
+
toolCount: 46
|
|
2789
|
+
},
|
|
2790
|
+
transport: "mcp",
|
|
2791
|
+
hostedRuntime: {
|
|
2792
|
+
enabled: true,
|
|
2793
|
+
requiresDist: true,
|
|
2794
|
+
requiresEmptySeed: true
|
|
2795
|
+
},
|
|
2796
|
+
cli: {
|
|
2797
|
+
bundleAssets: true,
|
|
2798
|
+
bundleToolSnapshot: true
|
|
2799
|
+
},
|
|
2800
|
+
routeProxy: {
|
|
2801
|
+
enabled: true,
|
|
2802
|
+
domains: [
|
|
2803
|
+
{
|
|
2804
|
+
domain: "api.ramp.com"
|
|
2805
|
+
},
|
|
2806
|
+
{
|
|
2807
|
+
domain: "app.ramp.com"
|
|
2808
|
+
}
|
|
2809
|
+
]
|
|
2810
|
+
},
|
|
2811
|
+
seeds: {
|
|
2812
|
+
generatedCatalog: true,
|
|
2813
|
+
default: "default"
|
|
2814
|
+
},
|
|
2815
|
+
smoke: {
|
|
2816
|
+
production: "health-and-seed"
|
|
2817
|
+
},
|
|
2818
|
+
architecture: {
|
|
2819
|
+
class: "replay-shell",
|
|
2820
|
+
summary: "Ramp is CLI-native and fixture-scoped with seeded state overlays; it is not a broad state-derived Ramp or finance simulator.",
|
|
2821
|
+
domainPrimitives: [
|
|
2822
|
+
"checked-in Ramp CLI fixture corpus",
|
|
2823
|
+
"CLI-native command routing",
|
|
2824
|
+
"seeded approval and comment overlays"
|
|
2825
|
+
],
|
|
2826
|
+
promotionEvidence: [
|
|
2827
|
+
"clones/ramp/SPEC.md tracks state-derived Ramp vertical slice beyond fixture replay",
|
|
2828
|
+
"clones/ramp/SPEC.md tracks approval/comment/CLI reset sequence invariants"
|
|
2829
|
+
]
|
|
2830
|
+
},
|
|
2831
|
+
inferenceKeywords: [
|
|
2832
|
+
"ramp",
|
|
2833
|
+
"bill",
|
|
2834
|
+
"expense",
|
|
2835
|
+
"reimbursement",
|
|
2836
|
+
"fund",
|
|
2837
|
+
"card spend"
|
|
2838
|
+
]
|
|
2839
|
+
},
|
|
2840
|
+
{
|
|
2841
|
+
name: "sendgrid",
|
|
2842
|
+
package: "@archal/clone-sendgrid",
|
|
2843
|
+
path: "clones/sendgrid",
|
|
2844
|
+
stage: "internal",
|
|
2845
|
+
display: {
|
|
2846
|
+
icon: "SG",
|
|
2847
|
+
name: "SendGrid",
|
|
2848
|
+
description: "Internal replay evidence for account, scopes, templates, suppressions, tracking, mail settings, alerts, and sender authentication.",
|
|
2849
|
+
toolCount: 26
|
|
2850
|
+
},
|
|
2851
|
+
transport: "rest",
|
|
2852
|
+
hostedRuntime: {
|
|
2853
|
+
enabled: true,
|
|
2854
|
+
requiresDist: true,
|
|
2855
|
+
requiresEmptySeed: true
|
|
2856
|
+
},
|
|
2857
|
+
cli: {
|
|
2858
|
+
bundleAssets: false,
|
|
2859
|
+
bundleToolSnapshot: false
|
|
2860
|
+
},
|
|
2861
|
+
routeProxy: {
|
|
2862
|
+
enabled: true,
|
|
2863
|
+
domains: [
|
|
2864
|
+
{
|
|
2865
|
+
domain: "api.sendgrid.com"
|
|
2866
|
+
}
|
|
2867
|
+
]
|
|
2868
|
+
},
|
|
2869
|
+
seeds: {
|
|
2870
|
+
generatedCatalog: true,
|
|
2871
|
+
default: "empty"
|
|
2872
|
+
},
|
|
2873
|
+
smoke: {
|
|
2874
|
+
production: "none"
|
|
2875
|
+
},
|
|
2876
|
+
architecture: {
|
|
2877
|
+
class: "replay-shell",
|
|
2878
|
+
summary: "SendGrid is internal replay evidence only; send/event/suppression behavior and stateful account-setting invariants are not modeled yet.",
|
|
2879
|
+
domainPrimitives: [
|
|
2880
|
+
"recorded account and settings reads",
|
|
2881
|
+
"mail send state",
|
|
2882
|
+
"event and suppression history"
|
|
2883
|
+
],
|
|
2884
|
+
promotionEvidence: [
|
|
2885
|
+
"clones/sendgrid/SPEC.md tracks send/event/suppression state model",
|
|
2886
|
+
"clones/sendgrid/SPEC.md tracks runtime recording-backed handlers replaced for send/event/suppression behavior",
|
|
2887
|
+
"clones/sendgrid/SPEC.md tracks send-to-event and suppression replay invariants"
|
|
2888
|
+
]
|
|
2889
|
+
}
|
|
2890
|
+
},
|
|
2891
|
+
{
|
|
2892
|
+
name: "sentry",
|
|
2893
|
+
package: "@archal/clone-sentry",
|
|
2894
|
+
path: "clones/sentry",
|
|
2895
|
+
stage: "preview",
|
|
2896
|
+
display: {
|
|
2897
|
+
icon: "SN",
|
|
2898
|
+
name: "Sentry",
|
|
2899
|
+
description: "Error monitoring across organizations, projects, teams, issues, events, releases, and DSN keys.",
|
|
2900
|
+
toolCount: 25
|
|
2901
|
+
},
|
|
2902
|
+
transport: "rest",
|
|
2903
|
+
hostedRuntime: {
|
|
2904
|
+
enabled: true,
|
|
2905
|
+
requiresDist: true,
|
|
2906
|
+
requiresEmptySeed: true
|
|
2907
|
+
},
|
|
2908
|
+
cli: {
|
|
2909
|
+
bundleAssets: true,
|
|
2910
|
+
bundleToolSnapshot: true
|
|
2911
|
+
},
|
|
2912
|
+
routeProxy: {
|
|
2913
|
+
enabled: true,
|
|
2914
|
+
domains: [
|
|
2915
|
+
{
|
|
2916
|
+
domain: "sentry.io"
|
|
2917
|
+
}
|
|
2918
|
+
]
|
|
2919
|
+
},
|
|
2920
|
+
seeds: {
|
|
2921
|
+
generatedCatalog: true,
|
|
2922
|
+
default: "empty"
|
|
2923
|
+
},
|
|
2924
|
+
smoke: {
|
|
2925
|
+
production: "clone-surface"
|
|
2926
|
+
},
|
|
2927
|
+
architecture: {
|
|
2928
|
+
class: "domain-simulator",
|
|
2929
|
+
summary: "Error-monitoring behavior depends on event grouping, issue state, releases, and time-windowed queries.",
|
|
2930
|
+
domainPrimitives: [
|
|
2931
|
+
"event grouping",
|
|
2932
|
+
"issue state transitions",
|
|
2933
|
+
"release and time-window projections"
|
|
2934
|
+
],
|
|
2935
|
+
behaviorOwner: "src/domain/sentry-simulator.ts"
|
|
2936
|
+
}
|
|
2937
|
+
},
|
|
2938
|
+
{
|
|
2939
|
+
name: "slack",
|
|
2940
|
+
package: "@archal/clone-slack",
|
|
2941
|
+
path: "clones/slack",
|
|
2942
|
+
stage: "public",
|
|
2943
|
+
display: {
|
|
2944
|
+
icon: "SL",
|
|
2945
|
+
name: "Slack",
|
|
2946
|
+
description: "Channels, messages, threads, users, and reactions.",
|
|
2947
|
+
toolCount: 9
|
|
2948
|
+
},
|
|
2949
|
+
transport: "both",
|
|
2950
|
+
hostedRuntime: {
|
|
2951
|
+
enabled: true,
|
|
2952
|
+
requiresDist: true,
|
|
2953
|
+
requiresEmptySeed: true
|
|
2954
|
+
},
|
|
2955
|
+
cli: {
|
|
2956
|
+
bundleAssets: true,
|
|
2957
|
+
bundleToolSnapshot: true
|
|
2958
|
+
},
|
|
2959
|
+
routeProxy: {
|
|
2960
|
+
enabled: true,
|
|
2961
|
+
domains: [
|
|
2962
|
+
{
|
|
2963
|
+
domain: "slack.com"
|
|
2964
|
+
},
|
|
2965
|
+
{
|
|
2966
|
+
domain: "api.slack.com"
|
|
2967
|
+
}
|
|
2968
|
+
]
|
|
2969
|
+
},
|
|
2970
|
+
seeds: {
|
|
2971
|
+
generatedCatalog: true,
|
|
2972
|
+
default: "engineering-team"
|
|
2973
|
+
},
|
|
2974
|
+
smoke: {
|
|
2975
|
+
production: "health-and-seed"
|
|
2976
|
+
},
|
|
2977
|
+
architecture: {
|
|
2978
|
+
class: "domain-simulator",
|
|
2979
|
+
summary: "Slack behavior depends on message timelines, threads, channel membership, permissions, and delivery semantics.",
|
|
2980
|
+
domainPrimitives: [
|
|
2981
|
+
"channel membership and visibility",
|
|
2982
|
+
"message/thread ordering",
|
|
2983
|
+
"event delivery semantics"
|
|
2984
|
+
],
|
|
2985
|
+
behaviorOwner: "src/domain/slack-simulator.ts"
|
|
2986
|
+
},
|
|
2987
|
+
inferenceKeywords: [
|
|
2988
|
+
"slack",
|
|
2989
|
+
"slack channel",
|
|
2990
|
+
"send_message",
|
|
2991
|
+
"slack message",
|
|
2992
|
+
"direct message"
|
|
2993
|
+
]
|
|
2994
|
+
},
|
|
2995
|
+
{
|
|
2996
|
+
name: "stripe",
|
|
2997
|
+
package: "@archal/clone-stripe",
|
|
2998
|
+
path: "clones/stripe",
|
|
2999
|
+
stage: "preview",
|
|
3000
|
+
display: {
|
|
3001
|
+
icon: "ST",
|
|
3002
|
+
name: "Stripe",
|
|
3003
|
+
description: "Customers, payments, subscriptions, invoices, and refunds.",
|
|
3004
|
+
toolCount: 28
|
|
3005
|
+
},
|
|
3006
|
+
transport: "both",
|
|
3007
|
+
hostedRuntime: {
|
|
3008
|
+
enabled: true,
|
|
3009
|
+
requiresDist: true,
|
|
3010
|
+
requiresEmptySeed: true
|
|
3011
|
+
},
|
|
3012
|
+
cli: {
|
|
3013
|
+
bundleAssets: true,
|
|
3014
|
+
bundleToolSnapshot: true
|
|
3015
|
+
},
|
|
3016
|
+
routeProxy: {
|
|
3017
|
+
enabled: true,
|
|
3018
|
+
domains: [
|
|
3019
|
+
{
|
|
3020
|
+
domain: "api.stripe.com"
|
|
3021
|
+
}
|
|
3022
|
+
]
|
|
3023
|
+
},
|
|
3024
|
+
seeds: {
|
|
3025
|
+
generatedCatalog: true,
|
|
3026
|
+
default: "small-business"
|
|
3027
|
+
},
|
|
3028
|
+
smoke: {
|
|
3029
|
+
production: "health-and-seed"
|
|
3030
|
+
},
|
|
3031
|
+
architecture: {
|
|
3032
|
+
class: "domain-simulator",
|
|
3033
|
+
summary: "Payment behavior depends on ledgers, object lifecycle, webhooks, balances, and idempotency semantics.",
|
|
3034
|
+
domainPrimitives: [
|
|
3035
|
+
"payment object lifecycle",
|
|
3036
|
+
"balance and ledger projection",
|
|
3037
|
+
"webhook/idempotency semantics"
|
|
3038
|
+
],
|
|
3039
|
+
behaviorOwner: "src/domain/stripe-simulator.ts"
|
|
3040
|
+
},
|
|
3041
|
+
inferenceKeywords: [
|
|
3042
|
+
"stripe",
|
|
3043
|
+
"payment",
|
|
3044
|
+
"refund",
|
|
3045
|
+
"subscription",
|
|
3046
|
+
"invoice",
|
|
3047
|
+
"charge"
|
|
3048
|
+
]
|
|
3049
|
+
},
|
|
3050
|
+
{
|
|
3051
|
+
name: "woocommerce",
|
|
3052
|
+
package: "@archal/clone-woocommerce",
|
|
3053
|
+
path: "clones/woocommerce",
|
|
3054
|
+
stage: "internal",
|
|
3055
|
+
display: {
|
|
3056
|
+
icon: "WC",
|
|
3057
|
+
name: "WooCommerce",
|
|
3058
|
+
description: "Preview, coverage pending: products, customers, orders, coupons, categories, payment gateways, shipping zones, and taxes.",
|
|
3059
|
+
toolCount: 24
|
|
3060
|
+
},
|
|
3061
|
+
transport: "rest",
|
|
3062
|
+
hostedRuntime: {
|
|
3063
|
+
enabled: true,
|
|
3064
|
+
requiresDist: true,
|
|
3065
|
+
requiresEmptySeed: true
|
|
3066
|
+
},
|
|
3067
|
+
cli: {
|
|
3068
|
+
bundleAssets: false,
|
|
3069
|
+
bundleToolSnapshot: false
|
|
3070
|
+
},
|
|
3071
|
+
routeProxy: {
|
|
3072
|
+
enabled: false,
|
|
3073
|
+
domains: []
|
|
3074
|
+
},
|
|
3075
|
+
seeds: {
|
|
3076
|
+
generatedCatalog: false,
|
|
3077
|
+
default: "demo"
|
|
3078
|
+
},
|
|
3079
|
+
smoke: {
|
|
3080
|
+
production: "none"
|
|
3081
|
+
},
|
|
3082
|
+
architecture: {
|
|
3083
|
+
class: "standard-core",
|
|
3084
|
+
summary: "Current internal commerce slice is product/order/customer records; payment/fulfillment ledgers are not claimed."
|
|
3085
|
+
}
|
|
3086
|
+
},
|
|
3087
|
+
{
|
|
3088
|
+
name: "supabase",
|
|
3089
|
+
package: "@archal/clone-supabase",
|
|
3090
|
+
path: "clones/supabase",
|
|
3091
|
+
stage: "public",
|
|
3092
|
+
display: {
|
|
3093
|
+
icon: "SB",
|
|
3094
|
+
name: "Supabase",
|
|
3095
|
+
description: "SQL, migrations, logs, branches, and project metadata.",
|
|
3096
|
+
toolCount: 29
|
|
3097
|
+
},
|
|
3098
|
+
transport: "both",
|
|
3099
|
+
hostedRuntime: {
|
|
3100
|
+
enabled: true,
|
|
3101
|
+
requiresDist: true,
|
|
3102
|
+
requiresEmptySeed: true
|
|
3103
|
+
},
|
|
3104
|
+
cli: {
|
|
3105
|
+
bundleAssets: true,
|
|
3106
|
+
bundleToolSnapshot: true
|
|
3107
|
+
},
|
|
3108
|
+
routeProxy: {
|
|
3109
|
+
enabled: true,
|
|
3110
|
+
domains: [
|
|
3111
|
+
{
|
|
3112
|
+
domain: "supabase.co",
|
|
3113
|
+
matchSubdomains: true
|
|
3114
|
+
},
|
|
3115
|
+
{
|
|
3116
|
+
domain: "api.supabase.com"
|
|
3117
|
+
}
|
|
3118
|
+
]
|
|
3119
|
+
},
|
|
3120
|
+
seeds: {
|
|
3121
|
+
generatedCatalog: true,
|
|
3122
|
+
default: "small-project"
|
|
3123
|
+
},
|
|
3124
|
+
smoke: {
|
|
3125
|
+
production: "health-and-seed"
|
|
3126
|
+
},
|
|
3127
|
+
architecture: {
|
|
3128
|
+
class: "domain-simulator",
|
|
3129
|
+
summary: "Supabase already requires and uses a Postgres/query/realtime simulator beside clone-core.",
|
|
3130
|
+
domainPrimitives: [
|
|
3131
|
+
"Postgres schema and SQL execution",
|
|
3132
|
+
"RLS and API projections",
|
|
3133
|
+
"realtime row changes"
|
|
3134
|
+
],
|
|
3135
|
+
behaviorOwner: "src/pg-engine.ts"
|
|
3136
|
+
},
|
|
3137
|
+
inferenceKeywords: [
|
|
3138
|
+
"supabase",
|
|
3139
|
+
"database",
|
|
3140
|
+
"sql query",
|
|
3141
|
+
"database table",
|
|
3142
|
+
"sql"
|
|
3143
|
+
]
|
|
3144
|
+
},
|
|
3145
|
+
{
|
|
3146
|
+
name: "tavily",
|
|
3147
|
+
package: "@archal/clone-tavily",
|
|
3148
|
+
path: "clones/tavily",
|
|
3149
|
+
stage: "preview",
|
|
3150
|
+
display: {
|
|
3151
|
+
icon: "TV",
|
|
3152
|
+
name: "Tavily",
|
|
3153
|
+
description: "Search, extract, crawl, map, research, usage, and API key operations.",
|
|
3154
|
+
toolCount: 11
|
|
3155
|
+
},
|
|
3156
|
+
transport: "rest",
|
|
3157
|
+
hostedRuntime: {
|
|
3158
|
+
enabled: true,
|
|
3159
|
+
requiresDist: true,
|
|
3160
|
+
requiresEmptySeed: true
|
|
3161
|
+
},
|
|
3162
|
+
cli: {
|
|
3163
|
+
bundleAssets: true,
|
|
3164
|
+
bundleToolSnapshot: true
|
|
3165
|
+
},
|
|
3166
|
+
routeProxy: {
|
|
3167
|
+
enabled: true,
|
|
3168
|
+
domains: [
|
|
3169
|
+
{
|
|
3170
|
+
domain: "api.tavily.com"
|
|
3171
|
+
},
|
|
3172
|
+
{
|
|
3173
|
+
domain: "tavily.com"
|
|
3174
|
+
}
|
|
3175
|
+
]
|
|
3176
|
+
},
|
|
3177
|
+
seeds: {
|
|
3178
|
+
generatedCatalog: true,
|
|
3179
|
+
default: "empty"
|
|
3180
|
+
},
|
|
3181
|
+
smoke: {
|
|
3182
|
+
production: "clone-surface"
|
|
3183
|
+
},
|
|
3184
|
+
architecture: {
|
|
3185
|
+
class: "replay-shell",
|
|
3186
|
+
summary: "Tavily mixes deterministic search/extract helpers with runtime recording replay for REST/tool parity; treat it as replay-backed until all supported responses are state-derived.",
|
|
3187
|
+
domainPrimitives: [
|
|
3188
|
+
"recording-backed REST/tool response replay",
|
|
3189
|
+
"deterministic search/extract response helpers",
|
|
3190
|
+
"research request state overlay"
|
|
3191
|
+
],
|
|
3192
|
+
promotionEvidence: [
|
|
3193
|
+
"clones/tavily/SPEC.md tracks state-derived response fixtures or query/result simulator",
|
|
3194
|
+
"clones/tavily/SPEC.md tracks runtime recording-backed handlers replaced for the promoted search/extract/research surface",
|
|
3195
|
+
"clones/tavily/SPEC.md tracks search/extract/research replay determinism invariants"
|
|
3196
|
+
]
|
|
3197
|
+
}
|
|
3198
|
+
},
|
|
3199
|
+
{
|
|
3200
|
+
name: "typeform",
|
|
3201
|
+
package: "@archal/clone-typeform",
|
|
3202
|
+
path: "clones/typeform",
|
|
3203
|
+
stage: "internal",
|
|
3204
|
+
display: {
|
|
3205
|
+
icon: "TF",
|
|
3206
|
+
name: "Typeform",
|
|
3207
|
+
description: "Preview, coverage pending: forms, responses, workspaces, themes, and webhooks.",
|
|
3208
|
+
toolCount: 22
|
|
3209
|
+
},
|
|
3210
|
+
transport: "mcp",
|
|
3211
|
+
hostedRuntime: {
|
|
3212
|
+
enabled: true,
|
|
3213
|
+
requiresDist: true,
|
|
3214
|
+
requiresEmptySeed: true
|
|
3215
|
+
},
|
|
3216
|
+
cli: {
|
|
3217
|
+
bundleAssets: false,
|
|
3218
|
+
bundleToolSnapshot: false
|
|
3219
|
+
},
|
|
3220
|
+
routeProxy: {
|
|
3221
|
+
enabled: false,
|
|
3222
|
+
domains: []
|
|
3223
|
+
},
|
|
3224
|
+
seeds: {
|
|
3225
|
+
generatedCatalog: false,
|
|
3226
|
+
default: "empty"
|
|
3227
|
+
},
|
|
3228
|
+
smoke: {
|
|
3229
|
+
production: "none"
|
|
3230
|
+
},
|
|
3231
|
+
architecture: {
|
|
3232
|
+
class: "domain-simulator",
|
|
3233
|
+
summary: "Typeform relies on form response projection, webhook subscription scoping, and signed delivery semantics beyond generic CRUD.",
|
|
3234
|
+
domainPrimitives: [
|
|
3235
|
+
"form response submission projection",
|
|
3236
|
+
"webhook subscription scoping and signing",
|
|
3237
|
+
"responses API and webhook payload consistency"
|
|
3238
|
+
],
|
|
3239
|
+
behaviorOwner: "src/state/webhook-delivery-config.ts"
|
|
3240
|
+
}
|
|
3241
|
+
},
|
|
3242
|
+
{
|
|
3243
|
+
name: "telegram",
|
|
3244
|
+
package: "@archal/clone-telegram",
|
|
3245
|
+
path: "clones/telegram",
|
|
3246
|
+
stage: "internal",
|
|
3247
|
+
display: {
|
|
3248
|
+
icon: "TG",
|
|
3249
|
+
name: "Telegram",
|
|
3250
|
+
description: "Bot messages, chats, updates, and webhooks.",
|
|
3251
|
+
toolCount: 32
|
|
3252
|
+
},
|
|
3253
|
+
transport: "both",
|
|
3254
|
+
hostedRuntime: {
|
|
3255
|
+
enabled: true,
|
|
3256
|
+
requiresDist: true,
|
|
3257
|
+
requiresEmptySeed: true
|
|
3258
|
+
},
|
|
3259
|
+
cli: {
|
|
3260
|
+
bundleAssets: false,
|
|
3261
|
+
bundleToolSnapshot: false
|
|
3262
|
+
},
|
|
3263
|
+
routeProxy: {
|
|
3264
|
+
enabled: false,
|
|
3265
|
+
domains: []
|
|
3266
|
+
},
|
|
3267
|
+
seeds: {
|
|
3268
|
+
generatedCatalog: false,
|
|
3269
|
+
default: "empty"
|
|
3270
|
+
},
|
|
3271
|
+
smoke: {
|
|
3272
|
+
production: "none"
|
|
3273
|
+
},
|
|
3274
|
+
architecture: {
|
|
3275
|
+
class: "domain-simulator",
|
|
3276
|
+
summary: "Telegram behavior depends on outbound message timelines, getUpdates ordering/filtering, bot configuration, and webhook registration state.",
|
|
3277
|
+
domainPrimitives: [
|
|
3278
|
+
"outbound message and update ordering",
|
|
3279
|
+
"bot configuration state",
|
|
3280
|
+
"webhook registration state"
|
|
3281
|
+
],
|
|
3282
|
+
behaviorOwner: "src/domain/telegram-simulator.ts"
|
|
3283
|
+
}
|
|
3284
|
+
},
|
|
3285
|
+
{
|
|
3286
|
+
name: "twilio",
|
|
3287
|
+
package: "@archal/clone-twilio",
|
|
3288
|
+
path: "clones/twilio",
|
|
3289
|
+
stage: "internal",
|
|
3290
|
+
display: {
|
|
3291
|
+
icon: "TW",
|
|
3292
|
+
name: "Twilio",
|
|
3293
|
+
description: "Messages, calls, phone numbers, and verifications.",
|
|
3294
|
+
toolCount: 35
|
|
3295
|
+
},
|
|
3296
|
+
transport: "rest",
|
|
3297
|
+
hostedRuntime: {
|
|
3298
|
+
enabled: true,
|
|
3299
|
+
requiresDist: true,
|
|
3300
|
+
requiresEmptySeed: true
|
|
3301
|
+
},
|
|
3302
|
+
cli: {
|
|
3303
|
+
bundleAssets: false,
|
|
3304
|
+
bundleToolSnapshot: false
|
|
3305
|
+
},
|
|
3306
|
+
routeProxy: {
|
|
3307
|
+
enabled: false,
|
|
3308
|
+
domains: []
|
|
3309
|
+
},
|
|
3310
|
+
seeds: {
|
|
3311
|
+
generatedCatalog: false,
|
|
3312
|
+
default: "empty"
|
|
3313
|
+
},
|
|
3314
|
+
smoke: {
|
|
3315
|
+
production: "none"
|
|
3316
|
+
},
|
|
3317
|
+
architecture: {
|
|
3318
|
+
class: "replay-shell",
|
|
3319
|
+
summary: "Twilio is still a recording-backed REST shell with a narrow stateful free-resource overlay, not a broad communications simulator.",
|
|
3320
|
+
domainPrimitives: [
|
|
3321
|
+
"recording-backed REST route replay",
|
|
3322
|
+
"Keys, SigningKeys, Applications, and Queues overlay",
|
|
3323
|
+
"future message/call/conversation lifecycle simulator"
|
|
3324
|
+
],
|
|
3325
|
+
promotionEvidence: [
|
|
3326
|
+
"clones/twilio/SPEC.md tracks message or call lifecycle state model",
|
|
3327
|
+
"clones/twilio/SPEC.md tracks runtime recording-backed handlers replaced for the promoted message/call slice",
|
|
3328
|
+
"clones/twilio/SPEC.md tracks delivery/status/webhook sequence invariants"
|
|
3329
|
+
]
|
|
3330
|
+
}
|
|
3331
|
+
},
|
|
3332
|
+
{
|
|
3333
|
+
name: "unipile",
|
|
3334
|
+
package: "@archal/clone-unipile",
|
|
3335
|
+
path: "clones/unipile",
|
|
3336
|
+
stage: "preview",
|
|
3337
|
+
display: {
|
|
3338
|
+
icon: "UP",
|
|
3339
|
+
name: "Unipile",
|
|
3340
|
+
description: "LinkedIn and email messaging, accounts, and chats.",
|
|
3341
|
+
toolCount: 31
|
|
3342
|
+
},
|
|
3343
|
+
transport: "rest",
|
|
3344
|
+
hostedRuntime: {
|
|
3345
|
+
enabled: true,
|
|
3346
|
+
requiresDist: true,
|
|
3347
|
+
requiresEmptySeed: true
|
|
3348
|
+
},
|
|
3349
|
+
cli: {
|
|
3350
|
+
bundleAssets: true,
|
|
3351
|
+
bundleToolSnapshot: true
|
|
3352
|
+
},
|
|
3353
|
+
routeProxy: {
|
|
3354
|
+
enabled: true,
|
|
3355
|
+
domains: [
|
|
3356
|
+
{
|
|
3357
|
+
domain: "api.unipile.com"
|
|
3358
|
+
},
|
|
3359
|
+
{
|
|
3360
|
+
domain: "unipile.com",
|
|
3361
|
+
matchSubdomains: true
|
|
3362
|
+
}
|
|
3363
|
+
]
|
|
3364
|
+
},
|
|
3365
|
+
seeds: {
|
|
3366
|
+
generatedCatalog: true,
|
|
3367
|
+
default: "empty"
|
|
3368
|
+
},
|
|
3369
|
+
smoke: {
|
|
3370
|
+
production: "clone-surface"
|
|
3371
|
+
},
|
|
3372
|
+
architecture: {
|
|
3373
|
+
class: "replay-shell",
|
|
3374
|
+
summary: "Unipile is currently an authenticated REST replay surface with one approved stateful WhatsApp send mutation, not a broad provider messaging simulator.",
|
|
3375
|
+
domainPrimitives: [
|
|
3376
|
+
"authenticated account and chat reads",
|
|
3377
|
+
"single WhatsApp send mutation",
|
|
3378
|
+
"future account, chat, and webhook simulator"
|
|
3379
|
+
],
|
|
3380
|
+
promotionEvidence: [
|
|
3381
|
+
"clones/unipile/SPEC.md tracks chat timeline state model",
|
|
3382
|
+
"clones/unipile/SPEC.md tracks runtime recording-backed handlers replaced for the promoted chat/provider surface",
|
|
3383
|
+
"clones/unipile/SPEC.md tracks send/delivery/read/webhook sequence invariants"
|
|
3384
|
+
]
|
|
3385
|
+
}
|
|
3386
|
+
},
|
|
3387
|
+
{
|
|
3388
|
+
name: "webflow",
|
|
3389
|
+
package: "@archal/clone-webflow",
|
|
3390
|
+
path: "clones/webflow",
|
|
3391
|
+
stage: "preview",
|
|
3392
|
+
display: {
|
|
3393
|
+
icon: "WF",
|
|
3394
|
+
name: "Webflow",
|
|
3395
|
+
description: "Sites, pages, CMS collections, items, assets, forms, and webhooks.",
|
|
3396
|
+
toolCount: 19
|
|
3397
|
+
},
|
|
3398
|
+
transport: "rest",
|
|
3399
|
+
hostedRuntime: {
|
|
3400
|
+
enabled: true,
|
|
3401
|
+
requiresDist: true,
|
|
3402
|
+
requiresEmptySeed: true
|
|
3403
|
+
},
|
|
3404
|
+
cli: {
|
|
3405
|
+
bundleAssets: true,
|
|
3406
|
+
bundleToolSnapshot: true
|
|
3407
|
+
},
|
|
3408
|
+
routeProxy: {
|
|
3409
|
+
enabled: true,
|
|
3410
|
+
domains: [
|
|
3411
|
+
{
|
|
3412
|
+
domain: "api.webflow.com"
|
|
3413
|
+
},
|
|
3414
|
+
{
|
|
3415
|
+
domain: "webflow.com"
|
|
3416
|
+
}
|
|
3417
|
+
]
|
|
3418
|
+
},
|
|
3419
|
+
seeds: {
|
|
3420
|
+
generatedCatalog: true,
|
|
3421
|
+
default: "empty"
|
|
3422
|
+
},
|
|
3423
|
+
smoke: {
|
|
3424
|
+
production: "clone-surface"
|
|
3425
|
+
},
|
|
3426
|
+
architecture: {
|
|
3427
|
+
class: "replay-shell",
|
|
3428
|
+
summary: "Webflow is recording-replay backed with focused CMS stateful overlays; it should not be treated as a full Webflow behavior engine.",
|
|
3429
|
+
domainPrimitives: [
|
|
3430
|
+
"recording replay cursor",
|
|
3431
|
+
"CMS collection/item overlays",
|
|
3432
|
+
"publish lifecycle captures"
|
|
3433
|
+
],
|
|
3434
|
+
promotionEvidence: [
|
|
3435
|
+
"clones/webflow/SPEC.md tracks staged/live publish projection state model",
|
|
3436
|
+
"clones/webflow/SPEC.md tracks runtime recording-backed handlers replaced for the promoted publish/CMS surface",
|
|
3437
|
+
"clones/webflow/SPEC.md tracks CMS publish/reset sequence invariants"
|
|
3438
|
+
]
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
];
|
|
3442
|
+
var CLONE_MANIFEST = manifest_default;
|
|
3443
|
+
var CLONE_NAMES = CLONE_MANIFEST.map((t) => t.name);
|
|
3444
|
+
var CLONE_PACKAGES = CLONE_MANIFEST.map((t) => t.package);
|
|
3445
|
+
|
|
3446
|
+
// ../../clones/core/dist/chunk-SXNWBK3S.js
|
|
3447
|
+
var CLONE_PREFIX_PATTERN = new RegExp(`^(${CLONE_NAMES.join("|")})_`, "i");
|
|
3448
|
+
|
|
3449
|
+
// ../../clones/core/dist/chunk-EZKTFYZI.js
|
|
3450
|
+
var CLONE_STAGES = ["wip", "internal", "preview", "public", "retired"];
|
|
3451
|
+
var CLONE_TRANSPORTS = ["mcp", "rest", "both"];
|
|
3452
|
+
var CLONE_SMOKE_PROFILES = ["none", "health-and-seed", "clone-surface"];
|
|
3453
|
+
var STARTABLE_CLONE_STAGES = ["public", "preview"];
|
|
3454
|
+
var CATALOG_VISIBLE_CLONE_STAGES = ["public", "preview"];
|
|
3455
|
+
var CLONE_STAGE_SET = new Set(CLONE_STAGES);
|
|
3456
|
+
var CLONE_TRANSPORT_SET = new Set(CLONE_TRANSPORTS);
|
|
3457
|
+
var CLONE_SMOKE_PROFILE_SET = new Set(CLONE_SMOKE_PROFILES);
|
|
3458
|
+
var STARTABLE_CLONE_STAGE_SET = new Set(STARTABLE_CLONE_STAGES);
|
|
3459
|
+
var CATALOG_VISIBLE_CLONE_STAGE_SET = new Set(CATALOG_VISIBLE_CLONE_STAGES);
|
|
3460
|
+
|
|
3461
|
+
// ../../clones/core/dist/chunk-ENO6SYMV.js
|
|
3462
|
+
var CLONE_ARCHITECTURE_CLASSES = [
|
|
3463
|
+
"standard-core",
|
|
3464
|
+
"domain-simulator",
|
|
3465
|
+
"replay-shell",
|
|
3466
|
+
"redesign-before-expansion"
|
|
3467
|
+
];
|
|
3468
|
+
var CLONE_ARCHITECTURE_PREVIEW_CLASSES = [
|
|
3469
|
+
"replay-shell",
|
|
3470
|
+
"redesign-before-expansion"
|
|
3471
|
+
];
|
|
3472
|
+
var CLONE_ARCHITECTURE_CLASS_SET = new Set(CLONE_ARCHITECTURE_CLASSES);
|
|
3473
|
+
var CLONE_ARCHITECTURE_PREVIEW_CLASS_SET = new Set(CLONE_ARCHITECTURE_PREVIEW_CLASSES);
|
|
3474
|
+
|
|
3475
|
+
// ../../clones/core/dist/chunk-4B2Y764J.js
|
|
3476
|
+
var CLONE_RUNTIME_SOURCE_EXTENSIONS = Object.freeze([
|
|
3477
|
+
".cjs",
|
|
3478
|
+
".cts",
|
|
3479
|
+
".js",
|
|
3480
|
+
".jsx",
|
|
3481
|
+
".mjs",
|
|
3482
|
+
".mts",
|
|
3483
|
+
".ts",
|
|
3484
|
+
".tsx"
|
|
3485
|
+
]);
|
|
3486
|
+
var RUNTIME_SOURCE_EXTENSIONS = new Set(CLONE_RUNTIME_SOURCE_EXTENSIONS);
|
|
3487
|
+
|
|
3488
|
+
// ../../clones/core/dist/chunk-CDEMQFER.js
|
|
3489
|
+
var FIELD_POLICY = {
|
|
3490
|
+
behaviorOwner: {
|
|
3491
|
+
required: /* @__PURE__ */ new Set(["domain-simulator"]),
|
|
3492
|
+
allowed: /* @__PURE__ */ new Set(["domain-simulator"])
|
|
3493
|
+
},
|
|
3494
|
+
domainPrimitives: {
|
|
3495
|
+
required: /* @__PURE__ */ new Set(["domain-simulator", "replay-shell", "redesign-before-expansion"]),
|
|
3496
|
+
allowed: /* @__PURE__ */ new Set(["domain-simulator", "replay-shell", "redesign-before-expansion"])
|
|
3497
|
+
},
|
|
3498
|
+
promotionEvidence: {
|
|
3499
|
+
required: /* @__PURE__ */ new Set(["replay-shell"]),
|
|
3500
|
+
allowed: /* @__PURE__ */ new Set(["replay-shell"])
|
|
3501
|
+
},
|
|
3502
|
+
redesignContract: {
|
|
3503
|
+
required: /* @__PURE__ */ new Set(["redesign-before-expansion"]),
|
|
3504
|
+
allowed: /* @__PURE__ */ new Set(["redesign-before-expansion"])
|
|
3505
|
+
}
|
|
3506
|
+
};
|
|
3507
|
+
var CLONE_ARCHITECTURE_POLICY_FIELDS = Object.freeze(Object.keys(FIELD_POLICY));
|
|
3508
|
+
var CLONE_ARCHITECTURE_MANIFEST_FIELDS = Object.freeze([
|
|
3509
|
+
"class",
|
|
3510
|
+
"summary",
|
|
3511
|
+
...CLONE_ARCHITECTURE_POLICY_FIELDS
|
|
3512
|
+
]);
|
|
3513
|
+
var CLONE_ARCHITECTURE_MANIFEST_FIELD_SET = new Set(CLONE_ARCHITECTURE_MANIFEST_FIELDS);
|
|
3514
|
+
|
|
3515
|
+
// src/runtime/service-profiles.ts
|
|
3516
|
+
function unsupportedServiceMessage(unsupportedServices) {
|
|
3517
|
+
const supportedServices = listSharedRouteManifests().map((manifest) => manifest.service).sort();
|
|
3518
|
+
return [
|
|
3519
|
+
`Unsupported route-mode service${unsupportedServices.length === 1 ? "" : "s"}: ${unsupportedServices.sort().join(", ")}.`,
|
|
3520
|
+
`Supported services in this Vitest adapter: ${supportedServices.join(", ")}.`
|
|
3521
|
+
].join(" ");
|
|
3522
|
+
}
|
|
3523
|
+
function assertSupportedArchalVitestServices(services) {
|
|
3524
|
+
const unsupportedServices = Object.entries(services).filter(([, serviceConfig]) => serviceConfig.mode === "route").map(([serviceName]) => serviceName).filter((serviceName) => !getSharedRouteManifest(serviceName));
|
|
3525
|
+
if (unsupportedServices.length === 0) {
|
|
3526
|
+
return;
|
|
3527
|
+
}
|
|
3528
|
+
throw new Error(unsupportedServiceMessage(unsupportedServices));
|
|
3529
|
+
}
|
|
3530
|
+
function resolveArchalVitestServiceProfiles(services) {
|
|
3531
|
+
const declaredRouteServices = new Set(
|
|
3532
|
+
Object.entries(services).filter(([, serviceConfig]) => serviceConfig.mode === "route").map(([serviceName]) => serviceName)
|
|
3533
|
+
);
|
|
3534
|
+
const profiles = listSharedRouteManifests().map((manifest) => ({
|
|
3535
|
+
serviceName: manifest.service,
|
|
3536
|
+
profile: buildServiceCompatibilityProfile(manifest)
|
|
3537
|
+
}));
|
|
3538
|
+
const declaredProfiles = profiles.filter(({ serviceName }) => declaredRouteServices.has(serviceName)).map(({ profile }) => profile);
|
|
3539
|
+
const undeclaredEnforcementProfiles = profiles.filter(({ serviceName }) => !declaredRouteServices.has(serviceName)).map(({ profile }) => profile);
|
|
3540
|
+
return [...declaredProfiles, ...undeclaredEnforcementProfiles];
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
// src/runtime/hosted-session-provider.ts
|
|
3544
|
+
import { createHash, randomUUID } from "crypto";
|
|
3545
|
+
import { spawn } from "child_process";
|
|
3546
|
+
import { promises as fs } from "fs";
|
|
3547
|
+
import { join, resolve as resolve2 } from "path";
|
|
3548
|
+
import { tmpdir } from "os";
|
|
3549
|
+
|
|
3550
|
+
// src/url-resolution.ts
|
|
3551
|
+
function readFirstConfiguredApiBaseUrl(envVars) {
|
|
3552
|
+
for (const envVar of envVars) {
|
|
3553
|
+
const configured = trimEnv(envVar);
|
|
3554
|
+
if (!configured) {
|
|
3555
|
+
continue;
|
|
3556
|
+
}
|
|
3557
|
+
return normalizeApiBaseUrl(configured);
|
|
3558
|
+
}
|
|
3559
|
+
return null;
|
|
3560
|
+
}
|
|
3561
|
+
function resolveArchalVitestApiBaseUrl(defaultBaseUrl) {
|
|
3562
|
+
return readFirstConfiguredApiBaseUrl([
|
|
3563
|
+
"ARCHAL_VITEST_API_URL",
|
|
3564
|
+
"ARCHAL_API_URL",
|
|
3565
|
+
"ARCHAL_API_BASE_URL",
|
|
3566
|
+
"ARCHAL_AUTH_URL",
|
|
3567
|
+
"ARCHAL_AUTH_BASE_URL"
|
|
3568
|
+
]) ?? defaultBaseUrl;
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3571
|
+
// src/runtime/hosted-session-provider.ts
|
|
3572
|
+
var COORDINATOR_LOCK_TIMEOUT_MS = 3e4;
|
|
3573
|
+
var COORDINATOR_STALE_LOCK_MS = COORDINATOR_LOCK_TIMEOUT_MS;
|
|
3574
|
+
var COORDINATOR_POLL_INTERVAL_MS = 50;
|
|
3575
|
+
var ROUTE_CONTROL_AUTHORIZATION_HEADER2 = "x-route-authorization";
|
|
3576
|
+
var ROUTE_SERVICE_ORIGIN_HEADER2 = "x-route-service-origin";
|
|
3577
|
+
var WORKER_ID_HEADER = "x-archal-worker-id";
|
|
3578
|
+
function currentVitestWorkerId() {
|
|
3579
|
+
const raw = process.env["VITEST_WORKER_ID"];
|
|
3580
|
+
if (!raw || typeof raw !== "string") return void 0;
|
|
3581
|
+
const trimmed = raw.trim();
|
|
3582
|
+
if (trimmed.length === 0) return void 0;
|
|
3583
|
+
return `vitest-${trimmed}`;
|
|
3584
|
+
}
|
|
3585
|
+
function resolveWorkerId() {
|
|
3586
|
+
const vitestWorkerId = trimEnv("VITEST_WORKER_ID");
|
|
3587
|
+
return vitestWorkerId ? `vitest-worker-${vitestWorkerId}` : `pid-${process.pid}`;
|
|
3588
|
+
}
|
|
3589
|
+
function resolveRunnerPid(config) {
|
|
3590
|
+
return typeof config.runnerPid === "number" && Number.isInteger(config.runnerPid) && config.runnerPid > 0 ? config.runnerPid : process.pid;
|
|
3591
|
+
}
|
|
3592
|
+
function toSafeCoordinatorDirectoryName(sessionKey) {
|
|
3593
|
+
if (sessionKey !== "." && sessionKey !== ".." && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(sessionKey)) {
|
|
3594
|
+
return sessionKey;
|
|
3595
|
+
}
|
|
3596
|
+
const slug = sessionKey.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^[.-]+|[.-]+$/g, "").slice(0, 48);
|
|
3597
|
+
const hash = createHash("sha256").update(sessionKey).digest("hex").slice(0, 12);
|
|
3598
|
+
return `${slug || "session"}-${hash}`;
|
|
3599
|
+
}
|
|
3600
|
+
function resolveCoordinatorPaths(config) {
|
|
3601
|
+
const baseDirectory = resolve2(
|
|
3602
|
+
trimEnv("ARCHAL_VITEST_COORDINATOR_DIR") ?? join(tmpdir(), "archal-vitest")
|
|
3603
|
+
);
|
|
3604
|
+
const sessionKey = config.sessionKey ?? `${config.projectName}-${randomUUID()}`;
|
|
3605
|
+
const directory = resolve2(baseDirectory, toSafeCoordinatorDirectoryName(sessionKey));
|
|
3606
|
+
return {
|
|
3607
|
+
directory,
|
|
3608
|
+
lockDirectory: join(directory, "lock"),
|
|
3609
|
+
statePath: join(directory, "state.json"),
|
|
3610
|
+
reaperPidPath: join(directory, "reaper.pid")
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
function shouldStopSessionOnRelease() {
|
|
3614
|
+
const explicitSetting = trimEnv("ARCHAL_VITEST_STOP_SESSION_ON_RELEASE");
|
|
3615
|
+
if (explicitSetting) {
|
|
3616
|
+
return !/^(0|false)$/i.test(explicitSetting);
|
|
3617
|
+
}
|
|
3618
|
+
return trimEnv("CI") !== void 0 || trimEnv("GITHUB_ACTIONS") !== void 0;
|
|
3619
|
+
}
|
|
3620
|
+
async function resolveHostedSessionEnvironment() {
|
|
3621
|
+
const apiBaseUrl = resolveArchalVitestApiBaseUrl(DEFAULT_HOSTED_API_BASE_URL);
|
|
3622
|
+
const auth = await createHostedAuthLease(VITEST_AUTH_LEASE_OPTIONS);
|
|
3623
|
+
const ttlFromMinutes = parsePositiveInteger(trimEnv("ARCHAL_SESSION_TTL_MINUTES"), 30, 1) * 60;
|
|
3624
|
+
const ttlSeconds = parsePositiveInteger(
|
|
3625
|
+
trimEnv("ARCHAL_VITEST_SESSION_TTL_SECONDS"),
|
|
3626
|
+
ttlFromMinutes,
|
|
3627
|
+
1
|
|
3628
|
+
);
|
|
3629
|
+
const readyTimeoutMs = Math.max(
|
|
3630
|
+
MIN_READY_TIMEOUT_MS,
|
|
3631
|
+
parsePositiveInteger(
|
|
3632
|
+
trimEnv("ARCHAL_VITEST_SESSION_READY_TIMEOUT_MS") ?? trimEnv("ARCHAL_SESSION_READY_TIMEOUT_MS"),
|
|
3633
|
+
DEFAULT_READY_TIMEOUT_MS,
|
|
3634
|
+
1
|
|
3635
|
+
)
|
|
3636
|
+
);
|
|
3637
|
+
const renewIntervalMs = Math.max(
|
|
3638
|
+
MIN_RENEW_INTERVAL_MS,
|
|
3639
|
+
parsePositiveInteger(
|
|
3640
|
+
trimEnv("ARCHAL_VITEST_SESSION_RENEW_INTERVAL_MS"),
|
|
3641
|
+
Math.min(Math.floor((ttlSeconds || DEFAULT_SESSION_TTL_SECONDS) * 500), DEFAULT_RENEW_INTERVAL_MS),
|
|
3642
|
+
1
|
|
3643
|
+
)
|
|
3644
|
+
);
|
|
3645
|
+
return {
|
|
3646
|
+
auth,
|
|
3647
|
+
apiBaseUrl,
|
|
3648
|
+
ttlSeconds: ttlSeconds || DEFAULT_SESSION_TTL_SECONDS,
|
|
3649
|
+
readyTimeoutMs,
|
|
3650
|
+
renewIntervalMs
|
|
3651
|
+
};
|
|
3652
|
+
}
|
|
3653
|
+
async function withCoordinatorLock(paths, lockTimeoutMs, action) {
|
|
3654
|
+
await fs.mkdir(paths.directory, { recursive: true });
|
|
3655
|
+
const deadline = Date.now() + resolveCoordinatorLockWaitTimeoutMs(lockTimeoutMs);
|
|
3656
|
+
while (true) {
|
|
3657
|
+
try {
|
|
3658
|
+
await fs.mkdir(paths.lockDirectory);
|
|
3659
|
+
break;
|
|
3660
|
+
} catch (error) {
|
|
3661
|
+
const code = error instanceof Error && "code" in error ? String(error.code) : "";
|
|
3662
|
+
if (code !== "EEXIST") {
|
|
3663
|
+
throw error;
|
|
3664
|
+
}
|
|
3665
|
+
const stale = await fs.stat(paths.lockDirectory).then((stats) => Date.now() - stats.mtimeMs >= COORDINATOR_STALE_LOCK_MS).catch(() => false);
|
|
3666
|
+
if (stale) {
|
|
3667
|
+
await fs.rm(paths.lockDirectory, { recursive: true, force: true }).catch(() => {
|
|
3668
|
+
});
|
|
3669
|
+
continue;
|
|
3670
|
+
}
|
|
3671
|
+
if (Date.now() >= deadline) {
|
|
3672
|
+
throw new Error(
|
|
3673
|
+
`Timed out waiting for Archal Vitest session coordinator lock in ${paths.directory}.`
|
|
3674
|
+
);
|
|
3675
|
+
}
|
|
3676
|
+
await sleep(COORDINATOR_POLL_INTERVAL_MS);
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
try {
|
|
3680
|
+
const keepalive = setInterval(() => {
|
|
3681
|
+
void fs.utimes(paths.lockDirectory, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
|
|
3682
|
+
}, 1e3);
|
|
3683
|
+
keepalive.unref();
|
|
3684
|
+
try {
|
|
3685
|
+
return await action();
|
|
3686
|
+
} finally {
|
|
3687
|
+
clearInterval(keepalive);
|
|
3688
|
+
}
|
|
3689
|
+
} finally {
|
|
3690
|
+
await fs.rm(paths.lockDirectory, { recursive: true, force: true });
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
function resolveCoordinatorLockWaitTimeoutMs(readyTimeoutMs) {
|
|
3694
|
+
return Math.max(COORDINATOR_LOCK_TIMEOUT_MS, readyTimeoutMs);
|
|
3695
|
+
}
|
|
3696
|
+
async function readCoordinatorState(paths) {
|
|
3697
|
+
if (!await fileExists(paths.statePath)) {
|
|
3698
|
+
return { kind: "missing" };
|
|
3699
|
+
}
|
|
3700
|
+
const raw = await fs.readFile(paths.statePath, "utf8");
|
|
3701
|
+
try {
|
|
3702
|
+
const normalizedState = normalizeCoordinatorState(JSON.parse(raw));
|
|
3703
|
+
if (!normalizedState) {
|
|
3704
|
+
await fs.rm(paths.statePath, { force: true }).catch(() => void 0);
|
|
3705
|
+
return { kind: "corrupt" };
|
|
3706
|
+
}
|
|
3707
|
+
return {
|
|
3708
|
+
kind: "ready",
|
|
3709
|
+
state: normalizedState
|
|
3710
|
+
};
|
|
3711
|
+
} catch {
|
|
3712
|
+
await fs.rm(paths.statePath, { force: true }).catch(() => void 0);
|
|
3713
|
+
return { kind: "corrupt" };
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
function normalizeCoordinatorState(input) {
|
|
3717
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
3718
|
+
return null;
|
|
3719
|
+
}
|
|
3720
|
+
const record = input;
|
|
3721
|
+
const session = normalizeHostedSessionSnapshot(record["session"]);
|
|
3722
|
+
if (!session) {
|
|
3723
|
+
return null;
|
|
3724
|
+
}
|
|
3725
|
+
const leases = normalizeWorkerLeases(record["leases"]);
|
|
3726
|
+
if (!leases) {
|
|
3727
|
+
return null;
|
|
3728
|
+
}
|
|
3729
|
+
return { session, leases };
|
|
3730
|
+
}
|
|
3731
|
+
function normalizeHostedSessionSnapshot(input) {
|
|
3732
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
3733
|
+
return null;
|
|
3734
|
+
}
|
|
3735
|
+
const record = input;
|
|
3736
|
+
const sessionId = typeof record["sessionId"] === "string" ? record["sessionId"].trim() : "";
|
|
3737
|
+
const apiBaseUrls = normalizeStringRecord(record["apiBaseUrls"]);
|
|
3738
|
+
const resolvedRuntime = normalizeResolvedRuntime(record["resolvedRuntime"]);
|
|
3739
|
+
if (!sessionId || !apiBaseUrls || !resolvedRuntime) {
|
|
3740
|
+
return null;
|
|
3741
|
+
}
|
|
3742
|
+
return {
|
|
3743
|
+
sessionId,
|
|
3744
|
+
apiBaseUrls,
|
|
3745
|
+
resolvedRuntime
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
function normalizeResolvedRuntime(input) {
|
|
3749
|
+
if (input === void 0 || input === null) {
|
|
3750
|
+
return null;
|
|
3751
|
+
}
|
|
3752
|
+
if (typeof input !== "object" || Array.isArray(input)) {
|
|
3753
|
+
return null;
|
|
3754
|
+
}
|
|
3755
|
+
const record = input;
|
|
3756
|
+
const resolvedServices = normalizeStringArray(record["resolvedServices"] ?? []);
|
|
3757
|
+
const resolvedSeeds = normalizeStringRecord(record["resolvedSeeds"] ?? {});
|
|
3758
|
+
const manifestVersions = normalizeStringRecord(record["manifestVersions"] ?? {});
|
|
3759
|
+
if (!resolvedServices || !resolvedSeeds || !manifestVersions) {
|
|
3760
|
+
return null;
|
|
3761
|
+
}
|
|
3762
|
+
const capabilityVersion = typeof record["capabilityVersion"] === "string" ? record["capabilityVersion"] : void 0;
|
|
3763
|
+
const runtimeVersion = typeof record["runtimeVersion"] === "string" ? record["runtimeVersion"] : void 0;
|
|
3764
|
+
if (resolvedServices.length === 0 && Object.keys(resolvedSeeds).length === 0 && Object.keys(manifestVersions).length === 0 && !capabilityVersion && !runtimeVersion) {
|
|
3765
|
+
return null;
|
|
3766
|
+
}
|
|
3767
|
+
return {
|
|
3768
|
+
resolvedServices,
|
|
3769
|
+
resolvedSeeds,
|
|
3770
|
+
manifestVersions,
|
|
3771
|
+
...capabilityVersion ? { capabilityVersion } : {},
|
|
3772
|
+
...runtimeVersion ? { runtimeVersion } : {}
|
|
3773
|
+
};
|
|
3774
|
+
}
|
|
3775
|
+
function normalizeWorkerLeases(input) {
|
|
3776
|
+
if (input === void 0) {
|
|
3777
|
+
return [];
|
|
3778
|
+
}
|
|
3779
|
+
if (!Array.isArray(input)) {
|
|
3780
|
+
return null;
|
|
3781
|
+
}
|
|
3782
|
+
return input.flatMap((lease) => {
|
|
3783
|
+
if (!lease || typeof lease !== "object" || Array.isArray(lease)) {
|
|
3784
|
+
return [];
|
|
3785
|
+
}
|
|
3786
|
+
const record = lease;
|
|
3787
|
+
const workerId = typeof record["workerId"] === "string" ? record["workerId"].trim() : "";
|
|
3788
|
+
const updatedAt = typeof record["updatedAt"] === "string" ? record["updatedAt"].trim() : "";
|
|
3789
|
+
const pid = typeof record["pid"] === "number" ? record["pid"] : Number.NaN;
|
|
3790
|
+
if (!workerId || !updatedAt || !Number.isInteger(pid) || pid <= 0) {
|
|
3791
|
+
return [];
|
|
3792
|
+
}
|
|
3793
|
+
return [{
|
|
3794
|
+
workerId,
|
|
3795
|
+
pid,
|
|
3796
|
+
updatedAt
|
|
3797
|
+
}];
|
|
3798
|
+
});
|
|
3799
|
+
}
|
|
3800
|
+
function normalizeStringArray(input) {
|
|
3801
|
+
if (!Array.isArray(input)) {
|
|
3802
|
+
return null;
|
|
3803
|
+
}
|
|
3804
|
+
const values = input.flatMap((value) => {
|
|
3805
|
+
if (typeof value !== "string") {
|
|
3806
|
+
return [];
|
|
3807
|
+
}
|
|
3808
|
+
const trimmed = value.trim();
|
|
3809
|
+
return trimmed ? [trimmed] : [];
|
|
3810
|
+
});
|
|
3811
|
+
return values.length === input.length ? values : null;
|
|
3812
|
+
}
|
|
3813
|
+
function normalizeStringRecord(input) {
|
|
3814
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
3815
|
+
return null;
|
|
3816
|
+
}
|
|
3817
|
+
const record = input;
|
|
3818
|
+
const entries = Object.entries(record).flatMap(([key, value]) => {
|
|
3819
|
+
if (typeof value !== "string") {
|
|
3820
|
+
return [];
|
|
3821
|
+
}
|
|
3822
|
+
return [[key, value]];
|
|
3823
|
+
});
|
|
3824
|
+
return entries.length === Object.keys(record).length ? Object.fromEntries(entries) : null;
|
|
3825
|
+
}
|
|
3826
|
+
async function writeCoordinatorState(paths, state) {
|
|
3827
|
+
await fs.writeFile(paths.statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
3828
|
+
}
|
|
3829
|
+
async function clearCoordinatorState(paths) {
|
|
3830
|
+
await Promise.all([
|
|
3831
|
+
fs.rm(paths.statePath, { force: true }),
|
|
3832
|
+
fs.rm(paths.reaperPidPath, { force: true })
|
|
3833
|
+
]);
|
|
3834
|
+
}
|
|
3835
|
+
async function readRunnerReaperState(paths) {
|
|
3836
|
+
const raw = await fs.readFile(paths.reaperPidPath, "utf8").catch(() => null);
|
|
3837
|
+
if (!raw) {
|
|
3838
|
+
return null;
|
|
3839
|
+
}
|
|
3840
|
+
try {
|
|
3841
|
+
const parsed = JSON.parse(raw);
|
|
3842
|
+
if (typeof parsed === "number") {
|
|
3843
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
3844
|
+
return null;
|
|
3845
|
+
}
|
|
3846
|
+
return {
|
|
3847
|
+
pid: parsed,
|
|
3848
|
+
sessionId: ""
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3851
|
+
if (!parsed || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.sessionId !== "string" || parsed.sessionId.trim().length === 0) {
|
|
3852
|
+
return null;
|
|
3853
|
+
}
|
|
3854
|
+
return {
|
|
3855
|
+
pid: parsed.pid,
|
|
3856
|
+
sessionId: parsed.sessionId
|
|
3857
|
+
};
|
|
3858
|
+
} catch {
|
|
3859
|
+
const pid = Number.parseInt(raw.trim(), 10);
|
|
3860
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
3861
|
+
return null;
|
|
3862
|
+
}
|
|
3863
|
+
return {
|
|
3864
|
+
pid,
|
|
3865
|
+
sessionId: ""
|
|
3866
|
+
};
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
async function writeRunnerReaperState(paths, state) {
|
|
3870
|
+
await fs.writeFile(paths.reaperPidPath, JSON.stringify(state) + "\n", "utf8");
|
|
3871
|
+
}
|
|
3872
|
+
async function stopRunnerReaper(paths) {
|
|
3873
|
+
const reaperState = await readRunnerReaperState(paths);
|
|
3874
|
+
if (!reaperState) {
|
|
3875
|
+
await fs.rm(paths.reaperPidPath, { force: true }).catch(() => void 0);
|
|
3876
|
+
return;
|
|
3877
|
+
}
|
|
3878
|
+
if (isProcessAlive(reaperState.pid)) {
|
|
3879
|
+
try {
|
|
3880
|
+
process.kill(reaperState.pid, "SIGTERM");
|
|
3881
|
+
} catch {
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
await fs.rm(paths.reaperPidPath, { force: true }).catch(() => void 0);
|
|
3885
|
+
}
|
|
3886
|
+
function pruneLeases(leases) {
|
|
3887
|
+
return leases.filter((lease) => isProcessAlive(lease.pid));
|
|
3888
|
+
}
|
|
3889
|
+
function runnerIsAlive(runnerPid) {
|
|
3890
|
+
return isProcessAlive(runnerPid);
|
|
3891
|
+
}
|
|
3892
|
+
function resolveRequestedSeeds(config, serviceNames) {
|
|
3893
|
+
const fileSeedServices = new Set(Object.keys(config.fileSeedContents ?? {}));
|
|
3894
|
+
const requestedSeeds = Object.fromEntries(
|
|
3895
|
+
serviceNames.flatMap((serviceName) => {
|
|
3896
|
+
if (fileSeedServices.has(serviceName)) return [];
|
|
3897
|
+
const seed = config.services[serviceName]?.seed?.trim();
|
|
3898
|
+
return seed ? [[serviceName, seed]] : [];
|
|
3899
|
+
})
|
|
3900
|
+
);
|
|
3901
|
+
return Object.keys(requestedSeeds).length > 0 ? requestedSeeds : void 0;
|
|
3902
|
+
}
|
|
3903
|
+
function retainedSessionWithoutLeases(state) {
|
|
3904
|
+
if (!state?.session) {
|
|
3905
|
+
return null;
|
|
3906
|
+
}
|
|
3907
|
+
return pruneLeases(state.leases).length === 0 ? state.session : null;
|
|
3908
|
+
}
|
|
3909
|
+
function activeWorkerSession(state) {
|
|
3910
|
+
if (!state?.session) {
|
|
3911
|
+
return null;
|
|
3912
|
+
}
|
|
3913
|
+
const leases = pruneLeases(state.leases);
|
|
3914
|
+
if (leases.length === 0) {
|
|
3915
|
+
return null;
|
|
3916
|
+
}
|
|
3917
|
+
return {
|
|
3918
|
+
session: state.session,
|
|
3919
|
+
leases
|
|
3920
|
+
};
|
|
3921
|
+
}
|
|
3922
|
+
var CoordinatedHostedSessionManager = class {
|
|
3923
|
+
constructor(paths, client, environment, config) {
|
|
3924
|
+
this.paths = paths;
|
|
3925
|
+
this.client = client;
|
|
3926
|
+
this.environment = environment;
|
|
3927
|
+
this.config = config;
|
|
3928
|
+
}
|
|
3929
|
+
paths;
|
|
3930
|
+
client;
|
|
3931
|
+
environment;
|
|
3932
|
+
config;
|
|
3933
|
+
async acquire(serviceNames, requestedSeeds) {
|
|
3934
|
+
const workerId = resolveWorkerId();
|
|
3935
|
+
const runnerPid = resolveRunnerPid(this.config);
|
|
3936
|
+
return await withCoordinatorLock(this.paths, this.environment.readyTimeoutMs, async () => {
|
|
3937
|
+
const stateResult = await readCoordinatorState(this.paths);
|
|
3938
|
+
const existingState = stateResult.kind === "ready" ? stateResult.state : null;
|
|
3939
|
+
if (stateResult.kind === "corrupt") {
|
|
3940
|
+
const recoveredSession = await this.recoverCorruptCoordinatorState(
|
|
3941
|
+
serviceNames,
|
|
3942
|
+
requestedSeeds,
|
|
3943
|
+
workerId,
|
|
3944
|
+
runnerPid
|
|
3945
|
+
);
|
|
3946
|
+
if (recoveredSession) {
|
|
3947
|
+
return { session: recoveredSession, acquisition: "reused" };
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
const activeSession = activeWorkerSession(existingState);
|
|
3951
|
+
if (activeSession && requestedSeedsMatchSession(requestedSeeds, activeSession.session)) {
|
|
3952
|
+
const reusableSession = await this.tryReuseSession(
|
|
3953
|
+
activeSession.session,
|
|
3954
|
+
serviceNames
|
|
3955
|
+
);
|
|
3956
|
+
if (!reusableSession) {
|
|
3957
|
+
await this.client.stop(activeSession.session.sessionId).catch(() => void 0);
|
|
3958
|
+
await stopRunnerReaper(this.paths);
|
|
3959
|
+
await clearCoordinatorState(this.paths);
|
|
3960
|
+
} else {
|
|
3961
|
+
await this.ensureRunnerReaper(reusableSession.sessionId, runnerPid);
|
|
3962
|
+
await writeCoordinatorState(this.paths, {
|
|
3963
|
+
session: reusableSession,
|
|
3964
|
+
leases: this.upsertLease(activeSession.leases, workerId)
|
|
3965
|
+
});
|
|
3966
|
+
return { session: reusableSession, acquisition: "reused" };
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3969
|
+
if (activeSession && !requestedSeedsMatchSession(requestedSeeds, activeSession.session)) {
|
|
3970
|
+
await this.client.stop(activeSession.session.sessionId).catch(() => void 0);
|
|
3971
|
+
await stopRunnerReaper(this.paths);
|
|
3972
|
+
await clearCoordinatorState(this.paths);
|
|
3973
|
+
}
|
|
3974
|
+
const retainedSession = retainedSessionWithoutLeases(existingState);
|
|
3975
|
+
if (retainedSession) {
|
|
3976
|
+
if (!runnerIsAlive(runnerPid)) {
|
|
3977
|
+
await this.client.stop(retainedSession.sessionId).catch(() => void 0);
|
|
3978
|
+
await stopRunnerReaper(this.paths);
|
|
3979
|
+
await clearCoordinatorState(this.paths);
|
|
3980
|
+
} else if (!requestedSeedsMatchSession(requestedSeeds, retainedSession)) {
|
|
3981
|
+
await this.client.stop(retainedSession.sessionId).catch(() => void 0);
|
|
3982
|
+
await stopRunnerReaper(this.paths);
|
|
3983
|
+
await clearCoordinatorState(this.paths);
|
|
3984
|
+
} else {
|
|
3985
|
+
const reusableSession = await this.tryReuseSession(retainedSession, serviceNames);
|
|
3986
|
+
if (reusableSession) {
|
|
3987
|
+
await this.ensureRunnerReaper(reusableSession.sessionId, runnerPid);
|
|
3988
|
+
await writeCoordinatorState(this.paths, {
|
|
3989
|
+
session: reusableSession,
|
|
3990
|
+
leases: this.upsertLease([], workerId)
|
|
3991
|
+
});
|
|
3992
|
+
return { session: reusableSession, acquisition: "reused" };
|
|
3993
|
+
}
|
|
3994
|
+
await this.client.stop(retainedSession.sessionId).catch(() => void 0);
|
|
3995
|
+
await stopRunnerReaper(this.paths);
|
|
3996
|
+
await clearCoordinatorState(this.paths);
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
const startedSession = await this.client.startRouteSession(serviceNames, requestedSeeds, {
|
|
4000
|
+
source: "test"
|
|
4001
|
+
});
|
|
4002
|
+
await this.ensureRunnerReaper(startedSession.sessionId, runnerPid);
|
|
4003
|
+
await writeCoordinatorState(this.paths, {
|
|
4004
|
+
session: startedSession,
|
|
4005
|
+
leases: this.upsertLease([], workerId)
|
|
4006
|
+
});
|
|
4007
|
+
return { session: startedSession, acquisition: "fresh" };
|
|
4008
|
+
});
|
|
4009
|
+
}
|
|
4010
|
+
async release() {
|
|
4011
|
+
const workerId = resolveWorkerId();
|
|
4012
|
+
const runnerPid = resolveRunnerPid(this.config);
|
|
4013
|
+
await withCoordinatorLock(this.paths, this.environment.readyTimeoutMs, async () => {
|
|
4014
|
+
const stateResult = await readCoordinatorState(this.paths);
|
|
4015
|
+
if (stateResult.kind !== "ready") {
|
|
4016
|
+
return;
|
|
4017
|
+
}
|
|
4018
|
+
const existingState = stateResult.state;
|
|
4019
|
+
const remainingLeases = pruneLeases(existingState.leases).filter((lease) => lease.workerId !== workerId);
|
|
4020
|
+
if (remainingLeases.length > 0) {
|
|
4021
|
+
await writeCoordinatorState(this.paths, {
|
|
4022
|
+
session: existingState.session,
|
|
4023
|
+
leases: remainingLeases
|
|
4024
|
+
});
|
|
4025
|
+
return;
|
|
4026
|
+
}
|
|
4027
|
+
const shouldRetainSession = runnerIsAlive(runnerPid) && !shouldStopSessionOnRelease();
|
|
4028
|
+
if (shouldRetainSession) {
|
|
4029
|
+
await writeCoordinatorState(this.paths, {
|
|
4030
|
+
session: existingState.session,
|
|
4031
|
+
leases: []
|
|
4032
|
+
});
|
|
4033
|
+
return;
|
|
4034
|
+
}
|
|
4035
|
+
await this.client.stop(existingState.session.sessionId);
|
|
4036
|
+
await stopRunnerReaper(this.paths);
|
|
4037
|
+
await clearCoordinatorState(this.paths);
|
|
4038
|
+
});
|
|
4039
|
+
}
|
|
4040
|
+
upsertLease(leases, workerId) {
|
|
4041
|
+
const nextLease = {
|
|
4042
|
+
workerId,
|
|
4043
|
+
pid: process.pid,
|
|
4044
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4045
|
+
};
|
|
4046
|
+
const withoutExisting = leases.filter((lease) => lease.workerId !== workerId);
|
|
4047
|
+
return [...withoutExisting, nextLease];
|
|
4048
|
+
}
|
|
4049
|
+
async recoverCorruptCoordinatorState(serviceNames, requestedSeeds, workerId, runnerPid) {
|
|
4050
|
+
const reaperState = await readRunnerReaperState(this.paths);
|
|
4051
|
+
if (!reaperState) {
|
|
4052
|
+
await clearCoordinatorState(this.paths);
|
|
4053
|
+
return null;
|
|
4054
|
+
}
|
|
4055
|
+
if (!reaperState.sessionId) {
|
|
4056
|
+
await stopRunnerReaper(this.paths);
|
|
4057
|
+
await clearCoordinatorState(this.paths);
|
|
4058
|
+
return null;
|
|
4059
|
+
}
|
|
4060
|
+
if (!runnerIsAlive(runnerPid)) {
|
|
4061
|
+
await this.client.stop(reaperState.sessionId).catch(() => void 0);
|
|
4062
|
+
await stopRunnerReaper(this.paths);
|
|
4063
|
+
await clearCoordinatorState(this.paths);
|
|
4064
|
+
return null;
|
|
4065
|
+
}
|
|
4066
|
+
const reusableSession = await this.client.reuseRouteSession(
|
|
4067
|
+
{
|
|
4068
|
+
sessionId: reaperState.sessionId,
|
|
4069
|
+
apiBaseUrls: {},
|
|
4070
|
+
resolvedRuntime: {
|
|
4071
|
+
resolvedServices: [],
|
|
4072
|
+
resolvedSeeds: {},
|
|
4073
|
+
manifestVersions: {}
|
|
4074
|
+
}
|
|
4075
|
+
},
|
|
4076
|
+
serviceNames
|
|
4077
|
+
);
|
|
4078
|
+
if (reusableSession && requestedSeedsMatchSession(requestedSeeds, reusableSession)) {
|
|
4079
|
+
await this.ensureRunnerReaper(reusableSession.sessionId, runnerPid);
|
|
4080
|
+
if (trimEnv("ARCHAL_VITEST_DISABLE_RUNNER_REAPER") === "1") {
|
|
4081
|
+
await fs.rm(this.paths.reaperPidPath, { force: true }).catch(() => void 0);
|
|
4082
|
+
}
|
|
4083
|
+
await writeCoordinatorState(this.paths, {
|
|
4084
|
+
session: reusableSession,
|
|
4085
|
+
leases: this.upsertLease([], workerId)
|
|
4086
|
+
});
|
|
4087
|
+
return reusableSession;
|
|
4088
|
+
}
|
|
4089
|
+
await this.client.stop(reaperState.sessionId).catch(() => void 0);
|
|
4090
|
+
await stopRunnerReaper(this.paths);
|
|
4091
|
+
await clearCoordinatorState(this.paths);
|
|
4092
|
+
return null;
|
|
4093
|
+
}
|
|
4094
|
+
async tryReuseSession(session, serviceNames) {
|
|
4095
|
+
try {
|
|
4096
|
+
return await this.client.reuseRouteSession(session, serviceNames);
|
|
4097
|
+
} catch {
|
|
4098
|
+
return null;
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
async ensureRunnerReaper(sessionId, runnerPid) {
|
|
4102
|
+
if (trimEnv("ARCHAL_VITEST_DISABLE_RUNNER_REAPER") === "1") {
|
|
4103
|
+
return;
|
|
4104
|
+
}
|
|
4105
|
+
const existingReaper = await readRunnerReaperState(this.paths);
|
|
4106
|
+
if (existingReaper && isProcessAlive(existingReaper.pid)) {
|
|
4107
|
+
if (existingReaper.sessionId === sessionId) {
|
|
4108
|
+
return;
|
|
4109
|
+
}
|
|
4110
|
+
await stopRunnerReaper(this.paths);
|
|
4111
|
+
}
|
|
4112
|
+
const reaperPath = resolveRuntimeModule("./runtime/hosted-session-reaper.js");
|
|
4113
|
+
const child = spawn(process.execPath, [reaperPath], {
|
|
4114
|
+
detached: true,
|
|
4115
|
+
stdio: "ignore",
|
|
4116
|
+
env: {
|
|
4117
|
+
...process.env,
|
|
4118
|
+
ARCHAL_VITEST_API_URL: this.environment.apiBaseUrl,
|
|
4119
|
+
ARCHAL_VITEST_REAPER_API_BASE_URL: this.environment.apiBaseUrl,
|
|
4120
|
+
ARCHAL_VITEST_REAPER_SESSION_ID: sessionId,
|
|
4121
|
+
ARCHAL_VITEST_REAPER_RUNNER_PID: String(runnerPid),
|
|
4122
|
+
ARCHAL_VITEST_REAPER_RENEW_INTERVAL_MS: String(this.environment.renewIntervalMs),
|
|
4123
|
+
ARCHAL_VITEST_REAPER_COORDINATOR_DIR: this.paths.directory,
|
|
4124
|
+
ARCHAL_VITEST_REAPER_LOCK_DIR: this.paths.lockDirectory,
|
|
4125
|
+
ARCHAL_VITEST_REAPER_STATE_PATH: this.paths.statePath,
|
|
4126
|
+
ARCHAL_VITEST_REAPER_PID_PATH: this.paths.reaperPidPath
|
|
4127
|
+
}
|
|
4128
|
+
});
|
|
4129
|
+
child.unref();
|
|
4130
|
+
if (!child.pid) {
|
|
4131
|
+
throw new Error("Archal Vitest runner reaper failed to start.");
|
|
4132
|
+
}
|
|
4133
|
+
await writeRunnerReaperState(this.paths, { pid: child.pid, sessionId });
|
|
4134
|
+
}
|
|
4135
|
+
};
|
|
4136
|
+
function buildNoSession() {
|
|
4137
|
+
return {
|
|
4138
|
+
sessionId: `archal-vitest-${randomUUID()}`,
|
|
4139
|
+
services: [],
|
|
4140
|
+
resolvedRuntime: {
|
|
4141
|
+
resolvedServices: [],
|
|
4142
|
+
resolvedSeeds: {},
|
|
4143
|
+
manifestVersions: {}
|
|
4144
|
+
},
|
|
4145
|
+
// No hosted session was provisioned (route-mode config had no services)
|
|
4146
|
+
// so there's no cold-start cost to report. Mark as `reused` so the
|
|
4147
|
+
// bootstrap's clone-startup log treats this as zero-cost and stays quiet.
|
|
4148
|
+
acquisition: "reused"
|
|
4149
|
+
};
|
|
4150
|
+
}
|
|
4151
|
+
function buildRoutedServices(serviceNames, apiBaseUrls, environment) {
|
|
4152
|
+
return serviceNames.map((serviceName) => {
|
|
4153
|
+
const baseUrl = apiBaseUrls[serviceName];
|
|
4154
|
+
if (!baseUrl) {
|
|
4155
|
+
throw new Error(
|
|
4156
|
+
`Hosted Vitest session did not return apiBaseUrls.${serviceName}.`
|
|
4157
|
+
);
|
|
4158
|
+
}
|
|
4159
|
+
const manifest = getSharedRouteManifest(serviceName);
|
|
4160
|
+
return {
|
|
4161
|
+
name: serviceName,
|
|
4162
|
+
mode: "route",
|
|
4163
|
+
baseUrl,
|
|
4164
|
+
upstreamBasePath: manifest?.upstreamBasePath,
|
|
4165
|
+
routedRequestHeaders: () => {
|
|
4166
|
+
const authorization = environment.auth.getAuthorizationHeader();
|
|
4167
|
+
if (!authorization) {
|
|
4168
|
+
throw new Error("Hosted Vitest routing auth is unavailable.");
|
|
4169
|
+
}
|
|
4170
|
+
const workerId = currentVitestWorkerId();
|
|
4171
|
+
const headers = {
|
|
4172
|
+
[ROUTE_CONTROL_AUTHORIZATION_HEADER2]: authorization,
|
|
4173
|
+
[ROUTE_SERVICE_ORIGIN_HEADER2]: "1"
|
|
4174
|
+
};
|
|
4175
|
+
if (workerId) headers[WORKER_ID_HEADER] = workerId;
|
|
4176
|
+
return headers;
|
|
4177
|
+
}
|
|
4178
|
+
};
|
|
4179
|
+
});
|
|
4180
|
+
}
|
|
4181
|
+
async function startHostedArchalVitestSession(config) {
|
|
4182
|
+
assertSupportedArchalVitestServices(config.services);
|
|
4183
|
+
const routedServiceNames = Object.entries(config.services).filter(([, serviceConfig]) => serviceConfig.mode === "route").map(([serviceName]) => serviceName);
|
|
4184
|
+
if (routedServiceNames.length === 0) {
|
|
4185
|
+
return buildNoSession();
|
|
4186
|
+
}
|
|
4187
|
+
const requestedSeeds = resolveRequestedSeeds(config, routedServiceNames);
|
|
4188
|
+
const environment = await resolveHostedSessionEnvironment();
|
|
4189
|
+
const hostedClient = new HostedSessionClient(environment);
|
|
4190
|
+
const coordinator = new CoordinatedHostedSessionManager(
|
|
4191
|
+
resolveCoordinatorPaths(config),
|
|
4192
|
+
hostedClient,
|
|
4193
|
+
environment,
|
|
4194
|
+
config
|
|
4195
|
+
);
|
|
4196
|
+
try {
|
|
4197
|
+
const { session, acquisition } = await coordinator.acquire(routedServiceNames, requestedSeeds);
|
|
4198
|
+
return {
|
|
4199
|
+
sessionId: session.sessionId,
|
|
4200
|
+
services: buildRoutedServices(routedServiceNames, session.apiBaseUrls, environment),
|
|
4201
|
+
resolvedRuntime: session.resolvedRuntime,
|
|
4202
|
+
acquisition,
|
|
4203
|
+
stop: async () => {
|
|
4204
|
+
try {
|
|
4205
|
+
await coordinator.release();
|
|
4206
|
+
} finally {
|
|
4207
|
+
environment.auth.stop();
|
|
4208
|
+
}
|
|
4209
|
+
}
|
|
4210
|
+
};
|
|
4211
|
+
} catch (error) {
|
|
4212
|
+
environment.auth.stop();
|
|
4213
|
+
throw error;
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
var VITEST_AUTH_LEASE_OPTIONS = {
|
|
4217
|
+
apiUrlEnvVar: "ARCHAL_VITEST_API_URL"
|
|
4218
|
+
};
|
|
4219
|
+
|
|
4220
|
+
// src/runtime/bootstrap.ts
|
|
4221
|
+
var GLOBAL_RUNTIME_KEY = "__archalVitestRuntime";
|
|
4222
|
+
var GLOBAL_SESSION_KEY = "__archalVitestSession";
|
|
4223
|
+
var GLOBAL_CLEANUP_HANDLER_KEY = "__archalVitestRuntimeCleanupHandlerRegistered";
|
|
4224
|
+
var GLOBAL_BOOTSTRAP_PROMISE_KEY = "__archalVitestRuntimeBootstrapPromise";
|
|
4225
|
+
var GLOBAL_STOP_PROMISE_KEY = "__archalVitestRuntimeStopPromise";
|
|
4226
|
+
var ARCHAL_VITEST_ROUTE_TRACE_ENV = "ARCHAL_VITEST_ROUTE_TRACE";
|
|
4227
|
+
var activeFullSession;
|
|
4228
|
+
var baselineCloneStates = /* @__PURE__ */ new Map();
|
|
4229
|
+
var baselineCaptured = false;
|
|
4230
|
+
var sessionStartedAt;
|
|
4231
|
+
function emitUsageEstimate(services) {
|
|
4232
|
+
if (sessionStartedAt === void 0 || services.length === 0) return;
|
|
4233
|
+
if (process.env["ARCHAL_VITEST_QUIET_USAGE"] === "1") return;
|
|
4234
|
+
const durationSec = (Date.now() - sessionStartedAt) / 1e3;
|
|
4235
|
+
const cloneMinutes = Math.max(1, Math.ceil(durationSec / 60)) * services.length;
|
|
4236
|
+
const cloneList = services.map((s) => s.name).sort().join(", ");
|
|
4237
|
+
process.stderr.write(
|
|
4238
|
+
`\x1B[2m[archal] ~${cloneMinutes} clone-minute${cloneMinutes === 1 ? "" : "s"} for this run (${durationSec.toFixed(1)}s x ${services.length} clone${services.length === 1 ? "" : "s"}: ${cloneList})\x1B[0m
|
|
4239
|
+
`
|
|
4240
|
+
);
|
|
4241
|
+
}
|
|
4242
|
+
function cloneStateUrl(serviceBaseUrl) {
|
|
4243
|
+
return `${cloneRootUrl(serviceBaseUrl)}/state`;
|
|
4244
|
+
}
|
|
4245
|
+
function resolveRoutedHeaders(service) {
|
|
4246
|
+
try {
|
|
4247
|
+
const headers = service.routedRequestHeaders;
|
|
4248
|
+
if (typeof headers === "function") return headers();
|
|
4249
|
+
return headers ?? {};
|
|
4250
|
+
} catch (err) {
|
|
4251
|
+
activeFullSession = void 0;
|
|
4252
|
+
throw new Error(
|
|
4253
|
+
`Archal auth expired mid-test-run for clone "${service.name}". Re-run tests to refresh. (${err instanceof Error ? err.message : String(err)})`
|
|
4254
|
+
);
|
|
4255
|
+
}
|
|
4256
|
+
}
|
|
4257
|
+
var WORKER_ID_HEADER2 = "x-archal-worker-id";
|
|
4258
|
+
var ROUTE_SERVICE_ORIGIN_HEADER3 = "x-route-service-origin";
|
|
4259
|
+
function resolveControlHeaders(service) {
|
|
4260
|
+
const all = resolveRoutedHeaders(service);
|
|
4261
|
+
const { [ROUTE_SERVICE_ORIGIN_HEADER3]: _discarded, ...rest } = all;
|
|
4262
|
+
return rest;
|
|
4263
|
+
}
|
|
4264
|
+
function resolveDefaultScopeHeaders(service) {
|
|
4265
|
+
const all = resolveControlHeaders(service);
|
|
4266
|
+
const { [WORKER_ID_HEADER2]: _discarded, ...rest } = all;
|
|
4267
|
+
return rest;
|
|
4268
|
+
}
|
|
4269
|
+
function cloneRootUrl(serviceBaseUrl) {
|
|
4270
|
+
const trimmed = serviceBaseUrl.replace(/\/+$/, "");
|
|
4271
|
+
if (/\/mcp$/i.test(trimmed)) {
|
|
4272
|
+
return `${trimmed.slice(0, -4)}/api`;
|
|
4273
|
+
}
|
|
4274
|
+
if (/\/(?:api|rest)$/i.test(trimmed)) {
|
|
4275
|
+
return trimmed;
|
|
4276
|
+
}
|
|
4277
|
+
try {
|
|
4278
|
+
const parsed = new URL(trimmed);
|
|
4279
|
+
if (/\/runtime\/[^/]+\/[^/]+$/i.test(parsed.pathname)) {
|
|
4280
|
+
return `${trimmed}/api`;
|
|
4281
|
+
}
|
|
4282
|
+
} catch {
|
|
4283
|
+
}
|
|
4284
|
+
return trimmed;
|
|
4285
|
+
}
|
|
4286
|
+
async function fetchArchalClone(serviceName, path, init) {
|
|
4287
|
+
const session = activeFullSession;
|
|
4288
|
+
if (!session) {
|
|
4289
|
+
throw new Error(
|
|
4290
|
+
"Archal clone fetch called before the route runtime was installed. Ensure archalVitestProject() or withArchal() is wired into your vitest config, and that setup has completed."
|
|
4291
|
+
);
|
|
4292
|
+
}
|
|
4293
|
+
const service = session.services.find((s) => s.name === serviceName);
|
|
4294
|
+
if (!service) {
|
|
4295
|
+
const available = session.services.map((s) => s.name).sort().join(", ") || "(none)";
|
|
4296
|
+
throw new Error(`No clone named "${serviceName}" in this session. Available: ${available}.`);
|
|
4297
|
+
}
|
|
4298
|
+
const headers = resolveControlHeaders(service);
|
|
4299
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
4300
|
+
const url = `${cloneRootUrl(service.baseUrl)}${suffix}`;
|
|
4301
|
+
const requestInit = {
|
|
4302
|
+
...init,
|
|
4303
|
+
headers: { ...headers, ...init?.headers }
|
|
4304
|
+
};
|
|
4305
|
+
return fetch(url, requestInit);
|
|
4306
|
+
}
|
|
4307
|
+
async function fetchArchalCloneDefaultScope(serviceName, path, init) {
|
|
4308
|
+
const session = activeFullSession;
|
|
4309
|
+
if (!session) {
|
|
4310
|
+
throw new Error("Archal clone fetch: no active session");
|
|
4311
|
+
}
|
|
4312
|
+
const service = session.services.find((s) => s.name === serviceName);
|
|
4313
|
+
if (!service) {
|
|
4314
|
+
throw new Error(`No clone "${serviceName}" in session`);
|
|
4315
|
+
}
|
|
4316
|
+
const headers = resolveDefaultScopeHeaders(service);
|
|
4317
|
+
const suffix = path.startsWith("/") ? path : `/${path}`;
|
|
4318
|
+
const url = `${cloneRootUrl(service.baseUrl)}${suffix}`;
|
|
4319
|
+
return fetch(url, {
|
|
4320
|
+
...init,
|
|
4321
|
+
headers: { ...headers, ...init?.headers }
|
|
4322
|
+
});
|
|
4323
|
+
}
|
|
4324
|
+
async function captureBaselineCloneStates(session) {
|
|
4325
|
+
baselineCloneStates.clear();
|
|
4326
|
+
baselineCaptured = false;
|
|
4327
|
+
await Promise.all(
|
|
4328
|
+
session.services.map(async (service) => {
|
|
4329
|
+
try {
|
|
4330
|
+
const headers = resolveDefaultScopeHeaders(service);
|
|
4331
|
+
const res = await fetch(cloneStateUrl(service.baseUrl), {
|
|
4332
|
+
method: "GET",
|
|
4333
|
+
headers: { ...headers, accept: "application/json" }
|
|
4334
|
+
});
|
|
4335
|
+
if (!res.ok) {
|
|
4336
|
+
process.stderr.write(
|
|
4337
|
+
`[archal] warning: failed to capture baseline for clone "${service.name}": ${res.status} ${res.statusText}
|
|
4338
|
+
`
|
|
4339
|
+
);
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
baselineCloneStates.set(service.name, await res.text());
|
|
4343
|
+
} catch (err) {
|
|
4344
|
+
process.stderr.write(
|
|
4345
|
+
`[archal] warning: failed to capture baseline for clone "${service.name}": ${err instanceof Error ? err.message : String(err)}
|
|
4346
|
+
`
|
|
4347
|
+
);
|
|
4348
|
+
}
|
|
4349
|
+
})
|
|
4350
|
+
);
|
|
4351
|
+
baselineCaptured = true;
|
|
4352
|
+
}
|
|
4353
|
+
async function resetArchalCloneState() {
|
|
4354
|
+
const session = activeFullSession;
|
|
4355
|
+
if (!session) {
|
|
4356
|
+
throw new Error(
|
|
4357
|
+
"resetArchalClones() called before the Archal route runtime was installed. Ensure archalVitestProject() or withArchal() is wired into your vitest config, and that setup has completed."
|
|
4358
|
+
);
|
|
4359
|
+
}
|
|
4360
|
+
if (!baselineCaptured) {
|
|
4361
|
+
throw new Error(
|
|
4362
|
+
"resetArchalClones() called but baseline state was never captured. This usually means bootstrapArchalVitestRouting() has not completed."
|
|
4363
|
+
);
|
|
4364
|
+
}
|
|
4365
|
+
await Promise.all(
|
|
4366
|
+
session.services.map(async (service) => {
|
|
4367
|
+
const baseline = baselineCloneStates.get(service.name);
|
|
4368
|
+
if (baseline === void 0) {
|
|
4369
|
+
throw new Error(
|
|
4370
|
+
`No baseline state captured for clone "${service.name}". This usually means the clone was not reachable during setup.`
|
|
4371
|
+
);
|
|
4372
|
+
}
|
|
4373
|
+
const stateUrl = cloneStateUrl(service.baseUrl);
|
|
4374
|
+
const defaultHeaders = resolveDefaultScopeHeaders(service);
|
|
4375
|
+
const defaultRes = await fetch(stateUrl, {
|
|
4376
|
+
method: "PUT",
|
|
4377
|
+
headers: { ...defaultHeaders, "content-type": "application/json" },
|
|
4378
|
+
body: baseline
|
|
4379
|
+
});
|
|
4380
|
+
if (!defaultRes.ok) {
|
|
4381
|
+
throw new Error(
|
|
4382
|
+
`Failed to reset shared state for clone "${service.name}": ${defaultRes.status} ${defaultRes.statusText}`
|
|
4383
|
+
);
|
|
4384
|
+
}
|
|
4385
|
+
const workerHeaders = resolveControlHeaders(service);
|
|
4386
|
+
const workerRes = await fetch(stateUrl, {
|
|
4387
|
+
method: "PUT",
|
|
4388
|
+
headers: { ...workerHeaders, "content-type": "application/json" },
|
|
4389
|
+
body: baseline
|
|
4390
|
+
});
|
|
4391
|
+
if (!workerRes.ok) {
|
|
4392
|
+
throw new Error(
|
|
4393
|
+
`Failed to reset per-worker state for clone "${service.name}": ${workerRes.status} ${workerRes.statusText}`
|
|
4394
|
+
);
|
|
4395
|
+
}
|
|
4396
|
+
try {
|
|
4397
|
+
const whRes = await fetch(`${cloneRootUrl(service.baseUrl)}/webhooks/pending`, {
|
|
4398
|
+
method: "DELETE",
|
|
4399
|
+
headers: workerHeaders
|
|
4400
|
+
});
|
|
4401
|
+
if (!whRes.ok && whRes.status !== 404) {
|
|
4402
|
+
process.stderr.write(
|
|
4403
|
+
`[archal] warning: failed to drain webhook queue for "${service.name}": ${whRes.status} ${whRes.statusText}
|
|
4404
|
+
`
|
|
4405
|
+
);
|
|
4406
|
+
}
|
|
4407
|
+
} catch {
|
|
4408
|
+
}
|
|
4409
|
+
})
|
|
4410
|
+
);
|
|
4411
|
+
}
|
|
4412
|
+
async function resetArchalClones() {
|
|
4413
|
+
await resetArchalCloneState();
|
|
4414
|
+
}
|
|
4415
|
+
function emitManifestDriftWarning(session) {
|
|
4416
|
+
if (process.env["ARCHAL_VITEST_QUIET_DRIFT"] === "1") return;
|
|
4417
|
+
const serverVersions = session.resolvedRuntime?.manifestVersions ?? {};
|
|
4418
|
+
const drifts = [];
|
|
4419
|
+
for (const service of session.services) {
|
|
4420
|
+
const serverVersion = serverVersions[service.name];
|
|
4421
|
+
const local = getSharedRouteManifest(service.name);
|
|
4422
|
+
if (!serverVersion || !local) continue;
|
|
4423
|
+
if (local.manifestVersion !== serverVersion) {
|
|
4424
|
+
drifts.push({
|
|
4425
|
+
service: service.name,
|
|
4426
|
+
server: serverVersion,
|
|
4427
|
+
client: local.manifestVersion
|
|
4428
|
+
});
|
|
4429
|
+
}
|
|
4430
|
+
}
|
|
4431
|
+
if (drifts.length === 0) return;
|
|
4432
|
+
const lines = drifts.map((d) => ` ${d.service}: client ${d.client} vs server ${d.server}`);
|
|
4433
|
+
process.stderr.write(
|
|
4434
|
+
"\x1B[33m[archal] manifest version drift - upgrade the `archal` package to match the hosted clones:\n" + lines.join("\n") + "\x1B[0m\n"
|
|
4435
|
+
);
|
|
4436
|
+
}
|
|
4437
|
+
function emitProgressTicker(config) {
|
|
4438
|
+
if (process.env["ARCHAL_VITEST_QUIET_PROGRESS"] === "1") {
|
|
4439
|
+
return () => {
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
const serviceNames = Object.keys(config.services).sort();
|
|
4443
|
+
const services = serviceNames.join(", ");
|
|
4444
|
+
const cloneWord = serviceNames.length === 1 ? "clone" : "clones";
|
|
4445
|
+
const start = Date.now();
|
|
4446
|
+
const ticker = setInterval(() => {
|
|
4447
|
+
const elapsed = Math.round((Date.now() - start) / 1e3);
|
|
4448
|
+
if (elapsed >= 60) {
|
|
4449
|
+
clearInterval(ticker);
|
|
4450
|
+
return;
|
|
4451
|
+
}
|
|
4452
|
+
process.stderr.write(
|
|
4453
|
+
`\x1B[2m[archal] provisioning ${services} ${cloneWord}... ${elapsed}s\x1B[0m
|
|
4454
|
+
`
|
|
4455
|
+
);
|
|
4456
|
+
}, 5e3);
|
|
4457
|
+
ticker.unref();
|
|
4458
|
+
return () => clearInterval(ticker);
|
|
4459
|
+
}
|
|
4460
|
+
function isTruthyEnv(value) {
|
|
4461
|
+
if (!value) {
|
|
4462
|
+
return false;
|
|
4463
|
+
}
|
|
4464
|
+
switch (value.trim().toLowerCase()) {
|
|
4465
|
+
case "1":
|
|
4466
|
+
case "true":
|
|
4467
|
+
case "yes":
|
|
4468
|
+
case "on":
|
|
4469
|
+
return true;
|
|
4470
|
+
default:
|
|
4471
|
+
return false;
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
function formatTraceRecord(record) {
|
|
4475
|
+
const manifestStatus = record.manifestMatched ? `matched ${record.service ?? "manifest"}` : "no manifest match";
|
|
4476
|
+
const destination = record.target === "twin" ? "service endpoint" : record.target === "upstream" ? "real upstream" : "blocked";
|
|
4477
|
+
const status = record.statusCode === void 0 ? record.error ? `error=${record.error}` : "no status" : `${record.statusCode}${record.statusText ? ` ${record.statusText}` : ""}`;
|
|
4478
|
+
return `\x1B[2m[archal:route] ${record.method} ${record.sourceUrl} | ${manifestStatus} | ${record.outcome} -> ${destination} | ${status}\x1B[0m
|
|
4479
|
+
`;
|
|
4480
|
+
}
|
|
4481
|
+
async function startSession(config) {
|
|
4482
|
+
const stopTicker = emitProgressTicker(config);
|
|
4483
|
+
try {
|
|
4484
|
+
sessionStartedAt = Date.now();
|
|
4485
|
+
return await startHostedArchalVitestSession(config);
|
|
4486
|
+
} finally {
|
|
4487
|
+
stopTicker();
|
|
4488
|
+
}
|
|
4489
|
+
}
|
|
4490
|
+
var DEFAULT_STARTUP_LOG_MIN_MS = 3e3;
|
|
4491
|
+
function resolveStartupLogThresholdMs() {
|
|
4492
|
+
const raw = process.env["ARCHAL_VITEST_STARTUP_LOG_MIN_MS"];
|
|
4493
|
+
if (!raw) return DEFAULT_STARTUP_LOG_MIN_MS;
|
|
4494
|
+
const parsed = Number.parseInt(raw.trim(), 10);
|
|
4495
|
+
if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_STARTUP_LOG_MIN_MS;
|
|
4496
|
+
return parsed;
|
|
4497
|
+
}
|
|
4498
|
+
function emitCloneStartupSummary(phases) {
|
|
4499
|
+
if (process.env["ARCHAL_VITEST_QUIET_STARTUP"] === "1") return;
|
|
4500
|
+
const threshold = resolveStartupLogThresholdMs();
|
|
4501
|
+
if (phases.totalMs < threshold) return;
|
|
4502
|
+
if (phases.cloneCount === 0) return;
|
|
4503
|
+
const cloneLabel = phases.cloneCount === 1 ? "clone" : "clones";
|
|
4504
|
+
const cloneList = phases.cloneNames.length > 0 ? ` [${phases.cloneNames.slice().sort().join(", ")}]` : "";
|
|
4505
|
+
const parts = [];
|
|
4506
|
+
if (phases.acquireMs >= 10) parts.push(`acquire=${phases.acquireMs}ms`);
|
|
4507
|
+
if (phases.seedLoadMs >= 10) parts.push(`seed-load=${phases.seedLoadMs}ms`);
|
|
4508
|
+
if (phases.baselineMs >= 10) parts.push(`baseline=${phases.baselineMs}ms`);
|
|
4509
|
+
const breakdown = parts.length > 0 ? ` (${parts.join(", ")})` : "";
|
|
4510
|
+
process.stderr.write(
|
|
4511
|
+
`\x1B[2m[archal/vitest] Started ${phases.cloneCount} ${cloneLabel}${cloneList} in ${phases.totalMs}ms${breakdown}\x1B[0m
|
|
4512
|
+
`
|
|
4513
|
+
);
|
|
4514
|
+
}
|
|
4515
|
+
var pendingStartupPhases;
|
|
4516
|
+
async function buildRuntimeConfig(scope) {
|
|
4517
|
+
const config = readArchalVitestConfigFromEnv();
|
|
4518
|
+
let acquireMs = 0;
|
|
4519
|
+
let acquisition = "unknown";
|
|
4520
|
+
let session = activeFullSession;
|
|
4521
|
+
if (!session) {
|
|
4522
|
+
const acquireStart = Date.now();
|
|
4523
|
+
session = await startSession(config);
|
|
4524
|
+
acquireMs = Date.now() - acquireStart;
|
|
4525
|
+
acquisition = session.acquisition ?? "unknown";
|
|
4526
|
+
} else {
|
|
4527
|
+
acquisition = "reused";
|
|
4528
|
+
}
|
|
4529
|
+
activeFullSession = session;
|
|
4530
|
+
pendingStartupPhases = {
|
|
4531
|
+
acquireMs,
|
|
4532
|
+
seedLoadMs: 0,
|
|
4533
|
+
baselineMs: 0,
|
|
4534
|
+
totalMs: acquireMs,
|
|
4535
|
+
acquisition,
|
|
4536
|
+
cloneCount: session.services.length,
|
|
4537
|
+
cloneNames: session.services.map((s) => s.name)
|
|
4538
|
+
};
|
|
4539
|
+
scope[GLOBAL_SESSION_KEY] = redactSessionSnapshot(session);
|
|
4540
|
+
try {
|
|
4541
|
+
const { writeFileSync, mkdirSync } = await import("fs");
|
|
4542
|
+
const { dirname: dirname2 } = await import("path");
|
|
4543
|
+
const sessionIdPath = getSessionIdFilePath(config.sessionKey);
|
|
4544
|
+
mkdirSync(dirname2(sessionIdPath), { recursive: true });
|
|
4545
|
+
writeFileSync(sessionIdPath, session.sessionId, "utf-8");
|
|
4546
|
+
} catch {
|
|
4547
|
+
}
|
|
4548
|
+
return {
|
|
4549
|
+
profiles: resolveArchalVitestServiceProfiles(config.services),
|
|
4550
|
+
services: session.services,
|
|
4551
|
+
trace: isTruthyEnv(process.env[ARCHAL_VITEST_ROUTE_TRACE_ENV]) ? {
|
|
4552
|
+
enabled: true,
|
|
4553
|
+
onTrace: (record) => {
|
|
4554
|
+
process.stderr.write(formatTraceRecord(record));
|
|
4555
|
+
}
|
|
4556
|
+
} : void 0
|
|
4557
|
+
};
|
|
4558
|
+
}
|
|
4559
|
+
async function stopArchalVitestRouting() {
|
|
4560
|
+
const scope = globalThis;
|
|
4561
|
+
if (scope[GLOBAL_BOOTSTRAP_PROMISE_KEY]) {
|
|
4562
|
+
await scope[GLOBAL_BOOTSTRAP_PROMISE_KEY].catch(() => {
|
|
4563
|
+
});
|
|
4564
|
+
}
|
|
4565
|
+
if (scope[GLOBAL_STOP_PROMISE_KEY]) {
|
|
4566
|
+
await scope[GLOBAL_STOP_PROMISE_KEY];
|
|
4567
|
+
return;
|
|
4568
|
+
}
|
|
4569
|
+
scope[GLOBAL_STOP_PROMISE_KEY] = (async () => {
|
|
4570
|
+
const runtime = scope[GLOBAL_RUNTIME_KEY];
|
|
4571
|
+
const session = activeFullSession;
|
|
4572
|
+
const services = session?.services.map((s) => ({ name: s.name })) ?? [];
|
|
4573
|
+
delete scope[GLOBAL_RUNTIME_KEY];
|
|
4574
|
+
delete scope[GLOBAL_SESSION_KEY];
|
|
4575
|
+
emitUsageEstimate(services);
|
|
4576
|
+
sessionStartedAt = void 0;
|
|
4577
|
+
activeFullSession = void 0;
|
|
4578
|
+
baselineCloneStates.clear();
|
|
4579
|
+
baselineCaptured = false;
|
|
4580
|
+
await runtime?.stop();
|
|
4581
|
+
await session?.stop?.();
|
|
4582
|
+
})();
|
|
4583
|
+
try {
|
|
4584
|
+
await scope[GLOBAL_STOP_PROMISE_KEY];
|
|
4585
|
+
} finally {
|
|
4586
|
+
delete scope[GLOBAL_STOP_PROMISE_KEY];
|
|
4587
|
+
}
|
|
4588
|
+
}
|
|
4589
|
+
async function bootstrapArchalVitestRouting() {
|
|
4590
|
+
const scope = globalThis;
|
|
4591
|
+
if (scope[GLOBAL_RUNTIME_KEY]) {
|
|
4592
|
+
return scope[GLOBAL_RUNTIME_KEY];
|
|
4593
|
+
}
|
|
4594
|
+
if (scope[GLOBAL_BOOTSTRAP_PROMISE_KEY]) {
|
|
4595
|
+
return await scope[GLOBAL_BOOTSTRAP_PROMISE_KEY];
|
|
4596
|
+
}
|
|
4597
|
+
scope[GLOBAL_BOOTSTRAP_PROMISE_KEY] = (async () => {
|
|
4598
|
+
const runtime = new NodeRouteRuntime(await buildRuntimeConfig(scope));
|
|
4599
|
+
await runtime.start();
|
|
4600
|
+
runtime.installRouting();
|
|
4601
|
+
scope[GLOBAL_RUNTIME_KEY] = runtime;
|
|
4602
|
+
const config = readArchalVitestConfigFromEnv();
|
|
4603
|
+
if (config.fileSeedContents && Object.keys(config.fileSeedContents).length > 0) {
|
|
4604
|
+
const seedStart = Date.now();
|
|
4605
|
+
await loadFileSeedsIntoClones(config, fetchArchalCloneDefaultScope, {
|
|
4606
|
+
timeoutEnvVar: "ARCHAL_VITEST_SEED_LOAD_TIMEOUT_MS",
|
|
4607
|
+
...activeFullSession?.sessionId ? { hostedSessionId: activeFullSession.sessionId } : {}
|
|
4608
|
+
});
|
|
4609
|
+
if (pendingStartupPhases) {
|
|
4610
|
+
pendingStartupPhases.seedLoadMs = Date.now() - seedStart;
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
if (activeFullSession) {
|
|
4614
|
+
emitManifestDriftWarning(activeFullSession);
|
|
4615
|
+
const baselineStart = Date.now();
|
|
4616
|
+
await captureBaselineCloneStates(activeFullSession);
|
|
4617
|
+
if (pendingStartupPhases) {
|
|
4618
|
+
pendingStartupPhases.baselineMs = Date.now() - baselineStart;
|
|
4619
|
+
}
|
|
4620
|
+
}
|
|
4621
|
+
if (pendingStartupPhases) {
|
|
4622
|
+
pendingStartupPhases.totalMs = pendingStartupPhases.acquireMs + pendingStartupPhases.seedLoadMs + pendingStartupPhases.baselineMs;
|
|
4623
|
+
emitCloneStartupSummary(pendingStartupPhases);
|
|
4624
|
+
pendingStartupPhases = void 0;
|
|
4625
|
+
}
|
|
4626
|
+
if (!scope[GLOBAL_CLEANUP_HANDLER_KEY]) {
|
|
4627
|
+
const asyncCleanupAndExit = (exitCode) => {
|
|
4628
|
+
void stopArchalVitestRouting().finally(() => {
|
|
4629
|
+
process.exit(exitCode);
|
|
4630
|
+
});
|
|
4631
|
+
};
|
|
4632
|
+
process.once("beforeExit", () => {
|
|
4633
|
+
void stopArchalVitestRouting();
|
|
4634
|
+
});
|
|
4635
|
+
process.once("disconnect", () => {
|
|
4636
|
+
void stopArchalVitestRouting();
|
|
4637
|
+
});
|
|
4638
|
+
process.once("SIGINT", () => asyncCleanupAndExit(130));
|
|
4639
|
+
process.once("SIGTERM", () => asyncCleanupAndExit(143));
|
|
4640
|
+
scope[GLOBAL_CLEANUP_HANDLER_KEY] = true;
|
|
4641
|
+
}
|
|
4642
|
+
return runtime;
|
|
4643
|
+
})();
|
|
4644
|
+
try {
|
|
4645
|
+
return await scope[GLOBAL_BOOTSTRAP_PROMISE_KEY];
|
|
4646
|
+
} finally {
|
|
4647
|
+
delete scope[GLOBAL_BOOTSTRAP_PROMISE_KEY];
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
function getInstalledArchalVitestRuntime() {
|
|
4651
|
+
return globalThis[GLOBAL_RUNTIME_KEY];
|
|
4652
|
+
}
|
|
4653
|
+
function getInstalledArchalVitestSession() {
|
|
4654
|
+
return globalThis[GLOBAL_SESSION_KEY];
|
|
4655
|
+
}
|
|
4656
|
+
|
|
4657
|
+
export {
|
|
4658
|
+
resolveRuntimeModule,
|
|
4659
|
+
ARCHAL_VITEST_CONFIG_ENV,
|
|
4660
|
+
readArchalVitestConfig,
|
|
4661
|
+
assertSupportedArchalVitestServices,
|
|
4662
|
+
fetchArchalClone,
|
|
4663
|
+
resetArchalClones,
|
|
4664
|
+
bootstrapArchalVitestRouting,
|
|
4665
|
+
getInstalledArchalVitestRuntime,
|
|
4666
|
+
getInstalledArchalVitestSession
|
|
4667
|
+
};
|