@orangecheck/agent-core 0.2.0 → 1.0.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.mts CHANGED
@@ -1,8 +1,9 @@
1
- import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, SubdelegationEnvelope, RevocationEnvelope, ChainLink, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult, VerifySubdelegationResult } from './types.mjs';
1
+ import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, SubdelegationEnvelope, RevocationEnvelope, ChainLink, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult, VerifySubdelegationResult, ScopesEncryptedEnvelope } from './types.mjs';
2
2
  export { ActionCanonicalInput, ActionContent, ActionOts, ActorRef, DelegationBond, DelegationCanonicalInput, DelegationRevocationRef, ENVELOPE_VERSION, EnvelopeKind, RevocationCanonicalInput, RevocationHolder, Signature, SubdelegationCanonicalInput, VerifyActionOkExtra, VerifyErr, VerifyOk } from './types.mjs';
3
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.mjs';
4
4
  import { ValidationOptions } from './scope.mjs';
5
5
  export { REGISTERED_SCOPES, Scope, ScopeConstraint, ScopeOp, ScopeParseError, canonicalizeScope, canonicalizeScopeString, isSubScope, parseScope, validateScope } from './scope.mjs';
6
+ import { SealInput, DeviceRecord, UnsealInput } from '@orangecheck/lock-core';
6
7
  export { canonicalize, hexEncode } from '@orangecheck/stamp-core/canonical';
7
8
 
8
9
  declare const DEFAULT_MAX_CHAIN_DEPTH = 5;
@@ -19,6 +20,10 @@ interface VerifyDelegationInput extends VerifyBase {
19
20
  envelope: DelegationEnvelope;
20
21
  now?: Date;
21
22
  skipTemporalCheck?: boolean;
23
+ decryptScopesWith?: {
24
+ device_id: string;
25
+ secretKey: Uint8Array;
26
+ };
22
27
  }
23
28
  declare function verifyDelegation(input: VerifyDelegationInput): Promise<VerifyDelegationResult>;
24
29
  interface VerifyActionInput extends VerifyBase {
@@ -30,6 +35,10 @@ interface VerifyActionInput extends VerifyBase {
30
35
  content?: Uint8Array;
31
36
  verifyOtsAnchor?: (proofB64: string, blockHeight: number, blockHash: string) => Promise<boolean>;
32
37
  resolveAnchorBlockHeight?: (env: ActionEnvelope | RevocationEnvelope) => number | null;
38
+ decryptScopesWith?: {
39
+ device_id: string;
40
+ secretKey: Uint8Array;
41
+ };
33
42
  }
34
43
  declare function verifyAction(input: VerifyActionInput): Promise<VerifyActionResult>;
35
44
  interface VerifyRevocationInput extends VerifyBase {
@@ -42,7 +51,43 @@ interface VerifySubdelegationInput extends VerifyBase {
42
51
  parent: ChainLink;
43
52
  skipTemporalCheck?: boolean;
44
53
  now?: Date;
54
+ decryptScopesWith?: {
55
+ device_id: string;
56
+ secretKey: Uint8Array;
57
+ };
45
58
  }
46
59
  declare function verifySubdelegation(input: VerifySubdelegationInput): Promise<VerifySubdelegationResult>;
47
60
 
48
- export { ActionEnvelope, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DelegationEnvelope, RevocationEnvelope, SubdelegationEnvelope, ValidationOptions, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, verifyAction, verifyDelegation, verifyRevocation, verifySubdelegation };
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
+ };
92
+
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.d.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, SubdelegationEnvelope, RevocationEnvelope, ChainLink, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult, VerifySubdelegationResult } from './types.js';
1
+ import { AgentErrorCode, ActionEnvelope, DelegationEnvelope, SubdelegationEnvelope, RevocationEnvelope, ChainLink, VerifyActionResult, VerifyDelegationResult, VerifyRevocationResult, VerifySubdelegationResult, ScopesEncryptedEnvelope } from './types.js';
2
2
  export { ActionCanonicalInput, ActionContent, ActionOts, ActorRef, DelegationBond, DelegationCanonicalInput, DelegationRevocationRef, ENVELOPE_VERSION, EnvelopeKind, RevocationCanonicalInput, RevocationHolder, Signature, SubdelegationCanonicalInput, VerifyActionOkExtra, VerifyErr, VerifyOk } from './types.js';
3
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
 
8
9
  declare const DEFAULT_MAX_CHAIN_DEPTH = 5;
@@ -19,6 +20,10 @@ interface VerifyDelegationInput extends VerifyBase {
19
20
  envelope: DelegationEnvelope;
20
21
  now?: Date;
21
22
  skipTemporalCheck?: boolean;
23
+ decryptScopesWith?: {
24
+ device_id: string;
25
+ secretKey: Uint8Array;
26
+ };
22
27
  }
23
28
  declare function verifyDelegation(input: VerifyDelegationInput): Promise<VerifyDelegationResult>;
24
29
  interface VerifyActionInput extends VerifyBase {
@@ -30,6 +35,10 @@ interface VerifyActionInput extends VerifyBase {
30
35
  content?: Uint8Array;
31
36
  verifyOtsAnchor?: (proofB64: string, blockHeight: number, blockHash: string) => Promise<boolean>;
32
37
  resolveAnchorBlockHeight?: (env: ActionEnvelope | RevocationEnvelope) => number | null;
38
+ decryptScopesWith?: {
39
+ device_id: string;
40
+ secretKey: Uint8Array;
41
+ };
33
42
  }
34
43
  declare function verifyAction(input: VerifyActionInput): Promise<VerifyActionResult>;
35
44
  interface VerifyRevocationInput extends VerifyBase {
@@ -42,7 +51,43 @@ interface VerifySubdelegationInput extends VerifyBase {
42
51
  parent: ChainLink;
43
52
  skipTemporalCheck?: boolean;
44
53
  now?: Date;
54
+ decryptScopesWith?: {
55
+ device_id: string;
56
+ secretKey: Uint8Array;
57
+ };
45
58
  }
46
59
  declare function verifySubdelegation(input: VerifySubdelegationInput): Promise<VerifySubdelegationResult>;
47
60
 
48
- export { ActionEnvelope, AgentError, AgentErrorCode, ChainLink, DEFAULT_MAX_CHAIN_DEPTH, DelegationEnvelope, RevocationEnvelope, SubdelegationEnvelope, ValidationOptions, type VerifyActionInput, VerifyActionResult, type VerifyBase, type VerifyDelegationInput, VerifyDelegationResult, type VerifyRevocationInput, VerifyRevocationResult, type VerifySubdelegationInput, VerifySubdelegationResult, verifyAction, verifyDelegation, verifyRevocation, verifySubdelegation };
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
+ };
92
+
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;
@@ -352,6 +353,50 @@ function canonicalRevocationBytes(env) {
352
353
  function sha256Hex(bytes) {
353
354
  return canonical.hexEncode(sha256.sha256(bytes));
354
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
355
400
  var DEFAULT_MAX_CHAIN_DEPTH = 5;
356
401
  var AgentError = class extends Error {
357
402
  constructor(code, message) {
@@ -367,19 +412,63 @@ async function verifyDelegation(input) {
367
412
  }
368
413
  const shape = checkDelegationShape(env);
369
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
+ }
370
459
  let canonicalScopes;
371
460
  try {
372
- for (const s of env.scopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
373
- canonicalScopes = canonicalizeScopes(env.scopes);
461
+ for (const s of workingScopes) validateScope(parseScope(s), { mode: input.scopeMode ?? "strict" });
462
+ canonicalScopes = canonicalizeScopes(workingScopes);
374
463
  } catch (e) {
375
464
  const msg = e instanceof ScopeParseError ? e.message : e.message;
376
465
  return err("E_BAD_SCOPE_GRAMMAR", msg);
377
466
  }
378
467
  for (let i = 0; i < canonicalScopes.length; i++) {
379
- if (env.scopes[i] !== canonicalScopes[i]) {
468
+ if (workingScopes[i] !== canonicalScopes[i]) {
380
469
  return err(
381
470
  "E_BAD_SCOPE_GRAMMAR",
382
- `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]}`
383
472
  );
384
473
  }
385
474
  }
@@ -416,9 +505,10 @@ async function verifyDelegation(input) {
416
505
  if (now < issued) return err("E_NOT_YET_VALID", `delegation not valid until ${env.issued_at}`);
417
506
  if (now >= expires) return err("E_EXPIRED", `delegation expired at ${env.expires_at}`);
418
507
  }
508
+ const returnedEnvelope = env.scopes_encrypted !== void 0 ? { ...env, scopes: canonicalScopes } : env;
419
509
  return {
420
510
  ok: true,
421
- envelope: env,
511
+ envelope: returnedEnvelope,
422
512
  canonicalMessage: reconstructedMessage,
423
513
  id: env.id
424
514
  };
@@ -439,17 +529,23 @@ async function verifyAction(input) {
439
529
  verifyBip322: input.verifyBip322,
440
530
  skipSignatureVerification: input.skipSignatureVerification,
441
531
  scopeMode: input.scopeMode,
442
- skipTemporalCheck: true
532
+ skipTemporalCheck: true,
443
533
  // action window check dominates
534
+ ...input.decryptScopesWith ? { decryptScopesWith: input.decryptScopesWith } : {}
444
535
  });
445
536
  if (!dr.ok) return dr;
446
- let parent = d;
537
+ const rootHydrated = dr.envelope;
538
+ let parent = rootHydrated;
539
+ const hydratedChain = [];
447
540
  for (const sub of chain) {
448
- const r = await verifyChainLink(sub, parent, input);
541
+ const hydrated = await hydrateSubdelegationScopes(sub, input);
542
+ if (!hydrated.ok) return hydrated;
543
+ const r = await verifyChainLink(hydrated.envelope, parent, input);
449
544
  if (!r.ok) return r;
450
- parent = sub;
545
+ hydratedChain.push(hydrated.envelope);
546
+ parent = hydrated.envelope;
451
547
  }
452
- const leaf = chain.length > 0 ? chain[chain.length - 1] : d;
548
+ const leaf = hydratedChain.length > 0 ? hydratedChain[hydratedChain.length - 1] : rootHydrated;
453
549
  if (a.v !== ENVELOPE_VERSION) {
454
550
  return err("E_UNSUPPORTED_VERSION", `action version ${a.v} not supported`);
455
551
  }
@@ -489,6 +585,9 @@ async function verifyAction(input) {
489
585
  if (signed < issued || signed >= expires) {
490
586
  return err("E_OUT_OF_WINDOW", `action.signed_at ${a.signed_at} is outside leaf window [${leaf.issued_at}, ${leaf.expires_at})`);
491
587
  }
588
+ if (!leaf.scopes) {
589
+ return err("E_SCOPES_NEITHER_PROVIDED", "leaf has no plaintext scopes after hydration");
590
+ }
492
591
  let exercised, accepted;
493
592
  try {
494
593
  exercised = canonicalizeScope(parseScope(a.scope_exercised));
@@ -502,7 +601,7 @@ async function verifyAction(input) {
502
601
  }
503
602
  if (!accepted) return err("E_SCOPE_DENIED", `scope_exercised (${exercised}) not a sub-scope of any granted scope`);
504
603
  if (input.revocations && input.revocations.length > 0) {
505
- const allLinks = [d, ...chain];
604
+ const allLinks = [rootHydrated, ...hydratedChain];
506
605
  for (const link of allLinks) {
507
606
  for (const rev of input.revocations) {
508
607
  if (rev.delegation_id !== link.id) continue;
@@ -553,12 +652,56 @@ async function verifyAction(input) {
553
652
  envelope: a,
554
653
  canonicalMessage: reconstructedMessage,
555
654
  id: a.id,
556
- delegation: d,
557
- chain,
655
+ delegation: rootHydrated,
656
+ chain: hydratedChain,
558
657
  scopeExercised: exercised,
559
658
  anchor
560
659
  };
561
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
+ }
562
705
  async function verifyRevocation(input) {
563
706
  const env = input.envelope;
564
707
  const d = input.delegation;
@@ -600,7 +743,10 @@ function checkDelegationShape(env) {
600
743
  if (!isHex64(env.id)) return err("E_MALFORMED", "id must be 64 lowercase hex chars");
601
744
  if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
602
745
  if (!env.agent?.address || env.agent.alg !== "bip322") return err("E_MALFORMED", "agent invalid");
603
- 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
+ }
604
750
  if (env.bond !== null) {
605
751
  if (!Number.isInteger(env.bond.sats) || env.bond.sats < 0) return err("E_MALFORMED", "bond.sats must be non-negative integer");
606
752
  if (!isHex64(env.bond.attestation_id)) return err("E_MALFORMED", "bond.attestation_id must be 64-hex");
@@ -644,7 +790,10 @@ function checkSubdelegationShape(env) {
644
790
  if (!isHex64(env.parent_id)) return err("E_MALFORMED", "parent_id must be 64-hex");
645
791
  if (!env.principal?.address || env.principal.alg !== "bip322") return err("E_MALFORMED", "principal invalid");
646
792
  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");
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
+ }
648
797
  if ("bond" in env && env.bond !== void 0) {
649
798
  return err("E_MALFORMED", "sub-delegation envelopes MUST NOT carry a bond field");
650
799
  }
@@ -661,6 +810,12 @@ async function verifyChainLink(s, parent, input) {
661
810
  }
662
811
  const shape = checkSubdelegationShape(s);
663
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
+ }
664
819
  let canonicalScopesList;
665
820
  try {
666
821
  canonicalScopesList = canonicalizeScopes(s.scopes);
@@ -722,6 +877,12 @@ async function verifyChainLink(s, parent, input) {
722
877
  `subdelegation window [${s.issued_at}, ${s.expires_at}) is not contained in parent's [${parent.issued_at}, ${parent.expires_at})`
723
878
  );
724
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
+ }
725
886
  const parentScopesParsed = parent.scopes.map((str) => parseScope(str));
726
887
  for (let i = 0; i < parsedScopes.length; i++) {
727
888
  const childScope = parsedScopes[i];
@@ -742,7 +903,9 @@ async function verifyChainLink(s, parent, input) {
742
903
  };
743
904
  }
744
905
  async function verifySubdelegation(input) {
745
- const r = await verifyChainLink(input.envelope, input.parent, 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);
746
909
  if (!r.ok) return r;
747
910
  if (!input.skipTemporalCheck) {
748
911
  const now = (input.now ?? /* @__PURE__ */ new Date()).getTime();
@@ -811,16 +974,21 @@ exports.computeActionId = computeActionId;
811
974
  exports.computeDelegationId = computeDelegationId;
812
975
  exports.computeRevocationId = computeRevocationId;
813
976
  exports.computeSubdelegationId = computeSubdelegationId;
977
+ exports.decodeScopesPayload = decodeScopesPayload;
814
978
  exports.delegationCanonicalBytes = delegationCanonicalBytes;
815
979
  exports.delegationCanonicalMessage = delegationCanonicalMessage;
980
+ exports.encodeScopesPayload = encodeScopesPayload;
981
+ exports.hasPrivateScopes = hasPrivateScopes;
816
982
  exports.isSubScope = isSubScope;
817
983
  exports.parseAndCanonicalizeScopes = parseAndCanonicalizeScopes;
818
984
  exports.parseScope = parseScope;
819
985
  exports.revocationCanonicalBytes = revocationCanonicalBytes;
820
986
  exports.revocationCanonicalMessage = revocationCanonicalMessage;
987
+ exports.sealScopes = sealScopes;
821
988
  exports.sha256Hex = sha256Hex;
822
989
  exports.subdelegationCanonicalBytes = subdelegationCanonicalBytes;
823
990
  exports.subdelegationCanonicalMessage = subdelegationCanonicalMessage;
991
+ exports.unsealScopes = unsealScopes;
824
992
  exports.validateScope = validateScope;
825
993
  exports.verifyAction = verifyAction;
826
994
  exports.verifyDelegation = verifyDelegation;