@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
package/src/index.ts CHANGED
@@ -1,9 +1,193 @@
1
- import { badRequestError, preconditionFailedError, ServiceError } from '@lowerdeck/error';
2
- import { createSlatesProviderProtoHandler, SlatesParticipant } from '@slates/proto';
3
- import { Slate, SlateContext, SlateLogger, SlateLogListener } from '@slates/provider';
4
- import { getAction, getActionWithType, getAuthMethod, mapAction, mapAuthMethod } from './spec';
1
+ import {
2
+ badRequestError,
3
+ notFoundError,
4
+ preconditionFailedError,
5
+ ServiceError
6
+ } from '@lowerdeck/error';
7
+ import {
8
+ createSlatesProviderProtoHandler,
9
+ SLATES_PROTOCOL_VERSION,
10
+ type SlatesMessageAuthAdminCallbackHandleRequest,
11
+ type SlatesMessageAuthAuthorizationUrlGetRequest,
12
+ type SlatesParticipant
13
+ } from '@slates/proto';
14
+ import {
15
+ AuthConfigSecretRedactor,
16
+ redactUrlAttachmentSecrets,
17
+ runWithContext,
18
+ type Slate,
19
+ type SlateAttachment,
20
+ SlateContext,
21
+ type SlateLiveInvocationInfo,
22
+ SlateLogger,
23
+ type SlateLogListener,
24
+ SlatePublicContext,
25
+ uploadAttachmentDirect
26
+ } from '@slates/provider';
27
+ import { PQueue } from './pQueue';
28
+ import {
29
+ evaluateTriggerMatches,
30
+ getActionWithType,
31
+ getAdapter,
32
+ getAuthMethod,
33
+ getExposedAction,
34
+ getExposedActions,
35
+ getExposedToolCategories,
36
+ getExposedTriggerGroups,
37
+ getTriggerGroup,
38
+ getWebhookAutoRegistration,
39
+ getWebhookManualRegistration,
40
+ isMappableTrigger,
41
+ mapAction,
42
+ mapAdapter,
43
+ mapAuthMethod,
44
+ mapToolCategory,
45
+ mapTriggerGroup
46
+ } from './spec';
5
47
  import { State } from './state';
6
48
  import { toJsonSchema, validate } from './validation';
49
+ import { serializeWebhookHttpResponse } from './webhook';
50
+
51
+ let DEFAULT_MAX_ATTACHMENT_SIZE_BYTES = 100_000_000;
52
+
53
+ let routeAttachmentsThroughDirectUpload = async (
54
+ attachments: SlateAttachment[] | undefined,
55
+ live: SlateLiveInvocationInfo | null
56
+ ): Promise<SlateAttachment[] | undefined> => {
57
+ if (!attachments || attachments.length === 0 || !live) return attachments;
58
+
59
+ let contentEntries = attachments
60
+ .map((attachment, index) => ({ attachment, index }))
61
+ .filter(entry => entry.attachment.content.type === 'content');
62
+ if (contentEntries.length === 0) return attachments;
63
+
64
+ let queue = new PQueue({ concurrency: 10 });
65
+ let replacements = new Map<number, SlateAttachment | null>();
66
+
67
+ await Promise.all(
68
+ contentEntries.map(entry =>
69
+ queue.add(async () => {
70
+ try {
71
+ let body =
72
+ entry.attachment.content.type === 'content'
73
+ ? entry.attachment.content.encoding === 'base64'
74
+ ? Buffer.from(entry.attachment.content.content, 'base64')
75
+ : Buffer.from(entry.attachment.content.content, 'utf-8')
76
+ : Buffer.alloc(0);
77
+
78
+ replacements.set(
79
+ entry.index,
80
+ await uploadAttachmentDirect({
81
+ live,
82
+ mimeType: entry.attachment.mimeType,
83
+ body
84
+ })
85
+ );
86
+ } catch {
87
+ // Upload failed or was interrupted -- we simply don't have this attachment, not a
88
+ // failed tool call.
89
+ replacements.set(entry.index, null);
90
+ }
91
+ })
92
+ )
93
+ );
94
+
95
+ return attachments
96
+ .map((attachment, index) =>
97
+ replacements.has(index) ? replacements.get(index) : attachment
98
+ )
99
+ .filter((a): a is SlateAttachment => a != null);
100
+ };
101
+
102
+ let isRecord = (value: unknown): value is Record<string, unknown> =>
103
+ typeof value === 'object' && value !== null && !Array.isArray(value);
104
+
105
+ let getObjectKeyCount = (value: unknown) =>
106
+ isRecord(value) ? Object.keys(value).length : undefined;
107
+
108
+ let DOWNLOAD_ATTACHMENT_URL_KEYS = new Set([
109
+ 'downloadUrl',
110
+ 'fileUrl',
111
+ 'temporaryDownloadUrl',
112
+ 'webContentLink'
113
+ ]);
114
+
115
+ let isAttachmentUrl = (value: string) => /^[a-z][a-z0-9+.-]*:\/\//i.test(value);
116
+
117
+ let collectOutputUrlAttachments = (
118
+ value: unknown,
119
+ seen = new Set<string>()
120
+ ): SlateAttachment[] => {
121
+ if (Array.isArray(value)) {
122
+ return value.flatMap(item => collectOutputUrlAttachments(item, seen));
123
+ }
124
+
125
+ if (!isRecord(value)) {
126
+ return [];
127
+ }
128
+
129
+ let attachments: SlateAttachment[] = [];
130
+
131
+ for (let [key, nestedValue] of Object.entries(value)) {
132
+ if (
133
+ DOWNLOAD_ATTACHMENT_URL_KEYS.has(key) &&
134
+ typeof nestedValue === 'string' &&
135
+ nestedValue.length > 0 &&
136
+ isAttachmentUrl(nestedValue) &&
137
+ !seen.has(nestedValue)
138
+ ) {
139
+ seen.add(nestedValue);
140
+ attachments.push({
141
+ content: {
142
+ type: 'url',
143
+ url: nestedValue
144
+ }
145
+ });
146
+ continue;
147
+ }
148
+
149
+ attachments.push(...collectOutputUrlAttachments(nestedValue, seen));
150
+ }
151
+
152
+ return attachments;
153
+ };
154
+
155
+ let mergeAttachments = (
156
+ explicitAttachments: SlateAttachment[] | undefined,
157
+ output: unknown
158
+ ): SlateAttachment[] | undefined => {
159
+ let attachments = [...(explicitAttachments ?? [])];
160
+ let seen = new Set(attachments.map(attachment => JSON.stringify(attachment)));
161
+
162
+ for (let attachment of collectOutputUrlAttachments(output)) {
163
+ let key = JSON.stringify(attachment);
164
+ if (seen.has(key)) continue;
165
+ seen.add(key);
166
+ attachments.push(attachment);
167
+ }
168
+
169
+ return attachments.length > 0 ? attachments : undefined;
170
+ };
171
+
172
+ let toErrorMetadata = (error: unknown) => {
173
+ if (error instanceof Error) {
174
+ return {
175
+ errorName: error.name,
176
+ errorMessage: error.message,
177
+ errorStack: error.stack
178
+ };
179
+ }
180
+
181
+ return {
182
+ errorValue: String(error)
183
+ };
184
+ };
185
+
186
+ let formatEntityLabel = (name: string, key: string) => `"${name}" (${key})`;
187
+ let resolveTraceMessage = <ResultType>(
188
+ message: string | ((result: ResultType) => string),
189
+ result: ResultType
190
+ ) => (typeof message === 'function' ? message(result) : message);
7
191
 
8
192
  export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
9
193
  slate: Slate<ConfigType, AuthType>,
@@ -15,10 +199,94 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
15
199
 
16
200
  let auth = new State<{ authenticationMethodId: string; output: AuthType } | null>(null);
17
201
  let config = new State<{ value: ConfigType } | null>(null);
18
-
19
202
  let session = new State<{ id: string; state: any } | null>(null);
20
203
 
204
+ let hubCapabilities = new State<{
205
+ attachments?: { directUpload?: { enabled: boolean; maxAttachmentSizeBytes?: number } };
206
+ adapters?: boolean;
207
+ triggers?: boolean;
208
+ } | null>(null);
209
+ let liveInvocation = new State<Pick<SlateLiveInvocationInfo, 'token' | 'baseUrl'> | null>(
210
+ null
211
+ );
212
+
213
+ let getLiveInvocation = (): SlateLiveInvocationInfo | null => {
214
+ let directUpload = hubCapabilities.get()?.attachments?.directUpload;
215
+ let live = liveInvocation.get();
216
+
217
+ if (!directUpload?.enabled || !live) return null;
218
+
219
+ return {
220
+ ...live,
221
+ maxAttachmentSizeBytes:
222
+ directUpload.maxAttachmentSizeBytes ?? DEFAULT_MAX_ATTACHMENT_SIZE_BYTES
223
+ };
224
+ };
225
+
226
+ let supportsTriggerGroups = () => !!hubCapabilities.get()?.triggers;
227
+ let supportsAdapters = () => !!hubCapabilities.get()?.adapters;
228
+
21
229
  let logger = new SlateLogger(listeners);
230
+ let providerTrace = {
231
+ providerId: slate.spec.key,
232
+ providerName: slate.spec.name
233
+ };
234
+
235
+ let traceProviderCall = async <ResultType>(
236
+ trace: {
237
+ component: 'config' | 'auth' | 'action';
238
+ functionName: string;
239
+ message: string;
240
+ successMessage: string | ((result: ResultType) => string);
241
+ errorMessage?: string;
242
+ metadata?: Record<string, unknown>;
243
+ onSuccess?: (result: ResultType) => Record<string, unknown> | undefined;
244
+ },
245
+ handler: () => Promise<ResultType>
246
+ ): Promise<ResultType> => {
247
+ let startedAt = Date.now();
248
+
249
+ logger.info({
250
+ ...providerTrace,
251
+ ...trace.metadata,
252
+ component: trace.component,
253
+ functionName: trace.functionName,
254
+ phase: 'start',
255
+ message: trace.message
256
+ });
257
+
258
+ try {
259
+ let result = await handler();
260
+ let successMessage = resolveTraceMessage(trace.successMessage, result);
261
+
262
+ logger.info({
263
+ ...providerTrace,
264
+ ...trace.metadata,
265
+ ...(trace.onSuccess?.(result) ?? {}),
266
+ component: trace.component,
267
+ functionName: trace.functionName,
268
+ phase: 'success',
269
+ durationMs: Date.now() - startedAt,
270
+ message: successMessage
271
+ });
272
+
273
+ return result;
274
+ } catch (error) {
275
+ logger.error({
276
+ ...providerTrace,
277
+ ...trace.metadata,
278
+ ...toErrorMetadata(error),
279
+ component: trace.component,
280
+ functionName: trace.functionName,
281
+ phase: 'error',
282
+ durationMs: Date.now() - startedAt,
283
+ message:
284
+ trace.errorMessage ??
285
+ `${typeof trace.successMessage === 'string' ? trace.successMessage : trace.message} failed`
286
+ });
287
+ throw error;
288
+ }
289
+ };
22
290
 
23
291
  let getContextBasic = () => {
24
292
  let currentProtocol = protocol.get();
@@ -65,6 +333,90 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
65
333
  };
66
334
  };
67
335
 
336
+ let getAuthConfig = (): Record<string, any> => config.get()?.value ?? {};
337
+
338
+ let getEmptyContext = () => new SlateContext({}, {}, {}, slate.spec as any, logger);
339
+ let getAuthContext = () =>
340
+ new SlateContext(getAuthConfig(), {}, {}, slate.spec as any, logger);
341
+ let withRequestTraces = <Result extends Record<string, any>>(
342
+ context: SlatePublicContext<any>,
343
+ result: Result
344
+ ) => {
345
+ let requestTraces = context.getHttpTraces();
346
+ return requestTraces.length > 0 ? { ...result, requestTraces } : result;
347
+ };
348
+
349
+ let buildAuthorizationUrlContext = (
350
+ params: SlatesMessageAuthAuthorizationUrlGetRequest['params']
351
+ ) => ({
352
+ redirectUri: params.redirectUri,
353
+ state: params.state,
354
+ input: params.input,
355
+ clientId: params.clientId,
356
+ clientSecret: params.clientSecret,
357
+ scopes: params.scopes,
358
+ config: getAuthConfig()
359
+ });
360
+
361
+ let buildCallbackContext = (
362
+ params: Omit<SlatesMessageAuthAdminCallbackHandleRequest['params'], 'code'>
363
+ ) => ({
364
+ state: params.state,
365
+ redirectUri: params.redirectUri,
366
+ input: params.input,
367
+ clientId: params.clientId,
368
+ clientSecret: params.clientSecret,
369
+ scopes: params.scopes,
370
+ callbackParams: params.callbackParams || {},
371
+ callbackState: params.callbackState || {},
372
+ config: getAuthConfig()
373
+ });
374
+
375
+ let getTracedAuthorizationUrl = async (
376
+ params: SlatesMessageAuthAuthorizationUrlGetRequest['params'],
377
+ authenticationMethodName: string,
378
+ trace: {
379
+ functionName: string;
380
+ message: string;
381
+ successMessage: string;
382
+ metadata?: Record<string, unknown>;
383
+ },
384
+ getAuthorizationUrl: (
385
+ ctx: ReturnType<typeof buildAuthorizationUrlContext>
386
+ ) => Promise<{ url: string; input?: any; callbackState?: Record<string, any> }>
387
+ ) => {
388
+ let context = getEmptyContext();
389
+ let urlRes = await traceProviderCall(
390
+ {
391
+ component: 'auth',
392
+ functionName: trace.functionName,
393
+ message: trace.message,
394
+ successMessage: trace.successMessage,
395
+ metadata: {
396
+ authenticationMethodId: params.authenticationMethodId,
397
+ authenticationMethodName,
398
+ scopeCount: params.scopes.length,
399
+ inputKeyCount: getObjectKeyCount(params.input),
400
+ ...trace.metadata
401
+ },
402
+ onSuccess: result => ({
403
+ returnedInput: !!result.input,
404
+ hasCallbackState: !!result.callbackState
405
+ })
406
+ },
407
+ () =>
408
+ runWithContext(context, () =>
409
+ getAuthorizationUrl(buildAuthorizationUrlContext(params))
410
+ )
411
+ );
412
+
413
+ return withRequestTraces(context, {
414
+ authorizationUrl: urlRes.url,
415
+ input: urlRes.input,
416
+ callbackState: urlRes.callbackState
417
+ });
418
+ };
419
+
68
420
  manager.onNotification('slates/hello', async ({ params }) => {
69
421
  protocol.set(params.protocol);
70
422
  });
@@ -118,6 +470,32 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
118
470
  });
119
471
  });
120
472
 
473
+ manager.onNotification('slates/hub.capabilities.set', async ({ params }) => {
474
+ hubCapabilities.set(params.capabilities);
475
+ });
476
+
477
+ manager.onNotification('slates/hub.live_invocation.set', async ({ params }) => {
478
+ liveInvocation.set({
479
+ token: params.token,
480
+ baseUrl: params.baseUrl
481
+ });
482
+ });
483
+
484
+ manager.onRequest('slates/provider.capabilities.get', async () => ({
485
+ capabilities: {
486
+ hub: {
487
+ capabilitiesNotification: true,
488
+ liveInvocation: true
489
+ },
490
+ provider: {
491
+ triggerGroups:
492
+ (slate.triggerGroups?.length ?? 0) > 0 &&
493
+ (slate.actions ?? []).every(isMappableTrigger),
494
+ toolCategories: (slate.toolCategories?.length ?? 0) > 0
495
+ }
496
+ }
497
+ }));
498
+
121
499
  manager.onRequest('slates/config.changed', async ({ params }) => {
122
500
  getContextBasic();
123
501
 
@@ -133,15 +511,37 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
133
511
  return { success: true, config: newConfig };
134
512
  }
135
513
 
136
- let updatedConfig = await configChanged({
137
- previousConfig: params.previousConfig as ConfigType | null,
138
- newConfig
139
- });
514
+ let context = getEmptyContext();
515
+ let updatedConfig = await traceProviderCall<{ config?: ConfigType } | undefined>(
516
+ {
517
+ component: 'config',
518
+ functionName: 'configChanged',
519
+ message: 'Running config change handler',
520
+ successMessage: 'Config change handler completed',
521
+ metadata: {
522
+ hasPreviousConfig: params.previousConfig !== null,
523
+ newConfigKeyCount: getObjectKeyCount(newConfig)
524
+ },
525
+ onSuccess: result => ({
526
+ returnedConfig: !!result?.config
527
+ })
528
+ },
529
+ () =>
530
+ runWithContext(context, async () =>
531
+ configChanged({
532
+ previousConfig: params.previousConfig as ConfigType | null,
533
+ newConfig
534
+ })
535
+ )
536
+ );
140
537
 
141
- return { success: true, config: updatedConfig?.config ?? newConfig };
538
+ return withRequestTraces(context, {
539
+ success: true,
540
+ config: (updatedConfig?.config ?? newConfig) as Record<string, any>
541
+ });
142
542
  });
143
543
 
144
- manager.onRequest('slates/config.get_default', async ({ params }) => {
544
+ manager.onRequest('slates/config.get_default', async () => {
145
545
  getContextBasic();
146
546
 
147
547
  let getDefaultConfig = slate.spec.config.handlers.getDefaultConfig;
@@ -149,32 +549,50 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
149
549
  return { config: null };
150
550
  }
151
551
 
152
- let defaultConfig = await getDefaultConfig();
153
- return { config: defaultConfig };
552
+ let context = getEmptyContext();
553
+ let defaultConfig = await traceProviderCall<ConfigType>(
554
+ {
555
+ component: 'config',
556
+ functionName: 'getDefaultConfig',
557
+ message: 'Getting default config',
558
+ successMessage: 'Default config retrieved',
559
+ onSuccess: result => ({
560
+ configKeyCount: getObjectKeyCount(result)
561
+ })
562
+ },
563
+ () => runWithContext(context, async () => getDefaultConfig())
564
+ );
565
+ return withRequestTraces(context, {
566
+ config: (defaultConfig ?? null) as Record<string, any> | null
567
+ });
154
568
  });
155
569
 
156
- manager.onRequest('slates/config.schema.get', async ({ params }) => {
570
+ manager.onRequest('slates/config.schema.get', async () => {
157
571
  getContextBasic();
158
572
 
159
- return { schema: toJsonSchema(slate.spec.configSchema) };
573
+ return {
574
+ schema: toJsonSchema(slate.spec.configSchema),
575
+ docs: slate.spec.config.docsReferences ?? []
576
+ };
160
577
  });
161
578
 
162
- manager.onRequest('slates/provider.identify', async ({ params }) => {
579
+ manager.onRequest('slates/provider.identify', async () => {
163
580
  getContextBasic();
164
581
 
165
582
  return {
166
- protocol: 'slates@2026-01-01',
583
+ protocol: SLATES_PROTOCOL_VERSION,
167
584
  provider: {
168
585
  type: 'provider',
169
586
  id: slate.spec.key,
170
587
  name: slate.spec.name,
171
588
  description: slate.spec.description,
172
589
  metadata: slate.spec.parameters.metadata
173
- }
590
+ },
591
+ docs: slate.spec.docs ?? []
174
592
  };
175
593
  });
176
594
 
177
- manager.onRequest('slates/auth.methods.list', async ({ params }) => {
595
+ manager.onRequest('slates/auth.methods.list', async () => {
178
596
  getContextBasic();
179
597
 
180
598
  return {
@@ -199,7 +617,25 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
199
617
  return { input: null };
200
618
  }
201
619
 
202
- return { input: await authMethod.getDefaultInput() };
620
+ let context = getEmptyContext();
621
+ let input = await traceProviderCall(
622
+ {
623
+ component: 'auth',
624
+ functionName: 'getDefaultInput',
625
+ message: 'Getting default authentication input',
626
+ successMessage: 'Default authentication input retrieved',
627
+ metadata: {
628
+ authenticationMethodId: params.authenticationMethodId,
629
+ authenticationMethodName: authMethod.name
630
+ },
631
+ onSuccess: result => ({
632
+ inputKeyCount: getObjectKeyCount(result)
633
+ })
634
+ },
635
+ () => runWithContext(context, () => authMethod.getDefaultInput!())
636
+ );
637
+
638
+ return withRequestTraces(context, { input });
203
639
  });
204
640
 
205
641
  manager.onRequest('slates/auth.input.changed', async ({ params }) => {
@@ -210,12 +646,36 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
210
646
  return { success: true, input: params.newInput };
211
647
  }
212
648
 
213
- let updatedInput = await authMethod.onInputChanged({
214
- previousInput: params.previousInput as any | null,
215
- newInput: params.newInput
216
- });
649
+ let context = getEmptyContext();
650
+ let updatedInput = await traceProviderCall(
651
+ {
652
+ component: 'auth',
653
+ functionName: 'onInputChanged',
654
+ message: 'Running authentication input change handler',
655
+ successMessage: 'Authentication input change handler completed',
656
+ metadata: {
657
+ authenticationMethodId: params.authenticationMethodId,
658
+ authenticationMethodName: authMethod.name,
659
+ hasPreviousInput: params.previousInput !== null,
660
+ newInputKeyCount: getObjectKeyCount(params.newInput)
661
+ },
662
+ onSuccess: result => ({
663
+ returnedInput: !!result?.input
664
+ })
665
+ },
666
+ () =>
667
+ runWithContext(context, () =>
668
+ authMethod.onInputChanged!({
669
+ previousInput: params.previousInput as any | null,
670
+ newInput: params.newInput
671
+ })
672
+ )
673
+ );
217
674
 
218
- return { success: true, input: updatedInput?.input ?? params.newInput };
675
+ return withRequestTraces(context, {
676
+ success: true,
677
+ input: updatedInput?.input ?? params.newInput
678
+ });
219
679
  });
220
680
 
221
681
  manager.onRequest('slates/auth.output.get', async ({ params }) => {
@@ -234,8 +694,35 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
234
694
  }
235
695
 
236
696
  if ('getOutput' in authMethod) {
237
- let outputRes = await authMethod.getOutput({ input });
238
- return { output: outputRes.output };
697
+ let context = getAuthContext();
698
+ let outputRes = await traceProviderCall(
699
+ {
700
+ component: 'auth',
701
+ functionName: 'getOutput',
702
+ message: 'Getting authentication output',
703
+ successMessage: 'Authentication output retrieved',
704
+ metadata: {
705
+ authenticationMethodId: params.authenticationMethodId,
706
+ authenticationMethodName: authMethod.name,
707
+ inputKeyCount: getObjectKeyCount(input)
708
+ },
709
+ onSuccess: result => ({
710
+ outputKeyCount: getObjectKeyCount(result.output),
711
+ scopeCount: result.scopes?.length ?? 0
712
+ })
713
+ },
714
+ () =>
715
+ runWithContext(context, () =>
716
+ authMethod.getOutput({
717
+ input,
718
+ config: getAuthConfig()
719
+ })
720
+ )
721
+ );
722
+ return withRequestTraces(context, {
723
+ output: outputRes.output,
724
+ scopes: outputRes.scopes
725
+ });
239
726
  }
240
727
 
241
728
  return { output: input as any };
@@ -246,20 +733,36 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
246
733
  let authMethod = getAuthMethod(slate, params.authenticationMethodId);
247
734
 
248
735
  if ('handleCallback' in authMethod) {
249
- let callbackRes = await authMethod.handleCallback({
250
- code: params.code,
251
- state: params.state,
252
- redirectUri: params.redirectUri,
253
- input: params.input,
254
- clientId: params.clientId,
255
- clientSecret: params.clientSecret,
256
- scopes: params.scopes
257
- });
736
+ let context = getEmptyContext();
737
+ let callbackRes = await traceProviderCall(
738
+ {
739
+ component: 'auth',
740
+ functionName: 'handleCallback',
741
+ message: 'Handling authentication callback',
742
+ successMessage: 'Authentication callback handled',
743
+ metadata: {
744
+ authenticationMethodId: params.authenticationMethodId,
745
+ authenticationMethodName: authMethod.name,
746
+ scopeCount: params.scopes.length,
747
+ hasCallbackState: !!params.callbackState
748
+ },
749
+ onSuccess: result => ({
750
+ outputKeyCount: getObjectKeyCount(result.output),
751
+ returnedInput: !!result.input,
752
+ returnedScopeCount: result.scopes?.length
753
+ })
754
+ },
755
+ () =>
756
+ runWithContext(context, () =>
757
+ authMethod.handleCallback({ ...buildCallbackContext(params), code: params.code })
758
+ )
759
+ );
258
760
 
259
- return {
761
+ return withRequestTraces(context, {
260
762
  output: callbackRes.output,
261
- input: callbackRes.input
262
- };
763
+ input: callbackRes.input,
764
+ scopes: callbackRes.scopes
765
+ });
263
766
  }
264
767
 
265
768
  throw new ServiceError(
@@ -274,24 +777,108 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
274
777
  let authMethod = getAuthMethod(slate, params.authenticationMethodId);
275
778
 
276
779
  if ('getAuthorizationUrl' in authMethod) {
277
- let urlRes = await authMethod.getAuthorizationUrl({
278
- redirectUri: params.redirectUri,
279
- state: params.state,
280
- input: params.input,
281
- clientId: params.clientId,
282
- clientSecret: params.clientSecret,
283
- scopes: params.scopes
284
- });
780
+ return getTracedAuthorizationUrl(
781
+ params,
782
+ authMethod.name,
783
+ {
784
+ functionName: 'getAuthorizationUrl',
785
+ message: 'Getting authentication authorization URL',
786
+ successMessage: 'Authentication authorization URL retrieved'
787
+ },
788
+ ctx => authMethod.getAuthorizationUrl(ctx)
789
+ );
790
+ }
285
791
 
286
- return {
287
- authorizationUrl: urlRes.url,
288
- input: urlRes.input
792
+ throw new ServiceError(
793
+ preconditionFailedError({
794
+ message: `Authentication method does not support authorization URL retrieval: ${params.authenticationMethodId}`
795
+ })
796
+ );
797
+ });
798
+
799
+ manager.onRequest('slates/auth.admin_authorization_url.get', async ({ params }) => {
800
+ getContextBasic();
801
+ let authMethod = getAuthMethod(slate, params.authenticationMethodId);
802
+
803
+ if (authMethod.type === 'auth.oauth' && authMethod.adminSetup) {
804
+ let { adminSetup } = authMethod;
805
+ return getTracedAuthorizationUrl(
806
+ params,
807
+ authMethod.name,
808
+ {
809
+ functionName: 'adminSetup.getAuthorizationUrl',
810
+ message: 'Getting admin setup authorization URL',
811
+ successMessage: 'Admin setup authorization URL retrieved',
812
+ metadata: { usesDefault: !adminSetup.getAuthorizationUrl }
813
+ },
814
+ ctx =>
815
+ adminSetup.getAuthorizationUrl
816
+ ? adminSetup.getAuthorizationUrl(ctx)
817
+ : authMethod.getAuthorizationUrl(ctx)
818
+ );
819
+ }
820
+
821
+ throw new ServiceError(
822
+ preconditionFailedError({
823
+ message: `Authentication method does not support admin setup: ${params.authenticationMethodId}`
824
+ })
825
+ );
826
+ });
827
+
828
+ manager.onRequest('slates/auth.admin_callback.handle', async ({ params }) => {
829
+ getContextBasic();
830
+ let authMethod = getAuthMethod(slate, params.authenticationMethodId);
831
+
832
+ if (authMethod.type === 'auth.oauth' && authMethod.adminSetup) {
833
+ let { adminSetup } = authMethod;
834
+ let ctx = { ...buildCallbackContext(params), code: params.code };
835
+
836
+ let handleCallback = async (): Promise<{ instructions?: string }> => {
837
+ if (adminSetup.handleCallback) return adminSetup.handleCallback(ctx);
838
+
839
+ let code = ctx.code;
840
+ if (!code) {
841
+ throw new ServiceError(
842
+ badRequestError({
843
+ message: `Admin setup callback did not include an authorization code: ${params.authenticationMethodId}`
844
+ })
845
+ );
846
+ }
847
+
848
+ await authMethod.handleCallback({ ...ctx, code });
849
+ return {};
289
850
  };
851
+
852
+ let context = getEmptyContext();
853
+ let callbackRes = await traceProviderCall(
854
+ {
855
+ component: 'auth',
856
+ functionName: 'adminSetup.handleCallback',
857
+ message: 'Handling admin setup callback',
858
+ successMessage: 'Admin setup callback handled',
859
+ metadata: {
860
+ authenticationMethodId: params.authenticationMethodId,
861
+ authenticationMethodName: authMethod.name,
862
+ scopeCount: params.scopes.length,
863
+ hasCode: !!params.code,
864
+ hasCallbackState: !!params.callbackState,
865
+ usesDefault: !adminSetup.handleCallback
866
+ },
867
+ onSuccess: result => ({
868
+ returnedInstructions: !!result.instructions
869
+ })
870
+ },
871
+ () => runWithContext(context, handleCallback)
872
+ );
873
+
874
+ return withRequestTraces(context, {
875
+ instructions: callbackRes.instructions
876
+ });
290
877
  }
291
878
 
292
879
  throw new ServiceError(
293
880
  preconditionFailedError({
294
- message: `Authentication method does not support authorization URL retrieval: ${params.authenticationMethodId}`
881
+ message: `Authentication method does not support admin setup: ${params.authenticationMethodId}`
295
882
  })
296
883
  );
297
884
  });
@@ -301,15 +888,47 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
301
888
  let authMethod = getAuthMethod(slate, params.authenticationMethodId);
302
889
 
303
890
  if (authMethod.getProfile) {
304
- let profileRes = await authMethod.getProfile({
305
- output: params.output as any,
306
- input: params.input,
307
- scopes: params.scopes
308
- });
891
+ let context = getAuthContext();
892
+ let profileRes = await traceProviderCall(
893
+ {
894
+ component: 'auth',
895
+ functionName: 'getProfile',
896
+ message: 'Getting authentication profile',
897
+ successMessage: 'Authentication profile retrieved',
898
+ metadata: {
899
+ authenticationMethodId: params.authenticationMethodId,
900
+ authenticationMethodName: authMethod.name,
901
+ scopeCount: params.scopes.length,
902
+ inputKeyCount: getObjectKeyCount(params.input),
903
+ outputKeyCount: getObjectKeyCount(params.output)
904
+ },
905
+ onSuccess: result => ({
906
+ profileKeyCount: getObjectKeyCount(result.profile)
907
+ })
908
+ },
909
+ () =>
910
+ runWithContext(context, () => {
911
+ if (authMethod.type === 'auth.oauth') {
912
+ return authMethod.getProfile!({
913
+ output: params.output as any,
914
+ input: params.input,
915
+ scopes: params.scopes,
916
+ config: getAuthConfig()
917
+ });
918
+ }
919
+
920
+ return authMethod.getProfile!({
921
+ output: params.output as any,
922
+ input: params.input,
923
+ scopes: params.scopes,
924
+ config: getAuthConfig()
925
+ })!;
926
+ })
927
+ );
309
928
 
310
- return {
929
+ return withRequestTraces(context, {
311
930
  profile: profileRes.profile
312
- };
931
+ });
313
932
  }
314
933
 
315
934
  throw new ServiceError(
@@ -324,18 +943,53 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
324
943
  let authMethod = getAuthMethod(slate, params.authenticationMethodId);
325
944
 
326
945
  if ('handleTokenRefresh' in authMethod && authMethod.handleTokenRefresh) {
327
- let refreshRes = await authMethod.handleTokenRefresh({
328
- output: params.output as any,
329
- input: params.input,
330
- clientId: params.clientId,
331
- clientSecret: params.clientSecret,
332
- scopes: params.scopes
333
- });
946
+ let context = getAuthContext();
947
+ let refreshRes = await traceProviderCall(
948
+ {
949
+ component: 'auth',
950
+ functionName: 'handleTokenRefresh',
951
+ message: 'Refreshing authentication token',
952
+ successMessage: 'Authentication token refreshed',
953
+ metadata: {
954
+ authenticationMethodId: params.authenticationMethodId,
955
+ authenticationMethodName: authMethod.name,
956
+ scopeCount: params.scopes.length,
957
+ inputKeyCount: getObjectKeyCount(params.input),
958
+ outputKeyCount: getObjectKeyCount(params.output)
959
+ },
960
+ onSuccess: result => ({
961
+ refreshedOutputKeyCount: getObjectKeyCount(result.output),
962
+ returnedInput: !!result.input
963
+ })
964
+ },
965
+ () =>
966
+ runWithContext(context, () => {
967
+ if (authMethod.type === 'auth.oauth') {
968
+ return authMethod.handleTokenRefresh!({
969
+ output: params.output as any,
970
+ input: params.input,
971
+ clientId: params.clientId,
972
+ clientSecret: params.clientSecret,
973
+ scopes: params.scopes,
974
+ config: getAuthConfig()
975
+ });
976
+ }
977
+
978
+ return authMethod.handleTokenRefresh!({
979
+ output: params.output as any,
980
+ input: params.input,
981
+ clientId: params.clientId,
982
+ clientSecret: params.clientSecret,
983
+ scopes: params.scopes,
984
+ config: getAuthConfig()
985
+ });
986
+ })
987
+ );
334
988
 
335
- return {
989
+ return withRequestTraces(context, {
336
990
  output: refreshRes.output,
337
991
  input: refreshRes.input
338
- };
992
+ });
339
993
  }
340
994
 
341
995
  throw new ServiceError(
@@ -348,22 +1002,77 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
348
1002
  manager.onRequest('slates/actions.list', async ({ params }) => {
349
1003
  getContextBasic();
350
1004
 
1005
+ let actions = getExposedActions(
1006
+ slate,
1007
+ !!params.includeAdapterActions && supportsAdapters()
1008
+ );
1009
+
1010
+ if (!supportsTriggerGroups()) {
1011
+ actions = actions.filter(action => action.type !== 'trigger');
1012
+ }
1013
+
1014
+ return {
1015
+ actions: actions.map(a => mapAction(slate, a))
1016
+ };
1017
+ });
1018
+
1019
+ manager.onRequest('slates/adapters.list', async () => {
1020
+ getContextBasic();
1021
+
1022
+ return {
1023
+ adapters: supportsAdapters() ? (slate.adapters ?? []).map(mapAdapter) : []
1024
+ };
1025
+ });
1026
+
1027
+ manager.onRequest('slates/adapter.get', async ({ params }) => {
1028
+ getContextBasic();
1029
+ if (!supportsAdapters()) {
1030
+ throw new ServiceError(notFoundError(`adapter`, params.adapterId));
1031
+ }
1032
+ let adapter = getAdapter(slate, params.adapterId);
1033
+
351
1034
  return {
352
- actions: slate.actions.map(a => mapAction(slate, a))
1035
+ adapter: mapAdapter(adapter)
353
1036
  };
354
1037
  });
355
1038
 
356
1039
  manager.onRequest('slates/action.get', async ({ params }) => {
357
1040
  getContextBasic();
358
- let action = getAction(slate, params.actionId);
1041
+ let action = getExposedAction(slate, params.actionId, supportsAdapters());
359
1042
 
360
1043
  return {
361
1044
  action: mapAction(slate, action)
362
1045
  };
363
1046
  });
364
1047
 
1048
+ manager.onRequest('slates/trigger_groups.list', async () => {
1049
+ getContextBasic();
1050
+
1051
+ return {
1052
+ triggerGroups: getExposedTriggerGroups(slate, supportsAdapters()).map(mapTriggerGroup)
1053
+ };
1054
+ });
1055
+
1056
+ manager.onRequest('slates/tool_categories.list', async () => {
1057
+ getContextBasic();
1058
+
1059
+ return {
1060
+ toolCategories: getExposedToolCategories(slate, supportsAdapters()).map(
1061
+ mapToolCategory
1062
+ )
1063
+ };
1064
+ });
1065
+
1066
+ manager.onRequest('slates/trigger_group.get', async ({ params }) => {
1067
+ getContextBasic();
1068
+ let triggerGroup = getTriggerGroup(slate, params.triggerGroupId);
1069
+
1070
+ return {
1071
+ triggerGroup: mapTriggerGroup(triggerGroup)
1072
+ };
1073
+ });
1074
+
365
1075
  manager.onRequest('slates/action.tool.invoke', async ({ params }) => {
366
- let ctx = getContextFull();
367
1076
  let action = getActionWithType(slate, 'tool', params.actionId);
368
1077
 
369
1078
  let input = validate(
@@ -373,11 +1082,69 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
373
1082
  `Invalid input for tool ID: ${params.actionId}`
374
1083
  );
375
1084
 
376
- let res = await action.handleInvocation(
377
- new SlateContext(ctx.config, input, ctx.auth?.output!, slate.spec, logger)
378
- );
1085
+ let invoke = async (context: SlatePublicContext<any>) => {
1086
+ let res = await traceProviderCall(
1087
+ {
1088
+ component: 'action',
1089
+ functionName: 'handleInvocation',
1090
+ message: `Starting tool ${formatEntityLabel(action.name, action.key)}`,
1091
+ successMessage: `Completed tool ${formatEntityLabel(action.name, action.key)}`,
1092
+ errorMessage: `Tool ${formatEntityLabel(action.name, action.key)} failed`,
1093
+ metadata: {
1094
+ actionId: action.key,
1095
+ actionName: action.name,
1096
+ actionType: action.type,
1097
+ isPublic: action.isPublic,
1098
+ inputKeyCount: getObjectKeyCount(input)
1099
+ },
1100
+ onSuccess: result => ({
1101
+ hasMessage: !!result.message,
1102
+ actionResultMessage: result.message,
1103
+ outputKeyCount: getObjectKeyCount(result.output),
1104
+ attachmentCount: result.attachments?.length
1105
+ })
1106
+ },
1107
+ () => runWithContext(context, () => action.handleInvocation(context as any))
1108
+ );
1109
+
1110
+ let contextAttachments = await context._finalizeAttachments();
1111
+ let merged = mergeAttachments(
1112
+ [...(res.attachments ?? []), ...contextAttachments],
1113
+ res.output
1114
+ );
1115
+
1116
+ let redacted = redactUrlAttachmentSecrets(
1117
+ merged,
1118
+ context._getAuthConfigForRedaction()
1119
+ );
1120
+ let finalAttachments = await routeAttachmentsThroughDirectUpload(
1121
+ redacted,
1122
+ getLiveInvocation()
1123
+ );
379
1124
 
380
- return { output: res.output, message: res.message };
1125
+ return withRequestTraces(context, {
1126
+ output: res.output,
1127
+ message: res.message,
1128
+ attachments: finalAttachments
1129
+ });
1130
+ };
1131
+
1132
+ if (action.isPublic) {
1133
+ getContextBasic();
1134
+ return invoke(new SlatePublicContext(input, slate.spec, logger, getLiveInvocation()));
1135
+ }
1136
+
1137
+ let ctx = getContextFull();
1138
+ return invoke(
1139
+ new SlateContext(
1140
+ ctx.config,
1141
+ input,
1142
+ ctx.auth?.output!,
1143
+ slate.spec,
1144
+ logger,
1145
+ getLiveInvocation()
1146
+ )
1147
+ );
381
1148
  });
382
1149
 
383
1150
  manager.onRequest('slates/action.trigger.map_event', async ({ params }) => {
@@ -391,46 +1158,283 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
391
1158
  `Invalid event for trigger ID: ${params.actionId}`
392
1159
  );
393
1160
 
394
- let res = await action.handleEvent(
395
- new SlateContext(ctx.config, input, ctx.auth?.output!, slate.spec, logger)
1161
+ let context = new SlateContext(ctx.config, input, ctx.auth?.output!, slate.spec, logger);
1162
+ let res = await traceProviderCall(
1163
+ {
1164
+ component: 'action',
1165
+ functionName: 'handleEvent',
1166
+ message: `Mapping event for trigger ${formatEntityLabel(action.name, action.key)}`,
1167
+ successMessage: result =>
1168
+ `Mapped trigger event "${result.type}" for ${formatEntityLabel(action.name, action.key)}`,
1169
+ errorMessage: `Trigger ${formatEntityLabel(action.name, action.key)} failed while mapping an event`,
1170
+ metadata: {
1171
+ actionId: action.key,
1172
+ actionName: action.name,
1173
+ actionType: action.type,
1174
+ inputKeyCount: getObjectKeyCount(input)
1175
+ },
1176
+ onSuccess: result => ({
1177
+ eventType: result.type,
1178
+ hasEventId: !!result.id,
1179
+ outputKeyCount: getObjectKeyCount(result.output)
1180
+ })
1181
+ },
1182
+ () => runWithContext(context, () => action.map(context))
396
1183
  );
397
1184
 
398
- return { id: res.id, type: res.type, output: res.output };
1185
+ return withRequestTraces(context, { id: res.id, type: res.type, output: res.output });
399
1186
  });
400
1187
 
401
1188
  manager.onRequest('slates/action.trigger.poll_events', async ({ params }) => {
402
- let ctx = getContextFull();
403
- let action = getActionWithType(slate, 'trigger', params.actionId);
1189
+ getContextBasic();
404
1190
 
405
- if (!action.pollEvents) {
1191
+ if (supportsTriggerGroups()) {
406
1192
  throw new ServiceError(
407
1193
  badRequestError({
408
- message: `Trigger action does not support polling: ${params.actionId}`
1194
+ message: `Legacy trigger polling is disabled once trigger_group support is announced: ${params.actionId}`
409
1195
  })
410
1196
  );
411
1197
  }
412
1198
 
413
- let res = await action.pollEvents(
414
- new SlateContext(
415
- ctx.config,
416
- { state: params.state },
417
- ctx.auth?.output!,
418
- slate.spec,
419
- logger
420
- )
1199
+ return { inputs: [], updatedState: params.state };
1200
+ });
1201
+
1202
+ manager.onRequest('slates/action.trigger.webhook_handle', async ({ params }) => {
1203
+ getContextBasic();
1204
+
1205
+ if (supportsTriggerGroups()) {
1206
+ throw new ServiceError(
1207
+ badRequestError({
1208
+ message: `Legacy trigger webhook handling is disabled once trigger_group support is announced: ${params.actionId}`
1209
+ })
1210
+ );
1211
+ }
1212
+
1213
+ return { inputs: [], updatedState: params.state, response: null };
1214
+ });
1215
+
1216
+ manager.onRequest('slates/action.trigger.webhook_register', async ({ params }) => {
1217
+ getContextBasic();
1218
+
1219
+ if (supportsTriggerGroups()) {
1220
+ throw new ServiceError(
1221
+ badRequestError({
1222
+ message: `Legacy trigger webhook registration is disabled once trigger_group support is announced: ${params.actionId}`
1223
+ })
1224
+ );
1225
+ }
1226
+
1227
+ return { registrationDetails: null };
1228
+ });
1229
+
1230
+ manager.onRequest('slates/action.trigger.webhook_unregister', async ({ params }) => {
1231
+ getContextBasic();
1232
+
1233
+ if (supportsTriggerGroups()) {
1234
+ throw new ServiceError(
1235
+ badRequestError({
1236
+ message: `Legacy trigger webhook unregistration is disabled once trigger_group support is announced: ${params.actionId}`
1237
+ })
1238
+ );
1239
+ }
1240
+
1241
+ return {};
1242
+ });
1243
+
1244
+ manager.onRequest('slates/trigger_group.webhook.targets_list', async ({ params }) => {
1245
+ let ctx = getContextFull();
1246
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1247
+ let autoRegistration = getWebhookAutoRegistration(group);
1248
+
1249
+ let context = new SlateContext(
1250
+ ctx.config,
1251
+ { pageToken: params.pageToken ?? null },
1252
+ ctx.auth?.output!,
1253
+ slate.spec,
1254
+ logger
1255
+ );
1256
+ let res = await traceProviderCall(
1257
+ {
1258
+ component: 'action',
1259
+ functionName: 'webhookTargetList',
1260
+ message: `Listing webhook targets for trigger group ${formatEntityLabel(group.name, group.key)}`,
1261
+ successMessage: result =>
1262
+ `Listed ${result.targets.length} webhook target(s) for trigger group ${formatEntityLabel(group.name, group.key)}`,
1263
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while listing webhook targets`,
1264
+ metadata: {
1265
+ triggerGroupId: group.key,
1266
+ triggerGroupName: group.name
1267
+ },
1268
+ onSuccess: result => ({
1269
+ targetCount: result.targets.length,
1270
+ hasNextPageToken: result.nextPageToken !== null
1271
+ })
1272
+ },
1273
+ () => runWithContext(context, () => autoRegistration.webhookTargetList(context))
421
1274
  );
422
1275
 
423
- return { inputs: res.inputs, updatedState: res.updatedState };
1276
+ return withRequestTraces(context, {
1277
+ targets: res.targets,
1278
+ nextPageToken: res.nextPageToken ?? null,
1279
+ isPartial: res.isPartial ?? false
1280
+ });
424
1281
  });
425
1282
 
426
- manager.onRequest('slates/action.trigger.webhook_handle', async ({ params }) => {
1283
+ manager.onRequest('slates/trigger_group.webhook.register', async ({ params }) => {
427
1284
  let ctx = getContextFull();
428
- let action = getActionWithType(slate, 'trigger', params.actionId);
1285
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1286
+ let autoRegistration = getWebhookAutoRegistration(group);
1287
+
1288
+ let context = new SlateContext(
1289
+ ctx.config,
1290
+ {
1291
+ webhookTargetIdentifier: params.webhookTargetIdentifier,
1292
+ webhookTargetPayload: params.webhookTargetPayload,
1293
+ webhookUrl: params.webhookUrl
1294
+ },
1295
+ ctx.auth?.output!,
1296
+ slate.spec,
1297
+ logger
1298
+ );
1299
+ let res = await traceProviderCall(
1300
+ {
1301
+ component: 'action',
1302
+ functionName: 'webhookRegister',
1303
+ message: `Registering webhook for trigger group ${formatEntityLabel(group.name, group.key)}`,
1304
+ successMessage: `Registered webhook for trigger group ${formatEntityLabel(group.name, group.key)}`,
1305
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while registering a webhook`,
1306
+ metadata: {
1307
+ triggerGroupId: group.key,
1308
+ triggerGroupName: group.name,
1309
+ webhookTargetIdentifier: params.webhookTargetIdentifier
1310
+ }
1311
+ },
1312
+ () => runWithContext(context, () => autoRegistration.webhookRegister(context))
1313
+ );
429
1314
 
430
- if (!action.handleRequest) {
1315
+ return withRequestTraces(context, {
1316
+ webhookRegistrationIdentifier: res.webhookRegistrationIdentifier,
1317
+ webhookRegistrationPayload: res.webhookRegistrationPayload
1318
+ });
1319
+ });
1320
+
1321
+ manager.onRequest('slates/trigger_group.webhook.unregister', async ({ params }) => {
1322
+ let ctx = getContextFull();
1323
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1324
+ let autoRegistration = getWebhookAutoRegistration(group);
1325
+
1326
+ let context = new SlateContext(
1327
+ ctx.config,
1328
+ {
1329
+ webhookRegistrationIdentifier: params.webhookRegistrationIdentifier,
1330
+ webhookRegistrationPayload: params.webhookRegistrationPayload
1331
+ },
1332
+ ctx.auth?.output!,
1333
+ slate.spec,
1334
+ logger
1335
+ );
1336
+ await traceProviderCall(
1337
+ {
1338
+ component: 'action',
1339
+ functionName: 'webhookUnregister',
1340
+ message: `Unregistering webhook for trigger group ${formatEntityLabel(group.name, group.key)}`,
1341
+ successMessage: `Unregistered webhook for trigger group ${formatEntityLabel(group.name, group.key)}`,
1342
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while unregistering a webhook`,
1343
+ metadata: {
1344
+ triggerGroupId: group.key,
1345
+ triggerGroupName: group.name,
1346
+ webhookRegistrationIdentifier: params.webhookRegistrationIdentifier
1347
+ }
1348
+ },
1349
+ () => runWithContext(context, () => autoRegistration.webhookUnregister(context))
1350
+ );
1351
+
1352
+ return withRequestTraces(context, {});
1353
+ });
1354
+
1355
+ manager.onRequest('slates/trigger_group.webhook.manual_setup', async ({ params }) => {
1356
+ getContextBasic();
1357
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1358
+ let manualRegistration = getWebhookManualRegistration(group);
1359
+
1360
+ let context = new SlateContext(
1361
+ {},
1362
+ { webhookUrl: params.webhookUrl },
1363
+ {},
1364
+ slate.spec as any,
1365
+ logger
1366
+ );
1367
+ let res = await traceProviderCall(
1368
+ {
1369
+ component: 'action',
1370
+ functionName: 'webhookManualSetup',
1371
+ message: `Building manual webhook setup for trigger group ${formatEntityLabel(group.name, group.key)}`,
1372
+ successMessage: `Built manual webhook setup for trigger group ${formatEntityLabel(group.name, group.key)}`,
1373
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while building a manual webhook setup`,
1374
+ metadata: {
1375
+ triggerGroupId: group.key,
1376
+ triggerGroupName: group.name
1377
+ }
1378
+ },
1379
+ () => runWithContext(context, () => manualRegistration.setup(context))
1380
+ );
1381
+
1382
+ return {
1383
+ webhookSetupDocument: res.webhookSetupDocument,
1384
+ partialWebhookRegistrationPayload: res.partialWebhookRegistrationPayload
1385
+ };
1386
+ });
1387
+
1388
+ manager.onRequest('slates/trigger_group.webhook.manual_finish', async ({ params }) => {
1389
+ getContextBasic();
1390
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1391
+ let manualRegistration = getWebhookManualRegistration(group);
1392
+
1393
+ if (!manualRegistration.finish) {
1394
+ throw new ServiceError(
1395
+ badRequestError({
1396
+ message: `Trigger group does not support manual webhook finish: ${params.triggerGroupId}`
1397
+ })
1398
+ );
1399
+ }
1400
+
1401
+ let context = new SlateContext(
1402
+ {},
1403
+ {
1404
+ webhookUrl: params.webhookUrl,
1405
+ partialWebhookRegistrationPayload: params.partialWebhookRegistrationPayload,
1406
+ userWebhookRegistrationPayload: params.userWebhookRegistrationPayload
1407
+ },
1408
+ {},
1409
+ slate.spec as any,
1410
+ logger
1411
+ );
1412
+ let res = await traceProviderCall(
1413
+ {
1414
+ component: 'action',
1415
+ functionName: 'webhookManualFinish',
1416
+ message: `Finishing manual webhook setup for trigger group ${formatEntityLabel(group.name, group.key)}`,
1417
+ successMessage: `Finished manual webhook setup for trigger group ${formatEntityLabel(group.name, group.key)}`,
1418
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while finishing a manual webhook setup`,
1419
+ metadata: {
1420
+ triggerGroupId: group.key,
1421
+ triggerGroupName: group.name
1422
+ }
1423
+ },
1424
+ () => runWithContext(context, () => manualRegistration.finish!(context))
1425
+ );
1426
+
1427
+ return withRequestTraces(context, {
1428
+ webhookRegistrationPayload: res.webhookRegistrationPayload
1429
+ });
1430
+ });
1431
+
1432
+ manager.onRequest('slates/trigger_group.webhook.process', async ({ params }) => {
1433
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1434
+ if (group.source !== 'webhook' || !group.webhook) {
431
1435
  throw new ServiceError(
432
1436
  badRequestError({
433
- message: `Trigger action does not support webhook requests: ${params.actionId}`
1437
+ message: `Trigger group does not support webhook processing: ${params.triggerGroupId}`
434
1438
  })
435
1439
  );
436
1440
  }
@@ -443,70 +1447,233 @@ export let createProviderHandler = <ConfigType extends {}, AuthType extends {}>(
443
1447
  : null
444
1448
  });
445
1449
 
446
- let res = await action.handleRequest(
447
- new SlateContext(
448
- ctx.config,
449
- { request: req, state: params.state },
450
- ctx.auth?.output!,
451
- slate.spec,
452
- logger
453
- )
1450
+ let context = new SlateContext(
1451
+ {},
1452
+ {
1453
+ request: req,
1454
+ webhookRegistrationPayload: params.webhookRegistrationPayload
1455
+ },
1456
+ {},
1457
+ slate.spec as any,
1458
+ logger
1459
+ );
1460
+ let res = await traceProviderCall(
1461
+ {
1462
+ component: 'action',
1463
+ functionName: 'webhookProcess',
1464
+ message: `Processing webhook request for trigger group ${formatEntityLabel(group.name, group.key)}`,
1465
+ successMessage: result =>
1466
+ `Extracted ${result.events.length} event(s) from webhook request for trigger group ${formatEntityLabel(group.name, group.key)}`,
1467
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while processing a webhook request`,
1468
+ metadata: {
1469
+ triggerGroupId: group.key,
1470
+ triggerGroupName: group.name,
1471
+ requestMethod: params.method,
1472
+ hasRequestBody: !!params.body
1473
+ },
1474
+ onSuccess: result => ({
1475
+ eventCount: result.events.length,
1476
+ hasResponse: result.response !== undefined,
1477
+ skipped: !!result.skipped
1478
+ })
1479
+ },
1480
+ () => runWithContext(context, () => group.webhook!.process(context))
454
1481
  );
455
1482
 
456
- return { inputs: res.inputs, updatedState: res.updatedState };
1483
+ for (let event of res.events) {
1484
+ if (!event.matchers) {
1485
+ throw new ServiceError(
1486
+ preconditionFailedError({
1487
+ message: `Trigger group "${group.key}" process handler must return matchers for every event (return [] if there is nothing to assert)`
1488
+ })
1489
+ );
1490
+ }
1491
+ }
1492
+
1493
+ let response =
1494
+ res.response === undefined
1495
+ ? undefined
1496
+ : await serializeWebhookHttpResponse(res.response);
1497
+
1498
+ return withRequestTraces(context, {
1499
+ events: res.events.map(event => ({
1500
+ matchers: event.matchers,
1501
+ payload: event.payload,
1502
+ idempotencyKey: event.idempotencyKey,
1503
+ triggerIds: evaluateTriggerMatches(slate, group.key, event.payload)
1504
+ })),
1505
+ response,
1506
+ skipped: res.skipped ?? null
1507
+ });
457
1508
  });
458
1509
 
459
- manager.onRequest('slates/action.trigger.webhook_register', async ({ params }) => {
1510
+ manager.onRequest('slates/trigger_group.routing_matchers.get', async ({ params }) => {
460
1511
  let ctx = getContextFull();
461
- let action = getActionWithType(slate, 'trigger', params.actionId);
1512
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1513
+
1514
+ let context = new SlateContext(ctx.config, {}, ctx.auth?.output!, slate.spec, logger);
1515
+ let matchers = await traceProviderCall(
1516
+ {
1517
+ component: 'action',
1518
+ functionName: 'routingMatchers',
1519
+ message: `Getting routing matchers for trigger group ${formatEntityLabel(group.name, group.key)}`,
1520
+ successMessage: result =>
1521
+ `Retrieved ${result.length} routing matcher(s) for trigger group ${formatEntityLabel(group.name, group.key)}`,
1522
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while getting routing matchers`,
1523
+ metadata: {
1524
+ triggerGroupId: group.key,
1525
+ triggerGroupName: group.name
1526
+ },
1527
+ onSuccess: result => ({
1528
+ matcherCount: result.length
1529
+ })
1530
+ },
1531
+ () => runWithContext(context, () => group.routingMatchers(context))
1532
+ );
1533
+
1534
+ return withRequestTraces(context, { matchers });
1535
+ });
462
1536
 
463
- if (!action.autoRegisterWebhook) {
1537
+ manager.onRequest('slates/trigger_group.polling.poll', async ({ params }) => {
1538
+ let ctx = getContextFull();
1539
+ let group = getTriggerGroup(slate, params.triggerGroupId);
1540
+ if (group.source !== 'polling' || !group.polling) {
464
1541
  throw new ServiceError(
465
1542
  badRequestError({
466
- message: `Trigger action does not support webhook auto-registration: ${params.actionId}`
1543
+ message: `Trigger group does not support polling: ${params.triggerGroupId}`
467
1544
  })
468
1545
  );
469
1546
  }
470
1547
 
471
- let res = await action.autoRegisterWebhook(
472
- new SlateContext(
473
- ctx.config,
474
- { webhookBaseUrl: params.webhookBaseUrl },
475
- ctx.auth?.output!,
476
- slate.spec,
477
- logger
478
- )
1548
+ let context = new SlateContext(
1549
+ ctx.config,
1550
+ { state: params.state },
1551
+ ctx.auth?.output!,
1552
+ slate.spec,
1553
+ logger
1554
+ );
1555
+ let res = await traceProviderCall(
1556
+ {
1557
+ component: 'action',
1558
+ functionName: 'pollEvents',
1559
+ message: `Polling events for trigger group ${formatEntityLabel(group.name, group.key)}`,
1560
+ successMessage: result =>
1561
+ `Polled ${result.events.length} event(s) for trigger group ${formatEntityLabel(group.name, group.key)}`,
1562
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while polling events`,
1563
+ metadata: {
1564
+ triggerGroupId: group.key,
1565
+ triggerGroupName: group.name,
1566
+ hasPreviousState: params.state !== null
1567
+ },
1568
+ onSuccess: result => ({
1569
+ eventCount: result.events.length,
1570
+ hasUpdatedState: result.updatedState !== undefined
1571
+ })
1572
+ },
1573
+ () => runWithContext(context, () => group.polling!.pollEvents(context))
479
1574
  );
480
1575
 
481
- return { registrationDetails: res.registrationDetails, state: res.state };
1576
+ return withRequestTraces(context, {
1577
+ updatedState: res.updatedState,
1578
+ events: res.events.map(event => ({
1579
+ payload: event.payload,
1580
+ idempotencyKey: event.idempotencyKey,
1581
+ triggerIds: evaluateTriggerMatches(slate, group.key, event.payload)
1582
+ }))
1583
+ });
482
1584
  });
483
1585
 
484
- manager.onRequest('slates/action.trigger.webhook_unregister', async ({ params }) => {
485
- let ctx = getContextFull();
486
- let action = getActionWithType(slate, 'trigger', params.actionId);
487
-
488
- if (!action.autoUnregisterWebhook) {
1586
+ let getGatewayTriggerGroup = (triggerGroupId: string) => {
1587
+ let group = getTriggerGroup(slate, triggerGroupId);
1588
+ if (group.source !== 'gateway' || !group.gateway) {
489
1589
  throw new ServiceError(
490
1590
  badRequestError({
491
- message: `Trigger action does not support webhook auto-unregistration: ${params.actionId}`
1591
+ message: `Trigger group does not support gateway connections: ${triggerGroupId}`
492
1592
  })
493
1593
  );
494
1594
  }
1595
+ return { group, gateway: group.gateway };
1596
+ };
495
1597
 
496
- await action.autoUnregisterWebhook(
497
- new SlateContext(
498
- ctx.config,
499
- {
500
- webhookBaseUrl: params.webhookBaseUrl,
501
- registrationDetails: params.registrationDetails,
502
- state: params.state
1598
+ manager.onRequest('slates/trigger_group.gateway.connect', async ({ params }) => {
1599
+ let ctx = getContextFull();
1600
+ let { group, gateway } = getGatewayTriggerGroup(params.triggerGroupId);
1601
+
1602
+ let context = new SlateContext(
1603
+ ctx.config,
1604
+ { state: params.state },
1605
+ ctx.auth?.output!,
1606
+ slate.spec,
1607
+ logger
1608
+ );
1609
+ let res = await traceProviderCall(
1610
+ {
1611
+ component: 'action',
1612
+ functionName: 'gatewayConnect',
1613
+ message: `Preparing gateway connection for trigger group ${formatEntityLabel(group.name, group.key)}`,
1614
+ successMessage: () =>
1615
+ `Prepared gateway connection for trigger group ${formatEntityLabel(group.name, group.key)}`,
1616
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while preparing a gateway connection`,
1617
+ metadata: {
1618
+ triggerGroupId: group.key,
1619
+ triggerGroupName: group.name,
1620
+ hasPreviousState: params.state !== null
1621
+ }
1622
+ },
1623
+ () => runWithContext(context, () => gateway.connect(context))
1624
+ );
1625
+
1626
+ return withRequestTraces(context, { url: res.url, state: res.state });
1627
+ });
1628
+
1629
+ manager.onRequest('slates/trigger_group.gateway.receive', async ({ params }) => {
1630
+ let ctx = getContextFull();
1631
+ let { group, gateway } = getGatewayTriggerGroup(params.triggerGroupId);
1632
+
1633
+ let context = new SlateContext(
1634
+ ctx.config,
1635
+ { state: params.state, frames: params.frames, closed: params.closed ?? null },
1636
+ ctx.auth?.output!,
1637
+ slate.spec,
1638
+ logger
1639
+ );
1640
+ let res = await traceProviderCall(
1641
+ {
1642
+ component: 'action',
1643
+ functionName: 'gatewayReceive',
1644
+ message: `Handling ${params.frames.length} gateway frame(s) for trigger group ${formatEntityLabel(group.name, group.key)}`,
1645
+ successMessage: result =>
1646
+ `Handled gateway frames for trigger group ${formatEntityLabel(group.name, group.key)} (${result.events?.length ?? 0} event(s))`,
1647
+ errorMessage: `Trigger group ${formatEntityLabel(group.name, group.key)} failed while handling gateway frames`,
1648
+ metadata: {
1649
+ triggerGroupId: group.key,
1650
+ triggerGroupName: group.name,
1651
+ frameCount: params.frames.length,
1652
+ closed: params.closed ?? null
503
1653
  },
504
- ctx.auth?.output!,
505
- slate.spec,
506
- logger
507
- )
1654
+ onSuccess: result => ({
1655
+ eventCount: result.events?.length ?? 0,
1656
+ sendCount: result.send?.length ?? 0,
1657
+ close: result.close ?? null
1658
+ })
1659
+ },
1660
+ () => runWithContext(context, () => gateway.receive(context))
508
1661
  );
509
1662
 
510
- return {};
1663
+ return withRequestTraces(context, {
1664
+ state: res.state,
1665
+ // Outbound frames (e.g. IDENTIFY) embed credentials; the hub restores them at the socket.
1666
+ send: new AuthConfigSecretRedactor(
1667
+ context._getAuthConfigForRedaction()
1668
+ ).redactEmbedded(res.send ?? []),
1669
+ heartbeat: res.heartbeat ?? null,
1670
+ heartbeatAcked: res.heartbeatAcked,
1671
+ close: res.close ?? null,
1672
+ events: (res.events ?? []).map(event => ({
1673
+ payload: event.payload,
1674
+ idempotencyKey: event.idempotencyKey,
1675
+ triggerIds: evaluateTriggerMatches(slate, group.key, event.payload)
1676
+ }))
1677
+ });
511
1678
  });
512
1679
  });