@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,207 @@
1
+ import assert from "node:assert";
2
+ import { test } from "node:test";
3
+ import {
4
+ detectMimeType,
5
+ isTypeAllowed,
6
+ validateFile,
7
+ } from "../src/file-validation.js";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // detectMimeType
11
+ // ---------------------------------------------------------------------------
12
+
13
+ test("FileValidation - detectMimeType identifies JPEG", () => {
14
+ const buf = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0, 0]);
15
+ assert.strictEqual(detectMimeType(buf), "image/jpeg");
16
+ });
17
+
18
+ test("FileValidation - detectMimeType identifies PNG", () => {
19
+ const buf = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
20
+ assert.strictEqual(detectMimeType(buf), "image/png");
21
+ });
22
+
23
+ test("FileValidation - detectMimeType identifies GIF", () => {
24
+ const buf = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61]);
25
+ assert.strictEqual(detectMimeType(buf), "image/gif");
26
+ });
27
+
28
+ test("FileValidation - detectMimeType identifies WebP", () => {
29
+ const buf = Buffer.alloc(16);
30
+ // RIFF....WEBP
31
+ buf.writeUInt8(0x52, 0); // R
32
+ buf.writeUInt8(0x49, 1); // I
33
+ buf.writeUInt8(0x46, 2); // F
34
+ buf.writeUInt8(0x46, 3); // F
35
+ buf.writeUInt8(0x57, 8); // W
36
+ buf.writeUInt8(0x45, 9); // E
37
+ buf.writeUInt8(0x42, 10); // B
38
+ buf.writeUInt8(0x50, 11); // P
39
+ assert.strictEqual(detectMimeType(buf), "image/webp");
40
+ });
41
+
42
+ test("FileValidation - detectMimeType identifies PDF", () => {
43
+ const buf = Buffer.from("%PDF-1.5", "ascii");
44
+ assert.strictEqual(detectMimeType(buf), "application/pdf");
45
+ });
46
+
47
+ test("FileValidation - detectMimeType identifies SVG", () => {
48
+ const buf = Buffer.from('<svg xmlns="http://www.w3.org/2000/svg"></svg>');
49
+ assert.strictEqual(detectMimeType(buf), "image/svg+xml");
50
+ });
51
+
52
+ test("FileValidation - detectMimeType identifies SVG with XML declaration", () => {
53
+ const buf = Buffer.from('<?xml version="1.0"?><svg></svg>');
54
+ assert.strictEqual(detectMimeType(buf), "image/svg+xml");
55
+ });
56
+
57
+ test("FileValidation - detectMimeType identifies ZIP", () => {
58
+ const buf = Buffer.from([0x50, 0x4b, 0x03, 0x04, 0, 0]);
59
+ assert.strictEqual(detectMimeType(buf), "application/zip");
60
+ });
61
+
62
+ test("FileValidation - detectMimeType identifies WebM", () => {
63
+ const buf = Buffer.from([0x1a, 0x45, 0xdf, 0xa3, 0, 0]);
64
+ assert.strictEqual(detectMimeType(buf), "video/webm");
65
+ });
66
+
67
+ test("FileValidation - detectMimeType returns null for unknown bytes", () => {
68
+ const buf = Buffer.from([0x00, 0x01, 0x02, 0x03]);
69
+ assert.strictEqual(detectMimeType(buf), null);
70
+ });
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // isTypeAllowed
74
+ // ---------------------------------------------------------------------------
75
+
76
+ test("FileValidation - isTypeAllowed exact match", () => {
77
+ assert.strictEqual(
78
+ isTypeAllowed("image/jpeg", ["image/jpeg", "image/png"]),
79
+ true,
80
+ );
81
+ assert.strictEqual(
82
+ isTypeAllowed("image/gif", ["image/jpeg", "image/png"]),
83
+ false,
84
+ );
85
+ });
86
+
87
+ test("FileValidation - isTypeAllowed wildcard category match", () => {
88
+ assert.strictEqual(isTypeAllowed("image/jpeg", ["image/*"]), true);
89
+ assert.strictEqual(isTypeAllowed("image/png", ["image/*"]), true);
90
+ assert.strictEqual(isTypeAllowed("application/pdf", ["image/*"]), false);
91
+ });
92
+
93
+ test("FileValidation - isTypeAllowed universal wildcard", () => {
94
+ assert.strictEqual(isTypeAllowed("video/mp4", ["*/*"]), true);
95
+ });
96
+
97
+ test("FileValidation - isTypeAllowed is case insensitive", () => {
98
+ assert.strictEqual(isTypeAllowed("Image/JPEG", ["image/jpeg"]), true);
99
+ });
100
+
101
+ test("FileValidation - isTypeAllowed returns false for null/empty mime", () => {
102
+ assert.strictEqual(isTypeAllowed(null, ["image/jpeg"]), false);
103
+ assert.strictEqual(isTypeAllowed("", ["image/jpeg"]), false);
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // validateFile
108
+ // ---------------------------------------------------------------------------
109
+
110
+ test("FileValidation - validateFile accepts valid file", () => {
111
+ const pngMagic = Buffer.from([
112
+ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
113
+ ]);
114
+ const result = validateFile(
115
+ {
116
+ filename: "test.png",
117
+ mimeType: "image/png",
118
+ size: 1024,
119
+ buffer: pngMagic,
120
+ },
121
+ { allowedTypes: ["image/*"] },
122
+ );
123
+ assert.strictEqual(result.valid, true);
124
+ assert.strictEqual(result.detectedType, "image/png");
125
+ });
126
+
127
+ test("FileValidation - validateFile rejects oversized file", () => {
128
+ const result = validateFile(
129
+ { filename: "big.jpg", mimeType: "image/jpeg", size: 20 * 1024 * 1024 },
130
+ { maxSize: 10 * 1024 * 1024 },
131
+ );
132
+ assert.strictEqual(result.valid, false);
133
+ assert.match(result.error, /exceeds maximum size/);
134
+ });
135
+
136
+ test("FileValidation - validateFile rejects disallowed MIME type", () => {
137
+ const result = validateFile(
138
+ { filename: "virus.exe", mimeType: "application/x-msdownload", size: 100 },
139
+ { allowedTypes: ["image/*"] },
140
+ );
141
+ assert.strictEqual(result.valid, false);
142
+ assert.match(result.error, /not allowed/);
143
+ });
144
+
145
+ test("FileValidation - validateFile detects MIME spoofing", () => {
146
+ // Claim it's an image/jpeg but magic bytes say PDF
147
+ const pdfMagic = Buffer.from("%PDF-1.5", "ascii");
148
+ const result = validateFile(
149
+ {
150
+ filename: "fake.jpg",
151
+ mimeType: "image/jpeg",
152
+ size: 100,
153
+ buffer: pdfMagic,
154
+ },
155
+ { allowedTypes: ["image/jpeg", "application/pdf"] },
156
+ );
157
+ assert.strictEqual(result.valid, false);
158
+ assert.match(result.error, /does not match declared type/);
159
+ assert.strictEqual(result.detectedType, "application/pdf");
160
+ });
161
+
162
+ test("FileValidation - validateFile skips magic bytes when disabled", () => {
163
+ const pdfMagic = Buffer.from("%PDF-1.5", "ascii");
164
+ const result = validateFile(
165
+ {
166
+ filename: "file.jpg",
167
+ mimeType: "image/jpeg",
168
+ size: 100,
169
+ buffer: pdfMagic,
170
+ },
171
+ { allowedTypes: ["image/jpeg"], verifyMagicBytes: false },
172
+ );
173
+ assert.strictEqual(result.valid, true);
174
+ });
175
+
176
+ test("FileValidation - validateFile passes when magic bytes unknown", () => {
177
+ // Unknown magic bytes should not block the file
178
+ const buf = Buffer.from([0xde, 0xad, 0xbe, 0xef]);
179
+ const result = validateFile(
180
+ {
181
+ filename: "data.bin",
182
+ mimeType: "application/pdf",
183
+ size: 100,
184
+ buffer: buf,
185
+ },
186
+ { allowedTypes: ["application/pdf"] },
187
+ );
188
+ assert.strictEqual(result.valid, true);
189
+ });
190
+
191
+ test("FileValidation - validateFile works without buffer", () => {
192
+ const result = validateFile(
193
+ { filename: "file.png", mimeType: "image/png", size: 100 },
194
+ { allowedTypes: ["image/*"] },
195
+ );
196
+ assert.strictEqual(result.valid, true);
197
+ });
198
+
199
+ test("FileValidation - validateFile uses env defaults gracefully", () => {
200
+ // No options → uses module defaults
201
+ const result = validateFile({
202
+ filename: "test.jpg",
203
+ mimeType: "image/jpeg",
204
+ size: 1024,
205
+ });
206
+ assert.strictEqual(result.valid, true);
207
+ });
package/src/index.js ADDED
@@ -0,0 +1,58 @@
1
+ // Authorization exports
2
+
3
+ // Admin exports
4
+ export * from "./admin.js";
5
+ // Admin auth exports
6
+ export * from "./admin-auth.js";
7
+ // Auth exports
8
+ export * from "./auth/index.js";
9
+ export * from "./authorization.js";
10
+ // Cache exports
11
+ export * from "./cache.js";
12
+ // CSRF protection exports
13
+ export * from "./csrf.js";
14
+ // Database exports
15
+ export * from "./db.js";
16
+ // Database instrumentation exports
17
+ export * from "./db-instrumentation.js";
18
+ // Email service exports
19
+ export * from "./email.js";
20
+ // Email template exports
21
+ export * from "./email-templates.js";
22
+ // Error Reporter exports
23
+ export * from "./error-reporter.js";
24
+ // Error exports
25
+ export * from "./errors.js";
26
+ // File validation exports
27
+ export * from "./file-validation.js";
28
+ // Logger exports
29
+ export * from "./logger.js";
30
+ // Mail exports
31
+ export * from "./mail.js";
32
+ // Metrics exports
33
+ export * from "./metrics.js";
34
+ // Multipart parsing exports
35
+ export * from "./multipart.js";
36
+ // Pagination exports
37
+ export * from "./pagination.js";
38
+ // Query builder exports
39
+ export * from "./query-builder.js";
40
+ // Queue exports
41
+ export * from "./queue/index.js";
42
+ // Rate limiting exports
43
+ export * from "./rate-limiter.js";
44
+ // Redis exports
45
+ export * from "./redis.js";
46
+ // Request logger exports
47
+ export * from "./request-logger.js";
48
+ // SMS service exports
49
+ export * from "./sms.js";
50
+ // Storage exports
51
+ export * from "./storage.js";
52
+
53
+ // Utility exports
54
+ export * from "./utils.js";
55
+ export * from "./validation.js";
56
+
57
+ // Testing utilities (import from "@techstream/quark-core/testing" in test files)
58
+ // Not re-exported from main to avoid polluting production imports
package/src/locale.js ADDED
@@ -0,0 +1,21 @@
1
+ const DEFAULT_LOCALE_FALLBACK = "en";
2
+ const SUPPORTED_LOCALES_FALLBACK = ["en"];
3
+
4
+ export function getDefaultLocale() {
5
+ const env = process.env.DEFAULT_LOCALE;
6
+ return env?.trim() ? env.trim() : DEFAULT_LOCALE_FALLBACK;
7
+ }
8
+
9
+ export function getSupportedLocales() {
10
+ const env = process.env.SUPPORTED_LOCALES;
11
+ if (!env?.trim()) return SUPPORTED_LOCALES_FALLBACK;
12
+ return env
13
+ .split(",")
14
+ .map((l) => l.trim())
15
+ .filter(Boolean);
16
+ }
17
+
18
+ export function isLocaleSupported(locale) {
19
+ if (!locale || typeof locale !== "string") return false;
20
+ return getSupportedLocales().includes(locale.trim());
21
+ }
package/src/logger.js ADDED
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @techstream/quark-core - Logger Module
3
+ * Lightweight structured logger with zero external dependencies
4
+ */
5
+
6
+ /** @enum {number} */
7
+ const LOG_LEVELS = {
8
+ debug: 10,
9
+ info: 20,
10
+ warn: 30,
11
+ error: 40,
12
+ fatal: 50,
13
+ };
14
+
15
+ const LEVEL_NAMES = /** @type {const} */ ({
16
+ 10: "debug",
17
+ 20: "info",
18
+ 30: "warn",
19
+ 40: "error",
20
+ 50: "fatal",
21
+ });
22
+
23
+ /** @type {Record<string, string>} */
24
+ const LEVEL_COLORS = {
25
+ debug: "\x1b[36m", // cyan
26
+ info: "\x1b[32m", // green
27
+ warn: "\x1b[33m", // yellow
28
+ error: "\x1b[31m", // red
29
+ fatal: "\x1b[35m", // magenta
30
+ };
31
+
32
+ const RESET = "\x1b[0m";
33
+ const DIM = "\x1b[2m";
34
+
35
+ const isProduction = () => process.env.NODE_ENV === "production";
36
+
37
+ /**
38
+ * Resolves the minimum log level from options and environment
39
+ * @param {string} [optionLevel] - Level from options
40
+ * @returns {number}
41
+ */
42
+ const resolveLevel = (optionLevel) => {
43
+ const level =
44
+ optionLevel || process.env.LOG_LEVEL || (isProduction() ? "info" : "debug");
45
+ return LOG_LEVELS[level] ?? LOG_LEVELS.info;
46
+ };
47
+
48
+ /**
49
+ * Formats a log entry for dev (colorized, human-readable)
50
+ * @param {object} entry - The log entry
51
+ * @returns {string}
52
+ */
53
+ const formatDev = (entry) => {
54
+ const { timestamp, level, name, msg, ...rest } = entry;
55
+ const color = LEVEL_COLORS[level] || "";
56
+ const time = timestamp.slice(11, 23); // HH:mm:ss.SSS
57
+ const contextStr =
58
+ Object.keys(rest).length > 0
59
+ ? ` ${DIM}${JSON.stringify(rest)}${RESET}`
60
+ : "";
61
+ return `${DIM}${time}${RESET} ${color}${level.toUpperCase().padEnd(5)}${RESET} ${DIM}[${name}]${RESET} ${msg}${contextStr}`;
62
+ };
63
+
64
+ /**
65
+ * Formats a log entry for production (single-line JSON)
66
+ * @param {object} entry - The log entry
67
+ * @returns {string}
68
+ */
69
+ const formatProd = (entry) => {
70
+ return JSON.stringify(entry);
71
+ };
72
+
73
+ /**
74
+ * Returns the console method for a given level
75
+ * @param {string} level - The log level name
76
+ * @returns {Function}
77
+ */
78
+ const getTransport = (level) => {
79
+ if (level === "error" || level === "fatal") return console.error;
80
+ if (level === "warn") return console.warn;
81
+ return console.log;
82
+ };
83
+
84
+ /**
85
+ * @typedef {Object} LoggerOptions
86
+ * @property {string} [name] - Logger name (e.g. "web", "worker", "db")
87
+ * @property {string} [level] - Minimum log level (default: "info" in production, "debug" in dev)
88
+ * @property {Object} [context] - Default context merged into every log entry
89
+ */
90
+
91
+ /**
92
+ * @typedef {Object} Logger
93
+ * @property {(msg: string, data?: Object) => void} debug - Log at debug level
94
+ * @property {(msg: string, data?: Object) => void} info - Log at info level
95
+ * @property {(msg: string, data?: Object) => void} warn - Log at warn level
96
+ * @property {(msg: string, data?: Object) => void} error - Log at error level
97
+ * @property {(msg: string, data?: Object) => void} fatal - Log at fatal level
98
+ * @property {(context: Object) => Logger} child - Create a child logger with merged context
99
+ */
100
+
101
+ /**
102
+ * Creates a structured logger instance
103
+ * @param {LoggerOptions} [options] - Logger configuration
104
+ * @returns {Logger}
105
+ */
106
+ export const createLogger = (options = {}) => {
107
+ const {
108
+ name = "app",
109
+ level: optionLevel,
110
+ context: defaultContext = {},
111
+ } = options;
112
+ const minLevel = resolveLevel(optionLevel);
113
+ const format = isProduction() ? formatProd : formatDev;
114
+
115
+ /**
116
+ * Writes a log entry if the level is at or above the minimum
117
+ * @param {string} levelName - The level name
118
+ * @param {string} msg - The log message
119
+ * @param {Object} [data] - Additional data
120
+ */
121
+ const write = (levelName, msg, data = {}) => {
122
+ const numeric = LOG_LEVELS[levelName];
123
+ if (numeric < minLevel) return;
124
+
125
+ const entry = {
126
+ timestamp: new Date().toISOString(),
127
+ level: levelName,
128
+ name,
129
+ msg,
130
+ ...defaultContext,
131
+ ...data,
132
+ };
133
+
134
+ const transport = getTransport(levelName);
135
+ transport(format(entry));
136
+ };
137
+
138
+ return {
139
+ debug: (msg, data) => write("debug", msg, data),
140
+ info: (msg, data) => write("info", msg, data),
141
+ warn: (msg, data) => write("warn", msg, data),
142
+ error: (msg, data) => write("error", msg, data),
143
+ fatal: (msg, data) => write("fatal", msg, data),
144
+
145
+ /**
146
+ * Creates a child logger with merged context
147
+ * @param {Object} childContext - Additional context for the child logger
148
+ * @returns {Logger}
149
+ */
150
+ child: (childContext = {}) => {
151
+ return createLogger({
152
+ name,
153
+ level: LEVEL_NAMES[minLevel],
154
+ context: { ...defaultContext, ...childContext },
155
+ });
156
+ },
157
+ };
158
+ };
159
+
160
+ /**
161
+ * Creates a request logger middleware helper.
162
+ * Returns a function that accepts a request object and produces
163
+ * a child logger enriched with request-specific fields.
164
+ * @param {Logger} parentLogger - The parent logger instance
165
+ * @returns {(request: { method?: string, url?: string, headers?: Object }) => Logger}
166
+ */
167
+ export const requestLogger = (parentLogger) => {
168
+ return (request) => {
169
+ const requestId = request.headers?.["x-request-id"] || crypto.randomUUID();
170
+ const method = request.method || "UNKNOWN";
171
+ const path = request.url
172
+ ? new URL(request.url, "http://localhost").pathname
173
+ : "/";
174
+
175
+ const child = parentLogger.child({ requestId, method, path });
176
+ child.info("request started");
177
+ return child;
178
+ };
179
+ };
180
+
181
+ /** Default logger instance */
182
+ export const logger = createLogger({ name: "quark" });