run402 4.83.0 → 4.84.0

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/gitvault-surface.json +1 -1
  2. package/lib/deploy-v2.mjs +30 -68
  3. package/lib/doctor-source-scan.mjs +2 -547
  4. package/lib/doctor.mjs +28 -8
  5. package/lib/sdk-errors.mjs +7 -0
  6. package/lib/status.mjs +1 -1
  7. package/lib/up.mjs +27 -10
  8. package/lib/update-check.mjs +11 -6
  9. package/lib/update-check.test.mjs +18 -5
  10. package/package.json +1 -1
  11. package/sdk/dist/actions.d.ts +16 -3
  12. package/sdk/dist/actions.d.ts.map +1 -1
  13. package/sdk/dist/config.d.ts +1 -1
  14. package/sdk/dist/config.d.ts.map +1 -1
  15. package/sdk/dist/config.js.map +1 -1
  16. package/sdk/dist/index.d.ts +2 -0
  17. package/sdk/dist/index.d.ts.map +1 -1
  18. package/sdk/dist/index.js +2 -0
  19. package/sdk/dist/index.js.map +1 -1
  20. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  21. package/sdk/dist/namespaces/deploy.js +2 -1
  22. package/sdk/dist/namespaces/deploy.js.map +1 -1
  23. package/sdk/dist/namespaces/deploy.types.d.ts +6 -0
  24. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  25. package/sdk/dist/namespaces/edge-evidence.d.ts +22 -0
  26. package/sdk/dist/namespaces/edge-evidence.d.ts.map +1 -0
  27. package/sdk/dist/namespaces/edge-evidence.js +38 -0
  28. package/sdk/dist/namespaces/edge-evidence.js.map +1 -0
  29. package/sdk/dist/namespaces/projects.d.ts.map +1 -1
  30. package/sdk/dist/namespaces/projects.js +9 -7
  31. package/sdk/dist/namespaces/projects.js.map +1 -1
  32. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  33. package/sdk/dist/node/actions-node.js +71 -57
  34. package/sdk/dist/node/actions-node.js.map +1 -1
  35. package/sdk/dist/node/app-scope.d.ts +24 -0
  36. package/sdk/dist/node/app-scope.d.ts.map +1 -0
  37. package/sdk/dist/node/app-scope.js +47 -0
  38. package/sdk/dist/node/app-scope.js.map +1 -0
  39. package/sdk/dist/node/deploy-manifest.d.ts +6 -0
  40. package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
  41. package/sdk/dist/node/deploy-manifest.js +61 -0
  42. package/sdk/dist/node/deploy-manifest.js.map +1 -1
  43. package/sdk/dist/node/index.d.ts +5 -0
  44. package/sdk/dist/node/index.d.ts.map +1 -1
  45. package/sdk/dist/node/index.js +5 -0
  46. package/sdk/dist/node/index.js.map +1 -1
  47. package/sdk/dist/node/manifest-export.d.ts +5 -0
  48. package/sdk/dist/node/manifest-export.d.ts.map +1 -0
  49. package/sdk/dist/node/manifest-export.js +78 -0
  50. package/sdk/dist/node/manifest-export.js.map +1 -0
  51. package/sdk/dist/node/preflight.d.ts +65 -0
  52. package/sdk/dist/node/preflight.d.ts.map +1 -0
  53. package/sdk/dist/node/preflight.js +23 -0
  54. package/sdk/dist/node/preflight.js.map +1 -0
  55. package/sdk/dist/node/source-scan.d.ts +73 -0
  56. package/sdk/dist/node/source-scan.d.ts.map +1 -0
  57. package/sdk/dist/node/source-scan.js +552 -0
  58. package/sdk/dist/node/source-scan.js.map +1 -0
  59. package/sdk/dist/rest-diagnostics.d.ts +15 -0
  60. package/sdk/dist/rest-diagnostics.d.ts.map +1 -0
  61. package/sdk/dist/rest-diagnostics.js +19 -0
  62. package/sdk/dist/rest-diagnostics.js.map +1 -0
@@ -1,3 +1,7 @@
1
+ import { serializeDeployManifest, manifestExportUnsupported } from "./manifest-export.js";
2
+ import { describeLocalPreflight } from "./preflight.js";
3
+ import { resolveApplicationScope, materializeTemplates } from "./app-scope.js";
4
+ import { scanDeploymentSources } from "./source-scan.js";
1
5
  import { createHash } from "node:crypto";
2
6
  import { execFile } from "node:child_process";
3
7
  import { lstat, mkdir, mkdtemp, readdir, readFile, rename, rm, writeFile, } from "node:fs/promises";
@@ -194,15 +198,20 @@ export class NodeActions {
194
198
  let workspaceDir = source.workspaceDir;
195
199
  const manifest = await this.#discoverAndValidateManifest(input, workspaceDir, source.metadata, run);
196
200
  workspaceDir = dirname(manifest.manifestPath);
197
- if (!input.verifyOnly) {
198
- await resolveDeploymentTarget({
199
- appRoot: workspaceDir, manifestPath: manifest.manifestPath, projectId: input.projectId,
200
- manifestProjectId: manifest.manifestProjectId ?? manifest.appSpec?.project.id, name: input.name,
201
- targetKind: this.#targetKind(), apiBase: this.sdk.apiBase,
202
- allowUnresolved: run.executionMode === "check" || run.executionMode === "printSpec" || run.dryRun,
203
- });
201
+ const target = !input.verifyOnly ? await resolveDeploymentTarget({
202
+ appRoot: workspaceDir, manifestPath: manifest.manifestPath, projectId: input.projectId,
203
+ manifestProjectId: manifest.manifestProjectId ?? manifest.appSpec?.project.id, name: input.name,
204
+ targetKind: this.#targetKind(), apiBase: this.sdk.apiBase,
205
+ allowUnresolved: run.executionMode === "check" || (run.executionMode === "printSpec" || run.executionMode === "printManifest") || run.dryRun,
206
+ }) : null;
207
+ if (!input.verifyOnly && process.env.RUN402_DEPLOY_SKIP_SCAN !== "1") {
208
+ const scan = scanDeploymentSources(manifest.releaseSpec ?? {}, workspaceDir);
209
+ if (scan.errors.length)
210
+ throw run.error("Source scan blocked this application.", "R402_AUTH_PREFLIGHT_FAILED", scan);
204
211
  }
205
212
  if (manifest.manifestKind === "app") {
213
+ if (run.executionMode === "printManifest")
214
+ manifestExportUnsupported("app", "app resources and build intent cannot be faithfully represented as a release manifest");
206
215
  if (input.verifyOnly) {
207
216
  return this.#verifyAppManifestOnly(input, manifest, workspaceDir, run, startedAt);
208
217
  }
@@ -211,22 +220,23 @@ export class NodeActions {
211
220
  startedAt,
212
221
  status: block ? "blocked" : "planned",
213
222
  dryRun: run.dryRun || run.executionMode === "check" || run.executionMode === "printSpec",
214
- projectId: input.projectId ?? (run.dryRun ? "prj_planned" : null),
223
+ projectId: target?.project_id ?? null,
215
224
  diagnostics: block?.diagnostics,
216
225
  nextActions: block?.nextActions,
217
226
  blockedNodeId: block?.nodeId,
218
227
  });
219
228
  if (run.executionMode === "check" || run.executionMode === "printSpec" || run.dryRun) {
220
229
  return run.result({
221
- project_id: input.projectId ?? "prj_planned",
230
+ project_id: target?.project_id ?? null,
222
231
  manifest_path: manifest.manifestPath,
223
232
  app_graph: manifest.appGraph,
224
233
  app_result: appResult,
234
+ preflight: describeLocalPreflight({ appRoot: workspaceDir, manifestPath: manifest.manifestPath, target: target, authoring: manifest.appSpec?.release, spec: manifest.releaseSpec ?? undefined, buildDeferred: !!manifest.appSpec?.build?.commands?.length, apiBase: this.sdk.apiBase, profile: this.opts.profile }),
225
235
  });
226
236
  }
227
237
  if (block) {
228
238
  return run.result({
229
- project_id: input.projectId ?? "prj_planned",
239
+ project_id: target?.project_id ?? null,
230
240
  manifest_path: manifest.manifestPath,
231
241
  app_graph: manifest.appGraph,
232
242
  app_result: appResult,
@@ -237,14 +247,16 @@ export class NodeActions {
237
247
  if (input.verifyOnly) {
238
248
  return this.#verifyDeployManifestOnly(input, manifest, workspaceDir, run);
239
249
  }
240
- if (run.executionMode === "check" || run.executionMode === "printSpec") {
250
+ if (run.executionMode === "check" || (run.executionMode === "printSpec" || run.executionMode === "printManifest")) {
241
251
  if (!manifest.releaseSpec) {
242
252
  throw run.error("Internal error: release manifest did not produce a ReleaseSpec.", "RUN402_ACTION_INTERNAL");
243
253
  }
244
254
  return run.result({
245
- project_id: manifest.releaseSpec.project,
255
+ project_id: target?.project_id ?? null,
246
256
  manifest_path: manifest.manifestPath,
247
- ...(run.executionMode === "printSpec" ? { spec: manifest.releaseSpec } : {}),
257
+ preflight: describeLocalPreflight({ appRoot: workspaceDir, manifestPath: manifest.manifestPath, target: target, authoring: manifest.loaded?.manifest, spec: manifest.releaseSpec, apiBase: this.sdk.apiBase, profile: this.opts.profile }),
258
+ ...(run.executionMode === "printManifest" ? { manifest: serializeDeployManifest(manifest.loaded, workspaceDir, input.projectId) } : {}),
259
+ ...(run.executionMode === "printSpec" ? { spec: { ...manifest.releaseSpec, project: target?.project_id ?? undefined } } : {}),
248
260
  });
249
261
  }
250
262
  if (run.executionMode === "plan") {
@@ -292,14 +304,14 @@ export class NodeActions {
292
304
  mutation: run.executionMode !== "plan",
293
305
  auto: false,
294
306
  details: {
295
- project_id: resolved.projectId,
307
+ project_id: resolved.projectId === "prj_planned" ? null : resolved.projectId,
296
308
  manifest_path: manifest.manifestPath,
297
309
  },
298
310
  });
299
311
  if (run.dryRun) {
300
312
  run.setState(deployStep, "planned");
301
313
  return run.result({
302
- project_id: resolved.projectId,
314
+ project_id: resolved.projectId === "prj_planned" ? null : resolved.projectId,
303
315
  manifest_path: manifest.manifestPath,
304
316
  ...(resolved.shouldWriteLink ? { workspace_link_path: resolved.linkPath } : {}),
305
317
  });
@@ -410,7 +422,7 @@ export class NodeActions {
410
422
  * principal has a display name before the deploy that will be credited to
411
423
  * it, then join the project's room under that name. Explicit
412
424
  * `identityName` / `RUN402_AGENT_NAME` wins and may rename an already-named
413
- * principal; otherwise a detected client name is set only when no name
425
+ * agent principal; human and unknown principals are preserved. Otherwise a detected client name is set only when no name
414
426
  * exists yet, and reported as `detected`. The detected client is always
415
427
  * reported (`detected` + `detection`), even when it was not applied, so an
416
428
  * agent whose principal is already named sees why. Best-effort end to
@@ -421,15 +433,23 @@ export class NodeActions {
421
433
  if (this.#targetKind() === "core") {
422
434
  return { display_name: null, source: "unavailable" };
423
435
  }
436
+ const explicitName = input.identityName?.trim() || declaredAgentName() || null;
437
+ const detectedName = detectClientName();
424
438
  let current = null;
439
+ let principalId = null;
440
+ let principalType = null;
425
441
  try {
426
442
  const me = await this.sdk.orgs.whoami();
443
+ principalId = me.principal?.id ?? null;
444
+ principalType = me.principal?.type ?? null;
427
445
  // The gateway never seeds display_name from a wallet subject (and
428
446
  // nulled the ones it once did), so null here means exactly "unset".
429
447
  current = me.principal?.display_name ?? null;
430
448
  }
431
449
  catch {
432
- return { display_name: null, source: "unavailable" };
450
+ return { display_name: null, source: "unavailable", detected: detectedName,
451
+ principal: { id: null, type: null, display_name: null },
452
+ client: { detected: detectedName, declared_name: explicitName } };
433
453
  }
434
454
  let displayName = current;
435
455
  let source = "existing";
@@ -437,8 +457,13 @@ export class NodeActions {
437
457
  // (RUN402_AGENT_NAME), then a specifically detected client. A generic
438
458
  // guess is never persisted as a name. Detection always runs so the
439
459
  // result can say what was seen even when it was not applied.
440
- const explicitName = input.identityName?.trim() || declaredAgentName() || null;
441
- const detectedName = detectClientName();
460
+ if (principalType !== "agent") {
461
+ run.skipStep({ action: "identity.name.set", description: "Preserve authenticated human or unknown principal identity; client detection does not rename it.", mutation: false, auto: true, details: { principal_type: principalType } });
462
+ return { display_name: current, source: "existing",
463
+ principal: { id: principalId, type: principalType, display_name: current },
464
+ client: { detected: detectedName, declared_name: explicitName },
465
+ detected: detectedName, detection: { applied: false, reason: "principal_identity_preserved" } };
466
+ }
442
467
  const detectionReport = (applied) => {
443
468
  const reason = applied
444
469
  ? "applied"
@@ -447,7 +472,9 @@ export class NodeActions {
447
472
  : explicitName
448
473
  ? "explicit_name_wins"
449
474
  : "name_already_set";
450
- return { detected: detectedName, detection: { applied, reason } };
475
+ return { detected: detectedName, detection: { applied, reason },
476
+ principal: { id: principalId, type: principalType, display_name: displayName },
477
+ client: { detected: detectedName, declared_name: explicitName } };
451
478
  };
452
479
  if (!displayName || (explicitName && explicitName !== displayName)) {
453
480
  const desired = explicitName ?? detectedName;
@@ -693,9 +720,7 @@ export class NodeActions {
693
720
  // output-dir field to narrow by), so the pre-build check is skipped
694
721
  // then and the post-build normalization performs it instead.
695
722
  const buildProducesFiles = (appSpec.build?.commands ?? []).length > 0;
696
- if (!buildProducesFiles) {
697
- await this.#assertAppReleaseFileReferences(appSpec, manifestPath, run);
698
- }
723
+ const checkedRelease = !buildProducesFiles ? await this.#assertAppReleaseFileReferences(appSpec, manifestPath, run) : undefined;
699
724
  run.setState(step, "succeeded", {
700
725
  manifest_kind: "app",
701
726
  manifest_path: manifestPath,
@@ -707,16 +732,17 @@ export class NodeActions {
707
732
  manifestKind: "app",
708
733
  manifestProjectId: appSpec.project.id,
709
734
  manifestPath,
710
- releaseSpec: null,
735
+ releaseSpec: checkedRelease ?? null,
711
736
  appGraph,
712
737
  appSpec,
713
738
  source,
714
739
  };
715
740
  }
716
741
  }
717
- const loaded = await loadDeployManifest(manifestPath, {
742
+ const loaded = await this.#normalizeWithTypedFileErrors(() => loadDeployManifest(manifestPath, {
718
743
  defaultProject: "prj_up_preflight_placeholder",
719
- });
744
+ validateFiles: true,
745
+ }), run);
720
746
  if (!hasDeployableContent(loaded.spec)) {
721
747
  throw run.error("Deploy manifest contains no deployable sections.", "MANIFEST_EMPTY", { manifest_path: manifestPath });
722
748
  }
@@ -724,13 +750,14 @@ export class NodeActions {
724
750
  run.setState(step, "succeeded", {
725
751
  manifest_kind: "release",
726
752
  manifest_path: manifestPath,
727
- project_id: loaded.spec.project,
753
+ project_id: loaded.spec.project === "prj_up_preflight_placeholder" ? null : loaded.spec.project,
728
754
  idempotency_key: loaded.idempotencyKey ?? null,
729
755
  });
730
756
  return {
731
757
  manifestKind: "release",
732
758
  manifestPath,
733
759
  releaseSpec: loaded.spec,
760
+ loaded,
734
761
  idempotencyKey: loaded.idempotencyKey,
735
762
  manifestProjectId: loaded.spec.project === "prj_up_preflight_placeholder"
736
763
  ? undefined
@@ -807,6 +834,11 @@ export class NodeActions {
807
834
  async #assertManifestFileReferences(spec, manifestPath, run) {
808
835
  try {
809
836
  await assertLocalFileReferencesExist(spec, { manifestPath });
837
+ if (process.env.RUN402_DEPLOY_SKIP_SCAN !== "1") {
838
+ const scan = scanDeploymentSources(spec, dirname(manifestPath));
839
+ if (scan.errors.length)
840
+ throw run.error("Source scan blocked this application. Fix the reported findings, or explicitly override with RUN402_DEPLOY_SKIP_SCAN=1.", "R402_AUTH_PREFLIGHT_FAILED", scan);
841
+ }
810
842
  }
811
843
  catch (err) {
812
844
  throw this.#retypeManifestFileError(err, run);
@@ -854,6 +886,8 @@ export class NodeActions {
854
886
  try {
855
887
  normalized = await normalizeDeployManifest(releaseInput, {
856
888
  project: "prj_up_preflight_placeholder",
889
+ validateFiles: true,
890
+ manifestPath,
857
891
  baseDir: dirname(manifestPath),
858
892
  });
859
893
  }
@@ -864,6 +898,7 @@ export class NodeActions {
864
898
  return;
865
899
  }
866
900
  await this.#assertManifestFileReferences(normalized.spec, manifestPath, run);
901
+ return normalized.spec;
867
902
  }
868
903
  #planAppUpResult(input, manifest, run, opts) {
869
904
  if (!manifest.appGraph) {
@@ -1007,7 +1042,7 @@ export class NodeActions {
1007
1042
  })),
1008
1043
  };
1009
1044
  }
1010
- if (run.executionMode !== "check" && run.executionMode !== "printSpec" && !run.dryRun) {
1045
+ if (run.executionMode !== "check" && run.executionMode !== "printSpec" && run.executionMode !== "printManifest" && !run.dryRun) {
1011
1046
  const spendBlock = spendApprovalBlock(input, run);
1012
1047
  if (spendBlock)
1013
1048
  return spendBlock;
@@ -1033,7 +1068,7 @@ export class NodeActions {
1033
1068
  blockedNodeId: block.nodeId,
1034
1069
  });
1035
1070
  return run.result({
1036
- project_id: input.projectId ?? "prj_planned",
1071
+ project_id: input.projectId ?? null,
1037
1072
  manifest_path: manifest.manifestPath,
1038
1073
  app_graph: manifest.appGraph,
1039
1074
  app_result: blocked,
@@ -1061,7 +1096,7 @@ export class NodeActions {
1061
1096
  blockedNodeId: "project.ensure",
1062
1097
  });
1063
1098
  return run.result({
1064
- project_id: input.projectId ?? "prj_planned",
1099
+ project_id: input.projectId ?? null,
1065
1100
  manifest_path: manifest.manifestPath,
1066
1101
  app_graph: manifest.appGraph,
1067
1102
  app_result: blocked,
@@ -1981,7 +2016,7 @@ export class NodeActions {
1981
2016
  description: "Write workspace project link",
1982
2017
  mutation: true,
1983
2018
  auto: true,
1984
- details: { path, project_id: link.project_id, name: link.name ?? null },
2019
+ details: { path, project_id: run.dryRun && link.project_id === "prj_planned" ? null : link.project_id, name: link.name ?? null },
1985
2020
  });
1986
2021
  await run.approve(step, ["workspace.link.write"], `Write ${shortPath(path)}.`);
1987
2022
  if (run.dryRun) {
@@ -2117,7 +2152,7 @@ class ActionRun {
2117
2152
  return {
2118
2153
  action: this.input.type,
2119
2154
  mode: this.executionMode,
2120
- dry_run: this.dryRun || this.executionMode === "check" || this.executionMode === "printSpec" || this.executionMode === "plan",
2155
+ dry_run: this.dryRun || this.executionMode === "check" || (this.executionMode === "printSpec" || this.executionMode === "printManifest") || this.executionMode === "plan",
2121
2156
  target: this.target,
2122
2157
  steps: this.steps,
2123
2158
  result,
@@ -2175,20 +2210,6 @@ function appPublicOrigin(input, spec) {
2175
2210
  return null;
2176
2211
  return `https://${subdomain}.run402.com`;
2177
2212
  }
2178
- function materializeTemplates(value, values) {
2179
- if (typeof value === "string") {
2180
- return value.replace(/\$\{([^}]+)\}/g, (_match, key) => values[key] ?? "");
2181
- }
2182
- if (Array.isArray(value))
2183
- return value.map((item) => materializeTemplates(item, values));
2184
- if (value && typeof value === "object") {
2185
- return Object.fromEntries(Object.entries(value).map(([key, nested]) => [
2186
- key,
2187
- materializeTemplates(nested, values),
2188
- ]));
2189
- }
2190
- return value;
2191
- }
2192
2213
  function appMailboxSlug(logicalName, spec) {
2193
2214
  return spec.slug ?? logicalName.replace(/_/g, "-");
2194
2215
  }
@@ -2310,6 +2331,9 @@ async function classifyAppVerifyAttempt(input) {
2310
2331
  if (input.attempt.status === input.check.expect.status) {
2311
2332
  return { kind: "success" };
2312
2333
  }
2334
+ if ((input.attempt.status === 400 || input.attempt.status === 429) && input.attempt.edgeCode === null && !input.attempt.edgeHeader) {
2335
+ return { kind: "failed" };
2336
+ }
2313
2337
  const host = hostnameFromUrl(input.url);
2314
2338
  const freshness = host ? freshnessForHost(input.context, host) : { fresh: false, ageSeconds: null };
2315
2339
  if (freshness.fresh &&
@@ -2604,17 +2628,7 @@ function appBuildBlock(input, manifest) {
2604
2628
  return null;
2605
2629
  }
2606
2630
  async function findManifest(workspaceDir) {
2607
- for (const candidate of MANIFEST_CANDIDATES) {
2608
- const path = join(workspaceDir, candidate);
2609
- try {
2610
- await lstat(path);
2611
- return path;
2612
- }
2613
- catch {
2614
- // try next candidate
2615
- }
2616
- }
2617
- return null;
2631
+ return (await resolveApplicationScope({ dir: workspaceDir })).manifest_path;
2618
2632
  }
2619
2633
  const NEARBY_MANIFEST_SCAN_LIMIT = 200;
2620
2634
  /**