@decidio/sdk 0.1.11 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -15
- package/dist/adapters/_gate.d.ts +8 -2
- package/dist/adapters/_gate.js +30 -8
- package/dist/adapters/langgraph.d.ts +3 -1
- package/dist/adapters/langgraph.js +2 -2
- package/dist/adapters/openai.d.ts +7 -2
- package/dist/adapters/openai.js +11 -3
- package/dist/adapters/temporal.d.ts +6 -2
- package/dist/adapters/temporal.js +3 -1
- package/dist/canonical.d.ts +1 -1
- package/dist/canonical.js +34 -6
- package/dist/cli.js +141 -52
- package/dist/errors.d.ts +12 -5
- package/dist/errors.js +37 -5
- package/dist/evidence.d.ts +23 -0
- package/dist/evidence.js +15 -4
- package/dist/guard.d.ts +20 -3
- package/dist/guard.js +8 -6
- package/dist/index.d.ts +28 -3
- package/dist/index.js +49 -17
- package/dist/inngest.d.ts +4 -0
- package/dist/inngest.js +1 -1
- package/dist/resume.d.ts +334 -57
- package/dist/resume.js +705 -145
- package/dist/signing.d.ts +8 -0
- package/dist/signing.js +36 -2
- package/openapi.yaml +89 -6
- package/package.json +13 -8
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createServer } from "node:http";
|
|
|
6
6
|
import { execFileSync } from "node:child_process";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
|
-
import { generateKeypair } from "./signing.js";
|
|
9
|
+
import { generateKeypair, assertSafeTransport } from "./signing.js";
|
|
10
10
|
import { withApproval, DecidioRateLimitError, didFromPrivateKey } from "./index.js";
|
|
11
11
|
import { verifyReceipt, runVerify } from "@decidio/verify";
|
|
12
12
|
const VERSION = (() => {
|
|
@@ -65,9 +65,14 @@ async function api(path, init = {}, bearer) {
|
|
|
65
65
|
err(` Fix the file's permissions, or set DECIDIO_API_URL explicitly for this command. Nothing was sent.`);
|
|
66
66
|
process.exit(1);
|
|
67
67
|
}
|
|
68
|
-
if (auth
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
if (auth) {
|
|
69
|
+
try {
|
|
70
|
+
assertSafeTransport(API, true);
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
err(`✘ ${String(e?.message ?? e).replace(/^\[decidio\] /, "")}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
71
76
|
}
|
|
72
77
|
let res;
|
|
73
78
|
try {
|
|
@@ -189,7 +194,10 @@ function ask(question, mute = false) {
|
|
|
189
194
|
});
|
|
190
195
|
}
|
|
191
196
|
async function acquireSession(purpose) {
|
|
192
|
-
|
|
197
|
+
try {
|
|
198
|
+
assertSafeTransport(API, true);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
193
201
|
err(`✘ refusing to send workspace credentials over plaintext http to ${API}.`);
|
|
194
202
|
err(` Use https (the hosted sandbox is https://decidio-api.onrender.com) or localhost for local dev.`);
|
|
195
203
|
return null;
|
|
@@ -250,6 +258,18 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
250
258
|
}
|
|
251
259
|
const { did: localDid, privateKeyBase64: localKey } = generateKeypair();
|
|
252
260
|
const registerBody = JSON.stringify({ agentId, did: localDid, ...(sourceSystem ? { sourceSystem } : {}) });
|
|
261
|
+
let envPrepared = false;
|
|
262
|
+
const prepareEnvForWrite = () => {
|
|
263
|
+
if (envPrepared)
|
|
264
|
+
return;
|
|
265
|
+
envPrepared = true;
|
|
266
|
+
ensureGitignore();
|
|
267
|
+
if (opts.force && existingKey && existingEnv.DECIDIO_AGENT_ID && existingEnv.DECIDIO_AGENT_ID !== agentId) {
|
|
268
|
+
const backup = `.env.${existingEnv.DECIDIO_AGENT_ID.replace(/[^\w.-]/g, "_")}.bak`;
|
|
269
|
+
writeSecretFile(backup, readFileSync(ENV_PATH, "utf8"));
|
|
270
|
+
log(`✔ backed the previous agent's .env up to ${backup} (its key would otherwise be unrecoverable)`);
|
|
271
|
+
}
|
|
272
|
+
};
|
|
253
273
|
let session = null;
|
|
254
274
|
let r = await api("/api/agents/register", { method: "POST", body: registerBody });
|
|
255
275
|
if (r.status === 401 || r.status === 403) {
|
|
@@ -280,6 +300,8 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
280
300
|
did = r.body?.did;
|
|
281
301
|
privateKey = localKey;
|
|
282
302
|
log(`✔ registered (did ${did}) — key generated locally; only the public half went to Decidio`);
|
|
303
|
+
prepareEnvForWrite();
|
|
304
|
+
upsertEnv({ DECIDIO_AGENT_ID: agentId, DECIDIO_AGENT_DID: did ?? localDid, DECIDIO_AGENT_KEY: localKey });
|
|
283
305
|
}
|
|
284
306
|
else {
|
|
285
307
|
err(`✘ registration failed (${r.status}): ${JSON.stringify(r.body)}`);
|
|
@@ -302,7 +324,7 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
302
324
|
}
|
|
303
325
|
else if (session) {
|
|
304
326
|
err(`✘ could not mint the agent's API token (${mint.status}): ${JSON.stringify(mint.body)}`);
|
|
305
|
-
err(` Without it the SDK has no runtime credential.
|
|
327
|
+
err(` Without it the SDK has no runtime credential. The agent IS registered and its key is saved in ${ENV_PATH} — fix the cause above and re-run init; it will reuse that key and mint again.`);
|
|
306
328
|
return 1;
|
|
307
329
|
}
|
|
308
330
|
else {
|
|
@@ -323,12 +345,7 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
323
345
|
envOut.DECIDIO_WORKSPACE_ID = workspaceId;
|
|
324
346
|
if (tokenJti)
|
|
325
347
|
envOut.DECIDIO_TOKEN_JTI = tokenJti;
|
|
326
|
-
|
|
327
|
-
if (opts.force && existingKey && existingEnv.DECIDIO_AGENT_ID && existingEnv.DECIDIO_AGENT_ID !== agentId) {
|
|
328
|
-
const backup = `.env.${existingEnv.DECIDIO_AGENT_ID.replace(/[^\w.-]/g, "_")}.bak`;
|
|
329
|
-
writeSecretFile(backup, readFileSync(ENV_PATH, "utf8"));
|
|
330
|
-
log(`✔ backed the previous agent's .env up to ${backup} (its key would otherwise be unrecoverable)`);
|
|
331
|
-
}
|
|
348
|
+
prepareEnvForWrite();
|
|
332
349
|
upsertEnv(envOut);
|
|
333
350
|
for (const [k, v] of Object.entries(envOut))
|
|
334
351
|
process.env[k] = v;
|
|
@@ -350,8 +367,9 @@ async function cmdInit(agentId, opts = {}) {
|
|
|
350
367
|
log(` { mode: "blocking" }, // blocking = watch it happen live; durable is the production mode`);
|
|
351
368
|
log(` );`);
|
|
352
369
|
log("");
|
|
353
|
-
log(` const
|
|
354
|
-
log(` console.log("executed after approval:",
|
|
370
|
+
log(` const { value, confirmation } = await payInvoice({ id: "INV-2026-001", amount: 86_000 });`);
|
|
371
|
+
log(` console.log("executed after approval:", value);`);
|
|
372
|
+
log(` console.log("Decidio recorded it as:", confirmation.evidenceTier);`);
|
|
355
373
|
log("");
|
|
356
374
|
log(" 2. Run it, approve it, watch it complete:");
|
|
357
375
|
log("");
|
|
@@ -403,13 +421,14 @@ async function cmdDoctor() {
|
|
|
403
421
|
if (typeof payload?.exp === "number") {
|
|
404
422
|
const msLeft = payload.exp * 1000 - Date.now();
|
|
405
423
|
const days = Math.floor((msLeft + 60_000) / 86_400_000);
|
|
406
|
-
const left = msLeft < 86_400_000 ? `${Math.max(1, Math.floor(msLeft / 3_600_000))} hour(s)` : `${
|
|
424
|
+
const left = msLeft < 86_400_000 ? `${Math.max(1, Math.floor(msLeft / 3_600_000))} hour(s)` : `${Math.round(msLeft / 86_400_000)} day(s)`;
|
|
425
|
+
const daysRounded = Math.round(msLeft / 86_400_000);
|
|
407
426
|
if (msLeft < 0)
|
|
408
427
|
line(false, `agent token: EXPIRED ${-days} day(s) ago — re-run \`npx @decidio/sdk init ${env.DECIDIO_AGENT_ID ?? "<agent>"}\` to mint a fresh one.`);
|
|
409
428
|
else if (days <= 14)
|
|
410
429
|
warn(`agent token: expires in ${left} — mint a fresh one before it does: npx @decidio/sdk init ${env.DECIDIO_AGENT_ID ?? "<agent>"}`);
|
|
411
430
|
else
|
|
412
|
-
log(`✔ agent token: unexpired for another ${
|
|
431
|
+
log(`✔ agent token: unexpired for another ${daysRounded} days (validity is decided by the server — see the token-scope check below)`);
|
|
413
432
|
}
|
|
414
433
|
}
|
|
415
434
|
catch { }
|
|
@@ -568,12 +587,18 @@ async function cmdReceipt(id, jsonMode = false) {
|
|
|
568
587
|
const detail = await api(`/api/records/${encodeURIComponent(id)}`, {}, bearer);
|
|
569
588
|
const conf = detail.body?.record?.confirmation;
|
|
570
589
|
const issuer = r.body?.issuer;
|
|
590
|
+
const resolved = r.body?.resolved;
|
|
571
591
|
if (jsonMode) {
|
|
572
|
-
log(JSON.stringify({ file: name, decisionId: id, evidenceTier: conf?.status ?? null, confirmed: !!conf?.confirmed, issuer: issuer ?? null }, null, 2));
|
|
592
|
+
log(JSON.stringify({ file: name, decisionId: id, evidenceTier: conf?.status ?? null, confirmed: !!conf?.confirmed, issuer: issuer ?? null, ...(resolved ? { resolved } : {}) }, null, 2));
|
|
573
593
|
return 0;
|
|
574
594
|
}
|
|
575
595
|
if (conf?.status)
|
|
576
596
|
log(` Sealed credential: the authority decision (its confirmation field reads "pending" forever — immutable). Current record evidence tier: ${conf.status}${conf.confirmed ? " (confirmed)" : ""}.`);
|
|
597
|
+
if (resolved && Object.keys(resolved).length) {
|
|
598
|
+
log(` Resolved for you (${Object.keys(resolved).length} field${Object.keys(resolved).length === 1 ? "" : "s"} your access covers — the FILE keeps the tokens, so it stays verifiable and safe to hand on):`);
|
|
599
|
+
for (const [tok, val] of Object.entries(resolved))
|
|
600
|
+
log(` ${tok} → ${val}`);
|
|
601
|
+
}
|
|
577
602
|
log(` Verify it offline — no Decidio account or network needed (the verifier is built in):`);
|
|
578
603
|
if (issuer) {
|
|
579
604
|
log(` npx @decidio/sdk verify --issuer ${issuer} ${name}`);
|
|
@@ -647,12 +672,31 @@ async function cmdQuickstart(opts = {}) {
|
|
|
647
672
|
const session = await acquireSession("The quickstart");
|
|
648
673
|
if (!session)
|
|
649
674
|
return 1;
|
|
650
|
-
const
|
|
651
|
-
|
|
675
|
+
const envFile = readEnvFile().DECIDIO_AGENT_KEY ? ".env.quickstart" : ENV_PATH;
|
|
676
|
+
if (!assertNotTracked(envFile))
|
|
677
|
+
return 1;
|
|
678
|
+
ensureGitignore();
|
|
679
|
+
let agentId = `quickstart-${randomBytes(5).toString("hex")}`;
|
|
680
|
+
log(`① Registering agent "${agentId}"…`);
|
|
652
681
|
const { did, privateKeyBase64 } = generateKeypair();
|
|
653
|
-
|
|
682
|
+
let reg = await api("/api/agents/register", { method: "POST", body: JSON.stringify({ agentId, did }) }, session);
|
|
683
|
+
for (let attempt = 0; reg.status === 409 && attempt < 3; attempt++) {
|
|
684
|
+
agentId = `quickstart-${randomBytes(5).toString("hex")}`;
|
|
685
|
+
reg = await api("/api/agents/register", { method: "POST", body: JSON.stringify({ agentId, did }) }, session);
|
|
686
|
+
if (reg.status >= 200 && reg.status < 300)
|
|
687
|
+
log(` (that name was taken — using "${agentId}" instead)`);
|
|
688
|
+
}
|
|
654
689
|
if (!(reg.status >= 200 && reg.status < 300)) {
|
|
655
|
-
err(`✘
|
|
690
|
+
err(`✘ could not register the tour's agent (${reg.status}): ${JSON.stringify(reg.body)}`);
|
|
691
|
+
if (reg.status === 409) {
|
|
692
|
+
err(` Every generated name collided, which should be almost impossible — something is likely`);
|
|
693
|
+
err(` wrong with this workspace's agent list rather than with your setup.`);
|
|
694
|
+
err(` See what is registered: npx @decidio/sdk tokens list`);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
err(` Check the target and your sign-in, then re-run: npx @decidio/sdk doctor`);
|
|
698
|
+
}
|
|
699
|
+
err(` Nothing was written to this directory.`);
|
|
656
700
|
return 1;
|
|
657
701
|
}
|
|
658
702
|
const mint = await api("/api/workspace/agent-token", { method: "POST", body: JSON.stringify({ agentId, label: "quickstart", expiresInDays: 7 }) }, session);
|
|
@@ -666,30 +710,36 @@ async function cmdQuickstart(opts = {}) {
|
|
|
666
710
|
DECIDIO_WORKSPACE_ID: mint.body.workspaceId ?? "",
|
|
667
711
|
...(mint.body.jti ? { DECIDIO_TOKEN_JTI: mint.body.jti } : {}),
|
|
668
712
|
};
|
|
669
|
-
|
|
670
|
-
if (readEnvFile().DECIDIO_AGENT_KEY) {
|
|
671
|
-
envFile = ".env.quickstart";
|
|
672
|
-
if (!assertNotTracked(envFile))
|
|
673
|
-
return 1;
|
|
674
|
-
ensureGitignore();
|
|
713
|
+
if (envFile !== ENV_PATH) {
|
|
675
714
|
if (existsSync(envFile)) {
|
|
676
715
|
const prev = readFileSync(envFile, "utf8");
|
|
677
716
|
const prevId = prev.match(/^DECIDIO_AGENT_ID=(.+)$/m)?.[1];
|
|
717
|
+
const prevJti = prev.match(/^DECIDIO_TOKEN_JTI=(.+)$/m)?.[1];
|
|
678
718
|
if (prevId && prevId !== agentId) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
719
|
+
if (prevJti) {
|
|
720
|
+
const rv = await api(`/api/workspace/agent-token/${encodeURIComponent(prevJti)}`, { method: "DELETE" }, session);
|
|
721
|
+
if (rv.status >= 200 && rv.status < 300)
|
|
722
|
+
log(` ✔ the previous tour agent (${prevId}) had its token REVOKED before this tour replaced it — no live credential is left behind`);
|
|
723
|
+
else
|
|
724
|
+
log(` ⚠ could not revoke the previous tour's token (jti ${prevJti}, ${rv.status}) — revoke it by hand: npx @decidio/sdk tokens revoke ${prevJti}`);
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
log(` ⚠ the previous tour file carried no revocation id, so its token (if any) must be revoked from your Decidio app.`);
|
|
728
|
+
}
|
|
682
729
|
}
|
|
683
730
|
}
|
|
684
731
|
writeSecretFile(envFile, Object.entries(envOut).map(([k, v]) => `${k}=${v}`).join("\n") + "\n");
|
|
685
732
|
log(` ✔ this directory already has an agent in .env — tour credentials went to ${envFile} (your agent is untouched)`);
|
|
686
733
|
}
|
|
687
734
|
else {
|
|
688
|
-
if (!assertNotTracked(ENV_PATH))
|
|
689
|
-
return 1;
|
|
690
|
-
ensureGitignore();
|
|
691
735
|
upsertEnv(envOut);
|
|
692
736
|
}
|
|
737
|
+
const failTour = async (code) => {
|
|
738
|
+
log("");
|
|
739
|
+
log(" The tour did not complete — cleaning up the credentials it created:");
|
|
740
|
+
await quickstartCleanup(session, envFile).catch(() => { });
|
|
741
|
+
return code;
|
|
742
|
+
};
|
|
693
743
|
log(` ✔ key generated locally (public half registered) · 7-day floor-limited token minted${mint.body.expiresAt ? ` (expires ${mint.body.expiresAt})` : ""} · ${envFile} written`);
|
|
694
744
|
log("");
|
|
695
745
|
log(`② Gating a demo action — the tour runs this (blocking mode: watch it live):`);
|
|
@@ -716,15 +766,15 @@ async function cmdQuickstart(opts = {}) {
|
|
|
716
766
|
err(`✘ this workspace's governed-action allowance is used up${earlyError.limit != null ? ` (${earlyError.limit} actions/hour on lab workspaces)` : ""}.`);
|
|
717
767
|
err(` Nothing was routed or executed — the cap is the sandbox protecting itself, not a fault${earlyError.resetAt ? `. It resets ${earlyError.resetAt}` : ""}.`);
|
|
718
768
|
err(` Re-run the tour after the reset (or talk to us about a production workspace).`);
|
|
719
|
-
return 1;
|
|
769
|
+
return failTour(1);
|
|
720
770
|
}
|
|
721
771
|
if (!decisionId && earlyError) {
|
|
722
772
|
err(`✘ the gate refused before routing: ${earlyError?.message ?? earlyError}`);
|
|
723
|
-
return 1;
|
|
773
|
+
return failTour(1);
|
|
724
774
|
}
|
|
725
775
|
if (!decisionId) {
|
|
726
776
|
err("✘ the gate did not route within 30s — check connectivity (npx @decidio/sdk doctor) and re-run.");
|
|
727
|
-
return 1;
|
|
777
|
+
return failTour(1);
|
|
728
778
|
}
|
|
729
779
|
log(` ✔ $86,000 payInvoice ROUTED to a human (decision ${decisionId}) — this agent has no auto-approve rule,`);
|
|
730
780
|
log(` so deny-by-default sends everything to a person. Nothing executes until someone decides.`);
|
|
@@ -749,35 +799,50 @@ async function cmdQuickstart(opts = {}) {
|
|
|
749
799
|
log(" Then re-run the quickstart interactively to watch the whole loop — or, for CI, re-run");
|
|
750
800
|
log(" with --auto-approve-demo to let the tour approve itself under that explicit label.");
|
|
751
801
|
log(` Tidy up this tour's agent + token any time: npx @decidio/sdk quickstart cleanup`);
|
|
752
|
-
|
|
802
|
+
log("");
|
|
803
|
+
log(" (exit 3 = the tour stopped at the human moment. Nothing executed and no receipt was");
|
|
804
|
+
log(" written, so this run is deliberately not reported as a completed tour.)");
|
|
805
|
+
log("");
|
|
806
|
+
log(` NOTE: this tour's credentials are still live in ${envFile} (a 7-day token + the agent's`);
|
|
807
|
+
log(` key), because the decision above is still waiting for you. Once you have approved it —`);
|
|
808
|
+
log(` or if you abandon the tour — remove them: npx @decidio/sdk quickstart cleanup`);
|
|
809
|
+
process.exit(3);
|
|
753
810
|
}
|
|
754
811
|
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);
|
|
755
812
|
if (!(ap.status >= 200 && ap.status < 300)) {
|
|
756
813
|
err(`✘ approve failed (${ap.status}): ${JSON.stringify(ap.body)}`);
|
|
757
|
-
return 1;
|
|
814
|
+
return failTour(1);
|
|
758
815
|
}
|
|
759
816
|
log(` ✔ approved${ap.body?.alreadyResolved ? " (it was already approved from your app — even better)" : ""}`);
|
|
760
|
-
let
|
|
817
|
+
let done;
|
|
761
818
|
try {
|
|
762
|
-
|
|
819
|
+
done = await running;
|
|
763
820
|
}
|
|
764
821
|
catch (e) {
|
|
765
822
|
err(`✘ the gated call did not complete: ${e?.message ?? e}`);
|
|
766
|
-
return 1;
|
|
823
|
+
return failTour(1);
|
|
767
824
|
}
|
|
768
|
-
log(` ✔ your function executed AFTER approval: ${JSON.stringify(
|
|
825
|
+
log(` ✔ your function executed AFTER approval: ${JSON.stringify(done.value)}`);
|
|
826
|
+
if (done.confirmation?.evidenceTier)
|
|
827
|
+
log(` …and the call itself told you what was recorded: confirmation.evidenceTier = ${done.confirmation.evidenceTier}`);
|
|
769
828
|
log("");
|
|
770
829
|
log("④ Downloading the sealed Authority Receipt and verifying it OFFLINE…");
|
|
771
830
|
const rec = await api(`/api/records/${encodeURIComponent(decisionId)}/verify`, {}, session);
|
|
772
831
|
if (!(rec.status >= 200 && rec.status < 300) || !rec.body?.credential) {
|
|
773
832
|
err(`✘ receipt fetch failed (${rec.status})`);
|
|
774
|
-
return 1;
|
|
833
|
+
return failTour(1);
|
|
775
834
|
}
|
|
776
835
|
const fname = `decidio-receipt-${decisionId.replace(/[^A-Za-z0-9_-]/g, "")}.json`;
|
|
777
836
|
writeFileSync(fname, JSON.stringify(rec.body.credential, null, 2) + "\n");
|
|
778
837
|
const issuer = rec.body?.issuer;
|
|
779
838
|
const v = verifyReceipt(rec.body.credential, issuer);
|
|
780
|
-
log(` ${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? `, PINNED to your workspace issuer ${issuer}` : "
|
|
839
|
+
log(` ${v.valid ? "✔ VALID" : "✘ INVALID"} — ${fname} (signature + content binding, offline${issuer ? `, PINNED to your workspace issuer ${issuer}` : ""})`);
|
|
840
|
+
if (!issuer) {
|
|
841
|
+
err(` ⚠ the server did not return an issuer DID, so this check ran UNPINNED — it proves the`);
|
|
842
|
+
err(` receipt is internally consistent, NOT who issued it (a self-signed forgery also passes).`);
|
|
843
|
+
err(` Get your workspace's issuer DID from the app's record-verify panel, then re-check:`);
|
|
844
|
+
err(` npx @decidio/sdk verify --issuer <did:key:...> ${fname}`);
|
|
845
|
+
}
|
|
781
846
|
if (issuer)
|
|
782
847
|
log(` Keep that DID: it is your trust anchor — npx @decidio/sdk verify --issuer ${issuer} <receipt.json>`);
|
|
783
848
|
const detail = await api(`/api/records/${encodeURIComponent(decisionId)}`, {}, session);
|
|
@@ -805,9 +870,8 @@ async function cmdQuickstart(opts = {}) {
|
|
|
805
870
|
doCleanup = true;
|
|
806
871
|
log(`\nAutomated run: cleaning the tour up (pass --keep-agent to keep the agent + token).`);
|
|
807
872
|
}
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
return v.valid ? 0 : 1;
|
|
873
|
+
const cleanupCode = doCleanup ? await quickstartCleanup(session, envFile) : 0;
|
|
874
|
+
return v.valid ? (cleanupCode || 0) : 1;
|
|
811
875
|
}
|
|
812
876
|
async function quickstartCleanup(session, envFile) {
|
|
813
877
|
const env = (() => {
|
|
@@ -834,7 +898,12 @@ async function quickstartCleanup(session, envFile) {
|
|
|
834
898
|
}
|
|
835
899
|
}
|
|
836
900
|
else {
|
|
837
|
-
|
|
901
|
+
err(`✘ no DECIDIO_TOKEN_JTI in ${envFile} (older tour) — this tour's token is STILL LIVE and this`);
|
|
902
|
+
err(` command cannot revoke it. Nothing was deleted. Find and revoke it, then re-run cleanup:`);
|
|
903
|
+
err(` npx @decidio/sdk tokens list`);
|
|
904
|
+
err(` npx @decidio/sdk tokens revoke <jti>`);
|
|
905
|
+
err(` (it also expires on its own within 7 days)`);
|
|
906
|
+
return 1;
|
|
838
907
|
}
|
|
839
908
|
if (existsSync(envFile)) {
|
|
840
909
|
const TOUR_KEYS = new Set(["DECIDIO_API_URL", "DECIDIO_AGENT_ID", "DECIDIO_AGENT_DID", "DECIDIO_AGENT_KEY", "DECIDIO_API_TOKEN", "DECIDIO_WORKSPACE_ID", "DECIDIO_TOKEN_JTI"]);
|
|
@@ -971,6 +1040,8 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
971
1040
|
at the routed decision; --auto-approve-demo for CI, labeled
|
|
972
1041
|
automated; ends with a keep-or-cleanup choice)
|
|
973
1042
|
npx @decidio/sdk quickstart cleanup revoke the tour's token + delete its local credentials
|
|
1043
|
+
npx @decidio/sdk quickstart --auto-approve-demo headless tour; cleans up after itself
|
|
1044
|
+
(add --keep-agent to KEEP the tour agent + token)
|
|
974
1045
|
npx @decidio/sdk tokens list [--mine] agent tokens with status (active/expired/REVOKED) by
|
|
975
1046
|
jti — token values are never re-shown; the list is
|
|
976
1047
|
workspace-wide, --mine narrows to this directory's agent
|
|
@@ -984,8 +1055,10 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
984
1055
|
npx @decidio/sdk approvals [--json] [--mine] list pending agent approvals
|
|
985
1056
|
npx @decidio/sdk approvals approve <id> [reason]
|
|
986
1057
|
npx @decidio/sdk approvals reject <id> [reason]
|
|
987
|
-
the reason is FREE TEXT
|
|
988
|
-
|
|
1058
|
+
the reason is FREE TEXT, sent exactly as typed. It seals
|
|
1059
|
+
immutably; personal or sensitive values are tokenized at
|
|
1060
|
+
rest, so the STORED text can differ (see: receipt <id>).
|
|
1061
|
+
If it starts with a dash, put it after -- or use --reason:
|
|
989
1062
|
... approve <id> -- "-50% cut, per the board"
|
|
990
1063
|
... approve <id> --reason "-50% cut, per the board"
|
|
991
1064
|
npx @decidio/sdk receipt <id> download a sealed decision's Authority Receipt (.json)
|
|
@@ -996,7 +1069,9 @@ const USAGE = `decidio — the Decidio SDK command line
|
|
|
996
1069
|
Every NETWORK command talks to the hosted sandbox (https://decidio-api.onrender.com) unless
|
|
997
1070
|
DECIDIO_API_URL says otherwise; the target host is announced on stderr each run. "verify" is
|
|
998
1071
|
fully offline — it touches no network and reads no .env.
|
|
999
|
-
Exit codes: 0 success · 1 failure or honest no-op (e.g. replay refused) · 2 usage error
|
|
1072
|
+
Exit codes: 0 success · 1 failure or honest no-op (e.g. replay refused) · 2 usage error ·
|
|
1073
|
+
3 quickstart stopped at the human moment (headless: routed, but nothing executed and no
|
|
1074
|
+
receipt written — pass --auto-approve-demo for a run that completes unattended).
|
|
1000
1075
|
Errors, and usage shown on ERROR paths, go to stderr; stdout carries results only (safe to
|
|
1001
1076
|
pipe). Asked-for help — including this bare invocation — IS a result, so it prints here.
|
|
1002
1077
|
Docs: the README that shipped with this package — node_modules/@decidio/sdk/README.md
|
|
@@ -1037,6 +1112,13 @@ async function main(argv) {
|
|
|
1037
1112
|
positionals.push(...argsIn.slice(i + 1));
|
|
1038
1113
|
break;
|
|
1039
1114
|
}
|
|
1115
|
+
const inFreeTextTail = cmd === "approvals"
|
|
1116
|
+
&& (positionals[0] === "approve" || positionals[0] === "reject")
|
|
1117
|
+
&& positionals.length >= 2;
|
|
1118
|
+
if (!inFreeTextTail && a.startsWith("-") && !a.startsWith("--")) {
|
|
1119
|
+
err(`✘ unknown option "${a}" for \`${cmd}\` — refusing to guess (a flag you meant to matter must never be silently ignored). Short flags are not supported; use the long form.\n\n${USAGE}`);
|
|
1120
|
+
return 2;
|
|
1121
|
+
}
|
|
1040
1122
|
if (!a.startsWith("--")) {
|
|
1041
1123
|
positionals.push(a);
|
|
1042
1124
|
continue;
|
|
@@ -1108,12 +1190,15 @@ async function main(argv) {
|
|
|
1108
1190
|
const head = dashdash >= 0 ? argsIn.slice(0, dashdash) : argsIn;
|
|
1109
1191
|
const literal = dashdash >= 0 ? argsIn.slice(dashdash + 1) : [];
|
|
1110
1192
|
const rIdx = head.indexOf("--reason");
|
|
1193
|
+
const rEq = head.find((x) => x.startsWith("--reason="));
|
|
1111
1194
|
const flagless = head.filter((x, i) => !x.startsWith("-") && !(rIdx >= 0 && i === rIdx + 1));
|
|
1112
1195
|
const sub = flagless[0];
|
|
1113
1196
|
const id = flagless[1];
|
|
1114
1197
|
let reason;
|
|
1115
1198
|
if (rIdx >= 0)
|
|
1116
1199
|
reason = head[rIdx + 1];
|
|
1200
|
+
else if (rEq !== undefined)
|
|
1201
|
+
reason = rEq.slice("--reason=".length);
|
|
1117
1202
|
else if (literal.length)
|
|
1118
1203
|
reason = literal.join(" ");
|
|
1119
1204
|
else if (sub && id) {
|
|
@@ -1121,10 +1206,14 @@ async function main(argv) {
|
|
|
1121
1206
|
const rest = head.slice(after).filter((x) => x !== "--json" && x !== "--mine");
|
|
1122
1207
|
reason = rest.length ? rest.join(" ") : undefined;
|
|
1123
1208
|
}
|
|
1124
|
-
if (rIdx >= 0 && (reason === undefined || reason.startsWith("
|
|
1209
|
+
if ((rIdx >= 0 || rEq !== undefined) && (reason === undefined || reason === "" || (rIdx >= 0 && reason.startsWith("--")))) {
|
|
1125
1210
|
err(`✘ --reason needs the text to record (quote it if it contains spaces): npx @decidio/sdk approvals ${sub ?? "approve"} <id> --reason "over budget -50% cut required"`);
|
|
1126
1211
|
return 2;
|
|
1127
1212
|
}
|
|
1213
|
+
if (reason !== undefined && sub !== "approve" && sub !== "reject") {
|
|
1214
|
+
err(`✘ --reason only applies to approve/reject (a sealed reason needs a decision to seal onto): npx @decidio/sdk approvals approve <id> --reason "..."`);
|
|
1215
|
+
return 2;
|
|
1216
|
+
}
|
|
1128
1217
|
return cmdApprovals(sub, id, reason, { json: head.includes("--json"), mine: head.includes("--mine") });
|
|
1129
1218
|
}
|
|
1130
1219
|
case "receipt": return cmdReceipt(argsIn.find((x) => !x.startsWith("-")), argsIn.includes("--json"));
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/** A gate response this SDK could not accept, carrying what a caller needs to BRANCH on rather
|
|
2
|
-
* than a stringified body they would have to parse. `status` is the HTTP status; `retryable`
|
|
3
|
-
* distinguishes a terminal refusal (401/403 — a credential problem no retry fixes) from a
|
|
4
|
-
* transient one (5xx, 429). Every long-running agent hits the terminal case eventually: an
|
|
5
|
-
* agent token expires, or `init` is re-run and revokes it. */
|
|
6
1
|
export declare class DecidioHttpError extends Error {
|
|
7
2
|
status: number;
|
|
8
3
|
endpoint: string;
|
|
@@ -10,3 +5,15 @@ export declare class DecidioHttpError extends Error {
|
|
|
10
5
|
readonly retryable: boolean;
|
|
11
6
|
constructor(status: number, endpoint: string, body?: unknown | undefined);
|
|
12
7
|
}
|
|
8
|
+
/** ONE rule for what counts as a usable decision id, returning a REASON rather than throwing, so
|
|
9
|
+
* every door can apply the same check and raise its own error type. (`resume.ts` cannot import
|
|
10
|
+
* `index.ts`, which is why the check was copy-pasted into four places and drifted.)
|
|
11
|
+
*
|
|
12
|
+
* All four copies tested truthiness only, so `" "` passed every one of them: the blocking path
|
|
13
|
+
* would poll `/agent/status/%20%20%20`, the durable path would park under a whitespace key, and
|
|
14
|
+
* the confirmation would attest a decision nobody can look up. Control characters are refused for
|
|
15
|
+
* the same reason a reason-string is sanitised — an id goes into a URL path, a filename and an
|
|
16
|
+
* audit line. Nothing is silently trimmed: an id that needs trimming is not the id the server
|
|
17
|
+
* sent, and quietly rewriting an identifier is how two systems end up disagreeing about which
|
|
18
|
+
* decision they mean. */
|
|
19
|
+
export declare function decisionIdProblem(raw: unknown): string | null;
|
package/dist/errors.js
CHANGED
|
@@ -1,18 +1,50 @@
|
|
|
1
|
+
const RETRYABLE_STATUS = (status) => status === 408 || status === 429 || (status >= 500 && status !== 501);
|
|
2
|
+
function sideEffectNote(endpoint) {
|
|
3
|
+
if (endpoint.startsWith("/agent/confirm")) {
|
|
4
|
+
return " Your action ALREADY RAN — only its report failed. Do NOT re-run it.";
|
|
5
|
+
}
|
|
6
|
+
if (endpoint.startsWith("/agent/status")) {
|
|
7
|
+
return " This call is a read, so it changed nothing; the decision itself is unaffected.";
|
|
8
|
+
}
|
|
9
|
+
return " Nothing was routed or executed.";
|
|
10
|
+
}
|
|
1
11
|
export class DecidioHttpError extends Error {
|
|
2
12
|
status;
|
|
3
13
|
endpoint;
|
|
4
14
|
body;
|
|
5
15
|
retryable;
|
|
6
16
|
constructor(status, endpoint, body) {
|
|
7
|
-
const
|
|
17
|
+
const credential = status === 401 || status === 403;
|
|
18
|
+
const retryable = !credential && RETRYABLE_STATUS(status);
|
|
8
19
|
super(`[decidio] ${endpoint} refused this call (HTTP ${status})` +
|
|
9
|
-
(
|
|
10
|
-
? ` — the credential was rejected. The agent token may have expired or been revoked (re-running \`init\` for the same agent revokes the previous one). Mint a fresh one: npx @decidio/sdk init <agent-name>, then restart this process
|
|
11
|
-
:
|
|
20
|
+
(credential
|
|
21
|
+
? ` — the credential was rejected. The agent token may have expired or been revoked (re-running \`init\` for the same agent revokes the previous one). Mint a fresh one: npx @decidio/sdk init <agent-name>, then restart this process.`
|
|
22
|
+
: retryable
|
|
23
|
+
? ` — this looks transient; retrying is safe (a retry creates a NEW request and can never double-execute).`
|
|
24
|
+
: ` — the server refused the request itself, so retrying it unchanged will fail the same way. Fix the request (or the decision it names) first.`) +
|
|
25
|
+
sideEffectNote(endpoint));
|
|
12
26
|
this.status = status;
|
|
13
27
|
this.endpoint = endpoint;
|
|
14
28
|
this.body = body;
|
|
15
29
|
this.name = "DecidioHttpError";
|
|
16
|
-
this.retryable =
|
|
30
|
+
this.retryable = retryable;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export function decisionIdProblem(raw) {
|
|
34
|
+
if (typeof raw !== "string")
|
|
35
|
+
return `it is ${raw === null ? "null" : typeof raw}, not a string`;
|
|
36
|
+
if (raw.length === 0)
|
|
37
|
+
return "it is empty";
|
|
38
|
+
if (raw.trim().length === 0)
|
|
39
|
+
return "it is only whitespace";
|
|
40
|
+
if (raw !== raw.trim())
|
|
41
|
+
return "it has leading or trailing whitespace (ids are used verbatim in URLs, filenames and audit lines — this SDK will not silently trim one)";
|
|
42
|
+
for (const ch of raw) {
|
|
43
|
+
const cp = ch.codePointAt(0);
|
|
44
|
+
if (cp < 0x20 || cp === 0x7f)
|
|
45
|
+
return "it contains control characters";
|
|
17
46
|
}
|
|
47
|
+
if (raw.length > 256)
|
|
48
|
+
return `it is ${raw.length} characters long (limit 256)`;
|
|
49
|
+
return null;
|
|
18
50
|
}
|
package/dist/evidence.d.ts
CHANGED
|
@@ -13,6 +13,29 @@ export interface EvidenceOutcome {
|
|
|
13
13
|
/** Present when the outcome is worth explaining (why a signed report did not confirm, or why
|
|
14
14
|
* a report was not recorded at all) — always actionable, never a shrug. */
|
|
15
15
|
diagnostic?: string;
|
|
16
|
+
/** Present when the gate reported on the SIGNED execution report: whether the attestation
|
|
17
|
+
* proof VERIFIED (C3, 0.1.12 — this field used to be read off the wire and discarded, so a
|
|
18
|
+
* failed attestation was invisible to every client door). */
|
|
19
|
+
executionReportVerified?: boolean;
|
|
20
|
+
/** WHY the attestation did not verify, verbatim from the gate (unknown did, expired proof,
|
|
21
|
+
* burnt nonce, payload mismatch). It was folded into `diagnostic` prose and nowhere else, so a
|
|
22
|
+
* caller who wanted to BRANCH on the reason — retry vs re-register vs alert — had to regex an
|
|
23
|
+
* English sentence. Same rule as the typed HTTP errors: give callers the fact, not a string to
|
|
24
|
+
* parse. */
|
|
25
|
+
executionReportReason?: string;
|
|
26
|
+
/** Whether re-sending this report could succeed. False for a refusal the server will repeat
|
|
27
|
+
* (an unrecognised tier, a denied success flag); true for a transport or availability failure.
|
|
28
|
+
* Undefined when the report WAS recorded and there is nothing to retry. */
|
|
29
|
+
confirmationRetryable?: boolean;
|
|
30
|
+
/** Whether the reported value reached durable storage, so `reconfirm()` can re-send it. On the
|
|
31
|
+
* inline path this is false by construction — nothing is parked — which is exactly why an
|
|
32
|
+
* inline caller needs to see the outcome at the call site rather than in a log line. */
|
|
33
|
+
reportJournalled?: boolean;
|
|
34
|
+
/** Set when the caller's `evidence` mapper (or the canonical model) rejected the return value,
|
|
35
|
+
* so an EMPTY captured response was reported. The execution is recorded; its evidence is not.
|
|
36
|
+
* This was previously a console.warn and nothing else — invisible to the code that could
|
|
37
|
+
* actually fix it. */
|
|
38
|
+
evidenceError?: string;
|
|
16
39
|
}
|
|
17
40
|
/** The SERVER's rule for `application_confirmed`, restated here exactly as implemented so this
|
|
18
41
|
* explanation can never drift from it: the captured response must carry a RECORD ID
|
package/dist/evidence.js
CHANGED
|
@@ -20,23 +20,34 @@ export function describeTierGap(capturedResponse) {
|
|
|
20
20
|
return "the report was accepted but the gate recorded a weaker tier — check that this agent's registration and signing key still match (`npx @decidio/sdk doctor`).";
|
|
21
21
|
}
|
|
22
22
|
export function readConfirmResponse(body, opts) {
|
|
23
|
-
const
|
|
23
|
+
const journalled = opts.journalled ?? false;
|
|
24
|
+
const retry = journalled
|
|
24
25
|
? "re-send the report with `guard.resume.reconfirm(decisionId)` once connectivity or the service is healthy."
|
|
25
26
|
: "this path journalled no report, so there is nothing to re-send from here — record the outcome from your own system, or run the action through `guard.protect`'s durable mode, where reports ARE journalled and `reconfirm` can replay them.";
|
|
26
27
|
const b = body;
|
|
27
28
|
if (!b || b.ok !== true || typeof b.confirmation !== "string") {
|
|
28
|
-
return { reportRecorded: false, executionConfirmed: false, diagnostic: `the gate answered 2xx but named no evidence tier — the execution report is NOT recorded. Your action DID run; ${retry}` };
|
|
29
|
+
return { reportRecorded: false, executionConfirmed: false, confirmationRetryable: true, reportJournalled: journalled, diagnostic: `the gate answered 2xx but named no evidence tier — the execution report is NOT recorded. Your action DID run; ${retry}` };
|
|
29
30
|
}
|
|
30
31
|
const tier = b.confirmation;
|
|
31
32
|
if (!EVIDENCE_TIERS.includes(tier)) {
|
|
32
|
-
return { reportRecorded: false, executionConfirmed: false, diagnostic: `the gate reported an evidence tier this SDK does not recognise (${JSON.stringify(tier)}) — refusing to treat it as evidence. Upgrade @decidio/sdk; your action DID run, and ${retry}` };
|
|
33
|
+
return { reportRecorded: false, executionConfirmed: false, confirmationRetryable: false, reportJournalled: journalled, diagnostic: `the gate reported an evidence tier this SDK does not recognise (${JSON.stringify(tier)}) — refusing to treat it as evidence. Upgrade @decidio/sdk; your action DID run, and ${retry}` };
|
|
33
34
|
}
|
|
34
35
|
const executionConfirmed = CONFIRMING.includes(tier);
|
|
35
|
-
const outcome = { reportRecorded: true, evidenceTier: tier, executionConfirmed };
|
|
36
|
+
const outcome = { reportRecorded: true, evidenceTier: tier, executionConfirmed, reportJournalled: journalled };
|
|
36
37
|
if (!executionConfirmed && opts.signed)
|
|
37
38
|
outcome.diagnostic = describeTierGap(opts.capturedResponse);
|
|
38
39
|
else if (!executionConfirmed)
|
|
39
40
|
outcome.diagnostic = "no signing key is configured, so the gate capped this report at an unproven agent assertion. Run `npx @decidio/sdk init` to generate the agent's key and earn a confirming tier.";
|
|
41
|
+
const rpt = body?.executionReport;
|
|
42
|
+
if (rpt && typeof rpt === "object" && typeof rpt.verified === "boolean") {
|
|
43
|
+
outcome.executionReportVerified = rpt.verified;
|
|
44
|
+
if (typeof rpt.reason === "string" && rpt.reason)
|
|
45
|
+
outcome.executionReportReason = rpt.reason;
|
|
46
|
+
if (!rpt.verified) {
|
|
47
|
+
const why = typeof rpt.reason === "string" && rpt.reason ? ` (${rpt.reason})` : "";
|
|
48
|
+
outcome.diagnostic = `${outcome.diagnostic ? outcome.diagnostic + " " : ""}The signed execution report did NOT verify${why} — the report was recorded without a proven attestation. Check the agent's key/DID/registration (npx @decidio/sdk doctor).`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
40
51
|
return outcome;
|
|
41
52
|
}
|
|
42
53
|
import { signExecutionReport, resultDigest } from "./signing.js";
|
package/dist/guard.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ApprovalContext } from "./index.js";
|
|
1
|
+
import { type ApprovalContext, type ProtectedExecution } from "./index.js";
|
|
2
2
|
import { type ResumeController, type ResumeOutcome } from "./resume.js";
|
|
3
3
|
export interface GuardFacadeConfig {
|
|
4
4
|
apiUrl: string;
|
|
@@ -28,6 +28,12 @@ export interface ProtectOptions {
|
|
|
28
28
|
* across every door), but their ENGINE integration is verified against structural mocks, not
|
|
29
29
|
* live engines. Use the core path for production until that changes. */
|
|
30
30
|
adapter?: "generic" | "langgraph" | "openai" | "temporal" | "inngest";
|
|
31
|
+
/** Hand the action an `ExecutionContext` as its FIRST argument: `{ decisionId, attemptId,
|
|
32
|
+
* idempotencyKey }`. Pass `idempotencyKey` to your downstream API and the one window this SDK
|
|
33
|
+
* cannot close — the vendor committing twice — closes on their side. Opt-in by design: arity
|
|
34
|
+
* inspection would have to guess, and guessing wrong hands a payment call a context object
|
|
35
|
+
* where it expected an invoice. */
|
|
36
|
+
withContext?: boolean;
|
|
31
37
|
/** "durable" (default) suspends for async approval; "blocking" polls (short approvals only). */
|
|
32
38
|
mode?: "durable" | "blocking";
|
|
33
39
|
/** The action name this wrapper produces, declared UP FRONT so the resume registry is populated
|
|
@@ -65,7 +71,7 @@ export declare class Guard {
|
|
|
65
71
|
/** Register a raw fn under its action name so a COLD process (one that never made the original
|
|
66
72
|
* call) can resume it. Call this at startup beside `guard.worker()` — or pass
|
|
67
73
|
* `{ action }` to `protect(...)`, which does it for you at wrap time. */
|
|
68
|
-
register(action: string, fn: (...a: any[]) => Promise<any
|
|
74
|
+
register(action: string, fn: (...a: any[]) => Promise<any>, evidence?: (result: any) => unknown): void;
|
|
69
75
|
/** Mount on a node/Express/Fastify route: `app.post("/decidio/resume", guard.resumeHandler())`. */
|
|
70
76
|
resumeHandler(): import("http").RequestListener;
|
|
71
77
|
/** Mount on a Next.js/Hono/CF/Bun route: `export const POST = (req) => guard.resumeFetchHandler()(req)`. */
|
|
@@ -84,7 +90,18 @@ export declare class Guard {
|
|
|
84
90
|
/** Throws when the facade has no gate URL — at USE time, not import time, so `import { guard }`
|
|
85
91
|
* stays safe in envless contexts. */
|
|
86
92
|
private assertConfigured;
|
|
87
|
-
|
|
93
|
+
/** Wrap a function so every call is authorized, recorded and — in 0.2.0 — REPORTED BACK.
|
|
94
|
+
*
|
|
95
|
+
* Returns `{ value, decisionId, confirmation }`, not the bare value. Through 0.1.x it returned
|
|
96
|
+
* `R` and the confirmation outcome went to a console warning, so the one thing a caller of an
|
|
97
|
+
* authority SDK could not do was find out what Decidio recorded. If you genuinely do not need
|
|
98
|
+
* it, `protectBestEffort` keeps the old shape. */
|
|
99
|
+
protect<A extends any[], R>(fn: (...a: A) => Promise<R>, describe: (...a: A) => ApprovalContext, opts?: ProtectOptions): (...a: A) => Promise<ProtectedExecution<R>>;
|
|
100
|
+
/** @deprecated The 0.1.x shape: resolves to your function's value alone and DISCARDS what
|
|
101
|
+
* Decidio recorded. Kept because a caller who truly does not care should not have to
|
|
102
|
+
* destructure — but "the report was not recorded" and "the execution could not be attested"
|
|
103
|
+
* become invisible again, which is the gap 0.2.0 exists to close. Prefer `protect`. */
|
|
104
|
+
protectBestEffort<A extends any[], R>(fn: (...a: A) => Promise<R>, describe: (...a: A) => ApprovalContext, opts?: ProtectOptions): (...a: A) => Promise<R>;
|
|
88
105
|
}
|
|
89
106
|
/** Preconfigured singleton — `import { guard } from "@decidio/sdk"`. */
|
|
90
107
|
export declare const guard: Guard;
|
package/dist/guard.js
CHANGED
|
@@ -24,7 +24,7 @@ export class Guard {
|
|
|
24
24
|
appUrl: process.env.DECIDIO_APP_URL,
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
|
-
register(action, fn) { this.ensureResume().register(action, fn); }
|
|
27
|
+
register(action, fn, evidence) { this.ensureResume().register(action, fn, evidence); }
|
|
28
28
|
resumeHandler() { return this.ensureResume().webhookHandler(); }
|
|
29
29
|
resumeFetchHandler() { return this.ensureResume().fetchHandler(); }
|
|
30
30
|
worker(opts) {
|
|
@@ -68,14 +68,16 @@ export class Guard {
|
|
|
68
68
|
`(pollTimeoutMs), and when it does, the decision stays OPEN in Decidio while this process stops waiting: ` +
|
|
69
69
|
`a later approval cannot resume it. Only safe for short, watched approvals — omit mode:"blocking" for ` +
|
|
70
70
|
`durable async resume on real (minutes-to-days) approvals.`);
|
|
71
|
-
return withApproval(fn, describe, base, opts?.evidence ? { evidence: opts.evidence } :
|
|
71
|
+
return withApproval(fn, describe, base, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}) });
|
|
72
72
|
}
|
|
73
73
|
const resume = this.ensureResume();
|
|
74
74
|
if (opts?.action)
|
|
75
|
-
resume.register(opts.action, fn);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
resume.register(opts.action, fn, opts?.evidence, opts?.withContext);
|
|
76
|
+
return withApproval(fn, describe, { ...base, resumeUrl: c.resumeUrl, resume }, { ...(opts?.evidence ? { evidence: opts.evidence } : {}), ...(opts?.withContext ? { withContext: true } : {}) });
|
|
77
|
+
}
|
|
78
|
+
protectBestEffort(fn, describe, opts) {
|
|
79
|
+
const wrapped = this.protect(fn, describe, opts);
|
|
80
|
+
return async (...a) => (await wrapped(...a)).value;
|
|
79
81
|
}
|
|
80
82
|
}
|
|
81
83
|
export const guard = Guard.fromEnv();
|