@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.
- package/dist/index.cjs +1614 -2
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.module.js +1597 -2
- package/package.json +12 -10
- package/src/attachments.test.ts +171 -0
- package/src/exposure.test.ts +216 -0
- package/src/index.ts +1178 -133
- package/src/pQueue.test.ts +18 -0
- package/src/pQueue.ts +18 -0
- package/src/spec.test.ts +225 -0
- package/src/spec.ts +217 -18
- package/src/validation.ts +1 -1
- package/src/webhook.test.ts +65 -0
- package/src/webhook.ts +69 -0
- package/src/webhookProcess.test.ts +99 -0
- package/dist/action/action.d.ts +0 -90
- package/dist/action/action.d.ts.map +0 -1
- package/dist/action/builder.d.ts +0 -27
- package/dist/action/builder.d.ts.map +0 -1
- package/dist/action/index.d.ts +0 -5
- package/dist/action/index.d.ts.map +0 -1
- package/dist/action/tool.d.ts +0 -11
- package/dist/action/tool.d.ts.map +0 -1
- package/dist/action/trigger.d.ts +0 -14
- package/dist/action/trigger.d.ts.map +0 -1
- package/dist/auth/auth.d.ts +0 -26
- package/dist/auth/auth.d.ts.map +0 -1
- package/dist/auth/index.d.ts +0 -3
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/types.d.ts +0 -148
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/axios/index.d.ts +0 -4
- package/dist/axios/index.d.ts.map +0 -1
- package/dist/config/config.d.ts +0 -22
- package/dist/config/config.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -2
- package/dist/config/index.d.ts.map +0 -1
- package/dist/context/context.d.ts +0 -20
- package/dist/context/context.d.ts.map +0 -1
- package/dist/context/hook.d.ts +0 -4
- package/dist/context/hook.d.ts.map +0 -1
- package/dist/context/index.d.ts +0 -2
- package/dist/context/index.d.ts.map +0 -1
- package/dist/error/base.d.ts +0 -5
- package/dist/error/base.d.ts.map +0 -1
- package/dist/error/declaration.d.ts +0 -6
- package/dist/error/declaration.d.ts.map +0 -1
- package/dist/error/index.d.ts +0 -3
- package/dist/error/index.d.ts.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.modern.js +0 -2
- package/dist/index.modern.js.map +0 -1
- package/dist/index.module.js.map +0 -1
- package/dist/index.umd.js +0 -2
- package/dist/index.umd.js.map +0 -1
- package/dist/spec.d.ts +0 -14
- package/dist/spec.d.ts.map +0 -1
- package/dist/specification/index.d.ts +0 -3
- package/dist/specification/index.d.ts.map +0 -1
- package/dist/specification/slate.d.ts +0 -15
- package/dist/specification/slate.d.ts.map +0 -1
- package/dist/specification/specification.d.ts +0 -30
- package/dist/specification/specification.d.ts.map +0 -1
- package/dist/specification/zero.d.ts +0 -13
- package/dist/specification/zero.d.ts.map +0 -1
- package/dist/state.d.ts +0 -8
- package/dist/state.d.ts.map +0 -1
- package/dist/tokens.d.ts +0 -34
- package/dist/tokens.d.ts.map +0 -1
- package/dist/validation.d.ts +0 -5
- 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);
|
package/src/spec.test.ts
ADDED
|
@@ -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 {
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
127
|
-
|
|
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
|
@@ -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
|
+
});
|