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