holosphere 2.0.0-alpha11 → 2.0.0-alpha13
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-D2OG2idw.js → 2019-CLMqIAfQ.js} +1722 -1668
- package/dist/{2019-D2OG2idw.js.map → 2019-CLMqIAfQ.js.map} +1 -1
- package/dist/2019-Cp3uYhyY.cjs +8 -0
- package/dist/{2019-EION3wKo.cjs.map → 2019-Cp3uYhyY.cjs.map} +1 -1
- package/dist/browser-D6cNVl0v.cjs +2 -0
- package/dist/{browser-Cq59Ij19.cjs.map → browser-D6cNVl0v.cjs.map} +1 -1
- package/dist/{browser-BSniCNqO.js → browser-nUQt1cnB.js} +2 -2
- package/dist/{browser-BSniCNqO.js.map → browser-nUQt1cnB.js.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +67 -50
- package/dist/{index-D-jZhliX.js → index-BN_uoxQK.js} +20324 -735
- package/dist/index-BN_uoxQK.js.map +1 -0
- package/dist/{index-Bl6rM1NW.js → index-CoAjtqsD.js} +2 -2
- package/dist/{index-Bl6rM1NW.js.map → index-CoAjtqsD.js.map} +1 -1
- package/dist/{index-Bwg3OzRM.cjs → index-Cp3tI53z.cjs} +3 -3
- package/dist/{index-Bwg3OzRM.cjs.map → index-Cp3tI53z.cjs.map} +1 -1
- package/dist/index-DJjGSwXG.cjs +13 -0
- package/dist/index-DJjGSwXG.cjs.map +1 -0
- package/dist/index-V8EHMYEY.cjs +29 -0
- package/dist/index-V8EHMYEY.cjs.map +1 -0
- package/dist/index-Z5TstN1e.js +11663 -0
- package/dist/index-Z5TstN1e.js.map +1 -0
- package/dist/indexeddb-storage-CZK5A7XH.cjs +2 -0
- package/dist/indexeddb-storage-CZK5A7XH.cjs.map +1 -0
- package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-bpA01pAU.js} +39 -2
- package/dist/indexeddb-storage-bpA01pAU.js.map +1 -0
- package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-B1k8Jszd.cjs} +2 -2
- package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-B1k8Jszd.cjs.map} +1 -1
- package/dist/{memory-storage-CI-gfmuG.js → memory-storage-BqhmytP_.js} +2 -2
- package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-BqhmytP_.js.map} +1 -1
- package/docs/FEDERATION.md +474 -0
- package/package.json +3 -1
- package/src/crypto/nostr-utils.js +7 -0
- package/src/crypto/secp256k1.js +104 -38
- package/src/federation/capabilities.js +162 -0
- package/src/federation/card-storage.js +376 -0
- package/src/federation/handshake.js +561 -9
- package/src/federation/hologram.js +194 -57
- package/src/federation/holon-registry.js +187 -0
- package/src/federation/index.js +68 -0
- package/src/federation/registry.js +164 -6
- package/src/federation/request-card.js +373 -0
- package/src/hierarchical/upcast.js +19 -3
- package/src/index.js +209 -75
- package/src/lib/federation-methods.js +527 -5
- package/src/storage/indexeddb-storage.js +41 -0
- package/src/storage/nostr-async.js +14 -5
- package/src/storage/nostr-client.js +471 -155
- package/src/storage/nostr-wrapper.js +6 -3
- package/dist/2019-EION3wKo.cjs +0 -8
- package/dist/_commonjsHelpers-C37NGDzP.cjs +0 -2
- package/dist/_commonjsHelpers-C37NGDzP.cjs.map +0 -1
- package/dist/_commonjsHelpers-CUmg6egw.js +0 -7
- package/dist/_commonjsHelpers-CUmg6egw.js.map +0 -1
- package/dist/browser-Cq59Ij19.cjs +0 -2
- package/dist/index-D-jZhliX.js.map +0 -1
- package/dist/index-Dc6Z8Aob.cjs +0 -18
- package/dist/index-Dc6Z8Aob.cjs.map +0 -1
- package/dist/indexeddb-storage-5eiUNsHC.js.map +0 -1
- package/dist/indexeddb-storage-FNFUVvTJ.cjs +0 -2
- package/dist/indexeddb-storage-FNFUVvTJ.cjs.map +0 -1
- package/dist/secp256k1-CEwJNcfV.js +0 -1890
- package/dist/secp256k1-CEwJNcfV.js.map +0 -1
- package/dist/secp256k1-CiEONUnj.cjs +0 -12
- package/dist/secp256k1-CiEONUnj.cjs.map +0 -1
|
@@ -15,8 +15,12 @@ import {
|
|
|
15
15
|
createDMEvent,
|
|
16
16
|
hexToNpub,
|
|
17
17
|
generateNonce,
|
|
18
|
+
getPublicKey,
|
|
18
19
|
} from '../crypto/nostr-utils.js';
|
|
19
20
|
import { addFederatedPartner, storeInboundCapability } from './registry.js';
|
|
21
|
+
import { registerHolon } from './holon-registry.js';
|
|
22
|
+
import { issueCapability } from '../crypto/secp256k1.js';
|
|
23
|
+
import * as cardStorage from './card-storage.js';
|
|
20
24
|
|
|
21
25
|
// ============================================================================
|
|
22
26
|
// Types (documented for reference)
|
|
@@ -194,26 +198,59 @@ export async function sendFederationResponse(client, privateKey, recipientPubKey
|
|
|
194
198
|
|
|
195
199
|
/**
|
|
196
200
|
* Subscribe to incoming federation DMs
|
|
201
|
+
* Fetches all historical DMs since last sync, then subscribes to new ones.
|
|
202
|
+
* DMs persist on relays like email - no need for both parties to be online.
|
|
197
203
|
* @param {Object} client - NostrClient instance with subscribe method
|
|
198
204
|
* @param {string} privateKey - Recipient's hex private key
|
|
199
205
|
* @param {string} publicKey - Recipient's hex public key
|
|
200
206
|
* @param {Object} handlers
|
|
201
207
|
* @param {Function} handlers.onRequest - Called with (request, senderPubKey)
|
|
202
208
|
* @param {Function} handlers.onResponse - Called with (response, senderPubKey)
|
|
209
|
+
* @param {Function} [handlers.onUpdate] - Called with (update, senderPubKey)
|
|
210
|
+
* @param {Function} [handlers.onUpdateResponse] - Called with (response, senderPubKey)
|
|
211
|
+
* @param {Object} options - Subscription options
|
|
212
|
+
* @param {string} options.appname - Application namespace (for persistent storage)
|
|
203
213
|
* @returns {Function} Unsubscribe function
|
|
204
214
|
*/
|
|
205
|
-
export function subscribeToFederationDMs(client, privateKey, publicKey, handlers) {
|
|
206
|
-
const { onRequest, onResponse } = handlers;
|
|
215
|
+
export function subscribeToFederationDMs(client, privateKey, publicKey, handlers, options = {}) {
|
|
216
|
+
const { onRequest, onResponse, onUpdate, onUpdateResponse } = handlers;
|
|
217
|
+
const { appname } = options;
|
|
207
218
|
let isActive = true;
|
|
219
|
+
|
|
220
|
+
// In-memory deduplication for this session
|
|
208
221
|
const processedEventIds = new Set();
|
|
209
222
|
|
|
223
|
+
// Track processed DMs and responses persistently to prevent duplicate notifications
|
|
224
|
+
let processedDMIds = new Set();
|
|
225
|
+
let processedResponseIds = new Set();
|
|
226
|
+
let lastFetchTime = 0;
|
|
227
|
+
|
|
228
|
+
// Load processed DMs and responses from storage if appname is provided
|
|
229
|
+
const loadPersistedState = async () => {
|
|
230
|
+
if (appname && client?.client) {
|
|
231
|
+
try {
|
|
232
|
+
processedDMIds = await cardStorage.getProcessedDMIds(client.client, appname);
|
|
233
|
+
processedResponseIds = await cardStorage.getProcessedResponseIds(client.client, appname);
|
|
234
|
+
lastFetchTime = await cardStorage.getLastDMFetchTime(client.client, appname);
|
|
235
|
+
console.log('[Handshake] Loaded persisted state - last fetch:', lastFetchTime, 'processed DMs:', processedDMIds.size);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
console.warn('[Handshake] Could not load persisted state:', err.message);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
|
|
210
242
|
const handleEvent = async (event) => {
|
|
211
243
|
if (!isActive) return;
|
|
212
244
|
|
|
213
|
-
// Skip duplicates
|
|
245
|
+
// Skip duplicates (in-memory check for this session)
|
|
214
246
|
if (processedEventIds.has(event.id)) return;
|
|
215
247
|
processedEventIds.add(event.id);
|
|
216
248
|
|
|
249
|
+
// Skip if already processed in persistent storage
|
|
250
|
+
if (processedDMIds.has(event.id)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
217
254
|
// Only kind 4 (encrypted DM) events tagged to us
|
|
218
255
|
if (event.kind !== 4) return;
|
|
219
256
|
|
|
@@ -234,11 +271,83 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
234
271
|
const payload = JSON.parse(decrypted);
|
|
235
272
|
|
|
236
273
|
if (payload.type === 'federation_request' && payload.version === '1.0') {
|
|
274
|
+
// Check if this request was already dismissed
|
|
275
|
+
if (appname && client?.client) {
|
|
276
|
+
const isDismissed = await cardStorage.isRequestDismissed(client.client, appname, payload.requestId);
|
|
277
|
+
if (isDismissed) {
|
|
278
|
+
console.log('[Handshake] Skipping dismissed request:', payload.requestId);
|
|
279
|
+
// Mark this DM as processed so we don't check again
|
|
280
|
+
await cardStorage.markDMProcessed(client.client, appname, event.id, 'request');
|
|
281
|
+
processedDMIds.add(event.id);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Mark DM as processed before calling handler
|
|
287
|
+
if (appname && client?.client) {
|
|
288
|
+
await cardStorage.markDMProcessed(client.client, appname, event.id, 'request');
|
|
289
|
+
processedDMIds.add(event.id);
|
|
290
|
+
}
|
|
291
|
+
|
|
237
292
|
console.log('[Handshake] Received federation request from:', event.pubkey.substring(0, 8) + '...');
|
|
238
293
|
onRequest?.(payload, event.pubkey);
|
|
239
294
|
} else if (payload.type === 'federation_response' && payload.version === '1.0') {
|
|
295
|
+
// Check if this response was already processed (persistent check)
|
|
296
|
+
const responseKey = `${payload.requestId}_${event.pubkey}`;
|
|
297
|
+
if (processedResponseIds.has(responseKey)) {
|
|
298
|
+
console.log('[Handshake] Skipping already processed response:', responseKey);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Mark as processed in persistent storage
|
|
303
|
+
if (appname && client?.client) {
|
|
304
|
+
await cardStorage.markResponseProcessed(client.client, appname, payload.requestId, event.pubkey);
|
|
305
|
+
await cardStorage.markDMProcessed(client.client, appname, event.id, 'response');
|
|
306
|
+
processedResponseIds.add(responseKey);
|
|
307
|
+
processedDMIds.add(event.id);
|
|
308
|
+
}
|
|
309
|
+
|
|
240
310
|
console.log('[Handshake] Received federation response from:', event.pubkey.substring(0, 8) + '...');
|
|
241
311
|
onResponse?.(payload, event.pubkey);
|
|
312
|
+
} else if (payload.type === 'federation_update' && payload.version === '1.0') {
|
|
313
|
+
// Federation update request (lens config change)
|
|
314
|
+
// Check if this update was already dismissed
|
|
315
|
+
if (appname && client?.client) {
|
|
316
|
+
const isDismissed = await cardStorage.isRequestDismissed(client.client, appname, payload.updateId);
|
|
317
|
+
if (isDismissed) {
|
|
318
|
+
console.log('[Handshake] Skipping dismissed update:', payload.updateId);
|
|
319
|
+
await cardStorage.markDMProcessed(client.client, appname, event.id, 'update');
|
|
320
|
+
processedDMIds.add(event.id);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Mark DM as processed
|
|
326
|
+
if (appname && client?.client) {
|
|
327
|
+
await cardStorage.markDMProcessed(client.client, appname, event.id, 'update');
|
|
328
|
+
processedDMIds.add(event.id);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
console.log('[Handshake] Received federation update from:', event.pubkey.substring(0, 8) + '...');
|
|
332
|
+
onUpdate?.(payload, event.pubkey);
|
|
333
|
+
} else if (payload.type === 'federation_update_response' && payload.version === '1.0') {
|
|
334
|
+
// Federation update response
|
|
335
|
+
const responseKey = `update_${payload.updateId}_${event.pubkey}`;
|
|
336
|
+
if (processedResponseIds.has(responseKey)) {
|
|
337
|
+
console.log('[Handshake] Skipping already processed update response:', responseKey);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Mark as processed
|
|
342
|
+
if (appname && client?.client) {
|
|
343
|
+
await cardStorage.markResponseProcessed(client.client, appname, payload.updateId, event.pubkey);
|
|
344
|
+
await cardStorage.markDMProcessed(client.client, appname, event.id, 'update_response');
|
|
345
|
+
processedResponseIds.add(responseKey);
|
|
346
|
+
processedDMIds.add(event.id);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
console.log('[Handshake] Received federation update response from:', event.pubkey.substring(0, 8) + '...');
|
|
350
|
+
onUpdateResponse?.(payload, event.pubkey);
|
|
242
351
|
}
|
|
243
352
|
} catch (error) {
|
|
244
353
|
// Silently ignore non-federation DMs
|
|
@@ -250,6 +359,9 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
250
359
|
let subscription = null;
|
|
251
360
|
|
|
252
361
|
const startSubscription = async () => {
|
|
362
|
+
// Load persisted state first
|
|
363
|
+
await loadPersistedState();
|
|
364
|
+
|
|
253
365
|
// Get the NostrClient from HoloSphere instance
|
|
254
366
|
const nostrClient = client?.client;
|
|
255
367
|
if (!nostrClient?.subscribe) {
|
|
@@ -257,16 +369,27 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
257
369
|
return;
|
|
258
370
|
}
|
|
259
371
|
|
|
372
|
+
// Use last fetch time or 0 for first-time sync (fetch all historical DMs)
|
|
373
|
+
// This makes federation DMs work like email - they persist on relays
|
|
374
|
+
const sinceTime = lastFetchTime || 0;
|
|
375
|
+
console.log('[Handshake] Fetching DMs since:', sinceTime ? new Date(sinceTime * 1000).toISOString() : 'beginning of time');
|
|
376
|
+
|
|
260
377
|
const filter = {
|
|
261
378
|
kinds: [4],
|
|
262
379
|
'#p': [publicKey],
|
|
263
|
-
since:
|
|
380
|
+
since: sinceTime,
|
|
264
381
|
};
|
|
265
382
|
|
|
266
383
|
try {
|
|
267
384
|
// NostrClient.subscribe(filter, onEvent, options) returns { unsubscribe }
|
|
268
385
|
subscription = await nostrClient.subscribe(filter, handleEvent, {});
|
|
269
386
|
console.log('[Handshake] Federation DM subscription started');
|
|
387
|
+
|
|
388
|
+
// Update last fetch time to now (for next session)
|
|
389
|
+
if (appname && client?.client) {
|
|
390
|
+
const now = Math.floor(Date.now() / 1000);
|
|
391
|
+
await cardStorage.setLastDMFetchTime(client.client, appname, now);
|
|
392
|
+
}
|
|
270
393
|
} catch (error) {
|
|
271
394
|
console.error('[Handshake] Failed to subscribe:', error);
|
|
272
395
|
}
|
|
@@ -313,16 +436,39 @@ export async function initiateFederationHandshake(holosphere, privateKey, params
|
|
|
313
436
|
|
|
314
437
|
try {
|
|
315
438
|
// Get sender's public key
|
|
316
|
-
const { getPublicKey } = await import('../crypto/nostr-utils.js');
|
|
317
439
|
const senderPubKey = getPublicKey(privateKey);
|
|
318
440
|
|
|
441
|
+
// Issue capabilities for outbound lenses (lenses we're sharing with partner)
|
|
442
|
+
const capabilities = [];
|
|
443
|
+
for (const lensName of (lensConfig.outbound || [])) {
|
|
444
|
+
try {
|
|
445
|
+
const token = await issueCapability(
|
|
446
|
+
['read'],
|
|
447
|
+
{ holonId, lensName, dataId: '*' },
|
|
448
|
+
partnerPubKey,
|
|
449
|
+
{
|
|
450
|
+
expiresIn: 365 * 24 * 60 * 60 * 1000, // 1 year
|
|
451
|
+
issuer: senderPubKey,
|
|
452
|
+
issuerKey: privateKey,
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
capabilities.push({
|
|
456
|
+
token,
|
|
457
|
+
scope: { holonId, lensName },
|
|
458
|
+
permissions: ['read'],
|
|
459
|
+
});
|
|
460
|
+
} catch (err) {
|
|
461
|
+
console.warn(`[Handshake] Failed to issue capability for ${lensName}:`, err.message);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
319
465
|
// Create federation request
|
|
320
466
|
const request = createFederationRequest({
|
|
321
467
|
senderHolonId: holonId,
|
|
322
468
|
senderHolonName: holonName,
|
|
323
469
|
senderPubKey,
|
|
324
470
|
lensConfig,
|
|
325
|
-
capabilities
|
|
471
|
+
capabilities,
|
|
326
472
|
message,
|
|
327
473
|
});
|
|
328
474
|
|
|
@@ -373,7 +519,6 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
373
519
|
|
|
374
520
|
try {
|
|
375
521
|
// Get responder's public key
|
|
376
|
-
const { getPublicKey } = await import('../crypto/nostr-utils.js');
|
|
377
522
|
const responderPubKey = getPublicKey(privateKey);
|
|
378
523
|
|
|
379
524
|
// Add sender as federated partner in Nostr registry
|
|
@@ -389,6 +534,18 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
389
534
|
await storeInboundCapability(holosphere.client, holosphere.config.appName, senderPubKey, cap);
|
|
390
535
|
}
|
|
391
536
|
}
|
|
537
|
+
|
|
538
|
+
// Register the sender's holon -> pubkey mapping in our holon registry
|
|
539
|
+
// This is critical for resolveHolonToPubkey to work when navigating to their holon
|
|
540
|
+
if (request.senderHolonId) {
|
|
541
|
+
await registerHolon(holosphere.client, holosphere.config.appName, request.senderHolonId, senderPubKey, {
|
|
542
|
+
alias: request.senderHolonName,
|
|
543
|
+
});
|
|
544
|
+
console.log('[Handshake] Registered partner holon:', {
|
|
545
|
+
holonId: request.senderHolonId,
|
|
546
|
+
pubKey: senderPubKey?.slice(0, 12) + '...'
|
|
547
|
+
});
|
|
548
|
+
}
|
|
392
549
|
}
|
|
393
550
|
|
|
394
551
|
// Create the actual federation record in GunDB storage
|
|
@@ -399,6 +556,30 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
399
556
|
partnerName: request.senderHolonName
|
|
400
557
|
});
|
|
401
558
|
|
|
559
|
+
// Issue capabilities for our outbound lenses (lenses we're sharing with partner)
|
|
560
|
+
const capabilities = [];
|
|
561
|
+
for (const lensName of (lensConfig.outbound || [])) {
|
|
562
|
+
try {
|
|
563
|
+
const token = await issueCapability(
|
|
564
|
+
['read'],
|
|
565
|
+
{ holonId, lensName, dataId: '*' },
|
|
566
|
+
senderPubKey,
|
|
567
|
+
{
|
|
568
|
+
expiresIn: 365 * 24 * 60 * 60 * 1000, // 1 year
|
|
569
|
+
issuer: responderPubKey,
|
|
570
|
+
issuerKey: privateKey,
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
capabilities.push({
|
|
574
|
+
token,
|
|
575
|
+
scope: { holonId, lensName },
|
|
576
|
+
permissions: ['read'],
|
|
577
|
+
});
|
|
578
|
+
} catch (err) {
|
|
579
|
+
console.warn(`[Handshake] Failed to issue capability for ${lensName}:`, err.message);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
402
583
|
// Create and send response
|
|
403
584
|
const response = createFederationResponse({
|
|
404
585
|
requestId: request.requestId,
|
|
@@ -407,14 +588,50 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
407
588
|
responderHolonName: holonName,
|
|
408
589
|
responderPubKey,
|
|
409
590
|
lensConfig,
|
|
410
|
-
capabilities
|
|
591
|
+
capabilities,
|
|
411
592
|
message,
|
|
412
593
|
});
|
|
413
594
|
|
|
414
595
|
const sent = await sendFederationResponse(holosphere.client, privateKey, senderPubKey, response);
|
|
415
596
|
|
|
416
597
|
if (sent) {
|
|
417
|
-
|
|
598
|
+
// Dismiss the request so it won't reappear
|
|
599
|
+
if (holosphere.client && request.requestId) {
|
|
600
|
+
await cardStorage.dismissRequest(holosphere.client, holosphere.config.appName, request.requestId);
|
|
601
|
+
console.log('[Handshake] Request dismissed after acceptance:', request.requestId);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Auto-receive federated lens data as holograms for configured inbound lenses
|
|
605
|
+
// This enables the user to immediately see the partner's data in their holon
|
|
606
|
+
const receivedHolograms = {};
|
|
607
|
+
const inboundLenses = lensConfig.inbound || [];
|
|
608
|
+
|
|
609
|
+
if (inboundLenses.length > 0 && holosphere.receiveFederatedLens) {
|
|
610
|
+
console.log('[Handshake] Receiving federated lens data as holograms...');
|
|
611
|
+
|
|
612
|
+
for (const lensName of inboundLenses) {
|
|
613
|
+
try {
|
|
614
|
+
// The sender's holon ID comes from the request
|
|
615
|
+
const senderHolonId = request.senderHolonId;
|
|
616
|
+
|
|
617
|
+
const result = await holosphere.receiveFederatedLens(
|
|
618
|
+
senderPubKey,
|
|
619
|
+
senderHolonId,
|
|
620
|
+
lensName,
|
|
621
|
+
holonId,
|
|
622
|
+
{ overwrite: false }
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
receivedHolograms[lensName] = result;
|
|
626
|
+
console.log(`[Handshake] Received ${result.received} holograms for lens "${lensName}"`);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
console.warn(`[Handshake] Failed to receive holograms for lens "${lensName}":`, err.message);
|
|
629
|
+
receivedHolograms[lensName] = { error: err.message };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return { success: true, requestId: request.requestId, receivedHolograms };
|
|
418
635
|
} else {
|
|
419
636
|
return { success: false, error: 'Failed to send response DM' };
|
|
420
637
|
}
|
|
@@ -452,6 +669,86 @@ export async function rejectFederationRequest(holosphere, privateKey, params) {
|
|
|
452
669
|
}
|
|
453
670
|
}
|
|
454
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Process a federation response (called by initiator when receiving acceptance)
|
|
674
|
+
* Stores capabilities from the responder in the local registry and receives holograms
|
|
675
|
+
* @param {Object} holosphere - HoloSphere instance
|
|
676
|
+
* @param {Object} response - Federation response payload
|
|
677
|
+
* @param {string} responderPubKey - Responder's public key
|
|
678
|
+
* @param {Object} [options={}] - Processing options
|
|
679
|
+
* @param {string} [options.holonId] - Initiator's holon ID (for receiving holograms)
|
|
680
|
+
* @param {string[]} [options.inboundLenses] - Lenses to receive from responder
|
|
681
|
+
* @returns {Promise<Object>} Result with success, stored capability count, and received holograms
|
|
682
|
+
*/
|
|
683
|
+
export async function processFederationResponse(holosphere, response, responderPubKey, options = {}) {
|
|
684
|
+
if (!response || response.status !== 'accepted') {
|
|
685
|
+
return { success: false, error: 'Response not accepted', storedCapabilities: 0 };
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const { holonId, inboundLenses = [] } = options;
|
|
689
|
+
let storedCapabilities = 0;
|
|
690
|
+
|
|
691
|
+
// Store capabilities from the responder
|
|
692
|
+
if (holosphere.client && response.capabilities && response.capabilities.length > 0) {
|
|
693
|
+
for (const cap of response.capabilities) {
|
|
694
|
+
try {
|
|
695
|
+
await storeInboundCapability(holosphere.client, holosphere.config.appName, responderPubKey, cap);
|
|
696
|
+
storedCapabilities++;
|
|
697
|
+
} catch (err) {
|
|
698
|
+
console.warn(`[Handshake] Failed to store capability:`, err.message);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Add responder as federated partner if not already
|
|
704
|
+
if (holosphere.client) {
|
|
705
|
+
await addFederatedPartner(holosphere.client, holosphere.config.appName, responderPubKey, {
|
|
706
|
+
alias: response.responderHolonName,
|
|
707
|
+
addedVia: 'handshake_accepted',
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
// Register the responder's holon -> pubkey mapping in our holon registry
|
|
711
|
+
// This is critical for resolveHolonToPubkey to work when navigating to their holon
|
|
712
|
+
if (response.responderHolonId) {
|
|
713
|
+
await registerHolon(holosphere.client, holosphere.config.appName, response.responderHolonId, responderPubKey, {
|
|
714
|
+
alias: response.responderHolonName,
|
|
715
|
+
});
|
|
716
|
+
console.log('[Handshake] Registered partner holon from response:', {
|
|
717
|
+
holonId: response.responderHolonId,
|
|
718
|
+
pubKey: responderPubKey?.slice(0, 12) + '...'
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Auto-receive federated lens data as holograms for configured inbound lenses
|
|
724
|
+
const receivedHolograms = {};
|
|
725
|
+
const responderHolonId = response.responderHolonId;
|
|
726
|
+
|
|
727
|
+
if (holonId && responderHolonId && inboundLenses.length > 0 && holosphere.receiveFederatedLens) {
|
|
728
|
+
console.log('[Handshake] Receiving federated lens data as holograms from responder...');
|
|
729
|
+
|
|
730
|
+
for (const lensName of inboundLenses) {
|
|
731
|
+
try {
|
|
732
|
+
const result = await holosphere.receiveFederatedLens(
|
|
733
|
+
responderPubKey,
|
|
734
|
+
responderHolonId,
|
|
735
|
+
lensName,
|
|
736
|
+
holonId,
|
|
737
|
+
{ overwrite: false }
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
receivedHolograms[lensName] = result;
|
|
741
|
+
console.log(`[Handshake] Received ${result.received} holograms for lens "${lensName}"`);
|
|
742
|
+
} catch (err) {
|
|
743
|
+
console.warn(`[Handshake] Failed to receive holograms for lens "${lensName}":`, err.message);
|
|
744
|
+
receivedHolograms[lensName] = { error: err.message };
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
return { success: true, storedCapabilities, receivedHolograms };
|
|
750
|
+
}
|
|
751
|
+
|
|
455
752
|
// ============================================================================
|
|
456
753
|
// Payload Validation
|
|
457
754
|
// ============================================================================
|
|
@@ -473,3 +770,258 @@ export function isFederationRequest(payload) {
|
|
|
473
770
|
export function isFederationResponse(payload) {
|
|
474
771
|
return payload?.type === 'federation_response' && payload?.version === '1.0';
|
|
475
772
|
}
|
|
773
|
+
|
|
774
|
+
// ============================================================================
|
|
775
|
+
// Federation Update (Renegotiation) Protocol
|
|
776
|
+
// ============================================================================
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* @typedef {Object} FederationUpdatePayload
|
|
780
|
+
* @property {'federation_update'} type
|
|
781
|
+
* @property {'1.0'} version
|
|
782
|
+
* @property {string} updateId - Unique update ID
|
|
783
|
+
* @property {number} timestamp
|
|
784
|
+
* @property {string} senderHolonId
|
|
785
|
+
* @property {string} senderHolonName
|
|
786
|
+
* @property {string} senderNpub
|
|
787
|
+
* @property {Object} newLensConfig - New lens configuration
|
|
788
|
+
* @property {string[]} newLensConfig.inbound
|
|
789
|
+
* @property {string[]} newLensConfig.outbound
|
|
790
|
+
* @property {string} [message]
|
|
791
|
+
*/
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* @typedef {Object} FederationUpdateResponsePayload
|
|
795
|
+
* @property {'federation_update_response'} type
|
|
796
|
+
* @property {'1.0'} version
|
|
797
|
+
* @property {string} updateId - References original update
|
|
798
|
+
* @property {number} timestamp
|
|
799
|
+
* @property {'accepted'|'rejected'} status
|
|
800
|
+
* @property {string} [message]
|
|
801
|
+
*/
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Create a federation update payload
|
|
805
|
+
* @param {Object} params
|
|
806
|
+
* @param {string} params.senderHolonId
|
|
807
|
+
* @param {string} params.senderHolonName
|
|
808
|
+
* @param {string} params.senderPubKey
|
|
809
|
+
* @param {Object} params.newLensConfig
|
|
810
|
+
* @param {string} [params.message]
|
|
811
|
+
* @returns {FederationUpdatePayload}
|
|
812
|
+
*/
|
|
813
|
+
export function createFederationUpdate({
|
|
814
|
+
senderHolonId,
|
|
815
|
+
senderHolonName,
|
|
816
|
+
senderPubKey,
|
|
817
|
+
newLensConfig,
|
|
818
|
+
message,
|
|
819
|
+
}) {
|
|
820
|
+
return {
|
|
821
|
+
type: 'federation_update',
|
|
822
|
+
version: '1.0',
|
|
823
|
+
updateId: generateNonce(),
|
|
824
|
+
timestamp: Date.now(),
|
|
825
|
+
senderHolonId,
|
|
826
|
+
senderHolonName,
|
|
827
|
+
senderNpub: hexToNpub(senderPubKey),
|
|
828
|
+
newLensConfig,
|
|
829
|
+
message,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Create a federation update response payload
|
|
835
|
+
* @param {Object} params
|
|
836
|
+
* @param {string} params.updateId
|
|
837
|
+
* @param {'accepted'|'rejected'} params.status
|
|
838
|
+
* @param {string} [params.message]
|
|
839
|
+
* @returns {FederationUpdateResponsePayload}
|
|
840
|
+
*/
|
|
841
|
+
export function createFederationUpdateResponse({
|
|
842
|
+
updateId,
|
|
843
|
+
status,
|
|
844
|
+
message,
|
|
845
|
+
}) {
|
|
846
|
+
return {
|
|
847
|
+
type: 'federation_update_response',
|
|
848
|
+
version: '1.0',
|
|
849
|
+
updateId,
|
|
850
|
+
timestamp: Date.now(),
|
|
851
|
+
status,
|
|
852
|
+
message,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Send a federation update request DM
|
|
858
|
+
* @param {Object} client - NostrClient instance
|
|
859
|
+
* @param {string} privateKey - Sender's hex private key
|
|
860
|
+
* @param {string} recipientPubKey - Recipient's hex public key
|
|
861
|
+
* @param {FederationUpdatePayload} update - Update payload
|
|
862
|
+
* @returns {Promise<boolean>}
|
|
863
|
+
*/
|
|
864
|
+
export async function sendFederationUpdate(client, privateKey, recipientPubKey, update) {
|
|
865
|
+
try {
|
|
866
|
+
const content = JSON.stringify(update);
|
|
867
|
+
const encrypted = encryptNIP44(privateKey, recipientPubKey, content);
|
|
868
|
+
const event = createDMEvent(recipientPubKey, encrypted, privateKey);
|
|
869
|
+
|
|
870
|
+
if (client?.publish) {
|
|
871
|
+
await client.publish(event);
|
|
872
|
+
console.log('[Handshake] Federation update sent to:', recipientPubKey.substring(0, 8) + '...');
|
|
873
|
+
return true;
|
|
874
|
+
}
|
|
875
|
+
return false;
|
|
876
|
+
} catch (error) {
|
|
877
|
+
console.error('[Handshake] Failed to send federation update:', error);
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Send a federation update response DM
|
|
884
|
+
* @param {Object} client - NostrClient instance
|
|
885
|
+
* @param {string} privateKey - Sender's hex private key
|
|
886
|
+
* @param {string} recipientPubKey - Recipient's hex public key
|
|
887
|
+
* @param {FederationUpdateResponsePayload} response - Response payload
|
|
888
|
+
* @returns {Promise<boolean>}
|
|
889
|
+
*/
|
|
890
|
+
export async function sendFederationUpdateResponse(client, privateKey, recipientPubKey, response) {
|
|
891
|
+
try {
|
|
892
|
+
const content = JSON.stringify(response);
|
|
893
|
+
const encrypted = encryptNIP44(privateKey, recipientPubKey, content);
|
|
894
|
+
const event = createDMEvent(recipientPubKey, encrypted, privateKey);
|
|
895
|
+
|
|
896
|
+
if (client?.publish) {
|
|
897
|
+
await client.publish(event);
|
|
898
|
+
console.log('[Handshake] Federation update response sent to:', recipientPubKey.substring(0, 8) + '...');
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
return false;
|
|
902
|
+
} catch (error) {
|
|
903
|
+
console.error('[Handshake] Failed to send federation update response:', error);
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* Request a federation update (lens config change)
|
|
910
|
+
* @param {Object} holosphere - HoloSphere instance
|
|
911
|
+
* @param {string} privateKey - Sender's hex private key
|
|
912
|
+
* @param {Object} params
|
|
913
|
+
* @param {string} params.partnerPubKey - Partner's hex public key
|
|
914
|
+
* @param {string} params.holonId - Current holon ID
|
|
915
|
+
* @param {string} params.holonName - Current holon name
|
|
916
|
+
* @param {Object} params.newLensConfig - New lens configuration
|
|
917
|
+
* @param {string} [params.message]
|
|
918
|
+
* @returns {Promise<Object>}
|
|
919
|
+
*/
|
|
920
|
+
export async function requestFederationUpdate(holosphere, privateKey, params) {
|
|
921
|
+
const { partnerPubKey, holonId, holonName, newLensConfig, message } = params;
|
|
922
|
+
|
|
923
|
+
try {
|
|
924
|
+
const senderPubKey = getPublicKey(privateKey);
|
|
925
|
+
|
|
926
|
+
const update = createFederationUpdate({
|
|
927
|
+
senderHolonId: holonId,
|
|
928
|
+
senderHolonName: holonName,
|
|
929
|
+
senderPubKey,
|
|
930
|
+
newLensConfig,
|
|
931
|
+
message,
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
const sent = await sendFederationUpdate(holosphere.client, privateKey, partnerPubKey, update);
|
|
935
|
+
|
|
936
|
+
if (sent) {
|
|
937
|
+
return { success: true, updateId: update.updateId };
|
|
938
|
+
}
|
|
939
|
+
return { success: false, error: 'Failed to send update DM' };
|
|
940
|
+
} catch (error) {
|
|
941
|
+
return { success: false, error: error.message };
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Accept a federation update request
|
|
947
|
+
* @param {Object} holosphere - HoloSphere instance
|
|
948
|
+
* @param {string} privateKey - Responder's hex private key
|
|
949
|
+
* @param {Object} params
|
|
950
|
+
* @param {string} params.updateId - Update ID
|
|
951
|
+
* @param {string} params.senderPubKey - Original sender's public key
|
|
952
|
+
* @param {string} params.holonId - Our holon ID
|
|
953
|
+
* @param {Object} params.newLensConfig - The new lens config to apply
|
|
954
|
+
* @param {string} [params.message]
|
|
955
|
+
* @returns {Promise<Object>}
|
|
956
|
+
*/
|
|
957
|
+
export async function acceptFederationUpdate(holosphere, privateKey, params) {
|
|
958
|
+
const { updateId, senderPubKey, holonId, newLensConfig, message } = params;
|
|
959
|
+
|
|
960
|
+
try {
|
|
961
|
+
// Update the local federation with new lens config
|
|
962
|
+
if (holosphere.federateHolon) {
|
|
963
|
+
await holosphere.federateHolon(holonId, senderPubKey, {
|
|
964
|
+
lensConfig: newLensConfig,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// Send acceptance response
|
|
969
|
+
const response = createFederationUpdateResponse({
|
|
970
|
+
updateId,
|
|
971
|
+
status: 'accepted',
|
|
972
|
+
message,
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
const sent = await sendFederationUpdateResponse(holosphere.client, privateKey, senderPubKey, response);
|
|
976
|
+
|
|
977
|
+
return { success: sent, error: sent ? undefined : 'Failed to send response' };
|
|
978
|
+
} catch (error) {
|
|
979
|
+
return { success: false, error: error.message };
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Reject a federation update request
|
|
985
|
+
* @param {Object} holosphere - HoloSphere instance
|
|
986
|
+
* @param {string} privateKey - Responder's hex private key
|
|
987
|
+
* @param {Object} params
|
|
988
|
+
* @param {string} params.updateId - Update ID
|
|
989
|
+
* @param {string} params.senderPubKey - Original sender's public key
|
|
990
|
+
* @param {string} [params.message]
|
|
991
|
+
* @returns {Promise<Object>}
|
|
992
|
+
*/
|
|
993
|
+
export async function rejectFederationUpdate(holosphere, privateKey, params) {
|
|
994
|
+
const { updateId, senderPubKey, message } = params;
|
|
995
|
+
|
|
996
|
+
try {
|
|
997
|
+
const response = createFederationUpdateResponse({
|
|
998
|
+
updateId,
|
|
999
|
+
status: 'rejected',
|
|
1000
|
+
message,
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
const sent = await sendFederationUpdateResponse(holosphere.client, privateKey, senderPubKey, response);
|
|
1004
|
+
|
|
1005
|
+
return { success: sent, error: sent ? undefined : 'Failed to send response' };
|
|
1006
|
+
} catch (error) {
|
|
1007
|
+
return { success: false, error: error.message };
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Check if payload is a federation update
|
|
1013
|
+
* @param {*} payload
|
|
1014
|
+
* @returns {boolean}
|
|
1015
|
+
*/
|
|
1016
|
+
export function isFederationUpdate(payload) {
|
|
1017
|
+
return payload?.type === 'federation_update' && payload?.version === '1.0';
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Check if payload is a federation update response
|
|
1022
|
+
* @param {*} payload
|
|
1023
|
+
* @returns {boolean}
|
|
1024
|
+
*/
|
|
1025
|
+
export function isFederationUpdateResponse(payload) {
|
|
1026
|
+
return payload?.type === 'federation_update_response' && payload?.version === '1.0';
|
|
1027
|
+
}
|