@slates/client 1.0.0-rc.2 → 1.0.0-rc.21
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 +5 -2
- package/dist/index.d.cts +64 -565
- package/dist/index.d.ts +64 -565
- package/dist/index.module.js +5 -2
- package/package.json +6 -6
- package/src/client.test.ts +593 -8
- package/src/client.ts +71 -32
- package/src/error.ts +11 -9
- package/src/index.ts +1 -1
- package/src/transport.ts +6 -6
- package/src/types.ts +1 -1
package/src/client.test.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
SlateAuth,
|
|
5
5
|
SlateConfig,
|
|
6
6
|
type SlateLogEntry,
|
|
7
|
+
SlatePublicTool,
|
|
7
8
|
SlateSpecification,
|
|
8
9
|
SlateTool,
|
|
9
10
|
SlateTrigger
|
|
@@ -30,9 +31,17 @@ let createDemoSlate = () => {
|
|
|
30
31
|
z.object({
|
|
31
32
|
prefix: z.string()
|
|
32
33
|
})
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
)
|
|
35
|
+
.getDefaultConfig(() => ({
|
|
36
|
+
prefix: 'Hello'
|
|
37
|
+
}))
|
|
38
|
+
.docs([
|
|
39
|
+
{
|
|
40
|
+
type: 'docs.config.general',
|
|
41
|
+
name: 'Demo config docs',
|
|
42
|
+
url: 'https://example.com/docs/config'
|
|
43
|
+
}
|
|
44
|
+
]);
|
|
36
45
|
|
|
37
46
|
let demoAuth = SlateAuth.create<{ token: string }>()
|
|
38
47
|
.output(
|
|
@@ -47,6 +56,13 @@ let createDemoSlate = () => {
|
|
|
47
56
|
inputSchema: z.object({
|
|
48
57
|
token: z.string()
|
|
49
58
|
}),
|
|
59
|
+
docs: [
|
|
60
|
+
{
|
|
61
|
+
type: 'docs.auth.token',
|
|
62
|
+
name: 'Demo token auth docs',
|
|
63
|
+
url: 'https://example.com/docs/auth/token'
|
|
64
|
+
}
|
|
65
|
+
],
|
|
50
66
|
getDefaultInput: async () => ({
|
|
51
67
|
token: 'default-token'
|
|
52
68
|
}),
|
|
@@ -58,7 +74,8 @@ let createDemoSlate = () => {
|
|
|
58
74
|
getOutput: async (ctx: { input: { token: string } }) => ({
|
|
59
75
|
output: {
|
|
60
76
|
token: ctx.input.token
|
|
61
|
-
}
|
|
77
|
+
},
|
|
78
|
+
scopes: [`scope:${ctx.input.token}`]
|
|
62
79
|
}),
|
|
63
80
|
getProfile: async (ctx: { output: { token: string } }) => ({
|
|
64
81
|
profile: {
|
|
@@ -71,13 +88,26 @@ let createDemoSlate = () => {
|
|
|
71
88
|
key: 'demo-slate',
|
|
72
89
|
name: 'Demo Slate',
|
|
73
90
|
description: 'A tiny test slate',
|
|
91
|
+
docs: [
|
|
92
|
+
{
|
|
93
|
+
name: 'Demo provider docs',
|
|
94
|
+
url: 'https://example.com/docs/provider'
|
|
95
|
+
}
|
|
96
|
+
],
|
|
74
97
|
config: demoConfig,
|
|
75
98
|
auth: demoAuth
|
|
76
99
|
});
|
|
77
100
|
|
|
78
101
|
let echoTool = SlateTool.create(spec, {
|
|
79
102
|
key: 'echo',
|
|
80
|
-
name: 'Echo'
|
|
103
|
+
name: 'Echo',
|
|
104
|
+
docs: [
|
|
105
|
+
{
|
|
106
|
+
type: 'docs.action.general',
|
|
107
|
+
name: 'Echo tool docs',
|
|
108
|
+
url: 'https://example.com/docs/actions/echo'
|
|
109
|
+
}
|
|
110
|
+
]
|
|
81
111
|
})
|
|
82
112
|
.input(
|
|
83
113
|
z.object({
|
|
@@ -97,6 +127,7 @@ let createDemoSlate = () => {
|
|
|
97
127
|
}
|
|
98
128
|
]
|
|
99
129
|
})
|
|
130
|
+
.authMethods(['token_auth'])
|
|
100
131
|
.handleInvocation(async ctx => ({
|
|
101
132
|
output: {
|
|
102
133
|
greeting: `${ctx.config.prefix} ${ctx.input.name}`,
|
|
@@ -170,6 +201,59 @@ let createTraceSlate = (baseUrl: string) => {
|
|
|
170
201
|
});
|
|
171
202
|
};
|
|
172
203
|
|
|
204
|
+
let createTraceErrorSlate = (baseUrl: string) => {
|
|
205
|
+
let config = SlateConfig.create(z.object({}));
|
|
206
|
+
let auth = SlateAuth.create<{}>().output(z.object({}));
|
|
207
|
+
|
|
208
|
+
let spec = SlateSpecification.create({
|
|
209
|
+
key: 'trace-error-slate',
|
|
210
|
+
name: 'Trace Error Slate',
|
|
211
|
+
description: 'A slate that traces failed HTTP requests',
|
|
212
|
+
config,
|
|
213
|
+
auth
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
let traceTool = SlateTool.create(spec, {
|
|
217
|
+
key: 'trace_http_error',
|
|
218
|
+
name: 'Trace HTTP Error'
|
|
219
|
+
})
|
|
220
|
+
.input(z.object({}))
|
|
221
|
+
.output(
|
|
222
|
+
z.object({
|
|
223
|
+
ok: z.boolean()
|
|
224
|
+
})
|
|
225
|
+
)
|
|
226
|
+
.handleInvocation(async () => {
|
|
227
|
+
await axios.post(
|
|
228
|
+
`${baseUrl}/trace-error?client_secret=request-secret`,
|
|
229
|
+
new URLSearchParams({
|
|
230
|
+
grant_type: 'authorization_code',
|
|
231
|
+
client_secret: 'client-secret'
|
|
232
|
+
}).toString(),
|
|
233
|
+
{
|
|
234
|
+
headers: {
|
|
235
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
236
|
+
Authorization: 'Bearer client-token'
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
output: {
|
|
243
|
+
ok: true
|
|
244
|
+
},
|
|
245
|
+
message: 'should not reach'
|
|
246
|
+
};
|
|
247
|
+
})
|
|
248
|
+
.build();
|
|
249
|
+
|
|
250
|
+
return Slate.create({
|
|
251
|
+
spec,
|
|
252
|
+
tools: [traceTool],
|
|
253
|
+
triggers: []
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
173
257
|
let createTriggerTraceSlate = () => {
|
|
174
258
|
let config = SlateConfig.create(z.object({}));
|
|
175
259
|
let auth = SlateAuth.create<{}>().output(z.object({}));
|
|
@@ -229,7 +313,14 @@ let createTriggerTraceSlate = () => {
|
|
|
229
313
|
)
|
|
230
314
|
.webhook({
|
|
231
315
|
handleRequest: async () => ({
|
|
232
|
-
inputs: [{ id: 'webhook-1' }, { id: 'webhook-2' }]
|
|
316
|
+
inputs: [{ id: 'webhook-1' }, { id: 'webhook-2' }],
|
|
317
|
+
response: {
|
|
318
|
+
status: 202,
|
|
319
|
+
headers: {
|
|
320
|
+
'x-trigger-result': 'accepted'
|
|
321
|
+
},
|
|
322
|
+
body: 'accepted'
|
|
323
|
+
}
|
|
233
324
|
}),
|
|
234
325
|
handleEvent: async ctx => ({
|
|
235
326
|
id: ctx.input.id,
|
|
@@ -249,6 +340,180 @@ let createTriggerTraceSlate = () => {
|
|
|
249
340
|
});
|
|
250
341
|
};
|
|
251
342
|
|
|
343
|
+
type OAuthConfig = {
|
|
344
|
+
loginHost: string;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
type OAuthOutput = {
|
|
348
|
+
token: string;
|
|
349
|
+
refreshToken?: string;
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
type SeenOAuthConfig = Partial<
|
|
353
|
+
Record<'authorizationUrl' | 'callback' | 'refresh' | 'profile', OAuthConfig>
|
|
354
|
+
>;
|
|
355
|
+
|
|
356
|
+
let getRequiredOAuthConfig = (ctx: { config?: Record<string, unknown> }): OAuthConfig => {
|
|
357
|
+
expect(ctx.config).toBeDefined();
|
|
358
|
+
return ctx.config as OAuthConfig;
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
let createOauthConfigSlate = (seenConfig: SeenOAuthConfig) => {
|
|
362
|
+
let config = SlateConfig.create(
|
|
363
|
+
z.object({
|
|
364
|
+
loginHost: z.string()
|
|
365
|
+
})
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
let auth = SlateAuth.create<{ token: string; refreshToken?: string }>()
|
|
369
|
+
.output(
|
|
370
|
+
z.object({
|
|
371
|
+
token: z.string(),
|
|
372
|
+
refreshToken: z.string().optional()
|
|
373
|
+
})
|
|
374
|
+
)
|
|
375
|
+
.addOauth({
|
|
376
|
+
type: 'auth.oauth',
|
|
377
|
+
key: 'oauth',
|
|
378
|
+
name: 'OAuth',
|
|
379
|
+
scopes: [],
|
|
380
|
+
inputSchema: z.object({}),
|
|
381
|
+
getAuthorizationUrl: async ctx => {
|
|
382
|
+
let config = getRequiredOAuthConfig(ctx);
|
|
383
|
+
seenConfig.authorizationUrl = config;
|
|
384
|
+
return {
|
|
385
|
+
url: `https://${config.loginHost}/authorize`,
|
|
386
|
+
callbackState: {
|
|
387
|
+
loginHost: config.loginHost
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
},
|
|
391
|
+
handleCallback: async ctx => {
|
|
392
|
+
let config = getRequiredOAuthConfig(ctx);
|
|
393
|
+
seenConfig.callback = config;
|
|
394
|
+
return {
|
|
395
|
+
output: {
|
|
396
|
+
token: `token:${config.loginHost}`,
|
|
397
|
+
refreshToken: 'refresh-token'
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
},
|
|
401
|
+
handleTokenRefresh: async (ctx: {
|
|
402
|
+
output: OAuthOutput;
|
|
403
|
+
config?: Record<string, unknown>;
|
|
404
|
+
}) => {
|
|
405
|
+
let config = getRequiredOAuthConfig(ctx);
|
|
406
|
+
seenConfig.refresh = config;
|
|
407
|
+
return {
|
|
408
|
+
output: {
|
|
409
|
+
...ctx.output,
|
|
410
|
+
token: `refreshed:${config.loginHost}`
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
getProfile: async (ctx: { config?: Record<string, unknown> }) => {
|
|
415
|
+
let config = getRequiredOAuthConfig(ctx);
|
|
416
|
+
seenConfig.profile = config;
|
|
417
|
+
return {
|
|
418
|
+
profile: {
|
|
419
|
+
loginHost: config.loginHost
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
let spec = SlateSpecification.create({
|
|
426
|
+
key: 'oauth-config-slate',
|
|
427
|
+
name: 'OAuth Config Slate',
|
|
428
|
+
description: 'A slate that reads config in OAuth callbacks',
|
|
429
|
+
config,
|
|
430
|
+
auth
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return Slate.create({
|
|
434
|
+
spec,
|
|
435
|
+
tools: [],
|
|
436
|
+
triggers: []
|
|
437
|
+
});
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
type TokenConfig = {
|
|
441
|
+
prefix: string;
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
type SeenTokenConfig = Partial<Record<'output' | 'profile', TokenConfig>>;
|
|
445
|
+
|
|
446
|
+
let getRequiredTokenConfig = (ctx: { config?: Record<string, unknown> }): TokenConfig => {
|
|
447
|
+
expect(ctx.config).toBeDefined();
|
|
448
|
+
return ctx.config as TokenConfig;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
type TokenAuthOutputContext = {
|
|
452
|
+
input: { token: string };
|
|
453
|
+
config?: Record<string, unknown>;
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
type TokenAuthProfileContext = {
|
|
457
|
+
output: { token: string };
|
|
458
|
+
config?: Record<string, unknown>;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
let createTokenConfigSlate = (seenConfig: SeenTokenConfig) => {
|
|
462
|
+
let config = SlateConfig.create(
|
|
463
|
+
z.object({
|
|
464
|
+
prefix: z.string()
|
|
465
|
+
})
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
let auth = SlateAuth.create<{ token: string }>()
|
|
469
|
+
.output(
|
|
470
|
+
z.object({
|
|
471
|
+
token: z.string()
|
|
472
|
+
})
|
|
473
|
+
)
|
|
474
|
+
.addTokenAuth({
|
|
475
|
+
type: 'auth.token',
|
|
476
|
+
key: 'token_auth',
|
|
477
|
+
name: 'Token Auth',
|
|
478
|
+
inputSchema: z.object({
|
|
479
|
+
token: z.string()
|
|
480
|
+
}),
|
|
481
|
+
getOutput: async (ctx: TokenAuthOutputContext) => {
|
|
482
|
+
let currentConfig = getRequiredTokenConfig(ctx);
|
|
483
|
+
seenConfig.output = currentConfig;
|
|
484
|
+
return {
|
|
485
|
+
output: {
|
|
486
|
+
token: `${currentConfig.prefix}:${ctx.input.token}`
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
},
|
|
490
|
+
getProfile: async (ctx: TokenAuthProfileContext) => {
|
|
491
|
+
let currentConfig = getRequiredTokenConfig(ctx);
|
|
492
|
+
seenConfig.profile = currentConfig;
|
|
493
|
+
return {
|
|
494
|
+
profile: {
|
|
495
|
+
prefix: currentConfig.prefix,
|
|
496
|
+
token: ctx.output.token
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
let spec = SlateSpecification.create({
|
|
503
|
+
key: 'token-config-slate',
|
|
504
|
+
name: 'Token Config Slate',
|
|
505
|
+
description: 'A slate that reads config in token auth hooks',
|
|
506
|
+
config,
|
|
507
|
+
auth
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
return Slate.create({
|
|
511
|
+
spec,
|
|
512
|
+
tools: [],
|
|
513
|
+
triggers: []
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
|
|
252
517
|
describe('@slates/client local transport', () => {
|
|
253
518
|
it('discovers auth/config and invokes tools with session state', async () => {
|
|
254
519
|
let slate = createDemoSlate();
|
|
@@ -258,10 +523,23 @@ describe('@slates/client local transport', () => {
|
|
|
258
523
|
|
|
259
524
|
let provider = await client.identify();
|
|
260
525
|
expect(provider.provider.id).toBe('demo-slate');
|
|
526
|
+
expect(provider.docs).toEqual([
|
|
527
|
+
{
|
|
528
|
+
name: 'Demo provider docs',
|
|
529
|
+
url: 'https://example.com/docs/provider'
|
|
530
|
+
}
|
|
531
|
+
]);
|
|
261
532
|
|
|
262
533
|
let actions = await client.listTools();
|
|
263
534
|
expect(actions).toHaveLength(1);
|
|
264
535
|
expect(actions[0]!.id).toBe('echo');
|
|
536
|
+
expect(actions[0]!.docs).toEqual([
|
|
537
|
+
{
|
|
538
|
+
type: 'docs.action.general',
|
|
539
|
+
name: 'Echo tool docs',
|
|
540
|
+
url: 'https://example.com/docs/actions/echo'
|
|
541
|
+
}
|
|
542
|
+
]);
|
|
265
543
|
expect(actions[0]!.scopes).toEqual({
|
|
266
544
|
AND: [
|
|
267
545
|
{
|
|
@@ -269,9 +547,18 @@ describe('@slates/client local transport', () => {
|
|
|
269
547
|
}
|
|
270
548
|
]
|
|
271
549
|
});
|
|
550
|
+
expect(actions[0]!.authMethods).toEqual(['token_auth']);
|
|
551
|
+
expect(actions[0]!.isPublic).toBe(false);
|
|
272
552
|
|
|
273
553
|
let configSchema = await client.getConfigSchema();
|
|
274
554
|
expect(configSchema.schema.properties.prefix.type).toBe('string');
|
|
555
|
+
expect(configSchema.docs).toEqual([
|
|
556
|
+
{
|
|
557
|
+
type: 'docs.config.general',
|
|
558
|
+
name: 'Demo config docs',
|
|
559
|
+
url: 'https://example.com/docs/config'
|
|
560
|
+
}
|
|
561
|
+
]);
|
|
275
562
|
|
|
276
563
|
let defaultConfig = await client.getDefaultConfig();
|
|
277
564
|
expect(defaultConfig.config).toEqual({ prefix: 'Hello' });
|
|
@@ -279,6 +566,13 @@ describe('@slates/client local transport', () => {
|
|
|
279
566
|
let authMethods = await client.listAuthMethods();
|
|
280
567
|
expect(authMethods.authenticationMethods).toHaveLength(1);
|
|
281
568
|
expect(authMethods.authenticationMethods[0]!.type).toBe('auth.token');
|
|
569
|
+
expect(authMethods.authenticationMethods[0]!.docs).toEqual([
|
|
570
|
+
{
|
|
571
|
+
type: 'docs.auth.token',
|
|
572
|
+
name: 'Demo token auth docs',
|
|
573
|
+
url: 'https://example.com/docs/auth/token'
|
|
574
|
+
}
|
|
575
|
+
]);
|
|
282
576
|
|
|
283
577
|
let defaultInput = await client.getDefaultAuthInput('token_auth');
|
|
284
578
|
expect(defaultInput.input).toEqual({ token: 'default-token' });
|
|
@@ -295,6 +589,7 @@ describe('@slates/client local transport', () => {
|
|
|
295
589
|
input: changedInput.input ?? { token: '' }
|
|
296
590
|
});
|
|
297
591
|
expect(authOutput.output).toEqual({ token: 'trimmed-token' });
|
|
592
|
+
expect(authOutput.scopes).toEqual(['scope:trimmed-token']);
|
|
298
593
|
|
|
299
594
|
client.setConfig({ prefix: 'Hi' });
|
|
300
595
|
client.setAuth({
|
|
@@ -409,7 +704,7 @@ describe('@slates/client local transport', () => {
|
|
|
409
704
|
state: null
|
|
410
705
|
});
|
|
411
706
|
|
|
412
|
-
await client.request('slates/action.trigger.webhook_handle', {
|
|
707
|
+
let webhookResult = await client.request('slates/action.trigger.webhook_handle', {
|
|
413
708
|
actionId: 'webhook_trigger',
|
|
414
709
|
url: 'https://example.com/webhook',
|
|
415
710
|
method: 'POST',
|
|
@@ -419,6 +714,16 @@ describe('@slates/client local transport', () => {
|
|
|
419
714
|
body: null,
|
|
420
715
|
state: null
|
|
421
716
|
});
|
|
717
|
+
expect(webhookResult.response).toEqual({
|
|
718
|
+
status: 202,
|
|
719
|
+
headers: {
|
|
720
|
+
'x-trigger-result': 'accepted'
|
|
721
|
+
},
|
|
722
|
+
body: {
|
|
723
|
+
encoding: 'base64',
|
|
724
|
+
content: Buffer.from('accepted').toString('base64')
|
|
725
|
+
}
|
|
726
|
+
});
|
|
422
727
|
|
|
423
728
|
await waitForLogs();
|
|
424
729
|
|
|
@@ -442,6 +747,7 @@ describe('@slates/client local transport', () => {
|
|
|
442
747
|
component: 'action',
|
|
443
748
|
functionName: 'handleRequest',
|
|
444
749
|
inputCount: 2,
|
|
750
|
+
hasResponse: true,
|
|
445
751
|
actionId: 'webhook_trigger'
|
|
446
752
|
})
|
|
447
753
|
})
|
|
@@ -449,6 +755,123 @@ describe('@slates/client local transport', () => {
|
|
|
449
755
|
);
|
|
450
756
|
});
|
|
451
757
|
|
|
758
|
+
it('passes current profile config into OAuth callbacks', async () => {
|
|
759
|
+
let seenConfig: SeenOAuthConfig = {};
|
|
760
|
+
let client = createSlatesClient({
|
|
761
|
+
transport: createLocalSlateTransport({
|
|
762
|
+
slate: createOauthConfigSlate(seenConfig)
|
|
763
|
+
}),
|
|
764
|
+
state: {
|
|
765
|
+
config: {
|
|
766
|
+
loginHost: 'sandbox.example.com'
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
let authorizationUrl = await client.getAuthorizationUrl({
|
|
772
|
+
authenticationMethodId: 'oauth',
|
|
773
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
774
|
+
state: 'state-1',
|
|
775
|
+
input: {},
|
|
776
|
+
clientId: 'client-id',
|
|
777
|
+
clientSecret: 'client-secret',
|
|
778
|
+
scopes: []
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
expect(authorizationUrl.authorizationUrl).toBe('https://sandbox.example.com/authorize');
|
|
782
|
+
expect(authorizationUrl.callbackState).toEqual({
|
|
783
|
+
loginHost: 'sandbox.example.com'
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
let callback = await client.handleAuthorizationCallback({
|
|
787
|
+
authenticationMethodId: 'oauth',
|
|
788
|
+
code: 'code-1',
|
|
789
|
+
state: 'state-1',
|
|
790
|
+
redirectUri: 'http://localhost:3000/callback',
|
|
791
|
+
input: {},
|
|
792
|
+
clientId: 'client-id',
|
|
793
|
+
clientSecret: 'client-secret',
|
|
794
|
+
scopes: [],
|
|
795
|
+
callbackState: authorizationUrl.callbackState
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
expect(callback.output).toEqual({
|
|
799
|
+
token: 'token:sandbox.example.com',
|
|
800
|
+
refreshToken: 'refresh-token'
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
let refreshed = await client.refreshToken({
|
|
804
|
+
authenticationMethodId: 'oauth',
|
|
805
|
+
output: callback.output,
|
|
806
|
+
input: {},
|
|
807
|
+
clientId: 'client-id',
|
|
808
|
+
clientSecret: 'client-secret',
|
|
809
|
+
scopes: []
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
expect(refreshed.output.token).toBe('refreshed:sandbox.example.com');
|
|
813
|
+
|
|
814
|
+
let profile = await client.getAuthProfile({
|
|
815
|
+
authenticationMethodId: 'oauth',
|
|
816
|
+
output: refreshed.output,
|
|
817
|
+
input: {},
|
|
818
|
+
scopes: []
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
expect(profile.profile).toEqual({
|
|
822
|
+
loginHost: 'sandbox.example.com'
|
|
823
|
+
});
|
|
824
|
+
expect(seenConfig).toEqual({
|
|
825
|
+
authorizationUrl: { loginHost: 'sandbox.example.com' },
|
|
826
|
+
callback: { loginHost: 'sandbox.example.com' },
|
|
827
|
+
refresh: { loginHost: 'sandbox.example.com' },
|
|
828
|
+
profile: { loginHost: 'sandbox.example.com' }
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('passes current profile config into token auth callbacks', async () => {
|
|
833
|
+
let seenConfig: SeenTokenConfig = {};
|
|
834
|
+
let client = createSlatesClient({
|
|
835
|
+
transport: createLocalSlateTransport({
|
|
836
|
+
slate: createTokenConfigSlate(seenConfig)
|
|
837
|
+
}),
|
|
838
|
+
state: {
|
|
839
|
+
config: {
|
|
840
|
+
prefix: 'configured'
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
let authOutput = await client.getAuthOutput({
|
|
846
|
+
authenticationMethodId: 'token_auth',
|
|
847
|
+
input: {
|
|
848
|
+
token: 'secret-token'
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
expect(authOutput.output).toEqual({
|
|
853
|
+
token: 'configured:secret-token'
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
let profile = await client.getAuthProfile({
|
|
857
|
+
authenticationMethodId: 'token_auth',
|
|
858
|
+
output: authOutput.output,
|
|
859
|
+
input: {
|
|
860
|
+
token: 'secret-token'
|
|
861
|
+
},
|
|
862
|
+
scopes: []
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
expect(profile.profile).toEqual({
|
|
866
|
+
prefix: 'configured',
|
|
867
|
+
token: 'configured:secret-token'
|
|
868
|
+
});
|
|
869
|
+
expect(seenConfig).toEqual({
|
|
870
|
+
output: { prefix: 'configured' },
|
|
871
|
+
profile: { prefix: 'configured' }
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
|
|
452
875
|
it('throws structured protocol errors for provider responses', async () => {
|
|
453
876
|
let client = createSlatesClient({
|
|
454
877
|
transport: {
|
|
@@ -546,7 +969,7 @@ describe('@slates/client local transport', () => {
|
|
|
546
969
|
expect(trace?.request.url).not.toContain('request-secret');
|
|
547
970
|
expect(trace?.request.headers).toMatchObject({
|
|
548
971
|
accept: 'application/json, text/plain, */*',
|
|
549
|
-
'user-agent': 'slates.dev
|
|
972
|
+
'user-agent': 'slates.dev/1.0.0 trace-slate',
|
|
550
973
|
'x-slates-provider': 'trace-slate'
|
|
551
974
|
});
|
|
552
975
|
expect(trace?.request.headers).not.toHaveProperty('authorization');
|
|
@@ -566,4 +989,166 @@ describe('@slates/client local transport', () => {
|
|
|
566
989
|
});
|
|
567
990
|
}
|
|
568
991
|
});
|
|
992
|
+
|
|
993
|
+
it('returns sanitized request traces on provider errors', async () => {
|
|
994
|
+
let server = createServer((_req, res) => {
|
|
995
|
+
res.statusCode = 400;
|
|
996
|
+
res.setHeader('Content-Type', 'application/json');
|
|
997
|
+
res.end(
|
|
998
|
+
JSON.stringify({
|
|
999
|
+
error: 'invalid_grant',
|
|
1000
|
+
client_secret: 'server-secret'
|
|
1001
|
+
})
|
|
1002
|
+
);
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
await new Promise<void>((resolve, reject) => {
|
|
1006
|
+
server.once('error', reject);
|
|
1007
|
+
server.listen(0, '127.0.0.1', () => resolve());
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
try {
|
|
1011
|
+
let { port } = server.address() as AddressInfo;
|
|
1012
|
+
let client = createSlatesClient({
|
|
1013
|
+
transport: createLocalSlateTransport({
|
|
1014
|
+
slate: createTraceErrorSlate(`http://127.0.0.1:${port}`)
|
|
1015
|
+
}),
|
|
1016
|
+
state: {
|
|
1017
|
+
config: {}
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
let error = await client
|
|
1022
|
+
.invokeTool('trace_http_error', {})
|
|
1023
|
+
.then(() => null)
|
|
1024
|
+
.catch(err => err as SlateProtocolError);
|
|
1025
|
+
|
|
1026
|
+
expect(error).toBeInstanceOf(SlateProtocolError);
|
|
1027
|
+
expect(error?.data.code).toBe('upstream.invalid_request');
|
|
1028
|
+
expect(error?.data.requestTraces).toHaveLength(1);
|
|
1029
|
+
|
|
1030
|
+
let trace = error?.data.requestTraces?.[0] as Record<string, any> | undefined;
|
|
1031
|
+
expect(trace?.request.method).toBe('POST');
|
|
1032
|
+
expect(trace?.request.url).not.toContain('request-secret');
|
|
1033
|
+
expect(trace?.request.headers).not.toHaveProperty('authorization');
|
|
1034
|
+
expect(trace?.request.body?.text).toContain('client_secret=[redacted]');
|
|
1035
|
+
expect(trace?.response?.status).toBe(400);
|
|
1036
|
+
expect(trace?.response?.body?.text).toContain('"client_secret":"[redacted]"');
|
|
1037
|
+
expect(trace?.response?.body?.text).not.toContain('server-secret');
|
|
1038
|
+
} finally {
|
|
1039
|
+
await new Promise<void>((resolve, reject) => {
|
|
1040
|
+
server.close(error => (error ? reject(error) : resolve()));
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
it('lists, gets, and invokes public tools without config or auth', async () => {
|
|
1046
|
+
let config = SlateConfig.create(
|
|
1047
|
+
z.object({
|
|
1048
|
+
prefix: z.string()
|
|
1049
|
+
})
|
|
1050
|
+
);
|
|
1051
|
+
let auth = SlateAuth.create<{ token: string }>()
|
|
1052
|
+
.output(
|
|
1053
|
+
z.object({
|
|
1054
|
+
token: z.string()
|
|
1055
|
+
})
|
|
1056
|
+
)
|
|
1057
|
+
.addTokenAuth({
|
|
1058
|
+
type: 'auth.token',
|
|
1059
|
+
key: 'token_auth',
|
|
1060
|
+
name: 'Token Auth',
|
|
1061
|
+
inputSchema: z.object({
|
|
1062
|
+
token: z.string()
|
|
1063
|
+
}),
|
|
1064
|
+
getOutput: async ctx => ({
|
|
1065
|
+
output: {
|
|
1066
|
+
token: ctx.input.token
|
|
1067
|
+
}
|
|
1068
|
+
})
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
let spec = SlateSpecification.create({
|
|
1072
|
+
key: 'public-tool-slate',
|
|
1073
|
+
name: 'Public Tool Slate',
|
|
1074
|
+
config,
|
|
1075
|
+
auth
|
|
1076
|
+
});
|
|
1077
|
+
|
|
1078
|
+
let echoTool = SlateTool.create(spec, {
|
|
1079
|
+
key: 'echo',
|
|
1080
|
+
name: 'Echo'
|
|
1081
|
+
})
|
|
1082
|
+
.input(
|
|
1083
|
+
z.object({
|
|
1084
|
+
name: z.string()
|
|
1085
|
+
})
|
|
1086
|
+
)
|
|
1087
|
+
.output(
|
|
1088
|
+
z.object({
|
|
1089
|
+
greeting: z.string()
|
|
1090
|
+
})
|
|
1091
|
+
)
|
|
1092
|
+
.handleInvocation(async ctx => ({
|
|
1093
|
+
output: {
|
|
1094
|
+
greeting: `${ctx.config.prefix} ${ctx.input.name}`
|
|
1095
|
+
},
|
|
1096
|
+
message: 'done'
|
|
1097
|
+
}))
|
|
1098
|
+
.build();
|
|
1099
|
+
|
|
1100
|
+
let setupTool = SlatePublicTool.create(spec, {
|
|
1101
|
+
key: 'setup',
|
|
1102
|
+
name: 'Setup'
|
|
1103
|
+
})
|
|
1104
|
+
.input(z.object({}))
|
|
1105
|
+
.output(
|
|
1106
|
+
z.object({
|
|
1107
|
+
docsUrl: z.string()
|
|
1108
|
+
})
|
|
1109
|
+
)
|
|
1110
|
+
.handleInvocation(async ctx => {
|
|
1111
|
+
expect(ctx).not.toHaveProperty('config');
|
|
1112
|
+
expect(ctx).not.toHaveProperty('auth');
|
|
1113
|
+
|
|
1114
|
+
return {
|
|
1115
|
+
output: {
|
|
1116
|
+
docsUrl: 'https://example.com/setup'
|
|
1117
|
+
},
|
|
1118
|
+
message: 'ok'
|
|
1119
|
+
};
|
|
1120
|
+
})
|
|
1121
|
+
.build();
|
|
1122
|
+
|
|
1123
|
+
let client = createSlatesClient({
|
|
1124
|
+
transport: createLocalSlateTransport({
|
|
1125
|
+
slate: Slate.create({
|
|
1126
|
+
spec,
|
|
1127
|
+
tools: [echoTool, setupTool],
|
|
1128
|
+
triggers: []
|
|
1129
|
+
})
|
|
1130
|
+
})
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
let tools = await client.listTools();
|
|
1134
|
+
expect(tools.map(tool => ({ id: tool.id, isPublic: tool.isPublic }))).toEqual([
|
|
1135
|
+
{ id: 'echo', isPublic: false },
|
|
1136
|
+
{ id: 'setup', isPublic: true }
|
|
1137
|
+
]);
|
|
1138
|
+
|
|
1139
|
+
let setup = await client.getTool('setup');
|
|
1140
|
+
expect(setup.isPublic).toBe(true);
|
|
1141
|
+
|
|
1142
|
+
let echo = await client.getTool('echo');
|
|
1143
|
+
expect(echo.isPublic).toBe(false);
|
|
1144
|
+
|
|
1145
|
+
let result = await client.invokeTool('setup', {});
|
|
1146
|
+
expect(result.output).toEqual({
|
|
1147
|
+
docsUrl: 'https://example.com/setup'
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
await expect(client.invokeTool('echo', { name: 'Ada' })).rejects.toMatchObject({
|
|
1151
|
+
message: 'Session context has not been initialized'
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
569
1154
|
});
|