holosphere 2.0.0-alpha12 → 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-DQdDE6DG.js → 2019-CLMqIAfQ.js} +1722 -1668
- package/dist/{2019-DQdDE6DG.js.map → 2019-CLMqIAfQ.js.map} +1 -1
- package/dist/2019-Cp3uYhyY.cjs +8 -0
- package/dist/{2019-Ew-DTDlI.cjs.map → 2019-Cp3uYhyY.cjs.map} +1 -1
- package/dist/{browser-D2qtVhH5.cjs → browser-D6cNVl0v.cjs} +2 -2
- package/dist/{browser-D2qtVhH5.cjs.map → browser-D6cNVl0v.cjs.map} +1 -1
- package/dist/{browser-CckFyRI9.js → browser-nUQt1cnB.js} +2 -2
- package/dist/{browser-CckFyRI9.js.map → browser-nUQt1cnB.js.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +1 -1
- package/dist/{index-TDDyakLc.js → index-BN_uoxQK.js} +610 -128
- package/dist/index-BN_uoxQK.js.map +1 -0
- package/dist/{index-DrYM1LOY.js → index-CoAjtqsD.js} +2 -2
- package/dist/{index-DrYM1LOY.js.map → index-CoAjtqsD.js.map} +1 -1
- package/dist/{index-kyf1sjaC.cjs → index-Cp3tI53z.cjs} +2 -2
- package/dist/{index-kyf1sjaC.cjs.map → index-Cp3tI53z.cjs.map} +1 -1
- package/dist/{index-C0ITDyFo.cjs → index-DJjGSwXG.cjs} +2 -2
- package/dist/{index-C0ITDyFo.cjs.map → index-DJjGSwXG.cjs.map} +1 -1
- package/dist/index-V8EHMYEY.cjs +29 -0
- package/dist/index-V8EHMYEY.cjs.map +1 -0
- package/dist/{index-lbSQUoRz.js → index-Z5TstN1e.js} +2 -2
- package/dist/{index-lbSQUoRz.js.map → index-Z5TstN1e.js.map} +1 -1
- package/dist/indexeddb-storage-CZK5A7XH.cjs +2 -0
- package/dist/indexeddb-storage-CZK5A7XH.cjs.map +1 -0
- package/dist/{indexeddb-storage-CXhjqwhA.js → indexeddb-storage-bpA01pAU.js} +39 -2
- package/dist/indexeddb-storage-bpA01pAU.js.map +1 -0
- package/dist/{memory-storage-D1tc1bjk.cjs → memory-storage-B1k8Jszd.cjs} +2 -2
- package/dist/{memory-storage-D1tc1bjk.cjs.map → memory-storage-B1k8Jszd.cjs.map} +1 -1
- package/dist/{memory-storage-DkewsdcM.js → memory-storage-BqhmytP_.js} +2 -2
- package/dist/{memory-storage-DkewsdcM.js.map → memory-storage-BqhmytP_.js.map} +1 -1
- package/package.json +1 -1
- package/src/crypto/secp256k1.js +58 -9
- package/src/federation/card-storage.js +72 -0
- package/src/federation/handshake.js +363 -8
- package/src/federation/hologram.js +31 -3
- package/src/federation/holon-registry.js +22 -1
- package/src/federation/registry.js +54 -4
- package/src/index.js +52 -4
- package/src/storage/indexeddb-storage.js +41 -0
- package/src/storage/nostr-async.js +12 -3
- package/src/storage/nostr-client.js +112 -11
- package/src/storage/nostr-wrapper.js +5 -2
- package/dist/2019-Ew-DTDlI.cjs +0 -8
- package/dist/index-9sqetkAn.cjs +0 -29
- package/dist/index-9sqetkAn.cjs.map +0 -1
- package/dist/index-TDDyakLc.js.map +0 -1
- package/dist/indexeddb-storage-CXhjqwhA.js.map +0 -1
- package/dist/indexeddb-storage-DFESDYIj.cjs +0 -2
- package/dist/indexeddb-storage-DFESDYIj.cjs.map +0 -1
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getPublicKey,
|
|
19
19
|
} from '../crypto/nostr-utils.js';
|
|
20
20
|
import { addFederatedPartner, storeInboundCapability } from './registry.js';
|
|
21
|
+
import { registerHolon } from './holon-registry.js';
|
|
21
22
|
import { issueCapability } from '../crypto/secp256k1.js';
|
|
22
23
|
import * as cardStorage from './card-storage.js';
|
|
23
24
|
|
|
@@ -197,34 +198,43 @@ export async function sendFederationResponse(client, privateKey, recipientPubKey
|
|
|
197
198
|
|
|
198
199
|
/**
|
|
199
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.
|
|
200
203
|
* @param {Object} client - NostrClient instance with subscribe method
|
|
201
204
|
* @param {string} privateKey - Recipient's hex private key
|
|
202
205
|
* @param {string} publicKey - Recipient's hex public key
|
|
203
206
|
* @param {Object} handlers
|
|
204
207
|
* @param {Function} handlers.onRequest - Called with (request, senderPubKey)
|
|
205
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)
|
|
206
211
|
* @param {Object} options - Subscription options
|
|
207
212
|
* @param {string} options.appname - Application namespace (for persistent storage)
|
|
208
213
|
* @returns {Function} Unsubscribe function
|
|
209
214
|
*/
|
|
210
215
|
export function subscribeToFederationDMs(client, privateKey, publicKey, handlers, options = {}) {
|
|
211
|
-
const { onRequest, onResponse } = handlers;
|
|
216
|
+
const { onRequest, onResponse, onUpdate, onUpdateResponse } = handlers;
|
|
212
217
|
const { appname } = options;
|
|
213
218
|
let isActive = true;
|
|
214
219
|
|
|
215
220
|
// In-memory deduplication for this session
|
|
216
221
|
const processedEventIds = new Set();
|
|
217
222
|
|
|
218
|
-
// Track processed responses persistently to prevent duplicate notifications
|
|
223
|
+
// Track processed DMs and responses persistently to prevent duplicate notifications
|
|
224
|
+
let processedDMIds = new Set();
|
|
219
225
|
let processedResponseIds = new Set();
|
|
226
|
+
let lastFetchTime = 0;
|
|
220
227
|
|
|
221
|
-
// Load processed responses from storage if appname is provided
|
|
222
|
-
const
|
|
228
|
+
// Load processed DMs and responses from storage if appname is provided
|
|
229
|
+
const loadPersistedState = async () => {
|
|
223
230
|
if (appname && client?.client) {
|
|
224
231
|
try {
|
|
232
|
+
processedDMIds = await cardStorage.getProcessedDMIds(client.client, appname);
|
|
225
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);
|
|
226
236
|
} catch (err) {
|
|
227
|
-
console.warn('[Handshake] Could not load
|
|
237
|
+
console.warn('[Handshake] Could not load persisted state:', err.message);
|
|
228
238
|
}
|
|
229
239
|
}
|
|
230
240
|
};
|
|
@@ -236,6 +246,11 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
236
246
|
if (processedEventIds.has(event.id)) return;
|
|
237
247
|
processedEventIds.add(event.id);
|
|
238
248
|
|
|
249
|
+
// Skip if already processed in persistent storage
|
|
250
|
+
if (processedDMIds.has(event.id)) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
239
254
|
// Only kind 4 (encrypted DM) events tagged to us
|
|
240
255
|
if (event.kind !== 4) return;
|
|
241
256
|
|
|
@@ -261,10 +276,19 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
261
276
|
const isDismissed = await cardStorage.isRequestDismissed(client.client, appname, payload.requestId);
|
|
262
277
|
if (isDismissed) {
|
|
263
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);
|
|
264
282
|
return;
|
|
265
283
|
}
|
|
266
284
|
}
|
|
267
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
|
+
|
|
268
292
|
console.log('[Handshake] Received federation request from:', event.pubkey.substring(0, 8) + '...');
|
|
269
293
|
onRequest?.(payload, event.pubkey);
|
|
270
294
|
} else if (payload.type === 'federation_response' && payload.version === '1.0') {
|
|
@@ -278,11 +302,52 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
278
302
|
// Mark as processed in persistent storage
|
|
279
303
|
if (appname && client?.client) {
|
|
280
304
|
await cardStorage.markResponseProcessed(client.client, appname, payload.requestId, event.pubkey);
|
|
305
|
+
await cardStorage.markDMProcessed(client.client, appname, event.id, 'response');
|
|
281
306
|
processedResponseIds.add(responseKey);
|
|
307
|
+
processedDMIds.add(event.id);
|
|
282
308
|
}
|
|
283
309
|
|
|
284
310
|
console.log('[Handshake] Received federation response from:', event.pubkey.substring(0, 8) + '...');
|
|
285
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);
|
|
286
351
|
}
|
|
287
352
|
} catch (error) {
|
|
288
353
|
// Silently ignore non-federation DMs
|
|
@@ -294,8 +359,8 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
294
359
|
let subscription = null;
|
|
295
360
|
|
|
296
361
|
const startSubscription = async () => {
|
|
297
|
-
// Load
|
|
298
|
-
await
|
|
362
|
+
// Load persisted state first
|
|
363
|
+
await loadPersistedState();
|
|
299
364
|
|
|
300
365
|
// Get the NostrClient from HoloSphere instance
|
|
301
366
|
const nostrClient = client?.client;
|
|
@@ -304,16 +369,27 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
304
369
|
return;
|
|
305
370
|
}
|
|
306
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
|
+
|
|
307
377
|
const filter = {
|
|
308
378
|
kinds: [4],
|
|
309
379
|
'#p': [publicKey],
|
|
310
|
-
since:
|
|
380
|
+
since: sinceTime,
|
|
311
381
|
};
|
|
312
382
|
|
|
313
383
|
try {
|
|
314
384
|
// NostrClient.subscribe(filter, onEvent, options) returns { unsubscribe }
|
|
315
385
|
subscription = await nostrClient.subscribe(filter, handleEvent, {});
|
|
316
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
|
+
}
|
|
317
393
|
} catch (error) {
|
|
318
394
|
console.error('[Handshake] Failed to subscribe:', error);
|
|
319
395
|
}
|
|
@@ -458,6 +534,18 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
458
534
|
await storeInboundCapability(holosphere.client, holosphere.config.appName, senderPubKey, cap);
|
|
459
535
|
}
|
|
460
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
|
+
}
|
|
461
549
|
}
|
|
462
550
|
|
|
463
551
|
// Create the actual federation record in GunDB storage
|
|
@@ -618,6 +706,18 @@ export async function processFederationResponse(holosphere, response, responderP
|
|
|
618
706
|
alias: response.responderHolonName,
|
|
619
707
|
addedVia: 'handshake_accepted',
|
|
620
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
|
+
}
|
|
621
721
|
}
|
|
622
722
|
|
|
623
723
|
// Auto-receive federated lens data as holograms for configured inbound lenses
|
|
@@ -670,3 +770,258 @@ export function isFederationRequest(payload) {
|
|
|
670
770
|
export function isFederationResponse(payload) {
|
|
671
771
|
return payload?.type === 'federation_response' && payload?.version === '1.0';
|
|
672
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
|
+
}
|
|
@@ -102,7 +102,8 @@ export async function wouldCreateCircularHologram(client, appname, sourceHolon,
|
|
|
102
102
|
* @throws {Error} If authorPubKey or capability is missing
|
|
103
103
|
*/
|
|
104
104
|
export function createHologram(sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
|
|
105
|
-
const { authorPubKey
|
|
105
|
+
const { authorPubKey } = options;
|
|
106
|
+
let { capability } = options;
|
|
106
107
|
|
|
107
108
|
// Validate required fields for unified model
|
|
108
109
|
if (!authorPubKey) {
|
|
@@ -112,6 +113,11 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
112
113
|
throw new Error('capability is required for hologram creation (unified model)');
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
// Normalize capability - ensure it's the token string, not a capability object
|
|
117
|
+
if (typeof capability === 'object' && capability.token) {
|
|
118
|
+
capability = capability.token;
|
|
119
|
+
}
|
|
120
|
+
|
|
115
121
|
const soul = buildPath(appname, sourceHolon, lensName, dataId);
|
|
116
122
|
|
|
117
123
|
const hologram = {
|
|
@@ -125,7 +131,7 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
125
131
|
dataId,
|
|
126
132
|
authorPubKey, // Always present in unified model
|
|
127
133
|
},
|
|
128
|
-
capability, // Always
|
|
134
|
+
capability, // Always the token string (normalized above)
|
|
129
135
|
_meta: {
|
|
130
136
|
created: Date.now(),
|
|
131
137
|
sourceHolon,
|
|
@@ -280,6 +286,11 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
280
286
|
let capability = hologram.capability;
|
|
281
287
|
const authorPubKey = target.authorPubKey;
|
|
282
288
|
|
|
289
|
+
// Normalize capability - extract token string if it's a capability object
|
|
290
|
+
if (capability && typeof capability === 'object' && capability.token) {
|
|
291
|
+
capability = capability.token;
|
|
292
|
+
}
|
|
293
|
+
|
|
283
294
|
if (!capability && options.appname && authorPubKey) {
|
|
284
295
|
// Fallback to registry lookup
|
|
285
296
|
const capEntry = await getCapabilityForAuthor(
|
|
@@ -293,7 +304,8 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
293
304
|
}
|
|
294
305
|
);
|
|
295
306
|
if (capEntry) {
|
|
296
|
-
|
|
307
|
+
// Extract token string from capability entry
|
|
308
|
+
capability = capEntry.token || capEntry;
|
|
297
309
|
}
|
|
298
310
|
}
|
|
299
311
|
|
|
@@ -587,6 +599,21 @@ export async function propagateData(
|
|
|
587
599
|
|
|
588
600
|
// UNIFIED MODEL: Create hologram with capability
|
|
589
601
|
const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
|
|
602
|
+
|
|
603
|
+
// Determine targetAuthorPubKey - the person who should be able to read the hologram
|
|
604
|
+
// If targetHolon is a 64-char hex pubkey, use it directly as the target author
|
|
605
|
+
// Otherwise, this needs to be resolved via holon registry (TODO: add resolution)
|
|
606
|
+
const isPubkey = typeof targetHolon === 'string' && /^[0-9a-f]{64}$/i.test(targetHolon);
|
|
607
|
+
const targetAuthorPubKey = options.targetAuthorPubKey || (isPubkey ? targetHolon : client.publicKey);
|
|
608
|
+
|
|
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
|
+
|
|
590
617
|
const hologram = await createHologramWithCapability(
|
|
591
618
|
client,
|
|
592
619
|
sourceHolon,
|
|
@@ -596,6 +623,7 @@ export async function propagateData(
|
|
|
596
623
|
appname,
|
|
597
624
|
{
|
|
598
625
|
sourceAuthorPubKey,
|
|
626
|
+
targetAuthorPubKey,
|
|
599
627
|
capability: options.capability,
|
|
600
628
|
permissions: ['read'],
|
|
601
629
|
}
|
|
@@ -38,6 +38,7 @@ export async function registerHolon(client, appname, holonId, publicKey, options
|
|
|
38
38
|
|
|
39
39
|
// Don't register if holonId is already a pubkey
|
|
40
40
|
if (isPubkey(holonId)) {
|
|
41
|
+
console.log('[HolonRegistry] registerHolon: holonId is already a pubkey, skipping registration');
|
|
41
42
|
return true; // No registration needed
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -51,7 +52,15 @@ export async function registerHolon(client, appname, holonId, publicKey, options
|
|
|
51
52
|
createdBy: client.publicKey,
|
|
52
53
|
};
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
console.log('[HolonRegistry] 📝 registerHolon:', {
|
|
56
|
+
holonId,
|
|
57
|
+
publicKey: publicKey?.slice(0, 12) + '...',
|
|
58
|
+
alias: options.alias
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = await writeGlobal(client, appname, HOLON_REGISTRY_TABLE, entry);
|
|
62
|
+
console.log('[HolonRegistry] registerHolon result:', result);
|
|
63
|
+
return result;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
/**
|
|
@@ -84,11 +93,23 @@ export async function lookupHolon(client, appname, holonId) {
|
|
|
84
93
|
export async function resolveHolonToPubkey(client, appname, holonId) {
|
|
85
94
|
// Direct pubkey
|
|
86
95
|
if (isPubkey(holonId)) {
|
|
96
|
+
console.log('[HolonRegistry] resolveHolonToPubkey: holonId is already a pubkey:', holonId?.slice(0, 12) + '...');
|
|
87
97
|
return holonId;
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
// Registry lookup
|
|
91
101
|
const entry = await lookupHolon(client, appname, holonId);
|
|
102
|
+
|
|
103
|
+
if (entry?.publicKey) {
|
|
104
|
+
console.log('[HolonRegistry] ✅ resolveHolonToPubkey: Found mapping', {
|
|
105
|
+
holonId,
|
|
106
|
+
publicKey: entry.publicKey?.slice(0, 12) + '...',
|
|
107
|
+
alias: entry.alias
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
console.log('[HolonRegistry] ❌ resolveHolonToPubkey: No mapping found for holonId:', holonId);
|
|
111
|
+
}
|
|
112
|
+
|
|
92
113
|
return entry?.publicKey || null;
|
|
93
114
|
}
|
|
94
115
|
|