@techstream/quark-core 1.1.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 ADDED
@@ -0,0 +1,419 @@
1
+ # @techstream/quark-core - The Quark Platform Core
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.
4
+
5
+ ## Philosophy
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
+ ```
53
+
54
+ ## Usage
55
+
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
+ ```javascript
153
+ import {
154
+ 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
+ retryAsync,
211
+ validateEnv,
212
+ deepMerge,
213
+ randomString,
214
+ sanitizeId,
215
+ measureTime,
216
+ memoize,
217
+ } 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
+ ```
278
+
279
+ ## Testing Core
280
+
281
+ Core includes unit tests verifying all functionality:
282
+
283
+ ```bash
284
+ cd packages/core
285
+ pnpm test
286
+ ```
287
+
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
418
+
419
+ ISC - Part of the Quark Platform
package/package.json ADDED
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @techstream/quark-core - Authentication Module
3
+ * Provides next-auth initialization and session management helpers
4
+ */
5
+
6
+ import { UnauthorizedError } from "../errors.js";
7
+
8
+ export * from "./password.js";
9
+
10
+ /**
11
+ * Creates a next-auth configuration object with sensible defaults
12
+ * Designed to be extended by applications
13
+ * @param {Object} options - Configuration options
14
+ * @param {Array} options.providers - Next-auth providers (GitHub, Google, etc.)
15
+ * @param {Object} options.callbacks - next-auth callbacks (jwt, session, etc.)
16
+ * @param {Object} options.session - Session configuration
17
+ * @param {string} options.secret - NEXTAUTH_SECRET (defaults to env var)
18
+ * @returns {Object} Complete next-auth configuration
19
+ */
20
+ export const createAuthConfig = (options = {}) => {
21
+ const {
22
+ providers = [],
23
+ callbacks = {},
24
+ session = {},
25
+ secret = process.env.NEXTAUTH_SECRET,
26
+ ...rest
27
+ } = options;
28
+
29
+ if (!secret) {
30
+ throw new Error(
31
+ "NEXTAUTH_SECRET must be set (env var or options.secret)",
32
+ );
33
+ }
34
+
35
+ return {
36
+ secret,
37
+ providers,
38
+ session: {
39
+ strategy: "jwt",
40
+ maxAge: 30 * 24 * 60 * 60, // 30 days
41
+ updateAge: 24 * 60 * 60, // 24 hours
42
+ ...session,
43
+ },
44
+ callbacks: {
45
+ async jwt({ token, user }) {
46
+ if (user) {
47
+ token.id = user.id;
48
+ token.email = user.email;
49
+ token.name = user.name;
50
+ token.role = user.role || "viewer";
51
+ }
52
+ return token;
53
+ },
54
+ async session({ session, token }) {
55
+ if (session.user) {
56
+ session.user.id = token.id;
57
+ session.user.role = token.role;
58
+ }
59
+ // Note: NextAuth v5+ includes CSRF token in session automatically
60
+ // For older versions, you can add: session.csrfToken = token.csrfToken;
61
+ return session;
62
+ },
63
+ ...callbacks,
64
+ },
65
+ pages: {
66
+ signIn: "/auth/signin",
67
+ error: "/auth/error",
68
+ },
69
+ ...rest,
70
+ };
71
+ };
72
+
73
+ /**
74
+ * Utility to safely get the current session (works in both server & edge)
75
+ * @param {Function} getSession - The next-auth getSession function
76
+ * @returns {Promise<Object|null>} Session object or null if not authenticated
77
+ */
78
+ export const getCurrentSession = async (getSession) => {
79
+ try {
80
+ return await getSession();
81
+ } catch (error) {
82
+ console.error("Failed to get session:", error);
83
+ return null;
84
+ }
85
+ };
86
+
87
+ /**
88
+ * Checks if a user is authenticated
89
+ * @param {Object} session - Session object from next-auth
90
+ * @returns {boolean} True if user is authenticated
91
+ */
92
+ export const isAuthenticated = (session) => {
93
+ return session?.user?.email;
94
+ };
95
+
96
+ /**
97
+ * Gets user ID from session
98
+ * @param {Object} session - Session object from next-auth
99
+ * @returns {string|null} User ID or null
100
+ */
101
+ export const getUserId = (session) => {
102
+ return session?.user?.id || null;
103
+ };
104
+
105
+ /**
106
+ * Gets user email from session
107
+ * @param {Object} session - Session object from next-auth
108
+ * @returns {string|null} User email or null
109
+ */
110
+ export const getUserEmail = (session) => {
111
+ return session?.user?.email || null;
112
+ };
113
+
114
+ /**
115
+ * Validates that a request has a valid session
116
+ * Throws UnauthorizedError if not authenticated
117
+ * @param {Object} session - Session object from next-auth
118
+ * @throws {UnauthorizedError} If session is invalid
119
+ * @returns {string} User ID
120
+ */
121
+ export const requireAuth = (session) => {
122
+ const userId = getUserId(session);
123
+ if (!userId) {
124
+ throw new UnauthorizedError("Authentication required");
125
+ }
126
+ return userId;
127
+ };
@@ -0,0 +1,9 @@
1
+ import bcrypt from "bcryptjs";
2
+
3
+ export async function hashPassword(password) {
4
+ return bcrypt.hash(password, 12);
5
+ }
6
+
7
+ export async function verifyPassword(password, hashedPassword) {
8
+ return bcrypt.compare(password, hashedPassword);
9
+ }
@@ -0,0 +1,90 @@
1
+ import assert from "node:assert";
2
+ import { test } from "node:test";
3
+ import {
4
+ createAuthConfig,
5
+ getUserEmail,
6
+ getUserId,
7
+ isAuthenticated,
8
+ requireAuth,
9
+ } from "../src/auth/index.js";
10
+
11
+ test("Auth Module", async (t) => {
12
+ await t.test("createAuthConfig returns valid config", () => {
13
+ const config = createAuthConfig({ secret: "test-secret" });
14
+ assert(config.session);
15
+ assert(config.session.strategy === "jwt");
16
+ assert(config.session.maxAge === 30 * 24 * 60 * 60);
17
+ assert.deepStrictEqual(config.providers, []);
18
+ });
19
+
20
+ await t.test("createAuthConfig throws when secret is missing", () => {
21
+ const orig = process.env.NEXTAUTH_SECRET;
22
+ delete process.env.NEXTAUTH_SECRET;
23
+ try {
24
+ assert.throws(
25
+ () => createAuthConfig(),
26
+ (err) =>
27
+ err instanceof Error &&
28
+ /NEXTAUTH_SECRET/.test(err.message),
29
+ );
30
+ } finally {
31
+ if (orig !== undefined) process.env.NEXTAUTH_SECRET = orig;
32
+ }
33
+ });
34
+
35
+ await t.test("createAuthConfig with custom options", () => {
36
+ const customProvider = { id: "custom" };
37
+ const config = createAuthConfig({
38
+ providers: [customProvider],
39
+ secret: "test-secret",
40
+ });
41
+
42
+ assert.deepStrictEqual(config.providers, [customProvider]);
43
+ assert(config.secret === "test-secret");
44
+ });
45
+
46
+ await t.test("isAuthenticated returns true for valid session", () => {
47
+ const session = {
48
+ user: { email: "test@example.com", id: "123" },
49
+ };
50
+ assert(isAuthenticated(session));
51
+ });
52
+
53
+ await t.test("isAuthenticated returns false for invalid session", () => {
54
+ assert(!isAuthenticated(null));
55
+ assert(!isAuthenticated({ user: null }));
56
+ assert(!isAuthenticated({ user: { email: null } }));
57
+ });
58
+
59
+ await t.test("getUserId extracts user ID", () => {
60
+ const session = { user: { id: "user-123" } };
61
+ assert(getUserId(session) === "user-123");
62
+ });
63
+
64
+ await t.test("getUserId returns null when missing", () => {
65
+ assert(getUserId(null) === null);
66
+ assert(getUserId({ user: null }) === null);
67
+ });
68
+
69
+ await t.test("getUserEmail extracts user email", () => {
70
+ const session = { user: { email: "test@example.com" } };
71
+ assert(getUserEmail(session) === "test@example.com");
72
+ });
73
+
74
+ await t.test("getUserEmail returns null when missing", () => {
75
+ assert(getUserEmail(null) === null);
76
+ });
77
+
78
+ await t.test("requireAuth returns user ID for authenticated session", () => {
79
+ const session = { user: { id: "user-123", email: "test@example.com" } };
80
+ const userId = requireAuth(session);
81
+ assert(userId === "user-123");
82
+ });
83
+
84
+ await t.test("requireAuth throws error for unauthenticated session", () => {
85
+ assert.throws(
86
+ () => requireAuth(null),
87
+ (err) => err instanceof Error,
88
+ );
89
+ });
90
+ });