@ixo/ucan 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.
Files changed (51) hide show
  1. package/.eslintrc.js +9 -0
  2. package/.prettierignore +3 -0
  3. package/.prettierrc.js +4 -0
  4. package/.turbo/turbo-build.log +4 -0
  5. package/CHANGELOG.md +0 -0
  6. package/README.md +189 -0
  7. package/dist/capabilities/capability.d.ts +33 -0
  8. package/dist/capabilities/capability.d.ts.map +1 -0
  9. package/dist/capabilities/capability.js +53 -0
  10. package/dist/capabilities/capability.js.map +1 -0
  11. package/dist/client/create-client.d.ts +33 -0
  12. package/dist/client/create-client.d.ts.map +1 -0
  13. package/dist/client/create-client.js +104 -0
  14. package/dist/client/create-client.js.map +1 -0
  15. package/dist/did/ixo-resolver.d.ts +8 -0
  16. package/dist/did/ixo-resolver.d.ts.map +1 -0
  17. package/dist/did/ixo-resolver.js +162 -0
  18. package/dist/did/ixo-resolver.js.map +1 -0
  19. package/dist/did/utils.d.ts +4 -0
  20. package/dist/did/utils.d.ts.map +1 -0
  21. package/dist/did/utils.js +85 -0
  22. package/dist/did/utils.js.map +1 -0
  23. package/dist/index.d.ts +13 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +12 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/store/memory.d.ts +25 -0
  28. package/dist/store/memory.d.ts.map +1 -0
  29. package/dist/store/memory.js +71 -0
  30. package/dist/store/memory.js.map +1 -0
  31. package/dist/tsconfig.tsbuildinfo +1 -0
  32. package/dist/types.d.ts +29 -0
  33. package/dist/types.d.ts.map +1 -0
  34. package/dist/types.js +2 -0
  35. package/dist/types.js.map +1 -0
  36. package/dist/validator/validator.d.ts +29 -0
  37. package/dist/validator/validator.d.ts.map +1 -0
  38. package/dist/validator/validator.js +179 -0
  39. package/dist/validator/validator.js.map +1 -0
  40. package/jest.config.js +3 -0
  41. package/package.json +78 -0
  42. package/scripts/test-ucan.ts +457 -0
  43. package/src/capabilities/capability.ts +244 -0
  44. package/src/client/create-client.ts +329 -0
  45. package/src/did/ixo-resolver.ts +325 -0
  46. package/src/did/utils.ts +141 -0
  47. package/src/index.ts +135 -0
  48. package/src/store/memory.ts +194 -0
  49. package/src/types.ts +108 -0
  50. package/src/validator/validator.ts +399 -0
  51. package/tsconfig.json +18 -0
@@ -0,0 +1,141 @@
1
+ // =============================================================================
2
+ // Base58 Encoding/Decoding (Bitcoin alphabet)
3
+ // =============================================================================
4
+
5
+ const BASE58_ALPHABET =
6
+ '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
7
+
8
+ // Create lookup table for faster decoding
9
+ const BASE58_MAP: Record<string, number> = {};
10
+ for (let i = 0; i < BASE58_ALPHABET.length; i++) {
11
+ BASE58_MAP[BASE58_ALPHABET[i]!] = i;
12
+ }
13
+
14
+ /**
15
+ * Decode a Base58-encoded string to bytes
16
+ *
17
+ * Algorithm: Treat input as base-58 number, convert to base-256
18
+ */
19
+ export function base58Decode(str: string): Uint8Array {
20
+ if (str.length === 0) {
21
+ return new Uint8Array(0);
22
+ }
23
+
24
+ // Count leading '1's (which represent leading zero bytes)
25
+ let leadingZeros = 0;
26
+ for (const char of str) {
27
+ if (char === '1') {
28
+ leadingZeros++;
29
+ } else {
30
+ break;
31
+ }
32
+ }
33
+
34
+ // Allocate enough space for the result
35
+ // Base58 uses ~5.86 bits per character, so we need at most ceil(len * log(58) / log(256)) bytes
36
+ const size = Math.ceil((str.length * Math.log(58)) / Math.log(256));
37
+ const bytes = new Uint8Array(size);
38
+
39
+ // Process each character
40
+ for (const char of str) {
41
+ const value = BASE58_MAP[char];
42
+ if (value === undefined) {
43
+ throw new Error(`Invalid Base58 character: ${char}`);
44
+ }
45
+
46
+ // Multiply existing bytes by 58 and add the new value
47
+ let carry = value;
48
+ for (let i = size - 1; i >= 0; i--) {
49
+ const current = bytes[i]! * 58 + carry;
50
+ bytes[i] = current % 256;
51
+ carry = Math.floor(current / 256);
52
+ }
53
+ }
54
+
55
+ // Find where the actual data starts (skip leading zeros in result)
56
+ let start = 0;
57
+ while (start < bytes.length && bytes[start] === 0) {
58
+ start++;
59
+ }
60
+
61
+ // Combine leading zero bytes with the decoded data
62
+ const result = new Uint8Array(leadingZeros + (bytes.length - start));
63
+ // Leading zeros are already 0 in a new Uint8Array
64
+ result.set(bytes.subarray(start), leadingZeros);
65
+
66
+ return result;
67
+ }
68
+
69
+ /**
70
+ * Encode bytes to Base58 string
71
+ *
72
+ * Algorithm: Treat input as base-256 number, convert to base-58
73
+ */
74
+ export function base58Encode(bytes: Uint8Array): string {
75
+ if (bytes.length === 0) {
76
+ return '';
77
+ }
78
+
79
+ // Count leading zeros
80
+ let leadingZeros = 0;
81
+ for (const byte of bytes) {
82
+ if (byte === 0) {
83
+ leadingZeros++;
84
+ } else {
85
+ break;
86
+ }
87
+ }
88
+
89
+ // Allocate enough space for the result
90
+ // We need at most ceil(len * log(256) / log(58)) digits
91
+ const size = Math.ceil((bytes.length * Math.log(256)) / Math.log(58));
92
+ const digits = new Uint8Array(size);
93
+
94
+ // Process each byte
95
+ for (const byte of bytes) {
96
+ let carry = byte;
97
+ for (let i = size - 1; i >= 0; i--) {
98
+ const current = digits[i]! * 256 + carry;
99
+ digits[i] = current % 58;
100
+ carry = Math.floor(current / 58);
101
+ }
102
+ }
103
+
104
+ // Find where the actual data starts
105
+ let start = 0;
106
+ while (start < digits.length && digits[start] === 0) {
107
+ start++;
108
+ }
109
+
110
+ // Build result string
111
+ let result = '1'.repeat(leadingZeros);
112
+ for (let i = start; i < digits.length; i++) {
113
+ result += BASE58_ALPHABET[digits[i]!];
114
+ }
115
+
116
+ return result;
117
+ }
118
+
119
+ // =============================================================================
120
+ // Hex Encoding/Decoding
121
+ // =============================================================================
122
+
123
+ /**
124
+ * Decode a hex string to bytes
125
+ */
126
+ export function hexDecode(hex: string): Uint8Array {
127
+ // Remove optional 0x prefix
128
+ const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
129
+
130
+ if (cleanHex.length % 2 !== 0) {
131
+ throw new Error('Invalid hex string: odd length');
132
+ }
133
+
134
+ const bytes = new Uint8Array(cleanHex.length / 2);
135
+ for (let i = 0; i < bytes.length; i++) {
136
+ bytes[i] = parseInt(cleanHex.slice(i * 2, i * 2 + 2), 16);
137
+ }
138
+
139
+ return bytes;
140
+ }
141
+
package/src/index.ts ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @fileoverview @ixo/ucan - UCAN authorization for any service
3
+ *
4
+ * This package provides UCAN (User Controlled Authorization Networks) support
5
+ * built on top of the battle-tested ucanto library.
6
+ *
7
+ * Features:
8
+ * - Generic capability definitions (define your own)
9
+ * - Framework-agnostic validator (works with Express, Fastify, etc.)
10
+ * - Client helpers for creating delegations and invocations
11
+ * - did:ixo resolution via IXO blockchain indexer (optional)
12
+ * - In-memory invocation store for replay protection
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * // 1. Define your capabilities
17
+ * import { defineCapability, createUCANValidator, generateKeypair } from '@ixo/ucan';
18
+ *
19
+ * const EmployeesRead = defineCapability({
20
+ * can: 'employees/read',
21
+ * protocol: 'myapp:'
22
+ * });
23
+ *
24
+ * // 2. Create validator
25
+ * const validator = createUCANValidator({
26
+ * serverDid: 'did:key:z6Mk...',
27
+ * rootIssuers: ['did:key:z6MkAdmin...'],
28
+ * });
29
+ *
30
+ * // 3. Validate in your route (any framework)
31
+ * app.post('/protected', async (req, res) => {
32
+ * const result = await validator.validate(req.body.invocation, {
33
+ * can: 'employees/read',
34
+ * with: 'myapp:employees'
35
+ * });
36
+ *
37
+ * if (!result.ok) {
38
+ * return res.status(403).json({ error: result.error });
39
+ * }
40
+ *
41
+ * res.json({ employees: [...] });
42
+ * });
43
+ * ```
44
+ *
45
+ * @packageDocumentation
46
+ */
47
+
48
+ // =============================================================================
49
+ // Re-export ucanto packages for advanced usage
50
+ // =============================================================================
51
+
52
+ export * as UcantoServer from '@ucanto/server';
53
+ export * as UcantoClient from '@ucanto/client';
54
+ export * as UcantoValidator from '@ucanto/validator';
55
+ export * as UcantoPrincipal from '@ucanto/principal';
56
+ export { ed25519, Verifier } from '@ucanto/principal';
57
+
58
+ // =============================================================================
59
+ // Types
60
+ // =============================================================================
61
+
62
+ export type {
63
+ IxoDID,
64
+ KeyDID,
65
+ SupportedDID,
66
+ DIDKeyResolutionResult,
67
+ DIDKeyResolver,
68
+ InvocationStore,
69
+ ValidationResult,
70
+ SerializedInvocation,
71
+ } from './types.js';
72
+
73
+ // =============================================================================
74
+ // Capability Definition
75
+ // =============================================================================
76
+
77
+ export {
78
+ defineCapability,
79
+ Schema,
80
+ type DefineCapabilityOptions,
81
+ } from './capabilities/capability.js';
82
+
83
+ // =============================================================================
84
+ // Validator
85
+ // =============================================================================
86
+
87
+ export {
88
+ createUCANValidator,
89
+ type CreateValidatorOptions,
90
+ type ValidateResult,
91
+ type UCANValidator,
92
+ } from './validator/validator.js';
93
+
94
+ // =============================================================================
95
+ // Client Helpers (for creating delegations and invocations)
96
+ // =============================================================================
97
+
98
+ export {
99
+ generateKeypair,
100
+ parseSigner,
101
+ signerFromMnemonic,
102
+ createDelegation,
103
+ createInvocation,
104
+ serializeInvocation,
105
+ serializeDelegation,
106
+ parseDelegation,
107
+ type Signer,
108
+ type Delegation,
109
+ type Capability,
110
+ } from './client/create-client.js';
111
+
112
+ // =============================================================================
113
+ // DID Resolution (optional, for did:ixo support)
114
+ // =============================================================================
115
+
116
+ export {
117
+ createIxoDIDResolver,
118
+ createCompositeDIDResolver,
119
+ type IxoDIDResolverConfig,
120
+ } from './did/ixo-resolver.js';
121
+
122
+ // =============================================================================
123
+ // Store (for replay protection)
124
+ // =============================================================================
125
+
126
+ export {
127
+ InMemoryInvocationStore,
128
+ createInvocationStore,
129
+ } from './store/memory.js';
130
+
131
+ // =============================================================================
132
+ // Version
133
+ // =============================================================================
134
+
135
+ export const VERSION = '1.0.0';
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @fileoverview In-memory invocation store for replay protection
3
+ *
4
+ * This module provides a simple in-memory implementation of the InvocationStore
5
+ * interface for tracking used invocation CIDs to prevent replay attacks.
6
+ *
7
+ * For production use with multiple instances, consider using a distributed
8
+ * store like Redis.
9
+ */
10
+
11
+ import type { InvocationStore } from '../types.js';
12
+
13
+ /**
14
+ * Entry in the invocation store
15
+ */
16
+ interface StoreEntry {
17
+ /** Timestamp when this entry expires */
18
+ expiresAt: number;
19
+ }
20
+
21
+ /**
22
+ * In-memory implementation of InvocationStore for replay protection
23
+ *
24
+ * Features:
25
+ * - Automatic TTL-based expiration
26
+ * - Periodic cleanup of expired entries
27
+ * - Thread-safe (single-threaded JS)
28
+ *
29
+ * Limitations:
30
+ * - Data is lost on process restart
31
+ * - Not suitable for distributed deployments
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * const store = new InMemoryInvocationStore();
36
+ *
37
+ * // Check if invocation was already used
38
+ * if (await store.has(invocationCid)) {
39
+ * throw new Error('Replay attack detected');
40
+ * }
41
+ *
42
+ * // Mark invocation as used
43
+ * await store.add(invocationCid);
44
+ * ```
45
+ */
46
+ export class InMemoryInvocationStore implements InvocationStore {
47
+ private store = new Map<string, StoreEntry>();
48
+ private cleanupInterval: ReturnType<typeof setInterval> | null = null;
49
+
50
+ /** Default TTL: 24 hours */
51
+ private readonly defaultTtlMs: number;
52
+
53
+ /** Cleanup interval: 1 hour */
54
+ private readonly cleanupIntervalMs: number;
55
+
56
+ /**
57
+ * Create a new in-memory invocation store
58
+ *
59
+ * @param options - Configuration options
60
+ * @param options.defaultTtlMs - Default TTL for entries (default: 24 hours)
61
+ * @param options.cleanupIntervalMs - Interval for cleanup (default: 1 hour)
62
+ * @param options.enableAutoCleanup - Whether to enable automatic cleanup (default: true)
63
+ */
64
+ constructor(
65
+ options: {
66
+ defaultTtlMs?: number;
67
+ cleanupIntervalMs?: number;
68
+ enableAutoCleanup?: boolean;
69
+ } = {},
70
+ ) {
71
+ this.defaultTtlMs = options.defaultTtlMs ?? 24 * 60 * 60 * 1000; // 24 hours
72
+ this.cleanupIntervalMs = options.cleanupIntervalMs ?? 60 * 60 * 1000; // 1 hour
73
+
74
+ if (options.enableAutoCleanup !== false) {
75
+ this.startAutoCleanup();
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Check if an invocation CID has already been used
81
+ *
82
+ * @param cid - The CID of the invocation
83
+ * @returns True if the CID has been used and is not expired
84
+ */
85
+ async has(cid: string): Promise<boolean> {
86
+ const entry = this.store.get(cid);
87
+ if (!entry) {
88
+ return false;
89
+ }
90
+
91
+ // Check if expired
92
+ if (Date.now() > entry.expiresAt) {
93
+ this.store.delete(cid);
94
+ return false;
95
+ }
96
+
97
+ return true;
98
+ }
99
+
100
+ /**
101
+ * Mark an invocation CID as used
102
+ *
103
+ * @param cid - The CID of the invocation
104
+ * @param ttlMs - Time-to-live in milliseconds (default: 24 hours)
105
+ */
106
+ async add(cid: string, ttlMs?: number): Promise<void> {
107
+ const ttl = ttlMs ?? this.defaultTtlMs;
108
+ this.store.set(cid, {
109
+ expiresAt: Date.now() + ttl,
110
+ });
111
+ }
112
+
113
+ /**
114
+ * Remove all expired entries from the store
115
+ */
116
+ async cleanup(): Promise<void> {
117
+ const now = Date.now();
118
+ let cleaned = 0;
119
+
120
+ for (const [cid, entry] of this.store.entries()) {
121
+ if (now > entry.expiresAt) {
122
+ this.store.delete(cid);
123
+ cleaned++;
124
+ }
125
+ }
126
+
127
+ if (cleaned > 0) {
128
+ console.log(`[InMemoryInvocationStore] Cleaned up ${cleaned} expired entries`);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Get the current size of the store
134
+ */
135
+ get size(): number {
136
+ return this.store.size;
137
+ }
138
+
139
+ /**
140
+ * Clear all entries from the store
141
+ */
142
+ clear(): void {
143
+ this.store.clear();
144
+ }
145
+
146
+ /**
147
+ * Start automatic cleanup interval
148
+ */
149
+ private startAutoCleanup(): void {
150
+ if (this.cleanupInterval) {
151
+ return;
152
+ }
153
+
154
+ this.cleanupInterval = setInterval(() => {
155
+ void this.cleanup();
156
+ }, this.cleanupIntervalMs);
157
+
158
+ // Don't prevent process from exiting
159
+ if (this.cleanupInterval.unref) {
160
+ this.cleanupInterval.unref();
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Stop automatic cleanup and release resources
166
+ */
167
+ destroy(): void {
168
+ if (this.cleanupInterval) {
169
+ clearInterval(this.cleanupInterval);
170
+ this.cleanupInterval = null;
171
+ }
172
+ this.store.clear();
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Create an invocation store instance
178
+ * Factory function for easier testing and dependency injection
179
+ *
180
+ * @param options - Store configuration
181
+ * @returns An InvocationStore implementation
182
+ */
183
+ export function createInvocationStore(options?: {
184
+ defaultTtlMs?: number;
185
+ cleanupIntervalMs?: number;
186
+ enableAutoCleanup?: boolean;
187
+ }): InvocationStore {
188
+ return new InMemoryInvocationStore(options);
189
+ }
190
+
191
+ // TODO: Add Redis implementation for distributed deployments
192
+ // TODO: Add SQLite implementation for persistence across restarts
193
+ // TODO: Add metrics/monitoring for store size and cleanup operations
194
+
package/src/types.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @fileoverview Core type definitions for @ixo/ucan
3
+ *
4
+ * This module provides type definitions that extend ucanto's types
5
+ * for general use in any service that needs UCAN authorization.
6
+ */
7
+
8
+ import type { DID, Capability as UcantoCapability } from '@ucanto/interface';
9
+
10
+ // =============================================================================
11
+ // DID Types
12
+ // =============================================================================
13
+
14
+ /**
15
+ * IXO DID type (for IXO blockchain identities)
16
+ */
17
+ export type IxoDID = `did:ixo:${string}`;
18
+
19
+ /**
20
+ * Key DID type (self-describing public key DIDs)
21
+ */
22
+ export type KeyDID = `did:key:${string}`;
23
+
24
+ /**
25
+ * DIDs supported by this package
26
+ */
27
+ export type SupportedDID = IxoDID | KeyDID;
28
+
29
+ // =============================================================================
30
+ // DID Resolution
31
+ // =============================================================================
32
+
33
+ /**
34
+ * Result of DID key resolution
35
+ */
36
+ export interface DIDKeyResolutionResult {
37
+ /** Array of did:key identifiers that can verify signatures for this DID */
38
+ keys: KeyDID[];
39
+ }
40
+
41
+ /**
42
+ * DID key resolver function type
43
+ * Takes a DID and returns the associated did:key identifiers
44
+ */
45
+ export type DIDKeyResolver = (
46
+ did: DID,
47
+ ) => Promise<
48
+ { ok: KeyDID[] } | { error: { name: string; did: string; message: string } }
49
+ >;
50
+
51
+ // =============================================================================
52
+ // Invocation Store (Replay Protection)
53
+ // =============================================================================
54
+
55
+ /**
56
+ * Invocation store for replay protection
57
+ *
58
+ * Implementations can use in-memory, Redis, database, etc.
59
+ */
60
+ export interface InvocationStore {
61
+ /**
62
+ * Check if an invocation CID has already been used
63
+ * @param cid - The CID of the invocation
64
+ */
65
+ has(cid: string): Promise<boolean>;
66
+
67
+ /**
68
+ * Mark an invocation CID as used
69
+ * @param cid - The CID of the invocation
70
+ * @param ttlMs - Time-to-live in milliseconds (for cleanup)
71
+ */
72
+ add(cid: string, ttlMs?: number): Promise<void>;
73
+
74
+ /**
75
+ * Remove expired entries (optional cleanup method)
76
+ */
77
+ cleanup?(): Promise<void>;
78
+ }
79
+
80
+ // =============================================================================
81
+ // Validation
82
+ // =============================================================================
83
+
84
+ /**
85
+ * Result of UCAN validation
86
+ */
87
+ export interface ValidationResult {
88
+ /** Whether the validation succeeded */
89
+ valid: boolean;
90
+
91
+ /** Error message if validation failed */
92
+ error?: string;
93
+
94
+ /** The DID of the invoker (if valid) */
95
+ invokerDid?: string;
96
+
97
+ /** The validated capability (if valid) */
98
+ capability?: UcantoCapability;
99
+ }
100
+
101
+ // =============================================================================
102
+ // Client Configuration
103
+ // =============================================================================
104
+
105
+ /**
106
+ * Serialized invocation that can be sent in HTTP requests
107
+ */
108
+ export type SerializedInvocation = string;