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.
Files changed (92) hide show
  1. package/README.md +9 -1
  2. package/agents/github-octokit/.archal.json +8 -0
  3. package/agents/github-octokit/Dockerfile +8 -0
  4. package/agents/github-octokit/README.md +113 -0
  5. package/agents/github-octokit/agent.mjs +54 -0
  6. package/agents/github-octokit/package.json +9 -0
  7. package/agents/github-octokit/scenarios/test-repo-access.md +27 -0
  8. package/agents/google-workspace-local-tools/Dockerfile +6 -0
  9. package/agents/google-workspace-local-tools/README.md +58 -0
  10. package/agents/google-workspace-local-tools/agent.mjs +196 -0
  11. package/agents/google-workspace-local-tools/archal-harness.json +7 -0
  12. package/agents/google-workspace-local-tools/run-input.yaml +16 -0
  13. package/agents/google-workspace-local-tools/scenario.md +29 -0
  14. package/agents/hermes/.archal.json +8 -0
  15. package/agents/hermes/Dockerfile +46 -0
  16. package/agents/hermes/README.md +87 -0
  17. package/agents/hermes/SOUL.md +27 -0
  18. package/agents/hermes/config.yaml +34 -0
  19. package/agents/hermes/drive.mjs +113 -0
  20. package/agents/hermes/scenarios/stripe-customers-read-only.md +32 -0
  21. package/agents/openclaw/.archal.json +8 -0
  22. package/agents/openclaw/Dockerfile +96 -0
  23. package/agents/openclaw/README.md +120 -0
  24. package/agents/openclaw/drive.mjs +311 -0
  25. package/agents/openclaw/package.json +9 -0
  26. package/agents/openclaw/scenarios/github-issue-triage-read-only.md +44 -0
  27. package/agents/openclaw/workspace/AGENTS.md +23 -0
  28. package/agents/openclaw/workspace/IDENTITY.md +8 -0
  29. package/agents/openclaw/workspace/SOUL.md +14 -0
  30. package/agents/openclaw/workspace/TOOLS.md +35 -0
  31. package/agents/pagination-test/README.md +24 -0
  32. package/agents/pagination-test/scenario.md +24 -0
  33. package/agents/replay-capsule-harness/README.md +29 -0
  34. package/agents/replay-capsule-harness/observability-install-offline-e2e.mts +1517 -0
  35. package/agents/replay-capsule-harness/replay-capsule-e2e.mjs +104 -0
  36. package/clone-assets/apify/tools.json +256 -22
  37. package/clone-assets/calcom/tools.json +510 -0
  38. package/clone-assets/clickup/tools.json +1258 -0
  39. package/clone-assets/customerio/tools.json +386 -0
  40. package/clone-assets/datadog/tools.json +734 -0
  41. package/clone-assets/github/tools.json +306 -25
  42. package/clone-assets/gitlab/tools.json +999 -0
  43. package/clone-assets/google-workspace/tools.json +18 -6
  44. package/clone-assets/hubspot/tools.json +1406 -0
  45. package/clone-assets/jira/fidelity.json +1 -1
  46. package/clone-assets/jira/tools.json +266 -543
  47. package/clone-assets/linear/tools.json +238 -40
  48. package/clone-assets/ownerrez/tools.json +548 -0
  49. package/clone-assets/pricelabs/tools.json +343 -0
  50. package/clone-assets/sentry/tools.json +745 -0
  51. package/clone-assets/slack/tools.json +1 -2
  52. package/clone-assets/stripe/tools.json +185 -46
  53. package/clone-assets/supabase/tools.json +437 -0
  54. package/clone-assets/unipile/tools.json +408 -0
  55. package/clone-assets/webflow/tools.json +415 -0
  56. package/dist/autoloop-worker-types-BEb_E44z.d.cts +196 -0
  57. package/dist/cli.cjs +150299 -87430
  58. package/dist/commands/autoloop-hosted-worker.cjs +43942 -0
  59. package/dist/commands/autoloop-hosted-worker.d.cts +143 -0
  60. package/dist/commands/autoloop-pr-verification.cjs +4227 -0
  61. package/dist/commands/autoloop-pr-verification.d.cts +17 -0
  62. package/dist/{vitest/chunk-L36NXAU6.js → commands/autoloop-result-parser.cjs} +16445 -18852
  63. package/dist/commands/autoloop-result-parser.d.cts +39 -0
  64. package/dist/commands/autoloop-worker.cjs +36163 -0
  65. package/dist/commands/autoloop-worker.d.cts +97 -0
  66. package/dist/harness.cjs +1 -0
  67. package/dist/index.cjs +1 -1
  68. package/dist/replay.cjs +49624 -0
  69. package/dist/replay.d.cts +4625 -0
  70. package/dist/scenarios.cjs +80343 -0
  71. package/dist/scenarios.d.cts +562 -0
  72. package/dist/vitest/chunk-6CBYFCFK.js +4667 -0
  73. package/dist/vitest/chunk-ARVS45PP.js +2764 -0
  74. package/dist/vitest/index.cjs +6011 -75261
  75. package/dist/vitest/index.d.ts +7 -6
  76. package/dist/vitest/index.js +8 -8
  77. package/dist/vitest/runtime/hosted-session-reaper.cjs +792 -34359
  78. package/dist/vitest/runtime/hosted-session-reaper.js +1 -1
  79. package/dist/vitest/runtime/setup-files.js +2 -2
  80. package/package.json +8 -3
  81. package/skills/archal-agent/SKILL.md +87 -0
  82. package/skills/{attach → autoloop}/SKILL.md +94 -120
  83. package/skills/autoloop/references/hosted-sources.md +62 -0
  84. package/skills/autoloop/references/trace-schema-mapping.md +73 -0
  85. package/skills/eval/SKILL.md +35 -1
  86. package/skills/install-agent/SKILL.md +221 -0
  87. package/skills/onboard/SKILL.md +73 -5
  88. package/skills/scenario/SKILL.md +19 -4
  89. package/skills/seed/SKILL.md +237 -0
  90. package/dist/seed/dynamic-generator.cjs +0 -45687
  91. package/dist/seed/dynamic-generator.d.cts +0 -106
  92. package/dist/vitest/chunk-WZ7SA4CK.js +0 -47369
@@ -0,0 +1,2764 @@
1
+ // ../runtime/src/session-provider.ts
2
+ import { promises as fs } from "fs";
3
+
4
+ // ../retry/dist/index.js
5
+ function abortReason(signal) {
6
+ return signal.reason instanceof Error ? signal.reason : new DOMException("The operation was aborted.", "AbortError");
7
+ }
8
+ function sleep(ms, options) {
9
+ const signal = options?.signal;
10
+ if (signal?.aborted) {
11
+ return Promise.reject(abortReason(signal));
12
+ }
13
+ if (ms <= 0) {
14
+ return Promise.resolve();
15
+ }
16
+ if (!signal) {
17
+ return new Promise((resolve) => {
18
+ setTimeout(resolve, ms);
19
+ });
20
+ }
21
+ const abortSignal = signal;
22
+ return new Promise((resolve, reject) => {
23
+ const timeout = setTimeout(() => {
24
+ abortSignal.removeEventListener("abort", onAbort);
25
+ resolve();
26
+ }, ms);
27
+ function onAbort() {
28
+ clearTimeout(timeout);
29
+ reject(abortReason(abortSignal));
30
+ }
31
+ abortSignal.addEventListener("abort", onAbort, { once: true });
32
+ });
33
+ }
34
+
35
+ // ../runtime/src/hosted-auth-errors.ts
36
+ var NON_REFRESHABLE_HOSTED_AUTH_CODES = /* @__PURE__ */ new Set([
37
+ "CLONE_ACCESS_DENIED",
38
+ "TWIN_ACCESS_DENIED",
39
+ "TWIN_SELECTION_REQUIRED"
40
+ ]);
41
+ function normalizeErrorCode(value) {
42
+ return typeof value === "string" && value.trim() ? value.trim().toUpperCase() : null;
43
+ }
44
+ function extractErrorCode(value) {
45
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
46
+ return normalizeErrorCode(value);
47
+ }
48
+ const record = value;
49
+ return normalizeErrorCode(record["code"]) ?? normalizeErrorCode(record["errno"]) ?? normalizeErrorCode(record["error"]) ?? extractErrorCode(record["cause"]);
50
+ }
51
+ function parseHostedAuthErrorCode(rawText) {
52
+ if (!rawText.trim()) return null;
53
+ try {
54
+ return extractErrorCode(JSON.parse(rawText));
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ function shouldValidateHostedAuth(status, errorCode) {
60
+ if (status === 401) {
61
+ return true;
62
+ }
63
+ if (status !== 403) {
64
+ return false;
65
+ }
66
+ return !errorCode || !NON_REFRESHABLE_HOSTED_AUTH_CODES.has(errorCode);
67
+ }
68
+ function isTerminalHostedAuthFailure(status) {
69
+ return status === 401 || status === 403;
70
+ }
71
+
72
+ // ../runtime/src/session-provider.ts
73
+ function errorMessage(error) {
74
+ return error instanceof Error ? error.message : String(error);
75
+ }
76
+ var DEFAULT_HOSTED_API_BASE_URL = "https://api.archal.ai";
77
+ var DEFAULT_REQUEST_TIMEOUT_MS = 8e3;
78
+ var DEFAULT_SESSION_CREATE_REQUEST_TIMEOUT_MS = 3e4;
79
+ var DEFAULT_SESSION_TTL_SECONDS = 30 * 60;
80
+ var DEFAULT_READY_TIMEOUT_MS = 3e5;
81
+ var DEFAULT_RENEW_INTERVAL_MS = 5 * 6e4;
82
+ var MIN_RENEW_INTERVAL_MS = 3e4;
83
+ var MIN_READY_TIMEOUT_MS = 12e4;
84
+ var SESSION_POLL_INTERVAL_MS = 2e3;
85
+ var SESSION_CREATE_MAX_ATTEMPTS = 4;
86
+ var SESSION_CREATE_RETRY_DELAYS_MS = [500, 1e3, 2e3];
87
+ var SESSION_STOP_CONFIRM_POLL_INTERVAL_MS = 250;
88
+ var SESSION_STOP_CONFIRM_MAX_ATTEMPTS = 40;
89
+ var ARCHAL_WORKSPACE_HEADER = "x-archal-workspace";
90
+ var SESSION_RESPONSE_SCOPE_HEADER = "x-archal-session-response";
91
+ var CLI_SESSION_TRANSPORT_SCOPE = "cli-transport";
92
+ var INFRASTRUCTURE_DETAIL_PATTERN = /(?:archal[-_\s]?(?:proxy|runtime|control[-_\s]?plane|service[-_\s]?gateway|openclaw[-_\s]?gateway)|control[-_\s]?plane|CLI_JWT_SECRET|CONTROL_PLANE|JWT_SECRET|ARCHAL_|runtime provider|runtime\/session|worker[-_\s]?endpoint|x[-_]?archal|service[-_\s]?gateway|service[-_\s]?runtime|provider[-_\s]?gateway|graphql[-_\s]?gateway|workflow[-_\s]?(?:runtime|harness|session)|harness[-_\s]?bootstrap|service_mcp_servers|\/service-gateway|tls intercept|sidecar|openclaw[-_\s]?gateway|credential_issuance_seed|hosted\s+clone|cloud(?:Clone|Twin)Urls|(?:clone|twin)[-_]access[-_]denied|twin[-_]not[-_]in[-_]catalog|pre[-_\s]?migration|stale[-_\s]?data)/i;
93
+ function publicHostedSessionDetail(detail) {
94
+ if (!detail?.trim()) {
95
+ return "";
96
+ }
97
+ return INFRASTRUCTURE_DETAIL_PATTERN.test(detail) ? "service routing failed to prepare the requested services." : detail;
98
+ }
99
+ function hostedSessionRequestErrorMessage(method, path, status, detail) {
100
+ const publicDetail = publicHostedSessionDetail(detail);
101
+ return `Hosted session request failed (${method} ${path}, HTTP ${status}${publicDetail ? `: ${publicDetail}` : ""}).`;
102
+ }
103
+ var HostedSessionRequestError = class extends Error {
104
+ constructor(method, path, status, detail) {
105
+ super(hostedSessionRequestErrorMessage(method, path, status, detail));
106
+ this.method = method;
107
+ this.path = path;
108
+ this.status = status;
109
+ this.name = "HostedSessionRequestError";
110
+ this.detail = publicHostedSessionDetail(detail);
111
+ Object.defineProperty(this, "internalDetail", {
112
+ value: detail,
113
+ enumerable: false,
114
+ configurable: false,
115
+ writable: false
116
+ });
117
+ }
118
+ method;
119
+ path;
120
+ status;
121
+ detail;
122
+ internalDetail;
123
+ /** @internal */
124
+ getInternalDetailForRetry() {
125
+ return this.internalDetail;
126
+ }
127
+ };
128
+ function trimEnv(name) {
129
+ const value = process.env[name]?.trim();
130
+ return value ? value : void 0;
131
+ }
132
+ function parsePositiveInteger(rawValue, fallback, minimum = 1) {
133
+ if (!rawValue) return fallback;
134
+ const normalized = rawValue.trim();
135
+ if (!/^[1-9]\d*$/.test(normalized)) return fallback;
136
+ const parsed = Number(normalized);
137
+ return Number.isSafeInteger(parsed) && parsed >= minimum ? parsed : fallback;
138
+ }
139
+ function normalizeApiBaseUrl(value) {
140
+ if (!value) return null;
141
+ const normalized = value.trim().replace(/\/+$/, "");
142
+ if (!normalized) return null;
143
+ let parsed;
144
+ try {
145
+ parsed = new URL(normalized);
146
+ } catch {
147
+ throw new Error(`Invalid ARCHAL_API_URL: "${normalized}" is not a valid URL`);
148
+ }
149
+ const isLocalhostHttp = parsed.protocol === "http:" && (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1");
150
+ if (parsed.protocol !== "https:" && !isLocalhostHttp) {
151
+ throw new Error(`Hosted API URL must use HTTPS (or localhost HTTP). Received ${normalized}.`);
152
+ }
153
+ const pathname = parsed.pathname.replace(/\/+$/, "");
154
+ parsed.pathname = pathname.endsWith("/api") ? pathname.slice(0, -4) || "/" : pathname || "/";
155
+ parsed.search = "";
156
+ parsed.hash = "";
157
+ return parsed.toString().replace(/\/$/, "");
158
+ }
159
+ function resolveHostedWorkspaceHeader() {
160
+ return trimEnv("ARCHAL_WORKSPACE_ID");
161
+ }
162
+ async function readJsonResponse(response) {
163
+ if (response.status === 204) return void 0;
164
+ return await response.json();
165
+ }
166
+ async function requestJson(environment, method, path, body, options) {
167
+ const wantsSessionTransportResponse = method === "POST" && path === "/api/sessions" || method === "GET" && /^\/api\/sessions\/[^/?#]+$/.test(path);
168
+ const sendRequest = async (forceValidation = false) => {
169
+ await environment.auth.ensureFresh(forceValidation);
170
+ const authorization = environment.auth.getAuthorizationHeader();
171
+ if (!authorization) {
172
+ throw new Error("Hosted session auth is unavailable.");
173
+ }
174
+ const headers = {
175
+ authorization,
176
+ "content-type": "application/json"
177
+ };
178
+ if (wantsSessionTransportResponse) {
179
+ headers[SESSION_RESPONSE_SCOPE_HEADER] = CLI_SESSION_TRANSPORT_SCOPE;
180
+ }
181
+ const requestInit = {
182
+ method,
183
+ headers,
184
+ signal: AbortSignal.timeout(options?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS)
185
+ };
186
+ const workspaceHeader = resolveHostedWorkspaceHeader();
187
+ if (workspaceHeader) {
188
+ headers[ARCHAL_WORKSPACE_HEADER] = workspaceHeader;
189
+ }
190
+ if (body !== void 0) {
191
+ if (method === "GET") {
192
+ throw new Error(`session-provider request: GET ${path} cannot carry a body \u2014 pass query params via the URL instead.`);
193
+ }
194
+ requestInit.body = JSON.stringify(body);
195
+ }
196
+ return await fetch(`${environment.apiBaseUrl}${path}`, requestInit);
197
+ };
198
+ let response = await sendRequest();
199
+ const responseErrorCode = response.ok ? null : parseHostedAuthErrorCode(
200
+ await response.clone().text().catch(() => "")
201
+ );
202
+ if (shouldValidateHostedAuth(response.status, responseErrorCode)) {
203
+ response = await sendRequest(true);
204
+ }
205
+ if (!response.ok) {
206
+ const detail = await response.text().catch(() => "");
207
+ throw new HostedSessionRequestError(method, path, response.status, detail);
208
+ }
209
+ return await readJsonResponse(response);
210
+ }
211
+ var TRANSIENT_HOSTED_SESSION_DETAIL_PATTERNS = [
212
+ /session_provision_failed/i,
213
+ /\btimed?\s*out\b/i,
214
+ /\btimeout\b/i,
215
+ /(HTTP |status[=:]\s*)50[234]\b/i,
216
+ // upstream 502/503/504 (anchored to canonical HTTP/status prefixes)
217
+ /upstream\s+returned\s+50[234]\b/i,
218
+ // legacy phrasing in some emitters
219
+ /\bECONNRESET\b/i,
220
+ /\bECONNREFUSED\b/i,
221
+ /\bETIMEDOUT\b/i,
222
+ /\bEAI_AGAIN\b/i,
223
+ /network\s+(error|failure)/i
224
+ ];
225
+ function detailLooksTransient(detail) {
226
+ return TRANSIENT_HOSTED_SESSION_DETAIL_PATTERNS.some((pattern) => pattern.test(detail));
227
+ }
228
+ var TERMINAL_HOSTED_WORKER_STATES = /* @__PURE__ */ new Set(["dead", "failed"]);
229
+ function serviceWorkerEntries(workerStates, services) {
230
+ if (!workerStates) {
231
+ return [];
232
+ }
233
+ return services.map((service) => {
234
+ const state = workerStates[service];
235
+ return typeof state === "string" ? [service, state] : null;
236
+ }).filter((entry) => entry !== null);
237
+ }
238
+ function hasTerminalWorkerState(workerStates, services) {
239
+ return serviceWorkerEntries(workerStates, services).some(
240
+ ([, state]) => TERMINAL_HOSTED_WORKER_STATES.has(state.toLowerCase())
241
+ );
242
+ }
243
+ function formatServiceWorkerStateMap(workerStates, services) {
244
+ const entries = serviceWorkerEntries(workerStates, services);
245
+ if (entries.length === 0) {
246
+ return "none";
247
+ }
248
+ return entries.map(([service, state]) => `${service}:${state}`).join(", ");
249
+ }
250
+ function formatWorkerFailureDetail(failure) {
251
+ const parts = [
252
+ failure.lastStatus,
253
+ failure.stopCode ? `stopCode=${failure.stopCode}` : void 0,
254
+ typeof failure.containerExitCode === "number" ? `exit=${failure.containerExitCode}` : void 0,
255
+ failure.stoppedReason ? `reason=${publicHostedSessionDetail(failure.stoppedReason)}` : void 0,
256
+ failure.containerReason ? `container=${publicHostedSessionDetail(failure.containerReason)}` : void 0
257
+ ].filter((part) => Boolean(part));
258
+ return `${failure.twinName ?? "worker"}:${parts.join(",") || "failed"}`;
259
+ }
260
+ function formatWorkerFailureDetails(failures) {
261
+ if (!failures?.length) {
262
+ return void 0;
263
+ }
264
+ return failures.slice(0, 5).map(formatWorkerFailureDetail).join(", ");
265
+ }
266
+ var HostedSessionReadinessError = class extends Error {
267
+ lastError;
268
+ status;
269
+ hasTerminalWorkerState;
270
+ allRequestedWorkersTerminal;
271
+ requestedServiceCount;
272
+ retryableFailure;
273
+ internalLastError;
274
+ constructor(status, health, statusWorkerStates, services) {
275
+ const healthFailureReason = health.failure?.reason?.trim() || null;
276
+ const internalLastError = status.lastError?.trim() || healthFailureReason;
277
+ const publicLastError = publicHostedSessionDetail(internalLastError) || null;
278
+ const healthWorkerStates = toWorkerStates(health.clones);
279
+ const workerFailureDetails = formatWorkerFailureDetails(health.failure?.workerFailures);
280
+ const stateSummary = [
281
+ `status=${status.status}`,
282
+ `status.alive=${status.alive ?? "unknown"}`,
283
+ `workers=${formatServiceWorkerStateMap(statusWorkerStates, services)}`,
284
+ `health.alive=${health.alive}`,
285
+ `health.clones=${formatServiceWorkerStateMap(healthWorkerStates, services)}`,
286
+ workerFailureDetails ? `worker.failures=${workerFailureDetails}` : void 0
287
+ ].filter((part) => Boolean(part)).join("; ");
288
+ super(
289
+ `Hosted session ${status.status}${publicLastError ? `: ${publicLastError}` : ""} (${stateSummary})`
290
+ );
291
+ this.name = "HostedSessionReadinessError";
292
+ this.lastError = publicLastError;
293
+ this.status = status.status;
294
+ this.internalLastError = internalLastError;
295
+ this.retryableFailure = health.failure?.retryable;
296
+ this.requestedServiceCount = services.length;
297
+ this.hasTerminalWorkerState = hasTerminalWorkerState(statusWorkerStates, services) || hasTerminalWorkerState(healthWorkerStates, services);
298
+ this.allRequestedWorkersTerminal = services.length > 0 && services.every((service) => {
299
+ const state = statusWorkerStates?.[service] ?? healthWorkerStates?.[service];
300
+ return typeof state === "string" && TERMINAL_HOSTED_WORKER_STATES.has(state.toLowerCase());
301
+ });
302
+ }
303
+ /** @internal */
304
+ getInternalDetailForRetry() {
305
+ return this.internalLastError ?? "";
306
+ }
307
+ };
308
+ function isTransientHostedSessionStartError(error) {
309
+ if (error instanceof HostedSessionReadinessError) {
310
+ if (error.retryableFailure === true) {
311
+ return true;
312
+ }
313
+ if (error.retryableFailure === false) {
314
+ return false;
315
+ }
316
+ const retryDetail = error.getInternalDetailForRetry() || error.lastError;
317
+ if (retryDetail) {
318
+ return detailLooksTransient(retryDetail);
319
+ }
320
+ if (error.hasTerminalWorkerState) {
321
+ return error.allRequestedWorkersTerminal && error.requestedServiceCount > 1;
322
+ }
323
+ return error.status === "failed";
324
+ }
325
+ if (error instanceof HostedSessionRequestError) {
326
+ if (error.status === 401 || error.status === 429 || error.status === 500 || error.status === 502 || error.status === 503 || error.status === 504) {
327
+ return true;
328
+ }
329
+ return detailLooksTransient(error.getInternalDetailForRetry());
330
+ }
331
+ if (error instanceof Error) {
332
+ if (error.message.startsWith("Hosted session timed out waiting for readiness")) {
333
+ return true;
334
+ }
335
+ if (error.message.startsWith("Hosted session failed:")) {
336
+ const detail = error.message.slice("Hosted session failed:".length).trim();
337
+ return detailLooksTransient(detail);
338
+ }
339
+ if (error.message === "Hosted session failed") {
340
+ return true;
341
+ }
342
+ return error.name === "AbortError" || error.name === "TimeoutError" || error.name === "TypeError";
343
+ }
344
+ return false;
345
+ }
346
+ function describeUnknownError(error) {
347
+ if (error instanceof Error) {
348
+ return error.message;
349
+ }
350
+ return String(error);
351
+ }
352
+ function buildHostedSessionCleanupError(sessionId, startError, cleanupError) {
353
+ return new AggregateError(
354
+ [startError, cleanupError],
355
+ `Hosted session ${sessionId} failed readiness and cleanup failed: ${describeUnknownError(cleanupError)}`
356
+ );
357
+ }
358
+ function isHostedSessionAlreadyGoneError(error) {
359
+ return error instanceof HostedSessionRequestError && error.method === "DELETE" && (error.status === 404 || error.status === 410);
360
+ }
361
+ async function fileExists(path) {
362
+ try {
363
+ await fs.access(path);
364
+ return true;
365
+ } catch {
366
+ return false;
367
+ }
368
+ }
369
+ function toWorkerStates(value) {
370
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
371
+ return void 0;
372
+ }
373
+ const entries = Object.entries(value).filter(([, status]) => typeof status === "string");
374
+ if (entries.length === 0) {
375
+ return void 0;
376
+ }
377
+ return Object.fromEntries(entries);
378
+ }
379
+ function workersReady(workerStates, services) {
380
+ if (!workerStates) return false;
381
+ return services.every((service) => workerStates[service] === "ready");
382
+ }
383
+ function buildResolvedRuntime(serviceNames, source) {
384
+ return {
385
+ resolvedServices: source?.resolvedServices?.length ? [...source.resolvedServices] : [...serviceNames],
386
+ resolvedSeeds: { ...source?.resolvedSeeds },
387
+ manifestVersions: { ...source?.manifestVersions },
388
+ capabilityVersion: source?.capabilityVersion,
389
+ runtimeVersion: source?.runtimeVersion
390
+ };
391
+ }
392
+ function hasNonEmptyApiBaseUrls(value) {
393
+ if (!value) {
394
+ return false;
395
+ }
396
+ return Object.values(value).some((entry) => typeof entry === "string" && entry.trim().length > 0);
397
+ }
398
+ function coalesceApiBaseUrls(primary, fallback) {
399
+ if (hasNonEmptyApiBaseUrls(primary)) {
400
+ return primary;
401
+ }
402
+ if (hasNonEmptyApiBaseUrls(fallback)) {
403
+ return fallback;
404
+ }
405
+ return {};
406
+ }
407
+ function isTerminalHostedSessionStatus(status) {
408
+ return status === "failed" || status === "expired" || status === "completed" || status === "ended" || status === "tearing_down";
409
+ }
410
+ function hasResolvedRuntimeMetadata(value) {
411
+ const hasResolvedServices = Array.isArray(value?.resolvedServices) && value.resolvedServices.some((service) => typeof service === "string" && service.trim().length > 0);
412
+ const hasResolvedSeeds = Boolean(
413
+ value?.resolvedSeeds && Object.keys(value.resolvedSeeds).some((service) => {
414
+ const seed = value.resolvedSeeds?.[service];
415
+ return typeof service === "string" && service.trim().length > 0 && typeof seed === "string" && seed.trim().length > 0;
416
+ })
417
+ );
418
+ const hasManifestVersions = Boolean(
419
+ value?.manifestVersions && Object.keys(value.manifestVersions).some((service) => {
420
+ const version = value.manifestVersions?.[service];
421
+ return typeof service === "string" && service.trim().length > 0 && typeof version === "string" && version.trim().length > 0;
422
+ })
423
+ );
424
+ return hasResolvedServices || hasResolvedSeeds || hasManifestVersions || value?.capabilityVersion !== void 0 || value?.runtimeVersion !== void 0;
425
+ }
426
+ function requestedSeedsMatchSession(requestedSeeds, session) {
427
+ if (!requestedSeeds) {
428
+ return true;
429
+ }
430
+ const resolvedSeeds = session.resolvedRuntime?.resolvedSeeds;
431
+ return Object.entries(requestedSeeds).every(
432
+ ([serviceName, requestedSeed]) => resolvedSeeds?.[serviceName] === requestedSeed
433
+ );
434
+ }
435
+ async function readHostedSessionStatus(environment, sessionId) {
436
+ try {
437
+ return await requestJson(
438
+ environment,
439
+ "GET",
440
+ `/api/sessions/${encodeURIComponent(sessionId)}`
441
+ ) ?? null;
442
+ } catch (error) {
443
+ const message = errorMessage(error);
444
+ if (message.includes("HTTP 401") || message.includes("HTTP 403") || message.includes("HTTP 404")) {
445
+ return null;
446
+ }
447
+ throw error;
448
+ }
449
+ }
450
+ async function confirmHostedSessionStopped(environment, sessionId) {
451
+ const statusPath = `/api/sessions/${encodeURIComponent(sessionId)}`;
452
+ for (let attempt = 1; attempt <= SESSION_STOP_CONFIRM_MAX_ATTEMPTS; attempt += 1) {
453
+ try {
454
+ const status = await requestJson(
455
+ environment,
456
+ "GET",
457
+ statusPath
458
+ );
459
+ if (status && isTerminalHostedSessionStatus(status.status)) {
460
+ return true;
461
+ }
462
+ } catch (error) {
463
+ if (error instanceof HostedSessionRequestError && error.method === "GET" && error.path === statusPath && (error.status === 404 || error.status === 410)) {
464
+ return true;
465
+ }
466
+ }
467
+ if (attempt < SESSION_STOP_CONFIRM_MAX_ATTEMPTS) {
468
+ await sleep(SESSION_STOP_CONFIRM_POLL_INTERVAL_MS);
469
+ }
470
+ }
471
+ return false;
472
+ }
473
+ async function readHostedSessionHealth(environment, sessionId) {
474
+ try {
475
+ return await requestJson(
476
+ environment,
477
+ "GET",
478
+ `/api/sessions/${encodeURIComponent(sessionId)}/health`
479
+ ) ?? null;
480
+ } catch (error) {
481
+ const message = errorMessage(error);
482
+ if (message.includes("HTTP 401") || message.includes("HTTP 403") || message.includes("HTTP 404")) {
483
+ return null;
484
+ }
485
+ throw error;
486
+ }
487
+ }
488
+ async function resolveReusableSession(environment, session, serviceNames) {
489
+ const [status, health] = await Promise.all([
490
+ readHostedSessionStatus(environment, session.sessionId),
491
+ readHostedSessionHealth(environment, session.sessionId)
492
+ ]);
493
+ if (!status || !health) {
494
+ return null;
495
+ }
496
+ if (isTerminalHostedSessionStatus(status.status)) {
497
+ return null;
498
+ }
499
+ const workerStates = toWorkerStates(status.workers) ?? toWorkerStates(status.clones);
500
+ const statusWorkersReady = workerStates ? workersReady(workerStates, serviceNames) : true;
501
+ if (!health.alive || !status.alive && status.status !== "ready" && status.status !== "running" || !statusWorkersReady || !workersReady(health.clones, serviceNames)) {
502
+ return null;
503
+ }
504
+ return {
505
+ sessionId: session.sessionId,
506
+ apiBaseUrls: coalesceApiBaseUrls(status.apiBaseUrls, session.apiBaseUrls),
507
+ resolvedRuntime: buildResolvedRuntime(
508
+ serviceNames,
509
+ hasResolvedRuntimeMetadata(status) ? status : session.resolvedRuntime
510
+ )
511
+ };
512
+ }
513
+ async function waitForHostedSessionReady(environment, sessionId, serviceNames) {
514
+ const deadline = Date.now() + environment.readyTimeoutMs;
515
+ let lastIssue = "session still starting";
516
+ while (Date.now() < deadline) {
517
+ let status;
518
+ let health;
519
+ try {
520
+ [status, health] = await Promise.all([
521
+ requestJson(
522
+ environment,
523
+ "GET",
524
+ `/api/sessions/${encodeURIComponent(sessionId)}`
525
+ ),
526
+ requestJson(
527
+ environment,
528
+ "GET",
529
+ `/api/sessions/${encodeURIComponent(sessionId)}/health`
530
+ )
531
+ ]);
532
+ } catch (error) {
533
+ if (!isTransientHostedSessionStartError(error)) {
534
+ throw error;
535
+ }
536
+ lastIssue = errorMessage(error);
537
+ await sleep(SESSION_POLL_INTERVAL_MS);
538
+ continue;
539
+ }
540
+ if (!status || !health) {
541
+ await sleep(SESSION_POLL_INTERVAL_MS);
542
+ continue;
543
+ }
544
+ const statusWorkerStates = toWorkerStates(status.workers) ?? toWorkerStates(status.clones);
545
+ const healthWorkerStates = toWorkerStates(health.clones);
546
+ if (isTerminalHostedSessionStatus(status.status) || hasTerminalWorkerState(statusWorkerStates, serviceNames) || hasTerminalWorkerState(healthWorkerStates, serviceNames)) {
547
+ throw new HostedSessionReadinessError(status, health, statusWorkerStates, serviceNames);
548
+ }
549
+ const statusWorkersReady = statusWorkerStates ? workersReady(statusWorkerStates, serviceNames) : true;
550
+ if (health.alive && (status.alive || status.status === "ready") && statusWorkersReady && workersReady(health.clones, serviceNames)) {
551
+ return status;
552
+ }
553
+ const workerSummary = Object.entries(statusWorkerStates ?? {}).map(([service, state]) => `${service}:${state}`).join(", ");
554
+ lastIssue = workerSummary ? `workers not ready (${workerSummary})` : `status=${status.status}`;
555
+ await sleep(SESSION_POLL_INTERVAL_MS);
556
+ }
557
+ throw new Error(`Hosted session timed out waiting for readiness (${lastIssue}).`);
558
+ }
559
+ var HostedSessionClient = class {
560
+ constructor(environment) {
561
+ this.environment = environment;
562
+ }
563
+ environment;
564
+ async reuseRouteSession(session, serviceNames) {
565
+ return await resolveReusableSession(this.environment, session, serviceNames);
566
+ }
567
+ async startRouteSession(serviceNames, requestedSeeds, options) {
568
+ const requestBody = {
569
+ clones: serviceNames,
570
+ transport: "cloud",
571
+ routing: {
572
+ mode: "route",
573
+ services: serviceNames,
574
+ requestedSeeds
575
+ },
576
+ ttlSeconds: this.environment.ttlSeconds,
577
+ cloneReadyTimeoutMs: this.environment.readyTimeoutMs,
578
+ ...requestedSeeds ? { seeds: requestedSeeds } : {},
579
+ ...options?.source ? { source: options.source } : {}
580
+ };
581
+ let lastError;
582
+ for (let attempt = 1; attempt <= SESSION_CREATE_MAX_ATTEMPTS; attempt += 1) {
583
+ let started;
584
+ try {
585
+ started = await requestJson(
586
+ this.environment,
587
+ "POST",
588
+ "/api/sessions",
589
+ requestBody,
590
+ { timeoutMs: DEFAULT_SESSION_CREATE_REQUEST_TIMEOUT_MS }
591
+ );
592
+ if (!started) {
593
+ throw new Error("Hosted session failed to start.");
594
+ }
595
+ const ready = await waitForHostedSessionReady(this.environment, started.sessionId, serviceNames);
596
+ return {
597
+ sessionId: started.sessionId,
598
+ apiBaseUrls: coalesceApiBaseUrls(ready.apiBaseUrls, started.apiBaseUrls),
599
+ resolvedRuntime: buildResolvedRuntime(
600
+ serviceNames,
601
+ hasResolvedRuntimeMetadata(ready) ? ready : started
602
+ )
603
+ };
604
+ } catch (error) {
605
+ lastError = error;
606
+ if (started?.sessionId) {
607
+ try {
608
+ await this.stop(started.sessionId);
609
+ } catch (cleanupError) {
610
+ throw buildHostedSessionCleanupError(started.sessionId, error, cleanupError);
611
+ }
612
+ }
613
+ if (attempt >= SESSION_CREATE_MAX_ATTEMPTS || !isTransientHostedSessionStartError(error)) {
614
+ throw error;
615
+ }
616
+ await sleep(
617
+ SESSION_CREATE_RETRY_DELAYS_MS[attempt - 1] ?? SESSION_CREATE_RETRY_DELAYS_MS.at(-1) ?? 2e3
618
+ );
619
+ }
620
+ }
621
+ throw lastError instanceof Error ? lastError : new Error("Hosted session failed to start.");
622
+ }
623
+ async stop(sessionId) {
624
+ try {
625
+ await requestJson(
626
+ this.environment,
627
+ "DELETE",
628
+ `/api/sessions/${encodeURIComponent(sessionId)}`
629
+ );
630
+ } catch (error) {
631
+ if (isHostedSessionAlreadyGoneError(error)) {
632
+ return;
633
+ }
634
+ if (await confirmHostedSessionStopped(this.environment, sessionId)) {
635
+ return;
636
+ }
637
+ throw error;
638
+ }
639
+ }
640
+ };
641
+
642
+ // ../runtime/src/auth-lease.ts
643
+ import { promises as fs2 } from "fs";
644
+ import { join as join2 } from "path";
645
+
646
+ // ../node-auth/src/constants.ts
647
+ var CREDENTIALS_FILE = "credentials.json";
648
+ var CREDENTIALS_KEY_FILE = "credentials.key";
649
+ var AUTH_TOKEN_ENV_VAR = "ARCHAL_TOKEN";
650
+ var TOKEN_ENCRYPTION_PREFIX = "enc-v1";
651
+ var CREDENTIALS_MASTER_KEY_ENV_VAR = "ARCHAL_CREDENTIALS_MASTER_KEY";
652
+ var KEYCHAIN_SERVICE = "archal-cli";
653
+ var KEYCHAIN_ACCOUNT = "credentials-master-key";
654
+ var HOSTED_DEFAULT_AUTH_BASE_URL = "https://www.archal.ai";
655
+ var STRICT_ENDPOINTS_ENV_VAR = "ARCHAL_STRICT_ENDPOINTS";
656
+ var REQUEST_TIMEOUT_MS = 8e3;
657
+ var ENV_TOKEN_FALLBACK_TTL_SECONDS = 24 * 60 * 60;
658
+
659
+ // ../node-auth/src/types.ts
660
+ function isPlan(value) {
661
+ return value === "free" || value === "pro" || value === "enterprise";
662
+ }
663
+
664
+ // ../node-auth/src/errors.ts
665
+ function errorMessage2(error) {
666
+ return error instanceof Error ? error.message : String(error);
667
+ }
668
+
669
+ // ../node-auth/src/credential-store.ts
670
+ import { spawnSync } from "child_process";
671
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
672
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "fs";
673
+ import { homedir, tmpdir } from "os";
674
+ import { dirname, join } from "path";
675
+
676
+ // ../clone-catalog/dist/index.js
677
+ var GENERATED_CLONE_CATALOG = [
678
+ {
679
+ id: "apify",
680
+ icon: "AP",
681
+ name: "Apify",
682
+ description: "Actors, runs, datasets, key-value stores, and request queues.",
683
+ stage: "preview",
684
+ toolCount: 59,
685
+ transport: "rest",
686
+ architectureClass: "domain-simulator",
687
+ domainPrimitives: [
688
+ "Apify upstream error and pagination envelopes",
689
+ "dataset, key-value store, and request-queue stateful projections",
690
+ "fixture-backed fallback semantics for uncovered REST paths"
691
+ ],
692
+ behaviorOwner: "src/upstream.ts",
693
+ opsCoverage: null
694
+ },
695
+ {
696
+ id: "calcom",
697
+ icon: "CC",
698
+ name: "Cal.com",
699
+ description: "Event types, schedules, availability slots, bookings, calendars, and webhooks.",
700
+ stage: "preview",
701
+ toolCount: 18,
702
+ transport: "both",
703
+ architectureClass: "domain-simulator",
704
+ domainPrimitives: [
705
+ "schedule and event-type availability projection",
706
+ "booking create, cancel, and reschedule lifecycle",
707
+ "host-wide busy interval and slot computation",
708
+ "webhook summary/detail lifecycle projection"
709
+ ],
710
+ behaviorOwner: "src/state/relationships.ts",
711
+ opsCoverage: null
712
+ },
713
+ {
714
+ id: "clickup",
715
+ icon: "CU",
716
+ name: "ClickUp",
717
+ description: "Teams, spaces, folders, lists, tasks, comments, tags, time tracking, and checklists.",
718
+ stage: "preview",
719
+ toolCount: 40,
720
+ transport: "both",
721
+ architectureClass: "standard-core",
722
+ domainPrimitives: [],
723
+ behaviorOwner: null,
724
+ opsCoverage: null
725
+ },
726
+ {
727
+ id: "customerio",
728
+ icon: "CI",
729
+ name: "Customer.io",
730
+ description: "Messaging automation across campaigns, broadcasts, transactional email, segments, customers, and message delivery.",
731
+ stage: "preview",
732
+ toolCount: 22,
733
+ transport: "rest",
734
+ architectureClass: "standard-core",
735
+ domainPrimitives: [],
736
+ behaviorOwner: null,
737
+ opsCoverage: null
738
+ },
739
+ {
740
+ id: "datadog",
741
+ icon: "DD",
742
+ name: "Datadog",
743
+ description: "Observability REST API \u2014 metrics, logs, monitors, dashboards, SLOs, incidents, teams, users, and service definitions.",
744
+ stage: "preview",
745
+ toolCount: 63,
746
+ transport: "rest",
747
+ architectureClass: "domain-simulator",
748
+ domainPrimitives: [
749
+ "time-series query windows",
750
+ "log query filtering",
751
+ "monitor evaluation and alert state"
752
+ ],
753
+ behaviorOwner: "src/domain/simulator.ts",
754
+ opsCoverage: null
755
+ },
756
+ {
757
+ id: "discord",
758
+ icon: "DC",
759
+ name: "Discord",
760
+ description: "Guilds, channels, messages, webhooks, threads, commands, and interaction responses.",
761
+ stage: "preview",
762
+ toolCount: 67,
763
+ transport: "both",
764
+ architectureClass: "domain-simulator",
765
+ domainPrimitives: [
766
+ "channel membership",
767
+ "message timeline ordering",
768
+ "thread and event delivery semantics"
769
+ ],
770
+ behaviorOwner: "src/domain/discord-simulator.ts",
771
+ opsCoverage: 14
772
+ },
773
+ {
774
+ id: "github",
775
+ icon: "GH",
776
+ name: "GitHub",
777
+ description: "Repos, issues, pull requests, branches, and commits.",
778
+ stage: "public",
779
+ toolCount: 26,
780
+ transport: "both",
781
+ architectureClass: "domain-simulator",
782
+ domainPrimitives: [
783
+ "repository branch, commit, tree, and diff projection",
784
+ "issue and pull-request lifecycle projection",
785
+ "review, comment, and status/check aggregation",
786
+ "permission-scoped reads and webhook event delivery"
787
+ ],
788
+ behaviorOwner: "src/state/transition-rules.ts",
789
+ opsCoverage: 96
790
+ },
791
+ {
792
+ id: "gitlab",
793
+ icon: "GL",
794
+ name: "GitLab",
795
+ description: "Projects, branches, commits, issues, merge requests, pipelines, labels, milestones, releases, and webhooks.",
796
+ stage: "preview",
797
+ toolCount: 46,
798
+ transport: "both",
799
+ architectureClass: "domain-simulator",
800
+ domainPrimitives: [
801
+ "issue and merge-request state transitions",
802
+ "note/comment ordering and count projection",
803
+ "repository branch, commit, merge, and diff projection",
804
+ "GitLab webhook subscription, payload, and header emission"
805
+ ],
806
+ behaviorOwner: "src/state/webhook-delivery-config.ts",
807
+ opsCoverage: null
808
+ },
809
+ {
810
+ id: "google-workspace",
811
+ icon: "GW",
812
+ name: "Google Workspace",
813
+ description: "Gmail, Calendar, Drive, Sheets, and Contacts.",
814
+ stage: "preview",
815
+ toolCount: 249,
816
+ transport: "both",
817
+ architectureClass: "domain-simulator",
818
+ domainPrimitives: [
819
+ "Gmail thread/history engine",
820
+ "Drive changes and permissions",
821
+ "Calendar recurrence and watch sync"
822
+ ],
823
+ behaviorOwner: "src/domain/google-workspace-simulator.ts",
824
+ opsCoverage: 64
825
+ },
826
+ {
827
+ id: "hubspot",
828
+ icon: "HS",
829
+ name: "HubSpot",
830
+ description: "CRM contacts, companies, deals, tickets, and engagements.",
831
+ stage: "preview",
832
+ toolCount: 41,
833
+ transport: "rest",
834
+ architectureClass: "replay-shell",
835
+ domainPrimitives: [
836
+ "CRM object state and associations",
837
+ "Pipeline and property schema state",
838
+ "Recording-backed REST/MCP aliases awaiting stateful lift"
839
+ ],
840
+ behaviorOwner: null,
841
+ opsCoverage: 99
842
+ },
843
+ {
844
+ id: "jira",
845
+ icon: "JR",
846
+ name: "Jira",
847
+ description: "Issues, projects, boards, sprints, and versions.",
848
+ stage: "preview",
849
+ toolCount: 49,
850
+ transport: "both",
851
+ architectureClass: "domain-simulator",
852
+ domainPrimitives: [
853
+ "issue workflow transitions and mutation history",
854
+ "JQL/search and issue field projection",
855
+ "sprint, board, version, and SLA time-based transitions",
856
+ "permission-scoped reads and Jira webhook event delivery"
857
+ ],
858
+ behaviorOwner: "src/state/transition-rules.ts",
859
+ opsCoverage: 80
860
+ },
861
+ {
862
+ id: "linear",
863
+ icon: "LN",
864
+ name: "Linear",
865
+ description: "Issues, projects, teams, cycles, and workflows.",
866
+ stage: "public",
867
+ toolCount: 50,
868
+ transport: "both",
869
+ architectureClass: "domain-simulator",
870
+ domainPrimitives: [
871
+ "GraphQL operation dispatch",
872
+ "issue workflow transitions",
873
+ "comment and history projection",
874
+ "cycle and project progress projection"
875
+ ],
876
+ behaviorOwner: "src/rest/graphql-dispatch.ts",
877
+ opsCoverage: 98
878
+ },
879
+ {
880
+ id: "ownerrez",
881
+ icon: "OR",
882
+ name: "OwnerRez",
883
+ description: "Vacation-rental PMS \u2014 properties, bookings, guests, quotes, financial reads, tags, fields, and webhook subscriptions.",
884
+ stage: "preview",
885
+ toolCount: 25,
886
+ transport: "rest",
887
+ architectureClass: "replay-shell",
888
+ domainPrimitives: [
889
+ "recording replay cursor",
890
+ "tag and guest stateful overlays",
891
+ "booking/payment read projections"
892
+ ],
893
+ behaviorOwner: null,
894
+ opsCoverage: 100
895
+ },
896
+ {
897
+ id: "pricelabs",
898
+ icon: "PL",
899
+ name: "PriceLabs",
900
+ description: "Dynamic pricing \u2014 listings, overrides, neighborhood data, rate plans, and reservations.",
901
+ stage: "preview",
902
+ toolCount: 11,
903
+ transport: "rest",
904
+ architectureClass: "replay-shell",
905
+ domainPrimitives: [
906
+ "recording replay cursor",
907
+ "listing settings overlay",
908
+ "pricing and reservation projections"
909
+ ],
910
+ behaviorOwner: null,
911
+ opsCoverage: 100
912
+ },
913
+ {
914
+ id: "ramp",
915
+ icon: "RP",
916
+ name: "Ramp",
917
+ description: "Cards, funds, expenses, reimbursements, bills, and travel.",
918
+ stage: "preview",
919
+ toolCount: 46,
920
+ transport: "mcp",
921
+ architectureClass: "replay-shell",
922
+ domainPrimitives: [
923
+ "checked-in Ramp CLI fixture corpus",
924
+ "CLI-native command routing",
925
+ "seeded approval and comment overlays"
926
+ ],
927
+ behaviorOwner: null,
928
+ opsCoverage: null
929
+ },
930
+ {
931
+ id: "sentry",
932
+ icon: "SN",
933
+ name: "Sentry",
934
+ description: "Error monitoring across organizations, projects, teams, issues, events, releases, and DSN keys.",
935
+ stage: "preview",
936
+ toolCount: 25,
937
+ transport: "rest",
938
+ architectureClass: "domain-simulator",
939
+ domainPrimitives: [
940
+ "event grouping",
941
+ "issue state transitions",
942
+ "release and time-window projections"
943
+ ],
944
+ behaviorOwner: "src/domain/sentry-simulator.ts",
945
+ opsCoverage: null
946
+ },
947
+ {
948
+ id: "slack",
949
+ icon: "SL",
950
+ name: "Slack",
951
+ description: "Channels, messages, threads, users, and reactions.",
952
+ stage: "public",
953
+ toolCount: 9,
954
+ transport: "both",
955
+ architectureClass: "domain-simulator",
956
+ domainPrimitives: [
957
+ "channel membership and visibility",
958
+ "message/thread ordering",
959
+ "event delivery semantics"
960
+ ],
961
+ behaviorOwner: "src/domain/slack-simulator.ts",
962
+ opsCoverage: 100
963
+ },
964
+ {
965
+ id: "stripe",
966
+ icon: "ST",
967
+ name: "Stripe",
968
+ description: "Customers, payments, subscriptions, invoices, and refunds.",
969
+ stage: "preview",
970
+ toolCount: 28,
971
+ transport: "both",
972
+ architectureClass: "domain-simulator",
973
+ domainPrimitives: [
974
+ "payment object lifecycle",
975
+ "balance and ledger projection",
976
+ "webhook/idempotency semantics"
977
+ ],
978
+ behaviorOwner: "src/domain/stripe-simulator.ts",
979
+ opsCoverage: null
980
+ },
981
+ {
982
+ id: "supabase",
983
+ icon: "SB",
984
+ name: "Supabase",
985
+ description: "SQL, migrations, logs, branches, and project metadata.",
986
+ stage: "public",
987
+ toolCount: 29,
988
+ transport: "both",
989
+ architectureClass: "domain-simulator",
990
+ domainPrimitives: [
991
+ "Postgres schema and SQL execution",
992
+ "RLS and API projections",
993
+ "realtime row changes"
994
+ ],
995
+ behaviorOwner: "src/pg-engine.ts",
996
+ opsCoverage: 100
997
+ },
998
+ {
999
+ id: "tavily",
1000
+ icon: "TV",
1001
+ name: "Tavily",
1002
+ description: "Search, extract, crawl, map, research, usage, and API key operations.",
1003
+ stage: "preview",
1004
+ toolCount: 11,
1005
+ transport: "rest",
1006
+ architectureClass: "replay-shell",
1007
+ domainPrimitives: [
1008
+ "recording-backed REST/tool response replay",
1009
+ "deterministic search/extract response helpers",
1010
+ "research request state overlay"
1011
+ ],
1012
+ behaviorOwner: null,
1013
+ opsCoverage: 100
1014
+ },
1015
+ {
1016
+ id: "unipile",
1017
+ icon: "UP",
1018
+ name: "Unipile",
1019
+ description: "LinkedIn and email messaging, accounts, and chats.",
1020
+ stage: "preview",
1021
+ toolCount: 31,
1022
+ transport: "rest",
1023
+ architectureClass: "replay-shell",
1024
+ domainPrimitives: [
1025
+ "authenticated account and chat reads",
1026
+ "single WhatsApp send mutation",
1027
+ "future account, chat, and webhook simulator"
1028
+ ],
1029
+ behaviorOwner: null,
1030
+ opsCoverage: 100
1031
+ },
1032
+ {
1033
+ id: "webflow",
1034
+ icon: "WF",
1035
+ name: "Webflow",
1036
+ description: "Sites, pages, CMS collections, items, assets, forms, and webhooks.",
1037
+ stage: "preview",
1038
+ toolCount: 19,
1039
+ transport: "rest",
1040
+ architectureClass: "replay-shell",
1041
+ domainPrimitives: [
1042
+ "recording replay cursor",
1043
+ "CMS collection/item overlays",
1044
+ "publish lifecycle captures"
1045
+ ],
1046
+ behaviorOwner: null,
1047
+ opsCoverage: 100
1048
+ }
1049
+ ];
1050
+ var GENERATED_STARTABLE_CLONE_IDS = [
1051
+ "apify",
1052
+ "calcom",
1053
+ "clickup",
1054
+ "customerio",
1055
+ "datadog",
1056
+ "discord",
1057
+ "github",
1058
+ "gitlab",
1059
+ "google-workspace",
1060
+ "hubspot",
1061
+ "jira",
1062
+ "linear",
1063
+ "ownerrez",
1064
+ "pricelabs",
1065
+ "ramp",
1066
+ "sentry",
1067
+ "slack",
1068
+ "stripe",
1069
+ "supabase",
1070
+ "tavily",
1071
+ "unipile",
1072
+ "webflow"
1073
+ ];
1074
+ var CLONE_ARCHITECTURE_CLASSES = [
1075
+ "standard-core",
1076
+ "domain-simulator",
1077
+ "replay-shell",
1078
+ "redesign-before-expansion"
1079
+ ];
1080
+ var CLONE_ARCHITECTURE_PREVIEW_CLASSES = [
1081
+ "replay-shell",
1082
+ "redesign-before-expansion"
1083
+ ];
1084
+ var CLONE_ARCHITECTURE_CLASS_SET = new Set(CLONE_ARCHITECTURE_CLASSES);
1085
+ var CLONE_ARCHITECTURE_PREVIEW_CLASS_SET = new Set(CLONE_ARCHITECTURE_PREVIEW_CLASSES);
1086
+ var CLONE_RUNTIME_SOURCE_EXTENSIONS = Object.freeze([
1087
+ ".cjs",
1088
+ ".cts",
1089
+ ".js",
1090
+ ".jsx",
1091
+ ".mjs",
1092
+ ".mts",
1093
+ ".ts",
1094
+ ".tsx"
1095
+ ]);
1096
+ var RUNTIME_SOURCE_EXTENSIONS = new Set(CLONE_RUNTIME_SOURCE_EXTENSIONS);
1097
+ var FIELD_POLICY = {
1098
+ behaviorOwner: {
1099
+ required: /* @__PURE__ */ new Set(["domain-simulator"]),
1100
+ allowed: /* @__PURE__ */ new Set(["domain-simulator"])
1101
+ },
1102
+ domainPrimitives: {
1103
+ required: /* @__PURE__ */ new Set(["domain-simulator", "replay-shell", "redesign-before-expansion"]),
1104
+ allowed: /* @__PURE__ */ new Set(["domain-simulator", "replay-shell", "redesign-before-expansion"])
1105
+ },
1106
+ promotionEvidence: {
1107
+ required: /* @__PURE__ */ new Set(["replay-shell"]),
1108
+ allowed: /* @__PURE__ */ new Set(["replay-shell"])
1109
+ },
1110
+ redesignContract: {
1111
+ required: /* @__PURE__ */ new Set(["redesign-before-expansion"]),
1112
+ allowed: /* @__PURE__ */ new Set(["redesign-before-expansion"])
1113
+ }
1114
+ };
1115
+ var CLONE_ARCHITECTURE_POLICY_FIELDS = Object.freeze(Object.keys(FIELD_POLICY));
1116
+ var CLONE_ARCHITECTURE_MANIFEST_FIELDS = Object.freeze([
1117
+ "class",
1118
+ "summary",
1119
+ ...CLONE_ARCHITECTURE_POLICY_FIELDS
1120
+ ]);
1121
+ var CLONE_ARCHITECTURE_MANIFEST_FIELD_SET = new Set(CLONE_ARCHITECTURE_MANIFEST_FIELDS);
1122
+ var CLONE_STAGES = ["wip", "internal", "preview", "public", "retired"];
1123
+ var CLONE_TRANSPORTS = ["mcp", "rest", "both"];
1124
+ var CLONE_SMOKE_PROFILES = ["none", "health-and-seed", "clone-surface"];
1125
+ var STARTABLE_CLONE_STAGES = ["public", "preview"];
1126
+ var CATALOG_VISIBLE_CLONE_STAGES = ["public", "preview"];
1127
+ var CLONE_STAGE_SET = new Set(CLONE_STAGES);
1128
+ var CLONE_TRANSPORT_SET = new Set(CLONE_TRANSPORTS);
1129
+ var CLONE_SMOKE_PROFILE_SET = new Set(CLONE_SMOKE_PROFILES);
1130
+ var STARTABLE_CLONE_STAGE_SET = new Set(STARTABLE_CLONE_STAGES);
1131
+ var CATALOG_VISIBLE_CLONE_STAGE_SET = new Set(CATALOG_VISIBLE_CLONE_STAGES);
1132
+ var CLONE_CATALOG = GENERATED_CLONE_CATALOG;
1133
+ var ALL_CLONE_IDS = CLONE_CATALOG.map((entry) => entry.id);
1134
+ var ALL_STARTABLE_CLONE_IDS = [...GENERATED_STARTABLE_CLONE_IDS];
1135
+ var CLONE_ID_SET = new Set(ALL_CLONE_IDS);
1136
+ var STARTABLE_CLONE_ID_SET = new Set(ALL_STARTABLE_CLONE_IDS);
1137
+ var STARTABLE_CLONE_CATALOG = CLONE_CATALOG.filter(
1138
+ (entry) => STARTABLE_CLONE_ID_SET.has(entry.id)
1139
+ );
1140
+
1141
+ // ../billing-constants/src/credit-catalog.ts
1142
+ var CREDIT_COGS_MARKUP = 3;
1143
+ var CREDIT_TARGET_GROSS_MARGIN_PERCENT = Math.round(
1144
+ (1 - 1 / CREDIT_COGS_MARKUP) * 100
1145
+ );
1146
+
1147
+ // ../billing-constants/src/index.ts
1148
+ var PLAN_MONTHLY_TWIN_MINUTE_LIMITS = {
1149
+ free: 500,
1150
+ pro: 5e3,
1151
+ enterprise: null
1152
+ };
1153
+ var PLAN_CONCURRENT_SESSION_LIMITS = {
1154
+ free: 3,
1155
+ pro: 10,
1156
+ enterprise: 50
1157
+ };
1158
+ var FREE_WORKSPACE_MEMBER_LIMIT = 2;
1159
+ var PRO_PLAN_PRICE_USD = 199;
1160
+ function formatMinutesLabel(minutes) {
1161
+ return minutes.toLocaleString("en-US");
1162
+ }
1163
+ var FREE_MINUTES = PLAN_MONTHLY_TWIN_MINUTE_LIMITS.free;
1164
+ var PRO_MINUTES = PLAN_MONTHLY_TWIN_MINUTE_LIMITS.pro;
1165
+ if (FREE_MINUTES == null || PRO_MINUTES == null) {
1166
+ throw new Error("PLAN_MONTHLY_TWIN_MINUTE_LIMITS: free and pro must be non-null");
1167
+ }
1168
+ var PRO_EVALS_PER_SEAT = 500;
1169
+ var PRO_SESSION_MINUTES_PER_SEAT = 5e3;
1170
+ var PRO_MAX_SEATS = 5;
1171
+ var FREE_EVALS_PER_MONTH = 100;
1172
+ var PLAN_DISPLAY = {
1173
+ free: {
1174
+ tag: "Free",
1175
+ priceLabel: "$0",
1176
+ periodLabel: "",
1177
+ included: [
1178
+ `${formatMinutesLabel(FREE_MINUTES)} session-minutes / account`,
1179
+ `${FREE_EVALS_PER_MONTH} evals / account`,
1180
+ `1 workspace (${FREE_WORKSPACE_MEMBER_LIMIT} users max)`,
1181
+ `${PLAN_CONCURRENT_SESSION_LIMITS.free} concurrent sessions / workspace`,
1182
+ "All clones included"
1183
+ ]
1184
+ },
1185
+ pro: {
1186
+ tag: "Pro",
1187
+ priceLabel: `$${PRO_PLAN_PRICE_USD}`,
1188
+ periodLabel: "/mo per seat",
1189
+ included: [
1190
+ `${formatMinutesLabel(PRO_SESSION_MINUTES_PER_SEAT)} session-minutes / seat / month`,
1191
+ `${PRO_EVALS_PER_SEAT} evals / seat / month`,
1192
+ "All clones included",
1193
+ `${PLAN_CONCURRENT_SESSION_LIMITS.pro} concurrent sessions / workspace`,
1194
+ `1 workspace, up to ${PRO_MAX_SEATS} seats with pooled usage`
1195
+ ]
1196
+ },
1197
+ enterprise: {
1198
+ tag: "Enterprise",
1199
+ priceLabel: "Contact us",
1200
+ periodLabel: "",
1201
+ included: [
1202
+ "Unlimited session-minutes",
1203
+ "Unlimited seats per workspace",
1204
+ "Unlimited workspaces",
1205
+ `${PLAN_CONCURRENT_SESSION_LIMITS.enterprise} concurrent sessions / workspace`,
1206
+ "SSO / SAML",
1207
+ "SOC 2 (in progress)",
1208
+ "Custom clones & eval support",
1209
+ "Dedicated onboarding & support"
1210
+ ]
1211
+ }
1212
+ };
1213
+ var PLAN_MAX_SESSION_TTL_SECONDS = {
1214
+ free: 30 * 60,
1215
+ // 30 minutes
1216
+ pro: 60 * 60,
1217
+ // 60 minutes
1218
+ enterprise: 90 * 60
1219
+ // 90 minutes
1220
+ };
1221
+ var MAX_ABSOLUTE_SESSION_LIFETIME_SECONDS = 4 * 60 * 60;
1222
+ var LIFETIME_PERIOD_RESETS_AT = new Date(Date.UTC(9999, 11, 31));
1223
+ var ALL_ENTITLED_CLONES = ALL_STARTABLE_CLONE_IDS;
1224
+ var SCENARIO_USAGE_WINDOW_DAYS = 7;
1225
+ var SCENARIO_USAGE_WINDOW_MS = SCENARIO_USAGE_WINDOW_DAYS * 24 * 60 * 60 * 1e3;
1226
+ var SCENARIO_USAGE_WINDOW_SECONDS = SCENARIO_USAGE_WINDOW_DAYS * 24 * 60 * 60;
1227
+ var LLM_PRICING_USD_PER_M_TOKENS = {
1228
+ // Google Gemini (verified 2026-05-05, standard tier <=200K input tokens)
1229
+ "gemini-2.5-pro": { input: 1.25, output: 10 },
1230
+ "gemini-2.5-flash": { input: 0.3, output: 2.5 },
1231
+ // OpenAI flagship text models (verified 2026-05-21, standard short-context tier)
1232
+ "gpt-5.5": { input: 5, output: 30 },
1233
+ "gpt-5.5-pro": { input: 30, output: 180 },
1234
+ "gpt-5.4": { input: 2.5, output: 15 },
1235
+ "gpt-5.4-mini": { input: 0.75, output: 4.5 },
1236
+ "gpt-5.4-nano": { input: 0.2, output: 1.25 },
1237
+ "gpt-5.4-pro": { input: 30, output: 180 },
1238
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
1239
+ "gpt-4o": { input: 2.5, output: 10 },
1240
+ "gpt-4.1-mini": { input: 0.4, output: 1.6 },
1241
+ "gpt-4.1-nano": { input: 0.1, output: 0.4 },
1242
+ "gpt-4.1": { input: 2, output: 8 },
1243
+ // DeepSeek (verified 2026-05-05; legacy names map to v4-flash per vendor)
1244
+ "deepseek-chat": { input: 0.14, output: 0.28 },
1245
+ "deepseek-reasoner": { input: 0.14, output: 0.28 },
1246
+ "deepseek-v4-flash": { input: 0.14, output: 0.28 }
1247
+ };
1248
+ var LLM_PRICING_FAMILY_RATES = [
1249
+ { match: /^gemini-2\.5-pro/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gemini-2.5-pro"] },
1250
+ { match: /^gemini-2\.5-flash/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gemini-2.5-flash"] },
1251
+ { match: /^gpt-5\.5-pro/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-5.5-pro"] },
1252
+ { match: /^gpt-5\.5/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-5.5"] },
1253
+ { match: /^gpt-5\.4-pro/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-5.4-pro"] },
1254
+ { match: /^gpt-5\.4-mini/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-5.4-mini"] },
1255
+ { match: /^gpt-5\.4-nano/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-5.4-nano"] },
1256
+ { match: /^gpt-5\.4/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-5.4"] },
1257
+ { match: /^gpt-4o-mini/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-4o-mini"] },
1258
+ { match: /^gpt-4o(?!-mini)/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-4o"] },
1259
+ { match: /^gpt-4\.1-mini/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-4.1-mini"] },
1260
+ { match: /^gpt-4\.1-nano/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-4.1-nano"] },
1261
+ { match: /^gpt-4\.1(?!-mini|-nano)/i, rate: LLM_PRICING_USD_PER_M_TOKENS["gpt-4.1"] },
1262
+ // Anthropic Claude 4.x list prices (verified 2026): Opus $5/$25, Sonnet
1263
+ // $3/$15, Haiku 4.5 $1/$5 per 1M tokens (input/output).
1264
+ { match: /^claude-opus-/i, rate: { input: 5, output: 25 } },
1265
+ { match: /^claude-haiku-/i, rate: { input: 1, output: 5 } },
1266
+ { match: /^claude-sonnet-/i, rate: { input: 3, output: 15 } },
1267
+ { match: /^opus-/i, rate: { input: 5, output: 25 } },
1268
+ { match: /^haiku-/i, rate: { input: 1, output: 5 } },
1269
+ { match: /^sonnet-/i, rate: { input: 3, output: 15 } },
1270
+ { match: /^deepseek-chat/i, rate: LLM_PRICING_USD_PER_M_TOKENS["deepseek-chat"] },
1271
+ { match: /^deepseek-reasoner/i, rate: LLM_PRICING_USD_PER_M_TOKENS["deepseek-reasoner"] },
1272
+ { match: /^deepseek-v4-flash/i, rate: LLM_PRICING_USD_PER_M_TOKENS["deepseek-v4-flash"] }
1273
+ ];
1274
+
1275
+ // ../node-auth/src/clone-entitlements.ts
1276
+ var ENTITLED_CLONE_SET = new Set(ALL_ENTITLED_CLONES);
1277
+ function normalizeEntitledCloneIds(cloneIds) {
1278
+ if (!cloneIds) {
1279
+ return [];
1280
+ }
1281
+ const normalized = /* @__PURE__ */ new Set();
1282
+ for (const cloneId of cloneIds) {
1283
+ if (typeof cloneId !== "string") {
1284
+ continue;
1285
+ }
1286
+ const trimmed = cloneId.trim();
1287
+ if (ENTITLED_CLONE_SET.has(trimmed)) {
1288
+ normalized.add(trimmed);
1289
+ }
1290
+ }
1291
+ return [...normalized];
1292
+ }
1293
+
1294
+ // ../node-auth/src/credential-store.ts
1295
+ var KEYCHAIN_COMMAND_TIMEOUT_MS = 1500;
1296
+ var ARCHAL_DIR_NAME = ".archal";
1297
+ var TEST_SANDBOX_ID_ENV_VAR = "ARCHAL_TEST_SANDBOX_ID";
1298
+ var VITEST_CONFIG_ENV_VAR = "ARCHAL_VITEST_CONFIG";
1299
+ function debug(message) {
1300
+ if (process.env["ARCHAL_DEBUG"] !== "1") {
1301
+ return;
1302
+ }
1303
+ process.stderr.write(`[archal-node-auth] ${message}
1304
+ `);
1305
+ }
1306
+ function getWarn(options) {
1307
+ return options?.warn ?? console.warn;
1308
+ }
1309
+ function isExpired(expiresAt) {
1310
+ return expiresAt <= Math.floor(Date.now() / 1e3);
1311
+ }
1312
+ function isRunningUnderTests() {
1313
+ return process.env["VITEST"] === "true" || process.env["VITEST_WORKER_ID"] !== void 0 || process.env["JEST_WORKER_ID"] !== void 0 || process.env["NODE_TEST_CONTEXT"] !== void 0;
1314
+ }
1315
+ function getRealArchalDir() {
1316
+ return join(homedir(), ARCHAL_DIR_NAME);
1317
+ }
1318
+ function normalizeSandboxId(raw) {
1319
+ const trimmed = raw.trim();
1320
+ if (/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(trimmed)) {
1321
+ return trimmed;
1322
+ }
1323
+ return createHash("sha256").update(trimmed).digest("hex").slice(0, 16);
1324
+ }
1325
+ function getTestSandboxDir() {
1326
+ const sharedSandboxId = process.env[VITEST_CONFIG_ENV_VAR]?.trim() || process.env[TEST_SANDBOX_ID_ENV_VAR]?.trim();
1327
+ if (sharedSandboxId) {
1328
+ return join(tmpdir(), `archal-test-sandbox-${normalizeSandboxId(sharedSandboxId)}`);
1329
+ }
1330
+ const workerId = process.env["VITEST_WORKER_ID"] ?? process.env["JEST_WORKER_ID"] ?? String(process.pid);
1331
+ return join(tmpdir(), `archal-test-sandbox-${workerId}-${process.pid}`);
1332
+ }
1333
+ var seededSandboxes = /* @__PURE__ */ new Set();
1334
+ function seedSandboxFromRealHomeOnce(sandboxDir) {
1335
+ try {
1336
+ if (seededSandboxes.has(sandboxDir)) {
1337
+ const credsIntact = existsSync(join(sandboxDir, CREDENTIALS_FILE));
1338
+ if (credsIntact) return;
1339
+ seededSandboxes.delete(sandboxDir);
1340
+ }
1341
+ const realDir = getRealArchalDir();
1342
+ if (!existsSync(realDir)) {
1343
+ seededSandboxes.add(sandboxDir);
1344
+ return;
1345
+ }
1346
+ if (!existsSync(sandboxDir)) {
1347
+ mkdirSync(sandboxDir, { recursive: true, mode: 448 });
1348
+ }
1349
+ for (const filename of [CREDENTIALS_FILE, CREDENTIALS_KEY_FILE]) {
1350
+ const realPath = join(realDir, filename);
1351
+ const sandboxPath = join(sandboxDir, filename);
1352
+ if (!existsSync(realPath) || existsSync(sandboxPath)) continue;
1353
+ const contents = readFileSync(realPath);
1354
+ writeFileSync(sandboxPath, contents, { mode: 384 });
1355
+ try {
1356
+ chmodSync(sandboxPath, 384);
1357
+ } catch {
1358
+ }
1359
+ }
1360
+ seededSandboxes.add(sandboxDir);
1361
+ } catch (err) {
1362
+ debug(`Sandbox seed skipped: ${errorMessage2(err)}`);
1363
+ }
1364
+ }
1365
+ function getArchalDir() {
1366
+ const explicit = process.env["ARCHAL_HOME"];
1367
+ if (explicit) {
1368
+ return explicit;
1369
+ }
1370
+ if (isRunningUnderTests()) {
1371
+ return getTestSandboxDir();
1372
+ }
1373
+ return getRealArchalDir();
1374
+ }
1375
+ function getCredentialsPath() {
1376
+ return join(getArchalDir(), CREDENTIALS_FILE);
1377
+ }
1378
+ function ensureDir(dir) {
1379
+ if (!existsSync(dir)) {
1380
+ mkdirSync(dir, { recursive: true });
1381
+ debug(`Created archal directory at ${dir}`);
1382
+ }
1383
+ return dir;
1384
+ }
1385
+ function getCredentialsPathForDir(dir) {
1386
+ return join(dir, CREDENTIALS_FILE);
1387
+ }
1388
+ function getCredentialsKeyPathForDir(dir) {
1389
+ return join(dir, CREDENTIALS_KEY_FILE);
1390
+ }
1391
+ function readCredentialsKeyFromEnv() {
1392
+ const raw = process.env[CREDENTIALS_MASTER_KEY_ENV_VAR]?.trim();
1393
+ if (!raw) {
1394
+ return null;
1395
+ }
1396
+ if (/^[a-fA-F0-9]{64}$/.test(raw)) {
1397
+ return Buffer.from(raw, "hex");
1398
+ }
1399
+ try {
1400
+ const decoded = Buffer.from(raw, "base64");
1401
+ if (decoded.length === 32) {
1402
+ return decoded;
1403
+ }
1404
+ } catch (err) {
1405
+ debug(`Base64 master key decode failed: ${errorMessage2(err)}`);
1406
+ }
1407
+ return createHash("sha256").update(raw, "utf8").digest();
1408
+ }
1409
+ function readCredentialsKeyFromMacKeychain() {
1410
+ if (process.platform !== "darwin") return null;
1411
+ try {
1412
+ const result = spawnSync(
1413
+ "security",
1414
+ ["find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"],
1415
+ { encoding: "utf-8", timeout: KEYCHAIN_COMMAND_TIMEOUT_MS }
1416
+ );
1417
+ if (!result || result.error || result.status !== 0 || typeof result.stdout !== "string" || result.stdout.length === 0) {
1418
+ if (result?.error) {
1419
+ debug(`Keychain lookup failed: ${result.error.message}`);
1420
+ }
1421
+ return null;
1422
+ }
1423
+ const raw = result.stdout.trim();
1424
+ if (!/^[a-fA-F0-9]{64}$/.test(raw)) {
1425
+ return null;
1426
+ }
1427
+ return Buffer.from(raw, "hex");
1428
+ } catch (err) {
1429
+ debug(`Keychain lookup threw: ${errorMessage2(err)}`);
1430
+ return null;
1431
+ }
1432
+ }
1433
+ function readCredentialsKeyFromFile(dir = getArchalDir()) {
1434
+ const keyPath = getCredentialsKeyPathForDir(dir);
1435
+ if (!existsSync(keyPath)) {
1436
+ return null;
1437
+ }
1438
+ try {
1439
+ const raw = readFileSync(keyPath, "utf-8").trim();
1440
+ const key = Buffer.from(raw, "hex");
1441
+ if (key.length === 32) {
1442
+ return key;
1443
+ }
1444
+ } catch (err) {
1445
+ debug(`Failed to read credentials key file: ${errorMessage2(err)}`);
1446
+ }
1447
+ return null;
1448
+ }
1449
+ function collectCredentialKeysForDecrypt(dir = getArchalDir()) {
1450
+ const keys = [];
1451
+ for (const candidate of [
1452
+ readCredentialsKeyFromEnv(),
1453
+ readCredentialsKeyFromMacKeychain(),
1454
+ readCredentialsKeyFromFile(dir)
1455
+ ]) {
1456
+ if (!candidate) continue;
1457
+ if (!keys.some((entry) => entry.equals(candidate))) {
1458
+ keys.push(candidate);
1459
+ }
1460
+ }
1461
+ return keys;
1462
+ }
1463
+ function writeCredentialsKeyToMacKeychain(key) {
1464
+ if (process.platform !== "darwin") return false;
1465
+ try {
1466
+ const result = spawnSync(
1467
+ "security",
1468
+ [
1469
+ "add-generic-password",
1470
+ "-U",
1471
+ "-s",
1472
+ KEYCHAIN_SERVICE,
1473
+ "-a",
1474
+ KEYCHAIN_ACCOUNT,
1475
+ "-w",
1476
+ key.toString("hex")
1477
+ ],
1478
+ { encoding: "utf-8", timeout: KEYCHAIN_COMMAND_TIMEOUT_MS }
1479
+ );
1480
+ if (result?.error) {
1481
+ debug(`Keychain write failed: ${result.error.message}`);
1482
+ }
1483
+ return !!result && !result.error && result.status === 0;
1484
+ } catch (err) {
1485
+ debug(`Keychain write threw: ${errorMessage2(err)}`);
1486
+ return false;
1487
+ }
1488
+ }
1489
+ function getOrCreateCredentialsKey(dir = getArchalDir()) {
1490
+ const envKey = readCredentialsKeyFromEnv();
1491
+ if (envKey) {
1492
+ return envKey;
1493
+ }
1494
+ const keychainKey = readCredentialsKeyFromMacKeychain();
1495
+ if (keychainKey) {
1496
+ return keychainKey;
1497
+ }
1498
+ const fileKey = readCredentialsKeyFromFile(dir);
1499
+ if (fileKey) {
1500
+ if (writeCredentialsKeyToMacKeychain(fileKey)) {
1501
+ try {
1502
+ unlinkSync(getCredentialsKeyPathForDir(dir));
1503
+ } catch {
1504
+ }
1505
+ }
1506
+ return fileKey;
1507
+ }
1508
+ const generated = randomBytes(32);
1509
+ if (!writeCredentialsKeyToMacKeychain(generated)) {
1510
+ ensureDir(dir);
1511
+ writeFileSync(getCredentialsKeyPathForDir(dir), `${generated.toString("hex")}
1512
+ `, {
1513
+ encoding: "utf-8",
1514
+ mode: 384
1515
+ });
1516
+ }
1517
+ return generated;
1518
+ }
1519
+ function encryptToken(token, dir = getArchalDir()) {
1520
+ const key = getOrCreateCredentialsKey(dir);
1521
+ const iv = randomBytes(12);
1522
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
1523
+ const encrypted = Buffer.concat([cipher.update(token, "utf-8"), cipher.final()]);
1524
+ return `${TOKEN_ENCRYPTION_PREFIX}:${iv.toString("hex")}:${cipher.getAuthTag().toString("hex")}:${encrypted.toString("hex")}`;
1525
+ }
1526
+ function decryptToken(ciphertext, dir = getArchalDir()) {
1527
+ const parts = ciphertext.split(":");
1528
+ if (parts.length !== 4 || parts[0] !== TOKEN_ENCRYPTION_PREFIX) {
1529
+ return null;
1530
+ }
1531
+ const [, ivHex, tagHex, dataHex] = parts;
1532
+ if (!ivHex || !tagHex || !dataHex) {
1533
+ return null;
1534
+ }
1535
+ const iv = Buffer.from(ivHex, "hex");
1536
+ const tag = Buffer.from(tagHex, "hex");
1537
+ const data = Buffer.from(dataHex, "hex");
1538
+ const keys = collectCredentialKeysForDecrypt(dir);
1539
+ for (const key of keys) {
1540
+ try {
1541
+ const decipher = createDecipheriv("aes-256-gcm", key, iv);
1542
+ decipher.setAuthTag(tag);
1543
+ return Buffer.concat([decipher.update(data), decipher.final()]).toString("utf-8");
1544
+ } catch {
1545
+ }
1546
+ }
1547
+ debug("Token decryption failed with all available credential keys.");
1548
+ return null;
1549
+ }
1550
+ function decodeJwtPayload(token) {
1551
+ try {
1552
+ const payloadPart = token.split(".")[1];
1553
+ if (!payloadPart) return null;
1554
+ const normalized = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
1555
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
1556
+ return JSON.parse(Buffer.from(padded, "base64").toString("utf-8"));
1557
+ } catch (err) {
1558
+ debug(`JWT payload decode failed (expected for non-JWT tokens): ${errorMessage2(err)}`);
1559
+ return null;
1560
+ }
1561
+ }
1562
+ function hasValidSelectedClones(value) {
1563
+ return value === void 0 || Array.isArray(value) && value.every((twin) => typeof twin === "string");
1564
+ }
1565
+ function resolveStoredToken(parsed, dir = getArchalDir()) {
1566
+ if (typeof parsed.token === "string") {
1567
+ const token = parsed.token.trim();
1568
+ return { token: token.length > 0 ? token : null, source: "legacy" };
1569
+ }
1570
+ if (typeof parsed.accessToken === "string") {
1571
+ const token = parsed.accessToken.trim();
1572
+ return { token: token.length > 0 ? token : null, source: "legacy" };
1573
+ }
1574
+ if (typeof parsed.tokenEncrypted === "string") {
1575
+ const token = decryptToken(parsed.tokenEncrypted, dir)?.trim() || null;
1576
+ return { token: token?.length ? token : null, source: "encrypted" };
1577
+ }
1578
+ return { token: null, source: "legacy" };
1579
+ }
1580
+ function resolveStoredRefreshToken(parsed, dir = getArchalDir()) {
1581
+ if (typeof parsed.refreshTokenEncrypted === "string") {
1582
+ const refreshToken = decryptToken(parsed.refreshTokenEncrypted, dir)?.trim() || null;
1583
+ if (refreshToken !== null) {
1584
+ return { refreshToken, source: "encrypted" };
1585
+ }
1586
+ if (typeof parsed.refreshToken === "string") {
1587
+ return { refreshToken: parsed.refreshToken.trim(), source: "legacy" };
1588
+ }
1589
+ return { refreshToken: null, source: "encrypted" };
1590
+ }
1591
+ if (typeof parsed.refreshToken === "string") {
1592
+ return { refreshToken: parsed.refreshToken.trim(), source: "legacy" };
1593
+ }
1594
+ return { refreshToken: "", source: "none" };
1595
+ }
1596
+ function buildStoredCredentials(parsed, path, warn, options) {
1597
+ const dir = dirname(path);
1598
+ const { token, source: tokenSource } = resolveStoredToken(parsed, dir);
1599
+ const { refreshToken, source: refreshTokenSource } = resolveStoredRefreshToken(parsed, dir);
1600
+ if (token === null || refreshToken === null || parsed.refreshToken !== void 0 && typeof parsed.refreshToken !== "string" || parsed.refreshTokenEncrypted !== void 0 && typeof parsed.refreshTokenEncrypted !== "string" || !hasValidSelectedClones(parsed.selectedCloneIds)) {
1601
+ warn(
1602
+ `Credentials file at ${path} has missing or invalid fields. Run \`archal login\` to re-authenticate.`
1603
+ );
1604
+ return null;
1605
+ }
1606
+ const { email, plan, expiresAt } = parsed;
1607
+ if (typeof email !== "string" || !isPlan(plan) || typeof expiresAt !== "number") {
1608
+ warn(
1609
+ `Credentials file at ${path} has missing or invalid fields. Run \`archal login\` to re-authenticate.`
1610
+ );
1611
+ return null;
1612
+ }
1613
+ const creds = {
1614
+ token,
1615
+ refreshToken,
1616
+ email,
1617
+ plan,
1618
+ selectedCloneIds: normalizeEntitledCloneIds(parsed.selectedCloneIds),
1619
+ expiresAt
1620
+ };
1621
+ if (options?.migrateLegacyCredentials && (tokenSource === "legacy" || refreshTokenSource === "legacy")) {
1622
+ try {
1623
+ writeCredentialsAtPath(path, creds);
1624
+ } catch (err) {
1625
+ debug(`Credential migration failed: ${errorMessage2(err)}`);
1626
+ }
1627
+ }
1628
+ return creds;
1629
+ }
1630
+ function readCredentialsFileAtPath(path, options) {
1631
+ if (!existsSync(path)) {
1632
+ return null;
1633
+ }
1634
+ try {
1635
+ const warn = getWarn(options);
1636
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
1637
+ return buildStoredCredentials(parsed, path, warn, options);
1638
+ } catch {
1639
+ const warn = getWarn(options);
1640
+ warn(
1641
+ `Credentials file at ${path} exists but could not be parsed. Delete it and run \`archal login\` to re-authenticate.`
1642
+ );
1643
+ return null;
1644
+ }
1645
+ }
1646
+ function readCredentialsFile(options) {
1647
+ return readCredentialsFileAtPath(getCredentialsPath(), options);
1648
+ }
1649
+ function isWorkspaceApiKey(token) {
1650
+ return token.startsWith("archal_ws_") || token.startsWith("archal_") && !token.includes(".");
1651
+ }
1652
+ function getStoredCredentials(options) {
1653
+ const creds = readCredentialsFile(options);
1654
+ if (!creds) {
1655
+ return null;
1656
+ }
1657
+ if (options?.includeExpired) {
1658
+ return creds;
1659
+ }
1660
+ return isExpired(creds.expiresAt) ? null : creds;
1661
+ }
1662
+ function getRealHomeStoredCredentials(options) {
1663
+ const creds = readCredentialsFileAtPath(getCredentialsPathForDir(getRealArchalDir()), options);
1664
+ if (!creds) {
1665
+ return null;
1666
+ }
1667
+ if (options?.includeExpired) {
1668
+ return creds;
1669
+ }
1670
+ return isExpired(creds.expiresAt) ? null : creds;
1671
+ }
1672
+ function seedTestSandboxCredentialsFromRealHome() {
1673
+ if (!isRunningUnderTests() || process.env["ARCHAL_HOME"]) {
1674
+ return;
1675
+ }
1676
+ seedSandboxFromRealHomeOnce(getTestSandboxDir());
1677
+ }
1678
+ function writeCredentialsAtPath(path, creds) {
1679
+ const dir = dirname(path);
1680
+ ensureDir(dir);
1681
+ const payload = {
1682
+ email: creds.email,
1683
+ plan: creds.plan,
1684
+ selectedCloneIds: normalizeEntitledCloneIds(creds.selectedCloneIds),
1685
+ expiresAt: creds.expiresAt,
1686
+ tokenEncrypted: encryptToken(creds.token.trim(), dir),
1687
+ refreshTokenEncrypted: creds.refreshToken.trim().length > 0 ? encryptToken(creds.refreshToken.trim(), dir) : void 0
1688
+ };
1689
+ const tmpPath = `${path}.${randomBytes(4).toString("hex")}.tmp`;
1690
+ writeFileSync(tmpPath, `${JSON.stringify(payload, null, 2)}
1691
+ `, {
1692
+ encoding: "utf-8",
1693
+ mode: 384
1694
+ });
1695
+ renameSync(tmpPath, path);
1696
+ }
1697
+ function saveCredentials(creds) {
1698
+ writeCredentialsAtPath(getCredentialsPath(), creds);
1699
+ }
1700
+
1701
+ // ../node-auth/src/loopback.ts
1702
+ import { isIP } from "net";
1703
+ function normalizeHostname(hostname) {
1704
+ const trimmed = hostname.trim().toLowerCase();
1705
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
1706
+ return trimmed.slice(1, -1);
1707
+ }
1708
+ return trimmed;
1709
+ }
1710
+ function isLoopbackHostname(hostname) {
1711
+ const normalized = normalizeHostname(hostname);
1712
+ if (normalized === "localhost" || normalized === "::1") {
1713
+ return true;
1714
+ }
1715
+ if (isIP(normalized) === 4) {
1716
+ return normalized.startsWith("127.");
1717
+ }
1718
+ return false;
1719
+ }
1720
+ function isLoopbackHttpUrl(rawUrl) {
1721
+ try {
1722
+ const parsed = new URL(rawUrl);
1723
+ return parsed.protocol === "http:" && isLoopbackHostname(parsed.hostname);
1724
+ } catch {
1725
+ return false;
1726
+ }
1727
+ }
1728
+
1729
+ // ../node-auth/src/url-resolver.ts
1730
+ function getWarn2(options) {
1731
+ return options?.warn ?? console.warn;
1732
+ }
1733
+ function stripUrlCredentials(url) {
1734
+ try {
1735
+ const parsed = new URL(url);
1736
+ parsed.username = "";
1737
+ parsed.password = "";
1738
+ return parsed.toString();
1739
+ } catch {
1740
+ const atIndex = url.indexOf("@");
1741
+ return atIndex >= 0 ? url.slice(atIndex + 1) : url;
1742
+ }
1743
+ }
1744
+ function normalizeAuthUrl(value) {
1745
+ if (typeof value !== "string") {
1746
+ return null;
1747
+ }
1748
+ const trimmed = value.trim().replace(/\/+$/, "");
1749
+ if (trimmed.length === 0) {
1750
+ return null;
1751
+ }
1752
+ const normalized = trimmed.endsWith("/api") ? trimmed.slice(0, -4) : trimmed;
1753
+ if (normalized.length === 0) {
1754
+ return null;
1755
+ }
1756
+ const parsed = new URL(normalized);
1757
+ if (parsed.protocol === "https:") {
1758
+ return normalized;
1759
+ }
1760
+ const isLocalHttp = isLoopbackHttpUrl(normalized);
1761
+ if (!isLocalHttp) {
1762
+ throw new Error(
1763
+ `Auth URL must use HTTPS (got ${stripUrlCredentials(normalized)}). HTTP is only allowed for loopback hosts.`
1764
+ );
1765
+ }
1766
+ return normalized;
1767
+ }
1768
+ function parseBooleanEnvFlag(value) {
1769
+ if (typeof value !== "string") {
1770
+ return false;
1771
+ }
1772
+ switch (value.trim().toLowerCase()) {
1773
+ case "1":
1774
+ case "true":
1775
+ case "yes":
1776
+ case "on":
1777
+ return true;
1778
+ default:
1779
+ return false;
1780
+ }
1781
+ }
1782
+ function isStrictEndpointModeEnabled() {
1783
+ return parseBooleanEnvFlag(process.env[STRICT_ENDPOINTS_ENV_VAR]);
1784
+ }
1785
+ function readConfiguredUrl(options, ...envKeys) {
1786
+ for (const key of envKeys) {
1787
+ try {
1788
+ const value = normalizeAuthUrl(process.env[key]);
1789
+ if (value) {
1790
+ return value;
1791
+ }
1792
+ } catch (err) {
1793
+ getWarn2(options)(
1794
+ `Ignoring invalid ${key}: ${err instanceof Error ? err.message : String(err)}`
1795
+ );
1796
+ }
1797
+ }
1798
+ return null;
1799
+ }
1800
+ function getConfiguredAuthBaseUrl(options) {
1801
+ const explicit = readConfiguredUrl(options, "ARCHAL_AUTH_URL", "ARCHAL_AUTH_BASE_URL");
1802
+ if (explicit) {
1803
+ return explicit;
1804
+ }
1805
+ return isStrictEndpointModeEnabled() ? null : HOSTED_DEFAULT_AUTH_BASE_URL;
1806
+ }
1807
+
1808
+ // ../node-auth/src/request-metadata.ts
1809
+ function buildAuthRequestHeaders(metadata, includeContentType = false) {
1810
+ const headers = {};
1811
+ if (includeContentType) {
1812
+ headers["content-type"] = "application/json";
1813
+ }
1814
+ if (metadata?.userAgent) {
1815
+ headers["user-agent"] = metadata.userAgent;
1816
+ }
1817
+ if (metadata?.cliVersion) {
1818
+ headers["x-archal-cli-version"] = metadata.cliVersion;
1819
+ }
1820
+ return headers;
1821
+ }
1822
+
1823
+ // ../node-auth/src/entitlements.ts
1824
+ function getAuthBaseUrl(metadata) {
1825
+ const configured = metadata?.authBaseUrl?.trim();
1826
+ if (configured) {
1827
+ return configured.replace(/\/+$/, "");
1828
+ }
1829
+ return getConfiguredAuthBaseUrl();
1830
+ }
1831
+ async function readFailureDetail(response) {
1832
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
1833
+ if (!contentType.includes("application/json")) {
1834
+ return null;
1835
+ }
1836
+ try {
1837
+ const payload = await response.json();
1838
+ const error = typeof payload.error === "string" ? payload.error.trim() : "";
1839
+ const message = typeof payload.message === "string" ? payload.message.trim() : "";
1840
+ if (error && message) return `${error}: ${message}`;
1841
+ if (message) return message;
1842
+ if (error) return error;
1843
+ } catch {
1844
+ return null;
1845
+ }
1846
+ return null;
1847
+ }
1848
+ async function validateTokenWithServer(creds, metadata) {
1849
+ const authBaseUrl = getAuthBaseUrl(metadata);
1850
+ if (!authBaseUrl) {
1851
+ return {
1852
+ ok: false,
1853
+ code: "no_auth_url",
1854
+ status: null,
1855
+ reason: "no auth URL configured"
1856
+ };
1857
+ }
1858
+ try {
1859
+ const response = await fetch(`${authBaseUrl}/auth/me`, {
1860
+ method: "GET",
1861
+ headers: {
1862
+ authorization: `Bearer ${creds.token}`,
1863
+ ...buildAuthRequestHeaders(metadata, false)
1864
+ },
1865
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
1866
+ });
1867
+ if (!response.ok) {
1868
+ const detail = await readFailureDetail(response);
1869
+ return {
1870
+ ok: false,
1871
+ code: response.status === 401 ? "auth_rejected" : "http_error",
1872
+ status: response.status,
1873
+ reason: detail ? `HTTP ${response.status} ${detail}` : `HTTP ${response.status}`
1874
+ };
1875
+ }
1876
+ let data;
1877
+ try {
1878
+ data = await response.json();
1879
+ } catch {
1880
+ return {
1881
+ ok: false,
1882
+ code: "invalid_payload",
1883
+ status: null,
1884
+ reason: "invalid response payload"
1885
+ };
1886
+ }
1887
+ if (typeof data.email !== "string" || !isPlan(data.plan)) {
1888
+ return {
1889
+ ok: false,
1890
+ code: "invalid_payload",
1891
+ status: null,
1892
+ reason: "invalid response payload"
1893
+ };
1894
+ }
1895
+ const selectedCloneIds = Array.isArray(data.selectedCloneIds) ? normalizeEntitledCloneIds(data.selectedCloneIds) : normalizeEntitledCloneIds(creds.selectedCloneIds);
1896
+ const updated = {
1897
+ ...creds,
1898
+ email: data.email,
1899
+ plan: data.plan,
1900
+ selectedCloneIds
1901
+ };
1902
+ if ((updated.email !== creds.email || updated.plan !== creds.plan || JSON.stringify(updated.selectedCloneIds) !== JSON.stringify(creds.selectedCloneIds)) && !process.env[AUTH_TOKEN_ENV_VAR]) {
1903
+ saveCredentials(updated);
1904
+ }
1905
+ return { ok: true, credentials: updated };
1906
+ } catch (error) {
1907
+ return {
1908
+ ok: false,
1909
+ code: "network_error",
1910
+ status: null,
1911
+ reason: errorMessage2(error)
1912
+ };
1913
+ }
1914
+ }
1915
+ async function validateAuthWithServerResult(creds, metadata) {
1916
+ const result = await validateTokenWithServer(creds, metadata);
1917
+ if (result.ok) {
1918
+ return {
1919
+ credentials: result.credentials,
1920
+ validation: result
1921
+ };
1922
+ }
1923
+ return {
1924
+ credentials: creds,
1925
+ validation: result
1926
+ };
1927
+ }
1928
+
1929
+ // ../node-auth/src/secure-token.ts
1930
+ import { createHash as createHash2, timingSafeEqual } from "crypto";
1931
+
1932
+ // ../runtime/src/auth-lease.ts
1933
+ var STORED_CREDENTIALS_LOCK_TIMEOUT_MS = 3e4;
1934
+ var STORED_CREDENTIALS_STALE_LOCK_MS = STORED_CREDENTIALS_LOCK_TIMEOUT_MS;
1935
+ var STORED_CREDENTIALS_LOCK_POLL_INTERVAL_MS = 50;
1936
+ var DEFAULT_AUTH_BASE_URL = "https://www.archal.ai";
1937
+ var WORKSPACE_API_KEY_EXPIRES_SECONDS = 365 * 24 * 60 * 60;
1938
+ function readFirstConfiguredBaseUrl(envVars) {
1939
+ for (const envVar of envVars) {
1940
+ const configured = trimEnv(envVar);
1941
+ if (!configured) {
1942
+ continue;
1943
+ }
1944
+ const normalized = normalizeApiBaseUrl(configured);
1945
+ if (normalized) {
1946
+ return normalized;
1947
+ }
1948
+ }
1949
+ return null;
1950
+ }
1951
+ function resolveHostedAuthBaseUrl(options) {
1952
+ const authUrlEnvVar = options.authUrlEnvVar ?? "ARCHAL_AUTH_URL";
1953
+ const apiUrlEnvVar = options.apiUrlEnvVar ?? "ARCHAL_API_URL";
1954
+ const envVars = [.../* @__PURE__ */ new Set([
1955
+ apiUrlEnvVar,
1956
+ authUrlEnvVar,
1957
+ "ARCHAL_AUTH_URL",
1958
+ "ARCHAL_AUTH_BASE_URL",
1959
+ "ARCHAL_API_URL",
1960
+ "ARCHAL_API_BASE_URL"
1961
+ ])];
1962
+ return readFirstConfiguredBaseUrl(envVars) ?? DEFAULT_AUTH_BASE_URL;
1963
+ }
1964
+ function isImplicitTestSandboxFlow() {
1965
+ if (trimEnv("ARCHAL_HOME")) {
1966
+ return false;
1967
+ }
1968
+ return process.env["VITEST"] === "true" || process.env["VITEST_WORKER_ID"] !== void 0 || process.env["JEST_WORKER_ID"] !== void 0 || process.env["NODE_TEST_CONTEXT"] !== void 0;
1969
+ }
1970
+ function preferNewerCredentials(primary, candidate) {
1971
+ if (!primary) return candidate;
1972
+ if (!candidate) return primary;
1973
+ return candidate.expiresAt > primary.expiresAt ? candidate : primary;
1974
+ }
1975
+ function readExplicitEnvCredentials(envName) {
1976
+ const token = trimEnv(envName);
1977
+ if (!token) {
1978
+ return { kind: "missing" };
1979
+ }
1980
+ const claims = decodeJwtPayload(token);
1981
+ const nowSeconds = Math.floor(Date.now() / 1e3);
1982
+ if (claims === null) {
1983
+ if (isWorkspaceApiKey(token)) {
1984
+ return {
1985
+ kind: "valid",
1986
+ credentials: {
1987
+ token,
1988
+ refreshToken: "",
1989
+ email: "(workspace-api-key)",
1990
+ plan: "pro",
1991
+ selectedCloneIds: [],
1992
+ expiresAt: nowSeconds + WORKSPACE_API_KEY_EXPIRES_SECONDS
1993
+ }
1994
+ };
1995
+ }
1996
+ return {
1997
+ kind: "invalid",
1998
+ message: `${envName} must be a JWT with valid plan and exp claims or a workspace API key (archal_ws_...).`
1999
+ };
2000
+ }
2001
+ const email = typeof claims["email"] === "string" ? claims["email"] : `(from ${envName})`;
2002
+ if (!isPlan(claims["plan"])) {
2003
+ return {
2004
+ kind: "invalid",
2005
+ message: `${envName} JWT does not contain a valid plan claim.`
2006
+ };
2007
+ }
2008
+ const plan = claims["plan"];
2009
+ if (typeof claims["exp"] !== "number" || !Number.isFinite(claims["exp"])) {
2010
+ return {
2011
+ kind: "invalid",
2012
+ message: `${envName} JWT does not contain a valid exp claim.`
2013
+ };
2014
+ }
2015
+ const expiresAt = claims["exp"];
2016
+ if (expiresAt <= nowSeconds) {
2017
+ return {
2018
+ kind: "invalid",
2019
+ message: `${envName} is expired. Remove it or replace it with a valid token.`
2020
+ };
2021
+ }
2022
+ return {
2023
+ kind: "valid",
2024
+ credentials: {
2025
+ token,
2026
+ refreshToken: "",
2027
+ email,
2028
+ plan,
2029
+ selectedCloneIds: [],
2030
+ expiresAt
2031
+ }
2032
+ };
2033
+ }
2034
+ function readStoredCredentials(includeExpired = false) {
2035
+ prepareStoredCredentialSources();
2036
+ const sandboxCredentials = getStoredCredentials({ includeExpired });
2037
+ if (!isImplicitTestSandboxFlow()) {
2038
+ return sandboxCredentials;
2039
+ }
2040
+ const realHomeCredentials = getRealHomeStoredCredentials({ includeExpired });
2041
+ return preferNewerCredentials(sandboxCredentials, realHomeCredentials);
2042
+ }
2043
+ function resolveCredentialsWithSource(tokenEnvVar, includeExpiredStored = false) {
2044
+ const explicitCredentials = readExplicitEnvCredentials(tokenEnvVar);
2045
+ if (explicitCredentials.kind === "invalid") {
2046
+ throw new Error(explicitCredentials.message);
2047
+ }
2048
+ if (explicitCredentials.kind === "valid") {
2049
+ return { credentials: explicitCredentials.credentials, source: "explicit_token" };
2050
+ }
2051
+ const archalCredentials = readExplicitEnvCredentials("ARCHAL_TOKEN");
2052
+ if (archalCredentials.kind === "invalid") {
2053
+ throw new Error(archalCredentials.message);
2054
+ }
2055
+ if (archalCredentials.kind === "valid") {
2056
+ return { credentials: archalCredentials.credentials, source: "archal_token" };
2057
+ }
2058
+ const storedCredentials = readStoredCredentials(includeExpiredStored);
2059
+ if (!storedCredentials) {
2060
+ return null;
2061
+ }
2062
+ return { credentials: storedCredentials, source: "stored" };
2063
+ }
2064
+ function resolveHostedCredentials(tokenEnvVar = "ARCHAL_TOKEN") {
2065
+ return resolveCredentialsWithSource(tokenEnvVar)?.credentials ?? null;
2066
+ }
2067
+ function getCredentials2(tokenEnvVar = "ARCHAL_TOKEN") {
2068
+ const credentials = resolveHostedCredentials(tokenEnvVar);
2069
+ if (!credentials) {
2070
+ return null;
2071
+ }
2072
+ return credentials.expiresAt > Math.floor(Date.now() / 1e3) ? credentials : null;
2073
+ }
2074
+ function prepareStoredCredentialSources() {
2075
+ if (isImplicitTestSandboxFlow()) {
2076
+ seedTestSandboxCredentialsFromRealHome();
2077
+ }
2078
+ }
2079
+ function credentialsMatch(left, right) {
2080
+ return left.token === right.token && left.refreshToken === right.refreshToken && left.expiresAt === right.expiresAt;
2081
+ }
2082
+ function adoptLatestStoredCredentials(credentials) {
2083
+ const latest = readStoredCredentials(true);
2084
+ if (!latest) {
2085
+ return credentials;
2086
+ }
2087
+ return credentialsMatch(latest, credentials) ? credentials : latest;
2088
+ }
2089
+ async function withStoredCredentialsLock(action) {
2090
+ const archalDir = getArchalDir();
2091
+ const lockDirectory = join2(archalDir, "archal-auth-validation.lock");
2092
+ const deadline = Date.now() + STORED_CREDENTIALS_LOCK_TIMEOUT_MS;
2093
+ await fs2.mkdir(archalDir, { recursive: true });
2094
+ while (true) {
2095
+ try {
2096
+ await fs2.mkdir(lockDirectory);
2097
+ break;
2098
+ } catch (error) {
2099
+ const code = error instanceof Error && "code" in error ? String(error.code) : "";
2100
+ if (code !== "EEXIST") {
2101
+ throw error;
2102
+ }
2103
+ const stale = await fs2.stat(lockDirectory).then((stats) => Date.now() - stats.mtimeMs >= STORED_CREDENTIALS_STALE_LOCK_MS).catch(() => false);
2104
+ if (stale) {
2105
+ await fs2.rm(lockDirectory, { recursive: true, force: true });
2106
+ continue;
2107
+ }
2108
+ if (Date.now() >= deadline) {
2109
+ throw new Error(`Timed out waiting for stored Archal credential lock in ${archalDir}.`);
2110
+ }
2111
+ await sleep(STORED_CREDENTIALS_LOCK_POLL_INTERVAL_MS);
2112
+ }
2113
+ }
2114
+ const keepalive = setInterval(() => {
2115
+ void fs2.utimes(lockDirectory, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
2116
+ }, 1e3);
2117
+ keepalive.unref();
2118
+ try {
2119
+ return await action();
2120
+ } finally {
2121
+ clearInterval(keepalive);
2122
+ await fs2.rm(lockDirectory, { recursive: true, force: true });
2123
+ }
2124
+ }
2125
+ async function resolveCurrentCredentials(credentials, source, forceValidation, authBaseUrl) {
2126
+ if (source !== "stored") {
2127
+ return credentials;
2128
+ }
2129
+ let candidate = adoptLatestStoredCredentials(credentials);
2130
+ if (!forceValidation) {
2131
+ return candidate;
2132
+ }
2133
+ return await withStoredCredentialsLock(async () => {
2134
+ candidate = adoptLatestStoredCredentials(candidate);
2135
+ const validation = await validateAuthWithServerResult(candidate, { authBaseUrl });
2136
+ if (validation.validation.ok) {
2137
+ return validation.credentials;
2138
+ }
2139
+ if (validation.validation.code === "auth_rejected") {
2140
+ const reloaded = adoptLatestStoredCredentials(candidate);
2141
+ if (!credentialsMatch(reloaded, candidate)) {
2142
+ const reloadedValidation = await validateAuthWithServerResult(reloaded, {
2143
+ authBaseUrl
2144
+ });
2145
+ if (reloadedValidation.validation.ok) {
2146
+ return reloadedValidation.credentials;
2147
+ }
2148
+ candidate = reloadedValidation.credentials;
2149
+ }
2150
+ }
2151
+ if (validation.validation.code === "auth_rejected") {
2152
+ throw new Error(`Authentication failed. ${validation.validation.reason}`);
2153
+ }
2154
+ return candidate;
2155
+ });
2156
+ }
2157
+ async function createHostedAuthLease(options = {}) {
2158
+ const tokenEnvVar = options.tokenEnvVar ?? "ARCHAL_TOKEN";
2159
+ const authBaseUrl = resolveHostedAuthBaseUrl(options);
2160
+ const resolved = resolveCredentialsWithSource(tokenEnvVar, true);
2161
+ if (!resolved) {
2162
+ const tokenSources = Array.from(/* @__PURE__ */ new Set([tokenEnvVar, "ARCHAL_TOKEN"])).join(", ");
2163
+ throw new Error(
2164
+ `Hosted routing requires ${tokenSources} or stored credentials from \`archal login\`.`
2165
+ );
2166
+ }
2167
+ let current = resolved.credentials;
2168
+ let stopped = false;
2169
+ let refreshInFlight;
2170
+ let refreshInFlightValidates = false;
2171
+ let validated = resolved.source !== "stored";
2172
+ const refreshNow = async (forceValidation = false) => {
2173
+ if (stopped) {
2174
+ return current;
2175
+ }
2176
+ if (refreshInFlight) {
2177
+ const inFlight = refreshInFlight;
2178
+ const inFlightValidates = refreshInFlightValidates;
2179
+ const next = await inFlight;
2180
+ if (!forceValidation || inFlightValidates) {
2181
+ return next;
2182
+ }
2183
+ }
2184
+ const shouldValidate = forceValidation || !validated;
2185
+ refreshInFlightValidates = shouldValidate;
2186
+ refreshInFlight = (async () => {
2187
+ current = await resolveCurrentCredentials(
2188
+ current,
2189
+ resolved.source,
2190
+ shouldValidate,
2191
+ authBaseUrl
2192
+ );
2193
+ if (shouldValidate) {
2194
+ validated = true;
2195
+ }
2196
+ return current;
2197
+ })().finally(() => {
2198
+ refreshInFlight = void 0;
2199
+ refreshInFlightValidates = false;
2200
+ });
2201
+ return await refreshInFlight;
2202
+ };
2203
+ current = await refreshNow(true);
2204
+ return {
2205
+ getCredentials: () => stopped ? null : current,
2206
+ getAuthorizationHeader: () => stopped ? null : `Bearer ${current.token}`,
2207
+ ensureFresh: refreshNow,
2208
+ stop: () => {
2209
+ stopped = true;
2210
+ }
2211
+ };
2212
+ }
2213
+
2214
+ // ../runtime/src/session-reaper.ts
2215
+ import { promises as fs3 } from "fs";
2216
+
2217
+ // ../runtime/src/process-liveness.ts
2218
+ function isProcessAlive(pid) {
2219
+ try {
2220
+ process.kill(pid, 0);
2221
+ return true;
2222
+ } catch (error) {
2223
+ return error instanceof Error && "code" in error && error.code === "EPERM";
2224
+ }
2225
+ }
2226
+
2227
+ // ../runtime/src/session-reaper.ts
2228
+ var REQUEST_TIMEOUT_MS2 = 8e3;
2229
+ var LOCK_TIMEOUT_MS = 3e4;
2230
+ var STALE_LOCK_MS = LOCK_TIMEOUT_MS;
2231
+ var POLL_INTERVAL_MS = 250;
2232
+ async function withLock(lockDirectory, action) {
2233
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
2234
+ while (true) {
2235
+ try {
2236
+ await fs3.mkdir(lockDirectory);
2237
+ break;
2238
+ } catch (error) {
2239
+ const code = error instanceof Error && "code" in error ? String(error.code) : "";
2240
+ if (code !== "EEXIST") {
2241
+ throw error;
2242
+ }
2243
+ const stale = await fs3.stat(lockDirectory).then((stats) => Date.now() - stats.mtimeMs >= STALE_LOCK_MS).catch(() => false);
2244
+ if (stale) {
2245
+ await fs3.rm(lockDirectory, { recursive: true, force: true });
2246
+ continue;
2247
+ }
2248
+ if (Date.now() >= deadline) {
2249
+ throw new Error(`Timed out waiting for hosted-session reaper lock at ${lockDirectory}.`);
2250
+ }
2251
+ await sleep(POLL_INTERVAL_MS);
2252
+ }
2253
+ }
2254
+ try {
2255
+ const keepalive = setInterval(() => {
2256
+ void fs3.utimes(lockDirectory, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()).catch(() => void 0);
2257
+ }, 1e3);
2258
+ keepalive.unref();
2259
+ try {
2260
+ await action();
2261
+ } finally {
2262
+ clearInterval(keepalive);
2263
+ }
2264
+ } finally {
2265
+ await fs3.rm(lockDirectory, { recursive: true, force: true });
2266
+ }
2267
+ }
2268
+ async function stopHostedSession(apiBaseUrl, auth, sessionId, fetchFn = fetch) {
2269
+ const response = await requestHostedSession(auth, fetchFn, `${apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}`, {
2270
+ method: "DELETE"
2271
+ });
2272
+ if (!response.ok && response.status !== 404) {
2273
+ const detail = await response.text().catch(() => "");
2274
+ throw new Error(
2275
+ `Hosted-session reaper failed to stop ${sessionId} (HTTP ${response.status}${detail ? `: ${detail}` : ""}).`
2276
+ );
2277
+ }
2278
+ }
2279
+ async function renewHostedSession(apiBaseUrl, auth, sessionId, fetchFn = fetch) {
2280
+ const response = await requestHostedSession(
2281
+ auth,
2282
+ fetchFn,
2283
+ `${apiBaseUrl}/api/sessions/${encodeURIComponent(sessionId)}/renew`,
2284
+ {
2285
+ method: "POST",
2286
+ body: JSON.stringify({})
2287
+ }
2288
+ );
2289
+ if (response.ok) {
2290
+ return "renewed";
2291
+ }
2292
+ if (response.status === 404 || response.status === 409 || response.status === 410) {
2293
+ return "missing";
2294
+ }
2295
+ if (isTerminalHostedAuthFailure(response.status)) {
2296
+ return "terminal_auth_failure";
2297
+ }
2298
+ const detail = await response.text().catch(() => "");
2299
+ throw new Error(
2300
+ `Hosted-session reaper failed to renew ${sessionId} (HTTP ${response.status}${detail ? `: ${detail}` : ""}).`
2301
+ );
2302
+ }
2303
+ async function requestHostedSession(auth, fetchFn, url, init) {
2304
+ const sendRequest = async (forceValidation = false) => {
2305
+ await auth.ensureFresh(forceValidation);
2306
+ const authorization = auth.getAuthorizationHeader();
2307
+ if (!authorization) {
2308
+ throw new Error("Hosted-session reaper auth is unavailable.");
2309
+ }
2310
+ return await fetchFn(url, {
2311
+ method: init.method,
2312
+ headers: {
2313
+ authorization,
2314
+ "content-type": "application/json"
2315
+ },
2316
+ body: init.body,
2317
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
2318
+ });
2319
+ };
2320
+ let response = await sendRequest();
2321
+ const responseErrorCode = response.ok ? null : parseHostedAuthErrorCode(await response.clone().text().catch(() => ""));
2322
+ if (shouldValidateHostedAuth(response.status, responseErrorCode)) {
2323
+ response = await sendRequest(true);
2324
+ }
2325
+ return response;
2326
+ }
2327
+ async function removeCoordinatorState(coordinatorDirectory, statePath, pidPath) {
2328
+ await fs3.rm(statePath, { force: true }).catch(() => void 0);
2329
+ await fs3.rm(pidPath, { force: true }).catch(() => void 0);
2330
+ await fs3.rmdir(coordinatorDirectory).catch(() => void 0);
2331
+ }
2332
+ async function runHostedSessionReaper({
2333
+ apiBaseUrl,
2334
+ auth,
2335
+ sessionId,
2336
+ runnerPid,
2337
+ renewIntervalMs,
2338
+ coordinatorDirectory,
2339
+ lockDirectory,
2340
+ statePath,
2341
+ pidPath,
2342
+ fetchFn = fetch,
2343
+ isRunnerAlive = isProcessAlive,
2344
+ sleepFn = sleep
2345
+ }) {
2346
+ let nextRenewAt = Date.now() + renewIntervalMs;
2347
+ while (isRunnerAlive(runnerPid)) {
2348
+ if (Date.now() >= nextRenewAt) {
2349
+ const stateExists = await fs3.access(statePath).then(() => true).catch(() => false);
2350
+ if (!stateExists) {
2351
+ await fs3.rm(pidPath, { force: true }).catch(() => void 0);
2352
+ await fs3.rmdir(coordinatorDirectory).catch(() => void 0);
2353
+ return;
2354
+ }
2355
+ try {
2356
+ const renewResult = await renewHostedSession(apiBaseUrl, auth, sessionId, fetchFn);
2357
+ if (renewResult === "missing" || renewResult === "terminal_auth_failure") {
2358
+ await stopHostedSession(apiBaseUrl, auth, sessionId, fetchFn).catch(() => void 0);
2359
+ await removeCoordinatorState(coordinatorDirectory, statePath, pidPath);
2360
+ return;
2361
+ }
2362
+ } catch {
2363
+ }
2364
+ nextRenewAt = Date.now() + renewIntervalMs;
2365
+ }
2366
+ await sleepFn(POLL_INTERVAL_MS);
2367
+ }
2368
+ await withLock(lockDirectory, async () => {
2369
+ const stateExists = await fs3.access(statePath).then(() => true).catch(() => false);
2370
+ if (!stateExists) {
2371
+ return;
2372
+ }
2373
+ await stopHostedSession(apiBaseUrl, auth, sessionId, fetchFn);
2374
+ await fs3.rm(statePath, { force: true });
2375
+ });
2376
+ await fs3.rm(pidPath, { force: true }).catch(() => void 0);
2377
+ await fs3.rmdir(coordinatorDirectory).catch(() => void 0);
2378
+ }
2379
+
2380
+ // ../runtime/src/types.ts
2381
+ import { Buffer as Buffer2 } from "buffer";
2382
+ function redactSessionSnapshot(session) {
2383
+ return {
2384
+ sessionId: session.sessionId,
2385
+ services: session.services.map((service) => ({
2386
+ name: service.name,
2387
+ mode: service.mode,
2388
+ baseUrl: service.baseUrl
2389
+ })),
2390
+ resolvedRuntime: session.resolvedRuntime
2391
+ };
2392
+ }
2393
+ function encodeConfig(config) {
2394
+ return Buffer2.from(JSON.stringify(config), "utf8").toString("base64");
2395
+ }
2396
+ function decodeConfig(encoded) {
2397
+ return JSON.parse(Buffer2.from(encoded, "base64").toString("utf8"));
2398
+ }
2399
+
2400
+ // ../runtime/src/seed-loader.ts
2401
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "fs";
2402
+ import { dirname as dirname2 } from "path";
2403
+
2404
+ // ../runtime/src/temp-artifacts.ts
2405
+ import { join as join3 } from "path";
2406
+ import { tmpdir as tmpdir2 } from "os";
2407
+ function normalizeSessionKey(sessionKey) {
2408
+ const value = sessionKey?.trim() || "default";
2409
+ return value.replace(/[^A-Za-z0-9._-]+/g, "_");
2410
+ }
2411
+ function getSessionIdFilePath(sessionKey) {
2412
+ return join3(tmpdir2(), "archal-runtime", "sessions", `${normalizeSessionKey(sessionKey)}.session-id`);
2413
+ }
2414
+ function getSeedLoadingMarkerPath(sessionKey) {
2415
+ return join3(tmpdir2(), "archal-runtime-seed-markers", `${normalizeSessionKey(sessionKey)}.loading`);
2416
+ }
2417
+ function getSeedLoadedMarkerPath(sessionKey) {
2418
+ return join3(tmpdir2(), "archal-runtime-seed-markers", `${normalizeSessionKey(sessionKey)}.seeded`);
2419
+ }
2420
+
2421
+ // ../runtime/src/seed-loader.ts
2422
+ var SeedLoadError = class extends Error {
2423
+ constructor(message, status) {
2424
+ super(message);
2425
+ this.status = status;
2426
+ }
2427
+ status;
2428
+ get transient() {
2429
+ return this.status === 0 || isTransientSeedLoadStatus(this.status);
2430
+ }
2431
+ };
2432
+ async function loadFileSeedsIntoClones(config, fetchClone2, options = {}) {
2433
+ const contents = config.fileSeedContents;
2434
+ const formats = config.fileSeedFormats;
2435
+ if (!contents || !formats) return;
2436
+ const services = Object.keys(contents);
2437
+ if (services.length === 0) return;
2438
+ const sessionKey = config.sessionKey;
2439
+ const loadingMarkerPath = getSeedLoadingMarkerPath(sessionKey);
2440
+ const loadedMarkerPath = getSeedLoadedMarkerPath(sessionKey);
2441
+ const markerDir = dirname2(loadedMarkerPath);
2442
+ const forceReload = options.forceReload === true;
2443
+ mkdirSync2(markerDir, { recursive: true, mode: 448 });
2444
+ if (forceReload) {
2445
+ rmSync(loadedMarkerPath, { force: true });
2446
+ }
2447
+ while (true) {
2448
+ if (existsSync2(loadedMarkerPath)) {
2449
+ return;
2450
+ }
2451
+ try {
2452
+ writeFileSync2(loadingMarkerPath, String(process.pid), { flag: "wx" });
2453
+ break;
2454
+ } catch {
2455
+ await waitForSeedLoad(loadingMarkerPath, loadedMarkerPath, options);
2456
+ }
2457
+ }
2458
+ try {
2459
+ const parsedJsonSeeds = {};
2460
+ for (const serviceName of services) {
2461
+ const content = contents[serviceName] ?? "";
2462
+ const format = formats[serviceName] ?? "json";
2463
+ if (format !== "json") continue;
2464
+ try {
2465
+ parsedJsonSeeds[serviceName] = JSON.stringify(JSON.parse(content));
2466
+ } catch (parseErr) {
2467
+ const msg = parseErr instanceof Error ? parseErr.message : String(parseErr);
2468
+ throw new Error(`Invalid JSON in seed file for ${serviceName}: ${msg}`);
2469
+ }
2470
+ }
2471
+ const snapshots = await snapshotSeedTargets(services, fetchClone2);
2472
+ const committed = [];
2473
+ try {
2474
+ for (const serviceName of services) {
2475
+ const content = contents[serviceName] ?? "";
2476
+ const format = formats[serviceName] ?? "json";
2477
+ if (format === "json") {
2478
+ process.stderr.write(`Seed: loading ${serviceName} state from JSON file
2479
+ `);
2480
+ await loadSeedStateWithRetry(
2481
+ serviceName,
2482
+ fetchClone2,
2483
+ snapshots,
2484
+ parsedJsonSeeds[serviceName] ?? "{}",
2485
+ "application/json"
2486
+ );
2487
+ committed.push(serviceName);
2488
+ continue;
2489
+ }
2490
+ if (format === "sql") {
2491
+ process.stderr.write(`Seed: loading ${serviceName} state from SQL file
2492
+ `);
2493
+ await loadSeedStateWithRetry(serviceName, fetchClone2, snapshots, content, "text/sql");
2494
+ committed.push(serviceName);
2495
+ continue;
2496
+ }
2497
+ throw new Error(`Unsupported seed file format for ${serviceName}: ${format}`);
2498
+ }
2499
+ } catch (error) {
2500
+ const seedError = error instanceof Error ? error : new Error(String(error));
2501
+ try {
2502
+ await restoreSeedTargets(committed, snapshots, fetchClone2);
2503
+ } catch (rollbackError) {
2504
+ const rollbackMessage = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
2505
+ throw new Error(
2506
+ `${seedError.message}
2507
+ Rollback failed after partial seed load: ${rollbackMessage}`
2508
+ );
2509
+ }
2510
+ throw seedError;
2511
+ }
2512
+ writeFileSync2(loadedMarkerPath, String(process.pid), { flag: "w" });
2513
+ } finally {
2514
+ rmSync(loadingMarkerPath, { force: true });
2515
+ }
2516
+ }
2517
+ async function loadSeedStateWithRetry(serviceName, fetchClone2, snapshots, body, contentType, options = {}) {
2518
+ const maxAttempts = positiveIntegerFromEnv("ARCHAL_SEED_LOAD_MAX_ATTEMPTS", 8);
2519
+ const baseDelayMs = positiveIntegerFromEnv("ARCHAL_SEED_LOAD_RETRY_BASE_DELAY_MS", 1e3);
2520
+ const deadlineAt = Date.now() + positiveIntegerFromEnv("ARCHAL_SEED_LOAD_TIMEOUT_MS", 12e4);
2521
+ let lastError = null;
2522
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2523
+ try {
2524
+ await loadSeedState(serviceName, fetchClone2, body, contentType);
2525
+ return;
2526
+ } catch (error) {
2527
+ const seedError = error instanceof Error ? error : new Error(String(error));
2528
+ lastError = seedError;
2529
+ if (!(seedError instanceof SeedLoadError)) {
2530
+ throw seedError;
2531
+ }
2532
+ if (options.restoreOnFailure !== false && shouldRestoreFailedSeedLoad(seedError.status)) {
2533
+ await restoreSeedTargets([serviceName], snapshots, fetchClone2);
2534
+ }
2535
+ if (!seedError.transient || attempt === maxAttempts || Date.now() >= deadlineAt) {
2536
+ throw seedError;
2537
+ }
2538
+ await waitForSeedLoadRetry(baseDelayMs, attempt, deadlineAt);
2539
+ }
2540
+ }
2541
+ throw lastError ?? new Error(`Failed to load seed into ${serviceName} service.`);
2542
+ }
2543
+ async function snapshotSeedTargets(services, fetchClone2) {
2544
+ const snapshots = /* @__PURE__ */ new Map();
2545
+ await Promise.all(
2546
+ services.map(async (serviceName) => {
2547
+ snapshots.set(serviceName, await fetchSeedStateSnapshot(serviceName, fetchClone2));
2548
+ })
2549
+ );
2550
+ return snapshots;
2551
+ }
2552
+ async function fetchSeedStateSnapshot(serviceName, fetchClone2) {
2553
+ const maxAttempts = positiveIntegerFromEnv("ARCHAL_SEED_LOAD_MAX_ATTEMPTS", 8);
2554
+ const baseDelayMs = positiveIntegerFromEnv("ARCHAL_SEED_LOAD_RETRY_BASE_DELAY_MS", 1e3);
2555
+ const totalTimeoutMs = positiveIntegerFromEnv("ARCHAL_SEED_LOAD_TIMEOUT_MS", 12e4);
2556
+ const requestTimeoutMs = positiveIntegerFromEnv("ARCHAL_SEED_LOAD_REQUEST_TIMEOUT_MS", 3e4);
2557
+ const deadlineAt = Date.now() + totalTimeoutMs;
2558
+ let lastStatus = 0;
2559
+ let lastDetail = "";
2560
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
2561
+ const remainingMs = deadlineAt - Date.now();
2562
+ if (remainingMs <= 0) {
2563
+ lastDetail = `state snapshot exceeded ${totalTimeoutMs}ms total timeout`;
2564
+ break;
2565
+ }
2566
+ const controller = new AbortController();
2567
+ const requestTimer = setTimeout(
2568
+ () => controller.abort(),
2569
+ Math.min(requestTimeoutMs, remainingMs)
2570
+ );
2571
+ let res;
2572
+ try {
2573
+ res = await fetchClone2(serviceName, "/state", {
2574
+ method: "GET",
2575
+ headers: { accept: "application/json" },
2576
+ signal: controller.signal
2577
+ });
2578
+ } catch (err) {
2579
+ lastStatus = 0;
2580
+ lastDetail = controller.signal.aborted ? `state snapshot request exceeded ${Math.min(requestTimeoutMs, remainingMs)}ms request timeout` : err instanceof Error ? err.message : String(err);
2581
+ if (attempt === maxAttempts || Date.now() >= deadlineAt) break;
2582
+ await waitForSeedLoadRetry(baseDelayMs, attempt, deadlineAt);
2583
+ continue;
2584
+ } finally {
2585
+ clearTimeout(requestTimer);
2586
+ }
2587
+ if (res.ok) return res.text();
2588
+ lastStatus = res.status;
2589
+ lastDetail = await res.text().catch(() => "");
2590
+ if (!isTransientSeedLoadStatus(res.status) || attempt === maxAttempts) break;
2591
+ await waitForSeedLoadRetry(baseDelayMs, attempt, deadlineAt);
2592
+ }
2593
+ throw new Error(
2594
+ `Failed to snapshot ${serviceName} before loading seed (${lastStatus}).${lastDetail ? ` ${lastDetail.slice(0, 200)}` : ""}`
2595
+ );
2596
+ }
2597
+ async function restoreSeedTargets(services, snapshots, fetchClone2) {
2598
+ const failures = [];
2599
+ for (const serviceName of services) {
2600
+ const snapshot = snapshots.get(serviceName);
2601
+ if (snapshot === void 0) {
2602
+ failures.push(`${serviceName}: missing snapshot`);
2603
+ continue;
2604
+ }
2605
+ try {
2606
+ await loadSeedStateWithRetry(
2607
+ serviceName,
2608
+ fetchClone2,
2609
+ /* @__PURE__ */ new Map(),
2610
+ snapshot,
2611
+ "application/json",
2612
+ {
2613
+ restoreOnFailure: false
2614
+ }
2615
+ );
2616
+ } catch (error) {
2617
+ failures.push(`${serviceName}: ${error instanceof Error ? error.message : String(error)}`);
2618
+ }
2619
+ }
2620
+ if (failures.length > 0) {
2621
+ throw new Error(`Seed load failed and rollback also failed: ${failures.join("; ")}`);
2622
+ }
2623
+ }
2624
+ async function waitForSeedLoad(loadingMarkerPath, loadedMarkerPath, options = {}) {
2625
+ const envVar = options.timeoutEnvVar ?? "ARCHAL_SEED_LOAD_TIMEOUT_MS";
2626
+ const timeoutMs = Number(process.env[envVar]) || 12e4;
2627
+ const timeoutAt = Date.now() + timeoutMs;
2628
+ while (Date.now() < timeoutAt) {
2629
+ if (existsSync2(loadedMarkerPath) || !existsSync2(loadingMarkerPath)) {
2630
+ return;
2631
+ }
2632
+ if (isStaleSeedLoadingMarker(loadingMarkerPath)) {
2633
+ rmSync(loadingMarkerPath, { force: true });
2634
+ return;
2635
+ }
2636
+ await new Promise((resolve) => setTimeout(resolve, 25));
2637
+ }
2638
+ throw new Error("Timed out waiting for another worker to finish loading seed files.");
2639
+ }
2640
+ function isStaleSeedLoadingMarker(loadingMarkerPath) {
2641
+ let rawPid = "";
2642
+ try {
2643
+ rawPid = readFileSync2(loadingMarkerPath, "utf8").trim();
2644
+ } catch {
2645
+ return false;
2646
+ }
2647
+ const pid = Number(rawPid);
2648
+ if (!Number.isInteger(pid) || pid <= 0) {
2649
+ return true;
2650
+ }
2651
+ if (pid === process.pid) {
2652
+ return false;
2653
+ }
2654
+ try {
2655
+ process.kill(pid, 0);
2656
+ return false;
2657
+ } catch (error) {
2658
+ const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
2659
+ return code === "ESRCH";
2660
+ }
2661
+ }
2662
+ async function loadSeedState(serviceName, fetchClone2, body, contentType) {
2663
+ const requestTimeoutMs = positiveIntegerFromEnv("ARCHAL_SEED_LOAD_REQUEST_TIMEOUT_MS", 3e4);
2664
+ let lastStatus = 0;
2665
+ let lastDetail = "";
2666
+ let lastServerDetail = "";
2667
+ const controller = new AbortController();
2668
+ const requestTimer = setTimeout(() => controller.abort(), requestTimeoutMs);
2669
+ let res;
2670
+ try {
2671
+ res = await fetchClone2(serviceName, "/state", {
2672
+ method: "PUT",
2673
+ body,
2674
+ headers: { "content-type": contentType },
2675
+ signal: controller.signal
2676
+ });
2677
+ } catch (err) {
2678
+ lastStatus = 0;
2679
+ lastDetail = controller.signal.aborted ? `state load request exceeded ${requestTimeoutMs}ms request timeout` : err instanceof Error ? err.message : String(err);
2680
+ throw new SeedLoadError(
2681
+ formatSeedLoadFailure(serviceName, lastStatus, {
2682
+ detail: lastDetail,
2683
+ lastServerDetail
2684
+ }),
2685
+ lastStatus
2686
+ );
2687
+ } finally {
2688
+ clearTimeout(requestTimer);
2689
+ }
2690
+ if (res.ok) {
2691
+ process.stderr.write(`Seed: ${serviceName} state loaded
2692
+ `);
2693
+ return;
2694
+ }
2695
+ lastStatus = res.status;
2696
+ lastDetail = await res.text().catch(() => "");
2697
+ if (lastDetail) {
2698
+ lastServerDetail = lastDetail;
2699
+ }
2700
+ throw new SeedLoadError(
2701
+ formatSeedLoadFailure(serviceName, lastStatus, {
2702
+ detail: lastDetail,
2703
+ lastServerDetail
2704
+ }),
2705
+ lastStatus
2706
+ );
2707
+ }
2708
+ function formatSeedLoadFailure(serviceName, status, diagnostics) {
2709
+ const lines = [`Failed to load seed into ${serviceName} service (${status}).`];
2710
+ if (isTransientSeedLoadStatus(status)) {
2711
+ lines.push(
2712
+ " The clone control endpoint did not accept seed state before the seed-load budget expired.",
2713
+ " This usually indicates hosted clone/proxy/worker infrastructure, not seed schema."
2714
+ );
2715
+ } else {
2716
+ lines.push(" The seed structure may not match the service state schema.");
2717
+ }
2718
+ if (diagnostics.detail) {
2719
+ lines.push(` Server response: ${diagnostics.detail.slice(0, 200)}`);
2720
+ }
2721
+ if (diagnostics.lastServerDetail && diagnostics.lastServerDetail !== diagnostics.detail) {
2722
+ lines.push(` Last upstream response: ${diagnostics.lastServerDetail.slice(0, 200)}`);
2723
+ }
2724
+ return lines.join("\n");
2725
+ }
2726
+ async function waitForSeedLoadRetry(baseDelayMs, attempt, deadlineAt) {
2727
+ const delayMs = Math.min(baseDelayMs * attempt, Math.max(0, deadlineAt - Date.now()));
2728
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
2729
+ }
2730
+ function isTransientSeedLoadStatus(status) {
2731
+ return status === 502 || status === 503 || status === 504;
2732
+ }
2733
+ function shouldRestoreFailedSeedLoad(status) {
2734
+ return status === 0 || status >= 500;
2735
+ }
2736
+ function positiveIntegerFromEnv(name, fallback) {
2737
+ const value = Number(process.env[name]);
2738
+ return Number.isInteger(value) && value > 0 ? value : fallback;
2739
+ }
2740
+
2741
+ export {
2742
+ sleep,
2743
+ DEFAULT_HOSTED_API_BASE_URL,
2744
+ DEFAULT_SESSION_TTL_SECONDS,
2745
+ DEFAULT_READY_TIMEOUT_MS,
2746
+ DEFAULT_RENEW_INTERVAL_MS,
2747
+ MIN_RENEW_INTERVAL_MS,
2748
+ MIN_READY_TIMEOUT_MS,
2749
+ trimEnv,
2750
+ parsePositiveInteger,
2751
+ normalizeApiBaseUrl,
2752
+ fileExists,
2753
+ requestedSeedsMatchSession,
2754
+ HostedSessionClient,
2755
+ getCredentials2 as getCredentials,
2756
+ createHostedAuthLease,
2757
+ getSessionIdFilePath,
2758
+ loadFileSeedsIntoClones,
2759
+ isProcessAlive,
2760
+ runHostedSessionReaper,
2761
+ redactSessionSnapshot,
2762
+ encodeConfig,
2763
+ decodeConfig
2764
+ };