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