@cinchor/sdk 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/dist/index.js ADDED
@@ -0,0 +1,755 @@
1
+ // src/capability-registry.ts
2
+ import {
3
+ OmneClient,
4
+ OmneContract,
5
+ AbiEncode,
6
+ encodeContractCall
7
+ } from "@omne/sdk";
8
+
9
+ // src/address.ts
10
+ import { bech32m } from "@scure/base";
11
+ import { ArgType } from "@omne/sdk";
12
+ var ADDRESS_HRP = "om";
13
+ var WITNESS_VERSION = 2;
14
+ function decodeAddress(address) {
15
+ const { prefix, words } = bech32m.decode(address);
16
+ if (prefix !== ADDRESS_HRP) {
17
+ throw new Error(`Invalid address HRP: expected "${ADDRESS_HRP}", got "${prefix}"`);
18
+ }
19
+ if (words[0] !== WITNESS_VERSION) {
20
+ throw new Error(`Invalid witness version: expected ${WITNESS_VERSION}, got ${words[0]}`);
21
+ }
22
+ return new Uint8Array(bech32m.fromWords(Array.from(words.slice(1))));
23
+ }
24
+ function encodeAddress(payload) {
25
+ if (payload.length !== 32) {
26
+ throw new Error(`Address payload must be 32 bytes, got ${payload.length}`);
27
+ }
28
+ const dataWords = bech32m.toWords(payload);
29
+ const words = new Uint8Array(dataWords.length + 1);
30
+ words[0] = WITNESS_VERSION;
31
+ words.set(dataWords, 1);
32
+ return bech32m.encode(ADDRESS_HRP, words);
33
+ }
34
+ function addressArg(address) {
35
+ return { type: ArgType.Address, data: decodeAddress(address) };
36
+ }
37
+ async function sha256(bytes) {
38
+ const ab = new ArrayBuffer(bytes.byteLength);
39
+ new Uint8Array(ab).set(bytes);
40
+ return new Uint8Array(await crypto.subtle.digest("SHA-256", ab));
41
+ }
42
+ function u64be(value) {
43
+ const b = new Uint8Array(8);
44
+ new DataView(b.buffer).setBigUint64(0, BigInt(value), false);
45
+ return b;
46
+ }
47
+ function concat(...parts) {
48
+ const total = parts.reduce((n, p) => n + p.length, 0);
49
+ const out = new Uint8Array(total);
50
+ let off = 0;
51
+ for (const p of parts) {
52
+ out.set(p, off);
53
+ off += p.length;
54
+ }
55
+ return out;
56
+ }
57
+ async function deriveCapabilityId(principal, agent, nonce, createdAt) {
58
+ const preimage = concat(
59
+ decodeAddress(principal),
60
+ decodeAddress(agent),
61
+ u64be(nonce),
62
+ u64be(createdAt)
63
+ );
64
+ return encodeAddress(await sha256(preimage));
65
+ }
66
+ async function counterpartyKey(capabilityId, counterparty) {
67
+ const preimage = concat(decodeAddress(capabilityId), decodeAddress(counterparty));
68
+ return encodeAddress(await sha256(preimage));
69
+ }
70
+
71
+ // src/config.ts
72
+ function exportPrefixFor(contract) {
73
+ return contract.exportPrefix ?? `axiom_contract::${contract.name}::`;
74
+ }
75
+ var DEFAULT_GAS_LIMIT = 2e5;
76
+ var DEFAULT_GAS_PRICE = "5000";
77
+ var BURN_SENTINEL = "om1zmm0dahk7mm0dahk7mm0dahk7mm0dahk7mm0dahk7mm0dahk7mm0qdtuxap";
78
+ var IGNIS_LOCAL = {
79
+ name: "ignis",
80
+ chainId: 3,
81
+ rpcUrl: "http://127.0.0.1:26657"
82
+ };
83
+
84
+ // src/types.ts
85
+ var CapabilityStatus = /* @__PURE__ */ ((CapabilityStatus3) => {
86
+ CapabilityStatus3[CapabilityStatus3["NotFound"] = 0] = "NotFound";
87
+ CapabilityStatus3[CapabilityStatus3["Active"] = 1] = "Active";
88
+ CapabilityStatus3[CapabilityStatus3["Revoked"] = 2] = "Revoked";
89
+ return CapabilityStatus3;
90
+ })(CapabilityStatus || {});
91
+ var CAPABILITY_STATUS_LABELS = [
92
+ "not_found",
93
+ "active",
94
+ "revoked"
95
+ ];
96
+ var EnforcementCode = /* @__PURE__ */ ((EnforcementCode2) => {
97
+ EnforcementCode2[EnforcementCode2["NotFound"] = 0] = "NotFound";
98
+ EnforcementCode2[EnforcementCode2["Allowed"] = 1] = "Allowed";
99
+ EnforcementCode2[EnforcementCode2["Revoked"] = 2] = "Revoked";
100
+ EnforcementCode2[EnforcementCode2["Expired"] = 3] = "Expired";
101
+ EnforcementCode2[EnforcementCode2["OverBudget"] = 4] = "OverBudget";
102
+ EnforcementCode2[EnforcementCode2["OutOfAllowlist"] = 5] = "OutOfAllowlist";
103
+ return EnforcementCode2;
104
+ })(EnforcementCode || {});
105
+ var ENFORCEMENT_LABELS = {
106
+ [0 /* NotFound */]: "not_found",
107
+ [1 /* Allowed */]: "allowed",
108
+ [2 /* Revoked */]: "revoked",
109
+ [3 /* Expired */]: "expired",
110
+ [4 /* OverBudget */]: "over_budget",
111
+ [5 /* OutOfAllowlist */]: "out_of_allowlist"
112
+ };
113
+ var Verdict = /* @__PURE__ */ ((Verdict2) => {
114
+ Verdict2[Verdict2["InPolicy"] = 1] = "InPolicy";
115
+ Verdict2[Verdict2["OutOfPolicy"] = 2] = "OutOfPolicy";
116
+ return Verdict2;
117
+ })(Verdict || {});
118
+
119
+ // src/capability-registry.ts
120
+ function toBigInt(v) {
121
+ return BigInt(v ?? 0);
122
+ }
123
+ function toNumber(v) {
124
+ return Number(v ?? 0);
125
+ }
126
+ var CapabilityRegistry = class _CapabilityRegistry {
127
+ contract;
128
+ rpcUrl;
129
+ chainId;
130
+ contractAddress;
131
+ exportPrefix;
132
+ defaultGasLimit;
133
+ defaultGasPrice;
134
+ /** Per-signer next-nonce cache (see {@link sendSigned} for node nonce semantics). */
135
+ nonces = /* @__PURE__ */ new Map();
136
+ constructor(contract, config) {
137
+ this.contract = contract;
138
+ this.rpcUrl = config.network.rpcUrl;
139
+ this.chainId = config.network.chainId;
140
+ this.contractAddress = config.contract.address;
141
+ this.exportPrefix = exportPrefixFor(config.contract);
142
+ this.defaultGasLimit = config.defaultGasLimit ?? DEFAULT_GAS_LIMIT;
143
+ this.defaultGasPrice = config.defaultGasPrice ?? DEFAULT_GAS_PRICE;
144
+ }
145
+ static async connect(config) {
146
+ const client = new OmneClient(config.network.rpcUrl);
147
+ await client.connect();
148
+ const contract = new OmneContract(client, config.contract.address);
149
+ return new _CapabilityRegistry(contract, config);
150
+ }
151
+ fn(name) {
152
+ return this.exportPrefix + name;
153
+ }
154
+ /** A single, non-retrying JSON-RPC POST. */
155
+ async rpc(method, params) {
156
+ const resp = await fetch(this.rpcUrl, {
157
+ method: "POST",
158
+ headers: { "Content-Type": "application/json" },
159
+ body: JSON.stringify({ jsonrpc: "2.0", method, params, id: 1 })
160
+ });
161
+ if (!resp.ok) {
162
+ throw new Error(`${method} HTTP ${resp.status}: ${await resp.text()}`);
163
+ }
164
+ const json = await resp.json();
165
+ if (json.error) throw new Error(`${method} rejected: ${json.error.message ?? "unknown"}`);
166
+ return json.result;
167
+ }
168
+ // ── Write path ──────────────────────────────────────────────────
169
+ /**
170
+ * Build, SIGN, and submit a state-changing contract call. The Omne node
171
+ * mandates a valid ML-DSA-44 signature on every transaction; we encode the
172
+ * call data, build the transaction, sign it with the role's signer (chainId
173
+ * 3 = Ignis), and submit via a single raw JSON-RPC POST. The submitter
174
+ * ("from") must equal the signer's om1z address — the node re-derives the PQC
175
+ * address from the public key and checks it matches.
176
+ */
177
+ async sendSigned(signer, method, args, gasLimit = this.defaultGasLimit) {
178
+ const data = encodeContractCall(this.fn(method), args);
179
+ let nonce = this.nonces.get(signer.address);
180
+ if (nonce === void 0) nonce = await this.fetchNonce(signer.address);
181
+ for (let attempt = 0; attempt < 2; attempt++) {
182
+ const wire = this.buildSignedWire(signer, data, nonce, gasLimit);
183
+ const res = await this.submitOnce(wire);
184
+ if (res.status === "ok" || res.status === "duplicate") {
185
+ this.nonces.set(signer.address, nonce + 1);
186
+ return this.pollReceipt(res.txHash);
187
+ }
188
+ const fresh = await this.fetchNonce(signer.address);
189
+ if (attempt === 0 && fresh > nonce) {
190
+ nonce = fresh;
191
+ this.nonces.set(signer.address, fresh);
192
+ continue;
193
+ }
194
+ throw new Error(
195
+ `transaction rejected nonce ${nonce} as too low; node reports ${fresh} (cannot recover)`
196
+ );
197
+ }
198
+ throw new Error("sendSigned: exhausted submit attempts");
199
+ }
200
+ /** Fetch the signer's next nonce from the node (0 on this devnet). */
201
+ async fetchNonce(address) {
202
+ try {
203
+ const n = await this.rpc("omne_getNonce", [address]);
204
+ return Number(n ?? 0);
205
+ } catch {
206
+ const acct = await this.rpc("omne_getAccount", [address]);
207
+ return Number(acct?.nonce ?? 0);
208
+ }
209
+ }
210
+ /**
211
+ * Build a signed wire payload for a contract call at a given nonce, validating
212
+ * the ML-DSA-44 signature/pubkey/chainId lengths locally before submit so a
213
+ * malformed signed object fails fast here, not at the node.
214
+ */
215
+ buildSignedWire(signer, data, nonce, gasLimit) {
216
+ const tx = {
217
+ from: signer.address,
218
+ to: this.contractAddress,
219
+ value: "0",
220
+ gasLimit,
221
+ gasPrice: this.defaultGasPrice,
222
+ nonce,
223
+ data,
224
+ chainId: this.chainId
225
+ };
226
+ const signed = signer.signTransaction(tx, { chainId: this.chainId });
227
+ if (typeof signed.signature !== "string" || !/^[0-9a-f]{4840}$/i.test(signed.signature)) {
228
+ throw new Error(
229
+ `signTransaction produced an invalid ML-DSA-44 signature (expected 4840 hex chars, got ${signed.signature?.length ?? 0})`
230
+ );
231
+ }
232
+ if (typeof signed.publicKey !== "string" || !/^[0-9a-f]{2624}$/i.test(signed.publicKey)) {
233
+ throw new Error(
234
+ `signTransaction produced an invalid ML-DSA-44 public key (expected 2624 hex chars, got ${signed.publicKey?.length ?? 0})`
235
+ );
236
+ }
237
+ if (!Number.isInteger(signed.chainId) || signed.chainId < 0) {
238
+ throw new Error(`signed tx carries an invalid chainId: ${signed.chainId}`);
239
+ }
240
+ return {
241
+ from: signed.from,
242
+ to: signed.to,
243
+ value: signed.value,
244
+ gasLimit: signed.gasLimit,
245
+ gasPrice: signed.gasPrice,
246
+ nonce: signed.nonce,
247
+ chainId: signed.chainId,
248
+ data: signed.data ?? "",
249
+ signature: { signature: signed.signature, publicKey: signed.publicKey }
250
+ };
251
+ }
252
+ /** Submit a signed wire payload with one non-retrying POST. */
253
+ async submitOnce(wire) {
254
+ const resp = await fetch(this.rpcUrl, {
255
+ method: "POST",
256
+ headers: { "Content-Type": "application/json" },
257
+ body: JSON.stringify({ jsonrpc: "2.0", method: "omne_sendTransaction", params: [wire], id: 1 })
258
+ });
259
+ if (!resp.ok) {
260
+ throw new Error(`omne_sendTransaction HTTP ${resp.status}: ${await resp.text()}`);
261
+ }
262
+ const json = await resp.json();
263
+ if (json.error) {
264
+ const msg = String(json.error.message ?? json.error);
265
+ if (/already\s*known|already\s*exists|duplicate/i.test(msg)) {
266
+ return { status: "duplicate", txHash: null };
267
+ }
268
+ if (/nonce\s*too\s*low|invalid\s*nonce/i.test(msg)) {
269
+ return { status: "stale_nonce", txHash: null };
270
+ }
271
+ throw new Error(`omne_sendTransaction rejected: ${msg}`);
272
+ }
273
+ const r = json.result;
274
+ return { status: "ok", txHash: typeof r === "string" ? r : r?.transactionHash ?? null };
275
+ }
276
+ /**
277
+ * Poll for a transaction receipt, tolerating the node's "receipt not found"
278
+ * RPC error while the tx is still in the mempool. 60s default: a 4-node BFT
279
+ * mesh confirms contract calls in ~20-30s.
280
+ */
281
+ async pollReceipt(txHash, timeoutMs = 6e4, intervalMs = 1e3) {
282
+ if (!txHash) return { transactionHash: null, status: "submitted", blockNumber: null };
283
+ const start = Date.now();
284
+ let lastTransportError = null;
285
+ while (Date.now() - start < timeoutMs) {
286
+ try {
287
+ const r = await this.rpc("omne_getTransactionReceipt", [txHash]);
288
+ if (r) return r;
289
+ } catch (e) {
290
+ const msg = e instanceof Error ? e.message : String(e);
291
+ if (/receipt.*not\s*found|not\s*found|-32000|-32602/i.test(msg)) {
292
+ } else {
293
+ lastTransportError = msg;
294
+ }
295
+ }
296
+ await new Promise((res) => setTimeout(res, intervalMs));
297
+ }
298
+ return {
299
+ transactionHash: txHash,
300
+ status: "pending",
301
+ blockNumber: null,
302
+ note: lastTransportError ? `receipt not available within ${timeoutMs}ms; last transport error: ${lastTransportError}` : `receipt not available within ${timeoutMs}ms`
303
+ };
304
+ }
305
+ // ── Reads ───────────────────────────────────────────────────────
306
+ async queryNumber(method, id) {
307
+ const r = await this.contract.query(this.fn(method), [addressArg(id)]);
308
+ return toNumber(r.returnValue);
309
+ }
310
+ async queryBigInt(method, id) {
311
+ const r = await this.contract.query(this.fn(method), [addressArg(id)]);
312
+ return toBigInt(r.returnValue);
313
+ }
314
+ async queryAddress(method, id) {
315
+ const r = await this.contract.query(this.fn(method), [addressArg(id)]);
316
+ return parseAddressReturn(r.returnValue);
317
+ }
318
+ getStatus(capabilityId) {
319
+ return this.queryNumber("get_status", capabilityId);
320
+ }
321
+ getMaxSpend(capabilityId) {
322
+ return this.queryBigInt("get_max_spend", capabilityId);
323
+ }
324
+ getValidUntil(capabilityId) {
325
+ return this.queryBigInt("get_valid_until", capabilityId);
326
+ }
327
+ getTotalSpent(capabilityId) {
328
+ return this.queryBigInt("get_total_spent", capabilityId);
329
+ }
330
+ getActionCount(capabilityId) {
331
+ return this.queryBigInt("get_action_count", capabilityId);
332
+ }
333
+ getCreatedAt(capabilityId) {
334
+ return this.queryBigInt("get_created_at", capabilityId);
335
+ }
336
+ getRevokedAt(capabilityId) {
337
+ return this.queryBigInt("get_revoked_at", capabilityId);
338
+ }
339
+ getPolicyVersion(capabilityId) {
340
+ return this.queryBigInt("get_policy_version", capabilityId);
341
+ }
342
+ getAttestationCount(capabilityId) {
343
+ return this.queryBigInt("get_attestation_count", capabilityId);
344
+ }
345
+ async getAllowlistEnabled(capabilityId) {
346
+ return await this.queryNumber("get_allowlist_enabled", capabilityId) === 1;
347
+ }
348
+ async isCounterpartyAllowed(capabilityId, counterparty) {
349
+ const key = await counterpartyKey(capabilityId, counterparty);
350
+ const r = await this.contract.query(this.fn("is_counterparty_allowed"), [addressArg(key)]);
351
+ return toNumber(r.returnValue) === 1;
352
+ }
353
+ /** Read a decision attestation's on-chain record (the tamper-evidence anchor). */
354
+ async getAttestation(attestationId) {
355
+ const [exists, ch, pv, v, t] = await Promise.all([
356
+ this.contract.query(this.fn("get_attestation_exists"), [addressArg(attestationId)]),
357
+ this.contract.query(this.fn("get_attestation_context_hash"), [addressArg(attestationId)]),
358
+ this.contract.query(this.fn("get_attestation_policy_version"), [addressArg(attestationId)]),
359
+ this.contract.query(this.fn("get_attestation_verdict"), [addressArg(attestationId)]),
360
+ this.contract.query(this.fn("get_attestation_time"), [addressArg(attestationId)])
361
+ ]);
362
+ return {
363
+ exists: toNumber(exists.returnValue) === 1,
364
+ contextHash: parseAddressReturn(ch.returnValue),
365
+ policyVersion: toBigInt(pv.returnValue),
366
+ verdict: toNumber(v.returnValue),
367
+ time: toBigInt(t.returnValue)
368
+ };
369
+ }
370
+ /** Fetch the complete state of a capability in one call. */
371
+ async getCapabilityState(capabilityId) {
372
+ const [
373
+ status,
374
+ principal,
375
+ agent,
376
+ maxSpend,
377
+ validUntil,
378
+ totalSpent,
379
+ actionCount,
380
+ createdAt,
381
+ revokedAt
382
+ ] = await Promise.all([
383
+ this.getStatus(capabilityId),
384
+ this.queryAddress("get_principal", capabilityId).catch(() => ""),
385
+ this.queryAddress("get_agent", capabilityId).catch(() => ""),
386
+ this.getMaxSpend(capabilityId),
387
+ this.getValidUntil(capabilityId),
388
+ this.getTotalSpent(capabilityId),
389
+ this.getActionCount(capabilityId),
390
+ this.getCreatedAt(capabilityId),
391
+ this.getRevokedAt(capabilityId)
392
+ ]);
393
+ return {
394
+ capabilityId,
395
+ status,
396
+ statusLabel: CAPABILITY_STATUS_LABELS[status] ?? "not_found",
397
+ principal,
398
+ agent,
399
+ maxSpend,
400
+ validUntil,
401
+ totalSpent,
402
+ actionCount,
403
+ createdAt,
404
+ revokedAt
405
+ };
406
+ }
407
+ // ── Writes (signed; require a funded signer) ─────────────────────
408
+ /** Principal mints a scoped capability to an agent. Signed by the principal. */
409
+ mintPermission(opts) {
410
+ return this.sendSigned(
411
+ opts.signer,
412
+ "mint_permission",
413
+ [
414
+ addressArg(opts.capabilityId),
415
+ addressArg(opts.principal),
416
+ addressArg(opts.agent),
417
+ AbiEncode.i64(opts.maxSpend),
418
+ AbiEncode.i64(opts.validUntil),
419
+ AbiEncode.i64(opts.allowlistEnabled ? 1n : 0n),
420
+ AbiEncode.i64(opts.currentTime)
421
+ ],
422
+ opts.gasLimit
423
+ );
424
+ }
425
+ /** Principal authorizes a counterparty for a capability's allowlist. */
426
+ async addAllowedCounterparty(opts) {
427
+ const key = await counterpartyKey(opts.capabilityId, opts.counterparty);
428
+ return this.sendSigned(
429
+ opts.signer,
430
+ "add_allowed_counterparty",
431
+ [addressArg(opts.capabilityId), addressArg(key), AbiEncode.i64(opts.currentTime)],
432
+ opts.gasLimit
433
+ );
434
+ }
435
+ /** Principal updates a capability's policy (limits) and bumps its on-chain version. */
436
+ updatePolicy(opts) {
437
+ return this.sendSigned(
438
+ opts.signer,
439
+ "update_policy",
440
+ [
441
+ addressArg(opts.capabilityId),
442
+ AbiEncode.i64(opts.newMaxSpend),
443
+ AbiEncode.i64(opts.newValidUntil),
444
+ AbiEncode.i64(opts.currentTime)
445
+ ],
446
+ opts.gasLimit
447
+ );
448
+ }
449
+ /**
450
+ * Agent records a capability-bound action — the substrate Policy Enforcement
451
+ * Point. Signed by the agent. `counterparty` is ignored when the capability's
452
+ * allowlist is disabled; when enabled, the substrate refuses (code 5) unless
453
+ * the counterparty has been allowlisted.
454
+ */
455
+ async recordAction(opts) {
456
+ const cp = opts.counterparty ?? BURN_SENTINEL;
457
+ const key = await counterpartyKey(opts.capabilityId, cp);
458
+ return this.sendSigned(
459
+ opts.signer,
460
+ "record_action",
461
+ [
462
+ addressArg(opts.capabilityId),
463
+ AbiEncode.i64(opts.amountSpent),
464
+ addressArg(key),
465
+ AbiEncode.i64(opts.currentTime)
466
+ ],
467
+ opts.gasLimit
468
+ );
469
+ }
470
+ /** Principal revokes a capability. Terminal. Signed by the principal. */
471
+ revokePermission(opts) {
472
+ return this.sendSigned(
473
+ opts.signer,
474
+ "revoke_permission",
475
+ [addressArg(opts.capabilityId), AbiEncode.i64(opts.currentTime)],
476
+ opts.gasLimit
477
+ );
478
+ }
479
+ /**
480
+ * Agent records a decision attestation (provable-after). Commits the
481
+ * decision's context_hash + verdict, bound to the capability's current policy
482
+ * version. Signed by the agent.
483
+ */
484
+ recordAttestation(opts) {
485
+ return this.sendSigned(
486
+ opts.signer,
487
+ "record_attestation",
488
+ [
489
+ addressArg(opts.attestationId),
490
+ addressArg(opts.capabilityId),
491
+ addressArg(opts.contextHash),
492
+ AbiEncode.i64(BigInt(opts.verdict)),
493
+ AbiEncode.i64(opts.currentTime)
494
+ ],
495
+ opts.gasLimit
496
+ );
497
+ }
498
+ };
499
+ function parseAddressReturn(returnValue) {
500
+ if (typeof returnValue === "string" && /^(0x)?[0-9a-fA-F]+$/.test(returnValue)) {
501
+ let hex = returnValue.replace(/^0x/, "");
502
+ if (hex.length > 64) return returnValue;
503
+ hex = hex.padStart(64, "0");
504
+ const bytes = new Uint8Array(32);
505
+ for (let i = 0; i < 32; i++) {
506
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
507
+ }
508
+ try {
509
+ return encodeAddress(bytes);
510
+ } catch {
511
+ return returnValue;
512
+ }
513
+ }
514
+ if (returnValue === "0" || returnValue === null) return "";
515
+ return String(returnValue);
516
+ }
517
+
518
+ // src/attestation.ts
519
+ function canonicalJson(value) {
520
+ const norm = (v) => {
521
+ if (Array.isArray(v)) return v.map(norm);
522
+ if (v && typeof v === "object") {
523
+ return Object.keys(v).sort().reduce((acc, k) => {
524
+ acc[k] = norm(v[k]);
525
+ return acc;
526
+ }, {});
527
+ }
528
+ return v;
529
+ };
530
+ return JSON.stringify(norm(value));
531
+ }
532
+ async function hashDecisionContext(context) {
533
+ const bytes = new TextEncoder().encode(canonicalJson(context));
534
+ return encodeAddress(await sha256(bytes));
535
+ }
536
+ async function deriveAttestationId(capabilityId, contextHash, seq) {
537
+ const preimage = concat(
538
+ decodeAddress(capabilityId),
539
+ decodeAddress(contextHash),
540
+ u64be(seq)
541
+ );
542
+ return encodeAddress(await sha256(preimage));
543
+ }
544
+ async function verifyDecisionContext(context, onChainContextHash) {
545
+ const recomputed = await hashDecisionContext(context);
546
+ return { ok: recomputed === onChainContextHash, recomputed };
547
+ }
548
+
549
+ // src/client.ts
550
+ function nowSecs() {
551
+ return BigInt(Math.floor(Date.now() / 1e3));
552
+ }
553
+ async function settle(read, done, timeoutMs = 12e3, intervalMs = 750) {
554
+ const start = Date.now();
555
+ let last = await read();
556
+ while (!done(last) && Date.now() - start < timeoutMs) {
557
+ await new Promise((r) => setTimeout(r, intervalMs));
558
+ last = await read();
559
+ }
560
+ return last;
561
+ }
562
+ var CinchorClient = class _CinchorClient {
563
+ registry;
564
+ constructor(registry) {
565
+ this.registry = registry;
566
+ }
567
+ /** Connect to a network + deployed accountability contract. */
568
+ static async connect(config) {
569
+ return new _CinchorClient(await CapabilityRegistry.connect(config));
570
+ }
571
+ // ── Capability lifecycle ─────────────────────────────────────────
572
+ /**
573
+ * Mint a cryptographically-scoped capability to an agent: capability, spend
574
+ * ceiling, validity window, revocable at will. Returns the derived capability
575
+ * id and the commit receipt.
576
+ */
577
+ async mintCapability(opts) {
578
+ const currentTime = opts.currentTime ?? nowSecs();
579
+ const validUntil = opts.validUntil ?? (opts.ttlSeconds !== void 0 ? currentTime + BigInt(opts.ttlSeconds) : (() => {
580
+ throw new Error("mintCapability requires either validUntil or ttlSeconds");
581
+ })());
582
+ const nonce = opts.nonce ?? Math.floor(Math.random() * 2 ** 48);
583
+ const capabilityId = await deriveCapabilityId(
584
+ opts.principal.address,
585
+ opts.agent,
586
+ nonce,
587
+ Number(currentTime)
588
+ );
589
+ const receipt = await this.registry.mintPermission({
590
+ signer: opts.principal,
591
+ capabilityId,
592
+ principal: opts.principal.address,
593
+ agent: opts.agent,
594
+ maxSpend: opts.maxSpend,
595
+ validUntil,
596
+ allowlistEnabled: opts.allowlist,
597
+ currentTime,
598
+ gasLimit: opts.gasLimit
599
+ });
600
+ await settle(
601
+ () => this.getCapability(capabilityId),
602
+ (c) => c.status === 1 /* Active */
603
+ );
604
+ return { capabilityId, receipt };
605
+ }
606
+ /** Revoke a capability. Terminal. Signed by the principal. */
607
+ async revoke(opts) {
608
+ const receipt = await this.registry.revokePermission({
609
+ signer: opts.principal,
610
+ capabilityId: opts.capability,
611
+ currentTime: opts.currentTime ?? nowSecs(),
612
+ gasLimit: opts.gasLimit
613
+ });
614
+ await settle(
615
+ () => this.getCapability(opts.capability),
616
+ (c) => c.status === 2 /* Revoked */
617
+ );
618
+ return receipt;
619
+ }
620
+ /** Update a capability's policy (limits) and bump its on-chain version. */
621
+ updatePolicy(opts) {
622
+ return this.registry.updatePolicy({
623
+ signer: opts.principal,
624
+ capabilityId: opts.capability,
625
+ newMaxSpend: opts.maxSpend,
626
+ newValidUntil: opts.validUntil,
627
+ currentTime: opts.currentTime ?? nowSecs(),
628
+ gasLimit: opts.gasLimit
629
+ });
630
+ }
631
+ /** Authorize a counterparty for a capability's allowlist. */
632
+ allowCounterparty(opts) {
633
+ return this.registry.addAllowedCounterparty({
634
+ signer: opts.principal,
635
+ capabilityId: opts.capability,
636
+ counterparty: opts.counterparty,
637
+ currentTime: opts.currentTime ?? nowSecs(),
638
+ gasLimit: opts.gasLimit
639
+ });
640
+ }
641
+ // ── The two verbs ────────────────────────────────────────────────
642
+ /**
643
+ * Authorize-or-refuse a consequential action. The substrate enforces the
644
+ * capability's invariants atomically: an out-of-scope action commits no state
645
+ * change. The verdict is read back from committed state (a record_action
646
+ * receipt does not carry the contract's return code).
647
+ *
648
+ * Verdict classification assumes serial, single-signer use of the capability
649
+ * (one in-flight action at a time) — the documented integration pattern.
650
+ */
651
+ async enforce(opts) {
652
+ const currentTime = opts.currentTime ?? nowSecs();
653
+ const before = await this.registry.getCapabilityState(opts.capability);
654
+ const receipt = await this.registry.recordAction({
655
+ signer: opts.agent,
656
+ capabilityId: opts.capability,
657
+ amountSpent: opts.amount,
658
+ counterparty: opts.counterparty,
659
+ currentTime,
660
+ gasLimit: opts.gasLimit
661
+ });
662
+ const after = await settle(
663
+ () => this.registry.getCapabilityState(opts.capability),
664
+ (a) => a.actionCount > before.actionCount,
665
+ 8e3
666
+ );
667
+ const code = await this.classify(before, after, opts.amount, currentTime, opts);
668
+ return { allowed: code === 1 /* Allowed */, code, reason: ENFORCEMENT_LABELS[code], receipt };
669
+ }
670
+ async classify(before, after, amount, t, opts) {
671
+ if (after.actionCount > before.actionCount && after.totalSpent === before.totalSpent + amount) {
672
+ return 1 /* Allowed */;
673
+ }
674
+ if (before.status === 0 /* NotFound */) return 0 /* NotFound */;
675
+ if (before.status === 2 /* Revoked */) return 2 /* Revoked */;
676
+ if (t > before.validUntil) return 3 /* Expired */;
677
+ if (before.totalSpent + amount > before.maxSpend) return 4 /* OverBudget */;
678
+ if (await this.registry.getAllowlistEnabled(opts.capability)) {
679
+ const cp = opts.counterparty ?? BURN_SENTINEL;
680
+ if (!await this.registry.isCounterpartyAllowed(opts.capability, cp)) {
681
+ return 5 /* OutOfAllowlist */;
682
+ }
683
+ }
684
+ return 0 /* NotFound */;
685
+ }
686
+ /**
687
+ * Commit a tamper-evident attestation of a decision. The full context is
688
+ * hashed canonically; the hash + verdict are committed on-chain, bound to the
689
+ * capability's current policy version. An auditor later re-hashes the
690
+ * off-chain artifact and confirms it matches (see {@link verifyAttestation}).
691
+ */
692
+ async attest(opts) {
693
+ const currentTime = opts.currentTime ?? nowSecs();
694
+ const verdict = opts.verdict ?? 1 /* InPolicy */;
695
+ const seq = opts.seq ?? 0;
696
+ const contextHash = await hashDecisionContext(opts.context);
697
+ const attestationId = await deriveAttestationId(opts.capability, contextHash, seq);
698
+ const receipt = await this.registry.recordAttestation({
699
+ signer: opts.agent,
700
+ attestationId,
701
+ capabilityId: opts.capability,
702
+ contextHash,
703
+ verdict,
704
+ currentTime,
705
+ gasLimit: opts.gasLimit
706
+ });
707
+ await settle(() => this.registry.getAttestation(attestationId), (a) => a.exists);
708
+ return { attestationId, contextHash, verdict, receipt };
709
+ }
710
+ // ── Audit (reads; no signer required) ────────────────────────────
711
+ /** Read the full on-chain state of a capability. */
712
+ getCapability(capabilityId) {
713
+ return this.registry.getCapabilityState(capabilityId);
714
+ }
715
+ /** Read a decision attestation's on-chain record. */
716
+ getAttestation(attestationId) {
717
+ return this.registry.getAttestation(attestationId);
718
+ }
719
+ /**
720
+ * Tamper check: re-hash a decision context and confirm it matches the
721
+ * attestation's on-chain commitment. Returns whether it matches, the
722
+ * recomputed hash, and the on-chain record.
723
+ */
724
+ async verifyAttestation(context, attestationId) {
725
+ const onChain = await this.registry.getAttestation(attestationId);
726
+ const { ok, recomputed } = await verifyDecisionContext(context, onChain.contextHash);
727
+ return { ok: ok && onChain.exists, recomputed, onChain };
728
+ }
729
+ };
730
+ export {
731
+ BURN_SENTINEL,
732
+ CAPABILITY_STATUS_LABELS,
733
+ CapabilityRegistry,
734
+ CapabilityStatus,
735
+ CinchorClient,
736
+ DEFAULT_GAS_LIMIT,
737
+ DEFAULT_GAS_PRICE,
738
+ ENFORCEMENT_LABELS,
739
+ EnforcementCode,
740
+ IGNIS_LOCAL,
741
+ Verdict,
742
+ addressArg,
743
+ canonicalJson,
744
+ counterpartyKey,
745
+ decodeAddress,
746
+ deriveAttestationId,
747
+ deriveCapabilityId,
748
+ encodeAddress,
749
+ exportPrefixFor,
750
+ hashDecisionContext,
751
+ nowSecs,
752
+ parseAddressReturn,
753
+ verifyDecisionContext
754
+ };
755
+ //# sourceMappingURL=index.js.map