holosphere 2.0.0-alpha11 → 2.0.0-alpha12

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 (60) hide show
  1. package/dist/{2019-D2OG2idw.js → 2019-DQdDE6DG.js} +2 -2
  2. package/dist/{2019-D2OG2idw.js.map → 2019-DQdDE6DG.js.map} +1 -1
  3. package/dist/{2019-EION3wKo.cjs → 2019-Ew-DTDlI.cjs} +2 -2
  4. package/dist/{2019-EION3wKo.cjs.map → 2019-Ew-DTDlI.cjs.map} +1 -1
  5. package/dist/{browser-BSniCNqO.js → browser-CckFyRI9.js} +2 -2
  6. package/dist/{browser-BSniCNqO.js.map → browser-CckFyRI9.js.map} +1 -1
  7. package/dist/browser-D2qtVhH5.cjs +2 -0
  8. package/dist/{browser-Cq59Ij19.cjs.map → browser-D2qtVhH5.cjs.map} +1 -1
  9. package/dist/cjs/holosphere.cjs +1 -1
  10. package/dist/esm/holosphere.js +67 -50
  11. package/dist/index-9sqetkAn.cjs +29 -0
  12. package/dist/index-9sqetkAn.cjs.map +1 -0
  13. package/dist/index-C0ITDyFo.cjs +13 -0
  14. package/dist/index-C0ITDyFo.cjs.map +1 -0
  15. package/dist/{index-Bl6rM1NW.js → index-DrYM1LOY.js} +2 -2
  16. package/dist/{index-Bl6rM1NW.js.map → index-DrYM1LOY.js.map} +1 -1
  17. package/dist/{index-D-jZhliX.js → index-TDDyakLc.js} +19827 -720
  18. package/dist/index-TDDyakLc.js.map +1 -0
  19. package/dist/{index-Bwg3OzRM.cjs → index-kyf1sjaC.cjs} +3 -3
  20. package/dist/{index-Bwg3OzRM.cjs.map → index-kyf1sjaC.cjs.map} +1 -1
  21. package/dist/index-lbSQUoRz.js +11663 -0
  22. package/dist/index-lbSQUoRz.js.map +1 -0
  23. package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-CXhjqwhA.js} +2 -2
  24. package/dist/{indexeddb-storage-5eiUNsHC.js.map → indexeddb-storage-CXhjqwhA.js.map} +1 -1
  25. package/dist/{indexeddb-storage-FNFUVvTJ.cjs → indexeddb-storage-DFESDYIj.cjs} +2 -2
  26. package/dist/{indexeddb-storage-FNFUVvTJ.cjs.map → indexeddb-storage-DFESDYIj.cjs.map} +1 -1
  27. package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-D1tc1bjk.cjs} +2 -2
  28. package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-D1tc1bjk.cjs.map} +1 -1
  29. package/dist/{memory-storage-CI-gfmuG.js → memory-storage-DkewsdcM.js} +2 -2
  30. package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-DkewsdcM.js.map} +1 -1
  31. package/docs/FEDERATION.md +474 -0
  32. package/package.json +3 -1
  33. package/src/crypto/nostr-utils.js +7 -0
  34. package/src/crypto/secp256k1.js +46 -29
  35. package/src/federation/capabilities.js +162 -0
  36. package/src/federation/card-storage.js +304 -0
  37. package/src/federation/handshake.js +204 -7
  38. package/src/federation/hologram.js +167 -58
  39. package/src/federation/holon-registry.js +166 -0
  40. package/src/federation/index.js +68 -0
  41. package/src/federation/registry.js +110 -2
  42. package/src/federation/request-card.js +373 -0
  43. package/src/hierarchical/upcast.js +19 -3
  44. package/src/index.js +162 -76
  45. package/src/lib/federation-methods.js +527 -5
  46. package/src/storage/nostr-async.js +2 -2
  47. package/src/storage/nostr-client.js +370 -155
  48. package/src/storage/nostr-wrapper.js +1 -1
  49. package/dist/_commonjsHelpers-C37NGDzP.cjs +0 -2
  50. package/dist/_commonjsHelpers-C37NGDzP.cjs.map +0 -1
  51. package/dist/_commonjsHelpers-CUmg6egw.js +0 -7
  52. package/dist/_commonjsHelpers-CUmg6egw.js.map +0 -1
  53. package/dist/browser-Cq59Ij19.cjs +0 -2
  54. package/dist/index-D-jZhliX.js.map +0 -1
  55. package/dist/index-Dc6Z8Aob.cjs +0 -18
  56. package/dist/index-Dc6Z8Aob.cjs.map +0 -1
  57. package/dist/secp256k1-CEwJNcfV.js +0 -1890
  58. package/dist/secp256k1-CEwJNcfV.js.map +0 -1
  59. package/dist/secp256k1-CiEONUnj.cjs +0 -12
  60. package/dist/secp256k1-CiEONUnj.cjs.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "holosphere",
3
- "version": "2.0.0-alpha11",
3
+ "version": "2.0.0-alpha12",
4
4
  "description": "Holonic geospatial communication infrastructure combining H3 hexagonal indexing with distributed P2P storage",
5
5
  "type": "module",
6
6
  "bin": {
@@ -54,6 +54,7 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@noble/curves": "^1.3.0",
57
+ "@nostr-dev-kit/ndk": "^2.10.0",
57
58
  "ajv": "^8.12.0",
58
59
  "dotenv": "^17.2.3",
59
60
  "ethers": "^6.13.4",
@@ -64,6 +65,7 @@
64
65
  "ws": "^8.18.3"
65
66
  },
66
67
  "optionalDependencies": {
68
+ "@nostr-dev-kit/ndk-cache-dexie": "^2.5.0",
67
69
  "gun": "^0.2020.1241"
68
70
  },
69
71
  "devDependencies": {
@@ -2,11 +2,18 @@
2
2
  * @fileoverview Browser-compatible Nostr utility functions for key handling, encryption, and event management.
3
3
  * Provides NIP-04 (legacy) and NIP-44 (modern, audited) encryption support, key conversion utilities,
4
4
  * and event creation helpers. Applications can use these utilities without directly importing nostr-tools.
5
+ *
6
+ * This module uses nostr-tools for cryptographic operations while the main client uses NDK.
7
+ * Both libraries are compatible and can be used together.
8
+ *
5
9
  * @module crypto/nostr-utils
6
10
  */
7
11
 
8
12
  import { nip04, nip44, nip19, getPublicKey as nostrGetPublicKey, finalizeEvent, verifyEvent as nostrVerifyEvent } from 'nostr-tools';
9
13
 
14
+ // Re-export NDK types for consumers who want to use NDK directly
15
+ export { default as NDK, NDKEvent, NDKPrivateKeySigner, NDKUser, NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk';
16
+
10
17
  // ============================================================================
11
18
  // Key Conversion Utilities
12
19
  // ============================================================================
@@ -10,29 +10,15 @@
10
10
 
11
11
  import { sha256 } from '@noble/hashes/sha256';
12
12
  import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
13
-
14
- let secp256k1 = null;
15
-
16
- /**
17
- * Lazy load secp256k1 module
18
- * @private
19
- */
20
- async function loadCrypto() {
21
- if (!secp256k1) {
22
- const module = await import('@noble/curves/secp256k1');
23
- secp256k1 = module.secp256k1;
24
- }
25
- return secp256k1;
26
- }
13
+ import { secp256k1 } from '@noble/curves/secp256k1';
27
14
 
28
15
  /**
29
16
  * Get public key from private key
30
17
  * @param {string} privateKey - Private key (hex string)
31
- * @returns {Promise<string>} Public key (hex string)
18
+ * @returns {string} Public key (hex string)
32
19
  */
33
- export async function getPublicKey(privateKey) {
34
- const crypto = await loadCrypto();
35
- const pubKey = crypto.getPublicKey(privateKey);
20
+ export function getPublicKey(privateKey) {
21
+ const pubKey = secp256k1.getPublicKey(privateKey);
36
22
  return bytesToHex(pubKey);
37
23
  }
38
24
 
@@ -44,13 +30,11 @@ export async function getPublicKey(privateKey) {
44
30
  */
45
31
  export async function sign(content, privateKey) {
46
32
  try {
47
- const crypto = await loadCrypto();
48
-
49
33
  // Hash content
50
34
  const msgHash = hashContent(content);
51
35
 
52
36
  // Sign - secp256k1.sign returns Signature object
53
- const signature = crypto.sign(msgHash, privateKey);
37
+ const signature = secp256k1.sign(msgHash, privateKey);
54
38
  return signature.toCompactHex();
55
39
  } catch (error) {
56
40
  throw new Error(`Signature generation failed: ${error.message}`);
@@ -71,10 +55,8 @@ export async function verify(content, signature, publicKey) {
71
55
  throw new Error('Invalid public key format');
72
56
  }
73
57
 
74
- const crypto = await loadCrypto();
75
-
76
58
  const msgHash = hashContent(content);
77
- const isValid = crypto.verify(signature, msgHash, publicKey);
59
+ const isValid = secp256k1.verify(signature, msgHash, publicKey);
78
60
  return isValid;
79
61
  } catch (error) {
80
62
  // Invalid signatures or keys throw errors
@@ -127,18 +109,24 @@ export function matchScope(tokenScope, requestedScope) {
127
109
  }
128
110
 
129
111
  /**
130
- * Issue capability token
112
+ * Issue capability token - UNIFIED MODEL
113
+ *
114
+ * Supports both self-capabilities (issuer === recipient) and cross-capabilities.
115
+ * Self-capabilities are used for same-author federation, providing consistent
116
+ * security model across all federation types.
117
+ *
131
118
  * @param {string[]} permissions - Permissions array (e.g., ['read', 'write', 'delete'])
132
119
  * @param {Object|string} scope - Scope (holon/lens path or object). Supports wildcards: { holonId: "*", lensName: "*" }
133
- * @param {string} recipient - Recipient public key
120
+ * @param {string} recipient - Recipient public key (can be same as issuer for self-capability)
134
121
  * @param {Object} options - Options
135
- * @param {number} options.expiresIn - Expiration in milliseconds (default: 1 hour)
136
- * @param {string} options.issuer - Issuer ID
122
+ * @param {number} options.expiresIn - Expiration in milliseconds (default: 1 hour, longer for self-caps)
123
+ * @param {string} options.issuer - Issuer ID/public key
137
124
  * @param {string} options.issuerKey - Issuer private key for signing
125
+ * @param {boolean} options.isSelfCapability - If true, marks as self-capability (auto-detected if issuer === recipient)
138
126
  * @returns {Promise<string>} Capability token (base64-encoded JWT-like)
139
127
  */
140
128
  export async function issueCapability(permissions, scope, recipient, options = {}) {
141
- const { expiresIn = 3600000, issuer = 'holosphere', issuerKey } = options;
129
+ const { expiresIn = 3600000, issuer = 'holosphere', issuerKey, isSelfCapability } = options;
142
130
 
143
131
  // Validate permissions
144
132
  if (!Array.isArray(permissions) || permissions.length === 0) {
@@ -165,12 +153,16 @@ export async function issueCapability(permissions, scope, recipient, options = {
165
153
  throw new Error('Invalid issuer key');
166
154
  }
167
155
 
156
+ // Detect self-capability (issuer === recipient)
157
+ const selfCap = isSelfCapability !== undefined ? isSelfCapability : (issuer === recipient);
158
+
168
159
  const token = {
169
160
  type: 'capability',
170
161
  permissions,
171
162
  scope,
172
163
  recipient,
173
164
  issuer,
165
+ isSelfCapability: selfCap, // Mark self-capabilities for clarity
174
166
  nonce: generateNonce(),
175
167
  issued: Date.now(),
176
168
  expires: Date.now() + expiresIn,
@@ -191,6 +183,31 @@ export async function issueCapability(permissions, scope, recipient, options = {
191
183
  return encoded;
192
184
  }
193
185
 
186
+ /**
187
+ * Issue a self-capability token - UNIFIED MODEL
188
+ *
189
+ * Convenience method for creating self-capabilities (same author federation).
190
+ * Self-capabilities have longer expiration by default (1 year).
191
+ *
192
+ * @param {string[]} permissions - Permissions array
193
+ * @param {Object} scope - Scope object { holonId, lensName, dataId? }
194
+ * @param {string} authorPubKey - Author's public key (both issuer and recipient)
195
+ * @param {Object} options - Options
196
+ * @param {string} options.privateKey - Author's private key for signing
197
+ * @param {number} [options.expiresIn=31536000000] - Expiration in ms (default: 1 year)
198
+ * @returns {Promise<string>} Self-capability token
199
+ */
200
+ export async function issueSelfCapability(permissions, scope, authorPubKey, options = {}) {
201
+ const { privateKey, expiresIn = 365 * 24 * 60 * 60 * 1000 } = options; // 1 year default
202
+
203
+ return issueCapability(permissions, scope, authorPubKey, {
204
+ expiresIn,
205
+ issuer: authorPubKey,
206
+ issuerKey: privateKey,
207
+ isSelfCapability: true,
208
+ });
209
+ }
210
+
194
211
  /**
195
212
  * Verify capability token
196
213
  * @param {string|Object} token - Capability token (string or object)
@@ -0,0 +1,162 @@
1
+ /**
2
+ * @fileoverview Federation Capabilities Module
3
+ *
4
+ * Handles capability token issuance, verification, and management
5
+ * for federation operations. Extracted from federation-methods.js
6
+ * for better modularity.
7
+ *
8
+ * @module federation/capabilities
9
+ */
10
+
11
+ import * as crypto from '../crypto/secp256k1.js';
12
+ import * as registry from './registry.js';
13
+ import { sha256 } from '@noble/hashes/sha256';
14
+ import { bytesToHex } from '@noble/hashes/utils';
15
+
16
+ /**
17
+ * Issue a capability token for federation
18
+ * @param {Object} client - NostrClient instance
19
+ * @param {string} targetPubKey - Target public key to grant access to
20
+ * @param {Object} scope - Access scope { holonId, lensName, dataId }
21
+ * @param {string[]} permissions - Array of permissions ('read', 'write')
22
+ * @param {Object} options - Capability options
23
+ * @param {number} [options.expiresIn=3600000] - Expiration time in milliseconds
24
+ * @param {boolean} [options.isSelfCapability=false] - Whether this is a self-capability
25
+ * @returns {Promise<string>} Capability token
26
+ */
27
+ export async function issueCapability(client, targetPubKey, scope, permissions, options = {}) {
28
+ const {
29
+ expiresIn = 3600000,
30
+ isSelfCapability = false,
31
+ } = options;
32
+
33
+ const token = await crypto.issueCapability(
34
+ permissions,
35
+ scope,
36
+ targetPubKey,
37
+ {
38
+ expiresIn,
39
+ issuer: client.publicKey,
40
+ issuerKey: client.privateKey,
41
+ isSelfCapability,
42
+ }
43
+ );
44
+
45
+ return token;
46
+ }
47
+
48
+ /**
49
+ * Issue capability for a specific lens
50
+ * @param {Object} client - NostrClient instance
51
+ * @param {string} holonId - Holon ID
52
+ * @param {string} lensName - Lens name
53
+ * @param {string} targetPubKey - Target public key
54
+ * @param {Object} options - Options
55
+ * @returns {Promise<Object>} Capability info { token, scope, permissions }
56
+ */
57
+ export async function issueCapabilityForLens(client, holonId, lensName, targetPubKey, options = {}) {
58
+ const {
59
+ permissions = ['read'],
60
+ expiresIn = 365 * 24 * 60 * 60 * 1000, // 1 year default
61
+ } = options;
62
+
63
+ const scope = { holonId, lensName, dataId: '*' };
64
+
65
+ const token = await issueCapability(client, targetPubKey, scope, permissions, {
66
+ expiresIn,
67
+ isSelfCapability: client.publicKey === targetPubKey,
68
+ });
69
+
70
+ return {
71
+ token,
72
+ scope,
73
+ permissions,
74
+ expiresAt: Date.now() + expiresIn,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Issue capabilities for multiple lenses
80
+ * @param {Object} client - NostrClient instance
81
+ * @param {string} holonId - Holon ID
82
+ * @param {string[]} lensNames - Array of lens names
83
+ * @param {string} targetPubKey - Target public key
84
+ * @param {Object} options - Options
85
+ * @returns {Promise<Object[]>} Array of capability info objects
86
+ */
87
+ export async function issueCapabilitiesForLenses(client, holonId, lensNames, targetPubKey, options = {}) {
88
+ const capabilities = [];
89
+
90
+ for (const lensName of lensNames) {
91
+ try {
92
+ const capInfo = await issueCapabilityForLens(client, holonId, lensName, targetPubKey, options);
93
+ capabilities.push(capInfo);
94
+ } catch (err) {
95
+ console.warn(`[Capabilities] Failed to issue capability for ${lensName}:`, err.message);
96
+ }
97
+ }
98
+
99
+ return capabilities;
100
+ }
101
+
102
+ /**
103
+ * Store a capability in the registry
104
+ * @param {Object} client - NostrClient instance
105
+ * @param {string} appname - Application namespace
106
+ * @param {string} partnerPubKey - Partner's public key
107
+ * @param {Object} capabilityInfo - Capability info to store
108
+ * @param {Object} options - Options
109
+ * @returns {Promise<boolean>} Success indicator
110
+ */
111
+ export async function storeCapability(client, appname, partnerPubKey, capabilityInfo, options = {}) {
112
+ const { isSelf = false } = options;
113
+
114
+ if (isSelf) {
115
+ return registry.storeSelfCapability(client, appname, capabilityInfo);
116
+ } else {
117
+ return registry.storeInboundCapability(client, appname, partnerPubKey, capabilityInfo);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Hash a capability token for storage
123
+ * @param {string} token - Capability token to hash
124
+ * @returns {string} SHA256 hash of token
125
+ */
126
+ export function hashToken(token) {
127
+ const encoder = new TextEncoder();
128
+ return bytesToHex(sha256(encoder.encode(token)));
129
+ }
130
+
131
+ /**
132
+ * Verify a capability token
133
+ * @param {string} token - Capability token
134
+ * @param {string} permission - Required permission
135
+ * @param {Object} scope - Requested scope
136
+ * @returns {Promise<boolean>} True if valid
137
+ */
138
+ export async function verifyCapability(token, permission, scope) {
139
+ return crypto.verifyCapability(token, permission, scope);
140
+ }
141
+
142
+ /**
143
+ * Get capability for accessing a partner's data
144
+ * @param {Object} client - NostrClient instance
145
+ * @param {string} appname - Application namespace
146
+ * @param {string} partnerPubKey - Partner's public key
147
+ * @param {Object} scope - Requested scope
148
+ * @returns {Promise<Object|null>} Capability entry or null
149
+ */
150
+ export async function getCapabilityForPartner(client, appname, partnerPubKey, scope) {
151
+ return registry.getCapabilityForAuthor(client, appname, partnerPubKey, scope);
152
+ }
153
+
154
+ export default {
155
+ issueCapability,
156
+ issueCapabilityForLens,
157
+ issueCapabilitiesForLenses,
158
+ storeCapability,
159
+ hashToken,
160
+ verifyCapability,
161
+ getCapabilityForPartner,
162
+ };
@@ -0,0 +1,304 @@
1
+ /**
2
+ * @fileoverview Federation Card Storage
3
+ *
4
+ * Persists federation card state to prevent cards from reappearing
5
+ * after deletion. Manages card lifecycle (pending, sent, accepted,
6
+ * rejected, dismissed).
7
+ *
8
+ * @module federation/card-storage
9
+ */
10
+
11
+ import { writeGlobal, readGlobal, getAllGlobal } from '../storage/global-tables.js';
12
+ import { serializeCard, deserializeCard, shouldDisplayCard } from './request-card.js';
13
+
14
+ const FEDERATION_CARDS_TABLE = '_federation-cards';
15
+ const DISMISSED_REQUESTS_TABLE = '_dismissed-federation-requests';
16
+ const PROCESSED_RESPONSES_TABLE = '_processed-federation-responses';
17
+
18
+ /**
19
+ * Save a federation card
20
+ * @param {Object} client - NostrClient instance
21
+ * @param {string} appname - Application namespace
22
+ * @param {Object} card - Federation card configuration
23
+ * @returns {Promise<boolean>} Success indicator
24
+ */
25
+ export async function saveCard(client, appname, card) {
26
+ const serialized = serializeCard(card);
27
+ serialized.id = card.id;
28
+ return writeGlobal(client, appname, FEDERATION_CARDS_TABLE, serialized);
29
+ }
30
+
31
+ /**
32
+ * Get a specific federation card
33
+ * @param {Object} client - NostrClient instance
34
+ * @param {string} appname - Application namespace
35
+ * @param {string} cardId - Card ID
36
+ * @returns {Promise<Object|null>} Card or null
37
+ */
38
+ export async function getCard(client, appname, cardId) {
39
+ const data = await readGlobal(client, appname, FEDERATION_CARDS_TABLE, cardId);
40
+ return data ? deserializeCard(data) : null;
41
+ }
42
+
43
+ /**
44
+ * Get all federation cards
45
+ * @param {Object} client - NostrClient instance
46
+ * @param {string} appname - Application namespace
47
+ * @returns {Promise<Object[]>} Array of cards
48
+ */
49
+ export async function getAllCards(client, appname) {
50
+ const data = await getAllGlobal(client, appname, FEDERATION_CARDS_TABLE);
51
+ return (data || [])
52
+ .filter(d => d && !d._deleted)
53
+ .map(d => deserializeCard(d));
54
+ }
55
+
56
+ /**
57
+ * Get cards that should be displayed
58
+ * @param {Object} client - NostrClient instance
59
+ * @param {string} appname - Application namespace
60
+ * @returns {Promise<Object[]>} Array of displayable cards
61
+ */
62
+ export async function getDisplayableCards(client, appname) {
63
+ const cards = await getAllCards(client, appname);
64
+ return cards.filter(shouldDisplayCard);
65
+ }
66
+
67
+ /**
68
+ * Get card by partner public key
69
+ * @param {Object} client - NostrClient instance
70
+ * @param {string} appname - Application namespace
71
+ * @param {string} partnerPubKey - Partner's public key
72
+ * @returns {Promise<Object|null>} Card or null
73
+ */
74
+ export async function getCardByPartner(client, appname, partnerPubKey) {
75
+ const cards = await getAllCards(client, appname);
76
+ return cards.find(c => c.partnerPubKey === partnerPubKey) || null;
77
+ }
78
+
79
+ /**
80
+ * Delete a federation card
81
+ * @param {Object} client - NostrClient instance
82
+ * @param {string} appname - Application namespace
83
+ * @param {string} cardId - Card ID
84
+ * @returns {Promise<boolean>} Success indicator
85
+ */
86
+ export async function deleteCard(client, appname, cardId) {
87
+ const card = await getCard(client, appname, cardId);
88
+ if (!card) return false;
89
+
90
+ // Mark as deleted
91
+ const deleted = {
92
+ ...card,
93
+ id: cardId,
94
+ _deleted: true,
95
+ deletedAt: Date.now(),
96
+ };
97
+
98
+ return writeGlobal(client, appname, FEDERATION_CARDS_TABLE, deleted);
99
+ }
100
+
101
+ /**
102
+ * Dismiss a card (mark as dismissed so it won't reappear)
103
+ * @param {Object} client - NostrClient instance
104
+ * @param {string} appname - Application namespace
105
+ * @param {string} cardId - Card ID
106
+ * @returns {Promise<boolean>} Success indicator
107
+ */
108
+ export async function dismissCard(client, appname, cardId) {
109
+ const card = await getCard(client, appname, cardId);
110
+ if (!card) return false;
111
+
112
+ card.status = 'dismissed';
113
+ card.dismissedAt = Date.now();
114
+
115
+ return saveCard(client, appname, card);
116
+ }
117
+
118
+ /**
119
+ * Mark a federation request as dismissed (by request ID)
120
+ * This prevents the request from showing up again as a card
121
+ * @param {Object} client - NostrClient instance
122
+ * @param {string} appname - Application namespace
123
+ * @param {string} requestId - Request ID from federation request
124
+ * @returns {Promise<boolean>} Success indicator
125
+ */
126
+ export async function dismissRequest(client, appname, requestId) {
127
+ const entry = {
128
+ id: requestId,
129
+ dismissedAt: Date.now(),
130
+ };
131
+ return writeGlobal(client, appname, DISMISSED_REQUESTS_TABLE, entry);
132
+ }
133
+
134
+ /**
135
+ * Check if a request has been dismissed
136
+ * @param {Object} client - NostrClient instance
137
+ * @param {string} appname - Application namespace
138
+ * @param {string} requestId - Request ID
139
+ * @returns {Promise<boolean>} True if dismissed
140
+ */
141
+ export async function isRequestDismissed(client, appname, requestId) {
142
+ const entry = await readGlobal(client, appname, DISMISSED_REQUESTS_TABLE, requestId);
143
+ return !!entry;
144
+ }
145
+
146
+ /**
147
+ * Get all dismissed request IDs
148
+ * @param {Object} client - NostrClient instance
149
+ * @param {string} appname - Application namespace
150
+ * @returns {Promise<Set<string>>} Set of dismissed request IDs
151
+ */
152
+ export async function getDismissedRequestIds(client, appname) {
153
+ const entries = await getAllGlobal(client, appname, DISMISSED_REQUESTS_TABLE);
154
+ return new Set((entries || []).map(e => e.id));
155
+ }
156
+
157
+ /**
158
+ * Mark a federation response as processed
159
+ * This prevents duplicate "federation accepted" messages
160
+ * @param {Object} client - NostrClient instance
161
+ * @param {string} appname - Application namespace
162
+ * @param {string} requestId - Original request ID
163
+ * @param {string} responderPubKey - Responder's public key
164
+ * @returns {Promise<boolean>} Success indicator
165
+ */
166
+ export async function markResponseProcessed(client, appname, requestId, responderPubKey) {
167
+ const entry = {
168
+ id: `${requestId}_${responderPubKey}`,
169
+ requestId,
170
+ responderPubKey,
171
+ processedAt: Date.now(),
172
+ };
173
+ return writeGlobal(client, appname, PROCESSED_RESPONSES_TABLE, entry);
174
+ }
175
+
176
+ /**
177
+ * Check if a response has already been processed
178
+ * @param {Object} client - NostrClient instance
179
+ * @param {string} appname - Application namespace
180
+ * @param {string} requestId - Original request ID
181
+ * @param {string} responderPubKey - Responder's public key
182
+ * @returns {Promise<boolean>} True if already processed
183
+ */
184
+ export async function isResponseProcessed(client, appname, requestId, responderPubKey) {
185
+ const id = `${requestId}_${responderPubKey}`;
186
+ const entry = await readGlobal(client, appname, PROCESSED_RESPONSES_TABLE, id);
187
+ return !!entry;
188
+ }
189
+
190
+ /**
191
+ * Get all processed response IDs
192
+ * @param {Object} client - NostrClient instance
193
+ * @param {string} appname - Application namespace
194
+ * @returns {Promise<Set<string>>} Set of processed response IDs
195
+ */
196
+ export async function getProcessedResponseIds(client, appname) {
197
+ const entries = await getAllGlobal(client, appname, PROCESSED_RESPONSES_TABLE);
198
+ return new Set((entries || []).map(e => e.id));
199
+ }
200
+
201
+ /**
202
+ * Update card status
203
+ * @param {Object} client - NostrClient instance
204
+ * @param {string} appname - Application namespace
205
+ * @param {string} cardId - Card ID
206
+ * @param {string} status - New status
207
+ * @returns {Promise<boolean>} Success indicator
208
+ */
209
+ export async function updateCardStatus(client, appname, cardId, status) {
210
+ const card = await getCard(client, appname, cardId);
211
+ if (!card) return false;
212
+
213
+ card.status = status;
214
+ card.statusUpdatedAt = Date.now();
215
+
216
+ return saveCard(client, appname, card);
217
+ }
218
+
219
+ /**
220
+ * Find pending cards for a partner
221
+ * @param {Object} client - NostrClient instance
222
+ * @param {string} appname - Application namespace
223
+ * @param {string} partnerPubKey - Partner's public key
224
+ * @returns {Promise<Object[]>} Array of pending cards
225
+ */
226
+ export async function findPendingCardsForPartner(client, appname, partnerPubKey) {
227
+ const cards = await getAllCards(client, appname);
228
+ return cards.filter(c =>
229
+ c.partnerPubKey === partnerPubKey &&
230
+ c.status === 'pending'
231
+ );
232
+ }
233
+
234
+ /**
235
+ * Clean up old dismissed entries (older than maxAge)
236
+ * @param {Object} client - NostrClient instance
237
+ * @param {string} appname - Application namespace
238
+ * @param {number} maxAge - Maximum age in milliseconds (default: 30 days)
239
+ * @returns {Promise<Object>} Cleanup result
240
+ */
241
+ export async function cleanupOldEntries(client, appname, maxAge = 30 * 24 * 60 * 60 * 1000) {
242
+ const result = {
243
+ dismissedCleaned: 0,
244
+ responsesCleaned: 0,
245
+ cardsCleaned: 0,
246
+ };
247
+
248
+ const now = Date.now();
249
+ const cutoff = now - maxAge;
250
+
251
+ // Cleanup old dismissed requests
252
+ const dismissed = await getAllGlobal(client, appname, DISMISSED_REQUESTS_TABLE);
253
+ for (const entry of (dismissed || [])) {
254
+ if (entry.dismissedAt && entry.dismissedAt < cutoff) {
255
+ await writeGlobal(client, appname, DISMISSED_REQUESTS_TABLE, {
256
+ id: entry.id,
257
+ _deleted: true,
258
+ });
259
+ result.dismissedCleaned++;
260
+ }
261
+ }
262
+
263
+ // Cleanup old processed responses
264
+ const responses = await getAllGlobal(client, appname, PROCESSED_RESPONSES_TABLE);
265
+ for (const entry of (responses || [])) {
266
+ if (entry.processedAt && entry.processedAt < cutoff) {
267
+ await writeGlobal(client, appname, PROCESSED_RESPONSES_TABLE, {
268
+ id: entry.id,
269
+ _deleted: true,
270
+ });
271
+ result.responsesCleaned++;
272
+ }
273
+ }
274
+
275
+ // Cleanup old dismissed cards
276
+ const cards = await getAllCards(client, appname);
277
+ for (const card of cards) {
278
+ if (card.status === 'dismissed' && card.dismissedAt && card.dismissedAt < cutoff) {
279
+ await deleteCard(client, appname, card.id);
280
+ result.cardsCleaned++;
281
+ }
282
+ }
283
+
284
+ return result;
285
+ }
286
+
287
+ export default {
288
+ saveCard,
289
+ getCard,
290
+ getAllCards,
291
+ getDisplayableCards,
292
+ getCardByPartner,
293
+ deleteCard,
294
+ dismissCard,
295
+ dismissRequest,
296
+ isRequestDismissed,
297
+ getDismissedRequestIds,
298
+ markResponseProcessed,
299
+ isResponseProcessed,
300
+ getProcessedResponseIds,
301
+ updateCardStatus,
302
+ findPendingCardsForPartner,
303
+ cleanupOldEntries,
304
+ };