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,16 +1,21 @@
1
1
  import { Effect, Match, Option, Schema } from 'effect';
2
- import { FileSystem } from '@effect/platform';
3
2
  import { BadArgsError } from "../../../errors.js";
4
3
  import { GlobalOpts } from "../../../context/globalOpts.js";
5
- import { optOrPrompt, optOrPromptBoolean, runUIEffect, stripFirstBlankLine, validateRequired, } from "../../../lib/ui.js";
6
- import { addOAuthClient, findName, getClientNameAndProvider, getOrCreateProvider, } from "../../../lib/oauth.js";
7
- import { DEFAULT_OAUTH_CALLBACK_URL, GOOGLE_AUTHORIZATION_ENDPOINT, GOOGLE_DISCOVERY_ENDPOINT, GOOGLE_TOKEN_ENDPOINT, APPLE_AUTHORIZATION_ENDPOINT, APPLE_DISCOVERY_ENDPOINT, APPLE_TOKEN_ENDPOINT, LINKEDIN_AUTHORIZATION_ENDPOINT, LINKEDIN_DISCOVERY_ENDPOINT, LINKEDIN_TOKEN_ENDPOINT, } from '@instantdb/platform';
4
+ import { optOrPrompt, optOrPromptBoolean, runUIEffect, validateRequired, } from "../../../lib/ui.js";
5
+ import { addOAuthClient, findName, getClientNameAndProvider, getOrCreateProvider, GoogleAppTypeSchema, OAuthClient, } from "../../../lib/oauth.js";
6
+ import { DEFAULT_OAUTH_CALLBACK_URL, GOOGLE_AUTHORIZATION_ENDPOINT, GOOGLE_DISCOVERY_ENDPOINT, GOOGLE_TOKEN_ENDPOINT, APPLE_AUTHORIZATION_ENDPOINT, APPLE_DISCOVERY_ENDPOINT, APPLE_TOKEN_ENDPOINT, clerkDomainFromPublishableKey, LINKEDIN_AUTHORIZATION_ENDPOINT, LINKEDIN_DISCOVERY_ENDPOINT, LINKEDIN_TOKEN_ENDPOINT, } from '@instantdb/platform';
8
7
  import { UI } from "../../../ui/index.js";
9
8
  import chalk from 'chalk';
10
9
  import boxen from 'boxen';
11
10
  import { link } from "../../../logging.js";
11
+ import { appleKeyIdPrompt, applePrivateKeyFilePrompt, appleServicesIdPrompt, appleTeamIdPrompt, clerkPublishableKeyPrompt, clientIdPrompt, clientSecretPrompt, firebaseDiscoveryEndpoint, firebaseProjectIdPrompt, getFlag, hasAnyFlag, isTrueFlag, readPrivateKeyFile, redirectSetupMessages, redirectUriPrompt, validateFirebaseProjectId, } from "./shared.js";
12
12
  export const ClientTypeSchema = Schema.Literal('google', 'github', 'apple', 'linkedin', 'clerk', 'firebase');
13
- const GoogleAppTypeSchema = Schema.Literal('web', 'ios', 'android', 'button-for-web');
13
+ const googleConsoleUrl = 'https://console.developers.google.com/apis/credentials';
14
+ const githubDeveloperUrl = 'https://github.com/settings/developers';
15
+ const linkedinDeveloperUrl = 'https://www.linkedin.com/developers/apps';
16
+ const optionalRedirectPrompt = redirectUriPrompt({
17
+ heading: 'Custom redirect URI (optional):',
18
+ });
14
19
  const selectGoogleAppType = (value) => Effect.gen(function* () {
15
20
  const { yes } = yield* GlobalOpts;
16
21
  return yield* Option.fromNullable(value).pipe(Effect.catchTag('NoSuchElementException', () => {
@@ -40,10 +45,100 @@ const selectGoogleAppType = (value) => Effect.gen(function* () {
40
45
  message: 'Invalid app-type, must be one of: web, ios, android, button-for-web',
41
46
  })));
42
47
  });
48
+ const selectGoogleCredentialMode = Effect.fn(function* () {
49
+ return yield* runUIEffect(new UI.Select({
50
+ options: [
51
+ {
52
+ label: 'Use dev credentials' +
53
+ chalk.dim(' (works on localhost and Expo, no Google setup)'),
54
+ value: 'dev',
55
+ },
56
+ {
57
+ label: 'Use my own credentials' +
58
+ chalk.dim(' (client ID and secret from Google Console)'),
59
+ value: 'custom',
60
+ },
61
+ ],
62
+ promptText: 'Select Google credential mode:',
63
+ modifyOutput: UI.modifiers.piped([
64
+ UI.modifiers.topPadding,
65
+ UI.modifiers.dimOnComplete,
66
+ ]),
67
+ defaultValue: 'dev',
68
+ })).pipe(Effect.catchTag('UIError', (e) => BadArgsError.make({ message: `UI error: ${e.message}` })));
69
+ });
70
+ const resolveGoogleCredentialMode = Effect.fn(function* ({ appType, opts, }) {
71
+ const { yes } = yield* GlobalOpts;
72
+ const devCredentialsFlag = isTrueFlag(getFlag(opts, 'dev-credentials'));
73
+ const hasProvidedSomeCustomCredentials = hasAnyFlag(opts, [
74
+ 'client-id',
75
+ 'client-secret',
76
+ 'custom-redirect-uri',
77
+ ]);
78
+ if (devCredentialsFlag && appType !== 'web') {
79
+ return yield* BadArgsError.make({
80
+ message: '--dev-credentials is only supported for --app-type web. Native Google clients need credentials from Google.',
81
+ });
82
+ }
83
+ if (devCredentialsFlag && hasProvidedSomeCustomCredentials) {
84
+ return yield* BadArgsError.make({
85
+ message: '--dev-credentials cannot be combined with --client-id, --client-secret, or --custom-redirect-uri.',
86
+ });
87
+ }
88
+ if (appType !== 'web') {
89
+ return 'custom';
90
+ }
91
+ if (hasProvidedSomeCustomCredentials) {
92
+ return 'custom';
93
+ }
94
+ if (devCredentialsFlag) {
95
+ return 'dev';
96
+ }
97
+ if (yes) {
98
+ return 'dev';
99
+ }
100
+ return yield* selectGoogleCredentialMode();
101
+ });
102
+ const printGoogleDevCredentialsClient = Effect.fn(function* ({ appType, client, }) {
103
+ yield* Effect.log(boxen([
104
+ `Google OAuth client created: ${client.client_name}`,
105
+ `App type: ${appType}`,
106
+ `Credentials: Instant dev credentials`,
107
+ `ID: ${client.id}`,
108
+ '',
109
+ 'No Google Console setup required.',
110
+ 'Works on localhost and Expo during development.',
111
+ '',
112
+ chalk.bold('Ready for production? Run:'),
113
+ ` instant-cli auth client update --name ${client.client_name} --client-id <id> --client-secret <secret>`,
114
+ ].join('\n'), { dimBorder: true, padding: { right: 1, left: 1 } }));
115
+ });
116
+ const printGoogleCustomCredentialsClient = Effect.fn(function* ({ appType, client, clientId, customRedirectUri, redirectUri, }) {
117
+ const redirectMessages = [];
118
+ if (appType === 'web' && redirectUri) {
119
+ redirectMessages.push(...redirectSetupMessages({
120
+ prompt: 'Add this redirect URI in Google Console',
121
+ redirectUri,
122
+ showCustomRedirectInstructions: Boolean(customRedirectUri),
123
+ }));
124
+ }
125
+ yield* Effect.log(boxen([
126
+ `Google OAuth client created: ${client.client_name}`,
127
+ `App type: ${appType}`,
128
+ `ID: ${client.id}`,
129
+ `Google Client ID: ${client.client_id ?? clientId}`,
130
+ ...redirectMessages,
131
+ ].join('\n'), { dimBorder: true, padding: { right: 1, left: 1 } }));
132
+ });
43
133
  const handleGoogleClient = Effect.fn(function* (opts) {
44
134
  // This one requires special logic for getting client name
45
135
  // because the suggested name includes the app type
46
136
  const appType = yield* selectGoogleAppType(opts['app-type']);
137
+ const credentialMode = yield* resolveGoogleCredentialMode({
138
+ appType,
139
+ opts,
140
+ });
141
+ const useSharedCredentials = credentialMode === 'dev';
47
142
  const { auth, provider } = yield* getOrCreateProvider('google');
48
143
  const usedClientNames = new Set((auth.oauth_clients ?? []).map((client) => client.client_name));
49
144
  const suggestedClientName = findName(`google-${appType}`, usedClientNames);
@@ -56,7 +151,10 @@ const handleGoogleClient = Effect.fn(function* (opts) {
56
151
  defaultValue: suggestedClientName,
57
152
  placeholder: suggestedClientName,
58
153
  validate: validateRequired,
59
- modifyOutput: UI.modifiers.piped([UI.modifiers.dimOnComplete]),
154
+ modifyOutput: UI.modifiers.piped([
155
+ UI.modifiers.topPadding,
156
+ UI.modifiers.dimOnComplete,
157
+ ]),
60
158
  },
61
159
  });
62
160
  if (usedClientNames.has(clientName || '')) {
@@ -66,62 +164,41 @@ const handleGoogleClient = Effect.fn(function* (opts) {
66
164
  }
67
165
  const clientId = yield* optOrPrompt(opts['client-id'], {
68
166
  simpleName: '--client-id',
69
- required: true,
70
- skipIf: false,
71
- prompt: {
72
- prompt: `Client ID: ${chalk.dim(`(from ${link('https://console.developers.google.com/apis/credentials')})`)}`,
73
- modifyOutput: UI.modifiers.piped([
74
- UI.modifiers.topPadding,
75
- UI.modifiers.dimOnComplete,
76
- ]),
77
- validate: validateRequired,
78
- },
167
+ required: !useSharedCredentials,
168
+ skipIf: useSharedCredentials,
169
+ skipMessage: '--client-id is not compatible with --dev-credentials. Drop one or the other.',
170
+ prompt: clientIdPrompt({ providerUrl: googleConsoleUrl }),
79
171
  });
172
+ const usesCustomWebCredentials = !useSharedCredentials && appType === 'web';
80
173
  const clientSecret = yield* optOrPrompt(opts['client-secret'], {
81
- required: appType === 'web',
82
- skipIf: appType !== 'web',
174
+ required: usesCustomWebCredentials,
175
+ skipIf: !usesCustomWebCredentials,
83
176
  simpleName: '--client-secret',
84
- prompt: {
85
- prompt: `Client Secret: ${chalk.dim(`(from ${link('https://console.developers.google.com/apis/credentials')})`)}`,
86
- validate: validateRequired,
87
- sensitive: true,
88
- modifyOutput: UI.modifiers.piped([
89
- UI.modifiers.topPadding,
90
- UI.modifiers.dimOnComplete,
91
- ]),
92
- },
177
+ skipMessage: useSharedCredentials
178
+ ? '--client-secret is not compatible with --dev-credentials. Drop one or the other.'
179
+ : undefined,
180
+ prompt: clientSecretPrompt({ providerUrl: googleConsoleUrl }),
93
181
  });
94
182
  const customRedirectUri = yield* optOrPrompt(opts['custom-redirect-uri'], {
95
183
  required: false,
96
- prompt: {
97
- prompt: '',
98
- placeholder: 'https://yoursite.com/oauth/callback',
99
- modifyOutput: UI.modifiers.piped([
100
- (output, status) => {
101
- if (status === 'idle') {
102
- return (`\nCustom redirect URI (optional):
103
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
104
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
105
- stripFirstBlankLine(output));
106
- }
107
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
108
- },
109
- UI.modifiers.dimOnComplete,
110
- ]),
111
- },
184
+ prompt: optionalRedirectPrompt,
112
185
  simpleName: '--custom-redirect-uri',
113
- skipIf: appType !== 'web',
114
- skipMessage: 'Provided custom redirect URI when not using web app type.',
186
+ skipIf: !usesCustomWebCredentials,
187
+ skipMessage: useSharedCredentials
188
+ ? '--custom-redirect-uri is not compatible with --dev-credentials.'
189
+ : 'Provided custom redirect URI when not using web app type.',
115
190
  });
116
191
  if (!clientName) {
117
192
  return yield* BadArgsError.make({ message: 'Client name is required.' }); // Should never reach this
118
193
  }
119
- const redirectUri = customRedirectUri || DEFAULT_OAUTH_CALLBACK_URL;
194
+ const redirectUri = useSharedCredentials
195
+ ? undefined
196
+ : customRedirectUri || DEFAULT_OAUTH_CALLBACK_URL;
120
197
  const response = yield* addOAuthClient({
121
198
  providerId: provider.id,
122
199
  clientName,
123
- clientId,
124
- clientSecret: clientSecret,
200
+ clientId: useSharedCredentials ? undefined : clientId,
201
+ clientSecret: useSharedCredentials ? undefined : clientSecret,
125
202
  authorizationEndpoint: GOOGLE_AUTHORIZATION_ENDPOINT,
126
203
  tokenEndpoint: GOOGLE_TOKEN_ENDPOINT,
127
204
  discoveryEndpoint: GOOGLE_DISCOVERY_ENDPOINT,
@@ -130,22 +207,22 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
130
207
  appType,
131
208
  skipNonceChecks: true,
132
209
  },
210
+ useSharedCredentials,
133
211
  });
134
- const redirectMessages = [];
135
- if (appType === 'web') {
136
- redirectMessages.push(chalk.bold(`\nAdd this redirect URI in Google Console:\n${redirectUri}\n`));
137
- if (customRedirectUri) {
138
- redirectMessages.push(`Your custom redirect must forward to ${chalk.bold(DEFAULT_OAUTH_CALLBACK_URL)} with all query parameters preserved.`);
139
- redirectMessages.push(`You can test it by visiting: ${chalk.bold(redirectUri + '?test-redirect=true')}`);
140
- }
212
+ if (useSharedCredentials) {
213
+ yield* printGoogleDevCredentialsClient({
214
+ appType,
215
+ client: response.client,
216
+ });
217
+ return;
141
218
  }
142
- yield* Effect.log(boxen([
143
- `Google OAuth client created: ${response.client.client_name}`,
144
- `App type: ${appType}`,
145
- `ID: ${response.client.id}`,
146
- `Google Client ID: ${response.client.client_id ?? clientId}`,
147
- ...redirectMessages,
148
- ].join('\n'), { dimBorder: true, padding: { right: 1, left: 1 } }));
219
+ yield* printGoogleCustomCredentialsClient({
220
+ appType,
221
+ client: response.client,
222
+ clientId,
223
+ customRedirectUri,
224
+ redirectUri,
225
+ });
149
226
  });
150
227
  const handleGithubClient = Effect.fn(function* (opts) {
151
228
  const { clientName, provider } = yield* getClientNameAndProvider('github', opts);
@@ -153,49 +230,19 @@ const handleGithubClient = Effect.fn(function* (opts) {
153
230
  simpleName: '--client-id',
154
231
  required: true,
155
232
  skipIf: false,
156
- prompt: {
157
- prompt: `Client ID ${chalk.dim(`(from ${link('https://github.com/settings/developers')})`)}`,
158
- modifyOutput: UI.modifiers.piped([
159
- UI.modifiers.topPadding,
160
- UI.modifiers.dimOnComplete,
161
- ]),
162
- validate: validateRequired,
163
- },
233
+ prompt: clientIdPrompt({ providerUrl: githubDeveloperUrl }),
164
234
  });
165
235
  const clientSecret = yield* optOrPrompt(opts['client-secret'], {
166
236
  required: true,
167
237
  skipIf: false,
168
238
  simpleName: '--client-secret',
169
- prompt: {
170
- prompt: `Client Secret: ${chalk.dim(`(from ${link('https://github.com/settings/developers')})`)}`,
171
- validate: validateRequired,
172
- sensitive: true,
173
- modifyOutput: UI.modifiers.piped([
174
- UI.modifiers.topPadding,
175
- UI.modifiers.dimOnComplete,
176
- ]),
177
- },
239
+ prompt: clientSecretPrompt({ providerUrl: githubDeveloperUrl }),
178
240
  });
179
241
  const customRedirectUri = yield* optOrPrompt(opts['custom-redirect-uri'], {
180
242
  required: false,
181
243
  simpleName: '--custom-redirect-uri',
182
244
  skipIf: false,
183
- prompt: {
184
- prompt: '',
185
- placeholder: 'https://yoursite.com/oauth/callback',
186
- modifyOutput: UI.modifiers.piped([
187
- (output, status) => {
188
- if (status === 'idle') {
189
- return (`\nCustom redirect URI (optional):
190
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
191
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
192
- stripFirstBlankLine(output));
193
- }
194
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
195
- },
196
- UI.modifiers.dimOnComplete,
197
- ]),
198
- },
245
+ prompt: optionalRedirectPrompt,
199
246
  });
200
247
  if (!clientName) {
201
248
  return yield* BadArgsError.make({ message: 'Client name is required.' });
@@ -211,13 +258,11 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
211
258
  redirectTo: redirectUri,
212
259
  meta: { providerName: 'github' },
213
260
  });
214
- const redirectMessages = [
215
- chalk.bold(`\nAdd this callback URL in your GitHub OAuth App settings:\n${redirectUri}\n`),
216
- ];
217
- if (customRedirectUri) {
218
- redirectMessages.push(`Your custom redirect must forward to ${chalk.bold(DEFAULT_OAUTH_CALLBACK_URL)} with all query parameters preserved.`);
219
- redirectMessages.push(`You can test it by visiting: ${chalk.bold(redirectUri + '?test-redirect=true')}`);
220
- }
261
+ const redirectMessages = redirectSetupMessages({
262
+ prompt: 'Add this callback URL in your GitHub OAuth App settings',
263
+ redirectUri,
264
+ showCustomRedirectInstructions: Boolean(customRedirectUri),
265
+ });
221
266
  yield* Effect.log(boxen([
222
267
  `GitHub OAuth client created: ${response.client.client_name}`,
223
268
  `ID: ${response.client.id}`,
@@ -231,49 +276,19 @@ const handleLinkedInClient = Effect.fn(function* (opts) {
231
276
  simpleName: '--client-id',
232
277
  required: true,
233
278
  skipIf: false,
234
- prompt: {
235
- prompt: `Client ID: ${chalk.dim(`(from ${link('https://www.linkedin.com/developers/apps')})`)}`,
236
- modifyOutput: UI.modifiers.piped([
237
- UI.modifiers.topPadding,
238
- UI.modifiers.dimOnComplete,
239
- ]),
240
- validate: validateRequired,
241
- },
279
+ prompt: clientIdPrompt({ providerUrl: linkedinDeveloperUrl }),
242
280
  });
243
281
  const clientSecret = yield* optOrPrompt(opts['client-secret'], {
244
282
  required: true,
245
283
  skipIf: false,
246
284
  simpleName: '--client-secret',
247
- prompt: {
248
- prompt: `Client Secret: ${chalk.dim(`(from ${link('https://www.linkedin.com/developers/apps')})`)}`,
249
- validate: validateRequired,
250
- sensitive: true,
251
- modifyOutput: UI.modifiers.piped([
252
- UI.modifiers.topPadding,
253
- UI.modifiers.dimOnComplete,
254
- ]),
255
- },
285
+ prompt: clientSecretPrompt({ providerUrl: linkedinDeveloperUrl }),
256
286
  });
257
287
  const customRedirectUri = yield* optOrPrompt(opts['custom-redirect-uri'], {
258
288
  required: false,
259
289
  simpleName: '--custom-redirect-uri',
260
290
  skipIf: false,
261
- prompt: {
262
- prompt: '',
263
- placeholder: 'https://yoursite.com/oauth/callback',
264
- modifyOutput: UI.modifiers.piped([
265
- (output, status) => {
266
- if (status === 'idle') {
267
- return (`\nCustom redirect URI (optional):
268
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
269
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
270
- stripFirstBlankLine(output));
271
- }
272
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
273
- },
274
- UI.modifiers.dimOnComplete,
275
- ]),
276
- },
291
+ prompt: optionalRedirectPrompt,
277
292
  });
278
293
  if (!clientName) {
279
294
  return yield* BadArgsError.make({ message: 'Client name is required.' });
@@ -289,13 +304,11 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
289
304
  discoveryEndpoint: LINKEDIN_DISCOVERY_ENDPOINT,
290
305
  redirectTo: redirectUri,
291
306
  });
292
- const redirectMessages = [
293
- chalk.bold(`\nAdd this redirect URI in your LinkedIn app settings:\n${redirectUri}\n`),
294
- ];
295
- if (customRedirectUri) {
296
- redirectMessages.push(`Your custom redirect must forward to ${chalk.bold(DEFAULT_OAUTH_CALLBACK_URL)} with all query parameters preserved.`);
297
- redirectMessages.push(`You can test it by visiting: ${chalk.bold(redirectUri + '?test-redirect=true')}`);
298
- }
307
+ const redirectMessages = redirectSetupMessages({
308
+ prompt: 'Add this redirect URI in your LinkedIn app settings',
309
+ redirectUri,
310
+ showCustomRedirectInstructions: Boolean(customRedirectUri),
311
+ });
299
312
  yield* Effect.log(boxen([
300
313
  `LinkedIn OAuth client created: ${response.client.client_name}`,
301
314
  `ID: ${response.client.id}`,
@@ -303,22 +316,6 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
303
316
  ...redirectMessages,
304
317
  ].join('\n'), { dimBorder: true, padding: { right: 1, left: 1 } }));
305
318
  });
306
- const readPrivateKeyFile = Effect.fn('readPrivateKeyFile')(function* (path) {
307
- const fs = yield* FileSystem.FileSystem;
308
- // Strip shell-escape backslashes so paths like "file\ (2).p8" resolve correctly.
309
- // Only on POSIX — Windows uses backslashes as path separators.
310
- const normalizedPath = process.platform === 'win32' ? path : path.replace(/\\(.)/g, '$1');
311
- const contents = yield* fs.readFileString(normalizedPath, 'utf8').pipe(Effect.mapError((e) => new BadArgsError({
312
- message: `Could not read private key file at ${normalizedPath}: ${e.message}`,
313
- })));
314
- const trimmed = contents.trim();
315
- if (!trimmed) {
316
- return yield* BadArgsError.make({
317
- message: `Private key file at ${normalizedPath} is empty.`,
318
- });
319
- }
320
- return trimmed;
321
- });
322
319
  const handleAppleClient = Effect.fn(function* (opts) {
323
320
  const { yes } = yield* GlobalOpts;
324
321
  const { clientName, provider } = yield* getClientNameAndProvider('apple', opts);
@@ -326,14 +323,7 @@ const handleAppleClient = Effect.fn(function* (opts) {
326
323
  simpleName: '--services-id',
327
324
  required: true,
328
325
  skipIf: false,
329
- prompt: {
330
- prompt: `Services ID ${chalk.dim(`(from ${link('https://developer.apple.com/account/resources/identifiers/list/serviceId')})`)}`,
331
- modifyOutput: UI.modifiers.piped([
332
- UI.modifiers.topPadding,
333
- UI.modifiers.dimOnComplete,
334
- ]),
335
- validate: validateRequired,
336
- },
326
+ prompt: appleServicesIdPrompt({}),
337
327
  });
338
328
  // If any web-flow flag is provided, enable web flow; otherwise ask
339
329
  // (non-interactively with --yes we default to native-only).
@@ -359,42 +349,21 @@ const handleAppleClient = Effect.fn(function* (opts) {
359
349
  required: true,
360
350
  skipIf: skipWeb,
361
351
  skipMessage: `--team-id ${webSkipMessage}`,
362
- prompt: {
363
- prompt: `Team ID ${chalk.dim(`(from ${link('https://developer.apple.com/account#MembershipDetailsCard')})`)}`,
364
- validate: validateRequired,
365
- modifyOutput: UI.modifiers.piped([
366
- UI.modifiers.topPadding,
367
- UI.modifiers.dimOnComplete,
368
- ]),
369
- },
352
+ prompt: appleTeamIdPrompt({}),
370
353
  });
371
354
  const keyId = yield* optOrPrompt(opts['key-id'], {
372
355
  simpleName: '--key-id',
373
356
  required: true,
374
357
  skipIf: skipWeb,
375
358
  skipMessage: `--key-id ${webSkipMessage}`,
376
- prompt: {
377
- prompt: `Key ID ${chalk.dim(`(from ${link('https://developer.apple.com/account/resources/authkeys/list')})`)}`,
378
- validate: validateRequired,
379
- modifyOutput: UI.modifiers.piped([
380
- UI.modifiers.topPadding,
381
- UI.modifiers.dimOnComplete,
382
- ]),
383
- },
359
+ prompt: appleKeyIdPrompt({}),
384
360
  });
385
361
  const privateKeyPath = yield* optOrPrompt(opts['private-key-file'], {
386
362
  simpleName: '--private-key-file',
387
363
  required: true,
388
364
  skipIf: skipWeb,
389
365
  skipMessage: `--private-key-file ${webSkipMessage}`,
390
- prompt: {
391
- prompt: `Path to .p8 private key file ${chalk.dim('(downloaded from Apple)')}`,
392
- validate: validateRequired,
393
- modifyOutput: UI.modifiers.piped([
394
- UI.modifiers.topPadding,
395
- UI.modifiers.dimOnComplete,
396
- ]),
397
- },
366
+ prompt: applePrivateKeyFilePrompt({}),
398
367
  });
399
368
  const privateKey = privateKeyPath
400
369
  ? yield* readPrivateKeyFile(privateKeyPath)
@@ -404,22 +373,7 @@ const handleAppleClient = Effect.fn(function* (opts) {
404
373
  simpleName: '--custom-redirect-uri',
405
374
  skipIf: skipWeb,
406
375
  skipMessage: `--custom-redirect-uri ${webSkipMessage}`,
407
- prompt: {
408
- prompt: '',
409
- placeholder: 'https://yoursite.com/oauth/callback',
410
- modifyOutput: UI.modifiers.piped([
411
- (output, status) => {
412
- if (status === 'idle') {
413
- return (`\nCustom redirect URI (optional):
414
- ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
415
- ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
416
- stripFirstBlankLine(output));
417
- }
418
- return `\nCustom redirect URI (optional):\n${stripFirstBlankLine(output)}`;
419
- },
420
- UI.modifiers.dimOnComplete,
421
- ]),
422
- },
376
+ prompt: optionalRedirectPrompt,
423
377
  });
424
378
  if (!clientName) {
425
379
  return yield* BadArgsError.make({ message: 'Client name is required.' });
@@ -448,14 +402,14 @@ ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all que
448
402
  `ID: ${response.client.id}`,
449
403
  `Services ID: ${response.client.client_id ?? servicesId}`,
450
404
  ];
451
- if (privateKey) {
405
+ if (privateKey && redirectUri) {
452
406
  summaryLines.push(`Team ID: ${teamId}`);
453
407
  summaryLines.push(`Key ID: ${keyId}`);
454
- summaryLines.push(chalk.bold(`\nAdd this return URL under your Services ID on ${link('https://developer.apple.com', 'developer.apple.com')}:\n${redirectUri}\n`));
455
- if (customRedirectUri) {
456
- summaryLines.push(`Your custom redirect must forward to ${chalk.bold(DEFAULT_OAUTH_CALLBACK_URL)} with all query parameters preserved.`);
457
- summaryLines.push(`You can test it by visiting: ${chalk.bold(redirectUri + '?test-redirect=true')}`);
458
- }
408
+ summaryLines.push(...redirectSetupMessages({
409
+ prompt: `Add this return URL under your Services ID on ${link('https://developer.apple.com', 'developer.apple.com')}`,
410
+ redirectUri,
411
+ showCustomRedirectInstructions: Boolean(customRedirectUri),
412
+ }));
459
413
  }
460
414
  yield* Effect.log(boxen(summaryLines.join('\n'), {
461
415
  dimBorder: true,
@@ -468,22 +422,7 @@ const handleClerkClient = Effect.fn(function* (opts) {
468
422
  simpleName: '--publishable-key',
469
423
  required: true,
470
424
  skipIf: false,
471
- prompt: {
472
- prompt: `Clerk publishable key ${chalk.dim(`(from ${link('https://dashboard.clerk.com/last-active?path=api-keys')})`)}`,
473
- placeholder: 'pk_********************************************************',
474
- modifyOutput: UI.modifiers.piped([
475
- UI.modifiers.topPadding,
476
- UI.modifiers.dimOnComplete,
477
- ]),
478
- validate: (val) => {
479
- if (!val) {
480
- return 'Publishable key is required';
481
- }
482
- if (!val.startsWith('pk_')) {
483
- return 'Invalid publishable key. It should start with "pk_".';
484
- }
485
- },
486
- },
425
+ prompt: clerkPublishableKeyPrompt({}),
487
426
  });
488
427
  if (!clientName) {
489
428
  return yield* BadArgsError.make({ message: 'Client name is required.' });
@@ -493,7 +432,7 @@ const handleClerkClient = Effect.fn(function* (opts) {
493
432
  message: 'Publishable key is required.',
494
433
  });
495
434
  }
496
- const domain = domainFromClerkKey(publishableKey);
435
+ const domain = clerkDomainFromPublishableKey(publishableKey);
497
436
  if (!domain) {
498
437
  return yield* BadArgsError.make({
499
438
  message: 'Invalid publishable key. Could not extract domain.',
@@ -520,27 +459,11 @@ const handleClerkClient = Effect.fn(function* (opts) {
520
459
  });
521
460
  const handleFirebaseClient = Effect.fn(function* (opts) {
522
461
  const { clientName, provider } = yield* getClientNameAndProvider('firebase', opts);
523
- const firebaseProjectIdRegex = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/;
524
462
  const projectId = yield* optOrPrompt(opts['project-id'], {
525
463
  simpleName: '--project-id',
526
464
  required: true,
527
465
  skipIf: false,
528
- prompt: {
529
- prompt: `Firebase project ID: (From Project Settings page on ${link('https://console.firebase.google.com/')})`,
530
- placeholder: '',
531
- modifyOutput: UI.modifiers.piped([
532
- UI.modifiers.topPadding,
533
- UI.modifiers.dimOnComplete,
534
- ]),
535
- validate: (val) => {
536
- if (!val) {
537
- return 'Project ID is required';
538
- }
539
- if (!firebaseProjectIdRegex.test(val)) {
540
- 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.';
541
- }
542
- },
543
- },
466
+ prompt: firebaseProjectIdPrompt({}),
544
467
  });
545
468
  // typeguard
546
469
  if (!clientName || !projectId) {
@@ -548,15 +471,14 @@ const handleFirebaseClient = Effect.fn(function* (opts) {
548
471
  message: 'Missing required arguments',
549
472
  });
550
473
  }
551
- if (!firebaseProjectIdRegex.test(projectId)) {
552
- return yield* BadArgsError.make({
553
- message: '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.',
554
- });
474
+ const validationError = validateFirebaseProjectId(projectId);
475
+ if (validationError) {
476
+ return yield* BadArgsError.make({ message: validationError });
555
477
  }
556
478
  const response = yield* addOAuthClient({
557
479
  providerId: provider.id,
558
480
  clientName,
559
- discoveryEndpoint: `https://securetoken.google.com/${encodeURIComponent(projectId)}/.well-known/openid-configuration`,
481
+ discoveryEndpoint: firebaseDiscoveryEndpoint(projectId),
560
482
  });
561
483
  yield* Effect.log(boxen([
562
484
  `Firebase OAuth client created: ${response.client.client_name}`,
@@ -590,29 +512,4 @@ export const authClientAddCmd = Effect.fn(function* (opts) {
590
512
  yield* Effect.logError(e.message);
591
513
  yield* Effect.log(chalk.dim('hint: run `instant-cli auth client add --help` for the list of available arguments'));
592
514
  })));
593
- function domainFromClerkKey(key) {
594
- try {
595
- const parts = key.split('_');
596
- const domainPartB64 = parts[parts.length - 1];
597
- const domainPart = base64Decode(domainPartB64);
598
- return domainPart.replace('$', '');
599
- }
600
- catch (e) {
601
- console.error('Error getting domain from clerk key', e);
602
- return null;
603
- }
604
- }
605
- // Base64 decode, switching to url-safe decode if we hit an error
606
- // Can't be sure which method Clerk uses because you can't generate
607
- // `+` or `/` with characters that go in a normal host. Urls with
608
- // chinese characters exist, they might encode to `+` or `/`, and
609
- // Clerk might support them, so we'll be safe and do both.
610
- function base64Decode(s) {
611
- try {
612
- return Buffer.from(s, 'base64').toString('utf-8');
613
- }
614
- catch (e) {
615
- return Buffer.from(s, 'base64url').toString('utf-8');
616
- }
617
- }
618
515
  //# sourceMappingURL=add.js.map