incremnt 0.1.0

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,1165 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { capabilities as cliCapabilities, contractVersion, officialCommands } from './contract.js';
3
+ import { executeReadCommand } from './queries.js';
4
+
5
+ const MAX_BODY_BYTES = 10 * 1024 * 1024; // 10 MB
6
+ const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000;
7
+ const DEFAULT_RATE_LIMIT_RULES = {
8
+ 'dev-login': 10,
9
+ 'device-start': 20,
10
+ 'device-poll': 300,
11
+ 'device-approve': 30,
12
+ 'google-start': 20,
13
+ 'google-callback': 20,
14
+ 'apple-start': 20,
15
+ 'apple-callback': 20,
16
+ 'session-login': 60,
17
+ 'session-refresh': 30
18
+ };
19
+
20
+ function json(response, statusCode, payload) {
21
+ response.writeHead(statusCode, { 'content-type': 'application/json' });
22
+ response.end(JSON.stringify(payload));
23
+ }
24
+
25
+ function logRequest(request, statusCode, extra = '') {
26
+ const method = request.method ?? '?';
27
+ const rawUrl = request.url ?? '/';
28
+ const path = rawUrl.split('?')[0];
29
+ const suffix = extra ? ` ${extra}` : '';
30
+ console.log(`${method} ${path} ${statusCode}${suffix}`);
31
+ }
32
+
33
+ function unauthorized(response, request) {
34
+ if (request) logRequest(request, 401);
35
+ json(response, 401, { error: 'Unauthorized' });
36
+ }
37
+
38
+ function notFound(response, message = 'Not found') {
39
+ json(response, 404, { error: message });
40
+ }
41
+
42
+ function badRequest(response, message) {
43
+ json(response, 400, { error: message });
44
+ }
45
+
46
+ function methodNotAllowed(response, message = 'Method not allowed') {
47
+ json(response, 405, { error: message });
48
+ }
49
+
50
+ function internalError(response, error) {
51
+ console.error('Internal error:', error.message);
52
+ json(response, 500, { error: 'Internal server error' });
53
+ }
54
+
55
+ function constantTimeEqual(a, b) {
56
+ if (!a || !b) return false;
57
+ const bufA = Buffer.from(a);
58
+ const bufB = Buffer.from(b);
59
+ if (bufA.length !== bufB.length) return false;
60
+ return timingSafeEqual(bufA, bufB);
61
+ }
62
+
63
+ function bearerToken(request) {
64
+ const value = request.headers.authorization ?? '';
65
+ return value.startsWith('Bearer ') ? value.slice('Bearer '.length) : null;
66
+ }
67
+
68
+ function adminSecret(request) {
69
+ return request.headers['x-incremnt-admin-secret'] ?? '';
70
+ }
71
+
72
+ function clientAddress(request, { trustProxy = false } = {}) {
73
+ if (trustProxy) {
74
+ const forwarded = request.headers['x-forwarded-for'];
75
+ if (typeof forwarded === 'string' && forwarded.trim()) {
76
+ return forwarded.split(',')[0].trim();
77
+ }
78
+ }
79
+
80
+ return request.socket?.remoteAddress ?? 'unknown';
81
+ }
82
+
83
+ function createRateLimiter({
84
+ windowMs = DEFAULT_RATE_LIMIT_WINDOW_MS,
85
+ rules = {},
86
+ trustProxy = false
87
+ } = {}) {
88
+ const mergedRules = { ...DEFAULT_RATE_LIMIT_RULES, ...rules };
89
+ const buckets = new Map();
90
+
91
+ return {
92
+ check(request, command) {
93
+ const limit = mergedRules[command];
94
+ if (!limit) {
95
+ return { allowed: true };
96
+ }
97
+
98
+ const key = `${command}:${clientAddress(request, { trustProxy })}`;
99
+ const now = Date.now();
100
+ const bucket = buckets.get(key);
101
+
102
+ if (!bucket || bucket.resetAt <= now) {
103
+ buckets.set(key, { count: 1, resetAt: now + windowMs });
104
+ return { allowed: true };
105
+ }
106
+
107
+ if (bucket.count >= limit) {
108
+ return { allowed: false };
109
+ }
110
+
111
+ bucket.count += 1;
112
+ buckets.set(key, bucket);
113
+ return { allowed: true };
114
+ }
115
+ };
116
+ }
117
+
118
+ function routeRequest(url) {
119
+ const pathname = url.pathname;
120
+
121
+ if (pathname === '/healthz') {
122
+ return { command: 'healthz', options: {} };
123
+ }
124
+
125
+ if (pathname === '/auth/dev-login') {
126
+ return { command: 'dev-login', options: {} };
127
+ }
128
+
129
+ if (pathname === '/auth/config') {
130
+ return { command: 'auth-config', options: {} };
131
+ }
132
+
133
+ if (pathname === '/auth/session') {
134
+ return { command: 'session-login', options: {} };
135
+ }
136
+
137
+ if (pathname === '/auth/refresh') {
138
+ return { command: 'session-refresh', options: {} };
139
+ }
140
+
141
+ if (pathname === '/auth/device/start') {
142
+ return { command: 'device-start', options: {} };
143
+ }
144
+
145
+ if (pathname === '/auth/device/poll') {
146
+ return { command: 'device-poll', options: {} };
147
+ }
148
+
149
+ if (pathname === '/auth/device/approve') {
150
+ return { command: 'device-approve', options: {} };
151
+ }
152
+
153
+ if (pathname === '/auth/google/start') {
154
+ return { command: 'google-start', options: {} };
155
+ }
156
+
157
+ if (pathname === '/auth/google/callback') {
158
+ return { command: 'google-callback', options: {} };
159
+ }
160
+
161
+ if (pathname === '/auth/apple/start') {
162
+ return { command: 'apple-start', options: {} };
163
+ }
164
+
165
+ if (pathname === '/auth/apple/callback') {
166
+ return { command: 'apple-callback', options: {} };
167
+ }
168
+
169
+ if (pathname === '/admin/bootstrap-user') {
170
+ return { command: 'admin-bootstrap-user', options: {} };
171
+ }
172
+
173
+ if (pathname === '/cli/contract') {
174
+ return { command: 'contract', options: {} };
175
+ }
176
+
177
+ if (pathname === '/sync/snapshot') {
178
+ return { command: 'sync-upload', options: {} };
179
+ }
180
+
181
+ if (pathname === '/cli/sessions') {
182
+ return {
183
+ command: 'session-insights',
184
+ options: {
185
+ limit: url.searchParams.get('limit') ?? undefined
186
+ }
187
+ };
188
+ }
189
+
190
+ if (pathname === '/cli/programs') {
191
+ return { command: 'program-list', options: {} };
192
+ }
193
+
194
+ if (pathname === '/cli/programs/current') {
195
+ return { command: 'program-summary', options: {} };
196
+ }
197
+
198
+ if (pathname === '/cli/exercises/history') {
199
+ return {
200
+ command: 'exercise-history',
201
+ options: {
202
+ name: url.searchParams.get('name') ?? undefined
203
+ }
204
+ };
205
+ }
206
+
207
+ if (pathname === '/cli/records') {
208
+ return { command: 'records', options: {} };
209
+ }
210
+
211
+ const compareMatch = pathname.match(/^\/cli\/sessions\/([^/]+)\/compare$/);
212
+ if (compareMatch) {
213
+ return {
214
+ command: 'planned-vs-actual',
215
+ options: {
216
+ 'session-id': decodeURIComponent(compareMatch[1])
217
+ }
218
+ };
219
+ }
220
+
221
+ const explainMatch = pathname.match(/^\/cli\/sessions\/([^/]+)\/explain$/);
222
+ if (explainMatch) {
223
+ return {
224
+ command: 'why-did-this-change',
225
+ options: {
226
+ 'session-id': decodeURIComponent(explainMatch[1])
227
+ }
228
+ };
229
+ }
230
+
231
+ const showMatch = pathname.match(/^\/cli\/sessions\/([^/]+)$/);
232
+ if (showMatch) {
233
+ return {
234
+ command: 'session-show',
235
+ options: {
236
+ id: decodeURIComponent(showMatch[1])
237
+ }
238
+ };
239
+ }
240
+
241
+ return null;
242
+ }
243
+
244
+ async function readJsonBody(request) {
245
+ const chunks = [];
246
+ let totalSize = 0;
247
+ for await (const chunk of request) {
248
+ totalSize += chunk.length;
249
+ if (totalSize > MAX_BODY_BYTES) {
250
+ throw new Error('Request body too large.');
251
+ }
252
+ chunks.push(chunk);
253
+ }
254
+
255
+ const raw = Buffer.concat(chunks).toString('utf8');
256
+ if (!raw.trim()) {
257
+ throw new Error('Request body is required.');
258
+ }
259
+
260
+ try {
261
+ return JSON.parse(raw);
262
+ } catch {
263
+ throw new Error('Invalid JSON in request body.');
264
+ }
265
+ }
266
+
267
+ async function readUrlEncodedBody(request) {
268
+ const chunks = [];
269
+ let totalSize = 0;
270
+ for await (const chunk of request) {
271
+ totalSize += chunk.length;
272
+ if (totalSize > MAX_BODY_BYTES) {
273
+ throw new Error('Request body too large.');
274
+ }
275
+ chunks.push(chunk);
276
+ }
277
+
278
+ const raw = Buffer.concat(chunks).toString('utf8');
279
+ if (!raw.trim()) {
280
+ throw new Error('Request body is required.');
281
+ }
282
+
283
+ return Object.fromEntries(new URLSearchParams(raw));
284
+ }
285
+
286
+ function html(response, statusCode, markup) {
287
+ response.writeHead(statusCode, { 'content-type': 'text/html; charset=utf-8' });
288
+ response.end(markup);
289
+ }
290
+
291
+ function deviceApprovalPage({
292
+ title,
293
+ message,
294
+ userCode = '',
295
+ email = '',
296
+ userId = '',
297
+ includeManualForm = true,
298
+ appleStartPath = null,
299
+ googleStartPath = null,
300
+ isError = false
301
+ }) {
302
+ const escapedTitle = escapeHtml(title);
303
+ const escapedMessage = escapeHtml(message);
304
+ const escapedUserCode = escapeHtml(userCode);
305
+ const escapedEmail = escapeHtml(email);
306
+ const escapedUserId = escapeHtml(userId);
307
+ const accent = isError ? '#7f1d1d' : '#16324f';
308
+ const panel = isError ? '#fef2f2' : '#f6fbff';
309
+
310
+ return `<!doctype html>
311
+ <html lang="en">
312
+ <head>
313
+ <meta charset="utf-8" />
314
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
315
+ <title>${escapedTitle}</title>
316
+ <style>
317
+ :root {
318
+ color-scheme: light;
319
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
320
+ }
321
+ body {
322
+ margin: 0;
323
+ min-height: 100vh;
324
+ display: grid;
325
+ place-items: center;
326
+ background: linear-gradient(180deg, #f7fbff 0%, #edf4ff 100%);
327
+ color: #12212f;
328
+ }
329
+ main {
330
+ width: min(92vw, 28rem);
331
+ background: white;
332
+ border-radius: 24px;
333
+ box-shadow: 0 18px 50px rgba(17, 38, 57, 0.12);
334
+ padding: 1.5rem;
335
+ }
336
+ .badge {
337
+ display: inline-block;
338
+ padding: 0.35rem 0.65rem;
339
+ border-radius: 999px;
340
+ background: ${panel};
341
+ color: ${accent};
342
+ font-size: 0.85rem;
343
+ font-weight: 600;
344
+ }
345
+ h1 {
346
+ margin: 0.9rem 0 0.5rem;
347
+ font-size: 1.6rem;
348
+ line-height: 1.2;
349
+ }
350
+ p {
351
+ margin: 0 0 1rem;
352
+ color: #41576d;
353
+ }
354
+ form {
355
+ display: grid;
356
+ gap: 0.85rem;
357
+ margin-top: 1rem;
358
+ }
359
+ .actions {
360
+ display: grid;
361
+ gap: 0.75rem;
362
+ }
363
+ .oauth-link {
364
+ display: block;
365
+ text-align: center;
366
+ text-decoration: none;
367
+ border-radius: 14px;
368
+ background: #ffffff;
369
+ color: #12212f;
370
+ border: 1px solid #d0dded;
371
+ font-weight: 700;
372
+ padding: 0.9rem 1rem;
373
+ }
374
+ label {
375
+ display: grid;
376
+ gap: 0.35rem;
377
+ font-size: 0.95rem;
378
+ font-weight: 600;
379
+ }
380
+ input {
381
+ border: 1px solid #d0dded;
382
+ border-radius: 12px;
383
+ padding: 0.8rem 0.9rem;
384
+ font: inherit;
385
+ }
386
+ button {
387
+ margin-top: 0.4rem;
388
+ border: 0;
389
+ border-radius: 14px;
390
+ background: #0f4c81;
391
+ color: white;
392
+ font: inherit;
393
+ font-weight: 700;
394
+ padding: 0.9rem 1rem;
395
+ }
396
+ small {
397
+ color: #66798b;
398
+ }
399
+ </style>
400
+ </head>
401
+ <body>
402
+ <main>
403
+ <span class="badge">incremnt device login</span>
404
+ <h1>${escapedTitle}</h1>
405
+ <p>${escapedMessage}</p>
406
+ ${(appleStartPath || googleStartPath) ? `
407
+ <div class="actions">
408
+ ${appleStartPath ? `<a class="oauth-link" href="${escapeHtml(appleStartPath)}">Continue with Apple</a>` : ''}
409
+ ${googleStartPath ? `<a class="oauth-link" href="${escapeHtml(googleStartPath)}">Continue with Google</a>` : ''}
410
+ </div>
411
+ ` : ''}
412
+ ${includeManualForm ? `
413
+ <form method="post" action="/auth/device/approve">
414
+ <label>
415
+ Approval code
416
+ <input name="userCode" value="${escapedUserCode}" autocapitalize="characters" autocomplete="one-time-code" required />
417
+ </label>
418
+ <label>
419
+ Email
420
+ <input name="email" type="email" value="${escapedEmail}" autocomplete="email" />
421
+ </label>
422
+ <label>
423
+ User ID
424
+ <input name="userId" value="${escapedUserId}" />
425
+ </label>
426
+ <button type="submit">Approve login</button>
427
+ </form>
428
+ <small>Enter the code shown by <code>incremnt login</code>. Provide either the email or user ID for the account that should own this session.</small>
429
+ ` : `
430
+ <small>Continue with a configured identity provider to approve the code shown by <code>incremnt login</code>.</small>
431
+ `}
432
+ </main>
433
+ </body>
434
+ </html>`;
435
+ }
436
+
437
+ function deviceApprovalSuccessPage({ email, userId }) {
438
+ const displayName = escapeHtml(email || userId || 'your account');
439
+ return `<!doctype html>
440
+ <html lang="en">
441
+ <head>
442
+ <meta charset="utf-8" />
443
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
444
+ <title>Connected</title>
445
+ <style>
446
+ :root {
447
+ color-scheme: light;
448
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", sans-serif;
449
+ }
450
+ body {
451
+ margin: 0;
452
+ min-height: 100vh;
453
+ display: grid;
454
+ place-items: center;
455
+ background: linear-gradient(180deg, #f7fbff 0%, #edf4ff 100%);
456
+ color: #12212f;
457
+ }
458
+ main {
459
+ width: min(92vw, 28rem);
460
+ background: white;
461
+ border-radius: 24px;
462
+ box-shadow: 0 18px 50px rgba(17, 38, 57, 0.12);
463
+ padding: 2rem 1.5rem;
464
+ text-align: center;
465
+ }
466
+ .checkmark {
467
+ font-size: 3rem;
468
+ margin-bottom: 0.5rem;
469
+ }
470
+ h1 {
471
+ margin: 0 0 0.5rem;
472
+ font-size: 1.6rem;
473
+ line-height: 1.2;
474
+ }
475
+ p {
476
+ margin: 0;
477
+ color: #41576d;
478
+ }
479
+ </style>
480
+ </head>
481
+ <body>
482
+ <main>
483
+ <div class="checkmark">&#x2705;</div>
484
+ <h1>You're all set</h1>
485
+ <p>Signed in as ${displayName}. You can close this tab and return to your terminal.</p>
486
+ </main>
487
+ </body>
488
+ </html>`;
489
+ }
490
+
491
+ function escapeHtml(value) {
492
+ return String(value ?? '')
493
+ .replaceAll('&', '&amp;')
494
+ .replaceAll('<', '&lt;')
495
+ .replaceAll('>', '&gt;')
496
+ .replaceAll('"', '&quot;')
497
+ .replaceAll("'", '&#39;');
498
+ }
499
+
500
+ export function syncServiceContractPayload({
501
+ auth = {
502
+ tokenBootstrap: true,
503
+ deviceFlow: false,
504
+ browserApproval: false,
505
+ devEmail: false
506
+ },
507
+ providers = {
508
+ apple: {
509
+ available: true,
510
+ configured: false
511
+ },
512
+ google: {
513
+ available: true,
514
+ configured: false
515
+ }
516
+ }
517
+ } = {}) {
518
+ return {
519
+ contractVersion,
520
+ binary: 'incremnt',
521
+ capabilities: {
522
+ ...cliCapabilities,
523
+ localSnapshots: false,
524
+ remoteReads: true,
525
+ remoteBootstrap: false
526
+ },
527
+ auth,
528
+ providers,
529
+ officialCommands
530
+ };
531
+ }
532
+
533
+ export function createSyncServiceRequestHandler({
534
+ loadSnapshot,
535
+ token,
536
+ authenticateToken,
537
+ authenticateReadToken,
538
+ authenticateWriteToken,
539
+ loadSnapshotForAccount,
540
+ writeSnapshotForAccount,
541
+ issueDevLogin,
542
+ issueSession,
543
+ issueDeviceChallenge,
544
+ consumeDeviceChallenge,
545
+ readDeviceChallengeByUserCode,
546
+ approveDeviceChallenge,
547
+ completeGoogleDeviceApproval,
548
+ adminBootstrapSecret,
549
+ bootstrapAccount,
550
+ appleAuth = {
551
+ available: true,
552
+ configured: false
553
+ },
554
+ buildAppleAuthUrl = null,
555
+ completeAppleDeviceApproval,
556
+ googleAuth = {
557
+ available: true,
558
+ configured: false
559
+ },
560
+ buildGoogleAuthUrl = null,
561
+ refreshSession,
562
+ allowManualDeviceApproval = false,
563
+ rateLimitConfig = null
564
+ }) {
565
+ const rateLimiter = createRateLimiter(rateLimitConfig ?? {});
566
+
567
+ return async function handle(request, response) {
568
+ try {
569
+ const url = new URL(request.url ?? '/', `http://${request.headers.host ?? '127.0.0.1'}`);
570
+ const route = routeRequest(url);
571
+ if (!route) {
572
+ notFound(response);
573
+ return;
574
+ }
575
+
576
+ if (route.command === 'healthz') {
577
+ json(response, 200, { ok: true });
578
+ return;
579
+ }
580
+
581
+ if (!rateLimiter.check(request, route.command).allowed) {
582
+ json(response, 429, { error: 'Too many requests' });
583
+ return;
584
+ }
585
+
586
+ logRequest(request, '-', route.command);
587
+
588
+ const providerApprovalAvailable = Boolean(appleAuth?.configured || googleAuth?.configured);
589
+ const manualDeviceApprovalEnabled = allowManualDeviceApproval || !providerApprovalAvailable;
590
+
591
+ if (route.command === 'auth-config') {
592
+ json(response, 200, {
593
+ ok: true,
594
+ auth: {
595
+ tokenBootstrap: Boolean(issueSession || token),
596
+ deviceFlow: Boolean(issueDeviceChallenge && consumeDeviceChallenge),
597
+ browserApproval: Boolean(approveDeviceChallenge),
598
+ devEmail: Boolean(issueDevLogin)
599
+ },
600
+ providers: {
601
+ apple: {
602
+ available: Boolean(appleAuth?.available),
603
+ configured: Boolean(appleAuth?.configured)
604
+ },
605
+ google: {
606
+ available: Boolean(googleAuth?.available),
607
+ configured: Boolean(googleAuth?.configured)
608
+ }
609
+ }
610
+ });
611
+ return;
612
+ }
613
+
614
+ if (route.command === 'dev-login') {
615
+ if (request.method !== 'POST') {
616
+ methodNotAllowed(response, 'Use POST for /auth/dev-login.');
617
+ return;
618
+ }
619
+
620
+ if (!issueDevLogin) {
621
+ methodNotAllowed(response, 'Dev login is not enabled for this service mode.');
622
+ return;
623
+ }
624
+
625
+ try {
626
+ const body = await readJsonBody(request);
627
+ if (!body.email && !body.userId) {
628
+ badRequest(response, 'email or userId is required.');
629
+ return;
630
+ }
631
+
632
+ const result = await issueDevLogin({
633
+ email: body.email ?? null,
634
+ userId: body.userId ?? null
635
+ });
636
+ json(response, 200, {
637
+ ok: true,
638
+ token: result.token,
639
+ account: result.account
640
+ });
641
+ return;
642
+ } catch (error) {
643
+ badRequest(response, error.message);
644
+ return;
645
+ }
646
+ }
647
+
648
+ if (route.command === 'device-start') {
649
+ if (request.method !== 'POST') {
650
+ methodNotAllowed(response, 'Use POST for /auth/device/start.');
651
+ return;
652
+ }
653
+
654
+ if (!issueDeviceChallenge) {
655
+ methodNotAllowed(response, 'Device login is not enabled for this service mode.');
656
+ return;
657
+ }
658
+
659
+ const challenge = await issueDeviceChallenge();
660
+ json(response, 200, {
661
+ ok: true,
662
+ deviceCode: challenge.deviceCode,
663
+ userCode: challenge.userCode,
664
+ expiresAt: challenge.expiresAt,
665
+ intervalSeconds: challenge.intervalSeconds,
666
+ verificationUri: '/auth/device/approve'
667
+ });
668
+ return;
669
+ }
670
+
671
+ if (route.command === 'device-poll') {
672
+ if (request.method !== 'POST') {
673
+ methodNotAllowed(response, 'Use POST for /auth/device/poll.');
674
+ return;
675
+ }
676
+
677
+ if (!consumeDeviceChallenge) {
678
+ methodNotAllowed(response, 'Device login is not enabled for this service mode.');
679
+ return;
680
+ }
681
+
682
+ try {
683
+ const body = await readJsonBody(request);
684
+ if (!body.deviceCode) {
685
+ badRequest(response, 'deviceCode is required.');
686
+ return;
687
+ }
688
+
689
+ const result = await consumeDeviceChallenge(body.deviceCode);
690
+ if (result.status === 'pending') {
691
+ json(response, 202, {
692
+ ok: false,
693
+ error: 'authorization_pending',
694
+ intervalSeconds: result.intervalSeconds ?? 1
695
+ });
696
+ return;
697
+ }
698
+
699
+ if (result.status === 'expired') {
700
+ json(response, 410, { ok: false, error: 'expired_token' });
701
+ return;
702
+ }
703
+
704
+ if (result.status === 'not_found') {
705
+ badRequest(response, 'Unknown deviceCode.');
706
+ return;
707
+ }
708
+
709
+ json(response, 200, {
710
+ ok: true,
711
+ session: {
712
+ accessToken: result.session.accessToken,
713
+ expiresAt: result.session.expiresAt
714
+ },
715
+ account: result.session.account
716
+ });
717
+ return;
718
+ } catch (error) {
719
+ badRequest(response, error.message);
720
+ return;
721
+ }
722
+ }
723
+
724
+ if (route.command === 'google-start') {
725
+ if (request.method !== 'GET') {
726
+ methodNotAllowed(response, 'Use GET for /auth/google/start.');
727
+ return;
728
+ }
729
+
730
+ if (!googleAuth?.configured || !buildGoogleAuthUrl || !readDeviceChallengeByUserCode) {
731
+ methodNotAllowed(response, 'Google auth is not enabled for this service mode.');
732
+ return;
733
+ }
734
+
735
+ const userCode = url.searchParams.get('userCode') ?? '';
736
+ const challenge = await readDeviceChallengeByUserCode(userCode);
737
+ if (!challenge) {
738
+ badRequest(response, 'Unknown or expired userCode.');
739
+ return;
740
+ }
741
+
742
+ response.writeHead(302, {
743
+ location: buildGoogleAuthUrl(googleAuth, {
744
+ userCode: challenge.userCode,
745
+ nonce: challenge.oauthStateNonce
746
+ })
747
+ });
748
+ response.end();
749
+ return;
750
+ }
751
+
752
+ if (route.command === 'google-callback') {
753
+ if (request.method !== 'GET') {
754
+ methodNotAllowed(response, 'Use GET for /auth/google/callback.');
755
+ return;
756
+ }
757
+
758
+ if (!googleAuth?.configured || !completeGoogleDeviceApproval) {
759
+ methodNotAllowed(response, 'Google auth is not enabled for this service mode.');
760
+ return;
761
+ }
762
+
763
+ const code = url.searchParams.get('code') ?? '';
764
+ const state = url.searchParams.get('state') ?? '';
765
+ if (!code || !state) {
766
+ badRequest(response, 'code and state are required.');
767
+ return;
768
+ }
769
+
770
+ try {
771
+ const result = await completeGoogleDeviceApproval({ code, state });
772
+ html(response, 200, deviceApprovalSuccessPage({
773
+ email: result.account.email ?? '',
774
+ userId: result.account.id
775
+ }));
776
+ return;
777
+ } catch (error) {
778
+ html(response, 400, deviceApprovalPage({
779
+ title: 'Approval failed',
780
+ message: error.message,
781
+ isError: true
782
+ }));
783
+ return;
784
+ }
785
+ }
786
+
787
+ if (route.command === 'apple-start') {
788
+ if (request.method !== 'GET') {
789
+ methodNotAllowed(response, 'Use GET for /auth/apple/start.');
790
+ return;
791
+ }
792
+
793
+ if (!appleAuth?.configured || !buildAppleAuthUrl || !readDeviceChallengeByUserCode) {
794
+ methodNotAllowed(response, 'Apple auth is not enabled for this service mode.');
795
+ return;
796
+ }
797
+
798
+ const userCode = url.searchParams.get('userCode') ?? '';
799
+ const challenge = await readDeviceChallengeByUserCode(userCode);
800
+ if (!challenge) {
801
+ badRequest(response, 'Unknown or expired userCode.');
802
+ return;
803
+ }
804
+
805
+ response.writeHead(302, {
806
+ location: buildAppleAuthUrl(appleAuth, {
807
+ userCode: challenge.userCode,
808
+ nonce: challenge.oauthStateNonce
809
+ })
810
+ });
811
+ response.end();
812
+ return;
813
+ }
814
+
815
+ if (route.command === 'apple-callback') {
816
+ if (request.method !== 'GET' && request.method !== 'POST') {
817
+ methodNotAllowed(response, 'Use GET or POST for /auth/apple/callback.');
818
+ return;
819
+ }
820
+
821
+ if (!appleAuth?.configured || !completeAppleDeviceApproval) {
822
+ methodNotAllowed(response, 'Apple auth is not enabled for this service mode.');
823
+ return;
824
+ }
825
+
826
+ let code = url.searchParams.get('code') ?? '';
827
+ let state = url.searchParams.get('state') ?? '';
828
+ if (request.method === 'POST') {
829
+ const body = await readUrlEncodedBody(request);
830
+ code = body.code ?? code;
831
+ state = body.state ?? state;
832
+ }
833
+
834
+ if (!code || !state) {
835
+ badRequest(response, 'code and state are required.');
836
+ return;
837
+ }
838
+
839
+ try {
840
+ const result = await completeAppleDeviceApproval({ code, state });
841
+ html(response, 200, deviceApprovalSuccessPage({
842
+ email: result.account.email ?? '',
843
+ userId: result.account.id
844
+ }));
845
+ return;
846
+ } catch (error) {
847
+ console.error('Apple device approval failed', {
848
+ message: error?.message ?? String(error),
849
+ method: request.method,
850
+ hasCode: Boolean(code),
851
+ hasState: Boolean(state)
852
+ });
853
+ html(response, 400, deviceApprovalPage({
854
+ title: 'Approval failed',
855
+ message: error.message,
856
+ isError: true
857
+ }));
858
+ return;
859
+ }
860
+ }
861
+
862
+ if (route.command === 'device-approve') {
863
+ if (!approveDeviceChallenge) {
864
+ methodNotAllowed(response, 'Device approval is not enabled for this service mode.');
865
+ return;
866
+ }
867
+
868
+ if (request.method === 'GET') {
869
+ html(response, 200, deviceApprovalPage({
870
+ title: 'Approve incremnt login',
871
+ message: 'Enter the approval code shown by the CLI and the account identity that should own the session.',
872
+ userCode: url.searchParams.get('userCode') ?? '',
873
+ email: url.searchParams.get('email') ?? '',
874
+ userId: url.searchParams.get('userId') ?? '',
875
+ includeManualForm: manualDeviceApprovalEnabled,
876
+ appleStartPath: appleAuth?.configured
877
+ ? `/auth/apple/start?userCode=${encodeURIComponent(url.searchParams.get('userCode') ?? '')}`
878
+ : null,
879
+ googleStartPath: googleAuth?.configured
880
+ ? `/auth/google/start?userCode=${encodeURIComponent(url.searchParams.get('userCode') ?? '')}`
881
+ : null
882
+ }));
883
+ return;
884
+ }
885
+
886
+ if (request.method !== 'POST') {
887
+ methodNotAllowed(response, 'Use GET or POST for /auth/device/approve.');
888
+ return;
889
+ }
890
+
891
+ if (!manualDeviceApprovalEnabled) {
892
+ methodNotAllowed(response, 'Manual device approval is disabled for this service.');
893
+ return;
894
+ }
895
+
896
+ try {
897
+ const contentType = request.headers['content-type'] ?? '';
898
+ const body = contentType.includes('application/json')
899
+ ? await readJsonBody(request)
900
+ : await readUrlEncodedBody(request);
901
+ const result = await approveDeviceChallenge({
902
+ deviceCode: body.deviceCode ?? null,
903
+ userCode: body.userCode ?? body.user_code ?? null,
904
+ userId: body.userId ?? body.user_id ?? null,
905
+ email: body.email ?? null
906
+ });
907
+
908
+ if (contentType.includes('application/json')) {
909
+ json(response, 200, {
910
+ ok: true,
911
+ deviceCode: result.deviceCode,
912
+ userCode: result.userCode,
913
+ account: result.account,
914
+ expiresAt: result.expiresAt
915
+ });
916
+ return;
917
+ }
918
+
919
+ html(response, 200, deviceApprovalPage({
920
+ title: 'Login approved',
921
+ message: `The session for ${result.account.email ?? result.account.id} is ready. Return to the CLI to finish login.`,
922
+ userCode: result.userCode,
923
+ email: result.account.email ?? '',
924
+ userId: result.account.id
925
+ }));
926
+ return;
927
+ } catch (error) {
928
+ html(response, 400, deviceApprovalPage({
929
+ title: 'Approval failed',
930
+ message: error.message,
931
+ userCode: url.searchParams.get('userCode') ?? '',
932
+ email: url.searchParams.get('email') ?? '',
933
+ userId: url.searchParams.get('userId') ?? '',
934
+ isError: true
935
+ }));
936
+ return;
937
+ }
938
+ }
939
+
940
+ if (route.command === 'admin-bootstrap-user') {
941
+ if (request.method !== 'POST') {
942
+ methodNotAllowed(response, 'Use POST for /admin/bootstrap-user.');
943
+ return;
944
+ }
945
+
946
+ if (!adminBootstrapSecret || !bootstrapAccount) {
947
+ methodNotAllowed(response, 'Admin bootstrap is not enabled for this service mode.');
948
+ return;
949
+ }
950
+
951
+ if (!constantTimeEqual(adminSecret(request), adminBootstrapSecret)) {
952
+ unauthorized(response, request);
953
+ return;
954
+ }
955
+
956
+ try {
957
+ const body = await readJsonBody(request);
958
+ if (!body.userId) {
959
+ badRequest(response, 'userId is required.');
960
+ return;
961
+ }
962
+
963
+ if (!body.bootstrapToken) {
964
+ badRequest(response, 'bootstrapToken is required.');
965
+ return;
966
+ }
967
+
968
+ const result = await bootstrapAccount({
969
+ userId: body.userId,
970
+ email: body.email ?? null,
971
+ bootstrapToken: body.bootstrapToken,
972
+ snapshot: body.snapshot ?? null
973
+ });
974
+ json(response, 200, {
975
+ ok: true,
976
+ userId: result.userId,
977
+ email: result.email,
978
+ snapshotPath: result.snapshotPath
979
+ });
980
+ return;
981
+ } catch (error) {
982
+ badRequest(response, error.message);
983
+ return;
984
+ }
985
+ }
986
+
987
+ const requestToken = bearerToken(request);
988
+ if (route.command === 'session-login') {
989
+ if (request.method !== 'POST') {
990
+ methodNotAllowed(response, 'Use POST for /auth/session.');
991
+ return;
992
+ }
993
+
994
+ if (!requestToken) {
995
+ unauthorized(response, request);
996
+ return;
997
+ }
998
+
999
+ const session = issueSession
1000
+ ? await issueSession(requestToken)
1001
+ : requestToken === token
1002
+ ? {
1003
+ accessToken: token,
1004
+ expiresAt: '2999-01-01T00:00:00Z',
1005
+ account: { id: 'remote-user', email: null }
1006
+ }
1007
+ : null;
1008
+
1009
+ if (!session) {
1010
+ unauthorized(response, request);
1011
+ return;
1012
+ }
1013
+
1014
+ json(response, 200, {
1015
+ ok: true,
1016
+ session: {
1017
+ accessToken: session.accessToken,
1018
+ expiresAt: session.expiresAt
1019
+ },
1020
+ account: session.account
1021
+ });
1022
+ return;
1023
+ }
1024
+
1025
+ if (route.command === 'session-refresh') {
1026
+ if (request.method !== 'POST') {
1027
+ methodNotAllowed(response, 'Use POST for /auth/refresh.');
1028
+ return;
1029
+ }
1030
+
1031
+ if (!requestToken) {
1032
+ unauthorized(response, request);
1033
+ return;
1034
+ }
1035
+
1036
+ if (!refreshSession) {
1037
+ methodNotAllowed(response, 'Session refresh is not enabled for this service mode.');
1038
+ return;
1039
+ }
1040
+
1041
+ const result = await refreshSession(requestToken);
1042
+ if (!result) {
1043
+ unauthorized(response, request);
1044
+ return;
1045
+ }
1046
+
1047
+ json(response, 200, {
1048
+ ok: true,
1049
+ session: {
1050
+ accessToken: result.accessToken,
1051
+ expiresAt: result.expiresAt
1052
+ },
1053
+ account: result.account
1054
+ });
1055
+ return;
1056
+ }
1057
+
1058
+ const readAuthenticator = authenticateReadToken ?? authenticateToken;
1059
+ const writeAuthenticator = authenticateWriteToken ?? authenticateToken;
1060
+
1061
+ if (route.command === 'contract') {
1062
+ const account = readAuthenticator
1063
+ ? await readAuthenticator(requestToken)
1064
+ : requestToken === token
1065
+ ? { id: 'remote-user', email: null }
1066
+ : null;
1067
+ if (!account) {
1068
+ unauthorized(response, request);
1069
+ return;
1070
+ }
1071
+ json(response, 200, syncServiceContractPayload({
1072
+ auth: {
1073
+ tokenBootstrap: Boolean(issueSession || token),
1074
+ deviceFlow: Boolean(issueDeviceChallenge && consumeDeviceChallenge),
1075
+ browserApproval: Boolean(approveDeviceChallenge),
1076
+ devEmail: Boolean(issueDevLogin)
1077
+ }
1078
+ }));
1079
+ return;
1080
+ }
1081
+
1082
+ if (route.command === 'sync-upload') {
1083
+ if (request.method !== 'POST' && request.method !== 'PUT') {
1084
+ methodNotAllowed(response, 'Use POST or PUT for /sync/snapshot.');
1085
+ return;
1086
+ }
1087
+
1088
+ if (!writeSnapshotForAccount) {
1089
+ methodNotAllowed(response, 'Snapshot uploads are not enabled for this service mode.');
1090
+ return;
1091
+ }
1092
+
1093
+ const account = writeAuthenticator
1094
+ ? await writeAuthenticator(requestToken)
1095
+ : requestToken === token
1096
+ ? { id: 'remote-user', email: null }
1097
+ : null;
1098
+ if (!account) {
1099
+ unauthorized(response, request);
1100
+ return;
1101
+ }
1102
+
1103
+ try {
1104
+ const snapshot = await readJsonBody(request);
1105
+ if (
1106
+ !snapshot ||
1107
+ typeof snapshot !== 'object' ||
1108
+ !Array.isArray(snapshot.sessions)
1109
+ ) {
1110
+ badRequest(
1111
+ response,
1112
+ 'Invalid snapshot: must be an object with a sessions array'
1113
+ );
1114
+ return;
1115
+ }
1116
+ const result = await writeSnapshotForAccount(account, snapshot);
1117
+ const sessionCount = snapshot.sessions?.length ?? 0;
1118
+ console.log(`snapshot uploaded (${sessionCount} sessions)`);
1119
+ json(response, 200, {
1120
+ ok: true,
1121
+ userId: account.id,
1122
+ snapshotPath: result.snapshotPath
1123
+ });
1124
+ return;
1125
+ } catch (error) {
1126
+ badRequest(response, error.message);
1127
+ return;
1128
+ }
1129
+ }
1130
+
1131
+ const account = readAuthenticator
1132
+ ? await readAuthenticator(requestToken)
1133
+ : requestToken === token
1134
+ ? { id: 'remote-user', email: null }
1135
+ : null;
1136
+ if (!account) {
1137
+ unauthorized(response, request);
1138
+ return;
1139
+ }
1140
+
1141
+ let snapshot;
1142
+ try {
1143
+ snapshot = loadSnapshotForAccount
1144
+ ? await loadSnapshotForAccount(account)
1145
+ : await loadSnapshot();
1146
+ } catch {
1147
+ snapshot = { sessions: [], programs: [], activeProgramId: null };
1148
+ }
1149
+ const result = executeReadCommand(snapshot, route.command, route.options);
1150
+ if (!result.ok) {
1151
+ if (result.error.startsWith('Session not found')) {
1152
+ notFound(response, result.error);
1153
+ return;
1154
+ }
1155
+
1156
+ badRequest(response, result.error);
1157
+ return;
1158
+ }
1159
+
1160
+ json(response, 200, result.payload);
1161
+ } catch (error) {
1162
+ internalError(response, error);
1163
+ }
1164
+ };
1165
+ }