holosphere 2.0.0-alpha2 → 2.0.0-alpha5

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.
Files changed (98) hide show
  1. package/dist/2019-D2OG2idw.js +6680 -0
  2. package/dist/2019-D2OG2idw.js.map +1 -0
  3. package/dist/2019-EION3wKo.cjs +8 -0
  4. package/dist/2019-EION3wKo.cjs.map +1 -0
  5. package/dist/_commonjsHelpers-C37NGDzP.cjs +2 -0
  6. package/dist/_commonjsHelpers-C37NGDzP.cjs.map +1 -0
  7. package/dist/_commonjsHelpers-CUmg6egw.js +7 -0
  8. package/dist/_commonjsHelpers-CUmg6egw.js.map +1 -0
  9. package/dist/browser-BSniCNqO.js +3058 -0
  10. package/dist/browser-BSniCNqO.js.map +1 -0
  11. package/dist/browser-Cq59Ij19.cjs +2 -0
  12. package/dist/browser-Cq59Ij19.cjs.map +1 -0
  13. package/dist/cjs/holosphere.cjs +1 -1
  14. package/dist/esm/holosphere.js +50 -53
  15. package/dist/index-BG8FStkt.cjs +12 -0
  16. package/dist/index-BG8FStkt.cjs.map +1 -0
  17. package/dist/index-Bbey4GkP.js +37869 -0
  18. package/dist/index-Bbey4GkP.js.map +1 -0
  19. package/dist/index-Cp3xctq8.js +15104 -0
  20. package/dist/index-Cp3xctq8.js.map +1 -0
  21. package/dist/index-hfVGRwSr.cjs +5 -0
  22. package/dist/index-hfVGRwSr.cjs.map +1 -0
  23. package/dist/indexeddb-storage-BD70pN7q.cjs +2 -0
  24. package/dist/indexeddb-storage-BD70pN7q.cjs.map +1 -0
  25. package/dist/{indexeddb-storage-CMW4qRQS.js → indexeddb-storage-Bjg84U5R.js} +49 -13
  26. package/dist/indexeddb-storage-Bjg84U5R.js.map +1 -0
  27. package/dist/{memory-storage-DQzcAZlf.js → memory-storage-CD0XFayE.js} +6 -2
  28. package/dist/memory-storage-CD0XFayE.js.map +1 -0
  29. package/dist/{memory-storage-DmePEP2q.cjs → memory-storage-DmMyJtOo.cjs} +2 -2
  30. package/dist/memory-storage-DmMyJtOo.cjs.map +1 -0
  31. package/dist/{secp256k1-vOXp40Fx.js → secp256k1-69sS9O-P.js} +2 -393
  32. package/dist/secp256k1-69sS9O-P.js.map +1 -0
  33. package/dist/secp256k1-TcN6vWGh.cjs +12 -0
  34. package/dist/secp256k1-TcN6vWGh.cjs.map +1 -0
  35. package/docs/CONTRACTS.md +797 -0
  36. package/examples/demo.html +47 -0
  37. package/package.json +10 -5
  38. package/src/contracts/abis/Appreciative.json +1280 -0
  39. package/src/contracts/abis/AppreciativeFactory.json +101 -0
  40. package/src/contracts/abis/Bundle.json +1435 -0
  41. package/src/contracts/abis/BundleFactory.json +106 -0
  42. package/src/contracts/abis/Holon.json +881 -0
  43. package/src/contracts/abis/Holons.json +330 -0
  44. package/src/contracts/abis/Managed.json +1262 -0
  45. package/src/contracts/abis/ManagedFactory.json +149 -0
  46. package/src/contracts/abis/Membrane.json +261 -0
  47. package/src/contracts/abis/Splitter.json +1624 -0
  48. package/src/contracts/abis/SplitterFactory.json +220 -0
  49. package/src/contracts/abis/TestToken.json +321 -0
  50. package/src/contracts/abis/Zoned.json +1461 -0
  51. package/src/contracts/abis/ZonedFactory.json +154 -0
  52. package/src/contracts/chain-manager.js +375 -0
  53. package/src/contracts/deployer.js +443 -0
  54. package/src/contracts/event-listener.js +507 -0
  55. package/src/contracts/holon-contracts.js +344 -0
  56. package/src/contracts/index.js +83 -0
  57. package/src/contracts/networks.js +224 -0
  58. package/src/contracts/operations.js +670 -0
  59. package/src/contracts/queries.js +589 -0
  60. package/src/core/holosphere.js +453 -1
  61. package/src/crypto/nostr-utils.js +263 -0
  62. package/src/federation/handshake.js +455 -0
  63. package/src/federation/hologram.js +1 -1
  64. package/src/hierarchical/upcast.js +6 -5
  65. package/src/index.js +463 -1939
  66. package/src/lib/ai-methods.js +308 -0
  67. package/src/lib/contract-methods.js +293 -0
  68. package/src/lib/errors.js +23 -0
  69. package/src/lib/federation-methods.js +238 -0
  70. package/src/lib/index.js +26 -0
  71. package/src/spatial/h3-operations.js +2 -2
  72. package/src/storage/backends/gundb-backend.js +377 -46
  73. package/src/storage/global-tables.js +28 -1
  74. package/src/storage/gun-auth.js +303 -0
  75. package/src/storage/gun-federation.js +776 -0
  76. package/src/storage/gun-references.js +198 -0
  77. package/src/storage/gun-schema.js +291 -0
  78. package/src/storage/gun-wrapper.js +347 -31
  79. package/src/storage/indexeddb-storage.js +49 -11
  80. package/src/storage/memory-storage.js +5 -0
  81. package/src/storage/nostr-async.js +194 -37
  82. package/src/storage/nostr-client.js +580 -51
  83. package/src/storage/persistent-storage.js +6 -1
  84. package/src/storage/unified-storage.js +119 -0
  85. package/src/subscriptions/manager.js +1 -1
  86. package/types/index.d.ts +133 -0
  87. package/dist/index-CDfIuXew.js +0 -15974
  88. package/dist/index-CDfIuXew.js.map +0 -1
  89. package/dist/index-ifOgtDvd.cjs +0 -3
  90. package/dist/index-ifOgtDvd.cjs.map +0 -1
  91. package/dist/indexeddb-storage-CMW4qRQS.js.map +0 -1
  92. package/dist/indexeddb-storage-DLZOgetM.cjs +0 -2
  93. package/dist/indexeddb-storage-DLZOgetM.cjs.map +0 -1
  94. package/dist/memory-storage-DQzcAZlf.js.map +0 -1
  95. package/dist/memory-storage-DmePEP2q.cjs.map +0 -1
  96. package/dist/secp256k1-CP0ZkpAx.cjs +0 -13
  97. package/dist/secp256k1-CP0ZkpAx.cjs.map +0 -1
  98. package/dist/secp256k1-vOXp40Fx.js.map +0 -1
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Nostr Utility Functions
3
+ *
4
+ * Provides browser-compatible utilities for Nostr key handling and NIP-04 encryption.
5
+ * Apps can use these without directly importing nostr-tools.
6
+ */
7
+
8
+ import { nip04, nip19, getPublicKey as nostrGetPublicKey, finalizeEvent, verifyEvent as nostrVerifyEvent } from 'nostr-tools';
9
+
10
+ // ============================================================================
11
+ // Key Conversion Utilities
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Convert hex string to Uint8Array
16
+ * @param {string} hex - Hex string
17
+ * @returns {Uint8Array} Byte array
18
+ */
19
+ export function hexToBytes(hex) {
20
+ const bytes = new Uint8Array(hex.length / 2);
21
+ for (let i = 0; i < hex.length; i += 2) {
22
+ bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
23
+ }
24
+ return bytes;
25
+ }
26
+
27
+ /**
28
+ * Convert Uint8Array to hex string
29
+ * @param {Uint8Array} bytes - Byte array
30
+ * @returns {string} Hex string
31
+ */
32
+ export function bytesToHex(bytes) {
33
+ return Array.from(bytes)
34
+ .map((b) => b.toString(16).padStart(2, '0'))
35
+ .join('');
36
+ }
37
+
38
+ /**
39
+ * Parse an npub or hex public key string into hex format
40
+ * @param {string} input - npub (npub1...) or hex public key
41
+ * @returns {{ valid: boolean, hexPubKey?: string, error?: string }}
42
+ */
43
+ export function parseNpubOrHex(input) {
44
+ const trimmed = input?.trim();
45
+
46
+ if (!trimmed) {
47
+ return { valid: false, error: 'Public key is required' };
48
+ }
49
+
50
+ // Check if it's already hex (64 characters)
51
+ if (/^[0-9a-fA-F]{64}$/.test(trimmed)) {
52
+ return { valid: true, hexPubKey: trimmed.toLowerCase() };
53
+ }
54
+
55
+ // Handle nostr: URI prefix
56
+ let npubString = trimmed;
57
+ if (npubString.startsWith('nostr:')) {
58
+ npubString = npubString.slice(6);
59
+ }
60
+
61
+ // Try to decode as npub
62
+ if (npubString.startsWith('npub1')) {
63
+ try {
64
+ const decoded = nip19.decode(npubString);
65
+ if (decoded.type === 'npub') {
66
+ return { valid: true, hexPubKey: decoded.data };
67
+ }
68
+ return { valid: false, error: 'Invalid npub format' };
69
+ } catch (e) {
70
+ return { valid: false, error: 'Invalid npub: unable to decode' };
71
+ }
72
+ }
73
+
74
+ return { valid: false, error: 'Enter a valid npub (npub1...) or 64-character hex public key' };
75
+ }
76
+
77
+ /**
78
+ * Convert a hex public key to npub format
79
+ * @param {string} hexPubKey - Hex public key (64 chars)
80
+ * @returns {string} npub string
81
+ */
82
+ export function hexToNpub(hexPubKey) {
83
+ try {
84
+ return nip19.npubEncode(hexPubKey);
85
+ } catch (e) {
86
+ console.error('Failed to encode hex to npub:', e);
87
+ return hexPubKey; // Return original on error
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Convert npub to hex public key
93
+ * @param {string} npub - npub string
94
+ * @returns {string|null} Hex public key or null if invalid
95
+ */
96
+ export function npubToHex(npub) {
97
+ try {
98
+ const decoded = nip19.decode(npub);
99
+ if (decoded.type === 'npub') {
100
+ return decoded.data;
101
+ }
102
+ return null;
103
+ } catch (e) {
104
+ return null;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Shorten a public key for display (first 8 and last 8 chars)
110
+ * @param {string} pubKey - Public key (hex or npub)
111
+ * @returns {string} Shortened key
112
+ */
113
+ export function shortenPubKey(pubKey) {
114
+ if (!pubKey || pubKey.length <= 20) return pubKey || '';
115
+ return `${pubKey.slice(0, 8)}...${pubKey.slice(-8)}`;
116
+ }
117
+
118
+ /**
119
+ * Shorten an npub for display
120
+ * @param {string} npub - npub string
121
+ * @returns {string} Shortened npub
122
+ */
123
+ export function shortenNpub(npub) {
124
+ if (!npub || npub.length <= 20) return npub || '';
125
+ return `${npub.slice(0, 12)}...${npub.slice(-8)}`;
126
+ }
127
+
128
+ /**
129
+ * Get public key from private key (matches nostr-tools API)
130
+ * @param {string} privateKey - Hex private key
131
+ * @returns {string} Hex public key
132
+ */
133
+ export function getPublicKey(privateKey) {
134
+ return nostrGetPublicKey(hexToBytes(privateKey));
135
+ }
136
+
137
+ // ============================================================================
138
+ // NIP-04 Encryption
139
+ // ============================================================================
140
+
141
+ /**
142
+ * Encrypt a message using NIP-04 (for DMs)
143
+ * @param {string} privateKey - Sender's hex private key
144
+ * @param {string} recipientPubKey - Recipient's hex public key
145
+ * @param {string} content - Plain text content
146
+ * @returns {Promise<string>} Encrypted content
147
+ */
148
+ export async function encryptNIP04(privateKey, recipientPubKey, content) {
149
+ return await nip04.encrypt(privateKey, recipientPubKey, content);
150
+ }
151
+
152
+ /**
153
+ * Decrypt a NIP-04 encrypted message
154
+ * @param {string} privateKey - Recipient's hex private key
155
+ * @param {string} senderPubKey - Sender's hex public key
156
+ * @param {string} encryptedContent - Encrypted content
157
+ * @returns {Promise<string>} Decrypted content
158
+ */
159
+ export async function decryptNIP04(privateKey, senderPubKey, encryptedContent) {
160
+ return await nip04.decrypt(privateKey, senderPubKey, encryptedContent);
161
+ }
162
+
163
+ // ============================================================================
164
+ // Event Creation
165
+ // ============================================================================
166
+
167
+ /**
168
+ * Create and sign a Nostr event
169
+ * @param {number} kind - Event kind
170
+ * @param {string} content - Event content
171
+ * @param {string[][]} tags - Event tags
172
+ * @param {string} privateKey - Hex private key for signing
173
+ * @returns {Object} Signed Nostr event
174
+ */
175
+ export function createSignedEvent(kind, content, tags, privateKey) {
176
+ const event = {
177
+ kind,
178
+ content,
179
+ tags,
180
+ created_at: Math.floor(Date.now() / 1000),
181
+ };
182
+
183
+ return finalizeEvent(event, hexToBytes(privateKey));
184
+ }
185
+
186
+ /**
187
+ * Create a NIP-04 encrypted DM event (kind 4)
188
+ * @param {string} recipientPubKey - Recipient's hex public key
189
+ * @param {string} encryptedContent - NIP-04 encrypted content
190
+ * @param {string} privateKey - Sender's hex private key
191
+ * @returns {Object} Signed DM event
192
+ */
193
+ export function createDMEvent(recipientPubKey, encryptedContent, privateKey) {
194
+ return createSignedEvent(
195
+ 4, // NIP-04 encrypted DM
196
+ encryptedContent,
197
+ [['p', recipientPubKey]],
198
+ privateKey
199
+ );
200
+ }
201
+
202
+ // ============================================================================
203
+ // Validation
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Check if a string is a valid hex public key
208
+ * @param {string} str - String to check
209
+ * @returns {boolean}
210
+ */
211
+ export function isValidHexPubKey(str) {
212
+ return typeof str === 'string' && /^[0-9a-fA-F]{64}$/.test(str);
213
+ }
214
+
215
+ /**
216
+ * Check if a string is a valid npub
217
+ * @param {string} str - String to check
218
+ * @returns {boolean}
219
+ */
220
+ export function isValidNpub(str) {
221
+ if (typeof str !== 'string' || !str.startsWith('npub1')) {
222
+ return false;
223
+ }
224
+ try {
225
+ const decoded = nip19.decode(str);
226
+ return decoded.type === 'npub';
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Generate a unique nonce
234
+ * @returns {string} Unique nonce
235
+ */
236
+ export function generateNonce() {
237
+ return Date.now().toString(36) + Math.random().toString(36).substring(2, 15);
238
+ }
239
+
240
+ /**
241
+ * Sign a Nostr event (alias for createSignedEvent that accepts event object)
242
+ * @param {Object} event - Unsigned event object with kind, content, tags
243
+ * @param {string} privateKey - Hex private key for signing
244
+ * @returns {Object} Signed Nostr event
245
+ */
246
+ export function signEvent(event, privateKey) {
247
+ const eventToSign = {
248
+ kind: event.kind,
249
+ content: event.content,
250
+ tags: event.tags || [],
251
+ created_at: event.created_at || Math.floor(Date.now() / 1000),
252
+ };
253
+ return finalizeEvent(eventToSign, hexToBytes(privateKey));
254
+ }
255
+
256
+ /**
257
+ * Verify a Nostr event signature
258
+ * @param {Object} event - Signed Nostr event to verify
259
+ * @returns {boolean} True if signature is valid
260
+ */
261
+ export function verifyEvent(event) {
262
+ return nostrVerifyEvent(event);
263
+ }
@@ -0,0 +1,455 @@
1
+ /**
2
+ * Federation Handshake Protocol
3
+ *
4
+ * Uses NIP-04 encrypted DMs (kind 4) for bidirectional federation request/response.
5
+ * When user A federates with user B's pubkey, a DM is sent to B.
6
+ * B can accept/reject, creating a matching federation on their side.
7
+ */
8
+
9
+ import {
10
+ encryptNIP04,
11
+ decryptNIP04,
12
+ createDMEvent,
13
+ hexToNpub,
14
+ generateNonce,
15
+ } from '../crypto/nostr-utils.js';
16
+ import { addFederatedPartner, storeInboundCapability } from './registry.js';
17
+
18
+ // ============================================================================
19
+ // Types (documented for reference)
20
+ // ============================================================================
21
+
22
+ /**
23
+ * @typedef {Object} FederationRequestPayload
24
+ * @property {'federation_request'} type
25
+ * @property {'1.0'} version
26
+ * @property {string} requestId - Unique request ID
27
+ * @property {number} timestamp
28
+ * @property {string} senderHolonId - Holon ID of the sender
29
+ * @property {string} senderHolonName - Human-readable holon name
30
+ * @property {string} senderNpub - Sender's npub
31
+ * @property {Object} lensConfig - Lens configuration
32
+ * @property {string[]} lensConfig.inbound - Lenses sender will accept
33
+ * @property {string[]} lensConfig.outbound - Lenses sender will share
34
+ * @property {Array} capabilities - Capability tokens
35
+ * @property {string} [message] - Optional personal message
36
+ */
37
+
38
+ /**
39
+ * @typedef {Object} FederationResponsePayload
40
+ * @property {'federation_response'} type
41
+ * @property {'1.0'} version
42
+ * @property {string} requestId - References original request
43
+ * @property {number} timestamp
44
+ * @property {'accepted'|'rejected'} status
45
+ * @property {string} [responderHolonId]
46
+ * @property {string} [responderHolonName]
47
+ * @property {string} [responderNpub]
48
+ * @property {Object} [lensConfig]
49
+ * @property {Array} [capabilities]
50
+ * @property {string} [message]
51
+ */
52
+
53
+ // ============================================================================
54
+ // Request/Response Creation
55
+ // ============================================================================
56
+
57
+ /**
58
+ * Create a federation request payload
59
+ * @param {Object} params
60
+ * @param {string} params.senderHolonId
61
+ * @param {string} params.senderHolonName
62
+ * @param {string} params.senderPubKey - Hex public key
63
+ * @param {Object} params.lensConfig - { inbound: string[], outbound: string[] }
64
+ * @param {Array} [params.capabilities] - Capability tokens to include
65
+ * @param {string} [params.message] - Optional message
66
+ * @returns {FederationRequestPayload}
67
+ */
68
+ export function createFederationRequest({
69
+ senderHolonId,
70
+ senderHolonName,
71
+ senderPubKey,
72
+ lensConfig = { inbound: [], outbound: [] },
73
+ capabilities = [],
74
+ message,
75
+ }) {
76
+ return {
77
+ type: 'federation_request',
78
+ version: '1.0',
79
+ requestId: generateNonce(),
80
+ timestamp: Date.now(),
81
+ senderHolonId,
82
+ senderHolonName,
83
+ senderNpub: hexToNpub(senderPubKey),
84
+ lensConfig,
85
+ capabilities,
86
+ message,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Create a federation response payload
92
+ * @param {Object} params
93
+ * @param {string} params.requestId - Original request ID
94
+ * @param {'accepted'|'rejected'} params.status
95
+ * @param {string} [params.responderHolonId]
96
+ * @param {string} [params.responderHolonName]
97
+ * @param {string} [params.responderPubKey] - Hex public key
98
+ * @param {Object} [params.lensConfig]
99
+ * @param {Array} [params.capabilities]
100
+ * @param {string} [params.message]
101
+ * @returns {FederationResponsePayload}
102
+ */
103
+ export function createFederationResponse({
104
+ requestId,
105
+ status,
106
+ responderHolonId,
107
+ responderHolonName,
108
+ responderPubKey,
109
+ lensConfig,
110
+ capabilities,
111
+ message,
112
+ }) {
113
+ return {
114
+ type: 'federation_response',
115
+ version: '1.0',
116
+ requestId,
117
+ timestamp: Date.now(),
118
+ status,
119
+ responderHolonId,
120
+ responderHolonName,
121
+ responderNpub: responderPubKey ? hexToNpub(responderPubKey) : undefined,
122
+ lensConfig,
123
+ capabilities,
124
+ message,
125
+ };
126
+ }
127
+
128
+ // ============================================================================
129
+ // DM Sending
130
+ // ============================================================================
131
+
132
+ /**
133
+ * Send a federation request DM
134
+ * @param {Object} client - NostrClient instance with publish method
135
+ * @param {string} privateKey - Sender's hex private key
136
+ * @param {string} recipientPubKey - Recipient's hex public key
137
+ * @param {FederationRequestPayload} request - Request payload
138
+ * @returns {Promise<boolean>} Success indicator
139
+ */
140
+ export async function sendFederationRequest(client, privateKey, recipientPubKey, request) {
141
+ try {
142
+ const content = JSON.stringify(request);
143
+ const encrypted = await encryptNIP04(privateKey, recipientPubKey, content);
144
+ const event = createDMEvent(recipientPubKey, encrypted, privateKey);
145
+
146
+ if (client?.publish) {
147
+ await client.publish(event);
148
+ console.log('[Handshake] Federation request sent to:', recipientPubKey.substring(0, 8) + '...');
149
+ return true;
150
+ } else {
151
+ console.error('[Handshake] No publish method on client');
152
+ return false;
153
+ }
154
+ } catch (error) {
155
+ console.error('[Handshake] Failed to send federation request:', error);
156
+ return false;
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Send a federation response DM
162
+ * @param {Object} client - NostrClient instance with publish method
163
+ * @param {string} privateKey - Sender's hex private key
164
+ * @param {string} recipientPubKey - Recipient's hex public key
165
+ * @param {FederationResponsePayload} response - Response payload
166
+ * @returns {Promise<boolean>} Success indicator
167
+ */
168
+ export async function sendFederationResponse(client, privateKey, recipientPubKey, response) {
169
+ try {
170
+ const content = JSON.stringify(response);
171
+ const encrypted = await encryptNIP04(privateKey, recipientPubKey, content);
172
+ const event = createDMEvent(recipientPubKey, encrypted, privateKey);
173
+
174
+ if (client?.publish) {
175
+ await client.publish(event);
176
+ console.log('[Handshake] Federation response sent to:', recipientPubKey.substring(0, 8) + '...');
177
+ return true;
178
+ } else {
179
+ console.error('[Handshake] No publish method on client');
180
+ return false;
181
+ }
182
+ } catch (error) {
183
+ console.error('[Handshake] Failed to send federation response:', error);
184
+ return false;
185
+ }
186
+ }
187
+
188
+ // ============================================================================
189
+ // DM Subscription
190
+ // ============================================================================
191
+
192
+ /**
193
+ * Subscribe to incoming federation DMs
194
+ * @param {Object} client - NostrClient instance with subscribe method
195
+ * @param {string} privateKey - Recipient's hex private key
196
+ * @param {string} publicKey - Recipient's hex public key
197
+ * @param {Object} handlers
198
+ * @param {Function} handlers.onRequest - Called with (request, senderPubKey)
199
+ * @param {Function} handlers.onResponse - Called with (response, senderPubKey)
200
+ * @returns {Function} Unsubscribe function
201
+ */
202
+ export function subscribeToFederationDMs(client, privateKey, publicKey, handlers) {
203
+ const { onRequest, onResponse } = handlers;
204
+ let isActive = true;
205
+ const processedEventIds = new Set();
206
+
207
+ const handleEvent = async (event) => {
208
+ if (!isActive) return;
209
+
210
+ // Skip duplicates
211
+ if (processedEventIds.has(event.id)) return;
212
+ processedEventIds.add(event.id);
213
+
214
+ // Only kind 4 (encrypted DM) events tagged to us
215
+ if (event.kind !== 4) return;
216
+
217
+ const pTag = event.tags?.find((t) => t[0] === 'p');
218
+ if (!pTag || pTag[1] !== publicKey) return;
219
+
220
+ try {
221
+ const decrypted = await decryptNIP04(privateKey, event.pubkey, event.content);
222
+ const payload = JSON.parse(decrypted);
223
+
224
+ if (payload.type === 'federation_request' && payload.version === '1.0') {
225
+ console.log('[Handshake] Received federation request from:', event.pubkey.substring(0, 8) + '...');
226
+ onRequest?.(payload, event.pubkey);
227
+ } else if (payload.type === 'federation_response' && payload.version === '1.0') {
228
+ console.log('[Handshake] Received federation response from:', event.pubkey.substring(0, 8) + '...');
229
+ onResponse?.(payload, event.pubkey);
230
+ }
231
+ } catch (error) {
232
+ // Silently ignore non-federation DMs
233
+ }
234
+ };
235
+
236
+ // Subscribe via client
237
+ // Note: client is the HoloSphere instance, client.client is the NostrClient
238
+ let subscription = null;
239
+
240
+ const startSubscription = async () => {
241
+ // Get the NostrClient from HoloSphere instance
242
+ const nostrClient = client?.client;
243
+ if (!nostrClient?.subscribe) {
244
+ console.warn('[Handshake] No NostrClient subscribe method available');
245
+ return;
246
+ }
247
+
248
+ const filter = {
249
+ kinds: [4],
250
+ '#p': [publicKey],
251
+ since: Math.floor(Date.now() / 1000) - 86400, // Last 24 hours
252
+ };
253
+
254
+ try {
255
+ // NostrClient.subscribe(filter, onEvent, options) returns { unsubscribe }
256
+ subscription = await nostrClient.subscribe(filter, handleEvent, {});
257
+ console.log('[Handshake] Federation DM subscription started');
258
+ } catch (error) {
259
+ console.error('[Handshake] Failed to subscribe:', error);
260
+ }
261
+ };
262
+
263
+ startSubscription();
264
+
265
+ // Return unsubscribe function
266
+ return () => {
267
+ isActive = false;
268
+ if (subscription?.unsubscribe) {
269
+ subscription.unsubscribe();
270
+ } else if (subscription?.close) {
271
+ subscription.close();
272
+ }
273
+ };
274
+ }
275
+
276
+ // ============================================================================
277
+ // High-Level Handshake Operations
278
+ // ============================================================================
279
+
280
+ /**
281
+ * Initiate a federation handshake with a partner
282
+ * Creates local federation record and sends DM to partner
283
+ * @param {Object} holosphere - HoloSphere instance
284
+ * @param {string} privateKey - Sender's hex private key
285
+ * @param {Object} params
286
+ * @param {string} params.partnerPubKey - Partner's hex public key
287
+ * @param {string} params.holonId - Current holon ID
288
+ * @param {string} params.holonName - Current holon name
289
+ * @param {Object} [params.lensConfig] - Lens configuration
290
+ * @param {string} [params.message] - Optional message
291
+ * @returns {Promise<{ success: boolean, requestId?: string, error?: string }>}
292
+ */
293
+ export async function initiateFederationHandshake(holosphere, privateKey, params) {
294
+ const {
295
+ partnerPubKey,
296
+ holonId,
297
+ holonName,
298
+ lensConfig = { inbound: [], outbound: [] },
299
+ message,
300
+ } = params;
301
+
302
+ try {
303
+ // Get sender's public key
304
+ const { getPublicKeyFromPrivate } = await import('../crypto/nostr-utils.js');
305
+ const senderPubKey = getPublicKeyFromPrivate(privateKey);
306
+
307
+ // Create federation request
308
+ const request = createFederationRequest({
309
+ senderHolonId: holonId,
310
+ senderHolonName: holonName,
311
+ senderPubKey,
312
+ lensConfig,
313
+ capabilities: [],
314
+ message,
315
+ });
316
+
317
+ // Add partner to local registry (status: pending)
318
+ if (holosphere.client) {
319
+ await addFederatedPartner(holosphere.client, holosphere.config.appName, partnerPubKey, {
320
+ alias: null,
321
+ addedVia: 'handshake_initiated',
322
+ });
323
+ }
324
+
325
+ // Send DM to partner
326
+ const sent = await sendFederationRequest(holosphere.client, privateKey, partnerPubKey, request);
327
+
328
+ if (sent) {
329
+ return { success: true, requestId: request.requestId };
330
+ } else {
331
+ return { success: false, error: 'Failed to send DM' };
332
+ }
333
+ } catch (error) {
334
+ return { success: false, error: error.message };
335
+ }
336
+ }
337
+
338
+ /**
339
+ * Accept a federation request
340
+ * Creates local federation record and sends acceptance DM
341
+ * @param {Object} holosphere - HoloSphere instance
342
+ * @param {string} privateKey - Responder's hex private key
343
+ * @param {Object} params
344
+ * @param {FederationRequestPayload} params.request - Original request
345
+ * @param {string} params.senderPubKey - Original sender's public key
346
+ * @param {string} params.holonId - Current holon ID
347
+ * @param {string} params.holonName - Current holon name
348
+ * @param {Object} [params.lensConfig] - Our lens configuration
349
+ * @param {string} [params.message] - Optional message
350
+ * @returns {Promise<{ success: boolean, error?: string }>}
351
+ */
352
+ export async function acceptFederationRequest(holosphere, privateKey, params) {
353
+ const {
354
+ request,
355
+ senderPubKey,
356
+ holonId,
357
+ holonName,
358
+ lensConfig = { inbound: [], outbound: [] },
359
+ message,
360
+ } = params;
361
+
362
+ try {
363
+ // Get responder's public key
364
+ const { getPublicKeyFromPrivate } = await import('../crypto/nostr-utils.js');
365
+ const responderPubKey = getPublicKeyFromPrivate(privateKey);
366
+
367
+ // Add sender as federated partner
368
+ if (holosphere.client) {
369
+ await addFederatedPartner(holosphere.client, holosphere.config.appName, senderPubKey, {
370
+ alias: request.senderHolonName,
371
+ addedVia: 'handshake_accepted',
372
+ });
373
+
374
+ // Store any capabilities they sent
375
+ if (request.capabilities && request.capabilities.length > 0) {
376
+ for (const cap of request.capabilities) {
377
+ await storeInboundCapability(holosphere.client, holosphere.config.appName, senderPubKey, cap);
378
+ }
379
+ }
380
+ }
381
+
382
+ // Create and send response
383
+ const response = createFederationResponse({
384
+ requestId: request.requestId,
385
+ status: 'accepted',
386
+ responderHolonId: holonId,
387
+ responderHolonName: holonName,
388
+ responderPubKey,
389
+ lensConfig,
390
+ capabilities: [],
391
+ message,
392
+ });
393
+
394
+ const sent = await sendFederationResponse(holosphere.client, privateKey, senderPubKey, response);
395
+
396
+ if (sent) {
397
+ return { success: true };
398
+ } else {
399
+ return { success: false, error: 'Failed to send response DM' };
400
+ }
401
+ } catch (error) {
402
+ return { success: false, error: error.message };
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Reject a federation request
408
+ * Sends rejection DM without creating local federation
409
+ * @param {Object} holosphere - HoloSphere instance
410
+ * @param {string} privateKey - Responder's hex private key
411
+ * @param {Object} params
412
+ * @param {string} params.requestId - Original request ID
413
+ * @param {string} params.senderPubKey - Original sender's public key
414
+ * @param {string} [params.message] - Optional rejection message
415
+ * @returns {Promise<{ success: boolean, error?: string }>}
416
+ */
417
+ export async function rejectFederationRequest(holosphere, privateKey, params) {
418
+ const { requestId, senderPubKey, message } = params;
419
+
420
+ try {
421
+ const response = createFederationResponse({
422
+ requestId,
423
+ status: 'rejected',
424
+ message,
425
+ });
426
+
427
+ const sent = await sendFederationResponse(holosphere.client, privateKey, senderPubKey, response);
428
+
429
+ return { success: sent, error: sent ? undefined : 'Failed to send response DM' };
430
+ } catch (error) {
431
+ return { success: false, error: error.message };
432
+ }
433
+ }
434
+
435
+ // ============================================================================
436
+ // Payload Validation
437
+ // ============================================================================
438
+
439
+ /**
440
+ * Check if payload is a federation request
441
+ * @param {any} payload
442
+ * @returns {payload is FederationRequestPayload}
443
+ */
444
+ export function isFederationRequest(payload) {
445
+ return payload?.type === 'federation_request' && payload?.version === '1.0';
446
+ }
447
+
448
+ /**
449
+ * Check if payload is a federation response
450
+ * @param {any} payload
451
+ * @returns {payload is FederationResponsePayload}
452
+ */
453
+ export function isFederationResponse(payload) {
454
+ return payload?.type === 'federation_response' && payload?.version === '1.0';
455
+ }