@orangecheck/agent-core 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 +21 -0
- package/README.md +127 -0
- package/dist/canonical.d.mts +27 -0
- package/dist/canonical.d.ts +27 -0
- package/dist/canonical.js +263 -0
- package/dist/canonical.js.map +1 -0
- package/dist/canonical.mjs +237 -0
- package/dist/canonical.mjs.map +1 -0
- package/dist/index.d.mts +38 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +671 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +633 -0
- package/dist/index.mjs.map +1 -0
- package/dist/scope.d.mts +28 -0
- package/dist/scope.d.ts +28 -0
- package/dist/scope.js +249 -0
- package/dist/scope.js.map +1 -0
- package/dist/scope.mjs +241 -0
- package/dist/scope.mjs.map +1 -0
- package/dist/types.d.mts +127 -0
- package/dist/types.d.ts +127 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/types.mjs +6 -0
- package/dist/types.mjs.map +1 -0
- package/package.json +75 -0
- package/src/canonical.test.ts +95 -0
- package/src/canonical.ts +155 -0
- package/src/index.ts +45 -0
- package/src/scope.test.ts +162 -0
- package/src/scope.ts +330 -0
- package/src/test-vectors.test.ts +199 -0
- package/src/types.ts +189 -0
- package/src/verify.ts +460 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var sha256 = require('@noble/hashes/sha256');
|
|
4
|
+
var canonical = require('@orangecheck/stamp-core/canonical');
|
|
5
|
+
|
|
6
|
+
// src/types.ts
|
|
7
|
+
var ENVELOPE_VERSION = 1;
|
|
8
|
+
|
|
9
|
+
// src/scope.ts
|
|
10
|
+
var REGISTERED_SCOPES = {
|
|
11
|
+
"lock:seal": { keys: ["recipient", "mime", "max_bytes"] },
|
|
12
|
+
"lock:chat": { keys: ["recipient", "max_bytes_per_msg", "max_msgs"] },
|
|
13
|
+
"stamp:sign": { keys: ["mime", "max_bytes", "content_hash_prefix"] },
|
|
14
|
+
"vote:cast": { keys: ["poll_id", "choice"] },
|
|
15
|
+
"nostr:publish": { keys: ["kind", "relay", "max_bytes"] },
|
|
16
|
+
"http:request": { keys: ["origin", "method", "max_rps", "max_bytes_out"] },
|
|
17
|
+
"ln:send": { keys: ["max_sats", "node", "max_fee_sats"] },
|
|
18
|
+
"mcp:invoke": { keys: ["server", "tool", "max_invocations"] }
|
|
19
|
+
};
|
|
20
|
+
var NUMERIC_KEYS = /* @__PURE__ */ new Set([
|
|
21
|
+
"max_bytes",
|
|
22
|
+
"max_bytes_per_msg",
|
|
23
|
+
"max_msgs",
|
|
24
|
+
"max_bytes_out",
|
|
25
|
+
"max_rps",
|
|
26
|
+
"max_sats",
|
|
27
|
+
"max_fee_sats",
|
|
28
|
+
"max_invocations",
|
|
29
|
+
"kind"
|
|
30
|
+
]);
|
|
31
|
+
var IDENT_RE = /^[a-z][a-z0-9_]*$/;
|
|
32
|
+
var BARE_TOKEN_RE = /^[A-Za-z0-9_.:/@+\-]+$/;
|
|
33
|
+
var ScopeParseError = class extends Error {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "ScopeParseError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function parseScope(input) {
|
|
40
|
+
if (typeof input !== "string" || input.length === 0) {
|
|
41
|
+
throw new ScopeParseError("scope must be a non-empty string");
|
|
42
|
+
}
|
|
43
|
+
if (/\s/.test(input)) {
|
|
44
|
+
throw new ScopeParseError(`scope may not contain whitespace: ${JSON.stringify(input)}`);
|
|
45
|
+
}
|
|
46
|
+
const colonIdx = input.indexOf(":");
|
|
47
|
+
if (colonIdx < 0) throw new ScopeParseError('scope missing "product:verb" separator');
|
|
48
|
+
const product = input.slice(0, colonIdx);
|
|
49
|
+
if (!IDENT_RE.test(product)) throw new ScopeParseError(`invalid product: ${product}`);
|
|
50
|
+
const rest = input.slice(colonIdx + 1);
|
|
51
|
+
const parenIdx = rest.indexOf("(");
|
|
52
|
+
let verb;
|
|
53
|
+
let constraintText = "";
|
|
54
|
+
if (parenIdx < 0) {
|
|
55
|
+
verb = rest;
|
|
56
|
+
} else {
|
|
57
|
+
verb = rest.slice(0, parenIdx);
|
|
58
|
+
if (!rest.endsWith(")")) throw new ScopeParseError('scope constraint list must end with ")"');
|
|
59
|
+
constraintText = rest.slice(parenIdx + 1, -1);
|
|
60
|
+
}
|
|
61
|
+
if (!IDENT_RE.test(verb)) throw new ScopeParseError(`invalid verb: ${verb}`);
|
|
62
|
+
const constraints = [];
|
|
63
|
+
if (constraintText.length > 0) {
|
|
64
|
+
for (const piece of splitTopLevelCommas(constraintText)) {
|
|
65
|
+
constraints.push(parseConstraint(piece));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const seen = /* @__PURE__ */ new Set();
|
|
69
|
+
for (const c of constraints) {
|
|
70
|
+
if (seen.has(c.key)) throw new ScopeParseError(`duplicate constraint key: ${c.key}`);
|
|
71
|
+
seen.add(c.key);
|
|
72
|
+
}
|
|
73
|
+
return { product, verb, constraints };
|
|
74
|
+
}
|
|
75
|
+
function splitTopLevelCommas(text) {
|
|
76
|
+
const out = [];
|
|
77
|
+
let depth = 0;
|
|
78
|
+
let inQuotes = false;
|
|
79
|
+
let start = 0;
|
|
80
|
+
for (let i = 0; i < text.length; i++) {
|
|
81
|
+
const ch = text[i];
|
|
82
|
+
if (inQuotes) {
|
|
83
|
+
if (ch === "\\" && i + 1 < text.length) {
|
|
84
|
+
i++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (ch === '"') inQuotes = false;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (ch === '"') {
|
|
91
|
+
inQuotes = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (ch === "(") depth++;
|
|
95
|
+
else if (ch === ")") depth--;
|
|
96
|
+
else if (ch === "," && depth === 0) {
|
|
97
|
+
out.push(text.slice(start, i));
|
|
98
|
+
start = i + 1;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
out.push(text.slice(start));
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function parseConstraint(piece) {
|
|
105
|
+
if (piece.length === 0) throw new ScopeParseError("empty constraint");
|
|
106
|
+
const OPS = [">=", "<=", "!=", "=", ">", "<"];
|
|
107
|
+
const wildcardMatch = /^([a-z][a-z0-9_]*)(?:=\*|\*)$/.exec(piece);
|
|
108
|
+
if (wildcardMatch) {
|
|
109
|
+
return { key: wildcardMatch[1], op: "*", value: void 0, quoted: false };
|
|
110
|
+
}
|
|
111
|
+
for (const op of OPS) {
|
|
112
|
+
const idx = piece.indexOf(op);
|
|
113
|
+
if (idx <= 0) continue;
|
|
114
|
+
const key = piece.slice(0, idx);
|
|
115
|
+
if (!IDENT_RE.test(key)) continue;
|
|
116
|
+
const raw = piece.slice(idx + op.length);
|
|
117
|
+
const { value, quoted } = parseValue(raw);
|
|
118
|
+
return { key, op, value, quoted };
|
|
119
|
+
}
|
|
120
|
+
throw new ScopeParseError(`constraint missing operator: ${piece}`);
|
|
121
|
+
}
|
|
122
|
+
function parseValue(raw) {
|
|
123
|
+
if (raw.length === 0) throw new ScopeParseError("constraint value is empty");
|
|
124
|
+
if (raw.startsWith('"')) {
|
|
125
|
+
if (!raw.endsWith('"') || raw.length < 2) {
|
|
126
|
+
throw new ScopeParseError(`unterminated quoted value: ${raw}`);
|
|
127
|
+
}
|
|
128
|
+
let v = "";
|
|
129
|
+
for (let i = 1; i < raw.length - 1; i++) {
|
|
130
|
+
const ch = raw[i];
|
|
131
|
+
if (ch === "\\" && i + 1 < raw.length - 1) {
|
|
132
|
+
const next = raw[++i];
|
|
133
|
+
v += next;
|
|
134
|
+
} else if (ch === '"') {
|
|
135
|
+
throw new ScopeParseError(`unescaped quote in value: ${raw}`);
|
|
136
|
+
} else {
|
|
137
|
+
v += ch;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { value: v, quoted: true };
|
|
141
|
+
}
|
|
142
|
+
if (!BARE_TOKEN_RE.test(raw)) {
|
|
143
|
+
throw new ScopeParseError(`invalid bare-token value: ${JSON.stringify(raw)}`);
|
|
144
|
+
}
|
|
145
|
+
return { value: raw, quoted: false };
|
|
146
|
+
}
|
|
147
|
+
function canonicalizeScope(scope) {
|
|
148
|
+
const sorted = [...scope.constraints].sort((a, b) => a.key < b.key ? -1 : a.key > b.key ? 1 : 0);
|
|
149
|
+
const parts = sorted.map(serializeConstraint);
|
|
150
|
+
const inner = parts.join(",");
|
|
151
|
+
return `${scope.product}:${scope.verb}${parts.length === 0 ? "" : `(${inner})`}`;
|
|
152
|
+
}
|
|
153
|
+
function canonicalizeScopeString(input) {
|
|
154
|
+
return canonicalizeScope(parseScope(input));
|
|
155
|
+
}
|
|
156
|
+
function serializeConstraint(c) {
|
|
157
|
+
if (c.op === "*") return `${c.key}=*`;
|
|
158
|
+
const v = c.quoted ? quoteValue(c.value ?? "") : c.value ?? "";
|
|
159
|
+
return `${c.key}${c.op}${v}`;
|
|
160
|
+
}
|
|
161
|
+
function quoteValue(v) {
|
|
162
|
+
let out = '"';
|
|
163
|
+
for (const ch of v) {
|
|
164
|
+
if (ch === '"' || ch === "\\") out += "\\" + ch;
|
|
165
|
+
else out += ch;
|
|
166
|
+
}
|
|
167
|
+
out += '"';
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
function validateScope(scope, options = {}) {
|
|
171
|
+
const mode = options.mode ?? "strict";
|
|
172
|
+
const reg = REGISTERED_SCOPES[`${scope.product}:${scope.verb}`];
|
|
173
|
+
if (!reg) {
|
|
174
|
+
if (mode === "strict") {
|
|
175
|
+
throw new ScopeParseError(`unregistered scope: ${scope.product}:${scope.verb}`);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const registered = new Set(reg.keys);
|
|
180
|
+
for (const c of scope.constraints) {
|
|
181
|
+
if (!registered.has(c.key)) {
|
|
182
|
+
if (mode === "strict") {
|
|
183
|
+
throw new ScopeParseError(
|
|
184
|
+
`unregistered constraint key for ${scope.product}:${scope.verb}: ${c.key}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function isSubScope(exercised, granted) {
|
|
191
|
+
if (exercised.product !== granted.product) return false;
|
|
192
|
+
if (exercised.verb !== granted.verb) return false;
|
|
193
|
+
const exIndex = /* @__PURE__ */ new Map();
|
|
194
|
+
for (const c of exercised.constraints) exIndex.set(c.key, c);
|
|
195
|
+
for (const g of granted.constraints) {
|
|
196
|
+
const ex = exIndex.get(g.key);
|
|
197
|
+
if (g.op === "*") continue;
|
|
198
|
+
if (g.op === "=") {
|
|
199
|
+
if (!ex) return false;
|
|
200
|
+
if (ex.op !== "=" || ex.value !== g.value) return false;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (g.op === "!=") {
|
|
204
|
+
if (!ex) return false;
|
|
205
|
+
if (ex.op === "=" && ex.value !== g.value) continue;
|
|
206
|
+
if (ex.op === "!=" && ex.value === g.value) continue;
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (g.op === "<" || g.op === "<=" || g.op === ">" || g.op === ">=") {
|
|
210
|
+
if (!ex) return false;
|
|
211
|
+
if (!NUMERIC_KEYS.has(g.key)) return false;
|
|
212
|
+
if (ex.op === "*") return false;
|
|
213
|
+
if (ex.value === void 0 || g.value === void 0) return false;
|
|
214
|
+
if (!rangeSubset(ex, g)) return false;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
function rangeSubset(ex, g) {
|
|
221
|
+
const exRange = opToRange(ex);
|
|
222
|
+
const gRange = opToRange(g);
|
|
223
|
+
if (!exRange || !gRange) return false;
|
|
224
|
+
return gRange.lo <= exRange.lo && exRange.hi <= gRange.hi;
|
|
225
|
+
}
|
|
226
|
+
function opToRange(c) {
|
|
227
|
+
if (c.value === void 0) return null;
|
|
228
|
+
const n = Number(c.value);
|
|
229
|
+
if (!Number.isFinite(n)) return null;
|
|
230
|
+
switch (c.op) {
|
|
231
|
+
case "=":
|
|
232
|
+
return { lo: n, hi: n };
|
|
233
|
+
case "<":
|
|
234
|
+
return { lo: -Infinity, hi: n - 1 };
|
|
235
|
+
// integers only
|
|
236
|
+
case "<=":
|
|
237
|
+
return { lo: -Infinity, hi: n };
|
|
238
|
+
case ">":
|
|
239
|
+
return { lo: n + 1, hi: Infinity };
|
|
240
|
+
case ">=":
|
|
241
|
+
return { lo: n, hi: Infinity };
|
|
242
|
+
default:
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/canonical.ts
|
|
248
|
+
function canonicalizeScopes(scopes) {
|
|
249
|
+
const canonical = scopes.map((s) => canonicalizeScope(parseScope(s)));
|
|
250
|
+
return [...canonical].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
251
|
+
}
|
|
252
|
+
function parseAndCanonicalizeScopes(scopes) {
|
|
253
|
+
const parsed = scopes.map(parseScope);
|
|
254
|
+
const canonicalStrings = parsed.map(canonicalizeScope);
|
|
255
|
+
const indexed = canonicalStrings.map((s, i) => ({ s, p: parsed[i] }));
|
|
256
|
+
indexed.sort((a, b) => a.s < b.s ? -1 : a.s > b.s ? 1 : 0);
|
|
257
|
+
return {
|
|
258
|
+
canonical: indexed.map((x) => x.s),
|
|
259
|
+
parsed: indexed.map((x) => x.p)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function delegationCanonicalMessage(input) {
|
|
263
|
+
const scopeField = input.scopes.join(",");
|
|
264
|
+
return [
|
|
265
|
+
"oc-agent:delegation:v1",
|
|
266
|
+
`principal: ${input.principal}`,
|
|
267
|
+
`agent: ${input.agent}`,
|
|
268
|
+
`scopes: ${scopeField}`,
|
|
269
|
+
`bond_sats: ${input.bond_sats}`,
|
|
270
|
+
`bond_attestation: ${input.bond_attestation}`,
|
|
271
|
+
`issued_at: ${input.issued_at}`,
|
|
272
|
+
`expires_at: ${input.expires_at}`,
|
|
273
|
+
`nonce: ${input.nonce}`
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
function actionCanonicalMessage(input) {
|
|
277
|
+
return [
|
|
278
|
+
"oc-agent:action:v1",
|
|
279
|
+
`address: ${input.address}`,
|
|
280
|
+
`content_hash: ${input.content_hash}`,
|
|
281
|
+
`content_length: ${input.content_length}`,
|
|
282
|
+
`content_mime: ${input.content_mime}`,
|
|
283
|
+
`signed_at: ${input.signed_at}`,
|
|
284
|
+
`delegation_id: ${input.delegation_id}`,
|
|
285
|
+
`scope_exercised: ${input.scope_exercised}`
|
|
286
|
+
].join("\n");
|
|
287
|
+
}
|
|
288
|
+
function revocationCanonicalMessage(input) {
|
|
289
|
+
return [
|
|
290
|
+
"oc-agent:revocation:v1",
|
|
291
|
+
`address: ${input.address}`,
|
|
292
|
+
`delegation_id: ${input.delegation_id}`,
|
|
293
|
+
`reason: ${input.reason}`,
|
|
294
|
+
`signed_at: ${input.signed_at}`
|
|
295
|
+
].join("\n");
|
|
296
|
+
}
|
|
297
|
+
function delegationCanonicalBytes(input) {
|
|
298
|
+
return new TextEncoder().encode(delegationCanonicalMessage(input));
|
|
299
|
+
}
|
|
300
|
+
function actionCanonicalBytes(input) {
|
|
301
|
+
return new TextEncoder().encode(actionCanonicalMessage(input));
|
|
302
|
+
}
|
|
303
|
+
function revocationCanonicalBytes(input) {
|
|
304
|
+
return new TextEncoder().encode(revocationCanonicalMessage(input));
|
|
305
|
+
}
|
|
306
|
+
function computeDelegationId(input) {
|
|
307
|
+
return canonical.hexEncode(sha256.sha256(delegationCanonicalBytes(input)));
|
|
308
|
+
}
|
|
309
|
+
function computeActionId(input) {
|
|
310
|
+
return canonical.hexEncode(sha256.sha256(actionCanonicalBytes(input)));
|
|
311
|
+
}
|
|
312
|
+
function computeRevocationId(input) {
|
|
313
|
+
return canonical.hexEncode(sha256.sha256(revocationCanonicalBytes(input)));
|
|
314
|
+
}
|
|
315
|
+
function canonicalizeDelegation(env) {
|
|
316
|
+
return canonical.canonicalize(env);
|
|
317
|
+
}
|
|
318
|
+
function canonicalizeAction(env) {
|
|
319
|
+
return canonical.canonicalize(env);
|
|
320
|
+
}
|
|
321
|
+
function canonicalizeRevocation(env) {
|
|
322
|
+
return canonical.canonicalize(env);
|
|
323
|
+
}
|
|
324
|
+
function canonicalDelegationBytes(env) {
|
|
325
|
+
return new TextEncoder().encode(canonicalizeDelegation(env) + "\n");
|
|
326
|
+
}
|
|
327
|
+
function canonicalActionBytes(env) {
|
|
328
|
+
return new TextEncoder().encode(canonicalizeAction(env) + "\n");
|
|
329
|
+
}
|
|
330
|
+
function canonicalRevocationBytes(env) {
|
|
331
|
+
return new TextEncoder().encode(canonicalizeRevocation(env) + "\n");
|
|
332
|
+
}
|
|
333
|
+
function sha256Hex(bytes) {
|
|
334
|
+
return canonical.hexEncode(sha256.sha256(bytes));
|
|
335
|
+
}
|
|
336
|
+
var AgentError = class extends Error {
|
|
337
|
+
constructor(code, message) {
|
|
338
|
+
super(message);
|
|
339
|
+
this.code = code;
|
|
340
|
+
this.name = "AgentError";
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
async function verifyDelegation(input) {
|
|
344
|
+
const env = input.envelope;
|
|
345
|
+
if (env.v !== ENVELOPE_VERSION) {
|
|
346
|
+
return err("E_UNSUPPORTED_VERSION", `delegation version ${env.v} not supported`);
|
|
347
|
+
}
|
|
348
|
+
const shape = checkDelegationShape(env);
|
|
349
|
+
if (shape) return shape;
|
|
350
|
+
let canonicalScopes;
|
|
351
|
+
try {
|
|
352
|
+
for (const s of env.scopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
|
|
353
|
+
canonicalScopes = canonicalizeScopes(env.scopes);
|
|
354
|
+
} catch (e) {
|
|
355
|
+
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
356
|
+
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
357
|
+
}
|
|
358
|
+
for (let i = 0; i < canonicalScopes.length; i++) {
|
|
359
|
+
if (env.scopes[i] !== canonicalScopes[i]) {
|
|
360
|
+
return err(
|
|
361
|
+
"E_BAD_SCOPE_GRAMMAR",
|
|
362
|
+
`scope at index ${i} not in canonical form; expected ${canonicalScopes[i]} got ${env.scopes[i]}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const bondSats = env.bond?.sats ?? 0;
|
|
367
|
+
const bondAttestation = env.bond?.attestation_id ?? "none";
|
|
368
|
+
const canonInput = {
|
|
369
|
+
principal: env.principal.address,
|
|
370
|
+
agent: env.agent.address,
|
|
371
|
+
scopes: canonicalScopes,
|
|
372
|
+
bond_sats: bondSats,
|
|
373
|
+
bond_attestation: bondAttestation,
|
|
374
|
+
issued_at: env.issued_at,
|
|
375
|
+
expires_at: env.expires_at,
|
|
376
|
+
nonce: env.nonce
|
|
377
|
+
};
|
|
378
|
+
const reconstructedMessage = delegationCanonicalMessage(canonInput);
|
|
379
|
+
const reconstructedId = canonical.hexEncode(sha256.sha256(delegationCanonicalBytes(canonInput)));
|
|
380
|
+
if (reconstructedId !== env.id) {
|
|
381
|
+
return err(
|
|
382
|
+
"E_BAD_ID",
|
|
383
|
+
`reconstructed id (${reconstructedId}) does not match envelope.id (${env.id})`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (!input.skipSignatureVerification) {
|
|
387
|
+
if (!input.verifyBip322) return err("E_BAD_SIG", "no BIP-322 verifier supplied");
|
|
388
|
+
const ok = await input.verifyBip322(env.id, env.sig.value, env.principal.address);
|
|
389
|
+
if (!ok) return err("E_BAD_SIG", "BIP-322 signature did not verify");
|
|
390
|
+
}
|
|
391
|
+
if (!input.skipTemporalCheck) {
|
|
392
|
+
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
393
|
+
const issued = new Date(env.issued_at);
|
|
394
|
+
const expires = new Date(env.expires_at);
|
|
395
|
+
if (expires <= issued) return err("E_MALFORMED", "expires_at <= issued_at");
|
|
396
|
+
if (now < issued) return err("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
|
|
397
|
+
if (now >= expires) return err("E_EXPIRED", `delegation expired at ${env.expires_at}`);
|
|
398
|
+
}
|
|
399
|
+
return {
|
|
400
|
+
ok: true,
|
|
401
|
+
envelope: env,
|
|
402
|
+
canonicalMessage: reconstructedMessage,
|
|
403
|
+
id: env.id
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
async function verifyAction(input) {
|
|
407
|
+
const a = input.action;
|
|
408
|
+
const d = input.delegation;
|
|
409
|
+
const dr = await verifyDelegation({
|
|
410
|
+
envelope: d,
|
|
411
|
+
verifyBip322: input.verifyBip322,
|
|
412
|
+
skipSignatureVerification: input.skipSignatureVerification,
|
|
413
|
+
scopeMode: input.scopeMode,
|
|
414
|
+
skipTemporalCheck: true
|
|
415
|
+
// action window check dominates
|
|
416
|
+
});
|
|
417
|
+
if (!dr.ok) return dr;
|
|
418
|
+
if (a.v !== ENVELOPE_VERSION) {
|
|
419
|
+
return err("E_UNSUPPORTED_VERSION", `action version ${a.v} not supported`);
|
|
420
|
+
}
|
|
421
|
+
const shape = checkActionShape(a);
|
|
422
|
+
if (shape) return shape;
|
|
423
|
+
const canonInput = {
|
|
424
|
+
address: a.signer.address,
|
|
425
|
+
content_hash: a.content.hash,
|
|
426
|
+
content_length: a.content.length,
|
|
427
|
+
content_mime: a.content.mime,
|
|
428
|
+
signed_at: a.signed_at,
|
|
429
|
+
delegation_id: a.delegation_id,
|
|
430
|
+
scope_exercised: a.scope_exercised
|
|
431
|
+
};
|
|
432
|
+
const reconstructedMessage = actionCanonicalMessage(canonInput);
|
|
433
|
+
const reconstructedId = canonical.hexEncode(sha256.sha256(actionCanonicalBytes(canonInput)));
|
|
434
|
+
if (reconstructedId !== a.id) {
|
|
435
|
+
return err("E_BAD_ID", `reconstructed id (${reconstructedId}) does not match action.id (${a.id})`);
|
|
436
|
+
}
|
|
437
|
+
if (!input.skipSignatureVerification) {
|
|
438
|
+
if (!input.verifyBip322) return err("E_BAD_SIG", "no BIP-322 verifier supplied");
|
|
439
|
+
const ok = await input.verifyBip322(a.id, a.sig.value, a.signer.address);
|
|
440
|
+
if (!ok) return err("E_BAD_ACTION_STAMP", "action BIP-322 signature did not verify");
|
|
441
|
+
}
|
|
442
|
+
if (a.delegation_id !== d.id) {
|
|
443
|
+
return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) != delegation.id (${d.id})`);
|
|
444
|
+
}
|
|
445
|
+
if (a.signer.address !== d.agent.address) {
|
|
446
|
+
return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) != delegation.agent (${d.agent.address})`);
|
|
447
|
+
}
|
|
448
|
+
const issued = new Date(d.issued_at).getTime();
|
|
449
|
+
const expires = new Date(d.expires_at).getTime();
|
|
450
|
+
const signed = new Date(a.signed_at).getTime();
|
|
451
|
+
if (Number.isNaN(issued) || Number.isNaN(expires) || Number.isNaN(signed)) {
|
|
452
|
+
return err("E_MALFORMED", "unparseable ISO 8601 timestamp");
|
|
453
|
+
}
|
|
454
|
+
if (signed < issued || signed >= expires) {
|
|
455
|
+
return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside delegation window [${d.issued_at}, ${d.expires_at})`);
|
|
456
|
+
}
|
|
457
|
+
let exercised, accepted;
|
|
458
|
+
try {
|
|
459
|
+
exercised = canonicalizeScope(parseScope(a.scope_exercised));
|
|
460
|
+
const granted = d.scopes.map((s) => parseScope(s));
|
|
461
|
+
const exercisedParsed = parseScope(a.scope_exercised);
|
|
462
|
+
validateScope(exercisedParsed, { mode: input.scopeMode ?? "strict" });
|
|
463
|
+
accepted = granted.some((g) => isSubScope(exercisedParsed, g));
|
|
464
|
+
} catch (e) {
|
|
465
|
+
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
466
|
+
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
467
|
+
}
|
|
468
|
+
if (!accepted) return err("E_SCOPE_DENIED", `scope_exercised (${exercised}) not a sub-scope of any granted scope`);
|
|
469
|
+
if (input.revocations && input.revocations.length > 0) {
|
|
470
|
+
for (const rev of input.revocations) {
|
|
471
|
+
if (rev.delegation_id !== d.id) continue;
|
|
472
|
+
const rr = await verifyRevocation({
|
|
473
|
+
envelope: rev,
|
|
474
|
+
delegation: d,
|
|
475
|
+
verifyBip322: input.verifyBip322,
|
|
476
|
+
skipSignatureVerification: input.skipSignatureVerification
|
|
477
|
+
});
|
|
478
|
+
if (!rr.ok) continue;
|
|
479
|
+
const effective = effectiveRevocationTime(rev, input.resolveAnchorBlockHeight);
|
|
480
|
+
const actionTime = actionEffectiveTime(a, input.resolveAnchorBlockHeight);
|
|
481
|
+
if (compareTimes(effective, actionTime) <= 0) {
|
|
482
|
+
return err("E_REVOKED", `delegation was revoked by ${rev.id} before action was signed`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (input.content) {
|
|
487
|
+
const actualHash = "sha256:" + canonical.hexEncode(sha256.sha256(input.content));
|
|
488
|
+
if (actualHash !== a.content.hash) {
|
|
489
|
+
return err("E_BAD_ACTION_STAMP", `content hash (${actualHash}) != action.content.hash (${a.content.hash})`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
let anchor;
|
|
493
|
+
if (a.ots === null) {
|
|
494
|
+
anchor = { status: "none" };
|
|
495
|
+
} else if (a.ots.status === "pending") {
|
|
496
|
+
anchor = { status: "pending" };
|
|
497
|
+
} else {
|
|
498
|
+
const h = a.ots.block_height;
|
|
499
|
+
const hash = a.ots.block_hash;
|
|
500
|
+
if (h === null || hash === null) {
|
|
501
|
+
return err("E_MALFORMED", "confirmed OTS proof missing block_height or block_hash");
|
|
502
|
+
}
|
|
503
|
+
let verified = false;
|
|
504
|
+
if (input.verifyOtsAnchor) {
|
|
505
|
+
try {
|
|
506
|
+
verified = await input.verifyOtsAnchor(a.ots.proof, h, hash);
|
|
507
|
+
} catch (e) {
|
|
508
|
+
return err("E_MALFORMED", `anchor verifier threw: ${e.message}`);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
anchor = { status: "confirmed", blockHeight: h, blockHash: hash, verified };
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
ok: true,
|
|
515
|
+
envelope: a,
|
|
516
|
+
canonicalMessage: reconstructedMessage,
|
|
517
|
+
id: a.id,
|
|
518
|
+
delegation: d,
|
|
519
|
+
scopeExercised: exercised,
|
|
520
|
+
anchor
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
async function verifyRevocation(input) {
|
|
524
|
+
const env = input.envelope;
|
|
525
|
+
const d = input.delegation;
|
|
526
|
+
if (env.v !== ENVELOPE_VERSION) {
|
|
527
|
+
return err("E_UNSUPPORTED_VERSION", `revocation version ${env.v} not supported`);
|
|
528
|
+
}
|
|
529
|
+
const shape = checkRevocationShape(env);
|
|
530
|
+
if (shape) return shape;
|
|
531
|
+
if (env.delegation_id !== d.id) {
|
|
532
|
+
return err("E_DELEGATION_MISMATCH", `revocation.delegation_id (${env.delegation_id}) != delegation.id (${d.id})`);
|
|
533
|
+
}
|
|
534
|
+
const holders = d.revocation?.holders ?? ["principal"];
|
|
535
|
+
const holderAddrs = /* @__PURE__ */ new Set();
|
|
536
|
+
if (holders.includes("principal")) holderAddrs.add(d.principal.address);
|
|
537
|
+
if (holders.includes("agent")) holderAddrs.add(d.agent.address);
|
|
538
|
+
if (!holderAddrs.has(env.signer.address)) {
|
|
539
|
+
return err("E_REVOKER_UNAUTHORIZED", `revocation signer ${env.signer.address} not in delegation holders`);
|
|
540
|
+
}
|
|
541
|
+
const canonInput = {
|
|
542
|
+
address: env.signer.address,
|
|
543
|
+
delegation_id: env.delegation_id,
|
|
544
|
+
reason: env.reason,
|
|
545
|
+
signed_at: env.signed_at
|
|
546
|
+
};
|
|
547
|
+
const reconstructedMessage = revocationCanonicalMessage(canonInput);
|
|
548
|
+
const reconstructedId = canonical.hexEncode(sha256.sha256(revocationCanonicalBytes(canonInput)));
|
|
549
|
+
if (reconstructedId !== env.id) {
|
|
550
|
+
return err("E_BAD_ID", `reconstructed id (${reconstructedId}) does not match revocation.id (${env.id})`);
|
|
551
|
+
}
|
|
552
|
+
if (!input.skipSignatureVerification) {
|
|
553
|
+
if (!input.verifyBip322) return err("E_BAD_SIG", "no BIP-322 verifier supplied");
|
|
554
|
+
const ok = await input.verifyBip322(env.id, env.sig.value, env.signer.address);
|
|
555
|
+
if (!ok) return err("E_BAD_SIG", "revocation BIP-322 signature did not verify");
|
|
556
|
+
}
|
|
557
|
+
return { ok: true, envelope: env, canonicalMessage: reconstructedMessage, id: env.id };
|
|
558
|
+
}
|
|
559
|
+
function checkDelegationShape(env) {
|
|
560
|
+
if (env.kind !== "agent-delegation") return err("E_MALFORMED", 'kind must be "agent-delegation"');
|
|
561
|
+
if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
|
|
562
|
+
if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
|
|
563
|
+
if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
|
|
564
|
+
if (!Array.isArray(env.scopes) || env.scopes.length === 0) return err("E_MALFORMED", "scopes must be non-empty array");
|
|
565
|
+
if (env.bond !== null) {
|
|
566
|
+
if (!Number.isInteger(env.bond.sats) || env.bond.sats < 0) return err("E_MALFORMED", "bond.sats must be non-negative integer");
|
|
567
|
+
if (!isHex64(env.bond.attestation_id)) return err("E_MALFORMED", "bond.attestation_id must be 64-hex");
|
|
568
|
+
}
|
|
569
|
+
if (!isIsoUtc(env.issued_at)) return err("E_MALFORMED", "issued_at must be ISO 8601 UTC");
|
|
570
|
+
if (!isIsoUtc(env.expires_at)) return err("E_MALFORMED", "expires_at must be ISO 8601 UTC");
|
|
571
|
+
if (!/^[0-9a-f]{32}$/.test(env.nonce)) return err("E_MALFORMED", "nonce must be 32 lowercase hex chars");
|
|
572
|
+
if (env.sig?.alg !== "bip322" || typeof env.sig.value !== "string") return err("E_MALFORMED", "sig invalid");
|
|
573
|
+
if (env.sig.pubkey !== env.principal.address) return err("E_MALFORMED", "sig.pubkey must equal principal.address");
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
function checkActionShape(a) {
|
|
577
|
+
if (a.kind !== "agent-action") return err("E_MALFORMED", 'kind must be "agent-action"');
|
|
578
|
+
if (!isHex64(a.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
|
|
579
|
+
if (!a.content || typeof a.content.hash !== "string" || !a.content.hash.startsWith("sha256:")) {
|
|
580
|
+
return err("E_MALFORMED", 'content.hash must start with "sha256:"');
|
|
581
|
+
}
|
|
582
|
+
if (!Number.isInteger(a.content.length) || a.content.length < 0) return err("E_MALFORMED", "content.length invalid");
|
|
583
|
+
if (!a.signer?.address || a.signer.alg !== "bip322") return err("E_MALFORMED", "signer invalid");
|
|
584
|
+
if (!isIsoUtc(a.signed_at)) return err("E_MALFORMED", "signed_at must be ISO 8601 UTC");
|
|
585
|
+
if (!isHex64(a.delegation_id)) return err("E_MALFORMED", "delegation_id must be 64-hex");
|
|
586
|
+
if (typeof a.scope_exercised !== "string" || a.scope_exercised.length === 0) return err("E_MALFORMED", "scope_exercised required");
|
|
587
|
+
if (a.sig?.alg !== "bip322" || typeof a.sig.value !== "string") return err("E_MALFORMED", "sig invalid");
|
|
588
|
+
if (a.sig.pubkey !== a.signer.address) return err("E_MALFORMED", "sig.pubkey must equal signer.address");
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
function checkRevocationShape(env) {
|
|
592
|
+
if (env.kind !== "agent-revocation") return err("E_MALFORMED", 'kind must be "agent-revocation"');
|
|
593
|
+
if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
|
|
594
|
+
if (!isHex64(env.delegation_id)) return err("E_MALFORMED", "delegation_id must be 64-hex");
|
|
595
|
+
if (!env.signer?.address || env.signer.alg !== "bip322") return err("E_MALFORMED", "signer invalid");
|
|
596
|
+
if (typeof env.reason !== "string" || env.reason.length > 128) return err("E_MALFORMED", "reason must be a string <=128 bytes");
|
|
597
|
+
if (!isIsoUtc(env.signed_at)) return err("E_MALFORMED", "signed_at must be ISO 8601 UTC");
|
|
598
|
+
if (env.sig?.alg !== "bip322" || typeof env.sig.value !== "string") return err("E_MALFORMED", "sig invalid");
|
|
599
|
+
if (env.sig.pubkey !== env.signer.address) return err("E_MALFORMED", "sig.pubkey must equal signer.address");
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
function actionEffectiveTime(a, resolve) {
|
|
603
|
+
if (a.ots?.status === "confirmed") {
|
|
604
|
+
const h = resolve ? resolve(a) : a.ots.block_height;
|
|
605
|
+
if (h !== null && h !== void 0) return { kind: "anchor", blockHeight: h };
|
|
606
|
+
}
|
|
607
|
+
return { kind: "signed", ms: new Date(a.signed_at).getTime() };
|
|
608
|
+
}
|
|
609
|
+
function effectiveRevocationTime(r, resolve) {
|
|
610
|
+
if (r.ots?.status === "confirmed") {
|
|
611
|
+
const h = resolve ? resolve(r) : r.ots.block_height;
|
|
612
|
+
if (h !== null && h !== void 0) return { kind: "anchor", blockHeight: h };
|
|
613
|
+
}
|
|
614
|
+
return { kind: "signed", ms: new Date(r.signed_at).getTime() };
|
|
615
|
+
}
|
|
616
|
+
function compareTimes(a, b) {
|
|
617
|
+
if (a.kind === "anchor" && b.kind === "anchor") return a.blockHeight - b.blockHeight;
|
|
618
|
+
if (a.kind === "anchor") return -1;
|
|
619
|
+
if (b.kind === "anchor") return 1;
|
|
620
|
+
return a.ms - b.ms;
|
|
621
|
+
}
|
|
622
|
+
function err(code, message) {
|
|
623
|
+
return { ok: false, code, message };
|
|
624
|
+
}
|
|
625
|
+
function isHex64(s) {
|
|
626
|
+
return typeof s === "string" && /^[0-9a-f]{64}$/.test(s);
|
|
627
|
+
}
|
|
628
|
+
function isIsoUtc(s) {
|
|
629
|
+
return typeof s === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/.test(s);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
Object.defineProperty(exports, "canonicalize", {
|
|
633
|
+
enumerable: true,
|
|
634
|
+
get: function () { return canonical.canonicalize; }
|
|
635
|
+
});
|
|
636
|
+
Object.defineProperty(exports, "hexEncode", {
|
|
637
|
+
enumerable: true,
|
|
638
|
+
get: function () { return canonical.hexEncode; }
|
|
639
|
+
});
|
|
640
|
+
exports.AgentError = AgentError;
|
|
641
|
+
exports.ENVELOPE_VERSION = ENVELOPE_VERSION;
|
|
642
|
+
exports.REGISTERED_SCOPES = REGISTERED_SCOPES;
|
|
643
|
+
exports.ScopeParseError = ScopeParseError;
|
|
644
|
+
exports.actionCanonicalBytes = actionCanonicalBytes;
|
|
645
|
+
exports.actionCanonicalMessage = actionCanonicalMessage;
|
|
646
|
+
exports.canonicalActionBytes = canonicalActionBytes;
|
|
647
|
+
exports.canonicalDelegationBytes = canonicalDelegationBytes;
|
|
648
|
+
exports.canonicalRevocationBytes = canonicalRevocationBytes;
|
|
649
|
+
exports.canonicalizeAction = canonicalizeAction;
|
|
650
|
+
exports.canonicalizeDelegation = canonicalizeDelegation;
|
|
651
|
+
exports.canonicalizeRevocation = canonicalizeRevocation;
|
|
652
|
+
exports.canonicalizeScope = canonicalizeScope;
|
|
653
|
+
exports.canonicalizeScopeString = canonicalizeScopeString;
|
|
654
|
+
exports.canonicalizeScopes = canonicalizeScopes;
|
|
655
|
+
exports.computeActionId = computeActionId;
|
|
656
|
+
exports.computeDelegationId = computeDelegationId;
|
|
657
|
+
exports.computeRevocationId = computeRevocationId;
|
|
658
|
+
exports.delegationCanonicalBytes = delegationCanonicalBytes;
|
|
659
|
+
exports.delegationCanonicalMessage = delegationCanonicalMessage;
|
|
660
|
+
exports.isSubScope = isSubScope;
|
|
661
|
+
exports.parseAndCanonicalizeScopes = parseAndCanonicalizeScopes;
|
|
662
|
+
exports.parseScope = parseScope;
|
|
663
|
+
exports.revocationCanonicalBytes = revocationCanonicalBytes;
|
|
664
|
+
exports.revocationCanonicalMessage = revocationCanonicalMessage;
|
|
665
|
+
exports.sha256Hex = sha256Hex;
|
|
666
|
+
exports.validateScope = validateScope;
|
|
667
|
+
exports.verifyAction = verifyAction;
|
|
668
|
+
exports.verifyDelegation = verifyDelegation;
|
|
669
|
+
exports.verifyRevocation = verifyRevocation;
|
|
670
|
+
//# sourceMappingURL=index.js.map
|
|
671
|
+
//# sourceMappingURL=index.js.map
|