holosphere 2.0.0-alpha21 → 2.0.0-alpha23
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/README.md +1 -2
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +61 -58
- package/dist/{index-B6-8KAQm.js → index-BEkCLOwI.js} +2 -2
- package/dist/{index-B6-8KAQm.js.map → index-BEkCLOwI.js.map} +1 -1
- package/dist/{index-D2WstuZJ.js → index-BEvX6DxG.js} +2 -2
- package/dist/{index-D2WstuZJ.js.map → index-BEvX6DxG.js.map} +1 -1
- package/dist/{index--QsHG_gD.cjs → index-BGTOiJ2Y.cjs} +2 -2
- package/dist/{index--QsHG_gD.cjs.map → index-BGTOiJ2Y.cjs.map} +1 -1
- package/dist/{index-COpLk9gL.cjs → index-BH1woZXL.cjs} +2 -2
- package/dist/{index-COpLk9gL.cjs.map → index-BH1woZXL.cjs.map} +1 -1
- package/dist/{index-BHptWysv.js → index-Cvxov2jv.js} +2970 -7753
- package/dist/index-Cvxov2jv.js.map +1 -0
- package/dist/index-vTKI_BAX.cjs +29 -0
- package/dist/index-vTKI_BAX.cjs.map +1 -0
- package/dist/{indexeddb-storage-wKG4mICM.cjs → indexeddb-storage-BmnCNnSg.cjs} +2 -2
- package/dist/{indexeddb-storage-wKG4mICM.cjs.map → indexeddb-storage-BmnCNnSg.cjs.map} +1 -1
- package/dist/{indexeddb-storage-kQ53UHEE.js → indexeddb-storage-MIFisaPy.js} +2 -2
- package/dist/{indexeddb-storage-kQ53UHEE.js.map → indexeddb-storage-MIFisaPy.js.map} +1 -1
- package/dist/{memory-storage-CGC8xM2G.cjs → memory-storage-BJjK3F4r.cjs} +2 -2
- package/dist/{memory-storage-CGC8xM2G.cjs.map → memory-storage-BJjK3F4r.cjs.map} +1 -1
- package/dist/{memory-storage-DnXCSbBl.js → memory-storage-DhHXdKQ-.js} +2 -2
- package/dist/{memory-storage-DnXCSbBl.js.map → memory-storage-DhHXdKQ-.js.map} +1 -1
- package/examples/demo.html +2 -29
- package/package.json +3 -8
- package/src/content/social-protocols.js +3 -59
- package/src/core/holosphere.js +16 -554
- package/src/crypto/nostr-utils.js +98 -1
- package/src/crypto/secp256k1.js +4 -393
- package/src/federation/discovery.js +7 -75
- package/src/federation/handshake.js +69 -202
- package/src/federation/hologram.js +222 -298
- package/src/federation/index.js +2 -9
- package/src/federation/registry.js +67 -1257
- package/src/federation/request-card.js +21 -35
- package/src/hierarchical/upcast.js +4 -9
- package/src/index.js +145 -296
- package/src/lib/federation-methods.js +370 -909
- package/src/storage/global-tables.js +1 -1
- package/src/storage/nostr-wrapper.js +9 -5
- package/src/subscriptions/manager.js +1 -1
- package/types/index.d.ts +145 -37
- package/bin/holosphere-activitypub.js +0 -158
- package/dist/2019-BzVkRcax.js +0 -6680
- package/dist/2019-BzVkRcax.js.map +0 -1
- package/dist/2019-C1hPR_Os.cjs +0 -8
- package/dist/2019-C1hPR_Os.cjs.map +0 -1
- package/dist/browser-BcmACE3G.js +0 -3058
- package/dist/browser-BcmACE3G.js.map +0 -1
- package/dist/browser-DaqYUTcG.cjs +0 -2
- package/dist/browser-DaqYUTcG.cjs.map +0 -1
- package/dist/index-BHptWysv.js.map +0 -1
- package/dist/index-CDlhzxT2.cjs +0 -29
- package/dist/index-CDlhzxT2.cjs.map +0 -1
- package/src/federation/capabilities.js +0 -46
- package/src/storage/backend-factory.js +0 -130
- package/src/storage/backend-interface.js +0 -161
- package/src/storage/backends/activitypub/server.js +0 -675
- package/src/storage/backends/activitypub-backend.js +0 -295
- package/src/storage/backends/gundb-backend.js +0 -875
- package/src/storage/backends/nostr-backend.js +0 -251
- package/src/storage/gun-async.js +0 -341
- package/src/storage/gun-auth.js +0 -373
- package/src/storage/gun-federation.js +0 -785
- package/src/storage/gun-references.js +0 -209
- package/src/storage/gun-schema.js +0 -306
- package/src/storage/gun-wrapper.js +0 -642
- package/src/storage/migration.js +0 -351
- package/src/storage/unified-storage.js +0 -161
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Hologram — the core federation infrastructure.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* source of truth.
|
|
4
|
+
* A hologram is a lightweight reference (pointer) from one holon to data
|
|
5
|
+
* in another holon. It enables data to appear in multiple holons while
|
|
6
|
+
* maintaining a single source of truth.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* - authorPubKey:
|
|
10
|
-
* - capability
|
|
8
|
+
* SIMPLIFIED MODEL (v2):
|
|
9
|
+
* - authorPubKey: identifies WHO wrote the source data
|
|
10
|
+
* - No capability tokens — federation membership (pubkey list) controls visibility
|
|
11
|
+
* - Anyone can write anywhere (Nostr is open-write); reads are filtered by federation
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* The hologram is the core infrastructure of federation:
|
|
14
|
+
* 1. It points to source data (soul / target)
|
|
15
|
+
* 2. It knows the author (authorPubKey)
|
|
16
|
+
* 3. It resolves to the actual data on read
|
|
17
|
+
* 4. It supports local overrides (x, y, pinned, etc.)
|
|
14
18
|
*
|
|
15
19
|
* @module federation/hologram
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
|
-
import { buildPath, write, read, update } from '../storage/
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
22
|
+
import { buildPath, write, read, update } from '../storage/nostr-wrapper.js';
|
|
23
|
+
import { isPubkey } from '../crypto/secp256k1.js';
|
|
24
|
+
import {
|
|
25
|
+
encryptNIP44,
|
|
26
|
+
createDMEvent,
|
|
27
|
+
generateNonce,
|
|
28
|
+
} from '../crypto/nostr-utils.js';
|
|
21
29
|
|
|
22
30
|
/** @constant {number} Maximum depth for hologram resolution chain */
|
|
23
31
|
const MAX_RESOLUTION_DEPTH = 10;
|
|
@@ -85,40 +93,32 @@ export async function wouldCreateCircularHologram(client, appname, sourceHolon,
|
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
/**
|
|
88
|
-
* Create a hologram (lightweight reference)
|
|
96
|
+
* Create a hologram (lightweight reference).
|
|
89
97
|
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
98
|
+
* A hologram is a pointer — it says "the real data lives at soul,
|
|
99
|
+
* written by authorPubKey." No capability tokens needed; federation
|
|
100
|
+
* membership (a pubkey list) controls who gets to see what on read.
|
|
92
101
|
*
|
|
93
102
|
* @param {string} sourceHolon - Source holon ID
|
|
94
103
|
* @param {string} targetHolon - Target holon ID
|
|
95
104
|
* @param {string} lensName - Lens name
|
|
96
105
|
* @param {string} dataId - Data ID
|
|
97
106
|
* @param {string} appname - Application namespace
|
|
98
|
-
* @param {Object} options - Hologram options
|
|
99
|
-
* @param {string} options.authorPubKey - Source author's public key (REQUIRED
|
|
100
|
-
* @
|
|
101
|
-
* @
|
|
102
|
-
* @throws {Error} If authorPubKey or capability is missing
|
|
107
|
+
* @param {Object} options - Hologram options
|
|
108
|
+
* @param {string} options.authorPubKey - Source author's public key (REQUIRED)
|
|
109
|
+
* @returns {Object} Hologram object
|
|
110
|
+
* @throws {Error} If authorPubKey is missing
|
|
103
111
|
*/
|
|
104
112
|
export function createHologram(sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
|
|
105
113
|
const { authorPubKey } = options;
|
|
106
|
-
let { capability } = options;
|
|
107
114
|
|
|
108
|
-
// Validate required fields for unified model
|
|
109
115
|
if (!authorPubKey) {
|
|
110
|
-
throw new Error('authorPubKey is required for hologram creation
|
|
111
|
-
}
|
|
112
|
-
if (!capability) {
|
|
113
|
-
throw new Error('capability is required for hologram creation (unified model)');
|
|
116
|
+
throw new Error('authorPubKey is required for hologram creation');
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
// Normalize capability to plain token string
|
|
117
|
-
capability = normalizeTokenString(capability) || capability;
|
|
118
|
-
|
|
119
119
|
const soul = buildPath(appname, sourceHolon, lensName, dataId);
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
return {
|
|
122
122
|
id: dataId,
|
|
123
123
|
hologram: true,
|
|
124
124
|
soul,
|
|
@@ -127,29 +127,25 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
127
127
|
holonId: sourceHolon,
|
|
128
128
|
lensName,
|
|
129
129
|
dataId,
|
|
130
|
-
authorPubKey,
|
|
130
|
+
authorPubKey,
|
|
131
131
|
},
|
|
132
|
-
capability, // Always the token string (normalized above)
|
|
133
132
|
_meta: {
|
|
134
133
|
created: Date.now(),
|
|
135
134
|
lastUpdated: Date.now(),
|
|
136
135
|
sourceHolon,
|
|
137
|
-
source: sourceHolon,
|
|
136
|
+
source: sourceHolon,
|
|
138
137
|
sourcePubKey: authorPubKey,
|
|
139
|
-
grantedAt: Date.now(),
|
|
140
138
|
},
|
|
141
139
|
};
|
|
142
|
-
|
|
143
|
-
return hologram;
|
|
144
140
|
}
|
|
145
141
|
|
|
146
142
|
/**
|
|
147
|
-
* Create a hologram
|
|
143
|
+
* Create a hologram for a given author.
|
|
148
144
|
*
|
|
149
|
-
* This is the preferred method for creating holograms. It
|
|
150
|
-
*
|
|
145
|
+
* This is the preferred method for creating holograms. It just needs the
|
|
146
|
+
* source author's public key — no capability tokens are issued or stored.
|
|
151
147
|
*
|
|
152
|
-
* @param {Object} client - Nostr client instance (must have publicKey
|
|
148
|
+
* @param {Object} client - Nostr client instance (must have publicKey)
|
|
153
149
|
* @param {string} sourceHolon - Source holon ID
|
|
154
150
|
* @param {string} targetHolon - Target holon ID
|
|
155
151
|
* @param {string} lensName - Lens name
|
|
@@ -157,103 +153,59 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
157
153
|
* @param {string} appname - Application namespace
|
|
158
154
|
* @param {Object} options - Hologram options
|
|
159
155
|
* @param {string} [options.sourceAuthorPubKey] - Source author's public key (defaults to client.publicKey)
|
|
160
|
-
* @param {string} [options.
|
|
161
|
-
* @
|
|
162
|
-
* @param {string} [options.capability] - Pre-issued capability (if not provided, one will be issued)
|
|
163
|
-
* @param {string[]} [options.permissions=['read']] - Permissions for auto-issued capability
|
|
164
|
-
* @param {number} [options.expiresIn=null] - Capability expiration in ms (null = no expiration)
|
|
165
|
-
* @returns {Promise<Object>} Hologram object with unified structure
|
|
156
|
+
* @param {string} [options.capability] - Ignored (kept for backward compat)
|
|
157
|
+
* @returns {Promise<Object>} Hologram object
|
|
166
158
|
*/
|
|
167
159
|
export async function createHologramWithCapability(client, sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
|
|
168
|
-
const
|
|
169
|
-
sourceAuthorPubKey = client.publicKey,
|
|
170
|
-
sourceAuthorPrivKey = client.privateKey, // Allow passing author's private key for signing
|
|
171
|
-
targetAuthorPubKey = client.publicKey,
|
|
172
|
-
capability: providedCapability = null,
|
|
173
|
-
permissions = ['read'],
|
|
174
|
-
expiresIn = null,
|
|
175
|
-
} = options;
|
|
176
|
-
|
|
177
|
-
let capability = providedCapability;
|
|
178
|
-
|
|
179
|
-
// Issue capability if not provided
|
|
180
|
-
if (!capability) {
|
|
181
|
-
const isSelfCapability = sourceAuthorPubKey === targetAuthorPubKey;
|
|
182
|
-
|
|
183
|
-
// Issue capability - for self-caps, no expiration by default
|
|
184
|
-
capability = await issueCapability(
|
|
185
|
-
permissions,
|
|
186
|
-
{ holonId: sourceHolon, lensName, dataId },
|
|
187
|
-
targetAuthorPubKey,
|
|
188
|
-
{
|
|
189
|
-
expiresIn: expiresIn !== undefined ? expiresIn : null, // Hologram capabilities don't expire — they live as long as the hologram
|
|
190
|
-
issuer: sourceAuthorPubKey,
|
|
191
|
-
issuerKey: sourceAuthorPrivKey, // Use passed private key (holon's key when federated via holonsbot)
|
|
192
|
-
}
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
// Store the capability in registry for cross-author federation
|
|
196
|
-
if (!isSelfCapability && client.privateKey) {
|
|
197
|
-
await storeInboundCapability(client, appname, sourceAuthorPubKey, {
|
|
198
|
-
token: capability,
|
|
199
|
-
scope: { holonId: sourceHolon, lensName, dataId },
|
|
200
|
-
permissions,
|
|
201
|
-
isSelfCapability: false,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
160
|
+
const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
|
|
205
161
|
|
|
206
162
|
return createHologram(sourceHolon, targetHolon, lensName, dataId, appname, {
|
|
207
163
|
authorPubKey: sourceAuthorPubKey,
|
|
208
|
-
capability,
|
|
209
164
|
});
|
|
210
165
|
}
|
|
211
166
|
|
|
212
167
|
/**
|
|
213
|
-
* Resolve hologram to actual data, merging local overrides
|
|
168
|
+
* Resolve hologram to actual data, merging local overrides.
|
|
214
169
|
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
170
|
+
* Resolution is simple:
|
|
171
|
+
* 1. Detect circular references
|
|
172
|
+
* 2. Fetch source data using authorPubKey as author filter
|
|
173
|
+
* 3. Merge with local overrides
|
|
219
174
|
*
|
|
220
|
-
*
|
|
175
|
+
* No capability verification — federation membership controls visibility
|
|
176
|
+
* at the read() layer, not at hologram resolution.
|
|
221
177
|
*
|
|
222
178
|
* @param {Object} client - Nostr client instance
|
|
223
179
|
* @param {Object} hologram - Hologram object
|
|
224
180
|
* @param {Set} visited - Visited souls (circular reference detection)
|
|
225
181
|
* @param {Array} chain - Chain of souls for debugging circular references
|
|
226
182
|
* @param {Object} options - Resolution options
|
|
227
|
-
* @param {string} options.appname - Application namespace
|
|
183
|
+
* @param {string} options.appname - Application namespace
|
|
228
184
|
* @param {boolean} options.deleteCircular - If true, delete circular holograms when detected (default: true)
|
|
229
185
|
* @param {string} options.hologramPath - Path of the hologram being resolved (for deletion)
|
|
230
|
-
* @param {boolean} options.skipCapabilityVerification - Skip capability check (for internal use only)
|
|
231
186
|
* @returns {Promise<Object|null>} Resolved data with _hologram metadata, or null
|
|
232
187
|
*/
|
|
233
188
|
export async function resolveHologram(client, hologram, visited = new Set(), chain = [], options = {}) {
|
|
234
|
-
const { deleteCircular = true, hologramPath = null
|
|
189
|
+
const { deleteCircular = true, hologramPath = null } = options;
|
|
235
190
|
|
|
236
191
|
if (!hologram || !hologram.hologram) {
|
|
237
192
|
return hologram; // Not a hologram, return as-is
|
|
238
193
|
}
|
|
239
194
|
|
|
195
|
+
// Lens holograms expand at read time (in index.js), not during resolution
|
|
196
|
+
if (hologram.lens === true) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
240
200
|
const { soul } = hologram;
|
|
241
201
|
const target = hologram.target || {};
|
|
242
202
|
|
|
243
203
|
// Circular reference detection
|
|
244
204
|
if (visited.has(soul)) {
|
|
245
205
|
const chainStr = [...chain, soul].join(' → ');
|
|
246
|
-
console.warn(
|
|
247
|
-
|
|
248
|
-
console.warn(` Chain: ${chainStr}`);
|
|
249
|
-
console.warn(` Source holon: ${target.holonId || 'unknown'}`);
|
|
250
|
-
console.warn(` Lens: ${target.lensName || 'unknown'}`);
|
|
251
|
-
console.warn(` Data ID: ${target.dataId || hologram.id || 'unknown'}`);
|
|
252
|
-
console.warn(` Resolution depth: ${visited.size}`);
|
|
253
|
-
|
|
254
|
-
// Delete the circular hologram
|
|
206
|
+
console.warn(`Circular reference detected: ${chainStr}`);
|
|
207
|
+
|
|
255
208
|
if (deleteCircular && hologramPath) {
|
|
256
|
-
console.info(` 🗑️ Deleting circular hologram at: ${hologramPath}`);
|
|
257
209
|
await write(client, hologramPath, null);
|
|
258
210
|
}
|
|
259
211
|
|
|
@@ -261,102 +213,20 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
261
213
|
}
|
|
262
214
|
|
|
263
215
|
if (visited.size >= MAX_RESOLUTION_DEPTH) {
|
|
264
|
-
const chainStr = [...chain, soul].join(' → ');
|
|
265
|
-
console.warn(`⚠️ Max resolution depth (${MAX_RESOLUTION_DEPTH}) exceeded - removing hologram`);
|
|
266
|
-
console.warn(` Current soul: ${soul}`);
|
|
267
|
-
console.warn(` Chain: ${chainStr}`);
|
|
268
|
-
console.warn(` Source holon: ${target.holonId || 'unknown'}`);
|
|
269
|
-
console.warn(` Lens: ${target.lensName || 'unknown'}`);
|
|
270
|
-
|
|
271
|
-
// Delete the problematic hologram
|
|
272
216
|
if (deleteCircular && hologramPath) {
|
|
273
|
-
console.info(` 🗑️ Deleting deep chain hologram at: ${hologramPath}`);
|
|
274
217
|
await write(client, hologramPath, null);
|
|
275
218
|
}
|
|
276
|
-
|
|
277
219
|
return null;
|
|
278
220
|
}
|
|
279
221
|
|
|
280
222
|
visited.add(soul);
|
|
281
223
|
chain.push(soul);
|
|
282
224
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// UNIFIED MODEL: All holograms have authorPubKey and capability
|
|
286
|
-
// Get capability - try embedded first, then registry fallback
|
|
287
|
-
let capability = hologram.capability;
|
|
225
|
+
// Fetch source data — use authorPubKey as author filter if present
|
|
288
226
|
const authorPubKey = target.authorPubKey;
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
capability = normalizeTokenString(capability) || capability;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (!capability && options.appname && authorPubKey) {
|
|
296
|
-
// Fallback to registry lookup
|
|
297
|
-
const capEntry = await getCapabilityForAuthor(
|
|
298
|
-
client,
|
|
299
|
-
options.appname,
|
|
300
|
-
authorPubKey,
|
|
301
|
-
{
|
|
302
|
-
holonId: target.holonId,
|
|
303
|
-
lensName: target.lensName,
|
|
304
|
-
dataId: target.dataId,
|
|
305
|
-
}
|
|
306
|
-
);
|
|
307
|
-
if (capEntry) {
|
|
308
|
-
// Extract token string from capability entry
|
|
309
|
-
capability = capEntry.token || capEntry;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Verify capability (unified model - always verify unless explicitly skipped)
|
|
314
|
-
if (!skipCapabilityVerification) {
|
|
315
|
-
if (!capability) {
|
|
316
|
-
console.warn(`❌ Hologram missing capability: ${soul}`);
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// OBJECT-CAPABILITY SECURITY: Verify capability issuer matches claimed authorPubKey
|
|
321
|
-
// This prevents forged capabilities where someone signs a capability for data they don't own
|
|
322
|
-
if (authorPubKey) {
|
|
323
|
-
const issuerMatch = await verifyCapabilityIssuer(capability, authorPubKey);
|
|
324
|
-
if (!issuerMatch) {
|
|
325
|
-
console.warn(`❌ Capability issuer does not match authorPubKey for hologram: ${soul}`);
|
|
326
|
-
console.warn(` Expected author: ${authorPubKey?.slice(0, 12)}...`);
|
|
327
|
-
const decoded = decodeCapability(capability);
|
|
328
|
-
if (decoded) {
|
|
329
|
-
console.warn(` Actual issuer: ${decoded.issuer?.slice(0, 12)}...`);
|
|
330
|
-
}
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const isValid = await verifyCapability(
|
|
336
|
-
capability,
|
|
337
|
-
'read',
|
|
338
|
-
{
|
|
339
|
-
holonId: target.holonId,
|
|
340
|
-
lensName: target.lensName,
|
|
341
|
-
dataId: target.dataId,
|
|
342
|
-
}
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
if (!isValid) {
|
|
346
|
-
console.warn(`❌ Capability verification failed for hologram: ${soul}`);
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// UNIFIED MODEL: Always fetch with author filter if authorPubKey is present
|
|
352
|
-
if (authorPubKey) {
|
|
353
|
-
sourceData = await read(client, soul, {
|
|
354
|
-
authors: [authorPubKey]
|
|
355
|
-
});
|
|
356
|
-
} else {
|
|
357
|
-
// Legacy fallback for holograms without authorPubKey
|
|
358
|
-
sourceData = await read(client, soul);
|
|
359
|
-
}
|
|
227
|
+
const sourceData = authorPubKey
|
|
228
|
+
? await read(client, soul, { authors: [authorPubKey] })
|
|
229
|
+
: await read(client, soul);
|
|
360
230
|
|
|
361
231
|
if (!sourceData) {
|
|
362
232
|
return null;
|
|
@@ -364,7 +234,6 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
364
234
|
|
|
365
235
|
// If source data is also a hologram, recursively resolve
|
|
366
236
|
if (sourceData.hologram) {
|
|
367
|
-
// Pass the soul as the hologramPath so circular holograms can be deleted
|
|
368
237
|
return resolveHologram(client, sourceData, visited, chain, { ...options, hologramPath: soul });
|
|
369
238
|
}
|
|
370
239
|
|
|
@@ -373,7 +242,7 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
373
242
|
}
|
|
374
243
|
|
|
375
244
|
/**
|
|
376
|
-
* Merge hologram local overrides with source data
|
|
245
|
+
* Merge hologram local overrides with source data.
|
|
377
246
|
* @private
|
|
378
247
|
* @param {Object} hologram - Hologram object
|
|
379
248
|
* @param {Object} sourceData - Source data
|
|
@@ -455,10 +324,10 @@ export async function setupFederation(
|
|
|
455
324
|
}
|
|
456
325
|
|
|
457
326
|
/**
|
|
458
|
-
* Propagate data to federated location
|
|
327
|
+
* Propagate data to federated location.
|
|
459
328
|
*
|
|
460
|
-
*
|
|
461
|
-
*
|
|
329
|
+
* Creates a hologram (reference) or copy in the target holon.
|
|
330
|
+
* No capability tokens needed — just the author's pubkey.
|
|
462
331
|
*
|
|
463
332
|
* @param {Object} client - Nostr client instance (must have publicKey)
|
|
464
333
|
* @param {string} appname - Application namespace
|
|
@@ -469,8 +338,6 @@ export async function setupFederation(
|
|
|
469
338
|
* @param {string} mode - 'reference' or 'copy'
|
|
470
339
|
* @param {Object} options - Propagation options
|
|
471
340
|
* @param {string} [options.sourceAuthorPubKey] - Source author (defaults to client.publicKey)
|
|
472
|
-
* @param {string} [options.sourceAuthorPrivKey] - Source author's private key for signing capability (defaults to client.privateKey)
|
|
473
|
-
* @param {string} [options.capability] - Pre-issued capability (auto-issued if not provided)
|
|
474
341
|
* @returns {Promise<boolean>} Success indicator
|
|
475
342
|
*/
|
|
476
343
|
export async function propagateData(
|
|
@@ -491,97 +358,13 @@ export async function propagateData(
|
|
|
491
358
|
return true;
|
|
492
359
|
}
|
|
493
360
|
|
|
494
|
-
// Don't propagate holograms
|
|
495
|
-
// If this data is already a hologram pointing to the target, skip it
|
|
361
|
+
// Don't propagate holograms — holograms should only be created explicitly
|
|
496
362
|
if (data.hologram === true) {
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
console.info(` Target holon: ${targetHolon}`);
|
|
503
|
-
console.info(` Hologram points to: ${data.target.holonId}`);
|
|
504
|
-
console.info(` Lens: ${lensName}`);
|
|
505
|
-
console.info(` Original soul: ${data.soul || 'unknown'}`);
|
|
506
|
-
return true;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Check if targetHolon is already in the original source's activeHolograms
|
|
510
|
-
// This prevents creating duplicate holograms when the target already has one
|
|
511
|
-
if (data.target) {
|
|
512
|
-
const originalSourcePath = buildPath(
|
|
513
|
-
data.target.appname || appname,
|
|
514
|
-
data.target.holonId,
|
|
515
|
-
data.target.lensName || lensName,
|
|
516
|
-
data.target.dataId || dataId
|
|
517
|
-
);
|
|
518
|
-
const originalSource = await read(client, originalSourcePath);
|
|
519
|
-
|
|
520
|
-
if (originalSource?._meta?.activeHolograms) {
|
|
521
|
-
const alreadyHasHologram = originalSource._meta.activeHolograms.some(
|
|
522
|
-
h => h.targetHolon === targetHolon
|
|
523
|
-
);
|
|
524
|
-
if (alreadyHasHologram) {
|
|
525
|
-
console.info(`⏭️ Skipping propagation - target already in activeHolograms:`);
|
|
526
|
-
console.info(` Data ID: ${dataId}`);
|
|
527
|
-
console.info(` Original source: ${data.target.holonId}`);
|
|
528
|
-
console.info(` Target holon: ${targetHolon}`);
|
|
529
|
-
return true;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Deep circular check for hologram copy - ensure the original source doesn't eventually point to target
|
|
535
|
-
const originalSourceHolon = data.target.holonId;
|
|
536
|
-
const originalDataId = data.target.dataId || dataId;
|
|
537
|
-
const originalLens = data.target.lensName || lensName;
|
|
538
|
-
const originalAppname = data.target.appname || appname;
|
|
539
|
-
|
|
540
|
-
const circularCheck = await wouldCreateCircularHologram(
|
|
541
|
-
client, originalAppname, originalSourceHolon, targetHolon, originalLens, originalDataId
|
|
542
|
-
);
|
|
543
|
-
|
|
544
|
-
if (circularCheck.isCircular) {
|
|
545
|
-
const chainStr = circularCheck.chain.map(c => c.holon).join(' → ');
|
|
546
|
-
console.warn(`🔄 Preventing circular hologram copy:`);
|
|
547
|
-
console.warn(` Data ID: ${dataId}`);
|
|
548
|
-
console.warn(` Original source: ${originalSourceHolon}`);
|
|
549
|
-
console.warn(` Would copy to: ${targetHolon}`);
|
|
550
|
-
console.warn(` Existing chain: ${chainStr}`);
|
|
551
|
-
console.warn(` Reason: ${circularCheck.reason}`);
|
|
552
|
-
return false;
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// When propagating a hologram, copy it instead of creating a hologram of a hologram
|
|
556
|
-
// This maintains the reference to the original source
|
|
557
|
-
const targetPath = buildPath(appname, targetHolon, lensName, dataId);
|
|
558
|
-
const copiedHologram = {
|
|
559
|
-
...data,
|
|
560
|
-
_meta: {
|
|
561
|
-
...data._meta,
|
|
562
|
-
copiedFrom: sourceHolon,
|
|
563
|
-
copiedAt: Date.now()
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
const success = await write(client, targetPath, copiedHologram);
|
|
567
|
-
|
|
568
|
-
// Add this new location to the original source's activeHolograms
|
|
569
|
-
if (success && data.target) {
|
|
570
|
-
await addActiveHologram(
|
|
571
|
-
client,
|
|
572
|
-
data.target.appname || appname,
|
|
573
|
-
data.target.holonId,
|
|
574
|
-
data.target.lensName || lensName,
|
|
575
|
-
data.target.dataId || dataId,
|
|
576
|
-
targetHolon
|
|
577
|
-
);
|
|
578
|
-
console.info(`📋 Copied hologram to ${targetHolon}:`);
|
|
579
|
-
console.info(` Data ID: ${dataId}`);
|
|
580
|
-
console.info(` Original source: ${data.target.holonId}`);
|
|
581
|
-
console.info(` Copied from: ${sourceHolon}`);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return success;
|
|
363
|
+
console.info(`⏭️ Skipping propagation - data is already a hologram (no transitive copy):`);
|
|
364
|
+
console.info(` Data ID: ${dataId}`);
|
|
365
|
+
console.info(` Source holon: ${sourceHolon}`);
|
|
366
|
+
console.info(` Target holon: ${targetHolon}`);
|
|
367
|
+
return true;
|
|
585
368
|
}
|
|
586
369
|
|
|
587
370
|
// For non-hologram data, check if targetHolon is already in activeHolograms
|
|
@@ -614,13 +397,9 @@ export async function propagateData(
|
|
|
614
397
|
return false;
|
|
615
398
|
}
|
|
616
399
|
|
|
617
|
-
//
|
|
400
|
+
// Create hologram — just a pointer with the author's pubkey
|
|
618
401
|
const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
|
|
619
402
|
|
|
620
|
-
// Determine targetAuthorPubKey - the person who should be able to read the hologram
|
|
621
|
-
// If targetHolon is a 64-char hex pubkey, use it directly as the target author
|
|
622
|
-
const targetAuthorPubKey = options.targetAuthorPubKey || (isPubkey(targetHolon) ? targetHolon : client.publicKey);
|
|
623
|
-
|
|
624
403
|
const hologram = await createHologramWithCapability(
|
|
625
404
|
client,
|
|
626
405
|
sourceHolon,
|
|
@@ -628,13 +407,7 @@ export async function propagateData(
|
|
|
628
407
|
lensName,
|
|
629
408
|
dataId,
|
|
630
409
|
appname,
|
|
631
|
-
{
|
|
632
|
-
sourceAuthorPubKey,
|
|
633
|
-
sourceAuthorPrivKey: options.sourceAuthorPrivKey, // Pass through for holon-signed capabilities
|
|
634
|
-
targetAuthorPubKey,
|
|
635
|
-
capability: options.capability,
|
|
636
|
-
permissions: ['read'],
|
|
637
|
-
}
|
|
410
|
+
{ sourceAuthorPubKey }
|
|
638
411
|
);
|
|
639
412
|
|
|
640
413
|
const targetPath = buildPath(appname, targetHolon, lensName, dataId);
|
|
@@ -708,6 +481,157 @@ export function isHologram(data) {
|
|
|
708
481
|
return data && data.hologram === true;
|
|
709
482
|
}
|
|
710
483
|
|
|
484
|
+
// ============================================================================
|
|
485
|
+
// Lens Holograms — "share this entire lens from this author"
|
|
486
|
+
// ============================================================================
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Create a lens hologram — a reference meaning "all data in this lens from this author".
|
|
490
|
+
*
|
|
491
|
+
* Unlike an item hologram (which points at a single data item), a lens hologram
|
|
492
|
+
* expands at read time: the reader fetches every item in the source lens and merges
|
|
493
|
+
* them into the local results.
|
|
494
|
+
*
|
|
495
|
+
* @param {string} sourceHolon - Source holon ID (where data lives)
|
|
496
|
+
* @param {string} lensName - Lens name
|
|
497
|
+
* @param {string} appname - Application namespace
|
|
498
|
+
* @param {Object} options
|
|
499
|
+
* @param {string} options.author - Public key of the author whose data this points to
|
|
500
|
+
* @returns {Object} Lens hologram object
|
|
501
|
+
*/
|
|
502
|
+
export function createLensHologram(sourceHolon, lensName, appname, { author }) {
|
|
503
|
+
if (!author) {
|
|
504
|
+
throw new Error('author is required for lens hologram creation');
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const soul = buildPath(appname, sourceHolon, lensName);
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
id: `_lens_${author}`,
|
|
511
|
+
hologram: true,
|
|
512
|
+
lens: true, // distinguishes from item hologram
|
|
513
|
+
soul,
|
|
514
|
+
target: {
|
|
515
|
+
app: appname,
|
|
516
|
+
holon: sourceHolon,
|
|
517
|
+
lens: lensName,
|
|
518
|
+
author,
|
|
519
|
+
},
|
|
520
|
+
_meta: {
|
|
521
|
+
created: Date.now(),
|
|
522
|
+
lastUpdated: Date.now(),
|
|
523
|
+
sourceHolon,
|
|
524
|
+
sourcePubKey: author,
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Check if data is a lens hologram (all-items reference).
|
|
531
|
+
* @param {Object} data - Data to check
|
|
532
|
+
* @returns {boolean} True if this is a lens hologram
|
|
533
|
+
*/
|
|
534
|
+
export function isLensHologram(data) {
|
|
535
|
+
return !!(data && data.hologram === true && data.lens === true);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// ============================================================================
|
|
539
|
+
// DM Helpers — send share / share_ack payloads via NIP-44 encrypted DM
|
|
540
|
+
// ============================================================================
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Send a 'share' DM to a target pubkey.
|
|
544
|
+
*
|
|
545
|
+
* @param {Object} client - NostrClient instance with publish method
|
|
546
|
+
* @param {string} privateKey - Sender's hex private key
|
|
547
|
+
* @param {string} targetPubKey - Recipient's hex public key
|
|
548
|
+
* @param {Object} payload - Share payload
|
|
549
|
+
* @param {string} payload.source - Source holon ID
|
|
550
|
+
* @param {string} payload.lens - Lens name
|
|
551
|
+
* @param {string|null} payload.item - Data ID (null for lens-level share)
|
|
552
|
+
* @param {string} payload.author - Author public key
|
|
553
|
+
* @param {Array} [payload.keys] - Optional bundled lens encryption keys
|
|
554
|
+
* @param {string} [payload.msg] - Optional message
|
|
555
|
+
* @returns {Promise<boolean>} Success indicator
|
|
556
|
+
*/
|
|
557
|
+
export async function sendShare(client, privateKey, targetPubKey, payload) {
|
|
558
|
+
try {
|
|
559
|
+
const dm = {
|
|
560
|
+
type: 'share',
|
|
561
|
+
id: generateNonce(),
|
|
562
|
+
source: payload.source,
|
|
563
|
+
lens: payload.lens,
|
|
564
|
+
item: payload.item || null,
|
|
565
|
+
author: payload.author,
|
|
566
|
+
keys: payload.keys || [],
|
|
567
|
+
msg: payload.msg || '',
|
|
568
|
+
timestamp: Date.now(),
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
const content = JSON.stringify(dm);
|
|
572
|
+
const encrypted = encryptNIP44(privateKey, targetPubKey, content);
|
|
573
|
+
const event = createDMEvent(targetPubKey, encrypted, privateKey);
|
|
574
|
+
|
|
575
|
+
if (client?.publish) {
|
|
576
|
+
await client.publish(event);
|
|
577
|
+
console.log('[Share] Share DM sent to:', targetPubKey.substring(0, 8) + '...');
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
console.error('[Share] No publish method on client');
|
|
581
|
+
return false;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
console.error('[Share] Failed to send share DM:', error);
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Send a 'share_ack' DM (accept or reject response).
|
|
590
|
+
*
|
|
591
|
+
* @param {Object} client - NostrClient instance with publish method
|
|
592
|
+
* @param {string} privateKey - Sender's hex private key
|
|
593
|
+
* @param {string} targetPubKey - Recipient's hex public key
|
|
594
|
+
* @param {Object} payload - Ack payload
|
|
595
|
+
* @param {string} payload.id - Original share id being acknowledged
|
|
596
|
+
* @param {string} payload.source - Source holon ID
|
|
597
|
+
* @param {string} payload.lens - Lens name
|
|
598
|
+
* @param {string|null} payload.item - Data ID (null for lens-level)
|
|
599
|
+
* @param {string} payload.author - Author public key
|
|
600
|
+
* @param {'accepted'|'rejected'} payload.status - Acceptance status
|
|
601
|
+
* @param {string} [payload.msg] - Optional message
|
|
602
|
+
* @returns {Promise<boolean>} Success indicator
|
|
603
|
+
*/
|
|
604
|
+
export async function sendAck(client, privateKey, targetPubKey, payload) {
|
|
605
|
+
try {
|
|
606
|
+
const dm = {
|
|
607
|
+
type: 'share_ack',
|
|
608
|
+
id: payload.id,
|
|
609
|
+
source: payload.source,
|
|
610
|
+
lens: payload.lens,
|
|
611
|
+
item: payload.item || null,
|
|
612
|
+
author: payload.author,
|
|
613
|
+
status: payload.status,
|
|
614
|
+
msg: payload.msg || '',
|
|
615
|
+
timestamp: Date.now(),
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const content = JSON.stringify(dm);
|
|
619
|
+
const encrypted = encryptNIP44(privateKey, targetPubKey, content);
|
|
620
|
+
const event = createDMEvent(targetPubKey, encrypted, privateKey);
|
|
621
|
+
|
|
622
|
+
if (client?.publish) {
|
|
623
|
+
await client.publish(event);
|
|
624
|
+
console.log('[Share] Ack DM sent to:', targetPubKey.substring(0, 8) + '...', 'status:', payload.status);
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
console.error('[Share] No publish method on client');
|
|
628
|
+
return false;
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error('[Share] Failed to send ack DM:', error);
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
711
635
|
/**
|
|
712
636
|
* Check if data was resolved from a hologram
|
|
713
637
|
* @param {Object} data - Data to check
|