run402-mcp 3.5.3 → 3.5.5

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 (62) hide show
  1. package/README.md +13 -3
  2. package/core/dist/config.d.ts +19 -0
  3. package/core/dist/config.d.ts.map +1 -1
  4. package/core/dist/config.js +86 -4
  5. package/core/dist/config.js.map +1 -1
  6. package/dist/config.d.ts +1 -1
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +1 -1
  9. package/dist/config.js.map +1 -1
  10. package/dist/index.js +5 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/sdk.d.ts.map +1 -1
  13. package/dist/sdk.js +2 -1
  14. package/dist/sdk.js.map +1 -1
  15. package/dist/tools/project-archives.d.ts +62 -0
  16. package/dist/tools/project-archives.d.ts.map +1 -0
  17. package/dist/tools/project-archives.js +127 -0
  18. package/dist/tools/project-archives.js.map +1 -0
  19. package/package.json +3 -3
  20. package/sdk/README.md +3 -1
  21. package/sdk/core-dist/config.d.ts +19 -0
  22. package/sdk/core-dist/config.js +86 -4
  23. package/sdk/dist/credentials.d.ts +1 -1
  24. package/sdk/dist/credentials.d.ts.map +1 -1
  25. package/sdk/dist/index.d.ts +4 -0
  26. package/sdk/dist/index.d.ts.map +1 -1
  27. package/sdk/dist/index.js +4 -0
  28. package/sdk/dist/index.js.map +1 -1
  29. package/sdk/dist/namespaces/archives.d.ts +12 -0
  30. package/sdk/dist/namespaces/archives.d.ts.map +1 -0
  31. package/sdk/dist/namespaces/archives.js +202 -0
  32. package/sdk/dist/namespaces/archives.js.map +1 -0
  33. package/sdk/dist/namespaces/archives.types.d.ts +163 -0
  34. package/sdk/dist/namespaces/archives.types.d.ts.map +1 -0
  35. package/sdk/dist/namespaces/archives.types.js +2 -0
  36. package/sdk/dist/namespaces/archives.types.js.map +1 -0
  37. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  38. package/sdk/dist/namespaces/deploy.js +173 -15
  39. package/sdk/dist/namespaces/deploy.js.map +1 -1
  40. package/sdk/dist/namespaces/deploy.types.d.ts +16 -1
  41. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  42. package/sdk/dist/namespaces/deploy.types.js.map +1 -1
  43. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  44. package/sdk/dist/namespaces/projects.js +1 -0
  45. package/sdk/dist/namespaces/projects.js.map +1 -1
  46. package/sdk/dist/namespaces/projects.types.d.ts +7 -0
  47. package/sdk/dist/namespaces/projects.types.d.ts.map +1 -1
  48. package/sdk/dist/node/archives-node.d.ts +14 -0
  49. package/sdk/dist/node/archives-node.d.ts.map +1 -0
  50. package/sdk/dist/node/archives-node.js +715 -0
  51. package/sdk/dist/node/archives-node.js.map +1 -0
  52. package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
  53. package/sdk/dist/node/deploy-manifest.js +15 -2
  54. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  55. package/sdk/dist/node/index.d.ts +4 -1
  56. package/sdk/dist/node/index.d.ts.map +1 -1
  57. package/sdk/dist/node/index.js +3 -0
  58. package/sdk/dist/node/index.js.map +1 -1
  59. package/sdk/dist/scoped.d.ts +12 -0
  60. package/sdk/dist/scoped.d.ts.map +1 -1
  61. package/sdk/dist/scoped.js +25 -0
  62. package/sdk/dist/scoped.js.map +1 -1
@@ -0,0 +1,715 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstat, mkdir, mkdtemp, opendir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { LocalError, NetworkError } from "../errors.js";
6
+ import { Archives } from "../namespaces/archives.js";
7
+ const DEFAULT_CORE_URL = "http://127.0.0.1:4020";
8
+ const PROJECT_ARCHIVE_VERSION = "run402-project-archive.v1";
9
+ const PROJECT_ARCHIVE_DIGEST_IDENTITY = "run402-project-archive-logical-v1";
10
+ const TAR_BLOCK_BYTES = 512;
11
+ const ZERO_BLOCK = Buffer.alloc(TAR_BLOCK_BYTES);
12
+ const SUPPORTED_CAPABILITIES = new Set([
13
+ "run402.core.release-state.v1",
14
+ "run402.core.database.phased-postgres-copy.v1",
15
+ "run402.core.storage.cas.v1",
16
+ "run402.core.functions.node22.v1",
17
+ "run402.core.astro-ssr.v1",
18
+ "run402.core.auth-stubs.v1",
19
+ "run402.core.secret-requirements.v1",
20
+ ]);
21
+ export class NodeArchives extends Archives {
22
+ constructor(client) {
23
+ super(client);
24
+ }
25
+ inspect(archivePath) {
26
+ return inspectArchive(archivePath);
27
+ }
28
+ verify(archivePath) {
29
+ return verifyArchive(archivePath);
30
+ }
31
+ importToCore(opts) {
32
+ return importArchiveToCore(opts);
33
+ }
34
+ }
35
+ export async function inspectArchive(archivePath) {
36
+ return verifyLocalPortableArchive(path.resolve(archivePath));
37
+ }
38
+ export async function verifyArchive(archivePath) {
39
+ return verifyLocalPortableArchive(path.resolve(archivePath));
40
+ }
41
+ export async function importArchiveToCore(opts) {
42
+ const archivePath = path.resolve(opts.archivePath);
43
+ const verification = await verifyLocalPortableArchive(archivePath);
44
+ if (!verification.ok) {
45
+ return failedImportResult({
46
+ archiveDigest: verification.archive_digest,
47
+ requiredSecrets: verification.required_secrets,
48
+ diagnostics: [
49
+ diagnostic({
50
+ code: "IMPORT_VERIFY_FAILED",
51
+ resourceType: "archive",
52
+ message: "Portable archive verification failed locally; Core import was not called.",
53
+ context: { diagnostic_count: verification.diagnostics.length },
54
+ }),
55
+ ...verification.diagnostics,
56
+ ],
57
+ nextAction: {
58
+ type: "run_command",
59
+ message: "Run `run402 archives verify <archive> --json`, fix the archive issue, then retry import.",
60
+ },
61
+ });
62
+ }
63
+ const secretValues = {
64
+ ...(opts.envFile ? await readEnvFile(opts.envFile) : {}),
65
+ ...(opts.secretValues ?? {}),
66
+ };
67
+ const staged = await stageArchiveForCoreImport(archivePath, verification);
68
+ try {
69
+ const coreUrl = normalizeCoreUrl(opts.coreUrl);
70
+ let res;
71
+ try {
72
+ res = await fetch(`${coreUrl}/archives/v1/import`, {
73
+ method: "POST",
74
+ headers: { "content-type": "application/json" },
75
+ body: JSON.stringify({
76
+ archive_path: staged.archivePath,
77
+ name: opts.name,
78
+ dry_run: opts.dryRun ?? false,
79
+ require_runnable: opts.requireRunnable ?? false,
80
+ secret_values: secretValues,
81
+ }),
82
+ });
83
+ }
84
+ catch (err) {
85
+ throw new NetworkError(`Network error while importing archive into Core: ${err.message}`, err, "importing archive into Core");
86
+ }
87
+ const body = await parseJsonBody(res);
88
+ if (isImportResult(body))
89
+ return body;
90
+ if (!res.ok) {
91
+ return failedImportResult({
92
+ archiveDigest: verification.archive_digest,
93
+ requiredSecrets: verification.required_secrets,
94
+ diagnostics: [
95
+ diagnostic({
96
+ code: bodyErrorCode(body),
97
+ resourceType: "core_import",
98
+ message: bodyErrorMessage(body, `Core import failed with HTTP ${res.status}.`),
99
+ retryable: res.status >= 500 || res.status === 429,
100
+ context: {
101
+ http_status: res.status,
102
+ core_url: coreUrl,
103
+ body: safeBodyContext(body),
104
+ },
105
+ }),
106
+ ],
107
+ nextAction: {
108
+ type: res.status >= 500 ? "retry_later" : "read_docs",
109
+ message: "Fix the reported Core import error and retry.",
110
+ },
111
+ });
112
+ }
113
+ return failedImportResult({
114
+ archiveDigest: verification.archive_digest,
115
+ requiredSecrets: verification.required_secrets,
116
+ diagnostics: [
117
+ diagnostic({
118
+ code: "IMPORT_CONFORMANCE_FAILED",
119
+ resourceType: "core_import",
120
+ message: "Core import returned an unexpected success body.",
121
+ context: { core_url: coreUrl, body: safeBodyContext(body) },
122
+ }),
123
+ ],
124
+ nextAction: {
125
+ type: "contact_support",
126
+ message: "Report the unexpected Core import response with the JSON body.",
127
+ },
128
+ });
129
+ }
130
+ finally {
131
+ if (staged.cleanupPath) {
132
+ await rm(staged.cleanupPath, { recursive: true, force: true }).catch(() => undefined);
133
+ }
134
+ }
135
+ }
136
+ export async function readEnvFile(envFile) {
137
+ const fullPath = path.resolve(envFile);
138
+ const text = await readFile(fullPath, "utf8");
139
+ const values = {};
140
+ let lineNo = 0;
141
+ for (const originalLine of text.split(/\r?\n/)) {
142
+ lineNo += 1;
143
+ const line = originalLine.trim();
144
+ if (!line || line.startsWith("#"))
145
+ continue;
146
+ const normalized = line.startsWith("export ") ? line.slice("export ".length).trim() : line;
147
+ const eq = normalized.indexOf("=");
148
+ if (eq <= 0) {
149
+ throw new LocalError(`Invalid env file line ${lineNo}: expected KEY=value`, "reading archive env file", {
150
+ code: "INVALID_ENV_FILE",
151
+ details: { path: fullPath, line: lineNo },
152
+ });
153
+ }
154
+ const key = normalized.slice(0, eq).trim();
155
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
156
+ throw new LocalError(`Invalid env var name on line ${lineNo}: ${key}`, "reading archive env file", {
157
+ code: "INVALID_ENV_FILE",
158
+ details: { path: fullPath, line: lineNo, key },
159
+ });
160
+ }
161
+ values[key] = parseEnvValue(normalized.slice(eq + 1).trim());
162
+ }
163
+ return values;
164
+ }
165
+ async function stageArchiveForCoreImport(archivePath, verification) {
166
+ if (verification.transport === "directory") {
167
+ return { archivePath, cleanupPath: null };
168
+ }
169
+ if (verification.transport !== "tar") {
170
+ throw new LocalError("Core import supports directory or uncompressed tar archives only.", "staging archive for Core import", {
171
+ code: "ARCHIVE_MEDIA_TYPE_UNSUPPORTED",
172
+ details: { transport: verification.transport },
173
+ });
174
+ }
175
+ const tmpRoot = await mkdtemp(path.join(tmpdir(), "run402-archive-"));
176
+ await extractUncompressedTar(archivePath, tmpRoot);
177
+ return { archivePath: tmpRoot, cleanupPath: tmpRoot };
178
+ }
179
+ async function verifyLocalPortableArchive(archivePath) {
180
+ const diagnostics = [];
181
+ let archive;
182
+ try {
183
+ archive = await readArchiveEntries(archivePath);
184
+ }
185
+ catch (err) {
186
+ const d = errorToDiagnostic(err);
187
+ return {
188
+ ok: false,
189
+ archive_version: null,
190
+ archive_digest: null,
191
+ transport: null,
192
+ file_count: 0,
193
+ total_bytes: 0,
194
+ descriptor_count: 0,
195
+ required_capabilities: [],
196
+ required_secrets: [],
197
+ auth_subject_stub_count: 0,
198
+ export_report: null,
199
+ portability_report: null,
200
+ diagnostics: [d],
201
+ };
202
+ }
203
+ const layout = parseJsonEntry(archive, "run402-layout.json", diagnostics);
204
+ const index = parseJsonEntry(archive, "index.json", diagnostics);
205
+ let archiveDigest = null;
206
+ let requiredSecrets = [];
207
+ let authSubjectStubCount = 0;
208
+ let exportReport = null;
209
+ let portabilityReport = null;
210
+ if (layout && layout.archive_version !== PROJECT_ARCHIVE_VERSION) {
211
+ diagnostics.push(diagnostic({
212
+ code: "ARCHIVE_UNSUPPORTED_VERSION",
213
+ resourceType: "layout",
214
+ path: "run402-layout.json",
215
+ message: "Archive layout version is not supported.",
216
+ context: { archive_version: layout.archive_version },
217
+ }));
218
+ }
219
+ if (!index) {
220
+ diagnostics.push(diagnostic({
221
+ code: "ARCHIVE_DESCRIPTOR_MISSING",
222
+ resourceType: "archive",
223
+ path: "index.json",
224
+ message: "Portable archive is missing index.json.",
225
+ }));
226
+ }
227
+ else {
228
+ if (index.archive_version !== PROJECT_ARCHIVE_VERSION) {
229
+ diagnostics.push(diagnostic({
230
+ code: "ARCHIVE_UNSUPPORTED_VERSION",
231
+ resourceType: "archive",
232
+ path: "index.json",
233
+ message: "Archive version is not supported.",
234
+ context: { archive_version: index.archive_version },
235
+ }));
236
+ }
237
+ for (const cap of index.required_capabilities ?? []) {
238
+ if (!SUPPORTED_CAPABILITIES.has(cap)) {
239
+ diagnostics.push(diagnostic({
240
+ code: "ARCHIVE_UNSUPPORTED_REQUIRED_CAPABILITY",
241
+ resourceType: "capability",
242
+ resourceId: cap,
243
+ message: `Archive requires unsupported capability ${cap}.`,
244
+ context: { capability: cap },
245
+ }));
246
+ }
247
+ }
248
+ verifyDescriptors(index, archive, diagnostics);
249
+ archiveDigest = computeLogicalDigest(index);
250
+ if (index.archive_digest && index.archive_digest !== archiveDigest) {
251
+ diagnostics.push(diagnostic({
252
+ code: "ARCHIVE_DIGEST_MISMATCH",
253
+ resourceType: "archive",
254
+ path: "index.json",
255
+ message: "Archive logical digest does not match index.json.",
256
+ context: { expected_digest: index.archive_digest, actual_digest: archiveDigest },
257
+ }));
258
+ }
259
+ exportReport = readJsonDescriptor(index, archive, "export_report", diagnostics);
260
+ portabilityReport = readJsonDescriptor(index, archive, "portability_report", diagnostics);
261
+ const secretRequirements = readJsonDescriptor(index, archive, "secret_requirements", diagnostics);
262
+ requiredSecrets = Array.isArray(secretRequirements?.secrets)
263
+ ? secretRequirements.secrets.filter((s) => typeof s?.name === "string")
264
+ : [];
265
+ authSubjectStubCount = countAuthSubjectStubs(index, archive);
266
+ if (Array.isArray(portabilityReport?.entries)) {
267
+ diagnostics.push(...portabilityReport.entries.filter(isArchiveDiagnostic));
268
+ }
269
+ }
270
+ return {
271
+ ok: !diagnostics.some((d) => d.severity === "blocking"),
272
+ archive_version: index?.archive_version === PROJECT_ARCHIVE_VERSION ? PROJECT_ARCHIVE_VERSION : null,
273
+ archive_digest: archiveDigest,
274
+ transport: archive.transport,
275
+ file_count: archive.entries.size,
276
+ total_bytes: archive.totalBytes,
277
+ descriptor_count: index?.descriptors ? Object.keys(index.descriptors).length : 0,
278
+ required_capabilities: Array.isArray(index?.required_capabilities) ? index.required_capabilities : [],
279
+ required_secrets: requiredSecrets,
280
+ auth_subject_stub_count: authSubjectStubCount,
281
+ export_report: exportReport,
282
+ portability_report: portabilityReport,
283
+ diagnostics,
284
+ };
285
+ }
286
+ async function readArchiveEntries(archivePath) {
287
+ const st = await lstat(archivePath);
288
+ if (st.isDirectory()) {
289
+ const entries = new Map();
290
+ let totalBytes = 0;
291
+ async function walk(current, relBase) {
292
+ const dir = await opendir(current);
293
+ for await (const entry of dir) {
294
+ const abs = path.join(current, entry.name);
295
+ const rel = path.posix.join(relBase, entry.name);
296
+ const childStat = await lstat(abs);
297
+ if (childStat.isSymbolicLink()) {
298
+ throw new LocalError(`Archive contains unsupported symlink: ${rel}`, "reading archive", {
299
+ code: "ARCHIVE_ENTRY_TYPE_UNSUPPORTED",
300
+ details: { path: rel },
301
+ });
302
+ }
303
+ if (childStat.isDirectory()) {
304
+ await walk(abs, rel);
305
+ }
306
+ else if (childStat.isFile()) {
307
+ const safeRel = sanitizeTarPath(rel);
308
+ if (entries.has(safeRel)) {
309
+ throw new LocalError(`Duplicate archive path: ${safeRel}`, "reading archive", {
310
+ code: "ARCHIVE_DUPLICATE_PATH",
311
+ details: { path: safeRel },
312
+ });
313
+ }
314
+ const bytes = await readFile(abs);
315
+ entries.set(safeRel, bytes);
316
+ totalBytes += bytes.byteLength;
317
+ }
318
+ else {
319
+ throw new LocalError(`Archive contains unsupported entry: ${rel}`, "reading archive", {
320
+ code: "ARCHIVE_ENTRY_TYPE_UNSUPPORTED",
321
+ details: { path: rel },
322
+ });
323
+ }
324
+ }
325
+ }
326
+ await walk(archivePath, "");
327
+ return { entries, transport: "directory", totalBytes };
328
+ }
329
+ if (!st.isFile()) {
330
+ throw new LocalError("Archive path must be a directory or uncompressed tar file.", "reading archive", {
331
+ code: "ARCHIVE_ENTRY_TYPE_UNSUPPORTED",
332
+ });
333
+ }
334
+ return readTarEntries(archivePath);
335
+ }
336
+ async function readTarEntries(tarPath) {
337
+ const bytes = await readFile(tarPath);
338
+ const entries = new Map();
339
+ let totalBytes = 0;
340
+ let offset = 0;
341
+ while (offset + TAR_BLOCK_BYTES <= bytes.byteLength) {
342
+ const header = bytes.subarray(offset, offset + TAR_BLOCK_BYTES);
343
+ offset += TAR_BLOCK_BYTES;
344
+ if (header.equals(ZERO_BLOCK))
345
+ break;
346
+ const name = readTarString(header, 0, 100);
347
+ const prefix = readTarString(header, 345, 155);
348
+ const type = readTarString(header, 156, 1) || "0";
349
+ const size = readTarOctal(header, 124, 12);
350
+ const rel = sanitizeTarPath(prefix ? `${prefix}/${name}` : name);
351
+ if (entries.has(rel)) {
352
+ throw new LocalError(`Duplicate archive path: ${rel}`, "reading archive tar", {
353
+ code: "ARCHIVE_DUPLICATE_PATH",
354
+ details: { path: rel },
355
+ });
356
+ }
357
+ if (type === "5") {
358
+ offset += Math.ceil(size / TAR_BLOCK_BYTES) * TAR_BLOCK_BYTES;
359
+ continue;
360
+ }
361
+ if (type !== "0" && type !== "\0") {
362
+ throw new LocalError(`Unsupported tar entry type ${JSON.stringify(type)} at ${rel}`, "reading archive tar", {
363
+ code: "ARCHIVE_ENTRY_TYPE_UNSUPPORTED",
364
+ details: { path: rel, type },
365
+ });
366
+ }
367
+ const end = offset + size;
368
+ if (end > bytes.byteLength) {
369
+ throw new LocalError("Malformed tar: entry exceeds file size", "reading archive tar", {
370
+ code: "ARCHIVE_MALFORMED_TAR",
371
+ details: { path: rel },
372
+ });
373
+ }
374
+ const payload = bytes.subarray(offset, end);
375
+ entries.set(rel, payload);
376
+ totalBytes += payload.byteLength;
377
+ offset += Math.ceil(size / TAR_BLOCK_BYTES) * TAR_BLOCK_BYTES;
378
+ }
379
+ return { entries, transport: "tar", totalBytes };
380
+ }
381
+ function parseJsonEntry(archive, relPath, diagnostics) {
382
+ const bytes = archive.entries.get(relPath);
383
+ if (!bytes) {
384
+ diagnostics.push(diagnostic({
385
+ code: "ARCHIVE_DESCRIPTOR_MISSING",
386
+ resourceType: "descriptor",
387
+ path: relPath,
388
+ message: `Archive descriptor ${relPath} is missing.`,
389
+ }));
390
+ return null;
391
+ }
392
+ try {
393
+ return JSON.parse(Buffer.from(bytes).toString("utf8"));
394
+ }
395
+ catch (err) {
396
+ diagnostics.push(diagnostic({
397
+ code: "ARCHIVE_MALFORMED_JSON",
398
+ resourceType: "descriptor",
399
+ path: relPath,
400
+ message: `Archive descriptor ${relPath} is not valid JSON.`,
401
+ context: { error: err.message },
402
+ }));
403
+ return null;
404
+ }
405
+ }
406
+ function readJsonDescriptor(index, archive, key, diagnostics) {
407
+ const descriptor = index.descriptors?.[key];
408
+ const relPath = descriptor?.path ?? fallbackDescriptorPath(key);
409
+ if (!relPath || !archive.entries.has(relPath))
410
+ return null;
411
+ return parseJsonEntry(archive, relPath, diagnostics);
412
+ }
413
+ function fallbackDescriptorPath(key) {
414
+ switch (key) {
415
+ case "export_report": return "manifest/export-report.json";
416
+ case "portability_report": return "manifest/portability-report.json";
417
+ case "secret_requirements": return "secrets/requirements.json";
418
+ case "auth_subjects": return "auth/subjects.ndjson";
419
+ default: return null;
420
+ }
421
+ }
422
+ function verifyDescriptors(index, archive, diagnostics) {
423
+ for (const [name, descriptor] of Object.entries(index.descriptors ?? {})) {
424
+ if (!descriptor.path)
425
+ continue;
426
+ let rel;
427
+ try {
428
+ rel = sanitizeTarPath(descriptor.path);
429
+ }
430
+ catch (err) {
431
+ diagnostics.push(errorToDiagnostic(err));
432
+ continue;
433
+ }
434
+ const bytes = archive.entries.get(rel);
435
+ if (!bytes) {
436
+ diagnostics.push(diagnostic({
437
+ code: descriptor.mediaType === "application/octet-stream" ? "ARCHIVE_BLOB_MISSING" : "ARCHIVE_DESCRIPTOR_MISSING",
438
+ resourceType: "descriptor",
439
+ resourceId: name,
440
+ path: rel,
441
+ message: `Archive descriptor ${name} references missing path ${rel}.`,
442
+ }));
443
+ continue;
444
+ }
445
+ if (typeof descriptor.size === "number" && descriptor.size !== bytes.byteLength) {
446
+ diagnostics.push(diagnostic({
447
+ code: "ARCHIVE_SIZE_MISMATCH",
448
+ resourceType: "descriptor",
449
+ resourceId: name,
450
+ path: rel,
451
+ message: `Archive descriptor ${name} size mismatch.`,
452
+ context: { expected_size: descriptor.size, actual_size: bytes.byteLength },
453
+ }));
454
+ }
455
+ if (descriptor.digest && descriptor.digest !== digestBytes(bytes)) {
456
+ diagnostics.push(diagnostic({
457
+ code: "ARCHIVE_DIGEST_MISMATCH",
458
+ resourceType: "descriptor",
459
+ resourceId: name,
460
+ path: rel,
461
+ message: `Archive descriptor ${name} digest mismatch.`,
462
+ context: { expected_digest: descriptor.digest, actual_digest: digestBytes(bytes) },
463
+ }));
464
+ }
465
+ }
466
+ }
467
+ function computeLogicalDigest(index) {
468
+ const descriptors = [...(index.identity_descriptors ?? [])].sort().map((name) => {
469
+ const d = index.descriptors?.[name];
470
+ return { name, mediaType: d?.mediaType, digest: d?.digest, size: d?.size, path: d?.path };
471
+ });
472
+ return digestBytes(Buffer.from(stableJson({
473
+ identity: PROJECT_ARCHIVE_DIGEST_IDENTITY,
474
+ archive_version: index.archive_version,
475
+ core_compatibility: index.core_compatibility,
476
+ required_capabilities: [...(index.required_capabilities ?? [])].sort(),
477
+ consistency: index.consistency ?? null,
478
+ descriptors,
479
+ }), "utf8"));
480
+ }
481
+ function countAuthSubjectStubs(index, archive) {
482
+ const rel = index.descriptors?.auth_subjects?.path ?? "auth/subjects.ndjson";
483
+ const bytes = archive.entries.get(rel);
484
+ if (!bytes)
485
+ return 0;
486
+ return Buffer.from(bytes).toString("utf8").split(/\r?\n/).filter((line) => line.trim()).length;
487
+ }
488
+ function digestBytes(bytes) {
489
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
490
+ }
491
+ function stableJson(value) {
492
+ if (value === undefined)
493
+ return "null";
494
+ if (value === null || typeof value !== "object")
495
+ return JSON.stringify(value);
496
+ if (Array.isArray(value))
497
+ return `[${value.map(stableJson).join(",")}]`;
498
+ const obj = value;
499
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableJson(obj[key])}`).join(",")}}`;
500
+ }
501
+ function isArchiveDiagnostic(value) {
502
+ return Boolean(value &&
503
+ typeof value === "object" &&
504
+ !Array.isArray(value) &&
505
+ typeof value.code === "string" &&
506
+ typeof value.message === "string");
507
+ }
508
+ function errorToDiagnostic(err) {
509
+ const message = err instanceof Error ? err.message : String(err);
510
+ const body = err && typeof err === "object" && !Array.isArray(err)
511
+ ? err.body
512
+ : null;
513
+ const bodyRecord = body && typeof body === "object" && !Array.isArray(body)
514
+ ? body
515
+ : null;
516
+ const details = bodyRecord?.details && typeof bodyRecord.details === "object" && !Array.isArray(bodyRecord.details)
517
+ ? bodyRecord.details
518
+ : undefined;
519
+ const code = (bodyRecord && typeof bodyRecord.code === "string" ? bodyRecord.code : null) ??
520
+ (err && typeof err === "object" && typeof err.code === "string"
521
+ ? err.code
522
+ : null) ??
523
+ "IMPORT_VERIFY_FAILED";
524
+ return diagnostic({
525
+ code,
526
+ resourceType: "archive",
527
+ path: typeof details?.path === "string" ? details.path : undefined,
528
+ message,
529
+ context: details,
530
+ });
531
+ }
532
+ async function extractUncompressedTar(tarPath, destDir) {
533
+ const bytes = await readFile(tarPath);
534
+ const seen = new Set();
535
+ let offset = 0;
536
+ while (offset + TAR_BLOCK_BYTES <= bytes.byteLength) {
537
+ const header = bytes.subarray(offset, offset + TAR_BLOCK_BYTES);
538
+ offset += TAR_BLOCK_BYTES;
539
+ if (header.equals(ZERO_BLOCK))
540
+ break;
541
+ const name = readTarString(header, 0, 100);
542
+ const prefix = readTarString(header, 345, 155);
543
+ const type = readTarString(header, 156, 1) || "0";
544
+ const size = readTarOctal(header, 124, 12);
545
+ const rel = sanitizeTarPath(prefix ? `${prefix}/${name}` : name);
546
+ if (seen.has(rel)) {
547
+ throw new LocalError(`Duplicate archive path while extracting: ${rel}`, "extracting archive tar", {
548
+ code: "ARCHIVE_DUPLICATE_PATH",
549
+ details: { path: rel },
550
+ });
551
+ }
552
+ seen.add(rel);
553
+ const outputPath = path.join(destDir, rel);
554
+ if (!outputPath.startsWith(`${destDir}${path.sep}`)) {
555
+ throw new LocalError(`Unsafe archive path while extracting: ${rel}`, "extracting archive tar", {
556
+ code: "ARCHIVE_PATH_UNSAFE",
557
+ details: { path: rel },
558
+ });
559
+ }
560
+ if (type === "5") {
561
+ await mkdir(outputPath, { recursive: true });
562
+ }
563
+ else if (type === "0" || type === "\0") {
564
+ const end = offset + size;
565
+ if (end > bytes.byteLength) {
566
+ throw new LocalError("Malformed tar: entry exceeds file size", "extracting archive tar", {
567
+ code: "ARCHIVE_MALFORMED_TAR",
568
+ details: { path: rel },
569
+ });
570
+ }
571
+ await mkdir(path.dirname(outputPath), { recursive: true });
572
+ await writeFile(outputPath, bytes.subarray(offset, end));
573
+ await stat(outputPath).then((s) => {
574
+ if (!s.isFile()) {
575
+ throw new LocalError(`Archive entry did not extract as a regular file: ${rel}`, "extracting archive tar", {
576
+ code: "ARCHIVE_ENTRY_TYPE_UNSUPPORTED",
577
+ details: { path: rel },
578
+ });
579
+ }
580
+ });
581
+ }
582
+ else {
583
+ throw new LocalError(`Unsupported tar entry type ${JSON.stringify(type)} at ${rel}`, "extracting archive tar", {
584
+ code: "ARCHIVE_ENTRY_TYPE_UNSUPPORTED",
585
+ details: { path: rel, type },
586
+ });
587
+ }
588
+ offset += Math.ceil(size / TAR_BLOCK_BYTES) * TAR_BLOCK_BYTES;
589
+ }
590
+ }
591
+ function readTarString(header, start, len) {
592
+ const slice = header.subarray(start, start + len);
593
+ const nul = slice.indexOf(0);
594
+ return slice.subarray(0, nul === -1 ? slice.length : nul).toString("utf8").trim();
595
+ }
596
+ function readTarOctal(header, start, len) {
597
+ const raw = readTarString(header, start, len).replace(/\0/g, "").trim();
598
+ if (!raw)
599
+ return 0;
600
+ if (!/^[0-7]+$/.test(raw)) {
601
+ throw new LocalError(`Malformed tar octal value: ${raw}`, "extracting archive tar", {
602
+ code: "ARCHIVE_MALFORMED_TAR",
603
+ });
604
+ }
605
+ return Number.parseInt(raw, 8);
606
+ }
607
+ function sanitizeTarPath(value) {
608
+ const normalized = path.posix.normalize(value);
609
+ if (!normalized ||
610
+ normalized === "." ||
611
+ normalized.startsWith("/") ||
612
+ normalized.startsWith("../") ||
613
+ normalized.includes("/../") ||
614
+ /[\x00-\x1f\x7f]/.test(normalized)) {
615
+ throw new LocalError(`Unsafe archive path: ${value}`, "extracting archive tar", {
616
+ code: "ARCHIVE_PATH_UNSAFE",
617
+ details: { path: value },
618
+ });
619
+ }
620
+ return normalized;
621
+ }
622
+ function parseEnvValue(raw) {
623
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
624
+ return raw.slice(1, -1);
625
+ }
626
+ const comment = raw.search(/\s#/);
627
+ return (comment === -1 ? raw : raw.slice(0, comment)).trim();
628
+ }
629
+ function normalizeCoreUrl(value) {
630
+ return (value ?? process.env.RUN402_CORE_URL ?? process.env.CORE_GATEWAY_URL ?? DEFAULT_CORE_URL).replace(/\/+$/, "");
631
+ }
632
+ async function parseJsonBody(res) {
633
+ const text = await res.text();
634
+ if (!text)
635
+ return null;
636
+ try {
637
+ return JSON.parse(text);
638
+ }
639
+ catch {
640
+ return { error: "NON_JSON_RESPONSE", message: text.slice(0, 500) };
641
+ }
642
+ }
643
+ function isImportResult(value) {
644
+ return Boolean(value &&
645
+ typeof value === "object" &&
646
+ !Array.isArray(value) &&
647
+ value.schema_version === "run402.project_archive.import_result.v1");
648
+ }
649
+ function failedImportResult(input) {
650
+ return {
651
+ schema_version: "run402.project_archive.import_result.v1",
652
+ status: "failed",
653
+ archive_digest: input.archiveDigest,
654
+ required_secrets: input.requiredSecrets,
655
+ diagnostics: input.diagnostics,
656
+ next_action: input.nextAction,
657
+ };
658
+ }
659
+ function diagnostic(input) {
660
+ return {
661
+ code: input.code,
662
+ severity: "blocking",
663
+ resource_type: input.resourceType,
664
+ ...(input.resourceId ? { resource_id: input.resourceId } : {}),
665
+ ...(input.path ? { path: input.path } : {}),
666
+ message: input.message,
667
+ next_action: { type: "read_docs", message: "Review the archive diagnostic and retry." },
668
+ retryable: input.retryable ?? false,
669
+ ...(input.context ? { context: input.context } : {}),
670
+ };
671
+ }
672
+ function bodyErrorCode(body) {
673
+ if (body && typeof body === "object" && !Array.isArray(body)) {
674
+ const obj = body;
675
+ if (typeof obj.error === "string")
676
+ return obj.error;
677
+ if (typeof obj.code === "string")
678
+ return obj.code;
679
+ }
680
+ return "IMPORT_CONFORMANCE_FAILED";
681
+ }
682
+ function bodyErrorMessage(body, fallback) {
683
+ if (body && typeof body === "object" && !Array.isArray(body)) {
684
+ const obj = body;
685
+ if (typeof obj.message === "string")
686
+ return obj.message;
687
+ if (typeof obj.error === "string")
688
+ return obj.error;
689
+ }
690
+ return fallback;
691
+ }
692
+ function safeBodyContext(body) {
693
+ if (!body || typeof body !== "object" || Array.isArray(body))
694
+ return body;
695
+ const { details, ...rest } = body;
696
+ return {
697
+ ...rest,
698
+ ...(details && typeof details === "object" && !Array.isArray(details)
699
+ ? { details: redactContext(details) }
700
+ : {}),
701
+ };
702
+ }
703
+ function redactContext(input) {
704
+ const out = {};
705
+ for (const [key, value] of Object.entries(input)) {
706
+ if (/secret|token|password|credential|private/i.test(key)) {
707
+ out[key] = "[redacted]";
708
+ }
709
+ else {
710
+ out[key] = value;
711
+ }
712
+ }
713
+ return out;
714
+ }
715
+ //# sourceMappingURL=archives-node.js.map