@codeyam-editor/codeyam-editor 0.1.9 → 0.1.11-staging.g493e6ea
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/npm/cloud.js +84 -19
- package/npm/fleet-add-failure.js +156 -11
- package/npm/fleet-auth-account.js +61 -0
- package/npm/fleet-auth.js +109 -1
- package/npm/fleet-public-url.js +81 -12
- package/npm/fleet-reconcile.js +75 -14
- package/npm/fleet-setup-steps.js +27 -2
- package/npm/fleet-vm-readiness.js +27 -0
- package/npm/group-bounded-spawn.js +111 -0
- package/npm/launch-config.js +201 -33
- package/npm/orphan-ssh-reaper.js +81 -0
- package/npm/provisioning-contract.js +148 -0
- package/npm/scenario-check.js +7 -4
- package/npm/scenario-playwright.js +107 -6
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-C7I9wctz.js → ContainerInspector-BzuV4Xjv.js} +1 -1
- package/ui/dist/assets/EditorShell-BZgNmI00.js +330 -0
- package/ui/dist/assets/EditorShell-lndJDLqT.css +1 -0
- package/ui/dist/assets/ScenarioDataPanel-BQYKnGYU.js +21 -0
- package/ui/dist/assets/{ScenarioDataPanelFullPage-DT0TZC4M.js → ScenarioDataPanelFullPage-zOmqEQhg.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-CEbJsKhi.js +21 -0
- package/ui/dist/assets/{index-iMR5siiG.js → index-BhXDJg0i.js} +2 -2
- package/ui/dist/assets/index-DvkmhQJ8.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/EditorShell-9aYMeOdZ.js +0 -330
- package/ui/dist/assets/EditorShell-CE8iG_EM.css +0 -1
- package/ui/dist/assets/ScenarioDataPanel-CLwkrex0.js +0 -18
- package/ui/dist/assets/ScenarioRenderer-Bsarhcrl.js +0 -21
- package/ui/dist/assets/index-BRoAenc1.css +0 -1
package/npm/cloud.js
CHANGED
|
@@ -23,6 +23,11 @@ const {
|
|
|
23
23
|
killChildProcess,
|
|
24
24
|
} = require("./utils");
|
|
25
25
|
const { open } = require("./open");
|
|
26
|
+
// Bounded gcloud/ssh spawns run through spawnGroupBoundedSync so a timeout takes
|
|
27
|
+
// gcloud's ssh descendant down with it instead of orphaning it to PID 1; the
|
|
28
|
+
// reaper sweeps sessions already leaked before that existed.
|
|
29
|
+
const { spawnGroupBoundedSync } = require("./group-bounded-spawn");
|
|
30
|
+
const { reapOrphanedFleetSsh } = require("./orphan-ssh-reaper");
|
|
26
31
|
// `FORWARD_ENV_KEYS_ENV` is the env-var name (`CY_FORWARD_ENV_KEYS`) the
|
|
27
32
|
// fleet dashboard sets on `cloud:up`'s child to name the subset of
|
|
28
33
|
// `process.env` keys that came from the operator-supplied launch form.
|
|
@@ -110,6 +115,9 @@ const { addToRoster, readRoster } = require("./fleet-roster");
|
|
|
110
115
|
// container) apart from a real `docker compose up -d` failure, so the abort path
|
|
111
116
|
// can converge instead of escalating to instance deletion.
|
|
112
117
|
const { classifyComposeUpFailure } = require("./compose-conflict");
|
|
118
|
+
// The two streamed bootstrap scripts, named once for this file's STREAMED_SCRIPTS
|
|
119
|
+
// and for the advance's provisioning pre-flight, which checks the same set.
|
|
120
|
+
const { BOOTSTRAP_SCRIPT, PROJECT_BOOTSTRAP_SCRIPT } = require("./provisioning-contract");
|
|
113
121
|
|
|
114
122
|
// =============================================================================
|
|
115
123
|
// Pure helpers — every export here is unit-tested in cloud.test.js without
|
|
@@ -1090,6 +1098,10 @@ function buildGcloudArgs(verb, cfg = {}, extras = {}) {
|
|
|
1090
1098
|
// reference one value. 2 min is generous for any single describe/update/SSH probe
|
|
1091
1099
|
// yet comfortably under the 600000 ms outer reconcile budget.
|
|
1092
1100
|
//
|
|
1101
|
+
// All three spawn through spawnGroupBoundedSync, because a bare spawnSync timeout
|
|
1102
|
+
// signals only the direct child: gcloud died and its `ssh` survived, re-parented
|
|
1103
|
+
// to PID 1 — the budget bounded the wrapper, never the descendant.
|
|
1104
|
+
//
|
|
1093
1105
|
// It is a PROBE budget, and provisioning is exempt — see
|
|
1094
1106
|
// GCLOUD_PROVISION_TIMEOUT_MS below. Do not re-collapse the two: raising this
|
|
1095
1107
|
// constant to accommodate a slow create would un-bound the reconcile sweep,
|
|
@@ -1161,7 +1173,7 @@ function isSpawnTimeout(result) {
|
|
|
1161
1173
|
}
|
|
1162
1174
|
|
|
1163
1175
|
function runGcloud(args, opts = {}) {
|
|
1164
|
-
return
|
|
1176
|
+
return spawnGroupBoundedSync("gcloud", args, {
|
|
1165
1177
|
stdio: opts.stdio || "inherit",
|
|
1166
1178
|
input: opts.input,
|
|
1167
1179
|
encoding: opts.encoding || "utf8",
|
|
@@ -1531,6 +1543,7 @@ async function waitForIapReady(cfg) {
|
|
|
1531
1543
|
const deadline = Date.now() + 180_000;
|
|
1532
1544
|
let attempts = 0;
|
|
1533
1545
|
let lastErr = "";
|
|
1546
|
+
const probeStderrs = [];
|
|
1534
1547
|
while (Date.now() < deadline) {
|
|
1535
1548
|
attempts++;
|
|
1536
1549
|
const probe = runGcloud(
|
|
@@ -1541,17 +1554,42 @@ async function waitForIapReady(cfg) {
|
|
|
1541
1554
|
console.error(` ✓ IAP ready (${attempts} ${attempts === 1 ? "probe" : "probes"})`);
|
|
1542
1555
|
return;
|
|
1543
1556
|
}
|
|
1544
|
-
|
|
1557
|
+
const stderr = String(probe.stderr || "").trim();
|
|
1558
|
+
probeStderrs.push(stderr);
|
|
1559
|
+
lastErr = stderr.split("\n").slice(-1)[0] || "";
|
|
1545
1560
|
// Exponential-ish backoff: 2s, 3s, 5s, 8s, then cap at 8s.
|
|
1546
1561
|
const delay = Math.min(2000 * Math.pow(1.5, attempts - 1), 8000);
|
|
1547
1562
|
await new Promise((r) => setTimeout(r, delay));
|
|
1548
1563
|
}
|
|
1564
|
+
const cause = iapProbeCause(probeStderrs);
|
|
1549
1565
|
throw new Error(
|
|
1550
1566
|
`IAP did not become ready for ${cfg.instanceName} within 180s ` +
|
|
1551
|
-
`(${attempts} probes;
|
|
1567
|
+
`(${attempts} probes; ${cause ? `likely cause: ${cause}; ` : ""}` +
|
|
1568
|
+
`last error: ${lastErr || "unknown"})`,
|
|
1552
1569
|
);
|
|
1553
1570
|
}
|
|
1554
1571
|
|
|
1572
|
+
/** Name the likely cause behind a run of failed IAP SSH probes. The last stderr
|
|
1573
|
+
* line alone is almost always `ssh exited with return code [255]`, which
|
|
1574
|
+
* collapses three different waits — each with its own expected duration — into
|
|
1575
|
+
* one unactionable string. The full stderr of each probe still distinguishes
|
|
1576
|
+
* them. Scans newest-first so the label describes where the wait ENDED (IAP lag
|
|
1577
|
+
* often gives way to key propagation on a fresh box). Returns null when no
|
|
1578
|
+
* signature matches; the raw last line is kept beside it either way. */
|
|
1579
|
+
function iapProbeCause(probeStderrs) {
|
|
1580
|
+
const signatures = [
|
|
1581
|
+
[/4047|Failed to lookup instance/i, "IAP has not registered the instance yet"],
|
|
1582
|
+
[/Permission denied \(publickey/i, "OS Login SSH key not propagated yet"],
|
|
1583
|
+
[/Connection (refused|closed|reset)/i, "sshd on the instance not accepting connections yet"],
|
|
1584
|
+
];
|
|
1585
|
+
for (const text of [...(probeStderrs || [])].reverse()) {
|
|
1586
|
+
for (const [re, label] of signatures) {
|
|
1587
|
+
if (re.test(text)) return label;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
return null;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1555
1593
|
/** Read `process.env[FORWARD_ENV_KEYS_ENV]` (comma-separated key list set by
|
|
1556
1594
|
* the fleet dashboard) and return the subset of `process.env` that should
|
|
1557
1595
|
* ride along into the VM's project-bootstrap shell. Whitelisted to the
|
|
@@ -2207,6 +2245,20 @@ function buildReconcileAdminCommand(repoName, value) {
|
|
|
2207
2245
|
);
|
|
2208
2246
|
}
|
|
2209
2247
|
|
|
2248
|
+
// Every library `scripts/gcp-project-bootstrap.sh` sources, keyed by the path it
|
|
2249
|
+
// sources it AS — i.e. relative to the script's own directory, which on the VM is
|
|
2250
|
+
// `$HOME`. A streamed script is a SINGLE FILE, so each of these must be shipped
|
|
2251
|
+
// beside it or the script dies at its source line and cloud:up tears the
|
|
2252
|
+
// freshly-created VM back down. Adding a `source` line to that script without
|
|
2253
|
+
// adding it here has now broken every add on the fleet twice (`lib/npm-pin.sh`
|
|
2254
|
+
// 2026-09-04, `lib/render-cloud-compose.sh` 2026-09-18), so this list is the one
|
|
2255
|
+
// place to register it: STREAMED_SCRIPTS derives its library entries from this
|
|
2256
|
+
// constant rather than repeating them, and a unit test reads the shipped script
|
|
2257
|
+
// and fails on any source line missing from it. Keep it a ONE-LINE array literal:
|
|
2258
|
+
// the advance's provisioning pre-flight reads it out of a branch's cloud.js TEXT
|
|
2259
|
+
// (provisioning-contract.js → parseProjectBootstrapLibs).
|
|
2260
|
+
const PROJECT_BOOTSTRAP_LIBS = ["lib/npm-pin.sh", "lib/render-cloud-compose.sh"];
|
|
2261
|
+
|
|
2210
2262
|
async function ensureProjectBootstrapped(cfg, { sleepFn } = {}) {
|
|
2211
2263
|
const repoName = resolveProject(cfg).name;
|
|
2212
2264
|
console.error(`Checking project bootstrap state for ${repoName}...`);
|
|
@@ -2236,13 +2288,16 @@ async function ensureProjectBootstrapped(cfg, { sleepFn } = {}) {
|
|
|
2236
2288
|
console.error(` streaming scripts/gcp-project-bootstrap.sh to VM for ${cfg.repo}...`);
|
|
2237
2289
|
const scriptPath = path.join(rootDir, "scripts", "gcp-project-bootstrap.sh");
|
|
2238
2290
|
const scriptBody = readStreamedScript(scriptPath, cfg);
|
|
2239
|
-
// gcp-project-bootstrap.sh sources
|
|
2240
|
-
// directory. Read
|
|
2241
|
-
// to the same drift guard as the script that sources it — streaming a
|
|
2242
|
-
// remote script beside a stale on-disk library would defeat the guard.
|
|
2243
|
-
const bootstrapLibs =
|
|
2244
|
-
|
|
2245
|
-
|
|
2291
|
+
// gcp-project-bootstrap.sh sources its libraries relative to its own
|
|
2292
|
+
// directory. Read them through readStreamedScript too, so each library is
|
|
2293
|
+
// subject to the same drift guard as the script that sources it — streaming a
|
|
2294
|
+
// reviewed remote script beside a stale on-disk library would defeat the guard.
|
|
2295
|
+
const bootstrapLibs = Object.fromEntries(
|
|
2296
|
+
PROJECT_BOOTSTRAP_LIBS.map((rel) => [
|
|
2297
|
+
rel,
|
|
2298
|
+
readStreamedScript(path.join(rootDir, "scripts", ...rel.split("/")), cfg),
|
|
2299
|
+
]),
|
|
2300
|
+
);
|
|
2246
2301
|
const scriptArgs = clientProjectBootstrapArgs(cfg);
|
|
2247
2302
|
// Git identity, forwarded launch-form keys, fleet-domain ingress and the admin
|
|
2248
2303
|
// toggle — see projectBootstrapEnv for what each key is for and when it is
|
|
@@ -3626,14 +3681,18 @@ function promptForInstanceName(expected) {
|
|
|
3626
3681
|
}
|
|
3627
3682
|
|
|
3628
3683
|
// Every file streamed to a VM, and therefore every file the drift guard must
|
|
3629
|
-
// compare against the reviewed remote tip.
|
|
3630
|
-
//
|
|
3631
|
-
//
|
|
3632
|
-
//
|
|
3684
|
+
// compare against the reviewed remote tip. The libraries are here because
|
|
3685
|
+
// gcp-project-bootstrap.sh SOURCES them: a library that reaches the VM is as
|
|
3686
|
+
// much streamed content as the script that sources it, and leaving one out meant
|
|
3687
|
+
// the drift guard silently vouched for only half of what was shipped. They are
|
|
3688
|
+
// DERIVED from PROJECT_BOOTSTRAP_LIBS rather than listed again — when the two
|
|
3689
|
+
// were maintained separately, a library added to neither took the whole fleet's
|
|
3690
|
+
// provisioning down, and a library added to only one would have been shipped
|
|
3691
|
+
// unvouched-for.
|
|
3633
3692
|
const STREAMED_SCRIPTS = [
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3693
|
+
BOOTSTRAP_SCRIPT,
|
|
3694
|
+
PROJECT_BOOTSTRAP_SCRIPT,
|
|
3695
|
+
...PROJECT_BOOTSTRAP_LIBS.map((rel) => `scripts/${rel}`),
|
|
3637
3696
|
];
|
|
3638
3697
|
|
|
3639
3698
|
/** The configured editor branch to compare/stream streamed scripts against.
|
|
@@ -4966,6 +5025,10 @@ async function cmdIngressUp(args, projectDir = rootDir) {
|
|
|
4966
5025
|
async function cmdReconcileIngress(args, projectDir = rootDir) {
|
|
4967
5026
|
ensureGcloud();
|
|
4968
5027
|
parseFlags(args);
|
|
5028
|
+
// Clear ssh sessions earlier timed-out probes leaked before this sweep can add
|
|
5029
|
+
// load of its own. Only PID-1 orphans with the fleet signature — never a live
|
|
5030
|
+
// sweep's in-flight children.
|
|
5031
|
+
reapOrphanedFleetSsh();
|
|
4969
5032
|
const cfg = loadCloudConfig(projectDir) || {};
|
|
4970
5033
|
const domain = resolveIngressDomain(cfg, process.env);
|
|
4971
5034
|
if (!domain) {
|
|
@@ -4996,7 +5059,7 @@ async function cmdReconcileIngress(args, projectDir = rootDir) {
|
|
|
4996
5059
|
// whole sweep. On timeout res.stdout is empty, so parseIngressStatusVerdict
|
|
4997
5060
|
// returns null → an `unknown` verdict → selectIngressReconcileTargets SKIPS
|
|
4998
5061
|
// this VM and the sweep continues to the next one.
|
|
4999
|
-
const res =
|
|
5062
|
+
const res = spawnGroupBoundedSync(inv.command, inv.args, {
|
|
5000
5063
|
env: { ...process.env, ...inv.env },
|
|
5001
5064
|
encoding: "utf8",
|
|
5002
5065
|
timeout: GCLOUD_CALL_TIMEOUT_MS,
|
|
@@ -5057,7 +5120,7 @@ async function cmdReconcileIngress(args, projectDir = rootDir) {
|
|
|
5057
5120
|
// Bound the per-BES converge too: a hung `gcloud backend-services update`
|
|
5058
5121
|
// returns a non-zero (timed-out) result, so it falls into the skip branch
|
|
5059
5122
|
// below and the sweep moves on to the next BES rather than stalling.
|
|
5060
|
-
const res =
|
|
5123
|
+
const res = spawnGroupBoundedSync(inv.command, inv.args, {
|
|
5061
5124
|
encoding: "utf8",
|
|
5062
5125
|
timeout: GCLOUD_CALL_TIMEOUT_MS,
|
|
5063
5126
|
});
|
|
@@ -5634,6 +5697,7 @@ async function cloudMain() {
|
|
|
5634
5697
|
module.exports = {
|
|
5635
5698
|
// pure helpers
|
|
5636
5699
|
isSpawnTimeout,
|
|
5700
|
+
iapProbeCause,
|
|
5637
5701
|
gcloudCallTimeoutMs,
|
|
5638
5702
|
provisionFailureMessage,
|
|
5639
5703
|
GCLOUD_PROVISION_TIMEOUT_MS,
|
|
@@ -5747,6 +5811,7 @@ module.exports = {
|
|
|
5747
5811
|
gatherScriptDriftFacts,
|
|
5748
5812
|
readStreamedScript,
|
|
5749
5813
|
STREAMED_SCRIPTS,
|
|
5814
|
+
PROJECT_BOOTSTRAP_LIBS,
|
|
5750
5815
|
// ingress wiring (pure decision + spawn wrappers)
|
|
5751
5816
|
envPosInt,
|
|
5752
5817
|
ingressWiringPlan,
|
package/npm/fleet-add-failure.js
CHANGED
|
@@ -47,6 +47,13 @@ const { parseRosterMarkers } = require('./fleet-early-roster');
|
|
|
47
47
|
// `ensureBootstrapped` skips on the present image, and
|
|
48
48
|
// `ensureProjectBootstrapped` re-attaches to the in-flight install rather
|
|
49
49
|
// than starting a second `npm install` over the first.
|
|
50
|
+
// `iap-not-ready` is the same ground in its strongest form: the throw comes
|
|
51
|
+
// from waitForIapReady, which ensureInstance calls BEFORE ensureBootstrapped,
|
|
52
|
+
// ensureProjectBootstrapped or ensureEditorContainerUp — so the provable state
|
|
53
|
+
// is "an instance exists and nothing else has run". The retry reuses that
|
|
54
|
+
// live instance and every later step runs for the first time. (VM-4,
|
|
55
|
+
// 2026-09-17: destroy + an identical re-add succeeded in 10 minutes; the
|
|
56
|
+
// failure was a transient IAP/SSH race, not a broken box.)
|
|
50
57
|
//
|
|
51
58
|
// Ground 2 is deliberately narrow — it is NOT "the VM probably still works".
|
|
52
59
|
// `container-stuck` and `incomplete-workspace` also leave a live VM, but their
|
|
@@ -56,6 +63,7 @@ const ADD_FAILURE_RETRYABLE = new Set([
|
|
|
56
63
|
'instance-vanished',
|
|
57
64
|
'capacity-stockout',
|
|
58
65
|
'project-bootstrap-timeout',
|
|
66
|
+
'iap-not-ready',
|
|
59
67
|
]);
|
|
60
68
|
|
|
61
69
|
// Collect the ` - <path>` bullets the drift guard prints AFTER the "following
|
|
@@ -350,6 +358,92 @@ function wedgedInsertRecovery({ code, instance } = {}) {
|
|
|
350
358
|
);
|
|
351
359
|
}
|
|
352
360
|
|
|
361
|
+
// Facts for the iap-not-ready arm: waitForIapReady (npm/cloud.js) threw its
|
|
362
|
+
// readiness timeout. Returns { timedOut, instance, budgetSecs, cause } — `cause` is
|
|
363
|
+
// the label waitForIapReady derived from its probe stderr (null when none of its
|
|
364
|
+
// signatures matched, or on a transcript from before the label existed), so the
|
|
365
|
+
// recovery copy can name WHICH wait was blown instead of only `return code [255]`.
|
|
366
|
+
//
|
|
367
|
+
// The signature is emitted from exactly one place, and that place runs before any
|
|
368
|
+
// bootstrap work, which is what lets this arm assert the instance is bare.
|
|
369
|
+
const IAP_NOT_READY = /IAP did not become ready for (\S+) within (\d+)s/;
|
|
370
|
+
|
|
371
|
+
function iapNotReadyFacts(logTail) {
|
|
372
|
+
const m = String(logTail || '').match(IAP_NOT_READY);
|
|
373
|
+
if (!m) return { timedOut: false, instance: null, budgetSecs: null, cause: null };
|
|
374
|
+
const cause = (String(logTail).match(/likely cause: ([^;)]+)/) || [])[1] || null;
|
|
375
|
+
return { timedOut: true, instance: m[1], budgetSecs: Number(m[2]), cause: cause && cause.trim() };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Operator-facing recovery copy for the iap-not-ready arm. It states what the
|
|
379
|
+
// generic arm cannot: the instance EXISTS and is BARE. "A partial instance may
|
|
380
|
+
// exist" is what steered VM-4's operator toward destroy + re-add (15-25 minutes)
|
|
381
|
+
// when Retry resumes on the same box in seconds — and it left nothing on the card
|
|
382
|
+
// saying a RUNNING, billing instance was sitting there.
|
|
383
|
+
function iapNotReadyRecovery({ instance, budgetSecs, cause } = {}) {
|
|
384
|
+
const named = instance || 'The instance';
|
|
385
|
+
const within = budgetSecs ? ` within ${budgetSecs}s` : '';
|
|
386
|
+
const why = cause ? ` (likely cause: ${cause})` : '';
|
|
387
|
+
return (
|
|
388
|
+
`${named} was created and is RUNNING, but SSH over IAP did not answer${within}${why}. This is usually a transient ` +
|
|
389
|
+
'control-plane race. Nothing was installed on it — the wait runs before any bootstrap step — so it is a bare box and ' +
|
|
390
|
+
'there is nothing partial to reconcile. Retry add resumes on this same instance rather than creating a second one. ' +
|
|
391
|
+
'Until you retry or destroy it, the instance keeps running and billing.'
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Facts for the streamed-library-missing arm: a streamed bootstrap script sourced
|
|
396
|
+
// a `$HOME`-rooted `lib/…` file that was never shipped to the VM, and bash
|
|
397
|
+
// reported `<script>: line N: <path>: No such file or directory`. This took ALL
|
|
398
|
+
// fleet provisioning down twice (lib/npm-pin.sh 2026-09-04, lib/render-cloud-
|
|
399
|
+
// compose.sh 2026-09-18). Returns { missing, script, library } — `script` is the
|
|
400
|
+
// basename of the aborting script, `library` the `lib/…` path relative to $HOME,
|
|
401
|
+
// which is how PROJECT_BOOTSTRAP_LIBS (npm/cloud.js) names its entries.
|
|
402
|
+
const STREAMED_LIBRARY_MISSING = /(\S+): line \d+: \/(?:home\/[^/\s]+|root)\/(lib\/[^\s:]+): No such file or directory/;
|
|
403
|
+
|
|
404
|
+
function streamedLibraryMissingFacts(logTail) {
|
|
405
|
+
const m = String(logTail || '').match(STREAMED_LIBRARY_MISSING);
|
|
406
|
+
if (!m) return { missing: false, script: null, library: null };
|
|
407
|
+
return { missing: true, script: m[1].split('/').pop(), library: m[2] };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Did cloud:up report that it deleted the instance it created this run? The abort
|
|
411
|
+
// path (npm/cloud.js) prints the teardown marker BEFORE it issues the delete, and
|
|
412
|
+
// prints a `during abort teardown` warning when that delete fails — so the marker
|
|
413
|
+
// alone is a claim of intent, and only the marker WITHOUT the warning is evidence
|
|
414
|
+
// that no orphan was left behind.
|
|
415
|
+
function cleanTeardownReported(logTail) {
|
|
416
|
+
const log = String(logTail || '');
|
|
417
|
+
return /tearing down the VM cloud:up created this run \(no orphan left behind\)/.test(log)
|
|
418
|
+
&& !/during abort teardown/.test(log);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Operator-facing recovery copy for the streamed-library-missing arm. It names
|
|
422
|
+
// what to fix — register the library so it is streamed — and, when the transcript
|
|
423
|
+
// proves a clean teardown, says there is no orphan to hunt for. Every add on this
|
|
424
|
+
// branch fails identically until the registry is fixed, so it never offers Retry.
|
|
425
|
+
function streamedLibraryMissingRecovery({ script, library, cleanTeardown } = {}) {
|
|
426
|
+
const lib = library || 'a lib/ file';
|
|
427
|
+
const from = script ? ` sourced by ${script}` : '';
|
|
428
|
+
const orphan = cleanTeardown
|
|
429
|
+
? 'cloud:up already deleted the VM it created, so there is nothing to reconcile or destroy.'
|
|
430
|
+
: 'The transcript does not confirm the VM was deleted — check whether a partial instance exists before re-adding.';
|
|
431
|
+
return (
|
|
432
|
+
`${lib}${from} was never shipped to the VM, so the bootstrap aborted. Add it to PROJECT_BOOTSTRAP_LIBS in npm/cloud.js, ` +
|
|
433
|
+
`push, and re-add — every add from this branch fails the same way until then. ${orphan}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Operator-facing recovery copy for the generic cloud-up-failed arm. "A partial
|
|
438
|
+
// instance may exist" is only true when the transcript does not show cloud:up
|
|
439
|
+
// deleting what it created; saying it regardless sends the operator hunting for
|
|
440
|
+
// an orphan that is not there.
|
|
441
|
+
function cloudUpFailedRecovery({ cleanTeardown } = {}) {
|
|
442
|
+
return cleanTeardown
|
|
443
|
+
? 'cloud:up failed during provisioning and deleted the VM it created, so nothing was left behind. Read the transcript for the cause before re-adding.'
|
|
444
|
+
: 'cloud:up failed during provisioning, so a partial instance may exist. Reconcile or destroy this VM before re-adding.';
|
|
445
|
+
}
|
|
446
|
+
|
|
353
447
|
// Build a verdict, stamping `retryable` from the single-source-of-truth Set so it
|
|
354
448
|
// can never drift from the category.
|
|
355
449
|
function buildFailureVerdict(category, headline, recovery, extra) {
|
|
@@ -392,7 +486,12 @@ function buildFailureVerdict(category, headline, recovery, extra) {
|
|
|
392
486
|
// instance. `wedged-insert` (arm 8) is the third: it sits directly above the
|
|
393
487
|
// generic arm so a create GCE accepted-and-abandoned cannot report as an ordinary
|
|
394
488
|
// provisioning failure whose advice is to destroy the box — a destroy that, on a
|
|
395
|
-
// wedged slot, queues behind the wedge instead of clearing it.
|
|
489
|
+
// wedged slot, queues behind the wedge instead of clearing it. `iap-not-ready`
|
|
490
|
+
// (arm 9) is the fourth: it keeps a bare, retry-safe instance from reporting as a
|
|
491
|
+
// possibly-partial one whose only recovery is destroy + re-add.
|
|
492
|
+
// `streamed-library-missing` (arm 10) is the fifth: it names the unshipped library
|
|
493
|
+
// that broke every add, instead of advising a hunt for an orphan cloud:up already
|
|
494
|
+
// deleted.
|
|
396
495
|
function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy, wiringDeadlineBlown, wiringInFlight } = {}) {
|
|
397
496
|
const log = String(logTail || '');
|
|
398
497
|
|
|
@@ -554,28 +653,64 @@ function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy
|
|
|
554
653
|
wedgedInsertRecovery(wedgedInsert));
|
|
555
654
|
}
|
|
556
655
|
|
|
557
|
-
// 9.
|
|
558
|
-
//
|
|
559
|
-
//
|
|
656
|
+
// 9. iap-not-ready — the instance reached RUNNING but IAP-tunneled SSH never
|
|
657
|
+
// answered inside waitForIapReady's budget. That wait is called from
|
|
658
|
+
// ensureInstance before any bootstrap work, so the box exists and is bare.
|
|
659
|
+
// Retryable on the re-entrancy ground (see ADD_FAILURE_RETRYABLE); the budget
|
|
660
|
+
// was already raised 60s → 180s for this race, and VM-4 still blew it, so the
|
|
661
|
+
// answer is a cheap retry rather than a longer wait for every dead add.
|
|
662
|
+
//
|
|
663
|
+
// Tested BEFORE cloud-up-failed for the reason arm 6 documents: the provision
|
|
664
|
+
// shell echoes cloud:up's tail into every transcript, so matching the generic
|
|
665
|
+
// line first refuses the safe retry — exactly the VM-4 failure.
|
|
666
|
+
const iapNotReady = iapNotReadyFacts(log);
|
|
667
|
+
if (iapNotReady.timedOut) {
|
|
668
|
+
return buildFailureVerdict('iap-not-ready', 'SSH never became reachable',
|
|
669
|
+
iapNotReadyRecovery(iapNotReady));
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// 10. streamed-library-missing — a streamed bootstrap script sourced a `lib/…`
|
|
673
|
+
// file that was never shipped to the VM. The failure that has taken ALL fleet
|
|
674
|
+
// provisioning down twice (2026-09-04, 2026-09-18); both times it fell through
|
|
675
|
+
// to cloud-up-failed, whose recovery told six operators to hunt for a partial
|
|
676
|
+
// instance cloud:up had already deleted, and said nothing about the cause.
|
|
677
|
+
//
|
|
678
|
+
// NOT retryable: re-issuing the same add streams the same scripts without the
|
|
679
|
+
// library and fails identically — the missing-branch-ref reasoning. The fix is
|
|
680
|
+
// a code change (register the library), which is what the recovery names.
|
|
681
|
+
//
|
|
682
|
+
// Tested BEFORE cloud-up-failed for the reason arm 6 documents: the provision
|
|
683
|
+
// shell echoes cloud:up's tail into every transcript, so matching the generic
|
|
684
|
+
// line first is precisely the bug being fixed.
|
|
685
|
+
const missingLib = streamedLibraryMissingFacts(log);
|
|
686
|
+
if (missingLib.missing) {
|
|
687
|
+
return buildFailureVerdict('streamed-library-missing', `Bootstrap library never shipped: ${missingLib.library}`,
|
|
688
|
+
streamedLibraryMissingRecovery({ ...missingLib, cleanTeardown: cleanTeardownReported(log) }));
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// 11. cloud-up-failed — a generic cloud:up failure, reached ONLY when the drift /
|
|
692
|
+
// vanished / stuck / stockout / missing-ref / wedged-insert / iap-not-ready /
|
|
693
|
+
// streamed-library signatures above did not match. Not retryable. Whether a
|
|
694
|
+
// partial instance may exist is read off the transcript's teardown evidence.
|
|
560
695
|
if (/cloud:up FAILED \(exit/i.test(log)) {
|
|
561
696
|
return buildFailureVerdict('cloud-up-failed', 'Provisioning failed',
|
|
562
|
-
|
|
697
|
+
cloudUpFailedRecovery({ cleanTeardown: cleanTeardownReported(log) }));
|
|
563
698
|
}
|
|
564
699
|
|
|
565
|
-
//
|
|
700
|
+
// 12. interrupted — a signal kill (OOM 137, SIGTERM 143, …) or an explicit
|
|
566
701
|
// Killed / out-of-memory marker. The real VM state is unknown. Not retryable.
|
|
567
702
|
if (fc.isSignalKillExit(Number(exitCode)) || /\bKilled\b/.test(log) || /Out of memory/i.test(log)) {
|
|
568
703
|
return buildFailureVerdict('interrupted', 'Provisioning interrupted',
|
|
569
704
|
'The provision was interrupted (killed or out of memory), so the VM state is unknown. Reconcile or destroy this VM before re-adding.');
|
|
570
705
|
}
|
|
571
706
|
|
|
572
|
-
//
|
|
707
|
+
// 13. The post-add IAP read-back verdicts. Unlike every signature above, this one
|
|
573
708
|
// is not produced by the provision transcript at all — it is the BLOCKED
|
|
574
709
|
// reason iapReadbackOutcome hands to annotateAddFailure, which is what
|
|
575
710
|
// stops a read-back failure from being the one add failure that reaches the
|
|
576
711
|
// card with no category at all (and so no differentiated headline, no
|
|
577
712
|
// recovery, no guarded Retry, and no chance for holdConvergingBadge to hold it
|
|
578
|
-
// calm). Tested AFTER
|
|
713
|
+
// calm). Tested AFTER every hard-failure signature so a proven failure
|
|
579
714
|
// always wins if both texts are ever present, and BEFORE converging/verifying
|
|
580
715
|
// because it is strictly more specific than either.
|
|
581
716
|
if (/IAP policy authorizes no human/i.test(log)) {
|
|
@@ -599,7 +734,7 @@ function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy
|
|
|
599
734
|
`The VM is up but its IAP policy authorizes no human, so the public URL returns "Error code 51" for everyone. Its background wiring is no longer running, so this will not resolve on its own — grant access to fix it. Do NOT destroy or re-add this VM.${remedy ? `\n\n${remedy}` : ''}`);
|
|
600
735
|
}
|
|
601
736
|
|
|
602
|
-
//
|
|
737
|
+
// 14. converging — the add reached the early-roster marker (`CY_ROSTER`): the
|
|
603
738
|
// container gate passed, the instance is real, and the VM is rostered, yet it
|
|
604
739
|
// exited non-zero WITHOUT matching any specific hard-failure signature above.
|
|
605
740
|
// By construction of the provision script, every genuine hard failure (drift,
|
|
@@ -617,7 +752,7 @@ function classifyAddFailure({ logTail, exitCode, instanceAlive, containerHealthy
|
|
|
617
752
|
'The container is up and the VM is rostered; the LB ingress is still wiring in the background. This resolves on its own once the editor is reachable over the LB — no action needed unless it persists past the wiring deadline.');
|
|
618
753
|
}
|
|
619
754
|
|
|
620
|
-
//
|
|
755
|
+
// 15. The residue: no failure signature above matched. The polarity here is
|
|
621
756
|
// INVERTED from the original design. It used to read "no recognized success
|
|
622
757
|
// signature ⇒ failed", which made red the DEFAULT for "we don't know" — so a
|
|
623
758
|
// VM whose container was Up and whose editor answered /api/health 200 got a
|
|
@@ -739,4 +874,14 @@ function wedgedRetryMessage(n) {
|
|
|
739
874
|
);
|
|
740
875
|
}
|
|
741
876
|
|
|
742
|
-
|
|
877
|
+
// Refusal copy for a one-click "Retry add" after a streamed-library abort,
|
|
878
|
+
// extracted for the same reason wedgedRetryMessage was. The retry is refused
|
|
879
|
+
// because every add from this branch fails identically until the library is
|
|
880
|
+
// registered, and the generic refusal would again send the operator after an
|
|
881
|
+
// orphan cloud:up already deleted. Repeats the classified recovery, which names
|
|
882
|
+
// the fix and says whether anything was left behind. Pure; no I/O.
|
|
883
|
+
function streamedLibraryRetryMessage(n, recovery) {
|
|
884
|
+
return `VM-${n} cannot be retried until the code is fixed: ${recovery || 'a streamed bootstrap library was never shipped to the VM.'}`;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
module.exports = { classifyAddFailure, extractDriftFiles, lastNonEmptyLines, canRetryAdd, transcriptRostered, holdConvergingBadge, addFailureCorroborated, containerHealthWitness, stockoutCapacityPhrase, missingBranchRefFacts, missingBranchRefRecovery, deadRefRetryMessage, wedgedInsertFacts, wedgedInsertRecovery, wedgedRetryMessage, iapNotReadyFacts, iapNotReadyRecovery, streamedLibraryMissingFacts, cleanTeardownReported, streamedLibraryMissingRecovery, cloudUpFailedRecovery, streamedLibraryRetryMessage, CALM_ADD_CATEGORIES, ADD_FAILURE_RETRYABLE };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// npm/fleet-auth-account.js — the operator's chosen fleet Claude account
|
|
4
|
+
// (`.codeyam/logs/fleet-auth-account.json`). This is the account a newly added
|
|
5
|
+
// VM authenticates as: the Add path picks a credential donor holding it, and the
|
|
6
|
+
// Add dialog preselects it. Persisted so the choice survives a dashboard restart,
|
|
7
|
+
// and explicit so a background token refresh on some other VM can never re-point
|
|
8
|
+
// it (the drift the derived default in fleet-auth.js is subject to).
|
|
9
|
+
//
|
|
10
|
+
// Shape on disk: { "email": "<account email>", "updatedAt": "<ISO>" }. A seam
|
|
11
|
+
// modeled on fleet-vm-slugs.js: atomic temp-file + rename, tolerant of a
|
|
12
|
+
// missing/malformed file (both read as "never chosen" → null).
|
|
13
|
+
//
|
|
14
|
+
// Env override: `FLEET_AUTH_ACCOUNT_FILE` relocates the file (mirrors
|
|
15
|
+
// fleet-roster.js's `VMS_STATE_FILE`); resolved once at module load.
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const { rootDir } = require('./utils');
|
|
21
|
+
const { isAccountEmail } = require('./fleet-auth');
|
|
22
|
+
|
|
23
|
+
const FLEET_AUTH_ACCOUNT_FILE = process.env.FLEET_AUTH_ACCOUNT_FILE
|
|
24
|
+
|| path.join(rootDir, '.codeyam', 'logs', 'fleet-auth-account.json');
|
|
25
|
+
|
|
26
|
+
/** The chosen account email, or null when none has been chosen (file missing,
|
|
27
|
+
* malformed, or carrying no email). */
|
|
28
|
+
function readAuthAccount() {
|
|
29
|
+
try {
|
|
30
|
+
const data = JSON.parse(fs.readFileSync(FLEET_AUTH_ACCOUNT_FILE, 'utf8'));
|
|
31
|
+
if (data && typeof data.email === 'string' && data.email.trim()) return data.email.trim();
|
|
32
|
+
} catch { /* missing or malformed */ }
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Persist `email` as the fleet's chosen account, atomically: write a temp file
|
|
37
|
+
* then `rename` it over the target so a concurrent reader never observes a
|
|
38
|
+
* half-written file. Returns the email written. A value that is not a
|
|
39
|
+
* plausible email is rejected (nothing is written) — forgetting the choice is
|
|
40
|
+
* `clearAuthAccount`. */
|
|
41
|
+
function writeAuthAccount(email) {
|
|
42
|
+
const e = typeof email === 'string' ? email.trim() : '';
|
|
43
|
+
if (!isAccountEmail(e)) throw new Error(`not a valid account email: "${e}"`);
|
|
44
|
+
fs.mkdirSync(path.dirname(FLEET_AUTH_ACCOUNT_FILE), { recursive: true });
|
|
45
|
+
const tmp = `${FLEET_AUTH_ACCOUNT_FILE}.${process.pid}.tmp`;
|
|
46
|
+
fs.writeFileSync(tmp, JSON.stringify({ email: e, updatedAt: new Date().toISOString() }));
|
|
47
|
+
fs.renameSync(tmp, FLEET_AUTH_ACCOUNT_FILE); // atomic replace
|
|
48
|
+
return e;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Forget the chosen account, so the fleet falls back to the derived default.
|
|
52
|
+
* Removing an absent file is a no-op. */
|
|
53
|
+
function clearAuthAccount() {
|
|
54
|
+
try {
|
|
55
|
+
fs.unlinkSync(FLEET_AUTH_ACCOUNT_FILE);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
if (e.code !== 'ENOENT') throw e;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { FLEET_AUTH_ACCOUNT_FILE, readAuthAccount, writeAuthAccount, clearAuthAccount };
|
package/npm/fleet-auth.js
CHANGED
|
@@ -108,9 +108,117 @@ function deriveDefaultAuthAccount(vms, lastAuthAt) {
|
|
|
108
108
|
return best ? best.account : null;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// Enumerate the Claude accounts the fleet can seed a new VM from: one row per
|
|
112
|
+
// distinct account email held by an authenticated VM, carrying the account
|
|
113
|
+
// object and the VM numbers holding it (so the Add dialog can show
|
|
114
|
+
// "jared@codeyam.com — VM-14"). Pure over the polled VM snapshot — the same
|
|
115
|
+
// array deriveDefaultAuthAccount reads. Emails compare case-insensitively; rows
|
|
116
|
+
// sort by email and each row's VM numbers ascend, so the result is deterministic.
|
|
117
|
+
function listAvailableAccounts(vms) {
|
|
118
|
+
const byEmail = new Map();
|
|
119
|
+
for (const v of vms || []) {
|
|
120
|
+
const pa = v && v.providerAuth;
|
|
121
|
+
const acct = pa && pa.account;
|
|
122
|
+
if (!acct || !acct.email || pa.state !== 'authenticated') continue;
|
|
123
|
+
const key = String(acct.email).toLowerCase();
|
|
124
|
+
const row = byEmail.get(key) || { account: acct, vms: [] };
|
|
125
|
+
if (!row.vms.includes(v.n)) row.vms.push(v.n);
|
|
126
|
+
byEmail.set(key, row);
|
|
127
|
+
}
|
|
128
|
+
return [...byEmail.values()]
|
|
129
|
+
.map((r) => ({ account: r.account, vms: [...r.vms].sort((a, b) => a - b) }))
|
|
130
|
+
.sort((a, b) => String(a.account.email).localeCompare(String(b.account.email)));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Pick the credential donor for a chosen account: the lowest-numbered VM in
|
|
134
|
+
// `reachableDonors` whose authenticated account is `email`, or null when no
|
|
135
|
+
// reachable VM holds it (or no email was asked for). `vms` is the per-VM poll
|
|
136
|
+
// state keyed by VM number (`state.vms`). A donor whose credential failed the
|
|
137
|
+
// integrity probe (`providerAuth.credentialIntact === false`) is skipped as
|
|
138
|
+
// though it were not authenticated — its credential cannot be copied, so the
|
|
139
|
+
// sink would refuse it after an add had already committed to it. An unprobed
|
|
140
|
+
// verdict (null/absent) is usable: a probe failure is not evidence. Pure.
|
|
141
|
+
function selectDonorForAccount(reachableDonors, vms, email) {
|
|
142
|
+
if (!email) return null;
|
|
143
|
+
const want = String(email).toLowerCase();
|
|
144
|
+
for (const n of [...(reachableDonors || [])].sort((a, b) => a - b)) {
|
|
145
|
+
const pa = vms && vms[n] && vms[n].providerAuth;
|
|
146
|
+
if (pa && pa.credentialIntact === false) continue;
|
|
147
|
+
const got = pa && pa.state === 'authenticated' && pa.account && pa.account.email;
|
|
148
|
+
if (got && String(got).toLowerCase() === want) return n;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Split the reachable donor candidates into those whose credential can be
|
|
154
|
+
// copied and those the integrity probe failed, so the add can drop the failed
|
|
155
|
+
// ones BEFORE resolveCredsSource's first-donor fallback could pick one, and name
|
|
156
|
+
// each dropped donor in the add log. Order is preserved; unprobed is usable.
|
|
157
|
+
// Returns { usable: number[], skipped: number[] }. Pure.
|
|
158
|
+
function partitionDonorsByIntegrity(reachableDonors, vms) {
|
|
159
|
+
const usable = [];
|
|
160
|
+
const skipped = [];
|
|
161
|
+
for (const n of reachableDonors || []) {
|
|
162
|
+
const pa = vms && vms[n] && vms[n].providerAuth;
|
|
163
|
+
if (pa && pa.credentialIntact === false) skipped.push(n);
|
|
164
|
+
else usable.push(n);
|
|
165
|
+
}
|
|
166
|
+
return { usable, skipped };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Fold the dashboard's credential-integrity verdict into a VM's editor-reported
|
|
170
|
+
// providerAuth record. The editor derives `state` from whether the access token
|
|
171
|
+
// works NOW, so a credential with no refresh token still reads `authenticated`
|
|
172
|
+
// (VM-11, 2026-09-17) — a VM that will strand at its next token expiry and that
|
|
173
|
+
// no add can copy from. When the probe says the credential is incomplete, the
|
|
174
|
+
// card must say what the remedy is: `needs-login`. `credentialIntact` rides on
|
|
175
|
+
// the record either way (null = unprobed) so donor selection reads one place.
|
|
176
|
+
// Returns null for a null auth (editor didn't answer). Pure.
|
|
177
|
+
function applyCredentialIntegrity(auth, credentialIntact) {
|
|
178
|
+
if (!auth) return null;
|
|
179
|
+
const intact = credentialIntact === true || credentialIntact === false ? credentialIntact : null;
|
|
180
|
+
if (intact === false && auth.state === 'authenticated') {
|
|
181
|
+
return { ...auth, state: 'needs-login', credentialIntact: false };
|
|
182
|
+
}
|
|
183
|
+
return { ...auth, credentialIntact: intact };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// A plausible Claude account email: one "@", a dotted domain, no whitespace or
|
|
187
|
+
// shell-significant characters. The shared guard for every place an operator
|
|
188
|
+
// can name an account (the Add form's per-launch override, the set-default
|
|
189
|
+
// action, the persisted sidecar). Deliberately NOT checked against the fleet,
|
|
190
|
+
// whose roster can change between naming an account and provisioning with it.
|
|
191
|
+
const ACCOUNT_EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
|
|
192
|
+
function isAccountEmail(s) {
|
|
193
|
+
return typeof s === 'string' && ACCOUNT_EMAIL_RE.test(s.trim());
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Resolve the fleet's chosen Claude account email: the operator's persisted
|
|
197
|
+
// choice (`saved`, read from npm/fleet-auth-account.js by the caller) when it is
|
|
198
|
+
// a plausible email, else the derived default — the most recently auth-updated
|
|
199
|
+
// VM's account — so a fleet that has never chosen behaves as it did before the
|
|
200
|
+
// picker existed. Pure. Returns an email string, or null when neither exists.
|
|
201
|
+
function resolveChosenAccount(saved, vms, lastAuthAt) {
|
|
202
|
+
if (isAccountEmail(saved)) return saved.trim();
|
|
203
|
+
const derived = deriveDefaultAuthAccount(vms, lastAuthAt);
|
|
204
|
+
return (derived && derived.email) || null;
|
|
205
|
+
}
|
|
206
|
+
|
|
111
207
|
// Return true if this one VM is eligible for an auth-start action now.
|
|
112
208
|
function isAuthStartable(v, job) {
|
|
113
209
|
return classifyAuthVm(v, job).eligible;
|
|
114
210
|
}
|
|
115
211
|
|
|
116
|
-
module.exports = {
|
|
212
|
+
module.exports = {
|
|
213
|
+
classifyAuthVm,
|
|
214
|
+
selectAuthTargets,
|
|
215
|
+
summarizeAuthBatch,
|
|
216
|
+
deriveDefaultAuthAccount,
|
|
217
|
+
listAvailableAccounts,
|
|
218
|
+
selectDonorForAccount,
|
|
219
|
+
partitionDonorsByIntegrity,
|
|
220
|
+
applyCredentialIntegrity,
|
|
221
|
+
isAccountEmail,
|
|
222
|
+
resolveChosenAccount,
|
|
223
|
+
isAuthStartable,
|
|
224
|
+
};
|