@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.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 !== d.id) {
442
- return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) != delegation.id (${d.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 !== d.agent.address) {
445
- return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) != delegation.agent (${d.agent.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(d.issued_at).getTime();
448
- const expires = new Date(d.expires_at).getTime();
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 delegation window [${d.issued_at}, ${d.expires_at})`);
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 = d.scopes.map((s) => parseScope(s));
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
- for (const rev of input.revocations) {
470
- if (rev.delegation_id !== d.id) continue;
471
- const rr = await verifyRevocation({
472
- envelope: rev,
473
- delegation: d,
474
- verifyBip322: input.verifyBip322,
475
- skipSignatureVerification: input.skipSignatureVerification
476
- });
477
- if (!rr.ok) continue;
478
- const effective = effectiveRevocationTime(rev, input.resolveAnchorBlockHeight);
479
- const actionTime = actionEffectiveTime(a, input.resolveAnchorBlockHeight);
480
- if (compareTimes(effective, actionTime) <= 0) {
481
- return err("E_REVOKED", `delegation was revoked by ${rev.id} before action was signed`);
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