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.
Files changed (49) hide show
  1. package/dist/{2019-DQdDE6DG.js → 2019-CLMqIAfQ.js} +1722 -1668
  2. package/dist/{2019-DQdDE6DG.js.map → 2019-CLMqIAfQ.js.map} +1 -1
  3. package/dist/2019-Cp3uYhyY.cjs +8 -0
  4. package/dist/{2019-Ew-DTDlI.cjs.map → 2019-Cp3uYhyY.cjs.map} +1 -1
  5. package/dist/{browser-D2qtVhH5.cjs → browser-D6cNVl0v.cjs} +2 -2
  6. package/dist/{browser-D2qtVhH5.cjs.map → browser-D6cNVl0v.cjs.map} +1 -1
  7. package/dist/{browser-CckFyRI9.js → browser-nUQt1cnB.js} +2 -2
  8. package/dist/{browser-CckFyRI9.js.map → browser-nUQt1cnB.js.map} +1 -1
  9. package/dist/cjs/holosphere.cjs +1 -1
  10. package/dist/esm/holosphere.js +1 -1
  11. package/dist/{index-TDDyakLc.js → index-BN_uoxQK.js} +610 -128
  12. package/dist/index-BN_uoxQK.js.map +1 -0
  13. package/dist/{index-DrYM1LOY.js → index-CoAjtqsD.js} +2 -2
  14. package/dist/{index-DrYM1LOY.js.map → index-CoAjtqsD.js.map} +1 -1
  15. package/dist/{index-kyf1sjaC.cjs → index-Cp3tI53z.cjs} +2 -2
  16. package/dist/{index-kyf1sjaC.cjs.map → index-Cp3tI53z.cjs.map} +1 -1
  17. package/dist/{index-C0ITDyFo.cjs → index-DJjGSwXG.cjs} +2 -2
  18. package/dist/{index-C0ITDyFo.cjs.map → index-DJjGSwXG.cjs.map} +1 -1
  19. package/dist/index-V8EHMYEY.cjs +29 -0
  20. package/dist/index-V8EHMYEY.cjs.map +1 -0
  21. package/dist/{index-lbSQUoRz.js → index-Z5TstN1e.js} +2 -2
  22. package/dist/{index-lbSQUoRz.js.map → index-Z5TstN1e.js.map} +1 -1
  23. package/dist/indexeddb-storage-CZK5A7XH.cjs +2 -0
  24. package/dist/indexeddb-storage-CZK5A7XH.cjs.map +1 -0
  25. package/dist/{indexeddb-storage-CXhjqwhA.js → indexeddb-storage-bpA01pAU.js} +39 -2
  26. package/dist/indexeddb-storage-bpA01pAU.js.map +1 -0
  27. package/dist/{memory-storage-D1tc1bjk.cjs → memory-storage-B1k8Jszd.cjs} +2 -2
  28. package/dist/{memory-storage-D1tc1bjk.cjs.map → memory-storage-B1k8Jszd.cjs.map} +1 -1
  29. package/dist/{memory-storage-DkewsdcM.js → memory-storage-BqhmytP_.js} +2 -2
  30. package/dist/{memory-storage-DkewsdcM.js.map → memory-storage-BqhmytP_.js.map} +1 -1
  31. package/package.json +1 -1
  32. package/src/crypto/secp256k1.js +58 -9
  33. package/src/federation/card-storage.js +72 -0
  34. package/src/federation/handshake.js +363 -8
  35. package/src/federation/hologram.js +31 -3
  36. package/src/federation/holon-registry.js +22 -1
  37. package/src/federation/registry.js +54 -4
  38. package/src/index.js +52 -4
  39. package/src/storage/indexeddb-storage.js +41 -0
  40. package/src/storage/nostr-async.js +12 -3
  41. package/src/storage/nostr-client.js +112 -11
  42. package/src/storage/nostr-wrapper.js +5 -2
  43. package/dist/2019-Ew-DTDlI.cjs +0 -8
  44. package/dist/index-9sqetkAn.cjs +0 -29
  45. package/dist/index-9sqetkAn.cjs.map +0 -1
  46. package/dist/index-TDDyakLc.js.map +0 -1
  47. package/dist/indexeddb-storage-CXhjqwhA.js.map +0 -1
  48. package/dist/indexeddb-storage-DFESDYIj.cjs +0 -2
  49. 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 loadProcessedResponses = async () => {
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 processed responses:', err.message);
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 processed responses first
298
- await loadProcessedResponses();
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: Math.floor(Date.now() / 1000) - 86400, // Last 24 hours
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, capability } = options;
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 present in unified model
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
- capability = capEntry.token;
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
- return writeGlobal(client, appname, HOLON_REGISTRY_TABLE, entry);
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