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
@@ -1,5 +1,4 @@
1
1
  import { Effect, Match, Option, Schema } from 'effect';
2
- import { FileSystem } from '@effect/platform';
3
2
  import type { authClientAddDef, OptsFromCommand } from '../../../index.ts';
4
3
  import { BadArgsError } from '../../../errors.ts';
5
4
  import { GlobalOpts } from '../../../context/globalOpts.ts';
@@ -7,7 +6,6 @@ import {
7
6
  optOrPrompt,
8
7
  optOrPromptBoolean,
9
8
  runUIEffect,
10
- stripFirstBlankLine,
11
9
  validateRequired,
12
10
  } from '../../../lib/ui.ts';
13
11
  import {
@@ -15,6 +13,8 @@ import {
15
13
  findName,
16
14
  getClientNameAndProvider,
17
15
  getOrCreateProvider,
16
+ GoogleAppTypeSchema,
17
+ OAuthClient,
18
18
  } from '../../../lib/oauth.ts';
19
19
  import {
20
20
  DEFAULT_OAUTH_CALLBACK_URL,
@@ -24,6 +24,7 @@ import {
24
24
  APPLE_AUTHORIZATION_ENDPOINT,
25
25
  APPLE_DISCOVERY_ENDPOINT,
26
26
  APPLE_TOKEN_ENDPOINT,
27
+ clerkDomainFromPublishableKey,
27
28
  LINKEDIN_AUTHORIZATION_ENDPOINT,
28
29
  LINKEDIN_DISCOVERY_ENDPOINT,
29
30
  LINKEDIN_TOKEN_ENDPOINT,
@@ -32,6 +33,24 @@ import { UI } from '../../../ui/index.ts';
32
33
  import chalk from 'chalk';
33
34
  import boxen from 'boxen';
34
35
  import { link } from '../../../logging.ts';
36
+ import {
37
+ appleKeyIdPrompt,
38
+ applePrivateKeyFilePrompt,
39
+ appleServicesIdPrompt,
40
+ appleTeamIdPrompt,
41
+ clerkPublishableKeyPrompt,
42
+ clientIdPrompt,
43
+ clientSecretPrompt,
44
+ firebaseDiscoveryEndpoint,
45
+ firebaseProjectIdPrompt,
46
+ getFlag,
47
+ hasAnyFlag,
48
+ isTrueFlag,
49
+ readPrivateKeyFile,
50
+ redirectSetupMessages,
51
+ redirectUriPrompt,
52
+ validateFirebaseProjectId,
53
+ } from './shared.ts';
35
54
 
36
55
  export const ClientTypeSchema = Schema.Literal(
37
56
  'google',
@@ -42,12 +61,13 @@ export const ClientTypeSchema = Schema.Literal(
42
61
  'firebase',
43
62
  );
44
63
 
45
- const GoogleAppTypeSchema = Schema.Literal(
46
- 'web',
47
- 'ios',
48
- 'android',
49
- 'button-for-web',
50
- );
64
+ const googleConsoleUrl =
65
+ 'https://console.developers.google.com/apis/credentials';
66
+ const githubDeveloperUrl = 'https://github.com/settings/developers';
67
+ const linkedinDeveloperUrl = 'https://www.linkedin.com/developers/apps';
68
+ const optionalRedirectPrompt = redirectUriPrompt({
69
+ heading: 'Custom redirect URI (optional):',
70
+ });
51
71
 
52
72
  const selectGoogleAppType = (value: unknown) =>
53
73
  Effect.gen(function* () {
@@ -96,10 +116,159 @@ const selectGoogleAppType = (value: unknown) =>
96
116
  );
97
117
  });
98
118
 
119
+ const selectGoogleCredentialMode = Effect.fn(function* () {
120
+ return yield* runUIEffect(
121
+ new UI.Select({
122
+ options: [
123
+ {
124
+ label:
125
+ 'Use dev credentials' +
126
+ chalk.dim(' (works on localhost and Expo, no Google setup)'),
127
+ value: 'dev' as const,
128
+ },
129
+ {
130
+ label:
131
+ 'Use my own credentials' +
132
+ chalk.dim(' (client ID and secret from Google Console)'),
133
+ value: 'custom' as const,
134
+ },
135
+ ],
136
+ promptText: 'Select Google credential mode:',
137
+ modifyOutput: UI.modifiers.piped([
138
+ UI.modifiers.topPadding,
139
+ UI.modifiers.dimOnComplete,
140
+ ]),
141
+ defaultValue: 'dev' as const,
142
+ }),
143
+ ).pipe(
144
+ Effect.catchTag('UIError', (e) =>
145
+ BadArgsError.make({ message: `UI error: ${e.message}` }),
146
+ ),
147
+ );
148
+ });
149
+
150
+ const resolveGoogleCredentialMode = Effect.fn(function* ({
151
+ appType,
152
+ opts,
153
+ }: {
154
+ appType: typeof GoogleAppTypeSchema.Type;
155
+ opts: Record<string, unknown>;
156
+ }): Effect.fn.Return<'custom' | 'dev', BadArgsError, GlobalOpts> {
157
+ const { yes } = yield* GlobalOpts;
158
+ const devCredentialsFlag = isTrueFlag(getFlag(opts, 'dev-credentials'));
159
+ const hasProvidedSomeCustomCredentials = hasAnyFlag(opts, [
160
+ 'client-id',
161
+ 'client-secret',
162
+ 'custom-redirect-uri',
163
+ ]);
164
+
165
+ if (devCredentialsFlag && appType !== 'web') {
166
+ return yield* BadArgsError.make({
167
+ message:
168
+ '--dev-credentials is only supported for --app-type web. Native Google clients need credentials from Google.',
169
+ });
170
+ }
171
+
172
+ if (devCredentialsFlag && hasProvidedSomeCustomCredentials) {
173
+ return yield* BadArgsError.make({
174
+ message:
175
+ '--dev-credentials cannot be combined with --client-id, --client-secret, or --custom-redirect-uri.',
176
+ });
177
+ }
178
+
179
+ if (appType !== 'web') {
180
+ return 'custom';
181
+ }
182
+
183
+ if (hasProvidedSomeCustomCredentials) {
184
+ return 'custom';
185
+ }
186
+
187
+ if (devCredentialsFlag) {
188
+ return 'dev';
189
+ }
190
+
191
+ if (yes) {
192
+ return 'dev';
193
+ }
194
+
195
+ return yield* selectGoogleCredentialMode();
196
+ });
197
+
198
+ const printGoogleDevCredentialsClient = Effect.fn(function* ({
199
+ appType,
200
+ client,
201
+ }: {
202
+ appType: typeof GoogleAppTypeSchema.Type;
203
+ client: typeof OAuthClient.Type;
204
+ }) {
205
+ yield* Effect.log(
206
+ boxen(
207
+ [
208
+ `Google OAuth client created: ${client.client_name}`,
209
+ `App type: ${appType}`,
210
+ `Credentials: Instant dev credentials`,
211
+ `ID: ${client.id}`,
212
+ '',
213
+ 'No Google Console setup required.',
214
+ 'Works on localhost and Expo during development.',
215
+ '',
216
+ chalk.bold('Ready for production? Run:'),
217
+ ` instant-cli auth client update --name ${client.client_name} --client-id <id> --client-secret <secret>`,
218
+ ].join('\n'),
219
+ { dimBorder: true, padding: { right: 1, left: 1 } },
220
+ ),
221
+ );
222
+ });
223
+
224
+ const printGoogleCustomCredentialsClient = Effect.fn(function* ({
225
+ appType,
226
+ client,
227
+ clientId,
228
+ customRedirectUri,
229
+ redirectUri,
230
+ }: {
231
+ appType: typeof GoogleAppTypeSchema.Type;
232
+ client: typeof OAuthClient.Type;
233
+ clientId: string | undefined;
234
+ customRedirectUri: string | undefined;
235
+ redirectUri: string | undefined;
236
+ }) {
237
+ const redirectMessages: string[] = [];
238
+ if (appType === 'web' && redirectUri) {
239
+ redirectMessages.push(
240
+ ...redirectSetupMessages({
241
+ prompt: 'Add this redirect URI in Google Console',
242
+ redirectUri,
243
+ showCustomRedirectInstructions: Boolean(customRedirectUri),
244
+ }),
245
+ );
246
+ }
247
+
248
+ yield* Effect.log(
249
+ boxen(
250
+ [
251
+ `Google OAuth client created: ${client.client_name}`,
252
+ `App type: ${appType}`,
253
+ `ID: ${client.id}`,
254
+ `Google Client ID: ${client.client_id ?? clientId}`,
255
+ ...redirectMessages,
256
+ ].join('\n'),
257
+ { dimBorder: true, padding: { right: 1, left: 1 } },
258
+ ),
259
+ );
260
+ });
261
+
99
262
  const handleGoogleClient = Effect.fn(function* (opts: Record<string, unknown>) {
100
263
  // This one requires special logic for getting client name
101
264
  // because the suggested name includes the app type
102
265
  const appType = yield* selectGoogleAppType(opts['app-type']);
266
+ const credentialMode = yield* resolveGoogleCredentialMode({
267
+ appType,
268
+ opts,
269
+ });
270
+ const useSharedCredentials = credentialMode === 'dev';
271
+
103
272
  const { auth, provider } = yield* getOrCreateProvider('google');
104
273
  const usedClientNames = new Set(
105
274
  (auth.oauth_clients ?? []).map((client) => client.client_name),
@@ -115,7 +284,10 @@ const handleGoogleClient = Effect.fn(function* (opts: Record<string, unknown>) {
115
284
  defaultValue: suggestedClientName,
116
285
  placeholder: suggestedClientName,
117
286
  validate: validateRequired,
118
- modifyOutput: UI.modifiers.piped([UI.modifiers.dimOnComplete]),
287
+ modifyOutput: UI.modifiers.piped([
288
+ UI.modifiers.topPadding,
289
+ UI.modifiers.dimOnComplete,
290
+ ]),
119
291
  },
120
292
  });
121
293
 
@@ -127,68 +299,46 @@ const handleGoogleClient = Effect.fn(function* (opts: Record<string, unknown>) {
127
299
 
128
300
  const clientId = yield* optOrPrompt(opts['client-id'], {
129
301
  simpleName: '--client-id',
130
- required: true,
131
- skipIf: false,
132
- prompt: {
133
- prompt: `Client ID: ${chalk.dim(`(from ${link('https://console.developers.google.com/apis/credentials')})`)}`,
134
- modifyOutput: UI.modifiers.piped([
135
- UI.modifiers.topPadding,
136
- UI.modifiers.dimOnComplete,
137
- ]),
138
- validate: validateRequired,
139
- },
302
+ required: !useSharedCredentials,
303
+ skipIf: useSharedCredentials,
304
+ skipMessage:
305
+ '--client-id is not compatible with --dev-credentials. Drop one or the other.',
306
+ prompt: clientIdPrompt({ providerUrl: googleConsoleUrl }),
140
307
  });
141
308
 
309
+ const usesCustomWebCredentials = !useSharedCredentials && appType === 'web';
142
310
  const clientSecret = yield* optOrPrompt(opts['client-secret'], {
143
- required: appType === 'web',
144
- skipIf: appType !== 'web',
311
+ required: usesCustomWebCredentials,
312
+ skipIf: !usesCustomWebCredentials,
145
313
  simpleName: '--client-secret',
146
- prompt: {
147
- prompt: `Client Secret: ${chalk.dim(`(from ${link('https://console.developers.google.com/apis/credentials')})`)}`,
148
- validate: validateRequired,
149
- sensitive: true,
150
- modifyOutput: UI.modifiers.piped([
151
- UI.modifiers.topPadding,
152
- UI.modifiers.dimOnComplete,
153
- ]),
154
- },
314
+ skipMessage: useSharedCredentials
315
+ ? '--client-secret is not compatible with --dev-credentials. Drop one or the other.'
316
+ : undefined,
317
+ prompt: clientSecretPrompt({ providerUrl: googleConsoleUrl }),
155
318
  });
156
319
 
157
320
  const customRedirectUri = yield* optOrPrompt(opts['custom-redirect-uri'], {
158
321
  required: false,
159
- prompt: {
160
- prompt: '',
161
- placeholder: 'https://yoursite.com/oauth/callback',
162
- modifyOutput: UI.modifiers.piped([
163
- (output, status) => {
164
- if (status === 'idle') {
165
- return (
166
- `\nCustom redirect URI (optional):
167
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
168
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
169
- stripFirstBlankLine(output)
170
- );
171
- }
172
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
173
- },
174
- UI.modifiers.dimOnComplete,
175
- ]),
176
- },
322
+ prompt: optionalRedirectPrompt,
177
323
  simpleName: '--custom-redirect-uri',
178
- skipIf: appType !== 'web',
179
- skipMessage: 'Provided custom redirect URI when not using web app type.',
324
+ skipIf: !usesCustomWebCredentials,
325
+ skipMessage: useSharedCredentials
326
+ ? '--custom-redirect-uri is not compatible with --dev-credentials.'
327
+ : 'Provided custom redirect URI when not using web app type.',
180
328
  });
181
329
 
182
330
  if (!clientName) {
183
331
  return yield* BadArgsError.make({ message: 'Client name is required.' }); // Should never reach this
184
332
  }
185
- const redirectUri = customRedirectUri || DEFAULT_OAUTH_CALLBACK_URL;
333
+ const redirectUri = useSharedCredentials
334
+ ? undefined
335
+ : customRedirectUri || DEFAULT_OAUTH_CALLBACK_URL;
186
336
 
187
337
  const response = yield* addOAuthClient({
188
338
  providerId: provider.id,
189
339
  clientName,
190
- clientId,
191
- clientSecret: clientSecret,
340
+ clientId: useSharedCredentials ? undefined : clientId,
341
+ clientSecret: useSharedCredentials ? undefined : clientSecret,
192
342
  authorizationEndpoint: GOOGLE_AUTHORIZATION_ENDPOINT,
193
343
  tokenEndpoint: GOOGLE_TOKEN_ENDPOINT,
194
344
  discoveryEndpoint: GOOGLE_DISCOVERY_ENDPOINT,
@@ -197,37 +347,24 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
197
347
  appType,
198
348
  skipNonceChecks: true,
199
349
  },
350
+ useSharedCredentials,
200
351
  });
201
352
 
202
- const redirectMessages: string[] = [];
203
- if (appType === 'web') {
204
- redirectMessages.push(
205
- chalk.bold(
206
- `\nAdd this redirect URI in Google Console:\n${redirectUri}\n`,
207
- ),
208
- );
209
- if (customRedirectUri) {
210
- redirectMessages.push(
211
- `Your custom redirect must forward to ${chalk.bold(DEFAULT_OAUTH_CALLBACK_URL)} with all query parameters preserved.`,
212
- );
213
- redirectMessages.push(
214
- `You can test it by visiting: ${chalk.bold(redirectUri + '?test-redirect=true')}`,
215
- );
216
- }
353
+ if (useSharedCredentials) {
354
+ yield* printGoogleDevCredentialsClient({
355
+ appType,
356
+ client: response.client,
357
+ });
358
+ return;
217
359
  }
218
360
 
219
- yield* Effect.log(
220
- boxen(
221
- [
222
- `Google OAuth client created: ${response.client.client_name}`,
223
- `App type: ${appType}`,
224
- `ID: ${response.client.id}`,
225
- `Google Client ID: ${response.client.client_id ?? clientId}`,
226
- ...redirectMessages,
227
- ].join('\n'),
228
- { dimBorder: true, padding: { right: 1, left: 1 } },
229
- ),
230
- );
361
+ yield* printGoogleCustomCredentialsClient({
362
+ appType,
363
+ client: response.client,
364
+ clientId,
365
+ customRedirectUri,
366
+ redirectUri,
367
+ });
231
368
  });
232
369
 
233
370
  const handleGithubClient = Effect.fn(function* (opts: Record<string, unknown>) {
@@ -240,53 +377,21 @@ const handleGithubClient = Effect.fn(function* (opts: Record<string, unknown>) {
240
377
  simpleName: '--client-id',
241
378
  required: true,
242
379
  skipIf: false,
243
- prompt: {
244
- prompt: `Client ID ${chalk.dim(`(from ${link('https://github.com/settings/developers')})`)}`,
245
- modifyOutput: UI.modifiers.piped([
246
- UI.modifiers.topPadding,
247
- UI.modifiers.dimOnComplete,
248
- ]),
249
- validate: validateRequired,
250
- },
380
+ prompt: clientIdPrompt({ providerUrl: githubDeveloperUrl }),
251
381
  });
252
382
 
253
383
  const clientSecret = yield* optOrPrompt(opts['client-secret'], {
254
384
  required: true,
255
385
  skipIf: false,
256
386
  simpleName: '--client-secret',
257
- prompt: {
258
- prompt: `Client Secret: ${chalk.dim(`(from ${link('https://github.com/settings/developers')})`)}`,
259
- validate: validateRequired,
260
- sensitive: true,
261
- modifyOutput: UI.modifiers.piped([
262
- UI.modifiers.topPadding,
263
- UI.modifiers.dimOnComplete,
264
- ]),
265
- },
387
+ prompt: clientSecretPrompt({ providerUrl: githubDeveloperUrl }),
266
388
  });
267
389
 
268
390
  const customRedirectUri = yield* optOrPrompt(opts['custom-redirect-uri'], {
269
391
  required: false,
270
392
  simpleName: '--custom-redirect-uri',
271
393
  skipIf: false,
272
- prompt: {
273
- prompt: '',
274
- placeholder: 'https://yoursite.com/oauth/callback',
275
- modifyOutput: UI.modifiers.piped([
276
- (output, status) => {
277
- if (status === 'idle') {
278
- return (
279
- `\nCustom redirect URI (optional):
280
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
281
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
282
- stripFirstBlankLine(output)
283
- );
284
- }
285
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
286
- },
287
- UI.modifiers.dimOnComplete,
288
- ]),
289
- },
394
+ prompt: optionalRedirectPrompt,
290
395
  });
291
396
 
292
397
  if (!clientName) {
@@ -306,19 +411,11 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
306
411
  meta: { providerName: 'github' },
307
412
  });
308
413
 
309
- const redirectMessages: string[] = [
310
- chalk.bold(
311
- `\nAdd this callback URL in your GitHub OAuth App settings:\n${redirectUri}\n`,
312
- ),
313
- ];
314
- if (customRedirectUri) {
315
- redirectMessages.push(
316
- `Your custom redirect must forward to ${chalk.bold(DEFAULT_OAUTH_CALLBACK_URL)} with all query parameters preserved.`,
317
- );
318
- redirectMessages.push(
319
- `You can test it by visiting: ${chalk.bold(redirectUri + '?test-redirect=true')}`,
320
- );
321
- }
414
+ const redirectMessages = redirectSetupMessages({
415
+ prompt: 'Add this callback URL in your GitHub OAuth App settings',
416
+ redirectUri,
417
+ showCustomRedirectInstructions: Boolean(customRedirectUri),
418
+ });
322
419
 
323
420
  yield* Effect.log(
324
421
  boxen(
@@ -345,53 +442,21 @@ const handleLinkedInClient = Effect.fn(function* (
345
442
  simpleName: '--client-id',
346
443
  required: true,
347
444
  skipIf: false,
348
- prompt: {
349
- prompt: `Client ID: ${chalk.dim(`(from ${link('https://www.linkedin.com/developers/apps')})`)}`,
350
- modifyOutput: UI.modifiers.piped([
351
- UI.modifiers.topPadding,
352
- UI.modifiers.dimOnComplete,
353
- ]),
354
- validate: validateRequired,
355
- },
445
+ prompt: clientIdPrompt({ providerUrl: linkedinDeveloperUrl }),
356
446
  });
357
447
 
358
448
  const clientSecret = yield* optOrPrompt(opts['client-secret'], {
359
449
  required: true,
360
450
  skipIf: false,
361
451
  simpleName: '--client-secret',
362
- prompt: {
363
- prompt: `Client Secret: ${chalk.dim(`(from ${link('https://www.linkedin.com/developers/apps')})`)}`,
364
- validate: validateRequired,
365
- sensitive: true,
366
- modifyOutput: UI.modifiers.piped([
367
- UI.modifiers.topPadding,
368
- UI.modifiers.dimOnComplete,
369
- ]),
370
- },
452
+ prompt: clientSecretPrompt({ providerUrl: linkedinDeveloperUrl }),
371
453
  });
372
454
 
373
455
  const customRedirectUri = yield* optOrPrompt(opts['custom-redirect-uri'], {
374
456
  required: false,
375
457
  simpleName: '--custom-redirect-uri',
376
458
  skipIf: false,
377
- prompt: {
378
- prompt: '',
379
- placeholder: 'https://yoursite.com/oauth/callback',
380
- modifyOutput: UI.modifiers.piped([
381
- (output, status) => {
382
- if (status === 'idle') {
383
- return (
384
- `\nCustom redirect URI (optional):
385
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
386
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
387
- stripFirstBlankLine(output)
388
- );
389
- }
390
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
391
- },
392
- UI.modifiers.dimOnComplete,
393
- ]),
394
- },
459
+ prompt: optionalRedirectPrompt,
395
460
  });
396
461
 
397
462
  if (!clientName) {
@@ -411,19 +476,11 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
411
476
  redirectTo: redirectUri,
412
477
  });
413
478
 
414
- const redirectMessages: string[] = [
415
- chalk.bold(
416
- `\nAdd this redirect URI in your LinkedIn app settings:\n${redirectUri}\n`,
417
- ),
418
- ];
419
- if (customRedirectUri) {
420
- redirectMessages.push(
421
- `Your custom redirect must forward to ${chalk.bold(DEFAULT_OAUTH_CALLBACK_URL)} with all query parameters preserved.`,
422
- );
423
- redirectMessages.push(
424
- `You can test it by visiting: ${chalk.bold(redirectUri + '?test-redirect=true')}`,
425
- );
426
- }
479
+ const redirectMessages = redirectSetupMessages({
480
+ prompt: 'Add this redirect URI in your LinkedIn app settings',
481
+ redirectUri,
482
+ showCustomRedirectInstructions: Boolean(customRedirectUri),
483
+ });
427
484
 
428
485
  yield* Effect.log(
429
486
  boxen(
@@ -438,32 +495,6 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
438
495
  );
439
496
  });
440
497
 
441
- const readPrivateKeyFile = Effect.fn('readPrivateKeyFile')(function* (
442
- path: string,
443
- ) {
444
- const fs = yield* FileSystem.FileSystem;
445
- // Strip shell-escape backslashes so paths like "file\ (2).p8" resolve correctly.
446
- // Only on POSIX — Windows uses backslashes as path separators.
447
- const normalizedPath =
448
- process.platform === 'win32' ? path : path.replace(/\\(.)/g, '$1');
449
- const contents = yield* fs.readFileString(normalizedPath, 'utf8').pipe(
450
- Effect.mapError(
451
- (e) =>
452
- new BadArgsError({
453
- message: `Could not read private key file at ${normalizedPath}: ${e.message}`,
454
- }),
455
- ),
456
- );
457
-
458
- const trimmed = contents.trim();
459
- if (!trimmed) {
460
- return yield* BadArgsError.make({
461
- message: `Private key file at ${normalizedPath} is empty.`,
462
- });
463
- }
464
- return trimmed;
465
- });
466
-
467
498
  const handleAppleClient = Effect.fn(function* (opts: Record<string, unknown>) {
468
499
  const { yes } = yield* GlobalOpts;
469
500
  const { clientName, provider } = yield* getClientNameAndProvider(
@@ -475,14 +506,7 @@ const handleAppleClient = Effect.fn(function* (opts: Record<string, unknown>) {
475
506
  simpleName: '--services-id',
476
507
  required: true,
477
508
  skipIf: false,
478
- prompt: {
479
- prompt: `Services ID ${chalk.dim(`(from ${link('https://developer.apple.com/account/resources/identifiers/list/serviceId')})`)}`,
480
- modifyOutput: UI.modifiers.piped([
481
- UI.modifiers.topPadding,
482
- UI.modifiers.dimOnComplete,
483
- ]),
484
- validate: validateRequired,
485
- },
509
+ prompt: appleServicesIdPrompt({}),
486
510
  });
487
511
 
488
512
  // If any web-flow flag is provided, enable web flow; otherwise ask
@@ -518,14 +542,7 @@ const handleAppleClient = Effect.fn(function* (opts: Record<string, unknown>) {
518
542
  required: true,
519
543
  skipIf: skipWeb,
520
544
  skipMessage: `--team-id ${webSkipMessage}`,
521
- prompt: {
522
- prompt: `Team ID ${chalk.dim(`(from ${link('https://developer.apple.com/account#MembershipDetailsCard')})`)}`,
523
- validate: validateRequired,
524
- modifyOutput: UI.modifiers.piped([
525
- UI.modifiers.topPadding,
526
- UI.modifiers.dimOnComplete,
527
- ]),
528
- },
545
+ prompt: appleTeamIdPrompt({}),
529
546
  });
530
547
 
531
548
  const keyId = yield* optOrPrompt(opts['key-id'], {
@@ -533,14 +550,7 @@ const handleAppleClient = Effect.fn(function* (opts: Record<string, unknown>) {
533
550
  required: true,
534
551
  skipIf: skipWeb,
535
552
  skipMessage: `--key-id ${webSkipMessage}`,
536
- prompt: {
537
- prompt: `Key ID ${chalk.dim(`(from ${link('https://developer.apple.com/account/resources/authkeys/list')})`)}`,
538
- validate: validateRequired,
539
- modifyOutput: UI.modifiers.piped([
540
- UI.modifiers.topPadding,
541
- UI.modifiers.dimOnComplete,
542
- ]),
543
- },
553
+ prompt: appleKeyIdPrompt({}),
544
554
  });
545
555
 
546
556
  const privateKeyPath = yield* optOrPrompt(opts['private-key-file'], {
@@ -548,14 +558,7 @@ const handleAppleClient = Effect.fn(function* (opts: Record<string, unknown>) {
548
558
  required: true,
549
559
  skipIf: skipWeb,
550
560
  skipMessage: `--private-key-file ${webSkipMessage}`,
551
- prompt: {
552
- prompt: `Path to .p8 private key file ${chalk.dim('(downloaded from Apple)')}`,
553
- validate: validateRequired,
554
- modifyOutput: UI.modifiers.piped([
555
- UI.modifiers.topPadding,
556
- UI.modifiers.dimOnComplete,
557
- ]),
558
- },
561
+ prompt: applePrivateKeyFilePrompt({}),
559
562
  });
560
563
 
561
564
  const privateKey = privateKeyPath
@@ -567,24 +570,7 @@ const handleAppleClient = Effect.fn(function* (opts: Record<string, unknown>) {
567
570
  simpleName: '--custom-redirect-uri',
568
571
  skipIf: skipWeb,
569
572
  skipMessage: `--custom-redirect-uri ${webSkipMessage}`,
570
- prompt: {
571
- prompt: '',
572
- placeholder: 'https://yoursite.com/oauth/callback',
573
- modifyOutput: UI.modifiers.piped([
574
- (output, status) => {
575
- if (status === 'idle') {
576
- return (
577
- `\nCustom redirect URI (optional):
578
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
579
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
580
- stripFirstBlankLine(output)
581
- );
582
- }
583
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
584
- },
585
- UI.modifiers.dimOnComplete,
586
- ]),
587
- },
573
+ prompt: optionalRedirectPrompt,
588
574
  });
589
575
 
590
576
  if (!clientName) {
@@ -617,22 +603,16 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
617
603
  `Services ID: ${response.client.client_id ?? servicesId}`,
618
604
  ];
619
605
 
620
- if (privateKey) {
606
+ if (privateKey && redirectUri) {
621
607
  summaryLines.push(`Team ID: ${teamId}`);
622
608
  summaryLines.push(`Key ID: ${keyId}`);
623
609
  summaryLines.push(
624
- chalk.bold(
625
- `\nAdd this return URL under your Services ID on ${link('https://developer.apple.com', 'developer.apple.com')}:\n${redirectUri}\n`,
626
- ),
610
+ ...redirectSetupMessages({
611
+ prompt: `Add this return URL under your Services ID on ${link('https://developer.apple.com', 'developer.apple.com')}`,
612
+ redirectUri,
613
+ showCustomRedirectInstructions: Boolean(customRedirectUri),
614
+ }),
627
615
  );
628
- if (customRedirectUri) {
629
- summaryLines.push(
630
- `Your custom redirect must forward to ${chalk.bold(DEFAULT_OAUTH_CALLBACK_URL)} with all query parameters preserved.`,
631
- );
632
- summaryLines.push(
633
- `You can test it by visiting: ${chalk.bold(redirectUri + '?test-redirect=true')}`,
634
- );
635
- }
636
616
  }
637
617
  yield* Effect.log(
638
618
  boxen(summaryLines.join('\n'), {
@@ -652,23 +632,7 @@ const handleClerkClient = Effect.fn(function* (opts: Record<string, unknown>) {
652
632
  simpleName: '--publishable-key',
653
633
  required: true,
654
634
  skipIf: false,
655
- prompt: {
656
- prompt: `Clerk publishable key ${chalk.dim(`(from ${link('https://dashboard.clerk.com/last-active?path=api-keys')})`)}`,
657
- placeholder:
658
- 'pk_********************************************************',
659
- modifyOutput: UI.modifiers.piped([
660
- UI.modifiers.topPadding,
661
- UI.modifiers.dimOnComplete,
662
- ]),
663
- validate: (val) => {
664
- if (!val) {
665
- return 'Publishable key is required';
666
- }
667
- if (!val.startsWith('pk_')) {
668
- return 'Invalid publishable key. It should start with "pk_".';
669
- }
670
- },
671
- },
635
+ prompt: clerkPublishableKeyPrompt({}),
672
636
  });
673
637
 
674
638
  if (!clientName) {
@@ -680,7 +644,7 @@ const handleClerkClient = Effect.fn(function* (opts: Record<string, unknown>) {
680
644
  });
681
645
  }
682
646
 
683
- const domain = domainFromClerkKey(publishableKey);
647
+ const domain = clerkDomainFromPublishableKey(publishableKey);
684
648
  if (!domain) {
685
649
  return yield* BadArgsError.make({
686
650
  message: 'Invalid publishable key. Could not extract domain.',
@@ -731,27 +695,11 @@ const handleFirebaseClient = Effect.fn(function* (
731
695
  opts,
732
696
  );
733
697
 
734
- const firebaseProjectIdRegex = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
735
698
  const projectId = yield* optOrPrompt(opts['project-id'], {
736
699
  simpleName: '--project-id',
737
700
  required: true,
738
701
  skipIf: false,
739
- prompt: {
740
- prompt: `Firebase project ID: (From Project Settings page on ${link('https://console.firebase.google.com/')})`,
741
- placeholder: '',
742
- modifyOutput: UI.modifiers.piped([
743
- UI.modifiers.topPadding,
744
- UI.modifiers.dimOnComplete,
745
- ]),
746
- validate: (val) => {
747
- if (!val) {
748
- return 'Project ID is required';
749
- }
750
- if (!firebaseProjectIdRegex.test(val)) {
751
- 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.';
752
- }
753
- },
754
- },
702
+ prompt: firebaseProjectIdPrompt({}),
755
703
  });
756
704
  // typeguard
757
705
  if (!clientName || !projectId) {
@@ -759,16 +707,14 @@ const handleFirebaseClient = Effect.fn(function* (
759
707
  message: 'Missing required arguments',
760
708
  });
761
709
  }
762
- if (!firebaseProjectIdRegex.test(projectId)) {
763
- return yield* BadArgsError.make({
764
- message:
765
- '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.',
766
- });
710
+ const validationError = validateFirebaseProjectId(projectId);
711
+ if (validationError) {
712
+ return yield* BadArgsError.make({ message: validationError });
767
713
  }
768
714
  const response = yield* addOAuthClient({
769
715
  providerId: provider.id,
770
716
  clientName,
771
- discoveryEndpoint: `https://securetoken.google.com/${encodeURIComponent(projectId)}/.well-known/openid-configuration`,
717
+ discoveryEndpoint: firebaseDiscoveryEndpoint(projectId),
772
718
  });
773
719
 
774
720
  yield* Effect.log(
@@ -840,28 +786,3 @@ export const authClientAddCmd = Effect.fn(
840
786
  }),
841
787
  ),
842
788
  );
843
-
844
- function domainFromClerkKey(key: string): string | null {
845
- try {
846
- const parts = key.split('_');
847
- const domainPartB64 = parts[parts.length - 1];
848
- const domainPart = base64Decode(domainPartB64);
849
- return domainPart.replace('$', '');
850
- } catch (e) {
851
- console.error('Error getting domain from clerk key', e);
852
- return null;
853
- }
854
- }
855
-
856
- // Base64 decode, switching to url-safe decode if we hit an error
857
- // Can't be sure which method Clerk uses because you can't generate
858
- // `+` or `/` with characters that go in a normal host. Urls with
859
- // chinese characters exist, they might encode to `+` or `/`, and
860
- // Clerk might support them, so we'll be safe and do both.
861
- function base64Decode(s: string) {
862
- try {
863
- return Buffer.from(s, 'base64').toString('utf-8');
864
- } catch (e) {
865
- return Buffer.from(s, 'base64url').toString('utf-8');
866
- }
867
- }