holosphere 2.0.0-alpha20 → 2.0.0-alpha22

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 (69) hide show
  1. package/README.md +1 -2
  2. package/dist/cjs/holosphere.cjs +1 -1
  3. package/dist/esm/holosphere.js +43 -41
  4. package/dist/{index-COpLk9gL.cjs → index-B4xe-N5-.cjs} +2 -2
  5. package/dist/{index-COpLk9gL.cjs.map → index-B4xe-N5-.cjs.map} +1 -1
  6. package/dist/{index-D2WstuZJ.js → index-Bug_CGNq.js} +2 -2
  7. package/dist/{index-D2WstuZJ.js.map → index-Bug_CGNq.js.map} +1 -1
  8. package/dist/index-CaCPzdlv.cjs +29 -0
  9. package/dist/index-CaCPzdlv.cjs.map +1 -0
  10. package/dist/{index-B6-8KAQm.js → index-D76zMgwU.js} +2 -2
  11. package/dist/{index-B6-8KAQm.js.map → index-D76zMgwU.js.map} +1 -1
  12. package/dist/{index--QsHG_gD.cjs → index-DuOuk96g.cjs} +2 -2
  13. package/dist/{index--QsHG_gD.cjs.map → index-DuOuk96g.cjs.map} +1 -1
  14. package/dist/{index-BHptWysv.js → index-bYHRpACA.js} +2951 -7736
  15. package/dist/index-bYHRpACA.js.map +1 -0
  16. package/dist/{indexeddb-storage-kQ53UHEE.js → indexeddb-storage-BrIwr42m.js} +2 -2
  17. package/dist/{indexeddb-storage-kQ53UHEE.js.map → indexeddb-storage-BrIwr42m.js.map} +1 -1
  18. package/dist/{indexeddb-storage-wKG4mICM.cjs → indexeddb-storage-CFWfkdX9.cjs} +2 -2
  19. package/dist/{indexeddb-storage-wKG4mICM.cjs.map → indexeddb-storage-CFWfkdX9.cjs.map} +1 -1
  20. package/dist/{memory-storage-DnXCSbBl.js → memory-storage-BDQRj-2j.js} +2 -2
  21. package/dist/{memory-storage-DnXCSbBl.js.map → memory-storage-BDQRj-2j.js.map} +1 -1
  22. package/dist/{memory-storage-CGC8xM2G.cjs → memory-storage-bkatDnuR.cjs} +2 -2
  23. package/dist/{memory-storage-CGC8xM2G.cjs.map → memory-storage-bkatDnuR.cjs.map} +1 -1
  24. package/examples/demo.html +2 -29
  25. package/package.json +3 -8
  26. package/src/content/social-protocols.js +3 -59
  27. package/src/core/holosphere.js +16 -554
  28. package/src/crypto/nostr-utils.js +98 -1
  29. package/src/crypto/secp256k1.js +13 -411
  30. package/src/federation/discovery.js +7 -75
  31. package/src/federation/handshake.js +69 -202
  32. package/src/federation/hologram.js +275 -460
  33. package/src/federation/index.js +2 -9
  34. package/src/federation/registry.js +65 -1169
  35. package/src/federation/request-card.js +21 -35
  36. package/src/hierarchical/upcast.js +4 -9
  37. package/src/index.js +142 -296
  38. package/src/lib/federation-methods.js +388 -886
  39. package/src/storage/global-tables.js +1 -1
  40. package/src/storage/nostr-wrapper.js +9 -5
  41. package/src/subscriptions/manager.js +1 -1
  42. package/types/index.d.ts +145 -37
  43. package/bin/holosphere-activitypub.js +0 -158
  44. package/dist/2019-BzVkRcax.js +0 -6680
  45. package/dist/2019-BzVkRcax.js.map +0 -1
  46. package/dist/2019-C1hPR_Os.cjs +0 -8
  47. package/dist/2019-C1hPR_Os.cjs.map +0 -1
  48. package/dist/browser-BcmACE3G.js +0 -3058
  49. package/dist/browser-BcmACE3G.js.map +0 -1
  50. package/dist/browser-DaqYUTcG.cjs +0 -2
  51. package/dist/browser-DaqYUTcG.cjs.map +0 -1
  52. package/dist/index-BHptWysv.js.map +0 -1
  53. package/dist/index-CDlhzxT2.cjs +0 -29
  54. package/dist/index-CDlhzxT2.cjs.map +0 -1
  55. package/src/federation/capabilities.js +0 -162
  56. package/src/storage/backend-factory.js +0 -130
  57. package/src/storage/backend-interface.js +0 -161
  58. package/src/storage/backends/activitypub/server.js +0 -675
  59. package/src/storage/backends/activitypub-backend.js +0 -295
  60. package/src/storage/backends/gundb-backend.js +0 -875
  61. package/src/storage/backends/nostr-backend.js +0 -251
  62. package/src/storage/gun-async.js +0 -341
  63. package/src/storage/gun-auth.js +0 -373
  64. package/src/storage/gun-federation.js +0 -785
  65. package/src/storage/gun-references.js +0 -209
  66. package/src/storage/gun-schema.js +0 -306
  67. package/src/storage/gun-wrapper.js +0 -642
  68. package/src/storage/migration.js +0 -351
  69. package/src/storage/unified-storage.js +0 -161
@@ -9,7 +9,7 @@
9
9
  * @module crypto/nostr-utils
10
10
  */
11
11
 
12
- import { nip04, nip44, nip19, getPublicKey as nostrGetPublicKey, finalizeEvent, verifyEvent as nostrVerifyEvent } from 'nostr-tools';
12
+ import { nip04, nip44, nip19, getPublicKey as nostrGetPublicKey, finalizeEvent, verifyEvent as nostrVerifyEvent, generateSecretKey } from 'nostr-tools';
13
13
 
14
14
  // Re-export NDK types for consumers who want to use NDK directly
15
15
  export { default as NDK, NDKEvent, NDKPrivateKeySigner, NDKUser, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk';
@@ -113,6 +113,103 @@ export function npubToHex(npub) {
113
113
  }
114
114
  }
115
115
 
116
+ /**
117
+ * Convert a hex private key to nsec format.
118
+ * @param {string} hexPrivKey - Hex private key (64 characters)
119
+ * @returns {string} nsec-encoded private key (nsec1...)
120
+ */
121
+ export function hexToNsec(hexPrivKey) {
122
+ try {
123
+ return nip19.nsecEncode(hexToBytes(hexPrivKey));
124
+ } catch (e) {
125
+ console.error('Failed to encode hex to nsec:', e);
126
+ return hexPrivKey; // Return original on error
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Convert nsec to hex private key.
132
+ * @param {string} nsec - nsec-encoded private key string
133
+ * @returns {string|null} 64-character hex private key or null if invalid
134
+ */
135
+ export function nsecToHex(nsec) {
136
+ try {
137
+ const decoded = nip19.decode(nsec);
138
+ if (decoded.type === 'nsec') {
139
+ return bytesToHex(decoded.data);
140
+ }
141
+ return null;
142
+ } catch (e) {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Parse an nsec or hex private key string into hex format.
149
+ * Handles nostr: URI prefix and validates both nsec and hex formats.
150
+ * @param {string} input - nsec string (nsec1...) or 64-character hex private key
151
+ * @returns {Object} Validation result with valid, hexPrivKey (if valid), error (if invalid)
152
+ */
153
+ export function parseNsecOrHex(input) {
154
+ const trimmed = input?.trim();
155
+
156
+ if (!trimmed) {
157
+ return { valid: false, error: 'Private key is required' };
158
+ }
159
+
160
+ // Check if it's already hex (64 characters)
161
+ if (/^[0-9a-fA-F]{64}$/.test(trimmed)) {
162
+ return { valid: true, hexPrivKey: trimmed.toLowerCase() };
163
+ }
164
+
165
+ // Handle nostr: URI prefix
166
+ let nsecString = trimmed;
167
+ if (nsecString.startsWith('nostr:')) {
168
+ nsecString = nsecString.slice(6);
169
+ }
170
+
171
+ // Try to decode as nsec
172
+ if (nsecString.startsWith('nsec1')) {
173
+ try {
174
+ const decoded = nip19.decode(nsecString);
175
+ if (decoded.type === 'nsec') {
176
+ return { valid: true, hexPrivKey: bytesToHex(decoded.data) };
177
+ }
178
+ return { valid: false, error: 'Invalid nsec format' };
179
+ } catch (e) {
180
+ return { valid: false, error: 'Invalid nsec: unable to decode' };
181
+ }
182
+ }
183
+
184
+ return { valid: false, error: 'Enter a valid nsec (nsec1...) or 64-character hex private key' };
185
+ }
186
+
187
+ /**
188
+ * Check if a string is a valid nsec (starts with nsec1 and decodes correctly).
189
+ * @param {string} str - String to validate
190
+ * @returns {boolean} True if valid nsec format
191
+ */
192
+ export function isValidNsec(str) {
193
+ if (typeof str !== 'string' || !str.startsWith('nsec1')) {
194
+ return false;
195
+ }
196
+ try {
197
+ const decoded = nip19.decode(str);
198
+ return decoded.type === 'nsec';
199
+ } catch {
200
+ return false;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Generate a new random private key.
206
+ * @returns {string} 64-character hex private key
207
+ */
208
+ export function generatePrivateKey() {
209
+ const secretKey = generateSecretKey();
210
+ return bytesToHex(secretKey);
211
+ }
212
+
116
213
  /**
117
214
  * Shorten a public key for display (first 8 and last 8 chars).
118
215
  * @param {string} pubKey - Public key in hex or npub format
@@ -1,16 +1,24 @@
1
1
  /**
2
2
  * @fileoverview Cryptographic Operations using secp256k1.
3
3
  *
4
- * Provides signing, verification, and capability token operations using
5
- * the secp256k1 elliptic curve (same curve used by Bitcoin and Nostr).
6
- * Uses lazy loading for the crypto module to improve startup performance.
4
+ * Provides signing and verification using the secp256k1 elliptic curve
5
+ * (same curve used by Bitcoin and Nostr).
7
6
  *
8
7
  * @module crypto/secp256k1
9
8
  */
10
9
 
11
10
  import { sha256 } from '@noble/hashes/sha256';
12
- import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
13
- import { secp256k1, schnorr } from '@noble/curves/secp256k1';
11
+ import { bytesToHex } from '@noble/hashes/utils';
12
+ import { schnorr } from '@noble/curves/secp256k1';
13
+
14
+ /**
15
+ * Check if a string is a 64-char hex public key (x-only schnorr format).
16
+ * @param {string} str - String to check
17
+ * @returns {boolean} True if string is a valid public key format
18
+ */
19
+ export function isPubkey(str) {
20
+ return typeof str === 'string' && /^[0-9a-f]{64}$/i.test(str);
21
+ }
14
22
 
15
23
  /**
16
24
  * Get public key from private key (x-only / schnorr format for Nostr compatibility)
@@ -74,412 +82,6 @@ export async function verify(content, signature, publicKey) {
74
82
  }
75
83
  }
76
84
 
77
- /**
78
- * Match token scope against requested scope with wildcard support
79
- * Supports:
80
- * - Exact match: { holonId: "abc", lensName: "quests" }
81
- * - Item-level: { holonId: "abc", lensName: "quests", dataId: "quest-001" }
82
- * - Wildcards: { holonId: "*", lensName: "*" } matches everything
83
- * @param {Object} tokenScope - Scope from capability token
84
- * @param {Object} requestedScope - Scope being requested
85
- * @returns {boolean} True if token scope covers requested scope
86
- */
87
- export function matchScope(tokenScope, requestedScope) {
88
- // Handle string scopes (legacy support)
89
- if (typeof tokenScope === 'string' || typeof requestedScope === 'string') {
90
- const tokenStr = typeof tokenScope === 'string' ? tokenScope : JSON.stringify(tokenScope);
91
- const reqStr = typeof requestedScope === 'string' ? requestedScope : JSON.stringify(requestedScope);
92
- return tokenStr === reqStr;
93
- }
94
-
95
- // Handle holonId
96
- if (tokenScope.holonId !== '*' && tokenScope.holonId !== requestedScope.holonId) {
97
- return false;
98
- }
99
-
100
- // Handle lensName
101
- if (tokenScope.lensName !== '*' && tokenScope.lensName !== requestedScope.lensName) {
102
- return false;
103
- }
104
-
105
- // Handle optional dataId (item-level scope)
106
- // If token has specific dataId (not wildcard), it must match requested dataId
107
- if (tokenScope.dataId && tokenScope.dataId !== '*') {
108
- if (requestedScope.dataId && tokenScope.dataId !== requestedScope.dataId) {
109
- return false;
110
- }
111
- }
112
-
113
- return true;
114
- }
115
-
116
- /**
117
- * Issue capability token - UNIFIED MODEL
118
- *
119
- * Supports both self-capabilities (issuer === recipient) and cross-capabilities.
120
- * Self-capabilities are used for same-author federation, providing consistent
121
- * security model across all federation types.
122
- *
123
- * @param {string[]} permissions - Permissions array (e.g., ['read', 'write', 'delete'])
124
- * @param {Object|string} scope - Scope (holon/lens path or object). Supports wildcards: { holonId: "*", lensName: "*" }
125
- * @param {string} recipient - Recipient public key (can be same as issuer for self-capability)
126
- * @param {Object} options - Options
127
- * @param {number} options.expiresIn - Expiration in milliseconds (default: 1 hour, longer for self-caps)
128
- * @param {string} options.issuer - Issuer ID/public key
129
- * @param {string} options.issuerKey - Issuer private key for signing
130
- * @param {boolean} options.isSelfCapability - If true, marks as self-capability (auto-detected if issuer === recipient)
131
- * @returns {Promise<string>} Capability token (base64-encoded JWT-like)
132
- */
133
- export async function issueCapability(permissions, scope, recipient, options = {}) {
134
- const { expiresIn = 3600000, issuer = 'holosphere', issuerKey, isSelfCapability } = options;
135
-
136
- // Validate permissions
137
- if (!Array.isArray(permissions) || permissions.length === 0) {
138
- throw new Error('Permissions array cannot be empty');
139
- }
140
-
141
- // Validate scope - now supports wildcards
142
- if (typeof scope === 'object') {
143
- // holonId is required (can be '*' for wildcard)
144
- if (scope.holonId === undefined || scope.holonId === '') {
145
- throw new Error('Invalid scope: holonId is required (use "*" for wildcard)');
146
- }
147
- // lensName is required (can be '*' for wildcard)
148
- if (scope.lensName === undefined || scope.lensName === '') {
149
- throw new Error('Invalid scope: lensName is required (use "*" for wildcard)');
150
- }
151
- // dataId is optional (can be specific value, '*', or omitted)
152
- } else if (typeof scope === 'string' && scope === '') {
153
- throw new Error('Invalid scope: cannot be empty string');
154
- }
155
-
156
- // Validate issuerKey if provided
157
- if (issuerKey && (typeof issuerKey !== 'string' || issuerKey.length < 32)) {
158
- throw new Error('Invalid issuer key');
159
- }
160
-
161
- // Detect self-capability (issuer === recipient)
162
- const selfCap = isSelfCapability !== undefined ? isSelfCapability : (issuer === recipient);
163
-
164
- const token = {
165
- type: 'capability',
166
- permissions,
167
- scope,
168
- recipient,
169
- issuer,
170
- isSelfCapability: selfCap, // Mark self-capabilities for clarity
171
- nonce: generateNonce(),
172
- issued: Date.now(),
173
- expires: expiresIn != null ? Date.now() + expiresIn : null,
174
- };
175
-
176
- // Encode token as base64
177
- // Use browser-native btoa when available to avoid Buffer serialization issues
178
- // (Buffer can serialize to {"type":"Buffer","data":[...]} in JSON)
179
- const payload = JSON.stringify(token);
180
- const encoded = typeof btoa === 'function'
181
- ? btoa(payload)
182
- : Buffer.from(payload).toString('base64');
183
-
184
- // If issuerKey provided, sign the token
185
- if (issuerKey) {
186
- const signature = await sign(payload, issuerKey);
187
- return `${encoded}.${signature}`;
188
- }
189
-
190
- return encoded;
191
- }
192
-
193
- /**
194
- * Issue a self-capability token - UNIFIED MODEL
195
- *
196
- * Convenience method for creating self-capabilities (same author federation).
197
- * Self-capabilities have longer expiration by default (1 year).
198
- *
199
- * @param {string[]} permissions - Permissions array
200
- * @param {Object} scope - Scope object { holonId, lensName, dataId? }
201
- * @param {string} authorPubKey - Author's public key (both issuer and recipient)
202
- * @param {Object} options - Options
203
- * @param {string} options.privateKey - Author's private key for signing
204
- * @param {number} [options.expiresIn=31536000000] - Expiration in ms (default: 1 year)
205
- * @returns {Promise<string>} Self-capability token
206
- */
207
- export async function issueSelfCapability(permissions, scope, authorPubKey, options = {}) {
208
- const { privateKey, expiresIn = 365 * 24 * 60 * 60 * 1000 } = options; // 1 year default
209
-
210
- return issueCapability(permissions, scope, authorPubKey, {
211
- expiresIn,
212
- issuer: authorPubKey,
213
- issuerKey: privateKey,
214
- isSelfCapability: true,
215
- });
216
- }
217
-
218
- /**
219
- * Verify capability token
220
- * @param {string|Object} token - Capability token (string or object)
221
- * @param {string} requiredPermission - Required permission
222
- * @param {Object|string} scope - Scope to check (supports wildcards in token scope)
223
- * @returns {Promise<boolean>} True if valid
224
- */
225
- export async function verifyCapability(token, requiredPermission, scope) {
226
- try {
227
- let tokenObj;
228
-
229
- // Handle capability object wrapper { token, scope, permissions }
230
- // This normalizes both formats: raw token string and capability info object
231
- if (token && typeof token === 'object' && token.token) {
232
- token = token.token; // Extract the actual token string
233
- }
234
-
235
- // Handle Buffer serialization format {"type":"Buffer","data":[...]}
236
- // This can happen when Buffer is polyfilled and JSON serialized
237
- if (token && typeof token === 'object' && token.type === 'Buffer' && Array.isArray(token.data)) {
238
- try {
239
- // Convert Buffer data array back to string
240
- token = String.fromCharCode.apply(null, token.data);
241
- } catch (e) {
242
- console.log('[verifyCapability] ❌ Failed to decode Buffer object:', e.message);
243
- return false;
244
- }
245
- }
246
-
247
- // Handle comma-separated byte string (e.g., "123,34,116,...")
248
- // This can happen when Uint8Array.toString() is called
249
- if (typeof token === 'string' && /^\d+(,\d+)+$/.test(token.substring(0, 50))) {
250
- try {
251
- const bytes = token.split(',').map(Number);
252
- token = String.fromCharCode.apply(null, bytes);
253
- } catch (e) {
254
- console.log('[verifyCapability] ❌ Failed to decode byte string:', e.message);
255
- return false;
256
- }
257
- }
258
-
259
- // Decode if string
260
- if (typeof token === 'string') {
261
- // Check if it's a base64-encoded token (starts with "ey" for JSON base64)
262
- if (token.startsWith('ey') || (!token.includes('.') && !token.startsWith('{'))) {
263
- // Base64 encoded (with or without signature)
264
- try {
265
- const payload = token.includes('.') ? token.split('.')[0] : token;
266
- // Prefer browser-native atob to avoid Buffer issues
267
- const decoded = typeof atob === 'function'
268
- ? atob(payload)
269
- : Buffer.from(payload, 'base64').toString('utf8');
270
- tokenObj = JSON.parse(decoded);
271
- } catch (e) {
272
- console.log('[verifyCapability] ❌ Token is not valid base64 JSON:', {
273
- preview: token.substring(0, 50),
274
- error: e.message
275
- });
276
- return false;
277
- }
278
- } else if (token.startsWith('{')) {
279
- // Already JSON string
280
- try {
281
- tokenObj = JSON.parse(token);
282
- } catch (e) {
283
- console.log('[verifyCapability] ❌ Token is not valid JSON:', {
284
- preview: token.substring(0, 50),
285
- error: e.message
286
- });
287
- return false;
288
- }
289
- } else {
290
- console.log('[verifyCapability] ❌ Unknown token format:', token.substring(0, 50));
291
- return false;
292
- }
293
- } else if (token && typeof token === 'object') {
294
- // Already decoded token object
295
- tokenObj = token;
296
- } else {
297
- console.log('[verifyCapability] ❌ Invalid token:', typeof token);
298
- return false;
299
- }
300
-
301
- if (!tokenObj || tokenObj.type !== 'capability') {
302
- console.log('[verifyCapability] ❌ Invalid token type:', { type: tokenObj?.type, tokenObj });
303
- return false;
304
- }
305
-
306
- // Check expiration (null/undefined expires = no expiration)
307
- if (tokenObj.expires != null && Date.now() > tokenObj.expires) {
308
- console.log('[verifyCapability] ❌ Token expired:', {
309
- expires: tokenObj.expires,
310
- now: Date.now(),
311
- expiredAgo: `${(Date.now() - tokenObj.expires) / 1000}s ago`
312
- });
313
- return false;
314
- }
315
-
316
- // Check scope using matchScope (supports wildcards)
317
- if (!matchScope(tokenObj.scope, scope)) {
318
- console.log('[verifyCapability] ❌ Scope mismatch:', {
319
- tokenScope: tokenObj.scope,
320
- requestedScope: scope
321
- });
322
- return false;
323
- }
324
-
325
- // Check permission
326
- if (!tokenObj.permissions.includes(requiredPermission)) {
327
- console.log('[verifyCapability] ❌ Permission denied:', {
328
- required: requiredPermission,
329
- has: tokenObj.permissions
330
- });
331
- return false;
332
- }
333
-
334
- console.log('[verifyCapability] ✅ Capability valid');
335
- return true;
336
- } catch (error) {
337
- console.log('[verifyCapability] ❌ Error:', error.message);
338
- return false;
339
- }
340
- }
341
-
342
- /**
343
- * Verify that a capability token was issued by the expected author.
344
- * This is the core security check for the object-capability model.
345
- * A hologram should only be resolvable if the capability issuer matches
346
- * the claimed data author AND the signature was made by that author's key.
347
- *
348
- * @param {string} token - Capability token
349
- * @param {string} expectedIssuer - Expected issuer public key (data author)
350
- * @returns {Promise<boolean>} True if issuer matches expected AND signature is valid
351
- */
352
- export async function verifyCapabilityIssuer(token, expectedIssuer) {
353
- try {
354
- // Handle capability object wrapper
355
- if (token && typeof token === 'object' && token.token) {
356
- token = token.token;
357
- }
358
-
359
- // Handle Buffer serialization format
360
- if (token && typeof token === 'object' && token.type === 'Buffer' && Array.isArray(token.data)) {
361
- token = String.fromCharCode.apply(null, token.data);
362
- }
363
-
364
- // Must be a string at this point
365
- if (typeof token !== 'string') {
366
- console.log('[verifyCapabilityIssuer] ❌ Token is not a string');
367
- return false;
368
- }
369
-
370
- // Extract payload and signature from token
371
- let payload;
372
- let signature;
373
- let tokenObj;
374
-
375
- if (token.includes('.')) {
376
- // Token has signature: payload.signature
377
- const parts = token.split('.');
378
- const encodedPayload = parts[0];
379
- signature = parts[1];
380
-
381
- // Decode payload
382
- const decoded = typeof atob === 'function'
383
- ? atob(encodedPayload)
384
- : Buffer.from(encodedPayload, 'base64').toString('utf8');
385
- payload = decoded;
386
- tokenObj = JSON.parse(decoded);
387
- } else if (token.startsWith('ey')) {
388
- // Base64 encoded without signature
389
- const decoded = typeof atob === 'function'
390
- ? atob(token)
391
- : Buffer.from(token, 'base64').toString('utf8');
392
- payload = decoded;
393
- tokenObj = JSON.parse(decoded);
394
- } else if (token.startsWith('{')) {
395
- // Already JSON string
396
- payload = token;
397
- tokenObj = JSON.parse(token);
398
- } else {
399
- console.log('[verifyCapabilityIssuer] ❌ Unknown token format');
400
- return false;
401
- }
402
-
403
- // Verify issuer field matches expected
404
- if (tokenObj.issuer !== expectedIssuer) {
405
- console.log('[verifyCapabilityIssuer] ❌ Issuer mismatch:', {
406
- tokenIssuer: tokenObj.issuer?.slice(0, 12) + '...',
407
- expectedIssuer: expectedIssuer?.slice(0, 12) + '...',
408
- });
409
- return false;
410
- }
411
-
412
- // CRITICAL: Verify signature was made by the claimed issuer's private key
413
- // This prevents forged capabilities where someone claims a different issuer
414
- if (signature) {
415
- const signatureValid = await verify(payload, signature, expectedIssuer);
416
- if (!signatureValid) {
417
- console.log('[verifyCapabilityIssuer] ❌ Signature verification failed - capability not signed by claimed issuer');
418
- return false;
419
- }
420
- } else {
421
- // No signature - for backwards compatibility, allow unsigned tokens
422
- // but log a warning
423
- console.log('[verifyCapabilityIssuer] ⚠️ Token has no signature - cannot verify cryptographically');
424
- }
425
-
426
- console.log('[verifyCapabilityIssuer] ✅ Issuer verified');
427
- return true;
428
- } catch (error) {
429
- console.log('[verifyCapabilityIssuer] ❌ Error:', error.message);
430
- return false;
431
- }
432
- }
433
-
434
- /**
435
- * Decode a capability token to extract its payload.
436
- * Useful for inspecting capability contents without full verification.
437
- *
438
- * @param {string} token - Capability token
439
- * @returns {Object|null} Decoded token payload or null if invalid
440
- */
441
- export function decodeCapability(token) {
442
- try {
443
- // Handle capability object wrapper
444
- if (token && typeof token === 'object' && token.token) {
445
- token = token.token;
446
- }
447
-
448
- // Handle Buffer serialization format
449
- if (token && typeof token === 'object' && token.type === 'Buffer' && Array.isArray(token.data)) {
450
- token = String.fromCharCode.apply(null, token.data);
451
- }
452
-
453
- if (typeof token !== 'string') {
454
- return null;
455
- }
456
-
457
- if (token.startsWith('ey') || (!token.includes('.') && !token.startsWith('{'))) {
458
- const payload = token.includes('.') ? token.split('.')[0] : token;
459
- const decoded = typeof atob === 'function'
460
- ? atob(payload)
461
- : Buffer.from(payload, 'base64').toString('utf8');
462
- return JSON.parse(decoded);
463
- } else if (token.startsWith('{')) {
464
- return JSON.parse(token);
465
- }
466
-
467
- return null;
468
- } catch (error) {
469
- return null;
470
- }
471
- }
472
-
473
- /**
474
- * Generate unique nonce
475
- * @private
476
- */
477
- function generateNonce() {
478
- return (
479
- Date.now().toString(36) + Math.random().toString(36).substring(2, 15)
480
- );
481
- }
482
-
483
85
  /**
484
86
  * Hash content using SHA-256
485
87
  * @private