@usequark/quark-core 2.6.0 → 2.6.1
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 +29 -11
- package/package.json +1 -1
- package/src/csrf.js +18 -1
- package/src/csrf.test.js +76 -0
- package/src/db.js +1 -1
package/README.md
CHANGED
|
@@ -4,15 +4,21 @@ Shared infrastructure for the Quark platform - authentication, job queues, error
|
|
|
4
4
|
|
|
5
5
|
## What's Included
|
|
6
6
|
|
|
7
|
-
- **Authentication** - Auth.js-compatible config helpers (`createAuthConfig`, `requireAuth`, password hashing) plus framework-agnostic session guards (`requireSession`, `requireSessionRole`, `createAuthMiddleware`)
|
|
8
|
-
- **Database** -
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **Storage** - Local filesystem built in; S3
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
7
|
+
- **Authentication** - Auth.js-compatible config helpers (`createAuthConfig`, `requireAuth`, password hashing) plus framework-agnostic session guards (`requireSession`, `requireSessionRole`, `createAuthMiddleware`), and role checks (`authorization.js`)
|
|
8
|
+
- **Database** - `createPrismaClient(ClientClass)` builds a client from your generated Prisma class, plus connection-string/pool helpers, `pingDatabase()`, and `db-instrumentation`
|
|
9
|
+
- **Job Queue** - BullMQ integration (`createQueue`, `createWorker`, `addJob`, queue definitions)
|
|
10
|
+
- **Email** - Provider registry (`EmailProvider`, `registerEmailProvider`), `createEmailService`, and built-in templates
|
|
11
|
+
- **Storage** - Local filesystem built in; S3 and any S3-compatible endpoint (R2, MinIO) via optional AWS SDK install in the app
|
|
12
|
+
- **Metrics** - Prometheus-style registry (`createMetrics`, `metrics`, `httpRequestsTotal`, `jobQueueDepth`, `jobDuration`, and more)
|
|
13
|
+
- **Health** - `runHealthChecks`, `createDefaultProbes`, `checkStorage`, `checkQueues`, `logHealthReport`
|
|
14
|
+
- **Security** - CSRF tokens, rate limiting (in-memory with Redis fallback), file and multipart validation, request logging, error reporting
|
|
15
|
+
- **Errors** - Standardized error types (`ValidationError`, `NotFoundError`, `UnauthorizedError`, `AppError`, and others)
|
|
16
|
+
- **Utilities** - `retryAsync`, `deepMerge`, `randomString`, `sanitizeId`, `measureTime`, `memoize`, query builder, pagination, cache, locale helpers
|
|
14
17
|
- **Validation** - Zod-based request body validation
|
|
18
|
+
- **Payments and messaging** - Stripe client and webhook helpers, SMS provider
|
|
19
|
+
- **Schema introspection** - `parseSchema`, `getModels`, `getEnums`, `getModelByName`, `coerceId`, `resetSchemaCache`
|
|
15
20
|
- **Redis / Mail** - Connection helpers for Redis and local mail (Mailpit)
|
|
21
|
+
- **Testing** - factories, mocks, and helpers under `@usequark/quark-core/testing`
|
|
16
22
|
|
|
17
23
|
## Usage
|
|
18
24
|
|
|
@@ -34,18 +40,30 @@ All modules are re-exported from the package root. See JSDoc comments on each fu
|
|
|
34
40
|
|
|
35
41
|
Prefer a subpath when you want a smaller import graph or the browser-safe logger:
|
|
36
42
|
|
|
43
|
+
Every subpath in `packages/core/package.json` `exports`, verified against the file:
|
|
44
|
+
|
|
37
45
|
| Subpath | Contents |
|
|
38
46
|
|---|---|
|
|
47
|
+
| `@usequark/quark-core` | everything below, re-exported from the root |
|
|
39
48
|
| `@usequark/quark-core/core` | errors, logger, validation, utils, pagination, csrf, rate limiter, file validation, db |
|
|
40
|
-
| `@usequark/quark-core/db` | Prisma singleton, connection string, pool config, `pingDatabase()` |
|
|
41
|
-
| `@usequark/quark-core/admin` | Prisma schema introspection helpers |
|
|
42
49
|
| `@usequark/quark-core/auth` | auth config helpers + session guards |
|
|
43
50
|
| `@usequark/quark-core/auth/middleware` | session guards only |
|
|
51
|
+
| `@usequark/quark-core/db` | `createPrismaClient`, connection string, pool config, `pingDatabase()` |
|
|
52
|
+
| `@usequark/quark-core/queue` | BullMQ queue and worker helpers |
|
|
53
|
+
| `@usequark/quark-core/email` | `EmailProvider`, `registerEmailProvider`, `createEmailService` |
|
|
54
|
+
| `@usequark/quark-core/errors` | error classes only |
|
|
55
|
+
| `@usequark/quark-core/health` | health check runners and default probes |
|
|
56
|
+
| `@usequark/quark-core/metrics` | metrics registry and default instruments |
|
|
44
57
|
| `@usequark/quark-core/storage` | local adapter, storage factory, key/URL helpers, `createS3Storage` |
|
|
45
58
|
| `@usequark/quark-core/storage/s3` | S3 adapter only |
|
|
46
|
-
| `@usequark/quark-core/
|
|
47
|
-
| `@usequark/quark-core/locale` | locale helpers |
|
|
59
|
+
| `@usequark/quark-core/sms` | SMS provider |
|
|
48
60
|
| `@usequark/quark-core/stripe` | Stripe client + webhook helpers |
|
|
61
|
+
| `@usequark/quark-core/admin` | Prisma schema introspection helpers |
|
|
62
|
+
| `@usequark/quark-core/locale` | locale helpers |
|
|
63
|
+
| `@usequark/quark-core/logger` | browser-safe logger |
|
|
64
|
+
| `@usequark/quark-core/testing` | test factories, mocks, helpers |
|
|
65
|
+
|
|
66
|
+
`createPrismaClient` is a factory, not a singleton: it takes your generated Prisma `Client` class as its argument so core never depends on your generated client path.
|
|
49
67
|
|
|
50
68
|
`@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner`, `pg`, and `stripe` are optional peer dependencies. They are only loaded when the matching feature is used, so apps that do not use S3, `pingDatabase()`, or payments do not need them installed.
|
|
51
69
|
|
package/package.json
CHANGED
package/src/csrf.js
CHANGED
|
@@ -96,12 +96,29 @@ export function requireCsrfToken(request) {
|
|
|
96
96
|
/**
|
|
97
97
|
* Creates a CSRF-protected API route handler.
|
|
98
98
|
* Wraps your handler and automatically validates CSRF tokens.
|
|
99
|
+
*
|
|
100
|
+
* A failed check produces a `401` Response rather than a thrown error. The check
|
|
101
|
+
* runs in this wrapper, outside the route handler's own try/catch, so a throw here
|
|
102
|
+
* escapes `handleError` entirely: the client got an unhandled rejection instead of
|
|
103
|
+
* the status code its fetch handling expects. Returning the Response here keeps the
|
|
104
|
+
* rejection path reserved for genuine faults.
|
|
105
|
+
*
|
|
99
106
|
* @param {Function} handler - Your API route handler
|
|
100
107
|
* @returns {Function} Wrapped handler with CSRF protection
|
|
101
108
|
*/
|
|
102
109
|
export function withCsrfProtection(handler) {
|
|
103
110
|
return async (request, ...args) => {
|
|
104
|
-
|
|
111
|
+
try {
|
|
112
|
+
requireCsrfToken(request);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error instanceof UnauthorizedError) {
|
|
115
|
+
return Response.json(error.toJSON(), {
|
|
116
|
+
status: error.statusCode,
|
|
117
|
+
headers: { "Cache-Control": "no-store" },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
105
122
|
return handler(request, ...args);
|
|
106
123
|
};
|
|
107
124
|
}
|
package/src/csrf.test.js
CHANGED
|
@@ -166,4 +166,80 @@ test("CSRF Module", async (t) => {
|
|
|
166
166
|
assert.strictEqual(result.body, "success");
|
|
167
167
|
assert.strictEqual(result.request, request);
|
|
168
168
|
});
|
|
169
|
+
|
|
170
|
+
await t.test(
|
|
171
|
+
"withCsrfProtection returns 401 instead of throwing",
|
|
172
|
+
async () => {
|
|
173
|
+
// The check runs in the wrapper, outside the route handler's own
|
|
174
|
+
// try/catch, so a throw here escapes `handleError` and reaches the client
|
|
175
|
+
// as an unhandled rejection. The status code is what fetch handling expects.
|
|
176
|
+
const request = mockRequest({
|
|
177
|
+
method: "POST",
|
|
178
|
+
url: "http://localhost/api/posts",
|
|
179
|
+
headers: { "x-csrf-token": generateCsrfToken() },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
let handlerCalled = false;
|
|
183
|
+
const protectedHandler = withCsrfProtection(async () => {
|
|
184
|
+
handlerCalled = true;
|
|
185
|
+
return { unreachable: true };
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const response = await protectedHandler(request);
|
|
189
|
+
|
|
190
|
+
assert(response instanceof Response);
|
|
191
|
+
assert.strictEqual(response.status, 401);
|
|
192
|
+
assert.strictEqual(response.headers.get("Cache-Control"), "no-store");
|
|
193
|
+
|
|
194
|
+
const body = await response.json();
|
|
195
|
+
assert.strictEqual(body.code, "UNAUTHORIZED");
|
|
196
|
+
assert.strictEqual(body.statusCode, 401);
|
|
197
|
+
// The handler must not run on a rejected token.
|
|
198
|
+
assert.strictEqual(handlerCalled, false);
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
await t.test(
|
|
203
|
+
"withCsrfProtection returns 401 for a mismatched token",
|
|
204
|
+
async () => {
|
|
205
|
+
const token = generateCsrfToken();
|
|
206
|
+
const request = mockRequest({
|
|
207
|
+
method: "POST",
|
|
208
|
+
url: "http://localhost/api/posts",
|
|
209
|
+
headers: {
|
|
210
|
+
"x-csrf-token": "wrong-token-of-same-len",
|
|
211
|
+
cookie: `csrf_token=${encodeURIComponent(token)}`,
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const protectedHandler = withCsrfProtection(async () => ({
|
|
216
|
+
unreachable: true,
|
|
217
|
+
}));
|
|
218
|
+
const response = await protectedHandler(request);
|
|
219
|
+
|
|
220
|
+
assert.strictEqual(response.status, 401);
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
await t.test(
|
|
225
|
+
"withCsrfProtection still rejects for a non-CSRF error",
|
|
226
|
+
async () => {
|
|
227
|
+
// Only UnauthorizedError is converted. Anything else from the check is a
|
|
228
|
+
// genuine fault and must keep propagating rather than being reported to the
|
|
229
|
+
// client as an authentication problem.
|
|
230
|
+
const request = mockRequest({ method: "POST" });
|
|
231
|
+
request.headers.get = () => {
|
|
232
|
+
throw new TypeError("headers.get exploded");
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const protectedHandler = withCsrfProtection(async () => ({
|
|
236
|
+
unreachable: true,
|
|
237
|
+
}));
|
|
238
|
+
|
|
239
|
+
await assert.rejects(
|
|
240
|
+
() => protectedHandler(request),
|
|
241
|
+
/headers.get exploded/,
|
|
242
|
+
);
|
|
243
|
+
},
|
|
244
|
+
);
|
|
169
245
|
});
|
package/src/db.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Shared Prisma client singleton, connection-string builder, pool config,
|
|
5
5
|
* and a lightweight health-check ping. These replace the per-project
|
|
6
|
-
* re-implementations
|
|
6
|
+
* re-implementations that every app ends up writing by hand.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* import { createPrismaClient, getConnectionString, pingDatabase } from "@usequark/quark-core/db";
|