@orion-js/echoes 4.4.1 → 4.5.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.
package/dist/index.cjs CHANGED
@@ -32,28 +32,40 @@ __export(index_exports, {
32
32
  EchoEvent: () => EchoEvent,
33
33
  EchoRequest: () => EchoRequest,
34
34
  Echoes: () => Echoes,
35
+ EchoesUserError: () => EchoesUserError,
36
+ EchoesValidationError: () => EchoesValidationError,
37
+ cleanEchoesSchema: () => cleanEchoesSchema,
38
+ configureEchoesRuntime: () => configureEchoesRuntime,
35
39
  createEchoEvent: () => createEchoEvent,
36
40
  createEchoRequest: () => createEchoRequest,
41
+ createEchoesUserError: () => createEchoesUserError,
42
+ createEchoesValidationError: () => createEchoesValidationError,
37
43
  echo: () => echo,
44
+ getEchoesContext: () => getEchoesContext,
45
+ getEchoesLogger: () => getEchoesLogger,
46
+ getEchoesRuntime: () => getEchoesRuntime,
38
47
  getServiceEchoes: () => getServiceEchoes,
48
+ parseEchoesSchema: () => parseEchoesSchema,
39
49
  publish: () => publish,
40
50
  request: () => request,
51
+ runWithEchoesContext: () => runWithEchoesContext,
41
52
  startService: () => startService,
42
- stopService: () => stopService
53
+ stopService: () => stopService,
54
+ typedEchoesSchema: () => typedEchoesSchema
43
55
  });
44
56
  module.exports = __toCommonJS(index_exports);
45
57
 
46
- // src/startService/index.ts
47
- var import_http2 = require("@orion-js/http");
48
-
49
58
  // src/config.ts
50
59
  var config = {};
51
60
  var config_default = config;
52
61
 
53
- // src/startService/KafkaManager.ts
54
- var import_node_crypto = require("crypto");
55
- var import_logger = require("@orion-js/logger");
56
- var import_kafkajs = require("kafkajs");
62
+ // src/publish/index.ts
63
+ async function publish(options) {
64
+ if (!config_default.eventBus) {
65
+ throw new Error("You must initialize echoes configuration to use publish");
66
+ }
67
+ return await config_default.eventBus.publish(options);
68
+ }
57
69
 
58
70
  // src/echo/deserialize.ts
59
71
  function deserialize_default(serializedJavascript) {
@@ -66,14 +78,306 @@ function deserialize_default(serializedJavascript) {
66
78
 
67
79
  // src/publish/serialize.ts
68
80
  var import_serialize_javascript = __toESM(require("serialize-javascript"), 1);
69
- var import_helpers = require("@orion-js/helpers");
70
81
  function serialize_default(data) {
71
- const cloned = (0, import_helpers.clone)(data);
72
- const serialized = (0, import_serialize_javascript.default)(cloned, { ignoreFunction: true });
82
+ const serialized = (0, import_serialize_javascript.default)(data, { ignoreFunction: true });
73
83
  return serialized;
74
84
  }
75
85
 
86
+ // src/runtime.ts
87
+ var import_node_async_hooks = require("async_hooks");
88
+ var import_node_crypto = require("crypto");
89
+
90
+ // src/errors.ts
91
+ var EchoesUserError = class extends Error {
92
+ isEchoesError = true;
93
+ isOrionError = true;
94
+ isUserError = true;
95
+ code;
96
+ extra;
97
+ constructor(code, message, extra) {
98
+ if (!message) {
99
+ message = code;
100
+ code = "error";
101
+ }
102
+ super(message);
103
+ this.name = "EchoesUserError";
104
+ this.code = code;
105
+ this.extra = extra;
106
+ }
107
+ getInfo() {
108
+ return { error: this.code, message: this.message, extra: this.extra };
109
+ }
110
+ };
111
+ var EchoesValidationError = class extends Error {
112
+ isEchoesError = true;
113
+ isOrionError = true;
114
+ isValidationError = true;
115
+ code = "validationError";
116
+ validationErrors;
117
+ labels;
118
+ constructor(validationErrors, labels = {}) {
119
+ const printableErrors = Object.entries(validationErrors).map(([key, value]) => `${key}: ${value}`).join(", ");
120
+ super(`Validation Error: {${printableErrors}}`);
121
+ this.name = "EchoesValidationError";
122
+ this.validationErrors = validationErrors;
123
+ this.labels = Object.fromEntries(
124
+ Object.keys(validationErrors).filter((key) => labels[key]).map((key) => [key, labels[key]])
125
+ );
126
+ }
127
+ getInfo() {
128
+ return {
129
+ error: this.code,
130
+ message: "Validation Error",
131
+ validationErrors: this.validationErrors,
132
+ labels: this.labels
133
+ };
134
+ }
135
+ };
136
+
137
+ // src/runtime.ts
138
+ var defaultLogger = {
139
+ debug: (message, metadata) => metadata === void 0 ? console.debug(message) : console.debug(message, metadata),
140
+ info: (message, metadata) => metadata === void 0 ? console.info(message) : console.info(message, metadata),
141
+ warn: (message, metadata) => metadata === void 0 ? console.warn(message) : console.warn(message, metadata),
142
+ error: (message, metadata) => metadata === void 0 ? console.error(message) : console.error(message, metadata)
143
+ };
144
+ var runtime = {};
145
+ var contextStorage = new import_node_async_hooks.AsyncLocalStorage();
146
+ function configureEchoesRuntime(adapter) {
147
+ const previous = runtime;
148
+ runtime = { ...runtime, ...adapter };
149
+ return () => {
150
+ runtime = previous;
151
+ };
152
+ }
153
+ function getEchoesRuntime() {
154
+ return runtime;
155
+ }
156
+ function getEchoesLogger() {
157
+ return runtime.logger || defaultLogger;
158
+ }
159
+ function getEchoesContext() {
160
+ return contextStorage.getStore();
161
+ }
162
+ async function runWithEchoesContext(context, callback) {
163
+ const contextWithId = { contextId: context.contextId || (0, import_node_crypto.randomUUID)(), ...context };
164
+ return await contextStorage.run(contextWithId, async () => {
165
+ if (runtime.runWithContext) {
166
+ return await runtime.runWithContext(contextWithId, callback);
167
+ }
168
+ return await callback();
169
+ });
170
+ }
171
+ function isSimpleSchemaLike(schema) {
172
+ return !!schema && typeof schema === "object" && typeof schema.clean === "function" && typeof schema.validate === "function";
173
+ }
174
+ function cloneValue(value) {
175
+ if (value === null || typeof value !== "object") return value;
176
+ if (value instanceof Date) return new Date(value.getTime());
177
+ if (Buffer.isBuffer(value)) return Buffer.from(value);
178
+ if (Array.isArray(value)) return value.map(cloneValue);
179
+ const prototype = Object.getPrototypeOf(value);
180
+ if (prototype !== Object.prototype && prototype !== null) return value;
181
+ const result = {};
182
+ for (const [key, child] of Object.entries(value)) {
183
+ result[key] = cloneValue(child);
184
+ }
185
+ return result;
186
+ }
187
+ async function cleanSimpleSchema(schema, value) {
188
+ const cloned = cloneValue(value);
189
+ return await schema.clean(cloned, { mutate: false });
190
+ }
191
+ async function cleanEchoesSchema(schema, value) {
192
+ if (isSimpleSchemaLike(schema)) {
193
+ return await cleanSimpleSchema(schema, value);
194
+ }
195
+ if (runtime.schema) {
196
+ return await runtime.schema.clean(schema, value);
197
+ }
198
+ throw new Error(
199
+ "Echoes received a schema it cannot execute. Use a SimpleSchema-compatible object or configure an Echoes schema adapter."
200
+ );
201
+ }
202
+ async function parseEchoesSchema(schema, value) {
203
+ if (isSimpleSchemaLike(schema)) {
204
+ const cleaned = await cleanSimpleSchema(schema, value);
205
+ await schema.validate(cleaned);
206
+ return cleaned;
207
+ }
208
+ if (runtime.schema) {
209
+ return await runtime.schema.parse(schema, value);
210
+ }
211
+ throw new Error(
212
+ "Echoes received a schema it cannot execute. Use a SimpleSchema-compatible object or configure an Echoes schema adapter."
213
+ );
214
+ }
215
+ function createEchoesValidationError(info) {
216
+ return runtime.createValidationError ? runtime.createValidationError(info) : new EchoesValidationError(info.validationErrors || {}, info.labels);
217
+ }
218
+ function createEchoesUserError(info) {
219
+ return runtime.createUserError ? runtime.createUserError(info) : new EchoesUserError(info.error, info.message, info.extra);
220
+ }
221
+
222
+ // src/request/getSignature.ts
223
+ var import_node_crypto2 = require("crypto");
224
+
225
+ // src/request/getPassword.ts
226
+ function getEchoesPassword() {
227
+ var _a, _b;
228
+ const secret = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.key) || process.env.echoes_password || process.env.ECHOES_PASSWORD;
229
+ if (!secret) {
230
+ getEchoesLogger().warn(
231
+ 'Warning: no secret key found for echoes requests. Init echoes or set the env var "echoes_password" or process.env.ECHOES_PASSWORD'
232
+ );
233
+ }
234
+ return secret;
235
+ }
236
+
237
+ // src/request/getSignature.ts
238
+ function getSignature_default(_body) {
239
+ const password = getEchoesPassword();
240
+ return (0, import_node_crypto2.createHmac)("sha1", password || "").update("").digest("hex");
241
+ }
242
+
243
+ // src/request/getURL.ts
244
+ function getURL_default(serviceName) {
245
+ var _a, _b, _c;
246
+ if (serviceName.startsWith("http")) return serviceName;
247
+ const url = (_c = (_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.services) == null ? void 0 : _c[serviceName];
248
+ if (!url) {
249
+ throw new Error(`No URL found in echoes config for service ${serviceName}`);
250
+ }
251
+ return url;
252
+ }
253
+
254
+ // src/request/makeRequest.ts
255
+ var import_node_http = __toESM(require("http"), 1);
256
+ var import_node_https = __toESM(require("https"), 1);
257
+ async function executeWithRetries(callback, retries = 0, delayMs = 200) {
258
+ try {
259
+ return await callback();
260
+ } catch (error) {
261
+ if (retries <= 0) throw error;
262
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
263
+ return await executeWithRetries(callback, retries - 1, delayMs);
264
+ }
265
+ }
266
+ async function postJSON(urlValue, data, timeout, redirectsRemaining = 5) {
267
+ const url = new URL(urlValue);
268
+ const body = JSON.stringify(data);
269
+ const transport = url.protocol === "https:" ? import_node_https.default : import_node_http.default;
270
+ return await new Promise((resolve, reject) => {
271
+ const request2 = transport.request(
272
+ url,
273
+ {
274
+ method: "POST",
275
+ headers: {
276
+ "User-Agent": "Orionjs-Echoes/1.1",
277
+ "Content-Type": "application/json",
278
+ "Content-Length": Buffer.byteLength(body)
279
+ }
280
+ },
281
+ (response) => {
282
+ const chunks = [];
283
+ const statusCode = response.statusCode || 0;
284
+ if (statusCode >= 300 && statusCode < 400 && response.headers.location) {
285
+ response.resume();
286
+ if (redirectsRemaining <= 0) {
287
+ reject(new Error("Echoes request exceeded the redirect limit"));
288
+ return;
289
+ }
290
+ resolve(
291
+ postJSON(
292
+ new URL(response.headers.location, url).toString(),
293
+ data,
294
+ timeout,
295
+ redirectsRemaining - 1
296
+ )
297
+ );
298
+ return;
299
+ }
300
+ response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
301
+ response.on("end", () => {
302
+ try {
303
+ const responseBody = Buffer.concat(chunks).toString("utf8");
304
+ if (statusCode < 200 || statusCode >= 300) {
305
+ reject(new Error(`Request failed with status code ${statusCode}`));
306
+ return;
307
+ }
308
+ resolve({
309
+ statusCode,
310
+ data: responseBody ? JSON.parse(responseBody) : {}
311
+ });
312
+ } catch (error) {
313
+ reject(error);
314
+ }
315
+ });
316
+ }
317
+ );
318
+ request2.on("error", reject);
319
+ if (timeout) {
320
+ request2.setTimeout(timeout, () => {
321
+ request2.destroy(new Error(`Echoes request timed out after ${timeout}ms`));
322
+ });
323
+ }
324
+ request2.end(body);
325
+ });
326
+ }
327
+ var makeRequest = async (options) => {
328
+ return await executeWithRetries(
329
+ () => postJSON(options.url, options.data, options.timeout),
330
+ options.retries || 0
331
+ );
332
+ };
333
+
334
+ // src/request/index.ts
335
+ async function request(options) {
336
+ var _a, _b;
337
+ const { method, service, params } = options;
338
+ const serializedParams = serialize_default(params);
339
+ const date = /* @__PURE__ */ new Date();
340
+ const body = { method, service, serializedParams, date };
341
+ const signature = getSignature_default(body);
342
+ try {
343
+ const requestMaker = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.makeRequest) || makeRequest;
344
+ const requestOptions = {
345
+ url: getURL_default(service),
346
+ retries: options.retries,
347
+ timeout: options.timeout,
348
+ data: {
349
+ body,
350
+ signature
351
+ }
352
+ };
353
+ const result = await requestMaker(requestOptions);
354
+ if (result.statusCode !== 200) {
355
+ throw new Error(`Wrong status code ${result.statusCode}`);
356
+ }
357
+ const data = result.data;
358
+ if (data.error) {
359
+ const info = data.errorInfo;
360
+ if (info) {
361
+ if (data.isValidationError) {
362
+ throw createEchoesValidationError(info);
363
+ }
364
+ if (data.isUserError) {
365
+ throw createEchoesUserError(info);
366
+ }
367
+ }
368
+ throw new Error(`${data.error}`);
369
+ }
370
+ const response = deserialize_default(data.result);
371
+ return response;
372
+ } catch (error) {
373
+ const caught = error;
374
+ if (caught.isOrionError || caught.isEchoesError) throw caught;
375
+ throw new Error(`Echoes request network error calling ${service}/${method}: ${caught.message}`);
376
+ }
377
+ }
378
+
76
379
  // src/startService/KafkaManager.ts
380
+ var import_node_crypto3 = require("crypto");
77
381
  var HEARTBEAT_INTERVAL_SECONDS = 5;
78
382
  var CHECK_JOIN_CONSUMER_INTERVAL_SECONDS = 30;
79
383
  var DEFAULT_PARTITIONS_CONSUMED_CONCURRENTLY = 4;
@@ -91,11 +395,21 @@ var KafkaManager = class {
91
395
  producerConnected = false;
92
396
  interval;
93
397
  constructor(options) {
94
- this.kafka = new import_kafkajs.Kafka(options.client);
95
398
  this.options = options;
96
399
  }
97
400
  async start(options) {
98
401
  var _a;
402
+ let kafkaModule;
403
+ try {
404
+ kafkaModule = await import("kafkajs");
405
+ } catch (error) {
406
+ const wrapped = new Error(
407
+ "Echoes Kafka transport requires kafkajs to be installed in the application"
408
+ );
409
+ wrapped.cause = error;
410
+ throw wrapped;
411
+ }
412
+ this.kafka = new kafkaModule.Kafka(this.options.client);
99
413
  this.onEvent = options.onEvent;
100
414
  this.subscriptions = new Map(
101
415
  options.subscriptions.map((subscription) => [subscription.topic, subscription])
@@ -113,7 +427,7 @@ var KafkaManager = class {
113
427
  this.consumer = this.kafka.consumer(this.options.consumer);
114
428
  this.consumerStarted = await this.conditionalStart();
115
429
  if (this.consumerStarted) return;
116
- import_logger.logger.info("Echoes: Delaying consumer group join, waiting for conditions to be met");
430
+ getEchoesLogger().info("Echoes: Delaying consumer group join, waiting for conditions to be met");
117
431
  this.interval = setInterval(async () => {
118
432
  this.consumerStarted = await this.conditionalStart();
119
433
  if (this.consumerStarted) clearInterval(this.interval);
@@ -131,7 +445,7 @@ var KafkaManager = class {
131
445
  {
132
446
  value: serialize_default({ params: options.params }),
133
447
  headers: {
134
- "echoes-event-id": (0, import_node_crypto.randomUUID)()
448
+ "echoes-event-id": (0, import_node_crypto3.randomUUID)()
135
449
  }
136
450
  }
137
451
  ]
@@ -139,7 +453,8 @@ var KafkaManager = class {
139
453
  }
140
454
  async close() {
141
455
  var _a, _b;
142
- import_logger.logger.warn("Echoes: Stopping Kafka transport");
456
+ const logger = getEchoesLogger();
457
+ logger.warn("Echoes: Stopping Kafka transport");
143
458
  if (this.interval) clearInterval(this.interval);
144
459
  await Promise.all([
145
460
  (_a = this.consumer) == null ? void 0 : _a.disconnect(),
@@ -149,6 +464,7 @@ var KafkaManager = class {
149
464
  this.producerConnected = false;
150
465
  }
151
466
  async checkJoinConsumerGroupConditions() {
467
+ const logger = getEchoesLogger();
152
468
  const admin = this.kafka.admin();
153
469
  try {
154
470
  await admin.connect();
@@ -156,7 +472,7 @@ var KafkaManager = class {
156
472
  const groupDescriptions = await admin.describeGroups([groupId]);
157
473
  const group = groupDescriptions.groups[0];
158
474
  if (group.state === "Empty") {
159
- import_logger.logger.info(`Echoes: Consumer group ${groupId} is empty, joining`);
475
+ logger.info(`Echoes: Consumer group ${groupId} is empty, joining`);
160
476
  return true;
161
477
  }
162
478
  const topicsMetadata = await admin.fetchTopicMetadata({ topics: this.topics });
@@ -164,24 +480,24 @@ var KafkaManager = class {
164
480
  (acc, topic) => acc + topic.partitions.length,
165
481
  0
166
482
  );
167
- import_logger.logger.info(
483
+ logger.info(
168
484
  `Echoes: Consumer group ${groupId} has ${group.members.length} members and ${totalPartitions} partitions`
169
485
  );
170
486
  const partitionsRatio = this.options.membersToPartitionsRatio || DEFAULT_MEMBERS_TO_PARTITIONS_RATIO;
171
487
  const partitionsThreshold = Math.ceil(totalPartitions * partitionsRatio);
172
488
  if (partitionsThreshold > group.members.length) {
173
- import_logger.logger.info(
489
+ logger.info(
174
490
  `Echoes: Consumer group ${groupId} has room for more members ${group.members.length}/${partitionsThreshold}, joining`
175
491
  );
176
492
  return true;
177
493
  }
178
494
  return false;
179
495
  } catch (error) {
180
- import_logger.logger.error("Echoes: Error checking consumer group conditions, join", { error });
496
+ logger.error("Echoes: Error checking consumer group conditions, join", { error });
181
497
  return true;
182
498
  } finally {
183
499
  await admin.disconnect().catch((error) => {
184
- import_logger.logger.error("Echoes: Error disconnecting admin client", { error });
500
+ logger.error("Echoes: Error disconnecting admin client", { error });
185
501
  });
186
502
  }
187
503
  }
@@ -201,19 +517,20 @@ var KafkaManager = class {
201
517
  return false;
202
518
  }
203
519
  async handleMessage(params) {
520
+ const logger = getEchoesLogger();
204
521
  const subscription = this.subscriptions.get(params.topic);
205
522
  if (!subscription) {
206
- import_logger.logger.warn(`Echoes: Received a message for an unknown topic: ${params.topic}, ignoring it`);
523
+ logger.warn(`Echoes: Received a message for an unknown topic: ${params.topic}, ignoring it`);
207
524
  return;
208
525
  }
209
526
  let intervalsCount = 0;
210
527
  const heartbeatInterval = setInterval(async () => {
211
528
  await params.heartbeat().catch((error) => {
212
- import_logger.logger.warn(`Echoes: Error sending heartbeat: ${error.message}`);
529
+ logger.warn(`Echoes: Error sending heartbeat: ${error.message}`);
213
530
  });
214
531
  intervalsCount++;
215
532
  if (intervalsCount * HEARTBEAT_INTERVAL_SECONDS % 30 === 0) {
216
- import_logger.logger.warn(
533
+ logger.warn(
217
534
  `Echoes: Event is taking too long to process: ${params.topic} ${intervalsCount * HEARTBEAT_INTERVAL_SECONDS}s`
218
535
  );
219
536
  }
@@ -225,7 +542,7 @@ var KafkaManager = class {
225
542
  try {
226
543
  await this.handleRetries(subscription, params, error);
227
544
  } catch (retryError) {
228
- import_logger.logger.error("Echoes: error processing a message", {
545
+ logger.error("Echoes: error processing a message", {
229
546
  error: retryError,
230
547
  topic: params.topic
231
548
  });
@@ -238,6 +555,9 @@ var KafkaManager = class {
238
555
  createReceivedEvent(params) {
239
556
  var _a, _b, _c, _d;
240
557
  const { message, topic, partition } = params;
558
+ if (!message.value) {
559
+ throw new Error(`Echoes received an empty Kafka message for ${topic}`);
560
+ }
241
561
  const data = deserialize_default(message.value.toString());
242
562
  const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
243
563
  const timestamp = Number.parseInt(message.timestamp || "", 10);
@@ -254,6 +574,7 @@ var KafkaManager = class {
254
574
  }
255
575
  async handleRetries(subscription, params, error) {
256
576
  var _a, _b;
577
+ const logger = getEchoesLogger();
257
578
  const { message, topic } = params;
258
579
  const retries = Number.parseInt(((_b = (_a = message == null ? void 0 : message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
259
580
  if (subscription.attemptsBeforeDeadLetter === void 0 || subscription.attemptsBeforeDeadLetter === null) {
@@ -262,6 +583,7 @@ var KafkaManager = class {
262
583
  const maxRetries = subscription.attemptsBeforeDeadLetter || 0;
263
584
  const exceededMaxRetries = retries >= maxRetries;
264
585
  const nextTopic = exceededMaxRetries ? `DLQ-${topic}` : topic;
586
+ if (!message.value) throw error;
265
587
  await this.producer.send({
266
588
  topic: nextTopic,
267
589
  messages: [
@@ -276,19 +598,18 @@ var KafkaManager = class {
276
598
  ]
277
599
  });
278
600
  if (exceededMaxRetries) {
279
- import_logger.logger.error(
601
+ logger.error(
280
602
  "Echoes: a message has reached the maximum number of retries, sending it to DLQ",
281
603
  { topic: nextTopic }
282
604
  );
283
605
  } else {
284
- import_logger.logger.warn("Echoes: a retryable message failed", { error, topic: nextTopic });
606
+ logger.warn("Echoes: a retryable message failed", { error, topic: nextTopic });
285
607
  }
286
608
  }
287
609
  };
288
610
  var KafkaManager_default = KafkaManager;
289
611
 
290
612
  // src/events/EventBus.ts
291
- var import_logger2 = require("@orion-js/logger");
292
613
  var EventBus = class {
293
614
  echoes;
294
615
  transports;
@@ -348,9 +669,10 @@ var EventBus = class {
348
669
  await Promise.all(this.startedTransports.map((transport) => transport.close()));
349
670
  }
350
671
  async handleEvent(event) {
672
+ const logger = getEchoesLogger();
351
673
  const echo2 = this.echoes[event.topic];
352
674
  if (!echo2 || echo2.type !== "event") {
353
- import_logger2.logger.warn(`Echoes: Received a message for an unknown topic: ${event.topic}, ignoring it`);
675
+ logger.warn(`Echoes: Received a message for an unknown topic: ${event.topic}, ignoring it`);
354
676
  return;
355
677
  }
356
678
  if (event.transport === "kafka" && event.context) {
@@ -478,6 +800,14 @@ function createEventBus(options) {
478
800
  });
479
801
  }
480
802
 
803
+ // src/requestsHandler/checkSignature.ts
804
+ function checkSignature_default(body, signature) {
805
+ const generatedSignature = getSignature_default(body);
806
+ if (generatedSignature !== signature) {
807
+ throw new Error("Echoes invalid signature");
808
+ }
809
+ }
810
+
481
811
  // src/requestsHandler/getEcho.ts
482
812
  function getEcho_default(method) {
483
813
  const echo2 = config_default.echoes[method];
@@ -490,72 +820,29 @@ function getEcho_default(method) {
490
820
  return echo2;
491
821
  }
492
822
 
493
- // src/request/getSignature.ts
494
- var import_jssha = __toESM(require("jssha"), 1);
495
-
496
- // src/request/getPassword.ts
497
- var import_logger3 = require("@orion-js/logger");
498
- var import_env = require("@orion-js/env");
499
- function getEchoesPassword() {
500
- var _a, _b;
501
- const secret = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.key) || (0, import_env.internalGetEnv)("echoes_password", "ECHOES_PASSWORD");
502
- if (!secret) {
503
- import_logger3.logger.warn(
504
- 'Warning: no secret key found for echoes requests. Init echoes or set the env var "echoes_password" or process.env.ECHOES_PASSWORD'
505
- );
506
- }
507
- return secret;
508
- }
509
-
510
- // src/request/getSignature.ts
511
- function getSignature_default(body) {
512
- const password = getEchoesPassword();
513
- const shaObj = new import_jssha.default("SHA-1", "TEXT");
514
- shaObj.setHMACKey(password, "TEXT");
515
- shaObj.update(body);
516
- return shaObj.getHMAC("HEX");
517
- }
518
-
519
- // src/requestsHandler/checkSignature.ts
520
- function checkSignature_default(body, signature) {
521
- const generatedSignature = getSignature_default(body);
522
- if (generatedSignature !== signature) {
523
- throw new Error("Echoes invalid signature");
524
- }
525
- }
526
-
527
823
  // src/requestsHandler/index.ts
528
- var import_http = require("@orion-js/http");
529
- var requestsHandler_default = (options) => (0, import_http.route)({
824
+ var requestsHandler_default = (options) => ({
530
825
  method: "post",
531
826
  path: options.requests.handlerPath || "/echoes-services",
532
- bodyParser: "json",
533
- bodyParserOptions: {
534
- limit: "10mb"
535
- },
536
- async resolve(req) {
827
+ bodyLimit: "10mb",
828
+ async handle(requestBody) {
537
829
  try {
538
- const { body, signature } = req.body;
830
+ const { body, signature } = requestBody;
539
831
  checkSignature_default(body, signature);
540
832
  const { method, serializedParams } = body;
541
833
  const echo2 = getEcho_default(method);
542
834
  const result = await echo2.onRequest(serializedParams);
543
- return {
544
- body: {
545
- result: serialize_default(result)
546
- }
547
- };
835
+ return { result: serialize_default(result) };
548
836
  } catch (error) {
549
- if (!error.getInfo) {
550
- console.error("Error at echo requests handler:", error);
837
+ const caught = error;
838
+ if (!caught.getInfo) {
839
+ getEchoesLogger().error("Error at echo requests handler:", { error: caught });
551
840
  }
552
841
  return {
553
- body: {
554
- error: error.message,
555
- errorInfo: error.getInfo ? error.getInfo() : null,
556
- isValidationError: !!error.isValidationError,
557
- isUserError: !!error.isUserError
558
- }
842
+ error: caught.message,
843
+ errorInfo: caught.getInfo ? caught.getInfo() : null,
844
+ isValidationError: !!caught.isValidationError,
845
+ isUserError: !!caught.isUserError
559
846
  };
560
847
  }
561
848
  }
@@ -567,7 +854,13 @@ async function startService(options) {
567
854
  config_default.echoes = options.echoes;
568
855
  if (options.requests) {
569
856
  config_default.requests = options.requests;
570
- (0, import_http2.registerRoute)(requestsHandler_default(options));
857
+ const registerHandler = options.requests.registerHandler || getEchoesRuntime().registerRequestHandler;
858
+ if (!registerHandler) {
859
+ throw new Error(
860
+ "Echoes requests require requests.registerHandler in standalone servers. Orion applications can import @orion-js/echoes-orion once during startup."
861
+ );
862
+ }
863
+ await registerHandler(requestsHandler_default(options));
571
864
  }
572
865
  const nextEventBus = createEventBus(options);
573
866
  if (nextEventBus) {
@@ -578,115 +871,22 @@ async function startService(options) {
578
871
  }
579
872
  async function stopService() {
580
873
  if (eventBus) {
581
- console.info("Stoping echoes...");
874
+ const logger = getEchoesLogger();
875
+ logger.info("Stopping Echoes...");
582
876
  await eventBus.close();
583
877
  eventBus = null;
584
878
  config_default.eventBus = void 0;
585
- console.info("Echoes stopped");
586
- }
587
- }
588
-
589
- // src/publish/index.ts
590
- async function publish(options) {
591
- if (!config_default.eventBus) {
592
- throw new Error("You must initialize echoes configuration to use publish");
593
- }
594
- return await config_default.eventBus.publish(options);
595
- }
596
-
597
- // src/request/getURL.ts
598
- function getURL_default(serviceName) {
599
- var _a, _b;
600
- if (serviceName.startsWith("http")) return serviceName;
601
- const url = (_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.services[serviceName];
602
- if (!url) {
603
- throw new Error(`No URL found in echoes config for service ${serviceName}`);
604
- }
605
- return url;
606
- }
607
-
608
- // src/request/makeRequest.ts
609
- var import_axios = __toESM(require("axios"), 1);
610
- var import_helpers2 = require("@orion-js/helpers");
611
- var makeRequest = async (options) => {
612
- const result = await (0, import_helpers2.executeWithRetries)(
613
- async () => {
614
- return await (0, import_axios.default)({
615
- method: "post",
616
- url: options.url,
617
- timeout: options.timeout,
618
- headers: {
619
- "User-Agent": "Orionjs-Echoes/1.1"
620
- },
621
- data: options.data
622
- });
623
- },
624
- options.retries,
625
- 200
626
- );
627
- return {
628
- data: result.data,
629
- statusCode: result.status
630
- };
631
- };
632
-
633
- // src/request/index.ts
634
- var import_schema = require("@orion-js/schema");
635
- var import_helpers3 = require("@orion-js/helpers");
636
- async function request(options) {
637
- var _a, _b;
638
- const { method, service, params } = options;
639
- const serializedParams = serialize_default(params);
640
- const date = /* @__PURE__ */ new Date();
641
- const body = { method, service, serializedParams, date };
642
- const signature = getSignature_default(body);
643
- try {
644
- const requestMaker = ((_b = (_a = config_default) == null ? void 0 : _a.requests) == null ? void 0 : _b.makeRequest) || makeRequest;
645
- const requestOptions = {
646
- url: getURL_default(service),
647
- retries: options.retries,
648
- timeout: options.timeout,
649
- data: {
650
- body,
651
- signature
652
- }
653
- };
654
- const result = await requestMaker(requestOptions);
655
- if (result.statusCode !== 200) {
656
- throw new Error(`Wrong status code ${result.statusCode}`);
657
- }
658
- const data = result.data;
659
- if (data.error) {
660
- const info = data.errorInfo;
661
- if (info) {
662
- if (data.isValidationError) {
663
- throw new import_schema.ValidationError(info.validationErrors);
664
- }
665
- if (data.isUserError) {
666
- throw new import_helpers3.UserError(info.error, info.message, info.extra);
667
- }
668
- }
669
- throw new Error(`${data.error}`);
670
- }
671
- const response = deserialize_default(data.result);
672
- return response;
673
- } catch (error) {
674
- if (error.isOrionError) throw error;
675
- throw new Error(`Echoes request network error calling ${service}/${method}: ${error.message}`);
879
+ logger.info("Echoes stopped");
676
880
  }
677
881
  }
678
882
 
679
- // src/service/index.ts
680
- var import_logger4 = require("@orion-js/logger");
681
-
682
883
  // src/echo/index.ts
683
- var import_schema2 = require("@orion-js/schema");
684
884
  var echo = function createNewEcho(options) {
685
885
  const resolve = async (params, context) => {
686
- const cleaned = options.params ? await (0, import_schema2.cleanAndValidate)(options.params, params) : params ?? {};
886
+ const cleaned = options.params ? await parseEchoesSchema(options.params, params) : params ?? {};
687
887
  const result = await options.resolve(cleaned, context);
688
888
  if (options.returns) {
689
- return await (0, import_schema2.clean)(options.returns, result);
889
+ return await cleanEchoesSchema(options.returns, result);
690
890
  }
691
891
  return result;
692
892
  };
@@ -713,6 +913,9 @@ var echo = function createNewEcho(options) {
713
913
  onMessage: async (messageData) => {
714
914
  var _a, _b, _c, _d;
715
915
  const { message } = messageData;
916
+ if (!message.value) {
917
+ throw new Error(`Echoes received an empty Kafka message for ${messageData.topic}`);
918
+ }
716
919
  const data = deserialize_default(message.value.toString());
717
920
  const retries = Number.parseInt(((_b = (_a = message.headers) == null ? void 0 : _a.retries) == null ? void 0 : _b.toString()) || "0", 10);
718
921
  const timestamp = Number(message.timestamp);
@@ -743,15 +946,21 @@ function createEchoEvent(options) {
743
946
  return echo({ ...options, type: "event" });
744
947
  }
745
948
 
949
+ // src/schema.ts
950
+ function typedEchoesSchema(schema) {
951
+ return schema;
952
+ }
953
+
746
954
  // src/service/index.ts
747
- var import_services = require("@orion-js/services");
748
955
  var serviceMetadata = /* @__PURE__ */ new WeakMap();
749
956
  var echoesMetadata = /* @__PURE__ */ new WeakMap();
750
957
  var echoEntriesByClass = /* @__PURE__ */ new Map();
958
+ var standaloneInstances = /* @__PURE__ */ new WeakMap();
751
959
  var pendingEchoEntries = {};
752
960
  function Echoes() {
753
961
  return (target, context) => {
754
- (0, import_services.Service)()(target, context);
962
+ var _a, _b;
963
+ (_b = (_a = getEchoesRuntime()).decorateService) == null ? void 0 : _b.call(_a, target, context);
755
964
  serviceMetadata.set(target, { _serviceType: "echoes" });
756
965
  if (Object.keys(pendingEchoEntries).length > 0) {
757
966
  echoEntriesByClass.set(target, pendingEchoEntries);
@@ -768,7 +977,7 @@ function EchoEvent(options = {}) {
768
977
  return createEchoEvent({
769
978
  ...options,
770
979
  resolve: async (params, contextData) => {
771
- return await (0, import_logger4.runWithOrionAsyncContext)(
980
+ return await runWithEchoesContext(
772
981
  {
773
982
  controllerType: "echo",
774
983
  echoName: propertyKey,
@@ -797,7 +1006,7 @@ function EchoRequest(options = {}) {
797
1006
  return createEchoRequest({
798
1007
  ...options,
799
1008
  resolve: async (params, contextData) => {
800
- return await (0, import_logger4.runWithOrionAsyncContext)(
1009
+ return await runWithEchoesContext(
801
1010
  {
802
1011
  controllerType: "echo",
803
1012
  echoName: propertyKey,
@@ -827,7 +1036,18 @@ function initializeEchoesIfNeeded(instance) {
827
1036
  echoesMetadata.set(instance, echoes);
828
1037
  }
829
1038
  function getServiceEchoes(target) {
830
- const instance = (0, import_services.getInstance)(target);
1039
+ let instance;
1040
+ if (typeof target !== "function") {
1041
+ instance = target;
1042
+ } else if (getEchoesRuntime().getInstance) {
1043
+ instance = getEchoesRuntime().getInstance(target);
1044
+ } else {
1045
+ instance = standaloneInstances.get(target);
1046
+ if (!instance) {
1047
+ instance = new target();
1048
+ standaloneInstances.set(target, instance);
1049
+ }
1050
+ }
831
1051
  if (!serviceMetadata.has(instance.constructor)) {
832
1052
  throw new Error("You must pass a class decorated with @Echoes to getServiceEchoes");
833
1053
  }
@@ -844,13 +1064,25 @@ function getServiceEchoes(target) {
844
1064
  EchoEvent,
845
1065
  EchoRequest,
846
1066
  Echoes,
1067
+ EchoesUserError,
1068
+ EchoesValidationError,
1069
+ cleanEchoesSchema,
1070
+ configureEchoesRuntime,
847
1071
  createEchoEvent,
848
1072
  createEchoRequest,
1073
+ createEchoesUserError,
1074
+ createEchoesValidationError,
849
1075
  echo,
1076
+ getEchoesContext,
1077
+ getEchoesLogger,
1078
+ getEchoesRuntime,
850
1079
  getServiceEchoes,
1080
+ parseEchoesSchema,
851
1081
  publish,
852
1082
  request,
1083
+ runWithEchoesContext,
853
1084
  startService,
854
- stopService
1085
+ stopService,
1086
+ typedEchoesSchema
855
1087
  });
856
1088
  //# sourceMappingURL=index.cjs.map