@hiliosai/sdk 0.1.3 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hiliosai/sdk",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -66,6 +66,19 @@ export const CHANNELS = {
66
66
  },
67
67
  } as const;
68
68
 
69
+ /**
70
+ * Integration-specific channel names
71
+ */
72
+ export const INTEGRATION_CHANNELS = {
73
+ // Message events
74
+ MESSAGE_RECEIVED: `${NAMESPACE}.processing.message.received`,
75
+ MESSAGE_SENT: `${NAMESPACE}.processing.message.sent`,
76
+ MESSAGE_FAILED: `${NAMESPACE}.processing.message.failed`,
77
+ } as const;
78
+
79
+ export type IntegrationChannelName =
80
+ (typeof INTEGRATION_CHANNELS)[keyof typeof INTEGRATION_CHANNELS];
81
+
69
82
  /**
70
83
  * Channel Configuration Constants
71
84
  */
@@ -1,18 +1,25 @@
1
1
  import crypto from 'crypto';
2
2
 
3
- import {defineService} from './define-service';
3
+ import {CHANNEL_CONFIG, INTEGRATION_CHANNELS} from '../configs/constants';
4
+ import type {DatasourceConstructorRegistry} from '../middlewares/datasource.middleware';
5
+ import {
6
+ type IntegrationMessageFailedPayload,
7
+ type IntegrationMessageReceivedPayload,
8
+ type IntegrationMessageSentPayload,
9
+ } from '../types/channels';
4
10
  import type {AppContext} from '../types/context';
5
11
  import type {IntegrationConfig} from '../types/integration';
6
12
  import type {
7
13
  NormalizedMessage,
8
- PlatformMessage,
9
14
  SendResult,
10
15
  WebhookEvent,
11
16
  } from '../types/message';
17
+ import type {DatasourceInstanceTypes} from '../types/datasource';
12
18
  import type {
13
19
  IntegrationServiceConfig,
14
20
  IntegrationServiceSchema,
15
21
  } from '../types/service';
22
+ import {defineService} from './define-service';
16
23
 
17
24
  /**
18
25
  * Security helpers for webhook validation
@@ -38,13 +45,6 @@ const SecurityHelpers = {
38
45
  validateTimestamp(timestamp: number, maxAgeMs = 5 * 60 * 1000): boolean {
39
46
  return Date.now() - timestamp <= maxAgeMs;
40
47
  },
41
-
42
- /**
43
- * Generate correlation ID for request tracking
44
- */
45
- generateCorrelationId(): string {
46
- return crypto.randomUUID();
47
- },
48
48
  };
49
49
 
50
50
  /**
@@ -76,41 +76,52 @@ async function executeWithRetry<T>(
76
76
  }
77
77
 
78
78
  export function defineIntegration<
79
- TPlatformMessage extends PlatformMessage = PlatformMessage,
80
79
  TSettings = unknown,
81
- TDatasources = unknown
80
+ TDatasourceConstructors extends DatasourceConstructorRegistry = DatasourceConstructorRegistry
82
81
  >(
83
- config: IntegrationServiceConfig<
84
- TPlatformMessage,
85
- TSettings,
86
- AppContext<TDatasources>
87
- >
88
- ): IntegrationServiceSchema<TPlatformMessage, TSettings> {
82
+ config: IntegrationServiceConfig<TSettings, TDatasourceConstructors>
83
+ ): IntegrationServiceSchema<TSettings> {
84
+ // Type alias for inferred datasource instances
85
+ type TDatasources = DatasourceInstanceTypes<TDatasourceConstructors>;
86
+
89
87
  // Create actions from integration methods
90
88
  const actions = {
91
89
  i_receiveWebhook: {
92
90
  rest: {
93
91
  method: 'POST' as const,
94
- path: '/:tenantId',
92
+ path: '/:channelId',
95
93
  },
96
94
  params: {
97
- tenantId: 'string',
98
95
  channelId: 'string',
99
- integrationId: 'string',
100
- platform: 'string',
101
- rawPayload: 'any',
102
- rawBody: {type: 'string', optional: true},
96
+ payload: 'object',
103
97
  headers: 'object',
104
98
  timestamp: 'number',
105
99
  },
106
100
  async handler(
107
- ctx: AppContext<TDatasources, WebhookEvent>
108
- ): Promise<{success: boolean; messages: number}> {
109
- const webhook = ctx.params;
110
- const correlationId = SecurityHelpers.generateCorrelationId();
101
+ ctx: AppContext<
102
+ TDatasources,
103
+ {
104
+ channelId: string;
105
+ payload: object;
106
+ headers: object;
107
+ timestamp: number;
108
+ }
109
+ >
110
+ ): Promise<{success: boolean; messages: number; failed: number}> {
111
+ const {channelId, payload, headers, timestamp} = ctx.params;
112
+
113
+ // TODO: Look up channel configuration by channelId
114
+ // const channel = await getChannelConfig(channelId);
115
+ // For now, we'll construct webhook from meta
111
116
 
112
- // Add correlation ID to context for tracking
113
- ctx.meta.correlationId = correlationId;
117
+ const webhook: WebhookEvent = {
118
+ tenantId: ctx.meta.tenantId ?? 'unknown', // Should come from channel lookup
119
+ channelId,
120
+ platform: config.spec.platform,
121
+ payload, // Raw webhook payload from gateway
122
+ headers: headers as Record<string, string>,
123
+ timestamp,
124
+ };
114
125
 
115
126
  // Validate timestamp to prevent replay attacks
116
127
  if (!SecurityHelpers.validateTimestamp(webhook.timestamp)) {
@@ -134,25 +145,49 @@ export function defineIntegration<
134
145
  }
135
146
 
136
147
  // Normalize the message
137
- const normalizedMessages: NormalizedMessage[] = await config.normalize(
138
- webhook
148
+ const normalizedMessages = await config.normalize(webhook);
149
+
150
+ // Process each message with reliable delivery (parallel)
151
+ const results = await Promise.allSettled(
152
+ normalizedMessages.map(async (message) => {
153
+ const payload: IntegrationMessageReceivedPayload = {
154
+ tenantId: webhook.tenantId,
155
+ channelId: webhook.channelId,
156
+ platform: webhook.platform,
157
+ message,
158
+ timestamp: Date.now(),
159
+ };
160
+
161
+ // Send to reliable message channel (NATS JetStream)
162
+ return ctx.broker.sendToChannel(
163
+ INTEGRATION_CHANNELS.MESSAGE_RECEIVED,
164
+ payload,
165
+ CHANNEL_CONFIG.HIGH_PRIORITY
166
+ );
167
+ })
139
168
  );
140
169
 
141
- // Process each message
142
- for (const message of normalizedMessages) {
143
- // Emit event for further processing with correlation ID
144
- ctx.emit('integration.message.received', {
145
- correlationId,
146
- tenantId: webhook.tenantId,
147
- channelId: webhook.channelId,
148
- integrationId: webhook.integrationId,
149
- platform: webhook.platform,
150
- message,
151
- timestamp: Date.now(),
152
- });
170
+ // Count successful vs failed messages
171
+ const successful = results.filter(
172
+ (result) => result.status === 'fulfilled'
173
+ ).length;
174
+ const failed = results.filter(
175
+ (result) => result.status === 'rejected'
176
+ ).length;
177
+
178
+ // Log failures for monitoring (but don't fail the entire webhook)
179
+ if (failed > 0) {
180
+ const failures = results
181
+ .filter((result) => result.status === 'rejected')
182
+ .map((result) => (result as PromiseRejectedResult).reason);
183
+
184
+ ctx.broker.logger.warn(
185
+ `Webhook processing partial failure: ${failed}/${normalizedMessages.length} messages failed`,
186
+ {failures}
187
+ );
153
188
  }
154
189
 
155
- return {success: true, messages: normalizedMessages.length};
190
+ return {success: true, messages: successful, failed};
156
191
  },
157
192
  },
158
193
 
@@ -163,20 +198,26 @@ export function defineIntegration<
163
198
  },
164
199
  params: {
165
200
  message: 'object',
166
- config: 'object',
201
+ channelId: 'string',
167
202
  },
168
- async handler(ctx: AppContext<TDatasources>): Promise<unknown> {
169
- const {message, config: integrationConfig} = ctx.params as {
170
- message: NormalizedMessage;
171
- config: IntegrationConfig;
172
- };
203
+ async handler(
204
+ ctx: AppContext<
205
+ TDatasources,
206
+ {
207
+ message: NormalizedMessage;
208
+ channelId: string;
209
+ }
210
+ >
211
+ ): Promise<SendResult> {
212
+ const {message, channelId} = ctx.params;
173
213
 
174
- // Transform message if method exists
175
- if (config.transform) {
176
- await config.transform(message, integrationConfig);
214
+ // Load channel configuration
215
+ const integrationConfig = await config.getChannelConfig(ctx, channelId);
216
+ if (!integrationConfig) {
217
+ throw new Error(`Channel configuration not found: ${channelId}`);
177
218
  }
178
219
 
179
- // Send the message with retry mechanism
220
+ // Send the message with retry mechanism (transformation happens inside sendMessage)
180
221
  const result: SendResult = await executeWithRetry(
181
222
  () => config.sendMessage(ctx, message, integrationConfig),
182
223
  3,
@@ -184,18 +225,49 @@ export function defineIntegration<
184
225
  `Send message via ${config.spec.platform}`
185
226
  );
186
227
 
187
- // Emit event based on result
188
- if (result.success) {
189
- ctx.emit('integration.message.sent', {
228
+ // Send reliable events based on result (with error handling)
229
+ try {
230
+ if (result.success) {
231
+ const sentPayload: IntegrationMessageSentPayload = {
232
+ tenantId: integrationConfig.tenantId,
233
+ channelId,
234
+ platform: config.spec.platform,
235
+ messageId: result.messageId,
236
+ metadata: result.metadata,
237
+ timestamp: Date.now(),
238
+ };
239
+
240
+ await ctx.broker.sendToChannel(
241
+ INTEGRATION_CHANNELS.MESSAGE_SENT,
242
+ sentPayload,
243
+ CHANNEL_CONFIG.DEFAULTS
244
+ );
245
+ } else {
246
+ const failedPayload: IntegrationMessageFailedPayload = {
247
+ tenantId: integrationConfig.tenantId,
248
+ channelId,
249
+ platform: config.spec.platform,
250
+ error: result.error?.message ?? 'Unknown error',
251
+ message,
252
+ timestamp: Date.now(),
253
+ };
254
+
255
+ await ctx.broker.sendToChannel(
256
+ INTEGRATION_CHANNELS.MESSAGE_FAILED,
257
+ failedPayload,
258
+ CHANNEL_CONFIG.DEFAULTS
259
+ );
260
+ }
261
+ } catch (channelError: unknown) {
262
+ // Log channel error but don't fail the send operation
263
+ const err =
264
+ channelError instanceof Error
265
+ ? channelError
266
+ : new Error(String(channelError));
267
+ ctx.broker.logger.warn('Failed to send message event to channel', {
268
+ error: err.message,
190
269
  messageId: result.messageId,
191
270
  platform: config.spec.platform,
192
- metadata: result.metadata,
193
- });
194
- } else {
195
- ctx.emit('integration.message.failed', {
196
- error: result.error,
197
- platform: config.spec.platform,
198
- message,
199
271
  });
200
272
  }
201
273
 
@@ -271,23 +343,6 @@ export function defineIntegration<
271
343
  },
272
344
  },
273
345
 
274
- i_status: {
275
- rest: {
276
- method: 'GET' as const,
277
- path: '/status',
278
- },
279
- handler(): object {
280
- return {
281
- id: config.spec.id,
282
- name: config.spec.name,
283
- platform: config.spec.platform,
284
- version: config.spec.version,
285
- status: config.spec.status,
286
- capabilities: config.spec.capabilities,
287
- };
288
- },
289
- },
290
-
291
346
  i_validateCredentials: {
292
347
  params: {
293
348
  credentials: 'object',
@@ -309,7 +364,7 @@ export function defineIntegration<
309
364
  }
310
365
 
311
366
  // Use defineService to get all the mixins and proper setup
312
- const baseService = defineService<TDatasources, TSettings>({
367
+ const baseService = defineService<TSettings, TDatasourceConstructors>({
313
368
  name: config.name,
314
369
  version: config.version,
315
370
  settings: config.settings as TSettings,
@@ -324,12 +379,12 @@ export function defineIntegration<
324
379
  methods: {
325
380
  ...(config.methods ?? {}),
326
381
  // Add integration-specific methods to the service methods (filter out undefined)
327
- // normalize is required, no need for conditional
382
+ // Required methods - no need for conditional
328
383
  normalize: config.normalize,
329
- ...(config.transform && {transform: config.transform}),
330
- ...(config.validateWebhook && {validateWebhook: config.validateWebhook}),
331
- // sendMessage is required, no need for conditional
384
+ getChannelConfig: config.getChannelConfig,
332
385
  sendMessage: config.sendMessage,
386
+ // Optional methods
387
+ ...(config.validateWebhook && {validateWebhook: config.validateWebhook}),
333
388
  ...(config.verifyWebhook && {verifyWebhook: config.verifyWebhook}),
334
389
  ...(config.checkHealth && {checkHealth: config.checkHealth}),
335
390
  ...(config.validateCredentials && {
@@ -339,7 +394,6 @@ export function defineIntegration<
339
394
  validateSignature: config.validateSignature,
340
395
  }),
341
396
  },
342
- hooks: config.hooks ?? {},
343
397
  created: config.created,
344
398
  started: config.started,
345
399
  stopped: config.stopped,
@@ -350,5 +404,5 @@ export function defineIntegration<
350
404
  ...baseService,
351
405
  // Only add the integration spec
352
406
  spec: config.spec,
353
- } as IntegrationServiceSchema<TPlatformMessage, TSettings>;
407
+ } as IntegrationServiceSchema<TSettings>;
354
408
  }
@@ -2,6 +2,7 @@ import omit from 'lodash/omit';
2
2
  import type {ServiceSchema as MoleculerServiceSchema} from 'moleculer';
3
3
 
4
4
  import {MemoizeMixin} from '../middlewares';
5
+ import type {DatasourceConstructorRegistry} from '../middlewares/datasource.middleware';
5
6
  import {DatasourceMixin} from '../mixins';
6
7
  import type {ServiceConfig} from '../types/service';
7
8
 
@@ -34,8 +35,11 @@ import type {ServiceConfig} from '../types/service';
34
35
  * });
35
36
  * ```
36
37
  */
37
- export function defineService<TDatasources = unknown, TSettings = unknown>(
38
- config: ServiceConfig<TSettings, TDatasources>
38
+ export function defineService<
39
+ TSettings = unknown,
40
+ TDatasourceConstructors extends DatasourceConstructorRegistry = DatasourceConstructorRegistry
41
+ >(
42
+ config: ServiceConfig<TSettings, TDatasourceConstructors>
39
43
  ): MoleculerServiceSchema<TSettings> {
40
44
  const propsToOmit = ['datasources'];
41
45
  const serviceSchema = omit(
@@ -0,0 +1,60 @@
1
+ import type {IntegrationPlatform} from './platform';
2
+ import type {NormalizedMessage} from './message';
3
+
4
+ /**
5
+ * Channel event payload types for reliable messaging via @moleculer/channels
6
+ */
7
+
8
+ // Integration message events
9
+ export interface IntegrationMessageReceivedPayload {
10
+ tenantId: string;
11
+ channelId: string;
12
+ platform: IntegrationPlatform;
13
+ message: NormalizedMessage;
14
+ timestamp: number;
15
+ metadata?: Record<string, unknown>;
16
+ }
17
+
18
+ export interface IntegrationMessageSentPayload {
19
+ tenantId: string;
20
+ channelId: string;
21
+ platform: IntegrationPlatform;
22
+ messageId?: string;
23
+ metadata?: Record<string, unknown>;
24
+ timestamp: number;
25
+ }
26
+
27
+ export interface IntegrationMessageFailedPayload {
28
+ tenantId: string;
29
+ channelId: string;
30
+ platform: IntegrationPlatform;
31
+ error: string; // Serialized error message
32
+ message?: NormalizedMessage;
33
+ timestamp: number;
34
+ retryCount?: number;
35
+ metadata?: Record<string, unknown>;
36
+ }
37
+
38
+ // System events
39
+ export interface IntegrationRegisteredPayload {
40
+ tenantId: string;
41
+ channelId: string;
42
+ platform: IntegrationPlatform;
43
+ config: Record<string, unknown>;
44
+ timestamp: number;
45
+ }
46
+
47
+ export interface IntegrationUnregisteredPayload {
48
+ tenantId: string;
49
+ channelId: string;
50
+ platform: IntegrationPlatform;
51
+ timestamp: number;
52
+ }
53
+
54
+ // Re-export constants to avoid duplication
55
+ export {
56
+ CHANNELS,
57
+ INTEGRATION_CHANNELS,
58
+ NAMESPACE,
59
+ type IntegrationChannelName,
60
+ } from '../configs/constants';
@@ -2,15 +2,32 @@ import type {Context} from 'moleculer';
2
2
  import type {Tenant} from './tenant';
3
3
  import type {User} from './user';
4
4
 
5
+ // Moleculer Channels types
6
+ export interface ChannelSendOptions {
7
+ group?: string;
8
+ maxInFlight?: number;
9
+ maxRetries?: number;
10
+ deadLettering?: {
11
+ enabled?: boolean;
12
+ queueName?: string;
13
+ };
14
+ }
15
+
16
+ export interface SendToChannelMethod {
17
+ (
18
+ channelName: string,
19
+ payload: unknown,
20
+ options?: ChannelSendOptions
21
+ ): Promise<void>;
22
+ }
23
+
5
24
  export interface AppMeta {
6
25
  user?: User;
7
26
  tenantId?: string;
8
27
  tenantName?: string;
9
28
  userId?: string;
10
- integrationId?: string;
11
29
  channelId?: string;
12
30
  requestId?: string;
13
- correlationId?: string;
14
31
  userAgent?: string;
15
32
  clientIP?: string;
16
33
  [key: string]: unknown;
@@ -41,4 +58,7 @@ export type AppContext<
41
58
  > = Context<TParams, TMeta, TLocals> &
42
59
  PermissionHelpers & {
43
60
  datasources: TDatasources;
61
+ broker: Context['broker'] & {
62
+ sendToChannel: SendToChannelMethod;
63
+ };
44
64
  };
@@ -6,3 +6,4 @@ export * from './service';
6
6
  export * from './user';
7
7
  export * from './tenant';
8
8
  export * from './datasource';
9
+ export * from './channels';
@@ -20,7 +20,6 @@ export interface BaseSpec {
20
20
  export interface IntegrationConfig {
21
21
  id: string;
22
22
  tenantId: string;
23
- integrationId: string;
24
23
  name: string;
25
24
  version: string;
26
25
  status: IntegrationStatus;
@@ -74,16 +74,14 @@ export interface NormalizedMessage {
74
74
 
75
75
  export interface PlatformMessage {
76
76
  platform: IntegrationPlatform;
77
- rawPayload: any;
77
+ payload: any;
78
78
  }
79
79
 
80
- export interface WebhookEvent {
80
+ export interface WebhookEvent<TPayload = any> {
81
81
  tenantId: string;
82
82
  channelId: string;
83
- integrationId: string;
84
83
  platform: IntegrationPlatform;
85
- rawPayload: any;
86
- rawBody?: string;
84
+ payload: TPayload;
87
85
  headers: Record<string, string>;
88
86
  timestamp: number;
89
87
  }
@@ -9,13 +9,10 @@ import type {
9
9
 
10
10
  import type {DatasourceConstructorRegistry} from '../middlewares/datasource.middleware';
11
11
  import type {AppContext} from './context';
12
+ import type {DatasourceInstanceTypes} from './datasource';
12
13
  import type {BaseSpec, IntegrationConfig} from './integration';
13
- import type {
14
- NormalizedMessage,
15
- PlatformMessage,
16
- SendResult,
17
- WebhookEvent,
18
- } from './message';
14
+ import type {NormalizedMessage, SendResult, WebhookEvent} from './message';
15
+
19
16
 
20
17
  // Type to infer TypeScript types from Moleculer parameter schemas
21
18
  type InferParamsType<T> = T extends Record<string, any>
@@ -121,28 +118,34 @@ export interface ServiceSchema<TSettings = unknown, TDatasources = unknown>
121
118
  // ServiceConfig is what users provide to defineService
122
119
  export type ServiceConfig<
123
120
  TSettings = unknown,
124
- TDatasources = unknown
125
- > = ServiceSchema<TSettings, TDatasources> & {
126
- datasources?: DatasourceConstructorRegistry;
121
+ TDatasourceConstructors extends DatasourceConstructorRegistry = DatasourceConstructorRegistry
122
+ > = ServiceSchema<
123
+ TSettings,
124
+ DatasourceInstanceTypes<TDatasourceConstructors>
125
+ > & {
126
+ datasources?: TDatasourceConstructors;
127
127
  };
128
128
 
129
129
  // Integration-specific types
130
130
  export interface IntegrationServiceConfig<
131
- TPlatformMessage extends PlatformMessage = PlatformMessage,
132
131
  TSettings = unknown,
133
- TContext extends AppContext = AppContext,
134
- TDatasources = unknown
135
- > extends ServiceConfig<TSettings, TDatasources> {
132
+ TDatasourceConstructors extends DatasourceConstructorRegistry = DatasourceConstructorRegistry,
133
+ TContext extends AppContext<
134
+ DatasourceInstanceTypes<TDatasourceConstructors>
135
+ > = AppContext<DatasourceInstanceTypes<TDatasourceConstructors>>
136
+ > extends ServiceConfig<TSettings, TDatasourceConstructors> {
136
137
  name: string;
137
138
  spec: BaseSpec;
138
139
 
139
140
  // Core integration methods
140
- normalize(webhook: WebhookEvent): Promise<NormalizedMessage[]>;
141
- transform?(
142
- message: NormalizedMessage,
143
- config: IntegrationConfig
144
- ): Promise<TPlatformMessage>;
145
- validateWebhook?(webhook: WebhookEvent): boolean;
141
+ normalize<TPayload = any>(
142
+ webhook: WebhookEvent<TPayload>
143
+ ): Promise<NormalizedMessage[]>;
144
+ getChannelConfig(
145
+ ctx: TContext,
146
+ channelId: string
147
+ ): Promise<IntegrationConfig | null>;
148
+ validateWebhook?<TPayload = any>(webhook: WebhookEvent<TPayload>): boolean;
146
149
  sendMessage(
147
150
  ctx: TContext,
148
151
  message: NormalizedMessage,
@@ -170,20 +173,20 @@ export interface IntegrationServiceConfig<
170
173
  validateCredentials?(credentials: Record<string, string>): Promise<boolean>;
171
174
 
172
175
  // Optional signature validation
173
- validateSignature?(webhook: WebhookEvent): boolean;
176
+ validateSignature?<TPayload = any>(webhook: WebhookEvent<TPayload>): boolean;
174
177
  }
175
178
 
176
- export interface IntegrationServiceSchema<
177
- TPlatformMessage extends PlatformMessage = PlatformMessage,
178
- TSettings = unknown
179
- > extends MoleculerServiceSchema<TSettings> {
179
+ export interface IntegrationServiceSchema<TSettings = unknown>
180
+ extends MoleculerServiceSchema<TSettings> {
180
181
  spec: BaseSpec;
181
- normalize(webhook: WebhookEvent): Promise<NormalizedMessage[]>;
182
- transform?(
183
- message: NormalizedMessage,
184
- config: IntegrationConfig
185
- ): Promise<TPlatformMessage>;
186
- validateWebhook?(webhook: WebhookEvent): boolean;
182
+ normalize<TPayload = any>(
183
+ webhook: WebhookEvent<TPayload>
184
+ ): Promise<NormalizedMessage[]>;
185
+ getChannelConfig(
186
+ ctx: AppContext,
187
+ channelId: string
188
+ ): Promise<IntegrationConfig | null>;
189
+ validateWebhook?<TPayload = any>(webhook: WebhookEvent<TPayload>): boolean;
187
190
  sendMessage(
188
191
  ctx: AppContext,
189
192
  message: NormalizedMessage,
@@ -203,5 +206,5 @@ export interface IntegrationServiceSchema<
203
206
  details?: Record<string, any>;
204
207
  }>;
205
208
  validateCredentials?(credentials: Record<string, string>): Promise<boolean>;
206
- validateSignature?(webhook: WebhookEvent): boolean;
209
+ validateSignature?<TPayload = any>(webhook: WebhookEvent<TPayload>): boolean;
207
210
  }
package/src/types/user.ts CHANGED
@@ -3,8 +3,8 @@ export const UserRole = {
3
3
  ADMIN: 'ADMIN',
4
4
  MANAGER: 'MANAGER',
5
5
  AGENT: 'AGENT',
6
- VIEWER: 'VIEWER'
7
- } as const
6
+ VIEWER: 'VIEWER',
7
+ } as const;
8
8
 
9
9
  export interface User {
10
10
  id: string;
@@ -1,394 +0,0 @@
1
- # Integration Service Architecture
2
-
3
- ## Overview
4
-
5
- Integration Services are specialized Moleculer services that provide a standardized way to connect external messaging platforms (WhatsApp, Telegram, Discord, etc.) with the application. They handle webhook reception, message normalization, and platform-specific communication patterns.
6
-
7
- ## Purpose
8
-
9
- The Integration Service pattern solves several key challenges:
10
-
11
- 1. **Platform Abstraction**: Provides unified interface for different messaging platforms
12
- 2. **Webhook Security**: Implements signature validation and replay attack prevention
13
- 3. **Message Normalization**: Converts platform-specific messages to unified format
14
- 4. **Retry Logic**: Handles transient failures with exponential backoff
15
- 5. **Event Broadcasting**: Emits standardized events for message processing
16
- 6. **Health Monitoring**: Provides health checks and status reporting
17
-
18
- ## Architecture
19
-
20
- ```mermaid
21
- graph TB
22
- subgraph "External Platforms"
23
- WA[WhatsApp]
24
- TG[Telegram]
25
- DC[Discord]
26
- SL[Slack]
27
- end
28
-
29
- subgraph "Integration Layer"
30
- subgraph "Integration Services"
31
- WAS[WhatsApp Service]
32
- TGS[Telegram Service]
33
- DCS[Discord Service]
34
- SLS[Slack Service]
35
- end
36
-
37
- subgraph "Service Actions"
38
- RW[i_receiveWebhook]
39
- SM[i_sendMessage]
40
- HC[i_healthCheck]
41
- VW[i_verifyWebhook]
42
- ST[i_status]
43
- VC[i_validateCredentials]
44
- end
45
- end
46
-
47
- subgraph "Core Application"
48
- subgraph "Message Processing"
49
- MP[Message Processor]
50
- MR[Message Router]
51
- MS[Message Store]
52
- end
53
-
54
- subgraph "Event System"
55
- EB[Event Bus]
56
- EH[Event Handlers]
57
- end
58
- end
59
-
60
- subgraph "Infrastructure"
61
- DS[Datasources]
62
- CA[Cache]
63
- PE[Permissions]
64
- CH[Context Helpers]
65
- end
66
-
67
- %% External webhook flows
68
- WA -->|Webhook| WAS
69
- TG -->|Webhook| TGS
70
- DC -->|Webhook| DCS
71
- SL -->|Webhook| SLS
72
-
73
- %% Integration service actions
74
- WAS --> RW
75
- WAS --> SM
76
- WAS --> HC
77
-
78
- %% Service infrastructure
79
- WAS --> DS
80
- TGS --> CA
81
- DCS --> PE
82
- SLS --> CH
83
-
84
- %% Event flows
85
- RW -->|integration.message.received| EB
86
- SM -->|integration.message.sent| EB
87
- SM -->|integration.message.failed| EB
88
-
89
- %% Processing flows
90
- EB --> EH
91
- EH --> MP
92
- MP --> MR
93
- MR --> MS
94
-
95
- %% Response flows
96
- MP -->|Send Response| SM
97
- ```
98
-
99
- ## Integration Service Structure
100
-
101
- ### Core Components
102
-
103
- 1. **defineIntegration()** - Factory function that creates integration services
104
- 2. **Security Helpers** - Webhook validation, timestamp checking, correlation IDs
105
- 3. **Retry Mechanism** - Exponential backoff for failed operations
106
- 4. **Standard Actions** - Predefined actions for common integration operations
107
-
108
- ### Standard Actions
109
-
110
- All integration services automatically get these actions:
111
-
112
- | Action | REST Endpoint | Purpose |
113
- |--------|---------------|---------|
114
- | `i_receiveWebhook` | `POST /webhook` | Receive and process incoming webhooks |
115
- | `i_sendMessage` | `POST /send` | Send messages to external platform |
116
- | `i_healthCheck` | `GET /health` | Check integration health status |
117
- | `i_verifyWebhook` | `GET /webhook` | Verify webhook subscriptions |
118
- | `i_status` | `GET /status` | Get integration status information |
119
- | `i_validateCredentials` | `POST /validate` | Validate platform credentials |
120
-
121
- ### Service Methods
122
-
123
- Integration-specific logic is implemented as service methods:
124
-
125
- | Method | Purpose | Required |
126
- |--------|---------|----------|
127
- | `normalize()` | Convert platform message to unified format | ✅ |
128
- | `sendMessage()` | Send message to platform | ✅ |
129
- | `transform()` | Transform outbound message to platform format | ❌ |
130
- | `validateWebhook()` | Validate webhook payload structure | ❌ |
131
- | `validateSignature()` | Validate webhook signature | ❌ |
132
- | `verifyWebhook()` | Handle webhook verification challenge | ❌ |
133
- | `checkHealth()` | Custom health check logic | ❌ |
134
- | `validateCredentials()` | Validate platform credentials | ❌ |
135
-
136
- ## Usage Example
137
-
138
- ### Basic WhatsApp Integration
139
-
140
- ```typescript
141
- import {defineIntegration} from '@pkg/sdk';
142
- import type {BaseSpec, NormalizedMessage, WebhookEvent} from '@pkg/sdk';
143
-
144
- const whatsappIntegration: BaseSpec = {
145
- id: 'whatsapp-business',
146
- name: 'WhatsApp Business',
147
- platform: 'whatsapp',
148
- version: '1.0.0',
149
- status: 'ACTIVE',
150
- capabilities: ['SEND_MESSAGE', 'RECEIVE_MESSAGE', 'WEBHOOK_VALIDATION']
151
- };
152
-
153
- export default defineIntegration({
154
- name: 'whatsapp',
155
- spec: whatsappIntegration,
156
-
157
- // Required: Convert WhatsApp webhook to normalized message
158
- async normalize(webhook: WebhookEvent): Promise<NormalizedMessage[]> {
159
- const messages: NormalizedMessage[] = [];
160
-
161
- for (const entry of webhook.rawPayload.entry || []) {
162
- for (const change of entry.changes || []) {
163
- if (change.field === 'messages') {
164
- for (const message of change.value.messages || []) {
165
- messages.push({
166
- id: message.id,
167
- conversationId: message.from,
168
- from: {
169
- id: message.from,
170
- name: change.value.contacts?.[0]?.profile?.name,
171
- },
172
- to: {
173
- id: webhook.tenantId,
174
- },
175
- content: {
176
- type: message.type === 'text' ? 'TEXT' : 'FILE',
177
- text: message.text?.body,
178
- media: message.image ? {
179
- url: message.image.id,
180
- mimeType: message.image.mime_type
181
- } : undefined
182
- },
183
- timestamp: parseInt(message.timestamp) * 1000,
184
- platform: 'whatsapp',
185
- metadata: {
186
- phoneNumberId: change.value.metadata.phone_number_id
187
- }
188
- });
189
- }
190
- }
191
- }
192
- }
193
-
194
- return messages;
195
- },
196
-
197
- // Required: Send message to WhatsApp
198
- async sendMessage(ctx, message, config) {
199
- const phoneNumberId = config.credentials.phoneNumberId;
200
- const accessToken = config.credentials.accessToken;
201
-
202
- const response = await fetch(
203
- `https://graph.facebook.com/v18.0/${phoneNumberId}/messages`,
204
- {
205
- method: 'POST',
206
- headers: {
207
- 'Authorization': `Bearer ${accessToken}`,
208
- 'Content-Type': 'application/json'
209
- },
210
- body: JSON.stringify({
211
- messaging_product: 'whatsapp',
212
- to: message.to.id,
213
- text: { body: message.content.text }
214
- })
215
- }
216
- );
217
-
218
- if (!response.ok) {
219
- const error = await response.text();
220
- return {
221
- success: false,
222
- error: new Error(`WhatsApp API error: ${error}`)
223
- };
224
- }
225
-
226
- const result = await response.json();
227
- return {
228
- success: true,
229
- messageId: result.messages[0].id,
230
- metadata: { phoneNumberId }
231
- };
232
- },
233
-
234
- // Optional: Validate webhook signature
235
- validateSignature(webhook) {
236
- const signature = webhook.headers['x-hub-signature-256'];
237
- const secret = process.env.WHATSAPP_WEBHOOK_SECRET;
238
-
239
- if (!signature || !secret) return false;
240
-
241
- const expectedSignature = 'sha256=' +
242
- crypto.createHmac('sha256', secret)
243
- .update(webhook.rawBody || '')
244
- .digest('hex');
245
-
246
- return SecurityHelpers.secureCompare(signature, expectedSignature);
247
- },
248
-
249
- // Optional: Handle webhook verification
250
- verifyWebhook(params) {
251
- const { mode, token, challenge } = params;
252
- const verifyToken = process.env.WHATSAPP_VERIFY_TOKEN;
253
-
254
- if (mode === 'subscribe' && token === verifyToken) {
255
- return challenge;
256
- }
257
- return null;
258
- },
259
-
260
- // Optional: Custom health check
261
- async checkHealth(ctx, config) {
262
- try {
263
- const response = await fetch(
264
- `https://graph.facebook.com/v18.0/${config.credentials.phoneNumberId}`,
265
- {
266
- headers: {
267
- 'Authorization': `Bearer ${config.credentials.accessToken}`
268
- }
269
- }
270
- );
271
-
272
- return {
273
- status: response.ok ? 'healthy' : 'unhealthy',
274
- message: response.ok ? 'WhatsApp API accessible' : 'WhatsApp API error',
275
- details: {
276
- statusCode: response.status,
277
- timestamp: new Date().toISOString()
278
- }
279
- };
280
- } catch (error) {
281
- return {
282
- status: 'unhealthy',
283
- message: error.message,
284
- details: { timestamp: new Date().toISOString() }
285
- };
286
- }
287
- }
288
- });
289
- ```
290
-
291
- ### Advanced Integration with Datasources and Cache
292
-
293
- ```typescript
294
- import {defineIntegration} from '@pkg/sdk';
295
- import {WhatsAppDatasource} from './datasources';
296
-
297
- export default defineIntegration({
298
- name: 'whatsapp',
299
- spec: whatsappIntegration,
300
-
301
- // Configure per-service datasources
302
- datasources: {
303
- whatsapp: WhatsAppDatasource
304
- },
305
-
306
- // Configure per-service cache
307
- cache: {
308
- redisUrl: process.env.WHATSAPP_REDIS_URL,
309
- ttl: 5 * 60 * 1000, // 5 minutes
310
- namespace: 'whatsapp'
311
- },
312
-
313
- async normalize(webhook) {
314
- // Access datasource via context in actions
315
- // this.datasources is not available in methods
316
- // Use dependency injection pattern instead
317
- return await this.processWebhookMessages(webhook);
318
- },
319
-
320
- async sendMessage(ctx, message, config) {
321
- // Use cache for rate limiting
322
- const rateLimitKey = `rate_limit:${config.credentials.phoneNumberId}`;
323
- const currentCount = await ctx.cache.get(rateLimitKey) || 0;
324
-
325
- if (currentCount >= 1000) {
326
- return {
327
- success: false,
328
- error: new Error('Rate limit exceeded')
329
- };
330
- }
331
-
332
- // Send message...
333
- const result = await this.sendToWhatsApp(message, config);
334
-
335
- // Update rate limit counter
336
- await ctx.cache.set(rateLimitKey, currentCount + 1, 60 * 60 * 1000);
337
-
338
- return result;
339
- }
340
- });
341
- ```
342
-
343
- ## Event Flow
344
-
345
- ### Inbound Message Flow
346
-
347
- 1. **Webhook Received** → `i_receiveWebhook` action
348
- 2. **Security Validation** → Timestamp, signature, payload checks
349
- 3. **Message Normalization** → Convert to unified format
350
- 4. **Event Emission** → `integration.message.received` event
351
- 5. **Message Processing** → Application-specific logic
352
- 6. **Response Generation** → Optional automated responses
353
-
354
- ### Outbound Message Flow
355
-
356
- 1. **Send Request** → `i_sendMessage` action
357
- 2. **Message Transformation** → Convert to platform format
358
- 3. **Retry Logic** → Exponential backoff for failures
359
- 4. **Platform API Call** → Send via platform API
360
- 5. **Event Emission** → `integration.message.sent` or `integration.message.failed`
361
- 6. **Status Tracking** → Update message delivery status
362
-
363
- ## Security Features
364
-
365
- ### Webhook Validation
366
-
367
- - **Signature Verification**: HMAC-SHA256 validation using platform secrets
368
- - **Replay Attack Prevention**: Timestamp validation (5-minute window)
369
- - **Timing Attack Protection**: Constant-time string comparison
370
- - **Correlation IDs**: Request tracking for debugging and audit trails
371
-
372
- ### Best Practices
373
-
374
- 1. **Always validate webhook signatures** in production
375
- 2. **Use environment variables** for secrets and tokens
376
- 3. **Implement proper error handling** with structured errors
377
- 4. **Add correlation IDs** for request tracking
378
- 5. **Use retry logic** for transient failures
379
- 6. **Monitor health endpoints** for platform availability
380
- 7. **Cache frequently accessed data** to reduce API calls
381
-
382
- ## Infrastructure Integration
383
-
384
- Integration services automatically inherit:
385
-
386
- - **Datasource Access**: Database connections, external APIs
387
- - **Cache Layer**: Redis/in-memory caching with TTL
388
- - **Permission System**: Role-based access control
389
- - **Context Helpers**: Common utilities and operations
390
- - **Memoization**: Action result caching
391
- - **Health Monitoring**: Automated health checks
392
- - **Event System**: Pub/sub event broadcasting
393
-
394
- This architecture provides a robust, scalable foundation for building messaging platform integrations while maintaining consistency, security, and observability across all platforms.
@@ -1,8 +0,0 @@
1
- import type {DatasourceInstanceTypes} from '../../../types';
2
- import {UserDatasource} from './user.datasource';
3
-
4
- export const datasources = {
5
- user: UserDatasource,
6
- };
7
-
8
- export type UserDatasourceTypes = DatasourceInstanceTypes<typeof datasources>;
@@ -1,7 +0,0 @@
1
- import type {User} from '../../../types';
2
-
3
- export class UserDatasource {
4
- getUser(): User {
5
- return {} as User;
6
- }
7
- }
@@ -1,23 +0,0 @@
1
- import {defineService} from '../define-service';
2
- import {datasources, type UserDatasourceTypes} from './datasources';
3
-
4
- export type GetUserActionInputParams = {
5
- id: string;
6
- };
7
-
8
- export default defineService<UserDatasourceTypes>({
9
- name: 'test',
10
- datasources,
11
- actions: {
12
- user: {
13
- params: {
14
- id: 'string',
15
- },
16
- handler(ctx) {
17
- const userDs = ctx.datasources.user;
18
-
19
- return userDs.getUser();
20
- },
21
- },
22
- },
23
- });
File without changes