@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/src/index.ts CHANGED
@@ -9,8 +9,12 @@ import {
9
9
  SLATES_PROTOCOL_VERSION,
10
10
  type SlatesMessageAuthAdminCallbackHandleRequest,
11
11
  type SlatesMessageAuthAuthorizationUrlGetRequest,
12
+ type SlatesMessageTriggerGroupWebhookProcessRequest,
12
13
  type SlatesParticipant,
13
- type SlatesRuntimeInfo
14
+ type SlatesRuntimeInfo,
15
+ slatesTriggerGroupWebhookDeliveryAuthentication,
16
+ slatesTriggerGroupWebhookExpansionResult,
17
+ slatesTriggerGroupWebhookRegistrationResult
14
18
  } from '@slates/proto';
15
19
  import {
16
20
  AuthConfigSecretRedactor,
@@ -24,6 +28,7 @@ import {
24
28
  type SlateLogListener,
25
29
  SlatePublicContext,
26
30
  type SlateRuntimeInfo,
31
+ type SlateTriggerGroup,
27
32
  uploadAttachmentDirect
28
33
  } from '@slates/provider';
29
34
  import { PQueue } from './pQueue';
@@ -38,6 +43,7 @@ import {
38
43
  getExposedTriggerGroups,
39
44
  getTriggerGroup,
40
45
  getWebhookAutoRegistration,
46
+ getWebhookDeliveryConfig,
41
47
  getWebhookManualRegistration,
42
48
  isMappableTrigger,
43
49
  mapAction,
@@ -52,6 +58,39 @@ import { serializeWebhookHttpResponse } from './webhook';
52
58
 
53
59
  let DEFAULT_MAX_ATTACHMENT_SIZE_BYTES = 100_000_000;
54
60
 
61
+ let validateTriggerGroupSelection = (
62
+ group: SlateTriggerGroup<any, any>,
63
+ selection: Record<string, any> | undefined
64
+ ) =>
65
+ group.selectionSchema
66
+ ? validate(
67
+ group.selectionSchema,
68
+ selection === undefined ? {} : selection,
69
+ 'trigger_group.selection',
70
+ `Invalid selection for trigger group "${group.key}"`
71
+ )
72
+ : (selection ?? {});
73
+
74
+ let validateWebhookRegistration = (result: unknown) =>
75
+ validate(
76
+ slatesTriggerGroupWebhookRegistrationResult,
77
+ result,
78
+ 'trigger_group.webhook.registration',
79
+ 'Webhook registration returned an invalid result'
80
+ );
81
+
82
+ let toWebhookRequest = (
83
+ params: Pick<
84
+ SlatesMessageTriggerGroupWebhookProcessRequest['params'],
85
+ 'url' | 'method' | 'headers' | 'body'
86
+ >
87
+ ) =>
88
+ new Request(params.url, {
89
+ method: params.method,
90
+ headers: params.headers,
91
+ body: params.body ? Uint8Array.from(atob(params.body.content), c => c.charCodeAt(0)) : null
92
+ });
93
+
55
94
  let routeAttachmentsThroughDirectUpload = async (
56
95
  attachments: SlateAttachment[] | undefined,
57
96
  live: SlateLiveInvocationInfo | null
@@ -1278,7 +1317,10 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
1278
1317
 
1279
1318
  let context = createContext(
1280
1319
  ctx.config,
1281
- { pageToken: params.pageToken ?? null },
1320
+ {
1321
+ pageToken: params.pageToken ?? null,
1322
+ selection: validateTriggerGroupSelection(group, params.selection)
1323
+ },
1282
1324
  ctx.auth?.output!
1283
1325
  );
1284
1326
  let res = await traceProviderCall(
@@ -1318,7 +1360,10 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
1318
1360
  {
1319
1361
  webhookTargetIdentifier: params.webhookTargetIdentifier,
1320
1362
  webhookTargetPayload: params.webhookTargetPayload,
1321
- webhookUrl: params.webhookUrl
1363
+ webhookUrl: params.webhookUrl,
1364
+ operationId: params.operationId,
1365
+ selection: validateTriggerGroupSelection(group, params.selection),
1366
+ deliveryConfig: params.deliveryConfig
1322
1367
  },
1323
1368
  ctx.auth?.output!
1324
1369
  );
@@ -1335,13 +1380,58 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
1335
1380
  webhookTargetIdentifier: params.webhookTargetIdentifier
1336
1381
  }
1337
1382
  },
1338
- () => runWithContext(context, () => autoRegistration.webhookRegister(context))
1383
+ () =>
1384
+ runWithContext(context, async () =>
1385
+ validateWebhookRegistration(await autoRegistration.webhookRegister(context))
1386
+ )
1339
1387
  );
1340
1388
 
1341
- return withRequestTraces(context, {
1342
- webhookRegistrationIdentifier: res.webhookRegistrationIdentifier,
1343
- webhookRegistrationPayload: res.webhookRegistrationPayload
1344
- });
1389
+ return withRequestTraces(context, res);
1390
+ });
1391
+
1392
+ manager.onRequest('slates/trigger_group.webhook.renew', async ({ params }) => {
1393
+ let ctx = getContextFull();
1394
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1395
+ let autoRegistration = getWebhookAutoRegistration(group);
1396
+ let renew = autoRegistration.webhookRenew;
1397
+ if (!renew) {
1398
+ throw new ServiceError(
1399
+ badRequestError({
1400
+ message: `Trigger group does not support webhook renewal: ${params.triggerGroupId}`
1401
+ })
1402
+ );
1403
+ }
1404
+
1405
+ let context = createContext(
1406
+ ctx.config,
1407
+ {
1408
+ webhookRegistrationIdentifier: params.webhookRegistrationIdentifier,
1409
+ webhookRegistrationPayload: params.webhookRegistrationPayload,
1410
+ webhookUrl: params.webhookUrl,
1411
+ webhookTargetIdentifier: params.webhookTargetIdentifier,
1412
+ webhookTargetPayload: params.webhookTargetPayload,
1413
+ operationId: params.operationId,
1414
+ selection: validateTriggerGroupSelection(group, params.selection),
1415
+ deliveryConfig: params.deliveryConfig
1416
+ },
1417
+ ctx.auth?.output!
1418
+ );
1419
+ let res = await traceProviderCall(
1420
+ {
1421
+ component: 'action',
1422
+ functionName: 'webhookRenew',
1423
+ message: `Renewing webhook for trigger group ${formatEntityLabel(group.name, group.key)}`,
1424
+ successMessage: `Renewed webhook for trigger group ${formatEntityLabel(group.name, group.key)}`,
1425
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while renewing a webhook`,
1426
+ metadata: { triggerGroupId: group.key, triggerGroupName: group.name }
1427
+ },
1428
+ () =>
1429
+ runWithContext(context, async () =>
1430
+ validateWebhookRegistration(await renew(context))
1431
+ )
1432
+ );
1433
+
1434
+ return withRequestTraces(context, res);
1345
1435
  });
1346
1436
 
1347
1437
  manager.onRequest('slates/trigger_group.webhook.unregister', async ({ params }) => {
@@ -1455,18 +1545,10 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
1455
1545
  );
1456
1546
  }
1457
1547
 
1458
- let req = new Request(params.url, {
1459
- method: params.method,
1460
- headers: params.headers,
1461
- body: params.body
1462
- ? Uint8Array.from(atob(params.body.content), c => c.charCodeAt(0))
1463
- : null
1464
- });
1465
-
1466
1548
  let context = createContext(
1467
1549
  {},
1468
1550
  {
1469
- request: req,
1551
+ request: toWebhookRequest(params),
1470
1552
  webhookRegistrationPayload: params.webhookRegistrationPayload
1471
1553
  },
1472
1554
  {}
@@ -1514,13 +1596,121 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
1514
1596
  matchers: event.matchers,
1515
1597
  payload: event.payload,
1516
1598
  idempotencyKey: event.idempotencyKey,
1517
- triggerIds: evaluateTriggerMatches(slate, group.key, event.payload)
1599
+ triggerIds: group.webhook?.expandWebhook
1600
+ ? []
1601
+ : evaluateTriggerMatches(slate, group.key, event.payload)
1518
1602
  })),
1603
+ ...(group.webhook?.expandWebhook ? { requiresExpansion: true } : {}),
1519
1604
  response,
1520
1605
  skipped: res.skipped ?? null
1521
1606
  });
1522
1607
  });
1523
1608
 
1609
+ manager.onRequest(
1610
+ 'slates/trigger_group.webhook.delivery_authenticate',
1611
+ async ({ params }) => {
1612
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1613
+ let deliveryConfig = getWebhookDeliveryConfig(group);
1614
+
1615
+ let context = createContext(
1616
+ {},
1617
+ {
1618
+ request: toWebhookRequest(params),
1619
+ deliveryConfig: params.deliveryConfig
1620
+ },
1621
+ {}
1622
+ );
1623
+ let res = await traceProviderCall(
1624
+ {
1625
+ component: 'action',
1626
+ functionName: 'webhookDeliveryAuthenticate',
1627
+ message: `Authenticating a delivery for trigger group ${formatEntityLabel(group.name, group.key)}`,
1628
+ successMessage: result =>
1629
+ result.rejected
1630
+ ? `Rejected a delivery with status ${result.rejected.status} for trigger group ${formatEntityLabel(group.name, group.key)}`
1631
+ : `Authenticated a delivery for ${result.routingKeys.length} routing key(s) for trigger group ${formatEntityLabel(group.name, group.key)}`,
1632
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while authenticating a delivery`,
1633
+ metadata: {
1634
+ triggerGroupId: group.key,
1635
+ triggerGroupName: group.name,
1636
+ requestMethod: params.method,
1637
+ hasRequestBody: !!params.body
1638
+ },
1639
+ onSuccess: result => ({
1640
+ routingKeyCount: result.routingKeys.length,
1641
+ rejectedStatus: result.rejected?.status ?? null
1642
+ })
1643
+ },
1644
+ () =>
1645
+ runWithContext(context, async () => {
1646
+ let authentication = await deliveryConfig.authenticate(context);
1647
+ return validate(
1648
+ slatesTriggerGroupWebhookDeliveryAuthentication,
1649
+ authentication.rejected
1650
+ ? { routingKeys: [], rejected: authentication.rejected }
1651
+ : { routingKeys: authentication.routingKeys, rejected: null },
1652
+ 'trigger_group.webhook.delivery_authentication',
1653
+ 'Webhook delivery authentication returned an invalid result'
1654
+ );
1655
+ })
1656
+ );
1657
+
1658
+ return withRequestTraces(context, res);
1659
+ }
1660
+ );
1661
+
1662
+ manager.onRequest('slates/trigger_group.webhook.expand', async ({ params }) => {
1663
+ let ctx = getContextFull();
1664
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1665
+ let expand = group.source === 'webhook' ? group.webhook?.expandWebhook : undefined;
1666
+ if (!expand) {
1667
+ throw new ServiceError(
1668
+ badRequestError({
1669
+ message: `Trigger group does not support webhook expansion: ${params.triggerGroupId}`
1670
+ })
1671
+ );
1672
+ }
1673
+
1674
+ let context = createContext(
1675
+ ctx.config,
1676
+ {
1677
+ payload: params.payload,
1678
+ state: params.state ?? null,
1679
+ selection: validateTriggerGroupSelection(group, params.selection),
1680
+ deliveryId: params.deliveryId,
1681
+ webhookRegistrationPayload: params.webhookRegistrationPayload
1682
+ },
1683
+ ctx.auth?.output!
1684
+ );
1685
+ let res = await traceProviderCall(
1686
+ {
1687
+ component: 'action',
1688
+ functionName: 'expandWebhook',
1689
+ message: `Expanding webhook delivery for trigger group ${formatEntityLabel(group.name, group.key)}`,
1690
+ successMessage: `Expanded webhook delivery for trigger group ${formatEntityLabel(group.name, group.key)}`,
1691
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while expanding a webhook delivery`,
1692
+ metadata: { triggerGroupId: group.key, triggerGroupName: group.name }
1693
+ },
1694
+ () =>
1695
+ runWithContext(context, async () =>
1696
+ validate(
1697
+ slatesTriggerGroupWebhookExpansionResult,
1698
+ await expand(context),
1699
+ 'trigger_group.webhook.expansion',
1700
+ 'Webhook expansion returned an invalid result'
1701
+ )
1702
+ )
1703
+ );
1704
+
1705
+ return withRequestTraces(context, {
1706
+ ...(res.updatedState !== undefined ? { updatedState: res.updatedState } : {}),
1707
+ events: res.events.map(event => ({
1708
+ ...event,
1709
+ triggerIds: evaluateTriggerMatches(slate, group.key, event.payload)
1710
+ }))
1711
+ });
1712
+ });
1713
+
1524
1714
  manager.onRequest('slates/trigger_group.routing_matchers.get', async ({ params }) => {
1525
1715
  let ctx = getContextFull();
1526
1716
  let group = getTriggerGroup(slate, params.triggerGroupId);
@@ -1559,7 +1749,14 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
1559
1749
  );
1560
1750
  }
1561
1751
 
1562
- let context = createContext(ctx.config, { state: params.state }, ctx.auth?.output!);
1752
+ let context = createContext(
1753
+ ctx.config,
1754
+ {
1755
+ state: params.state,
1756
+ selection: validateTriggerGroupSelection(group, params.selection)
1757
+ },
1758
+ ctx.auth?.output!
1759
+ );
1563
1760
  let res = await traceProviderCall(
1564
1761
  {
1565
1762
  component: 'action',
package/src/spec.test.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { isServiceError } from '@lowerdeck/error';
2
+ import { slatesTriggerGroup } from '@slates/proto';
2
3
  import { describe, expect, it } from 'vitest';
3
4
  import { z } from 'zod';
4
5
  import {
@@ -8,11 +9,14 @@ import {
8
9
  getExposedTriggerGroups,
9
10
  getMappableAction,
10
11
  getTriggersForGroup,
12
+ getWebhookDeliveryConfig,
13
+ getWebhookManualRegistration,
11
14
  isMappableTrigger,
12
15
  mapAction,
13
16
  mapAdapter,
14
17
  mapAuthMethod,
15
- mapToolCategory
18
+ mapToolCategory,
19
+ mapTriggerGroup
16
20
  } from './spec';
17
21
 
18
22
  let baseAction = {
@@ -202,6 +206,92 @@ describe('adapter exposure', () => {
202
206
  });
203
207
  });
204
208
 
209
+ describe('webhook registration config', () => {
210
+ let config = {
211
+ userConfigSchema: z.object({ secret: z.string().describe('Signing secret') }),
212
+ fullConfigSchema: z.object({ secret: z.string(), endpoint: z.string() }),
213
+ setup: async () => ({ webhookSetupDocument: '', partialWebhookRegistrationPayload: {} }),
214
+ finish: async () => ({ webhookRegistrationPayload: {} }),
215
+ authenticate: async () => ({ routingKeys: [] })
216
+ };
217
+ let schemas = {
218
+ userConfigSchema: {
219
+ type: 'object',
220
+ properties: { secret: { type: 'string', description: 'Signing secret' } },
221
+ required: ['secret']
222
+ },
223
+ fullConfigSchema: {
224
+ type: 'object',
225
+ properties: { secret: { type: 'string' }, endpoint: { type: 'string' } },
226
+ required: ['secret', 'endpoint']
227
+ }
228
+ };
229
+ let autoRegistration = {
230
+ webhookTargetList: async () => ({ targets: [], nextPageToken: null }),
231
+ webhookRegister: async () => ({
232
+ webhookRegistrationIdentifier: 'reg',
233
+ webhookRegistrationPayload: {}
234
+ }),
235
+ webhookUnregister: async () => {}
236
+ };
237
+ let group = (webhook: Record<string, unknown>): any => ({
238
+ key: 'group',
239
+ name: 'Group',
240
+ source: 'webhook',
241
+ webhook: { process: async () => ({ events: [] }), ...webhook }
242
+ });
243
+ let auto = group({ autoRegistration });
244
+ let manual = group({ manualRegistration: config });
245
+ let configured = group({ autoRegistration, deliveryConfig: config });
246
+
247
+ it('keeps plain automatic and manual groups serialised as before', () => {
248
+ expect(mapTriggerGroup(auto).invocation).toEqual({
249
+ type: 'webhook',
250
+ registration: { mode: 'auto' }
251
+ });
252
+ expect(mapTriggerGroup(manual).invocation).toMatchObject({
253
+ type: 'webhook',
254
+ registration: { mode: 'manual', ...schemas }
255
+ });
256
+ expect(
257
+ Object.keys((mapTriggerGroup(manual).invocation as any).registration).sort()
258
+ ).toEqual(['fullConfigSchema', 'mode', 'userConfigSchema']);
259
+ });
260
+
261
+ it('advertises the delivery config of an automatic group', () => {
262
+ let mapped = mapTriggerGroup(configured);
263
+ expect(mapped.invocation).toMatchObject({
264
+ type: 'webhook',
265
+ registration: { mode: 'auto', deliveryConfig: schemas }
266
+ });
267
+ expect(slatesTriggerGroup.parse(mapped).invocation).toEqual(mapped.invocation);
268
+ });
269
+
270
+ it('authenticates deliveries only for automatic groups with a delivery config', () => {
271
+ expect(getWebhookDeliveryConfig(configured)).toBe(config);
272
+ for (let unsupported of [
273
+ auto,
274
+ manual,
275
+ { key: 'group', name: 'Group', source: 'polling' } as any
276
+ ]) {
277
+ expect(() => getWebhookDeliveryConfig(unsupported)).toThrow(
278
+ 'Trigger group does not support webhook delivery authentication: group'
279
+ );
280
+ }
281
+ });
282
+
283
+ it('serves setup and finish for manual groups and automatic groups with a delivery config', () => {
284
+ expect(getWebhookManualRegistration(manual)).toBe(config);
285
+ expect(getWebhookManualRegistration(configured)).toBe(config);
286
+ expect(() => getWebhookManualRegistration(auto)).toThrow(
287
+ 'Trigger group does not support manual webhook registration: group'
288
+ );
289
+ expect(() =>
290
+ getWebhookManualRegistration({ key: 'poll', name: 'Poll', source: 'polling' } as any)
291
+ ).toThrow('Trigger group does not support manual webhook registration: poll');
292
+ });
293
+ });
294
+
205
295
  describe('mapAuthMethod', () => {
206
296
  let slate: any = { spec: { auth: { outputSchema: z.object({ token: z.string() }) } } };
207
297
  let oauthMethod: any = {
package/src/spec.ts CHANGED
@@ -266,7 +266,11 @@ export let getWebhookAutoRegistration = <ConfigType extends {}, AuthType extends
266
266
  export let getWebhookManualRegistration = <ConfigType extends {}, AuthType extends {}>(
267
267
  group: ReturnType<typeof getTriggerGroup<ConfigType, AuthType>>
268
268
  ) => {
269
- if (group.source !== 'webhook' || !group.webhook?.manualRegistration) {
269
+ let registration =
270
+ group.source === 'webhook'
271
+ ? (group.webhook?.manualRegistration ?? group.webhook?.deliveryConfig)
272
+ : undefined;
273
+ if (!registration) {
270
274
  throw new ServiceError(
271
275
  badRequestError({
272
276
  message: `Trigger group does not support manual webhook registration: ${group.key}`
@@ -274,9 +278,31 @@ export let getWebhookManualRegistration = <ConfigType extends {}, AuthType exten
274
278
  );
275
279
  }
276
280
 
277
- return group.webhook.manualRegistration;
281
+ return registration;
278
282
  };
279
283
 
284
+ export let getWebhookDeliveryConfig = <ConfigType extends {}, AuthType extends {}>(
285
+ group: ReturnType<typeof getTriggerGroup<ConfigType, AuthType>>
286
+ ) => {
287
+ if (group.source !== 'webhook' || !group.webhook?.deliveryConfig) {
288
+ throw new ServiceError(
289
+ badRequestError({
290
+ message: `Trigger group does not support webhook delivery authentication: ${group.key}`
291
+ })
292
+ );
293
+ }
294
+
295
+ return group.webhook.deliveryConfig;
296
+ };
297
+
298
+ let mapWebhookConfigSchemas = (config: {
299
+ userConfigSchema: z.ZodType<any>;
300
+ fullConfigSchema: z.ZodType<any>;
301
+ }) => ({
302
+ userConfigSchema: toJsonSchema(config.userConfigSchema),
303
+ fullConfigSchema: toJsonSchema(config.fullConfigSchema)
304
+ });
305
+
280
306
  export let mapToolCategory = (category: SlateToolCategory): SlatesToolCategory => ({
281
307
  id: category.key,
282
308
  name: category.name,
@@ -307,6 +333,9 @@ export let mapTriggerGroup = <ConfigType extends {}, AuthType extends {}>(
307
333
  name: group.name,
308
334
  description: group.description,
309
335
  metadata: group.metadata,
336
+ ...(group.selectionSchema
337
+ ? { selectionSchema: toJsonSchema(group.selectionSchema, 'input') }
338
+ : {}),
310
339
  invocation:
311
340
  group.source === 'polling'
312
341
  ? {
@@ -317,16 +346,25 @@ export let mapTriggerGroup = <ConfigType extends {}, AuthType extends {}>(
317
346
  ? { type: 'gateway' }
318
347
  : {
319
348
  type: 'webhook',
349
+ ...(group.webhook?.expandWebhook ? { supportsExpansion: true } : {}),
350
+ ...(group.webhook?.autoRegistration?.webhookRenew
351
+ ? { supportsRenewal: true }
352
+ : {}),
353
+ ...(group.webhook?.reconcileIntervalSeconds !== undefined
354
+ ? { reconcileIntervalSeconds: group.webhook.reconcileIntervalSeconds }
355
+ : {}),
320
356
  registration: group.webhook?.manualRegistration
321
357
  ? {
322
358
  mode: 'manual',
323
- userConfigSchema: toJsonSchema(
324
- group.webhook.manualRegistration.userConfigSchema
325
- ),
326
- fullConfigSchema: toJsonSchema(
327
- group.webhook.manualRegistration.fullConfigSchema
328
- )
359
+ ...mapWebhookConfigSchemas(group.webhook.manualRegistration)
360
+ }
361
+ : {
362
+ mode: 'auto',
363
+ ...(group.webhook?.deliveryConfig
364
+ ? {
365
+ deliveryConfig: mapWebhookConfigSchemas(group.webhook.deliveryConfig)
366
+ }
367
+ : {})
329
368
  }
330
- : { mode: 'auto' }
331
369
  }
332
370
  });
package/src/validation.ts CHANGED
@@ -26,8 +26,9 @@ export let validate = <T>(
26
26
  return result.data;
27
27
  };
28
28
 
29
- export let toJsonSchema = (schema: z.ZodType<any>) =>
29
+ export let toJsonSchema = (schema: z.ZodType<any>, io: 'input' | 'output' = 'output') =>
30
30
  schema.toJSONSchema({
31
+ io,
31
32
  unrepresentable: 'any',
32
33
  override: ctx => {
33
34
  let def = ctx.zodSchema._zod.def;