holosphere 2.0.0-alpha11 → 2.0.0-alpha12
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-DQdDE6DG.js} +2 -2
- package/dist/{2019-D2OG2idw.js.map → 2019-DQdDE6DG.js.map} +1 -1
- package/dist/{2019-EION3wKo.cjs → 2019-Ew-DTDlI.cjs} +2 -2
- package/dist/{2019-EION3wKo.cjs.map → 2019-Ew-DTDlI.cjs.map} +1 -1
- package/dist/{browser-BSniCNqO.js → browser-CckFyRI9.js} +2 -2
- package/dist/{browser-BSniCNqO.js.map → browser-CckFyRI9.js.map} +1 -1
- package/dist/browser-D2qtVhH5.cjs +2 -0
- package/dist/{browser-Cq59Ij19.cjs.map → browser-D2qtVhH5.cjs.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +67 -50
- package/dist/index-9sqetkAn.cjs +29 -0
- package/dist/index-9sqetkAn.cjs.map +1 -0
- package/dist/index-C0ITDyFo.cjs +13 -0
- package/dist/index-C0ITDyFo.cjs.map +1 -0
- package/dist/{index-Bl6rM1NW.js → index-DrYM1LOY.js} +2 -2
- package/dist/{index-Bl6rM1NW.js.map → index-DrYM1LOY.js.map} +1 -1
- package/dist/{index-D-jZhliX.js → index-TDDyakLc.js} +19827 -720
- package/dist/index-TDDyakLc.js.map +1 -0
- package/dist/{index-Bwg3OzRM.cjs → index-kyf1sjaC.cjs} +3 -3
- package/dist/{index-Bwg3OzRM.cjs.map → index-kyf1sjaC.cjs.map} +1 -1
- package/dist/index-lbSQUoRz.js +11663 -0
- package/dist/index-lbSQUoRz.js.map +1 -0
- package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-CXhjqwhA.js} +2 -2
- package/dist/{indexeddb-storage-5eiUNsHC.js.map → indexeddb-storage-CXhjqwhA.js.map} +1 -1
- package/dist/{indexeddb-storage-FNFUVvTJ.cjs → indexeddb-storage-DFESDYIj.cjs} +2 -2
- package/dist/{indexeddb-storage-FNFUVvTJ.cjs.map → indexeddb-storage-DFESDYIj.cjs.map} +1 -1
- package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-D1tc1bjk.cjs} +2 -2
- package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-D1tc1bjk.cjs.map} +1 -1
- package/dist/{memory-storage-CI-gfmuG.js → memory-storage-DkewsdcM.js} +2 -2
- package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-DkewsdcM.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 +46 -29
- package/src/federation/capabilities.js +162 -0
- package/src/federation/card-storage.js +304 -0
- package/src/federation/handshake.js +204 -7
- package/src/federation/hologram.js +167 -58
- package/src/federation/holon-registry.js +166 -0
- package/src/federation/index.js +68 -0
- package/src/federation/registry.js +110 -2
- package/src/federation/request-card.js +373 -0
- package/src/hierarchical/upcast.js +19 -3
- package/src/index.js +162 -76
- package/src/lib/federation-methods.js +527 -5
- package/src/storage/nostr-async.js +2 -2
- package/src/storage/nostr-client.js +370 -155
- package/src/storage/nostr-wrapper.js +1 -1
- 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/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,11 @@ 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 { issueCapability } from '../crypto/secp256k1.js';
|
|
22
|
+
import * as cardStorage from './card-storage.js';
|
|
20
23
|
|
|
21
24
|
// ============================================================================
|
|
22
25
|
// Types (documented for reference)
|
|
@@ -200,17 +203,36 @@ export async function sendFederationResponse(client, privateKey, recipientPubKey
|
|
|
200
203
|
* @param {Object} handlers
|
|
201
204
|
* @param {Function} handlers.onRequest - Called with (request, senderPubKey)
|
|
202
205
|
* @param {Function} handlers.onResponse - Called with (response, senderPubKey)
|
|
206
|
+
* @param {Object} options - Subscription options
|
|
207
|
+
* @param {string} options.appname - Application namespace (for persistent storage)
|
|
203
208
|
* @returns {Function} Unsubscribe function
|
|
204
209
|
*/
|
|
205
|
-
export function subscribeToFederationDMs(client, privateKey, publicKey, handlers) {
|
|
210
|
+
export function subscribeToFederationDMs(client, privateKey, publicKey, handlers, options = {}) {
|
|
206
211
|
const { onRequest, onResponse } = handlers;
|
|
212
|
+
const { appname } = options;
|
|
207
213
|
let isActive = true;
|
|
214
|
+
|
|
215
|
+
// In-memory deduplication for this session
|
|
208
216
|
const processedEventIds = new Set();
|
|
209
217
|
|
|
218
|
+
// Track processed responses persistently to prevent duplicate notifications
|
|
219
|
+
let processedResponseIds = new Set();
|
|
220
|
+
|
|
221
|
+
// Load processed responses from storage if appname is provided
|
|
222
|
+
const loadProcessedResponses = async () => {
|
|
223
|
+
if (appname && client?.client) {
|
|
224
|
+
try {
|
|
225
|
+
processedResponseIds = await cardStorage.getProcessedResponseIds(client.client, appname);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.warn('[Handshake] Could not load processed responses:', err.message);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
210
232
|
const handleEvent = async (event) => {
|
|
211
233
|
if (!isActive) return;
|
|
212
234
|
|
|
213
|
-
// Skip duplicates
|
|
235
|
+
// Skip duplicates (in-memory check for this session)
|
|
214
236
|
if (processedEventIds.has(event.id)) return;
|
|
215
237
|
processedEventIds.add(event.id);
|
|
216
238
|
|
|
@@ -234,9 +256,31 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
234
256
|
const payload = JSON.parse(decrypted);
|
|
235
257
|
|
|
236
258
|
if (payload.type === 'federation_request' && payload.version === '1.0') {
|
|
259
|
+
// Check if this request was already dismissed
|
|
260
|
+
if (appname && client?.client) {
|
|
261
|
+
const isDismissed = await cardStorage.isRequestDismissed(client.client, appname, payload.requestId);
|
|
262
|
+
if (isDismissed) {
|
|
263
|
+
console.log('[Handshake] Skipping dismissed request:', payload.requestId);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
237
268
|
console.log('[Handshake] Received federation request from:', event.pubkey.substring(0, 8) + '...');
|
|
238
269
|
onRequest?.(payload, event.pubkey);
|
|
239
270
|
} else if (payload.type === 'federation_response' && payload.version === '1.0') {
|
|
271
|
+
// Check if this response was already processed (persistent check)
|
|
272
|
+
const responseKey = `${payload.requestId}_${event.pubkey}`;
|
|
273
|
+
if (processedResponseIds.has(responseKey)) {
|
|
274
|
+
console.log('[Handshake] Skipping already processed response:', responseKey);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Mark as processed in persistent storage
|
|
279
|
+
if (appname && client?.client) {
|
|
280
|
+
await cardStorage.markResponseProcessed(client.client, appname, payload.requestId, event.pubkey);
|
|
281
|
+
processedResponseIds.add(responseKey);
|
|
282
|
+
}
|
|
283
|
+
|
|
240
284
|
console.log('[Handshake] Received federation response from:', event.pubkey.substring(0, 8) + '...');
|
|
241
285
|
onResponse?.(payload, event.pubkey);
|
|
242
286
|
}
|
|
@@ -250,6 +294,9 @@ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers
|
|
|
250
294
|
let subscription = null;
|
|
251
295
|
|
|
252
296
|
const startSubscription = async () => {
|
|
297
|
+
// Load processed responses first
|
|
298
|
+
await loadProcessedResponses();
|
|
299
|
+
|
|
253
300
|
// Get the NostrClient from HoloSphere instance
|
|
254
301
|
const nostrClient = client?.client;
|
|
255
302
|
if (!nostrClient?.subscribe) {
|
|
@@ -313,16 +360,39 @@ export async function initiateFederationHandshake(holosphere, privateKey, params
|
|
|
313
360
|
|
|
314
361
|
try {
|
|
315
362
|
// Get sender's public key
|
|
316
|
-
const { getPublicKey } = await import('../crypto/nostr-utils.js');
|
|
317
363
|
const senderPubKey = getPublicKey(privateKey);
|
|
318
364
|
|
|
365
|
+
// Issue capabilities for outbound lenses (lenses we're sharing with partner)
|
|
366
|
+
const capabilities = [];
|
|
367
|
+
for (const lensName of (lensConfig.outbound || [])) {
|
|
368
|
+
try {
|
|
369
|
+
const token = await issueCapability(
|
|
370
|
+
['read'],
|
|
371
|
+
{ holonId, lensName, dataId: '*' },
|
|
372
|
+
partnerPubKey,
|
|
373
|
+
{
|
|
374
|
+
expiresIn: 365 * 24 * 60 * 60 * 1000, // 1 year
|
|
375
|
+
issuer: senderPubKey,
|
|
376
|
+
issuerKey: privateKey,
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
capabilities.push({
|
|
380
|
+
token,
|
|
381
|
+
scope: { holonId, lensName },
|
|
382
|
+
permissions: ['read'],
|
|
383
|
+
});
|
|
384
|
+
} catch (err) {
|
|
385
|
+
console.warn(`[Handshake] Failed to issue capability for ${lensName}:`, err.message);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
319
389
|
// Create federation request
|
|
320
390
|
const request = createFederationRequest({
|
|
321
391
|
senderHolonId: holonId,
|
|
322
392
|
senderHolonName: holonName,
|
|
323
393
|
senderPubKey,
|
|
324
394
|
lensConfig,
|
|
325
|
-
capabilities
|
|
395
|
+
capabilities,
|
|
326
396
|
message,
|
|
327
397
|
});
|
|
328
398
|
|
|
@@ -373,7 +443,6 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
373
443
|
|
|
374
444
|
try {
|
|
375
445
|
// Get responder's public key
|
|
376
|
-
const { getPublicKey } = await import('../crypto/nostr-utils.js');
|
|
377
446
|
const responderPubKey = getPublicKey(privateKey);
|
|
378
447
|
|
|
379
448
|
// Add sender as federated partner in Nostr registry
|
|
@@ -399,6 +468,30 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
399
468
|
partnerName: request.senderHolonName
|
|
400
469
|
});
|
|
401
470
|
|
|
471
|
+
// Issue capabilities for our outbound lenses (lenses we're sharing with partner)
|
|
472
|
+
const capabilities = [];
|
|
473
|
+
for (const lensName of (lensConfig.outbound || [])) {
|
|
474
|
+
try {
|
|
475
|
+
const token = await issueCapability(
|
|
476
|
+
['read'],
|
|
477
|
+
{ holonId, lensName, dataId: '*' },
|
|
478
|
+
senderPubKey,
|
|
479
|
+
{
|
|
480
|
+
expiresIn: 365 * 24 * 60 * 60 * 1000, // 1 year
|
|
481
|
+
issuer: responderPubKey,
|
|
482
|
+
issuerKey: privateKey,
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
capabilities.push({
|
|
486
|
+
token,
|
|
487
|
+
scope: { holonId, lensName },
|
|
488
|
+
permissions: ['read'],
|
|
489
|
+
});
|
|
490
|
+
} catch (err) {
|
|
491
|
+
console.warn(`[Handshake] Failed to issue capability for ${lensName}:`, err.message);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
402
495
|
// Create and send response
|
|
403
496
|
const response = createFederationResponse({
|
|
404
497
|
requestId: request.requestId,
|
|
@@ -407,14 +500,50 @@ export async function acceptFederationRequest(holosphere, privateKey, params) {
|
|
|
407
500
|
responderHolonName: holonName,
|
|
408
501
|
responderPubKey,
|
|
409
502
|
lensConfig,
|
|
410
|
-
capabilities
|
|
503
|
+
capabilities,
|
|
411
504
|
message,
|
|
412
505
|
});
|
|
413
506
|
|
|
414
507
|
const sent = await sendFederationResponse(holosphere.client, privateKey, senderPubKey, response);
|
|
415
508
|
|
|
416
509
|
if (sent) {
|
|
417
|
-
|
|
510
|
+
// Dismiss the request so it won't reappear
|
|
511
|
+
if (holosphere.client && request.requestId) {
|
|
512
|
+
await cardStorage.dismissRequest(holosphere.client, holosphere.config.appName, request.requestId);
|
|
513
|
+
console.log('[Handshake] Request dismissed after acceptance:', request.requestId);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Auto-receive federated lens data as holograms for configured inbound lenses
|
|
517
|
+
// This enables the user to immediately see the partner's data in their holon
|
|
518
|
+
const receivedHolograms = {};
|
|
519
|
+
const inboundLenses = lensConfig.inbound || [];
|
|
520
|
+
|
|
521
|
+
if (inboundLenses.length > 0 && holosphere.receiveFederatedLens) {
|
|
522
|
+
console.log('[Handshake] Receiving federated lens data as holograms...');
|
|
523
|
+
|
|
524
|
+
for (const lensName of inboundLenses) {
|
|
525
|
+
try {
|
|
526
|
+
// The sender's holon ID comes from the request
|
|
527
|
+
const senderHolonId = request.senderHolonId;
|
|
528
|
+
|
|
529
|
+
const result = await holosphere.receiveFederatedLens(
|
|
530
|
+
senderPubKey,
|
|
531
|
+
senderHolonId,
|
|
532
|
+
lensName,
|
|
533
|
+
holonId,
|
|
534
|
+
{ overwrite: false }
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
receivedHolograms[lensName] = result;
|
|
538
|
+
console.log(`[Handshake] Received ${result.received} holograms for lens "${lensName}"`);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
console.warn(`[Handshake] Failed to receive holograms for lens "${lensName}":`, err.message);
|
|
541
|
+
receivedHolograms[lensName] = { error: err.message };
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return { success: true, requestId: request.requestId, receivedHolograms };
|
|
418
547
|
} else {
|
|
419
548
|
return { success: false, error: 'Failed to send response DM' };
|
|
420
549
|
}
|
|
@@ -452,6 +581,74 @@ export async function rejectFederationRequest(holosphere, privateKey, params) {
|
|
|
452
581
|
}
|
|
453
582
|
}
|
|
454
583
|
|
|
584
|
+
/**
|
|
585
|
+
* Process a federation response (called by initiator when receiving acceptance)
|
|
586
|
+
* Stores capabilities from the responder in the local registry and receives holograms
|
|
587
|
+
* @param {Object} holosphere - HoloSphere instance
|
|
588
|
+
* @param {Object} response - Federation response payload
|
|
589
|
+
* @param {string} responderPubKey - Responder's public key
|
|
590
|
+
* @param {Object} [options={}] - Processing options
|
|
591
|
+
* @param {string} [options.holonId] - Initiator's holon ID (for receiving holograms)
|
|
592
|
+
* @param {string[]} [options.inboundLenses] - Lenses to receive from responder
|
|
593
|
+
* @returns {Promise<Object>} Result with success, stored capability count, and received holograms
|
|
594
|
+
*/
|
|
595
|
+
export async function processFederationResponse(holosphere, response, responderPubKey, options = {}) {
|
|
596
|
+
if (!response || response.status !== 'accepted') {
|
|
597
|
+
return { success: false, error: 'Response not accepted', storedCapabilities: 0 };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const { holonId, inboundLenses = [] } = options;
|
|
601
|
+
let storedCapabilities = 0;
|
|
602
|
+
|
|
603
|
+
// Store capabilities from the responder
|
|
604
|
+
if (holosphere.client && response.capabilities && response.capabilities.length > 0) {
|
|
605
|
+
for (const cap of response.capabilities) {
|
|
606
|
+
try {
|
|
607
|
+
await storeInboundCapability(holosphere.client, holosphere.config.appName, responderPubKey, cap);
|
|
608
|
+
storedCapabilities++;
|
|
609
|
+
} catch (err) {
|
|
610
|
+
console.warn(`[Handshake] Failed to store capability:`, err.message);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Add responder as federated partner if not already
|
|
616
|
+
if (holosphere.client) {
|
|
617
|
+
await addFederatedPartner(holosphere.client, holosphere.config.appName, responderPubKey, {
|
|
618
|
+
alias: response.responderHolonName,
|
|
619
|
+
addedVia: 'handshake_accepted',
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Auto-receive federated lens data as holograms for configured inbound lenses
|
|
624
|
+
const receivedHolograms = {};
|
|
625
|
+
const responderHolonId = response.responderHolonId;
|
|
626
|
+
|
|
627
|
+
if (holonId && responderHolonId && inboundLenses.length > 0 && holosphere.receiveFederatedLens) {
|
|
628
|
+
console.log('[Handshake] Receiving federated lens data as holograms from responder...');
|
|
629
|
+
|
|
630
|
+
for (const lensName of inboundLenses) {
|
|
631
|
+
try {
|
|
632
|
+
const result = await holosphere.receiveFederatedLens(
|
|
633
|
+
responderPubKey,
|
|
634
|
+
responderHolonId,
|
|
635
|
+
lensName,
|
|
636
|
+
holonId,
|
|
637
|
+
{ overwrite: false }
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
receivedHolograms[lensName] = result;
|
|
641
|
+
console.log(`[Handshake] Received ${result.received} holograms for lens "${lensName}"`);
|
|
642
|
+
} catch (err) {
|
|
643
|
+
console.warn(`[Handshake] Failed to receive holograms for lens "${lensName}":`, err.message);
|
|
644
|
+
receivedHolograms[lensName] = { error: err.message };
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return { success: true, storedCapabilities, receivedHolograms };
|
|
650
|
+
}
|
|
651
|
+
|
|
455
652
|
// ============================================================================
|
|
456
653
|
// Payload Validation
|
|
457
654
|
// ============================================================================
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Federation and Hologram (Reference) Management.
|
|
2
|
+
* @fileoverview Unified Federation and Hologram (Reference) Management.
|
|
3
3
|
*
|
|
4
4
|
* Provides hologram (lightweight reference) creation, resolution, and management.
|
|
5
5
|
* Holograms enable data to appear in multiple holons while maintaining a single
|
|
6
|
-
* source of truth.
|
|
7
|
-
*
|
|
6
|
+
* source of truth.
|
|
7
|
+
*
|
|
8
|
+
* UNIFIED MODEL: All federation is capability-based. Every hologram includes:
|
|
9
|
+
* - authorPubKey: The public key of the data owner (can be self)
|
|
10
|
+
* - capability: A capability token (self-issued for same-author federation)
|
|
11
|
+
*
|
|
12
|
+
* This unifies "in-federation" (same author) and "cross-federation" (different authors)
|
|
13
|
+
* into a single consistent model.
|
|
8
14
|
*
|
|
9
15
|
* @module federation/hologram
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
import { buildPath, write, read, update } from '../storage/unified-storage.js';
|
|
13
|
-
import { verifyCapability } from '../crypto/secp256k1.js';
|
|
14
|
-
import { getCapabilityForAuthor } from './registry.js';
|
|
19
|
+
import { verifyCapability, issueCapability } from '../crypto/secp256k1.js';
|
|
20
|
+
import { getCapabilityForAuthor, storeInboundCapability } from './registry.js';
|
|
15
21
|
|
|
16
22
|
/** @constant {number} Maximum depth for hologram resolution chain */
|
|
17
23
|
const MAX_RESOLUTION_DEPTH = 10;
|
|
@@ -79,19 +85,32 @@ export async function wouldCreateCircularHologram(client, appname, sourceHolon,
|
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
/**
|
|
82
|
-
* Create a hologram (lightweight reference)
|
|
88
|
+
* Create a hologram (lightweight reference) - UNIFIED MODEL
|
|
89
|
+
*
|
|
90
|
+
* All holograms now include authorPubKey and capability for consistency.
|
|
91
|
+
* This unifies "in-federation" (same author) and "cross-federation" (different authors).
|
|
92
|
+
*
|
|
83
93
|
* @param {string} sourceHolon - Source holon ID
|
|
84
94
|
* @param {string} targetHolon - Target holon ID
|
|
85
95
|
* @param {string} lensName - Lens name
|
|
86
96
|
* @param {string} dataId - Data ID
|
|
87
97
|
* @param {string} appname - Application namespace
|
|
88
|
-
* @param {Object} options -
|
|
89
|
-
* @param {string} options.authorPubKey - Source author's public key (
|
|
90
|
-
* @param {string} options.capability - Capability token (for
|
|
91
|
-
* @returns {Object} Hologram object
|
|
98
|
+
* @param {Object} options - Hologram options (authorPubKey and capability are required)
|
|
99
|
+
* @param {string} options.authorPubKey - Source author's public key (REQUIRED - can be self)
|
|
100
|
+
* @param {string} options.capability - Capability token (REQUIRED - self-issued for same author)
|
|
101
|
+
* @returns {Object} Hologram object with unified structure
|
|
102
|
+
* @throws {Error} If authorPubKey or capability is missing
|
|
92
103
|
*/
|
|
93
104
|
export function createHologram(sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
|
|
94
|
-
const { authorPubKey
|
|
105
|
+
const { authorPubKey, capability } = options;
|
|
106
|
+
|
|
107
|
+
// Validate required fields for unified model
|
|
108
|
+
if (!authorPubKey) {
|
|
109
|
+
throw new Error('authorPubKey is required for hologram creation (unified model)');
|
|
110
|
+
}
|
|
111
|
+
if (!capability) {
|
|
112
|
+
throw new Error('capability is required for hologram creation (unified model)');
|
|
113
|
+
}
|
|
95
114
|
|
|
96
115
|
const soul = buildPath(appname, sourceHolon, lensName, dataId);
|
|
97
116
|
|
|
@@ -104,32 +123,95 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
104
123
|
holonId: sourceHolon,
|
|
105
124
|
lensName,
|
|
106
125
|
dataId,
|
|
126
|
+
authorPubKey, // Always present in unified model
|
|
107
127
|
},
|
|
128
|
+
capability, // Always present in unified model
|
|
108
129
|
_meta: {
|
|
109
130
|
created: Date.now(),
|
|
110
131
|
sourceHolon,
|
|
111
132
|
source: sourceHolon, // Alias for compatibility
|
|
133
|
+
sourcePubKey: authorPubKey,
|
|
134
|
+
grantedAt: Date.now(),
|
|
112
135
|
},
|
|
113
136
|
};
|
|
114
137
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
return hologram;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create a hologram with automatic capability issuance - UNIFIED MODEL
|
|
143
|
+
*
|
|
144
|
+
* This is the preferred method for creating holograms. It automatically issues
|
|
145
|
+
* a capability token (self-capability if same author, cross-capability otherwise).
|
|
146
|
+
*
|
|
147
|
+
* @param {Object} client - Nostr client instance (must have publicKey and optionally privateKey)
|
|
148
|
+
* @param {string} sourceHolon - Source holon ID
|
|
149
|
+
* @param {string} targetHolon - Target holon ID
|
|
150
|
+
* @param {string} lensName - Lens name
|
|
151
|
+
* @param {string} dataId - Data ID
|
|
152
|
+
* @param {string} appname - Application namespace
|
|
153
|
+
* @param {Object} options - Hologram options
|
|
154
|
+
* @param {string} [options.sourceAuthorPubKey] - Source author's public key (defaults to client.publicKey)
|
|
155
|
+
* @param {string} [options.targetAuthorPubKey] - Target author's public key (defaults to client.publicKey)
|
|
156
|
+
* @param {string} [options.capability] - Pre-issued capability (if not provided, one will be issued)
|
|
157
|
+
* @param {string[]} [options.permissions=['read']] - Permissions for auto-issued capability
|
|
158
|
+
* @param {number} [options.expiresIn] - Capability expiration (null for self-caps = no expiration)
|
|
159
|
+
* @returns {Promise<Object>} Hologram object with unified structure
|
|
160
|
+
*/
|
|
161
|
+
export async function createHologramWithCapability(client, sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
|
|
162
|
+
const {
|
|
163
|
+
sourceAuthorPubKey = client.publicKey,
|
|
164
|
+
targetAuthorPubKey = client.publicKey,
|
|
165
|
+
capability: providedCapability = null,
|
|
166
|
+
permissions = ['read'],
|
|
167
|
+
expiresIn = null,
|
|
168
|
+
} = options;
|
|
169
|
+
|
|
170
|
+
let capability = providedCapability;
|
|
171
|
+
|
|
172
|
+
// Issue capability if not provided
|
|
173
|
+
if (!capability) {
|
|
174
|
+
const isSelfCapability = sourceAuthorPubKey === targetAuthorPubKey;
|
|
175
|
+
|
|
176
|
+
// Issue capability - for self-caps, no expiration by default
|
|
177
|
+
capability = await issueCapability(
|
|
178
|
+
permissions,
|
|
179
|
+
{ holonId: sourceHolon, lensName, dataId },
|
|
180
|
+
targetAuthorPubKey,
|
|
181
|
+
{
|
|
182
|
+
expiresIn: expiresIn !== null ? expiresIn : (isSelfCapability ? 365 * 24 * 60 * 60 * 1000 : 3600000), // 1 year for self, 1 hour for cross
|
|
183
|
+
issuer: sourceAuthorPubKey,
|
|
184
|
+
issuerKey: client.privateKey,
|
|
185
|
+
}
|
|
186
|
+
);
|
|
121
187
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
188
|
+
// Store the capability in registry for cross-author federation
|
|
189
|
+
if (!isSelfCapability && client.privateKey) {
|
|
190
|
+
await storeInboundCapability(client, appname, sourceAuthorPubKey, {
|
|
191
|
+
token: capability,
|
|
192
|
+
scope: { holonId: sourceHolon, lensName, dataId },
|
|
193
|
+
permissions,
|
|
194
|
+
isSelfCapability: false,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
125
197
|
}
|
|
126
198
|
|
|
127
|
-
return
|
|
199
|
+
return createHologram(sourceHolon, targetHolon, lensName, dataId, appname, {
|
|
200
|
+
authorPubKey: sourceAuthorPubKey,
|
|
201
|
+
capability,
|
|
202
|
+
});
|
|
128
203
|
}
|
|
129
204
|
|
|
130
205
|
/**
|
|
131
|
-
* Resolve hologram to actual data, merging local overrides
|
|
132
|
-
*
|
|
206
|
+
* Resolve hologram to actual data, merging local overrides - UNIFIED MODEL
|
|
207
|
+
*
|
|
208
|
+
* In the unified model, ALL holograms have authorPubKey and capability.
|
|
209
|
+
* Resolution always:
|
|
210
|
+
* 1. Verifies the capability token
|
|
211
|
+
* 2. Fetches data with author filter
|
|
212
|
+
*
|
|
213
|
+
* This provides consistent security for both self and cross-author holograms.
|
|
214
|
+
*
|
|
133
215
|
* @param {Object} client - Nostr client instance
|
|
134
216
|
* @param {Object} hologram - Hologram object
|
|
135
217
|
* @param {Set} visited - Visited souls (circular reference detection)
|
|
@@ -138,10 +220,11 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
138
220
|
* @param {string} options.appname - Application namespace (for registry lookup)
|
|
139
221
|
* @param {boolean} options.deleteCircular - If true, delete circular holograms when detected (default: true)
|
|
140
222
|
* @param {string} options.hologramPath - Path of the hologram being resolved (for deletion)
|
|
223
|
+
* @param {boolean} options.skipCapabilityVerification - Skip capability check (for internal use only)
|
|
141
224
|
* @returns {Promise<Object|null>} Resolved data with _hologram metadata, or null
|
|
142
225
|
*/
|
|
143
226
|
export async function resolveHologram(client, hologram, visited = new Set(), chain = [], options = {}) {
|
|
144
|
-
const { deleteCircular = true, hologramPath = null } = options;
|
|
227
|
+
const { deleteCircular = true, hologramPath = null, skipCapabilityVerification = false } = options;
|
|
145
228
|
|
|
146
229
|
if (!hologram || !hologram.hologram) {
|
|
147
230
|
return hologram; // Not a hologram, return as-is
|
|
@@ -192,31 +275,32 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
192
275
|
|
|
193
276
|
let sourceData;
|
|
194
277
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
);
|
|
212
|
-
if (capEntry) {
|
|
213
|
-
capability = capEntry.token;
|
|
278
|
+
// UNIFIED MODEL: All holograms have authorPubKey and capability
|
|
279
|
+
// Get capability - try embedded first, then registry fallback
|
|
280
|
+
let capability = hologram.capability;
|
|
281
|
+
const authorPubKey = target.authorPubKey;
|
|
282
|
+
|
|
283
|
+
if (!capability && options.appname && authorPubKey) {
|
|
284
|
+
// Fallback to registry lookup
|
|
285
|
+
const capEntry = await getCapabilityForAuthor(
|
|
286
|
+
client,
|
|
287
|
+
options.appname,
|
|
288
|
+
authorPubKey,
|
|
289
|
+
{
|
|
290
|
+
holonId: target.holonId,
|
|
291
|
+
lensName: target.lensName,
|
|
292
|
+
dataId: target.dataId,
|
|
214
293
|
}
|
|
294
|
+
);
|
|
295
|
+
if (capEntry) {
|
|
296
|
+
capability = capEntry.token;
|
|
215
297
|
}
|
|
298
|
+
}
|
|
216
299
|
|
|
217
|
-
|
|
300
|
+
// Verify capability (unified model - always verify unless explicitly skipped)
|
|
301
|
+
if (!skipCapabilityVerification) {
|
|
218
302
|
if (!capability) {
|
|
219
|
-
console.warn(`❌
|
|
303
|
+
console.warn(`❌ Hologram missing capability: ${soul}`);
|
|
220
304
|
return null;
|
|
221
305
|
}
|
|
222
306
|
|
|
@@ -231,16 +315,18 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
231
315
|
);
|
|
232
316
|
|
|
233
317
|
if (!isValid) {
|
|
234
|
-
console.warn(`❌ Capability verification failed for
|
|
318
|
+
console.warn(`❌ Capability verification failed for hologram: ${soul}`);
|
|
235
319
|
return null;
|
|
236
320
|
}
|
|
321
|
+
}
|
|
237
322
|
|
|
238
|
-
|
|
323
|
+
// UNIFIED MODEL: Always fetch with author filter if authorPubKey is present
|
|
324
|
+
if (authorPubKey) {
|
|
239
325
|
sourceData = await read(client, soul, {
|
|
240
|
-
authors: [
|
|
326
|
+
authors: [authorPubKey]
|
|
241
327
|
});
|
|
242
328
|
} else {
|
|
243
|
-
//
|
|
329
|
+
// Legacy fallback for holograms without authorPubKey
|
|
244
330
|
sourceData = await read(client, soul);
|
|
245
331
|
}
|
|
246
332
|
|
|
@@ -259,7 +345,7 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
259
345
|
}
|
|
260
346
|
|
|
261
347
|
/**
|
|
262
|
-
* Merge hologram local overrides with source data
|
|
348
|
+
* Merge hologram local overrides with source data - UNIFIED MODEL
|
|
263
349
|
* @private
|
|
264
350
|
* @param {Object} hologram - Hologram object
|
|
265
351
|
* @param {Object} sourceData - Source data
|
|
@@ -267,7 +353,7 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
267
353
|
*/
|
|
268
354
|
function mergeHologramWithSource(hologram, sourceData) {
|
|
269
355
|
// Extract local overrides from the hologram (exclude hologram-specific fields)
|
|
270
|
-
const hologramOnlyFields = ['hologram', 'soul', 'target', '_meta', 'id', 'capability'
|
|
356
|
+
const hologramOnlyFields = ['hologram', 'soul', 'target', '_meta', 'id', 'capability'];
|
|
271
357
|
const localOverrides = {};
|
|
272
358
|
const localOverrideKeys = [];
|
|
273
359
|
|
|
@@ -280,6 +366,8 @@ function mergeHologramWithSource(hologram, sourceData) {
|
|
|
280
366
|
|
|
281
367
|
// Merge: source data + local overrides + hologram metadata
|
|
282
368
|
const sourceHolon = hologram._meta?.sourceHolon || hologram._meta?.source || hologram.target?.holonId;
|
|
369
|
+
const sourcePubKey = hologram.target?.authorPubKey || hologram._meta?.sourcePubKey || null;
|
|
370
|
+
|
|
283
371
|
const resolved = {
|
|
284
372
|
...sourceData, // Original data from source
|
|
285
373
|
...localOverrides, // Local overrides (x, y, pinned, etc.)
|
|
@@ -288,8 +376,7 @@ function mergeHologramWithSource(hologram, sourceData) {
|
|
|
288
376
|
soul: hologram.soul,
|
|
289
377
|
sourceHolon: sourceHolon,
|
|
290
378
|
localOverrides: localOverrideKeys,
|
|
291
|
-
|
|
292
|
-
sourcePubKey: hologram._meta?.sourcePubKey || null,
|
|
379
|
+
sourcePubKey: sourcePubKey, // Always present in unified model
|
|
293
380
|
}
|
|
294
381
|
};
|
|
295
382
|
|
|
@@ -340,14 +427,21 @@ export async function setupFederation(
|
|
|
340
427
|
}
|
|
341
428
|
|
|
342
429
|
/**
|
|
343
|
-
* Propagate data to federated location
|
|
344
|
-
*
|
|
430
|
+
* Propagate data to federated location - UNIFIED MODEL
|
|
431
|
+
*
|
|
432
|
+
* In the unified model, all holograms require authorPubKey and capability.
|
|
433
|
+
* This function automatically issues capabilities for propagation.
|
|
434
|
+
*
|
|
435
|
+
* @param {Object} client - Nostr client instance (must have publicKey)
|
|
345
436
|
* @param {string} appname - Application namespace
|
|
346
437
|
* @param {Object} data - Data to propagate
|
|
347
438
|
* @param {string} sourceHolon - Source holon ID
|
|
348
439
|
* @param {string} targetHolon - Target holon ID
|
|
349
440
|
* @param {string} lensName - Lens name
|
|
350
441
|
* @param {string} mode - 'reference' or 'copy'
|
|
442
|
+
* @param {Object} options - Propagation options
|
|
443
|
+
* @param {string} [options.sourceAuthorPubKey] - Source author (defaults to client.publicKey)
|
|
444
|
+
* @param {string} [options.capability] - Pre-issued capability (auto-issued if not provided)
|
|
351
445
|
* @returns {Promise<boolean>} Success indicator
|
|
352
446
|
*/
|
|
353
447
|
export async function propagateData(
|
|
@@ -357,7 +451,8 @@ export async function propagateData(
|
|
|
357
451
|
sourceHolon,
|
|
358
452
|
targetHolon,
|
|
359
453
|
lensName,
|
|
360
|
-
mode = 'reference'
|
|
454
|
+
mode = 'reference',
|
|
455
|
+
options = {}
|
|
361
456
|
) {
|
|
362
457
|
const dataId = data.id;
|
|
363
458
|
|
|
@@ -490,8 +585,22 @@ export async function propagateData(
|
|
|
490
585
|
return false;
|
|
491
586
|
}
|
|
492
587
|
|
|
493
|
-
// Create hologram
|
|
494
|
-
const
|
|
588
|
+
// UNIFIED MODEL: Create hologram with capability
|
|
589
|
+
const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
|
|
590
|
+
const hologram = await createHologramWithCapability(
|
|
591
|
+
client,
|
|
592
|
+
sourceHolon,
|
|
593
|
+
targetHolon,
|
|
594
|
+
lensName,
|
|
595
|
+
dataId,
|
|
596
|
+
appname,
|
|
597
|
+
{
|
|
598
|
+
sourceAuthorPubKey,
|
|
599
|
+
capability: options.capability,
|
|
600
|
+
permissions: ['read'],
|
|
601
|
+
}
|
|
602
|
+
);
|
|
603
|
+
|
|
495
604
|
const targetPath = buildPath(appname, targetHolon, lensName, dataId);
|
|
496
605
|
const success = await write(client, targetPath, hologram);
|
|
497
606
|
|