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
|
@@ -17,27 +17,52 @@ import {
|
|
|
17
17
|
generateNonce,
|
|
18
18
|
getPublicKey,
|
|
19
19
|
} from '../crypto/nostr-utils.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
addFederatedPartner,
|
|
22
|
+
storeInboundCapability,
|
|
23
|
+
grantAccess,
|
|
24
|
+
convertLegacyLensConfig,
|
|
25
|
+
convertToLegacyLensConfig,
|
|
26
|
+
} from './registry.js';
|
|
21
27
|
import { registerHolon } from './holon-registry.js';
|
|
22
28
|
import { issueCapability } from '../crypto/secp256k1.js';
|
|
23
29
|
import * as cardStorage from './card-storage.js';
|
|
30
|
+
import { buildLensPath } from '../crypto/key-store.js';
|
|
24
31
|
|
|
25
32
|
// ============================================================================
|
|
26
33
|
// Types (documented for reference)
|
|
27
34
|
// ============================================================================
|
|
28
35
|
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {Object} LensConfigV1 - Legacy v1.0 lens configuration format
|
|
38
|
+
* @property {string[]} inbound - Lenses sender will accept (read)
|
|
39
|
+
* @property {string[]} outbound - Lenses sender will share (read)
|
|
40
|
+
* @property {string[]} [writeInbound] - Lenses sender wants write access to
|
|
41
|
+
* @property {string[]} [writeOutbound] - Lenses sender will grant write access for
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} LensPermissions - v2.0 permissions for a single lens
|
|
46
|
+
* @property {string[]} receive - Permissions received from partner ('read', 'write')
|
|
47
|
+
* @property {string[]} share - Permissions shared with partner ('read', 'write')
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @typedef {Object} LensConfigV2 - Unified v2.0 lens configuration format
|
|
52
|
+
* @property {'2.0'} version - Format version
|
|
53
|
+
* @property {Object.<string, LensPermissions>} permissions - Per-lens permissions
|
|
54
|
+
*/
|
|
55
|
+
|
|
29
56
|
/**
|
|
30
57
|
* @typedef {Object} FederationRequestPayload
|
|
31
58
|
* @property {'federation_request'} type
|
|
32
|
-
* @property {'1.0'} version
|
|
59
|
+
* @property {'1.0'|'2.0'} version - Protocol version
|
|
33
60
|
* @property {string} requestId - Unique request ID
|
|
34
61
|
* @property {number} timestamp
|
|
35
62
|
* @property {string} senderHolonId - Holon ID of the sender
|
|
36
63
|
* @property {string} senderHolonName - Human-readable holon name
|
|
37
64
|
* @property {string} senderNpub - Sender's npub
|
|
38
|
-
* @property {
|
|
39
|
-
* @property {string[]} lensConfig.inbound - Lenses sender will accept
|
|
40
|
-
* @property {string[]} lensConfig.outbound - Lenses sender will share
|
|
65
|
+
* @property {LensConfigV1|LensConfigV2} lensConfig - Lens configuration (v1 or v2 format)
|
|
41
66
|
* @property {Array} capabilities - Capability tokens
|
|
42
67
|
* @property {string} [message] - Optional personal message
|
|
43
68
|
*/
|
|
@@ -45,18 +70,87 @@ import * as cardStorage from './card-storage.js';
|
|
|
45
70
|
/**
|
|
46
71
|
* @typedef {Object} FederationResponsePayload
|
|
47
72
|
* @property {'federation_response'} type
|
|
48
|
-
* @property {'1.0'} version
|
|
73
|
+
* @property {'1.0'|'2.0'} version - Protocol version
|
|
49
74
|
* @property {string} requestId - References original request
|
|
50
75
|
* @property {number} timestamp
|
|
51
76
|
* @property {'accepted'|'rejected'} status
|
|
52
77
|
* @property {string} [responderHolonId]
|
|
53
78
|
* @property {string} [responderHolonName]
|
|
54
79
|
* @property {string} [responderNpub]
|
|
55
|
-
* @property {
|
|
80
|
+
* @property {LensConfigV1|LensConfigV2} [lensConfig] - Lens configuration
|
|
56
81
|
* @property {Array} [capabilities]
|
|
57
82
|
* @property {string} [message]
|
|
58
83
|
*/
|
|
59
84
|
|
|
85
|
+
/**
|
|
86
|
+
* @typedef {Object} LensKeySharePayload
|
|
87
|
+
* @property {'lens_key_share'} type
|
|
88
|
+
* @property {'1.0'} version - Protocol version
|
|
89
|
+
* @property {string} lensPath - Path identifying the lens (appName/holonId/lensName)
|
|
90
|
+
* @property {string} wrappedKey - NIP-44 wrapped symmetric key
|
|
91
|
+
* @property {number} timestamp
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
// ============================================================================
|
|
95
|
+
// Lens Config Format Utilities
|
|
96
|
+
// ============================================================================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if a lensConfig is in v2.0 format
|
|
100
|
+
* @param {Object} lensConfig - Lens configuration
|
|
101
|
+
* @returns {boolean} True if v2.0 format
|
|
102
|
+
*/
|
|
103
|
+
export function isV2LensConfig(lensConfig) {
|
|
104
|
+
return lensConfig && lensConfig.version === '2.0' && lensConfig.permissions;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Normalize lensConfig to ensure it has a consistent format.
|
|
109
|
+
* Accepts either v1.0 or v2.0 format and returns normalized v1.0 for backward compat.
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} lensConfig - Input lens configuration (v1 or v2)
|
|
112
|
+
* @returns {Object} Normalized v1.0 format { inbound, outbound, writeInbound, writeOutbound }
|
|
113
|
+
*/
|
|
114
|
+
export function normalizeLensConfig(lensConfig) {
|
|
115
|
+
if (!lensConfig) {
|
|
116
|
+
return { inbound: [], outbound: [], writeInbound: [], writeOutbound: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// If v2.0 format, convert to v1.0
|
|
120
|
+
if (isV2LensConfig(lensConfig)) {
|
|
121
|
+
return convertToLegacyLensConfig(lensConfig);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Already v1.0 format, just ensure all fields exist
|
|
125
|
+
return {
|
|
126
|
+
inbound: lensConfig.inbound || [],
|
|
127
|
+
outbound: lensConfig.outbound || [],
|
|
128
|
+
writeInbound: lensConfig.writeInbound || [],
|
|
129
|
+
writeOutbound: lensConfig.writeOutbound || [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Create unified permissions from lensConfig.
|
|
135
|
+
* Converts any format to the unified v2.0 format.
|
|
136
|
+
*
|
|
137
|
+
* @param {Object} lensConfig - Input lens configuration (v1 or v2)
|
|
138
|
+
* @returns {Object} v2.0 format { version: '2.0', permissions: {...} }
|
|
139
|
+
*/
|
|
140
|
+
export function toUnifiedPermissions(lensConfig) {
|
|
141
|
+
if (!lensConfig) {
|
|
142
|
+
return { version: '2.0', permissions: {} };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If already v2.0, return as-is
|
|
146
|
+
if (isV2LensConfig(lensConfig)) {
|
|
147
|
+
return lensConfig;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Convert v1.0 to v2.0
|
|
151
|
+
return convertLegacyLensConfig(lensConfig);
|
|
152
|
+
}
|
|
153
|
+
|
|
60
154
|
// ============================================================================
|
|
61
155
|
// Request/Response Creation
|
|
62
156
|
// ============================================================================
|
|
@@ -67,28 +161,35 @@ import * as cardStorage from './card-storage.js';
|
|
|
67
161
|
* @param {string} params.senderHolonId
|
|
68
162
|
* @param {string} params.senderHolonName
|
|
69
163
|
* @param {string} params.senderPubKey - Hex public key
|
|
70
|
-
* @param {Object} params.lensConfig -
|
|
164
|
+
* @param {Object} params.lensConfig - Lens config (v1 or v2 format supported)
|
|
71
165
|
* @param {Array} [params.capabilities] - Capability tokens to include
|
|
72
166
|
* @param {string} [params.message] - Optional message
|
|
167
|
+
* @param {boolean} [params.useV2=false] - Send in v2.0 format
|
|
73
168
|
* @returns {FederationRequestPayload}
|
|
74
169
|
*/
|
|
75
170
|
export function createFederationRequest({
|
|
76
171
|
senderHolonId,
|
|
77
172
|
senderHolonName,
|
|
78
173
|
senderPubKey,
|
|
79
|
-
lensConfig = { inbound: [], outbound: [] },
|
|
174
|
+
lensConfig = { inbound: [], outbound: [], writeInbound: [], writeOutbound: [] },
|
|
80
175
|
capabilities = [],
|
|
81
176
|
message,
|
|
177
|
+
useV2 = false,
|
|
82
178
|
}) {
|
|
179
|
+
// Normalize lensConfig
|
|
180
|
+
const normalizedLensConfig = useV2
|
|
181
|
+
? toUnifiedPermissions(lensConfig)
|
|
182
|
+
: normalizeLensConfig(lensConfig);
|
|
183
|
+
|
|
83
184
|
return {
|
|
84
185
|
type: 'federation_request',
|
|
85
|
-
version: '1.0',
|
|
186
|
+
version: useV2 ? '2.0' : '1.0',
|
|
86
187
|
requestId: generateNonce(),
|
|
87
188
|
timestamp: Date.now(),
|
|
88
189
|
senderHolonId,
|
|
89
190
|
senderHolonName,
|
|
90
191
|
senderNpub: hexToNpub(senderPubKey),
|
|
91
|
-
lensConfig,
|
|
192
|
+
lensConfig: normalizedLensConfig,
|
|
92
193
|
capabilities,
|
|
93
194
|
message,
|
|
94
195
|
};
|
|
@@ -102,9 +203,10 @@ export function createFederationRequest({
|
|
|
102
203
|
* @param {string} [params.responderHolonId]
|
|
103
204
|
* @param {string} [params.responderHolonName]
|
|
104
205
|
* @param {string} [params.responderPubKey] - Hex public key
|
|
105
|
-
* @param {Object} [params.lensConfig]
|
|
206
|
+
* @param {Object} [params.lensConfig] - Lens config (v1 or v2 format supported)
|
|
106
207
|
* @param {Array} [params.capabilities]
|
|
107
208
|
* @param {string} [params.message]
|
|
209
|
+
* @param {boolean} [params.useV2=false] - Respond in v2.0 format
|
|
108
210
|
* @returns {FederationResponsePayload}
|
|
109
211
|
*/
|
|
110
212
|
export function createFederationResponse({
|
|
@@ -116,17 +218,26 @@ export function createFederationResponse({
|
|
|
116
218
|
lensConfig,
|
|
117
219
|
capabilities,
|
|
118
220
|
message,
|
|
221
|
+
useV2 = false,
|
|
119
222
|
}) {
|
|
223
|
+
// Normalize lensConfig if provided
|
|
224
|
+
let normalizedLensConfig;
|
|
225
|
+
if (lensConfig) {
|
|
226
|
+
normalizedLensConfig = useV2
|
|
227
|
+
? toUnifiedPermissions(lensConfig)
|
|
228
|
+
: normalizeLensConfig(lensConfig);
|
|
229
|
+
}
|
|
230
|
+
|
|
120
231
|
return {
|
|
121
232
|
type: 'federation_response',
|
|
122
|
-
version: '1.0',
|
|
233
|
+
version: useV2 ? '2.0' : '1.0',
|
|
123
234
|
requestId,
|
|
124
235
|
timestamp: Date.now(),
|
|
125
236
|
status,
|
|
126
237
|
responderHolonId,
|
|
127
238
|
responderHolonName,
|
|
128
239
|
responderNpub: responderPubKey ? hexToNpub(responderPubKey) : undefined,
|
|
129
|
-
lensConfig,
|
|
240
|
+
lensConfig: normalizedLensConfig,
|
|
130
241
|
capabilities,
|
|
131
242
|
message,
|
|
132
243
|
};
|
|
@@ -192,6 +303,45 @@ export async function sendFederationResponse(client, privateKey, recipientPubKey
|
|
|
192
303
|
}
|
|
193
304
|
}
|
|
194
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Send a lens key share DM
|
|
308
|
+
* Used to share encrypted lens keys with federation partners
|
|
309
|
+
* @param {Object} client - NostrClient instance with publish method
|
|
310
|
+
* @param {string} privateKey - Sender's hex private key
|
|
311
|
+
* @param {string} recipientPubKey - Recipient's hex public key
|
|
312
|
+
* @param {Object} keyShare - Key share payload
|
|
313
|
+
* @param {string} keyShare.lensPath - Path identifying the lens
|
|
314
|
+
* @param {string} keyShare.wrappedKey - NIP-44 wrapped symmetric key for recipient
|
|
315
|
+
* @returns {Promise<boolean>} Success indicator
|
|
316
|
+
*/
|
|
317
|
+
export async function sendLensKeyShare(client, privateKey, recipientPubKey, keyShare) {
|
|
318
|
+
try {
|
|
319
|
+
const payload = {
|
|
320
|
+
type: 'lens_key_share',
|
|
321
|
+
version: '1.0',
|
|
322
|
+
lensPath: keyShare.lensPath,
|
|
323
|
+
wrappedKey: keyShare.wrappedKey,
|
|
324
|
+
timestamp: Date.now(),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
const content = JSON.stringify(payload);
|
|
328
|
+
const encrypted = encryptNIP44(privateKey, recipientPubKey, content);
|
|
329
|
+
const event = createDMEvent(recipientPubKey, encrypted, privateKey);
|
|
330
|
+
|
|
331
|
+
if (client?.publish) {
|
|
332
|
+
await client.publish(event);
|
|
333
|
+
console.log('[Handshake] Lens key share sent to:', recipientPubKey.substring(0, 8) + '...', 'for lens:', keyShare.lensPath);
|
|
334
|
+
return true;
|
|
335
|
+
} else {
|
|
336
|
+
console.error('[Handshake] No publish method on client');
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
} catch (error) {
|
|
340
|
+
console.error('[Handshake] Failed to send lens key share:', error);
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
195
345
|
// ============================================================================
|
|
196
346
|
// DM Subscription
|
|
197
347
|
// ============================================================================
|
|
@@ -208,12 +358,13 @@ export async function sendFederationResponse(client, privateKey, recipientPubKey
|
|
|
208
358
|
* @param {Function} handlers.onResponse - Called with (response, senderPubKey)
|
|
209
359
|
* @param {Function} [handlers.onUpdate] - Called with (update, senderPubKey)
|
|
210
360
|
* @param {Function} [handlers.onUpdateResponse] - Called with (response, senderPubKey)
|
|
361
|
+
* @param {Function} [handlers.onKeyShare] - Called with (keyShare, senderPubKey) for encrypted lens key shares
|
|
211
362
|
* @param {Object} options - Subscription options
|
|
212
363
|
* @param {string} options.appname - Application namespace (for persistent storage)
|
|
213
364
|
* @returns {Function} Unsubscribe function
|
|
214
365
|
*/
|
|
215
366
|
export function subscribeToFederationDMs(client, privateKey, publicKey, handlers, options = {}) {
|
|
216
|
-
const { onRequest, onResponse, onUpdate, onUpdateResponse } = handlers;
|
|
367
|
+
const { onRequest, onResponse, onUpdate, onUpdateResponse, onKeyShare } = handlers;
|
|
217
368
|
const { appname } = options;
|
|
218
369
|
let isActive = true;
|
|
219
370
|
|
|
@@ -348,6 +499,23 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
348
499
|
|
|
349
500
|
console.log('[Handshake] Received federation update response from:', event.pubkey.substring(0, 8) + '...');
|
|
350
501
|
onUpdateResponse?.(payload, event.pubkey);
|
|
502
|
+
} else if (payload.type === 'lens_key_share' && payload.version === '1.0') {
|
|
503
|
+
// Lens key share - receive encrypted symmetric key from partner
|
|
504
|
+
const keyShareKey = `key_${payload.lensPath}_${event.pubkey}`;
|
|
505
|
+
if (processedResponseIds.has(keyShareKey)) {
|
|
506
|
+
console.log('[Handshake] Skipping already processed key share:', keyShareKey);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Mark as processed
|
|
511
|
+
if (appname && client?.client) {
|
|
512
|
+
await cardStorage.markDMProcessed(client.client, appname, event.id, 'key_share');
|
|
513
|
+
processedResponseIds.add(keyShareKey);
|
|
514
|
+
processedDMIds.add(event.id);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
console.log('[Handshake] Received lens key share from:', event.pubkey.substring(0, 8) + '...', 'for lens:', payload.lensPath);
|
|
518
|
+
onKeyShare?.(payload, event.pubkey);
|
|
351
519
|
}
|
|
352
520
|
} catch (error) {
|
|
353
521
|
// Silently ignore non-federation DMs
|
|
@@ -430,7 +598,7 @@ export async function initiateFederationHandshake(holosphere, privateKey, params
|
|
|
430
598
|
partnerPubKey,
|
|
431
599
|
holonId,
|
|
432
600
|
holonName,
|
|
433
|
-
lensConfig = { inbound: [], outbound: [] },
|
|
601
|
+
lensConfig = { inbound: [], outbound: [], writeInbound: [], writeOutbound: [] },
|
|
434
602
|
message,
|
|
435
603
|
} = params;
|
|
436
604
|
|
|
@@ -438,9 +606,17 @@ export async function initiateFederationHandshake(holosphere, privateKey, params
|
|
|
438
606
|
// Get sender's public key
|
|
439
607
|
const senderPubKey = getPublicKey(privateKey);
|
|
440
608
|
|
|
609
|
+
// Normalize lensConfig
|
|
610
|
+
const normalizedLensConfig = {
|
|
611
|
+
inbound: lensConfig.inbound || [],
|
|
612
|
+
outbound: lensConfig.outbound || [],
|
|
613
|
+
writeInbound: lensConfig.writeInbound || [],
|
|
614
|
+
writeOutbound: lensConfig.writeOutbound || [],
|
|
615
|
+
};
|
|
616
|
+
|
|
441
617
|
// Issue capabilities for outbound lenses (lenses we're sharing with partner)
|
|
442
618
|
const capabilities = [];
|
|
443
|
-
for (const lensName of
|
|
619
|
+
for (const lensName of normalizedLensConfig.outbound) {
|
|
444
620
|
try {
|
|
445
621
|
const token = await issueCapability(
|
|
446
622
|
['read'],
|
|
@@ -462,12 +638,49 @@ export async function initiateFederationHandshake(holosphere, privateKey, params
|
|
|
462
638
|
}
|
|
463
639
|
}
|
|
464
640
|
|
|
641
|
+
// Grant write access to partner for our writeOutbound lenses
|
|
642
|
+
// This allows the partner to write to these lenses in our holon
|
|
643
|
+
if (holosphere.client) {
|
|
644
|
+
for (const lensName of normalizedLensConfig.writeOutbound) {
|
|
645
|
+
try {
|
|
646
|
+
// New unified grant
|
|
647
|
+
await grantAccess(
|
|
648
|
+
holosphere.client,
|
|
649
|
+
holosphere.config.appName,
|
|
650
|
+
partnerPubKey,
|
|
651
|
+
{ holonId: '*', lensName },
|
|
652
|
+
['write'],
|
|
653
|
+
{ direction: 'inbound' }
|
|
654
|
+
);
|
|
655
|
+
console.log(`[Handshake] Pre-granted write access to partner for lens "${lensName}"`);
|
|
656
|
+
} catch (err) {
|
|
657
|
+
console.warn(`[Handshake] Failed to grant write access for ${lensName}:`, err.message);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Also grant read access for outbound lenses using unified format
|
|
662
|
+
for (const lensName of normalizedLensConfig.outbound) {
|
|
663
|
+
try {
|
|
664
|
+
await grantAccess(
|
|
665
|
+
holosphere.client,
|
|
666
|
+
holosphere.config.appName,
|
|
667
|
+
partnerPubKey,
|
|
668
|
+
{ holonId: '*', lensName },
|
|
669
|
+
['read'],
|
|
670
|
+
{ direction: 'outbound' }
|
|
671
|
+
);
|
|
672
|
+
} catch (err) {
|
|
673
|
+
console.warn(`[Handshake] Failed to grant read access for ${lensName}:`, err.message);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
465
678
|
// Create federation request
|
|
466
679
|
const request = createFederationRequest({
|
|
467
680
|
senderHolonId: holonId,
|
|
468
681
|
senderHolonName: holonName,
|
|
469
682
|
senderPubKey,
|
|
470
|
-
lensConfig,
|
|
683
|
+
lensConfig: normalizedLensConfig,
|
|
471
684
|
capabilities,
|
|
472
685
|
message,
|
|
473
686
|
});
|
|
@@ -513,7 +726,7 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
513
726
|
senderPubKey,
|
|
514
727
|
holonId,
|
|
515
728
|
holonName,
|
|
516
|
-
lensConfig = { inbound: [], outbound: [] },
|
|
729
|
+
lensConfig = { inbound: [], outbound: [], writeInbound: [], writeOutbound: [] },
|
|
517
730
|
message,
|
|
518
731
|
} = params;
|
|
519
732
|
|
|
@@ -521,6 +734,14 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
521
734
|
// Get responder's public key
|
|
522
735
|
const responderPubKey = getPublicKey(privateKey);
|
|
523
736
|
|
|
737
|
+
// Normalize lensConfig
|
|
738
|
+
const normalizedLensConfig = {
|
|
739
|
+
inbound: lensConfig.inbound || [],
|
|
740
|
+
outbound: lensConfig.outbound || [],
|
|
741
|
+
writeInbound: lensConfig.writeInbound || [],
|
|
742
|
+
writeOutbound: lensConfig.writeOutbound || [],
|
|
743
|
+
};
|
|
744
|
+
|
|
524
745
|
// Add sender as federated partner in Nostr registry
|
|
525
746
|
if (holosphere.client) {
|
|
526
747
|
await addFederatedPartner(holosphere.client, holosphere.config.appName, senderPubKey, {
|
|
@@ -535,6 +756,48 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
535
756
|
}
|
|
536
757
|
}
|
|
537
758
|
|
|
759
|
+
// Process write grants from sender (they offered writeOutbound = we get write access)
|
|
760
|
+
const senderWriteOutbound = request.lensConfig?.writeOutbound || [];
|
|
761
|
+
if (senderWriteOutbound.length > 0) {
|
|
762
|
+
console.log('[Handshake] Sender is granting write access for lenses:', senderWriteOutbound);
|
|
763
|
+
// Note: This is tracked on the sender's side - we just acknowledge it in the response
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Grant write access to sender for our writeOutbound lenses
|
|
767
|
+
// This allows the sender to write to these lenses in our holon
|
|
768
|
+
for (const lensName of normalizedLensConfig.writeOutbound) {
|
|
769
|
+
try {
|
|
770
|
+
// New unified grant
|
|
771
|
+
await grantAccess(
|
|
772
|
+
holosphere.client,
|
|
773
|
+
holosphere.config.appName,
|
|
774
|
+
senderPubKey,
|
|
775
|
+
{ holonId: '*', lensName },
|
|
776
|
+
['write'],
|
|
777
|
+
{ direction: 'inbound' }
|
|
778
|
+
);
|
|
779
|
+
console.log(`[Handshake] Granted write access to sender for lens "${lensName}"`);
|
|
780
|
+
} catch (err) {
|
|
781
|
+
console.warn(`[Handshake] Failed to grant write access for ${lensName}:`, err.message);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Also grant read access for outbound lenses using unified format
|
|
786
|
+
for (const lensName of normalizedLensConfig.outbound) {
|
|
787
|
+
try {
|
|
788
|
+
await grantAccess(
|
|
789
|
+
holosphere.client,
|
|
790
|
+
holosphere.config.appName,
|
|
791
|
+
senderPubKey,
|
|
792
|
+
{ holonId: '*', lensName },
|
|
793
|
+
['read'],
|
|
794
|
+
{ direction: 'outbound' }
|
|
795
|
+
);
|
|
796
|
+
} catch (err) {
|
|
797
|
+
console.warn(`[Handshake] Failed to grant read access for ${lensName}:`, err.message);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
538
801
|
// Register the sender's holon -> pubkey mapping in our holon registry
|
|
539
802
|
// This is critical for resolveHolonToPubkey to work when navigating to their holon
|
|
540
803
|
if (request.senderHolonId) {
|
|
@@ -552,13 +815,13 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
552
815
|
// This is what makes the partner appear in getFederation() / loadFederationData()
|
|
553
816
|
// Store the sender's name so we can display it without reading their settings
|
|
554
817
|
await holosphere.federateHolon(holonId, senderPubKey, {
|
|
555
|
-
lensConfig,
|
|
818
|
+
lensConfig: normalizedLensConfig,
|
|
556
819
|
partnerName: request.senderHolonName
|
|
557
820
|
});
|
|
558
821
|
|
|
559
822
|
// Issue capabilities for our outbound lenses (lenses we're sharing with partner)
|
|
560
823
|
const capabilities = [];
|
|
561
|
-
for (const lensName of
|
|
824
|
+
for (const lensName of normalizedLensConfig.outbound) {
|
|
562
825
|
try {
|
|
563
826
|
const token = await issueCapability(
|
|
564
827
|
['read'],
|
|
@@ -580,6 +843,37 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
580
843
|
}
|
|
581
844
|
}
|
|
582
845
|
|
|
846
|
+
// Share lens keys for encrypted outbound lenses
|
|
847
|
+
// This allows the partner to decrypt data from our shared lenses
|
|
848
|
+
if (holosphere.keyStore && holosphere.client?.privateKey) {
|
|
849
|
+
for (const lensName of normalizedLensConfig.outbound) {
|
|
850
|
+
const lensPath = buildLensPath(holosphere.config.appName, responderPubKey, lensName);
|
|
851
|
+
const lensKey = holosphere.keyStore.getKey(lensPath);
|
|
852
|
+
|
|
853
|
+
if (lensKey) {
|
|
854
|
+
try {
|
|
855
|
+
// Wrap the lens key for the partner
|
|
856
|
+
const wrappedKey = holosphere.keyStore.wrapKeyForPartner(
|
|
857
|
+
lensPath,
|
|
858
|
+
holosphere.client.privateKey,
|
|
859
|
+
senderPubKey
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
if (wrappedKey) {
|
|
863
|
+
// Send the wrapped key via encrypted DM
|
|
864
|
+
await sendLensKeyShare(holosphere.client, privateKey, senderPubKey, {
|
|
865
|
+
lensPath,
|
|
866
|
+
wrappedKey,
|
|
867
|
+
});
|
|
868
|
+
console.log(`[Handshake] Shared lens key for "${lensName}" with partner`);
|
|
869
|
+
}
|
|
870
|
+
} catch (err) {
|
|
871
|
+
console.warn(`[Handshake] Failed to share lens key for ${lensName}:`, err.message);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
583
877
|
// Create and send response
|
|
584
878
|
const response = createFederationResponse({
|
|
585
879
|
requestId: request.requestId,
|
|
@@ -587,7 +881,7 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
587
881
|
responderHolonId: holonId,
|
|
588
882
|
responderHolonName: holonName,
|
|
589
883
|
responderPubKey,
|
|
590
|
-
lensConfig,
|
|
884
|
+
lensConfig: normalizedLensConfig,
|
|
591
885
|
capabilities,
|
|
592
886
|
message,
|
|
593
887
|
});
|
|
@@ -604,7 +898,7 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
604
898
|
// Auto-receive federated lens data as holograms for configured inbound lenses
|
|
605
899
|
// This enables the user to immediately see the partner's data in their holon
|
|
606
900
|
const receivedHolograms = {};
|
|
607
|
-
const inboundLenses =
|
|
901
|
+
const inboundLenses = normalizedLensConfig.inbound;
|
|
608
902
|
|
|
609
903
|
if (inboundLenses.length > 0 && holosphere.receiveFederatedLens) {
|
|
610
904
|
console.log('[Handshake] Receiving federated lens data as holograms...');
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { buildPath, write, read, update } from '../storage/unified-storage.js';
|
|
19
|
-
import { verifyCapability, issueCapability } from '../crypto/secp256k1.js';
|
|
19
|
+
import { verifyCapability, issueCapability, verifyCapabilityIssuer, decodeCapability } from '../crypto/secp256k1.js';
|
|
20
20
|
import { getCapabilityForAuthor, storeInboundCapability } from './registry.js';
|
|
21
21
|
|
|
22
22
|
/** @constant {number} Maximum depth for hologram resolution chain */
|
|
@@ -134,6 +134,7 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
134
134
|
capability, // Always the token string (normalized above)
|
|
135
135
|
_meta: {
|
|
136
136
|
created: Date.now(),
|
|
137
|
+
lastUpdated: Date.now(),
|
|
137
138
|
sourceHolon,
|
|
138
139
|
source: sourceHolon, // Alias for compatibility
|
|
139
140
|
sourcePubKey: authorPubKey,
|
|
@@ -158,6 +159,7 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
158
159
|
* @param {string} appname - Application namespace
|
|
159
160
|
* @param {Object} options - Hologram options
|
|
160
161
|
* @param {string} [options.sourceAuthorPubKey] - Source author's public key (defaults to client.publicKey)
|
|
162
|
+
* @param {string} [options.sourceAuthorPrivKey] - Source author's private key for signing capability (defaults to client.privateKey)
|
|
161
163
|
* @param {string} [options.targetAuthorPubKey] - Target author's public key (defaults to client.publicKey)
|
|
162
164
|
* @param {string} [options.capability] - Pre-issued capability (if not provided, one will be issued)
|
|
163
165
|
* @param {string[]} [options.permissions=['read']] - Permissions for auto-issued capability
|
|
@@ -167,6 +169,7 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
167
169
|
export async function createHologramWithCapability(client, sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
|
|
168
170
|
const {
|
|
169
171
|
sourceAuthorPubKey = client.publicKey,
|
|
172
|
+
sourceAuthorPrivKey = client.privateKey, // Allow passing author's private key for signing
|
|
170
173
|
targetAuthorPubKey = client.publicKey,
|
|
171
174
|
capability: providedCapability = null,
|
|
172
175
|
permissions = ['read'],
|
|
@@ -187,7 +190,7 @@ export async function createHologramWithCapability(client, sourceHolon, targetHo
|
|
|
187
190
|
{
|
|
188
191
|
expiresIn: expiresIn !== null ? expiresIn : (isSelfCapability ? 365 * 24 * 60 * 60 * 1000 : 3600000), // 1 year for self, 1 hour for cross
|
|
189
192
|
issuer: sourceAuthorPubKey,
|
|
190
|
-
issuerKey:
|
|
193
|
+
issuerKey: sourceAuthorPrivKey, // Use passed private key (holon's key when federated via holonsbot)
|
|
191
194
|
}
|
|
192
195
|
);
|
|
193
196
|
|
|
@@ -316,6 +319,21 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
316
319
|
return null;
|
|
317
320
|
}
|
|
318
321
|
|
|
322
|
+
// OBJECT-CAPABILITY SECURITY: Verify capability issuer matches claimed authorPubKey
|
|
323
|
+
// This prevents forged capabilities where someone signs a capability for data they don't own
|
|
324
|
+
if (authorPubKey) {
|
|
325
|
+
const issuerMatch = await verifyCapabilityIssuer(capability, authorPubKey);
|
|
326
|
+
if (!issuerMatch) {
|
|
327
|
+
console.warn(`❌ Capability issuer does not match authorPubKey for hologram: ${soul}`);
|
|
328
|
+
console.warn(` Expected author: ${authorPubKey?.slice(0, 12)}...`);
|
|
329
|
+
const decoded = decodeCapability(capability);
|
|
330
|
+
if (decoded) {
|
|
331
|
+
console.warn(` Actual issuer: ${decoded.issuer?.slice(0, 12)}...`);
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
319
337
|
const isValid = await verifyCapability(
|
|
320
338
|
capability,
|
|
321
339
|
'read',
|
|
@@ -453,6 +471,7 @@ export async function setupFederation(
|
|
|
453
471
|
* @param {string} mode - 'reference' or 'copy'
|
|
454
472
|
* @param {Object} options - Propagation options
|
|
455
473
|
* @param {string} [options.sourceAuthorPubKey] - Source author (defaults to client.publicKey)
|
|
474
|
+
* @param {string} [options.sourceAuthorPrivKey] - Source author's private key for signing capability (defaults to client.privateKey)
|
|
456
475
|
* @param {string} [options.capability] - Pre-issued capability (auto-issued if not provided)
|
|
457
476
|
* @returns {Promise<boolean>} Success indicator
|
|
458
477
|
*/
|
|
@@ -606,14 +625,6 @@ export async function propagateData(
|
|
|
606
625
|
const isPubkey = typeof targetHolon === 'string' && /^[0-9a-f]{64}$/i.test(targetHolon);
|
|
607
626
|
const targetAuthorPubKey = options.targetAuthorPubKey || (isPubkey ? targetHolon : client.publicKey);
|
|
608
627
|
|
|
609
|
-
console.log('[propagateData] Creating hologram with capability:', {
|
|
610
|
-
sourceHolon: sourceHolon?.slice(0, 12) + '...',
|
|
611
|
-
targetHolon: targetHolon?.slice(0, 12) + '...',
|
|
612
|
-
sourceAuthorPubKey: sourceAuthorPubKey?.slice(0, 12) + '...',
|
|
613
|
-
targetAuthorPubKey: targetAuthorPubKey?.slice(0, 12) + '...',
|
|
614
|
-
hasProvidedCapability: !!options.capability
|
|
615
|
-
});
|
|
616
|
-
|
|
617
628
|
const hologram = await createHologramWithCapability(
|
|
618
629
|
client,
|
|
619
630
|
sourceHolon,
|
|
@@ -623,6 +634,7 @@ export async function propagateData(
|
|
|
623
634
|
appname,
|
|
624
635
|
{
|
|
625
636
|
sourceAuthorPubKey,
|
|
637
|
+
sourceAuthorPrivKey: options.sourceAuthorPrivKey, // Pass through for holon-signed capabilities
|
|
626
638
|
targetAuthorPubKey,
|
|
627
639
|
capability: options.capability,
|
|
628
640
|
permissions: ['read'],
|
|
@@ -745,9 +757,10 @@ export async function addActiveHologram(client, appname, sourceHolon, lensName,
|
|
|
745
757
|
let sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
|
|
746
758
|
|
|
747
759
|
// Read existing source data
|
|
760
|
+
// Note: May return null if called immediately after write (before relay persistence)
|
|
761
|
+
// This is expected - the hologram still works, we just can't track activeHolograms
|
|
748
762
|
let sourceData = await read(client, sourcePath);
|
|
749
763
|
if (!sourceData) {
|
|
750
|
-
console.warn(`Source data not found at ${sourcePath}`);
|
|
751
764
|
return false;
|
|
752
765
|
}
|
|
753
766
|
|
|
@@ -765,14 +778,10 @@ export async function addActiveHologram(client, appname, sourceHolon, lensName,
|
|
|
765
778
|
|
|
766
779
|
sourceData = await read(client, sourcePath);
|
|
767
780
|
if (!sourceData) {
|
|
768
|
-
console.warn(`Real source data not found at ${sourcePath}`);
|
|
769
781
|
return false;
|
|
770
782
|
}
|
|
771
783
|
}
|
|
772
784
|
|
|
773
|
-
if (depth > 0) {
|
|
774
|
-
console.info(`📍 Followed hologram chain (depth: ${depth}) to real source: ${currentHolon}/${currentLens}/${currentDataId}`);
|
|
775
|
-
}
|
|
776
785
|
|
|
777
786
|
// Initialize _meta and activeHolograms if needed
|
|
778
787
|
if (!sourceData._meta) {
|
|
@@ -885,9 +894,9 @@ export async function updateActiveHologramPlatform(client, appname, sourceHolon,
|
|
|
885
894
|
let sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
|
|
886
895
|
|
|
887
896
|
// Read existing source data
|
|
897
|
+
// Note: May return null if called immediately after write (before relay persistence)
|
|
888
898
|
let sourceData = await read(client, sourcePath);
|
|
889
899
|
if (!sourceData) {
|
|
890
|
-
console.warn(`Source data not found at ${sourcePath}`);
|
|
891
900
|
return false;
|
|
892
901
|
}
|
|
893
902
|
|
|
@@ -904,7 +913,6 @@ export async function updateActiveHologramPlatform(client, appname, sourceHolon,
|
|
|
904
913
|
|
|
905
914
|
sourceData = await read(client, sourcePath);
|
|
906
915
|
if (!sourceData) {
|
|
907
|
-
console.warn(`Real source data not found at ${sourcePath}`);
|
|
908
916
|
return false;
|
|
909
917
|
}
|
|
910
918
|
}
|