@decidio/sdk 0.1.8 → 0.1.10
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 +43 -16
- package/dist/adapters/_gate.d.ts +2 -0
- package/dist/adapters/_gate.js +14 -2
- package/dist/adapters/langgraph.d.ts +13 -0
- package/dist/adapters/openai.d.ts +24 -0
- package/dist/adapters/temporal.d.ts +16 -0
- package/dist/cli.js +149 -168
- package/dist/guard.d.ts +32 -1
- package/dist/guard.js +13 -2
- package/dist/index.d.ts +46 -2
- package/dist/index.js +21 -6
- package/dist/inngest.d.ts +20 -0
- package/dist/resume.d.ts +88 -0
- package/dist/resume.js +62 -10
- package/dist/signing.d.ts +30 -0
- package/dist/signing.js +56 -0
- package/openapi.yaml +1 -1
- package/package.json +3 -3
package/dist/guard.js
CHANGED
|
@@ -9,7 +9,7 @@ export class Guard {
|
|
|
9
9
|
}
|
|
10
10
|
static fromEnv() {
|
|
11
11
|
return new Guard({
|
|
12
|
-
apiUrl: process.env.DECIDIO_API_URL ?? "
|
|
12
|
+
apiUrl: process.env.DECIDIO_API_URL ?? "",
|
|
13
13
|
agentId: process.env.DECIDIO_AGENT_ID ?? "agent",
|
|
14
14
|
apiToken: process.env.DECIDIO_API_TOKEN,
|
|
15
15
|
sourceSystem: process.env.DECIDIO_SOURCE_SYSTEM,
|
|
@@ -29,6 +29,7 @@ export class Guard {
|
|
|
29
29
|
return this.ensureResume().worker(opts);
|
|
30
30
|
}
|
|
31
31
|
ensureResume() {
|
|
32
|
+
this.assertConfigured();
|
|
32
33
|
if (!this.resume) {
|
|
33
34
|
const c = this.config;
|
|
34
35
|
this.resume = createDecidioResume({
|
|
@@ -40,14 +41,24 @@ export class Guard {
|
|
|
40
41
|
}
|
|
41
42
|
return this.resume;
|
|
42
43
|
}
|
|
44
|
+
assertConfigured() {
|
|
45
|
+
if (!this.config.apiUrl)
|
|
46
|
+
throw new Error("[decidio] no gate URL configured — set DECIDIO_API_URL (the hosted sandbox is https://decidio-api.onrender.com) or run `npx @decidio/sdk init`, then start your process with `node --env-file=.env`. Nothing was routed or executed.");
|
|
47
|
+
}
|
|
43
48
|
protect(fn, describe, opts) {
|
|
49
|
+
this.assertConfigured();
|
|
44
50
|
if (opts?.adapter && opts.adapter !== "generic") {
|
|
45
|
-
|
|
51
|
+
const REAL = ["langgraph", "openai", "temporal", "inngest"];
|
|
52
|
+
if (REAL.includes(opts.adapter)) {
|
|
53
|
+
throw new Error(`[decidio] adapter "${opts.adapter}" must be bound via the @decidio/sdk/${opts.adapter} subpath (it needs the engine handle). See that adapter's exports.`);
|
|
54
|
+
}
|
|
55
|
+
throw new Error(`[decidio] unknown adapter "${opts.adapter}" — the available engine adapters are ${REAL.map((a) => `"${a}"`).join(", ")} (each via its @decidio/sdk/<name> subpath), or omit the option for the core gate.`);
|
|
46
56
|
}
|
|
47
57
|
const c = this.config;
|
|
48
58
|
const base = {
|
|
49
59
|
apiUrl: c.apiUrl, agentId: c.agentId, apiToken: c.apiToken,
|
|
50
60
|
sourceSystem: c.sourceSystem, agentKey: c.agentKey, agentDid: c.agentDid, workspaceId: c.workspaceId, appUrl: c.appUrl, fetchImpl: c.fetchImpl,
|
|
61
|
+
pollTimeoutMs: opts?.pollTimeoutMs, pollIntervalMs: opts?.pollIntervalMs,
|
|
51
62
|
};
|
|
52
63
|
if ((opts?.mode ?? "durable") === "blocking") {
|
|
53
64
|
console.warn("[decidio] WARNING: blocking wait — only safe for short, watched approvals. " +
|
package/dist/index.d.ts
CHANGED
|
@@ -1,27 +1,54 @@
|
|
|
1
1
|
import type { ResumeController } from "./resume.js";
|
|
2
|
-
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume } from "./resume.js";
|
|
2
|
+
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
3
3
|
export type { PendingAction, PendingStore, ResumeController, ResumeOutcome } from "./resume.js";
|
|
4
4
|
export { guard, Guard } from "./guard.js";
|
|
5
5
|
export type { GuardFacadeConfig, ProtectOptions } from "./guard.js";
|
|
6
|
-
export { generateKeypair, signBoundProof, signConfirm, canonicalize } from "./signing.js";
|
|
6
|
+
export { generateKeypair, signBoundProof, signConfirm, canonicalize, assertJsonSafe, assertSafeTransport, DecidioUnsafeArgumentsError } from "./signing.js";
|
|
7
7
|
export type { RequesterIdentity } from "./signing.js";
|
|
8
8
|
export interface GuardConfig {
|
|
9
|
+
/** Base URL of the Decidio gate, e.g. "https://api.decidio.io". */
|
|
9
10
|
apiUrl: string;
|
|
11
|
+
/** The agent identity registered with Decidio (onboarding wizard), e.g. "salesforce/SalesforceBot". */
|
|
10
12
|
agentId: string;
|
|
13
|
+
/** Bearer token for the REST floor (a trusted, already-authenticated internal
|
|
14
|
+
* caller). Lives ONLY in the agent host's environment — never in client code. */
|
|
11
15
|
apiToken?: string;
|
|
16
|
+
/** Source system label sealed onto the receipt (e.g. "salesforce"). */
|
|
12
17
|
sourceSystem?: string;
|
|
18
|
+
/** The agent's PRIVATE signing key (PKCS8 base64), generated locally by `init` and held
|
|
19
|
+
* ONLY in the agent host's env as DECIDIO_AGENT_KEY. When set (with `agentDid`), the SDK
|
|
20
|
+
* signs a request-bound identity proof on each authorize so the floor PROVES the requester
|
|
21
|
+
* instead of trusting the label — required for a registered agent to auto-approve. */
|
|
13
22
|
agentKey?: string;
|
|
23
|
+
/** The agent's PUBLIC did:key (the one registered with Decidio). Pairs with `agentKey`. */
|
|
14
24
|
agentDid?: string;
|
|
25
|
+
/** The workspace this agent acts in (DECIDIO_WORKSPACE_ID, from the owner's agent-token mint).
|
|
26
|
+
* Bound into the signed proof (D-B3.6) so a captured proof can't be replayed in another tenant.
|
|
27
|
+
* For a tester's per-workspace agent token this is the tester's workspace id. */
|
|
15
28
|
workspaceId?: string;
|
|
29
|
+
/** Poll cadence + ceiling while a routed action awaits human approval. Used ONLY
|
|
30
|
+
* by the blocking transport (no `resume` configured). */
|
|
16
31
|
pollIntervalMs?: number;
|
|
17
32
|
pollTimeoutMs?: number;
|
|
33
|
+
/** Optional async notifier so a long human wait surfaces in your logs/UI. */
|
|
18
34
|
onPending?: (info: {
|
|
19
35
|
decisionId: string;
|
|
20
36
|
receiptId: string;
|
|
21
37
|
url: string | null;
|
|
22
38
|
}) => void;
|
|
39
|
+
/** Base URL of the Decidio app (DECIDIO_APP_URL). When set, a routed action can say WHERE to
|
|
40
|
+
* approve it — `<appUrl>/d/<decisionId>` opens that decision directly, on web or on the phone
|
|
41
|
+
* build. This link used to be built only inside our own example, so a developer who pasted the
|
|
42
|
+
* env block set DECIDIO_APP_URL and nothing ever read it. */
|
|
23
43
|
appUrl?: string;
|
|
44
|
+
/** Durable async resume. When set, a routed action SUSPENDS (parks its args in the
|
|
45
|
+
* store, throws DecidioSuspendedError) instead of blocking — the process may exit
|
|
46
|
+
* and resume later via the controller's webhook handler or poll worker. This is
|
|
47
|
+
* the production transport for real (minutes-to-days) human approvals. */
|
|
24
48
|
resume?: ResumeController;
|
|
49
|
+
/** Callback URL Decidio POSTs the verdict to on approval (webhook transport).
|
|
50
|
+
* Sent on /agent/authorize so the decision carries it. Falls back to
|
|
51
|
+
* resume.resumeUrl. Omit when using the poll worker. */
|
|
25
52
|
resumeUrl?: string;
|
|
26
53
|
fetchImpl?: typeof fetch;
|
|
27
54
|
}
|
|
@@ -29,6 +56,9 @@ export interface ApprovalContext {
|
|
|
29
56
|
action: string;
|
|
30
57
|
amount: number;
|
|
31
58
|
scope?: string;
|
|
59
|
+
/** Optional human-facing DETAILS of the action (e.g. { Account, "Lead Source", Stage }) shown to
|
|
60
|
+
* the approver as "what you're approving". Display-only — the policy decides on `amount`/`scope`,
|
|
61
|
+
* never these. Residual-safe tokenized server-side before persistence. */
|
|
32
62
|
context?: Record<string, string | number>;
|
|
33
63
|
}
|
|
34
64
|
export declare class DecidioBlockedError extends Error {
|
|
@@ -45,6 +75,13 @@ export declare class DecidioTimeoutError extends Error {
|
|
|
45
75
|
decisionId: string;
|
|
46
76
|
constructor(decisionId: string);
|
|
47
77
|
}
|
|
78
|
+
/** A transport failure with GUIDANCE instead of a raw fetch stack. Carries the endpoint, the
|
|
79
|
+
* operation, the underlying code, and the safety state a caller cares about most: whether
|
|
80
|
+
* anything could have executed. */
|
|
81
|
+
/** The workspace's governed-action allowance is exhausted (HTTP 429, code AGENT_RATE). The
|
|
82
|
+
* server says exactly how much and until when — `limit`, `remaining` and `resetAt` relay its
|
|
83
|
+
* answer verbatim so automation never has to parse a message. Nothing was routed, recorded,
|
|
84
|
+
* or executed; retry after `resetAt`. */
|
|
48
85
|
export declare class DecidioRateLimitError extends Error {
|
|
49
86
|
limit: number | undefined;
|
|
50
87
|
remaining: number | undefined;
|
|
@@ -57,5 +94,12 @@ export declare class DecidioNetworkError extends Error {
|
|
|
57
94
|
code: string | undefined;
|
|
58
95
|
constructor(endpoint: string, op: string, code: string | undefined, cause: unknown);
|
|
59
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Wrap an async action behind the Decidio gate. `describe` extracts the approval
|
|
99
|
+
* context (action, amount, scope) from the call arguments. Returns a function with
|
|
100
|
+
* the SAME signature as `fn` — drop-in.
|
|
101
|
+
*/
|
|
102
|
+
/** `<appUrl>/d/<decisionId>` — the deep link that opens this decision directly, on the web app or
|
|
103
|
+
* the phone build. Null when DECIDIO_APP_URL is not configured, never a half-built URL. */
|
|
60
104
|
export declare function decisionUrl(appUrl: string | undefined, decisionId: string): string | null;
|
|
61
105
|
export declare function withApproval<A extends any[], R>(fn: (...args: A) => Promise<R>, describe: (...args: A) => ApprovalContext, config: GuardConfig): (...args: A) => Promise<R>;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { DecidioSuspendedError } from "./resume.js";
|
|
2
|
-
import { signBoundProof, signConfirm } from "./signing.js";
|
|
3
|
-
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume } from "./resume.js";
|
|
1
|
+
import { DecidioSuspendedError, assertConfirmRecorded } from "./resume.js";
|
|
2
|
+
import { signBoundProof, signConfirm, assertJsonSafe, assertSafeTransport, DecidioUnsafeArgumentsError } from "./signing.js";
|
|
3
|
+
export { DecidioSuspendedError, FilePendingStore, MemoryPendingStore, createDecidioResume, assertConfirmRecorded } from "./resume.js";
|
|
4
4
|
export { guard, Guard } from "./guard.js";
|
|
5
|
-
export { generateKeypair, signBoundProof, signConfirm, canonicalize } from "./signing.js";
|
|
5
|
+
export { generateKeypair, signBoundProof, signConfirm, canonicalize, assertJsonSafe, assertSafeTransport, DecidioUnsafeArgumentsError } from "./signing.js";
|
|
6
6
|
export class DecidioBlockedError extends Error {
|
|
7
7
|
reason;
|
|
8
8
|
decisionId;
|
|
@@ -74,6 +74,7 @@ async function reach(f, url, op, init) {
|
|
|
74
74
|
}
|
|
75
75
|
async function post(cfg, path, body) {
|
|
76
76
|
const f = cfg.fetchImpl ?? fetch;
|
|
77
|
+
assertSafeTransport(cfg.apiUrl, !!cfg.apiToken);
|
|
77
78
|
const res = await reach(f, cfg.apiUrl.replace(/\/$/, "") + path, `POST ${path}`, {
|
|
78
79
|
method: "POST",
|
|
79
80
|
headers: { "content-type": "application/json", ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
|
|
@@ -89,6 +90,7 @@ async function post(cfg, path, body) {
|
|
|
89
90
|
}
|
|
90
91
|
async function get(cfg, path) {
|
|
91
92
|
const f = cfg.fetchImpl ?? fetch;
|
|
93
|
+
assertSafeTransport(cfg.apiUrl, !!cfg.apiToken);
|
|
92
94
|
const res = await reach(f, cfg.apiUrl.replace(/\/$/, "") + path, `GET ${path}`, {
|
|
93
95
|
headers: { ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
|
|
94
96
|
});
|
|
@@ -106,10 +108,22 @@ export function withApproval(fn, describe, config) {
|
|
|
106
108
|
const pollTimeout = config.pollTimeoutMs ?? 600_000;
|
|
107
109
|
const resumeUrl = config.resumeUrl ?? config.resume?.resumeUrl;
|
|
108
110
|
return async (...args) => {
|
|
111
|
+
try {
|
|
112
|
+
assertJsonSafe(args, "args");
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
throw new DecidioUnsafeArgumentsError(`${e?.message ?? e} The decision was NOT routed; nothing reached Decidio.`);
|
|
116
|
+
}
|
|
109
117
|
const ctx = describe(...args);
|
|
110
118
|
const requesterIdentity = (config.agentKey && config.agentDid)
|
|
111
119
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
112
120
|
: undefined;
|
|
121
|
+
try {
|
|
122
|
+
assertJsonSafe(ctx, "describe(...)");
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
throw new DecidioUnsafeArgumentsError(`${e?.message ?? e} The decision was NOT routed; nothing reached Decidio.`);
|
|
126
|
+
}
|
|
113
127
|
const auth = await post(config, "/agent/authorize", {
|
|
114
128
|
requester: config.agentId, action: ctx.action, amount: ctx.amount,
|
|
115
129
|
scope: ctx.scope, sourceSystem: config.sourceSystem,
|
|
@@ -126,7 +140,7 @@ export function withApproval(fn, describe, config) {
|
|
|
126
140
|
if (config.onPending)
|
|
127
141
|
config.onPending({ decisionId: auth.decisionId, receiptId: auth.receiptId, url: approvalUrl });
|
|
128
142
|
else
|
|
129
|
-
console.
|
|
143
|
+
console.error(`[decidio] ${ctx.action} is awaiting approval (decision ${auth.decisionId}) — approve it ${approvalUrl ? `at ${approvalUrl}` : `in your Decidio queue, or: npx @decidio/sdk approvals approve ${auth.decisionId}`}`);
|
|
130
144
|
if (config.resume) {
|
|
131
145
|
config.resume.agentId ??= config.agentId;
|
|
132
146
|
config.resume.agentKey ??= config.agentKey;
|
|
@@ -157,12 +171,13 @@ export function withApproval(fn, describe, config) {
|
|
|
157
171
|
const result = await fn(...args);
|
|
158
172
|
try {
|
|
159
173
|
const confirmIdentity = signConfirm(auth.decisionId, config);
|
|
160
|
-
await post(config, "/agent/confirm", {
|
|
174
|
+
const confirmBody = await post(config, "/agent/confirm", {
|
|
161
175
|
decisionId: auth.decisionId,
|
|
162
176
|
capturedResponse: result ?? null,
|
|
163
177
|
source: "wrapper",
|
|
164
178
|
...(confirmIdentity ? { requesterIdentity: confirmIdentity } : {}),
|
|
165
179
|
});
|
|
180
|
+
assertConfirmRecorded(confirmBody, auth.decisionId, !!confirmIdentity);
|
|
166
181
|
if (!confirmIdentity)
|
|
167
182
|
console.warn(`[decidio] execution evidence for ${auth.decisionId} recorded as agent_asserted — no signing key configured, so the report can't be proven to come from this agent. Set DECIDIO_AGENT_KEY/DID (npx @decidio/sdk init writes them) to earn application_confirmed.`);
|
|
168
183
|
}
|
package/dist/inngest.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { RequestListener } from "node:http";
|
|
2
2
|
import { verifyResumeSignature } from "./resume.js";
|
|
3
3
|
import type { GuardConfig, ApprovalContext } from "./index.js";
|
|
4
|
+
/** The Inngest event Decidio's verdict is translated into. The waiting function
|
|
5
|
+
* correlates on `data.decisionId`. */
|
|
4
6
|
export declare const DECIDIO_RESUME_EVENT = "decidio/decision.resolved";
|
|
7
|
+
/** The minimal slices of the Inngest step + client APIs we use, typed structurally so
|
|
8
|
+
* this file doesn't force an `inngest` type dependency on the core build. */
|
|
5
9
|
export interface InngestStepLike {
|
|
6
10
|
run<T>(id: string, fn: () => Promise<T>): Promise<T>;
|
|
7
11
|
waitForEvent(id: string, opts: {
|
|
@@ -25,6 +29,16 @@ export type GateStepOutcome<R> = {
|
|
|
25
29
|
decisionId: string;
|
|
26
30
|
result?: R;
|
|
27
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* Gate an action INSIDE an Inngest function, with durable suspend on routing.
|
|
34
|
+
* • proceed → run the action (as a step) + confirm.
|
|
35
|
+
* • route → `step.waitForEvent` durably suspends until Decidio's resume event for
|
|
36
|
+
* THIS decision arrives; approved → run + confirm; rejected → status.
|
|
37
|
+
* • block → return blocked (the action never runs).
|
|
38
|
+
* The action runs in YOUR process with YOUR credentials — Decidio only signals.
|
|
39
|
+
* `config.resumeUrl` must point at the bridge endpoint (createInngestResumeBridge),
|
|
40
|
+
* so Decidio's webhook is translated into the event this step waits on.
|
|
41
|
+
*/
|
|
28
42
|
export declare function decidioGateStep<R>(args: {
|
|
29
43
|
step: InngestStepLike;
|
|
30
44
|
config: GuardConfig;
|
|
@@ -32,6 +46,12 @@ export declare function decidioGateStep<R>(args: {
|
|
|
32
46
|
run: () => Promise<R>;
|
|
33
47
|
timeout?: string;
|
|
34
48
|
}): Promise<GateStepOutcome<R>>;
|
|
49
|
+
/**
|
|
50
|
+
* Decidio → Inngest bridge. Mount as an HTTP endpoint and register it as the agent's
|
|
51
|
+
* `resumeUrl`. It verifies Decidio's signed resume webhook (same HMAC as the core
|
|
52
|
+
* handler) and emits `DECIDIO_RESUME_EVENT` into Inngest, waking the suspended
|
|
53
|
+
* `decidioGateStep`. Fails CLOSED without a configured secret (unless allowUnsigned).
|
|
54
|
+
*/
|
|
35
55
|
export declare function createInngestResumeBridge(opts: {
|
|
36
56
|
inngest: InngestClientLike;
|
|
37
57
|
webhookSecret?: string;
|
package/dist/resume.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { RequestListener } from "node:http";
|
|
2
|
+
/** A routed action parked on the agent side, awaiting human approval. The `args`
|
|
3
|
+
* are the original call arguments, replayed verbatim when the action resumes. */
|
|
2
4
|
export interface PendingAction {
|
|
3
5
|
decisionId: string;
|
|
4
6
|
receiptId?: string;
|
|
@@ -7,17 +9,37 @@ export interface PendingAction {
|
|
|
7
9
|
scope?: string;
|
|
8
10
|
args: unknown[];
|
|
9
11
|
createdAt: string;
|
|
12
|
+
/** Integrity binding over {decisionId, action, amount, scope, args} — without it the parked
|
|
13
|
+
* payload would be mutable between routing and execution. ed25519 = detached
|
|
14
|
+
* signature by the agent's key; hmac = keyed by the webhook secret. Verified before ANY
|
|
15
|
+
* re-execution; a mismatch REFUSES and leaves the file for inspection. When the controller
|
|
16
|
+
* holds an agent key, a MISSING signature is also refused (a stripped signature is
|
|
17
|
+
* indistinguishable from tampering) — including entries parked by pre-0.1.6 versions:
|
|
18
|
+
* drain pending approvals before upgrading, or delete + re-run them. Only a keyless
|
|
19
|
+
* controller executes unbound entries (with a loud warning). Honest scope: this defends
|
|
20
|
+
* against modification by anything lacking the agent's key material — a fully compromised
|
|
21
|
+
* agent host can bypass the guard entirely and is out of client-side scope (server-sealed
|
|
22
|
+
* args binding is the receipt-v2 design). */
|
|
10
23
|
argsSig?: {
|
|
11
24
|
kind: "ed25519" | "hmac";
|
|
12
25
|
sig: string;
|
|
13
26
|
};
|
|
14
27
|
}
|
|
28
|
+
/** Where suspended actions live between request and approval. The default is a
|
|
29
|
+
* file store; swap in Redis/Postgres/your queue for production. AGENT-SIDE only —
|
|
30
|
+
* Decidio never sees this. */
|
|
15
31
|
export interface PendingStore {
|
|
16
32
|
put(p: PendingAction): Promise<void>;
|
|
17
33
|
get(decisionId: string): Promise<PendingAction | null>;
|
|
18
34
|
delete(decisionId: string): Promise<void>;
|
|
19
35
|
list(): Promise<PendingAction[]>;
|
|
36
|
+
/** Atomically take ownership of a parked action for execution. Returns true if THIS
|
|
37
|
+
* caller claimed it, false if it's already claimed/gone — the cross-process single-use
|
|
38
|
+
* guarantee (a production store backs this with rename/SETNX/SELECT-FOR-UPDATE). A
|
|
39
|
+
* claimed entry is excluded from list(). Optional: if absent, the controller falls
|
|
40
|
+
* back to an in-process guard (same-process only). */
|
|
20
41
|
claim?(decisionId: string): Promise<boolean>;
|
|
42
|
+
/** Undo a claim so a failed execution is retryable. */
|
|
21
43
|
release?(decisionId: string): Promise<void>;
|
|
22
44
|
}
|
|
23
45
|
export declare class MemoryPendingStore implements PendingStore {
|
|
@@ -30,6 +52,11 @@ export declare class MemoryPendingStore implements PendingStore {
|
|
|
30
52
|
claim(id: string): Promise<boolean>;
|
|
31
53
|
release(id: string): Promise<void>;
|
|
32
54
|
}
|
|
55
|
+
/** File-backed store so a COLD process (the original requester long gone) can still
|
|
56
|
+
* resume — the proof that nothing is "kept alive". One JSON file per decision. A claim
|
|
57
|
+
* renames `<id>.json` → `<id>.claimed` atomically (same-volume rename), so two processes
|
|
58
|
+
* sharing the dir can't both execute. (A stale `.claimed` from a crash mid-execute needs
|
|
59
|
+
* TTL recovery — tracked in the backlog.) */
|
|
33
60
|
export declare class FilePendingStore implements PendingStore {
|
|
34
61
|
private dir;
|
|
35
62
|
constructor(dir: string);
|
|
@@ -43,6 +70,10 @@ export declare class FilePendingStore implements PendingStore {
|
|
|
43
70
|
claim(id: string): Promise<boolean>;
|
|
44
71
|
release(id: string): Promise<void>;
|
|
45
72
|
}
|
|
73
|
+
/** Thrown by a `withApproval`-wrapped call when the action is routed and configured
|
|
74
|
+
* for async resume: the action is now SUSPENDED (parked in the store), not failed.
|
|
75
|
+
* The caller can stop here — the process may even exit; the resume transport will
|
|
76
|
+
* complete it later. */
|
|
46
77
|
export declare class DecidioSuspendedError extends Error {
|
|
47
78
|
decisionId: string;
|
|
48
79
|
action: string;
|
|
@@ -55,31 +86,71 @@ export interface ResumeOutcome {
|
|
|
55
86
|
action?: string;
|
|
56
87
|
result?: unknown;
|
|
57
88
|
reason?: string;
|
|
89
|
+
/** executed only: did the execution-evidence report reach Decidio AND come back with a recorded
|
|
90
|
+
* tier? `status: "executed"` describes the LOCAL run; this describes the record — a lost
|
|
91
|
+
* confirmation must never read as recorded evidence. */
|
|
92
|
+
confirmed?: boolean;
|
|
93
|
+
/** executed only: the evidence tier the server says it recorded (e.g. application_confirmed). */
|
|
94
|
+
evidenceTier?: string;
|
|
58
95
|
}
|
|
96
|
+
/** Minimal config the controller needs to confirm outcomes + poll status. Shared
|
|
97
|
+
* with GuardConfig (apiUrl/apiToken). */
|
|
59
98
|
export interface ResumeConfig {
|
|
60
99
|
apiUrl: string;
|
|
61
100
|
apiToken?: string;
|
|
101
|
+
/** Agent identity for SIGNING the confirm call on resume (so the durable path can earn the
|
|
102
|
+
* application_confirmed tier, not just the inline path). Default from DECIDIO_AGENT_ID/
|
|
103
|
+
* _KEY/_DID env when unset — a cold resume process that has the env signs automatically. */
|
|
62
104
|
agentId?: string;
|
|
63
105
|
agentKey?: string;
|
|
64
106
|
agentDid?: string;
|
|
107
|
+
/** The workspace the agent acts in (DECIDIO_WORKSPACE_ID) — bound into the cold-resume confirm
|
|
108
|
+
* proof (D-B3.6) so it's tenant-scoped too. Default from env when unset. */
|
|
65
109
|
workspaceId?: string;
|
|
110
|
+
/** Shared secret Decidio signs the resume webhook with (HMAC-SHA256). Defaults to
|
|
111
|
+
* the API token if unset. Verified before any re-execution. */
|
|
66
112
|
webhookSecret?: string;
|
|
113
|
+
/** Escape hatch to accept UNSIGNED resume webhooks (no secret). Off by default:
|
|
114
|
+
* with no secret and this unset, the webhook handler rejects every call (fail
|
|
115
|
+
* closed) so a misconfigured agent can't be driven by forged callbacks. Setting it
|
|
116
|
+
* true REMOVES webhook authenticity — anyone who can reach the endpoint can signal
|
|
117
|
+
* work — and the controller says so loudly at startup (an absolute fail-closed
|
|
118
|
+
* guarantee must disclose its opt-out). Only behind a trusted network boundary
|
|
119
|
+
* (e.g. mTLS / private mesh), never on an internet-reachable endpoint. */
|
|
67
120
|
allowUnsigned?: boolean;
|
|
121
|
+
/** The callback URL Decidio should POST the verdict to (webhook transport). Sent
|
|
122
|
+
* on /agent/authorize so the decision carries it; omit when using the poll worker. */
|
|
68
123
|
resumeUrl?: string;
|
|
124
|
+
/** Fired after every terminal resolution (executed / rejected / blocked / no_handler),
|
|
125
|
+
* via either transport. Use it to log, close a listener, or exit a CLI. */
|
|
69
126
|
onResolved?: (o: ResumeOutcome) => void;
|
|
70
127
|
fetchImpl?: typeof fetch;
|
|
71
128
|
}
|
|
129
|
+
/** Re-execute the agent's own fn on approval, confirm the captured outcome to
|
|
130
|
+
* Decidio, and clear the parked entry. Bound to a store + a registry of raw fns. */
|
|
72
131
|
export interface ResumeController extends ResumeConfig {
|
|
73
132
|
store: PendingStore;
|
|
133
|
+
/** Register a RAW fn under its action name so a (possibly cold) resume can replay
|
|
134
|
+
* it. `withApproval` auto-registers on the hot path; a cold worker/service should
|
|
135
|
+
* register at startup so it can resume actions it never originated. */
|
|
74
136
|
register(action: string, fn: (...args: any[]) => Promise<any>): void;
|
|
137
|
+
/** Park a routed action WITH its integrity binding (prefer this over store.put — the
|
|
138
|
+
* binding is what lets resolve() refuse a tampered payload). */
|
|
75
139
|
park(p: Omit<PendingAction, "createdAt" | "argsSig">): Promise<void>;
|
|
140
|
+
/** Drive a parked decision to completion once a verdict is known. Idempotent:
|
|
141
|
+
* a second call after success finds nothing parked → "unknown" (no double-write). */
|
|
76
142
|
resolve(input: {
|
|
77
143
|
decisionId: string;
|
|
78
144
|
verdict: "approved" | "rejected" | "blocked";
|
|
79
145
|
reason?: string;
|
|
80
146
|
}): Promise<ResumeOutcome>;
|
|
147
|
+
/** node:http listener for Decidio's signed approval webhook (Express/Fastify/node). */
|
|
81
148
|
webhookHandler(): RequestListener;
|
|
149
|
+
/** Web-Fetch handler (Request → Response) for Next.js route handlers, Hono,
|
|
150
|
+
* Cloudflare Workers, Bun, Deno. Same fail-closed signature check. */
|
|
82
151
|
fetchHandler(): (req: Request) => Promise<Response>;
|
|
152
|
+
/** Poll-based resume for hosts with no inbound URL (CLI/laptop). A long-lived
|
|
153
|
+
* watcher — this is a durable WORKER, not the request path blocking. */
|
|
83
154
|
worker(opts?: {
|
|
84
155
|
intervalMs?: number;
|
|
85
156
|
timeoutMs?: number;
|
|
@@ -89,7 +160,24 @@ export interface ResumeController extends ResumeConfig {
|
|
|
89
160
|
};
|
|
90
161
|
}
|
|
91
162
|
declare function sign(secret: string, body: string): string;
|
|
163
|
+
/** Verify Decidio's `x-decidio-signature` over the raw webhook body (HMAC-SHA256).
|
|
164
|
+
* Exported so engine adapters (e.g. the Inngest bridge) reuse the exact check. */
|
|
92
165
|
export declare function verifyResumeSignature(secret: string, body: string, header: string | undefined): boolean;
|
|
166
|
+
/** The ONE confirm-body rule, shared by every confirm door (resume, inline withApproval,
|
|
167
|
+
* adapter gate). Transport success is not confirmation: a 2xx whose body carries no recorded
|
|
168
|
+
* tier is a FAILED confirm; a SIGNED confirm coming back below application_confirmed is
|
|
169
|
+
* disclosed, never silent. Returns the recorded tier or throws. */
|
|
170
|
+
export declare function assertConfirmRecorded(body: any, decisionId: string, signed: boolean): string;
|
|
171
|
+
/**
|
|
172
|
+
* Build the resume controller. Spread it into your GuardConfig as `resume`, then
|
|
173
|
+
* either mount `webhookHandler()` (production) or run `worker()` (CLI/laptop).
|
|
174
|
+
*
|
|
175
|
+
* const resume = createDecidioResume({ apiUrl, apiToken, store: new FilePendingStore(".decidio-pending"), resumeUrl });
|
|
176
|
+
* const guard = { apiUrl, agentId, apiToken, resume };
|
|
177
|
+
* const createOpportunity = withApproval(createOpportunityRaw, describe, guard);
|
|
178
|
+
* // production: http.createServer(resume.webhookHandler()).listen(4100)
|
|
179
|
+
* // or CLI: resume.worker()
|
|
180
|
+
*/
|
|
93
181
|
export declare function createDecidioResume(opts: ResumeConfig & {
|
|
94
182
|
store: PendingStore;
|
|
95
183
|
}): ResumeController;
|
package/dist/resume.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual, createPrivateKey, createPublicKey, sign as edSign, verify as edVerify } from "node:crypto";
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, rmSync, renameSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { userInfo } from "node:os";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
6
|
+
import { signConfirm, canonicalize, assertJsonSafe, assertSafeTransport, DecidioUnsafeArgumentsError } from "./signing.js";
|
|
5
7
|
const argsBindingBytes = (p) => Buffer.from(canonicalize({ decisionId: p.decisionId, action: p.action, amount: p.amount, ...(p.scope !== undefined ? { scope: p.scope } : {}), args: p.args }), "utf8");
|
|
6
8
|
function signArgs(p, agentKey, webhookSecret) {
|
|
7
9
|
const bytes = argsBindingBytes(p);
|
|
@@ -53,6 +55,18 @@ export class FilePendingStore {
|
|
|
53
55
|
constructor(dir) {
|
|
54
56
|
this.dir = dir;
|
|
55
57
|
mkdirSync(dir, { recursive: true });
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
try {
|
|
60
|
+
const user = (() => { try {
|
|
61
|
+
return userInfo().username;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return process.env.USERNAME ?? process.env.USER ?? "";
|
|
65
|
+
} })();
|
|
66
|
+
execFileSync("icacls", [dir, "/inheritance:r", "/grant:r", `${user}:(OI)(CI)F`], { stdio: "ignore" });
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
}
|
|
56
70
|
}
|
|
57
71
|
key(id) { return id.replace(/[^\w.-]/g, "_"); }
|
|
58
72
|
path(id) { return join(this.dir, `${this.key(id)}.json`); }
|
|
@@ -118,20 +132,34 @@ export function verifyResumeSignature(secret, body, header) {
|
|
|
118
132
|
const a = Buffer.from(expected), b = Buffer.from(header);
|
|
119
133
|
return a.length === b.length && timingSafeEqual(a, b);
|
|
120
134
|
}
|
|
135
|
+
export function assertConfirmRecorded(body, decisionId, signed) {
|
|
136
|
+
if (!body || body.ok !== true || typeof body.confirmation !== "string") {
|
|
137
|
+
throw new Error(`confirm answered 2xx but reported no evidence tier — treating the confirmation as NOT recorded`);
|
|
138
|
+
}
|
|
139
|
+
if (signed && body.confirmation !== "application_confirmed") {
|
|
140
|
+
console.warn(`[decidio] execution evidence for ${decisionId} was recorded as "${body.confirmation}", not application_confirmed, despite a signed report — the server did not accept the identity proof (key/DID/registration mismatch?). Run \`npx @decidio/sdk doctor\` and re-init if this persists.`);
|
|
141
|
+
}
|
|
142
|
+
return body.confirmation;
|
|
143
|
+
}
|
|
121
144
|
async function confirm(cfg, decisionId, capturedResponse) {
|
|
122
145
|
const f = cfg.fetchImpl ?? fetch;
|
|
146
|
+
assertSafeTransport(cfg.apiUrl, !!cfg.apiToken);
|
|
123
147
|
const requesterIdentity = signConfirm(decisionId, cfg);
|
|
124
148
|
if (!requesterIdentity)
|
|
125
149
|
console.warn(`[decidio] execution evidence for ${decisionId} recorded as agent_asserted — no signing key configured, so the report can't be proven to come from this agent. Set DECIDIO_AGENT_KEY/DID to earn application_confirmed.`);
|
|
126
|
-
await f(cfg.apiUrl.replace(/\/$/, "") + "/agent/confirm", {
|
|
150
|
+
const r = await f(cfg.apiUrl.replace(/\/$/, "") + "/agent/confirm", {
|
|
127
151
|
method: "POST",
|
|
128
152
|
headers: { "content-type": "application/json", ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
|
|
129
153
|
body: JSON.stringify({ decisionId, capturedResponse: capturedResponse ?? null, source: "wrapper", ...(requesterIdentity ? { requesterIdentity } : {}) }),
|
|
130
|
-
})
|
|
131
|
-
|
|
154
|
+
});
|
|
155
|
+
if (!r.ok)
|
|
156
|
+
throw new Error(`confirm ${r.status}`);
|
|
157
|
+
const body = await r.json().catch(() => null);
|
|
158
|
+
return assertConfirmRecorded(body, decisionId, !!requesterIdentity);
|
|
132
159
|
}
|
|
133
160
|
async function statusOf(cfg, decisionId) {
|
|
134
161
|
const f = cfg.fetchImpl ?? fetch;
|
|
162
|
+
assertSafeTransport(cfg.apiUrl, !!cfg.apiToken);
|
|
135
163
|
const r = await f(cfg.apiUrl.replace(/\/$/, "") + "/agent/status/" + encodeURIComponent(decisionId), {
|
|
136
164
|
headers: { ...(cfg.apiToken ? { authorization: `Bearer ${cfg.apiToken}` } : {}) },
|
|
137
165
|
});
|
|
@@ -143,6 +171,15 @@ export function createDecidioResume(opts) {
|
|
|
143
171
|
const registry = new Map();
|
|
144
172
|
const inFlight = new Set();
|
|
145
173
|
const secret = opts.webhookSecret ?? opts.apiToken ?? "";
|
|
174
|
+
if (opts.allowUnsigned)
|
|
175
|
+
console.warn("[decidio] WARNING: allowUnsigned is ON — this controller will accept UNSIGNED resume webhooks. Anyone who can reach the endpoint can signal work (parked-payload integrity still gates execution). Use this only behind a trusted network boundary, never on an internet-reachable endpoint.");
|
|
176
|
+
const refusalWarned = new Set();
|
|
177
|
+
const refuseOnce = (decisionId, msg) => {
|
|
178
|
+
if (refusalWarned.has(decisionId))
|
|
179
|
+
return;
|
|
180
|
+
refusalWarned.add(decisionId);
|
|
181
|
+
console.error(msg + " (This prints once — the entry stays parked until you act.)");
|
|
182
|
+
};
|
|
146
183
|
const ctrl = {
|
|
147
184
|
...opts,
|
|
148
185
|
agentId: opts.agentId ?? process.env.DECIDIO_AGENT_ID,
|
|
@@ -158,6 +195,12 @@ export function createDecidioResume(opts) {
|
|
|
158
195
|
registry.set(action, fn);
|
|
159
196
|
},
|
|
160
197
|
async park(p) {
|
|
198
|
+
try {
|
|
199
|
+
assertJsonSafe(p.args, "args");
|
|
200
|
+
}
|
|
201
|
+
catch (e) {
|
|
202
|
+
throw new DecidioUnsafeArgumentsError(`${e?.message ?? e} NOT parked and NOT executable — but decision ${p.decisionId} may already be routed; reject it in your queue: npx @decidio/sdk approvals reject ${p.decisionId}`, p.decisionId);
|
|
203
|
+
}
|
|
161
204
|
const argsSig = signArgs(p, ctrl.agentKey, ctrl.webhookSecret);
|
|
162
205
|
if (!argsSig)
|
|
163
206
|
console.warn(`[decidio] parking ${p.decisionId} UNBOUND — no agent key or webhook secret to sign the payload with, so a modified pending file could not be detected. Run \`npx @decidio/sdk init\` to configure keys.`);
|
|
@@ -180,21 +223,21 @@ export function createDecidioResume(opts) {
|
|
|
180
223
|
if (verdict !== "approved" && verdict !== "auto_approved")
|
|
181
224
|
return { status: "unknown", decisionId };
|
|
182
225
|
if (pending.decisionId !== decisionId) {
|
|
183
|
-
|
|
226
|
+
refuseOnce(decisionId, `[decidio] REFUSING to execute ${decisionId}: the parked entry in this slot belongs to a DIFFERENT decision (${pending.decisionId}) — the pending file was moved or swapped. Delete it and re-run the action.`);
|
|
184
227
|
return finish({ status: "no_handler", decisionId, action: pending.action, reason: "parked payload decision-id mismatch" });
|
|
185
228
|
}
|
|
186
229
|
const strictBinding = !!ctrl.agentKey;
|
|
187
230
|
const integrity = verifyArgs(pending, ctrl.agentKey, ctrl.webhookSecret);
|
|
188
231
|
if (integrity === "mismatch") {
|
|
189
|
-
|
|
232
|
+
refuseOnce(decisionId, `[decidio] REFUSING to execute ${decisionId}: the parked payload FAILED its integrity check — the arguments on disk are not the arguments that were routed for approval. The pending file is untouched for inspection; delete it and re-run the action to request a fresh approval.`);
|
|
190
233
|
return finish({ status: "no_handler", decisionId, action: pending.action, reason: "parked payload integrity check failed" });
|
|
191
234
|
}
|
|
192
235
|
if (integrity === "unverifiable") {
|
|
193
|
-
|
|
236
|
+
refuseOnce(decisionId, `[decidio] REFUSING to execute ${decisionId}: the parked payload is signed but this process lacks the key material to verify it (DECIDIO_AGENT_KEY / DECIDIO_WEBHOOK_SECRET). Restore the agent's env and retry.`);
|
|
194
237
|
return finish({ status: "no_handler", decisionId, action: pending.action, reason: "parked payload unverifiable" });
|
|
195
238
|
}
|
|
196
239
|
if (integrity === "unbound" && strictBinding) {
|
|
197
|
-
|
|
240
|
+
refuseOnce(decisionId, `[decidio] REFUSING to execute ${decisionId}: the parked payload has NO integrity signature, but this agent has keys and would have signed it — a missing signature means the file was tampered with. Delete it and re-run the action.`);
|
|
198
241
|
return finish({ status: "no_handler", decisionId, action: pending.action, reason: "parked payload unsigned (expected a signature)" });
|
|
199
242
|
}
|
|
200
243
|
if (integrity === "unbound")
|
|
@@ -215,8 +258,9 @@ export function createDecidioResume(opts) {
|
|
|
215
258
|
try {
|
|
216
259
|
const result = await fn(...pending.args);
|
|
217
260
|
let confirmed = true;
|
|
261
|
+
let tier;
|
|
218
262
|
try {
|
|
219
|
-
await confirm(ctrl, decisionId, result);
|
|
263
|
+
tier = await confirm(ctrl, decisionId, result);
|
|
220
264
|
}
|
|
221
265
|
catch (e) {
|
|
222
266
|
confirmed = false;
|
|
@@ -231,7 +275,7 @@ export function createDecidioResume(opts) {
|
|
|
231
275
|
else
|
|
232
276
|
console.warn(` This store provides no claim() primitive, so the entry was DELETED to guarantee the action cannot execute twice. ${decisionId} executed, but its confirmation never reached Decidio — re-confirm it from your own records.`);
|
|
233
277
|
}
|
|
234
|
-
return finish({ status: "executed", decisionId, action: pending.action, result });
|
|
278
|
+
return finish({ status: "executed", decisionId, action: pending.action, result, confirmed, ...(tier ? { evidenceTier: tier } : {}) });
|
|
235
279
|
}
|
|
236
280
|
catch (e) {
|
|
237
281
|
if (ctrl.store.release)
|
|
@@ -320,6 +364,14 @@ export function createDecidioResume(opts) {
|
|
|
320
364
|
continue;
|
|
321
365
|
try {
|
|
322
366
|
const outcome = await ctrl.resolve({ decisionId: p.decisionId, verdict, reason: st.reason });
|
|
367
|
+
if (outcome.status === "executed" && outcome.confirmed)
|
|
368
|
+
console.error(`[decidio] resumed ${p.decisionId}: approved → "${p.action}" executed, evidence recorded as ${outcome.evidenceTier ?? "confirmed"}.`);
|
|
369
|
+
else if (outcome.status === "executed")
|
|
370
|
+
console.error(`[decidio] resumed ${p.decisionId}: approved → "${p.action}" executed, but its execution evidence did NOT reach Decidio — see the warning above.`);
|
|
371
|
+
else if (outcome.status === "rejected")
|
|
372
|
+
console.error(`[decidio] resumed ${p.decisionId}: REJECTED — the parked "${p.action}" was discarded, nothing executed.`);
|
|
373
|
+
else if (outcome.status === "blocked")
|
|
374
|
+
console.error(`[decidio] resumed ${p.decisionId}: BLOCKED by policy — the parked "${p.action}" was discarded, nothing executed.`);
|
|
323
375
|
workerOpts?.onResolved?.(outcome);
|
|
324
376
|
}
|
|
325
377
|
catch (e) {
|
package/dist/signing.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
/** Sorted-key JCS — must match apps/api/src/canonical.ts byte-for-byte. */
|
|
1
2
|
export declare function canonicalize(v: unknown): string;
|
|
3
|
+
/** Generate a fresh Ed25519 keypair LOCALLY. Returns the public `did:key` (register this)
|
|
4
|
+
* and the PKCS8 private key base64 (keep this in the agent host's env as DECIDIO_AGENT_KEY —
|
|
5
|
+
* it never goes back to Decidio). */
|
|
2
6
|
export declare function generateKeypair(): {
|
|
3
7
|
did: string;
|
|
4
8
|
privateKeyBase64: string;
|
|
@@ -17,12 +21,20 @@ export interface RequesterIdentity {
|
|
|
17
21
|
signature: string;
|
|
18
22
|
};
|
|
19
23
|
}
|
|
24
|
+
/** Sign the CONFIRM call (bound to {action:"confirm", scope:decisionId}) so the gate can
|
|
25
|
+
* prove the caller is the receipt's original requester before granting the wrapper tier
|
|
26
|
+
* (application_confirmed). Returns undefined when no signing key is configured (the server
|
|
27
|
+
* then caps the report at agent_asserted). Used by EVERY confirm path — inline, durable
|
|
28
|
+
* resume, and the engine adapters — so the production paths confirm with a proof too. Also
|
|
29
|
+
* binds `workspaceId` (D-B3.6) when configured, so the confirm proof is tenant-scoped too. */
|
|
20
30
|
export declare function signConfirm(decisionId: string, cfg: {
|
|
21
31
|
agentId?: string;
|
|
22
32
|
agentKey?: string;
|
|
23
33
|
agentDid?: string;
|
|
24
34
|
workspaceId?: string;
|
|
25
35
|
}): RequesterIdentity | undefined;
|
|
36
|
+
/** Sign a request-bound identity proof. The returned object is sent verbatim as the
|
|
37
|
+
* `requesterIdentity` field of /agent/authorize. */
|
|
26
38
|
export declare function signBoundProof(privateKeyBase64: string, fields: {
|
|
27
39
|
agentId: string;
|
|
28
40
|
did: string;
|
|
@@ -34,3 +46,21 @@ export declare function signBoundProof(privateKeyBase64: string, fields: {
|
|
|
34
46
|
now?: number;
|
|
35
47
|
nonce?: string;
|
|
36
48
|
}): RequesterIdentity;
|
|
49
|
+
/** Refuse values that do not survive `JSON.stringify → JSON.parse` with their meaning intact,
|
|
50
|
+
* BEFORE anything is routed, signed, or parked. `NaN` canonicalizes to `null` and a `Map` to
|
|
51
|
+
* `{}` — an integrity signature over those bytes would verify while the function executed
|
|
52
|
+
* arguments that meant something else, so the gate refuses them instead. Throws naming the
|
|
53
|
+
* offending path. The supported data model is plain JSON: null, boolean, string, finite
|
|
54
|
+
* number, plain array, plain object. */
|
|
55
|
+
/** Typed refusal for values that cannot survive JSON with their meaning intact. `decisionId`
|
|
56
|
+
* is set only when a decision already exists (defense-in-depth refusals after routing). */
|
|
57
|
+
export declare class DecidioUnsafeArgumentsError extends Error {
|
|
58
|
+
decisionId?: string | undefined;
|
|
59
|
+
constructor(message: string, decisionId?: string | undefined);
|
|
60
|
+
}
|
|
61
|
+
export declare function assertJsonSafe(value: unknown, path?: string): void;
|
|
62
|
+
/** The wire rule: a credentialed request must never ride plaintext http to a remote host — the
|
|
63
|
+
* rule is about the WIRE, not which credential (the interactive sign-in enforced this for
|
|
64
|
+
* session tokens while agent-token requests sailed through). localhost/127.0.0.1/[::1] stay
|
|
65
|
+
* allowed for local dev; https always allowed. Throws before anything is sent. */
|
|
66
|
+
export declare function assertSafeTransport(url: string, hasCredential: boolean): void;
|