run402-mcp 4.0.1 → 4.0.3

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 (63) hide show
  1. package/README.md +24 -4
  2. package/dist/index.js +13 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/tools/deploy-diagnose-url.d.ts.map +1 -1
  5. package/dist/tools/deploy-diagnose-url.js +10 -0
  6. package/dist/tools/deploy-diagnose-url.js.map +1 -1
  7. package/dist/tools/deploy-rehearse.d.ts +20 -0
  8. package/dist/tools/deploy-rehearse.d.ts.map +1 -0
  9. package/dist/tools/deploy-rehearse.js +36 -0
  10. package/dist/tools/deploy-rehearse.js.map +1 -0
  11. package/dist/tools/project-branches.d.ts +50 -0
  12. package/dist/tools/project-branches.d.ts.map +1 -0
  13. package/dist/tools/project-branches.js +84 -0
  14. package/dist/tools/project-branches.js.map +1 -0
  15. package/dist/tools/project-snapshots.d.ts +56 -0
  16. package/dist/tools/project-snapshots.d.ts.map +1 -0
  17. package/dist/tools/project-snapshots.js +100 -0
  18. package/dist/tools/project-snapshots.js.map +1 -0
  19. package/package.json +1 -1
  20. package/sdk/README.md +50 -5
  21. package/sdk/dist/actions.d.ts +7 -1
  22. package/sdk/dist/actions.d.ts.map +1 -1
  23. package/sdk/dist/app-up.d.ts +14 -3
  24. package/sdk/dist/app-up.d.ts.map +1 -1
  25. package/sdk/dist/app-up.js +1 -0
  26. package/sdk/dist/app-up.js.map +1 -1
  27. package/sdk/dist/credentials.d.ts +1 -1
  28. package/sdk/dist/credentials.d.ts.map +1 -1
  29. package/sdk/dist/index.d.ts +8 -0
  30. package/sdk/dist/index.d.ts.map +1 -1
  31. package/sdk/dist/index.js +8 -0
  32. package/sdk/dist/index.js.map +1 -1
  33. package/sdk/dist/namespaces/branches.d.ts +11 -0
  34. package/sdk/dist/namespaces/branches.d.ts.map +1 -0
  35. package/sdk/dist/namespaces/branches.js +92 -0
  36. package/sdk/dist/namespaces/branches.js.map +1 -0
  37. package/sdk/dist/namespaces/branches.types.d.ts +48 -0
  38. package/sdk/dist/namespaces/branches.types.d.ts.map +1 -0
  39. package/sdk/dist/namespaces/branches.types.js +2 -0
  40. package/sdk/dist/namespaces/branches.types.js.map +1 -0
  41. package/sdk/dist/namespaces/deploy.d.ts +2 -1
  42. package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
  43. package/sdk/dist/namespaces/deploy.js +34 -0
  44. package/sdk/dist/namespaces/deploy.js.map +1 -1
  45. package/sdk/dist/namespaces/deploy.types.d.ts +98 -0
  46. package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
  47. package/sdk/dist/namespaces/deploy.types.js +24 -2
  48. package/sdk/dist/namespaces/deploy.types.js.map +1 -1
  49. package/sdk/dist/namespaces/snapshots.d.ts +13 -0
  50. package/sdk/dist/namespaces/snapshots.d.ts.map +1 -0
  51. package/sdk/dist/namespaces/snapshots.js +115 -0
  52. package/sdk/dist/namespaces/snapshots.js.map +1 -0
  53. package/sdk/dist/namespaces/snapshots.types.d.ts +79 -0
  54. package/sdk/dist/namespaces/snapshots.types.d.ts.map +1 -0
  55. package/sdk/dist/namespaces/snapshots.types.js +2 -0
  56. package/sdk/dist/namespaces/snapshots.types.js.map +1 -0
  57. package/sdk/dist/node/actions-node.d.ts.map +1 -1
  58. package/sdk/dist/node/actions-node.js +460 -26
  59. package/sdk/dist/node/actions-node.js.map +1 -1
  60. package/sdk/dist/scoped.d.ts +28 -1
  61. package/sdk/dist/scoped.d.ts.map +1 -1
  62. package/sdk/dist/scoped.js +54 -0
  63. package/sdk/dist/scoped.js.map +1 -1
@@ -0,0 +1,79 @@
1
+ export type ProjectSnapshotKind = "manual" | "pre_migration" | "pre_restore" | "scheduled";
2
+ export type ProjectSnapshotProfile = "snapshot";
3
+ export type ProjectSnapshotStatus = "running" | "ready" | "failed" | "expired";
4
+ export interface SnapshotNextAction {
5
+ type: string;
6
+ command?: string;
7
+ message: string;
8
+ [key: string]: unknown;
9
+ }
10
+ export interface ProjectSnapshotDto {
11
+ snapshot_id: string;
12
+ operation_id: string;
13
+ project_id: string;
14
+ kind: ProjectSnapshotKind | (string & {});
15
+ profile: ProjectSnapshotProfile | (string & {});
16
+ status: ProjectSnapshotStatus | (string & {});
17
+ manifest_sha256: string | null;
18
+ size_bytes: number;
19
+ live_release_id: string | null;
20
+ captured_at: string | null;
21
+ expires_at: string | null;
22
+ error: unknown | null;
23
+ created_at: string;
24
+ updated_at: string;
25
+ next_actions: SnapshotNextAction[];
26
+ }
27
+ export interface ProjectSnapshotsListOptions {
28
+ limit?: number;
29
+ after?: string;
30
+ kind?: ProjectSnapshotKind | (string & {});
31
+ }
32
+ export interface ProjectSnapshotsListResult {
33
+ snapshots: ProjectSnapshotDto[];
34
+ has_more: boolean;
35
+ next_cursor: string | null;
36
+ }
37
+ export interface SnapshotRestoreOptions {
38
+ includeAuth?: boolean;
39
+ }
40
+ export interface SnapshotRestorePlanEnvelope {
41
+ restore_plan: SnapshotRestorePlan;
42
+ }
43
+ export interface SnapshotRestorePlan {
44
+ snapshot_id: string;
45
+ project_id: string;
46
+ snapshot_at: string;
47
+ data_loss_statement: string;
48
+ auth: {
49
+ mode: "not_restored" | "restore_on_confirm" | (string & {});
50
+ users: number;
51
+ passkeys: number;
52
+ message: string;
53
+ };
54
+ release: {
55
+ snapshot_live_release_id: string | null;
56
+ current_live_release_id: string | null;
57
+ };
58
+ target: {
59
+ current_schema_slot: string;
60
+ behavior: "offline_materialize_then_atomic_flip" | (string & {});
61
+ };
62
+ confirm: {
63
+ token: string;
64
+ expires_at: string;
65
+ };
66
+ next_actions: SnapshotNextAction[];
67
+ }
68
+ export interface SnapshotRestoreResult {
69
+ operation_id: string;
70
+ project_id: string;
71
+ snapshot_id: string;
72
+ pre_restore_snapshot_id: string;
73
+ old_schema_slot: string;
74
+ new_schema_slot: string;
75
+ migration_registry_rows: number;
76
+ status: "ready" | (string & {});
77
+ next_actions: SnapshotNextAction[];
78
+ }
79
+ //# sourceMappingURL=snapshots.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshots.types.d.ts","sourceRoot":"","sources":["../../src/namespaces/snapshots.types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=snapshots.types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshots.types.js","sourceRoot":"","sources":["../../src/namespaces/snapshots.types.ts"],"names":[],"mappings":""}
@@ -1 +1 @@
1
- {"version":3,"file":"actions-node.d.ts","sourceRoot":"","sources":["../../src/node/actions-node.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAIV,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EAGb,kCAAkC,EAClC,wBAAwB,EACxB,mBAAmB,EACnB,cAAc,EACf,MAAM,eAAe,CAAC;AAiBvB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,OAAO,KAAK,EAAkB,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,KAAK,EAAY,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGrE,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AA8GD;;;GAGG;AACH,qBAAa,WAAY,YAAW,aAAa;;IAE7C,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,kBAAuB;IAG1C,GAAG,CACP,KAAK,EAAE,kCAAkC,EACzC,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAC;IACzC,GAAG,CACP,KAAK,EAAE,wBAAwB,EAC/B,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;IACvC,GAAG,CACP,KAAK,EAAE,mBAAmB,EAC1B,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;IAmCxC,EAAE,CACN,KAAK,GAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,CAAM,EAC7C,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;CAq/C/C"}
1
+ {"version":3,"file":"actions-node.d.ts","sourceRoot":"","sources":["../../src/node/actions-node.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAIV,kBAAkB,EAClB,sBAAsB,EACtB,aAAa,EAGb,kCAAkC,EAClC,wBAAwB,EACxB,mBAAmB,EACnB,cAAc,EACf,MAAM,eAAe,CAAC;AAmBvB,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAS1C,OAAO,KAAK,EAAkB,eAAe,EAAE,MAAM,iCAAiC,CAAC;AACvF,OAAO,KAAK,EAAY,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGrE,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,oBAAoB,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAiKD;;;GAGG;AACH,qBAAa,WAAY,YAAW,aAAa;;IAE7C,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,kBAAuB;IAG1C,GAAG,CACP,KAAK,EAAE,kCAAkC,EACzC,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAC;IACzC,GAAG,CACP,KAAK,EAAE,wBAAwB,EAC/B,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,aAAa,CAAC,CAAC;IACvC,GAAG,CACP,KAAK,EAAE,mBAAmB,EAC1B,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;IAmCxC,EAAE,CACN,KAAK,GAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,CAAM,EAC7C,IAAI,CAAC,EAAE,sBAAsB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,cAAc,CAAC,CAAC;CAkvD/C"}
@@ -55,6 +55,14 @@ const TIER_RANK = {
55
55
  hobby: 2,
56
56
  team: 3,
57
57
  };
58
+ const DEFAULT_PROPAGATION_BUDGET_SECONDS = 120;
59
+ const EDGE_PROPAGATION_FRESH_WINDOW_MS = 300_000;
60
+ const EDGE_PROPAGATION_TYPICAL_SECONDS = 60;
61
+ const VERIFY_SENTINEL_CODES = new Set([
62
+ "SUBDOMAIN_NOT_CONFIGURED",
63
+ "HOST_NOT_CONFIGURED",
64
+ "NO_SITE_DEPLOYED",
65
+ ]);
58
66
  const execFileAsync = promisify(execFile);
59
67
  /**
60
68
  * Node implementation of the action runner. The public CLI should treat this
@@ -168,6 +176,9 @@ export class NodeActions {
168
176
  const workspaceDir = source.workspaceDir;
169
177
  const manifest = await this.#discoverAndValidateManifest(input, workspaceDir, source.metadata, run);
170
178
  if (manifest.manifestKind === "app") {
179
+ if (input.verifyOnly) {
180
+ return this.#verifyAppManifestOnly(input, manifest, workspaceDir, run, startedAt);
181
+ }
171
182
  const block = this.#firstAppUpBlock(input, manifest, run);
172
183
  const appResult = this.#planAppUpResult(input, manifest, run, {
173
184
  startedAt,
@@ -496,6 +507,72 @@ export class NodeActions {
496
507
  blockedNodeId: block.nodeId,
497
508
  });
498
509
  }
510
+ async #verifyAppManifestOnly(input, manifest, workspaceDir, run, startedAt) {
511
+ if (!manifest.appSpec || !manifest.appGraph) {
512
+ throw run.error("Internal error: app manifest did not produce an install graph.", "RUN402_ACTION_INTERNAL");
513
+ }
514
+ if ((manifest.appSpec.verify?.http ?? []).length === 0) {
515
+ throw run.error("App manifest does not define verify.http checks.", "VERIFY_CHECKS_REQUIRED", { manifest_path: manifest.manifestPath });
516
+ }
517
+ const resolved = await this.#resolveProjectForVerify(input, manifest, workspaceDir, run);
518
+ const projectKeys = await this.sdk.projects.keys(resolved.projectId);
519
+ const publicOrigin = appPublicOrigin(input, manifest.appSpec) ?? projectKeys.site_url ?? null;
520
+ const scoped = await this.sdk.project(resolved.projectId);
521
+ const verification = await this.#verifyAppHttp(manifest.appSpec, publicOrigin, run, {
522
+ projectId: resolved.projectId,
523
+ claimedHosts: new Set(),
524
+ bindings: [],
525
+ propagationBudgetMs: propagationBudgetMs(input),
526
+ propagationWait: input.propagationWait !== false,
527
+ resolve: (opts) => scoped.apply.resolve(opts),
528
+ });
529
+ markAppGraphNodesForVerify(manifest.appGraph, verification);
530
+ const appStatus = verification.ok
531
+ ? "succeeded"
532
+ : verification.propagationPending
533
+ ? "propagation_pending"
534
+ : "deployed_unverified";
535
+ const appResult = createRun402AppUpResult({
536
+ graph: manifest.appGraph,
537
+ manifest_path: manifest.manifestPath,
538
+ status: appStatus,
539
+ started_at: startedAt,
540
+ dry_run: false,
541
+ project_id: resolved.projectId,
542
+ project_name: input.name ?? manifest.appSpec.project.name ?? resolved.link?.name ?? null,
543
+ public_origin: publicOrigin,
544
+ diagnostics: verification.diagnostics,
545
+ next_actions: verification.nextAction ? [verification.nextAction] : [],
546
+ verify: appVerifyResult(verification),
547
+ approval_policy: {
548
+ yes: run.approval === "yes",
549
+ allow_prune: input.allowPrune === true,
550
+ max_spend_usd: input.maxSpendUsd ?? null,
551
+ build_mode: input.buildMode ?? manifest.appSpec.build?.mode ?? null,
552
+ shell_build_approved: input.allowShellBuild === true,
553
+ },
554
+ });
555
+ for (const check of appResult.verification.http) {
556
+ const actual = verification.results.get(check.id);
557
+ if (actual !== undefined) {
558
+ check.actual_status = actual.status;
559
+ check.propagation_wait_ms = actual.propagationWaitMs;
560
+ if (actual.diagnostic)
561
+ check.diagnostic = actual.diagnostic;
562
+ check.status = actual.ok
563
+ ? "succeeded"
564
+ : actual.propagationPending
565
+ ? "propagation_pending"
566
+ : "failed";
567
+ }
568
+ }
569
+ return run.result({
570
+ project_id: resolved.projectId,
571
+ manifest_path: manifest.manifestPath,
572
+ app_graph: manifest.appGraph,
573
+ app_result: appResult,
574
+ });
575
+ }
499
576
  #firstAppUpBlock(input, manifest, run) {
500
577
  const nameBlock = missingRequiredName(input, manifest);
501
578
  if (nameBlock)
@@ -666,12 +743,25 @@ export class NodeActions {
666
743
  });
667
744
  const webhooks = await this.#ensureAppWebhooks(resolved.projectId, manifest.appSpec, resources, run);
668
745
  resources.webhooks = webhooks;
669
- const verification = await this.#verifyAppHttp(manifest.appSpec, publicOrigin, run);
670
- markAppGraphNodes(manifest.appGraph, verification.ok ? "succeeded" : "failed");
746
+ const verifyContext = {
747
+ projectId: resolved.projectId,
748
+ claimedHosts: claimedHostsFromRelease(normalized.spec, publicOrigin),
749
+ bindings: deploy.subdomain_bindings ?? [],
750
+ propagationBudgetMs: propagationBudgetMs(input),
751
+ propagationWait: input.propagationWait !== false,
752
+ resolve: (opts) => scoped.apply.resolve(opts),
753
+ };
754
+ const verification = await this.#verifyAppHttp(manifest.appSpec, publicOrigin, run, verifyContext);
755
+ markAppGraphNodesForVerify(manifest.appGraph, verification);
756
+ const appStatus = verification.ok
757
+ ? "succeeded"
758
+ : verification.propagationPending
759
+ ? "propagation_pending"
760
+ : "deployed_unverified";
671
761
  const appResult = createRun402AppUpResult({
672
762
  graph: manifest.appGraph,
673
763
  manifest_path: manifest.manifestPath,
674
- status: verification.ok ? "succeeded" : "deployed_unverified",
764
+ status: appStatus,
675
765
  started_at: startedAt,
676
766
  dry_run: false,
677
767
  project_id: resolved.projectId,
@@ -679,6 +769,8 @@ export class NodeActions {
679
769
  public_origin: publicOrigin,
680
770
  operation_id: deploy.operation_id ?? null,
681
771
  diagnostics: verification.diagnostics,
772
+ next_actions: verification.nextAction ? [verification.nextAction] : [],
773
+ verify: appVerifyResult(verification),
682
774
  approval_policy: {
683
775
  yes: run.approval === "yes",
684
776
  allow_prune: input.allowPrune === true,
@@ -694,8 +786,15 @@ export class NodeActions {
694
786
  for (const check of appResult.verification.http) {
695
787
  const actual = verification.results.get(check.id);
696
788
  if (actual !== undefined) {
697
- check.actual_status = actual;
698
- check.status = actual === check.expected_status ? "succeeded" : "failed";
789
+ check.actual_status = actual.status;
790
+ check.propagation_wait_ms = actual.propagationWaitMs;
791
+ if (actual.diagnostic)
792
+ check.diagnostic = actual.diagnostic;
793
+ check.status = actual.ok
794
+ ? "succeeded"
795
+ : actual.propagationPending
796
+ ? "propagation_pending"
797
+ : "failed";
699
798
  }
700
799
  }
701
800
  await this.#recordAppInstallState({
@@ -993,24 +1092,39 @@ export class NodeActions {
993
1092
  run.setState(step, "succeeded", { webhooks });
994
1093
  return webhooks;
995
1094
  }
996
- async #verifyAppHttp(spec, publicOrigin, run) {
1095
+ async #verifyAppHttp(spec, publicOrigin, run, context) {
997
1096
  const checks = spec.verify?.http ?? [];
998
1097
  const results = new Map();
999
1098
  const diagnostics = [];
1000
- if (checks.length === 0)
1001
- return { ok: true, results, diagnostics };
1099
+ const warnings = [];
1100
+ let nextAction = null;
1101
+ let propagationWaitMs = 0;
1102
+ if (checks.length === 0) {
1103
+ return { ok: true, propagationPending: false, results, diagnostics, warnings, nextAction, propagationWaitMs };
1104
+ }
1002
1105
  const step = run.addStep({
1003
1106
  action: "app.verify",
1004
1107
  description: "Verify app HTTP checks",
1005
1108
  mutation: false,
1006
1109
  auto: true,
1007
- details: { count: checks.length },
1110
+ details: {
1111
+ count: checks.length,
1112
+ propagation_budget_s: Math.floor(context.propagationBudgetMs / 1000),
1113
+ propagation_wait: context.propagationWait,
1114
+ },
1008
1115
  });
1009
1116
  run.setState(step, "running");
1117
+ const verifyStarted = Date.now();
1010
1118
  for (const check of checks) {
1011
1119
  const url = check.url ?? (publicOrigin && check.path ? new URL(check.path, publicOrigin).toString() : null);
1012
1120
  if (!url) {
1013
- results.set(check.id, null);
1121
+ results.set(check.id, {
1122
+ id: check.id,
1123
+ ok: false,
1124
+ propagationPending: false,
1125
+ status: null,
1126
+ propagationWaitMs: 0,
1127
+ });
1014
1128
  diagnostics.push({
1015
1129
  code: "VERIFY_FAILED",
1016
1130
  severity: "error",
@@ -1021,34 +1135,111 @@ export class NodeActions {
1021
1135
  }
1022
1136
  const retries = Math.max(1, check.retries ?? 1);
1023
1137
  let status = null;
1024
- for (let attempt = 0; attempt < retries; attempt++) {
1025
- try {
1026
- const res = await fetch(url, { method: "GET" });
1027
- status = res.status;
1028
- if (status === check.expect.status)
1029
- break;
1138
+ let attempt = 0;
1139
+ let propagationAttempts = 0;
1140
+ let checkPropagationWaitMs = 0;
1141
+ let pendingDiagnosis = null;
1142
+ while (true) {
1143
+ const observed = await fetchAppVerifyUrl(url);
1144
+ status = observed.status;
1145
+ const classification = await classifyAppVerifyAttempt({
1146
+ attempt: observed,
1147
+ check,
1148
+ url,
1149
+ context,
1150
+ });
1151
+ if (classification.kind === "success") {
1152
+ results.set(check.id, {
1153
+ id: check.id,
1154
+ ok: true,
1155
+ propagationPending: false,
1156
+ status,
1157
+ propagationWaitMs: checkPropagationWaitMs,
1158
+ });
1159
+ break;
1030
1160
  }
1031
- catch {
1032
- status = null;
1161
+ if (classification.kind === "propagating") {
1162
+ propagationAttempts += 1;
1163
+ pendingDiagnosis = classification.diagnosis ?? null;
1164
+ const elapsed = Date.now() - verifyStarted;
1165
+ const remaining = Math.max(0, context.propagationBudgetMs - elapsed);
1166
+ run.setState(step, "running", {
1167
+ verify_event: "propagating",
1168
+ check_id: check.id,
1169
+ url,
1170
+ reason: classification.reason ?? "edge_propagation",
1171
+ binding_age_s: classification.bindingAgeSeconds ?? null,
1172
+ typical_s: EDGE_PROPAGATION_TYPICAL_SECONDS,
1173
+ budget_remaining_s: Math.ceil(remaining / 1000),
1174
+ propagation_wait_ms: propagationWaitMs,
1175
+ edge_propagation: classification.edge ?? pendingDiagnosis?.edge_propagation ?? null,
1176
+ });
1177
+ if (!context.propagationWait || remaining <= 0) {
1178
+ const warning = propagationWarning(check.id, url, classification, remaining);
1179
+ warnings.push(warning);
1180
+ diagnostics.push(warning);
1181
+ nextAction ??= verifyNextAction();
1182
+ results.set(check.id, {
1183
+ id: check.id,
1184
+ ok: false,
1185
+ propagationPending: true,
1186
+ status,
1187
+ propagationWaitMs: checkPropagationWaitMs,
1188
+ ...(pendingDiagnosis ? { diagnostic: { edge_propagation: pendingDiagnosis.edge_propagation ?? null, resolve: pendingDiagnosis } } : {}),
1189
+ });
1190
+ break;
1191
+ }
1192
+ const sleepMs = Math.min(remaining, propagationBackoffMs(propagationAttempts));
1193
+ await delay(sleepMs);
1194
+ propagationWaitMs += sleepMs;
1195
+ checkPropagationWaitMs += sleepMs;
1196
+ continue;
1033
1197
  }
1034
- if (attempt + 1 < retries)
1198
+ attempt += 1;
1199
+ if (attempt < retries) {
1035
1200
  await delay(1_000);
1036
- }
1037
- results.set(check.id, status);
1038
- if (status !== check.expect.status) {
1201
+ continue;
1202
+ }
1203
+ const diagnosis = classification.diagnosis ?? await diagnoseAppVerifyFailure(context, url, check);
1039
1204
  diagnostics.push({
1040
1205
  code: "VERIFY_FAILED",
1041
1206
  severity: "error",
1042
1207
  node_id: `verify.http.${check.id}`,
1043
1208
  message: `HTTP verification ${check.id} expected ${check.expect.status}, got ${status ?? "network_error"}.`,
1044
- details: { url, expected_status: check.expect.status, actual_status: status },
1209
+ details: {
1210
+ url,
1211
+ expected_status: check.expect.status,
1212
+ actual_status: status,
1213
+ ...(observed.error ? { error: observed.error } : {}),
1214
+ ...(diagnosis ? { resolve: diagnosis, edge_propagation: diagnosis.edge_propagation ?? null } : {}),
1215
+ },
1045
1216
  });
1217
+ results.set(check.id, {
1218
+ id: check.id,
1219
+ ok: false,
1220
+ propagationPending: false,
1221
+ status,
1222
+ propagationWaitMs: checkPropagationWaitMs,
1223
+ ...(diagnosis ? { diagnostic: { edge_propagation: diagnosis.edge_propagation ?? null, resolve: diagnosis } } : {}),
1224
+ });
1225
+ break;
1046
1226
  }
1047
1227
  }
1048
- run.setState(step, diagnostics.length === 0 ? "succeeded" : "failed", {
1049
- checks: Object.fromEntries(results),
1228
+ const propagationPending = [...results.values()].some((result) => result.propagationPending);
1229
+ const failed = [...results.values()].some((result) => !result.ok && !result.propagationPending);
1230
+ run.setState(step, failed ? "failed" : propagationPending ? "propagation_pending" : "succeeded", {
1231
+ checks: Object.fromEntries([...results].map(([id, result]) => [id, result.status])),
1232
+ propagation_wait_ms: propagationWaitMs,
1050
1233
  });
1051
- return { ok: diagnostics.length === 0, results, diagnostics };
1234
+ return {
1235
+ ok: !failed && !propagationPending,
1236
+ propagationPending,
1237
+ results,
1238
+ diagnostics,
1239
+ warnings,
1240
+ nextAction,
1241
+ propagationWaitMs,
1242
+ };
1052
1243
  }
1053
1244
  async #ensureAllowance(run, opts = { fund: false }) {
1054
1245
  const status = await this.sdk.allowance.status();
@@ -1339,6 +1530,51 @@ export class NodeActions {
1339
1530
  }
1340
1531
  throw run.error("No project is configured for this workspace. Pass --project, add project_id to the manifest, or pass --name to create one.", "RUN402_PROJECT_REQUIRED", { link_path: linkPath, manifest_path: manifest.manifestPath });
1341
1532
  }
1533
+ async #resolveProjectForVerify(input, manifest, workspaceDir, run) {
1534
+ const linkPath = join(workspaceDir, ".run402", "project.json");
1535
+ const link = await readWorkspaceProjectLink(linkPath);
1536
+ const step = run.addStep({
1537
+ action: "project.resolve",
1538
+ description: "Resolve project for app verification",
1539
+ mutation: false,
1540
+ auto: true,
1541
+ details: {
1542
+ explicit_project_id: input.projectId ?? null,
1543
+ linked_project_id: link?.project_id ?? null,
1544
+ manifest_project_id: manifest.manifestProjectId ?? manifest.appSpec?.project.id ?? null,
1545
+ verify_only: true,
1546
+ },
1547
+ });
1548
+ run.setState(step, "running");
1549
+ const projectId = input.projectId ?? link?.project_id ?? manifest.manifestProjectId ?? manifest.appSpec?.project.id ?? await this.sdk.projects.active();
1550
+ if (!projectId) {
1551
+ throw run.error("`run402 up verify` needs an existing project. Pass --project, keep .run402/project.json, add project.id to run402.json, or select an active project.", "RUN402_PROJECT_REQUIRED", { link_path: linkPath, manifest_path: manifest.manifestPath, verify_only: true });
1552
+ }
1553
+ run.setState(step, "succeeded", {
1554
+ project_id: projectId,
1555
+ source: input.projectId
1556
+ ? "explicit"
1557
+ : link?.project_id
1558
+ ? "workspace_link"
1559
+ : manifest.manifestProjectId ?? manifest.appSpec?.project.id
1560
+ ? "manifest"
1561
+ : "active",
1562
+ link_path: linkPath,
1563
+ });
1564
+ return {
1565
+ projectId,
1566
+ source: input.projectId
1567
+ ? "explicit"
1568
+ : link?.project_id
1569
+ ? "workspace_link"
1570
+ : manifest.manifestProjectId ?? manifest.appSpec?.project.id
1571
+ ? "manifest"
1572
+ : "active",
1573
+ link,
1574
+ linkPath,
1575
+ shouldWriteLink: false,
1576
+ };
1577
+ }
1342
1578
  async #findNameCollision(name, orgId) {
1343
1579
  const result = await this.sdk.projects.list(orgId ? { org: orgId } : {});
1344
1580
  const normalized = name.trim().toLowerCase();
@@ -1582,6 +1818,204 @@ function markAppGraphNodes(graph, status) {
1582
1818
  node.status = status;
1583
1819
  }
1584
1820
  }
1821
+ function markAppGraphNodesForVerify(graph, verification) {
1822
+ const terminal = verification.ok || verification.propagationPending ? "succeeded" : "failed";
1823
+ markAppGraphNodes(graph, terminal);
1824
+ for (const node of graph.nodes) {
1825
+ if (node.kind !== "verify.http")
1826
+ continue;
1827
+ const id = node.id.replace(/^verify\.http\./, "");
1828
+ const result = verification.results.get(id);
1829
+ if (!result)
1830
+ continue;
1831
+ node.status = result.ok
1832
+ ? "succeeded"
1833
+ : result.propagationPending
1834
+ ? "propagation_pending"
1835
+ : "failed";
1836
+ }
1837
+ }
1838
+ function appVerifyResult(verification) {
1839
+ return {
1840
+ status: verification.ok
1841
+ ? "verified"
1842
+ : verification.propagationPending
1843
+ ? "propagation_pending"
1844
+ : "failed",
1845
+ warnings: verification.warnings,
1846
+ next_action: verification.nextAction,
1847
+ propagation_wait_ms: verification.propagationWaitMs,
1848
+ };
1849
+ }
1850
+ function propagationBudgetMs(input) {
1851
+ const seconds = input.propagationBudgetSeconds ?? DEFAULT_PROPAGATION_BUDGET_SECONDS;
1852
+ if (!Number.isFinite(seconds) || seconds < 0)
1853
+ return DEFAULT_PROPAGATION_BUDGET_SECONDS * 1000;
1854
+ return Math.floor(seconds * 1000);
1855
+ }
1856
+ async function fetchAppVerifyUrl(url) {
1857
+ try {
1858
+ const res = await fetch(url, { method: "GET" });
1859
+ const text = await res.text().catch(() => "");
1860
+ return {
1861
+ status: res.status,
1862
+ edgeCode: edgeCodeFromBody(text),
1863
+ edgeHeader: res.headers.get("x-run402-edge"),
1864
+ error: null,
1865
+ };
1866
+ }
1867
+ catch (err) {
1868
+ return {
1869
+ status: null,
1870
+ edgeCode: null,
1871
+ edgeHeader: null,
1872
+ error: err instanceof Error ? err.message : String(err),
1873
+ };
1874
+ }
1875
+ }
1876
+ function edgeCodeFromBody(text) {
1877
+ if (!text || text.length > 4096)
1878
+ return null;
1879
+ try {
1880
+ const parsed = JSON.parse(text);
1881
+ const code = typeof parsed.code === "string"
1882
+ ? parsed.code
1883
+ : typeof parsed.error?.code === "string"
1884
+ ? parsed.error.code
1885
+ : null;
1886
+ return code && VERIFY_SENTINEL_CODES.has(code) ? code : null;
1887
+ }
1888
+ catch {
1889
+ return null;
1890
+ }
1891
+ }
1892
+ async function classifyAppVerifyAttempt(input) {
1893
+ if (input.attempt.status === input.check.expect.status) {
1894
+ return { kind: "success" };
1895
+ }
1896
+ const host = hostnameFromUrl(input.url);
1897
+ const freshness = host ? freshnessForHost(input.context, host) : { fresh: false, ageSeconds: null };
1898
+ if (freshness.fresh &&
1899
+ (input.attempt.edgeCode !== null ||
1900
+ input.attempt.edgeHeader === "kvs-miss" ||
1901
+ input.attempt.edgeHeader === "kv-miss")) {
1902
+ return {
1903
+ kind: "propagating",
1904
+ reason: input.attempt.edgeCode ?? input.attempt.edgeHeader ?? "edge_miss",
1905
+ bindingAgeSeconds: freshness.ageSeconds,
1906
+ };
1907
+ }
1908
+ const diagnosis = await diagnoseAppVerifyFailure(input.context, input.url, input.check);
1909
+ const edge = diagnosis?.edge_propagation ?? null;
1910
+ if (edge && edge.status !== "settled") {
1911
+ return {
1912
+ kind: "propagating",
1913
+ reason: edge.status,
1914
+ bindingAgeSeconds: bindingAgeSeconds(edge.claimed_at),
1915
+ edge,
1916
+ diagnosis,
1917
+ };
1918
+ }
1919
+ return { kind: "failed", diagnosis };
1920
+ }
1921
+ async function diagnoseAppVerifyFailure(context, url, check) {
1922
+ if (!context.resolve)
1923
+ return null;
1924
+ try {
1925
+ return await context.resolve({
1926
+ project: context.projectId,
1927
+ url,
1928
+ method: "GET",
1929
+ });
1930
+ }
1931
+ catch {
1932
+ try {
1933
+ return await context.resolve({
1934
+ project: context.projectId,
1935
+ url,
1936
+ method: String(check.expect.status) === "405" ? "POST" : "GET",
1937
+ });
1938
+ }
1939
+ catch {
1940
+ return null;
1941
+ }
1942
+ }
1943
+ }
1944
+ function propagationWarning(checkId, url, classification, remainingMs) {
1945
+ const edge = classification.edge;
1946
+ const visibleBy = edge?.expected_visible_by ? ` Expected visible by ${edge.expected_visible_by}.` : "";
1947
+ const reason = classification.reason ?? edge?.status ?? "edge_propagation";
1948
+ return {
1949
+ code: "VERIFY_PROPAGATION_PENDING",
1950
+ severity: "warning",
1951
+ node_id: `verify.http.${checkId}`,
1952
+ message: `HTTP verification ${checkId} is waiting on edge propagation for ${url} (${reason}).${visibleBy}`,
1953
+ details: {
1954
+ url,
1955
+ reason,
1956
+ binding_age_s: classification.bindingAgeSeconds ?? null,
1957
+ typical_s: EDGE_PROPAGATION_TYPICAL_SECONDS,
1958
+ budget_remaining_s: Math.ceil(Math.max(0, remainingMs) / 1000),
1959
+ edge_propagation: edge ?? null,
1960
+ },
1961
+ };
1962
+ }
1963
+ function verifyNextAction() {
1964
+ return {
1965
+ type: "retry_verify",
1966
+ code: "VERIFY_PROPAGATION_PENDING",
1967
+ message: "Rerun app verification after edge propagation settles.",
1968
+ command: "run402 up verify",
1969
+ argv: ["run402", "up", "verify"],
1970
+ };
1971
+ }
1972
+ function propagationBackoffMs(attempt) {
1973
+ const base = Math.min(10_000, 1_000 * 2 ** Math.min(4, attempt - 1));
1974
+ const jitter = Math.floor(Math.random() * 250);
1975
+ return base + jitter;
1976
+ }
1977
+ function hostnameFromUrl(url) {
1978
+ try {
1979
+ return new URL(url).hostname.toLowerCase();
1980
+ }
1981
+ catch {
1982
+ return null;
1983
+ }
1984
+ }
1985
+ function freshnessForHost(context, host) {
1986
+ const normalized = host.toLowerCase();
1987
+ const binding = context.bindings.find((candidate) => candidate.host.toLowerCase() === normalized);
1988
+ const ageSeconds = binding ? bindingAgeSeconds(binding.claimed_at) : null;
1989
+ if (context.claimedHosts.has(normalized))
1990
+ return { fresh: true, ageSeconds };
1991
+ if (ageSeconds === null)
1992
+ return { fresh: false, ageSeconds: null };
1993
+ return { fresh: ageSeconds * 1000 < EDGE_PROPAGATION_FRESH_WINDOW_MS, ageSeconds };
1994
+ }
1995
+ function bindingAgeSeconds(claimedAtIso) {
1996
+ if (!claimedAtIso)
1997
+ return null;
1998
+ const ms = Date.parse(claimedAtIso);
1999
+ if (!Number.isFinite(ms))
2000
+ return null;
2001
+ return Math.max(0, Math.floor((Date.now() - ms) / 1000));
2002
+ }
2003
+ function claimedHostsFromRelease(spec, publicOrigin) {
2004
+ const hosts = new Set();
2005
+ const subdomains = spec.subdomains;
2006
+ for (const value of [...arrayOfStrings(subdomains?.set), ...arrayOfStrings(subdomains?.add)]) {
2007
+ hosts.add(`${value}.run402.com`.toLowerCase());
2008
+ }
2009
+ if (publicOrigin) {
2010
+ const host = hostnameFromUrl(publicOrigin);
2011
+ if (host)
2012
+ hosts.add(host);
2013
+ }
2014
+ return hosts;
2015
+ }
2016
+ function arrayOfStrings(value) {
2017
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
2018
+ }
1585
2019
  function applyResourceStateToAppResult(appResult, resources) {
1586
2020
  for (const [name, mailbox] of Object.entries(resources.mailboxes)) {
1587
2021
  if (!appResult.resources.mailboxes[name])