@decidio/sdk 0.4.1 → 0.4.4
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/CHANGELOG.md +195 -0
- package/README.md +19 -6
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +621 -151
- package/dist/resume.d.ts +4 -3
- package/dist/resume.js +56 -23
- package/openapi.yaml +1 -1
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFileSync, writeFileSync, existsSync, appendFileSync, chmodSync, rmSync, openSync, writeSync, fsyncSync, closeSync, renameSync } from "node:fs";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, appendFileSync, chmodSync, rmSync, openSync, writeSync, fsyncSync, closeSync, renameSync, mkdirSync } from "node:fs";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join as joinPath, resolve as resolvePath, sep as pathSep } from "node:path";
|
|
4
6
|
import { createServer } from "node:http";
|
|
5
|
-
import { execFileSync } from "node:child_process";
|
|
7
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
6
8
|
import { createInterface } from "node:readline";
|
|
7
9
|
import { pathToFileURL as toFileUrl, fileURLToPath as fromFileUrl } from "node:url";
|
|
8
10
|
import { realpathSync as realPath } from "node:fs";
|
|
9
11
|
import { createRequire } from "node:module";
|
|
10
|
-
import { generateKeypair, assertSafeTransport } from "./signing.js";
|
|
12
|
+
import { generateKeypair, assertSafeTransport, redactUrl } from "./signing.js";
|
|
11
13
|
import { hardenFile } from "./resume.js";
|
|
12
|
-
import { withApproval, DecidioRateLimitError, DecidioRejectedError, didFromPrivateKey } from "./index.js";
|
|
14
|
+
import { withApproval, DecidioRateLimitError, DecidioRejectedError, DecidioTimeoutError, didFromPrivateKey } from "./index.js";
|
|
13
15
|
import { verifyReceipt, runVerify } from "@decidio/verify";
|
|
14
16
|
const VERSION = (() => {
|
|
15
17
|
try {
|
|
@@ -41,9 +43,10 @@ function readEnvFile() {
|
|
|
41
43
|
return out;
|
|
42
44
|
}
|
|
43
45
|
const wantsHelp = (s) => s === "help" || s === "--help" || s === "-h";
|
|
46
|
+
const helpFlagAnywhere = (argv) => { const dd = argv.indexOf("--"); return (dd >= 0 ? argv.slice(0, dd) : argv).some((s) => s === "--help" || s === "-h"); };
|
|
44
47
|
const ARGV_TAIL = process.argv.slice(2);
|
|
45
48
|
const OFFLINE_CMD = ["verify", "--version", "-v", "-V", "version", undefined].includes(ARGV_TAIL[0])
|
|
46
|
-
|| wantsHelp(ARGV_TAIL[0]) || wantsHelp(ARGV_TAIL[1]);
|
|
49
|
+
|| wantsHelp(ARGV_TAIL[0]) || wantsHelp(ARGV_TAIL[1]) || helpFlagAnywhere(ARGV_TAIL);
|
|
47
50
|
if (!OFFLINE_CMD) {
|
|
48
51
|
readEnvFile();
|
|
49
52
|
if (envUnreadable) {
|
|
@@ -52,6 +55,7 @@ if (!OFFLINE_CMD) {
|
|
|
52
55
|
process.exit(1);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
58
|
+
const SHELL_ENV = { ...process.env };
|
|
55
59
|
if (!OFFLINE_CMD)
|
|
56
60
|
for (const [k, v] of Object.entries(readEnvFile())) {
|
|
57
61
|
if (k === "DECIDIO_SESSION_TOKEN") {
|
|
@@ -67,7 +71,7 @@ const ENV_PATH = ".env";
|
|
|
67
71
|
const log = (...a) => console.log(...a);
|
|
68
72
|
const err = (...a) => console.error(...a);
|
|
69
73
|
let hostAnnounced = false;
|
|
70
|
-
async function api(path, init = {}, bearer) {
|
|
74
|
+
async function api(path, init = {}, bearer, soft = false) {
|
|
71
75
|
if (!hostAnnounced) {
|
|
72
76
|
hostAnnounced = true;
|
|
73
77
|
err(`· decidio → ${API}${process.env.DECIDIO_API_URL ? "" : " (default: the hosted sandbox — set DECIDIO_API_URL to point elsewhere)"}`);
|
|
@@ -92,6 +96,8 @@ async function api(path, init = {}, bearer) {
|
|
|
92
96
|
res = await fetch(API + path, { ...init, headers: { "content-type": "application/json", ...(auth ? { authorization: `Bearer ${auth}` } : {}), ...(init.headers ?? {}) } });
|
|
93
97
|
}
|
|
94
98
|
catch (e) {
|
|
99
|
+
if (soft)
|
|
100
|
+
return { status: 0, body: null };
|
|
95
101
|
err(`✘ could not reach Decidio at ${API} (${e?.cause?.code ?? e?.message ?? "network error"}).`);
|
|
96
102
|
if (API.includes("localhost")) {
|
|
97
103
|
err(` You're pointed at localhost — for the hosted sandbox run:`);
|
|
@@ -268,11 +274,12 @@ function credentialJtisOnDisk() {
|
|
|
268
274
|
return out;
|
|
269
275
|
}
|
|
270
276
|
function journalOwed() {
|
|
271
|
-
|
|
277
|
+
const held = credentialJtisOnDisk();
|
|
278
|
+
return journalOutstanding().filter((j) => j.label === "quickstart" || (!j.persisted && !held.has(j.jti)));
|
|
272
279
|
}
|
|
273
280
|
function journalUnheld() {
|
|
274
281
|
const held = credentialJtisOnDisk();
|
|
275
|
-
return journalOutstanding().filter((j) => j.label !== "quickstart" &&
|
|
282
|
+
return journalOutstanding().filter((j) => j.label !== "quickstart" && !held.has(j.jti) && j.persisted);
|
|
276
283
|
}
|
|
277
284
|
const REFUSE_MINT_CORRUPT_JOURNAL = (e) => `✘ the token recovery journal cannot be trusted (${e.message}) - it may hold the id of a token that is still live. Refusing to mint on top of uncertain history. Inspect the file, revoke anything it names (npx @decidio/sdk tokens list --mine), and repair or move it aside; every byte is left as found. Nothing was minted.`;
|
|
278
285
|
function journalProbe() {
|
|
@@ -291,10 +298,18 @@ function journalProbe() {
|
|
|
291
298
|
}
|
|
292
299
|
}
|
|
293
300
|
function journalMinted(jti, agentId, label) { journalAppend({ event: "minted", jti, agentId, label, apiUrl: API }); }
|
|
294
|
-
function journalRetire(jti, how) {
|
|
295
|
-
|
|
301
|
+
function journalRetire(jti, how) {
|
|
302
|
+
try {
|
|
303
|
+
journalAppend({ event: "retired", jti, how });
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
catch (e) {
|
|
307
|
+
err(how === "persisted"
|
|
308
|
+
? ` ⚠ could not note in ${JOURNAL_PATH} that token ${jti} was written to its credential file (${e?.message ?? e}). The credential is safe in the file; doctor will list the token as live and held.`
|
|
309
|
+
: ` ⚠ could not note in ${JOURNAL_PATH} that token ${jti} was revoked (${e?.message ?? e}). It IS revoked; a later cleanup will find the server already agrees and retire the row.`);
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
296
312
|
}
|
|
297
|
-
catch { } }
|
|
298
313
|
function journalOutstanding() {
|
|
299
314
|
const minted = new Map();
|
|
300
315
|
for (const e of journalRows()) {
|
|
@@ -363,13 +378,75 @@ function assertNotTracked(path) {
|
|
|
363
378
|
}
|
|
364
379
|
function ensureGitignore() {
|
|
365
380
|
const entries = [".env", ".env.quickstart", ".env.*.bak", ".env.*.tmp", ".decidio-pending/", ".decidio-token-journal"];
|
|
366
|
-
const
|
|
381
|
+
const creating = !existsSync(".gitignore");
|
|
382
|
+
if (creating)
|
|
383
|
+
entries.push("node_modules/");
|
|
384
|
+
const existing = creating ? "" : readFileSync(".gitignore", "utf8");
|
|
367
385
|
const missing = entries.filter((e) => !existing.split(/\r?\n/).some((l) => l.trim() === e || l.trim() === e.replace(/\/$/, "")));
|
|
368
386
|
if (!missing.length)
|
|
369
387
|
return;
|
|
370
388
|
appendFileSync(".gitignore", (existing && !existing.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n");
|
|
371
|
-
log(`✔ added ${missing.join(" + ")} to .gitignore (
|
|
389
|
+
log(`✔ ${creating ? "wrote" : "added"} ${missing.join(" + ")} to .gitignore (secret files here will not be committed)`);
|
|
390
|
+
}
|
|
391
|
+
function recordKindOf(credential) {
|
|
392
|
+
try {
|
|
393
|
+
const rc = credential?.credentialSubject?.recordCanonical;
|
|
394
|
+
const rec = typeof rc === "string" ? JSON.parse(rc) : rc;
|
|
395
|
+
return typeof rec?.recordKind === "string" ? rec.recordKind : undefined;
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
372
400
|
}
|
|
401
|
+
function sessionReminder() {
|
|
402
|
+
log(` ⚠ agent code launched from THIS terminal inherits your sign-in — run logout first (it forgets the sign-in here; the token itself stays valid until it expires); if you`);
|
|
403
|
+
log(` exported DECIDIO_SESSION_TOKEN, unset it ${dim("(bash/zsh: unset DECIDIO_SESSION_TOKEN · PowerShell: Remove-Item Env:DECIDIO_SESSION_TOKEN)")}`);
|
|
404
|
+
}
|
|
405
|
+
const TTY_OUT = !!process.stdout.isTTY && !process.env.NO_COLOR && process.env.TERM !== "dumb";
|
|
406
|
+
const paint = (code, s) => (TTY_OUT ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
407
|
+
const bold = (s) => paint("1", s);
|
|
408
|
+
const dim = (s) => paint("2", s);
|
|
409
|
+
const link = (url) => (TTY_OUT ? `\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\` : url);
|
|
410
|
+
const FRAME_WIDTH = 84;
|
|
411
|
+
const visibleLength = (s) => s.replace(/\x1b\[[0-9;]*m|\x1b\]8;;[^\x1b]*\x1b\\/g, "").length;
|
|
412
|
+
function frameWidth(rows) {
|
|
413
|
+
const cols = (process.stdout.columns ?? 0) > 20 ? process.stdout.columns : 120;
|
|
414
|
+
const widest = rows.reduce((m, r) => Math.max(m, visibleLength(r)), 0);
|
|
415
|
+
return Math.min(cols - 1, Math.max(FRAME_WIDTH, widest + 4));
|
|
416
|
+
}
|
|
417
|
+
function block(n, title, rows, tone) {
|
|
418
|
+
const tint = tone === "choice" ? (process.env.TERM?.includes("256") || process.env.COLORTERM || process.env.WT_SESSION ? "38;5;208" : "33") : tone === "good" ? "32" : tone === "bad" ? "31" : null;
|
|
419
|
+
const f = (s) => (tint ? paint(tint, s) : s);
|
|
420
|
+
const width = frameWidth(rows);
|
|
421
|
+
const inner = width - 4;
|
|
422
|
+
const head = `┌ ${n} ${title} `;
|
|
423
|
+
log(f(head + "─".repeat(Math.max(1, width - visibleLength(head) - 1)) + "┐"));
|
|
424
|
+
for (const row of rows) {
|
|
425
|
+
const pieces = [];
|
|
426
|
+
if (visibleLength(row) <= inner)
|
|
427
|
+
pieces.push(row);
|
|
428
|
+
else {
|
|
429
|
+
let cur = "";
|
|
430
|
+
for (const w of row.split(" ")) {
|
|
431
|
+
if (cur && visibleLength(cur) + 1 + visibleLength(w) > inner) {
|
|
432
|
+
pieces.push(cur);
|
|
433
|
+
cur = " ".repeat(Math.min(13, Math.max(0, inner - 24))) + w;
|
|
434
|
+
}
|
|
435
|
+
else
|
|
436
|
+
cur = cur ? `${cur} ${w}` : w;
|
|
437
|
+
}
|
|
438
|
+
if (cur)
|
|
439
|
+
pieces.push(cur);
|
|
440
|
+
}
|
|
441
|
+
for (const piece of pieces)
|
|
442
|
+
log(`${f("│")} ${piece}${TTY_OUT ? "\x1b[0m" : ""}${" ".repeat(Math.max(0, inner - visibleLength(piece)))} ${f("│")}`);
|
|
443
|
+
}
|
|
444
|
+
log(f("└" + "─".repeat(width - 2) + "┘"));
|
|
445
|
+
}
|
|
446
|
+
const APP_URL = API.replace("decidio-api.onrender.com", "app.decidioai.com");
|
|
447
|
+
let stdinReadAbandoned = false;
|
|
448
|
+
let EXPLAIN = false;
|
|
449
|
+
let currentRl = null;
|
|
373
450
|
const APPROVE_WORDS = new Set(["y", "yes", "approve", "approved"]);
|
|
374
451
|
const REJECT_WORDS = new Set(["n", "no", "reject", "rejected", "deny", "denied"]);
|
|
375
452
|
export function approvalAnswer(answer) {
|
|
@@ -385,64 +462,176 @@ function ask(question, mute = false) {
|
|
|
385
462
|
return Promise.resolve("");
|
|
386
463
|
return new Promise((resolve) => {
|
|
387
464
|
const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true });
|
|
465
|
+
currentRl = rl;
|
|
388
466
|
if (mute) {
|
|
389
467
|
const anyRl = rl;
|
|
390
468
|
const write = anyRl._writeToOutput?.bind(rl);
|
|
391
469
|
anyRl._writeToOutput = (s) => { if (s.includes(question))
|
|
392
470
|
write?.(s); };
|
|
393
471
|
}
|
|
394
|
-
rl.once("close", () =>
|
|
472
|
+
rl.once("close", () => { if (currentRl === rl)
|
|
473
|
+
currentRl = null; resolve(""); });
|
|
395
474
|
rl.question(question, (answer) => { resolve(answer.trim()); rl.close(); if (mute)
|
|
396
475
|
process.stderr.write("\n"); });
|
|
397
476
|
});
|
|
398
477
|
}
|
|
399
|
-
|
|
478
|
+
function terminalId() {
|
|
479
|
+
for (const k of ["WT_SESSION", "VSCODE_IPC_HOOK_CLI", "TERM_SESSION_ID", "ITERM_SESSION_ID", "TMUX_PANE", "KONSOLE_DBUS_SESSION", "GNOME_TERMINAL_SCREEN", "WINDOWID"]) {
|
|
480
|
+
const v = process.env[k];
|
|
481
|
+
if (v)
|
|
482
|
+
return `${k}=${v}`;
|
|
483
|
+
}
|
|
484
|
+
return "";
|
|
485
|
+
}
|
|
486
|
+
function sessionDir() {
|
|
487
|
+
const base = process.platform === "win32" ? (process.env.LOCALAPPDATA ?? joinPath(homedir(), "AppData", "Local")) : (process.env.XDG_CONFIG_HOME ?? joinPath(homedir(), ".config"));
|
|
488
|
+
return joinPath(base, "decidio");
|
|
489
|
+
}
|
|
490
|
+
const sessionFile = () => joinPath(sessionDir(), "session.json");
|
|
491
|
+
function jwtExpiry(token) {
|
|
400
492
|
try {
|
|
401
|
-
|
|
493
|
+
const p = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
494
|
+
return typeof p?.exp === "number" ? new Date(p.exp * 1000) : null;
|
|
402
495
|
}
|
|
403
496
|
catch {
|
|
404
|
-
err(`✘ refusing to send workspace credentials over plaintext http to ${API}.`);
|
|
405
|
-
err(` Use https (the hosted sandbox is https://decidio-api.onrender.com) or localhost for local dev.`);
|
|
406
497
|
return null;
|
|
407
498
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
499
|
+
}
|
|
500
|
+
function saveSession(token, email) {
|
|
501
|
+
mkdirSync(sessionDir(), { recursive: true });
|
|
502
|
+
const s = { v: 1, api: API, token, email, expiresAt: (jwtExpiry(token) ?? new Date(Date.now() + 30 * 24 * 3600 * 1000)).toISOString(), terminal: terminalId(), createdAt: new Date().toISOString() };
|
|
503
|
+
writeSecretFile(sessionFile(), JSON.stringify(s) + "\n");
|
|
504
|
+
return s;
|
|
505
|
+
}
|
|
506
|
+
function loadSession() {
|
|
507
|
+
const f = sessionFile();
|
|
508
|
+
if (!existsSync(f))
|
|
509
|
+
return null;
|
|
510
|
+
let s;
|
|
511
|
+
try {
|
|
512
|
+
s = JSON.parse(readFileSync(f, "utf8"));
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
err(`⚠ the stored sign-in (${f}) could not be read (${e?.message ?? e}) - removing it; sign in again with: npx @decidio/sdk login`);
|
|
516
|
+
endSession();
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
if (!s || s.v !== 1 || typeof s.token !== "string" || s.api !== API)
|
|
520
|
+
return null;
|
|
521
|
+
if (new Date(String(s.expiresAt)).getTime() <= Date.now()) {
|
|
522
|
+
endSession();
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
const here = terminalId();
|
|
526
|
+
if (s.terminal && s.terminal !== here)
|
|
527
|
+
return null;
|
|
528
|
+
return s;
|
|
529
|
+
}
|
|
530
|
+
function endSession() {
|
|
531
|
+
const f = sessionFile();
|
|
532
|
+
if (!existsSync(f))
|
|
533
|
+
return false;
|
|
534
|
+
try {
|
|
535
|
+
rmSync(f);
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
catch (e) {
|
|
539
|
+
err(`⚠ could not remove the stored sign-in (${f}: ${e?.message ?? e}) - remove it by hand.`);
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
async function loginFlow(purpose, emailArg) {
|
|
544
|
+
log(purpose === "Signing in" ? `Sign in once — kept on this computer for 30 days, or until this terminal closes, logout, or cleanup.` : `${purpose} needs your Decidio workspace sign-in — once: kept on this computer for 30 days, or until this terminal closes, logout, or cleanup.`);
|
|
545
|
+
const email = emailArg ?? await ask(" email: ");
|
|
546
|
+
if (!email) {
|
|
547
|
+
log(" ✘ no email given - re-run when you have it.");
|
|
548
|
+
return null;
|
|
549
|
+
}
|
|
550
|
+
const password = await ask(" password (Enter = emailed sign-in link): ", true);
|
|
551
|
+
if (password) {
|
|
552
|
+
const r = await api("/demo/login", { method: "POST", body: JSON.stringify({ email, password }) }, "");
|
|
553
|
+
if (r.status >= 200 && r.status < 300 && r.body?.token) {
|
|
554
|
+
const s = saveSession(String(r.body.token), email);
|
|
555
|
+
log(` ✔ signed in as ${email} until ${s.expiresAt.slice(0, 10)}${s.terminal ? "" : dim(" (no terminal id here: the sign-in ends on logout or cleanup, not on close)")}`);
|
|
556
|
+
return s.token;
|
|
557
|
+
}
|
|
558
|
+
log(` ✘ password sign-in failed (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}. No problem — the emailed link works for every account:`);
|
|
559
|
+
}
|
|
560
|
+
const req = await api("/demo/login-link", { method: "POST", body: JSON.stringify({ email }) }, "");
|
|
561
|
+
if (!(req.status >= 200 && req.status < 300)) {
|
|
562
|
+
log(` ✘ could not request a sign-in email (${req.status})${req.body?.error ? ` — ${req.body.error}` : ""}. Check the address and re-run.`);
|
|
416
563
|
return null;
|
|
417
564
|
}
|
|
418
|
-
log(
|
|
419
|
-
log(`
|
|
420
|
-
|
|
421
|
-
const email = await ask(" email: ");
|
|
422
|
-
if (email) {
|
|
423
|
-
const password = await ask(" password (Enter to skip): ", true);
|
|
424
|
-
if (password) {
|
|
425
|
-
const r = await api("/demo/login", { method: "POST", body: JSON.stringify({ email, password }) }, "");
|
|
426
|
-
if (r.status >= 200 && r.status < 300 && r.body?.token)
|
|
427
|
-
return r.body.token;
|
|
428
|
-
log(` ✘ password sign-in failed (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}. No problem — the link path works for every account:`);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
log(` Request a sign-in link from the app's sign-in screen, then paste the LINK (or just its token) here.`);
|
|
432
|
-
const pasted = await ask(" link or token: ");
|
|
565
|
+
log(` ✔ sign-in email requested for ${email} — check your inbox and spam; paste the link (or its login= code; single-use, 15 minutes)`);
|
|
566
|
+
log(dim(` invite-only lab: a link arrives only for an invited address — not invited yet? request access: decidioai.com/#early-access`));
|
|
567
|
+
const pasted = await ask(" link or code: ");
|
|
433
568
|
if (!pasted) {
|
|
434
|
-
log(" ✘ nothing pasted — re-run when you have the
|
|
569
|
+
log(" ✘ nothing pasted — re-run when you have the email.");
|
|
435
570
|
return null;
|
|
436
571
|
}
|
|
437
572
|
const m = pasted.match(/[?&]login=([^&\s]+)/);
|
|
438
573
|
const linkToken = m ? decodeURIComponent(m[1]) : pasted;
|
|
439
574
|
const r = await api("/demo/login-consume", { method: "POST", body: JSON.stringify({ token: linkToken }) }, "");
|
|
440
|
-
if (r.status >= 200 && r.status < 300 && r.body?.token)
|
|
441
|
-
|
|
575
|
+
if (r.status >= 200 && r.status < 300 && r.body?.token) {
|
|
576
|
+
const s = saveSession(String(r.body.token), email);
|
|
577
|
+
log(` ✔ signed in as ${email} until ${s.expiresAt.slice(0, 10)}${s.terminal ? "" : dim(" (no terminal id here: the sign-in ends on logout or cleanup, not on close)")}`);
|
|
578
|
+
return s.token;
|
|
579
|
+
}
|
|
442
580
|
log(` ✘ that link didn't work (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}.`);
|
|
443
581
|
log(` Sign-in links are single-use and expire in 15 minutes — request a fresh one and re-run.`);
|
|
444
582
|
return null;
|
|
445
583
|
}
|
|
584
|
+
async function acquireSession(purpose) {
|
|
585
|
+
try {
|
|
586
|
+
assertSafeTransport(API, true);
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
err(`✘ refusing to send workspace credentials over plaintext http to ${API}.`);
|
|
590
|
+
err(` Use https (the hosted sandbox is https://decidio-api.onrender.com) or localhost for local dev.`);
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
if (process.env.DECIDIO_SESSION_TOKEN)
|
|
594
|
+
return process.env.DECIDIO_SESSION_TOKEN;
|
|
595
|
+
const stored = loadSession();
|
|
596
|
+
if (stored)
|
|
597
|
+
return stored.token;
|
|
598
|
+
if (!process.stdin.isTTY) {
|
|
599
|
+
err(`✘ ${purpose} needs a Decidio workspace sign-in, and this terminal can't prompt for one.`);
|
|
600
|
+
err(` Fix: run \`npx @decidio/sdk login\` in an interactive terminal first (the sign-in is stored for this terminal),`);
|
|
601
|
+
err(` or set DECIDIO_SESSION_TOKEN. To mint one by hand: request a sign-in link, then`);
|
|
602
|
+
err(` curl -X POST ${API}/demo/login-consume -H "content-type: application/json" -d '{"token":"<login value>"}' # the response's "token"`);
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
return loginFlow(purpose);
|
|
606
|
+
}
|
|
607
|
+
async function cmdLogin(emailArg) {
|
|
608
|
+
try {
|
|
609
|
+
assertSafeTransport(API, true);
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
err(`✘ refusing to send workspace credentials over plaintext http to ${API}.`);
|
|
613
|
+
return 1;
|
|
614
|
+
}
|
|
615
|
+
const stored = loadSession();
|
|
616
|
+
if (stored) {
|
|
617
|
+
log(`• already signed in${stored.email ? ` as ${stored.email}` : ""} on this terminal until ${stored.expiresAt.slice(0, 10)} - run \`npx @decidio/sdk logout\` first to switch accounts.`);
|
|
618
|
+
return 0;
|
|
619
|
+
}
|
|
620
|
+
const token = await loginFlow("Signing in", emailArg);
|
|
621
|
+
return token ? 0 : 1;
|
|
622
|
+
}
|
|
623
|
+
const TRUTH_SESSION = "The session token itself stays valid until it expires (up to 30 days) — there is no server-side revocation yet";
|
|
624
|
+
function cmdLogout() {
|
|
625
|
+
if (endSession()) {
|
|
626
|
+
log("✔ signed out — the stored sign-in is removed from this computer.");
|
|
627
|
+
log(` ${TRUTH_SESSION}; if you exported DECIDIO_SESSION_TOKEN or pasted the link anywhere else, those copies still work.`);
|
|
628
|
+
}
|
|
629
|
+
else
|
|
630
|
+
log("• no stored sign-in on this machine for this Decidio.");
|
|
631
|
+
if (process.env.DECIDIO_SESSION_TOKEN)
|
|
632
|
+
log(" • DECIDIO_SESSION_TOKEN is still exported in this shell — unset it too (bash: unset DECIDIO_SESSION_TOKEN · PowerShell: Remove-Item Env:DECIDIO_SESSION_TOKEN).");
|
|
633
|
+
return 0;
|
|
634
|
+
}
|
|
446
635
|
async function cmdInit(agentId, opts = {}) {
|
|
447
636
|
if (!assertNotTracked(ENV_PATH))
|
|
448
637
|
return 1;
|
|
@@ -794,6 +983,21 @@ async function cmdApprovals(sub, id, reason, flags = {}) {
|
|
|
794
983
|
log(`Reject: npx @decidio/sdk approvals reject <id> [reason]`);
|
|
795
984
|
return 0;
|
|
796
985
|
}
|
|
986
|
+
export function receiptFileName(id) {
|
|
987
|
+
return `decidio-receipt-${id.replace(/[^A-Za-z0-9_-]/g, (c) => "~" + c.codePointAt(0).toString(16).padStart(2, "0"))}.json`;
|
|
988
|
+
}
|
|
989
|
+
function writeReceiptFile(name, bytes, say = log) {
|
|
990
|
+
if (existsSync(name)) {
|
|
991
|
+
if (readFileSync(name, "utf8") !== bytes) {
|
|
992
|
+
err(`✘ ${name} already exists with DIFFERENT bytes — refusing to overwrite an audit artifact. Move it aside or delete it, then re-run.`);
|
|
993
|
+
return "conflict";
|
|
994
|
+
}
|
|
995
|
+
say(`• ${name} already exists (byte-identical — receipts are immutable); leaving it in place.`);
|
|
996
|
+
return "identical";
|
|
997
|
+
}
|
|
998
|
+
writeFileSync(name, bytes);
|
|
999
|
+
return "written";
|
|
1000
|
+
}
|
|
797
1001
|
async function cmdReceipt(id, jsonMode = false) {
|
|
798
1002
|
if (!id) {
|
|
799
1003
|
err("usage: npx @decidio/sdk receipt <decisionId>");
|
|
@@ -819,19 +1023,13 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
819
1023
|
err(`✘ could not fetch the receipt (${r.status}): ${JSON.stringify(r.body).slice(0, 300)}`);
|
|
820
1024
|
return 1;
|
|
821
1025
|
}
|
|
822
|
-
const name =
|
|
1026
|
+
const name = receiptFileName(id);
|
|
823
1027
|
const nextBytes = JSON.stringify(r.body.credential, null, 2) + "\n";
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
}
|
|
829
|
-
(jsonMode ? err : log)(`• ${name} already exists (byte-identical — receipts are immutable); leaving it in place.`);
|
|
830
|
-
}
|
|
831
|
-
else {
|
|
832
|
-
writeFileSync(name, nextBytes);
|
|
1028
|
+
const wrote = writeReceiptFile(name, nextBytes, jsonMode ? err : log);
|
|
1029
|
+
if (wrote === "conflict")
|
|
1030
|
+
return 1;
|
|
1031
|
+
if (wrote === "written")
|
|
833
1032
|
(jsonMode ? err : log)(`✔ wrote ${name} (downloaded from your workspace — the download is online; VERIFYING it is offline)`);
|
|
834
|
-
}
|
|
835
1033
|
const detail = await api(`/api/records/${encodeURIComponent(id)}`, {}, bearer);
|
|
836
1034
|
const conf = detail.body?.record?.confirmation;
|
|
837
1035
|
const issuer = r.body?.issuer;
|
|
@@ -923,27 +1121,47 @@ function cmdVerify(argsIn) {
|
|
|
923
1121
|
log(l);
|
|
924
1122
|
return ok ? 0 : 1;
|
|
925
1123
|
}
|
|
1124
|
+
function valuedOpt(args, flag) {
|
|
1125
|
+
const i = args.indexOf(flag);
|
|
1126
|
+
if (i >= 0)
|
|
1127
|
+
return args[i + 1];
|
|
1128
|
+
const eq = args.find((x) => x.startsWith(flag + "="));
|
|
1129
|
+
return eq ? eq.slice(flag.length + 1) : undefined;
|
|
1130
|
+
}
|
|
926
1131
|
async function cmdQuickstart(opts = {}) {
|
|
927
1132
|
const t0 = Date.now();
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
log("
|
|
934
|
-
log(
|
|
935
|
-
|
|
936
|
-
|
|
1133
|
+
EXPLAIN = !!opts.explain;
|
|
1134
|
+
if (opts.phone !== undefined && opts.phone !== "app" && opts.phone !== "web") {
|
|
1135
|
+
err(`✘ --phone takes app (default: the QR opens the installed app when there is one) or web (the QR opens the phone web rendering, always).`);
|
|
1136
|
+
return 2;
|
|
1137
|
+
}
|
|
1138
|
+
log(bold("Decidio quickstart") + " — gate an action, route it to a human, execute on approval, hold the proof.");
|
|
1139
|
+
log(` Target: ${API}${process.env.DECIDIO_API_URL ? "" : dim(" (the hosted synthetic sandbox — set DECIDIO_API_URL to use your own)")}`);
|
|
1140
|
+
hostAnnounced = true;
|
|
1141
|
+
if (EXPLAIN) {
|
|
1142
|
+
log(" This tour will: register one synthetic agent · mint a 7-DAY floor-limited token ·");
|
|
1143
|
+
log(" write .env (or .env.quickstart), .decidio-token-journal (revocation ids, no secrets) and .gitignore");
|
|
1144
|
+
log(" entries (incl. .decidio-pending/, the durable store your real agent will use) · save the receipt");
|
|
1145
|
+
log(" as decidio-receipt-<id>.json here (shareable: action, amount, policy, the human's reason - no secrets; it stays)");
|
|
1146
|
+
log(" · create one synthetic decision and its sealed record (records are permanent by design) · wait up to");
|
|
1147
|
+
log(" 5 minutes for your y (approve) or n (reject) at the human step. Cleanup offer at the end.");
|
|
1148
|
+
}
|
|
1149
|
+
else {
|
|
1150
|
+
log(dim(" Creates here: a synthetic agent + 7-day token (.env), .decidio-token-journal (revocation ids, no secrets), .gitignore entries incl."));
|
|
1151
|
+
log(dim(" .decidio-pending/ (the durable store), the receipt as decidio-receipt-<id>.json, one sealed record (permanent). --explain for the long form."));
|
|
1152
|
+
}
|
|
937
1153
|
log("");
|
|
938
1154
|
const session = await acquireSession("The quickstart");
|
|
939
1155
|
if (!session)
|
|
940
1156
|
return 1;
|
|
941
|
-
const
|
|
1157
|
+
const prior = readEnvFile();
|
|
1158
|
+
const envFile = ["DECIDIO_AGENT_KEY", "DECIDIO_AGENT_ID", "DECIDIO_AGENT_DID", "DECIDIO_API_TOKEN"].some((k) => (prior[k] ?? "").trim()) ? ".env.quickstart" : ENV_PATH;
|
|
942
1159
|
if (!assertNotTracked(envFile))
|
|
943
1160
|
return 1;
|
|
944
1161
|
ensureGitignore();
|
|
945
1162
|
let agentId = `quickstart-${randomBytes(5).toString("hex")}`;
|
|
946
|
-
|
|
1163
|
+
if (EXPLAIN)
|
|
1164
|
+
log(` Registering agent "${agentId}"…`);
|
|
947
1165
|
const { did, privateKeyBase64 } = generateKeypair();
|
|
948
1166
|
let reg = await api("/api/agents/register", { method: "POST", body: JSON.stringify({ agentId, did }) }, session);
|
|
949
1167
|
for (let attempt = 0; reg.status === 409 && attempt < 3; attempt++) {
|
|
@@ -1039,7 +1257,7 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1039
1257
|
err(`✘ could not write ${envFile} (${e?.message ?? e}). The tour token minted a moment ago is LIVE and the server returned no revocation id - find and revoke it: npx @decidio/sdk tokens list --mine`);
|
|
1040
1258
|
return 1;
|
|
1041
1259
|
}
|
|
1042
|
-
log(` ✔
|
|
1260
|
+
log(` ✔ .env already holds your agent — this tour uses ${envFile}`);
|
|
1043
1261
|
}
|
|
1044
1262
|
else {
|
|
1045
1263
|
try {
|
|
@@ -1061,14 +1279,20 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1061
1279
|
await quickstartCleanup(session, envFile).catch(() => { });
|
|
1062
1280
|
return code;
|
|
1063
1281
|
};
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1282
|
+
block("①", "Register a synthetic agent", [
|
|
1283
|
+
`${bold("agent")} ${agentId}`,
|
|
1284
|
+
`${bold("key")} generated locally · only the public half was registered`,
|
|
1285
|
+
`${bold("token")} 7-day, floor-limited${mint.body.expiresAt ? ` · expires ${String(mint.body.expiresAt).slice(0, 10)}` : ""} · written to ${envFile}`,
|
|
1286
|
+
]);
|
|
1287
|
+
if (EXPLAIN) {
|
|
1288
|
+
log(` The tour runs exactly this, the BLOCKING primitive (your real agent uses guard.protect instead:`);
|
|
1289
|
+
log(` durable by default, resumed by a worker — the README shows both):`);
|
|
1290
|
+
log("");
|
|
1291
|
+
log(` const payInvoice = withApproval(payInvoiceRaw,`);
|
|
1292
|
+
log(` (inv) => ({ action: "payInvoice", amount: inv.amount, scope: "Invoice" }),`);
|
|
1293
|
+
log(` { ...config, pollTimeoutMs: 300_000 }); // waits up to 5 minutes for the human`);
|
|
1294
|
+
log("");
|
|
1295
|
+
}
|
|
1072
1296
|
let decisionId = "";
|
|
1073
1297
|
const cfg = {
|
|
1074
1298
|
apiUrl: API, agentId, apiToken: mint.body.token, agentKey: privateKeyBase64,
|
|
@@ -1097,30 +1321,82 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1097
1321
|
err("✘ the gate did not route within 30s — check connectivity (npx @decidio/sdk doctor) and re-run.");
|
|
1098
1322
|
return failTour(1);
|
|
1099
1323
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1324
|
+
block("②", "Gate a demo action", [
|
|
1325
|
+
`${bold("call")} withApproval(payInvoiceRaw, (inv) => ({ action: "payInvoice", amount: inv.amount, scope: "Invoice" }), { pollTimeoutMs: 300_000 }) ${dim("// blocking here; your agent's guard.protect is durable")}`,
|
|
1326
|
+
`${bold("routed")} $86,000 payInvoice ROUTED to a human (decision ${decisionId}) — nothing executes until someone decides ${dim("(routed, not a BLOCK: a human can still say yes; a block no one can override)")}`,
|
|
1327
|
+
]);
|
|
1328
|
+
if (EXPLAIN) {
|
|
1329
|
+
log(` This agent has no auto-approve rule, so the policy ROUTES it to a person (routing-by-default). That is`);
|
|
1330
|
+
log(` distinct from a BLOCK, which no human can override: a block is a named rule, or the universal backstop`);
|
|
1331
|
+
log(` when no active policy governs agents at all.`);
|
|
1332
|
+
log("");
|
|
1333
|
+
}
|
|
1105
1334
|
const interactive = process.stdin.isTTY;
|
|
1106
1335
|
let verdict = "approve";
|
|
1336
|
+
let decidedOutside = null;
|
|
1337
|
+
let denialVerified = false;
|
|
1107
1338
|
if (interactive) {
|
|
1339
|
+
const handoff = await api("/demo/session-link", { method: "POST", body: JSON.stringify({ decisionId, ...(opts.phone === "web" ? { phone: "web" } : {}) }) }, session, true);
|
|
1340
|
+
const doorLink = handoff.status >= 200 && handoff.status < 300 && typeof handoff.body?.link === "string" ? String(handoff.body.link) : null;
|
|
1341
|
+
const qr = doorLink && typeof handoff.body?.qr === "string" ? String(handoff.body.qr) : null;
|
|
1342
|
+
const plainLink = `${APP_URL}/queue?focus=${encodeURIComponent(decisionId)}`;
|
|
1343
|
+
const rows = [
|
|
1344
|
+
`${bold("here")} Type y to approve or n to reject, then Enter ${dim("(waits up to 5 minutes)")}`,
|
|
1345
|
+
`${bold("web app")} ${link(doorLink ?? plainLink)} ${dim(doorLink ? "(one-time sign-in · 5 minutes)" : "(sign in there if it asks)")}`,
|
|
1346
|
+
];
|
|
1347
|
+
if (qr) {
|
|
1348
|
+
rows.push(`${bold("your phone")} scan — the same one-time sign-in${opts.phone === "web" ? ", in the phone browser (--phone web)" : ": the Decidio app if it is installed, the browser if not"}`);
|
|
1349
|
+
for (const l of qr.split("\n"))
|
|
1350
|
+
if (l.trim().length)
|
|
1351
|
+
rows.push(" " + l);
|
|
1352
|
+
}
|
|
1353
|
+
else if (doorLink)
|
|
1354
|
+
rows.push(`${bold("your phone")} ${dim("the link above works on a phone too (no QR came back this time)")}`);
|
|
1355
|
+
rows.push(dim("whichever answers first wins — the terminal follows the web app or your phone."));
|
|
1356
|
+
block("③", "Your call", rows, "choice");
|
|
1357
|
+
let settledState = null;
|
|
1358
|
+
const settledP = running.then(() => { settledState = "approved"; }, (e) => { settledState = e instanceof DecidioRejectedError ? "rejected" : "other"; });
|
|
1108
1359
|
verdict = "again";
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1360
|
+
const promptP = (async () => {
|
|
1361
|
+
for (let attempt = 0; attempt < 3 && verdict === "again" && !settledState; attempt++) {
|
|
1362
|
+
const answer = await ask(attempt === 0 ? ` › ` : ` Please answer y (approve) or n (reject): `);
|
|
1363
|
+
if (settledState)
|
|
1364
|
+
break;
|
|
1365
|
+
verdict = approvalAnswer(answer);
|
|
1366
|
+
if (process.stdin.readableEnded || process.stdin.destroyed) {
|
|
1367
|
+
verdict = "again";
|
|
1368
|
+
break;
|
|
1369
|
+
}
|
|
1117
1370
|
}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1371
|
+
})();
|
|
1372
|
+
await Promise.race([promptP, settledP]);
|
|
1373
|
+
if (settledState) {
|
|
1374
|
+
currentRl?.close();
|
|
1375
|
+
stdinReadAbandoned = true;
|
|
1376
|
+
await promptP;
|
|
1377
|
+
log("");
|
|
1378
|
+
}
|
|
1379
|
+
if (settledState === "approved" || settledState === "rejected") {
|
|
1380
|
+
decidedOutside = settledState;
|
|
1381
|
+
verdict = settledState === "approved" ? "approve" : "reject";
|
|
1382
|
+
}
|
|
1383
|
+
else if (settledState === "other")
|
|
1384
|
+
verdict = "again";
|
|
1385
|
+
if (verdict === "again" && !(earlyError instanceof DecidioTimeoutError)) {
|
|
1120
1386
|
log(`③ No decision was made (no y or n answer), so the tour stops here rather than guess.`);
|
|
1121
1387
|
log(` The request is still ROUTED and waiting in your queue - approve or reject it there, or re-run the tour.`);
|
|
1122
1388
|
log(` NOTE: this tour's credentials are still live in ${envFile} (a 7-day token + the agent's key). Once you`);
|
|
1123
1389
|
log(` have decided - or if you abandon the tour - remove them: npx @decidio/sdk quickstart cleanup`);
|
|
1390
|
+
log("");
|
|
1391
|
+
sessionReminder();
|
|
1392
|
+
process.exit(3);
|
|
1393
|
+
}
|
|
1394
|
+
if (earlyError instanceof DecidioTimeoutError) {
|
|
1395
|
+
log(`③ the tour's blocking call gave up after 5 minutes while waiting for your answer, so your answer is NOT posted:`);
|
|
1396
|
+
log(` the request is still ROUTED in your queue - decide it there, or re-run the tour and answer within 5 minutes.`);
|
|
1397
|
+
log(` NOTE: this tour's credentials are still live in ${envFile} - remove them: npx @decidio/sdk quickstart cleanup`);
|
|
1398
|
+
log("");
|
|
1399
|
+
sessionReminder();
|
|
1124
1400
|
process.exit(3);
|
|
1125
1401
|
}
|
|
1126
1402
|
}
|
|
@@ -1147,17 +1423,22 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1147
1423
|
log(` NOTE: this tour's credentials are still live in ${envFile} (a 7-day token + the agent's`);
|
|
1148
1424
|
log(` key), because the decision above is still waiting for you. Once you have approved it —`);
|
|
1149
1425
|
log(` or if you abandon the tour — remove them: npx @decidio/sdk quickstart cleanup`);
|
|
1426
|
+
log("");
|
|
1427
|
+
sessionReminder();
|
|
1150
1428
|
process.exit(3);
|
|
1151
1429
|
}
|
|
1152
1430
|
const rejected = verdict === "reject";
|
|
1153
1431
|
if (rejected) {
|
|
1154
|
-
const rj = await api(`/api/decisions/${encodeURIComponent(decisionId)}/reject`, { method: "POST", body: JSON.stringify({ reason: "quickstart: rejected by the approver" }) }, session);
|
|
1432
|
+
const rj = decidedOutside ? { status: 200, body: { alreadyResolved: true, sealed: true } } : await api(`/api/decisions/${encodeURIComponent(decisionId)}/reject`, { method: "POST", body: JSON.stringify({ reason: "quickstart: rejected by the approver" }) }, session);
|
|
1155
1433
|
if (!(rj.status >= 200 && rj.status < 300)) {
|
|
1156
1434
|
err(`✘ reject failed (${rj.status}): ${JSON.stringify(rj.body)}`);
|
|
1157
1435
|
return failTour(1);
|
|
1158
1436
|
}
|
|
1437
|
+
if (decidedOutside)
|
|
1438
|
+
log(` ✔ rejected in the web app (or on your phone) — this terminal saw it and followed`);
|
|
1159
1439
|
const decidedInApp = rj.body?.alreadyResolved === true;
|
|
1160
|
-
|
|
1440
|
+
if (!decidedOutside)
|
|
1441
|
+
log(decidedInApp ? ` • this decision was already decided from your app — the tour follows that decision` : ` ✔ rejected`);
|
|
1161
1442
|
let leaked;
|
|
1162
1443
|
let refused;
|
|
1163
1444
|
try {
|
|
@@ -1175,8 +1456,36 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1175
1456
|
log(` ✔ your app APPROVED it first, so that decision stood and your function ran: ${JSON.stringify(leaked)}. Re-run the tour to watch a rejection.`);
|
|
1176
1457
|
}
|
|
1177
1458
|
else if (refused instanceof DecidioRejectedError) {
|
|
1178
|
-
log(` ✔ your function did NOT run — the wrapper refused: ${refused.message}`);
|
|
1179
|
-
|
|
1459
|
+
log(` ✔ your function did NOT run — the wrapper refused: ${refused.message}${EXPLAIN ? "" : dim(" (a human said no, and nothing happened)")}`);
|
|
1460
|
+
if (EXPLAIN)
|
|
1461
|
+
log(` That is the product: a human said no, and nothing happened.`);
|
|
1462
|
+
if (EXPLAIN)
|
|
1463
|
+
log(" Downloading the sealed record of the DENIAL and verifying it OFFLINE…");
|
|
1464
|
+
const rec = await api(`/api/records/${encodeURIComponent(decisionId)}/verify`, {}, session);
|
|
1465
|
+
if (rec.status >= 200 && rec.status < 300 && rec.body?.credential) {
|
|
1466
|
+
const fname = receiptFileName(decisionId);
|
|
1467
|
+
const wrote = writeReceiptFile(fname, JSON.stringify(rec.body.credential, null, 2) + "\n");
|
|
1468
|
+
if (wrote === "conflict")
|
|
1469
|
+
return failTour(1);
|
|
1470
|
+
const issuer = rec.body?.issuer;
|
|
1471
|
+
const v = verifyReceipt(rec.body.credential, issuer);
|
|
1472
|
+
const kindLine = `${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ", PINNED to your workspace issuer" : ""}) · record kind: ${recordKindOf(rec.body.credential) ?? "?"}`;
|
|
1473
|
+
if (!v.valid || !issuer) {
|
|
1474
|
+
log(` ${kindLine}`);
|
|
1475
|
+
err(` ✘ the denial's receipt did not verify PINNED (${!issuer ? "no issuer DID from the server" : v.reason ?? "no reason given"}) — the tour did not complete.`);
|
|
1476
|
+
return failTour(1);
|
|
1477
|
+
}
|
|
1478
|
+
block("④", "The record", [
|
|
1479
|
+
`✔ VALID — ${fname} ${dim("(signature + content binding, offline, PINNED to your workspace issuer)")}`,
|
|
1480
|
+
`${bold("decision")} payInvoice $86,000 · REJECTED · routed to a human by policy (no auto-approve rule for this agent)`,
|
|
1481
|
+
`${bold("record")} record kind: ${recordKindOf(rec.body.credential) ?? "?"} ${dim("— the denial is sealed too")}`,
|
|
1482
|
+
`${bold("issuer")} ${issuer} ${dim("· your trust anchor: verify --issuer <did> <receipt.json>")}`,
|
|
1483
|
+
], "bad");
|
|
1484
|
+
denialVerified = true;
|
|
1485
|
+
}
|
|
1486
|
+
else {
|
|
1487
|
+
log(` • no sealed record was issued for this rejection (${rec.status}) — the denial is on the decision in your queue.`);
|
|
1488
|
+
}
|
|
1180
1489
|
}
|
|
1181
1490
|
else {
|
|
1182
1491
|
err(`✘ the gated call ended with an error rather than a clean refusal: ${refused?.message ?? refused} — nothing ran, but the tour did not complete.`);
|
|
@@ -1184,21 +1493,29 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1184
1493
|
}
|
|
1185
1494
|
log("");
|
|
1186
1495
|
const secsR = Math.round((Date.now() - t0) / 1000);
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1496
|
+
if (ranOnAppApproval)
|
|
1497
|
+
log(`Done in ${secsR}s: gated → routed to a human → decided in your app (approved) → your function ran once.`);
|
|
1498
|
+
else {
|
|
1499
|
+
block("✔", `Done in ${secsR}s`, [
|
|
1500
|
+
`${bold("gated")} payInvoice $86,000 — ROUTED to a human as ${decisionId}`,
|
|
1501
|
+
`${bold("decided")} REJECTED ${decidedOutside ? "in the web app (or on your phone)" : "by you, here"}`,
|
|
1502
|
+
`${bold("executed")} nothing — your function never ran`,
|
|
1503
|
+
`${bold("record")} ${denialVerified ? "the DENIAL is sealed too — VALID, verified offline (④ above)" : "none was issued for this rejection (④ above)"}`,
|
|
1504
|
+
dim("gated → routed to a human → REJECTED → your function never ran"),
|
|
1505
|
+
], "bad");
|
|
1191
1506
|
log(`Re-run the tour and answer y to watch the approval half, with its offline-verifiable receipt.`);
|
|
1507
|
+
}
|
|
1192
1508
|
log("");
|
|
1193
|
-
|
|
1509
|
+
sessionReminder();
|
|
1510
|
+
log(`${bold("Where next")} your queue ${link(APP_URL)} · README "Production mode: durable async resume" ${dim("· built by OmniTwin — decidioai.com/#early-access")}`);
|
|
1194
1511
|
}
|
|
1195
1512
|
else {
|
|
1196
|
-
const ap = await api(`/api/decisions/${encodeURIComponent(decisionId)}/approve`, { method: "POST", body: JSON.stringify({ reason: opts.autoApproveDemo && !interactive ? "quickstart --auto-approve-demo (automated)" : "quickstart" }) }, session);
|
|
1513
|
+
const ap = decidedOutside ? { status: 200, body: { alreadyResolved: true } } : await api(`/api/decisions/${encodeURIComponent(decisionId)}/approve`, { method: "POST", body: JSON.stringify({ reason: opts.autoApproveDemo && !interactive ? "quickstart --auto-approve-demo (automated)" : "quickstart" }) }, session);
|
|
1197
1514
|
if (!(ap.status >= 200 && ap.status < 300)) {
|
|
1198
1515
|
err(`✘ approve failed (${ap.status}): ${JSON.stringify(ap.body)}`);
|
|
1199
1516
|
return failTour(1);
|
|
1200
1517
|
}
|
|
1201
|
-
log(` ✔ approved${ap.body?.alreadyResolved ? " (already decided from your app — the tour follows that decision)" : ""}`);
|
|
1518
|
+
log(decidedOutside ? ` ✔ approved in the web app (or on your phone) — this terminal saw it and followed` : ` ✔ approved${ap.body?.alreadyResolved ? " (already decided from your app — the tour follows that decision)" : ""}`);
|
|
1202
1519
|
let done;
|
|
1203
1520
|
try {
|
|
1204
1521
|
done = await running;
|
|
@@ -1211,21 +1528,24 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1211
1528
|
err(`✘ the gated call did not complete: ${e?.message ?? e}`);
|
|
1212
1529
|
return failTour(1);
|
|
1213
1530
|
}
|
|
1214
|
-
log(` ✔ your function executed AFTER approval: ${JSON.stringify(done.value)}`);
|
|
1215
|
-
if (done.confirmation?.evidenceTier)
|
|
1216
|
-
log(`
|
|
1217
|
-
|
|
1218
|
-
|
|
1531
|
+
log(` ✔ your function executed AFTER approval: ${JSON.stringify(done.value)}${done.confirmation?.evidenceTier ? ` · confirmation.evidenceTier = ${done.confirmation.evidenceTier}` : ""}`);
|
|
1532
|
+
if (EXPLAIN && done.confirmation?.evidenceTier)
|
|
1533
|
+
log(` (the call itself told you what was recorded — the wrapper's signed report)`);
|
|
1534
|
+
if (EXPLAIN)
|
|
1535
|
+
log(" Downloading the sealed Authority Receipt and verifying it OFFLINE…");
|
|
1219
1536
|
const rec = await api(`/api/records/${encodeURIComponent(decisionId)}/verify`, {}, session);
|
|
1220
1537
|
if (!(rec.status >= 200 && rec.status < 300) || !rec.body?.credential) {
|
|
1221
1538
|
err(`✘ receipt fetch failed (${rec.status})`);
|
|
1222
1539
|
return failTour(1);
|
|
1223
1540
|
}
|
|
1224
|
-
const fname =
|
|
1225
|
-
|
|
1541
|
+
const fname = receiptFileName(decisionId);
|
|
1542
|
+
if (writeReceiptFile(fname, JSON.stringify(rec.body.credential, null, 2) + "\n") === "conflict")
|
|
1543
|
+
return failTour(1);
|
|
1226
1544
|
const issuer = rec.body?.issuer;
|
|
1227
1545
|
const v = verifyReceipt(rec.body.credential, issuer);
|
|
1228
|
-
|
|
1546
|
+
const kindLine = `${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ", PINNED to your workspace issuer" : ""}) · record kind: ${recordKindOf(rec.body.credential) ?? "?"}`;
|
|
1547
|
+
if (!v.valid || !issuer)
|
|
1548
|
+
log(` ${kindLine}`);
|
|
1229
1549
|
if (!v.valid) {
|
|
1230
1550
|
err(` ✘ the receipt did not verify (${v.reason ?? "no reason given"}) — the tour did not complete.`);
|
|
1231
1551
|
return failTour(1);
|
|
@@ -1238,29 +1558,33 @@ async function cmdQuickstart(opts = {}) {
|
|
|
1238
1558
|
err(` ✘ the tour does not count an UNPINNED check as success.`);
|
|
1239
1559
|
return failTour(1);
|
|
1240
1560
|
}
|
|
1241
|
-
if (issuer)
|
|
1242
|
-
log(` Keep that DID: it is your trust anchor — npx @decidio/sdk verify --issuer ${issuer} <receipt.json>`);
|
|
1243
1561
|
const detail = await api(`/api/records/${encodeURIComponent(decisionId)}`, {}, session);
|
|
1244
1562
|
const tier = detail.body?.record?.confirmation?.status;
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1563
|
+
block("④", "The receipt", [
|
|
1564
|
+
`✔ VALID — ${fname} ${dim("(signature + content binding, offline, PINNED to your workspace issuer)")}`,
|
|
1565
|
+
`${bold("decision")} payInvoice $86,000 · APPROVED · routed to a human by policy (no auto-approve rule for this agent)`,
|
|
1566
|
+
`${bold("record")} record kind: ${recordKindOf(rec.body.credential) ?? "?"}${tier ? ` · evidence: ${tier}` : ""} ${dim("— the receipt seals the DECISION; execution evidence lives on the record")}`,
|
|
1567
|
+
`${bold("issuer")} ${issuer} ${dim("· your trust anchor: verify --issuer <did> <receipt.json>")}`,
|
|
1568
|
+
], "good");
|
|
1248
1569
|
const secs = Math.round((Date.now() - t0) / 1000);
|
|
1249
|
-
|
|
1570
|
+
block("✔", `Done in ${secs}s`, [
|
|
1571
|
+
`${bold("gated")} payInvoice $86,000 — ROUTED to a human as ${decisionId}`,
|
|
1572
|
+
`${bold("decided")} ${decidedOutside ? "approved in the web app (or on your phone)" : opts.autoApproveDemo ? "approved by --auto-approve-demo (automated, and labeled so on the record)" : "approved by you, here"}`,
|
|
1573
|
+
`${bold("executed")} your function ran once, AFTER approval`,
|
|
1574
|
+
`${bold("receipt")} VALID — verified offline, pinned to your workspace issuer (④ above)`,
|
|
1575
|
+
dim("gated → routed to a human → approved → executed once → a receipt you can verify offline"),
|
|
1576
|
+
], "good");
|
|
1250
1577
|
log("");
|
|
1251
|
-
log("Take it to your real agent — the .env here already works:");
|
|
1578
|
+
log(bold("Take it to your real agent — the .env here already works:"));
|
|
1252
1579
|
log(` import { guard } from "@decidio/sdk";`);
|
|
1253
|
-
log(` const doIt = guard.protect(doItRaw, (a) => ({ action: "yourAction", amount: a.amount, scope: "YourScope" }))
|
|
1254
|
-
log(` // production is DURABLE mode (default) + one worker: guard.worker(); // README has the full pattern`);
|
|
1255
|
-
log("");
|
|
1256
|
-
log(`Before launching agent code from THIS shell, unset DECIDIO_SESSION_TOKEN if you exported it: child processes`);
|
|
1257
|
-
log(`inherit exported variables, and that credential is your workspace login, not the agent's.`);
|
|
1580
|
+
log(` const doIt = guard.protect(doItRaw, (a) => ({ action: "yourAction", amount: a.amount, scope: "YourScope" }));${dim(" // durable by default")}`);
|
|
1258
1581
|
log("");
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1582
|
+
if (EXPLAIN) {
|
|
1583
|
+
log(` The README's seven-step flow is this same blocking flow in your own code; if you keep this agent, its .env`);
|
|
1584
|
+
log(` already works here, so skip that flow's init step. Production is DURABLE mode (default) + one worker.`);
|
|
1585
|
+
}
|
|
1586
|
+
sessionReminder();
|
|
1587
|
+
log(`${bold("Where next")} your queue ${link(APP_URL)} · README "Production mode: durable async resume" ${dim("· built by OmniTwin — decidioai.com/#early-access")}`);
|
|
1264
1588
|
}
|
|
1265
1589
|
let doCleanup = false;
|
|
1266
1590
|
if (interactive) {
|
|
@@ -1437,6 +1761,10 @@ async function quickstartCleanup(session, envFile) {
|
|
|
1437
1761
|
log(` pending and you do not want the history, remove it:`);
|
|
1438
1762
|
log(` ${process.platform === "win32" ? "Remove-Item -Recurse .decidio-pending" : "rm -r .decidio-pending"}`);
|
|
1439
1763
|
}
|
|
1764
|
+
if (endSession())
|
|
1765
|
+
log(` ✔ signed out — the stored sign-in is removed from this computer (the token itself stays valid until it expires; npx @decidio/sdk login to sign in again).`);
|
|
1766
|
+
else if (process.env.DECIDIO_SESSION_TOKEN)
|
|
1767
|
+
log(` • DECIDIO_SESSION_TOKEN is still exported in this shell — unset it when you are done.`);
|
|
1440
1768
|
return 0;
|
|
1441
1769
|
}
|
|
1442
1770
|
async function cmdTokens(sub, jti, mineOnly = false) {
|
|
@@ -1536,11 +1864,14 @@ function cmdDev(target, port) {
|
|
|
1536
1864
|
res.end(JSON.stringify({ error: e?.message }));
|
|
1537
1865
|
}
|
|
1538
1866
|
});
|
|
1539
|
-
}).listen(port);
|
|
1867
|
+
}).listen(port, "127.0.0.1");
|
|
1540
1868
|
return 0;
|
|
1541
1869
|
}
|
|
1542
1870
|
const USAGE = `decidio — the Decidio SDK command line
|
|
1543
1871
|
|
|
1872
|
+
The wrapper IS the SDK: guard.protect(fn, describe) ships in this package (import { guard } from "@decidio/sdk").
|
|
1873
|
+
This command line is the tour, the sign-in, and the token admin around it.
|
|
1874
|
+
|
|
1544
1875
|
New here? See the whole thing work in about two minutes:
|
|
1545
1876
|
|
|
1546
1877
|
npx @decidio/sdk quickstart
|
|
@@ -1550,23 +1881,37 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
1550
1881
|
hosted record — the receipt seals who authorized what, not what ran afterward). Guided and
|
|
1551
1882
|
explicit — this CLI never acts, signs you in, or writes files unless you ask it to.
|
|
1552
1883
|
|
|
1884
|
+
THE TOUR
|
|
1553
1885
|
npx @decidio/sdk quickstart the guided two-minute tour: gate → approve → execute → proof
|
|
1554
|
-
(interactive:
|
|
1886
|
+
(interactive: y approves, n rejects - Enter is not an answer. Headless it stops
|
|
1555
1887
|
at the routed decision; --auto-approve-demo for CI, labeled
|
|
1556
|
-
automated; ends with a keep-or-cleanup choice
|
|
1888
|
+
automated; ends with a keep-or-cleanup choice; --explain prints
|
|
1889
|
+
the long narration of each step; --phone web makes the QR open
|
|
1890
|
+
the phone web rendering even when the Decidio app is installed)
|
|
1557
1891
|
npx @decidio/sdk quickstart cleanup revoke the tour's token + delete its local credentials
|
|
1892
|
+
(and end the stored sign-in)
|
|
1558
1893
|
npx @decidio/sdk quickstart --auto-approve-demo headless tour; cleans up after itself
|
|
1559
1894
|
(add --keep-agent to KEEP the tour agent + token)
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1895
|
+
|
|
1896
|
+
SIGN IN (once)
|
|
1897
|
+
npx @decidio/sdk login [email] sign in ONCE: the CLI emails you a link, you paste it; the sign-in
|
|
1898
|
+
is kept on this computer 30 days, or until this terminal closes, logout, or cleanup
|
|
1899
|
+
npx @decidio/sdk logout forget the stored sign-in on this computer (the token itself stays valid
|
|
1900
|
+
until it expires — there is no server-side revocation yet)
|
|
1901
|
+
|
|
1902
|
+
YOUR AGENT AND ITS TOKENS
|
|
1564
1903
|
npx @decidio/sdk init [agentId] sign in, register the agent, mint its API token, write .env
|
|
1565
1904
|
--unique append a random suffix (collision-proof samples)
|
|
1566
1905
|
--force replace a different agent already in this .env
|
|
1567
1906
|
(its .env is backed up first)
|
|
1568
1907
|
--expires-days N agent-token lifetime (default 90)
|
|
1569
1908
|
npx @decidio/sdk doctor check config, connectivity, and what your token can do
|
|
1909
|
+
npx @decidio/sdk tokens list [--mine] agent tokens with status (active/expired/REVOKED) by
|
|
1910
|
+
jti — token values are never re-shown; the list is
|
|
1911
|
+
workspace-wide, --mine narrows to this directory's agent
|
|
1912
|
+
npx @decidio/sdk tokens revoke <jti> kill a token now instead of waiting out its lifetime
|
|
1913
|
+
|
|
1914
|
+
APPROVALS AND RECEIPTS
|
|
1570
1915
|
npx @decidio/sdk approvals [--json] [--mine] list pending agent approvals
|
|
1571
1916
|
npx @decidio/sdk approvals approve <id> [reason]
|
|
1572
1917
|
npx @decidio/sdk approvals reject <id> [reason]
|
|
@@ -1576,21 +1921,121 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
1576
1921
|
If it starts with a dash, put it after -- or use --reason:
|
|
1577
1922
|
... approve <id> -- "-50% cut, per the board"
|
|
1578
1923
|
... approve <id> --reason "-50% cut, per the board"
|
|
1579
|
-
npx @decidio/sdk receipt <id>
|
|
1924
|
+
npx @decidio/sdk receipt <id> [--json] download a sealed decision's Authority Receipt (.json)
|
|
1580
1925
|
npx @decidio/sdk verify [--issuer <did>] <receipt.json> [more...] verify receipts OFFLINE
|
|
1926
|
+
|
|
1927
|
+
DEVELOPMENT
|
|
1581
1928
|
npx @decidio/sdk dev [--target URL] [--port N] local relay for the resume webhook
|
|
1582
1929
|
npx @decidio/sdk --version print the CLI version
|
|
1583
1930
|
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
(
|
|
1931
|
+
NOTES
|
|
1932
|
+
Every NETWORK command talks to the hosted sandbox (https://decidio-api.onrender.com) unless
|
|
1933
|
+
DECIDIO_API_URL says otherwise; the target host is announced on stderr each run. "verify" is
|
|
1934
|
+
fully offline — it touches no network and reads no .env.
|
|
1935
|
+
Update check: network commands ask the public npm registry once a day whether a newer version exists. If so,
|
|
1936
|
+
an interactive run asks "Upgrade now? [y/n]" first (y upgrades the way you installed - global, or in the
|
|
1937
|
+
project for a local install; under npx it prints the @latest form - and re-runs your command; n exits); a headless
|
|
1938
|
+
run prints the notice and continues (DECIDIO_REQUIRE_LATEST=1 makes it exit). DECIDIO_NO_UPDATE_CHECK=1 skips
|
|
1939
|
+
the check. Nothing is sent to Decidio; verify never checks.
|
|
1940
|
+
Help: --help (or -h) after any command prints this page - one page for every command.
|
|
1941
|
+
Exit codes: 0 success · 1 failure or honest no-op (e.g. replay refused) · 2 usage error ·
|
|
1942
|
+
3 quickstart stopped at the human moment (headless: routed, but nothing executed and no
|
|
1943
|
+
receipt written — pass --auto-approve-demo for a run that completes unattended).
|
|
1944
|
+
Errors, and usage shown on ERROR paths, go to stderr; stdout carries results only (safe to
|
|
1945
|
+
pipe). Asked-for help — including this bare invocation — IS a result, so it prints here.
|
|
1946
|
+
Docs: the README that shipped with this package — node_modules/@decidio/sdk/README.md
|
|
1947
|
+
(quickstart, durable resume, workers, adapters).`;
|
|
1948
|
+
const GATED_CMDS = new Set(["quickstart", "init", "login", "doctor", "approvals", "receipt", "tokens"]);
|
|
1949
|
+
function semverNewer(a, b) {
|
|
1950
|
+
const pa = a.split(".").map((x) => parseInt(x, 10)), pb = b.split(".").map((x) => parseInt(x, 10));
|
|
1951
|
+
if (pa.length < 3 || pb.length < 3 || pa.some((x) => Number.isNaN(x)) || pb.some((x) => Number.isNaN(x)))
|
|
1952
|
+
return false;
|
|
1953
|
+
for (let i = 0; i < 3; i++)
|
|
1954
|
+
if (pa[i] !== pb[i])
|
|
1955
|
+
return pa[i] > pb[i];
|
|
1956
|
+
return false;
|
|
1957
|
+
}
|
|
1958
|
+
async function latestPublished() {
|
|
1959
|
+
const cache = joinPath(sessionDir(), "update-check.json");
|
|
1960
|
+
try {
|
|
1961
|
+
const c = JSON.parse(readFileSync(cache, "utf8"));
|
|
1962
|
+
if (c && typeof c.latest === "string" && Date.now() - Number(c.checkedAt) < 24 * 3600 * 1000)
|
|
1963
|
+
return c.latest;
|
|
1964
|
+
}
|
|
1965
|
+
catch { }
|
|
1966
|
+
const reg = (SHELL_ENV.DECIDIO_UPDATE_REGISTRY ?? "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
1967
|
+
try {
|
|
1968
|
+
const ctl = new AbortController();
|
|
1969
|
+
const t = setTimeout(() => ctl.abort(), 2000);
|
|
1970
|
+
const r = await fetch(`${reg}/@decidio%2fsdk/latest`, { signal: ctl.signal });
|
|
1971
|
+
clearTimeout(t);
|
|
1972
|
+
if (!r.ok)
|
|
1973
|
+
return null;
|
|
1974
|
+
const body = await r.json();
|
|
1975
|
+
const latest = typeof body?.version === "string" && /^\d+\.\d+\.\d+$/.test(body.version) ? body.version : null;
|
|
1976
|
+
if (latest) {
|
|
1977
|
+
try {
|
|
1978
|
+
mkdirSync(sessionDir(), { recursive: true });
|
|
1979
|
+
writeFileSync(cache, JSON.stringify({ checkedAt: Date.now(), latest }));
|
|
1980
|
+
}
|
|
1981
|
+
catch { }
|
|
1982
|
+
}
|
|
1983
|
+
return latest;
|
|
1984
|
+
}
|
|
1985
|
+
catch {
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
async function updateGate(cmd, argv) {
|
|
1990
|
+
if (!cmd || !GATED_CMDS.has(cmd) || SHELL_ENV.DECIDIO_NO_UPDATE_CHECK || SHELL_ENV.DECIDIO_UPDATE_REEXEC || VERSION === "unknown" || VERSION.includes("-"))
|
|
1991
|
+
return null;
|
|
1992
|
+
const latest = await latestPublished();
|
|
1993
|
+
if (!latest || !semverNewer(latest, VERSION))
|
|
1994
|
+
return null;
|
|
1995
|
+
const entry = process.argv[1] ?? "";
|
|
1996
|
+
const underNpx = /[\\/]_npx[\\/]/.test(entry);
|
|
1997
|
+
const localRoot = (() => {
|
|
1998
|
+
const m = /^(.*?)[\\/]node_modules[\\/]@decidio[\\/]sdk[\\/]/.exec(entry);
|
|
1999
|
+
if (!m)
|
|
2000
|
+
return null;
|
|
2001
|
+
const norm = (s) => (process.platform === "win32" ? s.toLowerCase() : s);
|
|
2002
|
+
const root = resolvePath(m[1]);
|
|
2003
|
+
const cwd = norm(process.cwd());
|
|
2004
|
+
return cwd === norm(root) || cwd.startsWith(norm(root) + pathSep) ? root : null;
|
|
2005
|
+
})();
|
|
2006
|
+
const upgradeCmd = underNpx ? `npx @decidio/sdk@latest ${argv.slice(2).join(" ")}` : localRoot ? "npm i @decidio/sdk@latest" : "npm i -g @decidio/sdk@latest";
|
|
2007
|
+
const notice = `${latest} is available (you have ${VERSION}).`;
|
|
2008
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY)) {
|
|
2009
|
+
err(`⚠ ${notice} Upgrade: ${upgradeCmd}`);
|
|
2010
|
+
if (SHELL_ENV.DECIDIO_REQUIRE_LATEST) {
|
|
2011
|
+
err(`✘ DECIDIO_REQUIRE_LATEST is set — not running a stale version.`);
|
|
2012
|
+
return 1;
|
|
2013
|
+
}
|
|
2014
|
+
return null;
|
|
2015
|
+
}
|
|
2016
|
+
const a = (await ask(`${notice} Upgrade now? [y/n]: `)).toLowerCase();
|
|
2017
|
+
if (a !== "y" && a !== "yes") {
|
|
2018
|
+
err(`✘ not upgraded — run this when ready: ${upgradeCmd}`);
|
|
2019
|
+
return 1;
|
|
2020
|
+
}
|
|
2021
|
+
if (underNpx) {
|
|
2022
|
+
err(` Under npx there is nothing installed to upgrade — re-run as: ${upgradeCmd}`);
|
|
2023
|
+
return 1;
|
|
2024
|
+
}
|
|
2025
|
+
if (SHELL_ENV.DECIDIO_UPDATE_DRY_RUN) {
|
|
2026
|
+
log(` would run: ${upgradeCmd}`);
|
|
2027
|
+
return null;
|
|
2028
|
+
}
|
|
2029
|
+
log(` upgrading: ${upgradeCmd}${localRoot ? ` (in ${localRoot})` : ""}`);
|
|
2030
|
+
const up = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", localRoot ? ["i", "@decidio/sdk@latest"] : ["i", "-g", "@decidio/sdk@latest"], { stdio: "inherit", shell: process.platform === "win32", env: SHELL_ENV, ...(localRoot ? { cwd: localRoot } : {}) });
|
|
2031
|
+
if (up.status !== 0) {
|
|
2032
|
+
err(`✘ the upgrade did not complete (exit ${up.status ?? "?"}) — run it by hand: ${upgradeCmd}`);
|
|
2033
|
+
return 1;
|
|
2034
|
+
}
|
|
2035
|
+
log(` ✔ upgraded — re-running: decidio ${argv.slice(2).join(" ")}`);
|
|
2036
|
+
const rerun = spawnSync(process.execPath, [process.argv[1], ...argv.slice(2)], { stdio: "inherit", env: { ...SHELL_ENV, DECIDIO_UPDATE_REEXEC: "1" } });
|
|
2037
|
+
return rerun.status ?? 1;
|
|
2038
|
+
}
|
|
1594
2039
|
async function main(argv) {
|
|
1595
2040
|
const [cmd, a, b, ...rest] = argv.slice(2);
|
|
1596
2041
|
if (cmd === "--version" || cmd === "-v" || cmd === "-V" || cmd === "version") {
|
|
@@ -1605,9 +2050,15 @@ async function main(argv) {
|
|
|
1605
2050
|
log(USAGE);
|
|
1606
2051
|
return 0;
|
|
1607
2052
|
}
|
|
2053
|
+
if (helpFlagAnywhere(argv.slice(2))) {
|
|
2054
|
+
log(USAGE);
|
|
2055
|
+
return 0;
|
|
2056
|
+
}
|
|
1608
2057
|
const argsIn = argv.slice(3);
|
|
1609
2058
|
const KNOWN = {
|
|
1610
|
-
quickstart: { flags: ["--auto-approve-demo", "--keep-agent"] },
|
|
2059
|
+
quickstart: { flags: ["--explain", "--auto-approve-demo", "--keep-agent"], valued: ["--phone"] },
|
|
2060
|
+
login: { flags: [] },
|
|
2061
|
+
logout: { flags: [] },
|
|
1611
2062
|
init: { flags: ["--unique", "--force"], valued: ["--expires-days"] },
|
|
1612
2063
|
doctor: { flags: [] },
|
|
1613
2064
|
approvals: { flags: ["--json", "--mine"], valued: ["--reason"] },
|
|
@@ -1648,7 +2099,7 @@ async function main(argv) {
|
|
|
1648
2099
|
return 2;
|
|
1649
2100
|
}
|
|
1650
2101
|
}
|
|
1651
|
-
const MAX_POS = { quickstart: 1, init: 1, doctor: 0, receipt: 1, tokens: 2, dev: 0 };
|
|
2102
|
+
const MAX_POS = { login: 1, logout: 0, quickstart: 1, init: 1, doctor: 0, receipt: 1, tokens: 2, dev: 0 };
|
|
1652
2103
|
const exemptTail = (cmd === "approvals" && (positionals[0] === "approve" || positionals[0] === "reject")) || cmd === "verify";
|
|
1653
2104
|
if (!exemptTail && cmd === "approvals" && positionals.length > 0) {
|
|
1654
2105
|
err(`✘ unknown approvals subcommand "${positionals[0]}" — use approve or reject (or no subcommand to list).`);
|
|
@@ -1667,7 +2118,12 @@ async function main(argv) {
|
|
|
1667
2118
|
return 2;
|
|
1668
2119
|
}
|
|
1669
2120
|
}
|
|
2121
|
+
const gated = await updateGate(cmd, argv);
|
|
2122
|
+
if (gated !== null)
|
|
2123
|
+
return gated;
|
|
1670
2124
|
switch (cmd) {
|
|
2125
|
+
case "login": return cmdLogin(argsIn.find((x) => !x.startsWith("-")));
|
|
2126
|
+
case "logout": return cmdLogout();
|
|
1671
2127
|
case "quickstart": {
|
|
1672
2128
|
if (argsIn.includes("cleanup")) {
|
|
1673
2129
|
const envFile = existsSync(".env.quickstart") ? ".env.quickstart"
|
|
@@ -1679,7 +2135,14 @@ async function main(argv) {
|
|
|
1679
2135
|
return ".env.quickstart";
|
|
1680
2136
|
} })();
|
|
1681
2137
|
if (!envFile) {
|
|
1682
|
-
|
|
2138
|
+
let held = [];
|
|
2139
|
+
try {
|
|
2140
|
+
held = journalOutstanding();
|
|
2141
|
+
}
|
|
2142
|
+
catch { }
|
|
2143
|
+
log(held.length
|
|
2144
|
+
? `Nothing to clean up here: no tour credentials. The token recovery journal names ${held.length} live token(s) held by a credential file here (${held.map((j) => j.jti).join(", ")}) — not tour tokens, left alone; doctor lists them.`
|
|
2145
|
+
: "Nothing to clean up here: no .env.quickstart, .env doesn't belong to a quickstart agent, and the token recovery journal has no live token to act on.");
|
|
1683
2146
|
return 0;
|
|
1684
2147
|
}
|
|
1685
2148
|
const session = await acquireSession("Cleaning the quickstart up");
|
|
@@ -1687,7 +2150,7 @@ async function main(argv) {
|
|
|
1687
2150
|
return 1;
|
|
1688
2151
|
return quickstartCleanup(session, envFile);
|
|
1689
2152
|
}
|
|
1690
|
-
return cmdQuickstart({ autoApproveDemo: argsIn.includes("--auto-approve-demo"), keepAgent: argsIn.includes("--keep-agent") });
|
|
2153
|
+
return cmdQuickstart({ autoApproveDemo: argsIn.includes("--auto-approve-demo"), keepAgent: argsIn.includes("--keep-agent"), explain: argsIn.includes("--explain"), phone: valuedOpt(argsIn, "--phone") });
|
|
1691
2154
|
}
|
|
1692
2155
|
case "init": {
|
|
1693
2156
|
const unique = argsIn.includes("--unique");
|
|
@@ -1758,7 +2221,7 @@ async function main(argv) {
|
|
|
1758
2221
|
return 2;
|
|
1759
2222
|
}
|
|
1760
2223
|
if (!/^https?:\/\//.test(target)) {
|
|
1761
|
-
err(`✘ --target needs an http(s) URL to forward the webhook to (got "${target}").`);
|
|
2224
|
+
err(`✘ --target needs an http(s) URL to forward the webhook to (got "${redactUrl(target)}").`);
|
|
1762
2225
|
return 2;
|
|
1763
2226
|
}
|
|
1764
2227
|
return cmdDev(target, Number(portRaw));
|
|
@@ -1785,5 +2248,12 @@ const invokedAsProgram = (() => {
|
|
|
1785
2248
|
}
|
|
1786
2249
|
})();
|
|
1787
2250
|
if (invokedAsProgram)
|
|
1788
|
-
main(process.argv).then((code) => {
|
|
1789
|
-
|
|
2251
|
+
main(process.argv).then(async (code) => {
|
|
2252
|
+
if (code !== 0)
|
|
2253
|
+
process.exitCode = code;
|
|
2254
|
+
if (stdinReadAbandoned) {
|
|
2255
|
+
await new Promise((r) => process.stdout.write("", () => r()));
|
|
2256
|
+
await new Promise((r) => process.stderr.write("", () => r()));
|
|
2257
|
+
process.exit(code);
|
|
2258
|
+
}
|
|
2259
|
+
}).catch((e) => { console.error(`✘ unexpected error: ${e?.message ?? e}`); process.exit(1); });
|