@lotargo/memory_plugin 1.6.7 → 1.6.9
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/CHANGELOG.md +154 -129
- package/README.md +611 -586
- package/mcp-server/admin/auth.js +675 -645
- package/mcp-server/ingest/normalizer.js +1 -1
- package/package.json +33 -17
- package/mcp-server/benchmarks/fetch_real_corpus.js +0 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +0 -170
- package/mcp-server/benchmarks/policy_dominance_test.js +0 -221
- package/mcp-server/benchmarks/quality_evaluator.js +0 -598
- package/mcp-server/benchmarks/raw_corpus_data.js +0 -613
- package/mcp-server/benchmarks/run_benchmarks.js +0 -366
- package/mcp-server/benchmarks/stress_ingestion.js +0 -195
- package/mcp-server/benchmarks/table_code_retrieval.js +0 -453
- package/mcp-server/benchmarks/test_dual_layer.js +0 -141
package/mcp-server/admin/auth.js
CHANGED
|
@@ -1,645 +1,675 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
import crypto from "node:crypto";
|
|
3
|
-
import { spawn } from "node:child_process";
|
|
4
|
-
import { saveSecrets, deleteSecrets, loadSecrets, resolveEnvSecrets, getSecretsSource, invalidateAuthCache, onSecretsChanged } from "../config/auth_store.js";
|
|
5
|
-
import { getConfig, updateConfig } from "../config/config_manager.js";
|
|
6
|
-
|
|
7
|
-
// Register callback so resolveCloudSecrets() cache is cleared when secrets change
|
|
8
|
-
onSecretsChanged(() => { _cachedResolvedSecrets = undefined; _cachedResolvedAt = 0; });
|
|
9
|
-
|
|
10
|
-
export const TURSO_API_BASE = () => process.env.TURSO_API_BASE || "https://api.turso.tech";
|
|
11
|
-
|
|
12
|
-
function
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
console.log(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
return
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { saveSecrets, deleteSecrets, loadSecrets, resolveEnvSecrets, getSecretsSource, invalidateAuthCache, onSecretsChanged } from "../config/auth_store.js";
|
|
5
|
+
import { getConfig, updateConfig } from "../config/config_manager.js";
|
|
6
|
+
|
|
7
|
+
// Register callback so resolveCloudSecrets() cache is cleared when secrets change
|
|
8
|
+
onSecretsChanged(() => { _cachedResolvedSecrets = undefined; _cachedResolvedAt = 0; });
|
|
9
|
+
|
|
10
|
+
export const TURSO_API_BASE = () => process.env.TURSO_API_BASE || "https://api.turso.tech";
|
|
11
|
+
|
|
12
|
+
export function browserLaunchSpec(url, platform = process.platform) {
|
|
13
|
+
if (platform === "win32") {
|
|
14
|
+
// Do not route URLs through cmd.exe. '&' is a command separator there and
|
|
15
|
+
// can truncate Turso's query string before redirect/state/type parameters.
|
|
16
|
+
return { command: "rundll32.exe", args: ["url.dll,FileProtocolHandler", url] };
|
|
17
|
+
}
|
|
18
|
+
if (platform === "darwin") return { command: "open", args: [url] };
|
|
19
|
+
return { command: "xdg-open", args: [url] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function openBrowser(url) {
|
|
23
|
+
try {
|
|
24
|
+
const { command, args } = browserLaunchSpec(url);
|
|
25
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
26
|
+
child.on("error", () => {});
|
|
27
|
+
child.unref();
|
|
28
|
+
} catch {
|
|
29
|
+
// Browser auto-open is best-effort; the printed URL can be opened manually.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Starts a temporary loopback HTTP server to receive the OAuth callback.
|
|
34
|
+
// Turso redirects the browser back to the root path: /?jwt=<JWT>&username=<USERNAME>
|
|
35
|
+
// `expectedState` protects against OAuth CSRF: the callback is only accepted
|
|
36
|
+
// when it echoes the random `state` value that was embedded in the login URL.
|
|
37
|
+
export async function createAuthLoopbackServer(port = 0, expectedState = null) {
|
|
38
|
+
let resolveResult;
|
|
39
|
+
let rejectResult;
|
|
40
|
+
const result = new Promise((resolve, reject) => {
|
|
41
|
+
resolveResult = resolve;
|
|
42
|
+
rejectResult = reject;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const server = http.createServer((req, res) => {
|
|
46
|
+
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
47
|
+
const token = url.searchParams.get("jwt") || url.searchParams.get("token");
|
|
48
|
+
const username = url.searchParams.get("username");
|
|
49
|
+
const error = url.searchParams.get("error");
|
|
50
|
+
const receivedState = url.searchParams.get("state");
|
|
51
|
+
|
|
52
|
+
if (token) {
|
|
53
|
+
if (expectedState && receivedState !== expectedState) {
|
|
54
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
55
|
+
res.end("Invalid state parameter");
|
|
56
|
+
server.close(() => rejectResult(new Error("OAuth callback rejected: state parameter mismatch (possible CSRF attempt).")));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
60
|
+
res.end(`
|
|
61
|
+
<!DOCTYPE html>
|
|
62
|
+
<html lang="en">
|
|
63
|
+
<head>
|
|
64
|
+
<meta charset="utf-8" />
|
|
65
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
66
|
+
<title>Authorization Successful</title>
|
|
67
|
+
<style>
|
|
68
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
69
|
+
body {
|
|
70
|
+
min-height: 100vh;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
75
|
+
-webkit-font-smoothing: antialiased;
|
|
76
|
+
background: radial-gradient(1200px 600px at 50% -10%, #1c2028 0%, #101218 55%, #0c0e13 100%);
|
|
77
|
+
color: #e8ebf2;
|
|
78
|
+
padding: 24px;
|
|
79
|
+
}
|
|
80
|
+
.card {
|
|
81
|
+
max-width: 420px;
|
|
82
|
+
width: 100%;
|
|
83
|
+
background: #161a21;
|
|
84
|
+
border: 1px solid rgba(255, 255, 255, 0.07);
|
|
85
|
+
border-radius: 20px;
|
|
86
|
+
padding: 46px 38px;
|
|
87
|
+
text-align: center;
|
|
88
|
+
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
|
|
89
|
+
}
|
|
90
|
+
.badge {
|
|
91
|
+
width: 76px;
|
|
92
|
+
height: 76px;
|
|
93
|
+
margin: 0 auto 26px;
|
|
94
|
+
border-radius: 50%;
|
|
95
|
+
display: flex;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
background: rgba(94, 224, 154, 0.10);
|
|
99
|
+
border: 1px solid rgba(94, 224, 154, 0.28);
|
|
100
|
+
}
|
|
101
|
+
.badge svg { width: 36px; height: 36px; }
|
|
102
|
+
h1 { font-size: 22px; font-weight: 600; letter-spacing: 0.2px; color: #f2f4f8; margin-bottom: 12px; }
|
|
103
|
+
p { font-size: 14px; line-height: 1.65; color: #9aa3b2; }
|
|
104
|
+
.hint { margin-top: 24px; font-size: 12.5px; color: #6f7887; }
|
|
105
|
+
</style>
|
|
106
|
+
</head>
|
|
107
|
+
<body>
|
|
108
|
+
<div class="card">
|
|
109
|
+
<div class="badge">
|
|
110
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="#5ee09a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
111
|
+
<path d="M20 6 9 17l-5-5" />
|
|
112
|
+
</svg>
|
|
113
|
+
</div>
|
|
114
|
+
<h1>Authorization successful</h1>
|
|
115
|
+
<p>Your credentials were received and stored securely on this device.</p>
|
|
116
|
+
<div class="hint">You can now close this tab and return to the terminal.</div>
|
|
117
|
+
</div>
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
120
|
+
`);
|
|
121
|
+
|
|
122
|
+
server.close(() => {
|
|
123
|
+
resolveResult({ token, username: username || "" });
|
|
124
|
+
});
|
|
125
|
+
} else if (error) {
|
|
126
|
+
res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
|
|
127
|
+
res.end(`
|
|
128
|
+
<!DOCTYPE html>
|
|
129
|
+
<html lang="en">
|
|
130
|
+
<head>
|
|
131
|
+
<meta charset="utf-8" />
|
|
132
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
133
|
+
<title>Authorization Failed</title>
|
|
134
|
+
<style>
|
|
135
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
136
|
+
body {
|
|
137
|
+
min-height: 100vh;
|
|
138
|
+
display: flex;
|
|
139
|
+
align-items: center;
|
|
140
|
+
justify-content: center;
|
|
141
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
142
|
+
-webkit-font-smoothing: antialiased;
|
|
143
|
+
background: radial-gradient(1200px 600px at 50% -10%, #1c2028 0%, #101218 55%, #0c0e13 100%);
|
|
144
|
+
color: #e8ebf2;
|
|
145
|
+
padding: 24px;
|
|
146
|
+
}
|
|
147
|
+
.card {
|
|
148
|
+
max-width: 400px;
|
|
149
|
+
width: 100%;
|
|
150
|
+
background: #161a21;
|
|
151
|
+
border: 1px solid rgba(255, 255, 255, 0.07);
|
|
152
|
+
border-radius: 20px;
|
|
153
|
+
padding: 40px 34px;
|
|
154
|
+
text-align: center;
|
|
155
|
+
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.45);
|
|
156
|
+
}
|
|
157
|
+
h1 { font-size: 20px; font-weight: 600; color: #f2f4f8; margin-bottom: 12px; }
|
|
158
|
+
p { font-size: 14px; line-height: 1.65; color: #9aa3b2; }
|
|
159
|
+
</style>
|
|
160
|
+
</head>
|
|
161
|
+
<body>
|
|
162
|
+
<div class="card">
|
|
163
|
+
<h1>Authorization failed</h1>
|
|
164
|
+
<p>An error occurred during the login flow. Close this tab, return to the terminal, and try again.</p>
|
|
165
|
+
</div>
|
|
166
|
+
</body>
|
|
167
|
+
</html>
|
|
168
|
+
`);
|
|
169
|
+
server.close(() => rejectResult(new Error(`Authentication error: ${error}`)));
|
|
170
|
+
} else {
|
|
171
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
172
|
+
res.end("Not Found");
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await new Promise((resolve, reject) => {
|
|
177
|
+
const onError = (err) => {
|
|
178
|
+
server.off("listening", onListening);
|
|
179
|
+
reject(err);
|
|
180
|
+
};
|
|
181
|
+
const onListening = () => {
|
|
182
|
+
server.off("error", onError);
|
|
183
|
+
resolve();
|
|
184
|
+
};
|
|
185
|
+
server.once("error", onError);
|
|
186
|
+
server.once("listening", onListening);
|
|
187
|
+
server.listen(port, "127.0.0.1");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const address = server.address();
|
|
191
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
192
|
+
console.log(`\n [*] Waiting for authorization on local port http://localhost:${boundPort}/...`);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
port: boundPort,
|
|
196
|
+
result,
|
|
197
|
+
close: () => new Promise((resolve) => server.close(() => resolve())),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Backward-compatible helper used by tests/older internal callers.
|
|
202
|
+
export function startAuthLoopbackServer(port = 48900, expectedState = null) {
|
|
203
|
+
return createAuthLoopbackServer(port, expectedState).then(({ result }) => result);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function apiRequest(token, pathname, { method = "GET", body } = {}) {
|
|
207
|
+
const res = await fetch(`${TURSO_API_BASE()}${pathname}`, {
|
|
208
|
+
method,
|
|
209
|
+
headers: {
|
|
210
|
+
Authorization: `Bearer ${token}`,
|
|
211
|
+
"Content-Type": "application/json",
|
|
212
|
+
Accept: "application/json",
|
|
213
|
+
},
|
|
214
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const text = await res.text();
|
|
218
|
+
let data = null;
|
|
219
|
+
try {
|
|
220
|
+
data = JSON.parse(text);
|
|
221
|
+
} catch {}
|
|
222
|
+
|
|
223
|
+
if (!res.ok) {
|
|
224
|
+
const err = new Error(data?.error || `Turso API ${res.status}: ${text}`);
|
|
225
|
+
err.status = res.status;
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
228
|
+
return data;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Validate the account JWT obtained from OAuth and return current-user info.
|
|
232
|
+
export async function validateTursoToken(token) {
|
|
233
|
+
return apiRequest(token, "/v1/current-user");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function listOrganizations(token) {
|
|
237
|
+
const data = await apiRequest(token, "/v1/organizations");
|
|
238
|
+
const orgs = data?.organizations || [];
|
|
239
|
+
return orgs.map((o) => ({
|
|
240
|
+
slug: o.slug || o.Slug || o.id || o.Id || null,
|
|
241
|
+
name: o.name || o.Name || null,
|
|
242
|
+
id: o.id || o.Id || null,
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function listDatabases(token, org) {
|
|
247
|
+
const data = await apiRequest(token, `/v1/organizations/${encodeURIComponent(org)}/databases`);
|
|
248
|
+
const dbs = data?.databases || [];
|
|
249
|
+
return dbs.map((d) => ({
|
|
250
|
+
name: d.name || d.Name,
|
|
251
|
+
hostname: d.hostname || d.Hostname,
|
|
252
|
+
id: d.id || d.Id,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function createDatabase(token, org, name) {
|
|
257
|
+
try {
|
|
258
|
+
const data = await apiRequest(token, `/v1/organizations/${encodeURIComponent(org)}/databases`, {
|
|
259
|
+
method: "POST",
|
|
260
|
+
body: { name },
|
|
261
|
+
});
|
|
262
|
+
const d = data?.database || data;
|
|
263
|
+
return { name: d.name || d.Name, hostname: d.hostname || d.Hostname, id: d.id || d.Id };
|
|
264
|
+
} catch (err) {
|
|
265
|
+
// Fresh accounts have no default group; create one, then retry.
|
|
266
|
+
if (!String(err.message || "").toLowerCase().includes("group")) {
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
console.log(` [CLOUD] No group found. Creating group "default"...`);
|
|
270
|
+
await createGroup(token, org, "default");
|
|
271
|
+
const data = await apiRequest(token, `/v1/organizations/${encodeURIComponent(org)}/databases`, {
|
|
272
|
+
method: "POST",
|
|
273
|
+
body: { name, group: "default" },
|
|
274
|
+
});
|
|
275
|
+
const d = data?.database || data;
|
|
276
|
+
return { name: d.name || d.Name, hostname: d.hostname || d.Hostname, id: d.id || d.Id };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Turso's public closest-region endpoint (no auth required).
|
|
281
|
+
// Returns e.g. { server: "aws-eu-west-1", client: "ams" }.
|
|
282
|
+
async function getClosestLocation() {
|
|
283
|
+
if (process.env.TURSO_LOCATION) return process.env.TURSO_LOCATION;
|
|
284
|
+
const fallback = "ams";
|
|
285
|
+
try {
|
|
286
|
+
const res = await fetch("https://region.turso.io/", { signal: AbortSignal.timeout(8000) });
|
|
287
|
+
const data = await res.json().catch(() => null);
|
|
288
|
+
const loc = data?.server || data?.client || null;
|
|
289
|
+
if (loc && /^[a-z0-9-]+$/i.test(loc)) return loc;
|
|
290
|
+
} catch {
|
|
291
|
+
// ignore
|
|
292
|
+
}
|
|
293
|
+
return fallback;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function createGroup(token, org, name) {
|
|
297
|
+
// Always provide an explicit location: Turso's internal auto-lookup fails
|
|
298
|
+
// with "invalid location: Host not found" when the group has no location.
|
|
299
|
+
const location = await getClosestLocation();
|
|
300
|
+
const data = await apiRequest(token, `/v1/organizations/${encodeURIComponent(org)}/groups`, {
|
|
301
|
+
method: "POST",
|
|
302
|
+
body: { name, location },
|
|
303
|
+
});
|
|
304
|
+
return data?.group || data;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export async function createDatabaseToken(token, org, db, { expiration = "never", authorization = "full-access" } = {}) {
|
|
308
|
+
const data = await apiRequest(
|
|
309
|
+
token,
|
|
310
|
+
`/v1/organizations/${encodeURIComponent(org)}/databases/${encodeURIComponent(db)}/auth/tokens?expiration=${encodeURIComponent(expiration)}&authorization=${encodeURIComponent(authorization)}`,
|
|
311
|
+
{ method: "POST", body: {} }
|
|
312
|
+
);
|
|
313
|
+
return data?.jwt || null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function dbHostname(org, dbName) {
|
|
317
|
+
return `${dbName}-${org}.turso.io`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Shared post-auth resolution: validate happens in the caller. Steps:
|
|
321
|
+
// 1. Resolve an organization (explicit, first available, or username fallback).
|
|
322
|
+
// 2. Pick or create a database.
|
|
323
|
+
// 3. Mint a full-access token for that database.
|
|
324
|
+
// 4. Persist the encrypted token + dbUrl and mark the session as authorized.
|
|
325
|
+
async function finalizeCloudLogin({ token, username, org = null, databaseName = null, autoCreate = true, persist = true, apiToken = null }) {
|
|
326
|
+
const accountUsername = username || "user";
|
|
327
|
+
|
|
328
|
+
// Step 1: resolve organization + database namespace
|
|
329
|
+
const orgs = await listOrganizations(token);
|
|
330
|
+
let orgSlug;
|
|
331
|
+
let orgName;
|
|
332
|
+
if (orgs && orgs.length > 0) {
|
|
333
|
+
const requested = org ? orgs.find((o) => (o.slug || o.name || o.id) === org) : null;
|
|
334
|
+
const chosen = requested || orgs[0];
|
|
335
|
+
orgSlug = chosen.slug || chosen.name || chosen.id || String(chosen);
|
|
336
|
+
orgName = chosen.name || orgSlug;
|
|
337
|
+
} else {
|
|
338
|
+
// Personal accounts are not listed in /v1/organizations, but their own
|
|
339
|
+
// username acts as the organization namespace in the Platform API.
|
|
340
|
+
orgSlug = accountUsername;
|
|
341
|
+
orgName = accountUsername;
|
|
342
|
+
console.log(` [CLOUD] No organizations found. Using personal account "${orgSlug}" as the database namespace.`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const dbs = await listDatabases(token, orgSlug);
|
|
346
|
+
if (dbs.length > 0) {
|
|
347
|
+
console.log(`\n [CLOUD] Databases in organization "${orgName}":`);
|
|
348
|
+
dbs.forEach((d, i) => console.log(` ${i + 1}. ${d.name}`));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
let dbName = databaseName;
|
|
352
|
+
if (!dbName) {
|
|
353
|
+
if (dbs.length > 0) {
|
|
354
|
+
dbName = dbs[0].name;
|
|
355
|
+
console.log(`\n [CLOUD] Using existing database: "${dbName}"`);
|
|
356
|
+
} else if (autoCreate) {
|
|
357
|
+
dbName = `memory-${accountUsername}`;
|
|
358
|
+
console.log(`\n [CLOUD] No database found. Creating "${dbName}"...`);
|
|
359
|
+
await createDatabase(token, orgSlug, dbName);
|
|
360
|
+
console.log(` [OK] Database "${dbName}" created.`);
|
|
361
|
+
} else {
|
|
362
|
+
throw new Error("No databases found and autoCreate is disabled.");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Step 2: mint a full-access token for the database
|
|
367
|
+
console.log(" [CLOUD] Issuing database access token...");
|
|
368
|
+
const dbJwt = await createDatabaseToken(token, orgSlug, dbName);
|
|
369
|
+
if (!dbJwt) {
|
|
370
|
+
throw new Error("Failed to create database auth token.");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const dbUrl = `libsql://${dbHostname(orgSlug, dbName)}`;
|
|
374
|
+
|
|
375
|
+
// Step 3: persist secrets and mark authorized
|
|
376
|
+
if (persist) {
|
|
377
|
+
saveSecrets({
|
|
378
|
+
token: dbJwt,
|
|
379
|
+
dbUrl,
|
|
380
|
+
username: accountUsername,
|
|
381
|
+
org: orgSlug,
|
|
382
|
+
db: dbName,
|
|
383
|
+
authorized: true,
|
|
384
|
+
...(apiToken ? { apiToken } : {}),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
updateConfig({ tursoUrl: dbUrl, authorized: true, username: accountUsername });
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
token: dbJwt,
|
|
391
|
+
dbUrl,
|
|
392
|
+
username: accountUsername,
|
|
393
|
+
org: orgSlug,
|
|
394
|
+
db: dbName,
|
|
395
|
+
authorized: true,
|
|
396
|
+
...(apiToken ? { apiToken } : {}),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Perform the full cloud login flow:
|
|
401
|
+
// 1. OAuth browser flow against Turso (api.turso.tech).
|
|
402
|
+
// 2. Validate the received account JWT.
|
|
403
|
+
// 3. Resolve an organization and pick/create a database.
|
|
404
|
+
// 4. Mint a full-access token for that database.
|
|
405
|
+
// 5. Persist the encrypted token + dbUrl and mark the session as authorized.
|
|
406
|
+
export async function loginToCloud({
|
|
407
|
+
customPort = 0,
|
|
408
|
+
simulated = false,
|
|
409
|
+
simulatedParams = null,
|
|
410
|
+
autoCreate = true,
|
|
411
|
+
databaseName = null,
|
|
412
|
+
org = null,
|
|
413
|
+
} = {}) {
|
|
414
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
415
|
+
const loopback = await createAuthLoopbackServer(customPort, state);
|
|
416
|
+
const loginUrl = `${TURSO_API_BASE()}/?port=${loopback.port}&redirect=true&state=${state}&type=cli`;
|
|
417
|
+
|
|
418
|
+
console.log(`\n [CLOUD] Please open your system browser to authorize:`);
|
|
419
|
+
console.log(` \x1b[36m${loginUrl}\x1b[0m\n`);
|
|
420
|
+
|
|
421
|
+
let received;
|
|
422
|
+
if (simulated && simulatedParams) {
|
|
423
|
+
const req = http.request(
|
|
424
|
+
`http://127.0.0.1:${loopback.port}/?jwt=${encodeURIComponent(simulatedParams.jwt)}&username=${encodeURIComponent(simulatedParams.username)}&state=${encodeURIComponent(state)}`,
|
|
425
|
+
{ method: "GET" },
|
|
426
|
+
(res) => {
|
|
427
|
+
res.resume();
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
req.on("error", () => {});
|
|
431
|
+
req.end();
|
|
432
|
+
received = await loopback.result;
|
|
433
|
+
} else {
|
|
434
|
+
openBrowser(loginUrl);
|
|
435
|
+
received = await loopback.result;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const { token, username } = received;
|
|
439
|
+
|
|
440
|
+
// Step 2: validate the account token
|
|
441
|
+
let userInfo = null;
|
|
442
|
+
try {
|
|
443
|
+
userInfo = await validateTursoToken(token);
|
|
444
|
+
} catch (err) {
|
|
445
|
+
throw new Error(`Token validation failed: ${err.message}`);
|
|
446
|
+
}
|
|
447
|
+
const accountUsername = username || userInfo?.username || userInfo?.name || "user";
|
|
448
|
+
console.log(` [OK] Token is valid. User: ${accountUsername}`);
|
|
449
|
+
|
|
450
|
+
const secrets = await finalizeCloudLogin({ token, username: accountUsername, org, databaseName, autoCreate });
|
|
451
|
+
|
|
452
|
+
console.log(`\n \x1b[32m[OK] Successfully signed in to the cloud! Endpoint: ${secrets.dbUrl}\x1b[0m`);
|
|
453
|
+
return secrets;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Headless login with a Turso account API token (no browser, no loopback).
|
|
457
|
+
// Create one at https://console.turso.tech or via `turso auth api-tokens create`.
|
|
458
|
+
// The same Platform API is used to resolve the organization + database and to
|
|
459
|
+
// mint a per-database token, exactly like the browser flow.
|
|
460
|
+
export async function loginWithApiToken({
|
|
461
|
+
token,
|
|
462
|
+
org = null,
|
|
463
|
+
databaseName = null,
|
|
464
|
+
autoCreate = true,
|
|
465
|
+
username = null,
|
|
466
|
+
persist = true,
|
|
467
|
+
} = {}) {
|
|
468
|
+
if (!token) throw new Error("An account API token is required.");
|
|
469
|
+
console.log("\n [CLOUD] Validating account API token...");
|
|
470
|
+
let userInfo = null;
|
|
471
|
+
try {
|
|
472
|
+
userInfo = await validateTursoToken(token);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
throw new Error(`Token validation failed: ${err.message}`);
|
|
475
|
+
}
|
|
476
|
+
const accountUsername = username || userInfo?.username || userInfo?.name || "user";
|
|
477
|
+
console.log(` [OK] API token is valid. User: ${accountUsername}`);
|
|
478
|
+
|
|
479
|
+
const secrets = await finalizeCloudLogin({
|
|
480
|
+
token,
|
|
481
|
+
username: accountUsername,
|
|
482
|
+
org,
|
|
483
|
+
databaseName,
|
|
484
|
+
autoCreate,
|
|
485
|
+
persist,
|
|
486
|
+
apiToken: persist ? token : null,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
console.log(`\n \x1b[32m[OK] Successfully signed in to the cloud! Endpoint: ${secrets.dbUrl}\x1b[0m`);
|
|
490
|
+
return secrets;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Direct headless login with an existing database URL + auth token.
|
|
494
|
+
// No Platform API calls are made; org/db are derived from the endpoint.
|
|
495
|
+
export async function loginWithDatabaseToken({
|
|
496
|
+
token,
|
|
497
|
+
dbUrl,
|
|
498
|
+
username = "",
|
|
499
|
+
org = "",
|
|
500
|
+
db = "",
|
|
501
|
+
validate = true,
|
|
502
|
+
} = {}) {
|
|
503
|
+
if (!token || !dbUrl) throw new Error("Both a database auth token and a libsql:// URL are required.");
|
|
504
|
+
|
|
505
|
+
// Derive org + database name from the endpoint (libsql://<db>-<org>.turso.io)
|
|
506
|
+
let resolvedOrg = org;
|
|
507
|
+
let resolvedDb = db;
|
|
508
|
+
const m = String(dbUrl).match(/^libsql:\/\/(.+)\.turso\.io$/);
|
|
509
|
+
if (m) {
|
|
510
|
+
const host = m[1];
|
|
511
|
+
const sep = host.lastIndexOf("-");
|
|
512
|
+
if (sep > 0) {
|
|
513
|
+
resolvedDb = resolvedDb || host.slice(0, sep);
|
|
514
|
+
resolvedOrg = resolvedOrg || host.slice(sep + 1);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (validate) {
|
|
519
|
+
console.log(" [CLOUD] Validating database token against the endpoint...");
|
|
520
|
+
try {
|
|
521
|
+
const { createClient } = await import("@libsql/client");
|
|
522
|
+
const client = createClient({ url: dbUrl, authToken: token });
|
|
523
|
+
await client.execute("SELECT 1");
|
|
524
|
+
client.close();
|
|
525
|
+
console.log(" [OK] Database token validated.");
|
|
526
|
+
} catch (err) {
|
|
527
|
+
throw new Error(`Database token validation failed: ${err.message}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
saveSecrets({ token, dbUrl, username, org: resolvedOrg, db: resolvedDb, authorized: true });
|
|
532
|
+
updateConfig({ tursoUrl: dbUrl, authorized: true, username });
|
|
533
|
+
|
|
534
|
+
console.log(`\n \x1b[32m[OK] Successfully signed in to the cloud! Endpoint: ${dbUrl}\x1b[0m`);
|
|
535
|
+
return { token, dbUrl, username, org: resolvedOrg, db: resolvedDb, authorized: true };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Pick up credentials from the environment or MEMORY_DIR/.env.
|
|
539
|
+
// - TURSO_DB_URL + TURSO_DB_TOKEN: direct endpoint login (preferred, no API calls).
|
|
540
|
+
// - TURSO_API_TOKEN: account API-token flow resolving org/db via the Platform API.
|
|
541
|
+
// When persist is true the resolved secrets are also written to the encrypted store.
|
|
542
|
+
export async function loginFromEnv({ persist = false } = {}) {
|
|
543
|
+
const env = resolveEnvSecrets();
|
|
544
|
+
if (!env) {
|
|
545
|
+
return { ok: false, reason: "No cloud secrets found in the environment or MEMORY_DIR/.env." };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (env.dbUrl && env.token) {
|
|
549
|
+
const secrets = {
|
|
550
|
+
token: env.token,
|
|
551
|
+
dbUrl: env.dbUrl,
|
|
552
|
+
username: env.username || "",
|
|
553
|
+
org: env.org || "",
|
|
554
|
+
db: env.database || "",
|
|
555
|
+
authorized: true,
|
|
556
|
+
};
|
|
557
|
+
if (persist) saveSecrets(secrets);
|
|
558
|
+
updateConfig({ tursoUrl: env.dbUrl, authorized: true, username: secrets.username });
|
|
559
|
+
console.log(`\n \x1b[32m[OK] Cloud credentials imported from the environment! Endpoint: ${env.dbUrl}\x1b[0m`);
|
|
560
|
+
return { ok: true, secrets, source: "env" };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (env.apiToken) {
|
|
564
|
+
// Resolve lazily; the raw API token stays in the environment and is NOT
|
|
565
|
+
// persisted to the encrypted store (explicit `login --api-token` does that).
|
|
566
|
+
const secrets = await loginWithApiToken({
|
|
567
|
+
token: env.apiToken,
|
|
568
|
+
org: env.org || null,
|
|
569
|
+
databaseName: env.database || null,
|
|
570
|
+
username: env.username || null,
|
|
571
|
+
persist: false,
|
|
572
|
+
});
|
|
573
|
+
return { ok: true, secrets: { ...secrets, apiToken: env.apiToken }, source: "env" };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
ok: false,
|
|
578
|
+
reason: "Incomplete cloud secrets. Set TURSO_DB_URL + TURSO_DB_TOKEN (preferred) or TURSO_API_TOKEN (with optional TURSO_ORG / TURSO_DATABASE).",
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Async resolution of the working cloud credentials (a DB URL + auth token).
|
|
583
|
+
// Used by database.js at startup so a raw TURSO_API_TOKEN environment token
|
|
584
|
+
// (which can only call the Platform API, not libsql) gets minted into a
|
|
585
|
+
// per-database JWT without any interactive step.
|
|
586
|
+
let _cachedResolvedSecrets = undefined;
|
|
587
|
+
let _cachedResolvedAt = 0;
|
|
588
|
+
const RESOLVED_CACHE_TTL_MS = 60_000; // 60s — avoids re-minting JWT on every getDatabase()
|
|
589
|
+
|
|
590
|
+
export function invalidateResolvedCache() {
|
|
591
|
+
_cachedResolvedSecrets = undefined;
|
|
592
|
+
_cachedResolvedAt = 0;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export async function resolveCloudSecrets() {
|
|
596
|
+
const now = Date.now();
|
|
597
|
+
if (_cachedResolvedSecrets !== undefined && (now - _cachedResolvedAt) < RESOLVED_CACHE_TTL_MS) {
|
|
598
|
+
return _cachedResolvedSecrets;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const secrets = loadSecrets();
|
|
602
|
+
if (!secrets) { _cachedResolvedSecrets = null; return null; }
|
|
603
|
+
if (secrets.apiToken && secrets.needsResolution) {
|
|
604
|
+
const resolved = await loginWithApiToken({
|
|
605
|
+
token: secrets.apiToken,
|
|
606
|
+
org: secrets.org || null,
|
|
607
|
+
databaseName: secrets.db || null,
|
|
608
|
+
username: secrets.username || null,
|
|
609
|
+
persist: false,
|
|
610
|
+
});
|
|
611
|
+
_cachedResolvedSecrets = { ...resolved, source: "env" };
|
|
612
|
+
_cachedResolvedAt = Date.now();
|
|
613
|
+
return _cachedResolvedSecrets;
|
|
614
|
+
}
|
|
615
|
+
_cachedResolvedSecrets = secrets;
|
|
616
|
+
_cachedResolvedAt = Date.now();
|
|
617
|
+
return _cachedResolvedSecrets;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Store/replace a Turso account API token. Alias of the headless login flow:
|
|
621
|
+
// the token is validated, an org/database is resolved and a per-database JWT
|
|
622
|
+
// is minted, then both the API token and the resolved session are persisted.
|
|
623
|
+
export async function setApiKey(token, { org = null, databaseName = null } = {}) {
|
|
624
|
+
if (!token || typeof token !== "string" || !token.trim()) {
|
|
625
|
+
throw new Error("An account API token is required.");
|
|
626
|
+
}
|
|
627
|
+
const secrets = await loginWithApiToken({ token: token.trim(), org, databaseName });
|
|
628
|
+
return { ok: true, secrets };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Remove a stored API token. The resolved database session is kept as a plain
|
|
632
|
+
// browser/database session so an already-synced deployment keeps working.
|
|
633
|
+
export function clearApiKey() {
|
|
634
|
+
const existing = loadSecrets();
|
|
635
|
+
if (!existing || !existing.apiToken) return { removed: false };
|
|
636
|
+
const rest = { ...existing };
|
|
637
|
+
delete rest.apiToken;
|
|
638
|
+
if (rest.token && rest.dbUrl) {
|
|
639
|
+
saveSecrets({ ...rest, authorized: true });
|
|
640
|
+
invalidateAuthCache();
|
|
641
|
+
invalidateResolvedCache();
|
|
642
|
+
return { removed: true, keptDbSession: true };
|
|
643
|
+
}
|
|
644
|
+
deleteSecrets();
|
|
645
|
+
invalidateAuthCache();
|
|
646
|
+
invalidateResolvedCache();
|
|
647
|
+
return { removed: true, keptDbSession: false };
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Non-throwing status report used by `auth-status` and the TUI.
|
|
651
|
+
export function getAuthStatus() {
|
|
652
|
+
const secrets = loadSecrets();
|
|
653
|
+
const config = getConfig();
|
|
654
|
+
const source = getSecretsSource();
|
|
655
|
+
return {
|
|
656
|
+
source: source || "none",
|
|
657
|
+
configured: !!(secrets?.dbUrl || config.tursoUrl),
|
|
658
|
+
authorized: !!config.authorized || source === "env" || source === "api-key",
|
|
659
|
+
hasApiKey: !!secrets?.apiToken,
|
|
660
|
+
dbUrl: secrets?.dbUrl || config.tursoUrl || "",
|
|
661
|
+
username: secrets?.username || config.username || "",
|
|
662
|
+
org: secrets?.org || "",
|
|
663
|
+
database: secrets?.db || "",
|
|
664
|
+
mode: config.mode || "only-local",
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Logout and reset configurations
|
|
669
|
+
export function logoutFromCloud() {
|
|
670
|
+
const deleted = deleteSecrets();
|
|
671
|
+
invalidateAuthCache();
|
|
672
|
+
invalidateResolvedCache();
|
|
673
|
+
updateConfig({ tursoUrl: "", mode: "only-local", authorized: false, username: "" });
|
|
674
|
+
return deleted;
|
|
675
|
+
}
|