@techstream/quark-core 1.1.0 → 1.4.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/README.md CHANGED
@@ -1,419 +1,41 @@
1
- # @techstream/quark-core - The Quark Platform Core
1
+ # @techstream/quark-core
2
2
 
3
- The central "plumbing" package for the Quark platform. Every Quark application inherits this core infrastructure, providing database access, authentication, job queuing, and error handling out of the box.
3
+ Shared infrastructure for the Quark platform authentication, job queues, error handling, and utilities.
4
4
 
5
- ## Philosophy
5
+ ## What's Included
6
6
 
7
- `@techstream/quark-core` provides **opinionated, zero-configuration defaults** for common infrastructure concerns:
8
-
9
- - **Database**: Prisma ORM with singleton pattern
10
- - **Authentication**: Next-auth helpers with sensible defaults
11
- - **Job Queue**: BullMQ integration for background jobs
12
- - **Error Handling**: Standardized, serializable error types
13
- - **Utilities**: Common functions for retries, validation, etc.
14
-
15
- Applications **inherit these tools** but can **extend and override** them as needed.
16
-
17
- ## Core Principles
18
-
19
- ### What Goes in Core
20
-
21
- ✅ **Belongs in Core:**
22
- - Infrastructure-level utilities (DB, Auth, Queue, Errors)
23
- - Patterns that every app needs
24
- - Zero-configuration defaults
25
- - Type definitions
26
- - Provider-agnostic helpers
27
-
28
- ❌ **Does NOT belong in Core:**
29
- - Application-specific business logic
30
- - Domain models (use Prisma schema instead)
31
- - UI components (use @techstream/quark-ui)
32
- - Config-specific settings (use environment variables)
33
-
34
- ### What Gets "Ejected"
35
-
36
- The "ejection" pattern allows apps to customize Core defaults:
37
-
38
- ```javascript
39
- // Import core helpers
40
- import { createAuthConfig, createQueue } from "@techstream/quark-core";
41
-
42
- // Override/extend with app-specific config
43
- export const authConfig = createAuthConfig({
44
- providers: [GitHubProvider(...)],
45
- callbacks: {
46
- async jwt({ token, user }) {
47
- // Custom JWT logic
48
- return token;
49
- }
50
- }
51
- });
52
- ```
7
+ - **Authentication** — Next-auth helpers (`createAuthConfig`, `requireAuth`, password hashing)
8
+ - **Job Queue** — BullMQ integration (`createQueue`, `createWorker`, `addJob`)
9
+ - **Errors** Standardized error types (`ValidationError`, `NotFoundError`, `UnauthorizedError`, etc.)
10
+ - **Utilities** `retryAsync`, `deepMerge`, `randomString`, `sanitizeId`, `measureTime`, `memoize`
11
+ - **Validation** Zod-based request body validation
12
+ - **Redis / Mailhog** Connection helpers for Redis and Mailhog
53
13
 
54
14
  ## Usage
55
15
 
56
- ### Database Client
57
-
58
- Access the Prisma singleton client:
59
-
60
- ```javascript
61
- import { createDbClient } from "@techstream/quark-core";
62
-
63
- const db = createDbClient();
64
-
65
- const user = await db.user.findUnique({
66
- where: { id: "123" }
67
- });
68
- ```
69
-
70
- In development, the client automatically attaches to `globalThis` to prevent hot-reload issues.
71
-
72
- ### Authentication
73
-
74
- Create a next-auth configuration with defaults:
75
-
76
- ```javascript
77
- import { createAuthConfig, getCurrentSession, getUserId } from "@techstream/quark-core";
78
-
79
- export const authConfig = createAuthConfig({
80
- providers: [GitHubProvider({ ... })],
81
- });
82
-
83
- // In a server action/API route:
84
- import { getSession } from "next-auth/react";
85
-
86
- const session = await getCurrentSession(getSession);
87
- if (!session) {
88
- throw new UnauthorizedError();
89
- }
90
-
91
- const userId = getUserId(session);
92
- ```
93
-
94
- #### Available Auth Functions
95
-
96
- - `createAuthConfig(options)` - Creates next-auth config
97
- - `getCurrentSession(getSession)` - Safely retrieves session
98
- - `isAuthenticated(session)` - Checks if session is valid
99
- - `getUserId(session)` - Extracts user ID
100
- - `getUserEmail(session)` - Extracts user email
101
- - `requireAuth(session)` - Throws error if not authenticated
102
-
103
- ### Job Queue
104
-
105
- Initialize background job processing:
106
-
107
- ```javascript
108
- import { createQueue, createWorker, addJob } from "@techstream/quark-core";
109
-
110
- // Create a queue
111
- const emailQueue = createQueue("emails", {
112
- redis: {
113
- host: process.env.REDIS_HOST,
114
- port: process.env.REDIS_PORT,
115
- },
116
- defaultJobOptions: {
117
- attempts: 3,
118
- backoff: {
119
- type: "exponential",
120
- delay: 2000,
121
- },
122
- }
123
- });
124
-
125
- // Process jobs
126
- createWorker("emails", async (job) => {
127
- await sendEmail(job.data);
128
- return { success: true };
129
- });
130
-
131
- // Add a job
132
- await addJob(emailQueue, "send-email", {
133
- to: "user@example.com",
134
- subject: "Welcome!",
135
- });
136
- ```
137
-
138
- #### Available Queue Functions
139
-
140
- - `createQueue(name, options)` - Creates a BullMQ queue
141
- - `createWorker(queueName, handler, options)` - Creates a job processor
142
- - `addJob(queue, jobName, data, jobOptions)` - Adds a job to queue
143
- - `getJobStatus(job)` - Gets job status/progress
144
- - `clearQueue(queue)` - Clears all jobs from queue
145
- - `closeAllQueues()` - Gracefully closes all queues
146
- - `checkQueueHealth()` - Checks Redis connectivity
147
-
148
- ### Error Handling
149
-
150
- Use standardized error types:
151
-
152
16
  ```javascript
153
17
  import {
18
+ createAuthConfig,
19
+ requireAuth,
20
+ createQueue,
21
+ createWorker,
22
+ addJob,
154
23
  ValidationError,
155
- UnauthorizedError,
156
- NotFoundError,
157
- AppError,
158
- logError,
159
- normalizeError,
160
- } from "@techstream/quark-core";
161
-
162
- // Throw specific errors
163
- if (!email) {
164
- throw new ValidationError("Email is required");
165
- }
166
-
167
- if (!session) {
168
- throw new UnauthorizedError("Please log in");
169
- }
170
-
171
- if (!user) {
172
- throw new NotFoundError("User not found");
173
- }
174
-
175
- // Error types automatically serialize to JSON:
176
- // {
177
- // "name": "ValidationError",
178
- // "message": "Email is required",
179
- // "code": "VALIDATION_ERROR",
180
- // "statusCode": 400,
181
- // "timestamp": "2026-02-04T10:00:00.000Z"
182
- // }
183
-
184
- // Handle errors with context
185
- try {
186
- await someOperation();
187
- } catch (error) {
188
- logError(error, { userId: "123", context: "email_signup" });
189
- }
190
- ```
191
-
192
- #### Available Error Types
193
-
194
- - `AppError` - Base class (500)
195
- - `ValidationError` - Bad input (400)
196
- - `UnauthorizedError` - Not authenticated (401)
197
- - `ForbiddenError` - Not authorized (403)
198
- - `NotFoundError` - Resource missing (404)
199
- - `ConflictError` - Resource conflict (409)
200
- - `RateLimitError` - Too many requests (429)
201
- - `DatabaseError` - Database issue (500)
202
- - `ServiceError` - External service failure (502)
203
-
204
- ### Utilities
205
-
206
- Common helper functions:
207
-
208
- ```javascript
209
- import {
210
24
  retryAsync,
211
- validateEnv,
212
- deepMerge,
213
- randomString,
214
- sanitizeId,
215
- measureTime,
216
- memoize,
217
25
  } from "@techstream/quark-core";
218
-
219
- // Retry with exponential backoff
220
- const result = await retryAsync(
221
- () => fetchFromExternalAPI(),
222
- {
223
- maxAttempts: 5,
224
- initialDelay: 1000,
225
- onRetry: ({ attempt, delay }) => {
226
- console.log(`Retry ${attempt} after ${delay}ms`);
227
- }
228
- }
229
- );
230
-
231
- // Validate environment
232
- validateEnv(["DATABASE_URL", "REDIS_HOST"]);
233
-
234
- // Merge configurations
235
- const config = deepMerge(defaults, userConfig);
236
-
237
- // Generate IDs
238
- const id = randomString(12);
239
-
240
- // Sanitize for URLs
241
- const slug = sanitizeId("Hello World"); // "hello-world"
242
-
243
- // Measure performance
244
- const { result, duration } = await measureTime(async () => {
245
- return await expensiveOperation();
246
- });
247
- console.log(`Completed in ${duration}ms`);
248
-
249
- // Cache results
250
- const getCachedUser = memoize(
251
- (id) => db.user.findUnique({ where: { id } }),
252
- 60000 // 1 minute TTL
253
- );
254
- ```
255
-
256
- ## Environment Configuration
257
-
258
- Core respects these environment variables:
259
-
260
- ```bash
261
- # Database
262
- DATABASE_URL=postgresql://...
263
-
264
- # Redis/Queue
265
- REDIS_HOST=localhost
266
- REDIS_PORT=6379
267
- REDIS_DB=0
268
-
269
- # Application URL
270
- APP_URL=http://localhost:3000
271
-
272
- # Authentication
273
- NEXTAUTH_SECRET=your-secret-key
274
-
275
- # Node
276
- NODE_ENV=development
277
26
  ```
278
27
 
279
- ## Testing Core
28
+ All modules are re-exported from the package root. See JSDoc comments on each function for options and usage details.
280
29
 
281
- Core includes unit tests verifying all functionality:
30
+ ## Testing
282
31
 
283
32
  ```bash
284
33
  cd packages/core
285
34
  pnpm test
286
35
  ```
287
36
 
288
- See [Core Tests](./test/) for examples.
289
-
290
- ## Extending Core
291
-
292
- ### Custom Error Types
293
-
294
- ```javascript
295
- import { AppError } from "@techstream/quark-core";
296
-
297
- export class PaymentError extends AppError {
298
- constructor(message, code = "PAYMENT_FAILED") {
299
- super(message, 402, code);
300
- }
301
- }
302
- ```
303
-
304
- ### Custom Queue Handlers
305
-
306
- ```javascript
307
- import { createWorker } from "@techstream/quark-core";
308
-
309
- createWorker("analytics", async (job) => {
310
- await trackEvent(job.data);
311
- }, {
312
- concurrency: 10,
313
- redis: {
314
- host: "redis-prod.internal",
315
- port: 6379,
316
- }
317
- });
318
- ```
319
-
320
- ### Middleware & Hooks
321
-
322
- ```javascript
323
- import { createDbClient } from "@techstream/quark-core";
324
-
325
- const db = createDbClient({
326
- middleware: [
327
- {
328
- $use: async (params, next) => {
329
- const before = Date.now();
330
- const result = await next(params);
331
- const after = Date.now();
332
- console.log(`${params.model}.${params.action} took ${after - before}ms`);
333
- return result;
334
- },
335
- },
336
- ],
337
- });
338
- ```
339
-
340
- ## Architecture
341
-
342
- ```
343
- @techstream/quark-core/
344
- ├── src/
345
- │ ├── index.js # Main exports
346
- │ ├── auth/
347
- │ │ ├── index.js # Next-auth helpers
348
- │ │ └── password.js # Password hashing (bcryptjs)
349
- │ ├── queue/
350
- │ │ └── index.js # BullMQ integration
351
- │ ├── errors.js # Error types & utilities
352
- │ ├── redis.js # Redis URL & config helpers
353
- │ ├── mailhog.js # Mailhog SMTP/UI config helpers
354
- │ ├── validation.js # Zod request body validation
355
- │ ├── utils.js # Common helpers
356
- │ ├── types.js # JSDoc type definitions
357
- │ ├── auth.test.js # Auth tests
358
- │ ├── errors.test.js # Error tests
359
- │ └── utils.test.js # Utils tests
360
- └── package.json
361
- ```
362
-
363
- ## Migration Path from Existing Apps
364
-
365
- If you're migrating from an existing app:
366
-
367
- 1. **Keep your current setup** - Core is not required
368
- 2. **Extract shared patterns** - Move common code to Core
369
- 3. **Test independently** - Ensure Core works standalone
370
- 4. **Adopt gradually** - Start using Core utilities piece by piece
371
- 5. **Eject where needed** - Override defaults in your app
372
-
373
- Example migration:
374
-
375
- ```javascript
376
- // Before: app-specific auth.js
377
- export const authConfig = { ... };
378
-
379
- // After: inherit and extend from Core
380
- import { createAuthConfig } from "@techstream/quark-core";
381
-
382
- export const authConfig = createAuthConfig({
383
- providers: [...], // add app-specific providers
384
- });
385
- ```
386
-
387
- ## Troubleshooting
388
-
389
- ### Prisma Client Issues
390
-
391
- **"Cannot find Prisma Client"**
392
- - Ensure `@prisma/client` is installed: `pnpm install`
393
- - Run `pnpm db:generate` to build Prisma client
394
-
395
- ### Queue Connection Issues
396
-
397
- **"Redis connection refused"**
398
- - Check REDIS_HOST and REDIS_PORT
399
- - Ensure Redis is running: `docker-compose up redis`
400
-
401
- ### Auth Issues
402
-
403
- **"NEXTAUTH_SECRET is not set"**
404
- - Set in production: `export NEXTAUTH_SECRET=<random-string>`
405
- - Or pass via `createAuthConfig({ secret: '...' })`
406
-
407
- ## Contributing
408
-
409
- When adding features to Core:
410
-
411
- 1. ✅ **Add comprehensive JSDoc comments**
412
- 2. ✅ **Create unit tests** in `*.test.js`
413
- 3. ✅ **Update this README**
414
- 4. ✅ **Keep it framework-agnostic** where possible
415
- 5. ✅ **Document configuration options**
416
-
417
- ## License
37
+ ## Support
418
38
 
419
- ISC - Part of the Quark Platform
39
+ For issues, questions, and discussions:
40
+ - 🐛 [Issue Tracker](https://github.com/Bobnoddle/quark/issues)
41
+ - 💬 [Discussions](https://github.com/Bobnoddle/quark/discussions)
package/package.json CHANGED
@@ -1,29 +1,34 @@
1
1
  {
2
- "name": "@techstream/quark-core",
3
- "version": "1.1.0",
4
- "type": "module",
5
- "main": "src/index.js",
6
- "exports": {
7
- ".": "./src/index.js",
8
- "./testing": "./src/testing/index.js"
9
- },
10
- "dependencies": {
11
- "bcryptjs": "^3.0.3",
12
- "bullmq": "^5.67.3",
13
- "ioredis": "^5.9.3",
14
- "next-auth": "5.0.0-beta.30",
15
- "nodemailer": "^6.10.1",
16
- "zod": "^4.3.6"
17
- },
18
- "devDependencies": {
19
- "@types/node": "^24.10.12"
20
- },
21
- "scripts": {
22
- "test": "node --test $(find src -name '*.test.js')",
23
- "lint": "biome format --write && biome check --write"
24
- },
25
- "publishConfig": {
26
- "registry": "https://registry.npmjs.org",
27
- "access": "public"
28
- }
29
- }
2
+ "name": "@techstream/quark-core",
3
+ "version": "1.4.0",
4
+ "type": "module",
5
+ "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./testing": "./src/testing/index.js"
9
+ },
10
+ "dependencies": {
11
+ "bcryptjs": "^3.0.3",
12
+ "bullmq": "^5.67.3",
13
+ "ioredis": "^5.9.3",
14
+ "next-auth": "5.0.0-beta.30",
15
+ "nodemailer": "^6.10.1",
16
+ "zod": "^4.3.6"
17
+ },
18
+ "devDependencies": {
19
+ "@types/node": "^24.10.12"
20
+ },
21
+ "files": [
22
+ "src",
23
+ "README.md"
24
+ ],
25
+ "publishConfig": {
26
+ "registry": "https://registry.npmjs.org",
27
+ "access": "public"
28
+ },
29
+ "license": "ISC",
30
+ "scripts": {
31
+ "test": "node --test $(find src -name '*.test.js')",
32
+ "lint": "biome format --write && biome check --write"
33
+ }
34
+ }
package/src/auth/index.js CHANGED
@@ -27,9 +27,7 @@ export const createAuthConfig = (options = {}) => {
27
27
  } = options;
28
28
 
29
29
  if (!secret) {
30
- throw new Error(
31
- "NEXTAUTH_SECRET must be set (env var or options.secret)",
32
- );
30
+ throw new Error("NEXTAUTH_SECRET must be set (env var or options.secret)");
33
31
  }
34
32
 
35
33
  return {
package/src/auth.test.js CHANGED
@@ -23,9 +23,7 @@ test("Auth Module", async (t) => {
23
23
  try {
24
24
  assert.throws(
25
25
  () => createAuthConfig(),
26
- (err) =>
27
- err instanceof Error &&
28
- /NEXTAUTH_SECRET/.test(err.message),
26
+ (err) => err instanceof Error && /NEXTAUTH_SECRET/.test(err.message),
29
27
  );
30
28
  } finally {
31
29
  if (orig !== undefined) process.env.NEXTAUTH_SECRET = orig;
package/src/cache.test.js CHANGED
@@ -31,7 +31,7 @@ function createMockRedis() {
31
31
  expires.delete(key);
32
32
  },
33
33
 
34
- async scan(cursor, ...args) {
34
+ async scan(_cursor, ...args) {
35
35
  // Simple mock: return all matching keys in one batch
36
36
  const matchIdx = args.indexOf("MATCH");
37
37
  const pattern = matchIdx !== -1 ? args[matchIdx + 1] : "*";
@@ -0,0 +1,134 @@
1
+ /**
2
+ * @techstream/quark-core - Email Templates
3
+ *
4
+ * Reusable HTML email templates with plain-text fallbacks.
5
+ * Each template function returns { subject, html, text }.
6
+ */
7
+
8
+ /**
9
+ * Escape HTML entities to prevent XSS in user-provided values
10
+ * @param {string} str
11
+ * @returns {string}
12
+ */
13
+ function escapeHtml(str) {
14
+ return String(str)
15
+ .replace(/&/g, "&amp;")
16
+ .replace(/</g, "&lt;")
17
+ .replace(/>/g, "&gt;")
18
+ .replace(/"/g, "&quot;")
19
+ .replace(/'/g, "&#039;");
20
+ }
21
+
22
+ /**
23
+ * Shared outer layout for all emails
24
+ * @param {string} body - Inner HTML content
25
+ * @returns {string}
26
+ */
27
+ function layout(body) {
28
+ return `<!DOCTYPE html>
29
+ <html lang="en">
30
+ <head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
31
+ <body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#f4f4f5">
32
+ <table width="100%" cellpadding="0" cellspacing="0" style="padding:32px 16px">
33
+ <tr><td align="center">
34
+ <table width="100%" style="max-width:560px;background:#fff;border-radius:8px;border:1px solid #e4e4e7;padding:32px">
35
+ <tr><td>${body}</td></tr>
36
+ </table>
37
+ <p style="color:#a1a1aa;font-size:12px;margin-top:24px">
38
+ You received this email because an account was created with this address.
39
+ </p>
40
+ </td></tr>
41
+ </table>
42
+ </body>
43
+ </html>`;
44
+ }
45
+
46
+ /**
47
+ * Welcome email — sent after user registration
48
+ *
49
+ * @param {{ name?: string, appName?: string, loginUrl?: string }} data
50
+ * @returns {{ subject: string, html: string, text: string }}
51
+ */
52
+ export function welcomeEmail(data = {}) {
53
+ const name = data.name ? escapeHtml(data.name) : "there";
54
+ const appName = data.appName || "Quark";
55
+ const loginUrl = data.loginUrl || "";
56
+
57
+ const buttonHtml = loginUrl
58
+ ? `<p style="text-align:center;margin:24px 0">
59
+ <a href="${escapeHtml(loginUrl)}" style="display:inline-block;padding:12px 24px;background:#18181b;color:#fff;text-decoration:none;border-radius:6px;font-weight:500">
60
+ Sign in to ${escapeHtml(appName)}
61
+ </a>
62
+ </p>`
63
+ : "";
64
+
65
+ const html = layout(`
66
+ <h1 style="margin:0 0 16px;font-size:24px;color:#18181b">Welcome, ${name}!</h1>
67
+ <p style="color:#3f3f46;line-height:1.6;margin:0 0 16px">
68
+ Your account has been created successfully. You can now sign in and start using ${escapeHtml(appName)}.
69
+ </p>
70
+ ${buttonHtml}
71
+ <p style="color:#71717a;font-size:14px;margin:0">
72
+ If you didn't create this account, you can safely ignore this email.
73
+ </p>
74
+ `);
75
+
76
+ const text = [
77
+ `Welcome, ${data.name || "there"}!`,
78
+ "",
79
+ `Your account has been created successfully. You can now sign in and start using ${appName}.`,
80
+ ...(loginUrl ? ["", `Sign in: ${loginUrl}`] : []),
81
+ "",
82
+ "If you didn't create this account, you can safely ignore this email.",
83
+ ].join("\n");
84
+
85
+ return { subject: `Welcome to ${appName}!`, html, text };
86
+ }
87
+
88
+ /**
89
+ * Password reset email — sent when user requests a reset
90
+ *
91
+ * @param {{ name?: string, resetUrl: string, appName?: string, expiresIn?: string }} data
92
+ * @returns {{ subject: string, html: string, text: string }}
93
+ */
94
+ export function passwordResetEmail(data) {
95
+ if (!data?.resetUrl) {
96
+ throw new Error("resetUrl is required for password reset email");
97
+ }
98
+
99
+ const name = data.name ? escapeHtml(data.name) : "there";
100
+ const appName = data.appName || "Quark";
101
+ const expiresIn = data.expiresIn || "1 hour";
102
+
103
+ const html = layout(`
104
+ <h1 style="margin:0 0 16px;font-size:24px;color:#18181b">Reset your password</h1>
105
+ <p style="color:#3f3f46;line-height:1.6;margin:0 0 16px">
106
+ Hi ${name}, we received a request to reset your ${escapeHtml(appName)} password.
107
+ </p>
108
+ <p style="text-align:center;margin:24px 0">
109
+ <a href="${escapeHtml(data.resetUrl)}" style="display:inline-block;padding:12px 24px;background:#18181b;color:#fff;text-decoration:none;border-radius:6px;font-weight:500">
110
+ Reset password
111
+ </a>
112
+ </p>
113
+ <p style="color:#71717a;font-size:14px;margin:0 0 8px">
114
+ This link will expire in ${escapeHtml(expiresIn)}.
115
+ </p>
116
+ <p style="color:#71717a;font-size:14px;margin:0">
117
+ If you didn't request this, you can safely ignore this email. Your password will not be changed.
118
+ </p>
119
+ `);
120
+
121
+ const text = [
122
+ `Reset your password`,
123
+ "",
124
+ `Hi ${data.name || "there"}, we received a request to reset your ${appName} password.`,
125
+ "",
126
+ `Reset your password: ${data.resetUrl}`,
127
+ "",
128
+ `This link will expire in ${expiresIn}.`,
129
+ "",
130
+ "If you didn't request this, you can safely ignore this email. Your password will not be changed.",
131
+ ].join("\n");
132
+
133
+ return { subject: `Reset your ${appName} password`, html, text };
134
+ }