@run402/sdk 4.96.0 → 4.98.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.
- package/README.md +5 -5
- package/core-dist/binding-file.d.ts +14 -0
- package/core-dist/binding-file.js +28 -1
- package/core-dist/wallet.js +3 -3
- package/dist/errors.d.ts +25 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +46 -4
- package/dist/errors.js.map +1 -1
- package/dist/grant-key-credentials.d.ts +38 -0
- package/dist/grant-key-credentials.d.ts.map +1 -0
- package/dist/grant-key-credentials.js +42 -0
- package/dist/grant-key-credentials.js.map +1 -0
- package/dist/index.d.ts +25 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -14
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +34 -0
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +15 -1
- package/dist/kernel.js.map +1 -1
- package/dist/namespaces/admin.d.ts.map +1 -1
- package/dist/namespaces/admin.js +8 -0
- package/dist/namespaces/admin.js.map +1 -1
- package/dist/namespaces/agent.d.ts +2 -2
- package/dist/namespaces/agent.d.ts.map +1 -1
- package/dist/namespaces/agent.js +4 -0
- package/dist/namespaces/agent.js.map +1 -1
- package/dist/namespaces/agent.types.d.ts +5 -0
- package/dist/namespaces/agent.types.d.ts.map +1 -1
- package/dist/namespaces/apps.d.ts.map +1 -1
- package/dist/namespaces/apps.js +2 -0
- package/dist/namespaces/apps.js.map +1 -1
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +9 -0
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/namespaces/billing.d.ts +1 -1
- package/dist/namespaces/billing.js +1 -1
- package/dist/namespaces/branches.d.ts.map +1 -1
- package/dist/namespaces/branches.js +2 -0
- package/dist/namespaces/branches.js.map +1 -1
- package/dist/namespaces/buzz.js +1 -1
- package/dist/namespaces/buzz.js.map +1 -1
- package/dist/namespaces/ci.d.ts.map +1 -1
- package/dist/namespaces/ci.js +2 -0
- package/dist/namespaces/ci.js.map +1 -1
- package/dist/namespaces/credentials.d.ts +4 -27
- package/dist/namespaces/credentials.d.ts.map +1 -1
- package/dist/namespaces/credentials.js +33 -4
- package/dist/namespaces/credentials.js.map +1 -1
- package/dist/namespaces/deploy.js +5 -5
- package/dist/namespaces/deploy.js.map +1 -1
- package/dist/namespaces/domains.d.ts +1 -1
- package/dist/namespaces/domains.js +1 -1
- package/dist/namespaces/errors.d.ts +1 -1
- package/dist/namespaces/errors.js +2 -2
- package/dist/namespaces/escalations.d.ts +2 -2
- package/dist/namespaces/escalations.d.ts.map +1 -1
- package/dist/namespaces/escalations.js +4 -2
- package/dist/namespaces/escalations.js.map +1 -1
- package/dist/namespaces/escalations.types.d.ts +3 -3
- package/dist/namespaces/escalations.types.d.ts.map +1 -1
- package/dist/namespaces/events.d.ts +1 -1
- package/dist/namespaces/events.js +1 -1
- package/dist/namespaces/functions.js +1 -1
- package/dist/namespaces/functions.js.map +1 -1
- package/dist/namespaces/grants.d.ts +43 -9
- package/dist/namespaces/grants.d.ts.map +1 -1
- package/dist/namespaces/grants.js +103 -10
- package/dist/namespaces/grants.js.map +1 -1
- package/dist/namespaces/grants.types.d.ts +120 -15
- package/dist/namespaces/grants.types.d.ts.map +1 -1
- package/dist/namespaces/grants.types.js +12 -3
- package/dist/namespaces/grants.types.js.map +1 -1
- package/dist/namespaces/identity-links.types.d.ts +2 -2
- package/dist/namespaces/identity-links.types.d.ts.map +1 -1
- package/dist/namespaces/live.d.ts +0 -16
- package/dist/namespaces/live.d.ts.map +1 -1
- package/dist/namespaces/live.js +19 -0
- package/dist/namespaces/live.js.map +1 -1
- package/dist/namespaces/me.d.ts.map +1 -1
- package/dist/namespaces/me.js +3 -0
- package/dist/namespaces/me.js.map +1 -1
- package/dist/namespaces/org.d.ts +1 -1
- package/dist/namespaces/org.d.ts.map +1 -1
- package/dist/namespaces/org.js +4 -1
- package/dist/namespaces/org.js.map +1 -1
- package/dist/namespaces/projects.d.ts +1 -1
- package/dist/namespaces/projects.d.ts.map +1 -1
- package/dist/namespaces/projects.js +6 -1
- package/dist/namespaces/projects.js.map +1 -1
- package/dist/namespaces/repos.d.ts.map +1 -1
- package/dist/namespaces/repos.js +7 -2
- package/dist/namespaces/repos.js.map +1 -1
- package/dist/namespaces/rooms.d.ts +3 -3
- package/dist/namespaces/rooms.d.ts.map +1 -1
- package/dist/namespaces/rooms.js +6 -3
- package/dist/namespaces/rooms.js.map +1 -1
- package/dist/namespaces/session.d.ts.map +1 -1
- package/dist/namespaces/session.js +18 -0
- package/dist/namespaces/session.js.map +1 -1
- package/dist/namespaces/transfers.d.ts.map +1 -1
- package/dist/namespaces/transfers.js +3 -0
- package/dist/namespaces/transfers.js.map +1 -1
- package/dist/namespaces/wallets.d.ts.map +1 -1
- package/dist/namespaces/wallets.js +2 -0
- package/dist/namespaces/wallets.js.map +1 -1
- package/dist/namespaces/write-approval.d.ts.map +1 -1
- package/dist/namespaces/write-approval.js +3 -0
- package/dist/namespaces/write-approval.js.map +1 -1
- package/dist/node/actions-node.d.ts.map +1 -1
- package/dist/node/actions-node.js +3 -0
- package/dist/node/actions-node.js.map +1 -1
- package/dist/node/buzz-doctor-contract.d.ts +155 -0
- package/dist/node/buzz-doctor-contract.d.ts.map +1 -0
- package/dist/node/buzz-doctor-contract.js +385 -0
- package/dist/node/buzz-doctor-contract.js.map +1 -0
- package/dist/node/buzz-doctor.d.ts +155 -0
- package/dist/node/buzz-doctor.d.ts.map +1 -0
- package/dist/node/buzz-doctor.js +810 -0
- package/dist/node/buzz-doctor.js.map +1 -0
- package/dist/node/credentials.d.ts +18 -13
- package/dist/node/credentials.d.ts.map +1 -1
- package/dist/node/credentials.js +13 -13
- package/dist/node/credentials.js.map +1 -1
- package/dist/node/diagnostics.d.ts +45 -0
- package/dist/node/diagnostics.d.ts.map +1 -0
- package/dist/node/diagnostics.js +67 -0
- package/dist/node/diagnostics.js.map +1 -0
- package/dist/node/doctor.d.ts +101 -0
- package/dist/node/doctor.d.ts.map +1 -0
- package/dist/node/doctor.js +618 -0
- package/dist/node/doctor.js.map +1 -0
- package/dist/node/index.d.ts +61 -9
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +37 -2
- package/dist/node/index.js.map +1 -1
- package/dist/node/init.d.ts +137 -0
- package/dist/node/init.d.ts.map +1 -0
- package/dist/node/init.js +507 -0
- package/dist/node/init.js.map +1 -0
- package/dist/node/lightning-wallet.d.ts +61 -0
- package/dist/node/lightning-wallet.d.ts.map +1 -0
- package/dist/node/lightning-wallet.js +126 -0
- package/dist/node/lightning-wallet.js.map +1 -0
- package/dist/node/local-actions.d.ts +23 -0
- package/dist/node/local-actions.d.ts.map +1 -0
- package/dist/node/local-actions.js +38 -0
- package/dist/node/local-actions.js.map +1 -0
- package/dist/node/org-context.d.ts +169 -0
- package/dist/node/org-context.d.ts.map +1 -0
- package/dist/node/org-context.js +403 -0
- package/dist/node/org-context.js.map +1 -0
- package/dist/node/status.d.ts +62 -0
- package/dist/node/status.d.ts.map +1 -0
- package/dist/node/status.js +132 -0
- package/dist/node/status.js.map +1 -0
- package/dist/node/vault-target.d.ts +49 -0
- package/dist/node/vault-target.d.ts.map +1 -0
- package/dist/node/vault-target.js +109 -0
- package/dist/node/vault-target.js.map +1 -0
- package/dist/node/wallets.d.ts +242 -0
- package/dist/node/wallets.d.ts.map +1 -0
- package/dist/node/wallets.js +484 -0
- package/dist/node/wallets.js.map +1 -0
- package/dist/project-auth-classification.d.ts +8 -8
- package/dist/project-auth-classification.d.ts.map +1 -1
- package/dist/project-auth-classification.js +7 -7
- package/dist/project-auth-classification.js.map +1 -1
- package/dist/scoped.d.ts +6 -14
- package/dist/scoped.d.ts.map +1 -1
- package/dist/scoped.js +10 -21
- package/dist/scoped.js.map +1 -1
- package/dist/secret-gate.d.ts +377 -0
- package/dist/secret-gate.d.ts.map +1 -0
- package/dist/secret-gate.js +273 -0
- package/dist/secret-gate.js.map +1 -0
- package/package.json +2 -1
- package/dist/delegate-credentials.d.ts +0 -37
- package/dist/delegate-credentials.d.ts.map +0 -1
- package/dist/delegate-credentials.js +0 -41
- package/dist/delegate-credentials.js.map +0 -1
- package/dist/namespaces/delegates.d.ts +0 -50
- package/dist/namespaces/delegates.d.ts.map +0 -1
- package/dist/namespaces/delegates.js +0 -99
- package/dist/namespaces/delegates.js.map +0 -1
- package/dist/namespaces/delegates.types.d.ts +0 -88
- package/dist/namespaces/delegates.types.d.ts.map +0 -1
- package/dist/namespaces/delegates.types.js +0 -10
- package/dist/namespaces/delegates.types.js.map +0 -1
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `r.doctor()` — local health and configuration diagnostics, owned by the
|
|
3
|
+
* Node SDK: config directory, wallet, local project keys, API reachability,
|
|
4
|
+
* tier and organization lifecycle, account health, function runtime
|
|
5
|
+
* staleness, recovery posture, the vault, and the scoped source scan.
|
|
6
|
+
*
|
|
7
|
+
* The report is `{ ok, blocking[], warnings[], checks[] }`. `ok` answers ONE
|
|
8
|
+
* question — can this agent ship from here — and is true exactly when
|
|
9
|
+
* `blocking[]` is empty. Every check carries a `severity`:
|
|
10
|
+
* blocking would stop a deploy
|
|
11
|
+
* advisory a gap worth surfacing that never stops a deploy (`warnings[]`)
|
|
12
|
+
* info nothing to act on
|
|
13
|
+
* A status outside the known vocabulary fails closed as blocking.
|
|
14
|
+
*
|
|
15
|
+
* `run402 doctor` is a shim over this; the checks that belong to the calling
|
|
16
|
+
* program itself (whether the installed CLI is current, its resident vault
|
|
17
|
+
* helper) are hooks the caller supplies.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, statSync } from "node:fs";
|
|
20
|
+
import { getConfigDir } from "../../core-dist/config.js";
|
|
21
|
+
import { readLocalWallet } from "./wallets.js";
|
|
22
|
+
import { loadKeyStore } from "../../core-dist/keystore.js";
|
|
23
|
+
import { LocalError } from "../errors.js";
|
|
24
|
+
import { VAULT_BYO_NO_PAYLOAD_COPY_STATEMENT } from "../namespaces/vault.crypto.js";
|
|
25
|
+
import { resolveApplicationScope, loadApplicationScanInput } from "./app-scope.js";
|
|
26
|
+
import { resolveScanRoot, scanDeploymentSources, scanSourceTree, SCAN_SEVERITY } from "./source-scan.js";
|
|
27
|
+
import { resolveVaultTarget } from "./vault-target.js";
|
|
28
|
+
/**
|
|
29
|
+
* The stable, complete registry of ordinary-mode check names, in the order
|
|
30
|
+
* each normally runs: the one list `only` validates against.
|
|
31
|
+
*/
|
|
32
|
+
export const DOCTOR_CHECK_NAMES = [
|
|
33
|
+
"config_dir",
|
|
34
|
+
"cli_update",
|
|
35
|
+
"wallet",
|
|
36
|
+
"projects",
|
|
37
|
+
"api_reachable",
|
|
38
|
+
"tier",
|
|
39
|
+
"account_health",
|
|
40
|
+
"runtime_staleness",
|
|
41
|
+
"recovery_posture",
|
|
42
|
+
"vault",
|
|
43
|
+
"source_scan",
|
|
44
|
+
];
|
|
45
|
+
const INFO_STATUSES = new Set(["ok", "skipped", "unknown"]);
|
|
46
|
+
const TIER_LIFECYCLE_STATUSES = new Set(["past_due", "frozen", "dormant", "purged"]);
|
|
47
|
+
function severityOf(check) {
|
|
48
|
+
if (check.severity === "blocking" || check.severity === "advisory" || check.severity === "info")
|
|
49
|
+
return check.severity;
|
|
50
|
+
if (INFO_STATUSES.has(check.status))
|
|
51
|
+
return "info";
|
|
52
|
+
if (check.status === "warning")
|
|
53
|
+
return "advisory";
|
|
54
|
+
return "blocking";
|
|
55
|
+
}
|
|
56
|
+
/** Stable codes for advisory checks that report ONE finding (no `value.gaps`). */
|
|
57
|
+
const ADVISORY_CODES = {
|
|
58
|
+
cli_update: "CLI_UPDATE_AVAILABLE",
|
|
59
|
+
runtime_staleness: "FUNCTION_RUNTIME_STALE",
|
|
60
|
+
source_scan: "SOURCE_SCAN_WARNINGS",
|
|
61
|
+
tier: "TIER_MISSING_ON_OWN_ORG",
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Fold per-check results into `{ ok, blocking[], warnings[], checks[] }`:
|
|
65
|
+
* one warning per gap string of every advisory check (one for an advisory
|
|
66
|
+
* check with no gaps), one blocking entry per blocking check. Pure.
|
|
67
|
+
*/
|
|
68
|
+
export function buildDoctorReport(rawChecks) {
|
|
69
|
+
const checks = rawChecks.map((check) => {
|
|
70
|
+
const { name, status, severity: _pinned, ...rest } = check;
|
|
71
|
+
return { name, status, severity: severityOf(check), ...rest };
|
|
72
|
+
});
|
|
73
|
+
const blocking = [];
|
|
74
|
+
const warnings = [];
|
|
75
|
+
for (const check of checks) {
|
|
76
|
+
if (check.severity === "blocking") {
|
|
77
|
+
blocking.push({
|
|
78
|
+
check: check.name,
|
|
79
|
+
status: check.status,
|
|
80
|
+
message: check.message ?? `${check.name}: ${check.status}`,
|
|
81
|
+
...(check.hint && { hint: check.hint }),
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (check.severity !== "advisory")
|
|
86
|
+
continue;
|
|
87
|
+
const value = check.value;
|
|
88
|
+
const gaps = Array.isArray(value?.gaps) ? value.gaps.filter((g) => typeof g === "string" && g.length > 0) : [];
|
|
89
|
+
if (gaps.length > 0) {
|
|
90
|
+
for (const gap of gaps)
|
|
91
|
+
warnings.push({ check: check.name, message: gap, ...(check.hint && { hint: check.hint }) });
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
const code = check.code ?? ADVISORY_CODES[check.name];
|
|
95
|
+
const message = check.message ?? check.hint ?? `${check.name}: ${check.status}`;
|
|
96
|
+
warnings.push({
|
|
97
|
+
check: check.name,
|
|
98
|
+
...(code && { code }),
|
|
99
|
+
message,
|
|
100
|
+
...(check.hint && check.hint !== message && { hint: check.hint }),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return { ok: blocking.length === 0, blocking, warnings, checks };
|
|
105
|
+
}
|
|
106
|
+
/** Refuse an `only` name outside {@link DOCTOR_CHECK_NAMES} (`BAD_USAGE`, listing the valid names). */
|
|
107
|
+
export function assertDoctorCheckNames(names) {
|
|
108
|
+
for (const name of names) {
|
|
109
|
+
if (!DOCTOR_CHECK_NAMES.includes(name)) {
|
|
110
|
+
throw new LocalError(`Unknown doctor check: '${name}'.`, "running doctor", {
|
|
111
|
+
code: "BAD_USAGE",
|
|
112
|
+
hint: `Valid check names: ${DOCTOR_CHECK_NAMES.join(", ")}.`,
|
|
113
|
+
details: { check: name, known_checks: [...DOCTOR_CHECK_NAMES] },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function redactWalletForDiagnostics(localWallet) {
|
|
119
|
+
const safe = { ...localWallet };
|
|
120
|
+
delete safe.privateKey;
|
|
121
|
+
if (typeof safe.funded === "boolean")
|
|
122
|
+
safe.faucet_used = safe.funded;
|
|
123
|
+
delete safe.funded;
|
|
124
|
+
return safe;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* A check-failure message, context first: the SDK's `<message> while
|
|
128
|
+
* <context>` reads as two jammed fragments, so the trailing context is cut
|
|
129
|
+
* and the check label leads.
|
|
130
|
+
*/
|
|
131
|
+
function describeCheckFailure(label, err) {
|
|
132
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
133
|
+
const context = typeof err?.context === "string" && (err.context).length > 0
|
|
134
|
+
? err.context
|
|
135
|
+
: null;
|
|
136
|
+
let reason = raw;
|
|
137
|
+
if (context) {
|
|
138
|
+
const marker = ` while ${context}`;
|
|
139
|
+
const idx = raw.indexOf(marker);
|
|
140
|
+
if (idx !== -1)
|
|
141
|
+
reason = (raw.slice(0, idx) + raw.slice(idx + marker.length)).trim();
|
|
142
|
+
}
|
|
143
|
+
return `${label} failed: ${reason}`;
|
|
144
|
+
}
|
|
145
|
+
const message = (err) => (err instanceof Error ? err.message : String(err));
|
|
146
|
+
/** Run the ordinary-mode checks against `r` and fold them into the report. */
|
|
147
|
+
export async function runDoctor(r, opts = {}) {
|
|
148
|
+
const verbose = opts.verbose === true;
|
|
149
|
+
const refresh = opts.refresh === true;
|
|
150
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
151
|
+
const only = new Set(opts.only ?? []);
|
|
152
|
+
assertDoctorCheckNames([...only]);
|
|
153
|
+
const wanted = (name) => only.size === 0 || only.has(name);
|
|
154
|
+
const checks = [];
|
|
155
|
+
const CONFIG_DIR = getConfigDir();
|
|
156
|
+
// 1. Config directory.
|
|
157
|
+
if (wanted("config_dir"))
|
|
158
|
+
try {
|
|
159
|
+
if (existsSync(CONFIG_DIR) && statSync(CONFIG_DIR).isDirectory()) {
|
|
160
|
+
checks.push({ name: "config_dir", status: "ok", value: CONFIG_DIR });
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
checks.push({ name: "config_dir", status: "missing", value: CONFIG_DIR, hint: "Run 'run402 init' to set up the config directory." });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
checks.push({ name: "config_dir", status: "error", message: message(err) });
|
|
168
|
+
}
|
|
169
|
+
// 1b. The calling program's update state: advisory, never hides the rest.
|
|
170
|
+
if (wanted("cli_update")) {
|
|
171
|
+
if (opts.cliUpdate) {
|
|
172
|
+
try {
|
|
173
|
+
checks.push(await opts.cliUpdate({ refresh }));
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
checks.push({ name: "cli_update", status: "unknown", message: message(err) });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
checks.push({ name: "cli_update", status: "skipped", message: "No CLI update check on this surface." });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// 2. Wallet.
|
|
184
|
+
let walletConfigured = false;
|
|
185
|
+
if (wanted("wallet"))
|
|
186
|
+
try {
|
|
187
|
+
const localWallet = readLocalWallet();
|
|
188
|
+
if (localWallet) {
|
|
189
|
+
walletConfigured = true;
|
|
190
|
+
checks.push({
|
|
191
|
+
name: "wallet",
|
|
192
|
+
status: "ok",
|
|
193
|
+
value: {
|
|
194
|
+
rail: localWallet.rail,
|
|
195
|
+
// Never keystore secrets, even verbose.
|
|
196
|
+
...(verbose && { details: redactWalletForDiagnostics(localWallet) }),
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
checks.push({ name: "wallet", status: "missing", hint: "Run 'run402 init' to create a wallet." });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
// A wallet file that exists but is unreadable stops the run: nothing
|
|
206
|
+
// after it can be trusted to describe this machine.
|
|
207
|
+
if (err?.code === "BAD_WALLET_FILE")
|
|
208
|
+
throw err;
|
|
209
|
+
checks.push({ name: "wallet", status: "error", message: message(err) });
|
|
210
|
+
}
|
|
211
|
+
// 3. Local project keys (the wallet itself is check 2). Empty is normal.
|
|
212
|
+
if (wanted("projects"))
|
|
213
|
+
try {
|
|
214
|
+
const keystore = loadKeyStore();
|
|
215
|
+
const projectCount = Object.keys(keystore?.projects ?? {}).length;
|
|
216
|
+
checks.push({
|
|
217
|
+
name: "projects",
|
|
218
|
+
status: "ok",
|
|
219
|
+
value: { project_count: projectCount },
|
|
220
|
+
...(projectCount === 0 && {
|
|
221
|
+
hint: walletConfigured
|
|
222
|
+
? "No projects yet — run 'run402 projects provision' to create one (wallet is already set up)."
|
|
223
|
+
: "No projects yet — run 'run402 init' to set up the wallet first, then 'run402 projects provision'.",
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
checks.push({ name: "projects", status: "error", message: message(err) });
|
|
229
|
+
}
|
|
230
|
+
// 4. API reachability (read-only, unauthenticated).
|
|
231
|
+
if (wanted("api_reachable"))
|
|
232
|
+
try {
|
|
233
|
+
const t0 = Date.now();
|
|
234
|
+
await r.service.status();
|
|
235
|
+
const elapsed = Date.now() - t0;
|
|
236
|
+
checks.push({ name: "api_reachable", status: "ok", ...(verbose && { value: { elapsed_ms: elapsed } }) });
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
checks.push({
|
|
240
|
+
name: "api_reachable",
|
|
241
|
+
status: "error",
|
|
242
|
+
message: message(err),
|
|
243
|
+
hint: "Check the RUN402_API_BASE env var and your network connection.",
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// 5. Tier. `status` is a FIXED vocabulary — ok | inactive | frozen |
|
|
247
|
+
// past_due | dormant | purged | missing | unknown | error — never a tier
|
|
248
|
+
// name; the name and raw lifecycle ride in value.
|
|
249
|
+
if (wanted("tier"))
|
|
250
|
+
try {
|
|
251
|
+
const tier = (await r.tier.status());
|
|
252
|
+
const tierName = typeof tier?.tier === "string" && tier.tier.length > 0 ? tier.tier : null;
|
|
253
|
+
const lifecycle = typeof tier?.organization_lifecycle_state === "string" && tier.organization_lifecycle_state.length > 0
|
|
254
|
+
? tier.organization_lifecycle_state
|
|
255
|
+
: null;
|
|
256
|
+
const active = tier?.active === true;
|
|
257
|
+
// A wallet whose own org holds no tier can still ship into another org's
|
|
258
|
+
// projects, so "missing" blocks only when there is nowhere to ship.
|
|
259
|
+
const reachableProjects = Array.isArray(tier?.projects) ? tier.projects.length : 0;
|
|
260
|
+
let status;
|
|
261
|
+
if (lifecycle !== null && lifecycle !== "active") {
|
|
262
|
+
status = TIER_LIFECYCLE_STATUSES.has(lifecycle) ? lifecycle : "inactive";
|
|
263
|
+
}
|
|
264
|
+
else if (tierName === null) {
|
|
265
|
+
status = "missing";
|
|
266
|
+
}
|
|
267
|
+
else if (!active) {
|
|
268
|
+
status = "inactive";
|
|
269
|
+
}
|
|
270
|
+
else if (lifecycle === null) {
|
|
271
|
+
status = "unknown";
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
status = "ok";
|
|
275
|
+
}
|
|
276
|
+
const value = {
|
|
277
|
+
tier: tierName,
|
|
278
|
+
lifecycle,
|
|
279
|
+
active,
|
|
280
|
+
organization_lifecycle_state: lifecycle,
|
|
281
|
+
lease_expires_at: tier?.lease_perpetual === true ? null : tier?.lease_expires_at ?? null,
|
|
282
|
+
reachable_projects: reachableProjects,
|
|
283
|
+
};
|
|
284
|
+
if (status === "ok") {
|
|
285
|
+
checks.push({ name: "tier", status, value });
|
|
286
|
+
}
|
|
287
|
+
else if (status === "missing" && reachableProjects > 0) {
|
|
288
|
+
checks.push({
|
|
289
|
+
name: "tier",
|
|
290
|
+
status,
|
|
291
|
+
severity: "advisory",
|
|
292
|
+
value,
|
|
293
|
+
message: `this wallet's own organization holds no tier, but it can reach ${reachableProjects} project(s) owned by another organization (membership or grant) — deploys to those are unaffected`,
|
|
294
|
+
hint: "Run 'run402 tier set prototype' only if you want to provision projects under this wallet's own organization.",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
checks.push({
|
|
299
|
+
name: "tier",
|
|
300
|
+
status,
|
|
301
|
+
value,
|
|
302
|
+
message: status === "unknown"
|
|
303
|
+
? "tier resolved, but the organization lifecycle could not be determined"
|
|
304
|
+
: status === "missing"
|
|
305
|
+
? "no tier on this wallet's organization and no reachable project — nothing can be provisioned or deployed from here"
|
|
306
|
+
: status === "inactive"
|
|
307
|
+
? `tier '${tierName ?? "(none)"}' is not active${lifecycle && lifecycle !== "active" ? ` (organization lifecycle '${lifecycle}')` : ""}`
|
|
308
|
+
: `organization lifecycle is '${lifecycle}' — the control plane is gated until the tier is reactivated`,
|
|
309
|
+
hint: status === "unknown"
|
|
310
|
+
? "Tier resolved, but organization lifecycle could not be determined. Check `run402 tier status` before assuming the account is healthy."
|
|
311
|
+
: "Run 'run402 tier set prototype' to set, renew, or reactivate the tier.",
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
checks.push({ name: "tier", status: "error", message: describeCheckFailure("tier status check", err) });
|
|
317
|
+
}
|
|
318
|
+
// 6. Account health, runtime staleness, and recovery posture all ride ONE
|
|
319
|
+
// `GET /agent/v1/me/status` read, skipped entirely when none is wanted.
|
|
320
|
+
if (wanted("account_health") || wanted("runtime_staleness") || wanted("recovery_posture"))
|
|
321
|
+
try {
|
|
322
|
+
const status = (await r.me.status());
|
|
323
|
+
const gaps = [];
|
|
324
|
+
if (status.contact.email_status !== "verified") {
|
|
325
|
+
const ch = status.email_verification?.last_challenge;
|
|
326
|
+
if (ch && ch.hint) {
|
|
327
|
+
const attemptsLine = ch.attempt_count > 0
|
|
328
|
+
? ` (${ch.attempt_count}/${ch.attempt_count + ch.remaining_attempts} attempts used, ${ch.remaining_attempts} remaining)`
|
|
329
|
+
: "";
|
|
330
|
+
gaps.push(`contact email not verified${attemptsLine}: ${ch.hint}`);
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
gaps.push(`contact email not verified (${status.contact.email_status}) — run 'run402 agent contact --email ...' then reply to the challenge`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (status.contact.passkey_status !== "verified") {
|
|
337
|
+
gaps.push("contact passkey not bound — run 'run402 login' (or 'run402 agent passkey enroll') after email verification");
|
|
338
|
+
}
|
|
339
|
+
const reach = status.reachability;
|
|
340
|
+
if (reach && reach.reachable === false) {
|
|
341
|
+
const skipped = reach.skipped_last_90d > 0
|
|
342
|
+
? ` (${reach.skipped_last_90d} notification(s) already skipped in the last 90 days)`
|
|
343
|
+
: "";
|
|
344
|
+
gaps.push(`no verified notification recipient — mandatory recovery/security notifications currently reach nobody${skipped}; run 'run402 agent contact --email ...' then reply to the challenge`);
|
|
345
|
+
}
|
|
346
|
+
if (Array.isArray(status.skipped_notifications) && status.skipped_notifications.length > 0) {
|
|
347
|
+
gaps.push(`${status.skipped_notifications.length} notification(s) skipped due to missing verified recipient`);
|
|
348
|
+
}
|
|
349
|
+
if (Array.isArray(status.critical_items) && status.critical_items.length > 0) {
|
|
350
|
+
for (const item of status.critical_items)
|
|
351
|
+
gaps.push(`${item.kind}: ${item.detail}`);
|
|
352
|
+
}
|
|
353
|
+
if (wanted("account_health")) {
|
|
354
|
+
if (gaps.length > 0) {
|
|
355
|
+
checks.push({
|
|
356
|
+
name: "account_health",
|
|
357
|
+
status: "warning",
|
|
358
|
+
value: { gaps },
|
|
359
|
+
hint: "Address the above gaps; they're what 'run402 notifications' is designed to surface.",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
checks.push({ name: "account_health", status: "ok" });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// 6b. Function runtime staleness: a redeploy with unchanged source does
|
|
367
|
+
// not refresh the platform wrapper; refreshing is opt-in.
|
|
368
|
+
if (wanted("runtime_staleness")) {
|
|
369
|
+
const runtime = status.runtime;
|
|
370
|
+
if (runtime && typeof runtime.stale_function_count === "number") {
|
|
371
|
+
if (runtime.stale_function_count > 0) {
|
|
372
|
+
checks.push({
|
|
373
|
+
name: "runtime_staleness",
|
|
374
|
+
status: "warning",
|
|
375
|
+
value: { stale_function_count: runtime.stale_function_count, stale_functions: runtime.stale_functions ?? [] },
|
|
376
|
+
hint: `${runtime.stale_function_count} function(s) are running an older platform runtime. Run 'run402 functions rebuild --all' to refresh (re-bundles from your stored source; no source change).`,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
checks.push({ name: "runtime_staleness", status: "ok", value: { stale_function_count: 0 } });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
checks.push({
|
|
385
|
+
name: "runtime_staleness",
|
|
386
|
+
status: "skipped",
|
|
387
|
+
...(verbose && { hint: "account status has no 'runtime' block; requires v1.69+ gateway." }),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// 6c. Org recovery posture: evidence levels, never guarantees.
|
|
392
|
+
if (wanted("recovery_posture")) {
|
|
393
|
+
const posture = status.recovery_posture;
|
|
394
|
+
if (!Array.isArray(posture)) {
|
|
395
|
+
checks.push({
|
|
396
|
+
name: "recovery_posture",
|
|
397
|
+
status: "skipped",
|
|
398
|
+
...(verbose && { hint: "account status has no 'recovery_posture' block; requires a vault-recovery-custody gateway." }),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
else if (posture.length === 0) {
|
|
402
|
+
checks.push({ name: "recovery_posture", status: "ok", value: { orgs: [] } });
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
const postureGaps = [];
|
|
406
|
+
for (const org of posture) {
|
|
407
|
+
const label = `org ${org.org_id} (${org.vault_count} vault${org.vault_count === 1 ? "" : "s"})`;
|
|
408
|
+
if (org.control_plane_configured === false) {
|
|
409
|
+
postureGaps.push(`${label}: no human owner with a working control-plane login — if this org's agent machine dies, nobody can sign in to recover it. Invite a backup human (run402 orgs invite create ${org.org_id} --email <their-email> --role owner) and have them complete login at console.run402.com.`);
|
|
410
|
+
}
|
|
411
|
+
if (org.source_backup_configured === false) {
|
|
412
|
+
postureGaps.push(`${label}: no human member holds a working source-access key — vault history has no member-side decryption backup. Have a member complete source enrollment at console.run402.com/account → Source access.`);
|
|
413
|
+
}
|
|
414
|
+
if (org.custody_legacy_present === true) {
|
|
415
|
+
postureGaps.push(`${label}: a member key is still on single-credential legacy custody (one passkey, no recovery code — losing that one credential loses source access). Re-enroll at console.run402.com/account to move to wrapper custody with a recovery code.`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
checks.push(postureGaps.length > 0
|
|
419
|
+
? {
|
|
420
|
+
name: "recovery_posture",
|
|
421
|
+
status: "warning",
|
|
422
|
+
value: { orgs: posture, gaps: postureGaps },
|
|
423
|
+
hint: "These are the org's disaster-recovery backstops — the same facts arrive as org_recovery_posture_degraded/_recovered feed events. After enrolling, export the recovery bundle (run402 repos recovery-bundle) and store it separately from the code.",
|
|
424
|
+
}
|
|
425
|
+
: { name: "recovery_posture", status: "ok", value: { orgs: posture } });
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
// An unreachable account status is a soft skip for all three checks it feeds.
|
|
431
|
+
if (wanted("account_health"))
|
|
432
|
+
checks.push({
|
|
433
|
+
name: "account_health",
|
|
434
|
+
status: "skipped",
|
|
435
|
+
message: describeCheckFailure("account status check", err),
|
|
436
|
+
...(verbose && { hint: "GET /agent/v1/me/status not reachable." }),
|
|
437
|
+
});
|
|
438
|
+
if (wanted("runtime_staleness"))
|
|
439
|
+
checks.push({ name: "runtime_staleness", status: "skipped", message: describeCheckFailure("account status check", err) });
|
|
440
|
+
if (wanted("recovery_posture"))
|
|
441
|
+
checks.push({ name: "recovery_posture", status: "skipped", message: describeCheckFailure("account status check", err) });
|
|
442
|
+
}
|
|
443
|
+
// 7. The vault: policy, whether THIS machine can produce the capture a
|
|
444
|
+
// `required` policy demands, open override journals, mirror currency, and
|
|
445
|
+
// where the keystore lives. Read-only and best-effort in every branch.
|
|
446
|
+
if (wanted("vault")) {
|
|
447
|
+
const daemonInfo = opts.daemonStatus ? await opts.daemonStatus().catch(() => ({ running: false })) : undefined;
|
|
448
|
+
const target = await resolveVaultTarget(r.repos, { repoDir: cwd, explicitProjectId: opts.project ?? undefined, ...(opts.warn ? { warn: opts.warn } : {}) });
|
|
449
|
+
const projectId = target.project_id ?? null;
|
|
450
|
+
const repoId = target.repo_id ?? null;
|
|
451
|
+
if (!projectId && !repoId) {
|
|
452
|
+
checks.push({
|
|
453
|
+
name: "vault",
|
|
454
|
+
status: "skipped",
|
|
455
|
+
...(verbose && { hint: "no active project — run 'run402 projects use <project_id>' to check its vault." }),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
try {
|
|
460
|
+
const gv = (await r.repos.status({
|
|
461
|
+
...(repoId ? { repo_id: repoId } : { project_id: projectId }),
|
|
462
|
+
repo_dir: cwd,
|
|
463
|
+
}));
|
|
464
|
+
const value = {
|
|
465
|
+
project_id: gv.project_id ?? projectId,
|
|
466
|
+
repo_id: gv.repo_id,
|
|
467
|
+
vault: gv.vault === null ? null : "allocated",
|
|
468
|
+
storage_profile: gv.vault?.storage_profile ?? null,
|
|
469
|
+
byo_destination: gv.vault?.byo_destination ?? null,
|
|
470
|
+
vault_policy: gv.vault_policy,
|
|
471
|
+
keystore_root: gv.keystore.root,
|
|
472
|
+
can_sign: gv.keystore.can_sign,
|
|
473
|
+
holds_repo_key: gv.keystore.holds_repo_key,
|
|
474
|
+
pending_overrides: gv.pending_overrides,
|
|
475
|
+
pins: gv.pins,
|
|
476
|
+
remote: gv.remote,
|
|
477
|
+
// null/<=1: the single-principal terminal-loss statement is honest;
|
|
478
|
+
// >=2: a second covering recipient is proven.
|
|
479
|
+
covering_recipients: gv.covering_recipients ?? null,
|
|
480
|
+
...(daemonInfo !== undefined ? { daemon: daemonInfo } : {}),
|
|
481
|
+
// This machine's standing on the chain-verified writer set; the
|
|
482
|
+
// read-only terminal state outranks it.
|
|
483
|
+
writer: gv.vault === null
|
|
484
|
+
? null
|
|
485
|
+
: gv.vault.read_only_terminal
|
|
486
|
+
? "read_only_vault"
|
|
487
|
+
: gv.vault.writer_set?.writers.some((w) => w.writer_key_id === gv.keystore.identity_fingerprint)
|
|
488
|
+
? "active"
|
|
489
|
+
: gv.vault.pending_writers?.some((p) => p.writer_key_id === gv.keystore.identity_fingerprint)
|
|
490
|
+
? "pending"
|
|
491
|
+
: "not_admitted",
|
|
492
|
+
};
|
|
493
|
+
const gaps = [];
|
|
494
|
+
if (value.writer === "read_only_vault") {
|
|
495
|
+
gaps.push("this vault has lost its last writer (D228 read-only terminal) — it still serves reads, but no push can be admitted until a new writer is admitted through a recovery path");
|
|
496
|
+
}
|
|
497
|
+
else if (value.writer === "pending") {
|
|
498
|
+
gaps.push("this machine's key is an eligible writer candidate but not yet admitted — run 'run402 repos access sync' if you already hold writer standing on this vault, or ask a current writer to run any vault operation");
|
|
499
|
+
}
|
|
500
|
+
else if (value.writer === "not_admitted") {
|
|
501
|
+
gaps.push("this machine's key is not an active writer on this vault — a push from here is refused VAULT_WRITER_NOT_ADMITTED; ask a current writer to admit you (org membership at role developer+ and a published signing key make you eligible)");
|
|
502
|
+
}
|
|
503
|
+
if (gv.vault_policy === "required" && !gv.keystore.holds_repo_key) {
|
|
504
|
+
gaps.push("vault_policy is 'required' but this machine holds no key for the vault — a deploy from here is refused with VAULT_CLIENT_UPGRADE_REQUIRED. " +
|
|
505
|
+
"Run 'run402 repos create --project <project_id>' (idempotent; resolves to the existing repo), or 'run402 repos policy grandfathered --reason <why>' to un-gate the project.");
|
|
506
|
+
}
|
|
507
|
+
else if (gv.vault_policy === "required" && !gv.keystore.can_sign) {
|
|
508
|
+
gaps.push("vault_policy is 'required' and this keystore is read-only (no signing key) — it can verify but cannot publish the capture a deploy needs");
|
|
509
|
+
}
|
|
510
|
+
if (gv.pending_overrides > 0) {
|
|
511
|
+
gaps.push(`${gv.pending_overrides} unvaulted-override journal(s) are still open — run 'run402 repos capture' to drain them`);
|
|
512
|
+
}
|
|
513
|
+
// `matches` is tri-state: only `false` is a mismatch.
|
|
514
|
+
if (gv.remote && gv.remote.matches === false) {
|
|
515
|
+
gaps.push(`the '${gv.remote.name}' git remote points at a different project than ${value.project_id} (${gv.remote.url})`);
|
|
516
|
+
}
|
|
517
|
+
for (const w of gv.warnings ?? [])
|
|
518
|
+
gaps.push(`${w.kind}: ${w.message}`);
|
|
519
|
+
// Mirror currency, alongside (never in place of) the deploy gaps; only
|
|
520
|
+
// STALE becomes a gap.
|
|
521
|
+
if (gv.vault !== null && value.repo_id) {
|
|
522
|
+
try {
|
|
523
|
+
const mirrorStatus = (await r.repos.mirrorStatus({ repo_id: value.repo_id, is_byo: value.storage_profile === "byo" }));
|
|
524
|
+
value.vault_mirror = {
|
|
525
|
+
configured: mirrorStatus.configured,
|
|
526
|
+
destination: mirrorStatus.destination,
|
|
527
|
+
mirrored_generation: mirrorStatus.mirrored_generation,
|
|
528
|
+
newest_generation: mirrorStatus.newest_generation,
|
|
529
|
+
is_current: mirrorStatus.is_current,
|
|
530
|
+
last_success_at: mirrorStatus.last_success_at,
|
|
531
|
+
finding: mirrorStatus.finding,
|
|
532
|
+
validity_not_freshness: mirrorStatus.validity_not_freshness,
|
|
533
|
+
keystore_still_required: mirrorStatus.keystore_still_required,
|
|
534
|
+
};
|
|
535
|
+
if (mirrorStatus.is_current === false) {
|
|
536
|
+
gaps.push(`the ciphertext mirror at ${mirrorStatus.destination} is STALE (mirrored generation ${mirrorStatus.mirrored_generation ?? "(none)"}, vault newest ${mirrorStatus.newest_generation ?? "(none)"}) — ${mirrorStatus.closing_command}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
// A mirror read failing is never a doctor failure.
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const byoDisclosure = value.storage_profile === "byo" ? ` Storage: byo (${value.byo_destination ?? "(unknown)"}) — ${VAULT_BYO_NO_PAYLOAD_COPY_STATEMENT}` : "";
|
|
544
|
+
checks.push({
|
|
545
|
+
name: "vault",
|
|
546
|
+
status: gaps.length > 0 ? "warning" : "ok",
|
|
547
|
+
value: gaps.length > 0 ? { ...value, gaps } : value,
|
|
548
|
+
hint: (gv.vault === null
|
|
549
|
+
? `No vault for this project (that is a normal shape). Allocate one with 'run402 repos create --project <project_id>'. Keystore: ${gv.keystore.root}`
|
|
550
|
+
: gv.durability_statement
|
|
551
|
+
? `Back up ${gv.keystore.root} anyway — ${gv.durability_statement} (covering_recipients: ${gv.covering_recipients})`
|
|
552
|
+
: `Back up ${gv.keystore.root} — whole-machine or whole-keystore loss is terminal for vault history.`) + byoDisclosure,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
catch (err) {
|
|
556
|
+
// A gateway without vaults, an unreachable API, or an unreadable
|
|
557
|
+
// project is not a local health problem.
|
|
558
|
+
checks.push({ name: "vault", status: "skipped", message: describeCheckFailure("vault status check", err) });
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// 8. Source scan, scoped to the selected application. Error findings block
|
|
563
|
+
// deploy; an explicit arbitrary scan directory is advisory only.
|
|
564
|
+
if (!opts.noScan && wanted("source_scan")) {
|
|
565
|
+
const scanDirOverride = opts.scanDir ?? null;
|
|
566
|
+
try {
|
|
567
|
+
const scope = await resolveApplicationScope({ ...(opts.cwd ? { cwd: opts.cwd } : {}), dir: opts.dir ?? undefined, manifest: opts.manifest ?? undefined });
|
|
568
|
+
if (!scanDirOverride && !scope.selected) {
|
|
569
|
+
checks.push({ name: "source_scan", status: "skipped", value: { scope: "unscoped", app_root: scope.app_root }, message: "No application selected. Run doctor --dir <app> or --manifest <path>; sibling applications are not deployment blockers." });
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
const scanRoot = scanDirOverride ?? resolveScanRoot(scope.app_root);
|
|
573
|
+
const scanContext = { scope: scanDirOverride ? "explicit_scan_directory" : "application", app_root: scope.app_root, manifest_path: scope.manifest_path };
|
|
574
|
+
let findings;
|
|
575
|
+
if (scanDirOverride)
|
|
576
|
+
findings = scanSourceTree(scanRoot, { cwd: scope.app_root });
|
|
577
|
+
else {
|
|
578
|
+
const selected = await loadApplicationScanInput(scope.manifest_path);
|
|
579
|
+
scanContext.build_outputs = selected.build_deferred ? "deferred_until_build" : "not_deferred";
|
|
580
|
+
findings = scanDeploymentSources(selected.spec, scope.app_root).findings;
|
|
581
|
+
}
|
|
582
|
+
const errorFindings = findings.filter((f) => f.severity === SCAN_SEVERITY.ERROR);
|
|
583
|
+
const warnFindings = findings.filter((f) => f.severity === SCAN_SEVERITY.WARN);
|
|
584
|
+
if (findings.length === 0) {
|
|
585
|
+
checks.push({ name: "source_scan", status: "ok", value: { ...scanContext, scan_root: scanRoot, file_count_with_findings: 0 } });
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
checks.push({
|
|
589
|
+
name: "source_scan",
|
|
590
|
+
status: errorFindings.length > 0 ? "error" : "warning",
|
|
591
|
+
...(scanDirOverride ? { severity: "advisory" } : {}),
|
|
592
|
+
value: {
|
|
593
|
+
...scanContext,
|
|
594
|
+
scan_root: scanRoot,
|
|
595
|
+
findings: errorFindings.length + warnFindings.length,
|
|
596
|
+
errors: errorFindings.length,
|
|
597
|
+
warnings: warnFindings.length,
|
|
598
|
+
details: findings,
|
|
599
|
+
},
|
|
600
|
+
hint: errorFindings.length > 0
|
|
601
|
+
? scanDirOverride ? "Findings are from the explicit arbitrary scan directory; they do not establish that an application deploy will be refused." : "Fix the findings in this application. The same scoped source scan gates up and deploy."
|
|
602
|
+
: "Source scan emitted warnings (non-blocking). Review and address when convenient.",
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch (err) {
|
|
608
|
+
checks.push({
|
|
609
|
+
name: "source_scan",
|
|
610
|
+
status: "error",
|
|
611
|
+
value: { code: err?.code ?? "APPLICATION_SCAN_FAILED", details: err?.details ?? null },
|
|
612
|
+
message: message(err),
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return buildDoctorReport(checks);
|
|
617
|
+
}
|
|
618
|
+
//# sourceMappingURL=doctor.js.map
|