@decidio/sdk 0.1.5 → 0.1.7
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 +57 -11
- package/dist/adapters/_gate.d.ts +0 -2
- package/dist/adapters/_gate.js +0 -15
- package/dist/adapters/langgraph.d.ts +0 -3
- package/dist/adapters/langgraph.js +0 -10
- package/dist/adapters/openai.d.ts +0 -4
- package/dist/adapters/openai.js +0 -20
- package/dist/adapters/temporal.d.ts +0 -1
- package/dist/adapters/temporal.js +1 -13
- package/dist/cli.js +149 -138
- package/dist/guard.d.ts +2 -14
- package/dist/guard.js +6 -19
- package/dist/index.d.ts +0 -41
- package/dist/index.js +3 -81
- package/dist/inngest.d.ts +0 -20
- package/dist/inngest.js +0 -26
- package/dist/resume.d.ts +5 -61
- package/dist/resume.js +107 -73
- package/dist/signing.d.ts +0 -12
- package/dist/signing.js +0 -25
- package/openapi.yaml +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -51,7 +51,8 @@ Every later command reads `.env` from this directory automatically.
|
|
|
51
51
|
|
|
52
52
|
What `.env` contains after a successful init: `DECIDIO_API_URL`, `DECIDIO_AGENT_ID`,
|
|
53
53
|
`DECIDIO_AGENT_DID` + `DECIDIO_AGENT_KEY` (the local keypair — the key is what earns
|
|
54
|
-
`application_confirmed` evidence later), `DECIDIO_API_TOKEN` (the floor-limited agent token
|
|
54
|
+
`application_confirmed` evidence later), `DECIDIO_API_TOKEN` (the floor-limited agent token —
|
|
55
|
+
90 days by default, `--expires-days N` to change),
|
|
55
56
|
`DECIDIO_WORKSPACE_ID`, and `DECIDIO_WEBHOOK_SECRET` (operator deployments only). Pick any
|
|
56
57
|
agent name — if it's already taken in your workspace, `init` stops cleanly and suggests a
|
|
57
58
|
fresh one (or pass `--unique` to auto-append a random suffix — collision-proof for shared
|
|
@@ -122,8 +123,13 @@ implementation as the standalone `@decidio/verify`). The receipt is a W3C Verifi
|
|
|
122
123
|
Credential: signature and content binding verify with no Decidio account and no network — the
|
|
123
124
|
evidence is yours, not ours. To also prove *who* issued it, pin your workspace's issuer:
|
|
124
125
|
`npx @decidio/sdk verify --issuer <did:key:...> receipt.json` (the DID is shown in your app's
|
|
125
|
-
record verify panel
|
|
126
|
-
you
|
|
126
|
+
record verify panel — or simply read it off the `receipt` command's output, which prints your
|
|
127
|
+
workspace's issuer DID from the authenticated response so you never have to hunt for it).
|
|
128
|
+
|
|
129
|
+
One receipt proves itself. Pass several files and their **ordering** is verified too: the first
|
|
130
|
+
file is the anchor and each subsequent receipt must follow it, so verifying any consecutive
|
|
131
|
+
*segment* works — you are told whether the segment starts at your workspace's genesis or
|
|
132
|
+
mid-history, and a set spanning two different issuers is refused outright.
|
|
127
133
|
|
|
128
134
|
What the credential contains, honestly: the sealed **authority decision** — who decided, what
|
|
129
135
|
was authorized, when, under which policy. It seals at decision time and is immutable — which is
|
|
@@ -168,15 +174,36 @@ waiting: it parks its call arguments agent-side (`.decidio-pending/` by default
|
|
|
168
174
|
stores none of your downstream payload) and throws `DecidioSuspendedError`. The process may
|
|
169
175
|
exit. Two ways the parked action resumes when a human decides:
|
|
170
176
|
|
|
171
|
-
**The worker (self-serve — start here).**
|
|
177
|
+
**The worker (self-serve — start here).** No inbound URL, no shared secrets, and it resumes work
|
|
178
|
+
the current process never started. Here is a complete `agent.mjs` you can run:
|
|
172
179
|
|
|
173
180
|
```js
|
|
174
181
|
import { guard } from "@decidio/sdk";
|
|
175
|
-
|
|
182
|
+
|
|
183
|
+
const payInvoiceRaw = async (invoice) => ({ paid: true, id: invoice.id });
|
|
184
|
+
|
|
185
|
+
// Declare the action name so the wrapper registers at LOAD time, not on first call —
|
|
186
|
+
// that is what lets a restarted process resume actions approved while it was down.
|
|
187
|
+
const payInvoice = guard.protect(
|
|
188
|
+
payInvoiceRaw,
|
|
189
|
+
(invoice) => ({ action: "payInvoice", amount: invoice.amount, scope: "Invoice" }),
|
|
190
|
+
{ action: "payInvoice" },
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
guard.worker(); // polls Decidio, re-executes parked actions on approval, one execution each
|
|
194
|
+
|
|
195
|
+
// New work suspends immediately; approvals land later, through the same worker.
|
|
196
|
+
try { await payInvoice({ id: "INV-1", amount: 86_000 }); }
|
|
197
|
+
catch (e) { console.log(e.name); } // DecidioSuspendedError — expected, not a failure
|
|
176
198
|
```
|
|
177
199
|
|
|
178
|
-
|
|
179
|
-
|
|
200
|
+
Run it with `node --env-file=.env agent.mjs`, approve from another terminal, and the running
|
|
201
|
+
worker executes it. Kill the process, restart it, then approve: it still executes, because the
|
|
202
|
+
action was registered at load. **If you can't declare the name at wrap time** (a dynamic
|
|
203
|
+
`describe`), register the raw function explicitly at startup instead —
|
|
204
|
+
`guard.register("payInvoice", payInvoiceRaw)` — before calling `guard.worker()`. Without one of
|
|
205
|
+
those two, a cold worker has an empty registry and reports `no_handler` (the parked action is
|
|
206
|
+
kept, not lost, and the message names the exact fix).
|
|
180
207
|
|
|
181
208
|
**The signed webhook (operator deployments).** Decidio POSTs a signed verdict to your agent's
|
|
182
209
|
`resumeUrl`; the handler verifies the HMAC signature fail-closed, then re-runs your function:
|
|
@@ -192,8 +219,27 @@ also allow-list resume hosts (default-deny). That's operator territory: if you r
|
|
|
192
219
|
Decidio (or we run a pilot with you), the webhook is set up then. Against the hosted sandbox,
|
|
193
220
|
use the worker — a webhook secret your server never learned would (correctly) fail closed.
|
|
194
221
|
|
|
195
|
-
Either way, re-execution is **single-use
|
|
196
|
-
duplicate signal
|
|
222
|
+
Either way, re-execution is **single-use on the happy path**: the parked entry is claimed before
|
|
223
|
+
execution and deleted on success, so a duplicate signal finds nothing to replay. Honest edge — if
|
|
224
|
+
your action succeeds but the process dies *before* the entry is cleared, the claim can be retried
|
|
225
|
+
(robust crash-recovery of stale claims is on the roadmap), so make your downstream action
|
|
226
|
+
idempotent if you need exactly-once under crashes.
|
|
227
|
+
|
|
228
|
+
**Parked arguments must be JSON-serializable.** A suspended action is signed and then written to
|
|
229
|
+
disk as JSON, so values that don't survive `JSON.stringify` round-tripping — `Date`, `Map`,
|
|
230
|
+
`undefined`, `NaN`, class instances with custom `toJSON` — come back changed and fail the
|
|
231
|
+
integrity check on resume (a refusal, never a wrong execution). Pass plain data; rehydrate
|
|
232
|
+
inside your function.
|
|
233
|
+
|
|
234
|
+
**Payload integrity.** When a routed action is parked, the SDK signs its arguments (bound to the
|
|
235
|
+
decision id) with your agent key; on resume it **refuses to execute** if the parked payload was
|
|
236
|
+
modified, its signature stripped, or the file swapped into another decision's slot. That defends
|
|
237
|
+
against tampering by anything lacking your agent's key — note a *fully compromised agent host*
|
|
238
|
+
can bypass the guard entirely (it holds both the key and the function), so the durable guarantee
|
|
239
|
+
that closes that gap is server-sealed argument binding, auditable in the receipt itself, which is
|
|
240
|
+
on the roadmap. **Upgrading from ≤0.1.5:** entries parked by an older version carry no signature
|
|
241
|
+
and are refused once your agent has a key — drain pending approvals before upgrading, or delete
|
|
242
|
+
the parked files and re-run those actions.
|
|
197
243
|
|
|
198
244
|
### Already on a durable engine? Use its native wait
|
|
199
245
|
|
|
@@ -214,9 +260,9 @@ imports none of them. Each adapter's shipped `.d.ts` header documents its wiring
|
|
|
214
260
|
`npx @decidio/sdk <cmd>`:
|
|
215
261
|
|
|
216
262
|
- `quickstart` — the guided two-minute tour (gate → approve → execute → proof)
|
|
217
|
-
- `init [agentId] [--unique]` — sign in, register, mint the agent token, write `.env` (+ `.gitignore` entries)
|
|
263
|
+
- `init [agentId] [--unique] [--force] [--expires-days N]` — sign in, register, mint the agent token, write `.env` (+ `.gitignore` entries). Refuses to overwrite a *different* agent's key in this directory; `--force` backs the old `.env` up first.
|
|
218
264
|
- `doctor` — config, connectivity, and what your current token can do
|
|
219
|
-
- `approvals` / `approvals approve|reject <id> [reason]` — the human side, from your terminal
|
|
265
|
+
- `approvals [--mine] [--json]` / `approvals approve|reject <id> [reason]` — the human side, from your terminal (the list is workspace-wide; `--mine` narrows it to this directory's agent)
|
|
220
266
|
- `receipt <id>` — download a sealed decision's Authority Receipt to a `.json` file
|
|
221
267
|
- `verify [--issuer <did>] <file>...` — verify receipts offline (the canonical verifier; the standalone `@decidio/verify` package is the same implementation, for auditors who want zero Decidio dependency)
|
|
222
268
|
- `dev [--target URL]` — local relay for the resume webhook (operator setups)
|
package/dist/adapters/_gate.d.ts
CHANGED
|
@@ -7,7 +7,5 @@ export interface GateAuth {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function gateAuthorize(config: GuardConfig, ctx: ApprovalContext): Promise<GateAuth>;
|
|
9
9
|
export declare function gateConfirm(config: GuardConfig, decisionId: string, result: unknown): Promise<void>;
|
|
10
|
-
/** Normalize a resume payload ({verdict} | "approved" | ...) to a verdict string. */
|
|
11
10
|
export declare function verdictFrom(payload: any): string;
|
|
12
|
-
/** Throw the right error for a non-approving verdict; return for approved/auto_approved. */
|
|
13
11
|
export declare function assertVerdict(verdict: string, decisionId: string, reason?: string): void;
|
package/dist/adapters/_gate.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
// Shared adapter gate logic (TS) — the thin-adapter rule in code. Every engine adapter
|
|
2
|
-
// reuses these gate calls + verdict handling; only the suspend/resume translation
|
|
3
|
-
// differs. Pure/testable so the offline smoke covers our logic; the engine-primitive
|
|
4
|
-
// call in each adapter is a thin one-liner verified by typecheck against real types.
|
|
5
1
|
import { DecidioBlockedError, DecidioRejectedError, signBoundProof, signConfirm } from "../index.js";
|
|
6
2
|
export async function gateAuthorize(config, ctx) {
|
|
7
3
|
const f = config.fetchImpl ?? fetch;
|
|
8
|
-
// Fall back to the resume controller's URL (matches the core wrapper) so an adapter
|
|
9
|
-
// decision carries a callback URL even when only resume.resumeUrl was set.
|
|
10
4
|
const resumeUrl = config.resumeUrl ?? config.resume?.resumeUrl;
|
|
11
|
-
// Sign the bound identity proof like the core wrapper, so an adapter-path agent can PROVE
|
|
12
|
-
// its identity and auto-approve (not just route). No key → unsigned → server routes.
|
|
13
5
|
const requesterIdentity = (config.agentKey && config.agentDid)
|
|
14
6
|
? signBoundProof(config.agentKey, { agentId: config.agentId, did: config.agentDid, action: ctx.action, amount: ctx.amount, scope: ctx.scope, workspaceId: config.workspaceId })
|
|
15
7
|
: undefined;
|
|
@@ -29,10 +21,7 @@ export async function gateAuthorize(config, ctx) {
|
|
|
29
21
|
export async function gateConfirm(config, decisionId, result) {
|
|
30
22
|
const f = config.fetchImpl ?? fetch;
|
|
31
23
|
try {
|
|
32
|
-
// Sign the confirm too (bound to {action:"confirm", scope:decisionId}) so the adapter path
|
|
33
|
-
// earns application_confirmed; without a key the server caps it at agent_asserted.
|
|
34
24
|
const requesterIdentity = signConfirm(decisionId, config);
|
|
35
|
-
// Same no-silent-downgrade rule as the inline wrap (guard equivalent-path catch).
|
|
36
25
|
if (!requesterIdentity)
|
|
37
26
|
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.`);
|
|
38
27
|
const res = await f(config.apiUrl.replace(/\/$/, "") + "/agent/confirm", {
|
|
@@ -40,8 +29,6 @@ export async function gateConfirm(config, decisionId, result) {
|
|
|
40
29
|
headers: { "content-type": "application/json", ...(config.apiToken ? { authorization: `Bearer ${config.apiToken}` } : {}) },
|
|
41
30
|
body: JSON.stringify({ decisionId, capturedResponse: result ?? null, source: "wrapper", ...(requesterIdentity ? { requesterIdentity } : {}) }),
|
|
42
31
|
});
|
|
43
|
-
// fetch only rejects on network error — surface a non-2xx (auth/500) too. Best-effort:
|
|
44
|
-
// log, don't throw (the action already happened).
|
|
45
32
|
if (!res.ok)
|
|
46
33
|
console.warn(`[decidio] confirm returned ${res.status} (action succeeded)`);
|
|
47
34
|
}
|
|
@@ -49,11 +36,9 @@ export async function gateConfirm(config, decisionId, result) {
|
|
|
49
36
|
console.warn(`[decidio] confirm failed (action succeeded): ${e.message}`);
|
|
50
37
|
}
|
|
51
38
|
}
|
|
52
|
-
/** Normalize a resume payload ({verdict} | "approved" | ...) to a verdict string. */
|
|
53
39
|
export function verdictFrom(payload) {
|
|
54
40
|
return (payload && typeof payload === "object" ? payload.verdict : payload) ?? "";
|
|
55
41
|
}
|
|
56
|
-
/** Throw the right error for a non-approving verdict; return for approved/auto_approved. */
|
|
57
42
|
export function assertVerdict(verdict, decisionId, reason) {
|
|
58
43
|
if (verdict === "approved" || verdict === "auto_approved")
|
|
59
44
|
return;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { Command } from "@langchain/langgraph";
|
|
2
2
|
import { type GuardConfig, type ApprovalContext } from "../index.js";
|
|
3
|
-
/** Gate `run()` behind Decidio inside a LangGraph node. */
|
|
4
3
|
export declare function decidioGate<R>(config: GuardConfig, ctx: ApprovalContext, run: () => Promise<R>): Promise<R>;
|
|
5
|
-
/** Translate Decidio's signed resume webhook into the LangGraph resume Command. The host
|
|
6
|
-
* maps decisionId → thread_id and passes this to graph.invoke/stream. */
|
|
7
4
|
export declare function decidioResumeCommand(body: {
|
|
8
5
|
verdict?: string;
|
|
9
6
|
reason?: string;
|
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
// @decidio/sdk/langgraph — durable suspend via LangGraph's native `interrupt()`.
|
|
2
|
-
// `interrupt` finds the running graph itself (no handle needed), so the gate drops into
|
|
3
|
-
// a node: authorize → on route, interrupt() durably pauses (the checkpointer persists;
|
|
4
|
-
// the process can die) → on resume (Command(resume={verdict})) run the agent's own
|
|
5
|
-
// action + confirm. Decidio is the approval system; the host bridges its webhook to the
|
|
6
|
-
// graph resume (decidioResumeCommand). Requires @langchain/langgraph (optional peer dep).
|
|
7
1
|
import { interrupt, Command } from "@langchain/langgraph";
|
|
8
2
|
import { DecidioBlockedError } from "../index.js";
|
|
9
3
|
import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict } from "./_gate.js";
|
|
10
|
-
/** Gate `run()` behind Decidio inside a LangGraph node. */
|
|
11
4
|
export async function decidioGate(config, ctx, run) {
|
|
12
5
|
const auth = await gateAuthorize(config, ctx);
|
|
13
6
|
if (auth.decision === "block")
|
|
14
7
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
15
8
|
if (auth.decision === "route") {
|
|
16
|
-
// Durable pause. On resume, interrupt() returns the Command(resume=...) payload.
|
|
17
9
|
const resumed = interrupt({ decidio: { decisionId: auth.decisionId, action: ctx.action, amount: ctx.amount, scope: ctx.scope } });
|
|
18
10
|
assertVerdict(verdictFrom(resumed), auth.decisionId, resumed?.reason);
|
|
19
11
|
}
|
|
@@ -21,8 +13,6 @@ export async function decidioGate(config, ctx, run) {
|
|
|
21
13
|
await gateConfirm(config, auth.decisionId, result);
|
|
22
14
|
return result;
|
|
23
15
|
}
|
|
24
|
-
/** Translate Decidio's signed resume webhook into the LangGraph resume Command. The host
|
|
25
|
-
* maps decisionId → thread_id and passes this to graph.invoke/stream. */
|
|
26
16
|
export function decidioResumeCommand(body) {
|
|
27
17
|
return new Command({ resume: { verdict: body.verdict, reason: body.reason } });
|
|
28
18
|
}
|
|
@@ -14,13 +14,9 @@ export interface Pending {
|
|
|
14
14
|
item: ToolApprovalItem;
|
|
15
15
|
decisionId: string;
|
|
16
16
|
}
|
|
17
|
-
/** Ask Decidio for each pending tool-approval on a RunState.
|
|
18
|
-
* proceed → state.approve(item); block → state.reject(item); route → pending (a human
|
|
19
|
-
* decides via Decidio; the host parks state.toString() + resumes via applyResume). */
|
|
20
17
|
export declare function gateInterruptions(config: GuardConfig, state: RunStateLike, describe: (item: ToolApprovalItem) => ApprovalContext): Promise<{
|
|
21
18
|
resolved: Resolved[];
|
|
22
19
|
pending: Pending[];
|
|
23
20
|
}>;
|
|
24
|
-
/** Apply a Decidio verdict to a resumed RunState (after RunState.fromString). */
|
|
25
21
|
export declare function applyResume(state: RunStateLike, item: ToolApprovalItem, verdict: string): void;
|
|
26
22
|
export {};
|
package/dist/adapters/openai.js
CHANGED
|
@@ -1,23 +1,4 @@
|
|
|
1
|
-
// @decidio/sdk/openai — gate OpenAI Agents SDK tool-approval interruptions through
|
|
2
|
-
// Decidio, with durable suspend via the SDK's native RunState serialization.
|
|
3
|
-
//
|
|
4
|
-
// The Agents loop surfaces tool approvals on the RunState (state.getInterruptions());
|
|
5
|
-
// you state.approve(item)/reject(item) then resume with run(agent, state). RunState
|
|
6
|
-
// serializes (state.toString() / RunState.fromString(agent, s)) — that's the durability:
|
|
7
|
-
// park the serialized state agent-side on `route`, resume on Decidio's approval.
|
|
8
|
-
//
|
|
9
|
-
// const state = result.state; // or RunState from the run
|
|
10
|
-
// const { resolved, pending } = await gateInterruptions(config, state, describe);
|
|
11
|
-
// if (pending.length) { await store.put(decisionId, state.toString()); return; } // suspend
|
|
12
|
-
// result = await run(agent, state); // all resolved → resume
|
|
13
|
-
// // on Decidio approval webhook: state = await RunState.fromString(agent, parked);
|
|
14
|
-
// // applyResume(state, item, verdict); result = await run(agent, state);
|
|
15
|
-
//
|
|
16
|
-
// Duck-typed on the state/item (no hard @openai/agents import) so it stays thin.
|
|
17
1
|
import { gateAuthorize } from "./_gate.js";
|
|
18
|
-
/** Ask Decidio for each pending tool-approval on a RunState.
|
|
19
|
-
* proceed → state.approve(item); block → state.reject(item); route → pending (a human
|
|
20
|
-
* decides via Decidio; the host parks state.toString() + resumes via applyResume). */
|
|
21
2
|
export async function gateInterruptions(config, state, describe) {
|
|
22
3
|
const resolved = [];
|
|
23
4
|
const pending = [];
|
|
@@ -36,7 +17,6 @@ export async function gateInterruptions(config, state, describe) {
|
|
|
36
17
|
}
|
|
37
18
|
return { resolved, pending };
|
|
38
19
|
}
|
|
39
|
-
/** Apply a Decidio verdict to a resumed RunState (after RunState.fromString). */
|
|
40
20
|
export function applyResume(state, item, verdict) {
|
|
41
21
|
if (verdict === "approved" || verdict === "auto_approved")
|
|
42
22
|
state.approve(item);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { type GuardConfig, type ApprovalContext } from "../index.js";
|
|
2
2
|
import { type GateAuth } from "./_gate.js";
|
|
3
3
|
export declare const decidioResumeSignal: import("@temporalio/workflow").SignalDefinition<[string, string, (string | undefined)?]>;
|
|
4
|
-
/** Call inside the workflow to wire the resume signal; returns the verdict store. */
|
|
5
4
|
export declare function installDecidioSignal(): Map<string, {
|
|
6
5
|
verdict: string;
|
|
7
6
|
reason?: string;
|
|
@@ -1,19 +1,7 @@
|
|
|
1
|
-
// @decidio/sdk/temporal — durable suspend via Temporal's native `condition()` + a
|
|
2
|
-
// Decidio resume signal. Strongest durability (the workflow survives worker restarts for
|
|
3
|
-
// days); the agent's action runs on resume. Requires @temporalio/workflow (optional peer).
|
|
4
|
-
//
|
|
5
|
-
// Inside your workflow:
|
|
6
|
-
// const verdicts = installDecidioSignal(); // wires the resume signal
|
|
7
|
-
// const out = await decidioGate(config, ctx, () => createOpp(o), verdicts, {
|
|
8
|
-
// authorize: () => executeActivity(authorizeActivity, ...), // HTTP belongs in activities
|
|
9
|
-
// confirm: (id, r) => executeActivity(confirmActivity, ...),
|
|
10
|
-
// });
|
|
11
|
-
// Decidio's webhook bridges to: handle.signal(decidioResumeSignal, decisionId, verdict, reason)
|
|
12
1
|
import { condition, defineSignal, setHandler } from "@temporalio/workflow";
|
|
13
2
|
import { DecidioBlockedError } from "../index.js";
|
|
14
3
|
import { gateAuthorize, gateConfirm, verdictFrom, assertVerdict } from "./_gate.js";
|
|
15
4
|
export const decidioResumeSignal = defineSignal("decidioResume");
|
|
16
|
-
/** Call inside the workflow to wire the resume signal; returns the verdict store. */
|
|
17
5
|
export function installDecidioSignal() {
|
|
18
6
|
const verdicts = new Map();
|
|
19
7
|
setHandler(decidioResumeSignal, (decisionId, verdict, reason) => { verdicts.set(decisionId, { verdict, reason }); });
|
|
@@ -24,7 +12,7 @@ export async function decidioGate(config, ctx, run, verdicts, opts = {}) {
|
|
|
24
12
|
if (auth.decision === "block")
|
|
25
13
|
throw new DecidioBlockedError(auth.reason ?? "blocked by policy", auth.decisionId);
|
|
26
14
|
if (auth.decision === "route") {
|
|
27
|
-
await condition(() => verdicts.has(auth.decisionId));
|
|
15
|
+
await condition(() => verdicts.has(auth.decisionId));
|
|
28
16
|
const v = verdicts.get(auth.decisionId);
|
|
29
17
|
assertVerdict(verdictFrom(v), auth.decisionId, v.reason);
|
|
30
18
|
}
|