@slates/cli 1.0.0-rc.4 → 1.0.0-rc.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slates/cli",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0-rc.6",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -25,9 +25,9 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@inquirer/prompts": "^7.4.0",
28
- "@slates/client": "1.0.0-rc.6",
29
- "@slates/oauth-microsoft": "1.0.0-rc.1",
30
- "@slates/profiles": "1.0.0-rc.4",
28
+ "@slates/client": "1.0.0-rc.8",
29
+ "@slates/oauth-microsoft": "1.0.0-rc.3",
30
+ "@slates/profiles": "1.0.0-rc.6",
31
31
  "sade": "^1.8.1"
32
32
  },
33
33
  "devDependencies": {
package/src/cli.ts CHANGED
@@ -59,203 +59,233 @@ if (isGlobalTestCommand) {
59
59
 
60
60
  cli.parse([process.argv[0] ?? 'bun', process.argv[1] ?? 'slates', ...argv]);
61
61
  } else {
62
+ cli
63
+ .command('profiles add')
64
+ .option('--name', 'Profile name')
65
+ .option('--entry', 'Local slate entry file')
66
+ .option('--export-name', 'Named export for the local slate provider')
67
+ .option('--default', 'Use this profile as the default')
68
+ .action(opts =>
69
+ printResult(() =>
70
+ addProfile({
71
+ integration: integration!,
72
+ name: opts.name,
73
+ entry: opts.entry,
74
+ exportName: opts.exportName,
75
+ useAsDefault: Boolean(opts.default)
76
+ })
77
+ )
78
+ );
62
79
 
63
- cli
64
- .command('profiles add')
65
- .option('--name', 'Profile name')
66
- .option('--entry', 'Local slate entry file')
67
- .option('--export-name', 'Named export for the local slate provider')
68
- .option('--default', 'Use this profile as the default')
69
- .action(opts =>
70
- printResult(() =>
71
- addProfile({
72
- integration: integration!,
73
- name: opts.name,
74
- entry: opts.entry,
75
- exportName: opts.exportName,
76
- useAsDefault: Boolean(opts.default)
77
- })
78
- )
79
- );
80
+ cli
81
+ .command('profiles list')
82
+ .action(() => printResult(() => listProfiles({ integration: integration! })));
80
83
 
81
- cli
82
- .command('profiles list')
83
- .action(() => printResult(() => listProfiles({ integration: integration! })));
84
+ cli
85
+ .command('profiles get [profile]')
86
+ .action((profile: string | undefined) =>
87
+ printResult(() => getProfile({ integration: integration!, profile }))
88
+ );
84
89
 
85
- cli
86
- .command('profiles get [profile]')
87
- .action((profile: string | undefined) =>
88
- printResult(() => getProfile({ integration: integration!, profile }))
89
- );
90
+ cli
91
+ .command('profiles use [profile]')
92
+ .action((profile: string | undefined) =>
93
+ printResult(() => useProfile({ integration: integration!, profile }))
94
+ );
90
95
 
91
- cli
92
- .command('profiles use [profile]')
93
- .action((profile: string | undefined) =>
94
- printResult(() => useProfile({ integration: integration!, profile }))
95
- );
96
+ cli
97
+ .command('profiles remove [profile]')
98
+ .action((profile: string | undefined) =>
99
+ printResult(() => removeProfile({ integration: integration!, profile }))
100
+ );
96
101
 
97
- cli
98
- .command('profiles remove [profile]')
99
- .action((profile: string | undefined) =>
100
- printResult(() => removeProfile({ integration: integration!, profile }))
101
- );
102
+ cli
103
+ .command('setup')
104
+ .option('--name', 'Profile name')
105
+ .option('--export-name', 'Named export for the local slate provider')
106
+ .action(opts =>
107
+ printResult(() =>
108
+ setupIntegration({
109
+ integration: integration!,
110
+ name: opts.name,
111
+ exportName: opts.exportName
112
+ })
113
+ )
114
+ );
102
115
 
103
- cli
104
- .command('setup')
105
- .option('--name', 'Profile name')
106
- .option('--export-name', 'Named export for the local slate provider')
107
- .action(opts =>
108
- printResult(() =>
109
- setupIntegration({
110
- integration: integration!,
111
- name: opts.name,
112
- exportName: opts.exportName
113
- })
114
- )
115
- );
116
+ cli
117
+ .command('tools list')
118
+ .option('--profile', 'Profile ID or name')
119
+ .action(opts =>
120
+ printResult(() =>
121
+ listTools({
122
+ integration: integration!,
123
+ profile: opts.profile
124
+ })
125
+ )
126
+ );
116
127
 
117
- cli
118
- .command('tools list')
119
- .option('--profile', 'Profile ID or name')
120
- .action(opts => printResult(() => listTools({ integration: integration!, profile: opts.profile })));
128
+ cli
129
+ .command('tools get [toolId]')
130
+ .option('--profile', 'Profile ID or name')
131
+ .action((toolId: string | undefined, opts) =>
132
+ printResult(() =>
133
+ getTool({
134
+ integration: integration!,
135
+ profile: opts.profile,
136
+ toolId
137
+ })
138
+ )
139
+ );
121
140
 
122
- cli
123
- .command('tools get [toolId]')
124
- .option('--profile', 'Profile ID or name')
125
- .action((toolId: string | undefined, opts) =>
126
- printResult(() => getTool({ integration: integration!, profile: opts.profile, toolId }))
127
- );
128
-
129
- cli
130
- .command('tools schema [toolId]')
131
- .option('--profile', 'Profile ID or name')
132
- .action((toolId: string | undefined, opts) =>
133
- printResult(async () => {
134
- let tool = await getTool({ integration: integration!, profile: opts.profile, toolId });
135
- return tool.inputSchema;
136
- })
137
- );
138
-
139
- cli
140
- .command('tools call [toolId]')
141
- .option('--profile', 'Profile ID or name')
142
- .option('--input', 'JSON input object')
143
- .option('--auth-method-id', 'Preferred auth method ID')
144
- .action((toolId: string | undefined, opts) =>
145
- printResult(() =>
146
- callTool({
147
- integration: integration!,
148
- profile: opts.profile,
149
- toolId,
150
- input: opts.input,
151
- authMethodId: opts.authMethodId
141
+ cli
142
+ .command('tools schema [toolId]')
143
+ .option('--profile', 'Profile ID or name')
144
+ .action((toolId: string | undefined, opts) =>
145
+ printResult(async () => {
146
+ let tool = await getTool({
147
+ integration: integration!,
148
+ profile: opts.profile,
149
+ toolId
150
+ });
151
+ return tool.inputSchema;
152
152
  })
153
- )
154
- );
153
+ );
155
154
 
156
- cli
157
- .command('auth list')
158
- .option('--profile', 'Profile ID or name')
159
- .action(opts => printResult(() => listAuth({ integration: integration!, profile: opts.profile })));
155
+ cli
156
+ .command('tools call [toolId]')
157
+ .option('--profile', 'Profile ID or name')
158
+ .option('--input', 'JSON input object')
159
+ .option('--auth-method-id', 'Preferred auth method ID')
160
+ .action((toolId: string | undefined, opts) =>
161
+ printResult(() =>
162
+ callTool({
163
+ integration: integration!,
164
+ profile: opts.profile,
165
+ toolId,
166
+ input: opts.input,
167
+ authMethodId: opts.authMethodId
168
+ })
169
+ )
170
+ );
160
171
 
161
- cli
162
- .command('auth get [authMethodId]')
163
- .option('--profile', 'Profile ID or name')
164
- .action((authMethodId: string | undefined, opts) =>
165
- printResult(() => getAuth({ integration: integration!, profile: opts.profile, authMethodId }))
166
- );
172
+ cli
173
+ .command('auth list')
174
+ .option('--profile', 'Profile ID or name')
175
+ .action(opts =>
176
+ printResult(() => listAuth({ integration: integration!, profile: opts.profile }))
177
+ );
167
178
 
168
- cli
169
- .command('auth setup [authMethodId]')
170
- .option('--profile', 'Profile ID or name')
171
- .option('--input', 'JSON auth input object')
172
- .option('--oauth-credential', 'OAuth credential ID or name')
173
- .option('--client-id', 'OAuth client ID')
174
- .option('--client-secret', 'OAuth client secret')
175
- .option('--scopes', 'Comma-separated OAuth scopes')
176
- .action((authMethodId: string | undefined, opts) =>
177
- printResult(() =>
178
- setupAuth({
179
- integration: integration!,
180
- profile: opts.profile,
181
- authMethodId,
182
- input: opts.input,
183
- oauthCredential: opts.oauthCredential,
184
- clientId: opts.clientId,
185
- clientSecret: opts.clientSecret,
186
- scopes: opts.scopes
187
- })
188
- )
189
- );
179
+ cli
180
+ .command('auth get [authMethodId]')
181
+ .option('--profile', 'Profile ID or name')
182
+ .action((authMethodId: string | undefined, opts) =>
183
+ printResult(() =>
184
+ getAuth({ integration: integration!, profile: opts.profile, authMethodId })
185
+ )
186
+ );
190
187
 
191
- cli
192
- .command('auth credentials list [authMethodId]')
193
- .action((authMethodId: string | undefined) =>
194
- printResult(() => listOAuthCredentials({ integration: integration!, authMethodId }))
195
- );
188
+ cli
189
+ .command('auth setup [authMethodId]')
190
+ .option('--profile', 'Profile ID or name')
191
+ .option('--input', 'JSON auth input object')
192
+ .option('--oauth-credential', 'OAuth credential ID or name')
193
+ .option('--client-id', 'OAuth client ID')
194
+ .option('--client-secret', 'OAuth client secret')
195
+ .option('--scopes', 'Comma-separated OAuth scopes')
196
+ .action((authMethodId: string | undefined, opts) =>
197
+ printResult(() =>
198
+ setupAuth({
199
+ integration: integration!,
200
+ profile: opts.profile,
201
+ authMethodId,
202
+ input: opts.input,
203
+ oauthCredential: opts.oauthCredential,
204
+ clientId: opts.clientId,
205
+ clientSecret: opts.clientSecret,
206
+ scopes: opts.scopes
207
+ })
208
+ )
209
+ );
196
210
 
197
- cli
198
- .command('auth credentials add [authMethodId]')
199
- .option('--name', 'Credential name')
200
- .option('--client-id', 'OAuth client ID')
201
- .option('--client-secret', 'OAuth client secret')
202
- .action((authMethodId: string | undefined, opts) =>
203
- printResult(() =>
204
- addOAuthCredentials({
205
- integration: integration!,
206
- authMethodId,
207
- name: opts.name,
208
- clientId: opts.clientId,
209
- clientSecret: opts.clientSecret
210
- })
211
- )
212
- );
211
+ cli
212
+ .command('auth credentials list [authMethodId]')
213
+ .action((authMethodId: string | undefined) =>
214
+ printResult(() => listOAuthCredentials({ integration: integration!, authMethodId }))
215
+ );
213
216
 
214
- cli
215
- .command('auth refresh [authMethodId]')
216
- .option('--profile', 'Profile ID or name')
217
- .action((authMethodId: string | undefined, opts) =>
218
- printResult(() => refreshAuth({ integration: integration!, profile: opts.profile, authMethodId }))
219
- );
217
+ cli
218
+ .command('auth credentials add [authMethodId]')
219
+ .option('--name', 'Credential name')
220
+ .option('--client-id', 'OAuth client ID')
221
+ .option('--client-secret', 'OAuth client secret')
222
+ .action((authMethodId: string | undefined, opts) =>
223
+ printResult(() =>
224
+ addOAuthCredentials({
225
+ integration: integration!,
226
+ authMethodId,
227
+ name: opts.name,
228
+ clientId: opts.clientId,
229
+ clientSecret: opts.clientSecret
230
+ })
231
+ )
232
+ );
220
233
 
221
- cli
222
- .command('config get')
223
- .option('--profile', 'Profile ID or name')
224
- .action(opts => printResult(() => getConfig({ integration: integration!, profile: opts.profile })));
234
+ cli
235
+ .command('auth refresh [authMethodId]')
236
+ .option('--profile', 'Profile ID or name')
237
+ .action((authMethodId: string | undefined, opts) =>
238
+ printResult(() =>
239
+ refreshAuth({ integration: integration!, profile: opts.profile, authMethodId })
240
+ )
241
+ );
225
242
 
226
- cli
227
- .command('config set')
228
- .option('--profile', 'Profile ID or name')
229
- .option('--input', 'JSON config object')
230
- .action(opts =>
231
- printResult(() => setConfig({ integration: integration!, profile: opts.profile, input: opts.input }))
232
- );
243
+ cli
244
+ .command('config get')
245
+ .option('--profile', 'Profile ID or name')
246
+ .action(opts =>
247
+ printResult(() => getConfig({ integration: integration!, profile: opts.profile }))
248
+ );
233
249
 
234
- cli
235
- .command('config schema')
236
- .option('--profile', 'Profile ID or name')
237
- .action(opts => printResult(() => getConfigSchema({ integration: integration!, profile: opts.profile })));
250
+ cli
251
+ .command('config set')
252
+ .option('--profile', 'Profile ID or name')
253
+ .option('--input', 'JSON config object')
254
+ .action(opts =>
255
+ printResult(() =>
256
+ setConfig({ integration: integration!, profile: opts.profile, input: opts.input })
257
+ )
258
+ );
238
259
 
239
- cli
240
- .command('test')
241
- .option('--profile', 'Profile ID or name')
242
- .action(opts =>
243
- printResult(async () => {
244
- let separatorIndex = process.argv.indexOf('--');
245
- await runVitestWithProfile({
246
- integration: integration!,
247
- profile: opts.profile,
248
- vitestArgs: separatorIndex === -1 ? [] : process.argv.slice(separatorIndex + 1)
249
- });
260
+ cli
261
+ .command('config schema')
262
+ .option('--profile', 'Profile ID or name')
263
+ .action(opts =>
264
+ printResult(() => getConfigSchema({ integration: integration!, profile: opts.profile }))
265
+ );
250
266
 
251
- return { success: true };
252
- })
253
- );
267
+ cli
268
+ .command('test')
269
+ .option('--profile', 'Profile ID or name')
270
+ .action(opts =>
271
+ printResult(async () => {
272
+ let separatorIndex = process.argv.indexOf('--');
273
+ await runVitestWithProfile({
274
+ integration: integration!,
275
+ profile: opts.profile,
276
+ vitestArgs: separatorIndex === -1 ? [] : process.argv.slice(separatorIndex + 1)
277
+ });
278
+
279
+ return { success: true };
280
+ })
281
+ );
254
282
 
255
- cli
256
- .command('repl')
257
- .option('--profile', 'Profile ID or name')
258
- .action(opts => printResult(() => startRepl({ integration: integration!, profile: opts.profile })));
283
+ cli
284
+ .command('repl')
285
+ .option('--profile', 'Profile ID or name')
286
+ .action(opts =>
287
+ printResult(() => startRepl({ integration: integration!, profile: opts.profile }))
288
+ );
259
289
 
260
290
  cli.parse([process.argv[0] ?? 'bun', process.argv[1] ?? 'slates', ...argv.slice(1)]);
261
291
  }
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { normalizeCallbackRedirectUriForIntegration } from './auth';
3
+
4
+ describe('normalizeCallbackRedirectUriForIntegration', () => {
5
+ it('normalizes Notion loopback redirects to localhost', () => {
6
+ expect(
7
+ normalizeCallbackRedirectUriForIntegration('notion', 'http://127.0.0.1:45873/callback')
8
+ ).toBe('http://localhost:45873/callback');
9
+ });
10
+
11
+ it('normalizes Intercom loopback redirects to localhost', () => {
12
+ expect(
13
+ normalizeCallbackRedirectUriForIntegration('intercom', 'http://127.0.0.1:45873/callback')
14
+ ).toBe('http://localhost:45873/callback');
15
+ });
16
+
17
+ it('normalizes Typeform loopback redirects to localhost', () => {
18
+ expect(
19
+ normalizeCallbackRedirectUriForIntegration('typeform', 'http://127.0.0.1:45873/callback')
20
+ ).toBe('http://localhost:45873/callback');
21
+ });
22
+
23
+ it('normalizes Xero loopback redirects to localhost', () => {
24
+ expect(
25
+ normalizeCallbackRedirectUriForIntegration('xero', 'http://127.0.0.1:45873/callback')
26
+ ).toBe('http://localhost:45873/callback');
27
+ });
28
+
29
+ it('normalizes Zendesk loopback redirects to localhost', () => {
30
+ expect(
31
+ normalizeCallbackRedirectUriForIntegration('zendesk', 'http://127.0.0.1:45873/callback')
32
+ ).toBe('http://localhost:45873/callback');
33
+ });
34
+
35
+ it('leaves unrelated integration redirects unchanged', () => {
36
+ expect(
37
+ normalizeCallbackRedirectUriForIntegration('attio', 'http://127.0.0.1:45873/callback')
38
+ ).toBe('http://127.0.0.1:45873/callback');
39
+ });
40
+
41
+ it('normalizes HubSpot developer platform OAuth redirects to localhost', () => {
42
+ expect(
43
+ normalizeCallbackRedirectUriForIntegration(
44
+ 'hubspot',
45
+ 'http://127.0.0.1:45873/callback',
46
+ 'developer_platform_oauth'
47
+ )
48
+ ).toBe('http://localhost:45873/callback');
49
+ });
50
+
51
+ it('leaves HubSpot legacy OAuth redirects unchanged', () => {
52
+ expect(
53
+ normalizeCallbackRedirectUriForIntegration(
54
+ 'hubspot',
55
+ 'http://127.0.0.1:45873/callback',
56
+ 'oauth'
57
+ )
58
+ ).toBe('http://127.0.0.1:45873/callback');
59
+ });
60
+ });
@@ -1,5 +1,8 @@
1
1
  import { confirm, select } from '@inquirer/prompts';
2
- import { normalizeMicrosoftRedirectUriForIntegration } from '@slates/oauth-microsoft';
2
+ import {
3
+ normalizeMicrosoftRedirectUri,
4
+ normalizeMicrosoftRedirectUriForIntegration
5
+ } from '@slates/oauth-microsoft';
3
6
  import { SlatesOAuthCredentialRecord, SlatesStoredAuth } from '@slates/profiles';
4
7
  import {
5
8
  chooseAuthMethod,
@@ -17,6 +20,22 @@ import {
17
20
  import { JsonInput, WithProfile } from '../lib/types';
18
21
 
19
22
  type JsonObject = Record<string, any>;
23
+ let NOTION_INTEGRATION_KEY = 'notion';
24
+ let SALESFORCE_INTEGRATION_KEY = 'salesforce';
25
+ let INTERCOM_INTEGRATION_KEY = 'intercom';
26
+ let TYPEFORM_INTEGRATION_KEY = 'typeform';
27
+ let XERO_INTEGRATION_KEY = 'xero';
28
+ let ZENDESK_INTEGRATION_KEY = 'zendesk';
29
+ let HUBSPOT_INTEGRATION_KEY = 'hubspot';
30
+ let HUBSPOT_DEVELOPER_PLATFORM_OAUTH_METHOD_ID = 'developer_platform_oauth';
31
+ let LOOPBACK_REDIRECT_NORMALIZED_INTEGRATIONS = new Set([
32
+ INTERCOM_INTEGRATION_KEY,
33
+ NOTION_INTEGRATION_KEY,
34
+ SALESFORCE_INTEGRATION_KEY,
35
+ TYPEFORM_INTEGRATION_KEY,
36
+ XERO_INTEGRATION_KEY,
37
+ ZENDESK_INTEGRATION_KEY
38
+ ]);
20
39
 
21
40
  type AuthSetupOptions = WithProfile &
22
41
  JsonInput & {
@@ -27,6 +46,22 @@ type AuthSetupOptions = WithProfile &
27
46
  scopes?: string;
28
47
  };
29
48
 
49
+ export let normalizeCallbackRedirectUriForIntegration = (
50
+ integration: string,
51
+ redirectUri: string,
52
+ authMethodId?: string
53
+ ) => {
54
+ if (
55
+ LOOPBACK_REDIRECT_NORMALIZED_INTEGRATIONS.has(integration) ||
56
+ (integration === HUBSPOT_INTEGRATION_KEY &&
57
+ authMethodId === HUBSPOT_DEVELOPER_PLATFORM_OAUTH_METHOD_ID)
58
+ ) {
59
+ return normalizeMicrosoftRedirectUri(redirectUri);
60
+ }
61
+
62
+ return normalizeMicrosoftRedirectUriForIntegration(integration, redirectUri);
63
+ };
64
+
30
65
  export let listAuth = async (opts: WithProfile) => {
31
66
  let { store, profile } = await createClientContext(opts);
32
67
  return store.listAuth(profile.id);
@@ -232,7 +267,11 @@ let chooseOAuthCredentialsForSetup = async (opts: {
232
267
  };
233
268
 
234
269
  let runAuthSetup = async (opts: AuthSetupOptions): Promise<SlatesStoredAuth> => {
235
- let { store, profile, client } = await createClientContext(opts);
270
+ let { store, profile, client } = await createClientContext({
271
+ ...opts,
272
+ autoRefresh: false
273
+ });
274
+ client.clearAuth();
236
275
  let authMethod = await chooseAuthMethod({
237
276
  client,
238
277
  authMethodId: opts.authMethodId,
@@ -264,9 +303,10 @@ let runAuthSetup = async (opts: AuthSetupOptions): Promise<SlatesStoredAuth> =>
264
303
 
265
304
  if (authMethod.type === 'auth.oauth') {
266
305
  let callback = await createOAuthCallbackListener();
267
- let redirectUri = normalizeMicrosoftRedirectUriForIntegration(
306
+ let redirectUri = normalizeCallbackRedirectUriForIntegration(
268
307
  opts.integration,
269
- callback.redirectUri
308
+ callback.redirectUri,
309
+ authMethod.id
270
310
  );
271
311
  console.log(`OAuth redirect URL: ${redirectUri}`);
272
312
 
@@ -313,6 +353,7 @@ let runAuthSetup = async (opts: AuthSetupOptions): Promise<SlatesStoredAuth> =>
313
353
  clientId,
314
354
  clientSecret,
315
355
  scopes,
356
+ callbackParams: callbackResult.callbackParams,
316
357
  callbackState: callbackState ?? undefined
317
358
  });
318
359
 
@@ -71,9 +71,16 @@ export let chooseProfile = async (d: {
71
71
  };
72
72
  };
73
73
 
74
- export let createClientContext = async (opts: { integration: string; profile?: string }) => {
74
+ export let createClientContext = async (opts: {
75
+ integration: string;
76
+ profile?: string;
77
+ autoRefresh?: boolean;
78
+ }) => {
75
79
  let { integration, store, profile } = await chooseProfile(opts);
76
- let client = await createSlatesClientFromProfile(profile, { store });
80
+ let client = await createSlatesClientFromProfile(profile, {
81
+ store,
82
+ autoRefresh: opts.autoRefresh
83
+ });
77
84
  return { integration, store, profile, client };
78
85
  };
79
86
 
package/src/lib/oauth.ts CHANGED
@@ -21,7 +21,7 @@ export let chooseScopes = async (
21
21
  checked:
22
22
  initialScopes.length > 0
23
23
  ? initialScopes.includes(scope.id)
24
- : scope.defaultChecked ?? true
24
+ : (scope.defaultChecked ?? true)
25
25
  }))
26
26
  })) as string[];
27
27
  };
@@ -34,7 +34,11 @@ export let createOAuthCallbackListener = async () => {
34
34
  return new Promise<{
35
35
  redirectUri: string;
36
36
  state: string;
37
- wait: () => Promise<{ code: string; state: string }>;
37
+ wait: () => Promise<{
38
+ code: string;
39
+ state: string;
40
+ callbackParams: Record<string, string>;
41
+ }>;
38
42
  }>((resolve, reject) => {
39
43
  let expectedState = randomUUID();
40
44
  let settled = false;
@@ -45,17 +49,45 @@ export let createOAuthCallbackListener = async () => {
45
49
  let url = new URL(req.url ?? '/', 'http://127.0.0.1');
46
50
  let code = url.searchParams.get('code');
47
51
  let state = url.searchParams.get('state');
52
+ let oauthError = url.searchParams.get('error');
53
+ let oauthErrorDescription = url.searchParams.get('error_description');
54
+ let oauthErrorUri = url.searchParams.get('error_uri');
55
+
56
+ if (oauthError) {
57
+ let description = oauthErrorDescription ?? 'No error description was provided.';
58
+ let errorMessage = `OAuth callback returned "${oauthError}": ${description}${
59
+ oauthErrorUri ? ` (${oauthErrorUri})` : ''
60
+ }`;
61
+
62
+ res.statusCode = 400;
63
+ res.end(errorMessage);
64
+ server.close();
65
+ settled = true;
66
+ waiter.reject(new Error(errorMessage));
67
+ return;
68
+ }
48
69
 
49
70
  if (!code || !state) {
50
71
  res.statusCode = 400;
51
72
  res.end('Missing code or state.');
73
+ server.close();
74
+ settled = true;
75
+ waiter.reject(
76
+ new Error(
77
+ `OAuth callback did not include the required query parameters. Received path: ${url.pathname}${url.search}`
78
+ )
79
+ );
52
80
  return;
53
81
  }
54
82
 
55
83
  res.end('Authentication complete. You can close this window.');
56
84
  server.close();
57
85
  settled = true;
58
- waiter.resolve({ code, state });
86
+ waiter.resolve({
87
+ code,
88
+ state,
89
+ callbackParams: Object.fromEntries(url.searchParams.entries())
90
+ });
59
91
  } catch (error) {
60
92
  server.close();
61
93
  settled = true;
@@ -64,9 +96,17 @@ export let createOAuthCallbackListener = async () => {
64
96
  });
65
97
 
66
98
  let waiter = (() => {
67
- let resolvePromise!: (value: { code: string; state: string }) => void;
99
+ let resolvePromise!: (value: {
100
+ code: string;
101
+ state: string;
102
+ callbackParams: Record<string, string>;
103
+ }) => void;
68
104
  let rejectPromise!: (error: unknown) => void;
69
- let promise = new Promise<{ code: string; state: string }>((resolveFn, rejectFn) => {
105
+ let promise = new Promise<{
106
+ code: string;
107
+ state: string;
108
+ callbackParams: Record<string, string>;
109
+ }>((resolveFn, rejectFn) => {
70
110
  resolvePromise = resolveFn;
71
111
  rejectPromise = rejectFn;
72
112
  });