@hiliosai/sdk 0.1.0 → 0.1.1

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.
@@ -1,10 +1,12 @@
1
- import type {Context, ServiceSchema} from 'moleculer';
1
+ import crypto from 'crypto';
2
2
 
3
+ import {defineService} from './define-service';
3
4
  import type {AppContext} from '../types/context';
4
5
  import type {IntegrationConfig} from '../types/integration';
5
6
  import type {
6
7
  NormalizedMessage,
7
8
  PlatformMessage,
9
+ SendResult,
8
10
  WebhookEvent,
9
11
  } from '../types/message';
10
12
  import type {
@@ -12,18 +14,83 @@ import type {
12
14
  IntegrationServiceSchema,
13
15
  } from '../types/service';
14
16
 
17
+ /**
18
+ * Security helpers for webhook validation
19
+ */
20
+ const SecurityHelpers = {
21
+ /**
22
+ * Secure comparison using Node.js crypto.timingSafeEqual
23
+ */
24
+ secureCompare(a: string, b: string): boolean {
25
+ try {
26
+ return crypto.timingSafeEqual(
27
+ Buffer.from(a, 'utf8'),
28
+ Buffer.from(b, 'utf8')
29
+ );
30
+ } catch {
31
+ return false;
32
+ }
33
+ },
34
+
35
+ /**
36
+ * Validate webhook timestamp to prevent replay attacks
37
+ */
38
+ validateTimestamp(timestamp: number, maxAgeMs = 5 * 60 * 1000): boolean {
39
+ return Date.now() - timestamp <= maxAgeMs;
40
+ },
41
+
42
+ /**
43
+ * Generate correlation ID for request tracking
44
+ */
45
+ generateCorrelationId(): string {
46
+ return crypto.randomUUID();
47
+ },
48
+ };
49
+
50
+ /**
51
+ * Retry mechanism with exponential backoff
52
+ */
53
+ async function executeWithRetry<T>(
54
+ operation: () => Promise<T>,
55
+ maxRetries = 3,
56
+ baseDelayMs = 1000,
57
+ context = 'operation'
58
+ ): Promise<T> {
59
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
60
+ try {
61
+ return await operation();
62
+ } catch (error: unknown) {
63
+ if (attempt === maxRetries) {
64
+ const err = error instanceof Error ? error : new Error(String(error));
65
+ throw new Error(
66
+ `${context} failed after ${maxRetries} retries: ${err.message}`
67
+ );
68
+ }
69
+
70
+ const delay = baseDelayMs * Math.pow(2, attempt);
71
+ const jitter = Math.random() * 1000;
72
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
73
+ }
74
+ }
75
+ throw new Error('Retry logic error');
76
+ }
77
+
15
78
  export function defineIntegration<
16
79
  TPlatformMessage extends PlatformMessage = PlatformMessage,
17
80
  TSettings = unknown,
18
- TContext extends AppContext = AppContext
81
+ TDatasources = unknown
19
82
  >(
20
- config: IntegrationServiceConfig<TPlatformMessage, TSettings, TContext>
83
+ config: IntegrationServiceConfig<
84
+ TPlatformMessage,
85
+ TSettings,
86
+ AppContext<TDatasources>
87
+ >
21
88
  ): IntegrationServiceSchema<TPlatformMessage, TSettings> {
22
89
  // Create actions from integration methods
23
- const actions: ServiceSchema['actions'] = {
24
- 'webhook.receive': {
90
+ const actions = {
91
+ i_receiveWebhook: {
25
92
  rest: {
26
- method: 'POST',
93
+ method: 'POST' as const,
27
94
  path: '/webhook',
28
95
  },
29
96
  params: {
@@ -36,14 +103,25 @@ export function defineIntegration<
36
103
  headers: 'object',
37
104
  timestamp: 'number',
38
105
  },
39
- async handler(ctx: Context<WebhookEvent>) {
106
+ async handler(
107
+ ctx: AppContext<TDatasources, WebhookEvent>
108
+ ): Promise<{success: boolean; messages: number}> {
40
109
  const webhook = ctx.params;
110
+ const correlationId = SecurityHelpers.generateCorrelationId();
111
+
112
+ // Add correlation ID to context for tracking
113
+ ctx.meta.correlationId = correlationId;
114
+
115
+ // Validate timestamp to prevent replay attacks
116
+ if (!SecurityHelpers.validateTimestamp(webhook.timestamp)) {
117
+ throw new Error('Webhook timestamp too old - possible replay attack');
118
+ }
41
119
 
42
120
  // Validate webhook if method exists
43
121
  if (config.validateWebhook) {
44
122
  const isValid = await config.validateWebhook(webhook);
45
123
  if (!isValid) {
46
- throw new Error('Invalid webhook');
124
+ throw new Error('Invalid webhook payload format');
47
125
  }
48
126
  }
49
127
 
@@ -51,22 +129,26 @@ export function defineIntegration<
51
129
  if (config.validateSignature) {
52
130
  const isValidSignature = config.validateSignature(webhook);
53
131
  if (!isValidSignature) {
54
- throw new Error('Invalid webhook signature');
132
+ throw new Error('Webhook signature validation failed');
55
133
  }
56
134
  }
57
135
 
58
136
  // Normalize the message
59
- const normalizedMessages = await config.normalize(webhook);
137
+ const normalizedMessages: NormalizedMessage[] = await config.normalize(
138
+ webhook
139
+ );
60
140
 
61
141
  // Process each message
62
142
  for (const message of normalizedMessages) {
63
- // Emit event for further processing
143
+ // Emit event for further processing with correlation ID
64
144
  ctx.emit('integration.message.received', {
145
+ correlationId,
65
146
  tenantId: webhook.tenantId,
66
147
  channelId: webhook.channelId,
67
148
  integrationId: webhook.integrationId,
68
149
  platform: webhook.platform,
69
150
  message,
151
+ timestamp: Date.now(),
70
152
  });
71
153
  }
72
154
 
@@ -74,16 +156,16 @@ export function defineIntegration<
74
156
  },
75
157
  },
76
158
 
77
- 'message.send': {
159
+ i_sendMessage: {
78
160
  rest: {
79
- method: 'POST',
161
+ method: 'POST' as const,
80
162
  path: '/send',
81
163
  },
82
164
  params: {
83
165
  message: 'object',
84
166
  config: 'object',
85
167
  },
86
- async handler(ctx: Context) {
168
+ async handler(ctx: AppContext<TDatasources>): Promise<unknown> {
87
169
  const {message, config: integrationConfig} = ctx.params as {
88
170
  message: NormalizedMessage;
89
171
  config: IntegrationConfig;
@@ -94,11 +176,12 @@ export function defineIntegration<
94
176
  await config.transform(message, integrationConfig);
95
177
  }
96
178
 
97
- // Send the message - cast ctx to expected type
98
- const result = await config.sendMessage(
99
- ctx as TContext,
100
- message,
101
- integrationConfig
179
+ // Send the message with retry mechanism
180
+ const result: SendResult = await executeWithRetry(
181
+ () => config.sendMessage(ctx, message, integrationConfig),
182
+ 3,
183
+ 1000,
184
+ `Send message via ${config.integration.platform}`
102
185
  );
103
186
 
104
187
  // Emit event based on result
@@ -120,31 +203,52 @@ export function defineIntegration<
120
203
  },
121
204
  },
122
205
 
123
- 'health.check': {
206
+ i_healthCheck: {
124
207
  rest: {
125
- method: 'GET',
208
+ method: 'GET' as const,
126
209
  path: '/health',
127
210
  },
128
211
  params: {
129
212
  config: {type: 'object', optional: true},
130
213
  },
131
- async handler(ctx: Context<{config?: IntegrationConfig}>) {
132
- if (config.checkHealth) {
133
- const integrationConfig = ctx.params.config;
134
- if (integrationConfig) {
135
- return await config.checkHealth(ctx as TContext, integrationConfig);
214
+ async handler(
215
+ ctx: AppContext<TDatasources, {config?: IntegrationConfig}>
216
+ ): Promise<unknown> {
217
+ try {
218
+ if (config.checkHealth) {
219
+ const integrationConfig = ctx.params.config;
220
+ if (integrationConfig) {
221
+ return await config.checkHealth(ctx, integrationConfig);
222
+ }
136
223
  }
224
+
225
+ return {
226
+ status: 'healthy',
227
+ message: `${config.integration.name} integration is running`,
228
+ details: {
229
+ integration: config.integration.id,
230
+ version: config.integration.version,
231
+ timestamp: new Date().toISOString(),
232
+ capabilities: config.integration.capabilities,
233
+ },
234
+ };
235
+ } catch (error: unknown) {
236
+ const err = error instanceof Error ? error : new Error(String(error));
237
+ return {
238
+ status: 'unhealthy',
239
+ message: err.message,
240
+ details: {
241
+ integration: config.integration.id,
242
+ timestamp: new Date().toISOString(),
243
+ },
244
+ };
137
245
  }
138
- return {
139
- status: 'healthy',
140
- message: 'Integration is running',
141
- };
142
246
  },
143
247
  },
144
248
 
145
- 'webhook.verify': {
249
+ i_verifyWebhook: {
146
250
  rest: {
147
- method: 'GET',
251
+ method: 'GET' as const,
148
252
  path: '/webhook',
149
253
  },
150
254
  params: {
@@ -152,21 +256,26 @@ export function defineIntegration<
152
256
  token: 'string',
153
257
  challenge: 'string',
154
258
  },
155
- handler(ctx: Context<{mode: string; token: string; challenge: string}>) {
259
+ handler(
260
+ ctx: AppContext<
261
+ TDatasources,
262
+ {mode: string; token: string; challenge: string}
263
+ >
264
+ ): string {
156
265
  if (config.verifyWebhook) {
157
- const result = config.verifyWebhook(ctx.params);
266
+ const result: string | null = config.verifyWebhook(ctx.params);
158
267
  return result ?? '';
159
268
  }
160
269
  return ctx.params.challenge;
161
270
  },
162
271
  },
163
272
 
164
- 'integration.status': {
273
+ i_status: {
165
274
  rest: {
166
- method: 'GET',
275
+ method: 'GET' as const,
167
276
  path: '/status',
168
277
  },
169
- handler() {
278
+ handler(): object {
170
279
  return {
171
280
  id: config.integration.id,
172
281
  name: config.integration.name,
@@ -178,11 +287,13 @@ export function defineIntegration<
178
287
  },
179
288
  },
180
289
 
181
- 'credentials.validate': {
290
+ i_validateCredentials: {
182
291
  params: {
183
292
  credentials: 'object',
184
293
  },
185
- async handler(ctx: Context<{credentials: Record<string, string>}>) {
294
+ async handler(
295
+ ctx: AppContext<TDatasources, {credentials: Record<string, string>}>
296
+ ): Promise<boolean> {
186
297
  if (config.validateCredentials) {
187
298
  return await config.validateCredentials(ctx.params.credentials);
188
299
  }
@@ -196,42 +307,47 @@ export function defineIntegration<
196
307
  Object.assign(actions, config.actions);
197
308
  }
198
309
 
199
- // Create the service schema
200
- const serviceSchema: IntegrationServiceSchema<TPlatformMessage, TSettings> = {
310
+ // Use defineService to get all the mixins and proper setup
311
+ const baseService = defineService<TDatasources, TSettings>({
201
312
  name: config.name,
202
313
  version: config.version,
203
314
  settings: config.settings as TSettings,
204
315
  dependencies: config.dependencies,
316
+ datasources: config.datasources,
205
317
  metadata: {
206
318
  ...config.metadata,
207
319
  integration: config.integration,
208
320
  },
209
321
  actions,
210
322
  events: config.events ?? {},
211
- methods: config.methods ?? {},
323
+ methods: {
324
+ ...(config.methods ?? {}),
325
+ // Add integration-specific methods to the service methods (filter out undefined)
326
+ // normalize is required, no need for conditional
327
+ normalize: config.normalize,
328
+ ...(config.transform && {transform: config.transform}),
329
+ ...(config.validateWebhook && {validateWebhook: config.validateWebhook}),
330
+ // sendMessage is required, no need for conditional
331
+ sendMessage: config.sendMessage,
332
+ ...(config.verifyWebhook && {verifyWebhook: config.verifyWebhook}),
333
+ ...(config.checkHealth && {checkHealth: config.checkHealth}),
334
+ ...(config.validateCredentials && {
335
+ validateCredentials: config.validateCredentials,
336
+ }),
337
+ ...(config.validateSignature && {
338
+ validateSignature: config.validateSignature,
339
+ }),
340
+ },
212
341
  hooks: config.hooks ?? {},
213
342
  created: config.created,
214
343
  started: config.started,
215
344
  stopped: config.stopped,
216
-
217
- // Integration-specific methods
218
- integration: config.integration,
219
- normalize: config.normalize,
220
- transform: config.transform,
221
- validateWebhook: config.validateWebhook,
222
- sendMessage: config.sendMessage,
223
- verifyWebhook: config.verifyWebhook,
224
- checkHealth: config.checkHealth,
225
- validateCredentials: config.validateCredentials,
226
- validateSignature: config.validateSignature,
227
- };
228
-
229
- // Remove undefined properties
230
- Object.keys(serviceSchema).forEach((key) => {
231
- if (serviceSchema[key as keyof typeof serviceSchema] === undefined) {
232
- delete serviceSchema[key as keyof typeof serviceSchema];
233
- }
234
345
  });
235
346
 
236
- return serviceSchema;
347
+ // Return the service schema - integration methods are available via service methods
348
+ return {
349
+ ...baseService,
350
+ // Only add the integration metadata
351
+ integration: config.integration,
352
+ } as IntegrationServiceSchema<TPlatformMessage, TSettings>;
237
353
  }
@@ -1,13 +1,8 @@
1
1
  import omit from 'lodash/omit';
2
2
  import type {ServiceSchema as MoleculerServiceSchema} from 'moleculer';
3
3
 
4
- import {
5
- ContextHelpersMiddleware,
6
- MemoizeMixin,
7
- PermissionsMiddleware,
8
- createDatasourceMiddleware,
9
- createCacheMiddleware,
10
- } from '../middlewares';
4
+ import {MemoizeMixin} from '../middlewares';
5
+ import {DatasourceMixin} from '../mixins';
11
6
  import type {ServiceConfig} from '../types/service';
12
7
 
13
8
  /**
@@ -18,13 +13,6 @@ import type {ServiceConfig} from '../types/service';
18
13
  * export default defineService({
19
14
  * name: 'user',
20
15
  *
21
- * // Per-service cache configuration
22
- * cache: {
23
- * redisUrl: 'redis://localhost:6379',
24
- * ttl: 10 * 60 * 1000, // 10 minutes
25
- * namespace: 'user-service',
26
- * },
27
- *
28
16
  * actions: {
29
17
  * // Action with schema
30
18
  * getUser: {
@@ -32,19 +20,14 @@ import type {ServiceConfig} from '../types/service';
32
20
  * id: 'string'
33
21
  * },
34
22
  * handler(ctx) {
35
- * // ctx is typed as AppContext with cache helpers
36
23
  * const { tenantId } = ctx.meta;
37
- *
38
- * // Use cache helpers
39
- * return ctx.cache.wrap(`user:${ctx.params.id}`, async () => {
40
- * return { id: ctx.params.id, tenantId };
41
- * });
24
+ *
25
+ * return { id: ctx.params.id, tenantId };
42
26
  * }
43
27
  * },
44
28
  *
45
29
  * // Direct handler
46
30
  * listUsers(ctx) {
47
- * // ctx is typed as AppContext
48
31
  * return [];
49
32
  * }
50
33
  * }
@@ -54,23 +37,20 @@ import type {ServiceConfig} from '../types/service';
54
37
  export function defineService<TDatasources = unknown, TSettings = unknown>(
55
38
  config: ServiceConfig<TSettings, TDatasources>
56
39
  ): MoleculerServiceSchema<TSettings> {
57
- const serviceSchema = omit(config, [
58
- 'datasources',
59
- 'cache',
60
- ]) as unknown as MoleculerServiceSchema<TSettings>;
40
+ const propsToOmit = ['datasources'];
41
+ const serviceSchema = omit(
42
+ config,
43
+ propsToOmit
44
+ ) as unknown as MoleculerServiceSchema<TSettings>;
61
45
 
62
46
  const datasources = config.datasources ?? {};
63
- const cacheOptions = config.cache;
64
47
 
65
48
  // TODO: Add mixins config support
66
49
  return {
67
50
  ...serviceSchema,
68
51
  mixins: [
69
- createDatasourceMiddleware(datasources),
70
- ...(cacheOptions ? [createCacheMiddleware(cacheOptions)] : []),
52
+ DatasourceMixin(datasources),
71
53
  MemoizeMixin(),
72
- PermissionsMiddleware,
73
- ContextHelpersMiddleware,
74
54
  ...(serviceSchema.mixins ?? []),
75
55
  ],
76
56
  };
@@ -8,11 +8,6 @@ export type GetUserActionInputParams = {
8
8
  export default defineService<UserDatasourceTypes>({
9
9
  name: 'test',
10
10
  datasources,
11
- cache: {
12
- redisUrl: process.env.USER_SERVICE_REDIS_URL,
13
- ttl: 5 * 60 * 1000, // 5 minutes
14
- namespace: 'user-service',
15
- },
16
11
  actions: {
17
12
  user: {
18
13
  params: {
@@ -20,11 +15,8 @@ export default defineService<UserDatasourceTypes>({
20
15
  },
21
16
  handler(ctx) {
22
17
  const userDs = ctx.datasources.user;
23
-
24
- // Use cache with wrap pattern
25
- return ctx.cache.wrap(`user:${ctx.params.id}`, async () => {
26
- return userDs.getUser();
27
- });
18
+
19
+ return userDs.getUser();
28
20
  },
29
21
  },
30
22
  },
@@ -1,7 +1,6 @@
1
1
  import type {Context} from 'moleculer';
2
- import type {User} from './user';
3
2
  import type {Tenant} from './tenant';
4
- import type {CacheHelpers} from '../middlewares/cache.middleware';
3
+ import type {User} from './user';
5
4
 
6
5
  export interface AppMeta {
7
6
  user?: User;
@@ -11,6 +10,7 @@ export interface AppMeta {
11
10
  integrationId?: string;
12
11
  channelId?: string;
13
12
  requestId?: string;
13
+ correlationId?: string;
14
14
  userAgent?: string;
15
15
  clientIP?: string;
16
16
  [key: string]: unknown;
@@ -25,7 +25,11 @@ export interface PermissionHelpers {
25
25
  ensureTenant(): Tenant;
26
26
  // New enhanced helpers
27
27
  getUserPermissions(): Promise<string[]>;
28
- auditLog(action: string, resource?: unknown, metadata?: Record<string, unknown>): void;
28
+ auditLog(
29
+ action: string,
30
+ resource?: unknown,
31
+ metadata?: Record<string, unknown>
32
+ ): void;
29
33
  createError(message: string, code: string, statusCode?: number): Error;
30
34
  }
31
35
 
@@ -37,5 +41,4 @@ export type AppContext<
37
41
  > = Context<TParams, TMeta, TLocals> &
38
42
  PermissionHelpers & {
39
43
  datasources: TDatasources;
40
- cache: CacheHelpers;
41
44
  };
@@ -1,3 +1,4 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  import type {IntegrationPlatform} from './platform';
2
3
 
3
4
  export interface MessageParticipant {
@@ -8,7 +8,6 @@ import type {
8
8
  } from 'moleculer';
9
9
 
10
10
  import type {DatasourceConstructorRegistry} from '../middlewares/datasource.middleware';
11
- import type {CacheOptions} from '../middlewares/cache.middleware';
12
11
  import type {AppContext} from './context';
13
12
  import type {BaseIntegration, IntegrationConfig} from './integration';
14
13
  import type {
@@ -125,15 +124,15 @@ export type ServiceConfig<
125
124
  TDatasources = unknown
126
125
  > = ServiceSchema<TSettings, TDatasources> & {
127
126
  datasources?: DatasourceConstructorRegistry;
128
- cache?: CacheOptions;
129
127
  };
130
128
 
131
129
  // Integration-specific types
132
130
  export interface IntegrationServiceConfig<
133
131
  TPlatformMessage extends PlatformMessage = PlatformMessage,
134
132
  TSettings = unknown,
135
- TContext extends AppContext = AppContext
136
- > extends Omit<ServiceConfig<TSettings>, 'name'> {
133
+ TContext extends AppContext = AppContext,
134
+ TDatasources = unknown
135
+ > extends ServiceConfig<TSettings, TDatasources> {
137
136
  name: string;
138
137
  integration: BaseIntegration;
139
138