@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.
@@ -12,27 +12,38 @@ import {
12
12
  computeActionId,
13
13
  computeDelegationId,
14
14
  computeRevocationId,
15
+ computeSubdelegationId,
15
16
  delegationCanonicalMessage,
16
17
  revocationCanonicalMessage,
18
+ subdelegationCanonicalMessage,
17
19
  } from './canonical.js';
18
- import { verifyAction, verifyDelegation, verifyRevocation } from './verify.js';
20
+ import { parseScope, ScopeParseError } from './scope.js';
21
+ import { verifyAction, verifyDelegation, verifyRevocation, verifySubdelegation } from './verify.js';
19
22
  import type {
20
23
  ActionEnvelope,
24
+ AgentErrorCode,
21
25
  DelegationEnvelope,
22
26
  RevocationEnvelope,
27
+ SubdelegationEnvelope,
23
28
  } from './types.js';
24
29
 
25
30
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
- const VECTORS_DIR = resolve(__dirname, '..', '..', '..', 'oc-agent-protocol', 'test-vectors');
31
+ const VECTORS_DIR =
32
+ process.env.OC_AGENT_VECTORS_DIR ??
33
+ resolve(__dirname, '..', '..', '..', 'oc-agent-protocol', 'test-vectors');
27
34
 
28
35
  interface BaseVector {
29
36
  description: string;
30
- kind: 'delegation' | 'action' | 'revocation';
37
+ kind: 'delegation' | 'action' | 'revocation' | 'subdelegation';
31
38
  expected: {
32
39
  canonical_message: string;
33
40
  canonical_message_bytes_len: number;
34
41
  id: string;
35
- envelope: DelegationEnvelope | ActionEnvelope | RevocationEnvelope;
42
+ envelope:
43
+ | DelegationEnvelope
44
+ | ActionEnvelope
45
+ | RevocationEnvelope
46
+ | SubdelegationEnvelope;
36
47
  };
37
48
  }
38
49
 
@@ -75,7 +86,106 @@ interface RevocationVector extends BaseVector {
75
86
  expected: BaseVector['expected'] & { envelope: RevocationEnvelope };
76
87
  }
77
88
 
78
- type Vector = DelegationVector | ActionVector | RevocationVector;
89
+ interface SubdelegationVector extends BaseVector {
90
+ kind: 'subdelegation';
91
+ inputs: {
92
+ parent_id: string;
93
+ principal: string;
94
+ agent: string;
95
+ scopes: string[];
96
+ issued_at: string;
97
+ expires_at: string;
98
+ nonce: string;
99
+ };
100
+ expected: BaseVector['expected'] & { envelope: SubdelegationEnvelope };
101
+ }
102
+
103
+ // Negative vectors carry "negative": true at the top level and document an
104
+ // expected verifier rejection (SPEC §11 error code) instead of a canonical-
105
+ // message round-trip.
106
+ interface NegativeVectorBase {
107
+ description: string;
108
+ kind: 'delegation' | 'action' | 'revocation';
109
+ negative: true;
110
+ expected: {
111
+ verification_outcome: 'REJECT';
112
+ error_code: AgentErrorCode;
113
+ spec_reference: string;
114
+ rejection_reason: string;
115
+ };
116
+ }
117
+
118
+ interface NegativeActionVector extends NegativeVectorBase {
119
+ kind: 'action';
120
+ inputs: {
121
+ address: string;
122
+ content_hash: string;
123
+ content_length: number;
124
+ content_mime: string;
125
+ signed_at: string;
126
+ delegation_id: string;
127
+ scope_exercised: string;
128
+ content_ref?: string | null;
129
+ ots?: unknown;
130
+ };
131
+ }
132
+
133
+ interface NegativeRevocationVector extends NegativeVectorBase {
134
+ kind: 'revocation';
135
+ inputs: {
136
+ address: string;
137
+ delegation_id: string;
138
+ reason: string;
139
+ signed_at: string;
140
+ };
141
+ }
142
+
143
+ interface NegativeDelegationVector extends NegativeVectorBase {
144
+ kind: 'delegation';
145
+ inputs: {
146
+ principal: string;
147
+ agent: string;
148
+ scopes: string[];
149
+ bond: { sats: number; attestation_id: string } | null;
150
+ issued_at: string;
151
+ expires_at: string;
152
+ nonce: string;
153
+ };
154
+ additional_malformed_examples_for_implementer_smoke_tests?: {
155
+ scope: string;
156
+ why: string;
157
+ }[];
158
+ }
159
+
160
+ interface NegativeSubdelegationVector extends NegativeVectorBase {
161
+ kind: 'subdelegation';
162
+ inputs: {
163
+ parent_id: string;
164
+ principal: string;
165
+ agent: string;
166
+ scopes: string[];
167
+ issued_at: string;
168
+ expires_at: string;
169
+ nonce: string;
170
+ };
171
+ }
172
+
173
+ type NegativeVector =
174
+ | NegativeActionVector
175
+ | NegativeRevocationVector
176
+ | NegativeDelegationVector
177
+ | NegativeSubdelegationVector;
178
+
179
+ type Vector =
180
+ | DelegationVector
181
+ | ActionVector
182
+ | RevocationVector
183
+ | SubdelegationVector
184
+ | NegativeVector;
185
+
186
+ function isNegative(v: Vector): v is NegativeVector {
187
+ return 'negative' in v && v.negative === true;
188
+ }
79
189
 
80
190
  async function loadVectors(): Promise<{ name: string; data: Vector }[]> {
81
191
  try {
@@ -100,16 +210,22 @@ describe('oc-agent-protocol test vectors', () => {
100
210
  return;
101
211
  }
102
212
 
103
- // First pass: the vectors can cross-reference each other (action cites delegation id).
104
- // We'll need the envelopes keyed for the verifyAction tests.
213
+ // First pass: the vectors can cross-reference each other (action cites delegation id;
214
+ // subdelegation cites parent id which may be a root or another sub).
215
+ // Key both kinds so chain walking works.
105
216
  const delegationEnvelopes = new Map<string, DelegationEnvelope>();
217
+ const subdelegationEnvelopes = new Map<string, SubdelegationEnvelope>();
106
218
  for (const { data } of vectors) {
219
+ if (isNegative(data)) continue;
107
220
  if (data.kind === 'delegation') {
108
221
  delegationEnvelopes.set(data.expected.id, data.expected.envelope);
222
+ } else if (data.kind === 'subdelegation') {
223
+ subdelegationEnvelopes.set(data.expected.id, data.expected.envelope);
109
224
  }
110
225
  }
111
226
 
112
227
  for (const { name, data } of vectors) {
228
+ if (isNegative(data)) continue;
113
229
  it(`${name} — canonical message reconstructs byte-identical`, () => {
114
230
  const msg = reconstructCanonical(data);
115
231
  expect(msg).toBe(data.expected.canonical_message);
@@ -143,7 +259,7 @@ describe('oc-agent-protocol test vectors', () => {
143
259
  skipSignatureVerification: true,
144
260
  });
145
261
  expect(r.ok).toBe(true);
146
- } else {
262
+ } else if (data.kind === 'revocation') {
147
263
  const delegation = delegationEnvelopes.get(data.inputs.delegation_id);
148
264
  if (!delegation) {
149
265
  throw new Error(`revocation vector ${name} references missing delegation ${data.inputs.delegation_id}`);
@@ -154,12 +270,36 @@ describe('oc-agent-protocol test vectors', () => {
154
270
  skipSignatureVerification: true,
155
271
  });
156
272
  expect(r.ok).toBe(true);
273
+ } else {
274
+ // subdelegation — parent may be a root delegation or another subdelegation.
275
+ const parentId = data.inputs.parent_id;
276
+ const parent =
277
+ delegationEnvelopes.get(parentId) ??
278
+ subdelegationEnvelopes.get(parentId);
279
+ if (!parent) {
280
+ throw new Error(
281
+ `subdelegation vector ${name} references missing parent ${parentId}`
282
+ );
283
+ }
284
+ const r = await verifySubdelegation({
285
+ envelope: data.expected.envelope,
286
+ parent,
287
+ skipSignatureVerification: true,
288
+ skipTemporalCheck: true,
289
+ });
290
+ expect(r.ok).toBe(true);
157
291
  }
158
292
  });
159
293
  }
160
294
  });
161
295
 
162
- function reconstructCanonical(v: Vector): string {
296
+ type PositiveVector =
297
+ | DelegationVector
298
+ | ActionVector
299
+ | RevocationVector
300
+ | SubdelegationVector;
301
+
302
+ function reconstructCanonical(v: PositiveVector): string {
163
303
  if (v.kind === 'delegation') {
164
304
  // Canonicalize each scope (constraints sorted by key) then sort the list.
165
305
  const canonical = canonicalizeScopes(v.inputs.scopes);
@@ -177,10 +317,23 @@ function reconstructCanonical(v: Vector): string {
177
317
  if (v.kind === 'action') {
178
318
  return actionCanonicalMessage(v.inputs);
179
319
  }
180
- return revocationCanonicalMessage(v.inputs);
320
+ if (v.kind === 'revocation') {
321
+ return revocationCanonicalMessage(v.inputs);
322
+ }
323
+ // subdelegation
324
+ const canonical = canonicalizeScopes(v.inputs.scopes);
325
+ return subdelegationCanonicalMessage({
326
+ parent_id: v.inputs.parent_id,
327
+ principal: v.inputs.principal,
328
+ agent: v.inputs.agent,
329
+ scopes: canonical,
330
+ issued_at: v.inputs.issued_at,
331
+ expires_at: v.inputs.expires_at,
332
+ nonce: v.inputs.nonce,
333
+ });
181
334
  }
182
335
 
183
- function reconstructId(v: Vector): string {
336
+ function reconstructId(v: PositiveVector): string {
184
337
  if (v.kind === 'delegation') {
185
338
  const canonical = canonicalizeScopes(v.inputs.scopes);
186
339
  return computeDelegationId({
@@ -195,5 +348,198 @@ function reconstructId(v: Vector): string {
195
348
  });
196
349
  }
197
350
  if (v.kind === 'action') return computeActionId(v.inputs);
198
- return computeRevocationId(v.inputs);
351
+ if (v.kind === 'revocation') return computeRevocationId(v.inputs);
352
+ // subdelegation
353
+ const canonical = canonicalizeScopes(v.inputs.scopes);
354
+ return computeSubdelegationId({
355
+ parent_id: v.inputs.parent_id,
356
+ principal: v.inputs.principal,
357
+ agent: v.inputs.agent,
358
+ scopes: canonical,
359
+ issued_at: v.inputs.issued_at,
360
+ expires_at: v.inputs.expires_at,
361
+ nonce: v.inputs.nonce,
362
+ });
199
363
  }
364
+
365
+ // ─────────────────────────────────────────────────────────────────────────────
366
+ // Negative vectors — verifier MUST reject with the declared error code.
367
+ // ─────────────────────────────────────────────────────────────────────────────
368
+
369
+ describe('oc-agent-protocol negative test vectors', () => {
370
+ const negatives = vectors.filter(({ data }) => isNegative(data)) as {
371
+ name: string;
372
+ data: NegativeVector;
373
+ }[];
374
+
375
+ if (negatives.length === 0) {
376
+ it.skip('(no negative vectors found)', () => {});
377
+ return;
378
+ }
379
+
380
+ // Need positive delegation envelopes to feed verifyAction / verifyRevocation
381
+ // for the cross-referenced negative vectors.
382
+ const delegationEnvelopes = new Map<string, DelegationEnvelope>();
383
+ for (const { data } of vectors) {
384
+ if (!isNegative(data) && data.kind === 'delegation') {
385
+ delegationEnvelopes.set(data.expected.id, data.expected.envelope);
386
+ }
387
+ }
388
+
389
+ for (const { name, data } of negatives) {
390
+ it(`${name} — verifier rejects with ${data.expected.error_code}`, async () => {
391
+ if (data.kind === 'action') {
392
+ const v = data as NegativeActionVector;
393
+ const delegation = delegationEnvelopes.get(v.inputs.delegation_id);
394
+ if (!delegation) {
395
+ throw new Error(
396
+ `negative action vector ${name} cites missing delegation ${v.inputs.delegation_id}`
397
+ );
398
+ }
399
+ // Build a syntactically well-formed action envelope from the
400
+ // inputs (the rejection here is semantic — scope / window —
401
+ // not structural). The id MUST be the real sha256(canonical)
402
+ // or the verifier will short-circuit with E_BAD_ID before
403
+ // reaching the rule we're actually testing.
404
+ const realId = computeActionId({
405
+ address: v.inputs.address,
406
+ content_hash: v.inputs.content_hash,
407
+ content_length: v.inputs.content_length,
408
+ content_mime: v.inputs.content_mime,
409
+ signed_at: v.inputs.signed_at,
410
+ delegation_id: v.inputs.delegation_id,
411
+ scope_exercised: v.inputs.scope_exercised,
412
+ });
413
+ const action: ActionEnvelope = {
414
+ v: 1,
415
+ kind: 'agent-action',
416
+ id: realId,
417
+ content: {
418
+ hash: v.inputs.content_hash,
419
+ length: v.inputs.content_length,
420
+ mime: v.inputs.content_mime,
421
+ ref: v.inputs.content_ref ?? null,
422
+ },
423
+ signer: { address: v.inputs.address, alg: 'bip322' },
424
+ signed_at: v.inputs.signed_at,
425
+ delegation_id: v.inputs.delegation_id,
426
+ scope_exercised: v.inputs.scope_exercised,
427
+ ots: null,
428
+ sig: { alg: 'bip322', pubkey: v.inputs.address, value: 'AAAA' },
429
+ };
430
+ const r = await verifyAction({
431
+ action,
432
+ delegation,
433
+ skipSignatureVerification: true,
434
+ });
435
+ expect(r.ok).toBe(false);
436
+ if (!r.ok) {
437
+ // v07 specifically allows either E_OUT_OF_WINDOW or E_EXPIRED
438
+ // since both fire for an action signed past the delegation's
439
+ // expires_at; harness_assertion documents this.
440
+ if (
441
+ v.expected.error_code === 'E_OUT_OF_WINDOW' &&
442
+ r.code === 'E_EXPIRED'
443
+ ) {
444
+ // accept the alternate code
445
+ } else {
446
+ expect(r.code).toBe(v.expected.error_code);
447
+ }
448
+ }
449
+ } else if (data.kind === 'revocation') {
450
+ const v = data as NegativeRevocationVector;
451
+ const delegation = delegationEnvelopes.get(v.inputs.delegation_id);
452
+ if (!delegation) {
453
+ throw new Error(
454
+ `negative revocation vector ${name} cites missing delegation ${v.inputs.delegation_id}`
455
+ );
456
+ }
457
+ const realRevId = computeRevocationId({
458
+ address: v.inputs.address,
459
+ delegation_id: v.inputs.delegation_id,
460
+ reason: v.inputs.reason,
461
+ signed_at: v.inputs.signed_at,
462
+ });
463
+ const revocation: RevocationEnvelope = {
464
+ v: 1,
465
+ kind: 'agent-revocation',
466
+ id: realRevId,
467
+ delegation_id: v.inputs.delegation_id,
468
+ signer: { address: v.inputs.address, alg: 'bip322' },
469
+ reason: v.inputs.reason,
470
+ signed_at: v.inputs.signed_at,
471
+ ots: null,
472
+ sig: { alg: 'bip322', pubkey: v.inputs.address, value: 'AAAA' },
473
+ };
474
+ const r = await verifyRevocation({
475
+ envelope: revocation,
476
+ delegation,
477
+ skipSignatureVerification: true,
478
+ });
479
+ expect(r.ok).toBe(false);
480
+ if (!r.ok) expect(r.code).toBe(v.expected.error_code);
481
+ } else if (data.kind === 'delegation') {
482
+ // Delegation negative vectors target the §7.1 grammar parser
483
+ // directly — that's where E_BAD_SCOPE_GRAMMAR fires per
484
+ // SPEC §8.1 step 4. Asserting parseScope throws is the
485
+ // right level of rigor.
486
+ const v = data as NegativeDelegationVector;
487
+ expect(v.expected.error_code).toBe('E_BAD_SCOPE_GRAMMAR');
488
+ for (const s of v.inputs.scopes) {
489
+ expect(() => parseScope(s)).toThrow(ScopeParseError);
490
+ }
491
+ for (const ex of v.additional_malformed_examples_for_implementer_smoke_tests ??
492
+ []) {
493
+ expect(
494
+ () => parseScope(ex.scope),
495
+ `additional malformed example: ${ex.why}`
496
+ ).toThrow(ScopeParseError);
497
+ }
498
+ } else if (data.kind === 'subdelegation') {
499
+ // v12, v13, v14 — chain-link checks via verifySubdelegation.
500
+ // Build a structurally valid subdelegation envelope from inputs
501
+ // (the rejection here is semantic — scope / window / linkage —
502
+ // not structural) and feed it to verifySubdelegation against the
503
+ // root delegation vector.
504
+ const v = data as NegativeSubdelegationVector;
505
+ const parent = delegationEnvelopes.get(v.inputs.parent_id);
506
+ if (!parent) {
507
+ throw new Error(
508
+ `negative subdelegation vector ${name} references missing parent ${v.inputs.parent_id}`
509
+ );
510
+ }
511
+ const realId = computeSubdelegationId({
512
+ parent_id: v.inputs.parent_id,
513
+ principal: v.inputs.principal,
514
+ agent: v.inputs.agent,
515
+ scopes: canonicalizeScopes(v.inputs.scopes),
516
+ issued_at: v.inputs.issued_at,
517
+ expires_at: v.inputs.expires_at,
518
+ nonce: v.inputs.nonce,
519
+ });
520
+ const sub: SubdelegationEnvelope = {
521
+ v: 1,
522
+ kind: 'agent-subdelegation',
523
+ id: realId,
524
+ parent_id: v.inputs.parent_id,
525
+ principal: { address: v.inputs.principal, alg: 'bip322' },
526
+ agent: { address: v.inputs.agent, alg: 'bip322' },
527
+ scopes: v.inputs.scopes,
528
+ issued_at: v.inputs.issued_at,
529
+ expires_at: v.inputs.expires_at,
530
+ nonce: v.inputs.nonce,
531
+ revocation: { holders: ['principal'], ref: null },
532
+ sig: { alg: 'bip322', pubkey: v.inputs.principal, value: 'AAAA' },
533
+ };
534
+ const r = await verifySubdelegation({
535
+ envelope: sub,
536
+ parent,
537
+ skipSignatureVerification: true,
538
+ skipTemporalCheck: true,
539
+ });
540
+ expect(r.ok).toBe(false);
541
+ if (!r.ok) expect(r.code).toBe(v.expected.error_code);
542
+ }
543
+ });
544
+ }
545
+ });
package/src/types.ts CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  export const ENVELOPE_VERSION = 1 as const;
4
4
 
5
- export type EnvelopeKind = 'agent-delegation' | 'agent-action' | 'agent-revocation';
5
+ export type EnvelopeKind =
6
+ | 'agent-delegation'
7
+ | 'agent-action'
8
+ | 'agent-revocation'
9
+ | 'agent-subdelegation';
6
10
 
7
11
  // ─────────────────────────────────────────────────────────────────────────────
8
12
  // Shared building blocks
@@ -40,14 +44,59 @@ export interface DelegationRevocationRef {
40
44
  ref: string | null;
41
45
  }
42
46
 
47
+ /**
48
+ * v1.2 private-scope mode: a wholesale OC Lock v2 LockEnvelope wrapping the
49
+ * canonical scope list as its payload. We re-import the LockEnvelope type
50
+ * structurally rather than depending on the @orangecheck/lock-core type — agent-
51
+ * core's type surface stays loose so verifiers can be authored in either an
52
+ * agent-only or full-family setup.
53
+ */
54
+ export interface ScopesEncryptedEnvelope {
55
+ v: 2;
56
+ kind: 'identity';
57
+ id: string;
58
+ alg: { kem: 'x25519'; aead: 'aes-256-gcm'; kdf: 'hkdf-sha256' };
59
+ from: { address: string; attestation_id?: string };
60
+ recipients: Array<{
61
+ address: string;
62
+ device_id: string;
63
+ device_pk: string;
64
+ eph_pk: string;
65
+ wrapped_key: string;
66
+ nonce_kek: string;
67
+ }>;
68
+ ciphertext: string;
69
+ nonce_ct: string;
70
+ hint?: string;
71
+ created_at: string;
72
+ expires_at: string | null;
73
+ payment: unknown | null;
74
+ sig: { alg: 'bip322'; pubkey: string; value: string };
75
+ }
76
+
43
77
  export interface DelegationEnvelope {
44
78
  v: typeof ENVELOPE_VERSION;
45
79
  kind: 'agent-delegation';
46
80
  id: string; // 64-hex sha256(canonical_message)
47
81
  principal: ActorRef;
48
82
  agent: ActorRef;
49
- /** Sorted lexicographically in the canonical message; stored in sorted order on the envelope too. */
50
- scopes: string[];
83
+ /**
84
+ * v1.0 / v1.1 public mode: sorted lexicographically in the canonical
85
+ * message; stored in sorted order on the envelope too.
86
+ *
87
+ * v1.2 private mode: this field is OMITTED from the envelope JSON;
88
+ * `scopes_encrypted` is set instead. After decryption, the recovered
89
+ * scope list takes this field's place in the in-memory envelope object
90
+ * for the remainder of verification.
91
+ */
92
+ scopes?: string[];
93
+ /**
94
+ * v1.2 private mode (PRIVATE-SCOPE.md §1.1): an OC Lock v2 envelope
95
+ * wrapping the canonical scope-list bytes as its payload, sealed to one
96
+ * or more recipients (typically the agent ± named verifiers). MUTUALLY
97
+ * EXCLUSIVE with `scopes`.
98
+ */
99
+ scopes_encrypted?: ScopesEncryptedEnvelope;
51
100
  bond: DelegationBond | null;
52
101
  issued_at: string; // ISO 8601 UTC
53
102
  expires_at: string; // ISO 8601 UTC
@@ -111,6 +160,46 @@ export interface ActionCanonicalInput {
111
160
  scope_exercised: string;
112
161
  }
113
162
 
163
+ // ─────────────────────────────────────────────────────────────────────────────
164
+ // Sub-delegation (SUB-DELEGATION.md, v1.1)
165
+ // ─────────────────────────────────────────────────────────────────────────────
166
+
167
+ export interface SubdelegationEnvelope {
168
+ v: typeof ENVELOPE_VERSION;
169
+ kind: 'agent-subdelegation';
170
+ id: string; // 64-hex sha256(canonical_message)
171
+ parent_id: string; // 64-hex; the immediate parent envelope's id
172
+ /** The sub-principal — equal to parent.agent.address. */
173
+ principal: ActorRef;
174
+ /** The recipient sub-agent. */
175
+ agent: ActorRef;
176
+ /**
177
+ * v1.0 / v1.1 public mode: each scope MUST be a sub-scope of some scope
178
+ * on the parent. v1.2 private mode: omitted; `scopes_encrypted` set.
179
+ */
180
+ scopes?: string[];
181
+ /** v1.2 private mode — same shape as the root delegation field. */
182
+ scopes_encrypted?: ScopesEncryptedEnvelope;
183
+ issued_at: string; // ISO 8601 UTC; >= parent.issued_at
184
+ expires_at: string; // ISO 8601 UTC; <= parent.expires_at
185
+ nonce: string; // 32-hex random
186
+ revocation: DelegationRevocationRef;
187
+ sig: Signature;
188
+ }
189
+
190
+ export interface SubdelegationCanonicalInput {
191
+ parent_id: string;
192
+ principal: string;
193
+ agent: string;
194
+ scopes: string[]; // pre-canonicalized, pre-sorted
195
+ issued_at: string;
196
+ expires_at: string;
197
+ nonce: string;
198
+ }
199
+
200
+ /** Either a root or a sub envelope; chain links walk up to a root delegation. */
201
+ export type ChainLink = DelegationEnvelope | SubdelegationEnvelope;
202
+
114
203
  // ─────────────────────────────────────────────────────────────────────────────
115
204
  // Revocation (SPEC §9)
116
205
  // ─────────────────────────────────────────────────────────────────────────────
@@ -157,7 +246,15 @@ export type AgentErrorCode =
157
246
  | 'E_BOND_UNMET'
158
247
  | 'E_BOND_UNVERIFIED'
159
248
  | 'E_REVOKER_UNAUTHORIZED'
160
- | 'E_CALENDAR_UNREACHABLE';
249
+ | 'E_CALENDAR_UNREACHABLE'
250
+ | 'E_SUBDELEGATION_DEPTH_EXCEEDED'
251
+ | 'E_SUBDELEGATION_PRINCIPAL_MISMATCH'
252
+ | 'E_SUBDELEGATION_EXPIRES_EXTENDED'
253
+ | 'E_SUBDELEGATION_SCOPE_ESCALATED'
254
+ | 'E_SCOPES_BOTH_PROVIDED'
255
+ | 'E_SCOPES_NEITHER_PROVIDED'
256
+ | 'E_SCOPES_UNREADABLE'
257
+ | 'E_BAD_LOCK_ENVELOPE';
161
258
 
162
259
  export interface VerifyOk<T> {
163
260
  ok: true;
@@ -174,9 +271,17 @@ export interface VerifyErr {
174
271
 
175
272
  export type VerifyDelegationResult = VerifyOk<DelegationEnvelope> | VerifyErr;
176
273
  export type VerifyRevocationResult = VerifyOk<RevocationEnvelope> | VerifyErr;
274
+ export type VerifySubdelegationResult = VerifyOk<SubdelegationEnvelope> | VerifyErr;
177
275
 
178
276
  export interface VerifyActionOkExtra {
277
+ /** The ROOT delegation rooting the authority chain. */
179
278
  delegation: DelegationEnvelope;
279
+ /**
280
+ * The sub-delegation chain `[S_1, …, S_leaf]`. Empty when the action cites
281
+ * the root directly. The action's authority leaf is
282
+ * `chain[chain.length - 1] ?? delegation`.
283
+ */
284
+ chain: SubdelegationEnvelope[];
180
285
  scopeExercised: string;
181
286
  anchor:
182
287
  | { status: 'none' }