@shipfox/api-integration-core 5.0.0 → 6.0.0

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 (73) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +65 -0
  3. package/dist/core/entities/provider.d.ts +7 -2
  4. package/dist/core/entities/provider.d.ts.map +1 -1
  5. package/dist/core/entities/provider.js.map +1 -1
  6. package/dist/core/errors.d.ts +7 -0
  7. package/dist/core/errors.d.ts.map +1 -1
  8. package/dist/core/errors.js +8 -2
  9. package/dist/core/errors.js.map +1 -1
  10. package/dist/index.d.ts +17 -4
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +50 -5
  13. package/dist/index.js.map +1 -1
  14. package/dist/presentation/inter-module.d.ts +9 -0
  15. package/dist/presentation/inter-module.d.ts.map +1 -0
  16. package/dist/presentation/inter-module.js +121 -0
  17. package/dist/presentation/inter-module.js.map +1 -0
  18. package/dist/presentation/routes/agent-tools-gateway/index.d.ts +1 -0
  19. package/dist/presentation/routes/agent-tools-gateway/index.d.ts.map +1 -1
  20. package/dist/presentation/routes/agent-tools-gateway/index.js +1 -0
  21. package/dist/presentation/routes/agent-tools-gateway/index.js.map +1 -1
  22. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts +5 -2
  23. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.d.ts.map +1 -1
  24. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js +53 -17
  25. package/dist/presentation/routes/agent-tools-gateway/resolve-authorized-tools.js.map +1 -1
  26. package/dist/presentation/routes/index.d.ts +2 -3
  27. package/dist/presentation/routes/index.d.ts.map +1 -1
  28. package/dist/presentation/routes/index.js +2 -2
  29. package/dist/presentation/routes/index.js.map +1 -1
  30. package/dist/providers/gitea.d.ts.map +1 -1
  31. package/dist/providers/gitea.js +10 -8
  32. package/dist/providers/gitea.js.map +1 -1
  33. package/dist/providers/github.d.ts.map +1 -1
  34. package/dist/providers/github.js +18 -13
  35. package/dist/providers/github.js.map +1 -1
  36. package/dist/providers/jira.d.ts.map +1 -1
  37. package/dist/providers/jira.js +4 -1
  38. package/dist/providers/jira.js.map +1 -1
  39. package/dist/providers/linear.d.ts.map +1 -1
  40. package/dist/providers/linear.js +34 -29
  41. package/dist/providers/linear.js.map +1 -1
  42. package/dist/providers/sentry.d.ts.map +1 -1
  43. package/dist/providers/sentry.js +13 -11
  44. package/dist/providers/sentry.js.map +1 -1
  45. package/dist/providers/slack.d.ts.map +1 -1
  46. package/dist/providers/slack.js +34 -29
  47. package/dist/providers/slack.js.map +1 -1
  48. package/dist/providers/types.d.ts +12 -0
  49. package/dist/providers/types.d.ts.map +1 -1
  50. package/dist/providers/types.js.map +1 -1
  51. package/dist/providers/webhook.d.ts.map +1 -1
  52. package/dist/providers/webhook.js +11 -9
  53. package/dist/providers/webhook.js.map +1 -1
  54. package/dist/tsconfig.test.tsbuildinfo +1 -1
  55. package/package.json +22 -17
  56. package/src/core/entities/provider.ts +7 -1
  57. package/src/core/errors.ts +12 -2
  58. package/src/index.test.ts +112 -0
  59. package/src/index.ts +102 -9
  60. package/src/presentation/inter-module.ts +155 -0
  61. package/src/presentation/routes/agent-tools-gateway/index.ts +1 -0
  62. package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.test.ts +36 -0
  63. package/src/presentation/routes/agent-tools-gateway/resolve-authorized-tools.ts +64 -22
  64. package/src/presentation/routes/index.ts +4 -5
  65. package/src/providers/gitea.ts +11 -8
  66. package/src/providers/github.ts +17 -11
  67. package/src/providers/jira.ts +3 -0
  68. package/src/providers/linear.ts +30 -24
  69. package/src/providers/sentry.ts +15 -12
  70. package/src/providers/slack.ts +31 -25
  71. package/src/providers/types.ts +15 -0
  72. package/src/providers/webhook.ts +11 -9
  73. package/tsconfig.build.tsbuildinfo +1 -1
@@ -4,6 +4,11 @@ import {
4
4
  materializedAgentStepConfigSchema,
5
5
  } from '@shipfox/api-agent-dto';
6
6
  import {requireLeasedJobContext} from '@shipfox/api-auth-context';
7
+ import {
8
+ type WorkflowsModuleClient,
9
+ workflowsInterModuleContract,
10
+ } from '@shipfox/api-workflows-dto/inter-module';
11
+ import {isInterModuleKnownError} from '@shipfox/inter-module';
7
12
  import {ClientError} from '@shipfox/node-fastify';
8
13
  import type {IntegrationConnection} from '#core/entities/connection.js';
9
14
  import type {IntegrationProviderKind} from '#core/entities/provider.js';
@@ -16,10 +21,49 @@ export type LeasedAgentStepLoader = (params: {
16
21
  stepId: string;
17
22
  attempt: number;
18
23
  }) => Promise<{
19
- step: {type: string; config: Record<string, unknown>};
20
24
  workspaceId: string;
25
+ integrations?: MaterializedAgentIntegrationConfigDto[];
26
+ step?: {type: string; config: Record<string, unknown>};
21
27
  }>;
22
28
 
29
+ export function createWorkflowsLeasedAgentStepLoader(
30
+ workflows: WorkflowsModuleClient,
31
+ ): LeasedAgentStepLoader {
32
+ return async ({request, stepId, attempt}) => {
33
+ const leasedJob = requireLeasedJobContext(request);
34
+ try {
35
+ const context = await workflows.getLeasedAgentToolContext({
36
+ jobId: leasedJob.jobId,
37
+ jobExecutionId: leasedJob.jobExecutionId,
38
+ runnerSessionId: leasedJob.runnerSessionId,
39
+ stepId,
40
+ attempt,
41
+ });
42
+ return {
43
+ workspaceId: context.workspaceId,
44
+ integrations: context.integrations,
45
+ };
46
+ } catch (error) {
47
+ throw toLeasedAgentToolClientError(error);
48
+ }
49
+ };
50
+ }
51
+
52
+ function toLeasedAgentToolClientError(error: unknown): unknown {
53
+ const method = workflowsInterModuleContract.methods.getLeasedAgentToolContext;
54
+ if (!isInterModuleKnownError(method, error)) return error;
55
+
56
+ const status = [
57
+ 'step-attempt-mismatch',
58
+ 'step-not-running',
59
+ 'leased-step-not-agent',
60
+ 'agent-step-config-invalid',
61
+ ].includes(error.code)
62
+ ? 409
63
+ : 404;
64
+ return new ClientError(error.code.replaceAll('-', ' '), error.code, {status});
65
+ }
66
+
23
67
  export interface AuthorizedIntegrationTool {
24
68
  mcpName: string;
25
69
  integration: MaterializedAgentIntegrationConfigDto;
@@ -49,18 +93,13 @@ export async function resolveAuthorizedIntegrationTools(
49
93
  });
50
94
  }
51
95
 
52
- const {step, workspaceId} = await params.loadLeasedAgentStep({
96
+ const loaded = await params.loadLeasedAgentStep({
53
97
  request: params.request,
54
98
  stepId: leasedJob.currentStepId,
55
99
  attempt: leasedJob.currentStepAttempt,
56
100
  });
57
- if (step.type !== 'agent') {
58
- throw new ClientError('Current leased step is not an agent step', 'leased-step-not-agent', {
59
- status: 409,
60
- });
61
- }
62
-
63
- const integrations = parseAgentIntegrations(step.config);
101
+ const workspaceId = loaded.workspaceId;
102
+ const integrations = loaded.integrations ?? legacyIntegrations(loaded.step);
64
103
  const authorizedTools: AuthorizedIntegrationToolMap = new Map();
65
104
 
66
105
  for (const integration of integrations) {
@@ -102,6 +141,22 @@ export async function resolveAuthorizedIntegrationTools(
102
141
  return authorizedTools;
103
142
  }
104
143
 
144
+ function legacyIntegrations(step: {type: string; config: Record<string, unknown>} | undefined) {
145
+ if (step?.type !== 'agent') {
146
+ throw new ClientError('Current leased step is not an agent step', 'leased-step-not-agent', {
147
+ status: 409,
148
+ });
149
+ }
150
+ try {
151
+ return materializedAgentStepConfigSchema.parse(step.config).integrations ?? [];
152
+ } catch (error) {
153
+ throw new ClientError('Agent step config is invalid', 'agent-step-config-invalid', {
154
+ status: 409,
155
+ cause: error,
156
+ });
157
+ }
158
+ }
159
+
105
160
  export function sanitizeSlug(slug: string): string {
106
161
  return slug.replaceAll('-', '_');
107
162
  }
@@ -132,19 +187,6 @@ function toolInputSchema(tool: MaterializedAgentIntegrationToolConfigDto): Agent
132
187
  return narrowMethodEnum(tool.inputSchema, authorizedMethods);
133
188
  }
134
189
 
135
- function parseAgentIntegrations(
136
- config: Record<string, unknown>,
137
- ): MaterializedAgentIntegrationConfigDto[] {
138
- try {
139
- return materializedAgentStepConfigSchema.parse(config).integrations ?? [];
140
- } catch (error) {
141
- throw new ClientError('Agent step config is invalid', 'agent-step-config-invalid', {
142
- status: 409,
143
- cause: error,
144
- });
145
- }
146
- }
147
-
148
190
  async function loadAuthorizedConnection(params: {
149
191
  integration: MaterializedAgentIntegrationConfigDto;
150
192
  workspaceId: string;
@@ -1,10 +1,11 @@
1
+ import type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';
1
2
  import type {RouteExport} from '@shipfox/node-fastify';
2
3
  import type {IntegrationProviderRegistry} from '#core/providers/registry.js';
3
4
  import type {IntegrationSourceControlService} from '#core/source-control-service.js';
4
5
  import type {GetIntegrationConnectionByIdFn} from '#db/connections.js';
5
6
  import {
6
7
  createAgentToolsGatewayRoutes,
7
- type LeasedAgentStepLoader,
8
+ createWorkflowsLeasedAgentStepLoader,
8
9
  } from './agent-tools-gateway/index.js';
9
10
  import {createListIntegrationConnectionsRoute} from './list-connections.js';
10
11
  import {createListIntegrationProvidersRoute} from './list-providers.js';
@@ -14,12 +15,10 @@ import {
14
15
  createUpdateIntegrationConnectionRoute,
15
16
  } from './manage-connections.js';
16
17
 
17
- export type {LeasedAgentStepLoader} from './agent-tools-gateway/index.js';
18
-
19
18
  export interface CreateIntegrationRoutesOptions {
20
19
  agentTools?:
21
20
  | {
22
- loadLeasedAgentStep: LeasedAgentStepLoader;
21
+ workflows: WorkflowsModuleClient;
23
22
  getIntegrationConnectionById: GetIntegrationConnectionByIdFn;
24
23
  }
25
24
  | undefined;
@@ -35,7 +34,7 @@ export function createIntegrationRoutes(
35
34
  ? [
36
35
  createAgentToolsGatewayRoutes({
37
36
  registry,
38
- loadLeasedAgentStep: options.agentTools.loadLeasedAgentStep,
37
+ loadLeasedAgentStep: createWorkflowsLeasedAgentStepLoader(options.agentTools.workflows),
39
38
  getIntegrationConnectionById: options.agentTools.getIntegrationConnectionById,
40
39
  }),
41
40
  ]
@@ -79,15 +79,18 @@ async function loadGiteaModuleParts(): Promise<IntegrationModuleParts> {
79
79
  );
80
80
  }
81
81
 
82
+ const integrationProvider = createGiteaIntegrationProvider({
83
+ getExistingGiteaConnection,
84
+ connectGiteaConnection,
85
+ publishSourcePush,
86
+ recordDeliveryOnly,
87
+ getIntegrationConnectionById,
88
+ coreDb: db,
89
+ });
90
+
82
91
  return {
83
- provider: createGiteaIntegrationProvider({
84
- getExistingGiteaConnection,
85
- connectGiteaConnection,
86
- publishSourcePush,
87
- recordDeliveryOnly,
88
- getIntegrationConnectionById,
89
- coreDb: db,
90
- }),
92
+ provider: integrationProvider,
93
+ webhookProcessors: integrationProvider.webhookProcessors,
91
94
  database: {
92
95
  db: giteaDb,
93
96
  migrationsPath: giteaMigrationsPath,
@@ -116,18 +116,24 @@ async function loadGithubModuleParts(
116
116
  );
117
117
  }
118
118
 
119
+ const integrationProvider = createGithubIntegrationProvider({
120
+ getExistingGithubConnection,
121
+ connectGithubInstallation,
122
+ publishIntegrationEventReceived,
123
+ publishSourcePush,
124
+ recordDeliveryOnly,
125
+ getIntegrationConnectionById,
126
+ coreDb: db,
127
+ deleteSecrets: options.secrets?.deleteSecrets,
128
+ agentTools: {tokenProvider},
129
+ ...(options.requireActiveWorkspaceMembership
130
+ ? {requireActiveWorkspaceMembership: options.requireActiveWorkspaceMembership}
131
+ : {}),
132
+ });
133
+
119
134
  return {
120
- provider: createGithubIntegrationProvider({
121
- getExistingGithubConnection,
122
- connectGithubInstallation,
123
- publishIntegrationEventReceived,
124
- publishSourcePush,
125
- recordDeliveryOnly,
126
- getIntegrationConnectionById,
127
- coreDb: db,
128
- deleteSecrets: options.secrets?.deleteSecrets,
129
- agentTools: {tokenProvider},
130
- }),
135
+ provider: integrationProvider,
136
+ webhookProcessors: integrationProvider.webhookProcessors,
131
137
  e2eRoutes: [
132
138
  createGithubE2eRoutes({
133
139
  getExistingGithubConnection,
@@ -152,6 +152,9 @@ async function loadJiraModuleParts(
152
152
  getExistingJiraConnection,
153
153
  connectJiraInstallation,
154
154
  disconnectJiraInstallation,
155
+ ...(options.requireActiveWorkspaceMembership
156
+ ? {requireActiveWorkspaceMembership: options.requireActiveWorkspaceMembership}
157
+ : {}),
155
158
  },
156
159
  }),
157
160
  database: {db: jiraDb, migrationsPath, migrationsTableName: JIRA_MIGRATIONS_TABLE},
@@ -136,32 +136,38 @@ async function loadLinearModuleParts(
136
136
  secrets,
137
137
  });
138
138
 
139
- return {
140
- provider: createLinearIntegrationProvider({
141
- agentTools: {tokenStore, endpoint: linearConfig.LINEAR_MCP_ENDPOINT},
142
- cleanup: {
143
- deleteConnectionRecords: async (connection, {tx}) => {
144
- await deleteLinearInstallationByConnectionId(connection.id, {tx});
145
- },
146
- deleteConnectionSecrets: async (connection) => {
147
- // Scoped secrets accept the provider-local suffix, after this helper validates its prefix.
148
- await (options.secrets?.linear?.deleteSecrets({
149
- workspaceId: connection.workspaceId,
150
- namespace: linearNamespaceSuffix(linearSecretsNamespace(connection.id)),
151
- }) ?? Promise.resolve());
152
- },
139
+ const integrationProvider = createLinearIntegrationProvider({
140
+ agentTools: {tokenStore, endpoint: linearConfig.LINEAR_MCP_ENDPOINT},
141
+ cleanup: {
142
+ deleteConnectionRecords: async (connection, {tx}) => {
143
+ await deleteLinearInstallationByConnectionId(connection.id, {tx});
153
144
  },
154
- routes: {
155
- tokenStore,
156
- getExistingLinearConnection,
157
- connectLinearInstallation,
158
- disconnectLinearInstallation,
159
- publishIntegrationEventReceived,
160
- recordDeliveryOnly,
161
- getIntegrationConnectionById,
162
- coreDb: db,
145
+ deleteConnectionSecrets: async (connection) => {
146
+ // Scoped secrets accept the provider-local suffix, after this helper validates its prefix.
147
+ await (options.secrets?.linear?.deleteSecrets({
148
+ workspaceId: connection.workspaceId,
149
+ namespace: linearNamespaceSuffix(linearSecretsNamespace(connection.id)),
150
+ }) ?? Promise.resolve());
163
151
  },
164
- }),
152
+ },
153
+ routes: {
154
+ tokenStore,
155
+ getExistingLinearConnection,
156
+ connectLinearInstallation,
157
+ disconnectLinearInstallation,
158
+ publishIntegrationEventReceived,
159
+ recordDeliveryOnly,
160
+ getIntegrationConnectionById,
161
+ coreDb: db,
162
+ ...(options.requireActiveWorkspaceMembership
163
+ ? {requireActiveWorkspaceMembership: options.requireActiveWorkspaceMembership}
164
+ : {}),
165
+ },
166
+ });
167
+
168
+ return {
169
+ provider: integrationProvider,
170
+ webhookProcessors: integrationProvider.webhookProcessors,
165
171
  e2eRoutes: [
166
172
  createLinearE2eRoutes({
167
173
  tokenStore,
@@ -84,19 +84,22 @@ async function loadSentryModuleParts(): Promise<IntegrationModuleParts> {
84
84
  );
85
85
  }
86
86
 
87
+ const integrationProvider = createSentryIntegrationProvider({
88
+ getSentryInstallation: ({installationUuid}) =>
89
+ getSentryInstallationByInstallationUuid(installationUuid),
90
+ getConnectionById,
91
+ connectSentryInstallation,
92
+ persistVerifiedUnclaimedInstallation,
93
+ coreDb: db,
94
+ publishIntegrationEventReceived,
95
+ recordDeliveryOnly,
96
+ getIntegrationConnectionById,
97
+ updateConnectionLifecycleStatus: updateIntegrationConnectionLifecycleStatus,
98
+ });
99
+
87
100
  return {
88
- provider: createSentryIntegrationProvider({
89
- getSentryInstallation: ({installationUuid}) =>
90
- getSentryInstallationByInstallationUuid(installationUuid),
91
- getConnectionById,
92
- connectSentryInstallation,
93
- persistVerifiedUnclaimedInstallation,
94
- coreDb: db,
95
- publishIntegrationEventReceived,
96
- recordDeliveryOnly,
97
- getIntegrationConnectionById,
98
- updateConnectionLifecycleStatus: updateIntegrationConnectionLifecycleStatus,
99
- }),
101
+ provider: integrationProvider,
102
+ webhookProcessors: integrationProvider.webhookProcessors,
100
103
  database: {
101
104
  db: sentryDb,
102
105
  migrationsPath: sentryMigrationsPath,
@@ -138,33 +138,39 @@ async function loadSlackModuleParts(
138
138
  secrets,
139
139
  });
140
140
 
141
- return {
142
- provider: createSlackIntegrationProvider({
143
- agentTools: {tokenStore},
144
- cleanup: {
145
- deleteConnectionRecords: async (connection, {tx}) => {
146
- await deleteSlackInstallationByConnectionId(connection.id, {tx});
147
- },
148
- deleteConnectionSecrets: async (connection) => {
149
- // Scoped secrets accept the provider-local suffix, after this helper validates its prefix.
150
- await (options.secrets?.slack?.deleteSecrets({
151
- workspaceId: connection.workspaceId,
152
- namespace: slackNamespaceSuffix(slackSecretsNamespace(connection.id)),
153
- }) ?? Promise.resolve());
154
- },
141
+ const integrationProvider = createSlackIntegrationProvider({
142
+ agentTools: {tokenStore},
143
+ cleanup: {
144
+ deleteConnectionRecords: async (connection, {tx}) => {
145
+ await deleteSlackInstallationByConnectionId(connection.id, {tx});
155
146
  },
156
- routes: {
157
- tokenStore,
158
- getExistingSlackConnection,
159
- connectSlackInstallation,
160
- disconnectSlackInstallation,
161
- coreDb: db,
162
- claimWebhookDelivery,
163
- publishIntegrationEventReceived,
164
- recordDeliveryOnly,
165
- getIntegrationConnectionById,
147
+ deleteConnectionSecrets: async (connection) => {
148
+ // Scoped secrets accept the provider-local suffix, after this helper validates its prefix.
149
+ await (options.secrets?.slack?.deleteSecrets({
150
+ workspaceId: connection.workspaceId,
151
+ namespace: slackNamespaceSuffix(slackSecretsNamespace(connection.id)),
152
+ }) ?? Promise.resolve());
166
153
  },
167
- }),
154
+ },
155
+ routes: {
156
+ tokenStore,
157
+ getExistingSlackConnection,
158
+ connectSlackInstallation,
159
+ disconnectSlackInstallation,
160
+ coreDb: db,
161
+ claimWebhookDelivery,
162
+ publishIntegrationEventReceived,
163
+ recordDeliveryOnly,
164
+ getIntegrationConnectionById,
165
+ ...(options.requireActiveWorkspaceMembership
166
+ ? {requireActiveWorkspaceMembership: options.requireActiveWorkspaceMembership}
167
+ : {}),
168
+ },
169
+ });
170
+
171
+ return {
172
+ provider: integrationProvider,
173
+ webhookProcessors: integrationProvider.webhookProcessors,
168
174
  e2eRoutes: [
169
175
  createSlackE2eRoutes({
170
176
  tokenStore,
@@ -1,3 +1,5 @@
1
+ import type {UserContextMembership} from '@shipfox/api-auth-context';
2
+ import type {WebhookRequestProcessor, WebhookRouteId} from '@shipfox/api-integration-core-dto';
1
3
  import type {RouteExport} from '@shipfox/node-fastify';
2
4
  import type {ModuleDatabase, ModuleWorker} from '@shipfox/node-module';
3
5
  import type {IntegrationProvider} from '#core/entities/provider.js';
@@ -17,6 +19,12 @@ export interface IntegrationModuleParts {
17
19
  e2eRoutes?: RouteExport[] | undefined;
18
20
  workers?: ModuleWorker[] | undefined;
19
21
  startupTasks?: Array<() => Promise<void>> | undefined;
22
+ webhookProcessors?: WebhookProcessorRegistration[] | undefined;
23
+ }
24
+
25
+ export interface WebhookProcessorRegistration {
26
+ routeIds: readonly WebhookRouteId[];
27
+ processor: WebhookRequestProcessor;
20
28
  }
21
29
 
22
30
  /**
@@ -32,6 +40,13 @@ export interface IntegrationProviderModule {
32
40
 
33
41
  export interface IntegrationProviderModuleLoadOptions {
34
42
  secrets?: IntegrationProviderSecrets | undefined;
43
+ requireActiveWorkspaceMembership?:
44
+ | ((input: {
45
+ workspaceId: string;
46
+ userId: string;
47
+ memberships: ReadonlyArray<UserContextMembership>;
48
+ }) => Promise<unknown>)
49
+ | undefined;
35
50
  }
36
51
 
37
52
  export interface IntegrationProviderSecrets {
@@ -15,16 +15,18 @@ export const webhookProviderModule: IntegrationProviderModule = {
15
15
  enabled: config.INTEGRATIONS_ENABLE_WEBHOOK_PROVIDER,
16
16
  load: async () => {
17
17
  const {createWebhookIntegrationProvider} = await import('@shipfox/api-integration-webhook');
18
+ const integrationProvider = createWebhookIntegrationProvider({
19
+ coreDb: db,
20
+ createIntegrationConnection,
21
+ listIntegrationConnections,
22
+ getIntegrationConnectionById,
23
+ updateIntegrationConnectionLifecycleStatus,
24
+ deleteIntegrationConnection,
25
+ publishIntegrationEventReceived,
26
+ });
18
27
  return {
19
- provider: createWebhookIntegrationProvider({
20
- coreDb: db,
21
- createIntegrationConnection,
22
- listIntegrationConnections,
23
- getIntegrationConnectionById,
24
- updateIntegrationConnectionLifecycleStatus,
25
- deleteIntegrationConnection,
26
- publishIntegrationEventReceived,
27
- }),
28
+ provider: integrationProvider,
29
+ webhookProcessors: integrationProvider.webhookProcessors,
28
30
  };
29
31
  },
30
32
  };