@hiliosai/sdk 0.1.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 (35) hide show
  1. package/package.json +27 -0
  2. package/src/configs/index.ts +1 -0
  3. package/src/configs/permissions.ts +109 -0
  4. package/src/env.ts +12 -0
  5. package/src/errors/auth.error.ts +33 -0
  6. package/src/errors/index.ts +2 -0
  7. package/src/errors/permission.error.ts +17 -0
  8. package/src/examples/cache-usage.example.ts +293 -0
  9. package/src/index.ts +5 -0
  10. package/src/middlewares/cache.middleware.ts +278 -0
  11. package/src/middlewares/context-helpers.middleware.ts +159 -0
  12. package/src/middlewares/datasource.middleware.ts +71 -0
  13. package/src/middlewares/health.middleware.ts +134 -0
  14. package/src/middlewares/index.ts +6 -0
  15. package/src/middlewares/memoize.middleware.ts +33 -0
  16. package/src/middlewares/permissions.middleware.ts +162 -0
  17. package/src/service/define-integration.ts +237 -0
  18. package/src/service/define-service.ts +77 -0
  19. package/src/service/example-user/datasources/index.ts +8 -0
  20. package/src/service/example-user/datasources/user.datasource.ts +7 -0
  21. package/src/service/example-user/user.service.ts +31 -0
  22. package/src/service/example-user/utils.ts +0 -0
  23. package/src/types/context.ts +41 -0
  24. package/src/types/datasource.ts +23 -0
  25. package/src/types/index.ts +8 -0
  26. package/src/types/integration.ts +48 -0
  27. package/src/types/message.ts +95 -0
  28. package/src/types/platform.ts +39 -0
  29. package/src/types/service.ts +208 -0
  30. package/src/types/tenant.ts +4 -0
  31. package/src/types/user.ts +16 -0
  32. package/src/utils/context-cache.ts +70 -0
  33. package/src/utils/permission-calculator.ts +62 -0
  34. package/tsconfig.json +13 -0
  35. package/tsup.config.ts +6 -0
@@ -0,0 +1,162 @@
1
+ import type {ActionSchema, Service} from 'moleculer';
2
+
3
+ import {PERMISSIONS, ROLE_PERMISSIONS} from '../configs';
4
+ import {isDev} from '../env';
5
+
6
+ import {PermissionError} from '../errors';
7
+ import type {AppContext} from '../types';
8
+
9
+ export type Permission =
10
+ | keyof typeof PERMISSIONS
11
+ | string
12
+ | ((ctx: AppContext, action: ActionSchema) => Promise<boolean> | boolean);
13
+
14
+ // Middleware interface for type safety
15
+ export interface ActionWithPermissions extends ActionSchema {
16
+ permissions?: Permission | Permission[];
17
+ }
18
+
19
+ const permissionHandlers = {
20
+ [PERMISSIONS.AUTHENTICATED]: async (ctx: AppContext) => !!ctx.meta.user?.id,
21
+
22
+ [PERMISSIONS.TENANT_OWNER]: async (ctx: AppContext) =>
23
+ ctx.meta.user?.roles.includes(PERMISSIONS.OWNER) ?? false,
24
+
25
+ [PERMISSIONS.TENANT_MEMBER]: async (ctx: AppContext) =>
26
+ !!(
27
+ ctx.meta.user?.tenantId &&
28
+ ctx.meta.tenantId &&
29
+ ctx.meta.user.tenantId === ctx.meta.tenantId
30
+ ),
31
+ };
32
+
33
+ export const PermissionsMiddleware = {
34
+ // Wrap local action handlers
35
+ localAction(
36
+ handler: (...args: unknown[]) => unknown,
37
+ action: ActionWithPermissions
38
+ ) {
39
+ // If permissions are not defined, return original handler
40
+ if (!action.permissions) {
41
+ return handler;
42
+ }
43
+
44
+ const permissions = Array.isArray(action.permissions)
45
+ ? action.permissions
46
+ : [action.permissions];
47
+
48
+ const permissionNames: string[] = [];
49
+ const permissionFunctions: Array<
50
+ (ctx: AppContext, action: ActionSchema) => Promise<boolean> | boolean
51
+ > = [];
52
+
53
+ // Process each permission
54
+ permissions.forEach((permission) => {
55
+ if (typeof permission === 'function') {
56
+ // Add custom permission function
57
+ permissionFunctions.push(permission);
58
+ return;
59
+ }
60
+
61
+ if (typeof permission === 'string') {
62
+ // Check if it's a built-in permission handler
63
+ if (permission in permissionHandlers) {
64
+ const handler =
65
+ permissionHandlers[permission as keyof typeof permissionHandlers];
66
+ permissionFunctions.push(handler);
67
+ return;
68
+ }
69
+
70
+ // Otherwise, treat as a permission name to check against user roles
71
+ permissionNames.push(permission);
72
+ return;
73
+ }
74
+ });
75
+
76
+ return async function CheckPermissionsMiddleware(
77
+ this: Service,
78
+ ctx: AppContext
79
+ ) {
80
+ let hasAccess = false;
81
+
82
+ // Check if user has OWNER role (super admin within tenant)
83
+ if (ctx.meta.user?.roles.includes(PERMISSIONS.OWNER)) {
84
+ hasAccess = true;
85
+ }
86
+
87
+ // REMOVED: DEVELOPER role bypass - security vulnerability
88
+ // Developers must have explicit permissions like any other role
89
+
90
+ // Check custom permission functions
91
+ if (!hasAccess && permissionFunctions.length > 0) {
92
+ const results = await Promise.allSettled(
93
+ permissionFunctions.map((fn) => fn(ctx, action))
94
+ );
95
+
96
+ hasAccess = results.some(
97
+ (result) => result.status === 'fulfilled' && !!result.value
98
+ );
99
+
100
+ // Log failed permissions without exposing details
101
+ const failures = results.filter((r) => r.status === 'rejected');
102
+ if (failures.length > 0) {
103
+ ctx.broker.logger.warn(
104
+ `${failures.length} permission functions failed`,
105
+ {
106
+ action: action.name,
107
+ userId: ctx.meta.user?.id,
108
+ }
109
+ );
110
+ }
111
+ }
112
+
113
+ // Check named permissions against user roles
114
+ if (!hasAccess && permissionNames.length > 0) {
115
+ const userRoles = ctx.meta.user?.roles ?? [];
116
+
117
+ // Check if user has any role that includes the required permissions
118
+ hasAccess = userRoles.some((role: string) => {
119
+ const rolePermissions = ROLE_PERMISSIONS[role] ?? [];
120
+ return permissionNames.some((permName) =>
121
+ rolePermissions.includes(permName)
122
+ );
123
+ });
124
+ }
125
+
126
+ // Throw error if access denied
127
+ if (!hasAccess) {
128
+ const user = ctx.meta.user;
129
+ ctx.broker.logger.warn('Access denied:', {
130
+ action: action.name,
131
+ userId: user?.id,
132
+ userRoles: user?.roles,
133
+ tenantId: ctx.meta.tenantId,
134
+ requiredPermissions: permissions,
135
+ });
136
+
137
+ // Sanitize error details for production
138
+ const errorDetails = isDev
139
+ ? {
140
+ action: action.name,
141
+ requiredPermissions: permissions.map((p) =>
142
+ typeof p === 'function' ? '[Function]' : String(p)
143
+ ),
144
+ userRoles: user?.roles ?? [],
145
+ userId: user?.id,
146
+ tenantId: ctx.meta.tenantId,
147
+ }
148
+ : {
149
+ action: action.name,
150
+ };
151
+
152
+ throw new PermissionError(
153
+ 'You do not have permission to perform this action',
154
+ errorDetails
155
+ );
156
+ }
157
+
158
+ // Call the original handler
159
+ return handler.call(this, ctx);
160
+ };
161
+ },
162
+ };
@@ -0,0 +1,237 @@
1
+ import type {Context, ServiceSchema} from 'moleculer';
2
+
3
+ import type {AppContext} from '../types/context';
4
+ import type {IntegrationConfig} from '../types/integration';
5
+ import type {
6
+ NormalizedMessage,
7
+ PlatformMessage,
8
+ WebhookEvent,
9
+ } from '../types/message';
10
+ import type {
11
+ IntegrationServiceConfig,
12
+ IntegrationServiceSchema,
13
+ } from '../types/service';
14
+
15
+ export function defineIntegration<
16
+ TPlatformMessage extends PlatformMessage = PlatformMessage,
17
+ TSettings = unknown,
18
+ TContext extends AppContext = AppContext
19
+ >(
20
+ config: IntegrationServiceConfig<TPlatformMessage, TSettings, TContext>
21
+ ): IntegrationServiceSchema<TPlatformMessage, TSettings> {
22
+ // Create actions from integration methods
23
+ const actions: ServiceSchema['actions'] = {
24
+ 'webhook.receive': {
25
+ rest: {
26
+ method: 'POST',
27
+ path: '/webhook',
28
+ },
29
+ params: {
30
+ tenantId: 'string',
31
+ channelId: 'string',
32
+ integrationId: 'string',
33
+ platform: 'string',
34
+ rawPayload: 'any',
35
+ rawBody: {type: 'string', optional: true},
36
+ headers: 'object',
37
+ timestamp: 'number',
38
+ },
39
+ async handler(ctx: Context<WebhookEvent>) {
40
+ const webhook = ctx.params;
41
+
42
+ // Validate webhook if method exists
43
+ if (config.validateWebhook) {
44
+ const isValid = await config.validateWebhook(webhook);
45
+ if (!isValid) {
46
+ throw new Error('Invalid webhook');
47
+ }
48
+ }
49
+
50
+ // Validate signature if method exists
51
+ if (config.validateSignature) {
52
+ const isValidSignature = config.validateSignature(webhook);
53
+ if (!isValidSignature) {
54
+ throw new Error('Invalid webhook signature');
55
+ }
56
+ }
57
+
58
+ // Normalize the message
59
+ const normalizedMessages = await config.normalize(webhook);
60
+
61
+ // Process each message
62
+ for (const message of normalizedMessages) {
63
+ // Emit event for further processing
64
+ ctx.emit('integration.message.received', {
65
+ tenantId: webhook.tenantId,
66
+ channelId: webhook.channelId,
67
+ integrationId: webhook.integrationId,
68
+ platform: webhook.platform,
69
+ message,
70
+ });
71
+ }
72
+
73
+ return {success: true, messages: normalizedMessages.length};
74
+ },
75
+ },
76
+
77
+ 'message.send': {
78
+ rest: {
79
+ method: 'POST',
80
+ path: '/send',
81
+ },
82
+ params: {
83
+ message: 'object',
84
+ config: 'object',
85
+ },
86
+ async handler(ctx: Context) {
87
+ const {message, config: integrationConfig} = ctx.params as {
88
+ message: NormalizedMessage;
89
+ config: IntegrationConfig;
90
+ };
91
+
92
+ // Transform message if method exists
93
+ if (config.transform) {
94
+ await config.transform(message, integrationConfig);
95
+ }
96
+
97
+ // Send the message - cast ctx to expected type
98
+ const result = await config.sendMessage(
99
+ ctx as TContext,
100
+ message,
101
+ integrationConfig
102
+ );
103
+
104
+ // Emit event based on result
105
+ if (result.success) {
106
+ ctx.emit('integration.message.sent', {
107
+ messageId: result.messageId,
108
+ platform: config.integration.platform,
109
+ metadata: result.metadata,
110
+ });
111
+ } else {
112
+ ctx.emit('integration.message.failed', {
113
+ error: result.error,
114
+ platform: config.integration.platform,
115
+ message,
116
+ });
117
+ }
118
+
119
+ return result;
120
+ },
121
+ },
122
+
123
+ 'health.check': {
124
+ rest: {
125
+ method: 'GET',
126
+ path: '/health',
127
+ },
128
+ params: {
129
+ config: {type: 'object', optional: true},
130
+ },
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);
136
+ }
137
+ }
138
+ return {
139
+ status: 'healthy',
140
+ message: 'Integration is running',
141
+ };
142
+ },
143
+ },
144
+
145
+ 'webhook.verify': {
146
+ rest: {
147
+ method: 'GET',
148
+ path: '/webhook',
149
+ },
150
+ params: {
151
+ mode: 'string',
152
+ token: 'string',
153
+ challenge: 'string',
154
+ },
155
+ handler(ctx: Context<{mode: string; token: string; challenge: string}>) {
156
+ if (config.verifyWebhook) {
157
+ const result = config.verifyWebhook(ctx.params);
158
+ return result ?? '';
159
+ }
160
+ return ctx.params.challenge;
161
+ },
162
+ },
163
+
164
+ 'integration.status': {
165
+ rest: {
166
+ method: 'GET',
167
+ path: '/status',
168
+ },
169
+ handler() {
170
+ return {
171
+ id: config.integration.id,
172
+ name: config.integration.name,
173
+ platform: config.integration.platform,
174
+ version: config.integration.version,
175
+ status: config.integration.status,
176
+ capabilities: config.integration.capabilities,
177
+ };
178
+ },
179
+ },
180
+
181
+ 'credentials.validate': {
182
+ params: {
183
+ credentials: 'object',
184
+ },
185
+ async handler(ctx: Context<{credentials: Record<string, string>}>) {
186
+ if (config.validateCredentials) {
187
+ return await config.validateCredentials(ctx.params.credentials);
188
+ }
189
+ return true;
190
+ },
191
+ },
192
+ };
193
+
194
+ // Add any custom actions
195
+ if (config.actions) {
196
+ Object.assign(actions, config.actions);
197
+ }
198
+
199
+ // Create the service schema
200
+ const serviceSchema: IntegrationServiceSchema<TPlatformMessage, TSettings> = {
201
+ name: config.name,
202
+ version: config.version,
203
+ settings: config.settings as TSettings,
204
+ dependencies: config.dependencies,
205
+ metadata: {
206
+ ...config.metadata,
207
+ integration: config.integration,
208
+ },
209
+ actions,
210
+ events: config.events ?? {},
211
+ methods: config.methods ?? {},
212
+ hooks: config.hooks ?? {},
213
+ created: config.created,
214
+ started: config.started,
215
+ 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
+ });
235
+
236
+ return serviceSchema;
237
+ }
@@ -0,0 +1,77 @@
1
+ import omit from 'lodash/omit';
2
+ import type {ServiceSchema as MoleculerServiceSchema} from 'moleculer';
3
+
4
+ import {
5
+ ContextHelpersMiddleware,
6
+ MemoizeMixin,
7
+ PermissionsMiddleware,
8
+ createDatasourceMiddleware,
9
+ createCacheMiddleware,
10
+ } from '../middlewares';
11
+ import type {ServiceConfig} from '../types/service';
12
+
13
+ /**
14
+ * Define a service
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * export default defineService({
19
+ * name: 'user',
20
+ *
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
+ * actions: {
29
+ * // Action with schema
30
+ * getUser: {
31
+ * params: {
32
+ * id: 'string'
33
+ * },
34
+ * handler(ctx) {
35
+ * // ctx is typed as AppContext with cache helpers
36
+ * 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
+ * });
42
+ * }
43
+ * },
44
+ *
45
+ * // Direct handler
46
+ * listUsers(ctx) {
47
+ * // ctx is typed as AppContext
48
+ * return [];
49
+ * }
50
+ * }
51
+ * });
52
+ * ```
53
+ */
54
+ export function defineService<TDatasources = unknown, TSettings = unknown>(
55
+ config: ServiceConfig<TSettings, TDatasources>
56
+ ): MoleculerServiceSchema<TSettings> {
57
+ const serviceSchema = omit(config, [
58
+ 'datasources',
59
+ 'cache',
60
+ ]) as unknown as MoleculerServiceSchema<TSettings>;
61
+
62
+ const datasources = config.datasources ?? {};
63
+ const cacheOptions = config.cache;
64
+
65
+ // TODO: Add mixins config support
66
+ return {
67
+ ...serviceSchema,
68
+ mixins: [
69
+ createDatasourceMiddleware(datasources),
70
+ ...(cacheOptions ? [createCacheMiddleware(cacheOptions)] : []),
71
+ MemoizeMixin(),
72
+ PermissionsMiddleware,
73
+ ContextHelpersMiddleware,
74
+ ...(serviceSchema.mixins ?? []),
75
+ ],
76
+ };
77
+ }
@@ -0,0 +1,8 @@
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>;
@@ -0,0 +1,7 @@
1
+ import type {User} from '../../../types';
2
+
3
+ export class UserDatasource {
4
+ getUser(): User {
5
+ return {} as User;
6
+ }
7
+ }
@@ -0,0 +1,31 @@
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
+ cache: {
12
+ redisUrl: process.env.USER_SERVICE_REDIS_URL,
13
+ ttl: 5 * 60 * 1000, // 5 minutes
14
+ namespace: 'user-service',
15
+ },
16
+ actions: {
17
+ user: {
18
+ params: {
19
+ id: 'string',
20
+ },
21
+ handler(ctx) {
22
+ 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
+ });
28
+ },
29
+ },
30
+ },
31
+ });
File without changes
@@ -0,0 +1,41 @@
1
+ import type {Context} from 'moleculer';
2
+ import type {User} from './user';
3
+ import type {Tenant} from './tenant';
4
+ import type {CacheHelpers} from '../middlewares/cache.middleware';
5
+
6
+ export interface AppMeta {
7
+ user?: User;
8
+ tenantId?: string;
9
+ tenantName?: string;
10
+ userId?: string;
11
+ integrationId?: string;
12
+ channelId?: string;
13
+ requestId?: string;
14
+ userAgent?: string;
15
+ clientIP?: string;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ export interface PermissionHelpers {
20
+ hasPermission(permission: string): boolean;
21
+ hasRole(role: string): boolean;
22
+ isTenantMember(): boolean;
23
+ isTenantOwner(): boolean;
24
+ ensureUser(): User;
25
+ ensureTenant(): Tenant;
26
+ // New enhanced helpers
27
+ getUserPermissions(): Promise<string[]>;
28
+ auditLog(action: string, resource?: unknown, metadata?: Record<string, unknown>): void;
29
+ createError(message: string, code: string, statusCode?: number): Error;
30
+ }
31
+
32
+ export type AppContext<
33
+ TDatasources = unknown,
34
+ TParams = unknown,
35
+ TMeta extends AppMeta = AppMeta,
36
+ TLocals = unknown
37
+ > = Context<TParams, TMeta, TLocals> &
38
+ PermissionHelpers & {
39
+ datasources: TDatasources;
40
+ cache: CacheHelpers;
41
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Utility types for datasources
3
+ */
4
+
5
+ /**
6
+ * Extract instance types from a datasource constructor registry
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * const datasources = {
11
+ * user: UserDatasource,
12
+ * whatsapp: WhatsAppDatasource,
13
+ * };
14
+ *
15
+ * type MyDatasources = DatasourceInstanceTypes<typeof datasources>;
16
+ * // Results in: { user: UserDatasource; whatsapp: WhatsAppDatasource; }
17
+ * ```
18
+ */
19
+ export type DatasourceInstanceTypes<
20
+ T extends Record<string, new (...args: unknown[]) => unknown>
21
+ > = {
22
+ [K in keyof T]: InstanceType<T[K]>;
23
+ };
@@ -0,0 +1,8 @@
1
+ export * from './platform';
2
+ export * from './message';
3
+ export * from './integration';
4
+ export * from './context';
5
+ export * from './service';
6
+ export * from './user';
7
+ export * from './tenant';
8
+ export * from './datasource';
@@ -0,0 +1,48 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type {
3
+ IntegrationCapability,
4
+ IntegrationPlatform,
5
+ IntegrationStatus,
6
+ } from './platform';
7
+
8
+ export interface BaseIntegration {
9
+ id: string;
10
+ name: string;
11
+ platform: IntegrationPlatform;
12
+ version: string;
13
+ status: IntegrationStatus;
14
+ capabilities: IntegrationCapability[];
15
+ description?: string;
16
+ icon?: string;
17
+ documentationUrl?: string;
18
+ }
19
+
20
+ export interface IntegrationConfig {
21
+ id: string;
22
+ tenantId: string;
23
+ integrationId: string;
24
+ name: string;
25
+ version: string;
26
+ status: IntegrationStatus;
27
+ config: {
28
+ webhookUrl?: string;
29
+ autoReply?: boolean;
30
+ businessHours?: BusinessHoursConfig;
31
+ [key: string]: any;
32
+ };
33
+ credentials: Record<string, string>;
34
+ metadata?: Record<string, any>;
35
+ createdAt?: Date;
36
+ updatedAt?: Date;
37
+ }
38
+
39
+ export interface BusinessHoursConfig {
40
+ enabled: boolean;
41
+ timezone: string;
42
+ schedule: {
43
+ [day: string]: {
44
+ start: string;
45
+ end: string;
46
+ };
47
+ };
48
+ }