@slates/profiles 1.0.0-rc.10

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/src/client.ts ADDED
@@ -0,0 +1,158 @@
1
+ import { createLocalSlateTransport, createSlatesClient } from '@slates/client';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+ import { resolveSlatesCliRoot, type SlatesCliStore } from './store';
5
+ import type { SlatesProfileRecord, SlatesStoredAuth } from './types';
6
+
7
+ let resolveEntryPath = (entry: string, opts: { cwd?: string; rootDir?: string } = {}) => {
8
+ if (path.isAbsolute(entry)) return entry;
9
+ return path.resolve(opts.rootDir ?? resolveSlatesCliRoot(opts.cwd), entry);
10
+ };
11
+
12
+ let loadSlateFromProfile = async (
13
+ profile: SlatesProfileRecord,
14
+ opts: { cwd?: string; store?: SlatesCliStore } = {}
15
+ ) => {
16
+ if (profile.target.type !== 'local') {
17
+ throw new Error(`Unsupported profile target type: ${(profile.target as any).type}`);
18
+ }
19
+
20
+ let modulePath = resolveEntryPath(profile.target.entry, {
21
+ cwd: opts.cwd,
22
+ rootDir: opts.store?.rootDir
23
+ });
24
+ let loaded = await import(pathToFileURL(modulePath).href);
25
+ let exportName = profile.target.exportName;
26
+
27
+ let slate =
28
+ (exportName ? loaded[exportName] : undefined) ??
29
+ loaded.provider ??
30
+ loaded.slate ??
31
+ loaded.default;
32
+
33
+ if (!slate) {
34
+ throw new Error(
35
+ `Could not find a slate export in ${profile.target.entry}. Tried ${
36
+ exportName ? `\`${exportName}\`, ` : ''
37
+ }\`provider\`, \`slate\`, and \`default\`.`
38
+ );
39
+ }
40
+
41
+ return slate;
42
+ };
43
+
44
+ export let createSlatesClientFromProfile = async (
45
+ profile: SlatesProfileRecord,
46
+ opts: { cwd?: string; store?: SlatesCliStore; autoRefresh?: boolean } = {}
47
+ ) => {
48
+ let firstAuth = Object.values(profile.auth)[0];
49
+ let slate = await loadSlateFromProfile(profile, opts);
50
+ let client = createSlatesClient({
51
+ transport: createLocalSlateTransport({ slate }),
52
+ state: {
53
+ config: profile.config,
54
+ session: profile.session,
55
+ auth: firstAuth
56
+ ? {
57
+ authenticationMethodId: firstAuth.authMethodId,
58
+ output: firstAuth.output
59
+ }
60
+ : null
61
+ }
62
+ });
63
+
64
+ if (firstAuth && opts.autoRefresh !== false) {
65
+ attachAutoRefresh(client, profile, firstAuth, opts.store);
66
+ }
67
+
68
+ return client;
69
+ };
70
+
71
+ let EXPIRY_BUFFER_MS = 60 * 1000;
72
+
73
+ let hasExpiredOrExpiringOutput = (auth: SlatesStoredAuth) => {
74
+ if (typeof auth.output?.expiresAt !== 'string' || !auth.output.expiresAt) return false;
75
+
76
+ let expiresAt = Date.parse(auth.output.expiresAt);
77
+ return Number.isFinite(expiresAt) && expiresAt <= Date.now() + EXPIRY_BUFFER_MS;
78
+ };
79
+
80
+ let shouldRefreshAuth = (auth: SlatesStoredAuth) => {
81
+ if (!hasExpiredOrExpiringOutput(auth)) return false;
82
+
83
+ if (auth.authType === 'auth.oauth') {
84
+ if (!auth.clientId || !auth.clientSecret) return false;
85
+ if (typeof auth.output?.refreshToken !== 'string' || !auth.output.refreshToken)
86
+ return false;
87
+ }
88
+
89
+ return true;
90
+ };
91
+
92
+ let attachAutoRefresh = (
93
+ client: ReturnType<typeof createSlatesClient>,
94
+ profile: SlatesProfileRecord,
95
+ storedAuth: SlatesStoredAuth,
96
+ store?: SlatesCliStore
97
+ ) => {
98
+ let baseRequest = client.request.bind(client);
99
+ let inflightRefresh: Promise<void> | null = null;
100
+
101
+ let refreshIfNeeded = async () => {
102
+ if (inflightRefresh) {
103
+ await inflightRefresh;
104
+ return;
105
+ }
106
+
107
+ if (!shouldRefreshAuth(storedAuth)) {
108
+ return;
109
+ }
110
+
111
+ inflightRefresh = (async () => {
112
+ let authMethod = await baseRequest('slates/auth.method.get', {
113
+ authenticationMethodId: storedAuth.authMethodId
114
+ });
115
+
116
+ if (!authMethod.authenticationMethod.capabilities.handleTokenRefresh?.enabled) {
117
+ return;
118
+ }
119
+
120
+ let refreshed = await baseRequest('slates/auth.token_refresh.handle', {
121
+ authenticationMethodId: storedAuth.authMethodId,
122
+ output: storedAuth.output,
123
+ input: storedAuth.input,
124
+ clientId: storedAuth.clientId ?? '',
125
+ clientSecret: storedAuth.clientSecret ?? '',
126
+ scopes: storedAuth.scopes
127
+ });
128
+
129
+ storedAuth.input = refreshed.input ?? storedAuth.input;
130
+ storedAuth.output = refreshed.output;
131
+ storedAuth.updatedAt = new Date().toISOString();
132
+
133
+ client.setAuth({
134
+ authenticationMethodId: storedAuth.authMethodId,
135
+ output: storedAuth.output
136
+ });
137
+
138
+ if (store) {
139
+ store.upsertAuth(profile.id, storedAuth);
140
+ await store.save();
141
+ }
142
+ })();
143
+
144
+ try {
145
+ await inflightRefresh;
146
+ } finally {
147
+ inflightRefresh = null;
148
+ }
149
+ };
150
+
151
+ client.request = (async (method, params) => {
152
+ if (method !== 'slates/auth.token_refresh.handle') {
153
+ await refreshIfNeeded();
154
+ }
155
+
156
+ return baseRequest(method as never, params as never);
157
+ }) as typeof client.request;
158
+ };
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './client';
2
+ export * from './store';
3
+ export * from './types';
@@ -0,0 +1,533 @@
1
+ import { access, mkdir, mkdtemp, rm, writeFile } from 'fs/promises';
2
+ import { tmpdir } from 'os';
3
+ import path from 'path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { createSlatesClientFromProfile } from './client';
6
+ import { SlatesCliStore } from './store';
7
+
8
+ let tempDirs: string[] = [];
9
+
10
+ let createTempDir = async () => {
11
+ let dir = await mkdtemp(path.join(tmpdir(), 'slates-profiles-'));
12
+ tempDirs.push(dir);
13
+ return dir;
14
+ };
15
+
16
+ afterEach(async () => {
17
+ await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })));
18
+ });
19
+
20
+ describe('@slates/profiles store', () => {
21
+ it('creates an integration-scoped store with profiles and auth', async () => {
22
+ let cwd = await createTempDir();
23
+ let store = await SlatesCliStore.open({
24
+ cwd,
25
+ scope: {
26
+ key: 'integrations/demo',
27
+ name: 'demo'
28
+ }
29
+ });
30
+
31
+ let profile = store.upsertProfile({
32
+ name: 'Demo',
33
+ target: {
34
+ type: 'local',
35
+ entry: './fixtures/demo-slate.mjs',
36
+ exportName: 'provider'
37
+ }
38
+ });
39
+
40
+ store.setCurrentProfile(profile.id);
41
+ store.setProfileConfig(profile.id, { prefix: 'Hello' });
42
+ store.upsertAuth(profile.id, {
43
+ authMethodId: 'token_auth',
44
+ authType: 'auth.token',
45
+ input: { token: 'abc' },
46
+ output: { token: 'abc' },
47
+ scopes: []
48
+ });
49
+ await store.save();
50
+
51
+ let reopened = await SlatesCliStore.open({ storePath: store.storePath });
52
+ let reopenedProfile = reopened.requireProfile(profile.id);
53
+
54
+ expect(reopened.getProfile()?.id).toBe(profile.id);
55
+ expect(reopenedProfile.config).toEqual({ prefix: 'Hello' });
56
+ expect(reopened.getAuth(profile.id, 'token_auth')?.output).toEqual({ token: 'abc' });
57
+ expect(reopened.scope?.key).toBe('integrations/demo');
58
+ expect(reopened.cliDir).toBe(path.join(cwd, '.slates-cli'));
59
+ expect(reopened.storePath).toBe(
60
+ path.join(cwd, '.slates-cli', 'profiles', 'integrations', 'demo', 'store.json')
61
+ );
62
+ });
63
+
64
+ it('opens an external scoped store with a separate logical root', async () => {
65
+ let workspaceRoot = await createTempDir();
66
+ let storageRoot = await createTempDir();
67
+ let storePath = path.join(
68
+ storageRoot,
69
+ '.slates-cli',
70
+ 'profiles',
71
+ 'integrations',
72
+ 'demo',
73
+ 'store.json'
74
+ );
75
+
76
+ let store = await SlatesCliStore.open({
77
+ storePath,
78
+ rootDir: workspaceRoot
79
+ });
80
+
81
+ let profile = store.upsertProfile({
82
+ name: 'Demo',
83
+ target: {
84
+ type: 'local',
85
+ entry: 'integrations/demo/src/index.ts',
86
+ exportName: 'provider'
87
+ }
88
+ });
89
+ store.setCurrentProfile(profile.id);
90
+ await store.save();
91
+
92
+ let reopened = await SlatesCliStore.open({
93
+ storePath,
94
+ rootDir: workspaceRoot
95
+ });
96
+
97
+ expect(reopened.rootDir).toBe(workspaceRoot);
98
+ expect(reopened.cliDir).toBe(path.join(storageRoot, '.slates-cli'));
99
+ expect(reopened.scope?.key).toBe('integrations/demo');
100
+ expect(reopened.getProfile()?.target.entry).toBe('integrations/demo/src/index.ts');
101
+
102
+ await expect(
103
+ access(path.join(storageRoot, '.slates-cli', '.gitignore'))
104
+ ).resolves.toBeUndefined();
105
+ await expect(access(path.join(workspaceRoot, '.slates-cli'))).rejects.toMatchObject({
106
+ code: 'ENOENT'
107
+ });
108
+ });
109
+
110
+ it('migrates matching legacy profiles into an integration-scoped store', async () => {
111
+ let cwd = await createTempDir();
112
+ let cliDir = path.join(cwd, '.slates-cli');
113
+ await mkdir(cliDir, { recursive: true });
114
+
115
+ await writeFile(
116
+ path.join(cliDir, 'store.json'),
117
+ JSON.stringify(
118
+ {
119
+ version: 1,
120
+ currentProfileId: 'demo-profile',
121
+ profiles: {
122
+ 'demo-profile': {
123
+ id: 'demo-profile',
124
+ name: 'Demo',
125
+ target: {
126
+ type: 'local',
127
+ entry: 'integrations/demo/src/index.ts',
128
+ exportName: 'provider'
129
+ },
130
+ config: { prefix: 'Hello' },
131
+ auth: {},
132
+ session: null,
133
+ metadata: {
134
+ provider: null,
135
+ actions: null
136
+ },
137
+ createdAt: '2026-01-01T00:00:00.000Z',
138
+ updatedAt: '2026-01-01T00:00:00.000Z'
139
+ },
140
+ 'other-profile': {
141
+ id: 'other-profile',
142
+ name: 'Other',
143
+ target: {
144
+ type: 'local',
145
+ entry: 'integrations/other/src/index.ts',
146
+ exportName: 'provider'
147
+ },
148
+ config: null,
149
+ auth: {},
150
+ session: null,
151
+ metadata: {
152
+ provider: null,
153
+ actions: null
154
+ },
155
+ createdAt: '2026-01-01T00:00:00.000Z',
156
+ updatedAt: '2026-01-01T00:00:00.000Z'
157
+ }
158
+ }
159
+ },
160
+ null,
161
+ 2
162
+ ),
163
+ 'utf-8'
164
+ );
165
+
166
+ let store = await SlatesCliStore.open({
167
+ cwd,
168
+ scope: {
169
+ key: 'integrations/demo',
170
+ name: 'demo'
171
+ }
172
+ });
173
+
174
+ expect(store.listProfiles().map(profile => profile.id)).toEqual(['demo-profile']);
175
+ expect(store.getProfile()?.config).toEqual({ prefix: 'Hello' });
176
+ expect(store.listOAuthCredentials()).toEqual([]);
177
+ });
178
+
179
+ it('creates a local client from a profile target', async () => {
180
+ let cwd = await createTempDir();
181
+ let entry = path.join(cwd, 'demo-slate.mjs');
182
+ await writeFile(
183
+ entry,
184
+ `
185
+ import { Slate, SlateAuth, SlateConfig, SlateSpecification } from '@slates/provider';
186
+ import { z } from 'zod';
187
+
188
+ let config = SlateConfig.create(
189
+ z.object({
190
+ prefix: z.string().optional()
191
+ })
192
+ ).getDefaultConfig(() => ({}));
193
+
194
+ let auth = SlateAuth.create();
195
+
196
+ export let provider = Slate.create({
197
+ spec: SlateSpecification.create({
198
+ key: 'demo',
199
+ name: 'Demo Slate',
200
+ description: 'Local test slate',
201
+ config,
202
+ auth
203
+ }),
204
+ tools: [],
205
+ triggers: []
206
+ });
207
+ `,
208
+ 'utf-8'
209
+ );
210
+
211
+ let store = await SlatesCliStore.open({
212
+ cwd,
213
+ scope: {
214
+ key: 'integrations/demo',
215
+ name: 'demo'
216
+ }
217
+ });
218
+ let profile = store.upsertProfile({
219
+ name: 'Demo',
220
+ target: {
221
+ type: 'local',
222
+ entry: './demo-slate.mjs',
223
+ exportName: 'provider'
224
+ }
225
+ });
226
+
227
+ let client = await createSlatesClientFromProfile(profile, { cwd });
228
+ let provider = await client.identify();
229
+
230
+ expect(provider.provider.id).toBe('demo');
231
+ expect(provider.provider.name).toBe('Demo Slate');
232
+ });
233
+
234
+ it('resolves profile entry paths against the store root when the store lives elsewhere', async () => {
235
+ let workspaceRoot = await createTempDir();
236
+ let storageRoot = await createTempDir();
237
+ let entry = path.join(workspaceRoot, 'integrations', 'demo-slate.mjs');
238
+ await mkdir(path.dirname(entry), { recursive: true });
239
+ await writeFile(
240
+ entry,
241
+ `
242
+ import { Slate, SlateAuth, SlateConfig, SlateSpecification } from '@slates/provider';
243
+ import { z } from 'zod';
244
+
245
+ let config = SlateConfig.create(z.object({})).getDefaultConfig(() => ({}));
246
+ let auth = SlateAuth.create();
247
+
248
+ export let provider = Slate.create({
249
+ spec: SlateSpecification.create({
250
+ key: 'external-demo',
251
+ name: 'External Demo',
252
+ description: 'Store root resolution test',
253
+ config,
254
+ auth
255
+ }),
256
+ tools: [],
257
+ triggers: []
258
+ });
259
+ `,
260
+ 'utf-8'
261
+ );
262
+
263
+ let store = await SlatesCliStore.open({
264
+ storePath: path.join(
265
+ storageRoot,
266
+ '.slates-cli',
267
+ 'profiles',
268
+ 'integrations',
269
+ 'demo',
270
+ 'store.json'
271
+ ),
272
+ rootDir: workspaceRoot
273
+ });
274
+ let profile = store.upsertProfile({
275
+ name: 'Demo',
276
+ target: {
277
+ type: 'local',
278
+ entry: 'integrations/demo-slate.mjs',
279
+ exportName: 'provider'
280
+ }
281
+ });
282
+
283
+ let client = await createSlatesClientFromProfile(profile, { store });
284
+ let provider = await client.identify();
285
+
286
+ expect(provider.provider.id).toBe('external-demo');
287
+ expect(provider.provider.name).toBe('External Demo');
288
+ });
289
+
290
+ it('stores OAuth client credentials per integration', async () => {
291
+ let cwd = await createTempDir();
292
+ let store = await SlatesCliStore.open({
293
+ cwd,
294
+ scope: {
295
+ key: 'integrations/demo',
296
+ name: 'demo'
297
+ }
298
+ });
299
+
300
+ let credential = store.upsertOAuthCredential({
301
+ name: 'Primary OAuth App',
302
+ authMethodId: 'oauth',
303
+ clientId: 'client-id',
304
+ clientSecret: 'client-secret'
305
+ });
306
+ await store.save();
307
+
308
+ let reopened = await SlatesCliStore.open({ storePath: store.storePath });
309
+ expect(reopened.getOAuthCredential(credential.id)?.clientId).toBe('client-id');
310
+ expect(reopened.getOAuthCredential('Primary OAuth App')?.clientSecret).toBe(
311
+ 'client-secret'
312
+ );
313
+ });
314
+
315
+ it('refreshes expired OAuth auth automatically in the profile-backed client', async () => {
316
+ let cwd = await createTempDir();
317
+ let entry = path.join(cwd, 'oauth-slate.mjs');
318
+ await writeFile(
319
+ entry,
320
+ `
321
+ import { Slate, SlateAuth, SlateConfig, SlateSpecification, SlateTool } from '@slates/provider';
322
+ import { z } from 'zod';
323
+
324
+ let config = SlateConfig.create(z.object({})).getDefaultConfig(() => ({}));
325
+ let auth = SlateAuth.create()
326
+ .output(
327
+ z.object({
328
+ token: z.string(),
329
+ refreshToken: z.string().optional(),
330
+ expiresAt: z.string().optional()
331
+ })
332
+ )
333
+ .addOauth({
334
+ type: 'auth.oauth',
335
+ key: 'oauth',
336
+ name: 'OAuth',
337
+ scopes: [],
338
+ getAuthorizationUrl: async () => ({ url: 'https://example.com' }),
339
+ handleCallback: async () => ({ output: { token: 'initial-token' } }),
340
+ handleTokenRefresh: async ctx => ({
341
+ output: {
342
+ token: \`\${ctx.output.token}-refreshed\`,
343
+ refreshToken: ctx.output.refreshToken,
344
+ expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString()
345
+ }
346
+ })
347
+ });
348
+
349
+ let spec = SlateSpecification.create({
350
+ key: 'oauth-demo',
351
+ name: 'OAuth Demo',
352
+ description: 'OAuth test slate',
353
+ config,
354
+ auth
355
+ });
356
+
357
+ let tool = SlateTool.create(spec, {
358
+ key: 'whoami',
359
+ name: 'Who Am I'
360
+ })
361
+ .input(z.object({}))
362
+ .output(
363
+ z.object({
364
+ token: z.string()
365
+ })
366
+ )
367
+ .handleInvocation(async ctx => ({
368
+ output: {
369
+ token: ctx.auth.token
370
+ }
371
+ }))
372
+ .build();
373
+
374
+ export let provider = Slate.create({
375
+ spec,
376
+ tools: [tool],
377
+ triggers: []
378
+ });
379
+ `,
380
+ 'utf-8'
381
+ );
382
+
383
+ let store = await SlatesCliStore.open({
384
+ cwd,
385
+ scope: {
386
+ key: 'integrations/demo',
387
+ name: 'demo'
388
+ }
389
+ });
390
+ let profile = store.upsertProfile({
391
+ name: 'Demo',
392
+ target: {
393
+ type: 'local',
394
+ entry: './oauth-slate.mjs',
395
+ exportName: 'provider'
396
+ }
397
+ });
398
+ store.setProfileConfig(profile.id, {});
399
+ store.upsertAuth(profile.id, {
400
+ authMethodId: 'oauth',
401
+ authType: 'auth.oauth',
402
+ input: {},
403
+ output: {
404
+ token: 'expired-token',
405
+ refreshToken: 'refresh-token',
406
+ expiresAt: new Date(Date.now() - 60 * 1000).toISOString()
407
+ },
408
+ scopes: [],
409
+ clientId: 'client-id',
410
+ clientSecret: 'client-secret'
411
+ });
412
+ await store.save();
413
+
414
+ let client = await createSlatesClientFromProfile(profile, { cwd, store });
415
+ let result = await client.invokeTool('whoami', {});
416
+
417
+ expect(result.output).toEqual({ token: 'expired-token-refreshed' });
418
+ expect(store.getAuth(profile.id, 'oauth')?.output).toMatchObject({
419
+ token: 'expired-token-refreshed'
420
+ });
421
+ });
422
+
423
+ it('refreshes expired custom auth automatically in the profile-backed client', async () => {
424
+ let cwd = await createTempDir();
425
+ let entry = path.join(cwd, 'custom-slate.mjs');
426
+ await writeFile(
427
+ entry,
428
+ `
429
+ import { Slate, SlateAuth, SlateConfig, SlateSpecification, SlateTool } from '@slates/provider';
430
+ import { z } from 'zod';
431
+
432
+ let config = SlateConfig.create(z.object({})).getDefaultConfig(() => ({}));
433
+ let auth = SlateAuth.create()
434
+ .output(
435
+ z.object({
436
+ token: z.string(),
437
+ expiresAt: z.string().optional()
438
+ })
439
+ )
440
+ .addCustomAuth({
441
+ type: 'auth.custom',
442
+ key: 'custom',
443
+ name: 'Custom',
444
+ inputSchema: z.object({
445
+ tokenPrefix: z.string()
446
+ }),
447
+ getOutput: async ctx => ({
448
+ output: {
449
+ token: \`\${ctx.input.tokenPrefix}-initial\`,
450
+ expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString()
451
+ }
452
+ }),
453
+ handleTokenRefresh: async ctx => ({
454
+ output: {
455
+ token: \`\${ctx.input.tokenPrefix}-refreshed\`,
456
+ expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString()
457
+ }
458
+ })
459
+ });
460
+
461
+ let spec = SlateSpecification.create({
462
+ key: 'custom-demo',
463
+ name: 'Custom Demo',
464
+ description: 'Custom auth test slate',
465
+ config,
466
+ auth
467
+ });
468
+
469
+ let tool = SlateTool.create(spec, {
470
+ key: 'whoami',
471
+ name: 'Who Am I'
472
+ })
473
+ .input(z.object({}))
474
+ .output(
475
+ z.object({
476
+ token: z.string()
477
+ })
478
+ )
479
+ .handleInvocation(async ctx => ({
480
+ output: {
481
+ token: ctx.auth.token
482
+ }
483
+ }))
484
+ .build();
485
+
486
+ export let provider = Slate.create({
487
+ spec,
488
+ tools: [tool],
489
+ triggers: []
490
+ });
491
+ `,
492
+ 'utf-8'
493
+ );
494
+
495
+ let store = await SlatesCliStore.open({
496
+ cwd,
497
+ scope: {
498
+ key: 'integrations/demo',
499
+ name: 'demo'
500
+ }
501
+ });
502
+ let profile = store.upsertProfile({
503
+ name: 'Demo',
504
+ target: {
505
+ type: 'local',
506
+ entry: './custom-slate.mjs',
507
+ exportName: 'provider'
508
+ }
509
+ });
510
+ store.setProfileConfig(profile.id, {});
511
+ store.upsertAuth(profile.id, {
512
+ authMethodId: 'custom',
513
+ authType: 'auth.custom',
514
+ input: {
515
+ tokenPrefix: 'custom-token'
516
+ },
517
+ output: {
518
+ token: 'expired-token',
519
+ expiresAt: new Date(Date.now() - 60 * 1000).toISOString()
520
+ },
521
+ scopes: []
522
+ });
523
+ await store.save();
524
+
525
+ let client = await createSlatesClientFromProfile(profile, { cwd, store });
526
+ let result = await client.invokeTool('whoami', {});
527
+
528
+ expect(result.output).toEqual({ token: 'custom-token-refreshed' });
529
+ expect(store.getAuth(profile.id, 'custom')?.output).toMatchObject({
530
+ token: 'custom-token-refreshed'
531
+ });
532
+ });
533
+ });