@countersign/api-contract 0.1.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/LICENSE +20 -0
- package/dist/index.cjs +30 -0
- package/dist/index.d.ts +426 -0
- package/dist/index.js +5 -0
- package/openapi.yaml +272 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2026 Simon Crean. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its source code (the "Software") are proprietary and
|
|
4
|
+
confidential. No license, right, or permission is granted to use, copy, modify,
|
|
5
|
+
merge, publish, distribute, sublicense, or sell copies of the Software, in whole
|
|
6
|
+
or in part, except with the prior written permission of the copyright holder.
|
|
7
|
+
|
|
8
|
+
Unauthorized copying, distribution, or use of the Software is strictly prohibited.
|
|
9
|
+
|
|
10
|
+
NOTE (intent): Countersign follows an open-core direction. Specific "front door"
|
|
11
|
+
components — the EnforcementProvider interface, the API contract, the SDK, and
|
|
12
|
+
the MCP server (packages: core interface, api-contract, sdk, mcp) — are expected
|
|
13
|
+
to be re-licensed under a permissive open-source license (e.g. MIT/Apache-2.0) to
|
|
14
|
+
drive adoption, while the control-plane "brain" (policy compiler, ledger, anomaly
|
|
15
|
+
engine, hosted Core) remains proprietary. Until that re-licensing is explicitly
|
|
16
|
+
published, all rights are reserved.
|
|
17
|
+
|
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
IMPLIED. IT IS A TESTNET-ONLY PROOF AND HAS NOT BEEN SECURITY-AUDITED. DO NOT USE
|
|
20
|
+
WITH MAINNET FUNDS OR IN PRODUCTION CUSTODY OF REAL ASSETS.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
WS_PATH: () => WS_PATH
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
var WS_PATH = "/events";
|
|
27
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
28
|
+
0 && (module.exports = {
|
|
29
|
+
WS_PATH
|
|
30
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Branded identifier types. Brands prevent accidentally passing an AgentId where a
|
|
5
|
+
* ProviderId is expected — a real class of bug once three backends are in play.
|
|
6
|
+
*
|
|
7
|
+
* These are zero-cost at runtime (just strings); the brand exists only in the type system.
|
|
8
|
+
*/
|
|
9
|
+
type ProviderId = string & {
|
|
10
|
+
readonly __brand: "ProviderId";
|
|
11
|
+
};
|
|
12
|
+
type AgentId = string & {
|
|
13
|
+
readonly __brand: "AgentId";
|
|
14
|
+
};
|
|
15
|
+
type SessionId = string & {
|
|
16
|
+
readonly __brand: "SessionId";
|
|
17
|
+
};
|
|
18
|
+
/** chain / network / venue, e.g. "base-sepolia" */
|
|
19
|
+
type Venue = string;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The ONE declarative policy an operator writes. The compiler lowers it to each backend's
|
|
23
|
+
* native controls; the evaluator (./evaluate.ts) is its executable semantics. Keep this small
|
|
24
|
+
* and backend-neutral — every field must mean the same thing on every rail.
|
|
25
|
+
*/
|
|
26
|
+
declare const UnifiedPolicySchema: z.ZodObject<{
|
|
27
|
+
schemaVersion: z.ZodLiteral<1>;
|
|
28
|
+
/** Asset the caps apply to, e.g. "USDC". */
|
|
29
|
+
asset: z.ZodString;
|
|
30
|
+
/** Max value of a single spend (base units). Absent = no per-tx cap. */
|
|
31
|
+
perTxCap: z.ZodOptional<z.ZodString>;
|
|
32
|
+
/** Max cumulative spend per rolling day (base units). Absent = no daily cap. */
|
|
33
|
+
dailyCap: z.ZodOptional<z.ZodString>;
|
|
34
|
+
/**
|
|
35
|
+
* Counterparty allowlist. ABSENT = any counterparty allowed (subject to other rules).
|
|
36
|
+
* PRESENT-but-EMPTY ([]) = deny everything (an explicit "allow nobody" sentinel).
|
|
37
|
+
*/
|
|
38
|
+
allowlist: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
39
|
+
/** Counterparty denylist — always wins over the allowlist. */
|
|
40
|
+
denylist: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
41
|
+
/** Spends STRICTLY ABOVE this require human approval before signing (base units). */
|
|
42
|
+
approvalThreshold: z.ZodOptional<z.ZodString>;
|
|
43
|
+
/** Hard kill — deny everything regardless of the rest. */
|
|
44
|
+
frozen: z.ZodOptional<z.ZodBoolean>;
|
|
45
|
+
/** Allowed venues/chains by name (see ./venues.ts). Absent = any venue. */
|
|
46
|
+
venues: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
47
|
+
}, "strict", z.ZodTypeAny, {
|
|
48
|
+
schemaVersion: 1;
|
|
49
|
+
asset: string;
|
|
50
|
+
perTxCap?: string | undefined;
|
|
51
|
+
dailyCap?: string | undefined;
|
|
52
|
+
allowlist?: string[] | undefined;
|
|
53
|
+
denylist?: string[] | undefined;
|
|
54
|
+
approvalThreshold?: string | undefined;
|
|
55
|
+
frozen?: boolean | undefined;
|
|
56
|
+
venues?: string[] | undefined;
|
|
57
|
+
}, {
|
|
58
|
+
schemaVersion: 1;
|
|
59
|
+
asset: string;
|
|
60
|
+
perTxCap?: string | undefined;
|
|
61
|
+
dailyCap?: string | undefined;
|
|
62
|
+
allowlist?: string[] | undefined;
|
|
63
|
+
denylist?: string[] | undefined;
|
|
64
|
+
approvalThreshold?: string | undefined;
|
|
65
|
+
frozen?: boolean | undefined;
|
|
66
|
+
venues?: string[] | undefined;
|
|
67
|
+
}>;
|
|
68
|
+
type UnifiedPolicy = z.infer<typeof UnifiedPolicySchema>;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Countersign Core — EnforcementProvider
|
|
72
|
+
* Location in repo: packages/core/src/enforcement-provider.ts
|
|
73
|
+
*
|
|
74
|
+
* THE KEYSTONE ABSTRACTION.
|
|
75
|
+
*
|
|
76
|
+
* Every wallet backend (Coinbase Agentic Wallets, Turnkey, Openfort, ...) is wrapped
|
|
77
|
+
* in an adapter that implements this interface. The freeze controller, policy compiler,
|
|
78
|
+
* ledger, and API depend ONLY on this interface — never on a vendor SDK directly.
|
|
79
|
+
*
|
|
80
|
+
* Enforcement is NATIVE to each backend (MPC/TEE policy, session caps, or on-chain rules).
|
|
81
|
+
* Countersign's job across all of them is the same four verbs:
|
|
82
|
+
* 1. APPLY a unified policy (each adapter compiles it to the backend's native controls)
|
|
83
|
+
* 2. FREEZE (revoke/zero/flip — a hard stop)
|
|
84
|
+
* 3. (optionally) APPROVE inline, for backends that can gate a signature on approval
|
|
85
|
+
* 4. OBSERVE (stream events into the tamper-evident ledger)
|
|
86
|
+
*
|
|
87
|
+
* FAIL-CLOSED CONTRACT (non-negotiable):
|
|
88
|
+
* - `freeze()` MUST be idempotent and MUST hard-stop. If an adapter cannot CONFIRM the
|
|
89
|
+
* stop, it returns `{ confirmed: false }` (or throws). The controller then treats the
|
|
90
|
+
* agent as STILL DANGEROUS and escalates (alert + retry +, if available, on-chain guard).
|
|
91
|
+
* - `applyPolicy()` that cannot confirm the new policy is live MUST throw — the caller
|
|
92
|
+
* responds by freezing, never by assuming the looser/old policy is fine.
|
|
93
|
+
* - No decision / no backend response => the action does NOT execute. Default deny.
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
type EnforcementMode = "native-session-caps" | "pre-sign-policy" | "onchain-policy";
|
|
97
|
+
interface ActionRequest {
|
|
98
|
+
id: string;
|
|
99
|
+
agentId: AgentId;
|
|
100
|
+
kind: "transfer" | "contract-call" | "x402-payment";
|
|
101
|
+
asset: string;
|
|
102
|
+
amount: string;
|
|
103
|
+
counterparty?: string;
|
|
104
|
+
venue: Venue;
|
|
105
|
+
raw?: unknown;
|
|
106
|
+
ts: number;
|
|
107
|
+
}
|
|
108
|
+
interface FreezeResult {
|
|
109
|
+
confirmed: boolean;
|
|
110
|
+
frozenAgents: AgentId[];
|
|
111
|
+
mechanism: "session-revoked" | "caps-zeroed" | "onchain-guard" | "policy-deny";
|
|
112
|
+
at: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The canonical Countersign event vocabulary. EVERY one of these is appended, in order, to the
|
|
117
|
+
* hash-chained ledger — the audit artifact and source of truth (prime directive #5).
|
|
118
|
+
*
|
|
119
|
+
* Two origins:
|
|
120
|
+
* - provider-origin events (mirror the adapter's ProviderEvent): what each backend did.
|
|
121
|
+
* - controller-origin events: the cross-vendor freeze orchestration (the moat made auditable).
|
|
122
|
+
*
|
|
123
|
+
* The ledger package stores these generically (payload-agnostic, hash-chained). Core owns the
|
|
124
|
+
* vocabulary; the ledger owns durability. That split keeps core free of any storage dependency.
|
|
125
|
+
*/
|
|
126
|
+
|
|
127
|
+
type FreezeMechanism = FreezeResult["mechanism"];
|
|
128
|
+
/** Per-provider outcome of a freeze attempt. unconfirmed AND failed are BOTH "still dangerous". */
|
|
129
|
+
type FreezeOutcome = "confirmed" | "unconfirmed" | "failed";
|
|
130
|
+
type LedgerEvent = {
|
|
131
|
+
kind: "action_requested";
|
|
132
|
+
providerId: ProviderId;
|
|
133
|
+
agentId: AgentId;
|
|
134
|
+
action: ActionRequest;
|
|
135
|
+
ts: number;
|
|
136
|
+
} | {
|
|
137
|
+
kind: "action_allowed";
|
|
138
|
+
providerId: ProviderId;
|
|
139
|
+
agentId: AgentId;
|
|
140
|
+
action: ActionRequest;
|
|
141
|
+
policyId: string;
|
|
142
|
+
ts: number;
|
|
143
|
+
} | {
|
|
144
|
+
kind: "action_blocked";
|
|
145
|
+
providerId: ProviderId;
|
|
146
|
+
agentId: AgentId;
|
|
147
|
+
action: ActionRequest;
|
|
148
|
+
policyId: string;
|
|
149
|
+
reason: string;
|
|
150
|
+
ts: number;
|
|
151
|
+
} | {
|
|
152
|
+
kind: "policy_applied";
|
|
153
|
+
providerId: ProviderId;
|
|
154
|
+
agentId: AgentId;
|
|
155
|
+
policyId: string;
|
|
156
|
+
ts: number;
|
|
157
|
+
} | {
|
|
158
|
+
kind: "session_revoked";
|
|
159
|
+
providerId: ProviderId;
|
|
160
|
+
agentId: AgentId;
|
|
161
|
+
sessionId?: SessionId | undefined;
|
|
162
|
+
ts: number;
|
|
163
|
+
} | {
|
|
164
|
+
kind: "needs_approval";
|
|
165
|
+
providerId: ProviderId;
|
|
166
|
+
agentId: AgentId;
|
|
167
|
+
action: ActionRequest;
|
|
168
|
+
approvalToken: string;
|
|
169
|
+
reason: string;
|
|
170
|
+
ts: number;
|
|
171
|
+
} | {
|
|
172
|
+
kind: "approval_resolved";
|
|
173
|
+
providerId: ProviderId;
|
|
174
|
+
agentId: AgentId;
|
|
175
|
+
approvalToken: string;
|
|
176
|
+
decision: "approved" | "denied";
|
|
177
|
+
ts: number;
|
|
178
|
+
} | {
|
|
179
|
+
kind: "freeze_requested";
|
|
180
|
+
freezeId: string;
|
|
181
|
+
targets: ProviderId[];
|
|
182
|
+
reason: string;
|
|
183
|
+
ts: number;
|
|
184
|
+
} | {
|
|
185
|
+
kind: "freeze_result";
|
|
186
|
+
freezeId: string;
|
|
187
|
+
providerId: ProviderId;
|
|
188
|
+
mode: EnforcementMode;
|
|
189
|
+
outcome: FreezeOutcome;
|
|
190
|
+
mechanism?: FreezeMechanism | undefined;
|
|
191
|
+
latencyMs: number;
|
|
192
|
+
detail?: string | undefined;
|
|
193
|
+
ts: number;
|
|
194
|
+
} | {
|
|
195
|
+
kind: "freeze_partial";
|
|
196
|
+
freezeId: string;
|
|
197
|
+
confirmed: ProviderId[];
|
|
198
|
+
dangerous: ProviderId[];
|
|
199
|
+
ts: number;
|
|
200
|
+
} | {
|
|
201
|
+
kind: "escalation_revoke_session";
|
|
202
|
+
freezeId: string;
|
|
203
|
+
providerId: ProviderId;
|
|
204
|
+
agentId: AgentId;
|
|
205
|
+
outcome: "confirmed" | "failed";
|
|
206
|
+
latencyMs: number;
|
|
207
|
+
ts: number;
|
|
208
|
+
} | {
|
|
209
|
+
kind: "freeze_resolved";
|
|
210
|
+
freezeId: string;
|
|
211
|
+
providerCount: number;
|
|
212
|
+
windowMs: number;
|
|
213
|
+
ts: number;
|
|
214
|
+
} | {
|
|
215
|
+
kind: "still_dangerous";
|
|
216
|
+
freezeId: string;
|
|
217
|
+
dangerous: {
|
|
218
|
+
providerId: ProviderId;
|
|
219
|
+
agentId?: AgentId | undefined;
|
|
220
|
+
}[];
|
|
221
|
+
windowMs: number;
|
|
222
|
+
ts: number;
|
|
223
|
+
} | {
|
|
224
|
+
kind: "anomaly_detected";
|
|
225
|
+
agentId: AgentId;
|
|
226
|
+
providerId?: ProviderId | undefined;
|
|
227
|
+
rule: "velocity" | "blocked_burst" | "new_counterparty" | "cumulative";
|
|
228
|
+
detail: string;
|
|
229
|
+
action: "alert" | "freeze";
|
|
230
|
+
ts: number;
|
|
231
|
+
} | {
|
|
232
|
+
kind: "error";
|
|
233
|
+
providerId?: ProviderId | undefined;
|
|
234
|
+
agentId?: AgentId | undefined;
|
|
235
|
+
message: string;
|
|
236
|
+
ts: number;
|
|
237
|
+
} | {
|
|
238
|
+
kind: "backend_connected";
|
|
239
|
+
providerId: ProviderId;
|
|
240
|
+
ts: number;
|
|
241
|
+
} | {
|
|
242
|
+
kind: "ledger_anchored";
|
|
243
|
+
index: number;
|
|
244
|
+
rowHash: string;
|
|
245
|
+
ref?: string | undefined;
|
|
246
|
+
ts: number;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The cross-venue freeze controller — the headline capability.
|
|
251
|
+
*
|
|
252
|
+
* ONE action fans out a hard stop to every connected backend CONCURRENTLY and returns a
|
|
253
|
+
* complete, audited verdict in well under a second. This is the one thing no single wallet
|
|
254
|
+
* vendor can do, because each only governs its own rail.
|
|
255
|
+
*
|
|
256
|
+
* Fail-closed contract (prime directive #3):
|
|
257
|
+
* - Every provider's verdict is kept (we never drop one because another failed).
|
|
258
|
+
* - A freeze that resolves { confirmed: false }, throws, or times out is treated as
|
|
259
|
+
* STILL DANGEROUS — never as "probably fine".
|
|
260
|
+
* - Unconfirmed providers are escalated to the harder kill (revokeSession per agent).
|
|
261
|
+
* If that also fails, the agent is surfaced as still-dangerous in the ledger + report.
|
|
262
|
+
*/
|
|
263
|
+
|
|
264
|
+
interface PerProviderFreeze {
|
|
265
|
+
providerId: ProviderId;
|
|
266
|
+
mode: EnforcementMode;
|
|
267
|
+
outcome: FreezeOutcome;
|
|
268
|
+
mechanism?: FreezeMechanism | undefined;
|
|
269
|
+
/** true if the freeze confirmed OR escalation (revokeSession) succeeded. */
|
|
270
|
+
stopped: boolean;
|
|
271
|
+
dangerousAgents: AgentId[];
|
|
272
|
+
latencyMs: number;
|
|
273
|
+
}
|
|
274
|
+
interface FreezeReport {
|
|
275
|
+
freezeId: string;
|
|
276
|
+
requestedAt: number;
|
|
277
|
+
windowMs: number;
|
|
278
|
+
allStopped: boolean;
|
|
279
|
+
providers: PerProviderFreeze[];
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @countersign/api-contract — the SINGLE SOURCE OF TRUTH for the Client<->Core wire interface.
|
|
284
|
+
* The Flutter client is generated from this (openapi.yaml for REST + these types for the ws stream),
|
|
285
|
+
* so the approve / freeze / ledger contract never drifts between Dart and TS.
|
|
286
|
+
*
|
|
287
|
+
* The language boundary (Dart client / TS core) is the trust boundary: a compromised client can
|
|
288
|
+
* still only call these endpoints — it holds no keys and cannot weaken policy or move funds.
|
|
289
|
+
*/
|
|
290
|
+
|
|
291
|
+
declare const WS_PATH = "/events";
|
|
292
|
+
interface ProviderHealth {
|
|
293
|
+
id: string;
|
|
294
|
+
mode: EnforcementMode;
|
|
295
|
+
healthy: boolean;
|
|
296
|
+
detail?: string;
|
|
297
|
+
}
|
|
298
|
+
interface HealthResponse {
|
|
299
|
+
ok: boolean;
|
|
300
|
+
providers: ProviderHealth[];
|
|
301
|
+
}
|
|
302
|
+
interface AgentDTO {
|
|
303
|
+
providerId: string;
|
|
304
|
+
agentId: string;
|
|
305
|
+
wallet: string;
|
|
306
|
+
venue: string;
|
|
307
|
+
mode: EnforcementMode;
|
|
308
|
+
}
|
|
309
|
+
interface AgentsResponse {
|
|
310
|
+
agents: AgentDTO[];
|
|
311
|
+
}
|
|
312
|
+
interface ApplyPolicyRequest {
|
|
313
|
+
/** Target a single agent, or omit to apply to every agent on every backend. */
|
|
314
|
+
agentId?: string;
|
|
315
|
+
policy: UnifiedPolicy;
|
|
316
|
+
}
|
|
317
|
+
interface ApplyPolicyResult {
|
|
318
|
+
applied: {
|
|
319
|
+
providerId: string;
|
|
320
|
+
agentId: string;
|
|
321
|
+
policyId: string;
|
|
322
|
+
}[];
|
|
323
|
+
/** Backends that could not confirm the policy — fail-closed: these are NOT live. */
|
|
324
|
+
failed: {
|
|
325
|
+
providerId: string;
|
|
326
|
+
agentId: string;
|
|
327
|
+
error: string;
|
|
328
|
+
}[];
|
|
329
|
+
}
|
|
330
|
+
interface FreezeRequest {
|
|
331
|
+
reason?: string;
|
|
332
|
+
}
|
|
333
|
+
type FreezeResponse = FreezeReport;
|
|
334
|
+
/**
|
|
335
|
+
* The agent-facing pre-flight guard: an agent asks Countersign "should I make this spend?" BEFORE it
|
|
336
|
+
* touches the wallet. Countersign answers from the unified policy and records the decision. This is the
|
|
337
|
+
* call that gets made on every transaction — the data flywheel.
|
|
338
|
+
*/
|
|
339
|
+
interface EvaluateRequest {
|
|
340
|
+
agentId: string;
|
|
341
|
+
amount: string;
|
|
342
|
+
asset: string;
|
|
343
|
+
counterparty?: string;
|
|
344
|
+
venue: string;
|
|
345
|
+
}
|
|
346
|
+
interface EvaluateResponse {
|
|
347
|
+
outcome: "allow" | "deny" | "needs_approval";
|
|
348
|
+
reason?: string;
|
|
349
|
+
approvalToken?: string;
|
|
350
|
+
policyId: string;
|
|
351
|
+
}
|
|
352
|
+
/** A spend held pending human approval (the Turnkey-style consensus path). */
|
|
353
|
+
interface PendingApprovalDTO {
|
|
354
|
+
approvalToken: string;
|
|
355
|
+
agentId: string;
|
|
356
|
+
providerId: string;
|
|
357
|
+
amount: string;
|
|
358
|
+
asset: string;
|
|
359
|
+
counterparty?: string;
|
|
360
|
+
venue: string;
|
|
361
|
+
reason: string;
|
|
362
|
+
ts: number;
|
|
363
|
+
}
|
|
364
|
+
interface ApprovalsResponse {
|
|
365
|
+
approvals: PendingApprovalDTO[];
|
|
366
|
+
}
|
|
367
|
+
interface ApproveRequest {
|
|
368
|
+
approvalToken: string;
|
|
369
|
+
}
|
|
370
|
+
interface DenyRequest {
|
|
371
|
+
approvalToken: string;
|
|
372
|
+
reason?: string;
|
|
373
|
+
}
|
|
374
|
+
interface ApprovalResolution {
|
|
375
|
+
outcome: "approved" | "denied";
|
|
376
|
+
agentId: string;
|
|
377
|
+
approvalToken: string;
|
|
378
|
+
reason?: string;
|
|
379
|
+
}
|
|
380
|
+
interface LedgerRecordDTO {
|
|
381
|
+
index: number;
|
|
382
|
+
prevHash: string;
|
|
383
|
+
payloadHash: string;
|
|
384
|
+
rowHash: string;
|
|
385
|
+
payload: LedgerEvent;
|
|
386
|
+
}
|
|
387
|
+
interface LedgerResponse {
|
|
388
|
+
records: LedgerRecordDTO[];
|
|
389
|
+
/** Result of re-verifying the hash chain (and signatures, if signed) at read time. */
|
|
390
|
+
verified: boolean;
|
|
391
|
+
/** Ed25519 public key (base64 SPKI) to independently verify the signed ledger, if signed. */
|
|
392
|
+
publicKey?: string;
|
|
393
|
+
}
|
|
394
|
+
/** Messages the Core pushes to the client over the websocket. */
|
|
395
|
+
type WsServerMessage = {
|
|
396
|
+
type: "hello";
|
|
397
|
+
providers: ProviderHealth[];
|
|
398
|
+
} | {
|
|
399
|
+
type: "ledger_append";
|
|
400
|
+
record: LedgerRecordDTO;
|
|
401
|
+
} | {
|
|
402
|
+
type: "freeze_report";
|
|
403
|
+
report: FreezeReport;
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* The operations the front door exposes — the single abstraction the SDK client, the embedded
|
|
407
|
+
* in-process Core, the MCP tools, and the x402 guard all program against. `CountersignClient` (HTTP) and
|
|
408
|
+
* the local adapter both implement this, so callers don't care whether the Core is remote or embedded.
|
|
409
|
+
*/
|
|
410
|
+
interface CountersignApi {
|
|
411
|
+
health(): Promise<HealthResponse>;
|
|
412
|
+
agents(): Promise<AgentsResponse>;
|
|
413
|
+
applyPolicy(req: ApplyPolicyRequest): Promise<ApplyPolicyResult>;
|
|
414
|
+
evaluate(req: EvaluateRequest): Promise<EvaluateResponse>;
|
|
415
|
+
approvals(): Promise<ApprovalsResponse>;
|
|
416
|
+
approve(req: ApproveRequest): Promise<ApprovalResolution>;
|
|
417
|
+
deny(req: DenyRequest): Promise<ApprovalResolution>;
|
|
418
|
+
freeze(req?: FreezeRequest): Promise<FreezeResponse>;
|
|
419
|
+
unfreeze(): Promise<{
|
|
420
|
+
ok: boolean;
|
|
421
|
+
}>;
|
|
422
|
+
ledger(): Promise<LedgerResponse>;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export { WS_PATH };
|
|
426
|
+
export type { AgentDTO, AgentsResponse, ApplyPolicyRequest, ApplyPolicyResult, ApprovalResolution, ApprovalsResponse, ApproveRequest, CountersignApi, DenyRequest, EvaluateRequest, EvaluateResponse, FreezeRequest, FreezeResponse, HealthResponse, LedgerRecordDTO, LedgerResponse, PendingApprovalDTO, ProviderHealth, WsServerMessage };
|
package/dist/index.js
ADDED
package/openapi.yaml
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
openapi: 3.1.0
|
|
2
|
+
info:
|
|
3
|
+
title: Countersign Core API
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
description: >
|
|
6
|
+
The neutral control plane the (key-less) Countersign client talks to. The client renders state and
|
|
7
|
+
fires approve / freeze; all crypto and vendor SDK weight lives behind this API. Source of truth
|
|
8
|
+
for the generated Dart client. The websocket event stream is typed in index.ts (WsServerMessage).
|
|
9
|
+
servers:
|
|
10
|
+
- url: http://localhost:8080
|
|
11
|
+
paths:
|
|
12
|
+
/health:
|
|
13
|
+
get:
|
|
14
|
+
summary: Liveness + per-backend health
|
|
15
|
+
responses:
|
|
16
|
+
"200":
|
|
17
|
+
description: OK
|
|
18
|
+
content:
|
|
19
|
+
application/json:
|
|
20
|
+
schema: { $ref: "#/components/schemas/HealthResponse" }
|
|
21
|
+
/agents:
|
|
22
|
+
get:
|
|
23
|
+
summary: List provisioned agents across all backends
|
|
24
|
+
responses:
|
|
25
|
+
"200":
|
|
26
|
+
description: OK
|
|
27
|
+
content:
|
|
28
|
+
application/json:
|
|
29
|
+
schema: { $ref: "#/components/schemas/AgentsResponse" }
|
|
30
|
+
/policy:
|
|
31
|
+
post:
|
|
32
|
+
summary: Compile + apply one unified policy across backends (fail-closed)
|
|
33
|
+
requestBody:
|
|
34
|
+
required: true
|
|
35
|
+
content:
|
|
36
|
+
application/json:
|
|
37
|
+
schema: { $ref: "#/components/schemas/ApplyPolicyRequest" }
|
|
38
|
+
responses:
|
|
39
|
+
"200":
|
|
40
|
+
description: Per-backend apply results
|
|
41
|
+
content:
|
|
42
|
+
application/json:
|
|
43
|
+
schema: { $ref: "#/components/schemas/ApplyPolicyResult" }
|
|
44
|
+
/freeze:
|
|
45
|
+
post:
|
|
46
|
+
summary: The kill switch — freeze every agent on every backend, concurrently
|
|
47
|
+
requestBody:
|
|
48
|
+
required: false
|
|
49
|
+
content:
|
|
50
|
+
application/json:
|
|
51
|
+
schema: { $ref: "#/components/schemas/FreezeRequest" }
|
|
52
|
+
responses:
|
|
53
|
+
"200":
|
|
54
|
+
description: Aggregate freeze report (sub-second, fail-closed)
|
|
55
|
+
content:
|
|
56
|
+
application/json:
|
|
57
|
+
schema: { $ref: "#/components/schemas/FreezeReport" }
|
|
58
|
+
/evaluate:
|
|
59
|
+
post:
|
|
60
|
+
summary: Pre-flight spend guard — ask Countersign whether a spend is allowed before touching the wallet
|
|
61
|
+
requestBody:
|
|
62
|
+
required: true
|
|
63
|
+
content:
|
|
64
|
+
application/json:
|
|
65
|
+
schema: { $ref: "#/components/schemas/EvaluateRequest" }
|
|
66
|
+
responses:
|
|
67
|
+
"200":
|
|
68
|
+
description: The decision (allow / deny / needs_approval), also recorded in the ledger
|
|
69
|
+
content:
|
|
70
|
+
application/json:
|
|
71
|
+
schema: { $ref: "#/components/schemas/EvaluateResponse" }
|
|
72
|
+
/approvals:
|
|
73
|
+
get:
|
|
74
|
+
summary: Spends currently held pending human approval
|
|
75
|
+
responses:
|
|
76
|
+
"200":
|
|
77
|
+
description: OK
|
|
78
|
+
content:
|
|
79
|
+
application/json:
|
|
80
|
+
schema: { $ref: "#/components/schemas/ApprovalsResponse" }
|
|
81
|
+
/approve:
|
|
82
|
+
post:
|
|
83
|
+
summary: Approve a pending spend (rejected if the system is frozen — fail-closed)
|
|
84
|
+
requestBody:
|
|
85
|
+
required: true
|
|
86
|
+
content:
|
|
87
|
+
application/json:
|
|
88
|
+
schema: { type: object, required: [approvalToken], properties: { approvalToken: { type: string } } }
|
|
89
|
+
responses:
|
|
90
|
+
"200":
|
|
91
|
+
description: Resolution
|
|
92
|
+
content:
|
|
93
|
+
application/json:
|
|
94
|
+
schema: { $ref: "#/components/schemas/ApprovalResolution" }
|
|
95
|
+
/deny:
|
|
96
|
+
post:
|
|
97
|
+
summary: Deny a pending spend
|
|
98
|
+
requestBody:
|
|
99
|
+
required: true
|
|
100
|
+
content:
|
|
101
|
+
application/json:
|
|
102
|
+
schema: { type: object, required: [approvalToken], properties: { approvalToken: { type: string }, reason: { type: string } } }
|
|
103
|
+
responses:
|
|
104
|
+
"200":
|
|
105
|
+
description: Resolution
|
|
106
|
+
content:
|
|
107
|
+
application/json:
|
|
108
|
+
schema: { $ref: "#/components/schemas/ApprovalResolution" }
|
|
109
|
+
/ledger:
|
|
110
|
+
get:
|
|
111
|
+
summary: The append-only, hash-chained audit ledger (re-verified at read time)
|
|
112
|
+
responses:
|
|
113
|
+
"200":
|
|
114
|
+
description: OK
|
|
115
|
+
content:
|
|
116
|
+
application/json:
|
|
117
|
+
schema: { $ref: "#/components/schemas/LedgerResponse" }
|
|
118
|
+
components:
|
|
119
|
+
schemas:
|
|
120
|
+
ProviderHealth:
|
|
121
|
+
type: object
|
|
122
|
+
required: [id, mode, healthy]
|
|
123
|
+
properties:
|
|
124
|
+
id: { type: string }
|
|
125
|
+
mode: { type: string, enum: [native-session-caps, pre-sign-policy, onchain-policy] }
|
|
126
|
+
healthy: { type: boolean }
|
|
127
|
+
detail: { type: string }
|
|
128
|
+
HealthResponse:
|
|
129
|
+
type: object
|
|
130
|
+
required: [ok, providers]
|
|
131
|
+
properties:
|
|
132
|
+
ok: { type: boolean }
|
|
133
|
+
providers: { type: array, items: { $ref: "#/components/schemas/ProviderHealth" } }
|
|
134
|
+
AgentsResponse:
|
|
135
|
+
type: object
|
|
136
|
+
required: [agents]
|
|
137
|
+
properties:
|
|
138
|
+
agents:
|
|
139
|
+
type: array
|
|
140
|
+
items:
|
|
141
|
+
type: object
|
|
142
|
+
required: [providerId, agentId, wallet, venue, mode]
|
|
143
|
+
properties:
|
|
144
|
+
providerId: { type: string }
|
|
145
|
+
agentId: { type: string }
|
|
146
|
+
wallet: { type: string }
|
|
147
|
+
venue: { type: string }
|
|
148
|
+
mode: { type: string }
|
|
149
|
+
UnifiedPolicy:
|
|
150
|
+
type: object
|
|
151
|
+
required: [schemaVersion, asset]
|
|
152
|
+
properties:
|
|
153
|
+
schemaVersion: { type: integer, enum: [1] }
|
|
154
|
+
asset: { type: string }
|
|
155
|
+
perTxCap: { type: string, description: base units }
|
|
156
|
+
dailyCap: { type: string, description: base units }
|
|
157
|
+
allowlist: { type: array, items: { type: string } }
|
|
158
|
+
denylist: { type: array, items: { type: string } }
|
|
159
|
+
approvalThreshold: { type: string, description: base units }
|
|
160
|
+
frozen: { type: boolean }
|
|
161
|
+
venues: { type: array, items: { type: string } }
|
|
162
|
+
ApplyPolicyRequest:
|
|
163
|
+
type: object
|
|
164
|
+
required: [policy]
|
|
165
|
+
properties:
|
|
166
|
+
agentId: { type: string }
|
|
167
|
+
policy: { $ref: "#/components/schemas/UnifiedPolicy" }
|
|
168
|
+
ApplyPolicyResult:
|
|
169
|
+
type: object
|
|
170
|
+
required: [applied, failed]
|
|
171
|
+
properties:
|
|
172
|
+
applied:
|
|
173
|
+
type: array
|
|
174
|
+
items:
|
|
175
|
+
type: object
|
|
176
|
+
properties:
|
|
177
|
+
providerId: { type: string }
|
|
178
|
+
agentId: { type: string }
|
|
179
|
+
policyId: { type: string }
|
|
180
|
+
failed:
|
|
181
|
+
type: array
|
|
182
|
+
items:
|
|
183
|
+
type: object
|
|
184
|
+
properties:
|
|
185
|
+
providerId: { type: string }
|
|
186
|
+
agentId: { type: string }
|
|
187
|
+
error: { type: string }
|
|
188
|
+
FreezeRequest:
|
|
189
|
+
type: object
|
|
190
|
+
properties:
|
|
191
|
+
reason: { type: string }
|
|
192
|
+
EvaluateRequest:
|
|
193
|
+
type: object
|
|
194
|
+
required: [agentId, amount, asset, venue]
|
|
195
|
+
properties:
|
|
196
|
+
agentId: { type: string }
|
|
197
|
+
amount: { type: string, description: base units }
|
|
198
|
+
asset: { type: string }
|
|
199
|
+
counterparty: { type: string }
|
|
200
|
+
venue: { type: string }
|
|
201
|
+
EvaluateResponse:
|
|
202
|
+
type: object
|
|
203
|
+
required: [outcome, policyId]
|
|
204
|
+
properties:
|
|
205
|
+
outcome: { type: string, enum: [allow, deny, needs_approval] }
|
|
206
|
+
reason: { type: string }
|
|
207
|
+
approvalToken: { type: string }
|
|
208
|
+
policyId: { type: string }
|
|
209
|
+
ApprovalsResponse:
|
|
210
|
+
type: object
|
|
211
|
+
required: [approvals]
|
|
212
|
+
properties:
|
|
213
|
+
approvals:
|
|
214
|
+
type: array
|
|
215
|
+
items:
|
|
216
|
+
type: object
|
|
217
|
+
required: [approvalToken, agentId, providerId, amount, asset, venue, reason, ts]
|
|
218
|
+
properties:
|
|
219
|
+
approvalToken: { type: string }
|
|
220
|
+
agentId: { type: string }
|
|
221
|
+
providerId: { type: string }
|
|
222
|
+
amount: { type: string }
|
|
223
|
+
asset: { type: string }
|
|
224
|
+
counterparty: { type: string }
|
|
225
|
+
venue: { type: string }
|
|
226
|
+
reason: { type: string }
|
|
227
|
+
ts: { type: integer }
|
|
228
|
+
ApprovalResolution:
|
|
229
|
+
type: object
|
|
230
|
+
required: [outcome, agentId, approvalToken]
|
|
231
|
+
properties:
|
|
232
|
+
outcome: { type: string, enum: [approved, denied] }
|
|
233
|
+
agentId: { type: string }
|
|
234
|
+
approvalToken: { type: string }
|
|
235
|
+
reason: { type: string }
|
|
236
|
+
FreezeReport:
|
|
237
|
+
type: object
|
|
238
|
+
required: [freezeId, requestedAt, windowMs, allStopped, providers]
|
|
239
|
+
properties:
|
|
240
|
+
freezeId: { type: string }
|
|
241
|
+
requestedAt: { type: integer }
|
|
242
|
+
windowMs: { type: integer }
|
|
243
|
+
allStopped: { type: boolean }
|
|
244
|
+
providers:
|
|
245
|
+
type: array
|
|
246
|
+
items:
|
|
247
|
+
type: object
|
|
248
|
+
properties:
|
|
249
|
+
providerId: { type: string }
|
|
250
|
+
mode: { type: string }
|
|
251
|
+
outcome: { type: string, enum: [confirmed, unconfirmed, failed] }
|
|
252
|
+
mechanism: { type: string }
|
|
253
|
+
stopped: { type: boolean }
|
|
254
|
+
dangerousAgents: { type: array, items: { type: string } }
|
|
255
|
+
latencyMs: { type: integer }
|
|
256
|
+
LedgerResponse:
|
|
257
|
+
type: object
|
|
258
|
+
required: [records, verified]
|
|
259
|
+
properties:
|
|
260
|
+
verified: { type: boolean }
|
|
261
|
+
publicKey: { type: string, description: Ed25519 public key (base64) to independently verify the signed ledger }
|
|
262
|
+
records:
|
|
263
|
+
type: array
|
|
264
|
+
items:
|
|
265
|
+
type: object
|
|
266
|
+
required: [index, prevHash, payloadHash, rowHash, payload]
|
|
267
|
+
properties:
|
|
268
|
+
index: { type: integer }
|
|
269
|
+
prevHash: { type: string }
|
|
270
|
+
payloadHash: { type: string }
|
|
271
|
+
rowHash: { type: string }
|
|
272
|
+
payload: { type: object, additionalProperties: true }
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@countersign/api-contract",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Countersign Core API contract — typed REST + ws schema and OpenAPI spec for the cross-vendor agent-spend control plane.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
},
|
|
15
|
+
"./openapi": "./openapi.yaml"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"openapi.yaml"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/countersign-network/countersign.git",
|
|
27
|
+
"directory": "api-contract"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://countersign.network",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"countersign",
|
|
32
|
+
"ai-agents",
|
|
33
|
+
"agent-payments",
|
|
34
|
+
"openapi",
|
|
35
|
+
"policy",
|
|
36
|
+
"freeze"
|
|
37
|
+
],
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"zod": "^3.23.8"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"tsup": "^8.5.1",
|
|
43
|
+
"@countersign/core": "0.0.0",
|
|
44
|
+
"@countersign/policy": "0.0.0"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc -p ../tsconfig.dts.json && tsup && rollup -c rollup.dts.config.mjs"
|
|
48
|
+
},
|
|
49
|
+
"module": "./dist/index.js"
|
|
50
|
+
}
|