@slates/provider-handler 1.0.0-rc.4 → 1.0.0-rc.40

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.
Files changed (73) hide show
  1. package/dist/index.cjs +1614 -2
  2. package/dist/index.d.cts +8 -0
  3. package/dist/index.d.ts +6 -3
  4. package/dist/index.module.js +1597 -2
  5. package/package.json +12 -10
  6. package/src/attachments.test.ts +171 -0
  7. package/src/exposure.test.ts +216 -0
  8. package/src/index.ts +1178 -133
  9. package/src/pQueue.test.ts +18 -0
  10. package/src/pQueue.ts +18 -0
  11. package/src/spec.test.ts +225 -0
  12. package/src/spec.ts +217 -18
  13. package/src/validation.ts +1 -1
  14. package/src/webhook.test.ts +65 -0
  15. package/src/webhook.ts +69 -0
  16. package/src/webhookProcess.test.ts +99 -0
  17. package/dist/action/action.d.ts +0 -90
  18. package/dist/action/action.d.ts.map +0 -1
  19. package/dist/action/builder.d.ts +0 -27
  20. package/dist/action/builder.d.ts.map +0 -1
  21. package/dist/action/index.d.ts +0 -5
  22. package/dist/action/index.d.ts.map +0 -1
  23. package/dist/action/tool.d.ts +0 -11
  24. package/dist/action/tool.d.ts.map +0 -1
  25. package/dist/action/trigger.d.ts +0 -14
  26. package/dist/action/trigger.d.ts.map +0 -1
  27. package/dist/auth/auth.d.ts +0 -26
  28. package/dist/auth/auth.d.ts.map +0 -1
  29. package/dist/auth/index.d.ts +0 -3
  30. package/dist/auth/index.d.ts.map +0 -1
  31. package/dist/auth/types.d.ts +0 -148
  32. package/dist/auth/types.d.ts.map +0 -1
  33. package/dist/axios/index.d.ts +0 -4
  34. package/dist/axios/index.d.ts.map +0 -1
  35. package/dist/config/config.d.ts +0 -22
  36. package/dist/config/config.d.ts.map +0 -1
  37. package/dist/config/index.d.ts +0 -2
  38. package/dist/config/index.d.ts.map +0 -1
  39. package/dist/context/context.d.ts +0 -20
  40. package/dist/context/context.d.ts.map +0 -1
  41. package/dist/context/hook.d.ts +0 -4
  42. package/dist/context/hook.d.ts.map +0 -1
  43. package/dist/context/index.d.ts +0 -2
  44. package/dist/context/index.d.ts.map +0 -1
  45. package/dist/error/base.d.ts +0 -5
  46. package/dist/error/base.d.ts.map +0 -1
  47. package/dist/error/declaration.d.ts +0 -6
  48. package/dist/error/declaration.d.ts.map +0 -1
  49. package/dist/error/index.d.ts +0 -3
  50. package/dist/error/index.d.ts.map +0 -1
  51. package/dist/index.cjs.map +0 -1
  52. package/dist/index.d.ts.map +0 -1
  53. package/dist/index.modern.js +0 -2
  54. package/dist/index.modern.js.map +0 -1
  55. package/dist/index.module.js.map +0 -1
  56. package/dist/index.umd.js +0 -2
  57. package/dist/index.umd.js.map +0 -1
  58. package/dist/spec.d.ts +0 -14
  59. package/dist/spec.d.ts.map +0 -1
  60. package/dist/specification/index.d.ts +0 -3
  61. package/dist/specification/index.d.ts.map +0 -1
  62. package/dist/specification/slate.d.ts +0 -15
  63. package/dist/specification/slate.d.ts.map +0 -1
  64. package/dist/specification/specification.d.ts +0 -30
  65. package/dist/specification/specification.d.ts.map +0 -1
  66. package/dist/specification/zero.d.ts +0 -13
  67. package/dist/specification/zero.d.ts.map +0 -1
  68. package/dist/state.d.ts +0 -8
  69. package/dist/state.d.ts.map +0 -1
  70. package/dist/tokens.d.ts +0 -34
  71. package/dist/tokens.d.ts.map +0 -1
  72. package/dist/validation.d.ts +0 -5
  73. package/dist/validation.d.ts.map +0 -1
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { resolveDefaultExport } from './pQueue';
3
+
4
+ class Example {
5
+ value: number;
6
+ constructor(value: number) {
7
+ this.value = value;
8
+ }
9
+ }
10
+
11
+ describe('resolveDefaultExport', () => {
12
+ it('unwraps nested CJS default exports from bundlers', () => {
13
+ expect(resolveDefaultExport<typeof Example>({ default: { default: Example } })).toBe(
14
+ Example
15
+ );
16
+ expect(new (resolveDefaultExport<typeof Example>({ default: Example }))(3).value).toBe(3);
17
+ });
18
+ });
package/src/pQueue.ts ADDED
@@ -0,0 +1,18 @@
1
+ import PQueueImport from 'p-queue';
2
+
3
+ export let resolveDefaultExport = <T>(value: unknown): T => {
4
+ let current = value;
5
+
6
+ for (let i = 0; i < 4; i++) {
7
+ if (typeof current === 'function') return current as T;
8
+ if (current && typeof current === 'object' && 'default' in current) {
9
+ current = (current as { default: unknown }).default;
10
+ continue;
11
+ }
12
+ break;
13
+ }
14
+
15
+ throw new TypeError('Module export is not a constructor');
16
+ };
17
+
18
+ export let PQueue = resolveDefaultExport<typeof PQueueImport>(PQueueImport);
@@ -0,0 +1,225 @@
1
+ import { isServiceError } from '@lowerdeck/error';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { z } from 'zod';
4
+ import {
5
+ getExposedAction,
6
+ getExposedActions,
7
+ getExposedToolCategories,
8
+ getExposedTriggerGroups,
9
+ getMappableAction,
10
+ getTriggersForGroup,
11
+ isMappableTrigger,
12
+ mapAction,
13
+ mapAdapter,
14
+ mapAuthMethod,
15
+ mapToolCategory
16
+ } from './spec';
17
+
18
+ let baseAction = {
19
+ key: 'do_thing',
20
+ name: 'Do Thing',
21
+ description: 'desc',
22
+ instructions: undefined,
23
+ constraints: undefined,
24
+ tags: [],
25
+ metadata: {},
26
+ scopes: [],
27
+ authMethods: [],
28
+ docs: [],
29
+ adapter: undefined,
30
+ inputSchema: z.object({}),
31
+ outputSchema: z.object({})
32
+ };
33
+
34
+ let toolAction: any = { ...baseAction, type: 'tool', isPublic: false };
35
+
36
+ let validTriggerGroup = { key: 'my_group', name: 'My Group' };
37
+ let validTrigger: any = {
38
+ ...baseAction,
39
+ type: 'trigger',
40
+ triggerGroup: validTriggerGroup,
41
+ matches: () => true,
42
+ map: () => ({})
43
+ };
44
+
45
+ let incompatibleTrigger: any = { ...baseAction, type: 'trigger', triggerGroup: undefined };
46
+
47
+ describe('isMappableTrigger', () => {
48
+ it('is true for tool actions', () => {
49
+ expect(isMappableTrigger(toolAction)).toBe(true);
50
+ });
51
+
52
+ it('is true for triggers with a trigger group', () => {
53
+ expect(isMappableTrigger(validTrigger)).toBe(true);
54
+ });
55
+
56
+ it('is false for triggers missing a trigger group', () => {
57
+ expect(isMappableTrigger(incompatibleTrigger)).toBe(false);
58
+ });
59
+ });
60
+
61
+ describe('mapAction', () => {
62
+ it('maps a well-formed trigger', () => {
63
+ let mapped = mapAction({} as any, validTrigger);
64
+ expect(mapped).toMatchObject({ type: 'action.trigger', triggerGroupId: 'my_group' });
65
+ });
66
+
67
+ it('includes the tool categories a tool belongs to', () => {
68
+ let slate = { getToolCategoryKeys: () => ['crm', 'reporting'] } as any;
69
+
70
+ expect(mapAction(slate, toolAction)).toMatchObject({
71
+ type: 'action.tool',
72
+ toolCategoryIds: ['crm', 'reporting']
73
+ });
74
+ });
75
+
76
+ it('omits categories for tools without any, including on slates that predate them', () => {
77
+ expect(mapAction({ getToolCategoryKeys: () => [] } as any, toolAction)).not.toHaveProperty(
78
+ 'toolCategoryIds'
79
+ );
80
+ expect(mapAction({} as any, toolAction)).not.toHaveProperty('toolCategoryIds');
81
+ });
82
+ });
83
+
84
+ describe('mapToolCategory', () => {
85
+ it('maps the category key to the protocol id', () => {
86
+ expect(
87
+ mapToolCategory({ key: 'crm', name: 'CRM', description: 'Records' } as any)
88
+ ).toEqual({ id: 'crm', name: 'CRM', description: 'Records', imageUrl: undefined });
89
+ });
90
+
91
+ it('includes the category image when one is set', () => {
92
+ expect(
93
+ mapToolCategory({ key: 'crm', name: 'CRM', imageUrl: 'https://cdn.test/crm.svg' } as any)
94
+ ).toMatchObject({ id: 'crm', imageUrl: 'https://cdn.test/crm.svg' });
95
+ });
96
+ });
97
+
98
+ describe('getTriggersForGroup', () => {
99
+ it('filters out triggers missing a trigger group instead of throwing', () => {
100
+ let slate = { actions: [toolAction, validTrigger, incompatibleTrigger] } as any;
101
+
102
+ expect(getTriggersForGroup(slate, 'my_group')).toEqual([validTrigger]);
103
+ expect(getTriggersForGroup(slate, 'other_group')).toEqual([]);
104
+ });
105
+ });
106
+
107
+ describe('getMappableAction', () => {
108
+ it('returns tool and well-formed trigger actions', () => {
109
+ let slate = { actions: [toolAction, validTrigger] } as any;
110
+
111
+ expect(getMappableAction(slate, 'do_thing')).toBe(toolAction);
112
+ });
113
+
114
+ it('treats a trigger missing its group as not found instead of crashing', () => {
115
+ let slate = { actions: [incompatibleTrigger] } as any;
116
+
117
+ try {
118
+ getMappableAction(slate, 'do_thing');
119
+ expect.unreachable();
120
+ } catch (e) {
121
+ expect(isServiceError(e)).toBe(true);
122
+ expect((e as any).data.status).toBe(404);
123
+ }
124
+ });
125
+ });
126
+
127
+ describe('adapter exposure', () => {
128
+ let providerTool: any = { ...toolAction, key: 'provider_tool' };
129
+ let adapterTool: any = { ...toolAction, key: 'adapter_tool', adapter: 'chat' };
130
+ let providerGroup = { key: 'provider_group', name: 'Provider Group' };
131
+ let adapterGroup = { key: 'adapter_group', name: 'Adapter Group' };
132
+ let emptyGroup = { key: 'empty_group', name: 'Empty Group' };
133
+ let providerTrigger: any = {
134
+ ...validTrigger,
135
+ key: 'provider_trigger',
136
+ triggerGroup: providerGroup
137
+ };
138
+ let adapterTrigger: any = {
139
+ ...validTrigger,
140
+ key: 'adapter_trigger',
141
+ adapter: 'chat',
142
+ triggerGroup: adapterGroup
143
+ };
144
+ let slate = {
145
+ actions: [providerTool, adapterTool, providerTrigger, adapterTrigger],
146
+ triggerGroups: [providerGroup, adapterGroup, emptyGroup]
147
+ } as any;
148
+
149
+ it('only exposes adapter actions when adapters are enabled', () => {
150
+ expect(getExposedActions(slate, false)).toEqual([providerTool, providerTrigger]);
151
+ expect(getExposedActions(slate, true)).toEqual([
152
+ providerTool,
153
+ adapterTool,
154
+ providerTrigger,
155
+ adapterTrigger
156
+ ]);
157
+ });
158
+
159
+ it('does not expose an adapter action directly when adapters are disabled', () => {
160
+ expect(() => getExposedAction(slate, 'adapter_tool', false)).toThrow();
161
+ expect(getExposedAction(slate, 'adapter_tool', true)).toBe(adapterTool);
162
+ });
163
+
164
+ it('only exposes tool categories that contain an exposed tool', () => {
165
+ let providerCategory = { key: 'provider_category', name: 'Provider' };
166
+ let adapterCategory = { key: 'adapter_category', name: 'Adapter' };
167
+ let emptyCategory = { key: 'empty_category', name: 'Empty' };
168
+ let categorizedSlate = {
169
+ ...slate,
170
+ toolCategories: [providerCategory, adapterCategory, emptyCategory],
171
+ getToolCategoryKeys: (key: string) =>
172
+ key === 'provider_tool'
173
+ ? ['provider_category']
174
+ : key === 'adapter_tool'
175
+ ? ['adapter_category']
176
+ : []
177
+ } as any;
178
+
179
+ expect(getExposedToolCategories(categorizedSlate, false)).toEqual([providerCategory]);
180
+ expect(getExposedToolCategories(categorizedSlate, true)).toEqual([
181
+ providerCategory,
182
+ adapterCategory
183
+ ]);
184
+ expect(getExposedToolCategories(slate, true)).toEqual([]);
185
+ });
186
+
187
+ it('only exposes trigger groups that contain an exposed trigger', () => {
188
+ expect(getExposedTriggerGroups(slate, false)).toEqual([providerGroup]);
189
+ expect(getExposedTriggerGroups(slate, true)).toEqual([providerGroup, adapterGroup]);
190
+ });
191
+
192
+ it('treats missing adapters and trigger groups as empty collections', () => {
193
+ let legacySlate = { actions: [providerTool, providerTrigger] } as any;
194
+
195
+ expect(getExposedActions(legacySlate, true)).toEqual([providerTool, providerTrigger]);
196
+ expect(getExposedTriggerGroups(legacySlate, true)).toEqual([]);
197
+ expect(mapAdapter({ id: 'chat', name: 'Chat' } as any)).toEqual({
198
+ id: 'chat',
199
+ name: 'Chat',
200
+ capabilities: []
201
+ });
202
+ });
203
+ });
204
+
205
+ describe('mapAuthMethod', () => {
206
+ let slate: any = { spec: { auth: { outputSchema: z.object({ token: z.string() }) } } };
207
+ let oauthMethod: any = {
208
+ type: 'auth.oauth',
209
+ key: 'oauth',
210
+ name: 'OAuth',
211
+ scopes: [],
212
+ getAuthorizationUrl: async () => ({ url: '' }),
213
+ handleCallback: async () => ({ output: { token: '' } })
214
+ };
215
+
216
+ it('emits syncTokensAcrossConnections only when the method opts in', () => {
217
+ expect(
218
+ mapAuthMethod(slate, { ...oauthMethod, syncTokensAcrossConnections: true })
219
+ ).toMatchObject({ syncTokensAcrossConnections: true });
220
+
221
+ for (let method of [oauthMethod, { ...oauthMethod, syncTokensAcrossConnections: false }]) {
222
+ expect(mapAuthMethod(slate, method)).not.toHaveProperty('syncTokensAcrossConnections');
223
+ }
224
+ });
225
+ });
package/src/spec.ts CHANGED
@@ -1,6 +1,18 @@
1
1
  import { badRequestError, notFoundError, ServiceError } from '@lowerdeck/error';
2
- import { SlateAuthenticationMethod, SlatesAction } from '@slates/proto';
3
- import { Slate, SlateDefaultPollingIntervalSeconds } from '@slates/provider';
2
+ import type {
3
+ SlateAuthenticationMethod,
4
+ SlatesAction,
5
+ SlateAdapter as SlatesAdapter,
6
+ SlatesToolCategory,
7
+ SlatesTriggerGroup
8
+ } from '@slates/proto';
9
+ import {
10
+ type Slate,
11
+ type SlateAdapter,
12
+ SlateDefaultPollingIntervalSeconds,
13
+ type SlateToolCategory,
14
+ type SlateTrigger
15
+ } from '@slates/provider';
4
16
  import z from 'zod';
5
17
  import { toJsonSchema } from './validation';
6
18
 
@@ -8,7 +20,7 @@ export let getAuthMethod = <ConfigType extends {}, AuthType extends {}>(
8
20
  slate: Slate<ConfigType, AuthType>,
9
21
  authenticationMethodId: string
10
22
  ) => {
11
- let authMethod = slate.spec.auth.authStack.find(m => m.key == authenticationMethodId);
23
+ let authMethod = slate.spec.auth.authStack.find(m => m.key === authenticationMethodId);
12
24
  if (!authMethod) {
13
25
  throw new ServiceError(
14
26
  badRequestError({
@@ -37,6 +49,12 @@ export let mapAuthMethod = <ConfigType extends {}, AuthType extends {}>(
37
49
  }))
38
50
  : undefined,
39
51
 
52
+ adapters: 'adapters' in m ? m.adapters : undefined,
53
+
54
+ ...('syncTokensAcrossConnections' in m && m.syncTokensAcrossConnections
55
+ ? { syncTokensAcrossConnections: true }
56
+ : {}),
57
+
40
58
  inputSchema: toJsonSchema(m.inputSchema ?? z.object({})),
41
59
  outputSchema: toJsonSchema(slate.spec.auth.outputSchema),
42
60
 
@@ -49,14 +67,16 @@ export let mapAuthMethod = <ConfigType extends {}, AuthType extends {}>(
49
67
  enabled: !!m.onInputChanged
50
68
  },
51
69
  getProfile: { enabled: !!m.getProfile }
52
- }
70
+ },
71
+
72
+ docs: m.docs ?? []
53
73
  });
54
74
 
55
75
  export let getAction = <ConfigType extends {}, AuthType extends {}>(
56
76
  slate: Slate<ConfigType, AuthType>,
57
77
  actionId: string
58
78
  ) => {
59
- let action = slate.actions.find(m => m.key == actionId);
79
+ let action = (slate.actions ?? []).find(m => m.key === actionId);
60
80
  if (!action) {
61
81
  throw new ServiceError(notFoundError(`action`, actionId));
62
82
  }
@@ -64,6 +84,26 @@ export let getAction = <ConfigType extends {}, AuthType extends {}>(
64
84
  return action;
65
85
  };
66
86
 
87
+ export let getAdapter = <ConfigType extends {}, AuthType extends {}>(
88
+ slate: Slate<ConfigType, AuthType>,
89
+ adapterId: string
90
+ ) => {
91
+ let adapter = (slate.adapters ?? []).find(m => m.id === adapterId);
92
+ if (!adapter) {
93
+ throw new ServiceError(notFoundError(`adapter`, adapterId));
94
+ }
95
+
96
+ return adapter;
97
+ };
98
+
99
+ export let mapAdapter = <ConfigType extends {}, AuthType extends {}>(
100
+ adapter: SlateAdapter<ConfigType, AuthType>
101
+ ): SlatesAdapter => ({
102
+ id: adapter.id,
103
+ name: adapter.name,
104
+ capabilities: adapter.capabilities ?? []
105
+ });
106
+
67
107
  export let getActionWithType = <
68
108
  Type extends 'tool' | 'trigger',
69
109
  ConfigType extends {},
@@ -74,7 +114,7 @@ export let getActionWithType = <
74
114
  actionId: string
75
115
  ): ReturnType<typeof getAction<ConfigType, AuthType>> & { type: Type } => {
76
116
  let action = getAction(slate, actionId);
77
- if (action.type != type) {
117
+ if (action.type !== type) {
78
118
  throw new ServiceError(
79
119
  badRequestError({
80
120
  message: `Action with ID ${actionId} is not of type ${type}`
@@ -97,16 +137,24 @@ export let mapAction = <ConfigType extends {}, AuthType extends {}>(
97
137
  constraints: a.constraints,
98
138
  tags: a.tags,
99
139
  metadata: a.metadata,
140
+ scopes: a.scopes,
141
+ authMethods: a.authMethods,
142
+ docs: a.docs ?? [],
143
+ ...(a.adapter ? { adapter: a.adapter } : {}),
100
144
 
101
145
  inputSchema: toJsonSchema(a.inputSchema),
102
146
  outputSchema: toJsonSchema(a.outputSchema)
103
147
  };
104
148
 
105
- if (a.type == 'tool') {
149
+ if (a.type === 'tool') {
150
+ let toolCategoryIds = slate.getToolCategoryKeys?.(a.key) ?? [];
151
+
106
152
  return {
107
153
  ...base,
108
154
  type: 'action.tool',
109
- capabilities: {}
155
+ capabilities: {},
156
+ isPublic: a.isPublic,
157
+ ...(toolCategoryIds.length ? { toolCategoryIds } : {})
110
158
  };
111
159
  }
112
160
 
@@ -114,17 +162,168 @@ export let mapAction = <ConfigType extends {}, AuthType extends {}>(
114
162
  ...base,
115
163
  type: 'action.trigger',
116
164
  capabilities: {},
165
+ triggerGroupId: a.triggerGroup.key
166
+ };
167
+ };
117
168
 
118
- invocation:
119
- a.source == 'polling'
120
- ? {
121
- type: 'polling',
122
- intervalSeconds: a.polling.intervalInSeconds ?? SlateDefaultPollingIntervalSeconds
123
- }
169
+ export let isMappableTrigger = <ConfigType extends {}, AuthType extends {}>(
170
+ action: Slate<ConfigType, AuthType>['actions'][number]
171
+ ): boolean => action.type !== 'trigger' || !!action.triggerGroup;
172
+
173
+ export let getMappableAction = <ConfigType extends {}, AuthType extends {}>(
174
+ slate: Slate<ConfigType, AuthType>,
175
+ actionId: string
176
+ ) => {
177
+ let action = getAction(slate, actionId);
178
+ if (!isMappableTrigger(action)) {
179
+ throw new ServiceError(notFoundError(`action`, actionId));
180
+ }
181
+
182
+ return action;
183
+ };
184
+
185
+ export let getExposedActions = <ConfigType extends {}, AuthType extends {}>(
186
+ slate: Slate<ConfigType, AuthType>,
187
+ includeAdapterActions: boolean
188
+ ) =>
189
+ (slate.actions ?? []).filter(
190
+ action => isMappableTrigger(action) && (includeAdapterActions || !action.adapter)
191
+ );
192
+
193
+ export let getExposedAction = <ConfigType extends {}, AuthType extends {}>(
194
+ slate: Slate<ConfigType, AuthType>,
195
+ actionId: string,
196
+ includeAdapterActions: boolean
197
+ ) => {
198
+ let action = getMappableAction(slate, actionId);
199
+ if (action.adapter && !includeAdapterActions) {
200
+ throw new ServiceError(notFoundError(`action`, actionId));
201
+ }
202
+
203
+ return action;
204
+ };
205
+
206
+ export let getTriggerGroup = <ConfigType extends {}, AuthType extends {}>(
207
+ slate: Slate<ConfigType, AuthType>,
208
+ triggerGroupId: string
209
+ ) => {
210
+ let group = (slate.triggerGroups ?? []).find(g => g.key === triggerGroupId);
211
+ if (!group) {
212
+ throw new ServiceError(notFoundError(`trigger_group`, triggerGroupId));
213
+ }
214
+
215
+ return group;
216
+ };
217
+
218
+ export let getTriggersForGroup = <ConfigType extends {}, AuthType extends {}>(
219
+ slate: Slate<ConfigType, AuthType>,
220
+ triggerGroupId: string
221
+ ): SlateTrigger<ConfigType, AuthType, any, any>[] =>
222
+ (slate.actions ?? []).filter(
223
+ (action): action is SlateTrigger<ConfigType, AuthType, any, any> =>
224
+ action.type === 'trigger' && action.triggerGroup?.key === triggerGroupId
225
+ );
226
+
227
+ export let getExposedTriggerGroups = <ConfigType extends {}, AuthType extends {}>(
228
+ slate: Slate<ConfigType, AuthType>,
229
+ includeAdapterActions: boolean
230
+ ) => {
231
+ let triggerGroupIds = new Set(
232
+ getExposedActions(slate, includeAdapterActions).flatMap(action =>
233
+ action.type === 'trigger' && action.triggerGroup ? [action.triggerGroup.key] : []
234
+ )
235
+ );
236
+
237
+ return (slate.triggerGroups ?? []).filter(group => triggerGroupIds.has(group.key));
238
+ };
239
+
240
+ export let evaluateTriggerMatches = <ConfigType extends {}, AuthType extends {}>(
241
+ slate: Slate<ConfigType, AuthType>,
242
+ triggerGroupId: string,
243
+ payload: unknown
244
+ ): string[] =>
245
+ getTriggersForGroup(slate, triggerGroupId)
246
+ .filter(trigger => trigger.matches(payload))
247
+ .map(trigger => trigger.key);
248
+
249
+ export let getWebhookAutoRegistration = <ConfigType extends {}, AuthType extends {}>(
250
+ group: ReturnType<typeof getTriggerGroup<ConfigType, AuthType>>
251
+ ) => {
252
+ if (group.source !== 'webhook' || !group.webhook?.autoRegistration) {
253
+ throw new ServiceError(
254
+ badRequestError({
255
+ message: `Trigger group does not support webhook auto-registration: ${group.key}`
256
+ })
257
+ );
258
+ }
259
+
260
+ return group.webhook.autoRegistration;
261
+ };
262
+
263
+ export let getWebhookManualRegistration = <ConfigType extends {}, AuthType extends {}>(
264
+ group: ReturnType<typeof getTriggerGroup<ConfigType, AuthType>>
265
+ ) => {
266
+ if (group.source !== 'webhook' || !group.webhook?.manualRegistration) {
267
+ throw new ServiceError(
268
+ badRequestError({
269
+ message: `Trigger group does not support manual webhook registration: ${group.key}`
270
+ })
271
+ );
272
+ }
273
+
274
+ return group.webhook.manualRegistration;
275
+ };
276
+
277
+ export let mapToolCategory = (category: SlateToolCategory): SlatesToolCategory => ({
278
+ id: category.key,
279
+ name: category.name,
280
+ description: category.description,
281
+ imageUrl: category.imageUrl
282
+ });
283
+
284
+ // Like trigger groups, a category is only listed when the hub can see one of its tools.
285
+ export let getExposedToolCategories = <ConfigType extends {}, AuthType extends {}>(
286
+ slate: Slate<ConfigType, AuthType>,
287
+ includeAdapterActions: boolean
288
+ ) => {
289
+ let exposedCategoryKeys = new Set(
290
+ getExposedActions(slate, includeAdapterActions)
291
+ .filter(action => action.type === 'tool')
292
+ .flatMap(action => slate.getToolCategoryKeys?.(action.key) ?? [])
293
+ );
294
+
295
+ return (slate.toolCategories ?? []).filter(category =>
296
+ exposedCategoryKeys.has(category.key)
297
+ );
298
+ };
299
+
300
+ export let mapTriggerGroup = <ConfigType extends {}, AuthType extends {}>(
301
+ group: ReturnType<typeof getTriggerGroup<ConfigType, AuthType>>
302
+ ): SlatesTriggerGroup => ({
303
+ id: group.key,
304
+ name: group.name,
305
+ description: group.description,
306
+ metadata: group.metadata,
307
+ invocation:
308
+ group.source === 'polling'
309
+ ? {
310
+ type: 'polling',
311
+ intervalSeconds: group.polling?.intervalSeconds ?? SlateDefaultPollingIntervalSeconds
312
+ }
313
+ : group.source === 'gateway'
314
+ ? { type: 'gateway' }
124
315
  : {
125
316
  type: 'webhook',
126
- autoRegistration: !!a.autoRegisterWebhook,
127
- autoUnregistration: !!a.autoUnregisterWebhook
317
+ registration: group.webhook?.manualRegistration
318
+ ? {
319
+ mode: 'manual',
320
+ userConfigSchema: toJsonSchema(
321
+ group.webhook.manualRegistration.userConfigSchema
322
+ ),
323
+ fullConfigSchema: toJsonSchema(
324
+ group.webhook.manualRegistration.fullConfigSchema
325
+ )
326
+ }
327
+ : { mode: 'auto' }
128
328
  }
129
- };
130
- };
329
+ });
package/src/validation.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ServiceError, validationError } from '@lowerdeck/error';
2
- import z from 'zod';
2
+ import type z from 'zod';
3
3
 
4
4
  export let zodToValidationError = (entity: string, message: string, e: z.ZodError) => {
5
5
  return validationError({
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ SLATE_WEBHOOK_RESPONSE_MAX_BODY_BYTES,
4
+ serializeWebhookHttpResponse
5
+ } from './webhook';
6
+
7
+ describe('serializeWebhookHttpResponse', () => {
8
+ it('serializes a Response with status, headers, and body', async () => {
9
+ await expect(
10
+ serializeWebhookHttpResponse(
11
+ new Response('created', {
12
+ status: 201,
13
+ headers: {
14
+ 'content-type': 'text/plain',
15
+ 'x-webhook-result': 'accepted'
16
+ }
17
+ })
18
+ )
19
+ ).resolves.toEqual({
20
+ status: 201,
21
+ headers: {
22
+ 'content-type': 'text/plain',
23
+ 'x-webhook-result': 'accepted'
24
+ },
25
+ body: {
26
+ encoding: 'base64',
27
+ content: Buffer.from('created').toString('base64')
28
+ }
29
+ });
30
+ });
31
+
32
+ it('applies defaults to a plain response init', async () => {
33
+ await expect(serializeWebhookHttpResponse({ body: 'ok' })).resolves.toEqual({
34
+ status: 200,
35
+ headers: {},
36
+ body: {
37
+ encoding: 'base64',
38
+ content: Buffer.from('ok').toString('base64')
39
+ }
40
+ });
41
+ });
42
+
43
+ it('serializes binary response bodies without changing their bytes', async () => {
44
+ await expect(
45
+ serializeWebhookHttpResponse({
46
+ status: 202,
47
+ body: new Uint8Array([0, 127, 128, 255])
48
+ })
49
+ ).resolves.toMatchObject({
50
+ status: 202,
51
+ body: {
52
+ encoding: 'base64',
53
+ content: Buffer.from([0, 127, 128, 255]).toString('base64')
54
+ }
55
+ });
56
+ });
57
+
58
+ it('rejects bodies larger than one MiB', async () => {
59
+ await expect(
60
+ serializeWebhookHttpResponse({
61
+ body: new Uint8Array(SLATE_WEBHOOK_RESPONSE_MAX_BODY_BYTES + 1)
62
+ })
63
+ ).rejects.toThrow('Webhook response body exceeds');
64
+ });
65
+ });