@hiliosai/sdk 0.1.12 → 0.1.15
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/dist/index.d.ts +904 -0
- package/dist/index.js +1809 -0
- package/package.json +10 -2
- package/src/configs/constants.ts +0 -135
- package/src/configs/index.ts +0 -2
- package/src/configs/moleculer/bulkhead.ts +0 -8
- package/src/configs/moleculer/channels.ts +0 -102
- package/src/configs/moleculer/circuit-breaker.ts +0 -17
- package/src/configs/moleculer/index.ts +0 -98
- package/src/configs/moleculer/logger.ts +0 -17
- package/src/configs/moleculer/metrics.ts +0 -20
- package/src/configs/moleculer/registry.ts +0 -7
- package/src/configs/moleculer/retry-policy.ts +0 -17
- package/src/configs/moleculer/tracing.ts +0 -6
- package/src/configs/moleculer/tracking.ts +0 -6
- package/src/configs/permissions.ts +0 -109
- package/src/datasources/base.datasource.ts +0 -111
- package/src/datasources/extensions/index.ts +0 -11
- package/src/datasources/extensions/retry.extension.ts +0 -91
- package/src/datasources/extensions/soft-delete.extension.ts +0 -114
- package/src/datasources/extensions/tenant.extension.ts +0 -105
- package/src/datasources/index.ts +0 -3
- package/src/datasources/prisma.datasource.ts +0 -317
- package/src/env.ts +0 -12
- package/src/errors/auth.error.ts +0 -33
- package/src/errors/index.ts +0 -2
- package/src/errors/permission.error.ts +0 -17
- package/src/index.ts +0 -10
- package/src/middlewares/context-helpers.middleware.ts +0 -162
- package/src/middlewares/datasource.middleware.ts +0 -73
- package/src/middlewares/health.middleware.ts +0 -134
- package/src/middlewares/index.ts +0 -5
- package/src/middlewares/memoize.middleware.ts +0 -33
- package/src/middlewares/permissions.middleware.ts +0 -162
- package/src/mixins/datasource.mixin.ts +0 -111
- package/src/mixins/index.ts +0 -1
- package/src/service/define-integration.ts +0 -404
- package/src/service/define-service.ts +0 -58
- package/src/types/channels.ts +0 -60
- package/src/types/context.ts +0 -64
- package/src/types/datasource.ts +0 -23
- package/src/types/index.ts +0 -9
- package/src/types/integration.ts +0 -28
- package/src/types/message.ts +0 -128
- package/src/types/platform.ts +0 -39
- package/src/types/service.ts +0 -209
- package/src/types/tenant.ts +0 -4
- package/src/types/user.ts +0 -16
- package/src/utils/context-cache.ts +0 -70
- package/src/utils/index.ts +0 -8
- package/src/utils/permission-calculator.ts +0 -62
- package/tsconfig.json +0 -13
- package/tsup.config.ts +0 -5
package/dist/index.js
ADDED
|
@@ -0,0 +1,1809 @@
|
|
|
1
|
+
import env3 from '@ltv/env';
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { Middleware } from '@moleculer/channels';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
|
|
7
|
+
// src/middlewares/datasource.middleware.ts
|
|
8
|
+
function initializeDatasources(constructorRegistry) {
|
|
9
|
+
const initializedDatasources = {};
|
|
10
|
+
for (const [key, DatasourceClass] of Object.entries(constructorRegistry)) {
|
|
11
|
+
initializedDatasources[key] = new DatasourceClass();
|
|
12
|
+
}
|
|
13
|
+
return initializedDatasources;
|
|
14
|
+
}
|
|
15
|
+
function createDatasourceMiddleware(datasources) {
|
|
16
|
+
const initializedDatasources = initializeDatasources(datasources);
|
|
17
|
+
return {
|
|
18
|
+
localAction(handler) {
|
|
19
|
+
return function DatasourceWrapper(ctx) {
|
|
20
|
+
ctx.datasources = initializedDatasources;
|
|
21
|
+
return handler.call(this, ctx);
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
remoteAction(handler) {
|
|
25
|
+
return function DatasourceWrapper(ctx) {
|
|
26
|
+
ctx.datasources = initializedDatasources;
|
|
27
|
+
return handler.call(this, ctx);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/middlewares/memoize.middleware.ts
|
|
34
|
+
function MemoizeMixin(options) {
|
|
35
|
+
return {
|
|
36
|
+
name: "",
|
|
37
|
+
methods: {
|
|
38
|
+
async memoize(name, params, fn) {
|
|
39
|
+
if (!this.broker.cacher) return fn();
|
|
40
|
+
const key = this.broker.cacher.defaultKeygen(
|
|
41
|
+
`${this.name}:memoize-${name}`,
|
|
42
|
+
params,
|
|
43
|
+
{},
|
|
44
|
+
[]
|
|
45
|
+
);
|
|
46
|
+
let res = await this.broker.cacher.get(key);
|
|
47
|
+
if (res) return res;
|
|
48
|
+
res = await fn();
|
|
49
|
+
this.broker.cacher.set(key, res, options?.ttl);
|
|
50
|
+
return res;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
var HEALTH_CHECK_PORT = env3.int("HEALTH_CHECK_PORT", 3301);
|
|
56
|
+
var HEALTH_CHECK_READINESS_PATH = env3.string(
|
|
57
|
+
"HEALTH_CHECK_READINESS_PATH",
|
|
58
|
+
"/readyz"
|
|
59
|
+
);
|
|
60
|
+
var HEALTH_CHECK_LIVENESS_PATH = env3.string(
|
|
61
|
+
"HEALTH_CHECK_LIVENESS_PATH",
|
|
62
|
+
"/livez"
|
|
63
|
+
);
|
|
64
|
+
var HEALTH_CHECK_DEFAULTS = {
|
|
65
|
+
PORT: HEALTH_CHECK_PORT,
|
|
66
|
+
READINESS_PATH: HEALTH_CHECK_READINESS_PATH,
|
|
67
|
+
LIVENESS_PATH: HEALTH_CHECK_LIVENESS_PATH
|
|
68
|
+
};
|
|
69
|
+
function CreateHealthCheckMiddleware(opts = {}) {
|
|
70
|
+
const config = {
|
|
71
|
+
port: opts.port ?? HEALTH_CHECK_DEFAULTS.PORT,
|
|
72
|
+
readiness: {
|
|
73
|
+
path: opts.readiness?.path ?? HEALTH_CHECK_DEFAULTS.READINESS_PATH
|
|
74
|
+
},
|
|
75
|
+
liveness: {
|
|
76
|
+
path: opts.liveness?.path ?? HEALTH_CHECK_DEFAULTS.LIVENESS_PATH
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
let state = "down";
|
|
80
|
+
let server;
|
|
81
|
+
function handler(req, res) {
|
|
82
|
+
if (res.headersSent) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (req.url === config.readiness.path || req.url === config.liveness.path) {
|
|
86
|
+
const resHeader = {
|
|
87
|
+
"Content-Type": "application/json; charset=utf-8"
|
|
88
|
+
};
|
|
89
|
+
const content = {
|
|
90
|
+
state,
|
|
91
|
+
uptime: process.uptime(),
|
|
92
|
+
timestamp: Date.now()
|
|
93
|
+
};
|
|
94
|
+
if (req.url === config.readiness.path) {
|
|
95
|
+
res.writeHead(state === "up" ? 200 : 503, resHeader);
|
|
96
|
+
} else {
|
|
97
|
+
res.writeHead(state !== "down" ? 200 : 503, resHeader);
|
|
98
|
+
}
|
|
99
|
+
res.end(JSON.stringify(content, null, 2));
|
|
100
|
+
} else {
|
|
101
|
+
res.writeHead(404, http.STATUS_CODES[404] ?? "Not Found", {});
|
|
102
|
+
res.end();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
created(broker) {
|
|
107
|
+
state = "starting";
|
|
108
|
+
server = http.createServer(handler);
|
|
109
|
+
server.listen(config.port, (err) => {
|
|
110
|
+
if (err) {
|
|
111
|
+
return broker.logger.error(
|
|
112
|
+
"Unable to start health-check server",
|
|
113
|
+
err
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
broker.logger.info("");
|
|
117
|
+
broker.logger.info("K8s health-check server listening on");
|
|
118
|
+
broker.logger.info(
|
|
119
|
+
` http://localhost:${config.port}${config.readiness.path}`
|
|
120
|
+
);
|
|
121
|
+
broker.logger.info(
|
|
122
|
+
` http://localhost:${config.port}${config.liveness.path}`
|
|
123
|
+
);
|
|
124
|
+
broker.logger.info("");
|
|
125
|
+
});
|
|
126
|
+
},
|
|
127
|
+
// After broker started
|
|
128
|
+
started() {
|
|
129
|
+
state = "up";
|
|
130
|
+
},
|
|
131
|
+
// Before broker stopping
|
|
132
|
+
stopping() {
|
|
133
|
+
state = "stopping";
|
|
134
|
+
},
|
|
135
|
+
// After broker stopped
|
|
136
|
+
stopped() {
|
|
137
|
+
state = "down";
|
|
138
|
+
server.close();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/configs/permissions.ts
|
|
144
|
+
var PERMISSIONS = {
|
|
145
|
+
// Authentication required
|
|
146
|
+
AUTHENTICATED: "authenticated",
|
|
147
|
+
// Role-based permissions
|
|
148
|
+
OWNER: "OWNER",
|
|
149
|
+
ADMIN: "ADMIN",
|
|
150
|
+
MANAGER: "MANAGER",
|
|
151
|
+
AGENT: "AGENT",
|
|
152
|
+
VIEWER: "VIEWER",
|
|
153
|
+
DEVELOPER: "DEVELOPER",
|
|
154
|
+
// Entity-specific permissions
|
|
155
|
+
TENANT_OWNER: "tenant.owner",
|
|
156
|
+
TENANT_MEMBER: "tenant.member",
|
|
157
|
+
// Resource permissions
|
|
158
|
+
"users.read": "users.read",
|
|
159
|
+
"users.write": "users.write",
|
|
160
|
+
"users.delete": "users.delete",
|
|
161
|
+
"tenants.read": "tenants.read",
|
|
162
|
+
"tenants.write": "tenants.write",
|
|
163
|
+
"tenants.delete": "tenants.delete",
|
|
164
|
+
"conversations.read": "conversations.read",
|
|
165
|
+
"conversations.write": "conversations.write",
|
|
166
|
+
"conversations.delete": "conversations.delete",
|
|
167
|
+
"messages.read": "messages.read",
|
|
168
|
+
"messages.write": "messages.write",
|
|
169
|
+
"settings.read": "settings.read",
|
|
170
|
+
"settings.write": "settings.write",
|
|
171
|
+
"config.read": "config.read",
|
|
172
|
+
"config.write": "config.write",
|
|
173
|
+
"billing.read": "billing.read",
|
|
174
|
+
"billing.write": "billing.write"
|
|
175
|
+
};
|
|
176
|
+
var ROLE_PERMISSIONS = {
|
|
177
|
+
[PERMISSIONS.OWNER]: [
|
|
178
|
+
PERMISSIONS.AUTHENTICATED,
|
|
179
|
+
PERMISSIONS["users.read"],
|
|
180
|
+
PERMISSIONS["users.write"],
|
|
181
|
+
PERMISSIONS["users.delete"],
|
|
182
|
+
PERMISSIONS["tenants.read"],
|
|
183
|
+
PERMISSIONS["tenants.write"],
|
|
184
|
+
PERMISSIONS["tenants.delete"],
|
|
185
|
+
PERMISSIONS["conversations.read"],
|
|
186
|
+
PERMISSIONS["conversations.write"],
|
|
187
|
+
PERMISSIONS["conversations.delete"],
|
|
188
|
+
PERMISSIONS["messages.read"],
|
|
189
|
+
PERMISSIONS["messages.write"],
|
|
190
|
+
PERMISSIONS["settings.read"],
|
|
191
|
+
PERMISSIONS["settings.write"],
|
|
192
|
+
PERMISSIONS["config.read"],
|
|
193
|
+
PERMISSIONS["config.write"],
|
|
194
|
+
PERMISSIONS["billing.read"],
|
|
195
|
+
PERMISSIONS["billing.write"]
|
|
196
|
+
],
|
|
197
|
+
[PERMISSIONS.ADMIN]: [
|
|
198
|
+
PERMISSIONS.AUTHENTICATED,
|
|
199
|
+
PERMISSIONS["users.read"],
|
|
200
|
+
PERMISSIONS["users.write"],
|
|
201
|
+
PERMISSIONS["users.delete"],
|
|
202
|
+
PERMISSIONS["tenants.read"],
|
|
203
|
+
PERMISSIONS["tenants.write"],
|
|
204
|
+
PERMISSIONS["tenants.delete"],
|
|
205
|
+
PERMISSIONS["conversations.read"],
|
|
206
|
+
PERMISSIONS["conversations.write"],
|
|
207
|
+
PERMISSIONS["conversations.delete"],
|
|
208
|
+
PERMISSIONS["messages.read"],
|
|
209
|
+
PERMISSIONS["messages.write"],
|
|
210
|
+
PERMISSIONS["settings.read"],
|
|
211
|
+
PERMISSIONS["settings.write"],
|
|
212
|
+
PERMISSIONS["config.read"],
|
|
213
|
+
PERMISSIONS["config.write"],
|
|
214
|
+
PERMISSIONS["billing.read"]
|
|
215
|
+
],
|
|
216
|
+
[PERMISSIONS.MANAGER]: [
|
|
217
|
+
PERMISSIONS.AUTHENTICATED,
|
|
218
|
+
PERMISSIONS["users.read"],
|
|
219
|
+
PERMISSIONS["users.write"],
|
|
220
|
+
PERMISSIONS["conversations.read"],
|
|
221
|
+
PERMISSIONS["conversations.write"],
|
|
222
|
+
PERMISSIONS["messages.read"],
|
|
223
|
+
PERMISSIONS["messages.write"],
|
|
224
|
+
PERMISSIONS["settings.read"],
|
|
225
|
+
PERMISSIONS["settings.write"]
|
|
226
|
+
],
|
|
227
|
+
[PERMISSIONS.AGENT]: [
|
|
228
|
+
PERMISSIONS.AUTHENTICATED,
|
|
229
|
+
PERMISSIONS["conversations.read"],
|
|
230
|
+
PERMISSIONS["conversations.write"],
|
|
231
|
+
PERMISSIONS["messages.read"],
|
|
232
|
+
PERMISSIONS["messages.write"]
|
|
233
|
+
],
|
|
234
|
+
[PERMISSIONS.VIEWER]: [
|
|
235
|
+
PERMISSIONS.AUTHENTICATED,
|
|
236
|
+
PERMISSIONS["conversations.read"],
|
|
237
|
+
PERMISSIONS["messages.read"]
|
|
238
|
+
],
|
|
239
|
+
[PERMISSIONS.DEVELOPER]: [
|
|
240
|
+
PERMISSIONS.AUTHENTICATED,
|
|
241
|
+
// Only specific debug/development permissions, not full access
|
|
242
|
+
"system.debug",
|
|
243
|
+
"health.check",
|
|
244
|
+
PERMISSIONS["config.read"]
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
// src/configs/moleculer/bulkhead.ts
|
|
249
|
+
var bulkheadConfig = {
|
|
250
|
+
// Enable feature.
|
|
251
|
+
enabled: false,
|
|
252
|
+
// Maximum concurrent executions.
|
|
253
|
+
concurrency: 10,
|
|
254
|
+
// Maximum size of queue
|
|
255
|
+
maxQueueSize: 100
|
|
256
|
+
};
|
|
257
|
+
var NAMESPACE = env3.string("NAMESPACE", "hios").toLowerCase();
|
|
258
|
+
var CHANNELS = {
|
|
259
|
+
// Webhook processing channels
|
|
260
|
+
WEBHOOK: {
|
|
261
|
+
// Pattern: hios.webhook.{tenantId}.{platform}
|
|
262
|
+
PATTERN: `${NAMESPACE}.webhook.*.*`,
|
|
263
|
+
PREFIX: `${NAMESPACE}.webhook`,
|
|
264
|
+
build: (tenantId, platform) => `${NAMESPACE}.webhook.${tenantId}.${platform}`
|
|
265
|
+
},
|
|
266
|
+
// Message processing channels
|
|
267
|
+
PROCESSING: {
|
|
268
|
+
// Pattern: hios.processing.{tenantId}.{messageType}
|
|
269
|
+
PATTERN: `${NAMESPACE}.processing.*.*`,
|
|
270
|
+
PREFIX: `${NAMESPACE}.processing`,
|
|
271
|
+
build: (tenantId, messageType) => `${NAMESPACE}.processing.${tenantId}.${messageType}`
|
|
272
|
+
},
|
|
273
|
+
// Response/outbound message channels
|
|
274
|
+
RESPONSE: {
|
|
275
|
+
// Pattern: hios.response.{tenantId}.{platform}
|
|
276
|
+
PATTERN: `${NAMESPACE}.response.*.*`,
|
|
277
|
+
PREFIX: `${NAMESPACE}.response`,
|
|
278
|
+
build: (tenantId, platform) => `${NAMESPACE}.response.${tenantId}.${platform}`
|
|
279
|
+
},
|
|
280
|
+
// System channels
|
|
281
|
+
SYSTEM: {
|
|
282
|
+
// Error handling
|
|
283
|
+
ERRORS: `${NAMESPACE}.system.errors`,
|
|
284
|
+
// Metrics and monitoring
|
|
285
|
+
METRICS: `${NAMESPACE}.system.metrics`,
|
|
286
|
+
// Health checks
|
|
287
|
+
HEALTH: `${NAMESPACE}.system.health`,
|
|
288
|
+
// Integration lifecycle events
|
|
289
|
+
INTEGRATION_REGISTERED: `${NAMESPACE}.system.integration.registered`,
|
|
290
|
+
INTEGRATION_UNREGISTERED: `${NAMESPACE}.system.integration.unregistered`
|
|
291
|
+
},
|
|
292
|
+
// Dead letter queues
|
|
293
|
+
DLQ: {
|
|
294
|
+
// Failed webhook processing
|
|
295
|
+
WEBHOOK_FAILED: `${NAMESPACE}.dlq.webhook.failed`,
|
|
296
|
+
// Failed message sends
|
|
297
|
+
SEND_FAILED: `${NAMESPACE}.dlq.send.failed`,
|
|
298
|
+
// Failed processing
|
|
299
|
+
PROCESSING_FAILED: `${NAMESPACE}.dlq.processing.failed`,
|
|
300
|
+
// Build DLQ name for specific integration
|
|
301
|
+
buildSendFailed: (platform) => `${NAMESPACE}.dlq.send.${platform}.failed`
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var INTEGRATION_CHANNELS = {
|
|
305
|
+
// Message events
|
|
306
|
+
MESSAGE_RECEIVED: `${NAMESPACE}.processing.message.received`,
|
|
307
|
+
MESSAGE_SENT: `${NAMESPACE}.processing.message.sent`,
|
|
308
|
+
MESSAGE_FAILED: `${NAMESPACE}.processing.message.failed`
|
|
309
|
+
};
|
|
310
|
+
var CHANNEL_CONFIG = {
|
|
311
|
+
// Default settings for message channels
|
|
312
|
+
DEFAULTS: {
|
|
313
|
+
maxInFlight: 10,
|
|
314
|
+
maxRetries: 3,
|
|
315
|
+
deadLettering: {
|
|
316
|
+
enabled: true
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
// High-priority channels (webhooks)
|
|
320
|
+
HIGH_PRIORITY: {
|
|
321
|
+
maxInFlight: 50,
|
|
322
|
+
maxRetries: 5,
|
|
323
|
+
deadLettering: {
|
|
324
|
+
enabled: true
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
// Low-priority channels (metrics, logs)
|
|
328
|
+
LOW_PRIORITY: {
|
|
329
|
+
maxInFlight: 5,
|
|
330
|
+
maxRetries: 1,
|
|
331
|
+
deadLettering: {
|
|
332
|
+
enabled: false
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
var SUBJECTS = {
|
|
337
|
+
// All webhook subjects
|
|
338
|
+
WEBHOOK_ALL: `${NAMESPACE}.webhook.>`,
|
|
339
|
+
// All processing subjects
|
|
340
|
+
PROCESSING_ALL: `${NAMESPACE}.processing.>`,
|
|
341
|
+
// All response subjects
|
|
342
|
+
RESPONSE_ALL: `${NAMESPACE}.response.>`,
|
|
343
|
+
// All system subjects
|
|
344
|
+
SYSTEM_ALL: `${NAMESPACE}.system.>`,
|
|
345
|
+
// All DLQ subjects
|
|
346
|
+
DLQ_ALL: `${NAMESPACE}.dlq.>`};
|
|
347
|
+
|
|
348
|
+
// src/configs/moleculer/channels.ts
|
|
349
|
+
var NAMESPACE2 = NAMESPACE.toUpperCase();
|
|
350
|
+
var middleware = Middleware({
|
|
351
|
+
adapter: {
|
|
352
|
+
type: "NATS",
|
|
353
|
+
options: {
|
|
354
|
+
nats: {
|
|
355
|
+
url: env3.string("NATS_URL", "nats://localhost:4222"),
|
|
356
|
+
/** Connection options for reliability */
|
|
357
|
+
connectionOptions: {
|
|
358
|
+
name: "hios",
|
|
359
|
+
timeout: 1e4,
|
|
360
|
+
reconnect: true,
|
|
361
|
+
maxReconnectAttempts: 10,
|
|
362
|
+
reconnectTimeWait: 2e3,
|
|
363
|
+
maxReconnectTimeWait: 3e4,
|
|
364
|
+
pingInterval: 2e4,
|
|
365
|
+
maxPingOut: 2
|
|
366
|
+
},
|
|
367
|
+
/**
|
|
368
|
+
* Stream configuration for multi-tenant messaging
|
|
369
|
+
*
|
|
370
|
+
* Environment variables for production:
|
|
371
|
+
* - NATS_MAX_MESSAGES: Default 100K (dev) -> 10M+ (prod)
|
|
372
|
+
* - NATS_MAX_BYTES_GB: Default 1GB (dev) -> 100GB+ (prod)
|
|
373
|
+
* - NATS_MAX_AGE_DAYS: Default 7 (dev) -> 30+ (prod)
|
|
374
|
+
* - NATS_MAX_MSG_SIZE_MB: Default 1MB (dev) -> 5MB (prod)
|
|
375
|
+
* - NATS_REPLICAS: Default 1 (dev) -> 3 (prod)
|
|
376
|
+
*/
|
|
377
|
+
streamConfig: {
|
|
378
|
+
name: `${NAMESPACE2}_MESSAGES`,
|
|
379
|
+
subjects: [
|
|
380
|
+
SUBJECTS.WEBHOOK_ALL,
|
|
381
|
+
SUBJECTS.PROCESSING_ALL,
|
|
382
|
+
SUBJECTS.RESPONSE_ALL,
|
|
383
|
+
SUBJECTS.SYSTEM_ALL,
|
|
384
|
+
SUBJECTS.DLQ_ALL
|
|
385
|
+
],
|
|
386
|
+
retention: "limits",
|
|
387
|
+
max_msgs: env3.int("NATS_MAX_MESSAGES", 1e5),
|
|
388
|
+
// 100K for dev, 10M+ for prod
|
|
389
|
+
max_bytes: env3.int("NATS_MAX_BYTES_GB", 1) * 1024 * 1024 * 1024,
|
|
390
|
+
// 1GB for dev, 100GB+ for prod
|
|
391
|
+
max_age: env3.int("NATS_MAX_AGE_DAYS", 7) * 24 * 60 * 60 * 1e9,
|
|
392
|
+
// 7 days dev, 30+ days prod
|
|
393
|
+
max_msg_size: env3.int("NATS_MAX_MSG_SIZE_MB", 1) * 1024 * 1024,
|
|
394
|
+
// 1MB dev, 5MB prod
|
|
395
|
+
storage: "file",
|
|
396
|
+
// Persistent storage
|
|
397
|
+
num_replicas: env3.int("NATS_REPLICAS", 1),
|
|
398
|
+
// 1 for dev, 3 for prod
|
|
399
|
+
discard: "old",
|
|
400
|
+
// Remove old messages when limits hit
|
|
401
|
+
duplicate_window: 2 * 60 * 1e9
|
|
402
|
+
// 2 minutes dedup window
|
|
403
|
+
},
|
|
404
|
+
/** Consumer options optimized for LLM processing */
|
|
405
|
+
consumerOptions: {
|
|
406
|
+
config: {
|
|
407
|
+
// Start with new messages (don't replay old ones on restart)
|
|
408
|
+
deliver_policy: "new",
|
|
409
|
+
// Explicit acknowledgment required (critical for LLM processing)
|
|
410
|
+
ack_policy: "explicit",
|
|
411
|
+
// Allow 5 unacknowledged messages per consumer (rate limiting)
|
|
412
|
+
max_ack_pending: 5,
|
|
413
|
+
// Acknowledgment timeout for LLM processing (2 minutes)
|
|
414
|
+
ack_wait: 120 * 1e9,
|
|
415
|
+
// 2 minutes in nanoseconds
|
|
416
|
+
// Maximum delivery attempts before dead letter
|
|
417
|
+
max_deliver: 3,
|
|
418
|
+
// Backoff for failed message retries
|
|
419
|
+
backoff: [
|
|
420
|
+
1e9,
|
|
421
|
+
// 1 second
|
|
422
|
+
5e9,
|
|
423
|
+
// 5 seconds
|
|
424
|
+
3e10
|
|
425
|
+
// 30 seconds
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
/** Application-level flow control */
|
|
431
|
+
maxInFlight: 5,
|
|
432
|
+
// Limit concurrent LLM requests per service
|
|
433
|
+
maxRetries: 2,
|
|
434
|
+
// App-level retries (NATS handles delivery retries)
|
|
435
|
+
/** Dead letter queue for failed messages */
|
|
436
|
+
deadLettering: {
|
|
437
|
+
enabled: true,
|
|
438
|
+
queueName: "FAILED_MESSAGES"
|
|
439
|
+
// Send to dead letter after NATS max_deliver attempts
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
var ChannelsMiddleware = {
|
|
445
|
+
...middleware
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
// src/configs/moleculer/circuit-breaker.ts
|
|
449
|
+
var circuitBreakerConfig = {
|
|
450
|
+
// Enable feature
|
|
451
|
+
enabled: false,
|
|
452
|
+
// Threshold value. 0.5 means that 50% should be failed for tripping.
|
|
453
|
+
threshold: 0.5,
|
|
454
|
+
// Minimum request count. Below it, CB does not trip.
|
|
455
|
+
minRequestCount: 20,
|
|
456
|
+
// Number of seconds for time window.
|
|
457
|
+
windowTime: 60,
|
|
458
|
+
// Number of milliseconds to switch from open to half-open state
|
|
459
|
+
halfOpenTime: 10 * 1e3,
|
|
460
|
+
// A function to check failed requests.
|
|
461
|
+
check: (err) => err.code >= 500
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// src/configs/moleculer/logger.ts
|
|
465
|
+
var loggerConfig = {
|
|
466
|
+
type: "Console",
|
|
467
|
+
options: {
|
|
468
|
+
// Using colors on the output
|
|
469
|
+
colors: true,
|
|
470
|
+
// Print module names with different colors (like docker-compose for containers)
|
|
471
|
+
moduleColors: false,
|
|
472
|
+
// Line formatter. It can be "json", "short", "simple", "full", a `Function` or a template string like "{timestamp} {level} {nodeID}/{mod}: {msg}"
|
|
473
|
+
formatter: "full",
|
|
474
|
+
// Custom object printer. If not defined, it uses the `util.inspect` method.
|
|
475
|
+
objectPrinter: null,
|
|
476
|
+
// Auto-padding the module name in order to messages begin at the same column.
|
|
477
|
+
autoPadding: false
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
var logger_default = loggerConfig;
|
|
481
|
+
|
|
482
|
+
// src/configs/moleculer/metrics.ts
|
|
483
|
+
var metricsConfig = {
|
|
484
|
+
enabled: false,
|
|
485
|
+
// Available built-in reporters: "Console", "CSV", "Event", "Prometheus", "Datadog", "StatsD"
|
|
486
|
+
reporter: {
|
|
487
|
+
type: "Prometheus",
|
|
488
|
+
options: {
|
|
489
|
+
// HTTP port
|
|
490
|
+
port: 3030,
|
|
491
|
+
// HTTP URL path
|
|
492
|
+
path: "/metrics",
|
|
493
|
+
// Default labels which are appended to all metrics labels
|
|
494
|
+
defaultLabels: (registry) => ({
|
|
495
|
+
namespace: registry.broker.namespace,
|
|
496
|
+
nodeID: registry.broker.nodeID
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
// src/configs/moleculer/registry.ts
|
|
503
|
+
var registryConfig = {
|
|
504
|
+
// Define balancing strategy. More info: https://moleculer.services/docs/0.14/balancing.html
|
|
505
|
+
// Available values: "RoundRobin", "Random", "CpuUsage", "Latency", "Shard"
|
|
506
|
+
strategy: "RoundRobin",
|
|
507
|
+
// Enable local action call preferring. Always call the local action instance if available.
|
|
508
|
+
preferLocal: true
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// src/configs/moleculer/retry-policy.ts
|
|
512
|
+
var retryPolicyConfig = {
|
|
513
|
+
// Enable feature
|
|
514
|
+
enabled: false,
|
|
515
|
+
// Count of retries
|
|
516
|
+
retries: 5,
|
|
517
|
+
// First delay in milliseconds.
|
|
518
|
+
delay: 100,
|
|
519
|
+
// Maximum delay in milliseconds.
|
|
520
|
+
maxDelay: 1e3,
|
|
521
|
+
// Backoff factor for delay. 2 means exponential backoff.
|
|
522
|
+
factor: 2,
|
|
523
|
+
// A function to check failed requests.
|
|
524
|
+
check: (err) => !!err.retryable
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
// src/configs/moleculer/tracing.ts
|
|
528
|
+
var tracingConfig = {
|
|
529
|
+
enabled: true,
|
|
530
|
+
exporter: "Console",
|
|
531
|
+
events: true,
|
|
532
|
+
stackTrace: true
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// src/configs/moleculer/tracking.ts
|
|
536
|
+
var trackingConfig = {
|
|
537
|
+
// Enable feature
|
|
538
|
+
enabled: false,
|
|
539
|
+
// Number of milliseconds to wait before shuting down the process.
|
|
540
|
+
shutdownTimeout: 5e3
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
// src/configs/moleculer/index.ts
|
|
544
|
+
var pkgNm = env3.string("NAMESPACE", "hios");
|
|
545
|
+
var nodeID = env3.string("NODE_ID") ?? `${pkgNm}-${os.hostname()}-${process.pid}`;
|
|
546
|
+
var configs = {
|
|
547
|
+
namespace: pkgNm,
|
|
548
|
+
nodeID,
|
|
549
|
+
metadata: {},
|
|
550
|
+
logger: logger_default,
|
|
551
|
+
// Default log level for built-in console logger. It can be overwritten in logger options above.
|
|
552
|
+
// Available values: trace, debug, info, warn, error, fatal
|
|
553
|
+
logLevel: "info",
|
|
554
|
+
cacher: env3.string("REDIS_URL", "Memory"),
|
|
555
|
+
// Define a serializer.
|
|
556
|
+
// Available values: "JSON", "Avro", "ProtoBuf", "MsgPack", "Notepack", "Thrift".
|
|
557
|
+
// More info: https://moleculer.services/docs/0.14/networking.html#Serialization
|
|
558
|
+
serializer: "JSON",
|
|
559
|
+
// Number of milliseconds to wait before reject a request with a RequestTimeout error. Disabled: 0
|
|
560
|
+
requestTimeout: 10 * 1e3,
|
|
561
|
+
// Retry policy settings. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Retry
|
|
562
|
+
retryPolicy: retryPolicyConfig,
|
|
563
|
+
// Limit of calling level. If it reaches the limit, broker will throw an MaxCallLevelError error. (Infinite loop protection)
|
|
564
|
+
maxCallLevel: 100,
|
|
565
|
+
// Number of seconds to send heartbeat packet to other nodes.
|
|
566
|
+
heartbeatInterval: 10,
|
|
567
|
+
// Number of seconds to wait before setting node to unavailable status.
|
|
568
|
+
heartbeatTimeout: 30,
|
|
569
|
+
// Cloning the params of context if enabled. High performance impact, use it with caution!
|
|
570
|
+
contextParamsCloning: false,
|
|
571
|
+
// Tracking requests and waiting for running requests before shuting down. More info: https://moleculer.services/docs/0.14/context.html#Context-tracking
|
|
572
|
+
tracking: trackingConfig,
|
|
573
|
+
// Disable built-in request & emit balancer. (Transporter must support it, as well.). More info: https://moleculer.services/docs/0.14/networking.html#Disabled-balancer
|
|
574
|
+
disableBalancer: false,
|
|
575
|
+
// Settings of Service Registry. More info: https://moleculer.services/docs/0.14/registry.html
|
|
576
|
+
registry: registryConfig,
|
|
577
|
+
// Settings of Circuit Breaker. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Circuit-Breaker
|
|
578
|
+
circuitBreaker: circuitBreakerConfig,
|
|
579
|
+
// Settings of bulkhead feature. More info: https://moleculer.services/docs/0.14/fault-tolerance.html#Bulkhead
|
|
580
|
+
bulkhead: bulkheadConfig,
|
|
581
|
+
// Enable action & event parameter validation. More info: https://moleculer.services/docs/0.14/validating.html
|
|
582
|
+
validator: "Fastest",
|
|
583
|
+
// errorHandler: null,
|
|
584
|
+
transporter: env3.string("TRANSPORTER_URL"),
|
|
585
|
+
// Enable/disable built-in metrics function. More info: https://moleculer.services/docs/0.14/metrics.html
|
|
586
|
+
metrics: metricsConfig,
|
|
587
|
+
// Enable built-in tracing function. More info: https://moleculer.services/docs/0.14/tracing.html
|
|
588
|
+
tracing: tracingConfig,
|
|
589
|
+
middlewares: [
|
|
590
|
+
ChannelsMiddleware,
|
|
591
|
+
PermissionsMiddleware,
|
|
592
|
+
ContextHelpersMiddleware
|
|
593
|
+
]
|
|
594
|
+
};
|
|
595
|
+
var moleculer_default = configs;
|
|
596
|
+
var nodeEnv = env3.string("NODE_ENV", "development");
|
|
597
|
+
var isDev = nodeEnv === "development";
|
|
598
|
+
var isTest = nodeEnv === "test";
|
|
599
|
+
var isProd = nodeEnv === "production";
|
|
600
|
+
var REDIS_URL = env3.string("REDIS_URL");
|
|
601
|
+
var env_default = env3;
|
|
602
|
+
|
|
603
|
+
// src/errors/permission.error.ts
|
|
604
|
+
var PermissionError = class _PermissionError extends Error {
|
|
605
|
+
constructor(message, data) {
|
|
606
|
+
super(message);
|
|
607
|
+
this.code = "PERMISSION_DENIED";
|
|
608
|
+
this.statusCode = 403;
|
|
609
|
+
this.name = "PermissionError";
|
|
610
|
+
this.data = data;
|
|
611
|
+
if (Error.captureStackTrace) {
|
|
612
|
+
Error.captureStackTrace(this, _PermissionError);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// src/errors/auth.error.ts
|
|
618
|
+
var AuthenticationError = class _AuthenticationError extends Error {
|
|
619
|
+
constructor(message, data) {
|
|
620
|
+
super(message);
|
|
621
|
+
this.code = "AUTH_REQUIRED";
|
|
622
|
+
this.statusCode = 401;
|
|
623
|
+
this.name = "AuthenticationError";
|
|
624
|
+
this.data = data;
|
|
625
|
+
if (Error.captureStackTrace) {
|
|
626
|
+
Error.captureStackTrace(this, _AuthenticationError);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
var TenantError = class _TenantError extends Error {
|
|
631
|
+
constructor(message, data) {
|
|
632
|
+
super(message);
|
|
633
|
+
this.code = "TENANT_REQUIRED";
|
|
634
|
+
this.statusCode = 401;
|
|
635
|
+
this.name = "TenantError";
|
|
636
|
+
this.data = data;
|
|
637
|
+
if (Error.captureStackTrace) {
|
|
638
|
+
Error.captureStackTrace(this, _TenantError);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
// src/middlewares/permissions.middleware.ts
|
|
644
|
+
var permissionHandlers = {
|
|
645
|
+
[PERMISSIONS.AUTHENTICATED]: async (ctx) => !!ctx.meta.user?.id,
|
|
646
|
+
[PERMISSIONS.TENANT_OWNER]: async (ctx) => ctx.meta.user?.roles.includes(PERMISSIONS.OWNER) ?? false,
|
|
647
|
+
[PERMISSIONS.TENANT_MEMBER]: async (ctx) => !!(ctx.meta.user?.tenantId && ctx.meta.tenantId && ctx.meta.user.tenantId === ctx.meta.tenantId)
|
|
648
|
+
};
|
|
649
|
+
var PermissionsMiddleware = {
|
|
650
|
+
// Wrap local action handlers
|
|
651
|
+
localAction(handler, action) {
|
|
652
|
+
if (!action.permissions) {
|
|
653
|
+
return handler;
|
|
654
|
+
}
|
|
655
|
+
const permissions = Array.isArray(action.permissions) ? action.permissions : [action.permissions];
|
|
656
|
+
const permissionNames = [];
|
|
657
|
+
const permissionFunctions = [];
|
|
658
|
+
permissions.forEach((permission) => {
|
|
659
|
+
if (typeof permission === "function") {
|
|
660
|
+
permissionFunctions.push(permission);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (typeof permission === "string") {
|
|
664
|
+
if (permission in permissionHandlers) {
|
|
665
|
+
const handler2 = permissionHandlers[permission];
|
|
666
|
+
permissionFunctions.push(handler2);
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
permissionNames.push(permission);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
});
|
|
673
|
+
return async function CheckPermissionsMiddleware(ctx) {
|
|
674
|
+
let hasAccess = false;
|
|
675
|
+
if (ctx.meta.user?.roles.includes(PERMISSIONS.OWNER)) {
|
|
676
|
+
hasAccess = true;
|
|
677
|
+
}
|
|
678
|
+
if (!hasAccess && permissionFunctions.length > 0) {
|
|
679
|
+
const results = await Promise.allSettled(
|
|
680
|
+
permissionFunctions.map((fn) => fn(ctx, action))
|
|
681
|
+
);
|
|
682
|
+
hasAccess = results.some(
|
|
683
|
+
(result) => result.status === "fulfilled" && !!result.value
|
|
684
|
+
);
|
|
685
|
+
const failures = results.filter((r) => r.status === "rejected");
|
|
686
|
+
if (failures.length > 0) {
|
|
687
|
+
ctx.broker.logger.warn(
|
|
688
|
+
`${failures.length} permission functions failed`,
|
|
689
|
+
{
|
|
690
|
+
action: action.name,
|
|
691
|
+
userId: ctx.meta.user?.id
|
|
692
|
+
}
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
if (!hasAccess && permissionNames.length > 0) {
|
|
697
|
+
const userRoles = ctx.meta.user?.roles ?? [];
|
|
698
|
+
hasAccess = userRoles.some((role) => {
|
|
699
|
+
const rolePermissions = ROLE_PERMISSIONS[role] ?? [];
|
|
700
|
+
return permissionNames.some(
|
|
701
|
+
(permName) => rolePermissions.includes(permName)
|
|
702
|
+
);
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
if (!hasAccess) {
|
|
706
|
+
const user = ctx.meta.user;
|
|
707
|
+
ctx.broker.logger.warn("Access denied:", {
|
|
708
|
+
action: action.name,
|
|
709
|
+
userId: user?.id,
|
|
710
|
+
userRoles: user?.roles,
|
|
711
|
+
tenantId: ctx.meta.tenantId,
|
|
712
|
+
requiredPermissions: permissions
|
|
713
|
+
});
|
|
714
|
+
const errorDetails = isDev ? {
|
|
715
|
+
action: action.name,
|
|
716
|
+
requiredPermissions: permissions.map(
|
|
717
|
+
(p) => typeof p === "function" ? "[Function]" : String(p)
|
|
718
|
+
),
|
|
719
|
+
userRoles: user?.roles ?? [],
|
|
720
|
+
userId: user?.id,
|
|
721
|
+
tenantId: ctx.meta.tenantId
|
|
722
|
+
} : {
|
|
723
|
+
action: action.name
|
|
724
|
+
};
|
|
725
|
+
throw new PermissionError(
|
|
726
|
+
"You do not have permission to perform this action",
|
|
727
|
+
errorDetails
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
return handler.call(this, ctx);
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
// src/utils/context-cache.ts
|
|
736
|
+
var ContextCache = class _ContextCache {
|
|
737
|
+
constructor() {
|
|
738
|
+
this.memoryCache = /* @__PURE__ */ new Map();
|
|
739
|
+
this.TTL = 5 * 60 * 1e3;
|
|
740
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 60 * 1e3);
|
|
741
|
+
}
|
|
742
|
+
static getInstance() {
|
|
743
|
+
if (!_ContextCache.instance) {
|
|
744
|
+
_ContextCache.instance = new _ContextCache();
|
|
745
|
+
}
|
|
746
|
+
return _ContextCache.instance;
|
|
747
|
+
}
|
|
748
|
+
async get(key, factory) {
|
|
749
|
+
const cached = this.memoryCache.get(key);
|
|
750
|
+
if (cached && Date.now() - cached.timestamp < this.TTL) {
|
|
751
|
+
return cached.value;
|
|
752
|
+
}
|
|
753
|
+
const value = await factory();
|
|
754
|
+
this.memoryCache.set(key, { value, timestamp: Date.now() });
|
|
755
|
+
return value;
|
|
756
|
+
}
|
|
757
|
+
set(key, value) {
|
|
758
|
+
this.memoryCache.set(key, { value, timestamp: Date.now() });
|
|
759
|
+
}
|
|
760
|
+
delete(key) {
|
|
761
|
+
return this.memoryCache.delete(key);
|
|
762
|
+
}
|
|
763
|
+
clear() {
|
|
764
|
+
this.memoryCache.clear();
|
|
765
|
+
}
|
|
766
|
+
cleanup() {
|
|
767
|
+
const now = Date.now();
|
|
768
|
+
const keysToDelete = [];
|
|
769
|
+
this.memoryCache.forEach((entry, key) => {
|
|
770
|
+
if (now - entry.timestamp > this.TTL) {
|
|
771
|
+
keysToDelete.push(key);
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
keysToDelete.forEach((key) => this.memoryCache.delete(key));
|
|
775
|
+
}
|
|
776
|
+
destroy() {
|
|
777
|
+
if (this.cleanupInterval) {
|
|
778
|
+
clearInterval(this.cleanupInterval);
|
|
779
|
+
}
|
|
780
|
+
this.clear();
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
// src/utils/permission-calculator.ts
|
|
785
|
+
var PermissionCalculator = class {
|
|
786
|
+
static calculateUserPermissions(user) {
|
|
787
|
+
if (!user || !Array.isArray(user.roles)) {
|
|
788
|
+
return [];
|
|
789
|
+
}
|
|
790
|
+
const explicitPermissions = Array.isArray(user.permissions) ? user.permissions : [];
|
|
791
|
+
const rolePermissions = user.roles.flatMap((role) => {
|
|
792
|
+
return ROLE_PERMISSIONS[role] ?? [];
|
|
793
|
+
});
|
|
794
|
+
const allPermissions = [...explicitPermissions, ...rolePermissions];
|
|
795
|
+
const uniquePermissions = [];
|
|
796
|
+
allPermissions.forEach((permission) => {
|
|
797
|
+
if (!uniquePermissions.includes(permission)) {
|
|
798
|
+
uniquePermissions.push(permission);
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
return uniquePermissions;
|
|
802
|
+
}
|
|
803
|
+
static hasPermission(user, permission) {
|
|
804
|
+
if (typeof permission !== "string" || !permission.trim()) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
if (!user || typeof user !== "object") {
|
|
808
|
+
return false;
|
|
809
|
+
}
|
|
810
|
+
if (!Array.isArray(user.roles)) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
if (Array.isArray(user.permissions) && user.permissions.includes(permission)) {
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
return user.roles.some((role) => {
|
|
817
|
+
const rolePermissions = ROLE_PERMISSIONS[role] ?? [];
|
|
818
|
+
return rolePermissions.includes(permission);
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// src/middlewares/context-helpers.middleware.ts
|
|
824
|
+
var ContextHelpersMiddleware = {
|
|
825
|
+
// Add helper functions to context before action handlers
|
|
826
|
+
localAction(handler) {
|
|
827
|
+
return function ContextHelpersWrapper(ctx) {
|
|
828
|
+
const cache = ContextCache.getInstance();
|
|
829
|
+
const memoizedPermissions = /* @__PURE__ */ new Map();
|
|
830
|
+
ctx.hasPermission = function(permission) {
|
|
831
|
+
if (memoizedPermissions.has(permission)) {
|
|
832
|
+
const cachedResult = memoizedPermissions.get(permission);
|
|
833
|
+
return cachedResult === true;
|
|
834
|
+
}
|
|
835
|
+
const user = ctx.meta.user;
|
|
836
|
+
if (!user) {
|
|
837
|
+
memoizedPermissions.set(permission, false);
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
const result = PermissionCalculator.hasPermission(user, permission);
|
|
841
|
+
memoizedPermissions.set(permission, result);
|
|
842
|
+
return result;
|
|
843
|
+
};
|
|
844
|
+
ctx.getUserPermissions = async function() {
|
|
845
|
+
const user = ctx.meta.user;
|
|
846
|
+
if (!user) return [];
|
|
847
|
+
const cacheKey = `permissions:${user.id}:${JSON.stringify(user.roles)}`;
|
|
848
|
+
return cache.get(cacheKey, () => {
|
|
849
|
+
return PermissionCalculator.calculateUserPermissions(user);
|
|
850
|
+
});
|
|
851
|
+
};
|
|
852
|
+
ctx.hasRole = function(role) {
|
|
853
|
+
const user = ctx.ensureUser();
|
|
854
|
+
return Array.isArray(user.roles) && user.roles.includes(role);
|
|
855
|
+
};
|
|
856
|
+
ctx.isTenantMember = function() {
|
|
857
|
+
const user = ctx.ensureUser();
|
|
858
|
+
return !!(user.tenantId && ctx.meta.tenantId && user.tenantId === ctx.meta.tenantId);
|
|
859
|
+
};
|
|
860
|
+
ctx.isTenantOwner = function() {
|
|
861
|
+
return ctx.isTenantMember() && ctx.hasRole("OWNER");
|
|
862
|
+
};
|
|
863
|
+
ctx.ensureUser = () => {
|
|
864
|
+
if (!ctx.meta.user) {
|
|
865
|
+
ctx.broker.logger.error("Authentication required", {
|
|
866
|
+
action: ctx.action?.name,
|
|
867
|
+
requestId: ctx.meta.requestId,
|
|
868
|
+
userAgent: ctx.meta.userAgent,
|
|
869
|
+
ip: ctx.meta.clientIP
|
|
870
|
+
});
|
|
871
|
+
throw new AuthenticationError("Authentication required", {
|
|
872
|
+
code: "AUTH_REQUIRED",
|
|
873
|
+
statusCode: 401,
|
|
874
|
+
requestId: ctx.meta.requestId
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
return ctx.meta.user;
|
|
878
|
+
};
|
|
879
|
+
ctx.ensureTenant = () => {
|
|
880
|
+
if (!ctx.meta.tenantId) {
|
|
881
|
+
ctx.broker.logger.error("Tenant required", {
|
|
882
|
+
action: ctx.action?.name,
|
|
883
|
+
userId: ctx.meta.user?.id,
|
|
884
|
+
requestId: ctx.meta.requestId
|
|
885
|
+
});
|
|
886
|
+
throw new TenantError("Tenant required", {
|
|
887
|
+
code: "TENANT_REQUIRED",
|
|
888
|
+
statusCode: 401,
|
|
889
|
+
tenantId: ctx.meta.tenantId,
|
|
890
|
+
requestId: ctx.meta.requestId
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
return {
|
|
894
|
+
id: ctx.meta.tenantId,
|
|
895
|
+
name: ctx.meta.tenantName ?? ""
|
|
896
|
+
};
|
|
897
|
+
};
|
|
898
|
+
ctx.auditLog = function(action, resource, metadata) {
|
|
899
|
+
ctx.broker.logger.info("Audit log", {
|
|
900
|
+
action,
|
|
901
|
+
resource: resource ? {
|
|
902
|
+
type: typeof resource,
|
|
903
|
+
id: resource.id
|
|
904
|
+
} : void 0,
|
|
905
|
+
userId: ctx.meta.user?.id,
|
|
906
|
+
tenantId: ctx.meta.tenantId,
|
|
907
|
+
requestId: ctx.meta.requestId,
|
|
908
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
909
|
+
...metadata
|
|
910
|
+
});
|
|
911
|
+
};
|
|
912
|
+
ctx.createError = function(message, code, statusCode = 400) {
|
|
913
|
+
const errorData = {
|
|
914
|
+
code,
|
|
915
|
+
statusCode,
|
|
916
|
+
userId: ctx.meta.user?.id,
|
|
917
|
+
tenantId: ctx.meta.tenantId,
|
|
918
|
+
requestId: ctx.meta.requestId,
|
|
919
|
+
action: ctx.action?.name,
|
|
920
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
921
|
+
};
|
|
922
|
+
ctx.broker.logger.warn("Context error created", {
|
|
923
|
+
message,
|
|
924
|
+
...errorData
|
|
925
|
+
});
|
|
926
|
+
if (code === "AUTH_REQUIRED") {
|
|
927
|
+
return new AuthenticationError(message, errorData);
|
|
928
|
+
}
|
|
929
|
+
if (code === "TENANT_REQUIRED") {
|
|
930
|
+
return new TenantError(message, errorData);
|
|
931
|
+
}
|
|
932
|
+
return new Error(message);
|
|
933
|
+
};
|
|
934
|
+
return handler.call(this, ctx);
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// src/mixins/datasource.mixin.ts
|
|
940
|
+
function DatasourceMixin(datasourceConstructors = {}) {
|
|
941
|
+
const datasourceInstances = {};
|
|
942
|
+
for (const [key, DatasourceClass] of Object.entries(datasourceConstructors)) {
|
|
943
|
+
datasourceInstances[key] = new DatasourceClass();
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
/**
|
|
947
|
+
* Service created lifecycle hook
|
|
948
|
+
* Initialize datasources and store on service
|
|
949
|
+
*/
|
|
950
|
+
async created() {
|
|
951
|
+
for (const [, datasource] of Object.entries(datasourceInstances)) {
|
|
952
|
+
datasource.broker = this.broker;
|
|
953
|
+
}
|
|
954
|
+
for (const [, datasource] of Object.entries(datasourceInstances)) {
|
|
955
|
+
if (typeof datasource.init === "function") {
|
|
956
|
+
await datasource.init();
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
this.$datasources = datasourceInstances;
|
|
960
|
+
},
|
|
961
|
+
/**
|
|
962
|
+
* Service started lifecycle hook
|
|
963
|
+
* Connect datasources that have connect method
|
|
964
|
+
*/
|
|
965
|
+
async started() {
|
|
966
|
+
for (const [, datasource] of Object.entries(datasourceInstances)) {
|
|
967
|
+
if (typeof datasource.connect === "function") {
|
|
968
|
+
await datasource.connect();
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
},
|
|
972
|
+
/**
|
|
973
|
+
* Service stopped lifecycle hook
|
|
974
|
+
* Disconnect datasources that have disconnect method
|
|
975
|
+
*/
|
|
976
|
+
async stopped() {
|
|
977
|
+
for (const [, datasource] of Object.entries(datasourceInstances)) {
|
|
978
|
+
if (typeof datasource.disconnect === "function") {
|
|
979
|
+
await datasource.disconnect();
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
/**
|
|
984
|
+
* Hooks to inject datasources into context
|
|
985
|
+
*/
|
|
986
|
+
hooks: {
|
|
987
|
+
before: {
|
|
988
|
+
"*": function injectDatasources(ctx) {
|
|
989
|
+
const datasources = this.$datasources ?? {};
|
|
990
|
+
for (const [, datasource] of Object.entries(datasources)) {
|
|
991
|
+
datasource.context = ctx;
|
|
992
|
+
}
|
|
993
|
+
ctx.datasources = datasources;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// src/utils/index.ts
|
|
1001
|
+
function omit(obj, keys) {
|
|
1002
|
+
const result = { ...obj };
|
|
1003
|
+
keys.forEach((key) => delete result[key]);
|
|
1004
|
+
return result;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/service/define-service.ts
|
|
1008
|
+
function defineService(config) {
|
|
1009
|
+
const propsToOmit = ["datasources"];
|
|
1010
|
+
const serviceSchema = omit(
|
|
1011
|
+
config,
|
|
1012
|
+
propsToOmit
|
|
1013
|
+
);
|
|
1014
|
+
return {
|
|
1015
|
+
...serviceSchema,
|
|
1016
|
+
mixins: [
|
|
1017
|
+
DatasourceMixin(config.datasources),
|
|
1018
|
+
MemoizeMixin(),
|
|
1019
|
+
...serviceSchema.mixins ?? []
|
|
1020
|
+
]
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
var SecurityHelpers = {
|
|
1024
|
+
/**
|
|
1025
|
+
* Secure comparison using Node.js crypto.timingSafeEqual
|
|
1026
|
+
*/
|
|
1027
|
+
secureCompare(a, b) {
|
|
1028
|
+
try {
|
|
1029
|
+
return crypto.timingSafeEqual(
|
|
1030
|
+
Buffer.from(a, "utf8"),
|
|
1031
|
+
Buffer.from(b, "utf8")
|
|
1032
|
+
);
|
|
1033
|
+
} catch {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
},
|
|
1037
|
+
/**
|
|
1038
|
+
* Validate webhook timestamp to prevent replay attacks
|
|
1039
|
+
*/
|
|
1040
|
+
validateTimestamp(timestamp, maxAgeMs = 5 * 60 * 1e3) {
|
|
1041
|
+
return Date.now() - timestamp <= maxAgeMs;
|
|
1042
|
+
}
|
|
1043
|
+
};
|
|
1044
|
+
async function executeWithRetry(operation, maxRetries = 3, baseDelayMs = 1e3, context = "operation") {
|
|
1045
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1046
|
+
try {
|
|
1047
|
+
return await operation();
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
if (attempt === maxRetries) {
|
|
1050
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1051
|
+
throw new Error(
|
|
1052
|
+
`${context} failed after ${maxRetries} retries: ${err.message}`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
const delay = baseDelayMs * Math.pow(2, attempt);
|
|
1056
|
+
const jitter = Math.random() * 1e3;
|
|
1057
|
+
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
throw new Error("Retry logic error");
|
|
1061
|
+
}
|
|
1062
|
+
function defineIntegration(config) {
|
|
1063
|
+
const actions = {
|
|
1064
|
+
i_receiveWebhook: {
|
|
1065
|
+
rest: {
|
|
1066
|
+
method: "POST",
|
|
1067
|
+
path: "/:channelId"
|
|
1068
|
+
},
|
|
1069
|
+
params: {
|
|
1070
|
+
channelId: "string",
|
|
1071
|
+
payload: "object",
|
|
1072
|
+
headers: "object",
|
|
1073
|
+
timestamp: "number"
|
|
1074
|
+
},
|
|
1075
|
+
async handler(ctx) {
|
|
1076
|
+
const { channelId, payload, headers, timestamp } = ctx.params;
|
|
1077
|
+
const webhook = {
|
|
1078
|
+
tenantId: ctx.meta.tenantId ?? "unknown",
|
|
1079
|
+
// Should come from channel lookup
|
|
1080
|
+
channelId,
|
|
1081
|
+
platform: config.spec.platform,
|
|
1082
|
+
payload,
|
|
1083
|
+
// Raw webhook payload from gateway
|
|
1084
|
+
headers,
|
|
1085
|
+
timestamp
|
|
1086
|
+
};
|
|
1087
|
+
if (!SecurityHelpers.validateTimestamp(webhook.timestamp)) {
|
|
1088
|
+
throw new Error("Webhook timestamp too old - possible replay attack");
|
|
1089
|
+
}
|
|
1090
|
+
if (config.validateWebhook) {
|
|
1091
|
+
const isValid = await config.validateWebhook(webhook);
|
|
1092
|
+
if (!isValid) {
|
|
1093
|
+
throw new Error("Invalid webhook payload format");
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (config.validateSignature) {
|
|
1097
|
+
const isValidSignature = config.validateSignature(webhook);
|
|
1098
|
+
if (!isValidSignature) {
|
|
1099
|
+
throw new Error("Webhook signature validation failed");
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
const normalizedMessages = await config.normalize(webhook);
|
|
1103
|
+
const results = await Promise.allSettled(
|
|
1104
|
+
normalizedMessages.map(async (message) => {
|
|
1105
|
+
const payload2 = {
|
|
1106
|
+
tenantId: webhook.tenantId,
|
|
1107
|
+
channelId: webhook.channelId,
|
|
1108
|
+
platform: webhook.platform,
|
|
1109
|
+
message,
|
|
1110
|
+
timestamp: Date.now()
|
|
1111
|
+
};
|
|
1112
|
+
return ctx.broker.sendToChannel(
|
|
1113
|
+
INTEGRATION_CHANNELS.MESSAGE_RECEIVED,
|
|
1114
|
+
payload2,
|
|
1115
|
+
CHANNEL_CONFIG.HIGH_PRIORITY
|
|
1116
|
+
);
|
|
1117
|
+
})
|
|
1118
|
+
);
|
|
1119
|
+
const successful = results.filter(
|
|
1120
|
+
(result) => result.status === "fulfilled"
|
|
1121
|
+
).length;
|
|
1122
|
+
const failed = results.filter(
|
|
1123
|
+
(result) => result.status === "rejected"
|
|
1124
|
+
).length;
|
|
1125
|
+
if (failed > 0) {
|
|
1126
|
+
const failures = results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
1127
|
+
ctx.broker.logger.warn(
|
|
1128
|
+
`Webhook processing partial failure: ${failed}/${normalizedMessages.length} messages failed`,
|
|
1129
|
+
{ failures }
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
return { success: true, messages: successful, failed };
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
i_sendMessage: {
|
|
1136
|
+
rest: {
|
|
1137
|
+
method: "POST",
|
|
1138
|
+
path: "/send"
|
|
1139
|
+
},
|
|
1140
|
+
params: {
|
|
1141
|
+
message: "object",
|
|
1142
|
+
channelId: "string"
|
|
1143
|
+
},
|
|
1144
|
+
async handler(ctx) {
|
|
1145
|
+
const { message, channelId } = ctx.params;
|
|
1146
|
+
const integrationConfig = await config.getChannelConfig(ctx, channelId);
|
|
1147
|
+
if (!integrationConfig) {
|
|
1148
|
+
throw new Error(`Channel configuration not found: ${channelId}`);
|
|
1149
|
+
}
|
|
1150
|
+
const result = await executeWithRetry(
|
|
1151
|
+
() => config.sendMessage(ctx, message, integrationConfig),
|
|
1152
|
+
3,
|
|
1153
|
+
1e3,
|
|
1154
|
+
`Send message via ${config.spec.platform}`
|
|
1155
|
+
);
|
|
1156
|
+
try {
|
|
1157
|
+
if (result.success) {
|
|
1158
|
+
const sentPayload = {
|
|
1159
|
+
tenantId: integrationConfig.tenantId,
|
|
1160
|
+
channelId,
|
|
1161
|
+
platform: config.spec.platform,
|
|
1162
|
+
messageId: result.messageId,
|
|
1163
|
+
metadata: result.metadata,
|
|
1164
|
+
timestamp: Date.now()
|
|
1165
|
+
};
|
|
1166
|
+
await ctx.broker.sendToChannel(
|
|
1167
|
+
INTEGRATION_CHANNELS.MESSAGE_SENT,
|
|
1168
|
+
sentPayload,
|
|
1169
|
+
CHANNEL_CONFIG.DEFAULTS
|
|
1170
|
+
);
|
|
1171
|
+
} else {
|
|
1172
|
+
const failedPayload = {
|
|
1173
|
+
tenantId: integrationConfig.tenantId,
|
|
1174
|
+
channelId,
|
|
1175
|
+
platform: config.spec.platform,
|
|
1176
|
+
error: result.error?.message ?? "Unknown error",
|
|
1177
|
+
message,
|
|
1178
|
+
timestamp: Date.now()
|
|
1179
|
+
};
|
|
1180
|
+
await ctx.broker.sendToChannel(
|
|
1181
|
+
INTEGRATION_CHANNELS.MESSAGE_FAILED,
|
|
1182
|
+
failedPayload,
|
|
1183
|
+
CHANNEL_CONFIG.DEFAULTS
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
} catch (channelError) {
|
|
1187
|
+
const err = channelError instanceof Error ? channelError : new Error(String(channelError));
|
|
1188
|
+
ctx.broker.logger.warn("Failed to send message event to channel", {
|
|
1189
|
+
error: err.message,
|
|
1190
|
+
messageId: result.messageId,
|
|
1191
|
+
platform: config.spec.platform
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
return result;
|
|
1195
|
+
}
|
|
1196
|
+
},
|
|
1197
|
+
i_healthCheck: {
|
|
1198
|
+
rest: {
|
|
1199
|
+
method: "GET",
|
|
1200
|
+
path: "/health"
|
|
1201
|
+
},
|
|
1202
|
+
params: {
|
|
1203
|
+
config: { type: "object", optional: true }
|
|
1204
|
+
},
|
|
1205
|
+
async handler(ctx) {
|
|
1206
|
+
try {
|
|
1207
|
+
if (config.checkHealth) {
|
|
1208
|
+
const integrationConfig = ctx.params.config;
|
|
1209
|
+
if (integrationConfig) {
|
|
1210
|
+
return await config.checkHealth(ctx, integrationConfig);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return {
|
|
1214
|
+
status: "healthy",
|
|
1215
|
+
message: `${config.spec.name} integration is running`,
|
|
1216
|
+
details: {
|
|
1217
|
+
id: config.spec.id,
|
|
1218
|
+
version: config.spec.version,
|
|
1219
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1220
|
+
capabilities: config.spec.capabilities
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1225
|
+
return {
|
|
1226
|
+
status: "unhealthy",
|
|
1227
|
+
message: err.message,
|
|
1228
|
+
details: {
|
|
1229
|
+
id: config.spec.id,
|
|
1230
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
},
|
|
1236
|
+
i_verifyWebhook: {
|
|
1237
|
+
rest: {
|
|
1238
|
+
method: "GET",
|
|
1239
|
+
path: "/:tenantId"
|
|
1240
|
+
},
|
|
1241
|
+
params: {
|
|
1242
|
+
tenantId: "string",
|
|
1243
|
+
mode: "string",
|
|
1244
|
+
token: "string",
|
|
1245
|
+
challenge: "string"
|
|
1246
|
+
},
|
|
1247
|
+
handler(ctx) {
|
|
1248
|
+
if (config.verifyWebhook) {
|
|
1249
|
+
const result = config.verifyWebhook(ctx.params);
|
|
1250
|
+
return result ?? "";
|
|
1251
|
+
}
|
|
1252
|
+
return ctx.params.challenge;
|
|
1253
|
+
}
|
|
1254
|
+
},
|
|
1255
|
+
i_validateCredentials: {
|
|
1256
|
+
params: {
|
|
1257
|
+
credentials: "object"
|
|
1258
|
+
},
|
|
1259
|
+
async handler(ctx) {
|
|
1260
|
+
if (config.validateCredentials) {
|
|
1261
|
+
return await config.validateCredentials(ctx.params.credentials);
|
|
1262
|
+
}
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
if (config.actions) {
|
|
1268
|
+
Object.assign(actions, config.actions);
|
|
1269
|
+
}
|
|
1270
|
+
const baseService = defineService({
|
|
1271
|
+
name: config.name,
|
|
1272
|
+
version: config.version,
|
|
1273
|
+
settings: config.settings,
|
|
1274
|
+
dependencies: config.dependencies,
|
|
1275
|
+
datasources: config.datasources,
|
|
1276
|
+
metadata: {
|
|
1277
|
+
...config.metadata,
|
|
1278
|
+
spec: config.spec
|
|
1279
|
+
},
|
|
1280
|
+
actions,
|
|
1281
|
+
events: config.events ?? {},
|
|
1282
|
+
methods: {
|
|
1283
|
+
...config.methods ?? {},
|
|
1284
|
+
// Add integration-specific methods to the service methods (filter out undefined)
|
|
1285
|
+
// Required methods - no need for conditional
|
|
1286
|
+
normalize: config.normalize,
|
|
1287
|
+
getChannelConfig: config.getChannelConfig,
|
|
1288
|
+
sendMessage: config.sendMessage,
|
|
1289
|
+
// Optional methods
|
|
1290
|
+
...config.validateWebhook && { validateWebhook: config.validateWebhook },
|
|
1291
|
+
...config.verifyWebhook && { verifyWebhook: config.verifyWebhook },
|
|
1292
|
+
...config.checkHealth && { checkHealth: config.checkHealth },
|
|
1293
|
+
...config.validateCredentials && {
|
|
1294
|
+
validateCredentials: config.validateCredentials
|
|
1295
|
+
},
|
|
1296
|
+
...config.validateSignature && {
|
|
1297
|
+
validateSignature: config.validateSignature
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
created: config.created,
|
|
1301
|
+
started: config.started,
|
|
1302
|
+
stopped: config.stopped
|
|
1303
|
+
});
|
|
1304
|
+
return {
|
|
1305
|
+
...baseService,
|
|
1306
|
+
// Only add the integration spec
|
|
1307
|
+
spec: config.spec
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/types/platform.ts
|
|
1312
|
+
var IntegrationPlatform = /* @__PURE__ */ ((IntegrationPlatform2) => {
|
|
1313
|
+
IntegrationPlatform2["WHATSAPP"] = "whatsapp";
|
|
1314
|
+
IntegrationPlatform2["TELEGRAM"] = "telegram";
|
|
1315
|
+
IntegrationPlatform2["SLACK"] = "slack";
|
|
1316
|
+
IntegrationPlatform2["EMAIL"] = "email";
|
|
1317
|
+
IntegrationPlatform2["SMS"] = "sms";
|
|
1318
|
+
IntegrationPlatform2["INSTAGRAM"] = "instagram";
|
|
1319
|
+
IntegrationPlatform2["FACEBOOK"] = "facebook";
|
|
1320
|
+
IntegrationPlatform2["DISCORD"] = "discord";
|
|
1321
|
+
IntegrationPlatform2["WEBCHAT"] = "webchat";
|
|
1322
|
+
IntegrationPlatform2["CUSTOM"] = "custom";
|
|
1323
|
+
return IntegrationPlatform2;
|
|
1324
|
+
})(IntegrationPlatform || {});
|
|
1325
|
+
var IntegrationStatus = /* @__PURE__ */ ((IntegrationStatus2) => {
|
|
1326
|
+
IntegrationStatus2["CONFIGURED"] = "CONFIGURED";
|
|
1327
|
+
IntegrationStatus2["ACTIVE"] = "ACTIVE";
|
|
1328
|
+
IntegrationStatus2["INACTIVE"] = "INACTIVE";
|
|
1329
|
+
IntegrationStatus2["ERROR"] = "ERROR";
|
|
1330
|
+
IntegrationStatus2["SUSPENDED"] = "SUSPENDED";
|
|
1331
|
+
return IntegrationStatus2;
|
|
1332
|
+
})(IntegrationStatus || {});
|
|
1333
|
+
var IntegrationCapability = /* @__PURE__ */ ((IntegrationCapability2) => {
|
|
1334
|
+
IntegrationCapability2["SEND_MESSAGE"] = "send_message";
|
|
1335
|
+
IntegrationCapability2["RECEIVE_MESSAGE"] = "receive_message";
|
|
1336
|
+
IntegrationCapability2["SEND_IMAGE"] = "send_image";
|
|
1337
|
+
IntegrationCapability2["SEND_VIDEO"] = "send_video";
|
|
1338
|
+
IntegrationCapability2["SEND_AUDIO"] = "send_audio";
|
|
1339
|
+
IntegrationCapability2["SEND_FILE"] = "send_file";
|
|
1340
|
+
IntegrationCapability2["SEND_LOCATION"] = "send_location";
|
|
1341
|
+
IntegrationCapability2["SEND_BUTTONS"] = "send_buttons";
|
|
1342
|
+
IntegrationCapability2["SEND_CAROUSEL"] = "send_carousel";
|
|
1343
|
+
IntegrationCapability2["TYPING_INDICATOR"] = "typing_indicator";
|
|
1344
|
+
IntegrationCapability2["READ_RECEIPT"] = "read_receipt";
|
|
1345
|
+
IntegrationCapability2["GROUP_CHAT"] = "group_chat";
|
|
1346
|
+
IntegrationCapability2["REACTIONS"] = "reactions";
|
|
1347
|
+
IntegrationCapability2["THREADS"] = "threads";
|
|
1348
|
+
IntegrationCapability2["VOICE_CALL"] = "voice_call";
|
|
1349
|
+
IntegrationCapability2["VIDEO_CALL"] = "video_call";
|
|
1350
|
+
return IntegrationCapability2;
|
|
1351
|
+
})(IntegrationCapability || {});
|
|
1352
|
+
|
|
1353
|
+
// src/types/message.ts
|
|
1354
|
+
var MessageContentType = /* @__PURE__ */ ((MessageContentType2) => {
|
|
1355
|
+
MessageContentType2["TEXT"] = "text";
|
|
1356
|
+
MessageContentType2["IMAGE"] = "image";
|
|
1357
|
+
MessageContentType2["VIDEO"] = "video";
|
|
1358
|
+
MessageContentType2["AUDIO"] = "audio";
|
|
1359
|
+
MessageContentType2["FILE"] = "file";
|
|
1360
|
+
MessageContentType2["LOCATION"] = "location";
|
|
1361
|
+
MessageContentType2["BUTTONS"] = "buttons";
|
|
1362
|
+
MessageContentType2["CAROUSEL"] = "carousel";
|
|
1363
|
+
MessageContentType2["REACTION"] = "reaction";
|
|
1364
|
+
return MessageContentType2;
|
|
1365
|
+
})(MessageContentType || {});
|
|
1366
|
+
|
|
1367
|
+
// src/types/user.ts
|
|
1368
|
+
var UserRole = {
|
|
1369
|
+
OWNER: "OWNER",
|
|
1370
|
+
ADMIN: "ADMIN",
|
|
1371
|
+
MANAGER: "MANAGER",
|
|
1372
|
+
AGENT: "AGENT",
|
|
1373
|
+
VIEWER: "VIEWER"
|
|
1374
|
+
};
|
|
1375
|
+
|
|
1376
|
+
// src/datasources/base.datasource.ts
|
|
1377
|
+
var AbstractDatasource = class {
|
|
1378
|
+
/**
|
|
1379
|
+
* Default health check - always returns true
|
|
1380
|
+
* Override for custom health check logic
|
|
1381
|
+
*/
|
|
1382
|
+
async healthCheck() {
|
|
1383
|
+
this.broker.logger.info(`Health check for datasource - ${this.name}`);
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Default clear method - does nothing
|
|
1388
|
+
* Override to implement clearing logic
|
|
1389
|
+
*/
|
|
1390
|
+
async clear() {
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
// src/datasources/prisma.datasource.ts
|
|
1395
|
+
var PrismaDatasource = class extends AbstractDatasource {
|
|
1396
|
+
constructor(prismaClient) {
|
|
1397
|
+
super();
|
|
1398
|
+
this.name = "prisma";
|
|
1399
|
+
this._client = null;
|
|
1400
|
+
this.providedClient = null;
|
|
1401
|
+
this.providedClient = prismaClient ?? null;
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Get Prisma client instance (singleton pattern)
|
|
1405
|
+
*/
|
|
1406
|
+
get client() {
|
|
1407
|
+
this._client ?? (this._client = this.initializePrismaClient());
|
|
1408
|
+
this.updateTenantFromContext();
|
|
1409
|
+
return this._client;
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Initialize Prisma client using singleton pattern or provided instance
|
|
1413
|
+
*/
|
|
1414
|
+
initializePrismaClient() {
|
|
1415
|
+
if (this.providedClient) {
|
|
1416
|
+
this.broker.logger.info("Using provided PrismaClient instance");
|
|
1417
|
+
return this.providedClient;
|
|
1418
|
+
}
|
|
1419
|
+
if (!globalThis.__prisma) {
|
|
1420
|
+
this.broker.logger.info("Creating new PrismaClient singleton");
|
|
1421
|
+
const baseClient = this.createClient();
|
|
1422
|
+
globalThis.__prisma = this.applyExtensions(baseClient);
|
|
1423
|
+
} else {
|
|
1424
|
+
this.broker.logger.info("Using existing PrismaClient singleton");
|
|
1425
|
+
}
|
|
1426
|
+
return globalThis.__prisma;
|
|
1427
|
+
}
|
|
1428
|
+
/**
|
|
1429
|
+
* Create a new Prisma client instance
|
|
1430
|
+
* Override this method in subclasses to provide the actual PrismaClient
|
|
1431
|
+
*/
|
|
1432
|
+
createClient() {
|
|
1433
|
+
throw new Error(
|
|
1434
|
+
"createClient() must be implemented by subclass or provide PrismaClient in constructor. Example: class MyPrismaDataSource extends PrismaDatasource { protected createClient() { return new PrismaClient(); } }"
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Apply extensions to the Prisma client
|
|
1439
|
+
* Override this method to add production extensions like soft delete, audit trails, etc.
|
|
1440
|
+
*/
|
|
1441
|
+
applyExtensions(client) {
|
|
1442
|
+
return client;
|
|
1443
|
+
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Get extended client with all applied extensions
|
|
1446
|
+
*/
|
|
1447
|
+
get extendedClient() {
|
|
1448
|
+
return this.applyExtensions(this.client);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Initialize datasource - called after broker injection
|
|
1452
|
+
*/
|
|
1453
|
+
async init() {
|
|
1454
|
+
this.broker.logger.info("Initializing Prisma datasource");
|
|
1455
|
+
}
|
|
1456
|
+
/**
|
|
1457
|
+
* Called automatically when context is injected
|
|
1458
|
+
* Sets tenant context from service context if available
|
|
1459
|
+
*/
|
|
1460
|
+
updateTenantFromContext() {
|
|
1461
|
+
const tenantId = this.context?.meta?.tenantId;
|
|
1462
|
+
if (tenantId && typeof tenantId === "string") {
|
|
1463
|
+
this.setTenantContext(tenantId);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Connect to database - called when service starts
|
|
1468
|
+
*/
|
|
1469
|
+
async connect() {
|
|
1470
|
+
try {
|
|
1471
|
+
this.broker.logger.info("Connecting to database via Prisma");
|
|
1472
|
+
await this.client.$connect();
|
|
1473
|
+
this.broker.logger.info("Successfully connected to database");
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
this.broker.logger.error("Failed to connect to database:", error);
|
|
1476
|
+
throw error;
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
/**
|
|
1480
|
+
* Disconnect from database - called when service stops
|
|
1481
|
+
*/
|
|
1482
|
+
async disconnect() {
|
|
1483
|
+
try {
|
|
1484
|
+
this.broker.logger.info("Disconnecting from database");
|
|
1485
|
+
await this.client.$disconnect();
|
|
1486
|
+
this.broker.logger.info("Successfully disconnected from database");
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
this.broker.logger.error("Error disconnecting from database:", error);
|
|
1489
|
+
throw error;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Health check - verify database connectivity
|
|
1494
|
+
*/
|
|
1495
|
+
async healthCheck() {
|
|
1496
|
+
try {
|
|
1497
|
+
this.broker.logger.info("Running Prisma health check");
|
|
1498
|
+
await this.client.$queryRaw`SELECT 1`;
|
|
1499
|
+
this.broker.logger.info("Prisma health check passed");
|
|
1500
|
+
return true;
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
this.broker.logger.error("Prisma health check failed:", error);
|
|
1503
|
+
return false;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Clear/reset data - useful for testing
|
|
1508
|
+
*/
|
|
1509
|
+
async clear() {
|
|
1510
|
+
if (isTest || isDev) {
|
|
1511
|
+
this.broker.logger.warn(
|
|
1512
|
+
"Clearing database (only allowed in test/dev mode)"
|
|
1513
|
+
);
|
|
1514
|
+
const modelNames = Object.keys(this.client).filter(
|
|
1515
|
+
(key) => !key.startsWith("_") && !key.startsWith("$")
|
|
1516
|
+
);
|
|
1517
|
+
for (const modelName of modelNames.reverse()) {
|
|
1518
|
+
try {
|
|
1519
|
+
const model = this.client[modelName];
|
|
1520
|
+
if (model.deleteMany) {
|
|
1521
|
+
await model.deleteMany();
|
|
1522
|
+
}
|
|
1523
|
+
} catch (error) {
|
|
1524
|
+
this.broker.logger.debug(`Could not clear ${modelName}:`, error);
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
} else {
|
|
1528
|
+
throw new Error(
|
|
1529
|
+
"Database clear is only allowed in test/development environments"
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* Transaction wrapper with proper error handling
|
|
1535
|
+
*/
|
|
1536
|
+
async transaction(fn, options) {
|
|
1537
|
+
try {
|
|
1538
|
+
return await this.client.$transaction(
|
|
1539
|
+
(tx) => fn(tx),
|
|
1540
|
+
options
|
|
1541
|
+
);
|
|
1542
|
+
} catch (error) {
|
|
1543
|
+
this.broker.logger.error("Transaction failed:", error);
|
|
1544
|
+
throw error;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Set tenant context for multi-tenant applications
|
|
1549
|
+
* Requires tenant extension to be applied
|
|
1550
|
+
*/
|
|
1551
|
+
setTenantContext(tenantId) {
|
|
1552
|
+
const tenantClient = this.client;
|
|
1553
|
+
if (tenantClient.$setTenant) {
|
|
1554
|
+
tenantClient.$setTenant(tenantId);
|
|
1555
|
+
this.broker.logger.debug("Tenant context set:", { tenantId });
|
|
1556
|
+
} else {
|
|
1557
|
+
this.broker.logger.warn(
|
|
1558
|
+
"Tenant extension not available - setTenantContext ignored"
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
/**
|
|
1563
|
+
* Get current tenant context
|
|
1564
|
+
*/
|
|
1565
|
+
getCurrentTenant() {
|
|
1566
|
+
const tenantClient = this.client;
|
|
1567
|
+
if (tenantClient.$getCurrentTenant) {
|
|
1568
|
+
return tenantClient.$getCurrentTenant();
|
|
1569
|
+
}
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* Execute with tenant context (automatically restores previous context)
|
|
1574
|
+
*/
|
|
1575
|
+
async withTenant(tenantId, fn) {
|
|
1576
|
+
const previousTenant = this.getCurrentTenant();
|
|
1577
|
+
try {
|
|
1578
|
+
this.setTenantContext(tenantId);
|
|
1579
|
+
return await fn();
|
|
1580
|
+
} finally {
|
|
1581
|
+
if (previousTenant) {
|
|
1582
|
+
this.setTenantContext(previousTenant);
|
|
1583
|
+
} else {
|
|
1584
|
+
const tenantClient = this.client;
|
|
1585
|
+
if (tenantClient.$clearTenant) {
|
|
1586
|
+
tenantClient.$clearTenant();
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
// src/datasources/extensions/soft-delete.extension.ts
|
|
1594
|
+
var softDeleteExtension = {
|
|
1595
|
+
name: "SoftDelete",
|
|
1596
|
+
query: {
|
|
1597
|
+
// Apply to all models
|
|
1598
|
+
$allModels: {
|
|
1599
|
+
// Override findMany to exclude soft deleted records
|
|
1600
|
+
async findMany({ args, query }) {
|
|
1601
|
+
args.where ?? (args.where = {});
|
|
1602
|
+
if (args.where.deletedAt === void 0) {
|
|
1603
|
+
args.where.deletedAt = null;
|
|
1604
|
+
}
|
|
1605
|
+
return query(args);
|
|
1606
|
+
},
|
|
1607
|
+
// Override findUnique to exclude soft deleted records
|
|
1608
|
+
async findUnique({ args, query }) {
|
|
1609
|
+
args.where ?? (args.where = {});
|
|
1610
|
+
if (args.where.deletedAt === void 0) {
|
|
1611
|
+
args.where.deletedAt = null;
|
|
1612
|
+
}
|
|
1613
|
+
return query(args);
|
|
1614
|
+
},
|
|
1615
|
+
// Override findFirst to exclude soft deleted records
|
|
1616
|
+
async findFirst({ args, query }) {
|
|
1617
|
+
args.where ?? (args.where = {});
|
|
1618
|
+
if (args.where.deletedAt === void 0) {
|
|
1619
|
+
args.where.deletedAt = null;
|
|
1620
|
+
}
|
|
1621
|
+
return query(args);
|
|
1622
|
+
},
|
|
1623
|
+
// Override delete to set deletedAt instead
|
|
1624
|
+
async delete({ args, query }) {
|
|
1625
|
+
return query({
|
|
1626
|
+
...args,
|
|
1627
|
+
data: { deletedAt: /* @__PURE__ */ new Date() }
|
|
1628
|
+
});
|
|
1629
|
+
},
|
|
1630
|
+
// Override deleteMany to set deletedAt instead
|
|
1631
|
+
async deleteMany({ args, query }) {
|
|
1632
|
+
return query({
|
|
1633
|
+
...args,
|
|
1634
|
+
data: { deletedAt: /* @__PURE__ */ new Date() }
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
},
|
|
1639
|
+
model: {
|
|
1640
|
+
$allModels: {
|
|
1641
|
+
// Add restore method to all models
|
|
1642
|
+
async restore(where) {
|
|
1643
|
+
const context = globalThis.Prisma?.getExtensionContext?.(this) ?? this;
|
|
1644
|
+
return context.update({
|
|
1645
|
+
where,
|
|
1646
|
+
data: { deletedAt: null }
|
|
1647
|
+
});
|
|
1648
|
+
},
|
|
1649
|
+
// Add findWithDeleted method to include soft deleted records
|
|
1650
|
+
async findWithDeleted(args) {
|
|
1651
|
+
const context = globalThis.Prisma?.getExtensionContext?.(this) ?? this;
|
|
1652
|
+
return context.findMany({
|
|
1653
|
+
...args,
|
|
1654
|
+
where: {
|
|
1655
|
+
...args.where
|
|
1656
|
+
// Don't filter by deletedAt
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
},
|
|
1660
|
+
// Add findDeleted method to find only soft deleted records
|
|
1661
|
+
async findDeleted(args) {
|
|
1662
|
+
const context = globalThis.Prisma?.getExtensionContext?.(this) ?? this;
|
|
1663
|
+
return context.findMany({
|
|
1664
|
+
...args,
|
|
1665
|
+
where: {
|
|
1666
|
+
...args.where,
|
|
1667
|
+
deletedAt: { not: null }
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
};
|
|
1674
|
+
|
|
1675
|
+
// src/datasources/extensions/tenant.extension.ts
|
|
1676
|
+
function createTenantExtension() {
|
|
1677
|
+
const tenantContext = {
|
|
1678
|
+
currentTenantId: null
|
|
1679
|
+
};
|
|
1680
|
+
return {
|
|
1681
|
+
name: "Tenant",
|
|
1682
|
+
client: {
|
|
1683
|
+
// Set tenant context
|
|
1684
|
+
$setTenant(tenantId) {
|
|
1685
|
+
tenantContext.currentTenantId = tenantId;
|
|
1686
|
+
},
|
|
1687
|
+
// Get current tenant
|
|
1688
|
+
$getCurrentTenant() {
|
|
1689
|
+
return tenantContext.currentTenantId;
|
|
1690
|
+
},
|
|
1691
|
+
// Clear tenant context
|
|
1692
|
+
$clearTenant() {
|
|
1693
|
+
tenantContext.currentTenantId = null;
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
query: {
|
|
1697
|
+
$allModels: {
|
|
1698
|
+
// Automatically add tenantId filter to all read operations
|
|
1699
|
+
async findMany({ args, query }) {
|
|
1700
|
+
var _a;
|
|
1701
|
+
if (tenantContext.currentTenantId) {
|
|
1702
|
+
args.where ?? (args.where = {});
|
|
1703
|
+
(_a = args.where).tenantId ?? (_a.tenantId = tenantContext.currentTenantId);
|
|
1704
|
+
}
|
|
1705
|
+
return query(args);
|
|
1706
|
+
},
|
|
1707
|
+
async findUnique({ args, query }) {
|
|
1708
|
+
var _a;
|
|
1709
|
+
if (tenantContext.currentTenantId) {
|
|
1710
|
+
args.where ?? (args.where = {});
|
|
1711
|
+
(_a = args.where).tenantId ?? (_a.tenantId = tenantContext.currentTenantId);
|
|
1712
|
+
}
|
|
1713
|
+
return query(args);
|
|
1714
|
+
},
|
|
1715
|
+
async findFirst({ args, query }) {
|
|
1716
|
+
var _a;
|
|
1717
|
+
if (tenantContext.currentTenantId) {
|
|
1718
|
+
args.where ?? (args.where = {});
|
|
1719
|
+
(_a = args.where).tenantId ?? (_a.tenantId = tenantContext.currentTenantId);
|
|
1720
|
+
}
|
|
1721
|
+
return query(args);
|
|
1722
|
+
},
|
|
1723
|
+
// Automatically add tenantId to create operations
|
|
1724
|
+
async create({ args, query }) {
|
|
1725
|
+
var _a;
|
|
1726
|
+
if (tenantContext.currentTenantId) {
|
|
1727
|
+
args.data ?? (args.data = {});
|
|
1728
|
+
(_a = args.data).tenantId ?? (_a.tenantId = tenantContext.currentTenantId);
|
|
1729
|
+
}
|
|
1730
|
+
return query(args);
|
|
1731
|
+
},
|
|
1732
|
+
// Add tenantId filter to update operations
|
|
1733
|
+
async update({ args, query }) {
|
|
1734
|
+
var _a;
|
|
1735
|
+
if (tenantContext.currentTenantId) {
|
|
1736
|
+
args.where ?? (args.where = {});
|
|
1737
|
+
(_a = args.where).tenantId ?? (_a.tenantId = tenantContext.currentTenantId);
|
|
1738
|
+
}
|
|
1739
|
+
return query(args);
|
|
1740
|
+
},
|
|
1741
|
+
// Add tenantId filter to delete operations
|
|
1742
|
+
async delete({ args, query }) {
|
|
1743
|
+
var _a;
|
|
1744
|
+
if (tenantContext.currentTenantId) {
|
|
1745
|
+
args.where ?? (args.where = {});
|
|
1746
|
+
(_a = args.where).tenantId ?? (_a.tenantId = tenantContext.currentTenantId);
|
|
1747
|
+
}
|
|
1748
|
+
return query(args);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/datasources/extensions/retry.extension.ts
|
|
1756
|
+
var retryExtension = {
|
|
1757
|
+
name: "Retry",
|
|
1758
|
+
client: {
|
|
1759
|
+
async $retryTransaction(fn, options = {}) {
|
|
1760
|
+
const {
|
|
1761
|
+
maxRetries = 3,
|
|
1762
|
+
baseDelay = 100,
|
|
1763
|
+
maxDelay = 5e3,
|
|
1764
|
+
retryableErrors = [
|
|
1765
|
+
"P2034",
|
|
1766
|
+
// Transaction failed due to a write conflict
|
|
1767
|
+
"P2002",
|
|
1768
|
+
// Unique constraint failed
|
|
1769
|
+
"P5000"
|
|
1770
|
+
// Raw query failed
|
|
1771
|
+
]
|
|
1772
|
+
} = options;
|
|
1773
|
+
let lastError;
|
|
1774
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1775
|
+
try {
|
|
1776
|
+
return await this.$transaction(fn, {
|
|
1777
|
+
timeout: 1e4,
|
|
1778
|
+
maxWait: 5e3
|
|
1779
|
+
});
|
|
1780
|
+
} catch (error) {
|
|
1781
|
+
lastError = error;
|
|
1782
|
+
const isRetryable = retryableErrors.some(
|
|
1783
|
+
(code) => error.code === code || error.message?.includes(code)
|
|
1784
|
+
);
|
|
1785
|
+
if (!isRetryable || attempt === maxRetries) {
|
|
1786
|
+
throw error;
|
|
1787
|
+
}
|
|
1788
|
+
const delay = Math.min(
|
|
1789
|
+
baseDelay * Math.pow(2, attempt) + Math.random() * 100,
|
|
1790
|
+
maxDelay
|
|
1791
|
+
);
|
|
1792
|
+
if (typeof globalThis !== "undefined" && globalThis.console) {
|
|
1793
|
+
globalThis.console.warn(
|
|
1794
|
+
`Transaction retry ${attempt + 1}/${maxRetries} after ${delay}ms:`,
|
|
1795
|
+
{
|
|
1796
|
+
error: error.message,
|
|
1797
|
+
code: error.code
|
|
1798
|
+
}
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
throw lastError ?? new Error("Transaction failed after maximum retries");
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
|
|
1809
|
+
export { AbstractDatasource, CHANNELS, ContextHelpersMiddleware, CreateHealthCheckMiddleware, DatasourceMixin, HEALTH_CHECK_DEFAULTS, INTEGRATION_CHANNELS, IntegrationCapability, IntegrationPlatform, IntegrationStatus, MemoizeMixin, MessageContentType, NAMESPACE, PERMISSIONS, PermissionsMiddleware, PrismaDatasource, REDIS_URL, ROLE_PERMISSIONS, UserRole, createDatasourceMiddleware, createTenantExtension, defineIntegration, defineService, env_default as env, isDev, isProd, isTest, moleculer_default as moleculer, nodeEnv, omit, retryExtension, softDeleteExtension };
|