holosphere 2.0.0-alpha13 → 2.0.0-alpha15
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/2019-ATLjawsU.cjs +8 -0
- package/dist/{2019-Cp3uYhyY.cjs.map → 2019-ATLjawsU.cjs.map} +1 -1
- package/dist/{2019-CLMqIAfQ.js → 2019-BfjzDRje.js} +1667 -1721
- package/dist/{2019-CLMqIAfQ.js.map → 2019-BfjzDRje.js.map} +1 -1
- package/dist/{browser-nUQt1cnB.js → browser-CKTczilW.js} +2 -2
- package/dist/{browser-nUQt1cnB.js.map → browser-CKTczilW.js.map} +1 -1
- package/dist/{browser-D6cNVl0v.cjs → browser-GOg6KKOV.cjs} +2 -2
- package/dist/{browser-D6cNVl0v.cjs.map → browser-GOg6KKOV.cjs.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +25 -21
- package/dist/{index-CoAjtqsD.js → index-BdnrGafX.js} +2 -2
- package/dist/{index-CoAjtqsD.js.map → index-BdnrGafX.js.map} +1 -1
- package/dist/{index-BN_uoxQK.js → index-C3Cag0SV.js} +2558 -495
- package/dist/index-C3Cag0SV.js.map +1 -0
- package/dist/index-ChpSfdYS.cjs +29 -0
- package/dist/index-ChpSfdYS.cjs.map +1 -0
- package/dist/{index-DJjGSwXG.cjs → index-D_QecZNu.cjs} +2 -2
- package/dist/{index-DJjGSwXG.cjs.map → index-D_QecZNu.cjs.map} +1 -1
- package/dist/{index-Z5TstN1e.js → index-nMC3dWZ5.js} +2 -2
- package/dist/{index-Z5TstN1e.js.map → index-nMC3dWZ5.js.map} +1 -1
- package/dist/{index-Cp3tI53z.cjs → index-z5HWfWMu.cjs} +2 -2
- package/dist/{index-Cp3tI53z.cjs.map → index-z5HWfWMu.cjs.map} +1 -1
- package/dist/{indexeddb-storage-CZK5A7XH.cjs → indexeddb-storage-DWSeL-YF.cjs} +2 -2
- package/dist/{indexeddb-storage-CZK5A7XH.cjs.map → indexeddb-storage-DWSeL-YF.cjs.map} +1 -1
- package/dist/{indexeddb-storage-bpA01pAU.js → indexeddb-storage-dx01N0ET.js} +2 -2
- package/dist/{indexeddb-storage-bpA01pAU.js.map → indexeddb-storage-dx01N0ET.js.map} +1 -1
- package/dist/{memory-storage-BqhmytP_.js → memory-storage-BPIfkpcf.js} +2 -2
- package/dist/{memory-storage-BqhmytP_.js.map → memory-storage-BPIfkpcf.js.map} +1 -1
- package/dist/{memory-storage-B1k8Jszd.cjs → memory-storage-CKUGDq2d.cjs} +2 -2
- package/dist/{memory-storage-B1k8Jszd.cjs.map → memory-storage-CKUGDq2d.cjs.map} +1 -1
- package/package.json +3 -1
- package/scripts/test-ndk-direct.js +104 -0
- package/src/crypto/key-store.js +356 -0
- package/src/crypto/lens-keys.js +205 -0
- package/src/crypto/secp256k1.js +181 -18
- package/src/federation/handshake.js +317 -23
- package/src/federation/hologram.js +25 -17
- package/src/federation/registry.js +779 -59
- package/src/index.js +416 -27
- package/src/lib/federation-methods.js +308 -4
- package/src/storage/nostr-async.js +144 -86
- package/src/storage/nostr-client.js +77 -18
- package/src/storage/nostr-wrapper.js +4 -1
- package/src/storage/unified-storage.js +5 -4
- package/src/subscriptions/manager.js +1 -1
- package/vitest.config.js +6 -1
- package/dist/2019-Cp3uYhyY.cjs +0 -8
- package/dist/index-BN_uoxQK.js.map +0 -1
- package/dist/index-V8EHMYEY.cjs +0 -29
- package/dist/index-V8EHMYEY.cjs.map +0 -1
package/src/crypto/secp256k1.js
CHANGED
|
@@ -10,20 +10,23 @@
|
|
|
10
10
|
|
|
11
11
|
import { sha256 } from '@noble/hashes/sha256';
|
|
12
12
|
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
|
13
|
-
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
13
|
+
import { secp256k1, schnorr } from '@noble/curves/secp256k1';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Get public key from private key
|
|
16
|
+
* Get public key from private key (x-only / schnorr format for Nostr compatibility)
|
|
17
17
|
* @param {string} privateKey - Private key (hex string)
|
|
18
|
-
* @returns {string} Public key (hex string)
|
|
18
|
+
* @returns {string} Public key (64-char hex string, x-only format)
|
|
19
19
|
*/
|
|
20
20
|
export function getPublicKey(privateKey) {
|
|
21
|
-
|
|
21
|
+
// Use schnorr.getPublicKey for x-only format (32 bytes, 64 hex chars)
|
|
22
|
+
// This is compatible with Nostr and matches nostr-tools format
|
|
23
|
+
const pubKey = schnorr.getPublicKey(privateKey);
|
|
22
24
|
return bytesToHex(pubKey);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
/**
|
|
26
|
-
* Sign content with private key
|
|
28
|
+
* Sign content with private key using schnorr signature
|
|
29
|
+
* Uses schnorr for compatibility with x-only public keys (Nostr standard)
|
|
27
30
|
* @param {string} content - Content to sign (will be hashed)
|
|
28
31
|
* @param {string} privateKey - Private key (hex string)
|
|
29
32
|
* @returns {Promise<string>} Signature (hex string)
|
|
@@ -33,30 +36,32 @@ export async function sign(content, privateKey) {
|
|
|
33
36
|
// Hash content
|
|
34
37
|
const msgHash = hashContent(content);
|
|
35
38
|
|
|
36
|
-
//
|
|
37
|
-
const signature =
|
|
38
|
-
return signature
|
|
39
|
+
// Use schnorr.sign for x-only key compatibility
|
|
40
|
+
const signature = schnorr.sign(msgHash, privateKey);
|
|
41
|
+
return bytesToHex(signature);
|
|
39
42
|
} catch (error) {
|
|
40
43
|
throw new Error(`Signature generation failed: ${error.message}`);
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
|
-
* Verify signature
|
|
48
|
+
* Verify schnorr signature
|
|
49
|
+
* Uses schnorr for compatibility with x-only public keys (Nostr standard)
|
|
46
50
|
* @param {string} content - Original content
|
|
47
51
|
* @param {string} signature - Signature (hex string)
|
|
48
|
-
* @param {string} publicKey - Public key (hex string)
|
|
52
|
+
* @param {string} publicKey - Public key (hex string, x-only format)
|
|
49
53
|
* @returns {Promise<boolean>} True if valid
|
|
50
54
|
*/
|
|
51
55
|
export async function verify(content, signature, publicKey) {
|
|
52
56
|
try {
|
|
53
|
-
// Validate public key format
|
|
57
|
+
// Validate public key format (x-only is 64 hex chars)
|
|
54
58
|
if (!publicKey || typeof publicKey !== 'string' || publicKey.length < 64) {
|
|
55
59
|
throw new Error('Invalid public key format');
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
const msgHash = hashContent(content);
|
|
59
|
-
|
|
63
|
+
// Use schnorr.verify for x-only key compatibility
|
|
64
|
+
const isValid = schnorr.verify(signature, msgHash, publicKey);
|
|
60
65
|
return isValid;
|
|
61
66
|
} catch (error) {
|
|
62
67
|
// Invalid signatures or keys throw errors
|
|
@@ -169,10 +174,12 @@ export async function issueCapability(permissions, scope, recipient, options = {
|
|
|
169
174
|
};
|
|
170
175
|
|
|
171
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)
|
|
172
179
|
const payload = JSON.stringify(token);
|
|
173
|
-
const encoded =
|
|
174
|
-
|
|
175
|
-
|
|
180
|
+
const encoded = typeof btoa === 'function'
|
|
181
|
+
? btoa(payload)
|
|
182
|
+
: Buffer.from(payload).toString('base64');
|
|
176
183
|
|
|
177
184
|
// If issuerKey provided, sign the token
|
|
178
185
|
if (issuerKey) {
|
|
@@ -225,6 +232,30 @@ export async function verifyCapability(token, requiredPermission, scope) {
|
|
|
225
232
|
token = token.token; // Extract the actual token string
|
|
226
233
|
}
|
|
227
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
|
+
|
|
228
259
|
// Decode if string
|
|
229
260
|
if (typeof token === 'string') {
|
|
230
261
|
// Check if it's a base64-encoded token (starts with "ey" for JSON base64)
|
|
@@ -232,9 +263,10 @@ export async function verifyCapability(token, requiredPermission, scope) {
|
|
|
232
263
|
// Base64 encoded (with or without signature)
|
|
233
264
|
try {
|
|
234
265
|
const payload = token.includes('.') ? token.split('.')[0] : token;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
atob(payload)
|
|
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');
|
|
238
270
|
tokenObj = JSON.parse(decoded);
|
|
239
271
|
} catch (e) {
|
|
240
272
|
console.log('[verifyCapability] ❌ Token is not valid base64 JSON:', {
|
|
@@ -307,6 +339,137 @@ export async function verifyCapability(token, requiredPermission, scope) {
|
|
|
307
339
|
}
|
|
308
340
|
}
|
|
309
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
|
+
|
|
310
473
|
/**
|
|
311
474
|
* Generate unique nonce
|
|
312
475
|
* @private
|