holosphere 2.0.0-alpha20 → 2.0.0-alpha22
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 +43 -41
- package/dist/{index-COpLk9gL.cjs → index-B4xe-N5-.cjs} +2 -2
- package/dist/{index-COpLk9gL.cjs.map → index-B4xe-N5-.cjs.map} +1 -1
- package/dist/{index-D2WstuZJ.js → index-Bug_CGNq.js} +2 -2
- package/dist/{index-D2WstuZJ.js.map → index-Bug_CGNq.js.map} +1 -1
- package/dist/index-CaCPzdlv.cjs +29 -0
- package/dist/index-CaCPzdlv.cjs.map +1 -0
- package/dist/{index-B6-8KAQm.js → index-D76zMgwU.js} +2 -2
- package/dist/{index-B6-8KAQm.js.map → index-D76zMgwU.js.map} +1 -1
- package/dist/{index--QsHG_gD.cjs → index-DuOuk96g.cjs} +2 -2
- package/dist/{index--QsHG_gD.cjs.map → index-DuOuk96g.cjs.map} +1 -1
- package/dist/{index-BHptWysv.js → index-bYHRpACA.js} +2951 -7736
- package/dist/index-bYHRpACA.js.map +1 -0
- package/dist/{indexeddb-storage-kQ53UHEE.js → indexeddb-storage-BrIwr42m.js} +2 -2
- package/dist/{indexeddb-storage-kQ53UHEE.js.map → indexeddb-storage-BrIwr42m.js.map} +1 -1
- package/dist/{indexeddb-storage-wKG4mICM.cjs → indexeddb-storage-CFWfkdX9.cjs} +2 -2
- package/dist/{indexeddb-storage-wKG4mICM.cjs.map → indexeddb-storage-CFWfkdX9.cjs.map} +1 -1
- package/dist/{memory-storage-DnXCSbBl.js → memory-storage-BDQRj-2j.js} +2 -2
- package/dist/{memory-storage-DnXCSbBl.js.map → memory-storage-BDQRj-2j.js.map} +1 -1
- package/dist/{memory-storage-CGC8xM2G.cjs → memory-storage-bkatDnuR.cjs} +2 -2
- package/dist/{memory-storage-CGC8xM2G.cjs.map → memory-storage-bkatDnuR.cjs.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 +13 -411
- package/src/federation/discovery.js +7 -75
- package/src/federation/handshake.js +69 -202
- package/src/federation/hologram.js +275 -460
- package/src/federation/index.js +2 -9
- package/src/federation/registry.js +65 -1169
- package/src/federation/request-card.js +21 -35
- package/src/hierarchical/upcast.js +4 -9
- package/src/index.js +142 -296
- package/src/lib/federation-methods.js +388 -886
- 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 -162
- 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,42 +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)');
|
|
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;
|
|
116
|
+
throw new Error('authorPubKey is required for hologram creation');
|
|
119
117
|
}
|
|
120
118
|
|
|
121
119
|
const soul = buildPath(appname, sourceHolon, lensName, dataId);
|
|
122
120
|
|
|
123
|
-
|
|
121
|
+
return {
|
|
124
122
|
id: dataId,
|
|
125
123
|
hologram: true,
|
|
126
124
|
soul,
|
|
@@ -129,29 +127,25 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
129
127
|
holonId: sourceHolon,
|
|
130
128
|
lensName,
|
|
131
129
|
dataId,
|
|
132
|
-
authorPubKey,
|
|
130
|
+
authorPubKey,
|
|
133
131
|
},
|
|
134
|
-
capability, // Always the token string (normalized above)
|
|
135
132
|
_meta: {
|
|
136
133
|
created: Date.now(),
|
|
137
134
|
lastUpdated: Date.now(),
|
|
138
135
|
sourceHolon,
|
|
139
|
-
source: sourceHolon,
|
|
136
|
+
source: sourceHolon,
|
|
140
137
|
sourcePubKey: authorPubKey,
|
|
141
|
-
grantedAt: Date.now(),
|
|
142
138
|
},
|
|
143
139
|
};
|
|
144
|
-
|
|
145
|
-
return hologram;
|
|
146
140
|
}
|
|
147
141
|
|
|
148
142
|
/**
|
|
149
|
-
* Create a hologram
|
|
143
|
+
* Create a hologram for a given author.
|
|
150
144
|
*
|
|
151
|
-
* This is the preferred method for creating holograms. It
|
|
152
|
-
*
|
|
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.
|
|
153
147
|
*
|
|
154
|
-
* @param {Object} client - Nostr client instance (must have publicKey
|
|
148
|
+
* @param {Object} client - Nostr client instance (must have publicKey)
|
|
155
149
|
* @param {string} sourceHolon - Source holon ID
|
|
156
150
|
* @param {string} targetHolon - Target holon ID
|
|
157
151
|
* @param {string} lensName - Lens name
|
|
@@ -159,103 +153,59 @@ export function createHologram(sourceHolon, targetHolon, lensName, dataId, appna
|
|
|
159
153
|
* @param {string} appname - Application namespace
|
|
160
154
|
* @param {Object} options - Hologram options
|
|
161
155
|
* @param {string} [options.sourceAuthorPubKey] - Source author's public key (defaults to client.publicKey)
|
|
162
|
-
* @param {string} [options.
|
|
163
|
-
* @
|
|
164
|
-
* @param {string} [options.capability] - Pre-issued capability (if not provided, one will be issued)
|
|
165
|
-
* @param {string[]} [options.permissions=['read']] - Permissions for auto-issued capability
|
|
166
|
-
* @param {number} [options.expiresIn=null] - Capability expiration in ms (null = no expiration)
|
|
167
|
-
* @returns {Promise<Object>} Hologram object with unified structure
|
|
156
|
+
* @param {string} [options.capability] - Ignored (kept for backward compat)
|
|
157
|
+
* @returns {Promise<Object>} Hologram object
|
|
168
158
|
*/
|
|
169
159
|
export async function createHologramWithCapability(client, sourceHolon, targetHolon, lensName, dataId, appname, options = {}) {
|
|
170
|
-
const
|
|
171
|
-
sourceAuthorPubKey = client.publicKey,
|
|
172
|
-
sourceAuthorPrivKey = client.privateKey, // Allow passing author's private key for signing
|
|
173
|
-
targetAuthorPubKey = client.publicKey,
|
|
174
|
-
capability: providedCapability = null,
|
|
175
|
-
permissions = ['read'],
|
|
176
|
-
expiresIn = null,
|
|
177
|
-
} = options;
|
|
178
|
-
|
|
179
|
-
let capability = providedCapability;
|
|
180
|
-
|
|
181
|
-
// Issue capability if not provided
|
|
182
|
-
if (!capability) {
|
|
183
|
-
const isSelfCapability = sourceAuthorPubKey === targetAuthorPubKey;
|
|
184
|
-
|
|
185
|
-
// Issue capability - for self-caps, no expiration by default
|
|
186
|
-
capability = await issueCapability(
|
|
187
|
-
permissions,
|
|
188
|
-
{ holonId: sourceHolon, lensName, dataId },
|
|
189
|
-
targetAuthorPubKey,
|
|
190
|
-
{
|
|
191
|
-
expiresIn: expiresIn !== undefined ? expiresIn : null, // Hologram capabilities don't expire — they live as long as the hologram
|
|
192
|
-
issuer: sourceAuthorPubKey,
|
|
193
|
-
issuerKey: sourceAuthorPrivKey, // Use passed private key (holon's key when federated via holonsbot)
|
|
194
|
-
}
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
// Store the capability in registry for cross-author federation
|
|
198
|
-
if (!isSelfCapability && client.privateKey) {
|
|
199
|
-
await storeInboundCapability(client, appname, sourceAuthorPubKey, {
|
|
200
|
-
token: capability,
|
|
201
|
-
scope: { holonId: sourceHolon, lensName, dataId },
|
|
202
|
-
permissions,
|
|
203
|
-
isSelfCapability: false,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
160
|
+
const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
|
|
207
161
|
|
|
208
162
|
return createHologram(sourceHolon, targetHolon, lensName, dataId, appname, {
|
|
209
163
|
authorPubKey: sourceAuthorPubKey,
|
|
210
|
-
capability,
|
|
211
164
|
});
|
|
212
165
|
}
|
|
213
166
|
|
|
214
167
|
/**
|
|
215
|
-
* Resolve hologram to actual data, merging local overrides
|
|
168
|
+
* Resolve hologram to actual data, merging local overrides.
|
|
216
169
|
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
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
|
|
221
174
|
*
|
|
222
|
-
*
|
|
175
|
+
* No capability verification — federation membership controls visibility
|
|
176
|
+
* at the read() layer, not at hologram resolution.
|
|
223
177
|
*
|
|
224
178
|
* @param {Object} client - Nostr client instance
|
|
225
179
|
* @param {Object} hologram - Hologram object
|
|
226
180
|
* @param {Set} visited - Visited souls (circular reference detection)
|
|
227
181
|
* @param {Array} chain - Chain of souls for debugging circular references
|
|
228
182
|
* @param {Object} options - Resolution options
|
|
229
|
-
* @param {string} options.appname - Application namespace
|
|
183
|
+
* @param {string} options.appname - Application namespace
|
|
230
184
|
* @param {boolean} options.deleteCircular - If true, delete circular holograms when detected (default: true)
|
|
231
185
|
* @param {string} options.hologramPath - Path of the hologram being resolved (for deletion)
|
|
232
|
-
* @param {boolean} options.skipCapabilityVerification - Skip capability check (for internal use only)
|
|
233
186
|
* @returns {Promise<Object|null>} Resolved data with _hologram metadata, or null
|
|
234
187
|
*/
|
|
235
188
|
export async function resolveHologram(client, hologram, visited = new Set(), chain = [], options = {}) {
|
|
236
|
-
const { deleteCircular = true, hologramPath = null
|
|
189
|
+
const { deleteCircular = true, hologramPath = null } = options;
|
|
237
190
|
|
|
238
191
|
if (!hologram || !hologram.hologram) {
|
|
239
192
|
return hologram; // Not a hologram, return as-is
|
|
240
193
|
}
|
|
241
194
|
|
|
195
|
+
// Lens holograms expand at read time (in index.js), not during resolution
|
|
196
|
+
if (hologram.lens === true) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
242
200
|
const { soul } = hologram;
|
|
243
201
|
const target = hologram.target || {};
|
|
244
202
|
|
|
245
203
|
// Circular reference detection
|
|
246
204
|
if (visited.has(soul)) {
|
|
247
205
|
const chainStr = [...chain, soul].join(' → ');
|
|
248
|
-
console.warn(
|
|
249
|
-
|
|
250
|
-
console.warn(` Chain: ${chainStr}`);
|
|
251
|
-
console.warn(` Source holon: ${target.holonId || 'unknown'}`);
|
|
252
|
-
console.warn(` Lens: ${target.lensName || 'unknown'}`);
|
|
253
|
-
console.warn(` Data ID: ${target.dataId || hologram.id || 'unknown'}`);
|
|
254
|
-
console.warn(` Resolution depth: ${visited.size}`);
|
|
255
|
-
|
|
256
|
-
// Delete the circular hologram
|
|
206
|
+
console.warn(`Circular reference detected: ${chainStr}`);
|
|
207
|
+
|
|
257
208
|
if (deleteCircular && hologramPath) {
|
|
258
|
-
console.info(` 🗑️ Deleting circular hologram at: ${hologramPath}`);
|
|
259
209
|
await write(client, hologramPath, null);
|
|
260
210
|
}
|
|
261
211
|
|
|
@@ -263,102 +213,20 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
263
213
|
}
|
|
264
214
|
|
|
265
215
|
if (visited.size >= MAX_RESOLUTION_DEPTH) {
|
|
266
|
-
const chainStr = [...chain, soul].join(' → ');
|
|
267
|
-
console.warn(`⚠️ Max resolution depth (${MAX_RESOLUTION_DEPTH}) exceeded - removing hologram`);
|
|
268
|
-
console.warn(` Current soul: ${soul}`);
|
|
269
|
-
console.warn(` Chain: ${chainStr}`);
|
|
270
|
-
console.warn(` Source holon: ${target.holonId || 'unknown'}`);
|
|
271
|
-
console.warn(` Lens: ${target.lensName || 'unknown'}`);
|
|
272
|
-
|
|
273
|
-
// Delete the problematic hologram
|
|
274
216
|
if (deleteCircular && hologramPath) {
|
|
275
|
-
console.info(` 🗑️ Deleting deep chain hologram at: ${hologramPath}`);
|
|
276
217
|
await write(client, hologramPath, null);
|
|
277
218
|
}
|
|
278
|
-
|
|
279
219
|
return null;
|
|
280
220
|
}
|
|
281
221
|
|
|
282
222
|
visited.add(soul);
|
|
283
223
|
chain.push(soul);
|
|
284
224
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// UNIFIED MODEL: All holograms have authorPubKey and capability
|
|
288
|
-
// Get capability - try embedded first, then registry fallback
|
|
289
|
-
let capability = hologram.capability;
|
|
225
|
+
// Fetch source data — use authorPubKey as author filter if present
|
|
290
226
|
const authorPubKey = target.authorPubKey;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
capability = capability.token;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (!capability && options.appname && authorPubKey) {
|
|
298
|
-
// Fallback to registry lookup
|
|
299
|
-
const capEntry = await getCapabilityForAuthor(
|
|
300
|
-
client,
|
|
301
|
-
options.appname,
|
|
302
|
-
authorPubKey,
|
|
303
|
-
{
|
|
304
|
-
holonId: target.holonId,
|
|
305
|
-
lensName: target.lensName,
|
|
306
|
-
dataId: target.dataId,
|
|
307
|
-
}
|
|
308
|
-
);
|
|
309
|
-
if (capEntry) {
|
|
310
|
-
// Extract token string from capability entry
|
|
311
|
-
capability = capEntry.token || capEntry;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Verify capability (unified model - always verify unless explicitly skipped)
|
|
316
|
-
if (!skipCapabilityVerification) {
|
|
317
|
-
if (!capability) {
|
|
318
|
-
console.warn(`❌ Hologram missing capability: ${soul}`);
|
|
319
|
-
return null;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// OBJECT-CAPABILITY SECURITY: Verify capability issuer matches claimed authorPubKey
|
|
323
|
-
// This prevents forged capabilities where someone signs a capability for data they don't own
|
|
324
|
-
if (authorPubKey) {
|
|
325
|
-
const issuerMatch = await verifyCapabilityIssuer(capability, authorPubKey);
|
|
326
|
-
if (!issuerMatch) {
|
|
327
|
-
console.warn(`❌ Capability issuer does not match authorPubKey for hologram: ${soul}`);
|
|
328
|
-
console.warn(` Expected author: ${authorPubKey?.slice(0, 12)}...`);
|
|
329
|
-
const decoded = decodeCapability(capability);
|
|
330
|
-
if (decoded) {
|
|
331
|
-
console.warn(` Actual issuer: ${decoded.issuer?.slice(0, 12)}...`);
|
|
332
|
-
}
|
|
333
|
-
return null;
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const isValid = await verifyCapability(
|
|
338
|
-
capability,
|
|
339
|
-
'read',
|
|
340
|
-
{
|
|
341
|
-
holonId: target.holonId,
|
|
342
|
-
lensName: target.lensName,
|
|
343
|
-
dataId: target.dataId,
|
|
344
|
-
}
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
if (!isValid) {
|
|
348
|
-
console.warn(`❌ Capability verification failed for hologram: ${soul}`);
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// UNIFIED MODEL: Always fetch with author filter if authorPubKey is present
|
|
354
|
-
if (authorPubKey) {
|
|
355
|
-
sourceData = await read(client, soul, {
|
|
356
|
-
authors: [authorPubKey]
|
|
357
|
-
});
|
|
358
|
-
} else {
|
|
359
|
-
// Legacy fallback for holograms without authorPubKey
|
|
360
|
-
sourceData = await read(client, soul);
|
|
361
|
-
}
|
|
227
|
+
const sourceData = authorPubKey
|
|
228
|
+
? await read(client, soul, { authors: [authorPubKey] })
|
|
229
|
+
: await read(client, soul);
|
|
362
230
|
|
|
363
231
|
if (!sourceData) {
|
|
364
232
|
return null;
|
|
@@ -366,7 +234,6 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
366
234
|
|
|
367
235
|
// If source data is also a hologram, recursively resolve
|
|
368
236
|
if (sourceData.hologram) {
|
|
369
|
-
// Pass the soul as the hologramPath so circular holograms can be deleted
|
|
370
237
|
return resolveHologram(client, sourceData, visited, chain, { ...options, hologramPath: soul });
|
|
371
238
|
}
|
|
372
239
|
|
|
@@ -375,7 +242,7 @@ export async function resolveHologram(client, hologram, visited = new Set(), cha
|
|
|
375
242
|
}
|
|
376
243
|
|
|
377
244
|
/**
|
|
378
|
-
* Merge hologram local overrides with source data
|
|
245
|
+
* Merge hologram local overrides with source data.
|
|
379
246
|
* @private
|
|
380
247
|
* @param {Object} hologram - Hologram object
|
|
381
248
|
* @param {Object} sourceData - Source data
|
|
@@ -457,10 +324,10 @@ export async function setupFederation(
|
|
|
457
324
|
}
|
|
458
325
|
|
|
459
326
|
/**
|
|
460
|
-
* Propagate data to federated location
|
|
327
|
+
* Propagate data to federated location.
|
|
461
328
|
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
329
|
+
* Creates a hologram (reference) or copy in the target holon.
|
|
330
|
+
* No capability tokens needed — just the author's pubkey.
|
|
464
331
|
*
|
|
465
332
|
* @param {Object} client - Nostr client instance (must have publicKey)
|
|
466
333
|
* @param {string} appname - Application namespace
|
|
@@ -471,8 +338,6 @@ export async function setupFederation(
|
|
|
471
338
|
* @param {string} mode - 'reference' or 'copy'
|
|
472
339
|
* @param {Object} options - Propagation options
|
|
473
340
|
* @param {string} [options.sourceAuthorPubKey] - Source author (defaults to client.publicKey)
|
|
474
|
-
* @param {string} [options.sourceAuthorPrivKey] - Source author's private key for signing capability (defaults to client.privateKey)
|
|
475
|
-
* @param {string} [options.capability] - Pre-issued capability (auto-issued if not provided)
|
|
476
341
|
* @returns {Promise<boolean>} Success indicator
|
|
477
342
|
*/
|
|
478
343
|
export async function propagateData(
|
|
@@ -493,97 +358,13 @@ export async function propagateData(
|
|
|
493
358
|
return true;
|
|
494
359
|
}
|
|
495
360
|
|
|
496
|
-
// Don't propagate holograms
|
|
497
|
-
// If this data is already a hologram pointing to the target, skip it
|
|
361
|
+
// Don't propagate holograms — holograms should only be created explicitly
|
|
498
362
|
if (data.hologram === true) {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
console.info(` Target holon: ${targetHolon}`);
|
|
505
|
-
console.info(` Hologram points to: ${data.target.holonId}`);
|
|
506
|
-
console.info(` Lens: ${lensName}`);
|
|
507
|
-
console.info(` Original soul: ${data.soul || 'unknown'}`);
|
|
508
|
-
return true;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Check if targetHolon is already in the original source's activeHolograms
|
|
512
|
-
// This prevents creating duplicate holograms when the target already has one
|
|
513
|
-
if (data.target) {
|
|
514
|
-
const originalSourcePath = buildPath(
|
|
515
|
-
data.target.appname || appname,
|
|
516
|
-
data.target.holonId,
|
|
517
|
-
data.target.lensName || lensName,
|
|
518
|
-
data.target.dataId || dataId
|
|
519
|
-
);
|
|
520
|
-
const originalSource = await read(client, originalSourcePath);
|
|
521
|
-
|
|
522
|
-
if (originalSource?._meta?.activeHolograms) {
|
|
523
|
-
const alreadyHasHologram = originalSource._meta.activeHolograms.some(
|
|
524
|
-
h => h.targetHolon === targetHolon
|
|
525
|
-
);
|
|
526
|
-
if (alreadyHasHologram) {
|
|
527
|
-
console.info(`⏭️ Skipping propagation - target already in activeHolograms:`);
|
|
528
|
-
console.info(` Data ID: ${dataId}`);
|
|
529
|
-
console.info(` Original source: ${data.target.holonId}`);
|
|
530
|
-
console.info(` Target holon: ${targetHolon}`);
|
|
531
|
-
return true;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// Deep circular check for hologram copy - ensure the original source doesn't eventually point to target
|
|
537
|
-
const originalSourceHolon = data.target.holonId;
|
|
538
|
-
const originalDataId = data.target.dataId || dataId;
|
|
539
|
-
const originalLens = data.target.lensName || lensName;
|
|
540
|
-
const originalAppname = data.target.appname || appname;
|
|
541
|
-
|
|
542
|
-
const circularCheck = await wouldCreateCircularHologram(
|
|
543
|
-
client, originalAppname, originalSourceHolon, targetHolon, originalLens, originalDataId
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
if (circularCheck.isCircular) {
|
|
547
|
-
const chainStr = circularCheck.chain.map(c => c.holon).join(' → ');
|
|
548
|
-
console.warn(`🔄 Preventing circular hologram copy:`);
|
|
549
|
-
console.warn(` Data ID: ${dataId}`);
|
|
550
|
-
console.warn(` Original source: ${originalSourceHolon}`);
|
|
551
|
-
console.warn(` Would copy to: ${targetHolon}`);
|
|
552
|
-
console.warn(` Existing chain: ${chainStr}`);
|
|
553
|
-
console.warn(` Reason: ${circularCheck.reason}`);
|
|
554
|
-
return false;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// When propagating a hologram, copy it instead of creating a hologram of a hologram
|
|
558
|
-
// This maintains the reference to the original source
|
|
559
|
-
const targetPath = buildPath(appname, targetHolon, lensName, dataId);
|
|
560
|
-
const copiedHologram = {
|
|
561
|
-
...data,
|
|
562
|
-
_meta: {
|
|
563
|
-
...data._meta,
|
|
564
|
-
copiedFrom: sourceHolon,
|
|
565
|
-
copiedAt: Date.now()
|
|
566
|
-
}
|
|
567
|
-
};
|
|
568
|
-
const success = await write(client, targetPath, copiedHologram);
|
|
569
|
-
|
|
570
|
-
// Add this new location to the original source's activeHolograms
|
|
571
|
-
if (success && data.target) {
|
|
572
|
-
await addActiveHologram(
|
|
573
|
-
client,
|
|
574
|
-
data.target.appname || appname,
|
|
575
|
-
data.target.holonId,
|
|
576
|
-
data.target.lensName || lensName,
|
|
577
|
-
data.target.dataId || dataId,
|
|
578
|
-
targetHolon
|
|
579
|
-
);
|
|
580
|
-
console.info(`📋 Copied hologram to ${targetHolon}:`);
|
|
581
|
-
console.info(` Data ID: ${dataId}`);
|
|
582
|
-
console.info(` Original source: ${data.target.holonId}`);
|
|
583
|
-
console.info(` Copied from: ${sourceHolon}`);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
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;
|
|
587
368
|
}
|
|
588
369
|
|
|
589
370
|
// For non-hologram data, check if targetHolon is already in activeHolograms
|
|
@@ -616,15 +397,9 @@ export async function propagateData(
|
|
|
616
397
|
return false;
|
|
617
398
|
}
|
|
618
399
|
|
|
619
|
-
//
|
|
400
|
+
// Create hologram — just a pointer with the author's pubkey
|
|
620
401
|
const sourceAuthorPubKey = options.sourceAuthorPubKey || client.publicKey;
|
|
621
402
|
|
|
622
|
-
// Determine targetAuthorPubKey - the person who should be able to read the hologram
|
|
623
|
-
// If targetHolon is a 64-char hex pubkey, use it directly as the target author
|
|
624
|
-
// Otherwise, this needs to be resolved via holon registry (TODO: add resolution)
|
|
625
|
-
const isPubkey = typeof targetHolon === 'string' && /^[0-9a-f]{64}$/i.test(targetHolon);
|
|
626
|
-
const targetAuthorPubKey = options.targetAuthorPubKey || (isPubkey ? targetHolon : client.publicKey);
|
|
627
|
-
|
|
628
403
|
const hologram = await createHologramWithCapability(
|
|
629
404
|
client,
|
|
630
405
|
sourceHolon,
|
|
@@ -632,13 +407,7 @@ export async function propagateData(
|
|
|
632
407
|
lensName,
|
|
633
408
|
dataId,
|
|
634
409
|
appname,
|
|
635
|
-
{
|
|
636
|
-
sourceAuthorPubKey,
|
|
637
|
-
sourceAuthorPrivKey: options.sourceAuthorPrivKey, // Pass through for holon-signed capabilities
|
|
638
|
-
targetAuthorPubKey,
|
|
639
|
-
capability: options.capability,
|
|
640
|
-
permissions: ['read'],
|
|
641
|
-
}
|
|
410
|
+
{ sourceAuthorPubKey }
|
|
642
411
|
);
|
|
643
412
|
|
|
644
413
|
const targetPath = buildPath(appname, targetHolon, lensName, dataId);
|
|
@@ -712,6 +481,157 @@ export function isHologram(data) {
|
|
|
712
481
|
return data && data.hologram === true;
|
|
713
482
|
}
|
|
714
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
|
+
|
|
715
635
|
/**
|
|
716
636
|
* Check if data was resolved from a hologram
|
|
717
637
|
* @param {Object} data - Data to check
|
|
@@ -738,36 +658,31 @@ export function getHologramSource(data) {
|
|
|
738
658
|
}
|
|
739
659
|
|
|
740
660
|
/**
|
|
741
|
-
*
|
|
742
|
-
*
|
|
743
|
-
*
|
|
661
|
+
* Follow a hologram chain to find the real (non-hologram) source data.
|
|
662
|
+
* If the data at the given location is a hologram, follows .target references
|
|
663
|
+
* until real data is found or max depth is reached.
|
|
664
|
+
* @private
|
|
744
665
|
* @param {Object} client - Nostr client instance
|
|
745
666
|
* @param {string} appname - Application namespace
|
|
746
|
-
* @param {string}
|
|
747
|
-
* @param {string}
|
|
667
|
+
* @param {string} holon - Starting holon ID
|
|
668
|
+
* @param {string} lens - Lens name
|
|
748
669
|
* @param {string} dataId - Data ID
|
|
749
|
-
* @param {
|
|
750
|
-
* @returns {Promise<
|
|
670
|
+
* @param {number} [maxDepth=10] - Maximum chain depth
|
|
671
|
+
* @returns {Promise<{ sourcePath: string, sourceData: Object }|null>} Real source or null
|
|
751
672
|
*/
|
|
752
|
-
|
|
673
|
+
async function followToRealSource(client, appname, holon, lens, dataId, maxDepth = 10) {
|
|
753
674
|
let currentAppname = appname;
|
|
754
|
-
let currentHolon =
|
|
755
|
-
let currentLens =
|
|
675
|
+
let currentHolon = holon;
|
|
676
|
+
let currentLens = lens;
|
|
756
677
|
let currentDataId = dataId;
|
|
757
678
|
let sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
|
|
758
679
|
|
|
759
|
-
// Read existing source data
|
|
760
|
-
// Note: May return null if called immediately after write (before relay persistence)
|
|
761
|
-
// This is expected - the hologram still works, we just can't track activeHolograms
|
|
762
680
|
let sourceData = await read(client, sourcePath);
|
|
763
681
|
if (!sourceData) {
|
|
764
|
-
return
|
|
682
|
+
return null;
|
|
765
683
|
}
|
|
766
684
|
|
|
767
|
-
// If source is a hologram, follow the chain to find the real source
|
|
768
|
-
// This ensures activeHolograms is always tracked on the actual data, not on holograms
|
|
769
685
|
let depth = 0;
|
|
770
|
-
const maxDepth = 10;
|
|
771
686
|
while (sourceData.hologram === true && sourceData.target && depth < maxDepth) {
|
|
772
687
|
depth++;
|
|
773
688
|
currentAppname = sourceData.target.appname || currentAppname;
|
|
@@ -778,10 +693,34 @@ export async function addActiveHologram(client, appname, sourceHolon, lensName,
|
|
|
778
693
|
|
|
779
694
|
sourceData = await read(client, sourcePath);
|
|
780
695
|
if (!sourceData) {
|
|
781
|
-
return
|
|
696
|
+
return null;
|
|
782
697
|
}
|
|
783
698
|
}
|
|
784
699
|
|
|
700
|
+
return { sourcePath, sourceData };
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Add a hologram reference to the source data's _meta.activeHolograms
|
|
705
|
+
* This tracks all holograms pointing to this specific data item
|
|
706
|
+
* If the source is itself a hologram, follows the chain to find the real source
|
|
707
|
+
* @param {Object} client - Nostr client instance
|
|
708
|
+
* @param {string} appname - Application namespace
|
|
709
|
+
* @param {string} sourceHolon - Source holon ID (where original data lives)
|
|
710
|
+
* @param {string} lensName - Lens name
|
|
711
|
+
* @param {string} dataId - Data ID
|
|
712
|
+
* @param {string} targetHolon - Target holon ID (where hologram lives)
|
|
713
|
+
* @returns {Promise<boolean>} Success indicator
|
|
714
|
+
*/
|
|
715
|
+
export async function addActiveHologram(client, appname, sourceHolon, lensName, dataId, targetHolon) {
|
|
716
|
+
// Note: May return null if called immediately after write (before relay persistence)
|
|
717
|
+
// This is expected - the hologram still works, we just can't track activeHolograms
|
|
718
|
+
const result = await followToRealSource(client, appname, sourceHolon, lensName, dataId);
|
|
719
|
+
if (!result) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const { sourcePath, sourceData } = result;
|
|
785
724
|
|
|
786
725
|
// Initialize _meta and activeHolograms if needed
|
|
787
726
|
if (!sourceData._meta) {
|
|
@@ -827,34 +766,12 @@ export async function addActiveHologram(client, appname, sourceHolon, lensName,
|
|
|
827
766
|
* @returns {Promise<boolean>} Success indicator
|
|
828
767
|
*/
|
|
829
768
|
export async function removeActiveHologram(client, appname, sourceHolon, lensName, dataId, targetHolon) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
let currentLens = lensName;
|
|
833
|
-
let currentDataId = dataId;
|
|
834
|
-
let sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
|
|
835
|
-
|
|
836
|
-
// Read existing source data
|
|
837
|
-
let sourceData = await read(client, sourcePath);
|
|
838
|
-
if (!sourceData) {
|
|
769
|
+
const result = await followToRealSource(client, appname, sourceHolon, lensName, dataId);
|
|
770
|
+
if (!result) {
|
|
839
771
|
return false;
|
|
840
772
|
}
|
|
841
773
|
|
|
842
|
-
|
|
843
|
-
let depth = 0;
|
|
844
|
-
const maxDepth = 10;
|
|
845
|
-
while (sourceData.hologram === true && sourceData.target && depth < maxDepth) {
|
|
846
|
-
depth++;
|
|
847
|
-
currentAppname = sourceData.target.appname || currentAppname;
|
|
848
|
-
currentHolon = sourceData.target.holonId;
|
|
849
|
-
currentLens = sourceData.target.lensName || currentLens;
|
|
850
|
-
currentDataId = sourceData.target.dataId || currentDataId;
|
|
851
|
-
sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
|
|
852
|
-
|
|
853
|
-
sourceData = await read(client, sourcePath);
|
|
854
|
-
if (!sourceData) {
|
|
855
|
-
return false;
|
|
856
|
-
}
|
|
857
|
-
}
|
|
774
|
+
const { sourcePath, sourceData } = result;
|
|
858
775
|
|
|
859
776
|
if (!sourceData._meta || !Array.isArray(sourceData._meta.activeHolograms)) {
|
|
860
777
|
return false;
|
|
@@ -887,35 +804,12 @@ export async function removeActiveHologram(client, appname, sourceHolon, lensNam
|
|
|
887
804
|
* @returns {Promise<boolean>} Success indicator
|
|
888
805
|
*/
|
|
889
806
|
export async function updateActiveHologramPlatform(client, appname, sourceHolon, lensName, dataId, targetHolon, platform, platformData) {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
let currentLens = lensName;
|
|
893
|
-
let currentDataId = dataId;
|
|
894
|
-
let sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
|
|
895
|
-
|
|
896
|
-
// Read existing source data
|
|
897
|
-
// Note: May return null if called immediately after write (before relay persistence)
|
|
898
|
-
let sourceData = await read(client, sourcePath);
|
|
899
|
-
if (!sourceData) {
|
|
807
|
+
const result = await followToRealSource(client, appname, sourceHolon, lensName, dataId);
|
|
808
|
+
if (!result) {
|
|
900
809
|
return false;
|
|
901
810
|
}
|
|
902
811
|
|
|
903
|
-
|
|
904
|
-
let depth = 0;
|
|
905
|
-
const maxDepth = 10;
|
|
906
|
-
while (sourceData.hologram === true && sourceData.target && depth < maxDepth) {
|
|
907
|
-
depth++;
|
|
908
|
-
currentAppname = sourceData.target.appname || currentAppname;
|
|
909
|
-
currentHolon = sourceData.target.holonId;
|
|
910
|
-
currentLens = sourceData.target.lensName || currentLens;
|
|
911
|
-
currentDataId = sourceData.target.dataId || currentDataId;
|
|
912
|
-
sourcePath = buildPath(currentAppname, currentHolon, currentLens, currentDataId);
|
|
913
|
-
|
|
914
|
-
sourceData = await read(client, sourcePath);
|
|
915
|
-
if (!sourceData) {
|
|
916
|
-
return false;
|
|
917
|
-
}
|
|
918
|
-
}
|
|
812
|
+
const { sourcePath, sourceData } = result;
|
|
919
813
|
|
|
920
814
|
if (!sourceData._meta || !Array.isArray(sourceData._meta.activeHolograms)) {
|
|
921
815
|
console.warn(`No activeHolograms found for ${sourcePath}`);
|
|
@@ -1132,96 +1026,17 @@ export async function deleteHologram(client, appname, holonId, lensName, dataId,
|
|
|
1132
1026
|
* @returns {Promise<Object>} Result with cleanup info
|
|
1133
1027
|
*/
|
|
1134
1028
|
export async function cleanupCircularHolograms(client, appname, holonId, lensName, options = {}) {
|
|
1135
|
-
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
console.info(`🧹 ${dryRun ? '[DRY RUN] ' : ''}Cleaning up circular holograms in ${holonId}/${lensName}...`);
|
|
1145
|
-
|
|
1146
|
-
try {
|
|
1147
|
-
// Get all data in this lens
|
|
1148
|
-
const basePath = buildPath(appname, holonId, lensName);
|
|
1149
|
-
// We need to iterate through all items - this requires listing capability
|
|
1150
|
-
// For now, we'll rely on the caller to provide a list of IDs to check
|
|
1151
|
-
// or we can use a different approach
|
|
1152
|
-
|
|
1153
|
-
// Alternative: Read the lens index if it exists
|
|
1154
|
-
const lensIndexPath = buildPath(appname, holonId, lensName, '_index');
|
|
1155
|
-
const lensIndex = await read(client, lensIndexPath);
|
|
1156
|
-
|
|
1157
|
-
if (!lensIndex || !Array.isArray(lensIndex.items)) {
|
|
1158
|
-
console.warn(` No index found for ${holonId}/${lensName}, cannot scan for circular references`);
|
|
1159
|
-
console.info(` Tip: Provide specific IDs to check using cleanupCircularHologramsByIds()`);
|
|
1160
|
-
return result;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
for (const itemId of lensIndex.items) {
|
|
1164
|
-
result.scanned++;
|
|
1165
|
-
const itemPath = buildPath(appname, holonId, lensName, itemId);
|
|
1166
|
-
const item = await read(client, itemPath);
|
|
1167
|
-
|
|
1168
|
-
if (!item || item.hologram !== true || !item.target) {
|
|
1169
|
-
continue; // Not a hologram, skip
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
// This is a hologram - check if it creates a circular reference
|
|
1173
|
-
const sourceHolon = item.target.holonId;
|
|
1174
|
-
const sourceDataId = item.target.dataId || itemId;
|
|
1175
|
-
const sourceLens = item.target.lensName || lensName;
|
|
1176
|
-
|
|
1177
|
-
// Read the source data
|
|
1178
|
-
const sourcePath = buildPath(item.target.appname || appname, sourceHolon, sourceLens, sourceDataId);
|
|
1179
|
-
const sourceData = await read(client, sourcePath);
|
|
1180
|
-
|
|
1181
|
-
if (sourceData && sourceData.hologram === true && sourceData.target) {
|
|
1182
|
-
// Source is also a hologram - check if it points back
|
|
1183
|
-
if (sourceData.target.holonId === holonId) {
|
|
1184
|
-
result.circularFound++;
|
|
1185
|
-
const detail = {
|
|
1186
|
-
itemId,
|
|
1187
|
-
path: itemPath,
|
|
1188
|
-
pointsTo: `${sourceHolon}/${sourceLens}/${sourceDataId}`,
|
|
1189
|
-
circularWith: `${sourceData.target.holonId}/${sourceData.target.lensName}/${sourceData.target.dataId}`
|
|
1190
|
-
};
|
|
1191
|
-
result.details.push(detail);
|
|
1192
|
-
|
|
1193
|
-
console.warn(` 🔄 Found circular hologram:`);
|
|
1194
|
-
console.warn(` ${holonId}/${lensName}/${itemId} → ${sourceHolon}/${sourceLens}/${sourceDataId} → ${holonId}`);
|
|
1195
|
-
|
|
1196
|
-
if (!dryRun) {
|
|
1197
|
-
// Delete this hologram
|
|
1198
|
-
const deleteResult = await deleteHologram(client, appname, holonId, lensName, itemId);
|
|
1199
|
-
if (deleteResult.success) {
|
|
1200
|
-
result.deleted++;
|
|
1201
|
-
console.info(` ✓ Deleted circular hologram`);
|
|
1202
|
-
} else {
|
|
1203
|
-
result.errors.push({ itemId, error: deleteResult.error });
|
|
1204
|
-
console.error(` ❌ Failed to delete: ${deleteResult.error}`);
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
console.info(`🧹 Cleanup complete for ${holonId}/${lensName}:`);
|
|
1212
|
-
console.info(` Scanned: ${result.scanned}`);
|
|
1213
|
-
console.info(` Circular found: ${result.circularFound}`);
|
|
1214
|
-
console.info(` Deleted: ${result.deleted}`);
|
|
1215
|
-
if (result.errors.length > 0) {
|
|
1216
|
-
console.warn(` Errors: ${result.errors.length}`);
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
return result;
|
|
1220
|
-
} catch (error) {
|
|
1221
|
-
console.error(`❌ Error during cleanup:`, error);
|
|
1222
|
-
result.errors.push({ error: error.message });
|
|
1223
|
-
return result;
|
|
1029
|
+
// Read the lens index to get all item IDs
|
|
1030
|
+
const lensIndexPath = buildPath(appname, holonId, lensName, '_index');
|
|
1031
|
+
const lensIndex = await read(client, lensIndexPath);
|
|
1032
|
+
|
|
1033
|
+
if (!lensIndex || !Array.isArray(lensIndex.items)) {
|
|
1034
|
+
console.warn(` No index found for ${holonId}/${lensName}, cannot scan for circular references`);
|
|
1035
|
+
console.info(` Tip: Provide specific IDs to check using cleanupCircularHologramsByIds()`);
|
|
1036
|
+
return { scanned: 0, circularFound: 0, deleted: 0, errors: [], details: [] };
|
|
1224
1037
|
}
|
|
1038
|
+
|
|
1039
|
+
return cleanupCircularHologramsByIds(client, appname, holonId, lensName, lensIndex.items, options);
|
|
1225
1040
|
}
|
|
1226
1041
|
|
|
1227
1042
|
/**
|