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,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
+ };