@slates/provider-handler 1.0.0-rc.43 → 1.0.0-rc.44
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 +206 -25
- package/dist/index.module.js +210 -26
- package/package.json +3 -3
- package/src/index.ts +216 -19
- package/src/spec.test.ts +91 -1
- package/src/spec.ts +47 -9
- package/src/validation.ts +2 -1
- package/src/webhookDeliveryAuthenticate.test.ts +179 -0
- package/src/webhookSetup.test.ts +179 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { SLATES_PROTOCOL_VERSION, SlatesProviderProtoHandlerManager } from '@slates/proto';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createProviderHandler } from './index';
|
|
5
|
+
|
|
6
|
+
type Authentication = { routingKeys?: string[]; rejected?: { status: number } };
|
|
7
|
+
|
|
8
|
+
let createManager = async (authenticate: (ctx: any) => Promise<Authentication>) => {
|
|
9
|
+
let contexts: {
|
|
10
|
+
auth: unknown;
|
|
11
|
+
config: unknown;
|
|
12
|
+
deliveryConfig: unknown;
|
|
13
|
+
request: Request;
|
|
14
|
+
}[] = [];
|
|
15
|
+
let autoRegistration = {
|
|
16
|
+
webhookTargetList: async () => ({ targets: [], nextPageToken: null }),
|
|
17
|
+
webhookRegister: async () => ({
|
|
18
|
+
webhookRegistrationIdentifier: 'reg',
|
|
19
|
+
webhookRegistrationPayload: {}
|
|
20
|
+
}),
|
|
21
|
+
webhookUnregister: async () => {}
|
|
22
|
+
};
|
|
23
|
+
let deliveryConfig = {
|
|
24
|
+
userConfigSchema: z.object({}),
|
|
25
|
+
fullConfigSchema: z.object({}),
|
|
26
|
+
setup: async () => ({ webhookSetupDocument: '', partialWebhookRegistrationPayload: {} }),
|
|
27
|
+
finish: async () => ({ webhookRegistrationPayload: {} }),
|
|
28
|
+
authenticate: async (ctx: any) => {
|
|
29
|
+
contexts.push({
|
|
30
|
+
auth: ctx.auth,
|
|
31
|
+
config: ctx.config,
|
|
32
|
+
deliveryConfig: ctx.input.deliveryConfig,
|
|
33
|
+
request: ctx.input.request
|
|
34
|
+
});
|
|
35
|
+
return authenticate(ctx);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
let group = (key: string, webhook: Record<string, unknown>) => ({
|
|
39
|
+
key,
|
|
40
|
+
name: key,
|
|
41
|
+
description: undefined,
|
|
42
|
+
metadata: {},
|
|
43
|
+
source: 'webhook',
|
|
44
|
+
webhook: { process: async () => ({ events: [] }), ...webhook }
|
|
45
|
+
});
|
|
46
|
+
let slate = {
|
|
47
|
+
spec: { key: 'test', name: 'Test' },
|
|
48
|
+
adapters: [],
|
|
49
|
+
actions: [],
|
|
50
|
+
triggerGroups: [
|
|
51
|
+
group('configured', { autoRegistration, deliveryConfig }),
|
|
52
|
+
group('auto', { autoRegistration }),
|
|
53
|
+
group('manual', { manualRegistration: deliveryConfig }),
|
|
54
|
+
{ key: 'poll', name: 'poll', source: 'polling', polling: { intervalSeconds: 600 } }
|
|
55
|
+
]
|
|
56
|
+
};
|
|
57
|
+
let manager = await createProviderHandler(slate as any, []).run();
|
|
58
|
+
|
|
59
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
60
|
+
jsonrpc: '2.0',
|
|
61
|
+
method: 'slates/hello',
|
|
62
|
+
params: { protocol: SLATES_PROTOCOL_VERSION }
|
|
63
|
+
});
|
|
64
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
65
|
+
jsonrpc: '2.0',
|
|
66
|
+
method: 'slates/participant.set',
|
|
67
|
+
params: { participants: [] }
|
|
68
|
+
});
|
|
69
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
70
|
+
jsonrpc: '2.0',
|
|
71
|
+
method: 'slates/hub.capabilities.set',
|
|
72
|
+
params: { capabilities: { triggers: true } }
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
contexts,
|
|
77
|
+
authenticate: (params: Record<string, unknown> = {}) =>
|
|
78
|
+
SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
79
|
+
jsonrpc: '2.0',
|
|
80
|
+
id: 'authenticate',
|
|
81
|
+
method: 'slates/trigger_group.webhook.delivery_authenticate',
|
|
82
|
+
params: {
|
|
83
|
+
triggerGroupId: 'configured',
|
|
84
|
+
url: 'https://hub.example/receive/config?attempt=1',
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { authorization: 'Bearer token', 'content-type': 'application/json' },
|
|
87
|
+
body: { encoding: 'base64', content: btoa('{"message":{}}') },
|
|
88
|
+
deliveryConfig: { audience: 'https://hub.example/receive/config', extra: [1, 2] },
|
|
89
|
+
...params
|
|
90
|
+
}
|
|
91
|
+
} as any)
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
describe('slates/trigger_group.webhook.delivery_authenticate', () => {
|
|
96
|
+
it('hands the received request and the saved config to the handler without credentials', async () => {
|
|
97
|
+
let { authenticate, contexts } = await createManager(async () => ({
|
|
98
|
+
routingKeys: ['mailbox:user@example.com', 'mailbox:other@example.com']
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
expect(await authenticate()).toEqual({
|
|
102
|
+
jsonrpc: '2.0',
|
|
103
|
+
id: 'authenticate',
|
|
104
|
+
result: {
|
|
105
|
+
routingKeys: ['mailbox:user@example.com', 'mailbox:other@example.com'],
|
|
106
|
+
rejected: null
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
let [received] = contexts;
|
|
110
|
+
expect(received).toMatchObject({
|
|
111
|
+
auth: {},
|
|
112
|
+
config: {},
|
|
113
|
+
deliveryConfig: { audience: 'https://hub.example/receive/config', extra: [1, 2] }
|
|
114
|
+
});
|
|
115
|
+
expect(received!.request.url).toBe('https://hub.example/receive/config?attempt=1');
|
|
116
|
+
expect(received!.request.method).toBe('POST');
|
|
117
|
+
expect(Object.fromEntries(received!.request.headers)).toEqual({
|
|
118
|
+
authorization: 'Bearer token',
|
|
119
|
+
'content-type': 'application/json'
|
|
120
|
+
});
|
|
121
|
+
expect(await received!.request.text()).toBe('{"message":{}}');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('reconstructs a request without a body', async () => {
|
|
125
|
+
let { authenticate, contexts } = await createManager(async () => ({ routingKeys: [] }));
|
|
126
|
+
|
|
127
|
+
await authenticate({ method: 'GET', headers: {}, body: null });
|
|
128
|
+
expect(contexts[0]!.request.method).toBe('GET');
|
|
129
|
+
expect(contexts[0]!.request.body).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('acknowledges an authenticated request that addresses no registration', async () => {
|
|
133
|
+
let { authenticate } = await createManager(async () => ({ routingKeys: [] }));
|
|
134
|
+
|
|
135
|
+
expect(await authenticate()).toMatchObject({
|
|
136
|
+
result: { routingKeys: [], rejected: null }
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it.each([
|
|
141
|
+
401, 403, 404, 503
|
|
142
|
+
])('passes a rejection with status %i through without routing keys', async status => {
|
|
143
|
+
let { authenticate } = await createManager(async () => ({ rejected: { status } }));
|
|
144
|
+
|
|
145
|
+
expect(await authenticate()).toMatchObject({
|
|
146
|
+
result: { routingKeys: [], rejected: { status } }
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it.each([
|
|
151
|
+
['a status outside 400-599', { rejected: { status: 200 } }],
|
|
152
|
+
['a fractional status', { rejected: { status: 401.5 } }],
|
|
153
|
+
['no routing keys and no rejection', {}],
|
|
154
|
+
['an empty routing key', { routingKeys: [''] }],
|
|
155
|
+
['a routing key over 512 characters', { routingKeys: ['x'.repeat(513)] }],
|
|
156
|
+
['more than 16 routing keys', { routingKeys: Array(17).fill('key') }]
|
|
157
|
+
])('rejects a handler result with %s', async (_, result) => {
|
|
158
|
+
let { authenticate } = await createManager(async () => result);
|
|
159
|
+
|
|
160
|
+
expect(await authenticate()).toMatchObject({
|
|
161
|
+
error: { message: 'Webhook delivery authentication returned an invalid result' }
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it.each([
|
|
166
|
+
'auto',
|
|
167
|
+
'manual',
|
|
168
|
+
'poll'
|
|
169
|
+
])('rejects the %s group, which has no delivery config', async triggerGroupId => {
|
|
170
|
+
let { authenticate, contexts } = await createManager(async () => ({ routingKeys: [] }));
|
|
171
|
+
|
|
172
|
+
expect(await authenticate({ triggerGroupId })).toMatchObject({
|
|
173
|
+
error: {
|
|
174
|
+
message: `Trigger group does not support webhook delivery authentication: ${triggerGroupId}`
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
expect(contexts).toEqual([]);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { SLATES_PROTOCOL_VERSION, SlatesProviderProtoHandlerManager } from '@slates/proto';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createProviderHandler } from './index';
|
|
5
|
+
|
|
6
|
+
let createManager = async () => {
|
|
7
|
+
let contexts: { operation: string; auth: unknown; config: unknown; input: unknown }[] = [];
|
|
8
|
+
let config = (finish = true) => ({
|
|
9
|
+
userConfigSchema: z.object({ secret: z.string() }),
|
|
10
|
+
fullConfigSchema: z.object({ secret: z.string(), endpoint: z.string() }),
|
|
11
|
+
setup: async (ctx: any) => {
|
|
12
|
+
contexts.push({
|
|
13
|
+
operation: 'setup',
|
|
14
|
+
auth: ctx.auth,
|
|
15
|
+
config: ctx.config,
|
|
16
|
+
input: ctx.input
|
|
17
|
+
});
|
|
18
|
+
return {
|
|
19
|
+
webhookSetupDocument: `Send events to ${ctx.input.webhookUrl}`,
|
|
20
|
+
partialWebhookRegistrationPayload: { endpoint: ctx.input.webhookUrl }
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
...(finish
|
|
24
|
+
? {
|
|
25
|
+
finish: async (ctx: any) => {
|
|
26
|
+
contexts.push({
|
|
27
|
+
operation: 'finish',
|
|
28
|
+
auth: ctx.auth,
|
|
29
|
+
config: ctx.config,
|
|
30
|
+
input: ctx.input
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
webhookRegistrationPayload: {
|
|
34
|
+
...ctx.input.partialWebhookRegistrationPayload,
|
|
35
|
+
...ctx.input.userWebhookRegistrationPayload
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
: {})
|
|
41
|
+
});
|
|
42
|
+
let autoRegistration = {
|
|
43
|
+
webhookTargetList: async () => ({ targets: [], nextPageToken: null }),
|
|
44
|
+
webhookRegister: async () => ({
|
|
45
|
+
webhookRegistrationIdentifier: 'reg',
|
|
46
|
+
webhookRegistrationPayload: {}
|
|
47
|
+
}),
|
|
48
|
+
webhookUnregister: async () => {}
|
|
49
|
+
};
|
|
50
|
+
let group = (key: string, webhook: Record<string, unknown>) => ({
|
|
51
|
+
key,
|
|
52
|
+
name: key,
|
|
53
|
+
description: undefined,
|
|
54
|
+
metadata: {},
|
|
55
|
+
source: 'webhook',
|
|
56
|
+
webhook: { process: async () => ({ events: [] }), ...webhook }
|
|
57
|
+
});
|
|
58
|
+
let slate = {
|
|
59
|
+
spec: { key: 'test', name: 'Test' },
|
|
60
|
+
adapters: [],
|
|
61
|
+
actions: [],
|
|
62
|
+
triggerGroups: [
|
|
63
|
+
group('manual', { manualRegistration: config() }),
|
|
64
|
+
group('manual_without_finish', { manualRegistration: config(false) }),
|
|
65
|
+
group('configured', { autoRegistration, deliveryConfig: config() }),
|
|
66
|
+
group('auto', { autoRegistration }),
|
|
67
|
+
{ key: 'poll', name: 'poll', source: 'polling', polling: { intervalSeconds: 600 } }
|
|
68
|
+
]
|
|
69
|
+
};
|
|
70
|
+
let manager = await createProviderHandler(slate as any, []).run();
|
|
71
|
+
|
|
72
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
73
|
+
jsonrpc: '2.0',
|
|
74
|
+
method: 'slates/hello',
|
|
75
|
+
params: { protocol: SLATES_PROTOCOL_VERSION }
|
|
76
|
+
});
|
|
77
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
78
|
+
jsonrpc: '2.0',
|
|
79
|
+
method: 'slates/participant.set',
|
|
80
|
+
params: { participants: [] }
|
|
81
|
+
});
|
|
82
|
+
await SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
83
|
+
jsonrpc: '2.0',
|
|
84
|
+
method: 'slates/hub.capabilities.set',
|
|
85
|
+
params: { capabilities: { triggers: true } }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
let request = (method: string, params: Record<string, unknown>) =>
|
|
89
|
+
SlatesProviderProtoHandlerManager.handleInput(manager, {
|
|
90
|
+
jsonrpc: '2.0',
|
|
91
|
+
id: method,
|
|
92
|
+
method,
|
|
93
|
+
params
|
|
94
|
+
} as any);
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
contexts,
|
|
98
|
+
setup: (triggerGroupId: string) =>
|
|
99
|
+
request('slates/trigger_group.webhook.manual_setup', {
|
|
100
|
+
triggerGroupId,
|
|
101
|
+
webhookUrl: 'https://receive.example/key'
|
|
102
|
+
}),
|
|
103
|
+
finish: (triggerGroupId: string) =>
|
|
104
|
+
request('slates/trigger_group.webhook.manual_finish', {
|
|
105
|
+
triggerGroupId,
|
|
106
|
+
webhookUrl: 'https://receive.example/key',
|
|
107
|
+
partialWebhookRegistrationPayload: { endpoint: 'https://receive.example/key' },
|
|
108
|
+
userWebhookRegistrationPayload: { secret: 'shared' }
|
|
109
|
+
})
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
describe('slates/trigger_group.webhook.manual_setup and manual_finish', () => {
|
|
114
|
+
it.each([
|
|
115
|
+
'manual',
|
|
116
|
+
'configured'
|
|
117
|
+
])('serve the %s group with unchanged shapes and an empty context', async triggerGroupId => {
|
|
118
|
+
let { setup, finish, contexts } = await createManager();
|
|
119
|
+
|
|
120
|
+
expect(await setup(triggerGroupId)).toMatchObject({
|
|
121
|
+
result: {
|
|
122
|
+
webhookSetupDocument: 'Send events to https://receive.example/key',
|
|
123
|
+
partialWebhookRegistrationPayload: { endpoint: 'https://receive.example/key' }
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
expect(await finish(triggerGroupId)).toMatchObject({
|
|
127
|
+
result: {
|
|
128
|
+
webhookRegistrationPayload: {
|
|
129
|
+
endpoint: 'https://receive.example/key',
|
|
130
|
+
secret: 'shared'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
expect(contexts).toEqual([
|
|
135
|
+
{
|
|
136
|
+
operation: 'setup',
|
|
137
|
+
auth: {},
|
|
138
|
+
config: {},
|
|
139
|
+
input: { webhookUrl: 'https://receive.example/key' }
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
operation: 'finish',
|
|
143
|
+
auth: {},
|
|
144
|
+
config: {},
|
|
145
|
+
input: {
|
|
146
|
+
webhookUrl: 'https://receive.example/key',
|
|
147
|
+
partialWebhookRegistrationPayload: { endpoint: 'https://receive.example/key' },
|
|
148
|
+
userWebhookRegistrationPayload: { secret: 'shared' }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it.each([
|
|
155
|
+
'auto',
|
|
156
|
+
'poll'
|
|
157
|
+
])('reject the %s group, which has no setup to complete', async triggerGroupId => {
|
|
158
|
+
let { setup, finish, contexts } = await createManager();
|
|
159
|
+
|
|
160
|
+
for (let response of [await setup(triggerGroupId), await finish(triggerGroupId)]) {
|
|
161
|
+
expect(response).toMatchObject({
|
|
162
|
+
error: {
|
|
163
|
+
message: `Trigger group does not support manual webhook registration: ${triggerGroupId}`
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
expect(contexts).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('reject finish for a manual group that does not implement it', async () => {
|
|
171
|
+
let { finish } = await createManager();
|
|
172
|
+
|
|
173
|
+
expect(await finish('manual_without_finish')).toMatchObject({
|
|
174
|
+
error: {
|
|
175
|
+
message: 'Trigger group does not support manual webhook finish: manual_without_finish'
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
});
|