@orangecheck/agent-core 0.1.0 → 0.2.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/canonical.d.mts +5 -2
- package/dist/canonical.d.ts +5 -2
- package/dist/canonical.js +22 -0
- package/dist/canonical.js.map +1 -1
- package/dist/canonical.mjs +20 -1
- package/dist/canonical.mjs.map +1 -1
- package/dist/index.d.mts +15 -5
- package/dist/index.d.ts +15 -5
- package/dist/index.js +180 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +176 -22
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +29 -3
- package/dist/types.d.ts +29 -3
- package/dist/types.js.map +1 -1
- package/dist/types.mjs.map +1 -1
- package/package.json +72 -69
- package/src/canonical.ts +23 -0
- package/src/index.ts +6 -0
- package/src/test-vectors.test.ts +358 -12
- package/src/types.ts +53 -2
- package/src/verify.ts +263 -32
package/dist/index.mjs
CHANGED
|
@@ -293,6 +293,19 @@ function revocationCanonicalMessage(input) {
|
|
|
293
293
|
`signed_at: ${input.signed_at}`
|
|
294
294
|
].join("\n");
|
|
295
295
|
}
|
|
296
|
+
function subdelegationCanonicalMessage(input) {
|
|
297
|
+
const scopeField = input.scopes.join(",");
|
|
298
|
+
return [
|
|
299
|
+
"oc-agent:subdelegation:v1",
|
|
300
|
+
`parent_id: ${input.parent_id}`,
|
|
301
|
+
`principal: ${input.principal}`,
|
|
302
|
+
`agent: ${input.agent}`,
|
|
303
|
+
`scopes: ${scopeField}`,
|
|
304
|
+
`issued_at: ${input.issued_at}`,
|
|
305
|
+
`expires_at: ${input.expires_at}`,
|
|
306
|
+
`nonce: ${input.nonce}`
|
|
307
|
+
].join("\n");
|
|
308
|
+
}
|
|
296
309
|
function delegationCanonicalBytes(input) {
|
|
297
310
|
return new TextEncoder().encode(delegationCanonicalMessage(input));
|
|
298
311
|
}
|
|
@@ -302,6 +315,9 @@ function actionCanonicalBytes(input) {
|
|
|
302
315
|
function revocationCanonicalBytes(input) {
|
|
303
316
|
return new TextEncoder().encode(revocationCanonicalMessage(input));
|
|
304
317
|
}
|
|
318
|
+
function subdelegationCanonicalBytes(input) {
|
|
319
|
+
return new TextEncoder().encode(subdelegationCanonicalMessage(input));
|
|
320
|
+
}
|
|
305
321
|
function computeDelegationId(input) {
|
|
306
322
|
return hexEncode(sha256(delegationCanonicalBytes(input)));
|
|
307
323
|
}
|
|
@@ -311,6 +327,9 @@ function computeActionId(input) {
|
|
|
311
327
|
function computeRevocationId(input) {
|
|
312
328
|
return hexEncode(sha256(revocationCanonicalBytes(input)));
|
|
313
329
|
}
|
|
330
|
+
function computeSubdelegationId(input) {
|
|
331
|
+
return hexEncode(sha256(subdelegationCanonicalBytes(input)));
|
|
332
|
+
}
|
|
314
333
|
function canonicalizeDelegation(env) {
|
|
315
334
|
return canonicalize(env);
|
|
316
335
|
}
|
|
@@ -332,6 +351,7 @@ function canonicalRevocationBytes(env) {
|
|
|
332
351
|
function sha256Hex(bytes) {
|
|
333
352
|
return hexEncode(sha256(bytes));
|
|
334
353
|
}
|
|
354
|
+
var DEFAULT_MAX_CHAIN_DEPTH = 5;
|
|
335
355
|
var AgentError = class extends Error {
|
|
336
356
|
constructor(code, message) {
|
|
337
357
|
super(message);
|
|
@@ -405,6 +425,14 @@ async function verifyDelegation(input) {
|
|
|
405
425
|
async function verifyAction(input) {
|
|
406
426
|
const a = input.action;
|
|
407
427
|
const d = input.delegation;
|
|
428
|
+
const chain = input.subdelegationChain ?? [];
|
|
429
|
+
const maxDepth = input.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH;
|
|
430
|
+
if (chain.length > maxDepth) {
|
|
431
|
+
return err(
|
|
432
|
+
"E_SUBDELEGATION_DEPTH_EXCEEDED",
|
|
433
|
+
`chain depth ${chain.length} exceeds maximum ${maxDepth}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
408
436
|
const dr = await verifyDelegation({
|
|
409
437
|
envelope: d,
|
|
410
438
|
verifyBip322: input.verifyBip322,
|
|
@@ -414,6 +442,13 @@ async function verifyAction(input) {
|
|
|
414
442
|
// action window check dominates
|
|
415
443
|
});
|
|
416
444
|
if (!dr.ok) return dr;
|
|
445
|
+
let parent = d;
|
|
446
|
+
for (const sub of chain) {
|
|
447
|
+
const r = await verifyChainLink(sub, parent, input);
|
|
448
|
+
if (!r.ok) return r;
|
|
449
|
+
parent = sub;
|
|
450
|
+
}
|
|
451
|
+
const leaf = chain.length > 0 ? chain[chain.length - 1] : d;
|
|
417
452
|
if (a.v !== ENVELOPE_VERSION) {
|
|
418
453
|
return err("E_UNSUPPORTED_VERSION", `action version ${a.v} not supported`);
|
|
419
454
|
}
|
|
@@ -438,25 +473,25 @@ async function verifyAction(input) {
|
|
|
438
473
|
const ok = await input.verifyBip322(a.id, a.sig.value, a.signer.address);
|
|
439
474
|
if (!ok) return err("E_BAD_ACTION_STAMP", "action BIP-322 signature did not verify");
|
|
440
475
|
}
|
|
441
|
-
if (a.delegation_id !==
|
|
442
|
-
return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) !=
|
|
476
|
+
if (a.delegation_id !== leaf.id) {
|
|
477
|
+
return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) != leaf.id (${leaf.id})`);
|
|
443
478
|
}
|
|
444
|
-
if (a.signer.address !==
|
|
445
|
-
return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) !=
|
|
479
|
+
if (a.signer.address !== leaf.agent.address) {
|
|
480
|
+
return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) != leaf.agent (${leaf.agent.address})`);
|
|
446
481
|
}
|
|
447
|
-
const issued = new Date(
|
|
448
|
-
const expires = new Date(
|
|
482
|
+
const issued = new Date(leaf.issued_at).getTime();
|
|
483
|
+
const expires = new Date(leaf.expires_at).getTime();
|
|
449
484
|
const signed = new Date(a.signed_at).getTime();
|
|
450
485
|
if (Number.isNaN(issued) || Number.isNaN(expires) || Number.isNaN(signed)) {
|
|
451
486
|
return err("E_MALFORMED", "unparseable ISO 8601 timestamp");
|
|
452
487
|
}
|
|
453
488
|
if (signed < issued || signed >= expires) {
|
|
454
|
-
return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside
|
|
489
|
+
return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside leaf window [${leaf.issued_at}, ${leaf.expires_at})`);
|
|
455
490
|
}
|
|
456
491
|
let exercised, accepted;
|
|
457
492
|
try {
|
|
458
493
|
exercised = canonicalizeScope(parseScope(a.scope_exercised));
|
|
459
|
-
const granted =
|
|
494
|
+
const granted = leaf.scopes.map((s) => parseScope(s));
|
|
460
495
|
const exercisedParsed = parseScope(a.scope_exercised);
|
|
461
496
|
validateScope(exercisedParsed, { mode: input.scopeMode ?? "strict" });
|
|
462
497
|
accepted = granted.some((g) => isSubScope(exercisedParsed, g));
|
|
@@ -466,19 +501,22 @@ async function verifyAction(input) {
|
|
|
466
501
|
}
|
|
467
502
|
if (!accepted) return err("E_SCOPE_DENIED", `scope_exercised (${exercised}) not a sub-scope of any granted scope`);
|
|
468
503
|
if (input.revocations && input.revocations.length > 0) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
504
|
+
const allLinks = [d, ...chain];
|
|
505
|
+
for (const link of allLinks) {
|
|
506
|
+
for (const rev of input.revocations) {
|
|
507
|
+
if (rev.delegation_id !== link.id) continue;
|
|
508
|
+
const rr = await verifyRevocation({
|
|
509
|
+
envelope: rev,
|
|
510
|
+
delegation: link,
|
|
511
|
+
verifyBip322: input.verifyBip322,
|
|
512
|
+
skipSignatureVerification: input.skipSignatureVerification
|
|
513
|
+
});
|
|
514
|
+
if (!rr.ok) continue;
|
|
515
|
+
const effective = effectiveRevocationTime(rev, input.resolveAnchorBlockHeight);
|
|
516
|
+
const actionTime = actionEffectiveTime(a, input.resolveAnchorBlockHeight);
|
|
517
|
+
if (compareTimes(effective, actionTime) <= 0) {
|
|
518
|
+
return err("E_REVOKED", `chain link ${link.id} was revoked by ${rev.id} before action was signed`);
|
|
519
|
+
}
|
|
482
520
|
}
|
|
483
521
|
}
|
|
484
522
|
}
|
|
@@ -515,6 +553,7 @@ async function verifyAction(input) {
|
|
|
515
553
|
canonicalMessage: reconstructedMessage,
|
|
516
554
|
id: a.id,
|
|
517
555
|
delegation: d,
|
|
556
|
+
chain,
|
|
518
557
|
scopeExercised: exercised,
|
|
519
558
|
anchor
|
|
520
559
|
};
|
|
@@ -598,6 +637,121 @@ function checkRevocationShape(env) {
|
|
|
598
637
|
if (env.sig.pubkey !== env.signer.address) return err("E_MALFORMED", "sig.pubkey must equal signer.address");
|
|
599
638
|
return null;
|
|
600
639
|
}
|
|
640
|
+
function checkSubdelegationShape(env) {
|
|
641
|
+
if (env.kind !== "agent-subdelegation") return err("E_MALFORMED", 'kind must be "agent-subdelegation"');
|
|
642
|
+
if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
|
|
643
|
+
if (!isHex64(env.parent_id)) return err("E_MALFORMED", "parent_id must be 64-hex");
|
|
644
|
+
if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
|
|
645
|
+
if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
|
|
646
|
+
if (!Array.isArray(env.scopes) || env.scopes.length === 0) return err("E_MALFORMED", "scopes must be non-empty array");
|
|
647
|
+
if ("bond" in env && env.bond !== void 0) {
|
|
648
|
+
return err("E_MALFORMED", "sub-delegation envelopes MUST NOT carry a bond field");
|
|
649
|
+
}
|
|
650
|
+
if (!isIsoUtc(env.issued_at)) return err("E_MALFORMED", "issued_at must be ISO 8601 UTC");
|
|
651
|
+
if (!isIsoUtc(env.expires_at)) return err("E_MALFORMED", "expires_at must be ISO 8601 UTC");
|
|
652
|
+
if (!/^[0-9a-f]{32}$/.test(env.nonce)) return err("E_MALFORMED", "nonce must be 32 lowercase hex chars");
|
|
653
|
+
if (env.sig?.alg !== "bip322" || typeof env.sig.value !== "string") return err("E_MALFORMED", "sig invalid");
|
|
654
|
+
if (env.sig.pubkey !== env.principal.address) return err("E_MALFORMED", "sig.pubkey must equal principal.address");
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
async function verifyChainLink(s, parent, input) {
|
|
658
|
+
if (s.v !== ENVELOPE_VERSION) {
|
|
659
|
+
return err("E_UNSUPPORTED_VERSION", `subdelegation version ${s.v} not supported`);
|
|
660
|
+
}
|
|
661
|
+
const shape = checkSubdelegationShape(s);
|
|
662
|
+
if (shape) return shape;
|
|
663
|
+
let canonicalScopesList;
|
|
664
|
+
try {
|
|
665
|
+
canonicalScopesList = canonicalizeScopes(s.scopes);
|
|
666
|
+
} catch (e) {
|
|
667
|
+
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
668
|
+
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
669
|
+
}
|
|
670
|
+
const canonInput = {
|
|
671
|
+
parent_id: s.parent_id,
|
|
672
|
+
principal: s.principal.address,
|
|
673
|
+
agent: s.agent.address,
|
|
674
|
+
scopes: canonicalScopesList,
|
|
675
|
+
issued_at: s.issued_at,
|
|
676
|
+
expires_at: s.expires_at,
|
|
677
|
+
nonce: s.nonce
|
|
678
|
+
};
|
|
679
|
+
const reconstructedId = computeSubdelegationId(canonInput);
|
|
680
|
+
if (reconstructedId !== s.id) {
|
|
681
|
+
return err("E_BAD_ID", `reconstructed subdelegation id (${reconstructedId}) does not match envelope id (${s.id})`);
|
|
682
|
+
}
|
|
683
|
+
let parsedScopes;
|
|
684
|
+
try {
|
|
685
|
+
parsedScopes = s.scopes.map((str) => parseScope(str));
|
|
686
|
+
for (const p of parsedScopes) validateScope(p, { mode: input.scopeMode ?? "strict" });
|
|
687
|
+
} catch (e) {
|
|
688
|
+
const msg = e instanceof ScopeParseError ? e.message : e.message;
|
|
689
|
+
return err("E_BAD_SCOPE_GRAMMAR", msg);
|
|
690
|
+
}
|
|
691
|
+
if (!input.skipSignatureVerification) {
|
|
692
|
+
if (!input.verifyBip322) return err("E_BAD_SIG", "no BIP-322 verifier supplied for subdelegation");
|
|
693
|
+
const ok = await input.verifyBip322(s.id, s.sig.value, s.principal.address);
|
|
694
|
+
if (!ok) return err("E_BAD_SIG", "subdelegation BIP-322 signature did not verify");
|
|
695
|
+
}
|
|
696
|
+
if (s.parent_id !== parent.id) {
|
|
697
|
+
return err(
|
|
698
|
+
"E_SUBDELEGATION_PRINCIPAL_MISMATCH",
|
|
699
|
+
`subdelegation.parent_id (${s.parent_id}) does not match parent envelope id (${parent.id})`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (s.principal.address !== parent.agent.address) {
|
|
703
|
+
return err(
|
|
704
|
+
"E_SUBDELEGATION_PRINCIPAL_MISMATCH",
|
|
705
|
+
`subdelegation.principal (${s.principal.address}) does not match parent.agent (${parent.agent.address})`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const sIssued = new Date(s.issued_at).getTime();
|
|
709
|
+
const sExpires = new Date(s.expires_at).getTime();
|
|
710
|
+
const pIssued = new Date(parent.issued_at).getTime();
|
|
711
|
+
const pExpires = new Date(parent.expires_at).getTime();
|
|
712
|
+
if (Number.isNaN(sIssued) || Number.isNaN(sExpires) || Number.isNaN(pIssued) || Number.isNaN(pExpires)) {
|
|
713
|
+
return err("E_MALFORMED", "unparseable ISO 8601 timestamp in chain");
|
|
714
|
+
}
|
|
715
|
+
if (sExpires <= sIssued) {
|
|
716
|
+
return err("E_MALFORMED", "subdelegation expires_at must be > issued_at");
|
|
717
|
+
}
|
|
718
|
+
if (sIssued < pIssued || sExpires > pExpires) {
|
|
719
|
+
return err(
|
|
720
|
+
"E_SUBDELEGATION_EXPIRES_EXTENDED",
|
|
721
|
+
`subdelegation window [${s.issued_at}, ${s.expires_at}) is not contained in parent's [${parent.issued_at}, ${parent.expires_at})`
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
const parentScopesParsed = parent.scopes.map((str) => parseScope(str));
|
|
725
|
+
for (let i = 0; i < parsedScopes.length; i++) {
|
|
726
|
+
const childScope = parsedScopes[i];
|
|
727
|
+
const containedBySomeParentScope = parentScopesParsed.some((p) => isSubScope(childScope, p));
|
|
728
|
+
if (!containedBySomeParentScope) {
|
|
729
|
+
return err(
|
|
730
|
+
"E_SUBDELEGATION_SCOPE_ESCALATED",
|
|
731
|
+
`subdelegation scope ${canonicalizeScope(childScope)} is not a sub-scope of any granted scope on the parent`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
ok: true,
|
|
737
|
+
envelope: s,
|
|
738
|
+
canonicalMessage: "",
|
|
739
|
+
// not populated for chain links; computeSubdelegationId is the binding form
|
|
740
|
+
id: s.id
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
async function verifySubdelegation(input) {
|
|
744
|
+
const r = await verifyChainLink(input.envelope, input.parent, input);
|
|
745
|
+
if (!r.ok) return r;
|
|
746
|
+
if (!input.skipTemporalCheck) {
|
|
747
|
+
const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
|
|
748
|
+
const issued = new Date(input.envelope.issued_at).getTime();
|
|
749
|
+
const expires = new Date(input.envelope.expires_at).getTime();
|
|
750
|
+
if (now < issued) return err("E_NOT_YET_VALID", `subdelegation issued_at ${input.envelope.issued_at} is in the future`);
|
|
751
|
+
if (now >= expires) return err("E_EXPIRED", `subdelegation expires_at ${input.envelope.expires_at} is past`);
|
|
752
|
+
}
|
|
753
|
+
return r;
|
|
754
|
+
}
|
|
601
755
|
function actionEffectiveTime(a, resolve) {
|
|
602
756
|
if (a.ots?.status === "confirmed") {
|
|
603
757
|
const h = resolve ? resolve(a) : a.ots.block_height;
|
|
@@ -628,6 +782,6 @@ function isIsoUtc(s) {
|
|
|
628
782
|
return typeof s === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/.test(s);
|
|
629
783
|
}
|
|
630
784
|
|
|
631
|
-
export { AgentError, ENVELOPE_VERSION, REGISTERED_SCOPES, ScopeParseError, actionCanonicalBytes, actionCanonicalMessage, canonicalActionBytes, canonicalDelegationBytes, canonicalRevocationBytes, canonicalizeAction, canonicalizeDelegation, canonicalizeRevocation, canonicalizeScope, canonicalizeScopeString, canonicalizeScopes, computeActionId, computeDelegationId, computeRevocationId, delegationCanonicalBytes, delegationCanonicalMessage, isSubScope, parseAndCanonicalizeScopes, parseScope, revocationCanonicalBytes, revocationCanonicalMessage, sha256Hex, validateScope, verifyAction, verifyDelegation, verifyRevocation };
|
|
785
|
+
export { AgentError, DEFAULT_MAX_CHAIN_DEPTH, ENVELOPE_VERSION, REGISTERED_SCOPES, ScopeParseError, actionCanonicalBytes, actionCanonicalMessage, canonicalActionBytes, canonicalDelegationBytes, canonicalRevocationBytes, canonicalizeAction, canonicalizeDelegation, canonicalizeRevocation, canonicalizeScope, canonicalizeScopeString, canonicalizeScopes, computeActionId, computeDelegationId, computeRevocationId, computeSubdelegationId, delegationCanonicalBytes, delegationCanonicalMessage, isSubScope, parseAndCanonicalizeScopes, parseScope, revocationCanonicalBytes, revocationCanonicalMessage, sha256Hex, subdelegationCanonicalBytes, subdelegationCanonicalMessage, validateScope, verifyAction, verifyDelegation, verifyRevocation, verifySubdelegation };
|
|
632
786
|
//# sourceMappingURL=index.mjs.map
|
|
633
787
|
//# sourceMappingURL=index.mjs.map
|