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.
- package/dist/{2019-D2OG2idw.js → 2019-DQdDE6DG.js} +2 -2
- package/dist/{2019-D2OG2idw.js.map → 2019-DQdDE6DG.js.map} +1 -1
- package/dist/{2019-EION3wKo.cjs → 2019-Ew-DTDlI.cjs} +2 -2
- package/dist/{2019-EION3wKo.cjs.map → 2019-Ew-DTDlI.cjs.map} +1 -1
- package/dist/{browser-BSniCNqO.js → browser-CckFyRI9.js} +2 -2
- package/dist/{browser-BSniCNqO.js.map → browser-CckFyRI9.js.map} +1 -1
- package/dist/browser-D2qtVhH5.cjs +2 -0
- package/dist/{browser-Cq59Ij19.cjs.map → browser-D2qtVhH5.cjs.map} +1 -1
- package/dist/cjs/holosphere.cjs +1 -1
- package/dist/esm/holosphere.js +67 -50
- package/dist/index-9sqetkAn.cjs +29 -0
- package/dist/index-9sqetkAn.cjs.map +1 -0
- package/dist/index-C0ITDyFo.cjs +13 -0
- package/dist/index-C0ITDyFo.cjs.map +1 -0
- package/dist/{index-Bl6rM1NW.js → index-DrYM1LOY.js} +2 -2
- package/dist/{index-Bl6rM1NW.js.map → index-DrYM1LOY.js.map} +1 -1
- package/dist/{index-D-jZhliX.js → index-TDDyakLc.js} +19827 -720
- package/dist/index-TDDyakLc.js.map +1 -0
- package/dist/{index-Bwg3OzRM.cjs → index-kyf1sjaC.cjs} +3 -3
- package/dist/{index-Bwg3OzRM.cjs.map → index-kyf1sjaC.cjs.map} +1 -1
- package/dist/index-lbSQUoRz.js +11663 -0
- package/dist/index-lbSQUoRz.js.map +1 -0
- package/dist/{indexeddb-storage-5eiUNsHC.js → indexeddb-storage-CXhjqwhA.js} +2 -2
- package/dist/{indexeddb-storage-5eiUNsHC.js.map → indexeddb-storage-CXhjqwhA.js.map} +1 -1
- package/dist/{indexeddb-storage-FNFUVvTJ.cjs → indexeddb-storage-DFESDYIj.cjs} +2 -2
- package/dist/{indexeddb-storage-FNFUVvTJ.cjs.map → indexeddb-storage-DFESDYIj.cjs.map} +1 -1
- package/dist/{memory-storage-DMt36uZO.cjs → memory-storage-D1tc1bjk.cjs} +2 -2
- package/dist/{memory-storage-DMt36uZO.cjs.map → memory-storage-D1tc1bjk.cjs.map} +1 -1
- package/dist/{memory-storage-CI-gfmuG.js → memory-storage-DkewsdcM.js} +2 -2
- package/dist/{memory-storage-CI-gfmuG.js.map → memory-storage-DkewsdcM.js.map} +1 -1
- package/docs/FEDERATION.md +474 -0
- package/package.json +3 -1
- package/src/crypto/nostr-utils.js +7 -0
- package/src/crypto/secp256k1.js +46 -29
- package/src/federation/capabilities.js +162 -0
- package/src/federation/card-storage.js +304 -0
- package/src/federation/handshake.js +204 -7
- package/src/federation/hologram.js +167 -58
- package/src/federation/holon-registry.js +166 -0
- package/src/federation/index.js +68 -0
- package/src/federation/registry.js +110 -2
- package/src/federation/request-card.js +373 -0
- package/src/hierarchical/upcast.js +19 -3
- package/src/index.js +162 -76
- package/src/lib/federation-methods.js +527 -5
- package/src/storage/nostr-async.js +2 -2
- package/src/storage/nostr-client.js +370 -155
- package/src/storage/nostr-wrapper.js +1 -1
- package/dist/_commonjsHelpers-C37NGDzP.cjs +0 -2
- package/dist/_commonjsHelpers-C37NGDzP.cjs.map +0 -1
- package/dist/_commonjsHelpers-CUmg6egw.js +0 -7
- package/dist/_commonjsHelpers-CUmg6egw.js.map +0 -1
- package/dist/browser-Cq59Ij19.cjs +0 -2
- package/dist/index-D-jZhliX.js.map +0 -1
- package/dist/index-Dc6Z8Aob.cjs +0 -18
- package/dist/index-Dc6Z8Aob.cjs.map +0 -1
- package/dist/secp256k1-CEwJNcfV.js +0 -1890
- package/dist/secp256k1-CEwJNcfV.js.map +0 -1
- package/dist/secp256k1-CiEONUnj.cjs +0 -12
- 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-
|
|
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
|
// ============================================================================
|
package/src/crypto/secp256k1.js
CHANGED
|
@@ -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 {
|
|
18
|
+
* @returns {string} Public key (hex string)
|
|
32
19
|
*/
|
|
33
|
-
export
|
|
34
|
-
const
|
|
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 =
|
|
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 =
|
|
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
|
+
};
|