@slates/client 1.0.0-rc.2 → 1.0.0-rc.20

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.
@@ -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
- ).getDefaultConfig(() => ({
34
- prefix: 'Hello'
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@1.0.0/trace-slate',
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
  });