@decidio/sdk 0.6.4 → 0.8.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/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
- return send({ decision: "route", decisionId: DECISION_ID, receiptId: "R-DEMO-1" });
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, boundScope, 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;
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
- import { DecidioSuspendedError } from "./resume.js";
2
- import { signBoundProof, signConfirm, assertSafeTransport, carriesCredential } from "./signing.js";
1
+ import { DecidioSuspendedError, checkApproval } from "./resume.js";
2
+ import { signBoundProof, signConfirm, assertSafeTransport, carriesCredential, bindingMaterial, requestCommitment } from "./signing.js";
3
3
  import { canonicalize, canonicalizeArgs } from "./canonical.js";
4
4
  import { readConfirmResponse, buildConfirmBody } from "./evidence.js";
5
- export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
5
+ export { DecidioSuspendedError, DecidioDeniedError, DecidioRecoveryUnavailableError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded, checkApproval } from "./resume.js";
6
6
  export { guard, Guard } from "./guard.js";
7
7
  export { DecidioHttpError, decisionIdProblem } from "./errors.js";
8
8
  import { DecidioHttpError, decisionIdProblem } from "./errors.js";
9
- 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, boundScope, requestCommitment, idempotencyKey, committedRequestJson } from "./signing.js";
10
10
  export { canonicalize, canonicalizeArgs, canonicalJson, DecidioUnsafeArgumentsError } from "./canonical.js";
11
11
  export { readConfirmResponse, buildConfirmBody, describeTierGap, EVIDENCE_TIERS } from "./evidence.js";
12
12
  export class DecidioBlockedError extends Error {
@@ -29,6 +29,40 @@ export class DecidioRejectedError extends Error {
29
29
  this.name = "DecidioRejectedError";
30
30
  }
31
31
  }
32
+ export class DecidioRequestChangedError extends DecidioBlockedError {
33
+ code = "request_changed";
34
+ constructor(decisionId, detail) {
35
+ super(detail ?? `the request changed after approval (decision ${decisionId})`, decisionId);
36
+ this.name = "DecidioRequestChangedError";
37
+ this.message = `[decidio] the request changed after approval: decision ${decisionId} was approved for a different request than the one about to run. Nothing was executed. ${detail && !/^the request changed after approval/.test(detail) ? `(${detail}) ` : ""}A changed request needs its own approval — reject ${decisionId} and run the action again.`;
38
+ }
39
+ }
40
+ export class DecidioApprovalUnverifiedError extends DecidioBlockedError {
41
+ code;
42
+ constructor(decisionId, code, detail) {
43
+ super(detail, decisionId);
44
+ this.code = code;
45
+ this.name = "DecidioApprovalUnverifiedError";
46
+ this.message = `[decidio] cannot confirm that decision ${decisionId} approved THIS request: ${detail}. Nothing was executed.`;
47
+ }
48
+ }
49
+ export function approvalRefusal(check) {
50
+ const id = check.decisionId;
51
+ switch (check.code) {
52
+ case "rejected": return new DecidioRejectedError(check.reason ?? "rejected by approver", id);
53
+ case "blocked": return new DecidioBlockedError(check.reason ?? "blocked by policy", id);
54
+ case "request_changed": return new DecidioRequestChangedError(id, check.reason);
55
+ case "commitment_missing": return new DecidioApprovalUnverifiedError(id, "commitment_missing", check.reason ?? "Decidio answered without the request commitment this SDK sent");
56
+ case "unverifiable": return new DecidioApprovalUnverifiedError(id, "unverifiable", check.reason ?? "the approval could not be tied to this request");
57
+ case "pending": return new DecidioBlockedError(`not approved - ${check.reason ?? `Decidio records ${id} as pending`}`, id);
58
+ case "spent": return new DecidioBlockedError(`not executed - ${check.reason ?? `the approval for ${id} is already spent`}`, id);
59
+ case "expired": return new DecidioBlockedError(`not executed - ${check.reason ?? `the approval for ${id} has expired`}`, id);
60
+ default: return new DecidioBlockedError(check.reason ?? `not approved (Decidio answered ${JSON.stringify(check.state)})`, id);
61
+ }
62
+ }
63
+ export function coreRequest(ctx, args) {
64
+ return { action: ctx.action, amount: ctx.amount, ...(ctx.scope !== undefined && ctx.scope !== null ? { scope: ctx.scope } : {}), args };
65
+ }
32
66
  export class DecidioTimeoutError extends Error {
33
67
  decisionId;
34
68
  withdrawn;
@@ -153,12 +187,16 @@ export function withApproval(fn, describe, config, opts) {
153
187
  ? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
154
188
  : undefined;
155
189
  assertDescribed(ctx);
190
+ const material = bindingMaterial(config);
191
+ const committed = coreRequest(ctx, args);
192
+ const sentCommitment = material ? requestCommitment(material, committed) : undefined;
156
193
  const auth = await post(config, "/agent/authorize", {
157
194
  requester: config.agentId, action: ctx.action, amount: ctx.amount,
158
195
  scope: ctx.scope, sourceSystem: config.sourceSystem,
159
196
  ...(ctx.context ? { context: ctx.context } : {}),
160
197
  ...(requesterIdentity ? { requesterIdentity } : {}),
161
198
  ...(resumeUrl ? { resumeUrl } : {}),
199
+ ...(sentCommitment ? { requestCommitment: sentCommitment } : {}),
162
200
  });
163
201
  if (auth.decision !== "proceed" && auth.decision !== "route" && auth.decision !== "block")
164
202
  throw new DecidioBlockedError(`unrecognized gate verdict: ${String(auth.decision)}`, auth.decisionId);
@@ -181,14 +219,18 @@ export function withApproval(fn, describe, config, opts) {
181
219
  await config.resume.park({
182
220
  decisionId: auth.decisionId, receiptId: auth.receiptId,
183
221
  action: ctx.action, amount: ctx.amount, scope: ctx.scope, args,
222
+ ...(sentCommitment ? { requestCommitment: sentCommitment } : {}),
184
223
  });
185
224
  throw new DecidioSuspendedError(auth.decisionId, ctx.action);
186
225
  }
187
226
  const deadline = Date.now() + pollTimeout;
227
+ let approvedStatus;
188
228
  while (true) {
189
229
  const st = await get(config, `/agent/status/${encodeURIComponent(auth.decisionId)}`);
190
- if (st.state === "approved" || st.state === "auto_approved")
230
+ if (st.state === "approved" || st.state === "auto_approved") {
231
+ approvedStatus = st;
191
232
  break;
233
+ }
192
234
  if (st.state === "rejected")
193
235
  throw new DecidioRejectedError(st.reason ?? "rejected by approver", auth.decisionId);
194
236
  if (st.state === "blocked")
@@ -215,6 +257,9 @@ export function withApproval(fn, describe, config, opts) {
215
257
  }
216
258
  await sleep(pollInterval);
217
259
  }
260
+ const check = await checkApproval(config, auth.decisionId, committed, { sent: !!sentCommitment, ...(approvedStatus ? { status: approvedStatus } : {}) });
261
+ if (!check.ok)
262
+ throw approvalRefusal(check);
218
263
  }
219
264
  const result = opts?.withContext
220
265
  ? await fn({ decisionId: auth.decisionId, attemptId: null, idempotencyKey: auth.decisionId }, ...args)
package/dist/inngest.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { RequestListener } from "node:http";
2
2
  import { verifyResumeSignature } from "./resume.js";
3
- import type { GuardConfig, ApprovalContext, EvidenceOutcome } from "./index.js";
3
+ import { type GuardConfig, type ApprovalContext, type EvidenceOutcome } from "./index.js";
4
4
  /** The Inngest event Decidio's verdict is translated into. The waiting function
5
5
  * correlates on `data.decisionId`. */
6
6
  export declare const DECIDIO_RESUME_EVENT = "decidio/decision.resolved";
@@ -51,7 +51,10 @@ export type GateStepOutcome<R> =
51
51
  * Gate an action INSIDE an Inngest function, with durable suspend on routing.
52
52
  * • proceed → run the action (as a step) + confirm.
53
53
  * • route → `step.waitForEvent` durably suspends until Decidio's resume event for
54
- * THIS decision arrives; approved → run + confirm; rejected → status.
54
+ * THIS decision arrives - a WAKE-UP (0.7.0): a memoized step then confirms with
55
+ * Decidio that THIS request was approved; approved → run + confirm; rejected /
56
+ * blocked → status; still pending → wait for the next event; the request changed
57
+ * after approval → DecidioRequestChangedError.
55
58
  * • block → return blocked (the action never runs).
56
59
  * The action runs in YOUR process with YOUR credentials — Decidio only signals.
57
60
  * `config.resumeUrl` must point at the bridge endpoint (createInngestResumeBridge),
package/dist/inngest.js CHANGED
@@ -1,5 +1,6 @@
1
- import { verifyResumeSignature } from "./resume.js";
2
- import { gateAuthorize, gateConfirm, assertAuthorizeVerdict, verdictFrom, checkEngineVersion } from "./adapters/_gate.js";
1
+ import { verifyResumeSignature, WEBHOOK_SECRET_REMEDY } from "./resume.js";
2
+ import { gateAuthorize, gateConfirm, assertAuthorizeVerdict, gateCheckApproval, assertApprovalCheckShape, checkEngineVersion } from "./adapters/_gate.js";
3
+ import { approvalRefusal } from "./index.js";
3
4
  export const DECIDIO_RESUME_EVENT = "decidio/decision.resolved";
4
5
  export async function decidioGateStep(args) {
5
6
  const { step, config, ctx } = args;
@@ -8,18 +9,25 @@ export async function decidioGateStep(args) {
8
9
  if (auth.decision === "block")
9
10
  return { status: "blocked", decisionId: auth.decisionId };
10
11
  if (auth.decision === "route") {
11
- const ev = await step.waitForEvent("decidio-await-approval", {
12
- event: DECIDIO_RESUME_EVENT,
13
- timeout: args.timeout ?? "30d",
14
- if: `async.data.decisionId == "${auth.decisionId}"`,
15
- });
16
- if (!ev)
17
- throw new Error(`decidio approval timed out (${auth.decisionId})`);
18
- const verdict = verdictFrom(ev.data);
19
- if (verdict === "rejected")
20
- return { status: "rejected", decisionId: auth.decisionId };
21
- if (verdict !== "approved" && verdict !== "auto_approved")
22
- return { status: "blocked", decisionId: auth.decisionId };
12
+ for (let n = 0;; n++) {
13
+ const suffix = n === 0 ? "" : `-${n}`;
14
+ const ev = await step.waitForEvent(`decidio-await-approval${suffix}`, {
15
+ event: DECIDIO_RESUME_EVENT,
16
+ timeout: args.timeout ?? "30d",
17
+ if: `async.data.decisionId == "${auth.decisionId}"`,
18
+ });
19
+ if (!ev)
20
+ throw new Error(`decidio approval timed out (${auth.decisionId})`);
21
+ const check = assertApprovalCheckShape(await step.run(`decidio-verify-approval${suffix}`, async () => gateCheckApproval(config, auth.decisionId, ctx)), auth.decisionId);
22
+ if (check.ok)
23
+ break;
24
+ if (check.code === "rejected")
25
+ return { status: "rejected", decisionId: auth.decisionId };
26
+ if (check.code === "blocked")
27
+ return { status: "blocked", decisionId: auth.decisionId };
28
+ if (check.code !== "pending")
29
+ throw approvalRefusal(check);
30
+ }
23
31
  }
24
32
  const result = await step.run("agent-action", args.run);
25
33
  const confirmation = await step.run("decidio-confirm", async () => gateConfirm(config, auth.decisionId, result, args.evidence));
@@ -42,7 +50,7 @@ export function createInngestResumeBridge(opts) {
42
50
  if (!secret) {
43
51
  if (!opts.allowUnsigned) {
44
52
  res.statusCode = 401;
45
- return res.end(JSON.stringify({ error: "resume webhook unsigned and no secret configured" }));
53
+ return res.end(JSON.stringify({ error: `resume webhook unsigned and no secret configured. ${WEBHOOK_SECRET_REMEDY}` }));
46
54
  }
47
55
  }
48
56
  else if (!verifyResumeSignature(secret, raw, req.headers["x-decidio-signature"])) {
package/dist/resume.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { RequestListener } from "node:http";
2
+ import { type CommittedRequest } from "./signing.js";
2
3
  import { type EvidenceOutcome, type EvidenceTier } from "./evidence.js";
3
4
  export declare function signEvidence(decisionId: string, attemptId: string | null, evidence: unknown, agentKey?: string, webhookSecret?: string): {
4
5
  kind: "ed25519" | "hmac";
@@ -30,6 +31,13 @@ export interface PendingAction {
30
31
  kind: "ed25519" | "hmac";
31
32
  sig: string;
32
33
  };
34
+ /** The request commitment this SDK SENT with /agent/authorize (0.7.0) — see `requestCommitment`
35
+ * in signing.ts. Recorded so `resolve()` can apply the one fail-closed rule that needs to know
36
+ * what was sent: a gate that answers with no commitment for a request this SDK bound is refused.
37
+ * The value that decides execution is always a FRESH recompute over these parked arguments,
38
+ * compared with the commitment Decidio returns. Absent on entries parked before 0.7.0 (and by
39
+ * an agent with no key material), which are checked on Decidio's state alone. */
40
+ requestCommitment?: string;
33
41
  }
34
42
  /** Where suspended actions live between request and approval. The default is a
35
43
  * file store; swap in Redis/Postgres/your queue for production. AGENT-SIDE only —
@@ -462,7 +470,17 @@ export declare class DecidioSuspendedError extends Error {
462
470
  * parked for a later signal. It used to read `unknown` (the word for "not ours") for the claim loser and
463
471
  * `no_handler` for the boundary loser although a handler was registered (prodsec V040-01). A loss to a FINAL
464
472
  * answer (a denial, an execution) stays `already_resolved`. Additive, like the two above. */
465
- export type ResumeStatus = "executed" | "rejected" | "blocked" | "no_handler" | "unknown" | "indeterminate" | "already_resolved" | "lost_race";
473
+ /** `not_approved` (0.7.0): the signal said "approved" but DECIDIO does not record an approval for this
474
+ * decision (it is still pending, or answered a state this SDK does not recognise). Every resume signal
475
+ * is a wake-up now, never the authority: the approval is confirmed with GET /agent/status right before
476
+ * the action runs. Nothing ran; the entry stays parked and a later signal resolves it. Not a resolution,
477
+ * so `onResolved` does not fire (like `unknown`).
478
+ * `request_unverified` (0.7.0): Decidio records an approval, but it cannot be tied to THIS parked
479
+ * request — the request changed after approval (the commitment Decidio holds does not match a fresh
480
+ * recompute over the parked arguments), the gate kept no commitment for a request this SDK bound, or
481
+ * this process lacks the key material to recompute it. `reason` says which. Nothing ran; the entry
482
+ * stays parked for inspection. Both members are additive, announced in the changelog. */
483
+ export type ResumeStatus = "executed" | "rejected" | "blocked" | "no_handler" | "unknown" | "indeterminate" | "already_resolved" | "lost_race" | "not_approved" | "request_unverified";
466
484
  /** The states a durable action can actually be in — every one of them nameable, including the
467
485
  * one nobody likes. */
468
486
  export type RecoveryState =
@@ -558,8 +576,15 @@ export interface ResumeConfig {
558
576
  /** The workspace the agent acts in (DECIDIO_WORKSPACE_ID) — bound into the cold-resume confirm
559
577
  * proof (D-B3.6) so it's tenant-scoped too. Default from env when unset. */
560
578
  workspaceId?: string;
561
- /** Shared secret Decidio signs the resume webhook with (HMAC-SHA256). Defaults to
562
- * the API token if unset. Verified before any re-execution. */
579
+ /** Shared secret Decidio signs the resume webhook with (HMAC-SHA256) — DECIDIO_WEBHOOK_SECRET, the
580
+ * dedicated secret your Decidio deployment signs resume webhooks with (never its API_AUTH_TOKEN, the
581
+ * admin bearer). NO FALLBACK since 0.7.0: it used to default to
582
+ * the API token, so anyone holding a leaked agent token could sign an "approved" callback. With
583
+ * no secret (and no `allowUnsigned`) the webhook handlers refuse every call. The poll worker
584
+ * (`worker()`) needs no inbound secret. The webhook is only a WAKE-UP either way: the approval
585
+ * is confirmed with Decidio (GET /agent/status + the request commitment) before anything runs.
586
+ * (The parked-payload integrity HMAC still falls back to the API token when this is unset — that
587
+ * key never authenticates a signal.) */
563
588
  webhookSecret?: string;
564
589
  /** @deprecated Ignored since 0.2.0 and accepted only so existing callers keep compiling.
565
590
  * Execution evidence now lives in the PendingStore's own execution record, beside the state
@@ -604,13 +629,22 @@ export interface ResumeController extends ResumeConfig {
604
629
  park(p: Omit<PendingAction, "createdAt" | "argsSig">): Promise<void>;
605
630
  /** Drive a parked decision to completion once a verdict is known. Idempotent: a second call
606
631
  * after success answers `already_resolved` (no double-write); an id this SDK never parked
607
- * answers `unknown`; a call that loses the pre-invocation claim answers `lost_race`. */
632
+ * answers `unknown`; a call that loses the pre-invocation claim answers `lost_race`.
633
+ *
634
+ * `verdict: "approved"` is a REQUEST to execute, never the authority (0.7.0): right before the
635
+ * action runs, resolve() confirms with Decidio that THIS parked request was approved (GET
636
+ * /agent/status + the request commitment — `checkApproval`). Decidio still pending → `not_approved`;
637
+ * Decidio says rejected/blocked → recorded as that denial; an approval that cannot be tied to these
638
+ * arguments → `request_unverified`. A transport failure during that check throws (nothing ran). A
639
+ * `rejected`/`blocked` verdict is applied as given — it can only ever prevent execution. */
608
640
  resolve(input: {
609
641
  decisionId: string;
610
642
  verdict: "approved" | "rejected" | "blocked";
611
643
  reason?: string;
612
644
  }): Promise<ResumeOutcome>;
613
- /** node:http listener for Decidio's signed approval webhook (Express/Fastify/node). */
645
+ /** node:http listener for Decidio's signed approval webhook (Express/Fastify/node). The webhook is a
646
+ * WAKE-UP (0.7.0): after its signature is verified — with `webhookSecret` only, never the API token —
647
+ * only its decision id is read; the outcome follows Decidio's own state for that decision. */
614
648
  webhookHandler(): RequestListener;
615
649
  /** Web-Fetch handler (Request → Response) for Next.js route handlers, Hono,
616
650
  * Cloudflare Workers, Bun, Deno. Same fail-closed signature check. */
@@ -666,6 +700,75 @@ export declare function verifyResumeSignature(secret: string, body: string, head
666
700
  * is never evidence). Its own comment claimed it was "shared by every confirm door", and no door
667
701
  * called it: the claim was what kept anyone from noticing the disagreement. */
668
702
  export declare function assertConfirmRecorded(body: any, decisionId: string, signed: boolean): string;
703
+ /** What GET /agent/status answers. `requestCommitment` (0.7.0) is the commitment the decision was
704
+ * authorized with, or null for a decision whose request was never bound. `spent` and `grantExpiresAt` (0.8.0,
705
+ * mini-approvals): the grant is ONE-TIME - `spent` once an execution was recorded on it, `grantExpiresAt` when
706
+ * it lapses (the seal's time plus the gate's TTL) for a granted decision, null otherwise. A gate that predates
707
+ * them answers neither. */
708
+ export interface DecisionStatus {
709
+ state: string;
710
+ decisionId?: string;
711
+ receiptId?: string | null;
712
+ reason?: string | null;
713
+ requestCommitment?: string | null;
714
+ spent?: boolean;
715
+ grantExpiresAt?: string | null;
716
+ }
717
+ type StatusConfig = {
718
+ apiUrl: string;
719
+ apiToken?: string;
720
+ agentKey?: string;
721
+ agentDid?: string;
722
+ fetchImpl?: typeof fetch;
723
+ };
724
+ /** Why an approval check did not pass.
725
+ * pending / unrecognized_state — Decidio records no approval (yet): nothing may run.
726
+ * rejected / blocked — Decidio's final answer is no.
727
+ * spent — the approval was already used: an execution is recorded on it (one approval
728
+ * covers one action, once) - ask again.
729
+ * expired — the approval lapsed (its grant's time passed) before anything ran - ask again.
730
+ * request_changed — the approval is for a DIFFERENT request than the one about to run.
731
+ * commitment_missing — this SDK bound the request, the gate answered without a commitment.
732
+ * unverifiable — Decidio holds a commitment this process cannot recompute (no or
733
+ * unreadable key material), or answered a malformed one. */
734
+ export type ApprovalCheckCode = "pending" | "unrecognized_state" | "rejected" | "blocked" | "spent" | "expired" | "request_changed" | "commitment_missing" | "unverifiable";
735
+ /** The outcome of the one check. Plain JSON on purpose: an engine that runs it as an activity or a
736
+ * step (Temporal, Inngest) memoizes it across its serialization boundary. */
737
+ export interface ApprovalCheck {
738
+ ok: boolean;
739
+ decisionId: string;
740
+ /** Decidio's derived state, verbatim ("" when it answered none). */
741
+ state: string;
742
+ code?: ApprovalCheckCode;
743
+ /** The server's (tokenized) reason for a rejection, or this SDK's explanation of a refusal. */
744
+ reason?: string;
745
+ }
746
+ /** CONFIRM WITH DECIDIO THAT *THIS* REQUEST WAS APPROVED — the check every door runs right before it
747
+ * executes a routed approval: the inline blocking wait, the durable resume (webhook AND worker), and
748
+ * each engine adapter (LangGraph, Temporal, Inngest, OpenAI Agents). A resume signal is a wake-up; it
749
+ * is never the authority.
750
+ *
751
+ * Approved means: GET /agent/status/:decisionId answers approved/auto_approved AND the commitment it
752
+ * returns equals a FRESH recompute over `request` — the exact request about to run. The fail-closed
753
+ * rules:
754
+ * - Decidio holds a commitment → it must match; no key material to recompute it → refused.
755
+ * - Decidio holds none, but this SDK bound the request (`sent`, default: this config has key
756
+ * material, so it would have sent one) → refused: the gate predates request binding.
757
+ * - Decidio holds none and nothing was bound (a keyless agent, or an entry parked before 0.7.0) →
758
+ * the state alone decides — nothing on either side could say more.
759
+ * A transport failure THROWS (it is not an answer); every other outcome is a value.
760
+ *
761
+ * `status` lets a caller that has just read the status (the blocking poll, the worker) pass it in
762
+ * instead of reading it twice. */
763
+ export declare function checkApproval(cfg: StatusConfig, decisionId: string, request: CommittedRequest, opts?: {
764
+ sent?: boolean;
765
+ status?: DecisionStatus;
766
+ }): Promise<ApprovalCheck>;
767
+ /** The webhook handlers' refusal when no webhook secret is configured - with the remedy. */
768
+ /** The one remedy every webhook-secret refusal names (#4B review W3): the dedicated webhook secret - NEVER the
769
+ * server's API_AUTH_TOKEN, the admin bearer, which a refusal that said "the value your deployment signs
770
+ * with" used to steer operators into copying onto agent hosts. */
771
+ export declare const WEBHOOK_SECRET_REMEDY = "Set webhookSecret (DECIDIO_WEBHOOK_SECRET) to the dedicated secret your Decidio deployment signs resume webhooks with - never the server's API_AUTH_TOKEN (its admin bearer, which must not be copied onto an agent host) - or use the poll worker (guard.worker()), which needs no inbound secret.";
669
772
  export declare function createDecidioResume(opts: ResumeConfig & {
670
773
  store: PendingStore;
671
774
  }): ResumeController;