@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/dist/index.cjs +462 -0
- package/dist/index.d.cts +126 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.module.js +420 -0
- package/package.json +44 -0
- package/src/client.ts +158 -0
- package/src/index.ts +3 -0
- package/src/store.test.ts +533 -0
- package/src/store.ts +442 -0
- package/src/types.ts +67 -0
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,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
|
+
});
|