instant-cli 1.0.22 → 1.0.23-branch-cli-codex-update.25390498340.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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/__tests__/authClientAddGoogle.test.ts +92 -0
  3. package/__tests__/authClientList.test.ts +90 -0
  4. package/__tests__/authClientUpdate.test.ts +583 -0
  5. package/__tests__/oauthMock.ts +9 -1
  6. package/__tests__/redirectUriPrompt.test.ts +26 -0
  7. package/dist/commands/auth/client/add.d.ts +1 -2
  8. package/dist/commands/auth/client/add.d.ts.map +1 -1
  9. package/dist/commands/auth/client/add.js +173 -276
  10. package/dist/commands/auth/client/add.js.map +1 -1
  11. package/dist/commands/auth/client/delete.d.ts +1 -2
  12. package/dist/commands/auth/client/delete.d.ts.map +1 -1
  13. package/dist/commands/auth/client/delete.js +8 -18
  14. package/dist/commands/auth/client/delete.js.map +1 -1
  15. package/dist/commands/auth/client/list.d.ts.map +1 -1
  16. package/dist/commands/auth/client/list.js +11 -2
  17. package/dist/commands/auth/client/list.js.map +1 -1
  18. package/dist/commands/auth/client/shared.d.ts +72 -0
  19. package/dist/commands/auth/client/shared.d.ts.map +1 -0
  20. package/dist/commands/auth/client/shared.js +145 -0
  21. package/dist/commands/auth/client/shared.js.map +1 -0
  22. package/dist/commands/auth/client/update.d.ts +8 -0
  23. package/dist/commands/auth/client/update.d.ts.map +1 -0
  24. package/dist/commands/auth/client/update.js +515 -0
  25. package/dist/commands/auth/client/update.js.map +1 -0
  26. package/dist/index.d.ts +5 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +60 -13
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/oauth.d.ts +114 -7
  31. package/dist/lib/oauth.d.ts.map +1 -1
  32. package/dist/lib/oauth.js +51 -1
  33. package/dist/lib/oauth.js.map +1 -1
  34. package/package.json +4 -4
  35. package/src/commands/auth/client/add.ts +251 -330
  36. package/src/commands/auth/client/delete.ts +8 -20
  37. package/src/commands/auth/client/list.ts +21 -2
  38. package/src/commands/auth/client/shared.ts +195 -0
  39. package/src/commands/auth/client/update.ts +853 -0
  40. package/src/index.ts +74 -13
  41. package/src/lib/oauth.ts +83 -1
@@ -0,0 +1,853 @@
1
+ import { Effect, Match } from 'effect';
2
+ import type { authClientUpdateDef, OptsFromCommand } from '../../../index.ts';
3
+ import { BadArgsError } from '../../../errors.ts';
4
+ import { GlobalOpts } from '../../../context/globalOpts.ts';
5
+ import { optOrPrompt, runUIEffect } from '../../../lib/ui.ts';
6
+ import {
7
+ findClientByIdOrName,
8
+ getAppsAuth,
9
+ OAuthClient,
10
+ updateOAuthClient,
11
+ } from '../../../lib/oauth.ts';
12
+ import { UI } from '../../../ui/index.ts';
13
+ import {
14
+ clerkDomainFromPublishableKey,
15
+ DEFAULT_OAUTH_CALLBACK_URL,
16
+ } from '@instantdb/platform';
17
+ import chalk from 'chalk';
18
+ import boxen from 'boxen';
19
+ import {
20
+ appleKeyIdPrompt,
21
+ applePrivateKeyFilePrompt,
22
+ appleServicesIdPrompt,
23
+ appleTeamIdPrompt,
24
+ clerkPublishableKeyPrompt,
25
+ clientIdPrompt,
26
+ clientSecretPrompt,
27
+ firebaseDiscoveryEndpoint,
28
+ firebaseProjectIdPrompt,
29
+ getFlag,
30
+ getMetaString,
31
+ hasAnyFlag,
32
+ hasFlag,
33
+ isTrueFlag,
34
+ readPrivateKeyFile,
35
+ redirectSetupMessages,
36
+ redirectUriPrompt,
37
+ validateFirebaseProjectId,
38
+ } from './shared.ts';
39
+ import { link } from '../../../logging.ts';
40
+
41
+ type OAuthClientRow = typeof OAuthClient.Type;
42
+
43
+ type ProviderRow = {
44
+ id: string;
45
+ provider_name: string;
46
+ };
47
+
48
+ const redirectPrompt = redirectUriPrompt({
49
+ heading: 'Custom redirect URI (optional):',
50
+ });
51
+ const newRedirectPrompt = redirectUriPrompt({ heading: 'New redirect URI:' });
52
+ const googleConsoleUrl =
53
+ 'https://console.developers.google.com/apis/credentials';
54
+
55
+ const resolveClient = Effect.fn(function* (params: {
56
+ id?: string;
57
+ name?: string;
58
+ }) {
59
+ const { yes } = yield* GlobalOpts;
60
+
61
+ if (params.id || params.name) {
62
+ return yield* findClientByIdOrName({
63
+ id: params.id,
64
+ name: params.name,
65
+ });
66
+ }
67
+
68
+ if (yes) {
69
+ return yield* BadArgsError.make({ message: 'Must specify --id or --name' });
70
+ }
71
+
72
+ const auth = yield* getAppsAuth();
73
+ const clients = (auth.oauth_clients ?? []) as OAuthClientRow[];
74
+
75
+ if (clients.length === 0) {
76
+ return yield* BadArgsError.make({
77
+ message: 'No OAuth clients found for this app.',
78
+ });
79
+ }
80
+
81
+ return yield* runUIEffect(
82
+ new UI.Select({
83
+ options: clients.map((client) => ({
84
+ label:
85
+ client.client_name +
86
+ (client.use_shared_credentials
87
+ ? chalk.dim(' (dev credentials)')
88
+ : '') +
89
+ chalk.dim(` (${client.id})`),
90
+ value: client,
91
+ })),
92
+ promptText: 'Select a client to update:',
93
+ modifyOutput: UI.modifiers.dimOnComplete,
94
+ }),
95
+ ).pipe(
96
+ Effect.catchTag('UIError', (e) =>
97
+ BadArgsError.make({ message: `UI error: ${e.message}` }),
98
+ ),
99
+ Effect.map((client) => ({ auth, client })),
100
+ );
101
+ });
102
+
103
+ const selectUpdateAction = Effect.fn(function* <T extends string>(
104
+ options: { label: string; value: T; secondary?: boolean }[],
105
+ ) {
106
+ return yield* runUIEffect(
107
+ new UI.Select({
108
+ options,
109
+ promptText: 'What do you want to update?',
110
+ modifyOutput: UI.modifiers.dimOnComplete,
111
+ }),
112
+ ).pipe(
113
+ Effect.catchTag('UIError', (e) =>
114
+ BadArgsError.make({ message: `UI error: ${e.message}` }),
115
+ ),
116
+ );
117
+ });
118
+
119
+ const updateGoogleToDevCredentials = Effect.fn(function* (
120
+ client: OAuthClientRow,
121
+ ) {
122
+ const response = yield* updateOAuthClient({
123
+ oauthClientId: client.id,
124
+ clientId: null,
125
+ clientSecret: null,
126
+ useSharedCredentials: true,
127
+ redirectTo: null,
128
+ });
129
+
130
+ yield* Effect.log(
131
+ boxen(
132
+ [
133
+ `Google OAuth client updated: ${response.client.client_name}`,
134
+ 'Credentials: Instant dev credentials',
135
+ `ID: ${response.client.id}`,
136
+ '',
137
+ 'No Google Console setup required.',
138
+ 'Works on localhost and Expo during development.',
139
+ '',
140
+ chalk.bold('Ready for production? Run:'),
141
+ ` instant-cli auth client update --name ${response.client.client_name} --client-id <id> --client-secret <secret>`,
142
+ ].join('\n'),
143
+ { dimBorder: true, padding: { right: 1, left: 1 } },
144
+ ),
145
+ );
146
+ });
147
+
148
+ type GoogleUpdateMode = 'dev' | 'custom' | 'redirect' | 'none';
149
+
150
+ const hasGoogleCustomCredentialFlags = (opts: Record<string, unknown>) =>
151
+ hasAnyFlag(opts, ['client-id', 'client-secret', 'custom-redirect-uri']);
152
+
153
+ const hasGoogleUpdateFlags = (opts: Record<string, unknown>) =>
154
+ isTrueFlag(getFlag(opts, 'dev-credentials')) ||
155
+ hasGoogleCustomCredentialFlags(opts);
156
+
157
+ const optOrPromptWhenNeeded = (
158
+ opts: Record<string, unknown>,
159
+ flag: string,
160
+ params: {
161
+ promptIf: boolean;
162
+ required?: boolean;
163
+ prompt: UI.TextInputProps;
164
+ },
165
+ ) =>
166
+ Effect.gen(function* () {
167
+ const value = getFlag(opts, flag);
168
+ if (value === undefined && !params.promptIf) return undefined;
169
+ return yield* optOrPrompt(value, {
170
+ simpleName: `--${flag}`,
171
+ required: params.required ?? params.promptIf,
172
+ skipIf: false,
173
+ prompt: params.prompt,
174
+ });
175
+ });
176
+
177
+ const selectGoogleUpdateMode = Effect.fn(function* ({
178
+ isWeb,
179
+ switchingFromShared,
180
+ }: {
181
+ isWeb: boolean;
182
+ switchingFromShared: boolean;
183
+ }) {
184
+ if (switchingFromShared) {
185
+ return yield* runUIEffect(
186
+ new UI.Select({
187
+ options: [
188
+ { label: 'Custom Google credentials', value: 'custom' },
189
+ {
190
+ label: 'Instant dev credentials' + chalk.dim(' (current)'),
191
+ value: 'none',
192
+ },
193
+ ],
194
+ promptText: 'Choose credential mode:',
195
+ modifyOutput: UI.modifiers.dimOnComplete,
196
+ }),
197
+ ).pipe(
198
+ Effect.catchTag('UIError', (e) =>
199
+ BadArgsError.make({ message: `UI error: ${e.message}` }),
200
+ ),
201
+ );
202
+ }
203
+
204
+ const options: { label: string; value: GoogleUpdateMode }[] = [
205
+ { label: 'Update Client ID and Client Secret', value: 'custom' },
206
+ ];
207
+
208
+ if (isWeb) {
209
+ options.push(
210
+ {
211
+ label:
212
+ 'Switch to Instant dev credentials' +
213
+ chalk.dim(' (localhost and Expo, no Google setup)'),
214
+ value: 'dev',
215
+ },
216
+ { label: 'Update redirect URI', value: 'redirect' },
217
+ );
218
+ }
219
+
220
+ return yield* selectUpdateAction(options);
221
+ });
222
+
223
+ const resolveGoogleUpdateMode = Effect.fn(function* ({
224
+ opts,
225
+ isWeb,
226
+ switchingFromShared,
227
+ }: {
228
+ opts: Record<string, unknown>;
229
+ isWeb: boolean;
230
+ switchingFromShared: boolean;
231
+ }) {
232
+ const { yes } = yield* GlobalOpts;
233
+ const devCredentialsFlag = isTrueFlag(getFlag(opts, 'dev-credentials'));
234
+ const hasProvidedSomeCustomCredentials = hasGoogleCustomCredentialFlags(opts);
235
+
236
+ if (devCredentialsFlag && !isWeb) {
237
+ return yield* BadArgsError.make({
238
+ message: '--dev-credentials is only supported for Google web clients.',
239
+ });
240
+ }
241
+
242
+ if (
243
+ !isWeb &&
244
+ (hasFlag(opts, 'client-secret') || hasFlag(opts, 'custom-redirect-uri'))
245
+ ) {
246
+ return yield* BadArgsError.make({
247
+ message:
248
+ '--client-secret and --custom-redirect-uri are only supported for Google web clients.',
249
+ });
250
+ }
251
+
252
+ if (devCredentialsFlag && hasProvidedSomeCustomCredentials) {
253
+ return yield* BadArgsError.make({
254
+ message:
255
+ '--dev-credentials cannot be combined with --client-id, --client-secret, or --custom-redirect-uri.',
256
+ });
257
+ }
258
+
259
+ if (devCredentialsFlag) {
260
+ return 'dev';
261
+ }
262
+
263
+ const hasAnyUpdateFlag = hasGoogleUpdateFlags(opts);
264
+
265
+ if (yes && !hasAnyUpdateFlag) {
266
+ return yield* BadArgsError.make({
267
+ message:
268
+ 'Must specify at least one of --client-id, --client-secret, --custom-redirect-uri, or --dev-credentials.',
269
+ });
270
+ }
271
+
272
+ if (
273
+ yes &&
274
+ isWeb &&
275
+ switchingFromShared &&
276
+ (!hasFlag(opts, 'client-id') || !hasFlag(opts, 'client-secret'))
277
+ ) {
278
+ return yield* BadArgsError.make({
279
+ message:
280
+ 'Must specify both --client-id and --client-secret when switching from Instant dev credentials to custom credentials with --yes.',
281
+ });
282
+ }
283
+
284
+ if (hasAnyUpdateFlag) {
285
+ return 'custom';
286
+ }
287
+
288
+ return yield* selectGoogleUpdateMode({ isWeb, switchingFromShared });
289
+ });
290
+
291
+ const updateGoogleRedirect = Effect.fn(function* ({
292
+ opts,
293
+ client,
294
+ }: {
295
+ opts: Record<string, unknown>;
296
+ client: OAuthClientRow;
297
+ }) {
298
+ const redirectTo = yield* optOrPromptWhenNeeded(opts, 'custom-redirect-uri', {
299
+ promptIf: true,
300
+ required: true,
301
+ prompt: newRedirectPrompt,
302
+ });
303
+ if (!redirectTo) {
304
+ return yield* BadArgsError.make({
305
+ message: 'Missing required value for --custom-redirect-uri',
306
+ });
307
+ }
308
+ const response = yield* updateOAuthClient({
309
+ oauthClientId: client.id,
310
+ redirectTo,
311
+ });
312
+ yield* Effect.log(
313
+ boxen(
314
+ [
315
+ `Google OAuth client updated: ${response.client.client_name}`,
316
+ `ID: ${response.client.id}`,
317
+ ...redirectSetupMessages({
318
+ prompt: 'Add this redirect URI in Google Console',
319
+ redirectUri: redirectTo,
320
+ showCustomRedirectInstructions: true,
321
+ }),
322
+ ].join('\n'),
323
+ { dimBorder: true, padding: { right: 1, left: 1 } },
324
+ ),
325
+ );
326
+ });
327
+
328
+ const updateGoogleCustomCredentials = Effect.fn(function* ({
329
+ opts,
330
+ client,
331
+ isWeb,
332
+ switchingFromShared,
333
+ promptCredentials,
334
+ }: {
335
+ opts: Record<string, unknown>;
336
+ client: OAuthClientRow;
337
+ isWeb: boolean;
338
+ switchingFromShared: boolean;
339
+ promptCredentials: boolean;
340
+ }) {
341
+ const mustCollectCredentials = promptCredentials || switchingFromShared;
342
+ const shouldPromptClientId =
343
+ promptCredentials || (switchingFromShared && !hasFlag(opts, 'client-id'));
344
+ const shouldPromptClientSecret =
345
+ isWeb &&
346
+ (promptCredentials ||
347
+ (switchingFromShared && !hasFlag(opts, 'client-secret')));
348
+ const shouldPromptRedirectUri =
349
+ isWeb && switchingFromShared && promptCredentials;
350
+
351
+ const clientId = yield* optOrPromptWhenNeeded(opts, 'client-id', {
352
+ promptIf: shouldPromptClientId,
353
+ required: mustCollectCredentials,
354
+ prompt: clientIdPrompt({ providerUrl: googleConsoleUrl }),
355
+ });
356
+ const clientSecret = yield* optOrPromptWhenNeeded(opts, 'client-secret', {
357
+ promptIf: shouldPromptClientSecret,
358
+ required: isWeb && mustCollectCredentials,
359
+ prompt: clientSecretPrompt({ providerUrl: googleConsoleUrl }),
360
+ });
361
+ const customRedirectUri = isWeb
362
+ ? yield* optOrPromptWhenNeeded(opts, 'custom-redirect-uri', {
363
+ promptIf: shouldPromptRedirectUri,
364
+ required: false,
365
+ prompt: redirectPrompt,
366
+ })
367
+ : undefined;
368
+
369
+ const redirectTo = switchingFromShared
370
+ ? customRedirectUri || client.redirect_to || DEFAULT_OAUTH_CALLBACK_URL
371
+ : customRedirectUri;
372
+
373
+ const response = yield* updateOAuthClient({
374
+ oauthClientId: client.id,
375
+ clientId,
376
+ clientSecret,
377
+ redirectTo,
378
+ useSharedCredentials: switchingFromShared ? false : undefined,
379
+ });
380
+
381
+ const lines = [
382
+ `Google OAuth client updated: ${response.client.client_name}`,
383
+ 'Credentials: custom',
384
+ `ID: ${response.client.id}`,
385
+ ];
386
+
387
+ if (switchingFromShared) {
388
+ lines.push('', 'This client no longer uses Instant dev credentials.');
389
+ }
390
+ if (isWeb && redirectTo) {
391
+ lines.push(
392
+ ...redirectSetupMessages({
393
+ prompt: 'Add this redirect URI in Google Console',
394
+ redirectUri: redirectTo,
395
+ showCustomRedirectInstructions: Boolean(customRedirectUri),
396
+ }),
397
+ );
398
+ }
399
+
400
+ yield* Effect.log(
401
+ boxen(lines.join('\n'), {
402
+ dimBorder: true,
403
+ padding: { right: 1, left: 1 },
404
+ }),
405
+ );
406
+ });
407
+
408
+ const handleGoogleUpdate = Effect.fn(function* (
409
+ opts: Record<string, unknown>,
410
+ client: OAuthClientRow,
411
+ ) {
412
+ const { yes } = yield* GlobalOpts;
413
+ const appType = getMetaString(client.meta, 'appType');
414
+ const isWeb = appType === 'web' || !appType;
415
+ const switchingFromShared = Boolean(client.use_shared_credentials);
416
+ const hasAnyUpdateFlag = hasGoogleUpdateFlags(opts);
417
+
418
+ if (!hasAnyUpdateFlag && !yes) {
419
+ yield* Effect.log(
420
+ `\nCurrent mode: ${
421
+ switchingFromShared
422
+ ? chalk.bold('Instant dev credentials')
423
+ : 'custom credentials'
424
+ }`,
425
+ );
426
+ }
427
+
428
+ const updateMode = yield* resolveGoogleUpdateMode({
429
+ opts,
430
+ isWeb,
431
+ switchingFromShared,
432
+ });
433
+
434
+ if (updateMode === 'dev') {
435
+ return yield* updateGoogleToDevCredentials(client);
436
+ }
437
+
438
+ if (updateMode === 'none') {
439
+ yield* Effect.log(chalk.dim('No changes made.'));
440
+ return;
441
+ }
442
+
443
+ if (updateMode === 'redirect') {
444
+ return yield* updateGoogleRedirect({ opts, client });
445
+ }
446
+
447
+ return yield* updateGoogleCustomCredentials({
448
+ opts,
449
+ client,
450
+ isWeb,
451
+ switchingFromShared,
452
+ promptCredentials: !hasAnyUpdateFlag && !yes,
453
+ });
454
+ });
455
+
456
+ const handleClientIdSecretUpdate = Effect.fn(function* (params: {
457
+ opts: Record<string, unknown>;
458
+ client: OAuthClientRow;
459
+ providerLabel: string;
460
+ providerUrl: string;
461
+ redirectSetupPrompt: string;
462
+ }) {
463
+ const { yes } = yield* GlobalOpts;
464
+ const hasAnyUpdateFlag = hasAnyFlag(params.opts, [
465
+ 'client-id',
466
+ 'client-secret',
467
+ 'custom-redirect-uri',
468
+ ]);
469
+ if (yes && !hasAnyUpdateFlag) {
470
+ return yield* BadArgsError.make({
471
+ message:
472
+ 'Must specify at least one of --client-id, --client-secret, or --custom-redirect-uri.',
473
+ });
474
+ }
475
+
476
+ let promptCredentials = false;
477
+ let promptRedirect = false;
478
+
479
+ if (!hasAnyUpdateFlag && !yes) {
480
+ const action = yield* selectUpdateAction([
481
+ { label: 'Update Client ID and Client Secret', value: 'rotate' },
482
+ { label: 'Update redirect URI', value: 'redirect' },
483
+ ]);
484
+ promptCredentials = action === 'rotate';
485
+ promptRedirect = action === 'redirect';
486
+ }
487
+
488
+ const clientId = yield* optOrPromptWhenNeeded(params.opts, 'client-id', {
489
+ promptIf: promptCredentials,
490
+ required: promptCredentials,
491
+ prompt: clientIdPrompt({ providerUrl: params.providerUrl }),
492
+ });
493
+ const clientSecret = yield* optOrPromptWhenNeeded(
494
+ params.opts,
495
+ 'client-secret',
496
+ {
497
+ promptIf: promptCredentials,
498
+ required: promptCredentials,
499
+ prompt: clientSecretPrompt({ providerUrl: params.providerUrl }),
500
+ },
501
+ );
502
+ const customRedirectUri = yield* optOrPromptWhenNeeded(
503
+ params.opts,
504
+ 'custom-redirect-uri',
505
+ {
506
+ promptIf: promptRedirect,
507
+ required: promptRedirect,
508
+ prompt: promptRedirect ? newRedirectPrompt : redirectPrompt,
509
+ },
510
+ );
511
+
512
+ const response = yield* updateOAuthClient({
513
+ oauthClientId: params.client.id,
514
+ clientId,
515
+ clientSecret,
516
+ redirectTo: customRedirectUri,
517
+ });
518
+
519
+ const lines = [
520
+ `${params.providerLabel} OAuth client updated: ${response.client.client_name}`,
521
+ `ID: ${response.client.id}`,
522
+ ];
523
+
524
+ if (customRedirectUri) {
525
+ lines.push(
526
+ ...redirectSetupMessages({
527
+ prompt: params.redirectSetupPrompt,
528
+ redirectUri: customRedirectUri,
529
+ showCustomRedirectInstructions: true,
530
+ }),
531
+ );
532
+ }
533
+
534
+ yield* Effect.log(
535
+ boxen(lines.join('\n'), {
536
+ dimBorder: true,
537
+ padding: { right: 1, left: 1 },
538
+ }),
539
+ );
540
+ });
541
+
542
+ const appleWebFlags = [
543
+ 'team-id',
544
+ 'key-id',
545
+ 'private-key-file',
546
+ 'custom-redirect-uri',
547
+ ];
548
+ const appleUpdateFlags = ['services-id', ...appleWebFlags];
549
+
550
+ const hasAppleWebFlags = (opts: Record<string, unknown>) =>
551
+ hasAnyFlag(opts, appleWebFlags);
552
+
553
+ const hasAppleUpdateFlags = (opts: Record<string, unknown>) =>
554
+ hasAnyFlag(opts, appleUpdateFlags);
555
+
556
+ const appleClientHasWebConfig = (client: OAuthClientRow) =>
557
+ Boolean(
558
+ getMetaString(client.meta, 'teamId') ||
559
+ getMetaString(client.meta, 'keyId') ||
560
+ client.redirect_to,
561
+ );
562
+
563
+ type AppleWebUpdate = {
564
+ privateKey?: string;
565
+ redirectTo?: string;
566
+ customRedirectUri?: string;
567
+ meta?: Record<string, string>;
568
+ };
569
+
570
+ const resolveAppleUpdateConfig = Effect.fn(function* ({
571
+ opts,
572
+ client,
573
+ yes,
574
+ }: {
575
+ opts: Record<string, unknown>;
576
+ client: OAuthClientRow;
577
+ yes: boolean;
578
+ }) {
579
+ const hasAnyUpdateFlag = hasAppleUpdateFlags(opts);
580
+
581
+ if (yes && !hasAnyUpdateFlag) {
582
+ return yield* BadArgsError.make({
583
+ message:
584
+ 'Must specify at least one of --services-id, --team-id, --key-id, --private-key-file, or --custom-redirect-uri.',
585
+ });
586
+ }
587
+
588
+ const promptAll = !hasAnyUpdateFlag && !yes;
589
+ if (!promptAll) {
590
+ return { promptAll, configureWeb: hasAppleWebFlags(opts) };
591
+ }
592
+
593
+ const configureWeb = yield* runUIEffect(
594
+ new UI.Confirmation({
595
+ promptText:
596
+ 'Configure web redirect flow? ' +
597
+ chalk.dim(
598
+ '(requires Team ID, Key ID, and a .p8 private key from Apple)',
599
+ ),
600
+ defaultValue: appleClientHasWebConfig(client),
601
+ }),
602
+ ).pipe(
603
+ Effect.catchTag('UIError', (e) =>
604
+ BadArgsError.make({ message: `UI error: ${e.message}` }),
605
+ ),
606
+ );
607
+
608
+ return { promptAll, configureWeb };
609
+ });
610
+
611
+ const readAppleWebUpdate = Effect.fn(function* ({
612
+ opts,
613
+ client,
614
+ promptAll,
615
+ }: {
616
+ opts: Record<string, unknown>;
617
+ client: OAuthClientRow;
618
+ promptAll: boolean;
619
+ }) {
620
+ const teamId = yield* optOrPromptWhenNeeded(opts, 'team-id', {
621
+ promptIf: promptAll,
622
+ required: promptAll,
623
+ prompt: appleTeamIdPrompt({}),
624
+ });
625
+ const keyId = yield* optOrPromptWhenNeeded(opts, 'key-id', {
626
+ promptIf: promptAll,
627
+ required: promptAll,
628
+ prompt: appleKeyIdPrompt({}),
629
+ });
630
+ const privateKeyPath = yield* optOrPromptWhenNeeded(
631
+ opts,
632
+ 'private-key-file',
633
+ {
634
+ promptIf: promptAll,
635
+ required: promptAll,
636
+ prompt: applePrivateKeyFilePrompt({}),
637
+ },
638
+ );
639
+ const privateKey = privateKeyPath
640
+ ? yield* readPrivateKeyFile(privateKeyPath)
641
+ : undefined;
642
+ const customRedirectUri = yield* optOrPromptWhenNeeded(
643
+ opts,
644
+ 'custom-redirect-uri',
645
+ {
646
+ promptIf: promptAll,
647
+ required: false,
648
+ prompt: redirectPrompt,
649
+ },
650
+ );
651
+
652
+ const meta: Record<string, string> = {};
653
+ if (teamId) meta.teamId = teamId;
654
+ if (keyId) meta.keyId = keyId;
655
+
656
+ return {
657
+ privateKey,
658
+ redirectTo: privateKey
659
+ ? customRedirectUri || client.redirect_to || DEFAULT_OAUTH_CALLBACK_URL
660
+ : customRedirectUri,
661
+ customRedirectUri,
662
+ meta: Object.keys(meta).length ? meta : undefined,
663
+ } satisfies AppleWebUpdate;
664
+ });
665
+
666
+ const handleAppleUpdate = Effect.fn(function* (
667
+ opts: Record<string, unknown>,
668
+ client: OAuthClientRow,
669
+ ) {
670
+ const { yes } = yield* GlobalOpts;
671
+ const { promptAll, configureWeb } = yield* resolveAppleUpdateConfig({
672
+ opts,
673
+ client,
674
+ yes,
675
+ });
676
+
677
+ const servicesId = yield* optOrPromptWhenNeeded(opts, 'services-id', {
678
+ promptIf: promptAll,
679
+ required: promptAll,
680
+ prompt: appleServicesIdPrompt({}),
681
+ });
682
+ const webUpdate: AppleWebUpdate = configureWeb
683
+ ? yield* readAppleWebUpdate({ opts, client, promptAll })
684
+ : {};
685
+
686
+ const response = yield* updateOAuthClient({
687
+ oauthClientId: client.id,
688
+ clientId: servicesId,
689
+ clientSecret: webUpdate.privateKey,
690
+ redirectTo: webUpdate.redirectTo,
691
+ meta: webUpdate.meta,
692
+ });
693
+
694
+ const lines = [
695
+ `Apple OAuth client updated: ${response.client.client_name}`,
696
+ `ID: ${response.client.id}`,
697
+ ];
698
+
699
+ if (webUpdate.redirectTo) {
700
+ lines.push(
701
+ ...redirectSetupMessages({
702
+ prompt: `Add this return URL under your Services ID on ${link('https://developer.apple.com', 'developer.apple.com')}`,
703
+ redirectUri: webUpdate.redirectTo,
704
+ showCustomRedirectInstructions: Boolean(webUpdate.customRedirectUri),
705
+ }),
706
+ );
707
+ }
708
+
709
+ yield* Effect.log(
710
+ boxen(lines.join('\n'), {
711
+ dimBorder: true,
712
+ padding: { right: 1, left: 1 },
713
+ }),
714
+ );
715
+ });
716
+
717
+ const handleClerkUpdate = Effect.fn(function* (
718
+ opts: Record<string, unknown>,
719
+ client: OAuthClientRow,
720
+ ) {
721
+ const { yes } = yield* GlobalOpts;
722
+ const publishableKey = yield* optOrPromptWhenNeeded(opts, 'publishable-key', {
723
+ promptIf: !yes && !hasFlag(opts, 'publishable-key'),
724
+ required: true,
725
+ prompt: clerkPublishableKeyPrompt({}),
726
+ });
727
+
728
+ if (!publishableKey) {
729
+ return yield* BadArgsError.make({
730
+ message: 'Missing required value for --publishable-key',
731
+ });
732
+ }
733
+
734
+ const domain = clerkDomainFromPublishableKey(publishableKey);
735
+ if (!domain) {
736
+ return yield* BadArgsError.make({
737
+ message: 'Invalid publishable key. Could not extract domain.',
738
+ });
739
+ }
740
+
741
+ const response = yield* updateOAuthClient({
742
+ oauthClientId: client.id,
743
+ discoveryEndpoint: `https://${domain}/.well-known/openid-configuration`,
744
+ meta: { clerkPublishableKey: publishableKey },
745
+ });
746
+
747
+ yield* Effect.log(
748
+ boxen(
749
+ [
750
+ `Clerk OAuth client updated: ${response.client.client_name}`,
751
+ `ID: ${response.client.id}`,
752
+ `Clerk Domain: https://${domain}`,
753
+ ].join('\n'),
754
+ { dimBorder: true, padding: { right: 1, left: 1 } },
755
+ ),
756
+ );
757
+ });
758
+
759
+ const handleFirebaseUpdate = Effect.fn(function* (
760
+ opts: Record<string, unknown>,
761
+ client: OAuthClientRow,
762
+ ) {
763
+ const { yes } = yield* GlobalOpts;
764
+ const projectId = yield* optOrPromptWhenNeeded(opts, 'project-id', {
765
+ promptIf: !yes && !hasFlag(opts, 'project-id'),
766
+ required: true,
767
+ prompt: firebaseProjectIdPrompt({}),
768
+ });
769
+
770
+ const validationError = validateFirebaseProjectId(projectId ?? '');
771
+ if (validationError) {
772
+ return yield* BadArgsError.make({ message: validationError });
773
+ }
774
+
775
+ const response = yield* updateOAuthClient({
776
+ oauthClientId: client.id,
777
+ discoveryEndpoint: firebaseDiscoveryEndpoint(projectId!),
778
+ });
779
+
780
+ yield* Effect.log(
781
+ boxen(
782
+ [
783
+ `Firebase OAuth client updated: ${response.client.client_name}`,
784
+ `ID: ${response.client.id}`,
785
+ `Firebase Project ID: ${projectId}`,
786
+ ].join('\n'),
787
+ { dimBorder: true, padding: { right: 1, left: 1 } },
788
+ ),
789
+ );
790
+ });
791
+
792
+ export const authClientUpdateCmd = Effect.fn(
793
+ function* (
794
+ opts: OptsFromCommand<typeof authClientUpdateDef> & Record<string, unknown>,
795
+ ) {
796
+ const { auth, client: resolvedClient } = yield* resolveClient({
797
+ id: opts.id,
798
+ name: opts.name,
799
+ });
800
+ const provider = (auth.oauth_service_providers ?? []).find(
801
+ (entry: ProviderRow) => entry.id === resolvedClient.provider_id,
802
+ );
803
+
804
+ if (!provider) {
805
+ return yield* BadArgsError.make({
806
+ message: `OAuth provider not found for client: ${resolvedClient.client_name}`,
807
+ });
808
+ }
809
+
810
+ yield* Match.value(provider.provider_name).pipe(
811
+ Match.withReturnType<Effect.Effect<void, any, any>>(),
812
+ Match.when('google', () => handleGoogleUpdate(opts, resolvedClient)),
813
+ Match.when('github', () =>
814
+ handleClientIdSecretUpdate({
815
+ opts,
816
+ client: resolvedClient,
817
+ providerLabel: 'GitHub',
818
+ providerUrl: 'https://github.com/settings/developers',
819
+ redirectSetupPrompt:
820
+ 'Add this callback URL in your GitHub OAuth App settings',
821
+ }),
822
+ ),
823
+ Match.when('linkedin', () =>
824
+ handleClientIdSecretUpdate({
825
+ opts,
826
+ client: resolvedClient,
827
+ providerLabel: 'LinkedIn',
828
+ providerUrl: 'https://www.linkedin.com/developers/apps',
829
+ redirectSetupPrompt:
830
+ 'Add this redirect URI in your LinkedIn app settings',
831
+ }),
832
+ ),
833
+ Match.when('apple', () => handleAppleUpdate(opts, resolvedClient)),
834
+ Match.when('clerk', () => handleClerkUpdate(opts, resolvedClient)),
835
+ Match.when('firebase', () => handleFirebaseUpdate(opts, resolvedClient)),
836
+ Match.orElse((providerName) =>
837
+ BadArgsError.make({
838
+ message: `Updating ${providerName} OAuth clients is not supported.`,
839
+ }),
840
+ ),
841
+ );
842
+ },
843
+ Effect.catchTag('BadArgsError', (e) =>
844
+ Effect.gen(function* () {
845
+ yield* Effect.logError(e.message);
846
+ yield* Effect.log(
847
+ chalk.dim(
848
+ 'hint: run `instant-cli auth client update --help` for available arguments',
849
+ ),
850
+ );
851
+ }),
852
+ ),
853
+ );