@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,257 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ import {
8
+ coerceId,
9
+ detectIdField,
10
+ getEnums,
11
+ getModelByName,
12
+ getModels,
13
+ parseSchema,
14
+ resetSchemaCache,
15
+ } from "./admin.js";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Fixture: a minimal Prisma schema
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const SCHEMA = `
22
+ generator client {
23
+ provider = "prisma-client-js"
24
+ }
25
+
26
+ datasource db {
27
+ provider = "postgresql"
28
+ url = env("DATABASE_URL")
29
+ }
30
+
31
+ enum Role {
32
+ ADMIN
33
+ EDITOR
34
+ VIEWER
35
+ }
36
+
37
+ model User {
38
+ id Int @id @default(autoincrement())
39
+ email String @unique
40
+ name String?
41
+ role Role @default(VIEWER)
42
+ createdAt DateTime @default(now())
43
+ updatedAt DateTime @updatedAt
44
+
45
+ posts Post[]
46
+ }
47
+
48
+ model Post {
49
+ id Int @id @default(autoincrement())
50
+ title String
51
+ body String?
52
+ published Boolean @default(false)
53
+ author User @relation(fields: [authorId], references: [id])
54
+ authorId Int
55
+ createdAt DateTime @default(now())
56
+ updatedAt DateTime @updatedAt
57
+ }
58
+ `;
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helpers
62
+ // ---------------------------------------------------------------------------
63
+
64
+ let schemaDir;
65
+
66
+ async function setup() {
67
+ schemaDir = await mkdir(join(tmpdir(), "admin-test"), { recursive: true });
68
+ await writeFile(join(schemaDir, "schema.prisma"), SCHEMA);
69
+ resetSchemaCache();
70
+ }
71
+
72
+ async function teardown() {
73
+ await rm(schemaDir, { recursive: true, force: true });
74
+ resetSchemaCache();
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Tests
79
+ // ---------------------------------------------------------------------------
80
+
81
+ test("admin - parseSchema returns models and enums", async () => {
82
+ await setup();
83
+ try {
84
+ const result = parseSchema(join(schemaDir, "schema.prisma"));
85
+ assert.ok(result.models.length > 0, "should have models");
86
+ assert.ok(result.enums.length > 0, "should have enums");
87
+ } finally {
88
+ await teardown();
89
+ }
90
+ });
91
+
92
+ test("admin - getModels returns User and Post", async () => {
93
+ await setup();
94
+ try {
95
+ const models = getModels(join(schemaDir, "schema.prisma"));
96
+ const names = models.map((m) => m.name);
97
+ assert.ok(names.includes("User"));
98
+ assert.ok(names.includes("Post"));
99
+ } finally {
100
+ await teardown();
101
+ }
102
+ });
103
+
104
+ test("admin - getEnums returns Role", async () => {
105
+ await setup();
106
+ try {
107
+ const enums = getEnums(join(schemaDir, "schema.prisma"));
108
+ const roleEnum = enums.find((e) => e.name === "Role");
109
+ assert.ok(roleEnum, "should find Role enum");
110
+ assert.deepEqual(roleEnum.values, ["ADMIN", "EDITOR", "VIEWER"]);
111
+ } finally {
112
+ await teardown();
113
+ }
114
+ });
115
+
116
+ test("admin - getModelByName is case-insensitive", async () => {
117
+ await setup();
118
+ try {
119
+ const user = getModelByName("user", join(schemaDir, "schema.prisma"));
120
+ assert.ok(user);
121
+ assert.equal(user.name, "User");
122
+ } finally {
123
+ await teardown();
124
+ }
125
+ });
126
+
127
+ test("admin - getModelByName returns undefined for unknown model", async () => {
128
+ await setup();
129
+ try {
130
+ const result = getModelByName("Comment", join(schemaDir, "schema.prisma"));
131
+ assert.equal(result, undefined);
132
+ } finally {
133
+ await teardown();
134
+ }
135
+ });
136
+
137
+ test("admin - detectIdField returns the @id field", async () => {
138
+ await setup();
139
+ try {
140
+ const user = getModelByName("User", join(schemaDir, "schema.prisma"));
141
+ const idField = detectIdField(user);
142
+ assert.ok(idField);
143
+ assert.equal(idField.name, "id");
144
+ assert.equal(idField.type, "Int");
145
+ assert.equal(idField.isId, true);
146
+ } finally {
147
+ await teardown();
148
+ }
149
+ });
150
+
151
+ test("admin - detectIdField returns null for model without @id", async () => {
152
+ const fakeModel = {
153
+ name: "NoId",
154
+ fields: [{ name: "name", type: "String", isId: false }],
155
+ };
156
+ assert.equal(detectIdField(fakeModel), null);
157
+ });
158
+
159
+ test("admin - coerceId converts string to number for Int @id", async () => {
160
+ await setup();
161
+ try {
162
+ const user = getModelByName("User", join(schemaDir, "schema.prisma"));
163
+ assert.equal(coerceId(user, "42"), 42);
164
+ } finally {
165
+ await teardown();
166
+ }
167
+ });
168
+
169
+ test("admin - coerceId returns string for String @id", async () => {
170
+ const stringIdModel = {
171
+ name: "Thing",
172
+ fields: [{ name: "id", type: "String", isId: true }],
173
+ };
174
+ assert.equal(coerceId(stringIdModel, "abc-123"), "abc-123");
175
+ });
176
+
177
+ test("admin - User model has expected fields", async () => {
178
+ await setup();
179
+ try {
180
+ const user = getModelByName("User", join(schemaDir, "schema.prisma"));
181
+ const fieldNames = user.fields.map((f) => f.name);
182
+ assert.ok(fieldNames.includes("id"));
183
+ assert.ok(fieldNames.includes("email"));
184
+ assert.ok(fieldNames.includes("name"));
185
+ assert.ok(fieldNames.includes("role"));
186
+ assert.ok(fieldNames.includes("createdAt"));
187
+ assert.ok(fieldNames.includes("updatedAt"));
188
+ } finally {
189
+ await teardown();
190
+ }
191
+ });
192
+
193
+ test("admin - User.email is marked @unique", async () => {
194
+ await setup();
195
+ try {
196
+ const user = getModelByName("User", join(schemaDir, "schema.prisma"));
197
+ const email = user.fields.find((f) => f.name === "email");
198
+ assert.ok(email.isUnique);
199
+ } finally {
200
+ await teardown();
201
+ }
202
+ });
203
+
204
+ test("admin - Post.author is an object relation", async () => {
205
+ await setup();
206
+ try {
207
+ const post = getModelByName("Post", join(schemaDir, "schema.prisma"));
208
+ const author = post.fields.find((f) => f.name === "author");
209
+ assert.equal(author.kind, "object");
210
+ assert.equal(author.type, "User");
211
+ } finally {
212
+ await teardown();
213
+ }
214
+ });
215
+
216
+ test("admin - Post.role field resolves enum kind", async () => {
217
+ await setup();
218
+ try {
219
+ const user = getModelByName("User", join(schemaDir, "schema.prisma"));
220
+ const role = user.fields.find((f) => f.name === "role");
221
+ assert.equal(role.kind, "enum");
222
+ assert.equal(role.type, "Role");
223
+ assert.deepEqual(role.enumValues, ["ADMIN", "EDITOR", "VIEWER"]);
224
+ } finally {
225
+ await teardown();
226
+ }
227
+ });
228
+
229
+ test("admin - resetSchemaCache clears the cache", async () => {
230
+ await setup();
231
+ try {
232
+ parseSchema(join(schemaDir, "schema.prisma"));
233
+ resetSchemaCache();
234
+ // After reset, next call re-parses from disk — no error should occur
235
+ const result = parseSchema(join(schemaDir, "schema.prisma"));
236
+ assert.ok(result.models.length > 0);
237
+ } finally {
238
+ await teardown();
239
+ }
240
+ });
241
+
242
+ test("admin - parseSchema returns empty models/enums when no schema found at default paths", () => {
243
+ // Override cwd to a path with no schema.prisma and no env var
244
+ const origCwd = process.cwd;
245
+ const origEnv = process.env.PRISMA_SCHEMA_PATH;
246
+ try {
247
+ process.cwd = () => "/nonexistent";
248
+ delete process.env.PRISMA_SCHEMA_PATH;
249
+ resetSchemaCache();
250
+ const result = parseSchema();
251
+ assert.ok(Array.isArray(result.models));
252
+ assert.ok(Array.isArray(result.enums));
253
+ } finally {
254
+ process.cwd = origCwd;
255
+ if (origEnv !== undefined) process.env.PRISMA_SCHEMA_PATH = origEnv;
256
+ }
257
+ });
@@ -0,0 +1,160 @@
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
+ import { createLogger } from "../logger.js";
8
+
9
+ export * from "./middleware.js";
10
+ export * from "./password.js";
11
+
12
+ const logger = createLogger({ name: "auth" });
13
+
14
+ /**
15
+ * Resolves the auth secret from the supported environment variables.
16
+ * Prefers NEXTAUTH_SECRET for compatibility, but also accepts AUTH_SECRET.
17
+ * @returns {string|null}
18
+ */
19
+ export const getAuthSecret = () => {
20
+ return process.env.NEXTAUTH_SECRET || process.env.AUTH_SECRET || null;
21
+ };
22
+
23
+ /**
24
+ * Detects whether the app is running on a deployed (non-local) environment.
25
+ * @returns {boolean}
26
+ */
27
+ function isDeployed() {
28
+ const url = process.env.AUTH_URL || process.env.NEXTAUTH_URL;
29
+ if (url && !/localhost|127\.0\.0\.1|0\.0\.0\.0/.test(url)) return true;
30
+ return !!(
31
+ process.env.VERCEL ||
32
+ process.env.CF_PAGES ||
33
+ process.env.RAILWAY_SERVICE_ID ||
34
+ process.env.AUTH_TRUST_HOST === "true" ||
35
+ process.env.NODE_ENV !== "production"
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Creates a next-auth configuration object with sensible defaults.
41
+ * Designed to be extended by applications.
42
+ * @param {Object} options - Configuration options
43
+ * @param {Array} options.providers - Next-auth providers (GitHub, Google, etc.)
44
+ * @param {Object} options.callbacks - next-auth callbacks (jwt, session, etc.)
45
+ * @param {Object} options.session - Session configuration
46
+ * @param {string} options.secret - NEXTAUTH_SECRET (defaults to env var)
47
+ * @param {string} options.trustHost - When false, next-auth rejects requests from unknown hosts
48
+ * @returns {Object} Complete next-auth configuration
49
+ */
50
+ export const createAuthConfig = (options = {}) => {
51
+ const {
52
+ providers = [],
53
+ callbacks = {},
54
+ session = {},
55
+ secret = getAuthSecret(),
56
+ ...rest
57
+ } = options;
58
+
59
+ if (!secret) {
60
+ throw new Error(
61
+ "NEXTAUTH_SECRET or AUTH_SECRET must be set (env var or options.secret)",
62
+ );
63
+ }
64
+
65
+ const trustHost = rest.trustHost ?? isDeployed();
66
+
67
+ return {
68
+ secret,
69
+ providers,
70
+ session: {
71
+ strategy: "jwt",
72
+ maxAge: 30 * 24 * 60 * 60, // 30 days
73
+ updateAge: 24 * 60 * 60, // 24 hours
74
+ ...session,
75
+ },
76
+ callbacks: {
77
+ async jwt({ token, user }) {
78
+ if (user) {
79
+ token.id = user.id;
80
+ token.email = user.email;
81
+ token.name = user.name;
82
+ token.role = user.role || "viewer";
83
+ }
84
+ return token;
85
+ },
86
+ async session({ session, token }) {
87
+ if (session.user) {
88
+ session.user.id = token.id;
89
+ session.user.role = token.role;
90
+ }
91
+ return session;
92
+ },
93
+ ...callbacks,
94
+ },
95
+ pages: {
96
+ signIn: "/auth/signin",
97
+ error: "/auth/error",
98
+ },
99
+ trustHost,
100
+ ...rest,
101
+ };
102
+ };
103
+
104
+ /**
105
+ * Utility to safely get the current session (works in both server & edge)
106
+ * @param {Function} getSession - The next-auth getSession function
107
+ * @returns {Promise<Object|null>} Session object or null if not authenticated
108
+ */
109
+ export const getCurrentSession = async (getSession) => {
110
+ try {
111
+ return await getSession();
112
+ } catch (error) {
113
+ logger.error("Failed to get session", {
114
+ message: error?.message ?? String(error),
115
+ });
116
+ return null;
117
+ }
118
+ };
119
+
120
+ /**
121
+ * Checks if a user is authenticated
122
+ * @param {Object} session - Session object from next-auth
123
+ * @returns {boolean} True if user is authenticated
124
+ */
125
+ export const isAuthenticated = (session) => {
126
+ return session?.user?.email;
127
+ };
128
+
129
+ /**
130
+ * Gets user ID from session
131
+ * @param {Object} session - Session object from next-auth
132
+ * @returns {string|null} User ID or null
133
+ */
134
+ export const getUserId = (session) => {
135
+ return session?.user?.id || null;
136
+ };
137
+
138
+ /**
139
+ * Gets user email from session
140
+ * @param {Object} session - Session object from next-auth
141
+ * @returns {string|null} User email or null
142
+ */
143
+ export const getUserEmail = (session) => {
144
+ return session?.user?.email || null;
145
+ };
146
+
147
+ /**
148
+ * Validates that a request has a valid session
149
+ * Throws UnauthorizedError if not authenticated
150
+ * @param {Object} session - Session object from next-auth
151
+ * @throws {UnauthorizedError} If session is invalid
152
+ * @returns {string} User ID
153
+ */
154
+ export const requireAuth = (session) => {
155
+ const userId = getUserId(session);
156
+ if (!userId) {
157
+ throw new UnauthorizedError("Authentication required");
158
+ }
159
+ return userId;
160
+ };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @techstream/quark-core - Auth Middleware
3
+ *
4
+ * Lightweight, framework-agnostic authentication middleware for Server
5
+ * Actions and API routes. The caller supplies a `getSession` function
6
+ * (e.g. NextAuth's `auth()`) — this module adds the guard logic.
7
+ *
8
+ * Usage:
9
+ * import { requireSession, requireSessionRole, createAuthMiddleware } from "@techstream/quark-core/auth/middleware";
10
+ *
11
+ * // In a Server Action:
12
+ * const session = await requireSession(() => auth());
13
+ * // session is guaranteed non-null; UnauthorizedError thrown otherwise.
14
+ */
15
+
16
+ import { ForbiddenError, UnauthorizedError } from "../errors.js";
17
+
18
+ /**
19
+ * Requires a valid session. Throws `UnauthorizedError` if the session
20
+ * is missing or has no user.
21
+ *
22
+ * @template T
23
+ * @param {() => Promise<T | null>} getSession - Async function that returns the session (or null)
24
+ * @returns {Promise<T>} The session, guaranteed non-null
25
+ * @throws {UnauthorizedError}
26
+ */
27
+ export async function requireSession(getSession) {
28
+ const session = await getSession();
29
+ if (!session) {
30
+ throw new UnauthorizedError("Authentication required");
31
+ }
32
+ return session;
33
+ }
34
+
35
+ /**
36
+ * Requires a valid session **and** that the user has the specified role.
37
+ *
38
+ * @template {{ user?: { role?: string } | null }} S
39
+ * @param {string} role - Required role (case-insensitive comparison)
40
+ * @param {() => Promise<S | null>} getSession - Async function that returns the session (or null)
41
+ * @returns {Promise<S>} The session, guaranteed non-null with the correct role
42
+ * @throws {UnauthorizedError} When the session is missing
43
+ * @throws {ForbiddenError} When the user's role does not match
44
+ */
45
+ export async function requireSessionRole(role, getSession) {
46
+ const session = await requireSession(getSession);
47
+ const userRole = session?.user?.role;
48
+
49
+ if (!userRole || userRole.toLowerCase() !== role.toLowerCase()) {
50
+ throw new ForbiddenError(
51
+ `Role "${role}" required — you have "${userRole || "none"}"`,
52
+ );
53
+ }
54
+
55
+ return session;
56
+ }
57
+
58
+ /**
59
+ * Creates a reusable auth middleware from a configuration object.
60
+ *
61
+ * @param {Object} options
62
+ * @param {() => Promise<any | null>} options.getSession - Async function returning the session
63
+ * @param {string} [options.roleField="role"] - Field on `session.user` that holds the role
64
+ * @returns {{
65
+ * requireSession: () => Promise<any>,
66
+ * requireSessionRole: (role: string) => Promise<any>
67
+ * }}
68
+ */
69
+ export function createAuthMiddleware({ getSession, roleField = "role" }) {
70
+ /**
71
+ * @returns {Promise<any>} The session
72
+ */
73
+ async function resolveSession() {
74
+ const session = await getSession();
75
+ if (!session) {
76
+ throw new UnauthorizedError("Authentication required");
77
+ }
78
+ return session;
79
+ }
80
+
81
+ return {
82
+ /**
83
+ * @returns {Promise<any>} The session
84
+ */
85
+ async requireSession() {
86
+ return resolveSession();
87
+ },
88
+
89
+ /**
90
+ * @param {string} role - Required role
91
+ * @returns {Promise<any>} The session
92
+ */
93
+ async requireSessionRole(role) {
94
+ const session = await resolveSession();
95
+ const userRole = session?.user?.[roleField];
96
+
97
+ if (!userRole || userRole.toLowerCase() !== role.toLowerCase()) {
98
+ throw new ForbiddenError(
99
+ `Role "${role}" required — you have "${userRole || "none"}"`,
100
+ );
101
+ }
102
+
103
+ return session;
104
+ },
105
+ };
106
+ }
@@ -0,0 +1,116 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { ForbiddenError, UnauthorizedError } from "../errors.js";
4
+ import {
5
+ createAuthMiddleware,
6
+ requireSession,
7
+ requireSessionRole,
8
+ } from "./middleware.js";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // requireSession
12
+ // ---------------------------------------------------------------------------
13
+
14
+ test("auth/middleware - requireSession returns session when valid", async () => {
15
+ const session = { user: { id: "1", email: "a@b.com" } };
16
+ const result = await requireSession(async () => session);
17
+ assert.deepEqual(result, session);
18
+ });
19
+
20
+ test("auth/middleware - requireSession throws UnauthorizedError when session is null", async () => {
21
+ await assert.rejects(
22
+ () => requireSession(async () => null),
23
+ (err) => err instanceof UnauthorizedError,
24
+ );
25
+ });
26
+
27
+ test("auth/middleware - requireSession throws UnauthorizedError when session is undefined", async () => {
28
+ await assert.rejects(
29
+ () => requireSession(async () => undefined),
30
+ (err) => err instanceof UnauthorizedError,
31
+ );
32
+ });
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // requireSessionRole
36
+ // ---------------------------------------------------------------------------
37
+
38
+ test("auth/middleware - requireSessionRole returns session when role matches", async () => {
39
+ const session = { user: { id: "1", role: "admin" } };
40
+ const result = await requireSessionRole("admin", async () => session);
41
+ assert.deepEqual(result, session);
42
+ });
43
+
44
+ test("auth/middleware - requireSessionRole is case-insensitive", async () => {
45
+ const session = { user: { id: "1", role: "Admin" } };
46
+ const result = await requireSessionRole("admin", async () => session);
47
+ assert.deepEqual(result, session);
48
+ });
49
+
50
+ test("auth/middleware - requireSessionRole throws ForbiddenError when role does not match", async () => {
51
+ const session = { user: { id: "1", role: "viewer" } };
52
+ await assert.rejects(
53
+ () => requireSessionRole("admin", async () => session),
54
+ (err) => err instanceof ForbiddenError,
55
+ );
56
+ });
57
+
58
+ test("auth/middleware - requireSessionRole throws UnauthorizedError when session is null", async () => {
59
+ await assert.rejects(
60
+ () => requireSessionRole("admin", async () => null),
61
+ (err) => err instanceof UnauthorizedError,
62
+ );
63
+ });
64
+
65
+ test("auth/middleware - requireSessionRole throws ForbiddenError when user has no role", async () => {
66
+ const session = { user: { id: "1" } };
67
+ await assert.rejects(
68
+ () => requireSessionRole("admin", async () => session),
69
+ (err) => err instanceof ForbiddenError,
70
+ );
71
+ });
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // createAuthMiddleware
75
+ // ---------------------------------------------------------------------------
76
+
77
+ test("auth/middleware - createAuthMiddleware requireSession works", async () => {
78
+ const session = { user: { id: "42", role: "editor" } };
79
+ const mw = createAuthMiddleware({ getSession: async () => session });
80
+ const result = await mw.requireSession();
81
+ assert.deepEqual(result, session);
82
+ });
83
+
84
+ test("auth/middleware - createAuthMiddleware requireSession throws on null session", async () => {
85
+ const mw = createAuthMiddleware({ getSession: async () => null });
86
+ await assert.rejects(
87
+ () => mw.requireSession(),
88
+ (err) => err instanceof UnauthorizedError,
89
+ );
90
+ });
91
+
92
+ test("auth/middleware - createAuthMiddleware requireSessionRole works", async () => {
93
+ const session = { user: { id: "42", role: "admin" } };
94
+ const mw = createAuthMiddleware({ getSession: async () => session });
95
+ const result = await mw.requireSessionRole("admin");
96
+ assert.deepEqual(result, session);
97
+ });
98
+
99
+ test("auth/middleware - createAuthMiddleware requireSessionRole throws ForbiddenError", async () => {
100
+ const session = { user: { id: "42", role: "viewer" } };
101
+ const mw = createAuthMiddleware({ getSession: async () => session });
102
+ await assert.rejects(
103
+ () => mw.requireSessionRole("admin"),
104
+ (err) => err instanceof ForbiddenError,
105
+ );
106
+ });
107
+
108
+ test("auth/middleware - createAuthMiddleware uses custom roleField", async () => {
109
+ const flatSession = { user: { id: "42", accessLevel: "admin" } };
110
+ const mw = createAuthMiddleware({
111
+ getSession: async () => flatSession,
112
+ roleField: "accessLevel",
113
+ });
114
+ const result = await mw.requireSessionRole("admin");
115
+ assert.deepEqual(result, flatSession);
116
+ });
@@ -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
+ }