@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/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 !== d.id) {
443
- return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) != delegation.id (${d.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 !== d.agent.address) {
446
- return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) != delegation.agent (${d.agent.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(d.issued_at).getTime();
449
- const expires = new Date(d.expires_at).getTime();
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 delegation window [${d.issued_at}, ${d.expires_at})`);
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 = d.scopes.map((s) => parseScope(s));
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
- 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`);
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