@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 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
+ });
@@ -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
@@ -0,0 +1,5 @@
1
+ // index.ts
2
+ var WS_PATH = "/events";
3
+ export {
4
+ WS_PATH
5
+ };
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
+ }