@orangecheck/agent-core 0.1.0 → 0.3.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
@@ -1,6 +1,7 @@
1
1
  import { sha256 } from '@noble/hashes/sha256';
2
2
  import { hexEncode, canonicalize } from '@orangecheck/stamp-core/canonical';
3
3
  export { canonicalize, hexEncode } from '@orangecheck/stamp-core/canonical';
4
+ import { seal, unseal } from '@orangecheck/lock-core';
4
5
 
5
6
  // src/types.ts
6
7
  var ENVELOPE_VERSION = 1;
@@ -293,6 +294,19 @@ function revocationCanonicalMessage(input) {
293
294
  `signed_at: ${input.signed_at}`
294
295
  ].join("\n");
295
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
+ }
296
310
  function delegationCanonicalBytes(input) {
297
311
  return new TextEncoder().encode(delegationCanonicalMessage(input));
298
312
  }
@@ -302,6 +316,9 @@ function actionCanonicalBytes(input) {
302
316
  function revocationCanonicalBytes(input) {
303
317
  return new TextEncoder().encode(revocationCanonicalMessage(input));
304
318
  }
319
+ function subdelegationCanonicalBytes(input) {
320
+ return new TextEncoder().encode(subdelegationCanonicalMessage(input));
321
+ }
305
322
  function computeDelegationId(input) {
306
323
  return hexEncode(sha256(delegationCanonicalBytes(input)));
307
324
  }
@@ -311,6 +328,9 @@ function computeActionId(input) {
311
328
  function computeRevocationId(input) {
312
329
  return hexEncode(sha256(revocationCanonicalBytes(input)));
313
330
  }
331
+ function computeSubdelegationId(input) {
332
+ return hexEncode(sha256(subdelegationCanonicalBytes(input)));
333
+ }
314
334
  function canonicalizeDelegation(env) {
315
335
  return canonicalize(env);
316
336
  }
@@ -332,6 +352,51 @@ function canonicalRevocationBytes(env) {
332
352
  function sha256Hex(bytes) {
333
353
  return hexEncode(sha256(bytes));
334
354
  }
355
+ function encodeScopesPayload(scopes) {
356
+ const canonical = canonicalizeScopes(scopes);
357
+ const json = JSON.stringify(canonical);
358
+ return new TextEncoder().encode(json);
359
+ }
360
+ function decodeScopesPayload(bytes) {
361
+ const json = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
362
+ const parsed = JSON.parse(json);
363
+ if (!Array.isArray(parsed) || !parsed.every((s) => typeof s === "string")) {
364
+ throw new Error("scopes payload must be a JSON array of strings");
365
+ }
366
+ return parsed;
367
+ }
368
+ async function sealScopes(input) {
369
+ const env = await seal({
370
+ kind: "identity",
371
+ payload: encodeScopesPayload(input.scopes),
372
+ sender: input.sender,
373
+ recipients: input.recipients,
374
+ ...input.hint !== void 0 && { hint: input.hint },
375
+ ...input.expiresAt !== void 0 && { expiresAt: input.expiresAt }
376
+ });
377
+ return env;
378
+ }
379
+ async function unsealScopes(input) {
380
+ const r = await unseal({
381
+ envelope: input.envelope,
382
+ device: input.device,
383
+ ...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
384
+ ...input.skipSenderVerification !== void 0 && {
385
+ skipSenderVerification: input.skipSenderVerification
386
+ }
387
+ });
388
+ return {
389
+ scopes: decodeScopesPayload(r.payload),
390
+ sender: r.sender,
391
+ matchedDeviceId: r.matchedDeviceId
392
+ };
393
+ }
394
+ function hasPrivateScopes(envelope) {
395
+ return !!envelope.scopes_encrypted;
396
+ }
397
+
398
+ // src/verify.ts
399
+ var DEFAULT_MAX_CHAIN_DEPTH = 5;
335
400
  var AgentError = class extends Error {
336
401
  constructor(code, message) {
337
402
  super(message);
@@ -346,19 +411,63 @@ async function verifyDelegation(input) {
346
411
  }
347
412
  const shape = checkDelegationShape(env);
348
413
  if (shape) return shape;
414
+ if (env.scopes !== void 0 && env.scopes_encrypted !== void 0) {
415
+ return err(
416
+ "E_SCOPES_BOTH_PROVIDED",
417
+ "envelope carries both scopes and scopes_encrypted; pick one"
418
+ );
419
+ }
420
+ if (env.scopes === void 0 && env.scopes_encrypted === void 0) {
421
+ return err(
422
+ "E_SCOPES_NEITHER_PROVIDED",
423
+ "envelope carries neither scopes nor scopes_encrypted"
424
+ );
425
+ }
426
+ let workingScopes;
427
+ if (env.scopes_encrypted !== void 0) {
428
+ if (env.scopes_encrypted.from?.address !== env.principal.address) {
429
+ return err(
430
+ "E_MALFORMED",
431
+ `scopes_encrypted.from.address (${env.scopes_encrypted.from?.address}) does not match principal.address (${env.principal.address})`
432
+ );
433
+ }
434
+ if (!input.decryptScopesWith) {
435
+ return err(
436
+ "E_SCOPES_UNREADABLE",
437
+ "envelope carries scopes_encrypted; no decryption key provided"
438
+ );
439
+ }
440
+ try {
441
+ const r = await unsealScopes({
442
+ envelope: env.scopes_encrypted,
443
+ device: input.decryptScopesWith,
444
+ ...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
445
+ skipSenderVerification: !!input.skipSignatureVerification
446
+ });
447
+ workingScopes = r.scopes;
448
+ } catch (e) {
449
+ const msg = e.message ?? String(e);
450
+ if (/no matching recipient|no recipient|device_id/i.test(msg)) {
451
+ return err("E_SCOPES_UNREADABLE", msg);
452
+ }
453
+ return err("E_BAD_LOCK_ENVELOPE", msg);
454
+ }
455
+ } else {
456
+ workingScopes = env.scopes;
457
+ }
349
458
  let canonicalScopes;
350
459
  try {
351
- for (const s of env.scopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
352
- canonicalScopes = canonicalizeScopes(env.scopes);
460
+ for (const s of workingScopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
461
+ canonicalScopes = canonicalizeScopes(workingScopes);
353
462
  } catch (e) {
354
463
  const msg = e instanceof ScopeParseError ? e.message : e.message;
355
464
  return err("E_BAD_SCOPE_GRAMMAR", msg);
356
465
  }
357
466
  for (let i = 0; i < canonicalScopes.length; i++) {
358
- if (env.scopes[i] !== canonicalScopes[i]) {
467
+ if (workingScopes[i] !== canonicalScopes[i]) {
359
468
  return err(
360
469
  "E_BAD_SCOPE_GRAMMAR",
361
- `scope at index ${i} not in canonical form; expected ${canonicalScopes[i]} got ${env.scopes[i]}`
470
+ `scope at index ${i} not in canonical form; expected ${canonicalScopes[i]} got ${workingScopes[i]}`
362
471
  );
363
472
  }
364
473
  }
@@ -395,9 +504,10 @@ async function verifyDelegation(input) {
395
504
  if (now < issued) return err("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
396
505
  if (now >= expires) return err("E_EXPIRED", `delegation expired at ${env.expires_at}`);
397
506
  }
507
+ const returnedEnvelope = env.scopes_encrypted !== void 0 ? { ...env, scopes: canonicalScopes } : env;
398
508
  return {
399
509
  ok: true,
400
- envelope: env,
510
+ envelope: returnedEnvelope,
401
511
  canonicalMessage: reconstructedMessage,
402
512
  id: env.id
403
513
  };
@@ -405,15 +515,36 @@ async function verifyDelegation(input) {
405
515
  async function verifyAction(input) {
406
516
  const a = input.action;
407
517
  const d = input.delegation;
518
+ const chain = input.subdelegationChain ?? [];
519
+ const maxDepth = input.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH;
520
+ if (chain.length > maxDepth) {
521
+ return err(
522
+ "E_SUBDELEGATION_DEPTH_EXCEEDED",
523
+ `chain depth ${chain.length} exceeds maximum ${maxDepth}`
524
+ );
525
+ }
408
526
  const dr = await verifyDelegation({
409
527
  envelope: d,
410
528
  verifyBip322: input.verifyBip322,
411
529
  skipSignatureVerification: input.skipSignatureVerification,
412
530
  scopeMode: input.scopeMode,
413
- skipTemporalCheck: true
531
+ skipTemporalCheck: true,
414
532
  // action window check dominates
533
+ ...input.decryptScopesWith ? { decryptScopesWith: input.decryptScopesWith } : {}
415
534
  });
416
535
  if (!dr.ok) return dr;
536
+ const rootHydrated = dr.envelope;
537
+ let parent = rootHydrated;
538
+ const hydratedChain = [];
539
+ for (const sub of chain) {
540
+ const hydrated = await hydrateSubdelegationScopes(sub, input);
541
+ if (!hydrated.ok) return hydrated;
542
+ const r = await verifyChainLink(hydrated.envelope, parent, input);
543
+ if (!r.ok) return r;
544
+ hydratedChain.push(hydrated.envelope);
545
+ parent = hydrated.envelope;
546
+ }
547
+ const leaf = hydratedChain.length > 0 ? hydratedChain[hydratedChain.length - 1] : rootHydrated;
417
548
  if (a.v !== ENVELOPE_VERSION) {
418
549
  return err("E_UNSUPPORTED_VERSION", `action version ${a.v} not supported`);
419
550
  }
@@ -438,25 +569,28 @@ async function verifyAction(input) {
438
569
  const ok = await input.verifyBip322(a.id, a.sig.value, a.signer.address);
439
570
  if (!ok) return err("E_BAD_ACTION_STAMP", "action BIP-322 signature did not verify");
440
571
  }
441
- if (a.delegation_id !== d.id) {
442
- return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) != delegation.id (${d.id})`);
572
+ if (a.delegation_id !== leaf.id) {
573
+ return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) != leaf.id (${leaf.id})`);
443
574
  }
444
- if (a.signer.address !== d.agent.address) {
445
- return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) != delegation.agent (${d.agent.address})`);
575
+ if (a.signer.address !== leaf.agent.address) {
576
+ return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) != leaf.agent (${leaf.agent.address})`);
446
577
  }
447
- const issued = new Date(d.issued_at).getTime();
448
- const expires = new Date(d.expires_at).getTime();
578
+ const issued = new Date(leaf.issued_at).getTime();
579
+ const expires = new Date(leaf.expires_at).getTime();
449
580
  const signed = new Date(a.signed_at).getTime();
450
581
  if (Number.isNaN(issued) || Number.isNaN(expires) || Number.isNaN(signed)) {
451
582
  return err("E_MALFORMED", "unparseable ISO 8601 timestamp");
452
583
  }
453
584
  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})`);
585
+ return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside leaf window [${leaf.issued_at}, ${leaf.expires_at})`);
586
+ }
587
+ if (!leaf.scopes) {
588
+ return err("E_SCOPES_NEITHER_PROVIDED", "leaf has no plaintext scopes after hydration");
455
589
  }
456
590
  let exercised, accepted;
457
591
  try {
458
592
  exercised = canonicalizeScope(parseScope(a.scope_exercised));
459
- const granted = d.scopes.map((s) => parseScope(s));
593
+ const granted = leaf.scopes.map((s) => parseScope(s));
460
594
  const exercisedParsed = parseScope(a.scope_exercised);
461
595
  validateScope(exercisedParsed, { mode: input.scopeMode ?? "strict" });
462
596
  accepted = granted.some((g) => isSubScope(exercisedParsed, g));
@@ -466,19 +600,22 @@ async function verifyAction(input) {
466
600
  }
467
601
  if (!accepted) return err("E_SCOPE_DENIED", `scope_exercised (${exercised}) not a sub-scope of any granted scope`);
468
602
  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`);
603
+ const allLinks = [rootHydrated, ...hydratedChain];
604
+ for (const link of allLinks) {
605
+ for (const rev of input.revocations) {
606
+ if (rev.delegation_id !== link.id) continue;
607
+ const rr = await verifyRevocation({
608
+ envelope: rev,
609
+ delegation: link,
610
+ verifyBip322: input.verifyBip322,
611
+ skipSignatureVerification: input.skipSignatureVerification
612
+ });
613
+ if (!rr.ok) continue;
614
+ const effective = effectiveRevocationTime(rev, input.resolveAnchorBlockHeight);
615
+ const actionTime = actionEffectiveTime(a, input.resolveAnchorBlockHeight);
616
+ if (compareTimes(effective, actionTime) <= 0) {
617
+ return err("E_REVOKED", `chain link ${link.id} was revoked by ${rev.id} before action was signed`);
618
+ }
482
619
  }
483
620
  }
484
621
  }
@@ -514,11 +651,56 @@ async function verifyAction(input) {
514
651
  envelope: a,
515
652
  canonicalMessage: reconstructedMessage,
516
653
  id: a.id,
517
- delegation: d,
654
+ delegation: rootHydrated,
655
+ chain: hydratedChain,
518
656
  scopeExercised: exercised,
519
657
  anchor
520
658
  };
521
659
  }
660
+ async function hydrateSubdelegationScopes(sub, input) {
661
+ if (sub.scopes !== void 0 && sub.scopes_encrypted !== void 0) {
662
+ return err(
663
+ "E_SCOPES_BOTH_PROVIDED",
664
+ "subdelegation carries both scopes and scopes_encrypted"
665
+ );
666
+ }
667
+ if (sub.scopes === void 0 && sub.scopes_encrypted === void 0) {
668
+ return err(
669
+ "E_SCOPES_NEITHER_PROVIDED",
670
+ "subdelegation carries neither scopes nor scopes_encrypted"
671
+ );
672
+ }
673
+ if (sub.scopes_encrypted === void 0) {
674
+ return { ok: true, envelope: sub };
675
+ }
676
+ if (sub.scopes_encrypted.from?.address !== sub.principal.address) {
677
+ return err(
678
+ "E_MALFORMED",
679
+ `subdelegation.scopes_encrypted.from.address (${sub.scopes_encrypted.from?.address}) does not match principal.address (${sub.principal.address})`
680
+ );
681
+ }
682
+ if (!input.decryptScopesWith) {
683
+ return err(
684
+ "E_SCOPES_UNREADABLE",
685
+ "subdelegation carries scopes_encrypted; no decryption key provided for the chain"
686
+ );
687
+ }
688
+ try {
689
+ const r = await unsealScopes({
690
+ envelope: sub.scopes_encrypted,
691
+ device: input.decryptScopesWith,
692
+ ...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
693
+ skipSenderVerification: !!input.skipSignatureVerification
694
+ });
695
+ return { ok: true, envelope: { ...sub, scopes: r.scopes } };
696
+ } catch (e) {
697
+ const msg = e.message ?? String(e);
698
+ if (/no matching recipient|no recipient|device_id/i.test(msg)) {
699
+ return err("E_SCOPES_UNREADABLE", msg);
700
+ }
701
+ return err("E_BAD_LOCK_ENVELOPE", msg);
702
+ }
703
+ }
522
704
  async function verifyRevocation(input) {
523
705
  const env = input.envelope;
524
706
  const d = input.delegation;
@@ -560,7 +742,10 @@ function checkDelegationShape(env) {
560
742
  if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
561
743
  if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
562
744
  if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
563
- if (!Array.isArray(env.scopes) || env.scopes.length === 0) return err("E_MALFORMED", "scopes must be non-empty array");
745
+ if (env.scopes !== void 0) {
746
+ if (!Array.isArray(env.scopes) || env.scopes.length === 0)
747
+ return err("E_MALFORMED", "scopes must be non-empty array");
748
+ }
564
749
  if (env.bond !== null) {
565
750
  if (!Number.isInteger(env.bond.sats) || env.bond.sats < 0) return err("E_MALFORMED", "bond.sats must be non-negative integer");
566
751
  if (!isHex64(env.bond.attestation_id)) return err("E_MALFORMED", "bond.attestation_id must be 64-hex");
@@ -598,6 +783,138 @@ function checkRevocationShape(env) {
598
783
  if (env.sig.pubkey !== env.signer.address) return err("E_MALFORMED", "sig.pubkey must equal signer.address");
599
784
  return null;
600
785
  }
786
+ function checkSubdelegationShape(env) {
787
+ if (env.kind !== "agent-subdelegation") return err("E_MALFORMED", 'kind must be "agent-subdelegation"');
788
+ if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
789
+ if (!isHex64(env.parent_id)) return err("E_MALFORMED", "parent_id must be 64-hex");
790
+ if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
791
+ if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
792
+ if (env.scopes !== void 0) {
793
+ if (!Array.isArray(env.scopes) || env.scopes.length === 0)
794
+ return err("E_MALFORMED", "scopes must be non-empty array");
795
+ }
796
+ if ("bond" in env && env.bond !== void 0) {
797
+ return err("E_MALFORMED", "sub-delegation envelopes MUST NOT carry a bond field");
798
+ }
799
+ if (!isIsoUtc(env.issued_at)) return err("E_MALFORMED", "issued_at must be ISO 8601 UTC");
800
+ if (!isIsoUtc(env.expires_at)) return err("E_MALFORMED", "expires_at must be ISO 8601 UTC");
801
+ if (!/^[0-9a-f]{32}$/.test(env.nonce)) return err("E_MALFORMED", "nonce must be 32 lowercase hex chars");
802
+ if (env.sig?.alg !== "bip322" || typeof env.sig.value !== "string") return err("E_MALFORMED", "sig invalid");
803
+ if (env.sig.pubkey !== env.principal.address) return err("E_MALFORMED", "sig.pubkey must equal principal.address");
804
+ return null;
805
+ }
806
+ async function verifyChainLink(s, parent, input) {
807
+ if (s.v !== ENVELOPE_VERSION) {
808
+ return err("E_UNSUPPORTED_VERSION", `subdelegation version ${s.v} not supported`);
809
+ }
810
+ const shape = checkSubdelegationShape(s);
811
+ if (shape) return shape;
812
+ if (!s.scopes) {
813
+ return err(
814
+ "E_SCOPES_NEITHER_PROVIDED",
815
+ "subdelegation has no plaintext scopes \u2014 caller must hydrate v1.2 envelopes before invoking verifyChainLink"
816
+ );
817
+ }
818
+ let canonicalScopesList;
819
+ try {
820
+ canonicalScopesList = canonicalizeScopes(s.scopes);
821
+ } catch (e) {
822
+ const msg = e instanceof ScopeParseError ? e.message : e.message;
823
+ return err("E_BAD_SCOPE_GRAMMAR", msg);
824
+ }
825
+ const canonInput = {
826
+ parent_id: s.parent_id,
827
+ principal: s.principal.address,
828
+ agent: s.agent.address,
829
+ scopes: canonicalScopesList,
830
+ issued_at: s.issued_at,
831
+ expires_at: s.expires_at,
832
+ nonce: s.nonce
833
+ };
834
+ const reconstructedId = computeSubdelegationId(canonInput);
835
+ if (reconstructedId !== s.id) {
836
+ return err("E_BAD_ID", `reconstructed subdelegation id (${reconstructedId}) does not match envelope id (${s.id})`);
837
+ }
838
+ let parsedScopes;
839
+ try {
840
+ parsedScopes = s.scopes.map((str) => parseScope(str));
841
+ for (const p of parsedScopes) validateScope(p, { mode: input.scopeMode ?? "strict" });
842
+ } catch (e) {
843
+ const msg = e instanceof ScopeParseError ? e.message : e.message;
844
+ return err("E_BAD_SCOPE_GRAMMAR", msg);
845
+ }
846
+ if (!input.skipSignatureVerification) {
847
+ if (!input.verifyBip322) return err("E_BAD_SIG", "no BIP-322 verifier supplied for subdelegation");
848
+ const ok = await input.verifyBip322(s.id, s.sig.value, s.principal.address);
849
+ if (!ok) return err("E_BAD_SIG", "subdelegation BIP-322 signature did not verify");
850
+ }
851
+ if (s.parent_id !== parent.id) {
852
+ return err(
853
+ "E_SUBDELEGATION_PRINCIPAL_MISMATCH",
854
+ `subdelegation.parent_id (${s.parent_id}) does not match parent envelope id (${parent.id})`
855
+ );
856
+ }
857
+ if (s.principal.address !== parent.agent.address) {
858
+ return err(
859
+ "E_SUBDELEGATION_PRINCIPAL_MISMATCH",
860
+ `subdelegation.principal (${s.principal.address}) does not match parent.agent (${parent.agent.address})`
861
+ );
862
+ }
863
+ const sIssued = new Date(s.issued_at).getTime();
864
+ const sExpires = new Date(s.expires_at).getTime();
865
+ const pIssued = new Date(parent.issued_at).getTime();
866
+ const pExpires = new Date(parent.expires_at).getTime();
867
+ if (Number.isNaN(sIssued) || Number.isNaN(sExpires) || Number.isNaN(pIssued) || Number.isNaN(pExpires)) {
868
+ return err("E_MALFORMED", "unparseable ISO 8601 timestamp in chain");
869
+ }
870
+ if (sExpires <= sIssued) {
871
+ return err("E_MALFORMED", "subdelegation expires_at must be > issued_at");
872
+ }
873
+ if (sIssued < pIssued || sExpires > pExpires) {
874
+ return err(
875
+ "E_SUBDELEGATION_EXPIRES_EXTENDED",
876
+ `subdelegation window [${s.issued_at}, ${s.expires_at}) is not contained in parent's [${parent.issued_at}, ${parent.expires_at})`
877
+ );
878
+ }
879
+ if (!parent.scopes) {
880
+ return err(
881
+ "E_SCOPES_NEITHER_PROVIDED",
882
+ "parent has no plaintext scopes \u2014 caller must hydrate v1.2 parents before invoking verifyChainLink"
883
+ );
884
+ }
885
+ const parentScopesParsed = parent.scopes.map((str) => parseScope(str));
886
+ for (let i = 0; i < parsedScopes.length; i++) {
887
+ const childScope = parsedScopes[i];
888
+ const containedBySomeParentScope = parentScopesParsed.some((p) => isSubScope(childScope, p));
889
+ if (!containedBySomeParentScope) {
890
+ return err(
891
+ "E_SUBDELEGATION_SCOPE_ESCALATED",
892
+ `subdelegation scope ${canonicalizeScope(childScope)} is not a sub-scope of any granted scope on the parent`
893
+ );
894
+ }
895
+ }
896
+ return {
897
+ ok: true,
898
+ envelope: s,
899
+ canonicalMessage: "",
900
+ // not populated for chain links; computeSubdelegationId is the binding form
901
+ id: s.id
902
+ };
903
+ }
904
+ async function verifySubdelegation(input) {
905
+ const hydrated = await hydrateSubdelegationScopes(input.envelope, input);
906
+ if (!hydrated.ok) return hydrated;
907
+ const r = await verifyChainLink(hydrated.envelope, input.parent, input);
908
+ if (!r.ok) return r;
909
+ if (!input.skipTemporalCheck) {
910
+ const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
911
+ const issued = new Date(input.envelope.issued_at).getTime();
912
+ const expires = new Date(input.envelope.expires_at).getTime();
913
+ if (now < issued) return err("E_NOT_YET_VALID", `subdelegation issued_at ${input.envelope.issued_at} is in the future`);
914
+ if (now >= expires) return err("E_EXPIRED", `subdelegation expires_at ${input.envelope.expires_at} is past`);
915
+ }
916
+ return r;
917
+ }
601
918
  function actionEffectiveTime(a, resolve) {
602
919
  if (a.ots?.status === "confirmed") {
603
920
  const h = resolve ? resolve(a) : a.ots.block_height;
@@ -628,6 +945,6 @@ function isIsoUtc(s) {
628
945
  return typeof s === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/.test(s);
629
946
  }
630
947
 
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 };
948
+ 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, decodeScopesPayload, delegationCanonicalBytes, delegationCanonicalMessage, encodeScopesPayload, hasPrivateScopes, isSubScope, parseAndCanonicalizeScopes, parseScope, revocationCanonicalBytes, revocationCanonicalMessage, sealScopes, sha256Hex, subdelegationCanonicalBytes, subdelegationCanonicalMessage, unsealScopes, validateScope, verifyAction, verifyDelegation, verifyRevocation, verifySubdelegation };
632
949
  //# sourceMappingURL=index.mjs.map
633
950
  //# sourceMappingURL=index.mjs.map