linkitylink 0.0.1

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.
@@ -0,0 +1,381 @@
1
+ /**
2
+ * relevantBDOs - Server-side middleware for Planet Nine Advancement purchases
3
+ *
4
+ * Express middleware that handles relevantBDOs in requests and sessions.
5
+ * Fetches BDOs and extracts payees for Addie payment intents.
6
+ *
7
+ * Usage:
8
+ * import {
9
+ * relevantBDOsMiddleware,
10
+ * getRelevantBDOs,
11
+ * fetchAndExtractPayees,
12
+ * configureBdoLib
13
+ * } from './lib/relevant-bdos-middleware.js';
14
+ *
15
+ * // Configure bdo-js instance (must be called before fetching)
16
+ * configureBdoLib(bdoLib);
17
+ *
18
+ * // Add middleware to Express app
19
+ * app.use(relevantBDOsMiddleware);
20
+ *
21
+ * // In your route handlers:
22
+ * app.post('/create-payment-intent', async (req, res) => {
23
+ * const relevantBDOs = getRelevantBDOs(req);
24
+ * const payees = await fetchAndExtractPayees(relevantBDOs);
25
+ * // Pass payees to Addie getPaymentIntent
26
+ * });
27
+ */
28
+
29
+ import fetch from 'node-fetch';
30
+
31
+ // BDO library reference (set via configureBdoLib)
32
+ let bdoLibInstance = null;
33
+
34
+ /**
35
+ * Express middleware that extracts relevantBDOs from request body
36
+ * and stores them in the session
37
+ */
38
+ export function relevantBDOsMiddleware(req, res, next) {
39
+ // Extract from request body if present
40
+ if (req.body && req.body.relevantBDOs) {
41
+ const { emojicodes = [], pubKeys = [] } = req.body.relevantBDOs;
42
+
43
+ // Validate and sanitize
44
+ const sanitizedEmojicodes = emojicodes
45
+ .filter(e => typeof e === 'string' && e.length > 0)
46
+ .slice(0, 20); // Limit to 20
47
+
48
+ const sanitizedPubKeys = pubKeys
49
+ .filter(k => typeof k === 'string' && /^[0-9a-fA-F]+$/.test(k))
50
+ .slice(0, 20); // Limit to 20
51
+
52
+ // Store in session if we have any
53
+ if (sanitizedEmojicodes.length > 0 || sanitizedPubKeys.length > 0) {
54
+ req.session.relevantBDOs = {
55
+ emojicodes: sanitizedEmojicodes,
56
+ pubKeys: sanitizedPubKeys
57
+ };
58
+
59
+ console.log('๐Ÿ“ฆ relevantBDOs stored in session:', {
60
+ emojicodes: sanitizedEmojicodes.length,
61
+ pubKeys: sanitizedPubKeys.length
62
+ });
63
+ }
64
+ }
65
+
66
+ next();
67
+ }
68
+
69
+ /**
70
+ * Get relevantBDOs from request (body or session)
71
+ * @param {Request} req - Express request object
72
+ * @returns {{ emojicodes: string[], pubKeys: string[] }}
73
+ */
74
+ export function getRelevantBDOs(req) {
75
+ // First check request body (fresh data takes priority)
76
+ if (req.body && req.body.relevantBDOs) {
77
+ const { emojicodes = [], pubKeys = [] } = req.body.relevantBDOs;
78
+ return { emojicodes, pubKeys };
79
+ }
80
+
81
+ // Fall back to session
82
+ if (req.session && req.session.relevantBDOs) {
83
+ return req.session.relevantBDOs;
84
+ }
85
+
86
+ return { emojicodes: [], pubKeys: [] };
87
+ }
88
+
89
+ /**
90
+ * Set relevantBDOs in session
91
+ * @param {Request} req - Express request object
92
+ * @param {{ emojicodes?: string[], pubKeys?: string[] }} data
93
+ */
94
+ export function setRelevantBDOs(req, data) {
95
+ if (!req.session) {
96
+ console.warn('โš ๏ธ No session available for relevantBDOs');
97
+ return;
98
+ }
99
+
100
+ req.session.relevantBDOs = {
101
+ emojicodes: data.emojicodes || [],
102
+ pubKeys: data.pubKeys || []
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Clear relevantBDOs from session (call after successful purchase)
108
+ * @param {Request} req - Express request object
109
+ */
110
+ export function clearRelevantBDOs(req) {
111
+ if (req.session) {
112
+ delete req.session.relevantBDOs;
113
+ console.log('๐Ÿงน relevantBDOs cleared from session');
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Check if there are any relevantBDOs
119
+ * @param {Request} req - Express request object
120
+ * @returns {boolean}
121
+ */
122
+ export function hasRelevantBDOs(req) {
123
+ const data = getRelevantBDOs(req);
124
+ return data.emojicodes.length > 0 || data.pubKeys.length > 0;
125
+ }
126
+
127
+ /**
128
+ * Convert relevantBDOs to Stripe metadata format
129
+ * Stripe metadata: max 50 keys, 500 char values, all strings
130
+ * @param {{ emojicodes: string[], pubKeys: string[] }} data
131
+ * @returns {Object}
132
+ */
133
+ export function toStripeMetadata(data) {
134
+ const metadata = {};
135
+
136
+ // Add emojicodes (limit to prevent exceeding Stripe limits)
137
+ const emojicodes = (data.emojicodes || []).slice(0, 20);
138
+ emojicodes.forEach((emojicode, i) => {
139
+ metadata[`bdo_emoji_${i}`] = emojicode.substring(0, 500);
140
+ });
141
+
142
+ // Add pubKeys (limit to prevent exceeding Stripe limits)
143
+ const pubKeys = (data.pubKeys || []).slice(0, 20);
144
+ pubKeys.forEach((pubKey, i) => {
145
+ metadata[`bdo_pubkey_${i}`] = pubKey.substring(0, 500);
146
+ });
147
+
148
+ // Add counts for easy parsing on webhook
149
+ metadata.bdo_emoji_count = String(emojicodes.length);
150
+ metadata.bdo_pubkey_count = String(pubKeys.length);
151
+
152
+ return metadata;
153
+ }
154
+
155
+ /**
156
+ * Parse relevantBDOs from Stripe metadata (for webhook processing)
157
+ * @param {Object} metadata - Stripe payment intent metadata
158
+ * @returns {{ emojicodes: string[], pubKeys: string[] }}
159
+ */
160
+ export function fromStripeMetadata(metadata) {
161
+ const emojicodes = [];
162
+ const pubKeys = [];
163
+
164
+ const emojiCount = parseInt(metadata.bdo_emoji_count || '0', 10);
165
+ const pubKeyCount = parseInt(metadata.bdo_pubkey_count || '0', 10);
166
+
167
+ for (let i = 0; i < emojiCount; i++) {
168
+ const emojicode = metadata[`bdo_emoji_${i}`];
169
+ if (emojicode) {
170
+ emojicodes.push(emojicode);
171
+ }
172
+ }
173
+
174
+ for (let i = 0; i < pubKeyCount; i++) {
175
+ const pubKey = metadata[`bdo_pubkey_${i}`];
176
+ if (pubKey) {
177
+ pubKeys.push(pubKey);
178
+ }
179
+ }
180
+
181
+ return { emojicodes, pubKeys };
182
+ }
183
+
184
+ /**
185
+ * Log relevantBDOs for debugging
186
+ * @param {{ emojicodes: string[], pubKeys: string[] }} data
187
+ * @param {string} prefix - Log prefix
188
+ */
189
+ export function logRelevantBDOs(data, prefix = '๐Ÿ“ฆ relevantBDOs') {
190
+ const { emojicodes = [], pubKeys = [] } = data;
191
+
192
+ if (emojicodes.length === 0 && pubKeys.length === 0) {
193
+ console.log(`${prefix}: (none)`);
194
+ return;
195
+ }
196
+
197
+ console.log(`${prefix}:`);
198
+ if (emojicodes.length > 0) {
199
+ console.log(` Emojicodes (${emojicodes.length}): ${emojicodes.join(', ')}`);
200
+ }
201
+ if (pubKeys.length > 0) {
202
+ console.log(` PubKeys (${pubKeys.length}): ${pubKeys.map(k => k.substring(0, 16) + '...').join(', ')}`);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Configure the bdo-js library instance
208
+ * Must be called before using fetchAndExtractPayees
209
+ * @param {Object} bdoLib - The bdo-js library instance
210
+ */
211
+ export function configureBdoLib(bdoLib) {
212
+ bdoLibInstance = bdoLib;
213
+ console.log('๐Ÿ“ฆ relevantBDOs: bdo-js configured');
214
+ }
215
+
216
+ /**
217
+ * Fetch a BDO by emojicode and return it
218
+ * @param {string} emojicode - The emojicode to fetch
219
+ * @returns {Promise<Object|null>} The BDO data or null on error
220
+ */
221
+ async function fetchBDOByEmojicode(emojicode) {
222
+ if (!bdoLibInstance) {
223
+ console.error('โŒ bdo-js not configured. Call configureBdoLib first.');
224
+ return null;
225
+ }
226
+
227
+ try {
228
+ console.log(`๐Ÿ“ฆ Fetching BDO by emojicode: ${emojicode}`);
229
+ const result = await bdoLibInstance.getBDOByEmojicode(emojicode);
230
+ const bdo = result.bdo || result;
231
+ console.log(`โœ… Fetched BDO: ${JSON.stringify(bdo).substring(0, 100)}...`);
232
+ return bdo;
233
+ } catch (error) {
234
+ console.error(`โŒ Failed to fetch BDO by emojicode ${emojicode}:`, error.message);
235
+ return null;
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Fetch a BDO by pubKey
241
+ * Note: This uses the BDO service's pubkey endpoint
242
+ * @param {string} pubKey - The public key to fetch
243
+ * @returns {Promise<Object|null>} The BDO data or null on error
244
+ */
245
+ async function fetchBDOByPubKey(pubKey) {
246
+ if (!bdoLibInstance) {
247
+ console.error('โŒ bdo-js not configured. Call configureBdoLib first.');
248
+ return null;
249
+ }
250
+
251
+ try {
252
+ console.log(`๐Ÿ“ฆ Fetching BDO by pubKey: ${pubKey.substring(0, 16)}...`);
253
+
254
+ // BDO service has a /pubkey/:pubKey endpoint for public BDOs
255
+ const baseURL = bdoLibInstance.baseURL || 'https://dev.bdo.allyabase.com/';
256
+ const url = `${baseURL}pubkey/${pubKey}`;
257
+
258
+ const response = await fetch(url);
259
+ if (!response.ok) {
260
+ console.warn(`โš ๏ธ BDO not found for pubKey ${pubKey.substring(0, 16)}...`);
261
+ return null;
262
+ }
263
+
264
+ const result = await response.json();
265
+ const bdo = result.bdo || result;
266
+ console.log(`โœ… Fetched BDO by pubKey: ${JSON.stringify(bdo).substring(0, 100)}...`);
267
+ return bdo;
268
+ } catch (error) {
269
+ console.error(`โŒ Failed to fetch BDO by pubKey ${pubKey.substring(0, 16)}...:`, error.message);
270
+ return null;
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Extract payees array from a BDO
276
+ * @param {Object} bdo - The BDO object
277
+ * @returns {Array} The payees array or empty array if not present
278
+ */
279
+ function extractPayeesFromBDO(bdo) {
280
+ if (!bdo) return [];
281
+
282
+ // Check for payees array in various locations
283
+ const payees = bdo.payees || bdo.data?.payees || [];
284
+
285
+ if (!Array.isArray(payees)) {
286
+ console.warn('โš ๏ธ BDO payees is not an array:', typeof payees);
287
+ return [];
288
+ }
289
+
290
+ return payees;
291
+ }
292
+
293
+ /**
294
+ * Fetch all relevant BDOs and extract their payees
295
+ * Returns a deduplicated, aggregated array of payees for Addie
296
+ *
297
+ * @param {{ emojicodes: string[], pubKeys: string[] }} relevantBDOs
298
+ * @returns {Promise<Array>} Aggregated payees array
299
+ */
300
+ export async function fetchAndExtractPayees(relevantBDOs) {
301
+ const { emojicodes = [], pubKeys = [] } = relevantBDOs;
302
+
303
+ if (emojicodes.length === 0 && pubKeys.length === 0) {
304
+ console.log('๐Ÿ“ฆ No relevantBDOs to fetch payees from');
305
+ return [];
306
+ }
307
+
308
+ console.log(`๐Ÿ“ฆ Fetching payees from ${emojicodes.length} emojicodes and ${pubKeys.length} pubKeys...`);
309
+
310
+ const allPayees = [];
311
+ const seenPayeeIds = new Set(); // For deduplication
312
+
313
+ // Fetch BDOs by emojicode
314
+ const emojiFetches = emojicodes.map(async (emojicode) => {
315
+ const bdo = await fetchBDOByEmojicode(emojicode);
316
+ return { source: `emojicode:${emojicode}`, bdo };
317
+ });
318
+
319
+ // Fetch BDOs by pubKey
320
+ const pubKeyFetches = pubKeys.map(async (pubKey) => {
321
+ const bdo = await fetchBDOByPubKey(pubKey);
322
+ return { source: `pubKey:${pubKey.substring(0, 16)}...`, bdo };
323
+ });
324
+
325
+ // Wait for all fetches
326
+ const results = await Promise.all([...emojiFetches, ...pubKeyFetches]);
327
+
328
+ // Extract and aggregate payees
329
+ for (const { source, bdo } of results) {
330
+ if (!bdo) continue;
331
+
332
+ const payees = extractPayeesFromBDO(bdo);
333
+ console.log(`๐Ÿ“ฆ Extracted ${payees.length} payees from ${source}`);
334
+
335
+ for (const payee of payees) {
336
+ // Deduplicate by pubKey or uuid
337
+ const payeeId = payee.pubKey || payee.uuid || JSON.stringify(payee);
338
+ if (!seenPayeeIds.has(payeeId)) {
339
+ seenPayeeIds.add(payeeId);
340
+ allPayees.push(payee);
341
+ }
342
+ }
343
+ }
344
+
345
+ console.log(`๐Ÿ“ฆ Total aggregated payees: ${allPayees.length}`);
346
+ return allPayees;
347
+ }
348
+
349
+ /**
350
+ * Log fetched payees for debugging
351
+ * @param {Array} payees - The payees array
352
+ * @param {string} prefix - Log prefix
353
+ */
354
+ export function logPayees(payees, prefix = '๐Ÿ’ฐ Payees') {
355
+ if (!payees || payees.length === 0) {
356
+ console.log(`${prefix}: (none)`);
357
+ return;
358
+ }
359
+
360
+ console.log(`${prefix} (${payees.length}):`);
361
+ payees.forEach((payee, i) => {
362
+ const id = payee.pubKey?.substring(0, 16) || payee.uuid || 'unknown';
363
+ const amount = payee.amount || payee.share || 'unspecified';
364
+ console.log(` ${i + 1}. ${id}... (${amount})`);
365
+ });
366
+ }
367
+
368
+ // Default export for CommonJS compatibility
369
+ export default {
370
+ relevantBDOsMiddleware,
371
+ getRelevantBDOs,
372
+ setRelevantBDOs,
373
+ clearRelevantBDOs,
374
+ hasRelevantBDOs,
375
+ toStripeMetadata,
376
+ fromStripeMetadata,
377
+ logRelevantBDOs,
378
+ configureBdoLib,
379
+ fetchAndExtractPayees,
380
+ logPayees
381
+ };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "linkitylink",
3
+ "version": "0.0.1",
4
+ "description": "Linkitylink - Privacy-first link page service",
5
+ "main": "linkitylink.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "start": "node linkitylink.js",
9
+ "dev": "node linkitylink.js"
10
+ },
11
+ "keywords": [
12
+ "planet-nine",
13
+ "linkitylink",
14
+ "links",
15
+ "linktree-alternative",
16
+ "privacy",
17
+ "bdo",
18
+ "emojicode"
19
+ ],
20
+ "author": "Planet Nine",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "addie-js": "latest",
24
+ "bdo-js": "latest",
25
+ "express": "^4.18.2",
26
+ "express-session": "^1.18.2",
27
+ "fount-js": "latest",
28
+ "memorystore": "^1.6.7",
29
+ "node-fetch": "^3.3.0",
30
+ "sessionless-node": "latest",
31
+ "write-file-atomic": "^6.0.0"
32
+ }
33
+ }