@decidio/sdk 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +167 -0
- package/README.md +103 -23
- package/dist/adapters/_gate.d.ts +27 -2
- package/dist/adapters/_gate.js +33 -2
- package/dist/adapters/langgraph.d.ts +5 -11
- package/dist/adapters/langgraph.js +57 -6
- package/dist/adapters/openai.d.ts +40 -10
- package/dist/adapters/openai.js +19 -11
- package/dist/adapters/temporal.d.ts +21 -2
- package/dist/adapters/temporal.js +18 -5
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +252 -37
- package/dist/demo-gate.js +6 -4
- package/dist/index.d.ts +36 -4
- package/dist/index.js +48 -5
- package/dist/inngest.d.ts +5 -2
- package/dist/inngest.js +23 -15
- package/dist/resume.d.ts +98 -5
- package/dist/resume.js +203 -109
- package/dist/signing.d.ts +35 -0
- package/dist/signing.js +40 -1
- package/openapi.yaml +65 -6
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -4,6 +4,7 @@ import { randomBytes } from "node:crypto";
|
|
|
4
4
|
import { homedir, tmpdir } from "node:os";
|
|
5
5
|
import { join as joinPath, resolve as resolvePath, sep as pathSep, basename } from "node:path";
|
|
6
6
|
import { createServer } from "node:http";
|
|
7
|
+
import { isIP } from "node:net";
|
|
7
8
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
8
9
|
import { createInterface } from "node:readline";
|
|
9
10
|
import { pathToFileURL as toFileUrl, fileURLToPath as fromFileUrl } from "node:url";
|
|
@@ -12,7 +13,7 @@ import { createRequire } from "node:module";
|
|
|
12
13
|
import { generateKeypair, assertSafeTransport, redactUrl } from "./signing.js";
|
|
13
14
|
import { hardenFile } from "./resume.js";
|
|
14
15
|
import { withApproval, DecidioRateLimitError, DecidioRejectedError, DecidioTimeoutError, didFromPrivateKey } from "./index.js";
|
|
15
|
-
import { verifyReceipt, runVerify, pinRequiredMessage } from "@decidio/verify";
|
|
16
|
+
import { verifyReceipt, runVerify, pinRequiredMessage, builtInAnchor, KNOWN_ISSUERS } from "@decidio/verify";
|
|
16
17
|
import { startDemoGate } from "./demo-gate.js";
|
|
17
18
|
const VERSION = (() => {
|
|
18
19
|
try {
|
|
@@ -63,16 +64,19 @@ if (!OFFLINE_CMD)
|
|
|
63
64
|
console.error("⚠ ignoring DECIDIO_SESSION_TOKEN found in .env — session tokens belong in your shell or CI secret store, never on disk. Remove that line; export it in the shell instead.");
|
|
64
65
|
continue;
|
|
65
66
|
}
|
|
67
|
+
if (!k.startsWith("DECIDIO_"))
|
|
68
|
+
continue;
|
|
66
69
|
if (process.env[k] === undefined)
|
|
67
70
|
process.env[k] = v;
|
|
68
71
|
}
|
|
69
|
-
|
|
72
|
+
const DEFAULT_API = "https://decidio-api.onrender.com";
|
|
73
|
+
let API = (process.env.DECIDIO_API_URL ?? DEFAULT_API).replace(/\/$/, "");
|
|
70
74
|
let DEMO = false;
|
|
71
75
|
function enterDemo(url) { API = url; process.env.DECIDIO_API_URL = url; DEMO = true; }
|
|
72
76
|
const pinWords = () => (DEMO ? "PINNED to this demo's own key" : "PINNED to your workspace issuer");
|
|
73
77
|
const issuerNote = () => (DEMO
|
|
74
78
|
? "· a key this demo generated for this run — real tours pin the host's published DID at https://decidio-api.onrender.com/api/did"
|
|
75
|
-
: `· your trust anchor —
|
|
79
|
+
: `· your trust anchor — named by this server over the signed-in connection, never read from the receipt; cross-check ${API}/api/did`);
|
|
76
80
|
const TOKEN = () => process.env.DECIDIO_API_TOKEN;
|
|
77
81
|
const ENV_PATH = ".env";
|
|
78
82
|
const log = (...a) => console.log(...a);
|
|
@@ -509,14 +513,15 @@ if (process.env.DECIDIO_DEBUG_INPUT && process.stdin.isTTY) {
|
|
|
509
513
|
}
|
|
510
514
|
function terminalId() {
|
|
511
515
|
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"]) {
|
|
512
|
-
const v =
|
|
516
|
+
const v = SHELL_ENV[k];
|
|
513
517
|
if (v)
|
|
514
518
|
return `${k}=${v}`;
|
|
515
519
|
}
|
|
516
520
|
return "";
|
|
517
521
|
}
|
|
518
522
|
function sessionDir() {
|
|
519
|
-
const
|
|
523
|
+
const home = process.platform === "win32" ? (SHELL_ENV.USERPROFILE ?? homedir()) : (SHELL_ENV.HOME ?? homedir());
|
|
524
|
+
const base = process.platform === "win32" ? (SHELL_ENV.LOCALAPPDATA ?? joinPath(home, "AppData", "Local")) : (SHELL_ENV.XDG_CONFIG_HOME ?? joinPath(home, ".config"));
|
|
520
525
|
return joinPath(base, "decidio");
|
|
521
526
|
}
|
|
522
527
|
const sessionFile = () => joinPath(sessionDir(), "session.json");
|
|
@@ -572,6 +577,140 @@ function endSession() {
|
|
|
572
577
|
return false;
|
|
573
578
|
}
|
|
574
579
|
}
|
|
580
|
+
const issuersFile = () => joinPath(sessionDir(), "issuers.json");
|
|
581
|
+
const DID_KEY_SHAPE = /^did:key:z[1-9A-HJ-NP-Za-km-z]{40,60}$/;
|
|
582
|
+
const STORE_HOST_LABELS = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/;
|
|
583
|
+
export function issuerStoreKey(url) {
|
|
584
|
+
const raw = url.trim();
|
|
585
|
+
const low = raw.replace(/\/+$/, "").toLowerCase();
|
|
586
|
+
const m = /^([a-zA-Z][a-zA-Z0-9+.-]*):\/\/([^/?#]*)(?:[/?#]|$)/.exec(raw);
|
|
587
|
+
if (!m)
|
|
588
|
+
return low;
|
|
589
|
+
const scheme = m[1].toLowerCase();
|
|
590
|
+
if (scheme !== "http" && scheme !== "https")
|
|
591
|
+
return low;
|
|
592
|
+
let authority = m[2];
|
|
593
|
+
const at = authority.lastIndexOf("@");
|
|
594
|
+
if (at >= 0)
|
|
595
|
+
authority = authority.slice(at + 1);
|
|
596
|
+
let host, portStr = "";
|
|
597
|
+
if (authority.startsWith("[")) {
|
|
598
|
+
const rb = authority.indexOf("]");
|
|
599
|
+
if (rb < 0)
|
|
600
|
+
return low;
|
|
601
|
+
const inner = authority.slice(1, rb).toLowerCase();
|
|
602
|
+
const rest = authority.slice(rb + 1);
|
|
603
|
+
if (isIP(inner) !== 6)
|
|
604
|
+
return low;
|
|
605
|
+
if (rest !== "") {
|
|
606
|
+
if (!/^:\d+$/.test(rest))
|
|
607
|
+
return low;
|
|
608
|
+
portStr = rest.slice(1);
|
|
609
|
+
}
|
|
610
|
+
host = `[${inner}]`;
|
|
611
|
+
}
|
|
612
|
+
else {
|
|
613
|
+
const c = authority.lastIndexOf(":");
|
|
614
|
+
if (c >= 0) {
|
|
615
|
+
const p = authority.slice(c + 1);
|
|
616
|
+
if (!/^\d+$/.test(p))
|
|
617
|
+
return low;
|
|
618
|
+
portStr = p;
|
|
619
|
+
host = authority.slice(0, c).toLowerCase();
|
|
620
|
+
}
|
|
621
|
+
else
|
|
622
|
+
host = authority.toLowerCase();
|
|
623
|
+
if (!STORE_HOST_LABELS.test(host))
|
|
624
|
+
return low;
|
|
625
|
+
}
|
|
626
|
+
const port = portStr === "" ? null : Number(portStr);
|
|
627
|
+
const dflt = scheme === "https" ? 443 : 80;
|
|
628
|
+
return port !== null && port !== dflt ? `${scheme}://${host}:${port}` : `${scheme}://${host}`;
|
|
629
|
+
}
|
|
630
|
+
function readIssuerStore() {
|
|
631
|
+
const f = issuersFile();
|
|
632
|
+
if (!existsSync(f))
|
|
633
|
+
return {};
|
|
634
|
+
const rawText = readFileSync(f, "utf8");
|
|
635
|
+
const s = JSON.parse(rawText);
|
|
636
|
+
const vLiteral = /"v"\s*:\s*(-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?)/.exec(rawText)?.[1] ?? null;
|
|
637
|
+
const hosts = s?.v === 1 && vLiteral === "1" && s.hosts && typeof s.hosts === "object" && !Array.isArray(s.hosts) ? s.hosts : null;
|
|
638
|
+
if (!hosts)
|
|
639
|
+
throw new Error("not a v1 issuer store");
|
|
640
|
+
const out = {};
|
|
641
|
+
for (const [h, e] of Object.entries(hosts)) {
|
|
642
|
+
if (typeof e?.did !== "string" || !DID_KEY_SHAPE.test(e.did) || typeof e?.firstSeen !== "string")
|
|
643
|
+
throw new Error(`malformed entry for ${h}`);
|
|
644
|
+
const k = issuerStoreKey(h);
|
|
645
|
+
if (out[k] && out[k].did !== e.did)
|
|
646
|
+
throw new Error(`two entries for ${k} name different DIDs`);
|
|
647
|
+
out[k] ??= { did: e.did, firstSeen: e.firstSeen };
|
|
648
|
+
}
|
|
649
|
+
return out;
|
|
650
|
+
}
|
|
651
|
+
function rememberedIssuer(host = API) { return readIssuerStore()[issuerStoreKey(host)] ?? null; }
|
|
652
|
+
const verifyHost = () => (SHELL_ENV.DECIDIO_API_URL ?? DEFAULT_API).replace(/\/$/, "");
|
|
653
|
+
function warnIssuerChanged(known, seen, via) {
|
|
654
|
+
const key = issuerStoreKey(API);
|
|
655
|
+
err(`⚠ THE ISSUER DID OF ${key} HAS CHANGED — it was NOT adopted.`);
|
|
656
|
+
err(` remembered (first seen ${known.firstSeen.slice(0, 10)}): ${known.did}`);
|
|
657
|
+
err(` ${via} now says: ${seen}`);
|
|
658
|
+
err(` Receipts from this host are still verified against the REMEMBERED DID. A new DID means a new signing key: a planned`);
|
|
659
|
+
err(` rotation, another deployment behind the same address, or someone answering in its place. Confirm it with the host's`);
|
|
660
|
+
err(` operator out of band before trusting it; to adopt it, remove the "${key}" entry from ${issuersFile()} and sign in again.`);
|
|
661
|
+
}
|
|
662
|
+
async function rememberHostIssuer() {
|
|
663
|
+
if (DEMO)
|
|
664
|
+
return;
|
|
665
|
+
let store;
|
|
666
|
+
try {
|
|
667
|
+
store = readIssuerStore();
|
|
668
|
+
}
|
|
669
|
+
catch (e) {
|
|
670
|
+
err(`⚠ the issuer DIDs this computer remembers (${issuersFile()}) could not be read (${e?.message ?? e}) — left as found, nothing new remembered. Repair the file or move it aside.`);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
const r = await api("/api/did", {}, "", true);
|
|
674
|
+
const did = typeof r.body?.did === "string" && DID_KEY_SHAPE.test(r.body.did) ? r.body.did : null;
|
|
675
|
+
if (!did) {
|
|
676
|
+
log(dim(` (${API}/api/did did not answer with a DID (${r.status || "no response"}), so this host's issuer is not remembered yet — verify needs --issuer for its receipts until a later sign-in remembers it)`));
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const key = issuerStoreKey(API);
|
|
680
|
+
const known = store[key];
|
|
681
|
+
if (known?.did === did)
|
|
682
|
+
return;
|
|
683
|
+
if (known) {
|
|
684
|
+
warnIssuerChanged(known, did, `${API}/api/did`);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
store[key] = { did, firstSeen: new Date().toISOString() };
|
|
688
|
+
try {
|
|
689
|
+
mkdirSync(sessionDir(), { recursive: true });
|
|
690
|
+
writeSecretFile(issuersFile(), JSON.stringify({ v: 1, hosts: store }, null, 2) + "\n");
|
|
691
|
+
}
|
|
692
|
+
catch (e) {
|
|
693
|
+
err(`⚠ could not remember this host's issuer DID (${issuersFile()}: ${e?.message ?? e}) — verify will need --issuer for its receipts.`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
log(` ✔ remembered this host's issuer DID (from ${API}/api/did) — verify pins its receipts to it, no --issuer needed: ${did}`);
|
|
697
|
+
}
|
|
698
|
+
function hostAnchor(body) {
|
|
699
|
+
const named = typeof body?.expectedIssuer === "string" && DID_KEY_SHAPE.test(body.expectedIssuer) ? body.expectedIssuer : null;
|
|
700
|
+
if (DEMO)
|
|
701
|
+
return named;
|
|
702
|
+
let mem;
|
|
703
|
+
try {
|
|
704
|
+
mem = rememberedIssuer();
|
|
705
|
+
}
|
|
706
|
+
catch (e) {
|
|
707
|
+
err(`⚠ the issuer DIDs this computer remembers (${issuersFile()}) could not be read (${e?.message ?? e}) — no trust anchor until it is repaired or moved aside.`);
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
if (mem && named && mem.did !== named)
|
|
711
|
+
warnIssuerChanged(mem, named, "this server's verify response");
|
|
712
|
+
return mem?.did ?? named;
|
|
713
|
+
}
|
|
575
714
|
async function loginFlow(purpose, emailArg) {
|
|
576
715
|
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.`);
|
|
577
716
|
const email = emailArg ?? await ask(" email: ");
|
|
@@ -585,6 +724,7 @@ async function loginFlow(purpose, emailArg) {
|
|
|
585
724
|
if (r.status >= 200 && r.status < 300 && r.body?.token) {
|
|
586
725
|
const s = saveSession(String(r.body.token), email);
|
|
587
726
|
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)")}`);
|
|
727
|
+
await rememberHostIssuer();
|
|
588
728
|
return s.token;
|
|
589
729
|
}
|
|
590
730
|
log(` ✘ password sign-in failed (${r.status})${r.body?.error ? ` — ${r.body.error}` : ""}. No problem — the emailed link works for every account:`);
|
|
@@ -610,6 +750,7 @@ async function loginFlow(purpose, emailArg) {
|
|
|
610
750
|
if (r.status >= 200 && r.status < 300 && r.body?.token) {
|
|
611
751
|
const s = saveSession(String(r.body.token), email);
|
|
612
752
|
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)")}`);
|
|
753
|
+
await rememberHostIssuer();
|
|
613
754
|
return s.token;
|
|
614
755
|
}
|
|
615
756
|
if (isLink) {
|
|
@@ -897,6 +1038,7 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
897
1038
|
log(`✔ wrote ${ENV_PATH} (owner-only where the OS supports it) — every decidio command reads it from this directory (your script loads it with node --env-file=.env)`);
|
|
898
1039
|
if (privateKey)
|
|
899
1040
|
log(` (the agent's private signing key is in there — for production, move it to your secret manager)`);
|
|
1041
|
+
await rememberHostIssuer();
|
|
900
1042
|
log("");
|
|
901
1043
|
log("Next: protect one function and watch it route. Two steps:");
|
|
902
1044
|
log("");
|
|
@@ -1151,19 +1293,42 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
1151
1293
|
}
|
|
1152
1294
|
const name = receiptFileName(id);
|
|
1153
1295
|
const nextBytes = JSON.stringify(r.body.credential, null, 2) + "\n";
|
|
1154
|
-
const
|
|
1296
|
+
const issuer = hostAnchor(r.body);
|
|
1297
|
+
const local = issuer ? verifyReceipt(r.body.credential, issuer) : null;
|
|
1298
|
+
const serverInvalid = r.body?.valid === false;
|
|
1299
|
+
const localInvalid = !!local && !local.valid;
|
|
1300
|
+
const bad = serverInvalid || localInvalid;
|
|
1301
|
+
const say = jsonMode ? err : log;
|
|
1302
|
+
const wrote = writeReceiptFile(name, nextBytes, say);
|
|
1155
1303
|
if (wrote === "conflict")
|
|
1156
1304
|
return 1;
|
|
1157
1305
|
if (wrote === "written")
|
|
1158
|
-
(
|
|
1306
|
+
say(bad ? `• wrote ${name} — kept for inspection only: it does NOT verify (below)` : `✔ wrote ${name} (downloaded from your workspace — the download is online; VERIFYING it is offline)`);
|
|
1159
1307
|
const detail = await api(`/api/records/${encodeURIComponent(id)}`, {}, bearer);
|
|
1160
1308
|
const conf = detail.body?.record?.confirmation;
|
|
1161
|
-
const issuer = r.body?.issuer;
|
|
1162
1309
|
const resolved = r.body?.resolved;
|
|
1310
|
+
const badSaid = () => {
|
|
1311
|
+
if (serverInvalid) {
|
|
1312
|
+
err(`✘ the server's own check says this record does NOT verify: ${r.body?.reason ?? "no reason given"}`);
|
|
1313
|
+
err(` (signature ${r.body?.signatureValid === false ? "INVALID" : "valid"} · content bound ${r.body?.contentBound === false ? "NO" : "yes"} · matches the stored record ${r.body?.matchesRecord === false ? "NO" : "yes"} · stored payload ${r.body?.payloadMatches === false ? "ALTERED" : "intact"})`);
|
|
1314
|
+
}
|
|
1315
|
+
if (localInvalid)
|
|
1316
|
+
err(`✘ this receipt does NOT verify against this Decidio's issuer DID (${issuer}): ${local.reason ?? "no reason given"}`);
|
|
1317
|
+
err(` Do not rely on ${name}. Report it to the operator of ${API} — a sealed record is immutable, so this is tampering, a defect, or an unannounced key change (see any warning above), never a normal state.`);
|
|
1318
|
+
};
|
|
1163
1319
|
if (jsonMode) {
|
|
1164
|
-
|
|
1320
|
+
const valid = bad ? false : local?.valid === true ? true : null;
|
|
1321
|
+
log(JSON.stringify({ file: name, decisionId: id, evidenceTier: conf?.status ?? null, confirmed: !!conf?.confirmed, issuer: issuer ?? null, valid, ...(bad ? { reason: serverInvalid ? r.body?.reason ?? null : local?.reason ?? null } : {}), ...(resolved ? { resolved } : {}) }, null, 2));
|
|
1322
|
+
if (bad) {
|
|
1323
|
+
badSaid();
|
|
1324
|
+
return 1;
|
|
1325
|
+
}
|
|
1165
1326
|
return 0;
|
|
1166
1327
|
}
|
|
1328
|
+
if (bad) {
|
|
1329
|
+
badSaid();
|
|
1330
|
+
return 1;
|
|
1331
|
+
}
|
|
1167
1332
|
if (conf?.status)
|
|
1168
1333
|
log(` Sealed credential: the authority decision (its confirmation field reads "pending" forever — immutable). Current record evidence tier: ${conf.status}${conf.confirmed ? " (confirmed)" : ""}.`);
|
|
1169
1334
|
if (resolved && Object.keys(resolved).length && !process.stdout.isTTY) {
|
|
@@ -1176,19 +1341,37 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
1176
1341
|
}
|
|
1177
1342
|
log(` Verify it offline — no Decidio account or network needed (the verifier is built in):`);
|
|
1178
1343
|
if (issuer) {
|
|
1179
|
-
|
|
1344
|
+
const vHost = verifyHost();
|
|
1345
|
+
let plain = false, via = "";
|
|
1346
|
+
try {
|
|
1347
|
+
const mem = rememberedIssuer(vHost);
|
|
1348
|
+
const claimed = String(r.body.credential?.issuer ?? "");
|
|
1349
|
+
plain = (mem ? mem.did : KNOWN_ISSUERS.find((k) => k.did === claimed)?.did) === issuer;
|
|
1350
|
+
via = mem ? "remembered on this computer" : "built into the verifier";
|
|
1351
|
+
}
|
|
1352
|
+
catch {
|
|
1353
|
+
plain = false;
|
|
1354
|
+
}
|
|
1355
|
+
if (plain) {
|
|
1356
|
+
log(` npx @decidio/sdk verify ${name}`);
|
|
1357
|
+
log(` No --issuer needed here: it pins to this Decidio's issuer (${via}), which the server also named:`);
|
|
1358
|
+
}
|
|
1359
|
+
else {
|
|
1360
|
+
log(` Record your workspace's issuer DID — copy this line, and pin it forever:`);
|
|
1361
|
+
}
|
|
1180
1362
|
log(``);
|
|
1181
1363
|
log(`${issuer}`);
|
|
1182
1364
|
log(``);
|
|
1183
1365
|
log(` Cross-check it out of band — the same DID is published by this Decidio host at ${API}/api/did (public, no sign-in).`);
|
|
1184
|
-
log(` Then verify, pinned to it (the same DID, inline — copy from the line above, not this one
|
|
1185
|
-
log(` which your terminal may wrap):`);
|
|
1366
|
+
log(plain ? ` Anywhere else, pin it explicitly (copy the DID from the line above — this one may wrap):` : ` Then verify, pinned to it (the same DID, inline — copy from the line above, not this one, which your terminal may wrap):`);
|
|
1186
1367
|
log(` npx @decidio/sdk verify --issuer ${issuer} ${name}`);
|
|
1187
|
-
|
|
1188
|
-
|
|
1368
|
+
if (!plain && issuerStoreKey(vHost) !== issuerStoreKey(API))
|
|
1369
|
+
log(` (verify reads no .env, so a flagless run pins to the DID for ${vHost}, not ${API} - export DECIDIO_API_URL=${API} in your shell to verify this host's receipts without --issuer.)`);
|
|
1370
|
+
else if (!plain)
|
|
1371
|
+
log(` Or sign in once (npx @decidio/sdk login): this computer then remembers the DID, and verify needs no --issuer.`);
|
|
1189
1372
|
}
|
|
1190
1373
|
else {
|
|
1191
|
-
log(` To also prove WHO issued it, pin your
|
|
1374
|
+
log(` To also prove WHO issued it, pin your Decidio's issuer DID — published at ${API}/api/did (public, no sign-in), never read off the receipt:`);
|
|
1192
1375
|
log(` npx @decidio/sdk verify --issuer <did:key:...> ${name}`);
|
|
1193
1376
|
}
|
|
1194
1377
|
return 0;
|
|
@@ -1237,7 +1420,7 @@ function cmdVerify(argsIn) {
|
|
|
1237
1420
|
}
|
|
1238
1421
|
}
|
|
1239
1422
|
if (!files.length) {
|
|
1240
|
-
err("usage: npx @decidio/sdk verify --issuer <did:key> <receipt.json> [more.json ...] (or --allow-unpinned)");
|
|
1423
|
+
err("usage: npx @decidio/sdk verify [--issuer <did:key>] <receipt.json> [more.json ...] (or --allow-unpinned)");
|
|
1241
1424
|
err(" Verifies Authority Receipts OFFLINE — signature, content binding, and (for several files) chain order.");
|
|
1242
1425
|
return 2;
|
|
1243
1426
|
}
|
|
@@ -1245,10 +1428,6 @@ function cmdVerify(argsIn) {
|
|
|
1245
1428
|
err("✘ --allow-unpinned and --issuer together — an unpinned check and a pinned one are different questions; ask one.");
|
|
1246
1429
|
return 2;
|
|
1247
1430
|
}
|
|
1248
|
-
if (expectedIssuer === undefined && !allowUnpinned) {
|
|
1249
|
-
err(pinRequiredMessage("npx @decidio/sdk verify"));
|
|
1250
|
-
return 2;
|
|
1251
|
-
}
|
|
1252
1431
|
const texts = new Map();
|
|
1253
1432
|
for (const f of files) {
|
|
1254
1433
|
try {
|
|
@@ -1259,7 +1438,34 @@ function cmdVerify(argsIn) {
|
|
|
1259
1438
|
return 1;
|
|
1260
1439
|
}
|
|
1261
1440
|
}
|
|
1262
|
-
const
|
|
1441
|
+
const read = (f) => texts.get(f);
|
|
1442
|
+
let anchor = expectedIssuer !== undefined ? "--issuer" : undefined;
|
|
1443
|
+
if (expectedIssuer === undefined && !allowUnpinned) {
|
|
1444
|
+
let mem;
|
|
1445
|
+
try {
|
|
1446
|
+
mem = rememberedIssuer(verifyHost());
|
|
1447
|
+
}
|
|
1448
|
+
catch (e) {
|
|
1449
|
+
err(`✘ the issuer DIDs this computer remembers (${issuersFile()}) could not be read (${e?.message ?? e}) — refusing to fall back to another trust anchor.`);
|
|
1450
|
+
err(` Pass --issuer <did:key> for this run, or repair the file (or move it aside and sign in again). Nothing was verified.`);
|
|
1451
|
+
return 2;
|
|
1452
|
+
}
|
|
1453
|
+
if (mem) {
|
|
1454
|
+
expectedIssuer = mem.did;
|
|
1455
|
+
anchor = `remembered for ${issuerStoreKey(verifyHost())} since ${mem.firstSeen.slice(0, 10)}`;
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
const pick = builtInAnchor(files, read);
|
|
1459
|
+
if ("refusal" in pick) {
|
|
1460
|
+
err(pinRequiredMessage("npx @decidio/sdk verify", pick.refusal));
|
|
1461
|
+
err(` Nothing is remembered for ${issuerStoreKey(verifyHost())} on this computer: signing in once (npx @decidio/sdk login) remembers its DID, and then no --issuer is needed.`);
|
|
1462
|
+
return 2;
|
|
1463
|
+
}
|
|
1464
|
+
expectedIssuer = pick.anchor.did;
|
|
1465
|
+
anchor = `built in: ${pick.anchor.label}, ${pick.anchor.host}`;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
const { ok, lines } = runVerify(files, expectedIssuer, read, anchor);
|
|
1263
1469
|
for (const l of lines)
|
|
1264
1470
|
log(l);
|
|
1265
1471
|
return ok ? 0 : 1;
|
|
@@ -1705,12 +1911,13 @@ async function cmdQuickstartInner(opts = {}) {
|
|
|
1705
1911
|
const wrote = writeReceiptFile(fname, JSON.stringify(rec.body.credential, null, 2) + "\n");
|
|
1706
1912
|
if (wrote === "conflict")
|
|
1707
1913
|
return failTour(1);
|
|
1708
|
-
const issuer = rec.body
|
|
1914
|
+
const issuer = hostAnchor(rec.body) ?? undefined;
|
|
1709
1915
|
const v = verifyReceipt(rec.body.credential, issuer);
|
|
1710
|
-
const
|
|
1711
|
-
|
|
1916
|
+
const serverInvalid = rec.body?.valid === false;
|
|
1917
|
+
const kindLine = `${v.valid && !serverInvalid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ", " + pinWords() : ""}) · record kind: ${recordKindOf(rec.body.credential) ?? "?"}`;
|
|
1918
|
+
if (!v.valid || !issuer || serverInvalid) {
|
|
1712
1919
|
log(` ${kindLine}`);
|
|
1713
|
-
err(` ✘ the denial's receipt did not verify PINNED (${!issuer ? "no issuer
|
|
1920
|
+
err(` ✘ the denial's receipt did not verify PINNED (${!issuer ? "no trust anchor: the server named no issuer and none is remembered for this host" : !v.valid ? v.reason ?? "no reason given" : `the server's own check says it does NOT verify: ${rec.body?.reason ?? "no reason given"}`}) — the tour did not complete.`);
|
|
1714
1921
|
return failTour(1);
|
|
1715
1922
|
}
|
|
1716
1923
|
block("④", "The record", [
|
|
@@ -1780,19 +1987,24 @@ async function cmdQuickstartInner(opts = {}) {
|
|
|
1780
1987
|
const fname = receiptFileName(decisionId);
|
|
1781
1988
|
if (writeReceiptFile(fname, JSON.stringify(rec.body.credential, null, 2) + "\n") === "conflict")
|
|
1782
1989
|
return failTour(1);
|
|
1783
|
-
const issuer = rec.body
|
|
1990
|
+
const issuer = hostAnchor(rec.body) ?? undefined;
|
|
1784
1991
|
const v = verifyReceipt(rec.body.credential, issuer);
|
|
1785
|
-
const
|
|
1786
|
-
|
|
1992
|
+
const serverInvalid = rec.body?.valid === false;
|
|
1993
|
+
const kindLine = `${v.valid && !serverInvalid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? ", " + pinWords() : ""}) · record kind: ${recordKindOf(rec.body.credential) ?? "?"}`;
|
|
1994
|
+
if (!v.valid || !issuer || serverInvalid)
|
|
1787
1995
|
log(` ${kindLine}`);
|
|
1788
1996
|
if (!v.valid) {
|
|
1789
1997
|
err(` ✘ the receipt did not verify (${v.reason ?? "no reason given"}) — the tour did not complete.`);
|
|
1790
1998
|
return failTour(1);
|
|
1791
1999
|
}
|
|
2000
|
+
if (serverInvalid) {
|
|
2001
|
+
err(` ✘ the server's own check says this record does NOT verify (${rec.body?.reason ?? "no reason given"}) — the tour did not complete.`);
|
|
2002
|
+
return failTour(1);
|
|
2003
|
+
}
|
|
1792
2004
|
if (!issuer) {
|
|
1793
|
-
err(` ⚠ the server
|
|
1794
|
-
err(` receipt is internally consistent, NOT who issued it (a self-signed forgery also passes).`);
|
|
1795
|
-
err(`
|
|
2005
|
+
err(` ⚠ the server named no issuer (expectedIssuer) and no DID is remembered for this host, so this check ran UNPINNED —`);
|
|
2006
|
+
err(` it proves the receipt is internally consistent, NOT who issued it (a self-signed forgery also passes).`);
|
|
2007
|
+
err(` Take your Decidio's issuer DID from ${API}/api/did (public, no sign-in) — never from the receipt — then re-check:`);
|
|
1796
2008
|
err(` npx @decidio/sdk verify --issuer <did:key:...> ${fname}`);
|
|
1797
2009
|
err(` ✘ the tour does not count an UNPINNED check as success.`);
|
|
1798
2010
|
return failTour(1);
|
|
@@ -2198,8 +2410,10 @@ APPROVALS AND RECEIPTS
|
|
|
2198
2410
|
npx @decidio/sdk receipt <id> [--json] download a sealed decision's Authority Receipt to a .json FILE; --json prints
|
|
2199
2411
|
a summary to stdout (file name, evidence, issuer, the identities you may see) -
|
|
2200
2412
|
the credential itself is the file
|
|
2201
|
-
npx @decidio/sdk verify --issuer <did> <receipt.json> [more...] verify receipts OFFLINE, pinned to
|
|
2202
|
-
|
|
2413
|
+
npx @decidio/sdk verify [--issuer <did>] <receipt.json> [more...] verify receipts OFFLINE, pinned to a trust
|
|
2414
|
+
anchor it prints first: --issuer if given, else the DID this computer remembered
|
|
2415
|
+
for your Decidio when you signed in, else the built-in hosted-service issuer
|
|
2416
|
+
(never the receipt's own issuer field; --allow-unpinned: structural check only)
|
|
2203
2417
|
|
|
2204
2418
|
DEVELOPMENT
|
|
2205
2419
|
npx @decidio/sdk dev [--target URL] [--port N] local relay for the resume webhook
|
|
@@ -2208,7 +2422,7 @@ DEVELOPMENT
|
|
|
2208
2422
|
NOTES
|
|
2209
2423
|
Every NETWORK command talks to the hosted sandbox (https://decidio-api.onrender.com) unless
|
|
2210
2424
|
DECIDIO_API_URL says otherwise; the target host is announced on stderr each run. "verify" is
|
|
2211
|
-
fully offline — it touches no network and reads no .env.
|
|
2425
|
+
fully offline — it touches no network and reads no .env (only the issuer DIDs remembered at sign-in).
|
|
2212
2426
|
Update check: network commands (and the bare command on a terminal) ask the public npm registry once a day
|
|
2213
2427
|
whether a newer version exists. If so,
|
|
2214
2428
|
an interactive run asks "Upgrade now? [y/n]" first (y upgrades the way you installed - global, or in the
|
|
@@ -2225,8 +2439,9 @@ NOTES
|
|
|
2225
2439
|
(quickstart, durable resume, workers, adapters).`;
|
|
2226
2440
|
const GATED_CMDS = new Set(["quickstart", "init", "login", "doctor", "approvals", "receipt", "tokens"]);
|
|
2227
2441
|
function semverNewer(a, b) {
|
|
2228
|
-
const
|
|
2229
|
-
|
|
2442
|
+
const parse = (s) => (/^\d+\.\d+\.\d+$/.test(s) ? s.split(".").map((x) => parseInt(x, 10)) : null);
|
|
2443
|
+
const pa = parse(a), pb = parse(b);
|
|
2444
|
+
if (!pa || !pb)
|
|
2230
2445
|
return false;
|
|
2231
2446
|
for (let i = 0; i < 3; i++)
|
|
2232
2447
|
if (pa[i] !== pb[i])
|
|
@@ -2237,7 +2452,7 @@ async function latestPublished() {
|
|
|
2237
2452
|
const cache = joinPath(sessionDir(), "update-check.json");
|
|
2238
2453
|
try {
|
|
2239
2454
|
const c = JSON.parse(readFileSync(cache, "utf8"));
|
|
2240
|
-
if (c && typeof c.latest === "string" && Date.now() - Number(c.checkedAt) < 24 * 3600 * 1000)
|
|
2455
|
+
if (c && typeof c.latest === "string" && /^\d+\.\d+\.\d+$/.test(c.latest) && Date.now() - Number(c.checkedAt) < 24 * 3600 * 1000)
|
|
2241
2456
|
return c.latest;
|
|
2242
2457
|
}
|
|
2243
2458
|
catch { }
|
package/dist/demo-gate.js
CHANGED
|
@@ -40,6 +40,7 @@ export async function startDemoGate(opts = {}) {
|
|
|
40
40
|
let lastReason = "quickstart";
|
|
41
41
|
let relayCode = null;
|
|
42
42
|
let relayOk = null;
|
|
43
|
+
let committed = null;
|
|
43
44
|
const relayFetch = async (path, init = {}) => {
|
|
44
45
|
if (!relayBase)
|
|
45
46
|
return null;
|
|
@@ -111,7 +112,8 @@ export async function startDemoGate(opts = {}) {
|
|
|
111
112
|
return send({ ok: true, changed: true });
|
|
112
113
|
if (path === "/agent/authorize") {
|
|
113
114
|
verdict = null;
|
|
114
|
-
|
|
115
|
+
committed = typeof body?.requestCommitment === "string" && /^[0-9a-f]{64}$/.test(body.requestCommitment) ? body.requestCommitment : null;
|
|
116
|
+
return send({ decision: "route", decisionId: DECISION_ID, receiptId: "R-DEMO-1", requestCommitment: committed });
|
|
115
117
|
}
|
|
116
118
|
if (path.startsWith("/agent/status/")) {
|
|
117
119
|
if (!verdict && relayCode) {
|
|
@@ -123,8 +125,8 @@ export async function startDemoGate(opts = {}) {
|
|
|
123
125
|
}
|
|
124
126
|
}
|
|
125
127
|
if (withdrawnDemo && !verdict)
|
|
126
|
-
return send({ state: "rejected", reason: "quickstart: withdrawn — the tour stopped waiting" });
|
|
127
|
-
return send(verdict === "reject" ? { state: "rejected", reason: "quickstart: rejected by the approver" } : { state: verdict === "approve" ? "approved" : "pending" });
|
|
128
|
+
return send({ state: "rejected", reason: "quickstart: withdrawn — the tour stopped waiting", requestCommitment: committed });
|
|
129
|
+
return send(verdict === "reject" ? { state: "rejected", reason: "quickstart: rejected by the approver", requestCommitment: committed } : { state: verdict === "approve" ? "approved" : "pending", requestCommitment: committed });
|
|
128
130
|
}
|
|
129
131
|
if (path.startsWith("/agent/withdraw/")) {
|
|
130
132
|
if (verdict)
|
|
@@ -157,7 +159,7 @@ export async function startDemoGate(opts = {}) {
|
|
|
157
159
|
}
|
|
158
160
|
if (/^\/api\/records\/[^/]+\/verify$/.test(path)) {
|
|
159
161
|
const cred = issueReceipt(verdict === "reject" ? "denial" : "approval");
|
|
160
|
-
return send({ dNumber: D_NUMBER, issuer: issuerDid, alg: "Ed25519", valid: true, signatureValid: true, contentBound: true, matchesRecord: true, payloadMatches: true, hash: cred.credentialSubject.contentHash, prevHash: null, credential: cred });
|
|
162
|
+
return send({ dNumber: D_NUMBER, issuer: issuerDid, expectedIssuer: issuerDid, alg: "Ed25519", valid: true, signatureValid: true, contentBound: true, matchesRecord: true, payloadMatches: true, hash: cred.credentialSubject.contentHash, prevHash: null, credential: cred });
|
|
161
163
|
}
|
|
162
164
|
if (/^\/api\/records\/[^/]+$/.test(path))
|
|
163
165
|
return send({ record: { confirmation: verdict === "approve" ? { status: "application_confirmed", confirmed: true } : { status: "pending", confirmed: false } } });
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { ResumeController } from "./resume.js";
|
|
1
|
+
import type { ResumeController, ApprovalCheck } from "./resume.js";
|
|
2
|
+
import { type CommittedRequest } from "./signing.js";
|
|
2
3
|
import { type EvidenceOutcome } from "./evidence.js";
|
|
3
|
-
export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
4
|
-
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome } from "./resume.js";
|
|
4
|
+
export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded, checkApproval } from "./resume.js";
|
|
5
|
+
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome, ApprovalCheck, ApprovalCheckCode, DecisionStatus } from "./resume.js";
|
|
5
6
|
export { guard, Guard } from "./guard.js";
|
|
6
7
|
export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
|
|
7
8
|
export { DecidioHttpError, decisionIdProblem } from "./errors.js";
|
|
8
|
-
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential } from "./signing.js";
|
|
9
|
+
export { generateKeypair, didFromPrivateKey, signBoundProof, signConfirm, signExecutionReport, verifyExecutionReport, resultDigest, assertSafeTransport, carriesCredential, bindingMaterial, requestCommitment, idempotencyKey, committedRequestJson } from "./signing.js";
|
|
10
|
+
export type { BindingMaterial, CommittedRequest } from "./signing.js";
|
|
9
11
|
export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
|
|
10
12
|
export type { JsonValue, JsonPrimitive } from "./canonical.js";
|
|
11
13
|
export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS } from "./evidence.js";
|
|
@@ -80,6 +82,36 @@ export declare class DecidioRejectedError extends Error {
|
|
|
80
82
|
decisionId: string;
|
|
81
83
|
constructor(reason: string, decisionId: string);
|
|
82
84
|
}
|
|
85
|
+
/** THE REQUEST CHANGED AFTER APPROVAL (0.7.0). Decidio records an approval for `decisionId`, but it is
|
|
86
|
+
* bound (by a keyed commitment) to a DIFFERENT request than the one about to run — the arguments, the
|
|
87
|
+
* amount or the described context changed after a human approved, or this agent's key material changed
|
|
88
|
+
* since it asked. Nothing was executed. A new request needs a new approval: reject this decision and
|
|
89
|
+
* run the action again. Extends DecidioBlockedError, so code that already treats a block as "did not
|
|
90
|
+
* run" keeps doing so. */
|
|
91
|
+
export declare class DecidioRequestChangedError extends DecidioBlockedError {
|
|
92
|
+
readonly code: "request_changed";
|
|
93
|
+
constructor(decisionId: string, detail?: string);
|
|
94
|
+
}
|
|
95
|
+
/** An approval that cannot be TIED to this request (0.7.0): Decidio answered without the request
|
|
96
|
+
* commitment this SDK sent (`commitment_missing` — the gate predates request binding), or holds one
|
|
97
|
+
* this process cannot recompute (`unverifiable` — no or unreadable key material, or a malformed
|
|
98
|
+
* answer). Fail closed: nothing was executed. Extends DecidioBlockedError. */
|
|
99
|
+
export declare class DecidioApprovalUnverifiedError extends DecidioBlockedError {
|
|
100
|
+
readonly code: "commitment_missing" | "unverifiable";
|
|
101
|
+
constructor(decisionId: string, code: "commitment_missing" | "unverifiable", detail: string);
|
|
102
|
+
}
|
|
103
|
+
/** The typed refusal for an approval check that did not pass — ONE mapping for every door that throws
|
|
104
|
+
* (inline, LangGraph, Temporal, Inngest, OpenAI Agents). `pending` is included for the doors that
|
|
105
|
+
* cannot wait again; the doors that can (LangGraph, Temporal, Inngest) wait instead of calling this. */
|
|
106
|
+
export declare function approvalRefusal(check: ApprovalCheck): Error;
|
|
107
|
+
/** What a routed approval must be bound to on the CORE door: exactly what executes — the canonical
|
|
108
|
+
* argument snapshot plus the described action/amount/scope. The durable resume recomputes the same
|
|
109
|
+
* shape over the parked payload. */
|
|
110
|
+
export declare function coreRequest(ctx: {
|
|
111
|
+
action: string;
|
|
112
|
+
amount: number;
|
|
113
|
+
scope?: string | null;
|
|
114
|
+
}, args: readonly unknown[]): CommittedRequest;
|
|
83
115
|
export declare class DecidioTimeoutError extends Error {
|
|
84
116
|
decisionId: string;
|
|
85
117
|
withdrawn: boolean;
|