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.
- package/CLAUDE.md +238 -0
- package/Dockerfile +20 -0
- package/Dockerfile.local +24 -0
- package/LICENSE +674 -0
- package/MANUAL-TESTING.md +399 -0
- package/README.md +119 -0
- package/TEMPLATE-FEDERATION-GO-LIVE.md +269 -0
- package/USER-TESTING-GUIDE.md +420 -0
- package/docker-compose.standalone.yml +14 -0
- package/docker-compose.yml +42 -0
- package/lib/app-handoff.js +315 -0
- package/lib/relevant-bdos-middleware.js +381 -0
- package/package.json +33 -0
- package/public/create.html +1468 -0
- package/public/index.html +117 -0
- package/public/moderate.html +465 -0
- package/public/my-tapestries.html +351 -0
- package/public/relevant-bdos.js +267 -0
- package/public/styles.css +1004 -0
- package/server.js +2914 -0
|
@@ -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
|
+
}
|