@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.d.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, RevocationEnvelope, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult } from './types.js';
2
- export { ActionCanonicalInput, ActionContent, ActionOts, ActorRef, DelegationBond, DelegationCanonicalInput, DelegationRevocationRef, ENVELOPE_VERSION, EnvelopeKind, RevocationCanonicalInput, RevocationHolder, Signature, VerifyActionOkExtra, VerifyErr, VerifyOk } from './types.js';
3
- export { actionCanonicalBytes, actionCanonicalMessage, canonicalActionBytes, canonicalDelegationBytes, canonicalRevocationBytes, canonicalizeAction, canonicalizeDelegation, canonicalizeRevocation, canonicalizeScopes, computeActionId, computeDelegationId, computeRevocationId, delegationCanonicalBytes, delegationCanonicalMessage, parseAndCanonicalizeScopes, revocationCanonicalBytes, revocationCanonicalMessage, sha256Hex } from './canonical.js';
1
+ import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, SubdelegationEnvelope, RevocationEnvelope, ChainLink, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult, VerifySubdelegationResult, ScopesEncryptedEnvelope } from './types.js';
2
+ export { ActionCanonicalInput, ActionContent, ActionOts, ActorRef, DelegationBond, DelegationCanonicalInput, DelegationRevocationRef, ENVELOPE_VERSION, EnvelopeKind, RevocationCanonicalInput, RevocationHolder, Signature, SubdelegationCanonicalInput, VerifyActionOkExtra, VerifyErr, VerifyOk } from './types.js';
3
+ export { actionCanonicalBytes, actionCanonicalMessage, canonicalActionBytes, canonicalDelegationBytes, canonicalRevocationBytes, canonicalizeAction, canonicalizeDelegation, canonicalizeRevocation, canonicalizeScopes, computeActionId, computeDelegationId, computeRevocationId, computeSubdelegationId, delegationCanonicalBytes, delegationCanonicalMessage, parseAndCanonicalizeScopes, revocationCanonicalBytes, revocationCanonicalMessage, sha256Hex, subdelegationCanonicalBytes, subdelegationCanonicalMessage } from './canonical.js';
4
4
  import { ValidationOptions } from './scope.js';
5
5
  export { REGISTERED_SCOPES, Scope, ScopeConstraint, ScopeOp, ScopeParseError, canonicalizeScope, canonicalizeScopeString, isSubScope, parseScope, validateScope } from './scope.js';
6
+ import { SealInput, DeviceRecord, UnsealInput } from '@orangecheck/lock-core';
6
7
  export { canonicalize, hexEncode } from '@orangecheck/stamp-core/canonical';
7
8
 
9
+ declare const DEFAULT_MAX_CHAIN_DEPTH = 5;
8
10
  interface VerifyBase {
9
11
  verifyBip322?: (msg: string, signatureB64: string, address: string) => Promise<boolean>;
10
12
  skipSignatureVerification?: boolean;
@@ -18,21 +20,74 @@ interface VerifyDelegationInput extends VerifyBase {
18
20
  envelope: DelegationEnvelope;
19
21
  now?: Date;
20
22
  skipTemporalCheck?: boolean;
23
+ decryptScopesWith?: {
24
+ device_id: string;
25
+ secretKey: Uint8Array;
26
+ };
21
27
  }
22
28
  declare function verifyDelegation(input: VerifyDelegationInput): Promise<VerifyDelegationResult>;
23
29
  interface VerifyActionInput extends VerifyBase {
24
30
  action: ActionEnvelope;
25
31
  delegation: DelegationEnvelope;
32
+ subdelegationChain?: SubdelegationEnvelope[];
33
+ maxChainDepth?: number;
26
34
  revocations?: RevocationEnvelope[];
27
35
  content?: Uint8Array;
28
36
  verifyOtsAnchor?: (proofB64: string, blockHeight: number, blockHash: string) => Promise<boolean>;
29
37
  resolveAnchorBlockHeight?: (env: ActionEnvelope | RevocationEnvelope) => number | null;
38
+ decryptScopesWith?: {
39
+ device_id: string;
40
+ secretKey: Uint8Array;
41
+ };
30
42
  }
31
43
  declare function verifyAction(input: VerifyActionInput): Promise<VerifyActionResult>;
32
44
  interface VerifyRevocationInput extends VerifyBase {
33
45
  envelope: RevocationEnvelope;
34
- delegation: DelegationEnvelope;
46
+ delegation: ChainLink;
35
47
  }
36
48
  declare function verifyRevocation(input: VerifyRevocationInput): Promise<VerifyRevocationResult>;
49
+ interface VerifySubdelegationInput extends VerifyBase {
50
+ envelope: SubdelegationEnvelope;
51
+ parent: ChainLink;
52
+ skipTemporalCheck?: boolean;
53
+ now?: Date;
54
+ decryptScopesWith?: {
55
+ device_id: string;
56
+ secretKey: Uint8Array;
57
+ };
58
+ }
59
+ declare function verifySubdelegation(input: VerifySubdelegationInput): Promise<VerifySubdelegationResult>;
60
+
61
+ declare function encodeScopesPayload(scopes: string[]): Uint8Array;
62
+ declare function decodeScopesPayload(bytes: Uint8Array): string[];
63
+ interface SealScopesInput {
64
+ scopes: string[];
65
+ sender: SealInput['sender'];
66
+ recipients: DeviceRecord[];
67
+ hint?: string;
68
+ expiresAt?: Date | null;
69
+ }
70
+ declare function sealScopes(input: SealScopesInput): Promise<ScopesEncryptedEnvelope>;
71
+ interface UnsealScopesInput {
72
+ envelope: ScopesEncryptedEnvelope;
73
+ device: UnsealInput['device'];
74
+ verifyBip322?: UnsealInput['verifyBip322'];
75
+ skipSenderVerification?: boolean;
76
+ }
77
+ interface UnsealedScopes {
78
+ scopes: string[];
79
+ sender: {
80
+ address: string;
81
+ attestation_id?: string;
82
+ };
83
+ matchedDeviceId: string;
84
+ }
85
+ declare function unsealScopes(input: UnsealScopesInput): Promise<UnsealedScopes>;
86
+ declare function hasPrivateScopes<T extends {
87
+ scopes?: string[];
88
+ scopes_encrypted?: ScopesEncryptedEnvelope;
89
+ }>(envelope: T): envelope is T & {
90
+ scopes_encrypted: ScopesEncryptedEnvelope;
91
+ };
37
92
 
38
- export { ActionEnvelope, AgentError, AgentErrorCode, DelegationEnvelope, RevocationEnvelope, ValidationOptions, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyRevocationInput, VerifyRevocationResult, verifyAction, verifyDelegation, verifyRevocation };
93
+ export { ActionEnvelope, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DelegationEnvelope, RevocationEnvelope, ScopesEncryptedEnvelope, type SealScopesInput, SubdelegationEnvelope, type UnsealScopesInput, type UnsealedScopes, ValidationOptions, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, decodeScopesPayload, encodeScopesPayload, hasPrivateScopes, sealScopes, unsealScopes, verifyAction, verifyDelegation, verifyRevocation, verifySubdelegation };
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var sha256 = require('@noble/hashes/sha256');
4
4
  var canonical = require('@orangecheck/stamp-core/canonical');
5
+ var lockCore = require('@orangecheck/lock-core');
5
6
 
6
7
  // src/types.ts
7
8
  var ENVELOPE_VERSION = 1;
@@ -294,6 +295,19 @@ function revocationCanonicalMessage(input) {
294
295
  `signed_at: ${input.signed_at}`
295
296
  ].join("\n");
296
297
  }
298
+ function subdelegationCanonicalMessage(input) {
299
+ const scopeField = input.scopes.join(",");
300
+ return [
301
+ "oc-agent:subdelegation:v1",
302
+ `parent_id: ${input.parent_id}`,
303
+ `principal: ${input.principal}`,
304
+ `agent: ${input.agent}`,
305
+ `scopes: ${scopeField}`,
306
+ `issued_at: ${input.issued_at}`,
307
+ `expires_at: ${input.expires_at}`,
308
+ `nonce: ${input.nonce}`
309
+ ].join("\n");
310
+ }
297
311
  function delegationCanonicalBytes(input) {
298
312
  return new TextEncoder().encode(delegationCanonicalMessage(input));
299
313
  }
@@ -303,6 +317,9 @@ function actionCanonicalBytes(input) {
303
317
  function revocationCanonicalBytes(input) {
304
318
  return new TextEncoder().encode(revocationCanonicalMessage(input));
305
319
  }
320
+ function subdelegationCanonicalBytes(input) {
321
+ return new TextEncoder().encode(subdelegationCanonicalMessage(input));
322
+ }
306
323
  function computeDelegationId(input) {
307
324
  return canonical.hexEncode(sha256.sha256(delegationCanonicalBytes(input)));
308
325
  }
@@ -312,6 +329,9 @@ function computeActionId(input) {
312
329
  function computeRevocationId(input) {
313
330
  return canonical.hexEncode(sha256.sha256(revocationCanonicalBytes(input)));
314
331
  }
332
+ function computeSubdelegationId(input) {
333
+ return canonical.hexEncode(sha256.sha256(subdelegationCanonicalBytes(input)));
334
+ }
315
335
  function canonicalizeDelegation(env) {
316
336
  return canonical.canonicalize(env);
317
337
  }
@@ -333,6 +353,51 @@ function canonicalRevocationBytes(env) {
333
353
  function sha256Hex(bytes) {
334
354
  return canonical.hexEncode(sha256.sha256(bytes));
335
355
  }
356
+ function encodeScopesPayload(scopes) {
357
+ const canonical = canonicalizeScopes(scopes);
358
+ const json = JSON.stringify(canonical);
359
+ return new TextEncoder().encode(json);
360
+ }
361
+ function decodeScopesPayload(bytes) {
362
+ const json = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
363
+ const parsed = JSON.parse(json);
364
+ if (!Array.isArray(parsed) || !parsed.every((s) => typeof s === "string")) {
365
+ throw new Error("scopes payload must be a JSON array of strings");
366
+ }
367
+ return parsed;
368
+ }
369
+ async function sealScopes(input) {
370
+ const env = await lockCore.seal({
371
+ kind: "identity",
372
+ payload: encodeScopesPayload(input.scopes),
373
+ sender: input.sender,
374
+ recipients: input.recipients,
375
+ ...input.hint !== void 0 && { hint: input.hint },
376
+ ...input.expiresAt !== void 0 && { expiresAt: input.expiresAt }
377
+ });
378
+ return env;
379
+ }
380
+ async function unsealScopes(input) {
381
+ const r = await lockCore.unseal({
382
+ envelope: input.envelope,
383
+ device: input.device,
384
+ ...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
385
+ ...input.skipSenderVerification !== void 0 && {
386
+ skipSenderVerification: input.skipSenderVerification
387
+ }
388
+ });
389
+ return {
390
+ scopes: decodeScopesPayload(r.payload),
391
+ sender: r.sender,
392
+ matchedDeviceId: r.matchedDeviceId
393
+ };
394
+ }
395
+ function hasPrivateScopes(envelope) {
396
+ return !!envelope.scopes_encrypted;
397
+ }
398
+
399
+ // src/verify.ts
400
+ var DEFAULT_MAX_CHAIN_DEPTH = 5;
336
401
  var AgentError = class extends Error {
337
402
  constructor(code, message) {
338
403
  super(message);
@@ -347,19 +412,63 @@ async function verifyDelegation(input) {
347
412
  }
348
413
  const shape = checkDelegationShape(env);
349
414
  if (shape) return shape;
415
+ if (env.scopes !== void 0 && env.scopes_encrypted !== void 0) {
416
+ return err(
417
+ "E_SCOPES_BOTH_PROVIDED",
418
+ "envelope carries both scopes and scopes_encrypted; pick one"
419
+ );
420
+ }
421
+ if (env.scopes === void 0 && env.scopes_encrypted === void 0) {
422
+ return err(
423
+ "E_SCOPES_NEITHER_PROVIDED",
424
+ "envelope carries neither scopes nor scopes_encrypted"
425
+ );
426
+ }
427
+ let workingScopes;
428
+ if (env.scopes_encrypted !== void 0) {
429
+ if (env.scopes_encrypted.from?.address !== env.principal.address) {
430
+ return err(
431
+ "E_MALFORMED",
432
+ `scopes_encrypted.from.address (${env.scopes_encrypted.from?.address}) does not match principal.address (${env.principal.address})`
433
+ );
434
+ }
435
+ if (!input.decryptScopesWith) {
436
+ return err(
437
+ "E_SCOPES_UNREADABLE",
438
+ "envelope carries scopes_encrypted; no decryption key provided"
439
+ );
440
+ }
441
+ try {
442
+ const r = await unsealScopes({
443
+ envelope: env.scopes_encrypted,
444
+ device: input.decryptScopesWith,
445
+ ...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
446
+ skipSenderVerification: !!input.skipSignatureVerification
447
+ });
448
+ workingScopes = r.scopes;
449
+ } catch (e) {
450
+ const msg = e.message ?? String(e);
451
+ if (/no matching recipient|no recipient|device_id/i.test(msg)) {
452
+ return err("E_SCOPES_UNREADABLE", msg);
453
+ }
454
+ return err("E_BAD_LOCK_ENVELOPE", msg);
455
+ }
456
+ } else {
457
+ workingScopes = env.scopes;
458
+ }
350
459
  let canonicalScopes;
351
460
  try {
352
- for (const s of env.scopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
353
- canonicalScopes = canonicalizeScopes(env.scopes);
461
+ for (const s of workingScopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
462
+ canonicalScopes = canonicalizeScopes(workingScopes);
354
463
  } catch (e) {
355
464
  const msg = e instanceof ScopeParseError ? e.message : e.message;
356
465
  return err("E_BAD_SCOPE_GRAMMAR", msg);
357
466
  }
358
467
  for (let i = 0; i < canonicalScopes.length; i++) {
359
- if (env.scopes[i] !== canonicalScopes[i]) {
468
+ if (workingScopes[i] !== canonicalScopes[i]) {
360
469
  return err(
361
470
  "E_BAD_SCOPE_GRAMMAR",
362
- `scope at index ${i} not in canonical form; expected ${canonicalScopes[i]} got ${env.scopes[i]}`
471
+ `scope at index ${i} not in canonical form; expected ${canonicalScopes[i]} got ${workingScopes[i]}`
363
472
  );
364
473
  }
365
474
  }
@@ -396,9 +505,10 @@ async function verifyDelegation(input) {
396
505
  if (now < issued) return err("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
397
506
  if (now >= expires) return err("E_EXPIRED", `delegation expired at ${env.expires_at}`);
398
507
  }
508
+ const returnedEnvelope = env.scopes_encrypted !== void 0 ? { ...env, scopes: canonicalScopes } : env;
399
509
  return {
400
510
  ok: true,
401
- envelope: env,
511
+ envelope: returnedEnvelope,
402
512
  canonicalMessage: reconstructedMessage,
403
513
  id: env.id
404
514
  };
@@ -406,15 +516,36 @@ async function verifyDelegation(input) {
406
516
  async function verifyAction(input) {
407
517
  const a = input.action;
408
518
  const d = input.delegation;
519
+ const chain = input.subdelegationChain ?? [];
520
+ const maxDepth = input.maxChainDepth ?? DEFAULT_MAX_CHAIN_DEPTH;
521
+ if (chain.length > maxDepth) {
522
+ return err(
523
+ "E_SUBDELEGATION_DEPTH_EXCEEDED",
524
+ `chain depth ${chain.length} exceeds maximum ${maxDepth}`
525
+ );
526
+ }
409
527
  const dr = await verifyDelegation({
410
528
  envelope: d,
411
529
  verifyBip322: input.verifyBip322,
412
530
  skipSignatureVerification: input.skipSignatureVerification,
413
531
  scopeMode: input.scopeMode,
414
- skipTemporalCheck: true
532
+ skipTemporalCheck: true,
415
533
  // action window check dominates
534
+ ...input.decryptScopesWith ? { decryptScopesWith: input.decryptScopesWith } : {}
416
535
  });
417
536
  if (!dr.ok) return dr;
537
+ const rootHydrated = dr.envelope;
538
+ let parent = rootHydrated;
539
+ const hydratedChain = [];
540
+ for (const sub of chain) {
541
+ const hydrated = await hydrateSubdelegationScopes(sub, input);
542
+ if (!hydrated.ok) return hydrated;
543
+ const r = await verifyChainLink(hydrated.envelope, parent, input);
544
+ if (!r.ok) return r;
545
+ hydratedChain.push(hydrated.envelope);
546
+ parent = hydrated.envelope;
547
+ }
548
+ const leaf = hydratedChain.length > 0 ? hydratedChain[hydratedChain.length - 1] : rootHydrated;
418
549
  if (a.v !== ENVELOPE_VERSION) {
419
550
  return err("E_UNSUPPORTED_VERSION", `action version ${a.v} not supported`);
420
551
  }
@@ -439,25 +570,28 @@ async function verifyAction(input) {
439
570
  const ok = await input.verifyBip322(a.id, a.sig.value, a.signer.address);
440
571
  if (!ok) return err("E_BAD_ACTION_STAMP", "action BIP-322 signature did not verify");
441
572
  }
442
- if (a.delegation_id !== d.id) {
443
- return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) != delegation.id (${d.id})`);
573
+ if (a.delegation_id !== leaf.id) {
574
+ return err("E_DELEGATION_MISMATCH", `action.delegation_id (${a.delegation_id}) != leaf.id (${leaf.id})`);
444
575
  }
445
- if (a.signer.address !== d.agent.address) {
446
- return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) != delegation.agent (${d.agent.address})`);
576
+ if (a.signer.address !== leaf.agent.address) {
577
+ return err("E_AGENT_MISMATCH", `action signer (${a.signer.address}) != leaf.agent (${leaf.agent.address})`);
447
578
  }
448
- const issued = new Date(d.issued_at).getTime();
449
- const expires = new Date(d.expires_at).getTime();
579
+ const issued = new Date(leaf.issued_at).getTime();
580
+ const expires = new Date(leaf.expires_at).getTime();
450
581
  const signed = new Date(a.signed_at).getTime();
451
582
  if (Number.isNaN(issued) || Number.isNaN(expires) || Number.isNaN(signed)) {
452
583
  return err("E_MALFORMED", "unparseable ISO 8601 timestamp");
453
584
  }
454
585
  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})`);
586
+ return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside leaf window [${leaf.issued_at}, ${leaf.expires_at})`);
587
+ }
588
+ if (!leaf.scopes) {
589
+ return err("E_SCOPES_NEITHER_PROVIDED", "leaf has no plaintext scopes after hydration");
456
590
  }
457
591
  let exercised, accepted;
458
592
  try {
459
593
  exercised = canonicalizeScope(parseScope(a.scope_exercised));
460
- const granted = d.scopes.map((s) => parseScope(s));
594
+ const granted = leaf.scopes.map((s) => parseScope(s));
461
595
  const exercisedParsed = parseScope(a.scope_exercised);
462
596
  validateScope(exercisedParsed, { mode: input.scopeMode ?? "strict" });
463
597
  accepted = granted.some((g) => isSubScope(exercisedParsed, g));
@@ -467,19 +601,22 @@ async function verifyAction(input) {
467
601
  }
468
602
  if (!accepted) return err("E_SCOPE_DENIED", `scope_exercised (${exercised}) not a sub-scope of any granted scope`);
469
603
  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`);
604
+ const allLinks = [rootHydrated, ...hydratedChain];
605
+ for (const link of allLinks) {
606
+ for (const rev of input.revocations) {
607
+ if (rev.delegation_id !== link.id) continue;
608
+ const rr = await verifyRevocation({
609
+ envelope: rev,
610
+ delegation: link,
611
+ verifyBip322: input.verifyBip322,
612
+ skipSignatureVerification: input.skipSignatureVerification
613
+ });
614
+ if (!rr.ok) continue;
615
+ const effective = effectiveRevocationTime(rev, input.resolveAnchorBlockHeight);
616
+ const actionTime = actionEffectiveTime(a, input.resolveAnchorBlockHeight);
617
+ if (compareTimes(effective, actionTime) <= 0) {
618
+ return err("E_REVOKED", `chain link ${link.id} was revoked by ${rev.id} before action was signed`);
619
+ }
483
620
  }
484
621
  }
485
622
  }
@@ -515,11 +652,56 @@ async function verifyAction(input) {
515
652
  envelope: a,
516
653
  canonicalMessage: reconstructedMessage,
517
654
  id: a.id,
518
- delegation: d,
655
+ delegation: rootHydrated,
656
+ chain: hydratedChain,
519
657
  scopeExercised: exercised,
520
658
  anchor
521
659
  };
522
660
  }
661
+ async function hydrateSubdelegationScopes(sub, input) {
662
+ if (sub.scopes !== void 0 && sub.scopes_encrypted !== void 0) {
663
+ return err(
664
+ "E_SCOPES_BOTH_PROVIDED",
665
+ "subdelegation carries both scopes and scopes_encrypted"
666
+ );
667
+ }
668
+ if (sub.scopes === void 0 && sub.scopes_encrypted === void 0) {
669
+ return err(
670
+ "E_SCOPES_NEITHER_PROVIDED",
671
+ "subdelegation carries neither scopes nor scopes_encrypted"
672
+ );
673
+ }
674
+ if (sub.scopes_encrypted === void 0) {
675
+ return { ok: true, envelope: sub };
676
+ }
677
+ if (sub.scopes_encrypted.from?.address !== sub.principal.address) {
678
+ return err(
679
+ "E_MALFORMED",
680
+ `subdelegation.scopes_encrypted.from.address (${sub.scopes_encrypted.from?.address}) does not match principal.address (${sub.principal.address})`
681
+ );
682
+ }
683
+ if (!input.decryptScopesWith) {
684
+ return err(
685
+ "E_SCOPES_UNREADABLE",
686
+ "subdelegation carries scopes_encrypted; no decryption key provided for the chain"
687
+ );
688
+ }
689
+ try {
690
+ const r = await unsealScopes({
691
+ envelope: sub.scopes_encrypted,
692
+ device: input.decryptScopesWith,
693
+ ...input.verifyBip322 ? { verifyBip322: input.verifyBip322 } : {},
694
+ skipSenderVerification: !!input.skipSignatureVerification
695
+ });
696
+ return { ok: true, envelope: { ...sub, scopes: r.scopes } };
697
+ } catch (e) {
698
+ const msg = e.message ?? String(e);
699
+ if (/no matching recipient|no recipient|device_id/i.test(msg)) {
700
+ return err("E_SCOPES_UNREADABLE", msg);
701
+ }
702
+ return err("E_BAD_LOCK_ENVELOPE", msg);
703
+ }
704
+ }
523
705
  async function verifyRevocation(input) {
524
706
  const env = input.envelope;
525
707
  const d = input.delegation;
@@ -561,7 +743,10 @@ function checkDelegationShape(env) {
561
743
  if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
562
744
  if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
563
745
  if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
564
- if (!Array.isArray(env.scopes) || env.scopes.length === 0) return err("E_MALFORMED", "scopes must be non-empty array");
746
+ if (env.scopes !== void 0) {
747
+ if (!Array.isArray(env.scopes) || env.scopes.length === 0)
748
+ return err("E_MALFORMED", "scopes must be non-empty array");
749
+ }
565
750
  if (env.bond !== null) {
566
751
  if (!Number.isInteger(env.bond.sats) || env.bond.sats < 0) return err("E_MALFORMED", "bond.sats must be non-negative integer");
567
752
  if (!isHex64(env.bond.attestation_id)) return err("E_MALFORMED", "bond.attestation_id must be 64-hex");
@@ -599,6 +784,138 @@ function checkRevocationShape(env) {
599
784
  if (env.sig.pubkey !== env.signer.address) return err("E_MALFORMED", "sig.pubkey must equal signer.address");
600
785
  return null;
601
786
  }
787
+ function checkSubdelegationShape(env) {
788
+ if (env.kind !== "agent-subdelegation") return err("E_MALFORMED", 'kind must be "agent-subdelegation"');
789
+ if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
790
+ if (!isHex64(env.parent_id)) return err("E_MALFORMED", "parent_id must be 64-hex");
791
+ if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
792
+ if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
793
+ if (env.scopes !== void 0) {
794
+ if (!Array.isArray(env.scopes) || env.scopes.length === 0)
795
+ return err("E_MALFORMED", "scopes must be non-empty array");
796
+ }
797
+ if ("bond" in env && env.bond !== void 0) {
798
+ return err("E_MALFORMED", "sub-delegation envelopes MUST NOT carry a bond field");
799
+ }
800
+ if (!isIsoUtc(env.issued_at)) return err("E_MALFORMED", "issued_at must be ISO 8601 UTC");
801
+ if (!isIsoUtc(env.expires_at)) return err("E_MALFORMED", "expires_at must be ISO 8601 UTC");
802
+ if (!/^[0-9a-f]{32}$/.test(env.nonce)) return err("E_MALFORMED", "nonce must be 32 lowercase hex chars");
803
+ if (env.sig?.alg !== "bip322" || typeof env.sig.value !== "string") return err("E_MALFORMED", "sig invalid");
804
+ if (env.sig.pubkey !== env.principal.address) return err("E_MALFORMED", "sig.pubkey must equal principal.address");
805
+ return null;
806
+ }
807
+ async function verifyChainLink(s, parent, input) {
808
+ if (s.v !== ENVELOPE_VERSION) {
809
+ return err("E_UNSUPPORTED_VERSION", `subdelegation version ${s.v} not supported`);
810
+ }
811
+ const shape = checkSubdelegationShape(s);
812
+ if (shape) return shape;
813
+ if (!s.scopes) {
814
+ return err(
815
+ "E_SCOPES_NEITHER_PROVIDED",
816
+ "subdelegation has no plaintext scopes \u2014 caller must hydrate v1.2 envelopes before invoking verifyChainLink"
817
+ );
818
+ }
819
+ let canonicalScopesList;
820
+ try {
821
+ canonicalScopesList = canonicalizeScopes(s.scopes);
822
+ } catch (e) {
823
+ const msg = e instanceof ScopeParseError ? e.message : e.message;
824
+ return err("E_BAD_SCOPE_GRAMMAR", msg);
825
+ }
826
+ const canonInput = {
827
+ parent_id: s.parent_id,
828
+ principal: s.principal.address,
829
+ agent: s.agent.address,
830
+ scopes: canonicalScopesList,
831
+ issued_at: s.issued_at,
832
+ expires_at: s.expires_at,
833
+ nonce: s.nonce
834
+ };
835
+ const reconstructedId = computeSubdelegationId(canonInput);
836
+ if (reconstructedId !== s.id) {
837
+ return err("E_BAD_ID", `reconstructed subdelegation id (${reconstructedId}) does not match envelope id (${s.id})`);
838
+ }
839
+ let parsedScopes;
840
+ try {
841
+ parsedScopes = s.scopes.map((str) => parseScope(str));
842
+ for (const p of parsedScopes) validateScope(p, { mode: input.scopeMode ?? "strict" });
843
+ } catch (e) {
844
+ const msg = e instanceof ScopeParseError ? e.message : e.message;
845
+ return err("E_BAD_SCOPE_GRAMMAR", msg);
846
+ }
847
+ if (!input.skipSignatureVerification) {
848
+ if (!input.verifyBip322) return err("E_BAD_SIG", "no BIP-322 verifier supplied for subdelegation");
849
+ const ok = await input.verifyBip322(s.id, s.sig.value, s.principal.address);
850
+ if (!ok) return err("E_BAD_SIG", "subdelegation BIP-322 signature did not verify");
851
+ }
852
+ if (s.parent_id !== parent.id) {
853
+ return err(
854
+ "E_SUBDELEGATION_PRINCIPAL_MISMATCH",
855
+ `subdelegation.parent_id (${s.parent_id}) does not match parent envelope id (${parent.id})`
856
+ );
857
+ }
858
+ if (s.principal.address !== parent.agent.address) {
859
+ return err(
860
+ "E_SUBDELEGATION_PRINCIPAL_MISMATCH",
861
+ `subdelegation.principal (${s.principal.address}) does not match parent.agent (${parent.agent.address})`
862
+ );
863
+ }
864
+ const sIssued = new Date(s.issued_at).getTime();
865
+ const sExpires = new Date(s.expires_at).getTime();
866
+ const pIssued = new Date(parent.issued_at).getTime();
867
+ const pExpires = new Date(parent.expires_at).getTime();
868
+ if (Number.isNaN(sIssued) || Number.isNaN(sExpires) || Number.isNaN(pIssued) || Number.isNaN(pExpires)) {
869
+ return err("E_MALFORMED", "unparseable ISO 8601 timestamp in chain");
870
+ }
871
+ if (sExpires <= sIssued) {
872
+ return err("E_MALFORMED", "subdelegation expires_at must be > issued_at");
873
+ }
874
+ if (sIssued < pIssued || sExpires > pExpires) {
875
+ return err(
876
+ "E_SUBDELEGATION_EXPIRES_EXTENDED",
877
+ `subdelegation window [${s.issued_at}, ${s.expires_at}) is not contained in parent's [${parent.issued_at}, ${parent.expires_at})`
878
+ );
879
+ }
880
+ if (!parent.scopes) {
881
+ return err(
882
+ "E_SCOPES_NEITHER_PROVIDED",
883
+ "parent has no plaintext scopes \u2014 caller must hydrate v1.2 parents before invoking verifyChainLink"
884
+ );
885
+ }
886
+ const parentScopesParsed = parent.scopes.map((str) => parseScope(str));
887
+ for (let i = 0; i < parsedScopes.length; i++) {
888
+ const childScope = parsedScopes[i];
889
+ const containedBySomeParentScope = parentScopesParsed.some((p) => isSubScope(childScope, p));
890
+ if (!containedBySomeParentScope) {
891
+ return err(
892
+ "E_SUBDELEGATION_SCOPE_ESCALATED",
893
+ `subdelegation scope ${canonicalizeScope(childScope)} is not a sub-scope of any granted scope on the parent`
894
+ );
895
+ }
896
+ }
897
+ return {
898
+ ok: true,
899
+ envelope: s,
900
+ canonicalMessage: "",
901
+ // not populated for chain links; computeSubdelegationId is the binding form
902
+ id: s.id
903
+ };
904
+ }
905
+ async function verifySubdelegation(input) {
906
+ const hydrated = await hydrateSubdelegationScopes(input.envelope, input);
907
+ if (!hydrated.ok) return hydrated;
908
+ const r = await verifyChainLink(hydrated.envelope, input.parent, input);
909
+ if (!r.ok) return r;
910
+ if (!input.skipTemporalCheck) {
911
+ const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
912
+ const issued = new Date(input.envelope.issued_at).getTime();
913
+ const expires = new Date(input.envelope.expires_at).getTime();
914
+ if (now < issued) return err("E_NOT_YET_VALID", `subdelegation issued_at ${input.envelope.issued_at} is in the future`);
915
+ if (now >= expires) return err("E_EXPIRED", `subdelegation expires_at ${input.envelope.expires_at} is past`);
916
+ }
917
+ return r;
918
+ }
602
919
  function actionEffectiveTime(a, resolve) {
603
920
  if (a.ots?.status === "confirmed") {
604
921
  const h = resolve ? resolve(a) : a.ots.block_height;
@@ -638,6 +955,7 @@ Object.defineProperty(exports, "hexEncode", {
638
955
  get: function () { return canonical.hexEncode; }
639
956
  });
640
957
  exports.AgentError = AgentError;
958
+ exports.DEFAULT_MAX_CHAIN_DEPTH = DEFAULT_MAX_CHAIN_DEPTH;
641
959
  exports.ENVELOPE_VERSION = ENVELOPE_VERSION;
642
960
  exports.REGISTERED_SCOPES = REGISTERED_SCOPES;
643
961
  exports.ScopeParseError = ScopeParseError;
@@ -655,17 +973,26 @@ exports.canonicalizeScopes = canonicalizeScopes;
655
973
  exports.computeActionId = computeActionId;
656
974
  exports.computeDelegationId = computeDelegationId;
657
975
  exports.computeRevocationId = computeRevocationId;
976
+ exports.computeSubdelegationId = computeSubdelegationId;
977
+ exports.decodeScopesPayload = decodeScopesPayload;
658
978
  exports.delegationCanonicalBytes = delegationCanonicalBytes;
659
979
  exports.delegationCanonicalMessage = delegationCanonicalMessage;
980
+ exports.encodeScopesPayload = encodeScopesPayload;
981
+ exports.hasPrivateScopes = hasPrivateScopes;
660
982
  exports.isSubScope = isSubScope;
661
983
  exports.parseAndCanonicalizeScopes = parseAndCanonicalizeScopes;
662
984
  exports.parseScope = parseScope;
663
985
  exports.revocationCanonicalBytes = revocationCanonicalBytes;
664
986
  exports.revocationCanonicalMessage = revocationCanonicalMessage;
987
+ exports.sealScopes = sealScopes;
665
988
  exports.sha256Hex = sha256Hex;
989
+ exports.subdelegationCanonicalBytes = subdelegationCanonicalBytes;
990
+ exports.subdelegationCanonicalMessage = subdelegationCanonicalMessage;
991
+ exports.unsealScopes = unsealScopes;
666
992
  exports.validateScope = validateScope;
667
993
  exports.verifyAction = verifyAction;
668
994
  exports.verifyDelegation = verifyDelegation;
669
995
  exports.verifyRevocation = verifyRevocation;
996
+ exports.verifySubdelegation = verifySubdelegation;
670
997
  //# sourceMappingURL=index.js.map
671
998
  //# sourceMappingURL=index.js.map