holosphere 2.0.0-alpha21 → 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 +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 +142 -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,12 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
2
|
+
* @fileoverview Federation Methods — Simplified.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* - Cross-author federation (previously "cross-federation")
|
|
4
|
+
* Federation = exchanging pubkeys.
|
|
5
|
+
* No capability tokens, no complex handshake.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* The hologram is the core infrastructure:
|
|
8
|
+
* - federate() adds a partner to the registry + stores config (no auto-hologram creation)
|
|
9
|
+
* - Reads are filtered by federation membership
|
|
10
|
+
* - Everyone can write everywhere; non-federated data is invisible
|
|
11
|
+
* - Holograms are only created explicitly via propagateData()
|
|
12
|
+
*
|
|
13
|
+
* For cryptographic privacy: encrypted lenses + key sharing via NIP-44 DMs.
|
|
14
|
+
* - shareLensKeys() shares symmetric keys with a federation partner
|
|
15
|
+
* - Only holders of the key can decrypt the data on the relay
|
|
10
16
|
*
|
|
11
17
|
* @module lib/federation-methods
|
|
12
18
|
*/
|
|
@@ -14,18 +20,16 @@
|
|
|
14
20
|
import * as registry from '../federation/registry.js';
|
|
15
21
|
import * as discovery from '../federation/discovery.js';
|
|
16
22
|
import * as federation from '../federation/hologram.js';
|
|
17
|
-
import * as storage from '../storage/
|
|
23
|
+
import * as storage from '../storage/nostr-wrapper.js';
|
|
18
24
|
import * as nostrAsync from '../storage/nostr-async.js';
|
|
19
|
-
import
|
|
20
|
-
import {
|
|
25
|
+
import { isPubkey } from '../crypto/secp256k1.js';
|
|
26
|
+
import { sendLensKeyShare } from '../federation/handshake.js';
|
|
27
|
+
import { buildLensPath } from '../crypto/key-store.js';
|
|
28
|
+
import { registerHolon } from '../federation/holon-registry.js';
|
|
21
29
|
import { ValidationError, AuthorizationError } from './errors.js';
|
|
22
30
|
|
|
23
31
|
/**
|
|
24
32
|
* Normalize a holon target (string or object) to { holonId, authorPubKey }.
|
|
25
|
-
* If the holonId is a 64-char hex pubkey, uses it as the authorPubKey.
|
|
26
|
-
* @param {string|Object} input - Holon ID string or { holonId, authorPubKey }
|
|
27
|
-
* @param {string} defaultPubKey - Default author public key (typically client.publicKey)
|
|
28
|
-
* @returns {{ holonId: string, authorPubKey: string }}
|
|
29
33
|
*/
|
|
30
34
|
function normalizeHolonTarget(input, defaultPubKey) {
|
|
31
35
|
if (typeof input === 'string') {
|
|
@@ -39,229 +43,86 @@ function normalizeHolonTarget(input, defaultPubKey) {
|
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
45
|
* Mixin that adds federation methods to a HoloSphere class.
|
|
42
|
-
* Enables unified federation with capability-based access control,
|
|
43
|
-
* hologram creation, and Nostr-based discovery protocols.
|
|
44
46
|
*
|
|
45
|
-
*
|
|
47
|
+
* SIMPLIFIED MODEL: Federation = pubkey list + holograms.
|
|
48
|
+
* No capability tokens. No complex access control.
|
|
46
49
|
*
|
|
47
50
|
* @param {Class} Base - Base class to extend
|
|
48
51
|
* @returns {Class} Extended class with federation methods
|
|
49
52
|
*/
|
|
50
53
|
export function withFederationMethods(Base) {
|
|
51
54
|
return class extends Base {
|
|
52
|
-
// === UNIFIED FEDERATION API ===
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
|
-
*
|
|
57
|
+
* Federate two holons — the core operation.
|
|
58
|
+
*
|
|
59
|
+
* This does two things:
|
|
60
|
+
* 1. Adds the partner to the federation registry (pubkey list)
|
|
61
|
+
* 2. Stores federation config
|
|
56
62
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
63
|
+
* Holograms are NOT automatically created. Use propagateData() explicitly
|
|
64
|
+
* to create holograms when needed.
|
|
59
65
|
*
|
|
60
|
-
* @param {string|Object} source - Source holon
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @param {string|Object} target - Target holon. Same format as source.
|
|
64
|
-
* @param {string} lensName - Name of the lens to federate
|
|
66
|
+
* @param {string|Object} source - Source holon
|
|
67
|
+
* @param {string|Object} target - Target holon
|
|
68
|
+
* @param {string} lensName - Lens name to federate
|
|
65
69
|
* @param {Object} [options={}] - Federation options
|
|
66
70
|
* @param {string} [options.direction='outbound'] - 'inbound', 'outbound', or 'bidirectional'
|
|
67
71
|
* @param {string} [options.mode='reference'] - 'reference' (hologram) or 'copy'
|
|
68
|
-
* @param {boolean} [options.
|
|
69
|
-
* @
|
|
70
|
-
* @param {string[]} [options.permissions=['read']] - Permissions for auto-issued capability
|
|
71
|
-
* @param {Function} [options.filter] - Filter function to select which data to federate
|
|
72
|
-
* @returns {Promise<Object>} Federation result with capability info
|
|
73
|
-
* @throws {ValidationError} If trying to federate a holon with itself
|
|
74
|
-
*
|
|
75
|
-
* @example
|
|
76
|
-
* // Same-author federation (simplified)
|
|
77
|
-
* await hs.federate('holon-a', 'holon-b', 'events');
|
|
78
|
-
*
|
|
79
|
-
* @example
|
|
80
|
-
* // Cross-author federation (explicit)
|
|
81
|
-
* await hs.federate(
|
|
82
|
-
* { holonId: 'holon-a', authorPubKey: 'abc123...' },
|
|
83
|
-
* { holonId: 'holon-b', authorPubKey: 'def456...' },
|
|
84
|
-
* 'events',
|
|
85
|
-
* { capability: preIssuedToken }
|
|
86
|
-
* );
|
|
72
|
+
* @param {boolean} [options.shareKeys=false] - Share encrypted lens keys with partner via NIP-44 DM
|
|
73
|
+
* @returns {Promise<Object>} Federation result
|
|
87
74
|
*/
|
|
88
75
|
async federate(source, target, lensName, options = {}) {
|
|
89
76
|
const {
|
|
90
77
|
direction = 'outbound',
|
|
91
78
|
mode = 'reference',
|
|
92
|
-
|
|
93
|
-
capability: providedCapability = null,
|
|
94
|
-
permissions = ['read'],
|
|
95
|
-
filter = null,
|
|
79
|
+
shareKeys = false, // Share encrypted lens keys with partner
|
|
96
80
|
} = options;
|
|
97
81
|
|
|
98
|
-
// Normalize source and target to { holonId, authorPubKey } format
|
|
99
82
|
const normalizedSource = normalizeHolonTarget(source, this.client.publicKey);
|
|
100
83
|
const normalizedTarget = normalizeHolonTarget(target, this.client.publicKey);
|
|
101
84
|
|
|
102
|
-
// Validation
|
|
103
85
|
if (normalizedSource.holonId === normalizedTarget.holonId &&
|
|
104
86
|
normalizedSource.authorPubKey === normalizedTarget.authorPubKey) {
|
|
105
87
|
throw new ValidationError('Cannot federate a holon with itself');
|
|
106
88
|
}
|
|
107
89
|
|
|
108
90
|
if (!['inbound', 'outbound', 'bidirectional'].includes(direction)) {
|
|
109
|
-
throw new ValidationError(`Invalid direction: ${direction}
|
|
91
|
+
throw new ValidationError(`Invalid direction: ${direction}`);
|
|
110
92
|
}
|
|
111
93
|
|
|
112
|
-
// Determine if this is self-federation or cross-federation
|
|
113
94
|
const isSelfFederation = normalizedSource.authorPubKey === normalizedTarget.authorPubKey;
|
|
114
95
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
? 365 * 24 * 60 * 60 * 1000 // 1 year for self
|
|
120
|
-
: 24 * 60 * 60 * 1000; // 24 hours for cross
|
|
121
|
-
|
|
122
|
-
const needsOutbound = direction === 'outbound' || direction === 'bidirectional';
|
|
123
|
-
const needsInbound = direction === 'inbound' || direction === 'bidirectional';
|
|
124
|
-
|
|
125
|
-
// Outbound capability: scoped to source holon
|
|
126
|
-
let outboundCapability = providedCapability;
|
|
127
|
-
if (!outboundCapability && needsOutbound) {
|
|
128
|
-
outboundCapability = await crypto.issueCapability(
|
|
129
|
-
permissions,
|
|
130
|
-
{ holonId: normalizedSource.holonId, lensName, dataId: '*' },
|
|
131
|
-
normalizedTarget.authorPubKey,
|
|
132
|
-
{
|
|
133
|
-
expiresIn,
|
|
134
|
-
issuer: normalizedSource.authorPubKey,
|
|
135
|
-
issuerKey: this.client.privateKey,
|
|
136
|
-
isSelfCapability: isSelfFederation,
|
|
137
|
-
}
|
|
138
|
-
);
|
|
96
|
+
// Add partner to federation list (if cross-author)
|
|
97
|
+
if (!isSelfFederation) {
|
|
98
|
+
const needsOutbound = direction === 'outbound' || direction === 'bidirectional';
|
|
99
|
+
const needsInbound = direction === 'inbound' || direction === 'bidirectional';
|
|
139
100
|
|
|
140
|
-
if (
|
|
141
|
-
await registry.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
});
|
|
146
|
-
} else {
|
|
147
|
-
await registry.storeInboundCapability(
|
|
148
|
-
this.client,
|
|
149
|
-
this.config.appName,
|
|
150
|
-
normalizedSource.authorPubKey,
|
|
151
|
-
{
|
|
152
|
-
token: outboundCapability,
|
|
153
|
-
scope: { holonId: normalizedSource.holonId, lensName },
|
|
154
|
-
permissions,
|
|
155
|
-
}
|
|
101
|
+
if (needsOutbound) {
|
|
102
|
+
await registry.addFederatedPartner(
|
|
103
|
+
this.client, this.config.appName,
|
|
104
|
+
normalizedTarget.authorPubKey,
|
|
105
|
+
{ addedVia: 'federate' }
|
|
156
106
|
);
|
|
157
107
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
inboundCapability = await crypto.issueCapability(
|
|
164
|
-
permissions,
|
|
165
|
-
{ holonId: normalizedTarget.holonId, lensName, dataId: '*' },
|
|
166
|
-
normalizedSource.authorPubKey,
|
|
167
|
-
{
|
|
168
|
-
expiresIn,
|
|
169
|
-
issuer: normalizedTarget.authorPubKey,
|
|
170
|
-
issuerKey: this.client.privateKey,
|
|
171
|
-
isSelfCapability: isSelfFederation,
|
|
172
|
-
}
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
if (isSelfFederation) {
|
|
176
|
-
await registry.storeSelfCapability(this.client, this.config.appName, {
|
|
177
|
-
token: inboundCapability,
|
|
178
|
-
scope: { holonId: normalizedTarget.holonId, lensName },
|
|
179
|
-
permissions,
|
|
180
|
-
});
|
|
181
|
-
} else {
|
|
182
|
-
await registry.storeInboundCapability(
|
|
183
|
-
this.client,
|
|
184
|
-
this.config.appName,
|
|
185
|
-
normalizedTarget.authorPubKey,
|
|
186
|
-
{
|
|
187
|
-
token: inboundCapability,
|
|
188
|
-
scope: { holonId: normalizedTarget.holonId, lensName },
|
|
189
|
-
permissions,
|
|
190
|
-
}
|
|
108
|
+
if (needsInbound) {
|
|
109
|
+
await registry.addFederatedPartner(
|
|
110
|
+
this.client, this.config.appName,
|
|
111
|
+
normalizedSource.authorPubKey,
|
|
112
|
+
{ addedVia: 'federate' }
|
|
191
113
|
);
|
|
192
114
|
}
|
|
193
115
|
}
|
|
194
116
|
|
|
195
|
-
// Setup federation based on direction
|
|
196
117
|
const results = {
|
|
197
118
|
source: normalizedSource,
|
|
198
119
|
target: normalizedTarget,
|
|
199
120
|
lensName,
|
|
200
121
|
direction,
|
|
201
122
|
mode,
|
|
202
|
-
propagate,
|
|
203
|
-
capability: outboundCapability || inboundCapability,
|
|
204
123
|
isSelfFederation,
|
|
205
|
-
propagated: { outbound: 0, inbound: 0 },
|
|
206
124
|
};
|
|
207
125
|
|
|
208
|
-
// Handle data propagation (skip if propagate: false)
|
|
209
|
-
if (propagate && needsOutbound) {
|
|
210
|
-
// Propagate existing data from source to target
|
|
211
|
-
const sourceData = await this.read(normalizedSource.holonId, lensName, null, {
|
|
212
|
-
resolveHolograms: false,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
if (sourceData && Array.isArray(sourceData)) {
|
|
216
|
-
for (const item of sourceData) {
|
|
217
|
-
if (filter && !filter(item)) continue;
|
|
218
|
-
|
|
219
|
-
await federation.propagateData(
|
|
220
|
-
this.client,
|
|
221
|
-
this.config.appName,
|
|
222
|
-
item,
|
|
223
|
-
normalizedSource.holonId,
|
|
224
|
-
normalizedTarget.holonId,
|
|
225
|
-
lensName,
|
|
226
|
-
mode,
|
|
227
|
-
{
|
|
228
|
-
sourceAuthorPubKey: normalizedSource.authorPubKey,
|
|
229
|
-
capability: outboundCapability,
|
|
230
|
-
}
|
|
231
|
-
);
|
|
232
|
-
results.propagated.outbound++;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (propagate && needsInbound) {
|
|
238
|
-
// Propagate from target to source
|
|
239
|
-
const targetData = await this.read(normalizedTarget.holonId, lensName, null, {
|
|
240
|
-
resolveHolograms: false,
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
if (targetData && Array.isArray(targetData)) {
|
|
244
|
-
for (const item of targetData) {
|
|
245
|
-
if (filter && !filter(item)) continue;
|
|
246
|
-
|
|
247
|
-
await federation.propagateData(
|
|
248
|
-
this.client,
|
|
249
|
-
this.config.appName,
|
|
250
|
-
item,
|
|
251
|
-
normalizedTarget.holonId,
|
|
252
|
-
normalizedSource.holonId,
|
|
253
|
-
lensName,
|
|
254
|
-
mode,
|
|
255
|
-
{
|
|
256
|
-
sourceAuthorPubKey: normalizedTarget.authorPubKey,
|
|
257
|
-
capability: inboundCapability,
|
|
258
|
-
}
|
|
259
|
-
);
|
|
260
|
-
results.propagated.inbound++;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
126
|
// Store federation config
|
|
266
127
|
await federation.setupFederation(
|
|
267
128
|
this.client,
|
|
@@ -272,135 +133,79 @@ export function withFederationMethods(Base) {
|
|
|
272
133
|
{ direction, mode }
|
|
273
134
|
);
|
|
274
135
|
|
|
136
|
+
// Share encrypted lens keys with partner (if requested and cross-author)
|
|
137
|
+
if (shareKeys && !isSelfFederation && this.client?.privateKey && this.keyStore) {
|
|
138
|
+
try {
|
|
139
|
+
const keyResult = await this.shareLensKeys(
|
|
140
|
+
normalizedTarget.authorPubKey,
|
|
141
|
+
[lensName],
|
|
142
|
+
{ holonId: normalizedSource.holonId }
|
|
143
|
+
);
|
|
144
|
+
results.keysShared = keyResult.shared;
|
|
145
|
+
} catch (err) {
|
|
146
|
+
results.keysShared = 0;
|
|
147
|
+
results.keyShareError = err.message;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
275
151
|
return results;
|
|
276
152
|
}
|
|
277
153
|
|
|
278
154
|
/**
|
|
279
|
-
* Unfederate
|
|
280
|
-
*
|
|
281
|
-
* Removes federation between source and target holons.
|
|
282
|
-
*
|
|
283
|
-
* @param {string|Object} source - Source holon (string or { holonId, authorPubKey })
|
|
284
|
-
* @param {string|Object} target - Target holon (string or { holonId, authorPubKey })
|
|
285
|
-
* @param {string} lensName - Lens name to unfederate
|
|
286
|
-
* @returns {Promise<boolean>} True if unfederation succeeded
|
|
155
|
+
* Unfederate — remove federation between source and target holons.
|
|
287
156
|
*/
|
|
288
157
|
async unfederate(source, target, lensName) {
|
|
289
158
|
const normalizedSource = normalizeHolonTarget(source, this.client.publicKey);
|
|
290
159
|
const normalizedTarget = normalizeHolonTarget(target, this.client.publicKey);
|
|
291
160
|
|
|
292
|
-
// Remove federation config
|
|
293
161
|
const configPath = storage.buildPath(this.config.appName, normalizedSource.holonId, lensName, '_federation');
|
|
294
162
|
try {
|
|
295
163
|
await storage.deleteData(this.client, configPath);
|
|
296
164
|
} catch (e) {
|
|
297
|
-
// Ignore
|
|
165
|
+
// Ignore — already unfederated
|
|
298
166
|
}
|
|
299
167
|
|
|
300
168
|
return true;
|
|
301
169
|
}
|
|
302
170
|
|
|
303
|
-
// ===
|
|
171
|
+
// === Federation Registry Methods ===
|
|
304
172
|
|
|
305
|
-
/**
|
|
306
|
-
* Add a federated HoloSphere partner by public key.
|
|
307
|
-
* @deprecated Use federate() with explicit authorPubKey instead
|
|
308
|
-
* @param {string} pubKey - Public key (hex) of the federated HoloSphere
|
|
309
|
-
* @param {Object} [options={}] - Federation options (metadata, capabilities, etc.)
|
|
310
|
-
* @returns {Promise<Object>} Federation registration result
|
|
311
|
-
* @throws {ValidationError} If pubKey is invalid
|
|
312
|
-
*/
|
|
313
173
|
async addFederatedHolosphere(pubKey, options = {}) {
|
|
314
174
|
if (!pubKey || typeof pubKey !== 'string') {
|
|
315
175
|
throw new ValidationError('pubKey must be a valid public key string');
|
|
316
176
|
}
|
|
317
|
-
return registry.addFederatedPartner(
|
|
318
|
-
this.client,
|
|
319
|
-
this.config.appName,
|
|
320
|
-
pubKey,
|
|
321
|
-
options
|
|
322
|
-
);
|
|
177
|
+
return registry.addFederatedPartner(this.client, this.config.appName, pubKey, options);
|
|
323
178
|
}
|
|
324
179
|
|
|
325
|
-
/**
|
|
326
|
-
* Remove a federated HoloSphere partner.
|
|
327
|
-
* @param {string} pubKey - Public key of the partner to remove
|
|
328
|
-
* @returns {Promise<void>}
|
|
329
|
-
*/
|
|
330
180
|
async removeFederatedHolosphere(pubKey) {
|
|
331
|
-
return registry.removeFederatedPartner(
|
|
332
|
-
this.client,
|
|
333
|
-
this.config.appName,
|
|
334
|
-
pubKey
|
|
335
|
-
);
|
|
181
|
+
return registry.removeFederatedPartner(this.client, this.config.appName, pubKey);
|
|
336
182
|
}
|
|
337
183
|
|
|
338
|
-
/**
|
|
339
|
-
* Get all federated HoloSphere public keys.
|
|
340
|
-
* @returns {Promise<string[]>} Array of federated public keys
|
|
341
|
-
*/
|
|
342
184
|
async getFederatedHolospheres() {
|
|
343
|
-
return registry.getFederatedAuthors(
|
|
344
|
-
this.client,
|
|
345
|
-
this.config.appName
|
|
346
|
-
);
|
|
185
|
+
return registry.getFederatedAuthors(this.client, this.config.appName);
|
|
347
186
|
}
|
|
348
187
|
|
|
349
|
-
/**
|
|
350
|
-
* Get the complete federation registry.
|
|
351
|
-
* @returns {Promise<Object>} Federation registry with all partners and capabilities
|
|
352
|
-
*/
|
|
353
188
|
async getFederationRegistry() {
|
|
354
|
-
return registry.getFederationRegistry(
|
|
355
|
-
this.client,
|
|
356
|
-
this.config.appName
|
|
357
|
-
);
|
|
189
|
+
return registry.getFederationRegistry(this.client, this.config.appName);
|
|
358
190
|
}
|
|
359
191
|
|
|
360
192
|
/**
|
|
361
|
-
*
|
|
362
|
-
* @param {string}
|
|
363
|
-
* @
|
|
364
|
-
* @returns {Promise<void>}
|
|
193
|
+
* Check if a pubkey is in this holosphere's federation.
|
|
194
|
+
* @param {string} pubKey - Public key to check
|
|
195
|
+
* @returns {Promise<boolean>}
|
|
365
196
|
*/
|
|
366
|
-
async
|
|
367
|
-
return registry.
|
|
368
|
-
this.client,
|
|
369
|
-
this.config.appName,
|
|
370
|
-
partnerPubKey,
|
|
371
|
-
capabilityInfo
|
|
372
|
-
);
|
|
197
|
+
async isFederated(pubKey) {
|
|
198
|
+
return registry.isFederated(this.client, this.config.appName, pubKey);
|
|
373
199
|
}
|
|
374
200
|
|
|
375
201
|
/**
|
|
376
|
-
* Read data from a federated source
|
|
377
|
-
*
|
|
378
|
-
* @param {string} holonId - Holon ID to read from
|
|
379
|
-
* @param {string} lensName - Lens name to read from
|
|
380
|
-
* @param {string|null} [dataId=null] - Optional specific data ID (null for all)
|
|
381
|
-
* @returns {Promise<Object|Object[]>} Data from federated source
|
|
382
|
-
* @throws {AuthorizationError} If capability verification fails
|
|
202
|
+
* Read data from a federated source.
|
|
203
|
+
* Simplified: just checks federation membership, then reads with author filter.
|
|
383
204
|
*/
|
|
384
205
|
async readFromFederatedSource(sourcePubKey, holonId, lensName, dataId = null) {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
sourcePubKey,
|
|
389
|
-
{ holonId, lensName, dataId }
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
if (!capabilityEntry) {
|
|
393
|
-
throw new AuthorizationError('No valid capability for federated source', 'read');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
const isValid = await this.verifyCapability(
|
|
397
|
-
capabilityEntry.token,
|
|
398
|
-
'read',
|
|
399
|
-
{ holonId, lensName, dataId }
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
if (!isValid) {
|
|
403
|
-
throw new AuthorizationError('Capability verification failed', 'read');
|
|
206
|
+
const federated = await this.isFederated(sourcePubKey);
|
|
207
|
+
if (!federated) {
|
|
208
|
+
throw new AuthorizationError('Source is not in the federation', 'read');
|
|
404
209
|
}
|
|
405
210
|
|
|
406
211
|
if (dataId) {
|
|
@@ -419,544 +224,137 @@ export function withFederationMethods(Base) {
|
|
|
419
224
|
}
|
|
420
225
|
|
|
421
226
|
/**
|
|
422
|
-
* Create a cross-
|
|
423
|
-
* Links data from another HoloSphere into this one using capability-based access.
|
|
424
|
-
* @param {string} sourcePubKey - Source HoloSphere public key
|
|
425
|
-
* @param {string} sourceHolon - Source holon ID
|
|
426
|
-
* @param {string} lensName - Lens name
|
|
427
|
-
* @param {string} dataId - Data ID
|
|
428
|
-
* @param {string} targetHolon - Target holon in this HoloSphere
|
|
429
|
-
* @param {Object} [options={}] - Hologram options
|
|
430
|
-
* @param {boolean} [options.embedCapability=true] - Embed capability in hologram
|
|
431
|
-
* @returns {Promise<Object>} Created hologram
|
|
432
|
-
* @throws {AuthorizationError} If no valid capability exists
|
|
227
|
+
* Create a cross-holosphere hologram reference.
|
|
433
228
|
*/
|
|
434
|
-
async createCrossHolosphereHologram(sourcePubKey, sourceHolon, lensName, dataId, targetHolon,
|
|
435
|
-
const { embedCapability = true } = options;
|
|
436
|
-
|
|
437
|
-
const capabilityEntry = await registry.getCapabilityForAuthor(
|
|
438
|
-
this.client,
|
|
439
|
-
this.config.appName,
|
|
440
|
-
sourcePubKey,
|
|
441
|
-
{ holonId: sourceHolon, lensName, dataId }
|
|
442
|
-
);
|
|
443
|
-
|
|
444
|
-
if (!capabilityEntry) {
|
|
445
|
-
throw new AuthorizationError('No valid capability for cross-holosphere hologram', 'read');
|
|
446
|
-
}
|
|
447
|
-
|
|
229
|
+
async createCrossHolosphereHologram(sourcePubKey, sourceHolon, lensName, dataId, targetHolon, _options = {}) {
|
|
448
230
|
const hologram = federation.createHologram(
|
|
449
|
-
sourceHolon,
|
|
450
|
-
targetHolon,
|
|
451
|
-
lensName,
|
|
452
|
-
dataId,
|
|
231
|
+
sourceHolon, targetHolon, lensName, dataId,
|
|
453
232
|
this.config.appName,
|
|
454
|
-
{
|
|
455
|
-
authorPubKey: sourcePubKey,
|
|
456
|
-
capability: embedCapability ? capabilityEntry.token : null,
|
|
457
|
-
}
|
|
233
|
+
{ authorPubKey: sourcePubKey }
|
|
458
234
|
);
|
|
459
235
|
|
|
460
236
|
const targetPath = storage.buildPath(this.config.appName, targetHolon, lensName, dataId);
|
|
461
237
|
await storage.write(this.client, targetPath, hologram);
|
|
462
|
-
|
|
463
238
|
return hologram;
|
|
464
239
|
}
|
|
465
240
|
|
|
466
|
-
/**
|
|
467
|
-
* Issue a capability token for federated access.
|
|
468
|
-
* Grants another HoloSphere permission to access data with specified scope.
|
|
469
|
-
* @param {string} targetPubKey - Target HoloSphere public key to grant access to
|
|
470
|
-
* @param {Object} scope - Access scope (holonId, lensName, dataId patterns)
|
|
471
|
-
* @param {string[]} permissions - Array of permissions ('read', 'write', etc.)
|
|
472
|
-
* @param {Object} [options={}] - Capability options
|
|
473
|
-
* @param {number} [options.expiresIn=3600000] - Expiration time in milliseconds
|
|
474
|
-
* @param {boolean} [options.trackInRegistry=true] - Store in federation registry
|
|
475
|
-
* @returns {Promise<string>} Capability token
|
|
476
|
-
*/
|
|
477
|
-
async issueCapabilityForFederation(targetPubKey, scope, permissions, options = {}) {
|
|
478
|
-
const { expiresIn = 3600000, trackInRegistry = true } = options;
|
|
479
|
-
|
|
480
|
-
const token = await crypto.issueCapability(
|
|
481
|
-
permissions,
|
|
482
|
-
scope,
|
|
483
|
-
targetPubKey,
|
|
484
|
-
{
|
|
485
|
-
expiresIn,
|
|
486
|
-
issuerKey: this.client.privateKey,
|
|
487
|
-
}
|
|
488
|
-
);
|
|
489
|
-
|
|
490
|
-
if (trackInRegistry) {
|
|
491
|
-
const partner = await registry.getFederatedPartner(
|
|
492
|
-
this.client,
|
|
493
|
-
this.config.appName,
|
|
494
|
-
targetPubKey
|
|
495
|
-
);
|
|
496
|
-
|
|
497
|
-
if (!partner) {
|
|
498
|
-
await registry.addFederatedPartner(
|
|
499
|
-
this.client,
|
|
500
|
-
this.config.appName,
|
|
501
|
-
targetPubKey,
|
|
502
|
-
{ addedVia: 'capability_issued' }
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
await registry.storeOutboundCapability(
|
|
507
|
-
this.client,
|
|
508
|
-
this.config.appName,
|
|
509
|
-
targetPubKey,
|
|
510
|
-
{
|
|
511
|
-
tokenHash: await this._hashToken(token),
|
|
512
|
-
scope,
|
|
513
|
-
permissions,
|
|
514
|
-
expires: Date.now() + expiresIn,
|
|
515
|
-
}
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
return token;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Hash a capability token for storage.
|
|
524
|
-
* @private
|
|
525
|
-
* @param {string} token - Capability token to hash
|
|
526
|
-
* @returns {string} SHA256 hash of token
|
|
527
|
-
*/
|
|
528
|
-
_hashToken(token) {
|
|
529
|
-
return hashToken(token);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
241
|
// === Nostr Discovery Protocol ===
|
|
533
242
|
|
|
534
|
-
/**
|
|
535
|
-
* Send a federation request to another HoloSphere via Nostr.
|
|
536
|
-
* @param {string} targetPubKey - Target HoloSphere public key
|
|
537
|
-
* @param {Object} [options={}] - Request options (message, metadata, etc.)
|
|
538
|
-
* @returns {Promise<Object>} Request event
|
|
539
|
-
*/
|
|
540
243
|
async sendFederationRequest(targetPubKey, options = {}) {
|
|
541
|
-
return discovery.sendFederationRequest(
|
|
542
|
-
this.client,
|
|
543
|
-
this.config.appName,
|
|
544
|
-
targetPubKey,
|
|
545
|
-
options
|
|
546
|
-
);
|
|
244
|
+
return discovery.sendFederationRequest(this.client, this.config.appName, targetPubKey, options);
|
|
547
245
|
}
|
|
548
246
|
|
|
549
|
-
/**
|
|
550
|
-
* Subscribe to incoming federation requests.
|
|
551
|
-
* @param {Function} callback - Callback function (request) => void
|
|
552
|
-
* @returns {Promise<Object>} Subscription object with unsubscribe method
|
|
553
|
-
*/
|
|
554
247
|
async subscribeFederationRequests(callback) {
|
|
555
248
|
return discovery.subscribeFederationRequests(this.client, callback);
|
|
556
249
|
}
|
|
557
250
|
|
|
558
|
-
/**
|
|
559
|
-
* Accept a federation request and establish partnership.
|
|
560
|
-
* @param {Object} request - Federation request object
|
|
561
|
-
* @param {Object} [options={}] - Acceptance options (capabilities to grant, etc.)
|
|
562
|
-
* @returns {Promise<Object>} Acceptance event
|
|
563
|
-
*/
|
|
564
251
|
async acceptFederationRequest(request, options = {}) {
|
|
565
|
-
return discovery.acceptFederationRequest(
|
|
566
|
-
this.client,
|
|
567
|
-
this.config.appName,
|
|
568
|
-
request,
|
|
569
|
-
options
|
|
570
|
-
);
|
|
252
|
+
return discovery.acceptFederationRequest(this.client, this.config.appName, request, options);
|
|
571
253
|
}
|
|
572
254
|
|
|
573
|
-
/**
|
|
574
|
-
* Decline a federation request.
|
|
575
|
-
* @param {Object} request - Federation request object
|
|
576
|
-
* @param {string} [reason=''] - Optional reason for declining
|
|
577
|
-
* @returns {Promise<Object>} Decline event
|
|
578
|
-
*/
|
|
579
255
|
async declineFederationRequest(request, reason = '') {
|
|
580
|
-
return discovery.declineFederationRequest(
|
|
581
|
-
this.client,
|
|
582
|
-
this.config.appName,
|
|
583
|
-
request,
|
|
584
|
-
reason
|
|
585
|
-
);
|
|
256
|
+
return discovery.declineFederationRequest(this.client, this.config.appName, request, reason);
|
|
586
257
|
}
|
|
587
258
|
|
|
588
|
-
/**
|
|
589
|
-
* Subscribe to federation request acceptances.
|
|
590
|
-
* @param {Function} callback - Callback function (acceptance) => void
|
|
591
|
-
* @returns {Promise<Object>} Subscription object with unsubscribe method
|
|
592
|
-
*/
|
|
593
259
|
async subscribeFederationAcceptances(callback) {
|
|
594
|
-
return discovery.subscribeFederationAcceptances(
|
|
595
|
-
this.client,
|
|
596
|
-
this.config.appName,
|
|
597
|
-
callback
|
|
598
|
-
);
|
|
260
|
+
return discovery.subscribeFederationAcceptances(this.client, this.config.appName, callback);
|
|
599
261
|
}
|
|
600
262
|
|
|
601
|
-
/**
|
|
602
|
-
* Subscribe to federation request declines.
|
|
603
|
-
* @param {Function} callback - Callback function (decline) => void
|
|
604
|
-
* @returns {Promise<Object>} Subscription object with unsubscribe method
|
|
605
|
-
*/
|
|
606
263
|
async subscribeFederationDeclines(callback) {
|
|
607
264
|
return discovery.subscribeFederationDeclines(this.client, callback);
|
|
608
265
|
}
|
|
609
266
|
|
|
610
|
-
/**
|
|
611
|
-
* Get all pending federation requests.
|
|
612
|
-
* @param {Object} [options={}] - Query options (limit, since, etc.)
|
|
613
|
-
* @returns {Promise<Object[]>} Array of pending requests
|
|
614
|
-
*/
|
|
615
267
|
async getPendingFederationRequests(options = {}) {
|
|
616
268
|
return discovery.getPendingFederationRequests(this.client, options);
|
|
617
269
|
}
|
|
618
270
|
|
|
619
|
-
// ===
|
|
271
|
+
// === Encrypted Federation (Key Sharing via NIP-44 DMs) ===
|
|
620
272
|
|
|
621
273
|
/**
|
|
622
|
-
*
|
|
623
|
-
* This creates holograms pointing to the partner's data, allowing you to see
|
|
624
|
-
* their data in your holon while the source of truth remains with the partner.
|
|
274
|
+
* Share lens encryption keys with a federation partner.
|
|
625
275
|
*
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
629
|
-
*
|
|
630
|
-
*
|
|
631
|
-
* @param {Function} [options.filter] - Optional filter function to select which items to receive
|
|
632
|
-
* @param {boolean} [options.overwrite=false] - Whether to overwrite existing holograms
|
|
633
|
-
* @returns {Promise<Object>} Result with count of received holograms and any errors
|
|
634
|
-
* @throws {AuthorizationError} If no valid capability exists for the source
|
|
276
|
+
* This is how you make your data readable ONLY by your federation:
|
|
277
|
+
* 1. Your lenses are encrypted with symmetric keys (AES-256-GCM)
|
|
278
|
+
* 2. This method wraps each key for the partner using NIP-44
|
|
279
|
+
* 3. Sends the wrapped key via encrypted DM (kind 4)
|
|
280
|
+
* 4. Partner unwraps with their private key → can now decrypt your data
|
|
635
281
|
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
638
|
-
*
|
|
639
|
-
*
|
|
640
|
-
*
|
|
641
|
-
*
|
|
642
|
-
*
|
|
643
|
-
* );
|
|
644
|
-
* console.log(`Received ${result.received} holograms`);
|
|
282
|
+
* Non-federation members see encrypted blobs they can't decrypt.
|
|
283
|
+
*
|
|
284
|
+
* @param {string} partnerPubKey - Partner's public key
|
|
285
|
+
* @param {string[]} [lensNames] - Specific lenses to share (default: all encrypted lenses)
|
|
286
|
+
* @param {Object} [options={}] - Options
|
|
287
|
+
* @param {string} [options.holonId] - Specific holon (default: client.publicKey)
|
|
288
|
+
* @returns {Promise<Object>} { shared: number, failed: number, lenses: string[] }
|
|
645
289
|
*/
|
|
646
|
-
async
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
// Get capability for accessing partner's data
|
|
650
|
-
const capabilityEntry = await registry.getCapabilityForAuthor(
|
|
651
|
-
this.client,
|
|
652
|
-
this.config.appName,
|
|
653
|
-
partnerPubKey,
|
|
654
|
-
{ holonId: sourceHolonId, lensName, dataId: '*' }
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
if (!capabilityEntry) {
|
|
658
|
-
throw new AuthorizationError(
|
|
659
|
-
`No valid capability to access ${sourceHolonId}/${lensName} from ${partnerPubKey}`,
|
|
660
|
-
'read'
|
|
661
|
-
);
|
|
290
|
+
async shareLensKeys(partnerPubKey, lensNames = null, options = {}) {
|
|
291
|
+
if (!this.client?.privateKey) {
|
|
292
|
+
throw new ValidationError('Private key required to share lens keys');
|
|
662
293
|
}
|
|
663
294
|
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
const partnerData = await nostrAsync.nostrGetAll(this.client, sourcePath, 30000, {
|
|
667
|
-
authors: [partnerPubKey],
|
|
668
|
-
includeAuthor: true,
|
|
669
|
-
});
|
|
295
|
+
const holonId = options.holonId || this.client.publicKey;
|
|
296
|
+
const result = { shared: 0, failed: 0, lenses: [] };
|
|
670
297
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
}
|
|
298
|
+
// Get all own keys from the key store
|
|
299
|
+
const ownKeys = this.keyStore.ownKeys;
|
|
674
300
|
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
// Apply filter if provided
|
|
681
|
-
if (filter && !filter(item)) {
|
|
682
|
-
result.skipped++;
|
|
683
|
-
continue;
|
|
301
|
+
for (const [lensPath, keyData] of ownKeys) {
|
|
302
|
+
// If specific lenses requested, filter
|
|
303
|
+
if (lensNames) {
|
|
304
|
+
const matchesLens = lensNames.some(name => lensPath.includes(`/${name}`));
|
|
305
|
+
if (!matchesLens) continue;
|
|
684
306
|
}
|
|
685
307
|
|
|
686
|
-
//
|
|
687
|
-
if (!
|
|
688
|
-
|
|
689
|
-
const existing = await storage.read(this.client, existingPath);
|
|
690
|
-
if (existing && existing.hologram) {
|
|
691
|
-
result.skipped++;
|
|
692
|
-
continue;
|
|
693
|
-
}
|
|
308
|
+
// If specific holonId, filter
|
|
309
|
+
if (holonId && !lensPath.includes(`/${holonId}/`) && !lensPath.startsWith(`${this.config.appName}/${holonId}/`)) {
|
|
310
|
+
continue;
|
|
694
311
|
}
|
|
695
312
|
|
|
696
313
|
try {
|
|
697
|
-
//
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
lensName,
|
|
703
|
-
item.id,
|
|
704
|
-
this.config.appName,
|
|
705
|
-
{
|
|
706
|
-
sourceAuthorPubKey: partnerPubKey,
|
|
707
|
-
targetAuthorPubKey: this.client.publicKey,
|
|
708
|
-
capability: capabilityEntry.token,
|
|
709
|
-
permissions: ['read'],
|
|
710
|
-
}
|
|
314
|
+
// Wrap the key for this partner
|
|
315
|
+
const wrappedKey = this.keyStore.wrapKeyForPartner(
|
|
316
|
+
lensPath,
|
|
317
|
+
this.client.privateKey,
|
|
318
|
+
partnerPubKey
|
|
711
319
|
);
|
|
712
320
|
|
|
713
|
-
|
|
714
|
-
await storage.write(this.client, targetPath, hologram);
|
|
715
|
-
result.received++;
|
|
716
|
-
} catch (error) {
|
|
717
|
-
result.errors.push({ id: item.id, error: error.message });
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
return result;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
/**
|
|
725
|
-
* Subscribe to a federated partner's lens for real-time hologram updates.
|
|
726
|
-
* When the partner writes new data, holograms are automatically created in your holon.
|
|
727
|
-
*
|
|
728
|
-
* @param {string} partnerPubKey - Partner's public key
|
|
729
|
-
* @param {string} sourceHolonId - Partner's holon ID to subscribe to
|
|
730
|
-
* @param {string} lensName - Lens name to subscribe to
|
|
731
|
-
* @param {string} targetHolonId - Your holon ID where holograms will be created
|
|
732
|
-
* @param {Object} [options={}] - Subscription options
|
|
733
|
-
* @param {Function} [options.onHologram] - Callback when a new hologram is created (hologram, sourceData) => void
|
|
734
|
-
* @param {Function} [options.onError] - Callback for errors (error) => void
|
|
735
|
-
* @param {Function} [options.filter] - Optional filter function to select which items to receive
|
|
736
|
-
* @returns {Promise<Object>} Subscription object with unsubscribe() method
|
|
737
|
-
*
|
|
738
|
-
* @example
|
|
739
|
-
* // Subscribe to partner's events lens
|
|
740
|
-
* const sub = await hs.subscribeFederatedLens(
|
|
741
|
-
* 'partner-pubkey',
|
|
742
|
-
* 'partner-holon-id',
|
|
743
|
-
* 'events',
|
|
744
|
-
* 'my-holon-id',
|
|
745
|
-
* {
|
|
746
|
-
* onHologram: (hologram, data) => {
|
|
747
|
-
* console.log(`New hologram received: ${data.id}`);
|
|
748
|
-
* }
|
|
749
|
-
* }
|
|
750
|
-
* );
|
|
751
|
-
*
|
|
752
|
-
* // Later, unsubscribe
|
|
753
|
-
* sub.unsubscribe();
|
|
754
|
-
*/
|
|
755
|
-
async subscribeFederatedLens(partnerPubKey, sourceHolonId, lensName, targetHolonId, options = {}) {
|
|
756
|
-
const { onHologram, onError, filter = null } = options;
|
|
757
|
-
|
|
758
|
-
// Get capability for accessing partner's data
|
|
759
|
-
const capabilityEntry = await registry.getCapabilityForAuthor(
|
|
760
|
-
this.client,
|
|
761
|
-
this.config.appName,
|
|
762
|
-
partnerPubKey,
|
|
763
|
-
{ holonId: sourceHolonId, lensName, dataId: '*' }
|
|
764
|
-
);
|
|
765
|
-
|
|
766
|
-
if (!capabilityEntry) {
|
|
767
|
-
throw new AuthorizationError(
|
|
768
|
-
`No valid capability to subscribe to ${sourceHolonId}/${lensName} from ${partnerPubKey}`,
|
|
769
|
-
'read'
|
|
770
|
-
);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
// Track processed items to avoid duplicates
|
|
774
|
-
const processedIds = new Set();
|
|
775
|
-
|
|
776
|
-
// Subscribe to partner's lens path
|
|
777
|
-
const sourcePath = storage.buildPath(this.config.appName, sourceHolonId, lensName);
|
|
778
|
-
|
|
779
|
-
const handleEvent = async (data) => {
|
|
780
|
-
if (!data || !data.id) return;
|
|
781
|
-
|
|
782
|
-
// Skip if already processed
|
|
783
|
-
if (processedIds.has(data.id)) return;
|
|
784
|
-
processedIds.add(data.id);
|
|
785
|
-
|
|
786
|
-
// Apply filter if provided
|
|
787
|
-
if (filter && !filter(data)) return;
|
|
321
|
+
if (!wrappedKey) continue;
|
|
788
322
|
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const existingPath = storage.buildPath(this.config.appName, targetHolonId, lensName, data.id);
|
|
792
|
-
const existing = await storage.read(this.client, existingPath);
|
|
793
|
-
|
|
794
|
-
if (existing && existing.hologram) {
|
|
795
|
-
// Hologram already exists, skip
|
|
796
|
-
return;
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// Create hologram in target holon
|
|
800
|
-
const hologram = await federation.createHologramWithCapability(
|
|
323
|
+
// Send via NIP-44 encrypted DM
|
|
324
|
+
await sendLensKeyShare(
|
|
801
325
|
this.client,
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
data.id,
|
|
806
|
-
this.config.appName,
|
|
807
|
-
{
|
|
808
|
-
sourceAuthorPubKey: partnerPubKey,
|
|
809
|
-
targetAuthorPubKey: this.client.publicKey,
|
|
810
|
-
capability: capabilityEntry.token,
|
|
811
|
-
permissions: ['read'],
|
|
812
|
-
}
|
|
326
|
+
this.client.privateKey,
|
|
327
|
+
partnerPubKey,
|
|
328
|
+
{ lensPath, wrappedKey }
|
|
813
329
|
);
|
|
814
330
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (onHologram) {
|
|
819
|
-
onHologram(hologram, data);
|
|
820
|
-
}
|
|
331
|
+
result.shared++;
|
|
332
|
+
result.lenses.push(lensPath);
|
|
821
333
|
} catch (error) {
|
|
822
|
-
|
|
823
|
-
onError(error);
|
|
824
|
-
}
|
|
334
|
+
result.failed++;
|
|
825
335
|
}
|
|
826
|
-
};
|
|
827
|
-
|
|
828
|
-
// Subscribe using nostr-async subscription with author filter
|
|
829
|
-
const subscription = await nostrAsync.nostrSubscribe(
|
|
830
|
-
this.client,
|
|
831
|
-
sourcePath,
|
|
832
|
-
handleEvent,
|
|
833
|
-
30000,
|
|
834
|
-
{ authors: [partnerPubKey] }
|
|
835
|
-
);
|
|
836
|
-
|
|
837
|
-
return {
|
|
838
|
-
unsubscribe: () => {
|
|
839
|
-
if (subscription && subscription.unsubscribe) {
|
|
840
|
-
subscription.unsubscribe();
|
|
841
|
-
}
|
|
842
|
-
processedIds.clear();
|
|
843
|
-
},
|
|
844
|
-
processedCount: () => processedIds.size,
|
|
845
|
-
};
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
/**
|
|
849
|
-
* Sync all federated lenses for a holon.
|
|
850
|
-
* Receives holograms from all federated partners for their configured inbound lenses.
|
|
851
|
-
*
|
|
852
|
-
* @param {string} holonId - Your holon ID
|
|
853
|
-
* @param {Object} [options={}] - Sync options
|
|
854
|
-
* @param {boolean} [options.overwrite=false] - Whether to overwrite existing holograms
|
|
855
|
-
* @returns {Promise<Object>} Sync results per partner
|
|
856
|
-
*
|
|
857
|
-
* @example
|
|
858
|
-
* // Sync all federated data for a holon
|
|
859
|
-
* const results = await hs.syncFederatedLenses('my-holon-id');
|
|
860
|
-
* console.log(results);
|
|
861
|
-
*/
|
|
862
|
-
async syncFederatedLenses(holonId, options = {}) {
|
|
863
|
-
const { overwrite = false } = options;
|
|
864
|
-
|
|
865
|
-
// Get federation configuration for this holon
|
|
866
|
-
const federationData = await this.readGlobal('federation', holonId);
|
|
867
|
-
if (!federationData) {
|
|
868
|
-
return { error: 'No federation configuration found for this holon' };
|
|
869
336
|
}
|
|
870
337
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
// Get all federated partners
|
|
874
|
-
const partners = await this.getFederatedHolospheres();
|
|
875
|
-
|
|
876
|
-
for (const partnerPubKey of partners) {
|
|
877
|
-
results[partnerPubKey] = { lenses: {}, errors: [] };
|
|
878
|
-
|
|
879
|
-
// Get partner's lens configuration
|
|
880
|
-
const partnerLensConfig = federationData.lensConfig?.[partnerPubKey];
|
|
881
|
-
const inboundLenses = partnerLensConfig?.inbound || [];
|
|
882
|
-
|
|
883
|
-
for (const lensName of inboundLenses) {
|
|
884
|
-
try {
|
|
885
|
-
// For cross-holosphere federation, the sourceHolonId would be the partner's holon
|
|
886
|
-
// We need to determine the partner's holon ID - this might be stored in the federation config
|
|
887
|
-
const partnerHolonId = partnerLensConfig?.holonId || holonId;
|
|
888
|
-
|
|
889
|
-
const result = await this.receiveFederatedLens(
|
|
890
|
-
partnerPubKey,
|
|
891
|
-
partnerHolonId,
|
|
892
|
-
lensName,
|
|
893
|
-
holonId,
|
|
894
|
-
{ overwrite }
|
|
895
|
-
);
|
|
896
|
-
|
|
897
|
-
results[partnerPubKey].lenses[lensName] = result;
|
|
898
|
-
} catch (error) {
|
|
899
|
-
results[partnerPubKey].errors.push({
|
|
900
|
-
lens: lensName,
|
|
901
|
-
error: error.message,
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
return results;
|
|
338
|
+
return result;
|
|
908
339
|
}
|
|
909
340
|
|
|
910
|
-
// ===
|
|
341
|
+
// === Access Control (Simplified) ===
|
|
911
342
|
|
|
912
343
|
/**
|
|
913
|
-
* Unified access
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
* @param {string} targetHolonId - The holon being accessed
|
|
918
|
-
* @param {string} lensName - The lens being accessed
|
|
919
|
-
* @param {string} actorPubKey - The actor's public key
|
|
920
|
-
* @param {string} permission - Permission to check ('read' or 'write')
|
|
921
|
-
* @param {Object} [options={}] - Options
|
|
922
|
-
* @param {string} [options.actingAsHolon] - The holon the actor is acting on behalf of
|
|
923
|
-
* @param {string} [options.capabilityToken] - Explicit capability token to verify
|
|
924
|
-
* @returns {Promise<Object>} { allowed: boolean, via: string, reason?: string, grant?: Object }
|
|
925
|
-
*
|
|
926
|
-
* @example
|
|
927
|
-
* // Check if current user can read from a holon
|
|
928
|
-
* const result = await hs.canAccess(targetHolonId, 'quests', myPubKey, 'read');
|
|
929
|
-
* if (result.allowed) {
|
|
930
|
-
* console.log('Access granted via:', result.via);
|
|
931
|
-
* }
|
|
932
|
-
*
|
|
933
|
-
* @example
|
|
934
|
-
* // Check write access with explicit capability
|
|
935
|
-
* const result = await hs.canAccess(targetHolonId, 'quests', myPubKey, 'write', {
|
|
936
|
-
* capabilityToken: myCapToken
|
|
937
|
-
* });
|
|
344
|
+
* Unified access check.
|
|
345
|
+
* Simplified: owner always has access; federated partners always have access.
|
|
346
|
+
* No capability tokens needed.
|
|
938
347
|
*/
|
|
939
348
|
async canAccess(targetHolonId, lensName, actorPubKey, permission, options = {}) {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
// 1. Owner check - actor owns the target holon
|
|
349
|
+
// 1. Owner check
|
|
943
350
|
if (actorPubKey === targetHolonId) {
|
|
944
351
|
return { allowed: true, via: 'owner' };
|
|
945
352
|
}
|
|
946
353
|
|
|
947
|
-
// 2.
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
holonId: targetHolonId,
|
|
952
|
-
lensName,
|
|
953
|
-
});
|
|
954
|
-
if (valid) {
|
|
955
|
-
return { allowed: true, via: 'capability' };
|
|
956
|
-
}
|
|
957
|
-
} catch (err) {
|
|
958
|
-
console.log('[canAccess] Capability verification failed:', err.message);
|
|
959
|
-
}
|
|
354
|
+
// 2. Federation check
|
|
355
|
+
const federated = await this.isFederated(actorPubKey);
|
|
356
|
+
if (federated) {
|
|
357
|
+
return { allowed: true, via: 'federation' };
|
|
960
358
|
}
|
|
961
359
|
|
|
962
360
|
// 3. Membership in target holon
|
|
@@ -964,248 +362,311 @@ export function withFederationMethods(Base) {
|
|
|
964
362
|
const members = await this.get(targetHolonId, 'users');
|
|
965
363
|
if (members && Array.isArray(members)) {
|
|
966
364
|
const isMember = members.some(m =>
|
|
967
|
-
m.pubKey === actorPubKey ||
|
|
968
|
-
m.id === actorPubKey ||
|
|
969
|
-
m.nostrPubKey === actorPubKey
|
|
365
|
+
m.pubKey === actorPubKey || m.id === actorPubKey || m.nostrPubKey === actorPubKey
|
|
970
366
|
);
|
|
971
367
|
if (isMember) {
|
|
972
368
|
return { allowed: true, via: 'membership' };
|
|
973
369
|
}
|
|
974
370
|
}
|
|
975
|
-
} catch (
|
|
976
|
-
// Users lens might not exist
|
|
977
|
-
console.log('[canAccess] Could not read users lens:', err.message);
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
// 4. Access grant via federation (unified check)
|
|
981
|
-
// First check using the new unified findAccessGrant
|
|
982
|
-
const scope = { holonId: targetHolonId, lensName };
|
|
983
|
-
|
|
984
|
-
// Check if actingAsHolon is specified
|
|
985
|
-
if (actingAsHolon) {
|
|
986
|
-
// Verify the actor is a member of the acting holon
|
|
987
|
-
const actorHolons = await this.getHolonsForPubKey(actorPubKey);
|
|
988
|
-
const isMemberOfActingHolon = actorHolons.some(h => h.id === actingAsHolon);
|
|
989
|
-
|
|
990
|
-
if (!isMemberOfActingHolon) {
|
|
991
|
-
return { allowed: false, via: 'none', reason: 'Not a member of the acting holon' };
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// Check if actingAsHolon has an access grant
|
|
995
|
-
const grant = await registry.findAccessGrant(
|
|
996
|
-
this.client,
|
|
997
|
-
this.config.appName,
|
|
998
|
-
actingAsHolon,
|
|
999
|
-
scope,
|
|
1000
|
-
permission,
|
|
1001
|
-
{ direction: 'inbound' }
|
|
1002
|
-
);
|
|
1003
|
-
|
|
1004
|
-
if (grant) {
|
|
1005
|
-
return {
|
|
1006
|
-
allowed: true,
|
|
1007
|
-
via: 'federation',
|
|
1008
|
-
grant,
|
|
1009
|
-
reason: `Acting as ${actingAsHolon} with ${permission} grant`,
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
} else {
|
|
1013
|
-
// Check all holons the actor is a member of
|
|
1014
|
-
const actorHolons = await this.getHolonsForPubKey(actorPubKey);
|
|
1015
|
-
|
|
1016
|
-
for (const holon of actorHolons) {
|
|
1017
|
-
const grant = await registry.findAccessGrant(
|
|
1018
|
-
this.client,
|
|
1019
|
-
this.config.appName,
|
|
1020
|
-
holon.id,
|
|
1021
|
-
scope,
|
|
1022
|
-
permission,
|
|
1023
|
-
{ direction: 'inbound' }
|
|
1024
|
-
);
|
|
1025
|
-
|
|
1026
|
-
if (grant) {
|
|
1027
|
-
return {
|
|
1028
|
-
allowed: true,
|
|
1029
|
-
via: 'federation',
|
|
1030
|
-
grant,
|
|
1031
|
-
reason: `Member of ${holon.name || holon.id} which has ${permission} grant`,
|
|
1032
|
-
viaHolon: holon.id,
|
|
1033
|
-
};
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
371
|
+
} catch (_err) {
|
|
372
|
+
// Users lens might not exist
|
|
1036
373
|
}
|
|
1037
374
|
|
|
1038
375
|
return { allowed: false, via: 'none', reason: 'No access' };
|
|
1039
376
|
}
|
|
1040
377
|
|
|
1041
|
-
// === CROSS-HOLON WRITE ACCESS ===
|
|
1042
|
-
|
|
1043
378
|
/**
|
|
1044
|
-
* Check
|
|
1045
|
-
* This is a backward-compatible wrapper that delegates to canAccess().
|
|
1046
|
-
*
|
|
1047
|
-
* @param {string} targetHolonId - The holon being written to
|
|
1048
|
-
* @param {string} lensName - The lens being written to
|
|
1049
|
-
* @param {string} writerPubKey - The writer's public key
|
|
1050
|
-
* @param {Object} [options={}] - Options
|
|
1051
|
-
* @param {string} [options.actingAsHolon] - The holon the writer is acting on behalf of
|
|
1052
|
-
* @param {string} [options.capabilityToken] - Explicit capability token to verify
|
|
1053
|
-
* @returns {Promise<Object>} { canWrite: boolean, reason: string, accessType: string }
|
|
1054
|
-
*
|
|
1055
|
-
* @example
|
|
1056
|
-
* // Check if current user can write to another holon
|
|
1057
|
-
* const result = await hs.canWrite(targetHolonId, 'quests', myPubKey);
|
|
1058
|
-
* if (result.canWrite) {
|
|
1059
|
-
* console.log('Access granted:', result.accessType);
|
|
1060
|
-
* }
|
|
379
|
+
* Check write access (delegates to canAccess).
|
|
1061
380
|
*/
|
|
1062
381
|
async canWrite(targetHolonId, lensName, writerPubKey, options = {}) {
|
|
1063
|
-
// Delegate to unified canAccess method
|
|
1064
382
|
const result = await this.canAccess(targetHolonId, lensName, writerPubKey, 'write', options);
|
|
1065
|
-
|
|
1066
|
-
// Convert to legacy format for backward compatibility
|
|
1067
383
|
return {
|
|
1068
384
|
canWrite: result.allowed,
|
|
1069
385
|
reason: result.reason || result.via,
|
|
1070
386
|
accessType: result.via,
|
|
1071
|
-
viaHolon: result.viaHolon,
|
|
1072
|
-
grant: result.grant,
|
|
1073
387
|
};
|
|
1074
388
|
}
|
|
1075
389
|
|
|
1076
390
|
/**
|
|
1077
|
-
* Check
|
|
1078
|
-
* Convenience wrapper around canAccess() for read operations.
|
|
1079
|
-
*
|
|
1080
|
-
* @param {string} targetHolonId - The holon being read from
|
|
1081
|
-
* @param {string} lensName - The lens being read from
|
|
1082
|
-
* @param {string} readerPubKey - The reader's public key
|
|
1083
|
-
* @param {Object} [options={}] - Options
|
|
1084
|
-
* @param {string} [options.actingAsHolon] - The holon the reader is acting on behalf of
|
|
1085
|
-
* @param {string} [options.capabilityToken] - Explicit capability token to verify
|
|
1086
|
-
* @returns {Promise<Object>} { canRead: boolean, reason: string, accessType: string }
|
|
391
|
+
* Check read access (delegates to canAccess).
|
|
1087
392
|
*/
|
|
1088
393
|
async canRead(targetHolonId, lensName, readerPubKey, options = {}) {
|
|
1089
|
-
// Delegate to unified canAccess method
|
|
1090
394
|
const result = await this.canAccess(targetHolonId, lensName, readerPubKey, 'read', options);
|
|
1091
|
-
|
|
1092
395
|
return {
|
|
1093
396
|
canRead: result.allowed,
|
|
1094
397
|
reason: result.reason || result.via,
|
|
1095
398
|
accessType: result.via,
|
|
1096
|
-
viaHolon: result.viaHolon,
|
|
1097
|
-
grant: result.grant,
|
|
1098
399
|
};
|
|
1099
400
|
}
|
|
1100
401
|
|
|
1101
402
|
/**
|
|
1102
403
|
* Get all holons that a public key is a member of.
|
|
1103
|
-
* Searches federation registry and users lenses.
|
|
1104
|
-
*
|
|
1105
|
-
* @param {string} pubKey - The public key to look up
|
|
1106
|
-
* @returns {Promise<Array>} Array of { id, name } for each holon
|
|
1107
404
|
*/
|
|
1108
405
|
async getHolonsForPubKey(pubKey) {
|
|
1109
|
-
const results = [];
|
|
1110
|
-
|
|
1111
|
-
// The user's own holon (their pubkey is also a holon)
|
|
1112
|
-
results.push({ id: pubKey, name: 'Personal Holon' });
|
|
406
|
+
const results = [{ id: pubKey, name: 'Personal Holon' }];
|
|
1113
407
|
|
|
1114
|
-
// Check federation registry for holons where user has membership
|
|
1115
408
|
try {
|
|
1116
409
|
const reg = await registry.getFederationRegistry(this.client, this.config.appName);
|
|
1117
410
|
if (reg && reg.federatedWith) {
|
|
1118
411
|
for (const partner of reg.federatedWith) {
|
|
1119
|
-
// Check if this partner's holon has the user as a member
|
|
1120
412
|
try {
|
|
1121
413
|
const members = await this.get(partner.pubKey, 'users');
|
|
1122
414
|
if (members && Array.isArray(members)) {
|
|
1123
415
|
const isMember = members.some(m =>
|
|
1124
|
-
m.pubKey === pubKey ||
|
|
1125
|
-
m.id === pubKey ||
|
|
1126
|
-
m.nostrPubKey === pubKey
|
|
416
|
+
m.pubKey === pubKey || m.id === pubKey || m.nostrPubKey === pubKey
|
|
1127
417
|
);
|
|
1128
418
|
if (isMember) {
|
|
1129
419
|
results.push({ id: partner.pubKey, name: partner.alias || partner.pubKey });
|
|
1130
420
|
}
|
|
1131
421
|
}
|
|
1132
|
-
} catch (
|
|
1133
|
-
// Skip holons we can't read
|
|
1134
|
-
}
|
|
422
|
+
} catch (_err) { /* skip */ }
|
|
1135
423
|
}
|
|
1136
424
|
}
|
|
1137
|
-
} catch (
|
|
1138
|
-
console.warn('[getHolonsForPubKey] Error reading federation registry:', err.message);
|
|
1139
|
-
}
|
|
425
|
+
} catch (_err) { /* skip */ }
|
|
1140
426
|
|
|
1141
427
|
return results;
|
|
1142
428
|
}
|
|
1143
429
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
* Members of the granted holon will be able to write to this lens.
|
|
1147
|
-
*
|
|
1148
|
-
* @param {string} holonId - The holon to grant write access to
|
|
1149
|
-
* @param {string} lensName - The lens to grant write access for (or '*' for all)
|
|
1150
|
-
* @param {Object} [options={}] - Grant options
|
|
1151
|
-
* @param {number} [options.expiresAt] - Optional expiration timestamp
|
|
1152
|
-
* @returns {Promise<boolean>} Success indicator
|
|
1153
|
-
*
|
|
1154
|
-
* @example
|
|
1155
|
-
* // Grant holon B write access to quests lens
|
|
1156
|
-
* await hs.grantWriteAccess('holon-b-pubkey', 'quests');
|
|
1157
|
-
*/
|
|
430
|
+
// === Write Grant Backward Compat ===
|
|
431
|
+
|
|
1158
432
|
async grantWriteAccess(holonId, lensName, options = {}) {
|
|
1159
|
-
return registry.
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
holonId,
|
|
1163
|
-
lensName,
|
|
1164
|
-
options
|
|
1165
|
-
);
|
|
433
|
+
return registry.addFederatedPartner(this.client, this.config.appName, holonId, {
|
|
434
|
+
addedVia: 'write_grant',
|
|
435
|
+
});
|
|
1166
436
|
}
|
|
1167
437
|
|
|
438
|
+
// === Share Protocol (Hologram DM as Federation) ===
|
|
439
|
+
|
|
1168
440
|
/**
|
|
1169
|
-
*
|
|
441
|
+
* Share data with another user via NIP-44 DM.
|
|
442
|
+
*
|
|
443
|
+
* - If `item` is provided → share one item (creates an item hologram on accept)
|
|
444
|
+
* - If `item` is omitted → share an entire lens (creates a lens hologram on accept)
|
|
1170
445
|
*
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
446
|
+
* This replaces `federate()` + `propagateData()` + `initiateFederationHandshake()`
|
|
447
|
+
* for cross-author cases. Same-author (H3 hierarchical) still uses propagateData().
|
|
448
|
+
*
|
|
449
|
+
* @param {string} target - Recipient's hex public key
|
|
450
|
+
* @param {string} lens - Lens name to share
|
|
451
|
+
* @param {string|null} [item=null] - Specific data ID, or null for lens-level share
|
|
452
|
+
* @param {Object} [opts={}] - Options
|
|
453
|
+
* @param {Array} [opts.keys] - Bundled lens encryption keys [{ path, key }]
|
|
454
|
+
* @param {string} [opts.msg] - Optional message
|
|
455
|
+
* @param {string} [opts.sourceHolon] - Source holon ID (defaults to client.publicKey)
|
|
456
|
+
* @returns {Promise<Object>} { success, shareId?, error? }
|
|
1174
457
|
*/
|
|
1175
|
-
async
|
|
1176
|
-
|
|
458
|
+
async share(target, lens, item = null, opts = {}) {
|
|
459
|
+
if (!target || typeof target !== 'string') {
|
|
460
|
+
throw new ValidationError('target must be a valid public key string');
|
|
461
|
+
}
|
|
462
|
+
if (!lens || typeof lens !== 'string') {
|
|
463
|
+
throw new ValidationError('lens must be a non-empty string');
|
|
464
|
+
}
|
|
465
|
+
if (!this.client?.privateKey) {
|
|
466
|
+
throw new ValidationError('Private key required to send share');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const sourceHolon = opts.sourceHolon || this.client.publicKey;
|
|
470
|
+
const author = this.client.publicKey;
|
|
471
|
+
|
|
472
|
+
// Bundle lens encryption keys if requested or if we have them
|
|
473
|
+
let keys = opts.keys || [];
|
|
474
|
+
if (keys.length === 0 && this.keyStore) {
|
|
475
|
+
const lensPath = buildLensPath(this.config.appName, sourceHolon, lens);
|
|
476
|
+
const lensKey = this.keyStore.getKey(lensPath);
|
|
477
|
+
if (lensKey && this.client.privateKey) {
|
|
478
|
+
try {
|
|
479
|
+
const wrappedKey = this.keyStore.wrapKeyForPartner(
|
|
480
|
+
lensPath,
|
|
481
|
+
this.client.privateKey,
|
|
482
|
+
target
|
|
483
|
+
);
|
|
484
|
+
if (wrappedKey) {
|
|
485
|
+
keys = [{ path: lensPath, key: wrappedKey }];
|
|
486
|
+
}
|
|
487
|
+
} catch (_err) {
|
|
488
|
+
// Key wrapping failed — share without keys
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const sent = await federation.sendShare(
|
|
1177
494
|
this.client,
|
|
1178
|
-
this.
|
|
1179
|
-
|
|
1180
|
-
|
|
495
|
+
this.client.privateKey,
|
|
496
|
+
target,
|
|
497
|
+
{
|
|
498
|
+
source: sourceHolon,
|
|
499
|
+
lens,
|
|
500
|
+
item,
|
|
501
|
+
author,
|
|
502
|
+
keys,
|
|
503
|
+
msg: opts.msg,
|
|
504
|
+
}
|
|
1181
505
|
);
|
|
506
|
+
|
|
507
|
+
if (sent) {
|
|
508
|
+
// Add target to federation registry proactively
|
|
509
|
+
await registry.addFederatedPartner(
|
|
510
|
+
this.client, this.config.appName, target,
|
|
511
|
+
{ addedVia: 'share_sent' }
|
|
512
|
+
);
|
|
513
|
+
return { success: true };
|
|
514
|
+
}
|
|
515
|
+
return { success: false, error: 'Failed to send share DM' };
|
|
1182
516
|
}
|
|
1183
517
|
|
|
1184
518
|
/**
|
|
1185
|
-
*
|
|
519
|
+
* Accept an incoming share.
|
|
1186
520
|
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
521
|
+
* Stores the hologram locally, adds the sender to the federation registry,
|
|
522
|
+
* unwraps any bundled keys, and sends a share_ack DM.
|
|
523
|
+
*
|
|
524
|
+
* @param {Object} sharePayload - The share DM payload
|
|
525
|
+
* @param {string} sharePayload.id - Share nonce
|
|
526
|
+
* @param {string} sharePayload.source - Source holon ID
|
|
527
|
+
* @param {string} sharePayload.lens - Lens name
|
|
528
|
+
* @param {string|null} sharePayload.item - Data ID (null for lens-level)
|
|
529
|
+
* @param {string} sharePayload.author - Author's public key
|
|
530
|
+
* @param {Array} [sharePayload.keys] - Bundled encryption keys
|
|
531
|
+
* @param {string} senderPubKey - The pubkey of who sent the share
|
|
532
|
+
* @param {Object} [opts={}] - Options
|
|
533
|
+
* @param {string} [opts.holonId] - Local holon to store hologram in (defaults to client.publicKey)
|
|
534
|
+
* @param {string} [opts.msg] - Optional message in ack
|
|
535
|
+
* @returns {Promise<Object>} { success, error? }
|
|
1189
536
|
*/
|
|
1190
|
-
async
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
537
|
+
async accept(sharePayload, senderPubKey, opts = {}) {
|
|
538
|
+
if (!sharePayload || !sharePayload.lens || !sharePayload.author) {
|
|
539
|
+
throw new ValidationError('Invalid share payload');
|
|
540
|
+
}
|
|
541
|
+
if (!this.client?.privateKey) {
|
|
542
|
+
throw new ValidationError('Private key required to accept share');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const localHolon = opts.holonId || this.client.publicKey;
|
|
546
|
+
const { source, lens, item, author, keys = [] } = sharePayload;
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
// 1. Create and store the hologram locally
|
|
550
|
+
let hologram;
|
|
551
|
+
if (item) {
|
|
552
|
+
// Item-level hologram
|
|
553
|
+
hologram = federation.createHologram(
|
|
554
|
+
source, localHolon, lens, item, this.config.appName,
|
|
555
|
+
{ authorPubKey: author }
|
|
556
|
+
);
|
|
557
|
+
} else {
|
|
558
|
+
// Lens-level hologram
|
|
559
|
+
hologram = federation.createLensHologram(
|
|
560
|
+
source, lens, this.config.appName,
|
|
561
|
+
{ author }
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const hologramPath = storage.buildPath(
|
|
566
|
+
this.config.appName, localHolon, lens, hologram.id
|
|
567
|
+
);
|
|
568
|
+
await storage.write(this.client, hologramPath, hologram);
|
|
569
|
+
|
|
570
|
+
// 2. Add sender to federation registry
|
|
571
|
+
await registry.addFederatedPartner(
|
|
572
|
+
this.client, this.config.appName, senderPubKey,
|
|
573
|
+
{ addedVia: 'share_accepted' }
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
// Also add the author if different from sender
|
|
577
|
+
if (author !== senderPubKey) {
|
|
578
|
+
await registry.addFederatedPartner(
|
|
579
|
+
this.client, this.config.appName, author,
|
|
580
|
+
{ addedVia: 'share_accepted_author' }
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Register the source holon -> author pubkey mapping
|
|
585
|
+
if (source) {
|
|
586
|
+
await registerHolon(this.client, this.config.appName, source, author, {
|
|
587
|
+
alias: null,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// 3. Unwrap bundled encryption keys
|
|
592
|
+
if (keys.length > 0 && this.keyStore && this.client.privateKey) {
|
|
593
|
+
for (const { path: kPath, key: wrappedKey } of keys) {
|
|
594
|
+
try {
|
|
595
|
+
this.keyStore.receiveKey(
|
|
596
|
+
kPath,
|
|
597
|
+
wrappedKey,
|
|
598
|
+
this.client.privateKey,
|
|
599
|
+
senderPubKey
|
|
600
|
+
);
|
|
601
|
+
} catch (_err) {
|
|
602
|
+
// Key unwrap failed for this entry — continue
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// 4. Send ack DM
|
|
608
|
+
await federation.sendAck(
|
|
609
|
+
this.client,
|
|
610
|
+
this.client.privateKey,
|
|
611
|
+
senderPubKey,
|
|
612
|
+
{
|
|
613
|
+
id: sharePayload.id,
|
|
614
|
+
source,
|
|
615
|
+
lens,
|
|
616
|
+
item,
|
|
617
|
+
author,
|
|
618
|
+
status: 'accepted',
|
|
619
|
+
msg: opts.msg,
|
|
620
|
+
}
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
return { success: true };
|
|
624
|
+
} catch (error) {
|
|
625
|
+
return { success: false, error: error.message };
|
|
626
|
+
}
|
|
1196
627
|
}
|
|
1197
628
|
|
|
1198
629
|
/**
|
|
1199
|
-
*
|
|
630
|
+
* Reject an incoming share.
|
|
631
|
+
*
|
|
632
|
+
* Sends a share_ack DM with status 'rejected'. No local state is changed.
|
|
1200
633
|
*
|
|
1201
|
-
* @
|
|
634
|
+
* @param {Object} sharePayload - The share DM payload
|
|
635
|
+
* @param {string} senderPubKey - The pubkey of who sent the share
|
|
636
|
+
* @param {Object} [opts={}] - Options
|
|
637
|
+
* @param {string} [opts.msg] - Optional rejection message
|
|
638
|
+
* @returns {Promise<Object>} { success, error? }
|
|
1202
639
|
*/
|
|
1203
|
-
async
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
)
|
|
640
|
+
async reject(sharePayload, senderPubKey, opts = {}) {
|
|
641
|
+
if (!sharePayload || !sharePayload.id) {
|
|
642
|
+
throw new ValidationError('Invalid share payload');
|
|
643
|
+
}
|
|
644
|
+
if (!this.client?.privateKey) {
|
|
645
|
+
throw new ValidationError('Private key required to reject share');
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
try {
|
|
649
|
+
await federation.sendAck(
|
|
650
|
+
this.client,
|
|
651
|
+
this.client.privateKey,
|
|
652
|
+
senderPubKey,
|
|
653
|
+
{
|
|
654
|
+
id: sharePayload.id,
|
|
655
|
+
source: sharePayload.source,
|
|
656
|
+
lens: sharePayload.lens,
|
|
657
|
+
item: sharePayload.item,
|
|
658
|
+
author: sharePayload.author,
|
|
659
|
+
status: 'rejected',
|
|
660
|
+
msg: opts.msg,
|
|
661
|
+
}
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
return { success: true };
|
|
665
|
+
} catch (error) {
|
|
666
|
+
return { success: false, error: error.message };
|
|
667
|
+
}
|
|
1208
668
|
}
|
|
669
|
+
|
|
1209
670
|
};
|
|
1210
671
|
}
|
|
1211
672
|
|