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/index.js
CHANGED
|
@@ -28,6 +28,8 @@ import * as requestCard from './federation/request-card.js';
|
|
|
28
28
|
import * as cardStorage from './federation/card-storage.js';
|
|
29
29
|
import * as crypto from './crypto/secp256k1.js';
|
|
30
30
|
import * as nostrUtils from './crypto/nostr-utils.js';
|
|
31
|
+
import { LensKeyStore, buildLensPath } from './crypto/key-store.js';
|
|
32
|
+
import * as lensKeys from './crypto/lens-keys.js';
|
|
31
33
|
import * as social from './content/social-protocols.js';
|
|
32
34
|
import * as subscriptions from './subscriptions/manager.js';
|
|
33
35
|
import * as hierarchical from './hierarchical/upcast.js';
|
|
@@ -128,6 +130,24 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
128
130
|
// Contracts module (lazy initialized)
|
|
129
131
|
/** @type {Object|null} */
|
|
130
132
|
this._contracts = null;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Lens key store for encrypted federation.
|
|
136
|
+
* Manages symmetric keys for encrypting/decrypting lens data.
|
|
137
|
+
* @type {LensKeyStore}
|
|
138
|
+
*/
|
|
139
|
+
this.keyStore = new LensKeyStore();
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Encryption configuration.
|
|
143
|
+
* @type {Object}
|
|
144
|
+
* @property {boolean} encryptByDefault - Whether to encrypt lens data by default
|
|
145
|
+
* @property {string[]} publicLenses - Lenses that should never be encrypted
|
|
146
|
+
*/
|
|
147
|
+
this._encryptionConfig = {
|
|
148
|
+
encryptByDefault: config.encryptByDefault ?? false,
|
|
149
|
+
publicLenses: config.publicLenses || ['profile', 'settings', 'public'],
|
|
150
|
+
};
|
|
131
151
|
}
|
|
132
152
|
|
|
133
153
|
/**
|
|
@@ -322,6 +342,7 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
322
342
|
* @param {Object} [options.propagationOptions] - Options for propagation
|
|
323
343
|
* @param {boolean} [options.blocking=false] - If true, wait for relay confirmation before returning
|
|
324
344
|
* @param {string} [options.signingKey] - Private key to sign with (hex format). If not provided, uses holosphere's default key.
|
|
345
|
+
* @param {boolean} [options.encrypt] - Whether to encrypt this data. Defaults to encryptByDefault config.
|
|
325
346
|
* @returns {Promise<boolean>} True if write succeeded (or queued for optimistic writes)
|
|
326
347
|
* @throws {ValidationError} If holonId, lensName, or data is invalid
|
|
327
348
|
* @throws {AuthorizationError} If capability token is invalid
|
|
@@ -341,8 +362,81 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
341
362
|
throw new ValidationError('ValidationError: data must be an object');
|
|
342
363
|
}
|
|
343
364
|
|
|
365
|
+
// Check write authorization BEFORE optimistic caching
|
|
366
|
+
// Helper function to detect if a string is a pubkey (64-char hex)
|
|
367
|
+
const isPubkeyHolon = typeof holonId === 'string' && /^[0-9a-f]{64}$/i.test(holonId);
|
|
368
|
+
|
|
369
|
+
// Determine the effective writer pubkey - use signingKey's pubkey if provided
|
|
370
|
+
let effectiveWriterPubKey = this.client?.publicKey;
|
|
371
|
+
if (options.signingKey) {
|
|
372
|
+
try {
|
|
373
|
+
effectiveWriterPubKey = crypto.getPublicKey(options.signingKey);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
this._log('WARN', '⚠️ Invalid signingKey provided, using client key', { error: err.message });
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// For pubkey-based holons, check if it's our own holon (matches effective writer's pubkey)
|
|
380
|
+
// For non-pubkey holons (H3 cells, UUIDs), skip authorization for backwards compatibility
|
|
381
|
+
const isOwnHolon = effectiveWriterPubKey && (holonId === effectiveWriterPubKey || !isPubkeyHolon);
|
|
344
382
|
const capToken = options.capabilityToken || options.capability;
|
|
345
|
-
|
|
383
|
+
const actingAsHolon = options.actingAs || options.actingAsHolon;
|
|
384
|
+
|
|
385
|
+
if (!isOwnHolon) {
|
|
386
|
+
// Writing to non-own holon - check authorization
|
|
387
|
+
// Priority: 1. Explicit capability token, 2. Membership-based access
|
|
388
|
+
|
|
389
|
+
if (capToken) {
|
|
390
|
+
// Explicit capability token provided - verify it
|
|
391
|
+
const authorized = await this.verifyCapability(capToken, 'write', { holonId, lensName });
|
|
392
|
+
if (!authorized) {
|
|
393
|
+
this._log('WARN', '🚫 Write denied: Invalid capability token', { holonId, lensName });
|
|
394
|
+
throw new AuthorizationError(
|
|
395
|
+
'Invalid capability token for write operation',
|
|
396
|
+
'write'
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
this._log('DEBUG', '✅ Write authorized via capability token', { holonId, lensName });
|
|
400
|
+
} else {
|
|
401
|
+
// No capability token - check unified access control
|
|
402
|
+
// Uses canAccess() which checks: 1. owner, 2. membership, 3. federation grants (unified)
|
|
403
|
+
const writerPubKey = effectiveWriterPubKey;
|
|
404
|
+
|
|
405
|
+
if (!writerPubKey) {
|
|
406
|
+
this._log('WARN', '🚫 Write denied: No authenticated user', { holonId, lensName });
|
|
407
|
+
throw new AuthorizationError(
|
|
408
|
+
'Authentication required for writing to federated holons',
|
|
409
|
+
'write'
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Check unified access control
|
|
414
|
+
const accessCheck = await this.canAccess(holonId, lensName, writerPubKey, 'write', { actingAsHolon });
|
|
415
|
+
|
|
416
|
+
if (!accessCheck.allowed) {
|
|
417
|
+
this._log('WARN', '🚫 Write denied: No write access', {
|
|
418
|
+
holonId,
|
|
419
|
+
lensName,
|
|
420
|
+
reason: accessCheck.reason,
|
|
421
|
+
via: accessCheck.via,
|
|
422
|
+
writerPubKey: writerPubKey?.slice(0, 12) + '...'
|
|
423
|
+
});
|
|
424
|
+
throw new AuthorizationError(
|
|
425
|
+
`Write access denied: ${accessCheck.reason}`,
|
|
426
|
+
'write'
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
this._log('DEBUG', '✅ Write authorized via unified access control', {
|
|
431
|
+
holonId,
|
|
432
|
+
lensName,
|
|
433
|
+
via: accessCheck.via,
|
|
434
|
+
reason: accessCheck.reason,
|
|
435
|
+
grant: accessCheck.grant ? 'yes' : 'no'
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
} else if (capToken) {
|
|
439
|
+
// Own holon with explicit token - validate it anyway
|
|
346
440
|
const authorized = await this.verifyCapability(capToken, 'write', { holonId, lensName });
|
|
347
441
|
if (!authorized) {
|
|
348
442
|
throw new AuthorizationError('AuthorizationError: Invalid capability token for write operation', 'write');
|
|
@@ -427,8 +521,35 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
427
521
|
return this._syncHologramWrite(existingData, data, path, lensName, options);
|
|
428
522
|
}
|
|
429
523
|
|
|
430
|
-
//
|
|
431
|
-
const
|
|
524
|
+
// Determine if encryption is needed
|
|
525
|
+
const shouldEncrypt = this._shouldEncryptLens(lensName, options);
|
|
526
|
+
let lensKey = null;
|
|
527
|
+
|
|
528
|
+
if (shouldEncrypt && this.client?.privateKey && this.client?.publicKey) {
|
|
529
|
+
// Get or create a key for this lens
|
|
530
|
+
const lensPath = buildLensPath(this.config.appName, holonId, lensName);
|
|
531
|
+
const keyResult = this.keyStore.getOrCreateKey(
|
|
532
|
+
lensPath,
|
|
533
|
+
this.client.privateKey,
|
|
534
|
+
this.client.publicKey
|
|
535
|
+
);
|
|
536
|
+
lensKey = keyResult.key;
|
|
537
|
+
|
|
538
|
+
if (keyResult.isNew) {
|
|
539
|
+
this._log('DEBUG', '🔑 Generated new lens key', { lensPath });
|
|
540
|
+
// Store the wrapped key in Nostr for persistence
|
|
541
|
+
this._storeWrappedKey(lensPath, keyResult.wrappedForSelf).catch(err => {
|
|
542
|
+
this._log('WARN', '⚠️ Failed to persist wrapped key', { lensPath, error: err.message });
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Regular write to relay (with optional encryption)
|
|
548
|
+
const writeOptions = {
|
|
549
|
+
...(options.signingKey ? { signingKey: options.signingKey } : {}),
|
|
550
|
+
...(lensKey ? { lensKey } : {}),
|
|
551
|
+
waitForRelays: true // Always wait for relay confirmation when sync is called
|
|
552
|
+
};
|
|
432
553
|
await storage.write(this.client, path, data, writeOptions);
|
|
433
554
|
|
|
434
555
|
const endTime = Date.now();
|
|
@@ -514,6 +635,26 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
514
635
|
await storage.write(this.client, path, localData);
|
|
515
636
|
|
|
516
637
|
if (Object.keys(sourceUpdates).length > 0) {
|
|
638
|
+
// Verify write capability before updating source
|
|
639
|
+
const capability = existingData.capability;
|
|
640
|
+
if (!capability) {
|
|
641
|
+
throw new Error('Hologram missing capability token for source update');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const hasWrite = await crypto.verifyCapability(
|
|
645
|
+
capability,
|
|
646
|
+
'write',
|
|
647
|
+
{
|
|
648
|
+
holonId: existingData.target.holonId,
|
|
649
|
+
lensName: existingData.target.lensName,
|
|
650
|
+
dataId: existingData.target.dataId
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
|
|
654
|
+
if (!hasWrite) {
|
|
655
|
+
throw new Error('Write capability required to update source data through hologram');
|
|
656
|
+
}
|
|
657
|
+
|
|
517
658
|
const sourcePath = storage.buildPath(
|
|
518
659
|
existingData.target.appname || this.config.appName,
|
|
519
660
|
existingData.target.holonId,
|
|
@@ -563,6 +704,176 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
563
704
|
return registry.getCapabilityForAuthor(this.client, this.config.appName, authorPubKey, scope);
|
|
564
705
|
}
|
|
565
706
|
|
|
707
|
+
/**
|
|
708
|
+
* Determine if a lens should be encrypted.
|
|
709
|
+
*
|
|
710
|
+
* @private
|
|
711
|
+
* @param {string} lensName - Name of the lens
|
|
712
|
+
* @param {Object} options - Write options
|
|
713
|
+
* @returns {boolean} True if lens should be encrypted
|
|
714
|
+
*/
|
|
715
|
+
_shouldEncryptLens(lensName, options = {}) {
|
|
716
|
+
// Explicit encrypt option takes precedence
|
|
717
|
+
if (options.encrypt !== undefined) {
|
|
718
|
+
return options.encrypt;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Public lenses are never encrypted
|
|
722
|
+
if (this._encryptionConfig.publicLenses.includes(lensName)) {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Use default setting
|
|
727
|
+
return this._encryptionConfig.encryptByDefault;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/**
|
|
731
|
+
* Store a wrapped lens key in Nostr for persistence.
|
|
732
|
+
* This allows recovering keys after restart.
|
|
733
|
+
*
|
|
734
|
+
* @private
|
|
735
|
+
* @param {string} lensPath - Path identifying the lens
|
|
736
|
+
* @param {string} wrappedKey - NIP-44 wrapped key for self
|
|
737
|
+
* @returns {Promise<void>}
|
|
738
|
+
*/
|
|
739
|
+
async _storeWrappedKey(lensPath, wrappedKey) {
|
|
740
|
+
const keyStorePath = storage.buildPath(this.config.appName, '_keys', 'lens-keys', lensPath.replace(/\//g, '_'));
|
|
741
|
+
await storage.write(this.client, keyStorePath, {
|
|
742
|
+
id: lensPath.replace(/\//g, '_'),
|
|
743
|
+
lensPath,
|
|
744
|
+
wrappedKey,
|
|
745
|
+
algorithm: 'aes-256-gcm',
|
|
746
|
+
keyWrap: 'nip44',
|
|
747
|
+
version: '1.0',
|
|
748
|
+
createdAt: Date.now()
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Load stored wrapped keys from Nostr and restore them to the keyStore.
|
|
754
|
+
* Called during initialization to recover keys after restart.
|
|
755
|
+
*
|
|
756
|
+
* @private
|
|
757
|
+
* @returns {Promise<number>} Number of keys restored
|
|
758
|
+
*/
|
|
759
|
+
async _loadStoredKeys() {
|
|
760
|
+
if (!this.client?.privateKey || !this.client?.publicKey) {
|
|
761
|
+
return 0;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const keyStorePath = storage.buildPath(this.config.appName, '_keys', 'lens-keys') + '/';
|
|
765
|
+
const storedKeys = await storage.readAll(this.client, keyStorePath);
|
|
766
|
+
|
|
767
|
+
let restored = 0;
|
|
768
|
+
for (const keyData of storedKeys) {
|
|
769
|
+
if (keyData && keyData.lensPath && keyData.wrappedKey) {
|
|
770
|
+
try {
|
|
771
|
+
const key = lensKeys.unwrapKey(
|
|
772
|
+
keyData.wrappedKey,
|
|
773
|
+
this.client.privateKey,
|
|
774
|
+
this.client.publicKey
|
|
775
|
+
);
|
|
776
|
+
this.keyStore.storeOwnKey(keyData.lensPath, key, keyData.wrappedKey);
|
|
777
|
+
restored++;
|
|
778
|
+
} catch (err) {
|
|
779
|
+
this._log('WARN', '⚠️ Failed to restore lens key', { lensPath: keyData.lensPath, error: err.message });
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (restored > 0) {
|
|
785
|
+
this._log('DEBUG', '🔑 Restored lens keys', { count: restored });
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return restored;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Get the lens key for reading encrypted data.
|
|
793
|
+
* Returns the key from keyStore if available, otherwise null.
|
|
794
|
+
*
|
|
795
|
+
* @private
|
|
796
|
+
* @param {string} holonId - Holon identifier
|
|
797
|
+
* @param {string} lensName - Lens name
|
|
798
|
+
* @returns {Buffer|null} Lens key or null if not available
|
|
799
|
+
*/
|
|
800
|
+
_getLensKey(holonId, lensName) {
|
|
801
|
+
const lensPath = buildLensPath(this.config.appName, holonId, lensName);
|
|
802
|
+
return this.keyStore.getKey(lensPath);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Handle a received lens key share from a federation partner.
|
|
807
|
+
* Call this from your onKeyShare handler in subscribeToFederationDMs.
|
|
808
|
+
*
|
|
809
|
+
* @param {Object} keyShare - Key share payload
|
|
810
|
+
* @param {string} keyShare.lensPath - Path identifying the lens
|
|
811
|
+
* @param {string} keyShare.wrappedKey - NIP-44 wrapped symmetric key
|
|
812
|
+
* @param {string} senderPubKey - Sender's public key
|
|
813
|
+
* @returns {boolean} True if key was successfully stored
|
|
814
|
+
*/
|
|
815
|
+
handleReceivedKeyShare(keyShare, senderPubKey) {
|
|
816
|
+
if (!this.client?.privateKey) {
|
|
817
|
+
this._log('WARN', '⚠️ Cannot receive key share: no private key');
|
|
818
|
+
return false;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
try {
|
|
822
|
+
const key = this.keyStore.receiveKey(
|
|
823
|
+
keyShare.lensPath,
|
|
824
|
+
keyShare.wrappedKey,
|
|
825
|
+
this.client.privateKey,
|
|
826
|
+
senderPubKey
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
this._log('DEBUG', '🔑 Received and stored lens key', {
|
|
830
|
+
lensPath: keyShare.lensPath,
|
|
831
|
+
from: senderPubKey.slice(0, 12) + '...'
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
return true;
|
|
835
|
+
} catch (error) {
|
|
836
|
+
this._log('ERROR', '❌ Failed to receive key share', {
|
|
837
|
+
lensPath: keyShare.lensPath,
|
|
838
|
+
error: error.message
|
|
839
|
+
});
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* Get encryption statistics for the key store.
|
|
846
|
+
*
|
|
847
|
+
* @returns {Object} { ownKeyCount, receivedKeyCount, totalPartnerGrants }
|
|
848
|
+
*/
|
|
849
|
+
getEncryptionStats() {
|
|
850
|
+
return this.keyStore.getStats();
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
/**
|
|
854
|
+
* Check if a lens has an encryption key available.
|
|
855
|
+
*
|
|
856
|
+
* @param {string} holonId - Holon identifier
|
|
857
|
+
* @param {string} lensName - Lens name
|
|
858
|
+
* @returns {boolean} True if key is available for this lens
|
|
859
|
+
*/
|
|
860
|
+
hasLensKey(holonId, lensName) {
|
|
861
|
+
const lensPath = buildLensPath(this.config.appName, holonId, lensName);
|
|
862
|
+
return this.keyStore.hasKey(lensPath);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Check if we own the key for a lens (vs received it from a partner).
|
|
867
|
+
*
|
|
868
|
+
* @param {string} holonId - Holon identifier
|
|
869
|
+
* @param {string} lensName - Lens name
|
|
870
|
+
* @returns {boolean} True if we own the key
|
|
871
|
+
*/
|
|
872
|
+
isOwnLensKey(holonId, lensName) {
|
|
873
|
+
const lensPath = buildLensPath(this.config.appName, holonId, lensName);
|
|
874
|
+
return this.keyStore.isOwnKey(lensPath);
|
|
875
|
+
}
|
|
876
|
+
|
|
566
877
|
/**
|
|
567
878
|
* Recursively resolves holograms (references) to their source data.
|
|
568
879
|
* Handles circular reference detection and local overrides.
|
|
@@ -719,12 +1030,22 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
719
1030
|
}
|
|
720
1031
|
|
|
721
1032
|
// Resolve holonId to public key (if registered)
|
|
722
|
-
|
|
1033
|
+
// Skip resolution if explicitly requested (used for backward compat reads from old locations)
|
|
1034
|
+
const targetPubkey = options.skipResolve ? null : await this._resolveHolonToPubkey(holonId);
|
|
723
1035
|
|
|
724
1036
|
// Determine if reading another author's data
|
|
725
1037
|
// If holonId can't be resolved, treat as own data (backwards compatible for H3 holons)
|
|
726
1038
|
const isOtherAuthor = targetPubkey && targetPubkey !== this.client.publicKey;
|
|
1039
|
+
this._log('DEBUG', '🔍 READ: author check', {
|
|
1040
|
+
targetPubkey: targetPubkey?.slice(0, 12),
|
|
1041
|
+
clientPubkey: this.client?.publicKey?.slice(0, 12),
|
|
1042
|
+
isOtherAuthor,
|
|
1043
|
+
holonId: holonId?.slice(0, 12)
|
|
1044
|
+
});
|
|
1045
|
+
// Start with caller's options that should be passed through
|
|
727
1046
|
let readOptions = {};
|
|
1047
|
+
if (options.forceRelay) readOptions.forceRelay = true;
|
|
1048
|
+
if (options.timeout) readOptions.timeout = options.timeout;
|
|
728
1049
|
|
|
729
1050
|
// Explicit capability token takes precedence
|
|
730
1051
|
const capToken = options.capabilityToken || options.capability;
|
|
@@ -765,19 +1086,26 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
765
1086
|
|
|
766
1087
|
// Include federated authors to see holograms written by federation partners
|
|
767
1088
|
// This enables visibility of holograms created during federation
|
|
1089
|
+
// We include ALL federated partners, not just those whose capabilities match the current scope,
|
|
1090
|
+
// because partners may have written holograms to our holon that we need to see
|
|
1091
|
+
this._log('DEBUG', '🔍 Checking for federated authors', { isOtherAuthor, holonId: holonId?.slice(0, 12) });
|
|
768
1092
|
if (!isOtherAuthor) {
|
|
769
1093
|
try {
|
|
770
|
-
|
|
1094
|
+
this._log('DEBUG', '🔍 Getting federated authors...');
|
|
1095
|
+
// Add timeout to prevent hanging on relay queries
|
|
1096
|
+
const fedAuthorsPromise = registry.getFederatedAuthors(
|
|
771
1097
|
this.client,
|
|
772
|
-
this.config.appName
|
|
773
|
-
{ holonId, lensName, dataId },
|
|
774
|
-
'read'
|
|
1098
|
+
this.config.appName
|
|
775
1099
|
);
|
|
1100
|
+
const fedAuthorsTimeout = new Promise((resolve) =>
|
|
1101
|
+
setTimeout(() => resolve([]), 5000) // 5 second timeout, resolve with empty array
|
|
1102
|
+
);
|
|
1103
|
+
const federatedAuthors = await Promise.race([fedAuthorsPromise, fedAuthorsTimeout]);
|
|
1104
|
+
this._log('DEBUG', '✅ Got federated authors', { count: federatedAuthors?.length || 0 });
|
|
776
1105
|
if (federatedAuthors.length > 0) {
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
this._log('DEBUG', 'Including federated authors', { count: partnerPubkeys.length });
|
|
1106
|
+
// Include self + all federated partners to see partner-written holograms
|
|
1107
|
+
readOptions.authors = [this.client.publicKey, ...federatedAuthors];
|
|
1108
|
+
this._log('DEBUG', 'Including federated authors', { count: federatedAuthors.length });
|
|
781
1109
|
}
|
|
782
1110
|
} catch (err) {
|
|
783
1111
|
this._log('WARN', 'Failed to get federated authors', { error: err.message });
|
|
@@ -787,6 +1115,12 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
787
1115
|
const startTime = Date.now();
|
|
788
1116
|
let result;
|
|
789
1117
|
|
|
1118
|
+
// Get lens key for decryption (if available)
|
|
1119
|
+
const lensKey = this._getLensKey(holonId, lensName);
|
|
1120
|
+
if (lensKey) {
|
|
1121
|
+
readOptions.lensKey = lensKey;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
790
1124
|
if (dataId) {
|
|
791
1125
|
const path = storage.buildPath(this.config.appName, holonId, lensName, dataId);
|
|
792
1126
|
|
|
@@ -813,7 +1147,7 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
813
1147
|
});
|
|
814
1148
|
result = cached.data;
|
|
815
1149
|
} else {
|
|
816
|
-
this._log('DEBUG', '📖 CACHE MISS: Reading from storage', { path, authors: readOptions.authors });
|
|
1150
|
+
this._log('DEBUG', '📖 CACHE MISS: Reading from storage', { path, authors: readOptions.authors, hasKey: !!lensKey });
|
|
817
1151
|
result = await storage.read(this.client, path, readOptions);
|
|
818
1152
|
this._log('DEBUG', '💾 STORAGE READ', {
|
|
819
1153
|
path,
|
|
@@ -825,7 +1159,7 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
825
1159
|
}
|
|
826
1160
|
} else {
|
|
827
1161
|
const path = storage.buildPath(this.config.appName, holonId, lensName);
|
|
828
|
-
this._log('DEBUG', 'readAll', { holonId, lensName, path, authors: readOptions.authors });
|
|
1162
|
+
this._log('DEBUG', 'readAll', { holonId, lensName, path, authors: readOptions.authors, hasKey: !!lensKey });
|
|
829
1163
|
|
|
830
1164
|
// For readAll, merge cached writes with storage results and filter deleted
|
|
831
1165
|
const storageResult = await storage.readAll(this.client, path, readOptions);
|
|
@@ -916,8 +1250,33 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
916
1250
|
throw new ValidationError('ValidationError: updates must be an object');
|
|
917
1251
|
}
|
|
918
1252
|
|
|
1253
|
+
// Check write authorization BEFORE optimistic caching
|
|
1254
|
+
// Helper function to detect if a string is a pubkey (64-char hex)
|
|
1255
|
+
const isPubkeyHolon = typeof holonId === 'string' && /^[0-9a-f]{64}$/i.test(holonId);
|
|
1256
|
+
// For pubkey-based holons, check if it's our own holon (our pubkey)
|
|
1257
|
+
// For non-pubkey holons (H3 cells, UUIDs), skip authorization for backwards compatibility
|
|
1258
|
+
const isOwnHolon = this.client && (holonId === this.client.publicKey || !isPubkeyHolon);
|
|
919
1259
|
const capToken = options.capabilityToken || options.capability;
|
|
920
|
-
|
|
1260
|
+
|
|
1261
|
+
if (!isOwnHolon) {
|
|
1262
|
+
// Updating federated holon - require capability token
|
|
1263
|
+
if (!capToken) {
|
|
1264
|
+
this._log('WARN', '🚫 Update denied: No capability token for federated holon', { holonId, lensName, dataId });
|
|
1265
|
+
throw new AuthorizationError(
|
|
1266
|
+
'Capability token required for writing to federated holons',
|
|
1267
|
+
'write'
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
const authorized = await this.verifyCapability(capToken, 'write', { holonId, lensName, dataId });
|
|
1271
|
+
if (!authorized) {
|
|
1272
|
+
this._log('WARN', '🚫 Update denied: Invalid capability token', { holonId, lensName, dataId });
|
|
1273
|
+
throw new AuthorizationError(
|
|
1274
|
+
'Invalid capability token for update operation',
|
|
1275
|
+
'write'
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
} else if (capToken) {
|
|
1279
|
+
// Own holon with explicit token - validate it anyway
|
|
921
1280
|
const authorized = await this.verifyCapability(capToken, 'write', { holonId, lensName, dataId });
|
|
922
1281
|
if (!authorized) {
|
|
923
1282
|
throw new AuthorizationError('AuthorizationError: Invalid capability token for update operation', 'write');
|
|
@@ -1008,7 +1367,7 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
1008
1367
|
|
|
1009
1368
|
if (existingData._meta && existingData._meta.activeHolograms) {
|
|
1010
1369
|
this._log('DEBUG', '🔗 Refreshing active holograms', { path });
|
|
1011
|
-
federation.refreshActiveHolograms(this.client, this.config.appName, holonId, lensName, dataId)
|
|
1370
|
+
await federation.refreshActiveHolograms(this.client, this.config.appName, holonId, lensName, dataId)
|
|
1012
1371
|
.catch(err => this._log('WARN', '⚠️ Hologram refresh failed', { path, error: err.message }));
|
|
1013
1372
|
}
|
|
1014
1373
|
|
|
@@ -1057,6 +1416,23 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
1057
1416
|
throw new ValidationError('ValidationError: dataId must be a non-empty string');
|
|
1058
1417
|
}
|
|
1059
1418
|
|
|
1419
|
+
// Check holon-level authorization BEFORE reading data or caching
|
|
1420
|
+
// Helper function to detect if a string is a pubkey (64-char hex)
|
|
1421
|
+
const isPubkeyHolon = typeof holonId === 'string' && /^[0-9a-f]{64}$/i.test(holonId);
|
|
1422
|
+
// For pubkey-based holons, check if it's our own holon (our pubkey)
|
|
1423
|
+
// For non-pubkey holons (H3 cells, UUIDs), skip authorization for backwards compatibility
|
|
1424
|
+
const isOwnHolon = this.client && (holonId === this.client.publicKey || !isPubkeyHolon);
|
|
1425
|
+
const capToken = options.capabilityToken || options.capability;
|
|
1426
|
+
|
|
1427
|
+
if (!isOwnHolon && !capToken) {
|
|
1428
|
+
// Deleting from federated holon without capability token - fail fast
|
|
1429
|
+
this._log('WARN', '🚫 Delete denied: No capability token for federated holon', { holonId, lensName, dataId });
|
|
1430
|
+
throw new AuthorizationError(
|
|
1431
|
+
'Capability token required for writing to federated holons',
|
|
1432
|
+
'delete'
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1060
1436
|
const path = storage.buildPath(this.config.appName, holonId, lensName, dataId);
|
|
1061
1437
|
|
|
1062
1438
|
// Check write cache first, then storage
|
|
@@ -1077,13 +1453,12 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
1077
1453
|
|
|
1078
1454
|
this._log('DEBUG', '🗑️ DELETE: Found data', { path, source: dataSource });
|
|
1079
1455
|
|
|
1080
|
-
// Check authorization: owner can delete, others need capability token
|
|
1456
|
+
// Check authorization: data owner can delete, others need capability token
|
|
1081
1457
|
const dataOwner = existingData.owner || existingData._creator;
|
|
1082
|
-
const
|
|
1458
|
+
const isDataOwner = !dataOwner || dataOwner === this.client.publicKey;
|
|
1083
1459
|
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
// Non-owner must provide a valid capability token
|
|
1460
|
+
if (!isDataOwner) {
|
|
1461
|
+
// Non-data-owner must provide a valid capability token
|
|
1087
1462
|
if (!capToken) {
|
|
1088
1463
|
throw new AuthorizationError('AuthorizationError: Capability token required for delete operation', 'delete');
|
|
1089
1464
|
}
|
|
@@ -1092,7 +1467,7 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
1092
1467
|
throw new AuthorizationError('AuthorizationError: Invalid capability token for delete operation', 'delete');
|
|
1093
1468
|
}
|
|
1094
1469
|
} else if (capToken) {
|
|
1095
|
-
//
|
|
1470
|
+
// Data owner provided a token - validate it anyway
|
|
1096
1471
|
const authorized = await this.verifyCapability(capToken, 'delete', { holonId, lensName, dataId });
|
|
1097
1472
|
if (!authorized) {
|
|
1098
1473
|
throw new AuthorizationError('AuthorizationError: Invalid capability token for delete operation', 'delete');
|
|
@@ -1434,15 +1809,23 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
1434
1809
|
* @param {string} lensName - Name of the lens
|
|
1435
1810
|
* @param {Object} data - Data object containing the id to reference
|
|
1436
1811
|
* @param {string} [targetHolon=null] - Target holon for the hologram, defaults to sourceHolon
|
|
1437
|
-
* @returns {Object} Hologram object structure
|
|
1812
|
+
* @returns {Promise<Object>} Hologram object structure
|
|
1438
1813
|
*/
|
|
1439
|
-
createHologram(sourceHolon, lensName, data, targetHolon = null) {
|
|
1440
|
-
|
|
1814
|
+
async createHologram(sourceHolon, lensName, data, targetHolon = null) {
|
|
1815
|
+
const target = targetHolon || sourceHolon;
|
|
1816
|
+
// Use createHologramWithCapability which auto-generates capability for self-operations
|
|
1817
|
+
return federation.createHologramWithCapability(
|
|
1818
|
+
this.client,
|
|
1441
1819
|
sourceHolon,
|
|
1442
|
-
|
|
1820
|
+
target,
|
|
1443
1821
|
lensName,
|
|
1444
1822
|
data.id,
|
|
1445
|
-
this.config.appName
|
|
1823
|
+
this.config.appName,
|
|
1824
|
+
{
|
|
1825
|
+
sourceAuthorPubKey: this.client.publicKey,
|
|
1826
|
+
targetAuthorPubKey: this.client.publicKey,
|
|
1827
|
+
permissions: ['read'],
|
|
1828
|
+
}
|
|
1446
1829
|
);
|
|
1447
1830
|
}
|
|
1448
1831
|
|
|
@@ -1785,6 +2168,8 @@ class HoloSphereBase extends HoloSphereCore {
|
|
|
1785
2168
|
federationData.lensConfig[targetHolon] = {
|
|
1786
2169
|
inbound: lensConfig.inbound || [],
|
|
1787
2170
|
outbound: lensConfig.outbound || [],
|
|
2171
|
+
writeInbound: lensConfig.writeInbound || [],
|
|
2172
|
+
writeOutbound: lensConfig.writeOutbound || [],
|
|
1788
2173
|
timestamp: Date.now()
|
|
1789
2174
|
};
|
|
1790
2175
|
|
|
@@ -2385,6 +2770,10 @@ export { capabilities, requestCard, cardStorage, holonRegistry, registry };
|
|
|
2385
2770
|
export { matchScope } from './crypto/secp256k1.js';
|
|
2386
2771
|
export { createHologram } from './federation/hologram.js';
|
|
2387
2772
|
|
|
2773
|
+
// Re-export lens encryption utilities
|
|
2774
|
+
export { LensKeyStore, buildLensPath, parseLensPath } from './crypto/key-store.js';
|
|
2775
|
+
export * as lensKeys from './crypto/lens-keys.js';
|
|
2776
|
+
|
|
2388
2777
|
// Re-export federation card functions
|
|
2389
2778
|
export {
|
|
2390
2779
|
createFederationCard,
|