@ixo/ucan 1.0.0 → 1.1.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.
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-console */
1
2
  /**
2
3
  * @fileoverview Framework-agnostic UCAN validator
3
4
  *
@@ -14,13 +15,13 @@
14
15
  import { ed25519 } from '@ucanto/principal';
15
16
  import { Delegation } from '@ucanto/core';
16
17
  import { claim } from '@ucanto/validator';
17
- import { capability } from '@ucanto/validator';
18
+ import { type capability } from '@ucanto/validator';
18
19
  import type { DIDKeyResolver, InvocationStore } from '../types.js';
19
20
  import { InMemoryInvocationStore } from '../store/memory.js';
20
21
 
21
22
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
23
  type CapabilityParser = ReturnType<typeof capability<any, any, any>>;
23
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+
24
25
  type Verifier = ReturnType<typeof ed25519.Verifier.parse>;
25
26
 
26
27
  /**
@@ -76,6 +77,21 @@ export interface ValidateResult {
76
77
  nb?: Record<string, unknown>;
77
78
  };
78
79
 
80
+ /**
81
+ * Effective expiration as Unix timestamp (seconds).
82
+ * This is the earliest expiration across the entire delegation chain,
83
+ * i.e. when the authorization effectively expires.
84
+ * Undefined if no expiration is set (never expires).
85
+ */
86
+ expiration?: number;
87
+
88
+ /**
89
+ * The delegation chain from root issuer to invoker.
90
+ * e.g. ["did:key:root", "did:key:alice", "did:key:bob"]
91
+ * For a direct root invocation (no delegation), this is just ["did:key:root"].
92
+ */
93
+ proofChain?: string[];
94
+
79
95
  /** Error details (if invalid) */
80
96
  error?: {
81
97
  code:
@@ -227,9 +243,7 @@ export async function createUCANValidator(
227
243
 
228
244
  // Try custom resolver for other DID methods (e.g., did:ixo)
229
245
  if (options.didResolver) {
230
- const result = await options.didResolver(
231
- did as `did:${string}:${string}`,
232
- );
246
+ const result = await options.didResolver(did);
233
247
  if ('ok' in result && result.ok.length > 0) {
234
248
  // Return the array of did:key strings (ucanto will parse them)
235
249
  return { ok: result.ok };
@@ -254,6 +268,43 @@ export async function createUCANValidator(
254
268
  };
255
269
  };
256
270
 
271
+ /**
272
+ * Build the delegation chain as an array of DIDs from root to invoker.
273
+ * Recursively traverses the first proof of each delegation.
274
+ */
275
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- delegation type from Delegation.extract() is complex
276
+ function buildProofChain(delegation: any): string[] {
277
+ if (!delegation?.proofs || delegation.proofs.length === 0) {
278
+ return [delegation.issuer.did()];
279
+ }
280
+ const parentChain = buildProofChain(delegation.proofs[0]);
281
+ return [...parentChain, delegation.issuer.did()];
282
+ }
283
+
284
+ /**
285
+ * Compute the effective (earliest) expiration across the entire delegation chain.
286
+ * Returns undefined if no expiration is set anywhere in the chain.
287
+ */
288
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
289
+ function computeEffectiveExpiration(delegation: any): number | undefined {
290
+ const exp =
291
+ typeof delegation?.expiration === 'number' &&
292
+ isFinite(delegation.expiration)
293
+ ? delegation.expiration
294
+ : undefined;
295
+
296
+ if (!delegation?.proofs || delegation.proofs.length === 0) {
297
+ return exp;
298
+ }
299
+
300
+ const parentExp = computeEffectiveExpiration(delegation.proofs[0]);
301
+
302
+ if (exp !== undefined && parentExp !== undefined) {
303
+ return Math.min(exp, parentExp);
304
+ }
305
+ return exp ?? parentExp;
306
+ }
307
+
257
308
  return {
258
309
  serverDid: options.serverDid,
259
310
 
@@ -264,7 +315,9 @@ export async function createUCANValidator(
264
315
  ): Promise<ValidateResult> {
265
316
  try {
266
317
  // 1. Decode the invocation from base64 CAR
267
- const carBytes = new Uint8Array(Buffer.from(invocationBase64, 'base64'));
318
+ const carBytes = new Uint8Array(
319
+ Buffer.from(invocationBase64, 'base64'),
320
+ );
268
321
 
269
322
  // 2. Extract the invocation from CAR
270
323
  const extracted = await Delegation.extract(carBytes);
@@ -320,8 +373,9 @@ export async function createUCANValidator(
320
373
  const claimResult = claim(capabilityDef, [invocation], {
321
374
  authority: serverVerifier,
322
375
  principal: ed25519.Verifier,
376
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ucanto claim() expects a specific DID resolver signature incompatible with our async resolver
323
377
  resolveDIDKey: resolveDIDKey as any,
324
- canIssue: (cap: any, issuer: string) => {
378
+ canIssue: (cap: { with: string }, issuer: string) => {
325
379
  // Root issuers can issue any capability
326
380
  if (options.rootIssuers.includes(issuer)) return true;
327
381
  // Allow self-issued capabilities where resource contains issuer DID
@@ -379,6 +433,10 @@ export async function createUCANValidator(
379
433
  await invocationStore.add(invocationCid);
380
434
  }
381
435
 
436
+ // 9. Build proof chain and compute effective expiration
437
+ const proofChain = buildProofChain(invocation);
438
+ const expiration = computeEffectiveExpiration(invocation);
439
+
382
440
  return {
383
441
  ok: true,
384
442
  invoker: invocation.issuer.did(),
@@ -389,6 +447,8 @@ export async function createUCANValidator(
389
447
  nb: validatedCap.nb as Record<string, unknown> | undefined,
390
448
  }
391
449
  : undefined,
450
+ expiration,
451
+ proofChain,
392
452
  };
393
453
  } catch (err) {
394
454
  const message = err instanceof Error ? err.message : 'Unknown error';
package/tsconfig.json CHANGED
@@ -14,5 +14,5 @@
14
14
  "skipLibCheck": true
15
15
  },
16
16
  "include": ["src/**/*"],
17
- "exclude": ["node_modules", "dist"]
17
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
18
18
  }
@@ -0,0 +1,2 @@
1
+ import config from '@ixo/vitest-config/nest';
2
+ export default config;
package/.eslintrc.js DELETED
@@ -1,9 +0,0 @@
1
- /** @type {import("eslint").Linter.Config} */
2
- module.exports = {
3
- extends: ['@ixo/eslint-config/nest.js'],
4
- parserOptions: {
5
- project: 'tsconfig.json',
6
- tsconfigRootDir: __dirname,
7
- sourceType: 'module',
8
- },
9
- };
package/.prettierignore DELETED
@@ -1,3 +0,0 @@
1
- pnpm-lock.yaml
2
-
3
- node_modules
package/.prettierrc.js DELETED
@@ -1,4 +0,0 @@
1
- /** @type {import("prettier").Config} */
2
- module.exports = {
3
- ...require('@ixo/eslint-config/prettier-base'),
4
- };
package/CHANGELOG.md DELETED
File without changes
package/jest.config.js DELETED
@@ -1,3 +0,0 @@
1
- import config from '@ixo/jest-config/nest.js';
2
-
3
- export default config;