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

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 (74) hide show
  1. package/dist/index.cjs +1702 -2
  2. package/dist/index.d.cts +8 -0
  3. package/dist/index.d.ts +6 -3
  4. package/dist/index.module.js +1685 -2
  5. package/package.json +12 -10
  6. package/src/adminSetup.test.ts +189 -0
  7. package/src/attachments.test.ts +171 -0
  8. package/src/exposure.test.ts +216 -0
  9. package/src/index.ts +1301 -134
  10. package/src/pQueue.test.ts +18 -0
  11. package/src/pQueue.ts +18 -0
  12. package/src/spec.test.ts +232 -0
  13. package/src/spec.ts +219 -19
  14. package/src/validation.ts +1 -1
  15. package/src/webhook.test.ts +65 -0
  16. package/src/webhook.ts +69 -0
  17. package/src/webhookProcess.test.ts +99 -0
  18. package/dist/action/action.d.ts +0 -90
  19. package/dist/action/action.d.ts.map +0 -1
  20. package/dist/action/builder.d.ts +0 -27
  21. package/dist/action/builder.d.ts.map +0 -1
  22. package/dist/action/index.d.ts +0 -5
  23. package/dist/action/index.d.ts.map +0 -1
  24. package/dist/action/tool.d.ts +0 -11
  25. package/dist/action/tool.d.ts.map +0 -1
  26. package/dist/action/trigger.d.ts +0 -14
  27. package/dist/action/trigger.d.ts.map +0 -1
  28. package/dist/auth/auth.d.ts +0 -26
  29. package/dist/auth/auth.d.ts.map +0 -1
  30. package/dist/auth/index.d.ts +0 -3
  31. package/dist/auth/index.d.ts.map +0 -1
  32. package/dist/auth/types.d.ts +0 -148
  33. package/dist/auth/types.d.ts.map +0 -1
  34. package/dist/axios/index.d.ts +0 -4
  35. package/dist/axios/index.d.ts.map +0 -1
  36. package/dist/config/config.d.ts +0 -22
  37. package/dist/config/config.d.ts.map +0 -1
  38. package/dist/config/index.d.ts +0 -2
  39. package/dist/config/index.d.ts.map +0 -1
  40. package/dist/context/context.d.ts +0 -20
  41. package/dist/context/context.d.ts.map +0 -1
  42. package/dist/context/hook.d.ts +0 -4
  43. package/dist/context/hook.d.ts.map +0 -1
  44. package/dist/context/index.d.ts +0 -2
  45. package/dist/context/index.d.ts.map +0 -1
  46. package/dist/error/base.d.ts +0 -5
  47. package/dist/error/base.d.ts.map +0 -1
  48. package/dist/error/declaration.d.ts +0 -6
  49. package/dist/error/declaration.d.ts.map +0 -1
  50. package/dist/error/index.d.ts +0 -3
  51. package/dist/error/index.d.ts.map +0 -1
  52. package/dist/index.cjs.map +0 -1
  53. package/dist/index.d.ts.map +0 -1
  54. package/dist/index.modern.js +0 -2
  55. package/dist/index.modern.js.map +0 -1
  56. package/dist/index.module.js.map +0 -1
  57. package/dist/index.umd.js +0 -2
  58. package/dist/index.umd.js.map +0 -1
  59. package/dist/spec.d.ts +0 -14
  60. package/dist/spec.d.ts.map +0 -1
  61. package/dist/specification/index.d.ts +0 -3
  62. package/dist/specification/index.d.ts.map +0 -1
  63. package/dist/specification/slate.d.ts +0 -15
  64. package/dist/specification/slate.d.ts.map +0 -1
  65. package/dist/specification/specification.d.ts +0 -30
  66. package/dist/specification/specification.d.ts.map +0 -1
  67. package/dist/specification/zero.d.ts +0 -13
  68. package/dist/specification/zero.d.ts.map +0 -1
  69. package/dist/state.d.ts +0 -8
  70. package/dist/state.d.ts.map +0 -1
  71. package/dist/tokens.d.ts +0 -34
  72. package/dist/tokens.d.ts.map +0 -1
  73. package/dist/validation.d.ts +0 -5
  74. 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,232 @@
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
+
226
+ it('omits the adminSetup capability when undeclared so existing spec hashes stay stable', () => {
227
+ expect(
228
+ mapAuthMethod(slate, { ...oauthMethod, adminSetup: {} }).capabilities
229
+ ).toMatchObject({ adminSetup: { enabled: true } });
230
+ expect(mapAuthMethod(slate, oauthMethod).capabilities).not.toHaveProperty('adminSetup');
231
+ });
232
+ });
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
 
@@ -48,15 +66,18 @@ export let mapAuthMethod = <ConfigType extends {}, AuthType extends {}>(
48
66
  handleChangedInput: {
49
67
  enabled: !!m.onInputChanged
50
68
  },
51
- getProfile: { enabled: !!m.getProfile }
52
- }
69
+ getProfile: { enabled: !!m.getProfile },
70
+ ...('adminSetup' in m && m.adminSetup ? { adminSetup: { enabled: true } } : {})
71
+ },
72
+
73
+ docs: m.docs ?? []
53
74
  });
54
75
 
55
76
  export let getAction = <ConfigType extends {}, AuthType extends {}>(
56
77
  slate: Slate<ConfigType, AuthType>,
57
78
  actionId: string
58
79
  ) => {
59
- let action = slate.actions.find(m => m.key == actionId);
80
+ let action = (slate.actions ?? []).find(m => m.key === actionId);
60
81
  if (!action) {
61
82
  throw new ServiceError(notFoundError(`action`, actionId));
62
83
  }
@@ -64,6 +85,26 @@ export let getAction = <ConfigType extends {}, AuthType extends {}>(
64
85
  return action;
65
86
  };
66
87
 
88
+ export let getAdapter = <ConfigType extends {}, AuthType extends {}>(
89
+ slate: Slate<ConfigType, AuthType>,
90
+ adapterId: string
91
+ ) => {
92
+ let adapter = (slate.adapters ?? []).find(m => m.id === adapterId);
93
+ if (!adapter) {
94
+ throw new ServiceError(notFoundError(`adapter`, adapterId));
95
+ }
96
+
97
+ return adapter;
98
+ };
99
+
100
+ export let mapAdapter = <ConfigType extends {}, AuthType extends {}>(
101
+ adapter: SlateAdapter<ConfigType, AuthType>
102
+ ): SlatesAdapter => ({
103
+ id: adapter.id,
104
+ name: adapter.name,
105
+ capabilities: adapter.capabilities ?? []
106
+ });
107
+
67
108
  export let getActionWithType = <
68
109
  Type extends 'tool' | 'trigger',
69
110
  ConfigType extends {},
@@ -74,7 +115,7 @@ export let getActionWithType = <
74
115
  actionId: string
75
116
  ): ReturnType<typeof getAction<ConfigType, AuthType>> & { type: Type } => {
76
117
  let action = getAction(slate, actionId);
77
- if (action.type != type) {
118
+ if (action.type !== type) {
78
119
  throw new ServiceError(
79
120
  badRequestError({
80
121
  message: `Action with ID ${actionId} is not of type ${type}`
@@ -97,16 +138,24 @@ export let mapAction = <ConfigType extends {}, AuthType extends {}>(
97
138
  constraints: a.constraints,
98
139
  tags: a.tags,
99
140
  metadata: a.metadata,
141
+ scopes: a.scopes,
142
+ authMethods: a.authMethods,
143
+ docs: a.docs ?? [],
144
+ ...(a.adapter ? { adapter: a.adapter } : {}),
100
145
 
101
146
  inputSchema: toJsonSchema(a.inputSchema),
102
147
  outputSchema: toJsonSchema(a.outputSchema)
103
148
  };
104
149
 
105
- if (a.type == 'tool') {
150
+ if (a.type === 'tool') {
151
+ let toolCategoryIds = slate.getToolCategoryKeys?.(a.key) ?? [];
152
+
106
153
  return {
107
154
  ...base,
108
155
  type: 'action.tool',
109
- capabilities: {}
156
+ capabilities: {},
157
+ isPublic: a.isPublic,
158
+ ...(toolCategoryIds.length ? { toolCategoryIds } : {})
110
159
  };
111
160
  }
112
161
 
@@ -114,17 +163,168 @@ export let mapAction = <ConfigType extends {}, AuthType extends {}>(
114
163
  ...base,
115
164
  type: 'action.trigger',
116
165
  capabilities: {},
166
+ triggerGroupId: a.triggerGroup.key
167
+ };
168
+ };
117
169
 
118
- invocation:
119
- a.source == 'polling'
120
- ? {
121
- type: 'polling',
122
- intervalSeconds: a.polling.intervalInSeconds ?? SlateDefaultPollingIntervalSeconds
123
- }
170
+ export let isMappableTrigger = <ConfigType extends {}, AuthType extends {}>(
171
+ action: Slate<ConfigType, AuthType>['actions'][number]
172
+ ): boolean => action.type !== 'trigger' || !!action.triggerGroup;
173
+
174
+ export let getMappableAction = <ConfigType extends {}, AuthType extends {}>(
175
+ slate: Slate<ConfigType, AuthType>,
176
+ actionId: string
177
+ ) => {
178
+ let action = getAction(slate, actionId);
179
+ if (!isMappableTrigger(action)) {
180
+ throw new ServiceError(notFoundError(`action`, actionId));
181
+ }
182
+
183
+ return action;
184
+ };
185
+
186
+ export let getExposedActions = <ConfigType extends {}, AuthType extends {}>(
187
+ slate: Slate<ConfigType, AuthType>,
188
+ includeAdapterActions: boolean
189
+ ) =>
190
+ (slate.actions ?? []).filter(
191
+ action => isMappableTrigger(action) && (includeAdapterActions || !action.adapter)
192
+ );
193
+
194
+ export let getExposedAction = <ConfigType extends {}, AuthType extends {}>(
195
+ slate: Slate<ConfigType, AuthType>,
196
+ actionId: string,
197
+ includeAdapterActions: boolean
198
+ ) => {
199
+ let action = getMappableAction(slate, actionId);
200
+ if (action.adapter && !includeAdapterActions) {
201
+ throw new ServiceError(notFoundError(`action`, actionId));
202
+ }
203
+
204
+ return action;
205
+ };
206
+
207
+ export let getTriggerGroup = <ConfigType extends {}, AuthType extends {}>(
208
+ slate: Slate<ConfigType, AuthType>,
209
+ triggerGroupId: string
210
+ ) => {
211
+ let group = (slate.triggerGroups ?? []).find(g => g.key === triggerGroupId);
212
+ if (!group) {
213
+ throw new ServiceError(notFoundError(`trigger_group`, triggerGroupId));
214
+ }
215
+
216
+ return group;
217
+ };
218
+
219
+ export let getTriggersForGroup = <ConfigType extends {}, AuthType extends {}>(
220
+ slate: Slate<ConfigType, AuthType>,
221
+ triggerGroupId: string
222
+ ): SlateTrigger<ConfigType, AuthType, any, any>[] =>
223
+ (slate.actions ?? []).filter(
224
+ (action): action is SlateTrigger<ConfigType, AuthType, any, any> =>
225
+ action.type === 'trigger' && action.triggerGroup?.key === triggerGroupId
226
+ );
227
+
228
+ export let getExposedTriggerGroups = <ConfigType extends {}, AuthType extends {}>(
229
+ slate: Slate<ConfigType, AuthType>,
230
+ includeAdapterActions: boolean
231
+ ) => {
232
+ let triggerGroupIds = new Set(
233
+ getExposedActions(slate, includeAdapterActions).flatMap(action =>
234
+ action.type === 'trigger' && action.triggerGroup ? [action.triggerGroup.key] : []
235
+ )
236
+ );
237
+
238
+ return (slate.triggerGroups ?? []).filter(group => triggerGroupIds.has(group.key));
239
+ };
240
+
241
+ export let evaluateTriggerMatches = <ConfigType extends {}, AuthType extends {}>(
242
+ slate: Slate<ConfigType, AuthType>,
243
+ triggerGroupId: string,
244
+ payload: unknown
245
+ ): string[] =>
246
+ getTriggersForGroup(slate, triggerGroupId)
247
+ .filter(trigger => trigger.matches(payload))
248
+ .map(trigger => trigger.key);
249
+
250
+ export let getWebhookAutoRegistration = <ConfigType extends {}, AuthType extends {}>(
251
+ group: ReturnType<typeof getTriggerGroup<ConfigType, AuthType>>
252
+ ) => {
253
+ if (group.source !== 'webhook' || !group.webhook?.autoRegistration) {
254
+ throw new ServiceError(
255
+ badRequestError({
256
+ message: `Trigger group does not support webhook auto-registration: ${group.key}`
257
+ })
258
+ );
259
+ }
260
+
261
+ return group.webhook.autoRegistration;
262
+ };
263
+
264
+ export let getWebhookManualRegistration = <ConfigType extends {}, AuthType extends {}>(
265
+ group: ReturnType<typeof getTriggerGroup<ConfigType, AuthType>>
266
+ ) => {
267
+ if (group.source !== 'webhook' || !group.webhook?.manualRegistration) {
268
+ throw new ServiceError(
269
+ badRequestError({
270
+ message: `Trigger group does not support manual webhook registration: ${group.key}`
271
+ })
272
+ );
273
+ }
274
+
275
+ return group.webhook.manualRegistration;
276
+ };
277
+
278
+ export let mapToolCategory = (category: SlateToolCategory): SlatesToolCategory => ({
279
+ id: category.key,
280
+ name: category.name,
281
+ description: category.description,
282
+ imageUrl: category.imageUrl
283
+ });
284
+
285
+ // Like trigger groups, a category is only listed when the hub can see one of its tools.
286
+ export let getExposedToolCategories = <ConfigType extends {}, AuthType extends {}>(
287
+ slate: Slate<ConfigType, AuthType>,
288
+ includeAdapterActions: boolean
289
+ ) => {
290
+ let exposedCategoryKeys = new Set(
291
+ getExposedActions(slate, includeAdapterActions)
292
+ .filter(action => action.type === 'tool')
293
+ .flatMap(action => slate.getToolCategoryKeys?.(action.key) ?? [])
294
+ );
295
+
296
+ return (slate.toolCategories ?? []).filter(category =>
297
+ exposedCategoryKeys.has(category.key)
298
+ );
299
+ };
300
+
301
+ export let mapTriggerGroup = <ConfigType extends {}, AuthType extends {}>(
302
+ group: ReturnType<typeof getTriggerGroup<ConfigType, AuthType>>
303
+ ): SlatesTriggerGroup => ({
304
+ id: group.key,
305
+ name: group.name,
306
+ description: group.description,
307
+ metadata: group.metadata,
308
+ invocation:
309
+ group.source === 'polling'
310
+ ? {
311
+ type: 'polling',
312
+ intervalSeconds: group.polling?.intervalSeconds ?? SlateDefaultPollingIntervalSeconds
313
+ }
314
+ : group.source === 'gateway'
315
+ ? { type: 'gateway' }
124
316
  : {
125
317
  type: 'webhook',
126
- autoRegistration: !!a.autoRegisterWebhook,
127
- autoUnregistration: !!a.autoUnregisterWebhook
318
+ registration: group.webhook?.manualRegistration
319
+ ? {
320
+ mode: 'manual',
321
+ userConfigSchema: toJsonSchema(
322
+ group.webhook.manualRegistration.userConfigSchema
323
+ ),
324
+ fullConfigSchema: toJsonSchema(
325
+ group.webhook.manualRegistration.fullConfigSchema
326
+ )
327
+ }
328
+ : { mode: 'auto' }
128
329
  }
129
- };
130
- };
330
+ });
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
+ });