instant-cli 1.0.21-branch-cli-codex.25190871505.1 → 1.0.21-branch-cli-codex-update.25190875900.1

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