run402-mcp 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/deploy-diagnose-url.d.ts.map +1 -1
- package/dist/tools/deploy-diagnose-url.js +10 -0
- package/dist/tools/deploy-diagnose-url.js.map +1 -1
- package/dist/tools/deploy.d.ts +62 -10
- package/dist/tools/deploy.d.ts.map +1 -1
- package/dist/tools/deploy.js +12 -2
- package/dist/tools/deploy.js.map +1 -1
- package/package.json +1 -1
- package/schemas/release-spec.v1.json +14 -3
- package/schemas/run402-app.v1.schema.json +56 -1
- package/sdk/README.md +4 -1
- package/sdk/dist/actions.d.ts +7 -1
- package/sdk/dist/actions.d.ts.map +1 -1
- package/sdk/dist/app-up.d.ts +14 -3
- package/sdk/dist/app-up.d.ts.map +1 -1
- package/sdk/dist/app-up.js +1 -0
- package/sdk/dist/app-up.js.map +1 -1
- package/sdk/dist/config.d.ts +22 -5
- package/sdk/dist/config.d.ts.map +1 -1
- package/sdk/dist/config.js +6 -1
- package/sdk/dist/config.js.map +1 -1
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +59 -8
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +43 -5
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.types.js +24 -2
- package/sdk/dist/namespaces/deploy.types.js.map +1 -1
- package/sdk/dist/node/actions-node.d.ts.map +1 -1
- package/sdk/dist/node/actions-node.js +460 -26
- package/sdk/dist/node/actions-node.js.map +1 -1
- package/sdk/dist/node/deploy-manifest.d.ts +14 -2
- package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
- package/sdk/dist/node/deploy-manifest.js +45 -8
- package/sdk/dist/node/deploy-manifest.js.map +1 -1
|
@@ -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
|
|
670
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
1001
|
-
|
|
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: {
|
|
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,
|
|
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
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
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
|
-
|
|
1198
|
+
attempt += 1;
|
|
1199
|
+
if (attempt < retries) {
|
|
1035
1200
|
await delay(1_000);
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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: {
|
|
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
|
-
|
|
1049
|
-
|
|
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 {
|
|
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])
|