@usequark/quark-core 2.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +63 -0
  2. package/package.json +80 -0
  3. package/src/admin-auth.js +121 -0
  4. package/src/admin.js +352 -0
  5. package/src/admin.test.js +257 -0
  6. package/src/auth/index.js +160 -0
  7. package/src/auth/middleware.js +106 -0
  8. package/src/auth/middleware.test.js +116 -0
  9. package/src/auth/password.js +9 -0
  10. package/src/auth.test.js +235 -0
  11. package/src/authorization.js +235 -0
  12. package/src/authorization.test.js +314 -0
  13. package/src/cache.js +137 -0
  14. package/src/cache.test.js +217 -0
  15. package/src/core.js +32 -0
  16. package/src/csrf.js +122 -0
  17. package/src/csrf.test.js +169 -0
  18. package/src/db-instrumentation.js +281 -0
  19. package/src/db-instrumentation.test.js +284 -0
  20. package/src/db-metrics.js +26 -0
  21. package/src/db.js +218 -0
  22. package/src/db.test.js +365 -0
  23. package/src/email-templates.js +134 -0
  24. package/src/email-templates.test.js +124 -0
  25. package/src/email.js +314 -0
  26. package/src/email.test.js +498 -0
  27. package/src/error-reporter.js +266 -0
  28. package/src/error-reporter.test.js +236 -0
  29. package/src/errors.js +192 -0
  30. package/src/errors.test.js +128 -0
  31. package/src/exports.test.js +154 -0
  32. package/src/file-validation.js +249 -0
  33. package/src/file-validation.test.js +207 -0
  34. package/src/index.js +58 -0
  35. package/src/locale.js +21 -0
  36. package/src/logger.js +182 -0
  37. package/src/logger.test.js +287 -0
  38. package/src/mail.js +50 -0
  39. package/src/metrics.js +379 -0
  40. package/src/metrics.test.js +380 -0
  41. package/src/multipart.js +144 -0
  42. package/src/multipart.test.js +188 -0
  43. package/src/pagination.js +139 -0
  44. package/src/pagination.test.js +210 -0
  45. package/src/query-builder.js +183 -0
  46. package/src/query-builder.test.js +234 -0
  47. package/src/queue/index.js +446 -0
  48. package/src/queue/queue.test.js +365 -0
  49. package/src/rate-limiter.js +253 -0
  50. package/src/rate-limiter.test.js +130 -0
  51. package/src/redis.js +154 -0
  52. package/src/redis.test.js +150 -0
  53. package/src/request-logger.js +211 -0
  54. package/src/request-logger.test.js +286 -0
  55. package/src/sms.js +208 -0
  56. package/src/storage-s3.js +236 -0
  57. package/src/storage.js +214 -0
  58. package/src/storage.test.js +457 -0
  59. package/src/stripe.js +89 -0
  60. package/src/testing/factories.js +89 -0
  61. package/src/testing/helpers.js +266 -0
  62. package/src/testing/index.js +46 -0
  63. package/src/testing/mocks.js +480 -0
  64. package/src/testing/testing.test.js +543 -0
  65. package/src/types.js +74 -0
  66. package/src/utils.js +219 -0
  67. package/src/utils.test.js +193 -0
  68. package/src/validation.js +26 -0
@@ -0,0 +1,266 @@
1
+ /**
2
+ * @techstream/quark-core - Error Reporter Module
3
+ * Adapter/hook system for pluggable error reporting
4
+ */
5
+
6
+ import { normalizeError } from "./errors.js";
7
+ import { logger } from "./logger.js";
8
+
9
+ const MAX_BREADCRUMBS = 50;
10
+
11
+ /**
12
+ * @typedef {Object} ErrorAdapter
13
+ * @property {string} name - Unique adapter identifier
14
+ * @property {(error: Error, context: Object) => void} report - Called when an error is reported
15
+ * @property {(message: string, level: string, context: Object) => void} [captureMessage] - For non-error events
16
+ * @property {(user: Object) => void} [setUser] - Sets user context
17
+ * @property {(message: string, category: string, data: Object) => void} [addBreadcrumb] - Adds a breadcrumb
18
+ */
19
+
20
+ /**
21
+ * @typedef {Object} Breadcrumb
22
+ * @property {string} message - Breadcrumb message
23
+ * @property {string} category - Breadcrumb category
24
+ * @property {Object} data - Associated data
25
+ * @property {string} timestamp - ISO timestamp
26
+ */
27
+
28
+ /**
29
+ * Built-in adapter that logs errors via the Quark logger.
30
+ * @type {ErrorAdapter}
31
+ */
32
+ export const consoleAdapter = {
33
+ name: "console",
34
+
35
+ report(error, context) {
36
+ const appError = normalizeError(error);
37
+ logger.error(appError.message, {
38
+ error: appError.toJSON(),
39
+ context,
40
+ stack: error.stack,
41
+ });
42
+ },
43
+
44
+ captureMessage(message, level = "info", context = {}) {
45
+ const logFn = logger[level] || logger.info;
46
+ logFn.call(logger, message, context);
47
+ },
48
+
49
+ setUser(_user) {
50
+ // Console adapter does not persist user context
51
+ },
52
+
53
+ addBreadcrumb(_message, _category, _data) {
54
+ // Console adapter does not store breadcrumbs
55
+ },
56
+ };
57
+
58
+ /**
59
+ * Error reporter with adapter registration.
60
+ * Distributes error reports to all registered adapters.
61
+ */
62
+ export class ErrorReporter {
63
+ /** @type {ErrorAdapter[]} */
64
+ #adapters = [];
65
+
66
+ /** @type {Breadcrumb[]} */
67
+ #breadcrumbs = [];
68
+
69
+ /** @type {Object|null} */
70
+ #user = null;
71
+
72
+ constructor() {
73
+ this.#adapters = [consoleAdapter];
74
+ this.#breadcrumbs = [];
75
+ this.#user = null;
76
+ }
77
+
78
+ /**
79
+ * Register an adapter
80
+ * @param {ErrorAdapter} adapter - The adapter to register
81
+ * @returns {void}
82
+ */
83
+ use(adapter) {
84
+ if (
85
+ !adapter ||
86
+ typeof adapter.name !== "string" ||
87
+ typeof adapter.report !== "function"
88
+ ) {
89
+ throw new Error(
90
+ "Adapter must have a name (string) and a report (function)",
91
+ );
92
+ }
93
+ this.#adapters.push(adapter);
94
+ }
95
+
96
+ /**
97
+ * Report an error to all registered adapters
98
+ * @param {Error} error - The error to report
99
+ * @param {Object} [context={}] - Additional context
100
+ * @returns {void}
101
+ */
102
+ report(error, context = {}) {
103
+ const enrichedContext = {
104
+ ...context,
105
+ ...(this.#user ? { user: this.#user } : {}),
106
+ breadcrumbs: [...this.#breadcrumbs],
107
+ };
108
+
109
+ for (const adapter of this.#adapters) {
110
+ try {
111
+ adapter.report(error, enrichedContext);
112
+ } catch (_adapterError) {
113
+ // Swallow adapter errors to avoid cascading failures
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Capture a non-error message event
120
+ * @param {string} message - The message to capture
121
+ * @param {string} [level="info"] - Log level
122
+ * @param {Object} [context={}] - Additional context
123
+ * @returns {void}
124
+ */
125
+ captureMessage(message, level = "info", context = {}) {
126
+ const enrichedContext = {
127
+ ...context,
128
+ ...(this.#user ? { user: this.#user } : {}),
129
+ };
130
+
131
+ for (const adapter of this.#adapters) {
132
+ try {
133
+ if (typeof adapter.captureMessage === "function") {
134
+ adapter.captureMessage(message, level, enrichedContext);
135
+ }
136
+ } catch (_adapterError) {
137
+ // Swallow adapter errors
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Set user context for subsequent reports
144
+ * @param {Object} user - User information
145
+ * @returns {void}
146
+ */
147
+ setUser(user) {
148
+ this.#user = user;
149
+
150
+ for (const adapter of this.#adapters) {
151
+ try {
152
+ if (typeof adapter.setUser === "function") {
153
+ adapter.setUser(user);
154
+ }
155
+ } catch (_adapterError) {
156
+ // Swallow adapter errors
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Add a breadcrumb to the trail (circular buffer, max 50)
163
+ * @param {string} message - Breadcrumb message
164
+ * @param {string} [category="default"] - Breadcrumb category
165
+ * @param {Object} [data={}] - Associated data
166
+ * @returns {void}
167
+ */
168
+ addBreadcrumb(message, category = "default", data = {}) {
169
+ /** @type {Breadcrumb} */
170
+ const breadcrumb = {
171
+ message,
172
+ category,
173
+ data,
174
+ timestamp: new Date().toISOString(),
175
+ };
176
+
177
+ if (this.#breadcrumbs.length >= MAX_BREADCRUMBS) {
178
+ this.#breadcrumbs.shift();
179
+ }
180
+ this.#breadcrumbs.push(breadcrumb);
181
+
182
+ for (const adapter of this.#adapters) {
183
+ try {
184
+ if (typeof adapter.addBreadcrumb === "function") {
185
+ adapter.addBreadcrumb(message, category, data);
186
+ }
187
+ } catch (_adapterError) {
188
+ // Swallow adapter errors
189
+ }
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Returns the list of registered adapters (for inspection/testing)
195
+ * @returns {ErrorAdapter[]}
196
+ */
197
+ get adapters() {
198
+ return [...this.#adapters];
199
+ }
200
+
201
+ /**
202
+ * Returns the current breadcrumbs (for inspection/testing)
203
+ * @returns {Breadcrumb[]}
204
+ */
205
+ get breadcrumbs() {
206
+ return [...this.#breadcrumbs];
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Creates a Sentry adapter factory (stub/example).
212
+ * Downstream projects can use this as a template to integrate Sentry.
213
+ *
214
+ * @param {Object} _options - Sentry configuration options
215
+ * @returns {ErrorAdapter} A Sentry adapter (stubbed)
216
+ *
217
+ * @example
218
+ * ```js
219
+ * import * as Sentry from "@sentry/node";
220
+ * import { errorReporter, createSentryAdapter } from "@techstream/quark-core";
221
+ *
222
+ * Sentry.init({ dsn: "https://example@sentry.io/123" });
223
+ * errorReporter.use(createSentryAdapter({ Sentry }));
224
+ * ```
225
+ */
226
+ export const createSentryAdapter = (_options = {}) => {
227
+ return {
228
+ name: "sentry",
229
+
230
+ report(_error, _context) {
231
+ // Example Sentry integration:
232
+ // const { Sentry } = options;
233
+ // Sentry.withScope((scope) => {
234
+ // if (context.user) scope.setUser(context.user);
235
+ // if (context.breadcrumbs) {
236
+ // for (const b of context.breadcrumbs) {
237
+ // Sentry.addBreadcrumb({ message: b.message, category: b.category, data: b.data });
238
+ // }
239
+ // }
240
+ // scope.setExtras(context);
241
+ // Sentry.captureException(error);
242
+ // });
243
+ },
244
+
245
+ captureMessage(_message, _level, _context) {
246
+ // Example:
247
+ // const { Sentry } = options;
248
+ // Sentry.captureMessage(message, level);
249
+ },
250
+
251
+ setUser(_user) {
252
+ // Example:
253
+ // const { Sentry } = options;
254
+ // Sentry.setUser(user);
255
+ },
256
+
257
+ addBreadcrumb(_message, _category, _data) {
258
+ // Example:
259
+ // const { Sentry } = options;
260
+ // Sentry.addBreadcrumb({ message, category, data });
261
+ },
262
+ };
263
+ };
264
+
265
+ /** Global singleton error reporter instance */
266
+ export const errorReporter = new ErrorReporter();
@@ -0,0 +1,236 @@
1
+ import assert from "node:assert/strict";
2
+ import { beforeEach, describe, it } from "node:test";
3
+ import {
4
+ createSentryAdapter,
5
+ ErrorReporter,
6
+ errorReporter,
7
+ } from "./error-reporter.js";
8
+
9
+ describe("ErrorReporter", () => {
10
+ /** @type {ErrorReporter} */
11
+ let reporter;
12
+
13
+ beforeEach(() => {
14
+ reporter = new ErrorReporter();
15
+ });
16
+
17
+ describe("default instance", () => {
18
+ it("has console adapter registered by default", () => {
19
+ assert.equal(reporter.adapters.length, 1);
20
+ assert.equal(reporter.adapters[0].name, "console");
21
+ });
22
+
23
+ it("global singleton has console adapter", () => {
24
+ const names = errorReporter.adapters.map((a) => a.name);
25
+ assert.ok(names.includes("console"));
26
+ });
27
+ });
28
+
29
+ describe("use()", () => {
30
+ it("adds an adapter", () => {
31
+ const adapter = { name: "test", report: () => {} };
32
+ reporter.use(adapter);
33
+ assert.equal(reporter.adapters.length, 2);
34
+ assert.equal(reporter.adapters[1].name, "test");
35
+ });
36
+
37
+ it("throws if adapter has no name", () => {
38
+ assert.throws(() => reporter.use({ report: () => {} }), /name/);
39
+ });
40
+
41
+ it("throws if adapter has no report function", () => {
42
+ assert.throws(() => reporter.use({ name: "bad" }), /report/);
43
+ });
44
+ });
45
+
46
+ describe("report()", () => {
47
+ it("calls all registered adapters", () => {
48
+ const calls = [];
49
+ const adapter1 = {
50
+ name: "a1",
51
+ report: (err, ctx) => calls.push({ adapter: "a1", err, ctx }),
52
+ };
53
+ const adapter2 = {
54
+ name: "a2",
55
+ report: (err, ctx) => calls.push({ adapter: "a2", err, ctx }),
56
+ };
57
+
58
+ reporter.use(adapter1);
59
+ reporter.use(adapter2);
60
+
61
+ const error = new Error("test error");
62
+ reporter.report(error, { requestId: "123" });
63
+
64
+ // console adapter + 2 custom = 3 calls
65
+ assert.equal(calls.length, 2);
66
+ assert.equal(calls[0].adapter, "a1");
67
+ assert.equal(calls[1].adapter, "a2");
68
+ assert.equal(calls[0].err, error);
69
+ assert.equal(calls[0].ctx.requestId, "123");
70
+ });
71
+
72
+ it("swallows adapter errors without throwing", () => {
73
+ const failAdapter = {
74
+ name: "fail",
75
+ report: () => {
76
+ throw new Error("adapter crash");
77
+ },
78
+ };
79
+ reporter.use(failAdapter);
80
+
81
+ assert.doesNotThrow(() => reporter.report(new Error("test")));
82
+ });
83
+ });
84
+
85
+ describe("captureMessage()", () => {
86
+ it("calls adapters that support captureMessage", () => {
87
+ const messages = [];
88
+ const adapter = {
89
+ name: "msg",
90
+ report: () => {},
91
+ captureMessage: (msg, level, ctx) => messages.push({ msg, level, ctx }),
92
+ };
93
+ reporter.use(adapter);
94
+
95
+ reporter.captureMessage("hello", "warn", { extra: true });
96
+
97
+ assert.equal(messages.length, 1);
98
+ assert.equal(messages[0].msg, "hello");
99
+ assert.equal(messages[0].level, "warn");
100
+ assert.equal(messages[0].ctx.extra, true);
101
+ });
102
+
103
+ it("skips adapters without captureMessage", () => {
104
+ const adapter = { name: "no-msg", report: () => {} };
105
+ reporter.use(adapter);
106
+
107
+ assert.doesNotThrow(() => reporter.captureMessage("hello"));
108
+ });
109
+ });
110
+
111
+ describe("setUser()", () => {
112
+ it("includes user in subsequent reports", () => {
113
+ const reported = [];
114
+ const adapter = {
115
+ name: "user-test",
116
+ report: (_err, ctx) => reported.push(ctx),
117
+ };
118
+ reporter.use(adapter);
119
+
120
+ reporter.setUser({ id: "u1", email: "a@b.com" });
121
+ reporter.report(new Error("test"));
122
+
123
+ assert.equal(reported.length, 1);
124
+ assert.deepEqual(reported[0].user, { id: "u1", email: "a@b.com" });
125
+ });
126
+
127
+ it("forwards user to adapters with setUser", () => {
128
+ let receivedUser = null;
129
+ const adapter = {
130
+ name: "user-fwd",
131
+ report: () => {},
132
+ setUser: (u) => {
133
+ receivedUser = u;
134
+ },
135
+ };
136
+ reporter.use(adapter);
137
+
138
+ reporter.setUser({ id: "u2" });
139
+ assert.deepEqual(receivedUser, { id: "u2" });
140
+ });
141
+ });
142
+
143
+ describe("addBreadcrumb()", () => {
144
+ it("stores breadcrumbs and attaches them to report context", () => {
145
+ const reported = [];
146
+ const adapter = {
147
+ name: "bc-test",
148
+ report: (_err, ctx) => reported.push(ctx),
149
+ };
150
+ reporter.use(adapter);
151
+
152
+ reporter.addBreadcrumb("clicked button", "ui", { buttonId: "submit" });
153
+ reporter.addBreadcrumb("navigated", "navigation");
154
+ reporter.report(new Error("test"));
155
+
156
+ assert.equal(reported.length, 1);
157
+ assert.equal(reported[0].breadcrumbs.length, 2);
158
+ assert.equal(reported[0].breadcrumbs[0].message, "clicked button");
159
+ assert.equal(reported[0].breadcrumbs[0].category, "ui");
160
+ assert.deepEqual(reported[0].breadcrumbs[0].data, { buttonId: "submit" });
161
+ assert.equal(reported[0].breadcrumbs[1].message, "navigated");
162
+ assert.equal(reported[0].breadcrumbs[1].category, "navigation");
163
+ assert.ok(reported[0].breadcrumbs[0].timestamp);
164
+ });
165
+
166
+ it("circular buffer keeps max 50 breadcrumbs", () => {
167
+ for (let i = 0; i < 60; i++) {
168
+ reporter.addBreadcrumb(`crumb-${i}`, "test");
169
+ }
170
+
171
+ const crumbs = reporter.breadcrumbs;
172
+ assert.equal(crumbs.length, 50);
173
+ // First 10 should have been evicted, first remaining is crumb-10
174
+ assert.equal(crumbs[0].message, "crumb-10");
175
+ assert.equal(crumbs[49].message, "crumb-59");
176
+ });
177
+
178
+ it("forwards breadcrumbs to adapters that support it", () => {
179
+ const received = [];
180
+ const adapter = {
181
+ name: "bc-fwd",
182
+ report: () => {},
183
+ addBreadcrumb: (msg, cat, data) => received.push({ msg, cat, data }),
184
+ };
185
+ reporter.use(adapter);
186
+
187
+ reporter.addBreadcrumb("test", "cat", { key: "val" });
188
+
189
+ assert.equal(received.length, 1);
190
+ assert.equal(received[0].msg, "test");
191
+ assert.equal(received[0].cat, "cat");
192
+ assert.deepEqual(received[0].data, { key: "val" });
193
+ });
194
+ });
195
+
196
+ describe("custom adapter receives correct data", () => {
197
+ it("passes error and enriched context", () => {
198
+ const reported = [];
199
+ const adapter = {
200
+ name: "custom",
201
+ report: (err, ctx) => reported.push({ err, ctx }),
202
+ };
203
+ reporter.use(adapter);
204
+
205
+ reporter.setUser({ id: "u1" });
206
+ reporter.addBreadcrumb("init", "app");
207
+
208
+ const error = new Error("custom test");
209
+ reporter.report(error, { route: "/api/test" });
210
+
211
+ assert.equal(reported.length, 1);
212
+ assert.equal(reported[0].err, error);
213
+ assert.equal(reported[0].ctx.route, "/api/test");
214
+ assert.deepEqual(reported[0].ctx.user, { id: "u1" });
215
+ assert.equal(reported[0].ctx.breadcrumbs.length, 1);
216
+ assert.equal(reported[0].ctx.breadcrumbs[0].message, "init");
217
+ });
218
+ });
219
+
220
+ describe("createSentryAdapter()", () => {
221
+ it("returns a valid adapter shape", () => {
222
+ const adapter = createSentryAdapter();
223
+ assert.equal(adapter.name, "sentry");
224
+ assert.equal(typeof adapter.report, "function");
225
+ assert.equal(typeof adapter.captureMessage, "function");
226
+ assert.equal(typeof adapter.setUser, "function");
227
+ assert.equal(typeof adapter.addBreadcrumb, "function");
228
+ });
229
+
230
+ it("can be registered without errors", () => {
231
+ const adapter = createSentryAdapter();
232
+ assert.doesNotThrow(() => reporter.use(adapter));
233
+ assert.doesNotThrow(() => reporter.report(new Error("test")));
234
+ });
235
+ });
236
+ });
package/src/errors.js ADDED
@@ -0,0 +1,192 @@
1
+ /**
2
+ * @techstream/quark-core - Error Handling Module
3
+ * Standardized error types and utilities for consistent error handling
4
+ */
5
+
6
+ import { errorReporter } from "./error-reporter.js";
7
+
8
+ /**
9
+ * Base application error class
10
+ */
11
+ export class AppError extends Error {
12
+ constructor(message, statusCode = 500, code = "INTERNAL_ERROR") {
13
+ super(message);
14
+ this.name = this.constructor.name;
15
+ this.statusCode = statusCode;
16
+ this.code = code;
17
+ this.timestamp = new Date().toISOString();
18
+ Error.captureStackTrace(this, this.constructor);
19
+ }
20
+
21
+ toJSON() {
22
+ return {
23
+ name: this.name,
24
+ message: this.message,
25
+ code: this.code,
26
+ statusCode: this.statusCode,
27
+ timestamp: this.timestamp,
28
+ };
29
+ }
30
+ }
31
+
32
+ /**
33
+ * 400 Bad Request Error
34
+ */
35
+ export class ValidationError extends AppError {
36
+ constructor(message, details = null) {
37
+ super(message, 400, "VALIDATION_ERROR");
38
+ this.details = details;
39
+ }
40
+
41
+ toJSON() {
42
+ return {
43
+ ...super.toJSON(),
44
+ details: this.details,
45
+ };
46
+ }
47
+ }
48
+
49
+ /**
50
+ * 401 Unauthorized Error
51
+ */
52
+ export class UnauthorizedError extends AppError {
53
+ constructor(message = "Authentication required") {
54
+ super(message, 401, "UNAUTHORIZED");
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 403 Forbidden Error
60
+ */
61
+ export class ForbiddenError extends AppError {
62
+ constructor(message = "Access denied") {
63
+ super(message, 403, "FORBIDDEN");
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 404 Not Found Error
69
+ */
70
+ export class NotFoundError extends AppError {
71
+ constructor(message = "Resource not found") {
72
+ super(message, 404, "NOT_FOUND");
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 409 Conflict Error
78
+ */
79
+ export class ConflictError extends AppError {
80
+ constructor(message = "Resource conflict") {
81
+ super(message, 409, "CONFLICT");
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 429 Rate Limit Error
87
+ */
88
+ export class RateLimitError extends AppError {
89
+ constructor(message = "Too many requests", retryAfter = 60) {
90
+ super(message, 429, "RATE_LIMIT");
91
+ this.retryAfter = retryAfter;
92
+ }
93
+
94
+ toJSON() {
95
+ return {
96
+ ...super.toJSON(),
97
+ retryAfter: this.retryAfter,
98
+ };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Database Error
104
+ */
105
+ export class DatabaseError extends AppError {
106
+ constructor(message = "Database error", originalError = null) {
107
+ super(message, 500, "DATABASE_ERROR");
108
+ this.originalError = originalError;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * External Service Error
114
+ */
115
+ export class ServiceError extends AppError {
116
+ constructor(serviceName, message = "Service error", statusCode = 502) {
117
+ super(message, statusCode, "SERVICE_ERROR");
118
+ this.serviceName = serviceName;
119
+ }
120
+
121
+ toJSON() {
122
+ return {
123
+ ...super.toJSON(),
124
+ serviceName: this.serviceName,
125
+ };
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Safely extracts error message from various error types
131
+ * @param {Error|string|Object} error - The error to extract from
132
+ * @returns {string} Extracted error message
133
+ */
134
+ export const getErrorMessage = (error) => {
135
+ if (typeof error === "string") return error;
136
+ if (error instanceof Error) return error.message;
137
+ if (error && typeof error === "object" && error.message) return error.message;
138
+ return "An unknown error occurred";
139
+ };
140
+
141
+ /**
142
+ * Safely extracts status code from error
143
+ * @param {Error|AppError} error - The error to extract from
144
+ * @returns {number} HTTP status code
145
+ */
146
+ export const getStatusCode = (error) => {
147
+ if (error instanceof AppError) return error.statusCode;
148
+ if (error.statusCode) return error.statusCode;
149
+ return 500;
150
+ };
151
+
152
+ /**
153
+ * Converts any error to an AppError instance
154
+ * @param {Error|AppError|any} error - The error to normalize
155
+ * @returns {AppError} Normalized error
156
+ */
157
+ export const normalizeError = (error) => {
158
+ if (error instanceof AppError) return error;
159
+ if (error instanceof Error) {
160
+ return new AppError(error.message, 500, "INTERNAL_ERROR");
161
+ }
162
+ return new AppError(
163
+ typeof error === "string" ? error : "An unknown error occurred",
164
+ 500,
165
+ "INTERNAL_ERROR",
166
+ );
167
+ };
168
+
169
+ /**
170
+ * Logs error with context
171
+ * @param {Error} error - The error to log
172
+ * @param {Object} context - Additional context
173
+ */
174
+ export const logError = (error, context = {}) => {
175
+ errorReporter.report(error, context);
176
+ };
177
+
178
+ /**
179
+ * Wraps async function and handles errors consistently
180
+ * @param {Function} fn - Async function to wrap
181
+ * @returns {Function} Wrapped function
182
+ */
183
+ export const withErrorHandling = (fn) => {
184
+ return async (...args) => {
185
+ try {
186
+ return await fn(...args);
187
+ } catch (error) {
188
+ errorReporter.report(error);
189
+ throw normalizeError(error);
190
+ }
191
+ };
192
+ };