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