@usequark/quark-create-app 1.25.5 → 1.25.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usequark/quark-create-app",
3
- "version": "1.25.5",
3
+ "version": "1.25.6",
4
4
  "description": "CLI to scaffold new Quark projects with Next.js, Prisma, BullMQ, and Railway deployment",
5
5
  "keywords": [
6
6
  "quark",
@@ -6,21 +6,32 @@
6
6
 
7
7
  import { generateCsrfToken } from "@usequark/quark-core";
8
8
  import { NextResponse } from "next/server";
9
- import { auth } from "@/lib/auth";
10
9
  import { getCsrfCookieOptions } from "@/lib/csrf-cookie";
11
10
 
11
+ /**
12
+ * Headers applied to every response.
13
+ *
14
+ * `no-store` is load-bearing, not hygiene. The body of this response *is* a
15
+ * write credential: paired with the cookie it sets, it authorises the holder's
16
+ * next mutating request. A shared cache that stored it could hand the token to
17
+ * a second caller, who would then be able to write as the first — so this
18
+ * response must never be cached, by the browser or anything between it and the
19
+ * client. `Vary: Cookie` keeps that true for a cache that keys on request
20
+ * headers rather than obeying `no-store` outright.
21
+ */
22
+ const NO_STORE_HEADERS = {
23
+ "Cache-Control": "no-store, no-cache, must-revalidate",
24
+ Vary: "Cookie",
25
+ };
26
+
12
27
  export async function GET(request) {
13
28
  try {
14
- const session = await auth();
15
-
16
- if (!session) {
17
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
18
- }
19
-
20
- // Generate CSRF token
21
29
  const csrfToken = generateCsrfToken();
22
30
 
23
- const response = NextResponse.json({ csrfToken });
31
+ const response = NextResponse.json(
32
+ { csrfToken },
33
+ { headers: NO_STORE_HEADERS },
34
+ );
24
35
 
25
36
  // Store the token in a secure HTTP-only cookie so the server can
26
37
  // validate it on subsequent state-changing requests.
@@ -34,7 +45,7 @@ export async function GET(request) {
34
45
  } catch (_error) {
35
46
  return NextResponse.json(
36
47
  { error: "Failed to generate CSRF token" },
37
- { status: 500 },
48
+ { status: 500, headers: NO_STORE_HEADERS },
38
49
  );
39
50
  }
40
51
  }
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { clearCsrfToken, getCsrfToken } from "@usequark/quark-core/csrf-client";
3
4
  import {
4
5
  Button,
5
6
  ErrorBanner,
@@ -98,16 +99,55 @@ function RegisterForm() {
98
99
  setPasswordError("");
99
100
  setLoading(true);
100
101
 
101
- const res = await fetch("/api/auth/register", {
102
+ // The CSRF token has to be in hand before the write. `GET /api/csrf` sets
103
+ // the matching httpOnly cookie and returns the same value; the server
104
+ // compares the two, so a request that arrives without the cookie is
105
+ // rejected before the handler runs. This page is pre-authentication, which
106
+ // is why the endpoint mints tokens for callers with no session.
107
+ //
108
+ // Both the token fetch and the write can fail on the network, and this
109
+ // handler has already set `loading`. Without a catch, either one would
110
+ // leave the button spinning forever.
111
+ try {
112
+ const res = await postRegistration(await getCsrfToken());
113
+
114
+ // The cache refetches on a timer, which covers expiry. It cannot cover
115
+ // the server rotating the cookie underneath us — a sign-out, or anything
116
+ // else that clears cookies — and that surfaces as a rejected write with
117
+ // nothing visibly wrong. Drop the stale token and try once more; a
118
+ // second failure is a real failure and falls through to the banner.
119
+ if (res.status === 401) {
120
+ clearCsrfToken();
121
+ return await completeRegistration(
122
+ await postRegistration(await getCsrfToken()),
123
+ );
124
+ }
125
+
126
+ return await completeRegistration(res);
127
+ } catch {
128
+ setError("Could not reach the server. Try again.");
129
+ setLoading(false);
130
+ }
131
+ }
132
+
133
+ /** POST the form to /api/auth/register with the given token. */
134
+ async function postRegistration(csrfToken) {
135
+ return await fetch("/api/auth/register", {
102
136
  method: "POST",
103
- headers: { "Content-Type": "application/json" },
137
+ headers: {
138
+ "Content-Type": "application/json",
139
+ "X-CSRF-Token": csrfToken,
140
+ },
104
141
  body: JSON.stringify({
105
142
  email: email.trim(),
106
143
  password,
107
144
  name: name.trim() || undefined,
108
145
  }),
109
146
  });
147
+ }
110
148
 
149
+ /** Surface a failure, or sign the new account in. */
150
+ async function completeRegistration(res) {
111
151
  if (!res.ok) {
112
152
  const body = await res.json().catch(() => ({}));
113
153
  setError(body.message || "Registration failed. Try again.");