instant-cli 1.0.21-branch-cli-codex.25186296431.1 → 1.0.21-branch-cli-codex.25187076703.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.
@@ -1,4 +1,4 @@
1
1
 
2
- > instant-cli@1.0.21-branch-cli-codex.25186296431.1 build /home/runner/work/instant/instant/client/packages/cli
2
+ > instant-cli@1.0.21-branch-cli-codex.25187076703.1 build /home/runner/work/instant/instant/client/packages/cli
3
3
  > rm -rf dist; tsc -p tsconfig.build.json
4
4
 
@@ -4,6 +4,7 @@ import { NodeContext } from '@effect/platform-node';
4
4
  import { GlobalOpts } from '../src/context/globalOpts.ts';
5
5
  import { CurrentApp } from '../src/context/currentApp.ts';
6
6
  import { InstantHttpAuthed } from '../src/lib/http.ts';
7
+ import { BadArgsError } from '../src/errors.ts';
7
8
 
8
9
  // Prevent src/index.ts side-effect (program.parse) from running.
9
10
  vi.mock('../src/index.ts', () => ({}));
@@ -17,7 +18,10 @@ vi.mock('../src/ui/lib.ts', async (importOriginal) => {
17
18
  ...orig,
18
19
  renderUnwrap: (prompt: any) => {
19
20
  prompts.push(prompt);
20
- return Promise.resolve(mockPromptReturn);
21
+ const value = Array.isArray(mockPromptReturn)
22
+ ? mockPromptReturn.shift()
23
+ : mockPromptReturn;
24
+ return Promise.resolve(value);
21
25
  },
22
26
  };
23
27
  });
@@ -27,6 +31,7 @@ let mockClients: any[] = [];
27
31
  const providers = [
28
32
  { id: 'prov-google', provider_name: 'google' },
29
33
  { id: 'prov-github', provider_name: 'github' },
34
+ { id: 'prov-apple', provider_name: 'apple' },
30
35
  { id: 'prov-clerk', provider_name: 'clerk' },
31
36
  { id: 'prov-firebase', provider_name: 'firebase' },
32
37
  ];
@@ -37,6 +42,34 @@ vi.mock('../src/lib/oauth.ts', () => ({
37
42
  oauth_service_providers: providers,
38
43
  oauth_clients: mockClients,
39
44
  }),
45
+ findClientByIdOrName: ({ id, name }: { id?: string; name?: string }) =>
46
+ Effect.gen(function* () {
47
+ if (id && name) {
48
+ return yield* BadArgsError.make({
49
+ message: 'Cannot specify both --id and --name',
50
+ });
51
+ }
52
+ if (!id && !name) {
53
+ return yield* BadArgsError.make({
54
+ message: 'Must specify --id or --name',
55
+ });
56
+ }
57
+ const client = id
58
+ ? mockClients.find((entry) => entry.id === id)
59
+ : mockClients.find((entry) => entry.client_name === name);
60
+ if (!client) {
61
+ return yield* BadArgsError.make({
62
+ message: `OAuth client not found: ${id ?? name}`,
63
+ });
64
+ }
65
+ return {
66
+ client,
67
+ auth: {
68
+ oauth_service_providers: providers,
69
+ oauth_clients: mockClients,
70
+ },
71
+ };
72
+ }),
40
73
  updateOAuthClient: (params: any) => {
41
74
  updatedClients.push(params);
42
75
  const client = mockClients.find((c) => c.id === params.oauthClientId);
@@ -109,6 +142,16 @@ beforeEach(() => {
109
142
  client_name: 'github',
110
143
  client_id: 'old-gh-id',
111
144
  },
145
+ {
146
+ id: 'apple',
147
+ provider_id: 'prov-apple',
148
+ client_name: 'apple',
149
+ client_id: 'old.apple.service',
150
+ meta: {
151
+ teamId: 'OLDTEAM',
152
+ keyId: 'OLDKEY',
153
+ },
154
+ },
112
155
  {
113
156
  id: 'clerk',
114
157
  provider_id: 'prov-clerk',
@@ -165,13 +208,47 @@ describe('google', () => {
165
208
  expect(logs.join('\n')).toContain('Credentials: Instant dev credentials');
166
209
  });
167
210
 
211
+ test('rejects dev credentials with custom credential flags', async () => {
212
+ await run(
213
+ {
214
+ name: 'google-web',
215
+ 'dev-credentials': true,
216
+ 'client-id': 'new-google-id',
217
+ },
218
+ { yes: true },
219
+ );
220
+
221
+ expect(logs.join('\n')).toContain(
222
+ '--dev-credentials cannot be combined with --client-id',
223
+ );
224
+ expect(updatedClients).toHaveLength(0);
225
+ });
226
+
227
+ test('updates redirect URI only', async () => {
228
+ await run(
229
+ {
230
+ name: 'google-web',
231
+ 'custom-redirect-uri': 'https://example.com/oauth/callback',
232
+ },
233
+ { yes: true },
234
+ );
235
+
236
+ expect(updatedClients).toHaveLength(1);
237
+ expect(updatedClients[0]).toMatchObject({
238
+ oauthClientId: 'google-web',
239
+ redirectTo: 'https://example.com/oauth/callback',
240
+ });
241
+ expect(updatedClients[0].clientId).toBeUndefined();
242
+ expect(updatedClients[0].clientSecret).toBeUndefined();
243
+ });
244
+
168
245
  test('interactive Google web update can select dev credentials', async () => {
169
- mockPromptReturn = 'dev';
246
+ mockPromptReturn = 'dev-credentials';
170
247
  await run({ name: 'google-web' }, { yes: false });
171
248
 
172
249
  expect(prompts).toHaveLength(1);
173
250
  expect((prompts[0] as any).params.promptText).toBe(
174
- 'Select Google credential mode:',
251
+ 'What do you want to update?',
175
252
  );
176
253
  expect(updatedClients[0]).toMatchObject({
177
254
  oauthClientId: 'google-web',
@@ -214,6 +291,60 @@ describe('provider credential updates', () => {
214
291
  expect(updatedClients[0].clientId).toBeUndefined();
215
292
  });
216
293
 
294
+ test('interactive GitHub update can select redirect URI', async () => {
295
+ mockPromptReturn = ['redirect', 'https://example.com/oauth/callback'];
296
+
297
+ await run({ name: 'github' }, { yes: false });
298
+
299
+ expect(prompts).toHaveLength(2);
300
+ expect((prompts[0] as any).params.promptText).toBe(
301
+ 'What do you want to update?',
302
+ );
303
+ expect(updatedClients).toHaveLength(1);
304
+ expect(updatedClients[0]).toMatchObject({
305
+ oauthClientId: 'github',
306
+ redirectTo: 'https://example.com/oauth/callback',
307
+ });
308
+ });
309
+
310
+ test('updates Apple Services ID only', async () => {
311
+ await run(
312
+ {
313
+ name: 'apple',
314
+ 'services-id': 'new.apple.service',
315
+ },
316
+ { yes: true },
317
+ );
318
+
319
+ expect(updatedClients).toHaveLength(1);
320
+ expect(updatedClients[0]).toMatchObject({
321
+ oauthClientId: 'apple',
322
+ clientId: 'new.apple.service',
323
+ });
324
+ expect(updatedClients[0].meta).toBeUndefined();
325
+ });
326
+
327
+ test('updates Apple team and key metadata', async () => {
328
+ await run(
329
+ {
330
+ name: 'apple',
331
+ 'team-id': 'TEAM123',
332
+ 'key-id': 'KEY456',
333
+ },
334
+ { yes: true },
335
+ );
336
+
337
+ expect(updatedClients).toHaveLength(1);
338
+ expect(updatedClients[0]).toMatchObject({
339
+ oauthClientId: 'apple',
340
+ meta: {
341
+ teamId: 'TEAM123',
342
+ keyId: 'KEY456',
343
+ },
344
+ });
345
+ expect(updatedClients[0].clientId).toBeUndefined();
346
+ });
347
+
217
348
  test('updates Clerk publishable key and discovery endpoint', async () => {
218
349
  await run(
219
350
  {
@@ -252,6 +383,19 @@ describe('provider credential updates', () => {
252
383
  'https://securetoken.google.com/my-app-123/.well-known/openid-configuration',
253
384
  });
254
385
  });
386
+
387
+ test('rejects invalid Firebase project ID', async () => {
388
+ await run(
389
+ {
390
+ name: 'firebase',
391
+ 'project-id': 'BAD',
392
+ },
393
+ { yes: true },
394
+ );
395
+
396
+ expect(logs.join('\n')).toContain('Invalid Firebase project ID');
397
+ expect(updatedClients).toHaveLength(0);
398
+ });
255
399
  });
256
400
 
257
401
  describe('--yes validation', () => {
@@ -268,4 +412,31 @@ describe('--yes validation', () => {
268
412
  );
269
413
  expect(updatedClients).toHaveLength(0);
270
414
  });
415
+
416
+ test('rejects both id and name', async () => {
417
+ await run(
418
+ {
419
+ id: 'github',
420
+ name: 'github',
421
+ 'client-secret': 'new-gh-secret',
422
+ },
423
+ { yes: true },
424
+ );
425
+
426
+ expect(logs.join('\n')).toContain('Cannot specify both --id and --name');
427
+ expect(updatedClients).toHaveLength(0);
428
+ });
429
+
430
+ test('rejects unknown client name', async () => {
431
+ await run(
432
+ {
433
+ name: 'unknown',
434
+ 'client-secret': 'new-gh-secret',
435
+ },
436
+ { yes: true },
437
+ );
438
+
439
+ expect(logs.join('\n')).toContain('OAuth client not found');
440
+ expect(updatedClients).toHaveLength(0);
441
+ });
271
442
  });
@@ -1 +1 @@
1
- {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/commands/auth/client/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAupB5D,eAAO,MAAM,mBAAmB;;;;;ieA6D/B,CAAC"}
1
+ {"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../../../src/commands/auth/client/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AA6rB5D,eAAO,MAAM,mBAAmB;;;;;ieAqE/B,CAAC"}
@@ -3,7 +3,7 @@ import { FileSystem } from '@effect/platform';
3
3
  import { BadArgsError } from "../../../errors.js";
4
4
  import { GlobalOpts } from "../../../context/globalOpts.js";
5
5
  import { optOrPrompt, runUIEffect, validateRequired } from "../../../lib/ui.js";
6
- import { getAppsAuth, updateOAuthClient } from "../../../lib/oauth.js";
6
+ import { findClientByIdOrName, getAppsAuth, updateOAuthClient, } from "../../../lib/oauth.js";
7
7
  import { UI } from "../../../ui/index.js";
8
8
  import { DEFAULT_OAUTH_CALLBACK_URL } from '@instantdb/platform';
9
9
  import chalk from 'chalk';
@@ -48,22 +48,24 @@ const clientSecretPrompt = (provider) => ({
48
48
  UI.modifiers.dimOnComplete,
49
49
  ]),
50
50
  });
51
- const redirectPrompt = {
51
+ const makeRedirectPrompt = (heading) => ({
52
52
  prompt: '',
53
53
  placeholder: 'https://yoursite.com/oauth/callback',
54
54
  modifyOutput: UI.modifiers.piped([
55
55
  (output, status) => {
56
56
  if (status === 'idle') {
57
- return (`\nCustom redirect URI (optional):
57
+ return (`\n${heading}
58
58
  ${chalk.dim('With a custom redirect URI, users will see "Redirecting to yoursite.com..." for a more branded experience.')}
59
59
  ${chalk.dim(`Your URI must forward to ${DEFAULT_OAUTH_CALLBACK_URL} with all query parameters preserved.`)}\n\n` +
60
60
  output.replace(/^\n/, ''));
61
61
  }
62
- return `\nCustom redirect URI (optional):\n${output.replace(/^\n/, '')}`;
62
+ return `\n${heading}\n${output.replace(/^\n/, '')}`;
63
63
  },
64
64
  UI.modifiers.dimOnComplete,
65
65
  ]),
66
- };
66
+ });
67
+ const redirectPrompt = makeRedirectPrompt('Custom redirect URI (optional):');
68
+ const newRedirectPrompt = makeRedirectPrompt('New redirect URI:');
67
69
  const readPrivateKeyFile = Effect.fn('readPrivateKeyFile')(function* (path) {
68
70
  const fs = yield* FileSystem.FileSystem;
69
71
  const normalizedPath = process.platform === 'win32' ? path : path.replace(/\\(.)/g, '$1');
@@ -123,26 +125,11 @@ const resolveClient = Effect.fn(function* (params) {
123
125
  modifyOutput: UI.modifiers.piped([UI.modifiers.dimOnComplete]),
124
126
  })).pipe(Effect.catchTag('UIError', (e) => BadArgsError.make({ message: `UI error: ${e.message}` })));
125
127
  });
126
- const selectGoogleCredentialMode = Effect.fn(function* () {
128
+ const selectUpdateAction = Effect.fn(function* (options) {
127
129
  return yield* runUIEffect(new UI.Select({
128
- options: [
129
- {
130
- label: 'Use my own credentials' +
131
- chalk.dim(' (client ID and secret from Google Console)'),
132
- value: 'custom',
133
- },
134
- {
135
- label: 'Use dev credentials' +
136
- chalk.dim(' (works on localhost and Expo, no Google setup)'),
137
- value: 'dev',
138
- },
139
- ],
140
- promptText: 'Select Google credential mode:',
141
- modifyOutput: UI.modifiers.piped([
142
- UI.modifiers.topPadding,
143
- UI.modifiers.dimOnComplete,
144
- ]),
145
- defaultValue: 'custom',
130
+ options,
131
+ promptText: 'What do you want to update?',
132
+ modifyOutput: UI.modifiers.piped([UI.modifiers.dimOnComplete]),
146
133
  })).pipe(Effect.catchTag('UIError', (e) => BadArgsError.make({ message: `UI error: ${e.message}` })));
147
134
  });
148
135
  const updateGoogleToDevCredentials = Effect.fn(function* (client) {
@@ -162,6 +149,7 @@ const updateGoogleToDevCredentials = Effect.fn(function* (client) {
162
149
  });
163
150
  const handleGoogleUpdate = Effect.fn(function* (opts, client) {
164
151
  const { yes } = yield* GlobalOpts;
152
+ const consoleUrl = link('https://console.developers.google.com/apis/credentials');
165
153
  const appType = metaString(client, 'appType');
166
154
  const isWeb = appType === 'web' || appType === undefined;
167
155
  const devCredentialsFlag = isTrueFlag(getFlag(opts, 'dev-credentials'));
@@ -202,28 +190,59 @@ const handleGoogleUpdate = Effect.fn(function* (opts, client) {
202
190
  message: 'Must specify at least one of --client-id, --client-secret, --custom-redirect-uri, or --dev-credentials.',
203
191
  });
204
192
  }
205
- const selectedMode = isWeb && !hasAnyUpdateFlag && !yes
206
- ? yield* selectGoogleCredentialMode()
207
- : 'custom';
208
- if (selectedMode === 'dev') {
209
- return yield* updateGoogleToDevCredentials(client);
210
- }
211
- const promptAll = !hasAnyUpdateFlag && !yes;
212
193
  const switchingFromShared = Boolean(client.use_shared_credentials);
194
+ if (!hasAnyUpdateFlag && !yes) {
195
+ yield* Effect.log(`\nCurrent mode: ${switchingFromShared
196
+ ? chalk.bold('Instant dev credentials')
197
+ : 'custom credentials'}`);
198
+ if (!switchingFromShared) {
199
+ const action = yield* selectUpdateAction([
200
+ { label: 'Rotate credentials', value: 'rotate' },
201
+ {
202
+ label: 'Switch to Instant dev credentials' +
203
+ chalk.dim(' (localhost and Expo, no Google setup)'),
204
+ value: 'dev-credentials',
205
+ },
206
+ { label: 'Update redirect URI', value: 'redirect' },
207
+ ]);
208
+ if (action === 'dev-credentials') {
209
+ return yield* updateGoogleToDevCredentials(client);
210
+ }
211
+ if (action === 'redirect') {
212
+ const redirectTo = yield* promptFlag(opts, 'custom-redirect-uri', {
213
+ promptIf: true,
214
+ required: true,
215
+ prompt: newRedirectPrompt,
216
+ });
217
+ const response = yield* updateOAuthClient({
218
+ oauthClientId: client.id,
219
+ redirectTo,
220
+ });
221
+ yield* Effect.log(boxen([
222
+ `Google OAuth client updated: ${response.client.client_name}`,
223
+ `ID: ${response.client.id}`,
224
+ `Redirect URI: ${redirectTo}`,
225
+ ].join('\n'), { dimBorder: true, padding: { right: 1, left: 1 } }));
226
+ return;
227
+ }
228
+ }
229
+ }
230
+ const promptCredentials = !hasAnyUpdateFlag && !yes;
213
231
  const clientId = yield* promptFlag(opts, 'client-id', {
214
- promptIf: promptAll || (switchingFromShared && !hasFlag(opts, 'client-id')),
215
- required: promptAll || switchingFromShared,
216
- prompt: clientIdPrompt(link('https://console.developers.google.com/apis/credentials')),
232
+ promptIf: promptCredentials || (switchingFromShared && !hasFlag(opts, 'client-id')),
233
+ required: promptCredentials || switchingFromShared,
234
+ prompt: clientIdPrompt(consoleUrl),
217
235
  });
218
236
  const clientSecret = yield* promptFlag(opts, 'client-secret', {
219
237
  promptIf: isWeb &&
220
- (promptAll || (switchingFromShared && !hasFlag(opts, 'client-secret'))),
221
- required: isWeb && (promptAll || switchingFromShared),
222
- prompt: clientSecretPrompt(link('https://console.developers.google.com/apis/credentials')),
238
+ (promptCredentials ||
239
+ (switchingFromShared && !hasFlag(opts, 'client-secret'))),
240
+ required: isWeb && (promptCredentials || switchingFromShared),
241
+ prompt: clientSecretPrompt(consoleUrl),
223
242
  });
224
243
  const customRedirectUri = isWeb
225
244
  ? yield* promptFlag(opts, 'custom-redirect-uri', {
226
- promptIf: promptAll,
245
+ promptIf: switchingFromShared && promptCredentials,
227
246
  required: false,
228
247
  prompt: redirectPrompt,
229
248
  })
@@ -266,21 +285,30 @@ const handleClientIdSecretUpdate = Effect.fn(function* (params) {
266
285
  message: 'Must specify at least one of --client-id, --client-secret, or --custom-redirect-uri.',
267
286
  });
268
287
  }
269
- const promptAll = !hasAnyUpdateFlag && !yes;
288
+ let promptCredentials = false;
289
+ let promptRedirect = false;
290
+ if (!hasAnyUpdateFlag && !yes) {
291
+ const action = yield* selectUpdateAction([
292
+ { label: 'Rotate credentials', value: 'rotate' },
293
+ { label: 'Update redirect URI', value: 'redirect' },
294
+ ]);
295
+ promptCredentials = action === 'rotate';
296
+ promptRedirect = action === 'redirect';
297
+ }
270
298
  const clientId = yield* promptFlag(params.opts, 'client-id', {
271
- promptIf: promptAll,
272
- required: promptAll,
299
+ promptIf: promptCredentials,
300
+ required: promptCredentials,
273
301
  prompt: clientIdPrompt(link(params.providerUrl)),
274
302
  });
275
303
  const clientSecret = yield* promptFlag(params.opts, 'client-secret', {
276
- promptIf: promptAll,
277
- required: promptAll,
304
+ promptIf: promptCredentials,
305
+ required: promptCredentials,
278
306
  prompt: clientSecretPrompt(link(params.providerUrl)),
279
307
  });
280
308
  const customRedirectUri = yield* promptFlag(params.opts, 'custom-redirect-uri', {
281
- promptIf: promptAll,
282
- required: false,
283
- prompt: redirectPrompt,
309
+ promptIf: promptRedirect,
310
+ required: promptRedirect,
311
+ prompt: promptRedirect ? newRedirectPrompt : redirectPrompt,
284
312
  });
285
313
  const response = yield* updateOAuthClient({
286
314
  oauthClientId: params.client.id,
@@ -475,42 +503,48 @@ const handleFirebaseUpdate = Effect.fn(function* (opts, client) {
475
503
  });
476
504
  export const authClientUpdateCmd = Effect.fn(function* (opts) {
477
505
  const { yes } = yield* GlobalOpts;
478
- const auth = yield* getAppsAuth();
479
- const client = yield* resolveClient({
480
- id: getFlag(opts, 'id'),
481
- name: getFlag(opts, 'name'),
482
- yes,
483
- clients: (auth.oauth_clients ?? []),
484
- });
485
- const provider = (auth.oauth_service_providers ?? []).find((entry) => entry.id === client.provider_id);
506
+ const id = getFlag(opts, 'id');
507
+ const name = getFlag(opts, 'name');
508
+ const { auth, client } = id || name
509
+ ? yield* findClientByIdOrName({ id, name })
510
+ : {
511
+ auth: yield* getAppsAuth(),
512
+ client: undefined,
513
+ };
514
+ const resolvedClient = client ??
515
+ (yield* resolveClient({
516
+ yes,
517
+ clients: (auth.oauth_clients ?? []),
518
+ }));
519
+ const provider = (auth.oauth_service_providers ?? []).find((entry) => entry.id === resolvedClient.provider_id);
486
520
  if (!provider) {
487
521
  return yield* BadArgsError.make({
488
- message: `OAuth provider not found for client: ${client.client_name}`,
522
+ message: `OAuth provider not found for client: ${resolvedClient.client_name}`,
489
523
  });
490
524
  }
491
525
  switch (provider.provider_name) {
492
526
  case 'google':
493
- return yield* handleGoogleUpdate(opts, client);
527
+ return yield* handleGoogleUpdate(opts, resolvedClient);
494
528
  case 'github':
495
529
  return yield* handleClientIdSecretUpdate({
496
530
  opts,
497
- client,
531
+ client: resolvedClient,
498
532
  providerLabel: 'GitHub',
499
533
  providerUrl: 'https://github.com/settings/developers',
500
534
  });
501
535
  case 'linkedin':
502
536
  return yield* handleClientIdSecretUpdate({
503
537
  opts,
504
- client,
538
+ client: resolvedClient,
505
539
  providerLabel: 'LinkedIn',
506
540
  providerUrl: 'https://www.linkedin.com/developers/apps',
507
541
  });
508
542
  case 'apple':
509
- return yield* handleAppleUpdate(opts, client);
543
+ return yield* handleAppleUpdate(opts, resolvedClient);
510
544
  case 'clerk':
511
- return yield* handleClerkUpdate(opts, client);
545
+ return yield* handleClerkUpdate(opts, resolvedClient);
512
546
  case 'firebase':
513
- return yield* handleFirebaseUpdate(opts, client);
547
+ return yield* handleFirebaseUpdate(opts, resolvedClient);
514
548
  default:
515
549
  return yield* BadArgsError.make({
516
550
  message: `Updating ${provider.provider_name} OAuth clients is not supported.`,