instant-cli 1.0.22 → 1.0.23-branch-cli-codex-update.25390647417.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.
- package/.turbo/turbo-build.log +1 -1
- package/__tests__/authClientAddGoogle.test.ts +92 -0
- package/__tests__/authClientList.test.ts +90 -0
- package/__tests__/authClientUpdate.test.ts +583 -0
- package/__tests__/oauthMock.ts +9 -1
- package/__tests__/redirectUriPrompt.test.ts +27 -0
- package/dist/commands/auth/client/add.d.ts +1 -2
- package/dist/commands/auth/client/add.d.ts.map +1 -1
- package/dist/commands/auth/client/add.js +173 -276
- package/dist/commands/auth/client/add.js.map +1 -1
- package/dist/commands/auth/client/delete.d.ts +1 -2
- package/dist/commands/auth/client/delete.d.ts.map +1 -1
- package/dist/commands/auth/client/delete.js +8 -18
- package/dist/commands/auth/client/delete.js.map +1 -1
- package/dist/commands/auth/client/list.d.ts.map +1 -1
- package/dist/commands/auth/client/list.js +11 -2
- package/dist/commands/auth/client/list.js.map +1 -1
- package/dist/commands/auth/client/shared.d.ts +72 -0
- package/dist/commands/auth/client/shared.d.ts.map +1 -0
- package/dist/commands/auth/client/shared.js +145 -0
- package/dist/commands/auth/client/shared.js.map +1 -0
- package/dist/commands/auth/client/update.d.ts +8 -0
- package/dist/commands/auth/client/update.d.ts.map +1 -0
- package/dist/commands/auth/client/update.js +515 -0
- package/dist/commands/auth/client/update.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -13
- package/dist/index.js.map +1 -1
- package/dist/lib/oauth.d.ts +114 -7
- package/dist/lib/oauth.d.ts.map +1 -1
- package/dist/lib/oauth.js +51 -1
- package/dist/lib/oauth.js.map +1 -1
- package/package.json +4 -4
- package/src/commands/auth/client/add.ts +251 -330
- package/src/commands/auth/client/delete.ts +8 -20
- package/src/commands/auth/client/list.ts +21 -2
- package/src/commands/auth/client/shared.ts +195 -0
- package/src/commands/auth/client/update.ts +853 -0
- package/src/index.ts +74 -13
- package/src/lib/oauth.ts +83 -1
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import { test, expect, describe, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Effect, Layer, Logger } from 'effect';
|
|
3
|
+
import { NodeContext } from '@effect/platform-node';
|
|
4
|
+
import { GlobalOpts } from '../src/context/globalOpts.ts';
|
|
5
|
+
import { CurrentApp } from '../src/context/currentApp.ts';
|
|
6
|
+
import { InstantHttpAuthed } from '../src/lib/http.ts';
|
|
7
|
+
import { BadArgsError } from '../src/errors.ts';
|
|
8
|
+
|
|
9
|
+
// Prevent src/index.ts side-effect (program.parse) from running.
|
|
10
|
+
vi.mock('../src/index.ts', () => ({}));
|
|
11
|
+
|
|
12
|
+
let prompts: any[] = [];
|
|
13
|
+
let mockPromptReturn: any = '';
|
|
14
|
+
|
|
15
|
+
vi.mock('../src/ui/lib.ts', async (importOriginal) => {
|
|
16
|
+
const orig: any = await importOriginal();
|
|
17
|
+
return {
|
|
18
|
+
...orig,
|
|
19
|
+
renderUnwrap: (prompt: any) => {
|
|
20
|
+
prompts.push(prompt);
|
|
21
|
+
const value = Array.isArray(mockPromptReturn)
|
|
22
|
+
? mockPromptReturn.shift()
|
|
23
|
+
: mockPromptReturn;
|
|
24
|
+
return Promise.resolve(value);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
let updatedClients: any[] = [];
|
|
30
|
+
let mockClients: any[] = [];
|
|
31
|
+
const providers = [
|
|
32
|
+
{ id: 'prov-google', provider_name: 'google' },
|
|
33
|
+
{ id: 'prov-github', provider_name: 'github' },
|
|
34
|
+
{ id: 'prov-linkedin', provider_name: 'linkedin' },
|
|
35
|
+
{ id: 'prov-apple', provider_name: 'apple' },
|
|
36
|
+
{ id: 'prov-clerk', provider_name: 'clerk' },
|
|
37
|
+
{ id: 'prov-firebase', provider_name: 'firebase' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
vi.mock('../src/lib/oauth.ts', () => ({
|
|
41
|
+
getAppsAuth: () =>
|
|
42
|
+
Effect.succeed({
|
|
43
|
+
oauth_service_providers: providers,
|
|
44
|
+
oauth_clients: mockClients,
|
|
45
|
+
}),
|
|
46
|
+
findClientByIdOrName: ({ id, name }: { id?: string; name?: string }) =>
|
|
47
|
+
Effect.gen(function* () {
|
|
48
|
+
if (id && name) {
|
|
49
|
+
return yield* BadArgsError.make({
|
|
50
|
+
message: 'Cannot specify both --id and --name',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!id && !name) {
|
|
54
|
+
return yield* BadArgsError.make({
|
|
55
|
+
message: 'Must specify --id or --name',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const client = id
|
|
59
|
+
? mockClients.find((entry) => entry.id === id)
|
|
60
|
+
: mockClients.find((entry) => entry.client_name === name);
|
|
61
|
+
if (!client) {
|
|
62
|
+
return yield* BadArgsError.make({
|
|
63
|
+
message: `OAuth client not found: ${id ?? name}`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
client,
|
|
68
|
+
auth: {
|
|
69
|
+
oauth_service_providers: providers,
|
|
70
|
+
oauth_clients: mockClients,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}),
|
|
74
|
+
updateOAuthClient: (params: any) => {
|
|
75
|
+
updatedClients.push(params);
|
|
76
|
+
const client = mockClients.find((c) => c.id === params.oauthClientId);
|
|
77
|
+
return Effect.succeed({
|
|
78
|
+
client: {
|
|
79
|
+
id: params.oauthClientId,
|
|
80
|
+
client_name: client?.client_name ?? 'unknown',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
},
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
const { authClientUpdateCmd } = await import(
|
|
87
|
+
'../src/commands/auth/client/update.ts'
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
let logs: string[] = [];
|
|
91
|
+
|
|
92
|
+
const run = (flags: Record<string, any>, { yes }: { yes: boolean }) =>
|
|
93
|
+
Effect.runPromise(
|
|
94
|
+
authClientUpdateCmd(flags as any).pipe(
|
|
95
|
+
Effect.provide(
|
|
96
|
+
Layer.mergeAll(
|
|
97
|
+
Layer.succeed(GlobalOpts, { yes }),
|
|
98
|
+
Layer.succeed(CurrentApp, { appId: 'test-app', source: 'env' }),
|
|
99
|
+
Layer.succeed(InstantHttpAuthed, {} as any),
|
|
100
|
+
NodeContext.layer,
|
|
101
|
+
Logger.replace(
|
|
102
|
+
Logger.defaultLogger,
|
|
103
|
+
Logger.make(({ message }) => {
|
|
104
|
+
logs.push(String(message));
|
|
105
|
+
}),
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
prompts = [];
|
|
114
|
+
updatedClients = [];
|
|
115
|
+
logs = [];
|
|
116
|
+
mockPromptReturn = '';
|
|
117
|
+
mockClients = [
|
|
118
|
+
{
|
|
119
|
+
id: 'google-shared',
|
|
120
|
+
provider_id: 'prov-google',
|
|
121
|
+
client_name: 'google-shared',
|
|
122
|
+
meta: { appType: 'web' },
|
|
123
|
+
use_shared_credentials: true,
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'google-web',
|
|
127
|
+
provider_id: 'prov-google',
|
|
128
|
+
client_name: 'google-web',
|
|
129
|
+
client_id: 'old-google-id',
|
|
130
|
+
redirect_to: 'https://api.instantdb.com/runtime/oauth/callback',
|
|
131
|
+
meta: { appType: 'web' },
|
|
132
|
+
use_shared_credentials: false,
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'google-ios',
|
|
136
|
+
provider_id: 'prov-google',
|
|
137
|
+
client_name: 'google-ios',
|
|
138
|
+
meta: { appType: 'ios' },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: 'github',
|
|
142
|
+
provider_id: 'prov-github',
|
|
143
|
+
client_name: 'github',
|
|
144
|
+
client_id: 'old-gh-id',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'linkedin',
|
|
148
|
+
provider_id: 'prov-linkedin',
|
|
149
|
+
client_name: 'linkedin',
|
|
150
|
+
client_id: 'old-linkedin-id',
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'apple',
|
|
154
|
+
provider_id: 'prov-apple',
|
|
155
|
+
client_name: 'apple',
|
|
156
|
+
client_id: 'old.apple.service',
|
|
157
|
+
meta: {
|
|
158
|
+
teamId: 'OLDTEAM',
|
|
159
|
+
keyId: 'OLDKEY',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'clerk',
|
|
164
|
+
provider_id: 'prov-clerk',
|
|
165
|
+
client_name: 'clerk',
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'firebase',
|
|
169
|
+
provider_id: 'prov-firebase',
|
|
170
|
+
client_name: 'firebase',
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('google', () => {
|
|
176
|
+
test('upgrades shared dev credentials to custom credentials', async () => {
|
|
177
|
+
await run(
|
|
178
|
+
{
|
|
179
|
+
name: 'google-shared',
|
|
180
|
+
'client-id': 'new-google-id',
|
|
181
|
+
'client-secret': 'new-google-secret',
|
|
182
|
+
},
|
|
183
|
+
{ yes: true },
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(updatedClients).toHaveLength(1);
|
|
187
|
+
expect(updatedClients[0]).toMatchObject({
|
|
188
|
+
oauthClientId: 'google-shared',
|
|
189
|
+
clientId: 'new-google-id',
|
|
190
|
+
clientSecret: 'new-google-secret',
|
|
191
|
+
redirectTo: 'https://api.instantdb.com/runtime/oauth/callback',
|
|
192
|
+
useSharedCredentials: false,
|
|
193
|
+
});
|
|
194
|
+
expect(logs.join('\n')).toContain(
|
|
195
|
+
'This client no longer uses Instant dev credentials.',
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test('rejects partial shared Google web credential updates with --yes', async () => {
|
|
200
|
+
await run(
|
|
201
|
+
{
|
|
202
|
+
name: 'google-shared',
|
|
203
|
+
'client-id': 'new-google-id',
|
|
204
|
+
},
|
|
205
|
+
{ yes: true },
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
expect(prompts).toHaveLength(0);
|
|
209
|
+
expect(logs.join('\n')).toContain(
|
|
210
|
+
'Must specify both --client-id and --client-secret when switching from Instant dev credentials to custom credentials with --yes.',
|
|
211
|
+
);
|
|
212
|
+
expect(updatedClients).toHaveLength(0);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('switches custom Google web client back to dev credentials', async () => {
|
|
216
|
+
await run(
|
|
217
|
+
{
|
|
218
|
+
name: 'google-web',
|
|
219
|
+
'dev-credentials': true,
|
|
220
|
+
},
|
|
221
|
+
{ yes: true },
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
expect(updatedClients).toHaveLength(1);
|
|
225
|
+
expect(updatedClients[0]).toMatchObject({
|
|
226
|
+
oauthClientId: 'google-web',
|
|
227
|
+
clientId: null,
|
|
228
|
+
clientSecret: null,
|
|
229
|
+
useSharedCredentials: true,
|
|
230
|
+
redirectTo: null,
|
|
231
|
+
});
|
|
232
|
+
const output = logs.join('\n');
|
|
233
|
+
expect(output).toContain('Credentials: Instant dev credentials');
|
|
234
|
+
expect(output).toContain('Ready for production? Run:');
|
|
235
|
+
expect(output).toContain(
|
|
236
|
+
'instant-cli auth client update --name google-web',
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('rejects dev credentials with custom credential flags', async () => {
|
|
241
|
+
await run(
|
|
242
|
+
{
|
|
243
|
+
name: 'google-web',
|
|
244
|
+
'dev-credentials': true,
|
|
245
|
+
'client-id': 'new-google-id',
|
|
246
|
+
},
|
|
247
|
+
{ yes: true },
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
expect(logs.join('\n')).toContain(
|
|
251
|
+
'--dev-credentials cannot be combined with --client-id',
|
|
252
|
+
);
|
|
253
|
+
expect(updatedClients).toHaveLength(0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('updates redirect URI only', async () => {
|
|
257
|
+
await run(
|
|
258
|
+
{
|
|
259
|
+
name: 'google-web',
|
|
260
|
+
'custom-redirect-uri': 'https://example.com/oauth/callback',
|
|
261
|
+
},
|
|
262
|
+
{ yes: true },
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
expect(updatedClients).toHaveLength(1);
|
|
266
|
+
expect(updatedClients[0]).toMatchObject({
|
|
267
|
+
oauthClientId: 'google-web',
|
|
268
|
+
redirectTo: 'https://example.com/oauth/callback',
|
|
269
|
+
});
|
|
270
|
+
expect(updatedClients[0].clientId).toBeUndefined();
|
|
271
|
+
expect(updatedClients[0].clientSecret).toBeUndefined();
|
|
272
|
+
expect(logs.join('\n')).toContain(
|
|
273
|
+
'Add this redirect URI in Google Console',
|
|
274
|
+
);
|
|
275
|
+
expect(logs.join('\n')).toContain('Your custom redirect must forward to');
|
|
276
|
+
expect(logs.join('\n')).toContain(
|
|
277
|
+
'https://example.com/oauth/callback?test-redirect=true',
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('interactive Google web update can select dev credentials', async () => {
|
|
282
|
+
mockPromptReturn = 'dev';
|
|
283
|
+
await run({ name: 'google-web' }, { yes: false });
|
|
284
|
+
|
|
285
|
+
expect(prompts).toHaveLength(1);
|
|
286
|
+
expect((prompts[0] as any).params.promptText).toBe(
|
|
287
|
+
'What do you want to update?',
|
|
288
|
+
);
|
|
289
|
+
expect(updatedClients[0]).toMatchObject({
|
|
290
|
+
oauthClientId: 'google-web',
|
|
291
|
+
useSharedCredentials: true,
|
|
292
|
+
redirectTo: null,
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test('interactive shared Google web update can switch credential mode', async () => {
|
|
297
|
+
mockPromptReturn = ['custom', 'new-google-id', 'new-google-secret', ''];
|
|
298
|
+
await run({ name: 'google-shared' }, { yes: false });
|
|
299
|
+
|
|
300
|
+
expect(prompts).toHaveLength(4);
|
|
301
|
+
expect((prompts[0] as any).params.promptText).toBe(
|
|
302
|
+
'Choose credential mode:',
|
|
303
|
+
);
|
|
304
|
+
expect((prompts[0] as any).params.options).toMatchObject([
|
|
305
|
+
{
|
|
306
|
+
label: 'Custom Google credentials',
|
|
307
|
+
value: 'custom',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
label: expect.stringContaining('Instant dev credentials'),
|
|
311
|
+
value: 'none',
|
|
312
|
+
},
|
|
313
|
+
]);
|
|
314
|
+
expect(updatedClients[0]).toMatchObject({
|
|
315
|
+
oauthClientId: 'google-shared',
|
|
316
|
+
clientId: 'new-google-id',
|
|
317
|
+
clientSecret: 'new-google-secret',
|
|
318
|
+
redirectTo: 'https://api.instantdb.com/runtime/oauth/callback',
|
|
319
|
+
useSharedCredentials: false,
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test('interactive shared Google web update can keep dev credentials', async () => {
|
|
324
|
+
mockPromptReturn = 'none';
|
|
325
|
+
await run({ name: 'google-shared' }, { yes: false });
|
|
326
|
+
|
|
327
|
+
expect(prompts).toHaveLength(1);
|
|
328
|
+
expect((prompts[0] as any).params.promptText).toBe(
|
|
329
|
+
'Choose credential mode:',
|
|
330
|
+
);
|
|
331
|
+
expect(updatedClients).toHaveLength(0);
|
|
332
|
+
expect(logs.join('\n')).toContain('No changes made.');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test('rejects dev credentials for native Google clients', async () => {
|
|
336
|
+
await run(
|
|
337
|
+
{
|
|
338
|
+
name: 'google-ios',
|
|
339
|
+
'dev-credentials': true,
|
|
340
|
+
},
|
|
341
|
+
{ yes: true },
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
expect(logs.join('\n')).toContain(
|
|
345
|
+
'--dev-credentials is only supported for Google web clients',
|
|
346
|
+
);
|
|
347
|
+
expect(updatedClients).toHaveLength(0);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test('interactive native Google update only offers credential rotation', async () => {
|
|
351
|
+
mockPromptReturn = ['custom', 'native-google-id'];
|
|
352
|
+
|
|
353
|
+
await run({ name: 'google-ios' }, { yes: false });
|
|
354
|
+
|
|
355
|
+
expect(prompts).toHaveLength(2);
|
|
356
|
+
expect((prompts[0] as any).params.options.map((o: any) => o.value)).toEqual(
|
|
357
|
+
['custom'],
|
|
358
|
+
);
|
|
359
|
+
expect(updatedClients[0]).toMatchObject({
|
|
360
|
+
oauthClientId: 'google-ios',
|
|
361
|
+
clientId: 'native-google-id',
|
|
362
|
+
});
|
|
363
|
+
expect(updatedClients[0].clientSecret).toBeUndefined();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('provider credential updates', () => {
|
|
368
|
+
test('updates GitHub client secret without requiring a new client ID', async () => {
|
|
369
|
+
await run(
|
|
370
|
+
{
|
|
371
|
+
name: 'github',
|
|
372
|
+
'client-secret': 'new-gh-secret',
|
|
373
|
+
},
|
|
374
|
+
{ yes: true },
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
expect(updatedClients).toHaveLength(1);
|
|
378
|
+
expect(updatedClients[0]).toMatchObject({
|
|
379
|
+
oauthClientId: 'github',
|
|
380
|
+
clientSecret: 'new-gh-secret',
|
|
381
|
+
});
|
|
382
|
+
expect(updatedClients[0].clientId).toBeUndefined();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test('interactive GitHub update can select redirect URI', async () => {
|
|
386
|
+
mockPromptReturn = ['redirect', 'https://example.com/oauth/callback'];
|
|
387
|
+
|
|
388
|
+
await run({ name: 'github' }, { yes: false });
|
|
389
|
+
|
|
390
|
+
expect(prompts).toHaveLength(2);
|
|
391
|
+
expect((prompts[0] as any).params.promptText).toBe(
|
|
392
|
+
'What do you want to update?',
|
|
393
|
+
);
|
|
394
|
+
expect(updatedClients).toHaveLength(1);
|
|
395
|
+
expect(updatedClients[0]).toMatchObject({
|
|
396
|
+
oauthClientId: 'github',
|
|
397
|
+
redirectTo: 'https://example.com/oauth/callback',
|
|
398
|
+
});
|
|
399
|
+
expect(logs.join('\n')).toContain(
|
|
400
|
+
'Add this callback URL in your GitHub OAuth App settings',
|
|
401
|
+
);
|
|
402
|
+
expect(logs.join('\n')).toContain(
|
|
403
|
+
'https://example.com/oauth/callback?test-redirect=true',
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('LinkedIn redirect update prints app settings guidance', async () => {
|
|
408
|
+
await run(
|
|
409
|
+
{
|
|
410
|
+
name: 'linkedin',
|
|
411
|
+
'custom-redirect-uri': 'https://example.com/linkedin/callback',
|
|
412
|
+
},
|
|
413
|
+
{ yes: true },
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
expect(updatedClients).toHaveLength(1);
|
|
417
|
+
expect(updatedClients[0]).toMatchObject({
|
|
418
|
+
oauthClientId: 'linkedin',
|
|
419
|
+
redirectTo: 'https://example.com/linkedin/callback',
|
|
420
|
+
});
|
|
421
|
+
expect(logs.join('\n')).toContain(
|
|
422
|
+
'Add this redirect URI in your LinkedIn app settings',
|
|
423
|
+
);
|
|
424
|
+
expect(logs.join('\n')).toContain(
|
|
425
|
+
'https://example.com/linkedin/callback?test-redirect=true',
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('updates Apple Services ID only', async () => {
|
|
430
|
+
await run(
|
|
431
|
+
{
|
|
432
|
+
name: 'apple',
|
|
433
|
+
'services-id': 'new.apple.service',
|
|
434
|
+
},
|
|
435
|
+
{ yes: true },
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
expect(updatedClients).toHaveLength(1);
|
|
439
|
+
expect(updatedClients[0]).toMatchObject({
|
|
440
|
+
oauthClientId: 'apple',
|
|
441
|
+
clientId: 'new.apple.service',
|
|
442
|
+
});
|
|
443
|
+
expect(updatedClients[0].meta).toBeUndefined();
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test('updates Apple team and key metadata', async () => {
|
|
447
|
+
await run(
|
|
448
|
+
{
|
|
449
|
+
name: 'apple',
|
|
450
|
+
'team-id': 'TEAM123',
|
|
451
|
+
'key-id': 'KEY456',
|
|
452
|
+
},
|
|
453
|
+
{ yes: true },
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
expect(updatedClients).toHaveLength(1);
|
|
457
|
+
expect(updatedClients[0]).toMatchObject({
|
|
458
|
+
oauthClientId: 'apple',
|
|
459
|
+
meta: {
|
|
460
|
+
teamId: 'TEAM123',
|
|
461
|
+
keyId: 'KEY456',
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
expect(updatedClients[0].clientId).toBeUndefined();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test('Apple redirect update prints Services ID return URL guidance', async () => {
|
|
468
|
+
await run(
|
|
469
|
+
{
|
|
470
|
+
name: 'apple',
|
|
471
|
+
'custom-redirect-uri': 'https://example.com/apple/callback',
|
|
472
|
+
},
|
|
473
|
+
{ yes: true },
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
expect(updatedClients).toHaveLength(1);
|
|
477
|
+
expect(updatedClients[0]).toMatchObject({
|
|
478
|
+
oauthClientId: 'apple',
|
|
479
|
+
redirectTo: 'https://example.com/apple/callback',
|
|
480
|
+
});
|
|
481
|
+
expect(logs.join('\n')).toContain(
|
|
482
|
+
'Add this return URL under your Services ID on',
|
|
483
|
+
);
|
|
484
|
+
expect(logs.join('\n')).toContain(
|
|
485
|
+
'https://example.com/apple/callback?test-redirect=true',
|
|
486
|
+
);
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test('updates Clerk publishable key and discovery endpoint', async () => {
|
|
490
|
+
await run(
|
|
491
|
+
{
|
|
492
|
+
name: 'clerk',
|
|
493
|
+
'publishable-key':
|
|
494
|
+
'pk_test_Z3VpZGluZy1wZWdhc3VzLTkzLmNsZXJrLmFjY291bnRzLmRldiQ',
|
|
495
|
+
},
|
|
496
|
+
{ yes: true },
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
expect(updatedClients).toHaveLength(1);
|
|
500
|
+
expect(updatedClients[0]).toMatchObject({
|
|
501
|
+
oauthClientId: 'clerk',
|
|
502
|
+
discoveryEndpoint:
|
|
503
|
+
'https://guiding-pegasus-93.clerk.accounts.dev/.well-known/openid-configuration',
|
|
504
|
+
meta: {
|
|
505
|
+
clerkPublishableKey:
|
|
506
|
+
'pk_test_Z3VpZGluZy1wZWdhc3VzLTkzLmNsZXJrLmFjY291bnRzLmRldiQ',
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
test('updates Firebase project ID and discovery endpoint', async () => {
|
|
512
|
+
await run(
|
|
513
|
+
{
|
|
514
|
+
name: 'firebase',
|
|
515
|
+
'project-id': 'my-app-123',
|
|
516
|
+
},
|
|
517
|
+
{ yes: true },
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
expect(updatedClients).toHaveLength(1);
|
|
521
|
+
expect(updatedClients[0]).toMatchObject({
|
|
522
|
+
oauthClientId: 'firebase',
|
|
523
|
+
discoveryEndpoint:
|
|
524
|
+
'https://securetoken.google.com/my-app-123/.well-known/openid-configuration',
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
test('rejects invalid Firebase project ID', async () => {
|
|
529
|
+
await run(
|
|
530
|
+
{
|
|
531
|
+
name: 'firebase',
|
|
532
|
+
'project-id': 'BAD',
|
|
533
|
+
},
|
|
534
|
+
{ yes: true },
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
expect(logs.join('\n')).toContain('Invalid Firebase project ID');
|
|
538
|
+
expect(updatedClients).toHaveLength(0);
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
describe('--yes validation', () => {
|
|
543
|
+
test('requires an identifier', async () => {
|
|
544
|
+
await run({ 'client-id': 'new-id' }, { yes: true });
|
|
545
|
+
expect(logs.join('\n')).toContain('Must specify --id or --name');
|
|
546
|
+
expect(updatedClients).toHaveLength(0);
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test('requires at least one update field', async () => {
|
|
550
|
+
await run({ name: 'github' }, { yes: true });
|
|
551
|
+
expect(logs.join('\n')).toContain(
|
|
552
|
+
'Must specify at least one of --client-id, --client-secret, or --custom-redirect-uri.',
|
|
553
|
+
);
|
|
554
|
+
expect(updatedClients).toHaveLength(0);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
test('rejects both id and name', async () => {
|
|
558
|
+
await run(
|
|
559
|
+
{
|
|
560
|
+
id: 'github',
|
|
561
|
+
name: 'github',
|
|
562
|
+
'client-secret': 'new-gh-secret',
|
|
563
|
+
},
|
|
564
|
+
{ yes: true },
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
expect(logs.join('\n')).toContain('Cannot specify both --id and --name');
|
|
568
|
+
expect(updatedClients).toHaveLength(0);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
test('rejects unknown client name', async () => {
|
|
572
|
+
await run(
|
|
573
|
+
{
|
|
574
|
+
name: 'unknown',
|
|
575
|
+
'client-secret': 'new-gh-secret',
|
|
576
|
+
},
|
|
577
|
+
{ yes: true },
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
expect(logs.join('\n')).toContain('OAuth client not found');
|
|
581
|
+
expect(updatedClients).toHaveLength(0);
|
|
582
|
+
});
|
|
583
|
+
});
|
package/__tests__/oauthMock.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect } from 'effect';
|
|
1
|
+
import { Effect, Schema } from 'effect';
|
|
2
2
|
import { optOrPrompt, validateRequired } from '../src/lib/ui.ts';
|
|
3
3
|
import { UI } from '../src/ui/index.ts';
|
|
4
4
|
import { BadArgsError } from '../src/errors.ts';
|
|
@@ -18,6 +18,13 @@ export const makeOAuthMock = (mocks: {
|
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
const GoogleAppTypeSchema = Schema.Literal(
|
|
22
|
+
'web',
|
|
23
|
+
'ios',
|
|
24
|
+
'android',
|
|
25
|
+
'button-for-web',
|
|
26
|
+
);
|
|
27
|
+
|
|
21
28
|
const getOrCreateProvider = Effect.fn(function* (type: string) {
|
|
22
29
|
const auth: any = yield* mocks.getAppsAuth();
|
|
23
30
|
const provider = auth.oauth_service_providers?.find(
|
|
@@ -62,6 +69,7 @@ export const makeOAuthMock = (mocks: {
|
|
|
62
69
|
|
|
63
70
|
return {
|
|
64
71
|
...mocks,
|
|
72
|
+
GoogleAppTypeSchema,
|
|
65
73
|
findName,
|
|
66
74
|
getOrCreateProvider,
|
|
67
75
|
getClientNameAndProvider,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { expect, test } from 'vitest';
|
|
2
|
+
import stripAnsi from 'strip-ansi';
|
|
3
|
+
import { redirectUriPrompt } from '../src/commands/auth/client/shared.ts';
|
|
4
|
+
|
|
5
|
+
test('redirectUriPrompt shows skipped when submitted empty', () => {
|
|
6
|
+
const prompt = redirectUriPrompt({
|
|
7
|
+
heading: 'Custom redirect URI (optional):',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const output = stripAnsi(prompt.modifyOutput!('\n', 'submitted'));
|
|
11
|
+
|
|
12
|
+
expect(output).toContain('Custom redirect URI (optional):\n(skipped)');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('redirectUriPrompt shows submitted custom redirect URI', () => {
|
|
16
|
+
const prompt = redirectUriPrompt({
|
|
17
|
+
heading: 'Custom redirect URI (optional):',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const output = stripAnsi(
|
|
21
|
+
prompt.modifyOutput!('\nhttps://example.com/oauth/callback', 'submitted'),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(output).toContain(
|
|
25
|
+
'Custom redirect URI (optional):\nhttps://example.com/oauth/callback',
|
|
26
|
+
);
|
|
27
|
+
});
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Effect, Schema } from 'effect';
|
|
2
|
-
import { FileSystem } from '@effect/platform';
|
|
3
2
|
import { GlobalOpts } from '../../../context/globalOpts.ts';
|
|
4
3
|
export declare const ClientTypeSchema: Schema.Literal<["google", "github", "apple", "linkedin", "clerk", "firebase"]>;
|
|
5
4
|
export declare const authClientAddCmd: (opts: {
|
|
6
5
|
type?: string | undefined;
|
|
7
6
|
name?: string | undefined;
|
|
8
7
|
app?: string | undefined;
|
|
9
|
-
} & Record<string, unknown>) => Effect.Effect<void | undefined, import("../../../lib/ui.ts").UIError | import("../../../lib/http.ts").InstantHttpError | import("effect/Cause").TimeoutException | import("@effect/platform/HttpClientError").RequestError | import("effect/ParseResult").ParseError | import("@effect/platform/HttpClientError").ResponseError, GlobalOpts | FileSystem.FileSystem | import("../../../lib/http.ts").InstantHttpAuthed | import("../../../context/currentApp.ts").CurrentApp>;
|
|
8
|
+
} & Record<string, unknown>) => Effect.Effect<void | undefined, import("../../../lib/ui.ts").UIError | import("../../../lib/http.ts").InstantHttpError | import("effect/Cause").TimeoutException | import("@effect/platform/HttpClientError").RequestError | import("effect/ParseResult").ParseError | import("@effect/platform/HttpClientError").ResponseError, GlobalOpts | import("@effect/platform/FileSystem").FileSystem | import("../../../lib/http.ts").InstantHttpAuthed | import("../../../context/currentApp.ts").CurrentApp>;
|
|
10
9
|
//# sourceMappingURL=add.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../../../src/commands/auth/client/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAiB,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../../../src/commands/auth/client/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAiB,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGvD,OAAO,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAmD5D,eAAO,MAAM,gBAAgB,gFAO5B,CAAC;AA8pBF,eAAO,MAAM,gBAAgB;;;;wgBAwD5B,CAAC"}
|