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

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.5",
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.7",
29
+ "@slates/oauth-microsoft": "1.0.0-rc.2",
30
+ "@slates/profiles": "1.0.0-rc.5",
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,36 @@
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('leaves unrelated integration redirects unchanged', () => {
12
+ expect(
13
+ normalizeCallbackRedirectUriForIntegration('attio', 'http://127.0.0.1:45873/callback')
14
+ ).toBe('http://127.0.0.1:45873/callback');
15
+ });
16
+
17
+ it('normalizes HubSpot developer platform OAuth redirects to localhost', () => {
18
+ expect(
19
+ normalizeCallbackRedirectUriForIntegration(
20
+ 'hubspot',
21
+ 'http://127.0.0.1:45873/callback',
22
+ 'developer_platform_oauth'
23
+ )
24
+ ).toBe('http://localhost:45873/callback');
25
+ });
26
+
27
+ it('leaves HubSpot legacy OAuth redirects unchanged', () => {
28
+ expect(
29
+ normalizeCallbackRedirectUriForIntegration(
30
+ 'hubspot',
31
+ 'http://127.0.0.1:45873/callback',
32
+ 'oauth'
33
+ )
34
+ ).toBe('http://127.0.0.1:45873/callback');
35
+ });
36
+ });
@@ -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,14 @@ 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 HUBSPOT_INTEGRATION_KEY = 'hubspot';
26
+ let HUBSPOT_DEVELOPER_PLATFORM_OAUTH_METHOD_ID = 'developer_platform_oauth';
27
+ let LOOPBACK_REDIRECT_NORMALIZED_INTEGRATIONS = new Set([
28
+ NOTION_INTEGRATION_KEY,
29
+ SALESFORCE_INTEGRATION_KEY
30
+ ]);
20
31
 
21
32
  type AuthSetupOptions = WithProfile &
22
33
  JsonInput & {
@@ -27,6 +38,22 @@ type AuthSetupOptions = WithProfile &
27
38
  scopes?: string;
28
39
  };
29
40
 
41
+ export let normalizeCallbackRedirectUriForIntegration = (
42
+ integration: string,
43
+ redirectUri: string,
44
+ authMethodId?: string
45
+ ) => {
46
+ if (
47
+ LOOPBACK_REDIRECT_NORMALIZED_INTEGRATIONS.has(integration) ||
48
+ (integration === HUBSPOT_INTEGRATION_KEY &&
49
+ authMethodId === HUBSPOT_DEVELOPER_PLATFORM_OAUTH_METHOD_ID)
50
+ ) {
51
+ return normalizeMicrosoftRedirectUri(redirectUri);
52
+ }
53
+
54
+ return normalizeMicrosoftRedirectUriForIntegration(integration, redirectUri);
55
+ };
56
+
30
57
  export let listAuth = async (opts: WithProfile) => {
31
58
  let { store, profile } = await createClientContext(opts);
32
59
  return store.listAuth(profile.id);
@@ -232,7 +259,11 @@ let chooseOAuthCredentialsForSetup = async (opts: {
232
259
  };
233
260
 
234
261
  let runAuthSetup = async (opts: AuthSetupOptions): Promise<SlatesStoredAuth> => {
235
- let { store, profile, client } = await createClientContext(opts);
262
+ let { store, profile, client } = await createClientContext({
263
+ ...opts,
264
+ autoRefresh: false
265
+ });
266
+ client.clearAuth();
236
267
  let authMethod = await chooseAuthMethod({
237
268
  client,
238
269
  authMethodId: opts.authMethodId,
@@ -264,9 +295,10 @@ let runAuthSetup = async (opts: AuthSetupOptions): Promise<SlatesStoredAuth> =>
264
295
 
265
296
  if (authMethod.type === 'auth.oauth') {
266
297
  let callback = await createOAuthCallbackListener();
267
- let redirectUri = normalizeMicrosoftRedirectUriForIntegration(
298
+ let redirectUri = normalizeCallbackRedirectUriForIntegration(
268
299
  opts.integration,
269
- callback.redirectUri
300
+ callback.redirectUri,
301
+ authMethod.id
270
302
  );
271
303
  console.log(`OAuth redirect URL: ${redirectUri}`);
272
304
 
@@ -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
  };
@@ -45,10 +45,34 @@ export let createOAuthCallbackListener = async () => {
45
45
  let url = new URL(req.url ?? '/', 'http://127.0.0.1');
46
46
  let code = url.searchParams.get('code');
47
47
  let state = url.searchParams.get('state');
48
+ let oauthError = url.searchParams.get('error');
49
+ let oauthErrorDescription = url.searchParams.get('error_description');
50
+ let oauthErrorUri = url.searchParams.get('error_uri');
51
+
52
+ if (oauthError) {
53
+ let description = oauthErrorDescription ?? 'No error description was provided.';
54
+ let errorMessage = `OAuth callback returned "${oauthError}": ${description}${
55
+ oauthErrorUri ? ` (${oauthErrorUri})` : ''
56
+ }`;
57
+
58
+ res.statusCode = 400;
59
+ res.end(errorMessage);
60
+ server.close();
61
+ settled = true;
62
+ waiter.reject(new Error(errorMessage));
63
+ return;
64
+ }
48
65
 
49
66
  if (!code || !state) {
50
67
  res.statusCode = 400;
51
68
  res.end('Missing code or state.');
69
+ server.close();
70
+ settled = true;
71
+ waiter.reject(
72
+ new Error(
73
+ `OAuth callback did not include the required query parameters. Received path: ${url.pathname}${url.search}`
74
+ )
75
+ );
52
76
  return;
53
77
  }
54
78