@slates/client 1.0.0-rc.11
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 +409 -0
- package/dist/index.d.cts +227 -0
- package/dist/index.d.ts +227 -0
- package/dist/index.module.js +383 -0
- package/package.json +43 -0
- package/src/client.test.ts +845 -0
- package/src/client.ts +401 -0
- package/src/error.ts +159 -0
- package/src/index.ts +10 -0
- package/src/transport.ts +57 -0
- package/src/types.ts +36 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
import {
|
|
2
|
+
axios,
|
|
3
|
+
Slate,
|
|
4
|
+
SlateAuth,
|
|
5
|
+
SlateConfig,
|
|
6
|
+
type SlateLogEntry,
|
|
7
|
+
SlateSpecification,
|
|
8
|
+
SlateTool,
|
|
9
|
+
SlateTrigger
|
|
10
|
+
} from '@slates/provider';
|
|
11
|
+
import { rm } from 'fs/promises';
|
|
12
|
+
import { createServer } from 'http';
|
|
13
|
+
import type { AddressInfo } from 'net';
|
|
14
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { createLocalSlateTransport, createSlatesClient, SlateProtocolError } from './index';
|
|
17
|
+
|
|
18
|
+
let tempDirs: string[] = [];
|
|
19
|
+
|
|
20
|
+
let waitForLogs = async () => {
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await Promise.all(tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
let createDemoSlate = () => {
|
|
29
|
+
let demoConfig = SlateConfig.create(
|
|
30
|
+
z.object({
|
|
31
|
+
prefix: z.string()
|
|
32
|
+
})
|
|
33
|
+
).getDefaultConfig(() => ({
|
|
34
|
+
prefix: 'Hello'
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
let demoAuth = SlateAuth.create<{ token: string }>()
|
|
38
|
+
.output(
|
|
39
|
+
z.object({
|
|
40
|
+
token: z.string()
|
|
41
|
+
})
|
|
42
|
+
)
|
|
43
|
+
.addTokenAuth({
|
|
44
|
+
type: 'auth.token',
|
|
45
|
+
key: 'token_auth',
|
|
46
|
+
name: 'Token Auth',
|
|
47
|
+
inputSchema: z.object({
|
|
48
|
+
token: z.string()
|
|
49
|
+
}),
|
|
50
|
+
getDefaultInput: async () => ({
|
|
51
|
+
token: 'default-token'
|
|
52
|
+
}),
|
|
53
|
+
onInputChanged: async (ctx: { newInput: { token: string } }) => ({
|
|
54
|
+
input: {
|
|
55
|
+
token: ctx.newInput.token.trim()
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
getOutput: async (ctx: { input: { token: string } }) => ({
|
|
59
|
+
output: {
|
|
60
|
+
token: ctx.input.token
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
getProfile: async (ctx: { output: { token: string } }) => ({
|
|
64
|
+
profile: {
|
|
65
|
+
tokenPreview: ctx.output.token.slice(0, 3)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let spec = SlateSpecification.create({
|
|
71
|
+
key: 'demo-slate',
|
|
72
|
+
name: 'Demo Slate',
|
|
73
|
+
description: 'A tiny test slate',
|
|
74
|
+
config: demoConfig,
|
|
75
|
+
auth: demoAuth
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
let echoTool = SlateTool.create(spec, {
|
|
79
|
+
key: 'echo',
|
|
80
|
+
name: 'Echo'
|
|
81
|
+
})
|
|
82
|
+
.input(
|
|
83
|
+
z.object({
|
|
84
|
+
name: z.string()
|
|
85
|
+
})
|
|
86
|
+
)
|
|
87
|
+
.output(
|
|
88
|
+
z.object({
|
|
89
|
+
greeting: z.string(),
|
|
90
|
+
token: z.string()
|
|
91
|
+
})
|
|
92
|
+
)
|
|
93
|
+
.scopes({
|
|
94
|
+
AND: [
|
|
95
|
+
{
|
|
96
|
+
OR: ['scope:echo', 'scope:echo:admin']
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
})
|
|
100
|
+
.handleInvocation(async ctx => ({
|
|
101
|
+
output: {
|
|
102
|
+
greeting: `${ctx.config.prefix} ${ctx.input.name}`,
|
|
103
|
+
token: ctx.auth.token
|
|
104
|
+
},
|
|
105
|
+
message: 'done'
|
|
106
|
+
}))
|
|
107
|
+
.build();
|
|
108
|
+
|
|
109
|
+
return Slate.create({
|
|
110
|
+
spec,
|
|
111
|
+
tools: [echoTool],
|
|
112
|
+
triggers: []
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
let createTraceSlate = (baseUrl: string) => {
|
|
117
|
+
let config = SlateConfig.create(z.object({}));
|
|
118
|
+
let auth = SlateAuth.create<{}>().output(z.object({}));
|
|
119
|
+
|
|
120
|
+
let spec = SlateSpecification.create({
|
|
121
|
+
key: 'trace-slate',
|
|
122
|
+
name: 'Trace Slate',
|
|
123
|
+
description: 'A slate that traces HTTP requests',
|
|
124
|
+
config,
|
|
125
|
+
auth
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let traceTool = SlateTool.create(spec, {
|
|
129
|
+
key: 'trace_http',
|
|
130
|
+
name: 'Trace HTTP'
|
|
131
|
+
})
|
|
132
|
+
.input(z.object({}))
|
|
133
|
+
.output(
|
|
134
|
+
z.object({
|
|
135
|
+
ok: z.boolean(),
|
|
136
|
+
status: z.number(),
|
|
137
|
+
traceCount: z.number()
|
|
138
|
+
})
|
|
139
|
+
)
|
|
140
|
+
.handleInvocation(async ctx => {
|
|
141
|
+
let response = await axios.post(
|
|
142
|
+
`${baseUrl}/trace?api_key=request-secret&visible=yes`,
|
|
143
|
+
{
|
|
144
|
+
message: 'hello',
|
|
145
|
+
secret: 'client-secret'
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
headers: {
|
|
149
|
+
Authorization: 'Bearer client-token',
|
|
150
|
+
'X-Api-Key': 'client-api-key'
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
output: {
|
|
157
|
+
ok: response.data.ok,
|
|
158
|
+
status: response.status,
|
|
159
|
+
traceCount: ctx.getHttpTraces().length
|
|
160
|
+
},
|
|
161
|
+
message: 'traced'
|
|
162
|
+
};
|
|
163
|
+
})
|
|
164
|
+
.build();
|
|
165
|
+
|
|
166
|
+
return Slate.create({
|
|
167
|
+
spec,
|
|
168
|
+
tools: [traceTool],
|
|
169
|
+
triggers: []
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
let createTraceErrorSlate = (baseUrl: string) => {
|
|
174
|
+
let config = SlateConfig.create(z.object({}));
|
|
175
|
+
let auth = SlateAuth.create<{}>().output(z.object({}));
|
|
176
|
+
|
|
177
|
+
let spec = SlateSpecification.create({
|
|
178
|
+
key: 'trace-error-slate',
|
|
179
|
+
name: 'Trace Error Slate',
|
|
180
|
+
description: 'A slate that traces failed HTTP requests',
|
|
181
|
+
config,
|
|
182
|
+
auth
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
let traceTool = SlateTool.create(spec, {
|
|
186
|
+
key: 'trace_http_error',
|
|
187
|
+
name: 'Trace HTTP Error'
|
|
188
|
+
})
|
|
189
|
+
.input(z.object({}))
|
|
190
|
+
.output(
|
|
191
|
+
z.object({
|
|
192
|
+
ok: z.boolean()
|
|
193
|
+
})
|
|
194
|
+
)
|
|
195
|
+
.handleInvocation(async () => {
|
|
196
|
+
await axios.post(
|
|
197
|
+
`${baseUrl}/trace-error?client_secret=request-secret`,
|
|
198
|
+
new URLSearchParams({
|
|
199
|
+
grant_type: 'authorization_code',
|
|
200
|
+
client_secret: 'client-secret'
|
|
201
|
+
}).toString(),
|
|
202
|
+
{
|
|
203
|
+
headers: {
|
|
204
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
205
|
+
Authorization: 'Bearer client-token'
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
output: {
|
|
212
|
+
ok: true
|
|
213
|
+
},
|
|
214
|
+
message: 'should not reach'
|
|
215
|
+
};
|
|
216
|
+
})
|
|
217
|
+
.build();
|
|
218
|
+
|
|
219
|
+
return Slate.create({
|
|
220
|
+
spec,
|
|
221
|
+
tools: [traceTool],
|
|
222
|
+
triggers: []
|
|
223
|
+
});
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
let createTriggerTraceSlate = () => {
|
|
227
|
+
let config = SlateConfig.create(z.object({}));
|
|
228
|
+
let auth = SlateAuth.create<{}>().output(z.object({}));
|
|
229
|
+
|
|
230
|
+
let spec = SlateSpecification.create({
|
|
231
|
+
key: 'trigger-trace-slate',
|
|
232
|
+
name: 'Trigger Trace Slate',
|
|
233
|
+
description: 'A slate that traces trigger execution',
|
|
234
|
+
config,
|
|
235
|
+
auth
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
let pollingTrigger = SlateTrigger.create(spec, {
|
|
239
|
+
key: 'poll_trigger',
|
|
240
|
+
name: 'Poll Trigger'
|
|
241
|
+
})
|
|
242
|
+
.input(
|
|
243
|
+
z.object({
|
|
244
|
+
id: z.string()
|
|
245
|
+
})
|
|
246
|
+
)
|
|
247
|
+
.output(
|
|
248
|
+
z.object({
|
|
249
|
+
type: z.string(),
|
|
250
|
+
value: z.string()
|
|
251
|
+
})
|
|
252
|
+
)
|
|
253
|
+
.polling({
|
|
254
|
+
pollEvents: async () => ({
|
|
255
|
+
inputs: [{ id: 'poll-1' }, { id: 'poll-2' }]
|
|
256
|
+
}),
|
|
257
|
+
handleEvent: async ctx => ({
|
|
258
|
+
id: ctx.input.id,
|
|
259
|
+
type: 'poll.event',
|
|
260
|
+
output: {
|
|
261
|
+
type: 'poll.event',
|
|
262
|
+
value: ctx.input.id
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
.build();
|
|
267
|
+
|
|
268
|
+
let webhookTrigger = SlateTrigger.create(spec, {
|
|
269
|
+
key: 'webhook_trigger',
|
|
270
|
+
name: 'Webhook Trigger'
|
|
271
|
+
})
|
|
272
|
+
.input(
|
|
273
|
+
z.object({
|
|
274
|
+
id: z.string()
|
|
275
|
+
})
|
|
276
|
+
)
|
|
277
|
+
.output(
|
|
278
|
+
z.object({
|
|
279
|
+
type: z.string(),
|
|
280
|
+
value: z.string()
|
|
281
|
+
})
|
|
282
|
+
)
|
|
283
|
+
.webhook({
|
|
284
|
+
handleRequest: async () => ({
|
|
285
|
+
inputs: [{ id: 'webhook-1' }, { id: 'webhook-2' }]
|
|
286
|
+
}),
|
|
287
|
+
handleEvent: async ctx => ({
|
|
288
|
+
id: ctx.input.id,
|
|
289
|
+
type: 'webhook.event',
|
|
290
|
+
output: {
|
|
291
|
+
type: 'webhook.event',
|
|
292
|
+
value: ctx.input.id
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
.build();
|
|
297
|
+
|
|
298
|
+
return Slate.create({
|
|
299
|
+
spec,
|
|
300
|
+
tools: [],
|
|
301
|
+
triggers: [pollingTrigger, webhookTrigger]
|
|
302
|
+
});
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
type OAuthConfig = {
|
|
306
|
+
loginHost: string;
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
type OAuthOutput = {
|
|
310
|
+
token: string;
|
|
311
|
+
refreshToken?: string;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
type SeenOAuthConfig = Partial<
|
|
315
|
+
Record<'authorizationUrl' | 'callback' | 'refresh' | 'profile', OAuthConfig>
|
|
316
|
+
>;
|
|
317
|
+
|
|
318
|
+
let getRequiredOAuthConfig = (ctx: { config?: Record<string, unknown> }): OAuthConfig => {
|
|
319
|
+
expect(ctx.config).toBeDefined();
|
|
320
|
+
return ctx.config as OAuthConfig;
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
let createOauthConfigSlate = (seenConfig: SeenOAuthConfig) => {
|
|
324
|
+
let config = SlateConfig.create(
|
|
325
|
+
z.object({
|
|
326
|
+
loginHost: z.string()
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
let auth = SlateAuth.create<{ token: string; refreshToken?: string }>()
|
|
331
|
+
.output(
|
|
332
|
+
z.object({
|
|
333
|
+
token: z.string(),
|
|
334
|
+
refreshToken: z.string().optional()
|
|
335
|
+
})
|
|
336
|
+
)
|
|
337
|
+
.addOauth({
|
|
338
|
+
type: 'auth.oauth',
|
|
339
|
+
key: 'oauth',
|
|
340
|
+
name: 'OAuth',
|
|
341
|
+
scopes: [],
|
|
342
|
+
inputSchema: z.object({}),
|
|
343
|
+
getAuthorizationUrl: async ctx => {
|
|
344
|
+
let config = getRequiredOAuthConfig(ctx);
|
|
345
|
+
seenConfig.authorizationUrl = config;
|
|
346
|
+
return {
|
|
347
|
+
url: `https://${config.loginHost}/authorize`,
|
|
348
|
+
callbackState: {
|
|
349
|
+
loginHost: config.loginHost
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
},
|
|
353
|
+
handleCallback: async ctx => {
|
|
354
|
+
let config = getRequiredOAuthConfig(ctx);
|
|
355
|
+
seenConfig.callback = config;
|
|
356
|
+
return {
|
|
357
|
+
output: {
|
|
358
|
+
token: `token:${config.loginHost}`,
|
|
359
|
+
refreshToken: 'refresh-token'
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
},
|
|
363
|
+
handleTokenRefresh: async (ctx: {
|
|
364
|
+
output: OAuthOutput;
|
|
365
|
+
config?: Record<string, unknown>;
|
|
366
|
+
}) => {
|
|
367
|
+
let config = getRequiredOAuthConfig(ctx);
|
|
368
|
+
seenConfig.refresh = config;
|
|
369
|
+
return {
|
|
370
|
+
output: {
|
|
371
|
+
...ctx.output,
|
|
372
|
+
token: `refreshed:${config.loginHost}`
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
},
|
|
376
|
+
getProfile: async (ctx: { config?: Record<string, unknown> }) => {
|
|
377
|
+
let config = getRequiredOAuthConfig(ctx);
|
|
378
|
+
seenConfig.profile = config;
|
|
379
|
+
return {
|
|
380
|
+
profile: {
|
|
381
|
+
loginHost: config.loginHost
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
let spec = SlateSpecification.create({
|
|
388
|
+
key: 'oauth-config-slate',
|
|
389
|
+
name: 'OAuth Config Slate',
|
|
390
|
+
description: 'A slate that reads config in OAuth callbacks',
|
|
391
|
+
config,
|
|
392
|
+
auth
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
return Slate.create({
|
|
396
|
+
spec,
|
|
397
|
+
tools: [],
|
|
398
|
+
triggers: []
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
describe('@slates/client local transport', () => {
|
|
403
|
+
it('discovers auth/config and invokes tools with session state', async () => {
|
|
404
|
+
let slate = createDemoSlate();
|
|
405
|
+
let client = createSlatesClient({
|
|
406
|
+
transport: createLocalSlateTransport({ slate })
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
let provider = await client.identify();
|
|
410
|
+
expect(provider.provider.id).toBe('demo-slate');
|
|
411
|
+
|
|
412
|
+
let actions = await client.listTools();
|
|
413
|
+
expect(actions).toHaveLength(1);
|
|
414
|
+
expect(actions[0]!.id).toBe('echo');
|
|
415
|
+
expect(actions[0]!.scopes).toEqual({
|
|
416
|
+
AND: [
|
|
417
|
+
{
|
|
418
|
+
OR: ['scope:echo', 'scope:echo:admin']
|
|
419
|
+
}
|
|
420
|
+
]
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
let configSchema = await client.getConfigSchema();
|
|
424
|
+
expect(configSchema.schema.properties.prefix.type).toBe('string');
|
|
425
|
+
|
|
426
|
+
let defaultConfig = await client.getDefaultConfig();
|
|
427
|
+
expect(defaultConfig.config).toEqual({ prefix: 'Hello' });
|
|
428
|
+
|
|
429
|
+
let authMethods = await client.listAuthMethods();
|
|
430
|
+
expect(authMethods.authenticationMethods).toHaveLength(1);
|
|
431
|
+
expect(authMethods.authenticationMethods[0]!.type).toBe('auth.token');
|
|
432
|
+
|
|
433
|
+
let defaultInput = await client.getDefaultAuthInput('token_auth');
|
|
434
|
+
expect(defaultInput.input).toEqual({ token: 'default-token' });
|
|
435
|
+
|
|
436
|
+
let changedInput = await client.updateAuthInput({
|
|
437
|
+
authenticationMethodId: 'token_auth',
|
|
438
|
+
previousInput: null,
|
|
439
|
+
newInput: { token: ' trimmed-token ' }
|
|
440
|
+
});
|
|
441
|
+
expect(changedInput.input).toEqual({ token: 'trimmed-token' });
|
|
442
|
+
|
|
443
|
+
let authOutput = await client.getAuthOutput({
|
|
444
|
+
authenticationMethodId: 'token_auth',
|
|
445
|
+
input: changedInput.input ?? { token: '' }
|
|
446
|
+
});
|
|
447
|
+
expect(authOutput.output).toEqual({ token: 'trimmed-token' });
|
|
448
|
+
|
|
449
|
+
client.setConfig({ prefix: 'Hi' });
|
|
450
|
+
client.setAuth({
|
|
451
|
+
authenticationMethodId: 'token_auth',
|
|
452
|
+
output: authOutput.output
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
let result = await client.invokeTool('echo', { name: 'Tobias' });
|
|
456
|
+
expect(result.output).toEqual({
|
|
457
|
+
greeting: 'Hi Tobias',
|
|
458
|
+
token: 'trimmed-token'
|
|
459
|
+
});
|
|
460
|
+
expect(client.state.session?.id).toBeTruthy();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('emits structured traces for provider callbacks', async () => {
|
|
464
|
+
let slate = createDemoSlate();
|
|
465
|
+
let logs: SlateLogEntry[] = [];
|
|
466
|
+
let client = createSlatesClient({
|
|
467
|
+
transport: createLocalSlateTransport({
|
|
468
|
+
slate,
|
|
469
|
+
listeners: [
|
|
470
|
+
entries => {
|
|
471
|
+
logs.push(...entries);
|
|
472
|
+
}
|
|
473
|
+
]
|
|
474
|
+
})
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await client.identify();
|
|
478
|
+
await client.getDefaultConfig();
|
|
479
|
+
let changedInput = await client.updateAuthInput({
|
|
480
|
+
authenticationMethodId: 'token_auth',
|
|
481
|
+
previousInput: null,
|
|
482
|
+
newInput: { token: ' traced-token ' }
|
|
483
|
+
});
|
|
484
|
+
let authOutput = await client.getAuthOutput({
|
|
485
|
+
authenticationMethodId: 'token_auth',
|
|
486
|
+
input: changedInput.input ?? { token: '' }
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
client.setConfig({ prefix: 'Hi' });
|
|
490
|
+
client.setAuth({
|
|
491
|
+
authenticationMethodId: 'token_auth',
|
|
492
|
+
output: authOutput.output
|
|
493
|
+
});
|
|
494
|
+
await client.invokeTool('echo', { name: 'Tracing' });
|
|
495
|
+
|
|
496
|
+
await waitForLogs();
|
|
497
|
+
|
|
498
|
+
expect(logs).toEqual(
|
|
499
|
+
expect.arrayContaining([
|
|
500
|
+
expect.objectContaining({
|
|
501
|
+
type: 'info',
|
|
502
|
+
message: 'Getting default config',
|
|
503
|
+
data: expect.objectContaining({
|
|
504
|
+
providerId: 'demo-slate',
|
|
505
|
+
component: 'config',
|
|
506
|
+
functionName: 'getDefaultConfig',
|
|
507
|
+
phase: 'start'
|
|
508
|
+
})
|
|
509
|
+
}),
|
|
510
|
+
expect.objectContaining({
|
|
511
|
+
type: 'info',
|
|
512
|
+
message: 'Authentication input change handler completed',
|
|
513
|
+
data: expect.objectContaining({
|
|
514
|
+
providerId: 'demo-slate',
|
|
515
|
+
component: 'auth',
|
|
516
|
+
functionName: 'onInputChanged',
|
|
517
|
+
phase: 'success',
|
|
518
|
+
authenticationMethodId: 'token_auth',
|
|
519
|
+
returnedInput: true
|
|
520
|
+
})
|
|
521
|
+
}),
|
|
522
|
+
expect.objectContaining({
|
|
523
|
+
type: 'info',
|
|
524
|
+
message: 'Completed tool "Echo" (echo)',
|
|
525
|
+
data: expect.objectContaining({
|
|
526
|
+
providerId: 'demo-slate',
|
|
527
|
+
component: 'action',
|
|
528
|
+
functionName: 'handleInvocation',
|
|
529
|
+
phase: 'success',
|
|
530
|
+
actionId: 'echo',
|
|
531
|
+
hasMessage: true,
|
|
532
|
+
actionResultMessage: 'done'
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
])
|
|
536
|
+
);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
it('emits human-readable trigger event count logs', async () => {
|
|
540
|
+
let logs: SlateLogEntry[] = [];
|
|
541
|
+
let client = createSlatesClient({
|
|
542
|
+
transport: createLocalSlateTransport({
|
|
543
|
+
slate: createTriggerTraceSlate(),
|
|
544
|
+
listeners: [
|
|
545
|
+
entries => {
|
|
546
|
+
logs.push(...entries);
|
|
547
|
+
}
|
|
548
|
+
]
|
|
549
|
+
}),
|
|
550
|
+
state: {
|
|
551
|
+
config: {}
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
client.ensureSession();
|
|
556
|
+
|
|
557
|
+
await client.request('slates/action.trigger.poll_events', {
|
|
558
|
+
actionId: 'poll_trigger',
|
|
559
|
+
state: null
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
await client.request('slates/action.trigger.webhook_handle', {
|
|
563
|
+
actionId: 'webhook_trigger',
|
|
564
|
+
url: 'https://example.com/webhook',
|
|
565
|
+
method: 'POST',
|
|
566
|
+
headers: {
|
|
567
|
+
'content-type': 'application/json'
|
|
568
|
+
},
|
|
569
|
+
body: null,
|
|
570
|
+
state: null
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
await waitForLogs();
|
|
574
|
+
|
|
575
|
+
expect(logs).toEqual(
|
|
576
|
+
expect.arrayContaining([
|
|
577
|
+
expect.objectContaining({
|
|
578
|
+
type: 'info',
|
|
579
|
+
message: 'Polled 2 event(s) for trigger "Poll Trigger" (poll_trigger)',
|
|
580
|
+
data: expect.objectContaining({
|
|
581
|
+
component: 'action',
|
|
582
|
+
functionName: 'pollEvents',
|
|
583
|
+
inputCount: 2,
|
|
584
|
+
actionId: 'poll_trigger'
|
|
585
|
+
})
|
|
586
|
+
}),
|
|
587
|
+
expect.objectContaining({
|
|
588
|
+
type: 'info',
|
|
589
|
+
message:
|
|
590
|
+
'Received 2 webhook event(s) for trigger "Webhook Trigger" (webhook_trigger)',
|
|
591
|
+
data: expect.objectContaining({
|
|
592
|
+
component: 'action',
|
|
593
|
+
functionName: 'handleRequest',
|
|
594
|
+
inputCount: 2,
|
|
595
|
+
actionId: 'webhook_trigger'
|
|
596
|
+
})
|
|
597
|
+
})
|
|
598
|
+
])
|
|
599
|
+
);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('passes current profile config into OAuth callbacks', async () => {
|
|
603
|
+
let seenConfig: SeenOAuthConfig = {};
|
|
604
|
+
let client = createSlatesClient({
|
|
605
|
+
transport: createLocalSlateTransport({
|
|
606
|
+
slate: createOauthConfigSlate(seenConfig)
|
|
607
|
+
}),
|
|
608
|
+
state: {
|
|
609
|
+
config: {
|
|
610
|
+
loginHost: 'sandbox.example.com'
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
let authorizationUrl = await client.getAuthorizationUrl({
|
|
616
|
+
authenticationMethodId: 'oauth',
|
|
617
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
618
|
+
state: 'state-1',
|
|
619
|
+
input: {},
|
|
620
|
+
clientId: 'client-id',
|
|
621
|
+
clientSecret: 'client-secret',
|
|
622
|
+
scopes: []
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
expect(authorizationUrl.authorizationUrl).toBe('https://sandbox.example.com/authorize');
|
|
626
|
+
expect(authorizationUrl.callbackState).toEqual({
|
|
627
|
+
loginHost: 'sandbox.example.com'
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
let callback = await client.handleAuthorizationCallback({
|
|
631
|
+
authenticationMethodId: 'oauth',
|
|
632
|
+
code: 'code-1',
|
|
633
|
+
state: 'state-1',
|
|
634
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
635
|
+
input: {},
|
|
636
|
+
clientId: 'client-id',
|
|
637
|
+
clientSecret: 'client-secret',
|
|
638
|
+
scopes: [],
|
|
639
|
+
callbackState: authorizationUrl.callbackState
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
expect(callback.output).toEqual({
|
|
643
|
+
token: 'token:sandbox.example.com',
|
|
644
|
+
refreshToken: 'refresh-token'
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
let refreshed = await client.refreshToken({
|
|
648
|
+
authenticationMethodId: 'oauth',
|
|
649
|
+
output: callback.output,
|
|
650
|
+
input: {},
|
|
651
|
+
clientId: 'client-id',
|
|
652
|
+
clientSecret: 'client-secret',
|
|
653
|
+
scopes: []
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
expect(refreshed.output.token).toBe('refreshed:sandbox.example.com');
|
|
657
|
+
|
|
658
|
+
let profile = await client.getAuthProfile({
|
|
659
|
+
authenticationMethodId: 'oauth',
|
|
660
|
+
output: refreshed.output,
|
|
661
|
+
input: {},
|
|
662
|
+
scopes: []
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
expect(profile.profile).toEqual({
|
|
666
|
+
loginHost: 'sandbox.example.com'
|
|
667
|
+
});
|
|
668
|
+
expect(seenConfig).toEqual({
|
|
669
|
+
authorizationUrl: { loginHost: 'sandbox.example.com' },
|
|
670
|
+
callback: { loginHost: 'sandbox.example.com' },
|
|
671
|
+
refresh: { loginHost: 'sandbox.example.com' },
|
|
672
|
+
profile: { loginHost: 'sandbox.example.com' }
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('throws structured protocol errors for provider responses', async () => {
|
|
677
|
+
let client = createSlatesClient({
|
|
678
|
+
transport: {
|
|
679
|
+
async send(messages) {
|
|
680
|
+
let request = messages.find(message => 'id' in message && message.id);
|
|
681
|
+
return [
|
|
682
|
+
{
|
|
683
|
+
jsonrpc: '2.0',
|
|
684
|
+
id: (request as { id: string }).id,
|
|
685
|
+
error: {
|
|
686
|
+
code: 'resource.not_found',
|
|
687
|
+
kind: 'upstream',
|
|
688
|
+
message: 'Resource contact_123 was not found',
|
|
689
|
+
status: 404,
|
|
690
|
+
provider: {
|
|
691
|
+
service: 'demo',
|
|
692
|
+
operation: 'failing.invoke'
|
|
693
|
+
},
|
|
694
|
+
baggage: {
|
|
695
|
+
resourceId: 'contact_123'
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
} as any
|
|
699
|
+
];
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
let promise = client.identify();
|
|
705
|
+
|
|
706
|
+
await expect(promise).rejects.toBeInstanceOf(SlateProtocolError);
|
|
707
|
+
|
|
708
|
+
try {
|
|
709
|
+
await promise;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
expect(error).toBeInstanceOf(SlateProtocolError);
|
|
712
|
+
expect((error as SlateProtocolError).data).toMatchObject({
|
|
713
|
+
code: 'resource.not_found',
|
|
714
|
+
kind: 'upstream',
|
|
715
|
+
message: 'Resource contact_123 was not found',
|
|
716
|
+
status: 404,
|
|
717
|
+
provider: {
|
|
718
|
+
service: 'demo',
|
|
719
|
+
operation: 'failing.invoke'
|
|
720
|
+
},
|
|
721
|
+
baggage: {
|
|
722
|
+
resourceId: 'contact_123'
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
it('returns sanitized request traces for shared axios calls', async () => {
|
|
729
|
+
let server = createServer((_req, res) => {
|
|
730
|
+
res.statusCode = 200;
|
|
731
|
+
res.setHeader('Content-Type', 'application/json');
|
|
732
|
+
res.setHeader('X-Request-Id', 'req-123');
|
|
733
|
+
res.end(
|
|
734
|
+
JSON.stringify({
|
|
735
|
+
ok: true,
|
|
736
|
+
token: 'server-secret',
|
|
737
|
+
note: 'x'.repeat(11_000)
|
|
738
|
+
})
|
|
739
|
+
);
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
await new Promise<void>((resolve, reject) => {
|
|
743
|
+
server.once('error', reject);
|
|
744
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
try {
|
|
748
|
+
let { port } = server.address() as AddressInfo;
|
|
749
|
+
let client = createSlatesClient({
|
|
750
|
+
transport: createLocalSlateTransport({
|
|
751
|
+
slate: createTraceSlate(`http://127.0.0.1:${port}`)
|
|
752
|
+
}),
|
|
753
|
+
state: {
|
|
754
|
+
config: {}
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
let result = await client.invokeTool('trace_http', {});
|
|
759
|
+
|
|
760
|
+
expect(result.output).toEqual({
|
|
761
|
+
ok: true,
|
|
762
|
+
status: 200,
|
|
763
|
+
traceCount: 1
|
|
764
|
+
});
|
|
765
|
+
expect(result.requestTraces).toHaveLength(1);
|
|
766
|
+
|
|
767
|
+
let trace = result.requestTraces?.[0];
|
|
768
|
+
expect(trace?.request.method).toBe('POST');
|
|
769
|
+
expect(trace?.request.url).toContain('visible=yes');
|
|
770
|
+
expect(trace?.request.url).not.toContain('request-secret');
|
|
771
|
+
expect(trace?.request.headers).toMatchObject({
|
|
772
|
+
accept: 'application/json, text/plain, */*',
|
|
773
|
+
'user-agent': 'slates.dev/1.0.0 trace-slate',
|
|
774
|
+
'x-slates-provider': 'trace-slate'
|
|
775
|
+
});
|
|
776
|
+
expect(trace?.request.headers).not.toHaveProperty('authorization');
|
|
777
|
+
expect(trace?.request.headers).not.toHaveProperty('x-api-key');
|
|
778
|
+
expect(trace?.request.body?.text).toContain('"secret":"[redacted]"');
|
|
779
|
+
expect(trace?.request.body?.text).not.toContain('client-secret');
|
|
780
|
+
expect(trace?.response?.headers).toMatchObject({
|
|
781
|
+
'content-type': 'application/json',
|
|
782
|
+
'x-request-id': 'req-123'
|
|
783
|
+
});
|
|
784
|
+
expect(trace?.response?.body?.truncated).toBe(true);
|
|
785
|
+
expect(trace?.response?.body?.text).toContain('"token":"[redacted]"');
|
|
786
|
+
expect(trace?.response?.body?.text).not.toContain('server-secret');
|
|
787
|
+
} finally {
|
|
788
|
+
await new Promise<void>((resolve, reject) => {
|
|
789
|
+
server.close(error => (error ? reject(error) : resolve()));
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('returns sanitized request traces on provider errors', async () => {
|
|
795
|
+
let server = createServer((_req, res) => {
|
|
796
|
+
res.statusCode = 400;
|
|
797
|
+
res.setHeader('Content-Type', 'application/json');
|
|
798
|
+
res.end(
|
|
799
|
+
JSON.stringify({
|
|
800
|
+
error: 'invalid_grant',
|
|
801
|
+
client_secret: 'server-secret'
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
await new Promise<void>((resolve, reject) => {
|
|
807
|
+
server.once('error', reject);
|
|
808
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
try {
|
|
812
|
+
let { port } = server.address() as AddressInfo;
|
|
813
|
+
let client = createSlatesClient({
|
|
814
|
+
transport: createLocalSlateTransport({
|
|
815
|
+
slate: createTraceErrorSlate(`http://127.0.0.1:${port}`)
|
|
816
|
+
}),
|
|
817
|
+
state: {
|
|
818
|
+
config: {}
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
let error = await client
|
|
823
|
+
.invokeTool('trace_http_error', {})
|
|
824
|
+
.then(() => null)
|
|
825
|
+
.catch(err => err as SlateProtocolError);
|
|
826
|
+
|
|
827
|
+
expect(error).toBeInstanceOf(SlateProtocolError);
|
|
828
|
+
expect(error?.data.code).toBe('upstream.invalid_request');
|
|
829
|
+
expect(error?.data.requestTraces).toHaveLength(1);
|
|
830
|
+
|
|
831
|
+
let trace = error?.data.requestTraces?.[0] as Record<string, any> | undefined;
|
|
832
|
+
expect(trace?.request.method).toBe('POST');
|
|
833
|
+
expect(trace?.request.url).not.toContain('request-secret');
|
|
834
|
+
expect(trace?.request.headers).not.toHaveProperty('authorization');
|
|
835
|
+
expect(trace?.request.body?.text).toContain('client_secret=[redacted]');
|
|
836
|
+
expect(trace?.response?.status).toBe(400);
|
|
837
|
+
expect(trace?.response?.body?.text).toContain('"client_secret":"[redacted]"');
|
|
838
|
+
expect(trace?.response?.body?.text).not.toContain('server-secret');
|
|
839
|
+
} finally {
|
|
840
|
+
await new Promise<void>((resolve, reject) => {
|
|
841
|
+
server.close(error => (error ? reject(error) : resolve()));
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
});
|