@usequark/quark-create-app 1.25.8 → 1.25.9

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 (29) hide show
  1. package/package.json +1 -1
  2. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +1 -1
  3. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +39 -1
  4. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +1 -1
  5. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +7 -12
  6. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +2 -1
  7. package/templates/base-project/apps/web/src/app/api/csrf/route.js +1 -1
  8. package/templates/base-project/apps/web/src/app/api/device/register/route.js +1 -1
  9. package/templates/base-project/apps/web/src/app/api/error-handler.js +1 -1
  10. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +2 -2
  11. package/templates/base-project/apps/web/src/app/api/files/route.js +6 -4
  12. package/templates/base-project/apps/web/src/app/api/health/route.js +4 -4
  13. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +1 -1
  14. package/templates/base-project/apps/web/src/app/api/users/me/route.js +1 -1
  15. package/templates/base-project/apps/web/src/app/api/users/route.js +1 -1
  16. package/templates/base-project/apps/web/src/lib/auth-middleware.js +1 -1
  17. package/templates/base-project/apps/web/src/lib/auth.js +2 -5
  18. package/templates/base-project/apps/web/src/lib/enqueue-welcome-email.js +51 -0
  19. package/templates/base-project/apps/web/src/lib/jwt.js +2 -1
  20. package/templates/base-project/apps/web/src/preflight.js +1 -1
  21. package/templates/base-project/biome.json +1 -1
  22. package/templates/base-project/packages/db/src/umami.js +1 -1
  23. package/templates/config/src/redis.js +1 -1
  24. package/templates/config/src/validate-env.js +1 -1
  25. package/templates/worker/src/handlers/email.js +2 -2
  26. package/templates/worker/src/handlers/files.js +1 -1
  27. package/templates/worker/src/index.js +4 -4
  28. package/templates/worker/src/lib/railway.js +1 -1
  29. package/templates/worker/src/lib/tokens.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usequark/quark-create-app",
3
- "version": "1.25.8",
3
+ "version": "1.25.9",
4
4
  "description": "CLI to scaffold new Quark projects with Next.js, Prisma, BullMQ, and Railway deployment",
5
5
  "keywords": [
6
6
  "quark",
@@ -1,6 +1,6 @@
1
1
  import { createHash, timingSafeEqual } from "node:crypto";
2
2
  import { getAppleClientIds } from "@usequark/quark-config/oauth";
3
- import { validateBody } from "@usequark/quark-core";
3
+ import { validateBody } from "@usequark/quark-core/core";
4
4
  import { user } from "@usequark/quark-db";
5
5
  import { importJWK, jwtVerify } from "jose";
6
6
  import { NextResponse } from "next/server";
@@ -2,7 +2,7 @@ import {
2
2
  getGoogleClientId,
3
3
  isAudienceValid,
4
4
  } from "@usequark/quark-config/oauth";
5
- import { validateBody } from "@usequark/quark-core";
5
+ import { validateBody } from "@usequark/quark-core/core";
6
6
  import { user } from "@usequark/quark-db";
7
7
  import { NextResponse } from "next/server";
8
8
  import { z } from "zod";
@@ -15,6 +15,26 @@ const googleAuthSchema = z.object({
15
15
 
16
16
  const GOOGLE_TOKENINFO_URL = "https://oauth2.googleapis.com/tokeninfo";
17
17
 
18
+ /**
19
+ * Whether Google's `email_verified` claim says the address is confirmed.
20
+ *
21
+ * `tokeninfo` returns the claim as the *string* `"true"` or `"false"`, not a
22
+ * boolean, so a plain truthiness check would do the exact opposite of the
23
+ * obvious thing: reject every real sign-in (`"false"` is truthy) while waving
24
+ * through the one case that matters. Only an explicit confirmation counts.
25
+ * Anything else — a missing claim, a number, a null — is unverified, because
26
+ * the failure modes here are asymmetric: a wrong refusal costs one unlucky
27
+ * sign-in, and a wrong acceptance hands an unconfirmed address a session.
28
+ *
29
+ * @param {unknown} value
30
+ * @returns {boolean}
31
+ */
32
+ function isEmailVerified(value) {
33
+ if (typeof value === "boolean") return value;
34
+ if (typeof value !== "string") return false;
35
+ return value.trim().toLowerCase() === "true";
36
+ }
37
+
18
38
  /**
19
39
  * POST /api/auth/google
20
40
  * Verify Google ID token and issue a custom JWT.
@@ -73,6 +93,24 @@ export async function POST(request) {
73
93
  );
74
94
  }
75
95
 
96
+ // An unverified address proves nothing about who controls the inbox, and
97
+ // this route creates the account on first sight — so without this check the
98
+ // confirmation that is supposed to establish ownership of the address is
99
+ // the attacker's to skip rather than the victim's to perform.
100
+ //
101
+ // It sits after the missing-email `400` on purpose. That one describes the
102
+ // shape of the token rather than reporting that the token was refused, and
103
+ // putting this check ahead of it would change a response operators already
104
+ // read for what it is. What matters for security is the position relative
105
+ // to the database: both checks land before `findByEmail`, so an unverified
106
+ // address can never become a registration or existence oracle.
107
+ if (!isEmailVerified(tokenInfo.email_verified)) {
108
+ return NextResponse.json(
109
+ { message: "Invalid Google token" },
110
+ { status: 401 },
111
+ );
112
+ }
113
+
76
114
  // Find or create user
77
115
  let existingUser = await user.findByEmail(googleEmail);
78
116
  if (!existingUser) {
@@ -1,4 +1,4 @@
1
- import { createLogger, validateBody } from "@usequark/quark-core";
1
+ import { createLogger, validateBody } from "@usequark/quark-core/core";
2
2
  import { NextResponse } from "next/server";
3
3
  import { z } from "zod";
4
4
  import { issueTokenPair, verifyMobileToken } from "../../../../lib/jwt";
@@ -1,13 +1,9 @@
1
- import {
2
- createQueue,
3
- hashPassword,
4
- validateBody,
5
- withCsrfProtection,
6
- } from "@usequark/quark-core";
1
+ import { hashPassword } from "@usequark/quark-core/auth";
2
+ import { validateBody, withCsrfProtection } from "@usequark/quark-core/core";
7
3
  import { user, userRegisterSchema } from "@usequark/quark-db";
8
- import { JOB_NAMES, JOB_QUEUES } from "@usequark/quark-jobs";
9
4
  import { NextResponse } from "next/server";
10
5
  import { isSignupEnabled } from "@/lib/auth-signup";
6
+ import { enqueueWelcomeEmail } from "@/lib/enqueue-welcome-email";
11
7
  import { handleError } from "../../error-handler";
12
8
 
13
9
  export const POST = withCsrfProtection(async (request) => {
@@ -46,12 +42,11 @@ export const POST = withCsrfProtection(async (request) => {
46
42
  // Don't return the password
47
43
  const { password: _, ...safeUser } = newUser;
48
44
 
49
- // Enqueue welcome email (fire-and-forget - don't block the response)
45
+ // Enqueue welcome email (fire-and-forget - don't block the response).
46
+ // The queue module is reached through a lazily-loaded helper, so BullMQ
47
+ // stays out of this process until a registration actually happens.
50
48
  try {
51
- const emailQueue = createQueue(JOB_QUEUES.EMAIL);
52
- await emailQueue.add(JOB_NAMES.SEND_WELCOME_EMAIL, {
53
- userId: newUser.id,
54
- });
49
+ await enqueueWelcomeEmail(newUser.id);
55
50
  } catch {
56
51
  // Non-critical - user is created even if email fails to enqueue
57
52
  }
@@ -1,4 +1,5 @@
1
- import { validateBody, verifyPassword } from "@usequark/quark-core";
1
+ import { verifyPassword } from "@usequark/quark-core/auth";
2
+ import { validateBody } from "@usequark/quark-core/core";
2
3
  import { user } from "@usequark/quark-db";
3
4
  import { NextResponse } from "next/server";
4
5
  import { z } from "zod";
@@ -4,7 +4,7 @@
4
4
  * and returns it to the client for inclusion in request headers.
5
5
  */
6
6
 
7
- import { generateCsrfToken } from "@usequark/quark-core";
7
+ import { generateCsrfToken } from "@usequark/quark-core/core";
8
8
  import { NextResponse } from "next/server";
9
9
  import { getCsrfCookieOptions } from "@/lib/csrf-cookie";
10
10
 
@@ -1,4 +1,4 @@
1
- import { validateBody } from "@usequark/quark-core";
1
+ import { validateBody } from "@usequark/quark-core/core";
2
2
  import { prisma } from "@usequark/quark-db";
3
3
  import { NextResponse } from "next/server";
4
4
  import { z } from "zod";
@@ -1,4 +1,4 @@
1
- import { AppError, createLogger } from "@usequark/quark-core";
1
+ import { AppError, createLogger } from "@usequark/quark-core/core";
2
2
  import { NextResponse } from "next/server";
3
3
 
4
4
  const logger = createLogger("api");
@@ -7,11 +7,11 @@
7
7
  import {
8
8
  ConflictError,
9
9
  createLogger,
10
- createStorage,
11
10
  ForbiddenError,
12
11
  NotFoundError,
13
12
  withCsrfProtection,
14
- } from "@usequark/quark-core";
13
+ } from "@usequark/quark-core/core";
14
+ import { createStorage } from "@usequark/quark-core/storage";
15
15
  import { file } from "@usequark/quark-db";
16
16
  import { NextResponse } from "next/server";
17
17
  import { requireAuth } from "@/lib/auth-middleware";
@@ -5,13 +5,15 @@
5
5
  */
6
6
 
7
7
  import {
8
- createStorage,
9
- generateStorageKey,
10
- parseMultipart,
11
8
  parsePaginationQuery,
12
9
  validateFile,
13
10
  withCsrfProtection,
14
- } from "@usequark/quark-core";
11
+ } from "@usequark/quark-core/core";
12
+ import { parseMultipart } from "@usequark/quark-core/multipart";
13
+ import {
14
+ createStorage,
15
+ generateStorageKey,
16
+ } from "@usequark/quark-core/storage";
15
17
  import { file } from "@usequark/quark-db";
16
18
  import { NextResponse } from "next/server";
17
19
  import { requireAuth } from "@/lib/auth-middleware";
@@ -17,14 +17,14 @@
17
17
  * what the core module replaces.
18
18
  */
19
19
 
20
+ import { createLogger } from "@usequark/quark-core/core";
20
21
  import {
21
22
  checkQueues,
22
23
  checkStorage,
23
- createLogger,
24
- getRegisteredQueues,
25
- pingRedis,
26
24
  runHealthChecks,
27
- } from "@usequark/quark-core";
25
+ } from "@usequark/quark-core/health";
26
+ import { getRegisteredQueues } from "@usequark/quark-core/queue";
27
+ import { pingRedis } from "@usequark/quark-core/redis";
28
28
  import { pingDatabase } from "@usequark/quark-db";
29
29
  import { NextResponse } from "next/server";
30
30
 
@@ -2,7 +2,7 @@ import {
2
2
  ConflictError,
3
3
  validateBody,
4
4
  withCsrfProtection,
5
- } from "@usequark/quark-core";
5
+ } from "@usequark/quark-core/core";
6
6
  import { user, userUpdateSchema } from "@usequark/quark-db";
7
7
  import { NextResponse } from "next/server";
8
8
  import { requireRole } from "@/lib/auth-middleware";
@@ -1,4 +1,4 @@
1
- import { validateBody, withCsrfProtection } from "@usequark/quark-core";
1
+ import { validateBody, withCsrfProtection } from "@usequark/quark-core/core";
2
2
  import { user, userUpdateSchema } from "@usequark/quark-db";
3
3
  import { NextResponse } from "next/server";
4
4
  import { extractBearerPayload } from "../../../../lib/jwt";
@@ -2,7 +2,7 @@ import {
2
2
  parsePaginationQuery,
3
3
  validateBody,
4
4
  withCsrfProtection,
5
- } from "@usequark/quark-core";
5
+ } from "@usequark/quark-core/core";
6
6
  import { user, userCreateSchema } from "@usequark/quark-db";
7
7
  import { NextResponse } from "next/server";
8
8
  import { requireRole } from "@/lib/auth-middleware";
@@ -1,4 +1,4 @@
1
- import { ForbiddenError, UnauthorizedError } from "@usequark/quark-core";
1
+ import { ForbiddenError, UnauthorizedError } from "@usequark/quark-core/errors";
2
2
  import { auth } from "./auth";
3
3
 
4
4
  const UNAUTHENTICATED = "You must be logged in to access this resource";
@@ -1,9 +1,6 @@
1
1
  import { PrismaAdapter } from "@auth/prisma-adapter";
2
- import {
3
- createAuthConfig,
4
- createLogger,
5
- verifyPassword,
6
- } from "@usequark/quark-core";
2
+ import { createAuthConfig, verifyPassword } from "@usequark/quark-core/auth";
3
+ import { createLogger } from "@usequark/quark-core/core";
7
4
  import { Prisma, prisma, user } from "@usequark/quark-db";
8
5
  import NextAuth from "next-auth";
9
6
  import CredentialsProvider from "next-auth/providers/credentials";
@@ -0,0 +1,51 @@
1
+ import { JOB_NAMES, JOB_QUEUES } from "@usequark/quark-jobs";
2
+
3
+ /**
4
+ * Loads the queue module on demand.
5
+ *
6
+ * Kept as a named binding rather than an inline `import()` so it can be
7
+ * substituted — see `loadQueue` on {@link enqueueWelcomeEmail}.
8
+ *
9
+ * @returns {Promise<typeof import("@usequark/quark-core/queue")>}
10
+ */
11
+ function loadQueueModule() {
12
+ return import("@usequark/quark-core/queue");
13
+ }
14
+
15
+ /**
16
+ * Enqueues the post-signup welcome email for a newly created user.
17
+ *
18
+ * This lives in its own module for two reasons.
19
+ *
20
+ * The queue module is imported dynamically from inside this file, so callers
21
+ * that import *this* statically never pull BullMQ into their module graph. That
22
+ * matters most for `/api/auth/register`, which is reachable on the web service:
23
+ * a module-scope `createQueue` import both loaded BullMQ at startup and pinned a
24
+ * Redis connection to the web process for as long as the process lived.
25
+ *
26
+ * It also gives the route a static boundary to substitute in tests. `mock.module`
27
+ * intercepts a dynamic import only when the importing module registered the mock,
28
+ * so a dynamic import made by the route itself cannot be mocked from the route's
29
+ * test. A static import of this module can.
30
+ *
31
+ * @param {string} userId - The user to welcome. The worker rejects a job with
32
+ * no userId, so this is required rather than defaulted.
33
+ * @param {() => Promise<{ createQueue: (name: string, options?: object) => object }>} [loadQueue]
34
+ * How to obtain the queue module. Defaults to the dynamic import above;
35
+ * overridden in tests.
36
+ * @returns {Promise<void>}
37
+ * @throws Whatever `createQueue` or `add` throws. The queue is closed either
38
+ * way, so a caller that swallows the error still leaves no connection behind.
39
+ */
40
+ export async function enqueueWelcomeEmail(userId, loadQueue = loadQueueModule) {
41
+ const { createQueue } = await loadQueue();
42
+ const queue = createQueue(JOB_QUEUES.EMAIL);
43
+
44
+ try {
45
+ await queue.add(JOB_NAMES.SEND_WELCOME_EMAIL, { userId });
46
+ } finally {
47
+ // Never let a failing close mask the enqueue outcome — the caller only
48
+ // needs to know whether the job reached the queue.
49
+ await queue.close().catch(() => {});
50
+ }
51
+ }
@@ -1,4 +1,5 @@
1
- import { AppError, getAuthSecret } from "@usequark/quark-core";
1
+ import { getAuthSecret } from "@usequark/quark-core/auth";
2
+ import { AppError } from "@usequark/quark-core/core";
2
3
  import { jwtVerify, SignJWT } from "jose";
3
4
 
4
5
  const ISSUER = "quark-mobile";
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import { loadConfig } from "@usequark/quark-config";
4
- import { createLogger } from "@usequark/quark-core";
4
+ import { createLogger } from "@usequark/quark-core/core";
5
5
  import { pingDatabase } from "@usequark/quark-db";
6
6
 
7
7
  const logger = createLogger("web-preflight");
@@ -23,7 +23,7 @@
23
23
  "linter": {
24
24
  "enabled": true,
25
25
  "rules": {
26
- "preset": "recommended"
26
+ "recommended": true
27
27
  }
28
28
  },
29
29
  "javascript": {
@@ -4,7 +4,7 @@
4
4
  * No API key needed — we connect to the same DB Umami uses.
5
5
  */
6
6
 
7
- import { createLogger } from "@usequark/quark-core";
7
+ import { createLogger } from "@usequark/quark-core/core";
8
8
 
9
9
  const log = createLogger("lib:umami-db");
10
10
 
@@ -1,4 +1,4 @@
1
- import { resolveRedisConnection } from "@usequark/quark-core";
1
+ import { resolveRedisConnection } from "@usequark/quark-core/redis";
2
2
 
3
3
  let _redis = null;
4
4
 
@@ -1,4 +1,4 @@
1
- import { createLogger } from "@usequark/quark-core";
1
+ import { createLogger } from "@usequark/quark-core/core";
2
2
  import { ValidationError } from "@usequark/quark-core/errors";
3
3
  import { z } from "zod";
4
4
  import { syncNextAuthUrl } from "./app-url.js";
@@ -3,11 +3,11 @@
3
3
  * Processes email-related background jobs
4
4
  */
5
5
 
6
+ import { createEmailService } from "@usequark/quark-core/email";
6
7
  import {
7
- createEmailService,
8
8
  passwordResetEmail,
9
9
  welcomeEmail,
10
- } from "@usequark/quark-core";
10
+ } from "@usequark/quark-core/email-templates";
11
11
  import { prisma } from "@usequark/quark-db";
12
12
  import { JOB_NAMES } from "@usequark/quark-jobs";
13
13
  import {
@@ -3,7 +3,7 @@
3
3
  * Processes file-related background jobs (cleanup, etc.)
4
4
  */
5
5
 
6
- import { createStorage } from "@usequark/quark-core";
6
+ import { createStorage } from "@usequark/quark-core/storage";
7
7
  import { file } from "@usequark/quark-db";
8
8
  import { JOB_NAMES } from "@usequark/quark-jobs";
9
9
 
@@ -4,15 +4,15 @@
4
4
  * Handles job execution, retries, and error tracking
5
5
  */
6
6
 
7
+ import { createLogger } from "@usequark/quark-core/core";
8
+ import { AppError } from "@usequark/quark-core/errors";
7
9
  import {
8
10
  checkQueueHealth,
9
- createLogger,
10
11
  createQueue,
11
12
  createWorker,
12
- getRedisEndpoint,
13
13
  updateQueueDepths,
14
- } from "@usequark/quark-core";
15
- import { AppError } from "@usequark/quark-core/errors";
14
+ } from "@usequark/quark-core/queue";
15
+ import { getRedisEndpoint, getRedisUrl } from "@usequark/quark-core/redis";
16
16
  import { job, prisma } from "@usequark/quark-db";
17
17
  import { JOB_NAMES, JOB_QUEUES } from "@usequark/quark-jobs";
18
18
  import { jobHandlers } from "./handlers/index.js";
@@ -3,7 +3,7 @@
3
3
  * Fetches estimated usage and deployment data from Railway's GraphQL API.
4
4
  */
5
5
 
6
- import { createLogger } from "@usequark/quark-core";
6
+ import { createLogger } from "@usequark/quark-core/core";
7
7
  import { AppError } from "@usequark/quark-core/errors";
8
8
 
9
9
  const log = createLogger("lib:railway");
@@ -1,4 +1,4 @@
1
- import { createLogger } from "@usequark/quark-core";
1
+ import { createLogger } from "@usequark/quark-core/core";
2
2
 
3
3
  const _logger = createLogger("worker:tokens");
4
4