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,42 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
# Planet Nine core services (existing container)
|
|
5
|
+
allyabase:
|
|
6
|
+
image: planetnine/allyabase:latest
|
|
7
|
+
container_name: planet-nine-ecosystem
|
|
8
|
+
ports:
|
|
9
|
+
- "2525:2525" # ContinueBee
|
|
10
|
+
- "2999:2999" # Julia
|
|
11
|
+
- "3000:3000" # Allyabase (wiki)
|
|
12
|
+
- "3001:3001" # Joan
|
|
13
|
+
- "3002:3002" # Pref
|
|
14
|
+
- "3003:3003" # BDO
|
|
15
|
+
- "3004:3004" # Fount
|
|
16
|
+
- "3005:3005" # Addie
|
|
17
|
+
- "3007:3007" # Dolores
|
|
18
|
+
- "7243:7243" # Aretha
|
|
19
|
+
- "7277:7277" # Sanora
|
|
20
|
+
networks:
|
|
21
|
+
- planetnine
|
|
22
|
+
|
|
23
|
+
# Linkitylink - Privacy-first link pages
|
|
24
|
+
linkitylink:
|
|
25
|
+
build:
|
|
26
|
+
context: .
|
|
27
|
+
dockerfile: Dockerfile
|
|
28
|
+
container_name: linkitylink
|
|
29
|
+
ports:
|
|
30
|
+
- "3010:3010"
|
|
31
|
+
environment:
|
|
32
|
+
- PORT=3010
|
|
33
|
+
- BDO_BASE_URL=http://allyabase:3003/
|
|
34
|
+
depends_on:
|
|
35
|
+
- allyabase
|
|
36
|
+
networks:
|
|
37
|
+
- planetnine
|
|
38
|
+
restart: unless-stopped
|
|
39
|
+
|
|
40
|
+
networks:
|
|
41
|
+
planetnine:
|
|
42
|
+
driver: bridge
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app-handoff.js - Web-to-App Handoff for Planet Nine Products
|
|
3
|
+
*
|
|
4
|
+
* Handles the flow of creating a BDO on web and handing it off to
|
|
5
|
+
* The Advancement app for discounted purchase.
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Web creates BDO with links/data (not yet purchased)
|
|
9
|
+
* 2. User selects "Buy in App" for discount
|
|
10
|
+
* 3. Authteam-style sequence game authenticates the app
|
|
11
|
+
* 4. App's pubKey becomes coordinating key for BDO
|
|
12
|
+
* 5. App displays BDO, relevantBDOs, and purchase CTA
|
|
13
|
+
* 6. After purchase, BDO added to carrierBag
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* import {
|
|
17
|
+
* createPendingHandoff,
|
|
18
|
+
* getPendingHandoff,
|
|
19
|
+
* completeHandoff,
|
|
20
|
+
* generateAuthSequence,
|
|
21
|
+
* verifyAuthSequence
|
|
22
|
+
* } from './lib/app-handoff.js';
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { randomBytes } from 'crypto';
|
|
26
|
+
|
|
27
|
+
// In-memory store for pending handoffs (keyed by handoff token)
|
|
28
|
+
// In production, use Redis or database
|
|
29
|
+
const pendingHandoffs = new Map();
|
|
30
|
+
|
|
31
|
+
// Cleanup expired handoffs every 5 minutes
|
|
32
|
+
setInterval(() => {
|
|
33
|
+
const now = Date.now();
|
|
34
|
+
for (const [token, handoff] of pendingHandoffs.entries()) {
|
|
35
|
+
if (handoff.expiresAt < now) {
|
|
36
|
+
console.log(`๐งน Cleaning up expired handoff: ${token.substring(0, 8)}...`);
|
|
37
|
+
pendingHandoffs.delete(token);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}, 5 * 60 * 1000);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Generate a secure random token
|
|
44
|
+
* @param {number} length - Length in bytes (default 32)
|
|
45
|
+
* @returns {string} Hex-encoded token
|
|
46
|
+
*/
|
|
47
|
+
function generateToken(length = 32) {
|
|
48
|
+
return randomBytes(length).toString('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate color sequence for authteam-style verification
|
|
53
|
+
* @param {number} length - Number of colors in sequence (default 5)
|
|
54
|
+
* @returns {string[]} Array of color names
|
|
55
|
+
*/
|
|
56
|
+
export function generateAuthSequence(length = 5) {
|
|
57
|
+
const colors = ['red', 'blue', 'green', 'yellow', 'purple', 'orange'];
|
|
58
|
+
const sequence = [];
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < length; i++) {
|
|
61
|
+
const randomIndex = Math.floor(Math.random() * colors.length);
|
|
62
|
+
sequence.push(colors[randomIndex]);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return sequence;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create a pending handoff for web-to-app transfer
|
|
70
|
+
*
|
|
71
|
+
* @param {Object} options
|
|
72
|
+
* @param {Object} options.bdoData - The BDO data (links, title, etc)
|
|
73
|
+
* @param {string} options.bdoPubKey - The BDO's public key
|
|
74
|
+
* @param {string} options.bdoEmojicode - The BDO's emojicode (if public)
|
|
75
|
+
* @param {Object} options.relevantBDOs - The relevantBDOs for payees
|
|
76
|
+
* @param {string} options.productType - Product type (e.g., 'linkitylink', 'bizbuz')
|
|
77
|
+
* @param {number} options.webPrice - Price in cents for web purchase
|
|
78
|
+
* @param {number} options.appPrice - Discounted price in cents for app purchase
|
|
79
|
+
* @param {number} options.expiresIn - Expiration time in ms (default 30 minutes)
|
|
80
|
+
* @returns {{ token: string, sequence: string[], expiresAt: number }}
|
|
81
|
+
*/
|
|
82
|
+
export function createPendingHandoff({
|
|
83
|
+
bdoData,
|
|
84
|
+
bdoPubKey,
|
|
85
|
+
bdoEmojicode,
|
|
86
|
+
relevantBDOs = { emojicodes: [], pubKeys: [] },
|
|
87
|
+
productType = 'linkitylink',
|
|
88
|
+
webPrice = 2000,
|
|
89
|
+
appPrice = 1500,
|
|
90
|
+
expiresIn = 30 * 60 * 1000 // 30 minutes
|
|
91
|
+
}) {
|
|
92
|
+
const token = generateToken();
|
|
93
|
+
const sequence = generateAuthSequence(5);
|
|
94
|
+
const expiresAt = Date.now() + expiresIn;
|
|
95
|
+
|
|
96
|
+
const handoff = {
|
|
97
|
+
token,
|
|
98
|
+
sequence,
|
|
99
|
+
sequenceCompleted: false,
|
|
100
|
+
bdoData,
|
|
101
|
+
bdoPubKey,
|
|
102
|
+
bdoEmojicode,
|
|
103
|
+
relevantBDOs,
|
|
104
|
+
productType,
|
|
105
|
+
webPrice,
|
|
106
|
+
appPrice,
|
|
107
|
+
appPubKey: null, // Set when app completes auth
|
|
108
|
+
appUUID: null,
|
|
109
|
+
expiresAt,
|
|
110
|
+
createdAt: Date.now(),
|
|
111
|
+
completedAt: null
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
pendingHandoffs.set(token, handoff);
|
|
115
|
+
|
|
116
|
+
console.log(`๐ฑ Created pending handoff: ${token.substring(0, 8)}... (expires in ${expiresIn / 60000} min)`);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
token,
|
|
120
|
+
sequence,
|
|
121
|
+
expiresAt
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get a pending handoff by token
|
|
127
|
+
* @param {string} token - The handoff token
|
|
128
|
+
* @returns {Object|null} The handoff data or null if not found/expired
|
|
129
|
+
*/
|
|
130
|
+
export function getPendingHandoff(token) {
|
|
131
|
+
const handoff = pendingHandoffs.get(token);
|
|
132
|
+
|
|
133
|
+
if (!handoff) {
|
|
134
|
+
console.log(`โ Handoff not found: ${token.substring(0, 8)}...`);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (handoff.expiresAt < Date.now()) {
|
|
139
|
+
console.log(`โ Handoff expired: ${token.substring(0, 8)}...`);
|
|
140
|
+
pendingHandoffs.delete(token);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return handoff;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Verify the auth sequence and mark as completed
|
|
149
|
+
* @param {string} token - The handoff token
|
|
150
|
+
* @param {string[]} submittedSequence - The sequence submitted by user
|
|
151
|
+
* @returns {{ success: boolean, error?: string }}
|
|
152
|
+
*/
|
|
153
|
+
export function verifyAuthSequence(token, submittedSequence) {
|
|
154
|
+
const handoff = getPendingHandoff(token);
|
|
155
|
+
|
|
156
|
+
if (!handoff) {
|
|
157
|
+
return { success: false, error: 'Handoff not found or expired' };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (handoff.sequenceCompleted) {
|
|
161
|
+
return { success: false, error: 'Sequence already completed' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Compare sequences
|
|
165
|
+
if (!Array.isArray(submittedSequence) ||
|
|
166
|
+
submittedSequence.length !== handoff.sequence.length) {
|
|
167
|
+
return { success: false, error: 'Invalid sequence length' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const matches = handoff.sequence.every((color, i) =>
|
|
171
|
+
color.toLowerCase() === submittedSequence[i]?.toLowerCase()
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
if (!matches) {
|
|
175
|
+
console.log(`โ Sequence mismatch for handoff: ${token.substring(0, 8)}...`);
|
|
176
|
+
return { success: false, error: 'Incorrect sequence' };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
handoff.sequenceCompleted = true;
|
|
180
|
+
console.log(`โ
Sequence verified for handoff: ${token.substring(0, 8)}...`);
|
|
181
|
+
|
|
182
|
+
return { success: true };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Associate app credentials with handoff after sequence completion
|
|
187
|
+
* Called by the app after authteam game is completed
|
|
188
|
+
*
|
|
189
|
+
* @param {string} token - The handoff token
|
|
190
|
+
* @param {Object} appCredentials
|
|
191
|
+
* @param {string} appCredentials.pubKey - App's public key
|
|
192
|
+
* @param {string} appCredentials.uuid - App's Julia UUID
|
|
193
|
+
* @param {string} appCredentials.timestamp - Timestamp for signature
|
|
194
|
+
* @param {string} appCredentials.signature - Signature proving ownership
|
|
195
|
+
* @returns {{ success: boolean, handoff?: Object, error?: string }}
|
|
196
|
+
*/
|
|
197
|
+
export function associateAppCredentials(token, appCredentials) {
|
|
198
|
+
const handoff = getPendingHandoff(token);
|
|
199
|
+
|
|
200
|
+
if (!handoff) {
|
|
201
|
+
return { success: false, error: 'Handoff not found or expired' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!handoff.sequenceCompleted) {
|
|
205
|
+
return { success: false, error: 'Sequence not yet completed' };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { pubKey, uuid, timestamp, signature } = appCredentials;
|
|
209
|
+
|
|
210
|
+
if (!pubKey || !uuid) {
|
|
211
|
+
return { success: false, error: 'Missing app credentials' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// TODO: Verify signature (timestamp + token + pubKey)
|
|
215
|
+
// For now, we trust the app credentials
|
|
216
|
+
|
|
217
|
+
handoff.appPubKey = pubKey;
|
|
218
|
+
handoff.appUUID = uuid;
|
|
219
|
+
|
|
220
|
+
console.log(`๐ฑ App associated with handoff: ${token.substring(0, 8)}... (app: ${pubKey.substring(0, 16)}...)`);
|
|
221
|
+
|
|
222
|
+
// Return the handoff data for the app to display
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
handoff: {
|
|
226
|
+
productType: handoff.productType,
|
|
227
|
+
bdoData: handoff.bdoData,
|
|
228
|
+
bdoPubKey: handoff.bdoPubKey,
|
|
229
|
+
bdoEmojicode: handoff.bdoEmojicode,
|
|
230
|
+
relevantBDOs: handoff.relevantBDOs,
|
|
231
|
+
appPrice: handoff.appPrice,
|
|
232
|
+
webPrice: handoff.webPrice,
|
|
233
|
+
discount: handoff.webPrice - handoff.appPrice
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Complete the handoff after successful purchase
|
|
240
|
+
* @param {string} token - The handoff token
|
|
241
|
+
* @returns {{ success: boolean, error?: string }}
|
|
242
|
+
*/
|
|
243
|
+
export function completeHandoff(token) {
|
|
244
|
+
const handoff = getPendingHandoff(token);
|
|
245
|
+
|
|
246
|
+
if (!handoff) {
|
|
247
|
+
return { success: false, error: 'Handoff not found or expired' };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
handoff.completedAt = Date.now();
|
|
251
|
+
|
|
252
|
+
console.log(`โ
Handoff completed: ${token.substring(0, 8)}...`);
|
|
253
|
+
|
|
254
|
+
// Keep it around for a bit for any follow-up queries, then let cleanup handle it
|
|
255
|
+
handoff.expiresAt = Date.now() + (5 * 60 * 1000); // 5 more minutes
|
|
256
|
+
|
|
257
|
+
return { success: true };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get handoff data for the app to display (after association)
|
|
262
|
+
* @param {string} token - The handoff token
|
|
263
|
+
* @param {string} appPubKey - The app's pubKey (for verification)
|
|
264
|
+
* @returns {{ success: boolean, data?: Object, error?: string }}
|
|
265
|
+
*/
|
|
266
|
+
export function getHandoffForApp(token, appPubKey) {
|
|
267
|
+
const handoff = getPendingHandoff(token);
|
|
268
|
+
|
|
269
|
+
if (!handoff) {
|
|
270
|
+
return { success: false, error: 'Handoff not found or expired' };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!handoff.appPubKey || handoff.appPubKey !== appPubKey) {
|
|
274
|
+
return { success: false, error: 'App not associated with this handoff' };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
success: true,
|
|
279
|
+
data: {
|
|
280
|
+
productType: handoff.productType,
|
|
281
|
+
bdoData: handoff.bdoData,
|
|
282
|
+
bdoPubKey: handoff.bdoPubKey,
|
|
283
|
+
bdoEmojicode: handoff.bdoEmojicode,
|
|
284
|
+
relevantBDOs: handoff.relevantBDOs,
|
|
285
|
+
appPrice: handoff.appPrice,
|
|
286
|
+
discount: handoff.webPrice - handoff.appPrice
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Get stats for debugging
|
|
293
|
+
*/
|
|
294
|
+
export function getHandoffStats() {
|
|
295
|
+
const active = Array.from(pendingHandoffs.values()).filter(
|
|
296
|
+
h => h.expiresAt > Date.now()
|
|
297
|
+
).length;
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
total: pendingHandoffs.size,
|
|
301
|
+
active,
|
|
302
|
+
expired: pendingHandoffs.size - active
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export default {
|
|
307
|
+
createPendingHandoff,
|
|
308
|
+
getPendingHandoff,
|
|
309
|
+
verifyAuthSequence,
|
|
310
|
+
associateAppCredentials,
|
|
311
|
+
completeHandoff,
|
|
312
|
+
getHandoffForApp,
|
|
313
|
+
generateAuthSequence,
|
|
314
|
+
getHandoffStats
|
|
315
|
+
};
|