@usequark/quark-create-app 1.23.11

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 (237) hide show
  1. package/README.md +210 -0
  2. package/package.json +56 -0
  3. package/src/dependabot-config.test.js +128 -0
  4. package/src/deploy/adapters/iac.js +203 -0
  5. package/src/deploy/adapters/index.js +27 -0
  6. package/src/deploy/adapters/railway.js +508 -0
  7. package/src/deploy/adapters/railway.test.js +318 -0
  8. package/src/deploy/contract.js +59 -0
  9. package/src/deploy/deploy.integration.test.js +183 -0
  10. package/src/deploy/deploy.js +430 -0
  11. package/src/deploy/deploy.test.js +105 -0
  12. package/src/deploy/discovery.js +115 -0
  13. package/src/deploy/discovery.test.js +141 -0
  14. package/src/deploy/index.js +38 -0
  15. package/src/deploy/inspect.js +130 -0
  16. package/src/index.js +2920 -0
  17. package/src/mobile-template.test.js +232 -0
  18. package/src/prepare-hooks.test.js +535 -0
  19. package/src/scaffold-guards.test.js +140 -0
  20. package/src/scaffold-output.test.js +228 -0
  21. package/src/template-scripts.test.js +160 -0
  22. package/src/utils.js +77 -0
  23. package/src/utils.test.js +95 -0
  24. package/src/worker-dockerfile.test.js +85 -0
  25. package/templates/base-project/.cursor/rules/quark.mdc +172 -0
  26. package/templates/base-project/.dockerignore +14 -0
  27. package/templates/base-project/.env.railway.example +87 -0
  28. package/templates/base-project/.github/copilot-instructions.md +65 -0
  29. package/templates/base-project/.github/dependabot.yml +31 -0
  30. package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
  31. package/templates/base-project/.github/workflows/ci.yml +116 -0
  32. package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
  33. package/templates/base-project/.github/workflows/install-windows.yml +27 -0
  34. package/templates/base-project/.github/workflows/release.yml +67 -0
  35. package/templates/base-project/.railway/railway.ts +40 -0
  36. package/templates/base-project/CLAUDE.md +163 -0
  37. package/templates/base-project/MAIN.md +21 -0
  38. package/templates/base-project/README.md +164 -0
  39. package/templates/base-project/apps/web/.env.example +99 -0
  40. package/templates/base-project/apps/web/Dockerfile +38 -0
  41. package/templates/base-project/apps/web/biome.json +8 -0
  42. package/templates/base-project/apps/web/jsconfig.json +9 -0
  43. package/templates/base-project/apps/web/next.config.js +154 -0
  44. package/templates/base-project/apps/web/package.json +42 -0
  45. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  46. package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
  47. package/templates/base-project/apps/web/public/quark.svg +47 -0
  48. package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
  49. package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
  50. package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
  51. package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
  52. package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
  53. package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
  54. package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
  55. package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
  56. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
  57. package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
  58. package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
  59. package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
  60. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
  61. package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
  62. package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
  63. package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
  64. package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
  65. package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
  66. package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
  67. package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
  68. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  69. package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
  70. package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
  71. package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
  72. package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
  73. package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
  74. package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
  75. package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
  76. package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
  77. package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
  78. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  79. package/templates/base-project/apps/web/src/app/global-error.js +58 -0
  80. package/templates/base-project/apps/web/src/app/globals.css +1166 -0
  81. package/templates/base-project/apps/web/src/app/icon.svg +47 -0
  82. package/templates/base-project/apps/web/src/app/layout.js +57 -0
  83. package/templates/base-project/apps/web/src/app/manifest.js +12 -0
  84. package/templates/base-project/apps/web/src/app/not-found.js +39 -0
  85. package/templates/base-project/apps/web/src/app/page.js +262 -0
  86. package/templates/base-project/apps/web/src/app/robots.js +23 -0
  87. package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
  88. package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
  89. package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
  90. package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
  91. package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
  92. package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
  93. package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
  94. package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
  95. package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
  96. package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
  97. package/templates/base-project/apps/web/src/lib/auth.js +191 -0
  98. package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
  99. package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
  100. package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
  101. package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
  102. package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
  103. package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
  104. package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
  105. package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
  106. package/templates/base-project/apps/web/src/preflight.js +55 -0
  107. package/templates/base-project/apps/web/src/proxy.js +328 -0
  108. package/templates/base-project/biome.json +52 -0
  109. package/templates/base-project/docker-compose.override.yml +17 -0
  110. package/templates/base-project/docker-compose.yml +37 -0
  111. package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
  112. package/templates/base-project/docs/START_HERE.md +70 -0
  113. package/templates/base-project/docs/adr/README.md +42 -0
  114. package/templates/base-project/package.json +58 -0
  115. package/templates/base-project/packages/db/package.json +38 -0
  116. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
  117. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  118. package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
  119. package/templates/base-project/packages/db/prisma/seed.js +152 -0
  120. package/templates/base-project/packages/db/prisma.config.js +27 -0
  121. package/templates/base-project/packages/db/src/client.js +75 -0
  122. package/templates/base-project/packages/db/src/connection.js +44 -0
  123. package/templates/base-project/packages/db/src/context.js +275 -0
  124. package/templates/base-project/packages/db/src/index.js +5 -0
  125. package/templates/base-project/packages/db/src/ping.js +66 -0
  126. package/templates/base-project/packages/db/src/queries.js +292 -0
  127. package/templates/base-project/packages/db/src/schemas.js +30 -0
  128. package/templates/base-project/packages/db/src/umami.js +299 -0
  129. package/templates/base-project/pnpm-workspace.yaml +20 -0
  130. package/templates/base-project/scripts/check-loading.mjs +49 -0
  131. package/templates/base-project/scripts/check-standards.mjs +186 -0
  132. package/templates/base-project/scripts/clean-workspace.mjs +479 -0
  133. package/templates/base-project/scripts/dev-preflight.mjs +48 -0
  134. package/templates/base-project/scripts/doctor.js +408 -0
  135. package/templates/base-project/scripts/prepare.js +490 -0
  136. package/templates/base-project/scripts/run-tests.mjs +71 -0
  137. package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
  138. package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
  139. package/templates/base-project/skills/add-model/SKILL.md +28 -0
  140. package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
  141. package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
  142. package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
  143. package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
  144. package/templates/base-project/skills/i18n/SKILL.md +211 -0
  145. package/templates/base-project/skills/payment/SKILL.md +280 -0
  146. package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
  147. package/templates/base-project/turbo.json +143 -0
  148. package/templates/config/package.json +20 -0
  149. package/templates/config/src/app-url.js +134 -0
  150. package/templates/config/src/environment.js +284 -0
  151. package/templates/config/src/index.js +16 -0
  152. package/templates/config/src/load-config.js +157 -0
  153. package/templates/config/src/rate-limit.js +136 -0
  154. package/templates/config/src/redis.js +43 -0
  155. package/templates/config/src/validate-env.js +532 -0
  156. package/templates/jobs/README.md +52 -0
  157. package/templates/jobs/package.json +9 -0
  158. package/templates/jobs/src/definitions.js +13 -0
  159. package/templates/jobs/src/index.js +1 -0
  160. package/templates/mobile/.env.example +16 -0
  161. package/templates/mobile/CHECKLIST.md +40 -0
  162. package/templates/mobile/README.md +130 -0
  163. package/templates/mobile/app/(app)/_layout.tsx +83 -0
  164. package/templates/mobile/app/(app)/index.tsx +147 -0
  165. package/templates/mobile/app/(app)/profile.tsx +204 -0
  166. package/templates/mobile/app/(app)/settings.tsx +80 -0
  167. package/templates/mobile/app/(auth)/_layout.tsx +10 -0
  168. package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
  169. package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
  170. package/templates/mobile/app/+not-found.tsx +32 -0
  171. package/templates/mobile/app/_layout.tsx +18 -0
  172. package/templates/mobile/app.json +47 -0
  173. package/templates/mobile/assets/adaptive-icon.png +0 -0
  174. package/templates/mobile/assets/icon.png +0 -0
  175. package/templates/mobile/assets/notification-icon.png +0 -0
  176. package/templates/mobile/assets/splash.png +0 -0
  177. package/templates/mobile/biome.json +27 -0
  178. package/templates/mobile/components/AuthGate.tsx +19 -0
  179. package/templates/mobile/components/ThemeProvider.tsx +23 -0
  180. package/templates/mobile/eas.json +29 -0
  181. package/templates/mobile/hooks/use-auth.ts +127 -0
  182. package/templates/mobile/lib/api-client.ts +138 -0
  183. package/templates/mobile/lib/api.ts +83 -0
  184. package/templates/mobile/lib/auth-providers-google.ts +74 -0
  185. package/templates/mobile/lib/auth-providers.ts +74 -0
  186. package/templates/mobile/lib/auth.ts +100 -0
  187. package/templates/mobile/lib/config.ts +14 -0
  188. package/templates/mobile/lib/errors.ts +46 -0
  189. package/templates/mobile/lib/notifications.ts +77 -0
  190. package/templates/mobile/lib/storage.ts +25 -0
  191. package/templates/mobile/lib/theme-tokens.ts +16 -0
  192. package/templates/mobile/package.json +37 -0
  193. package/templates/mobile/tsconfig.json +10 -0
  194. package/templates/pwa/app/_components/PWARegister.js +15 -0
  195. package/templates/pwa/app/manifest.js +32 -0
  196. package/templates/pwa/public/sw.js +72 -0
  197. package/templates/ui/README.md +44 -0
  198. package/templates/ui/package.json +21 -0
  199. package/templates/ui/src/badge.js +23 -0
  200. package/templates/ui/src/button.js +111 -0
  201. package/templates/ui/src/card.js +128 -0
  202. package/templates/ui/src/checkbox.js +20 -0
  203. package/templates/ui/src/container.js +13 -0
  204. package/templates/ui/src/dialog.js +137 -0
  205. package/templates/ui/src/error-banner.js +15 -0
  206. package/templates/ui/src/footer.js +239 -0
  207. package/templates/ui/src/form-field.integration-test.js +142 -0
  208. package/templates/ui/src/form-field.js +50 -0
  209. package/templates/ui/src/index.js +24 -0
  210. package/templates/ui/src/input.js +11 -0
  211. package/templates/ui/src/label.js +14 -0
  212. package/templates/ui/src/lightbox.integration-test.js +167 -0
  213. package/templates/ui/src/lightbox.js +243 -0
  214. package/templates/ui/src/logo.js +52 -0
  215. package/templates/ui/src/navbar.js +602 -0
  216. package/templates/ui/src/password-input.js +31 -0
  217. package/templates/ui/src/rich-text.js +514 -0
  218. package/templates/ui/src/select.js +317 -0
  219. package/templates/ui/src/skeleton.js +10 -0
  220. package/templates/ui/src/spinner.js +31 -0
  221. package/templates/ui/src/table.js +169 -0
  222. package/templates/ui/src/textarea.js +11 -0
  223. package/templates/ui/src/theme-constants.js +24 -0
  224. package/templates/ui/src/theme.js +196 -0
  225. package/templates/ui/src/toast.js +206 -0
  226. package/templates/worker/Dockerfile +64 -0
  227. package/templates/worker/README.md +56 -0
  228. package/templates/worker/package.json +36 -0
  229. package/templates/worker/src/config/pricing.js +46 -0
  230. package/templates/worker/src/handlers/email-job-validation.js +36 -0
  231. package/templates/worker/src/handlers/email.js +98 -0
  232. package/templates/worker/src/handlers/files.js +59 -0
  233. package/templates/worker/src/handlers/index.js +20 -0
  234. package/templates/worker/src/handlers/push-notification.js +234 -0
  235. package/templates/worker/src/index.js +505 -0
  236. package/templates/worker/src/lib/railway.js +438 -0
  237. package/templates/worker/src/lib/tokens.js +81 -0
@@ -0,0 +1,5 @@
1
+ import { loadConfig } from "@techstream/quark-config";
2
+
3
+ export function isSignupEnabled() {
4
+ return loadConfig().auth.allowSignup;
5
+ }
@@ -0,0 +1,191 @@
1
+ import { PrismaAdapter } from "@auth/prisma-adapter";
2
+ import {
3
+ createAuthConfig,
4
+ createLogger,
5
+ verifyPassword,
6
+ } from "@techstream/quark-core";
7
+ import { Prisma, prisma, user } from "@techstream/quark-db";
8
+ import NextAuth from "next-auth";
9
+ import CredentialsProvider from "next-auth/providers/credentials";
10
+ import GithubProvider from "next-auth/providers/github";
11
+ import GoogleProvider from "next-auth/providers/google";
12
+
13
+ const logger = createLogger({ name: "auth" });
14
+ const STALE_SESSION_PATTERN = /jwtsessionerror|no matching decryption secret/i;
15
+
16
+ function describeAuthIssue(issue, seen = new Set()) {
17
+ if (!issue) {
18
+ return "";
19
+ }
20
+
21
+ if (typeof issue === "string") {
22
+ return issue;
23
+ }
24
+
25
+ if (typeof issue === "number" || typeof issue === "boolean") {
26
+ return String(issue);
27
+ }
28
+
29
+ if (typeof issue !== "object" || seen.has(issue)) {
30
+ return "";
31
+ }
32
+
33
+ seen.add(issue);
34
+
35
+ const parts = [];
36
+ for (const key of ["name", "type", "code", "message"]) {
37
+ const value = issue[key];
38
+ if (typeof value === "string" && value) {
39
+ parts.push(value);
40
+ }
41
+ }
42
+
43
+ for (const key of ["cause", "error", "details"]) {
44
+ const nested = describeAuthIssue(issue[key], seen);
45
+ if (nested) {
46
+ parts.push(nested);
47
+ }
48
+ }
49
+
50
+ return parts.join(" ");
51
+ }
52
+
53
+ function isIgnorableSessionError(issue) {
54
+ return STALE_SESSION_PATTERN.test(describeAuthIssue(issue));
55
+ }
56
+
57
+ const providers = [
58
+ CredentialsProvider({
59
+ name: "Credentials",
60
+ credentials: {
61
+ email: { label: "Email", type: "email" },
62
+ password: { label: "Password", type: "password" },
63
+ },
64
+ async authorize(credentials) {
65
+ if (!credentials?.email || !credentials?.password) {
66
+ return null;
67
+ }
68
+
69
+ try {
70
+ const existingUser = await user.findByEmail(credentials.email);
71
+
72
+ if (!existingUser?.password) {
73
+ return null;
74
+ }
75
+
76
+ const isValid = await verifyPassword(
77
+ credentials.password,
78
+ existingUser.password,
79
+ );
80
+
81
+ if (!isValid) {
82
+ return null;
83
+ }
84
+
85
+ return {
86
+ id: existingUser.id,
87
+ email: existingUser.email,
88
+ name: existingUser.name,
89
+ image: existingUser.image,
90
+ role: existingUser.role,
91
+ };
92
+ } catch (err) {
93
+ // Re-throw if the database is unreachable so Auth.js surfaces a server error
94
+ // rather than "Invalid credentials", which misleads both user and developer.
95
+ // PrismaClientInitializationError: client could not connect on startup.
96
+ // PrismaClientKnownRequestError P1xxx: connection lost / timeout mid-request.
97
+ const isDbDown =
98
+ err instanceof Prisma.PrismaClientInitializationError ||
99
+ (err instanceof Prisma.PrismaClientKnownRequestError &&
100
+ err.errorCode?.startsWith("P1"));
101
+ if (isDbDown) {
102
+ logger.error("Database unavailable during authentication", {
103
+ errorCode: err.errorCode,
104
+ });
105
+ throw err;
106
+ }
107
+ // For all other unexpected errors, log and fail closed.
108
+ logger.error("Auth authorize error", {
109
+ message: err?.message ?? String(err),
110
+ });
111
+ return null;
112
+ }
113
+ },
114
+ }),
115
+ ];
116
+
117
+ if (process.env.GITHUB_ID && process.env.GITHUB_SECRET) {
118
+ providers.push(
119
+ GithubProvider({
120
+ clientId: process.env.GITHUB_ID,
121
+ clientSecret: process.env.GITHUB_SECRET,
122
+ }),
123
+ );
124
+ }
125
+
126
+ if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
127
+ providers.push(
128
+ GoogleProvider({
129
+ clientId: process.env.GOOGLE_CLIENT_ID,
130
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
131
+ }),
132
+ );
133
+ }
134
+
135
+ export function getAuthOptions() {
136
+ return createAuthConfig({
137
+ adapter: PrismaAdapter(prisma),
138
+ providers: providers,
139
+ logger: {
140
+ error(...issues) {
141
+ if (issues.some(isIgnorableSessionError)) {
142
+ return;
143
+ }
144
+
145
+ logger.error("Auth.js error", {
146
+ details: issues.map((issue) => describeAuthIssue(issue)).join(" | "),
147
+ });
148
+ },
149
+ },
150
+ session: {
151
+ strategy: "jwt",
152
+ },
153
+ });
154
+ }
155
+
156
+ let authInstance = null;
157
+
158
+ function getAuthInstance() {
159
+ if (!authInstance) {
160
+ authInstance = NextAuth(getAuthOptions());
161
+ }
162
+ return authInstance;
163
+ }
164
+
165
+ export function getAuth() {
166
+ return getAuthInstance();
167
+ }
168
+
169
+ export async function auth() {
170
+ try {
171
+ return await getAuthInstance().auth();
172
+ } catch (error) {
173
+ if (isIgnorableSessionError(error)) {
174
+ return null;
175
+ }
176
+
177
+ throw error;
178
+ }
179
+ }
180
+
181
+ export const handlers = new Proxy(
182
+ {},
183
+ {
184
+ get(_target, prop) {
185
+ return getAuthInstance().handlers[prop];
186
+ },
187
+ },
188
+ );
189
+
190
+ export const signIn = (...args) => getAuthInstance().signIn(...args);
191
+ export const signOut = (...args) => getAuthInstance().signOut(...args);
@@ -0,0 +1,11 @@
1
+ import { shouldUseSecureAuthCookie } from "./proxy-auth.js";
2
+
3
+ export function getCsrfCookieOptions(request) {
4
+ return {
5
+ httpOnly: true,
6
+ secure: shouldUseSecureAuthCookie(request),
7
+ sameSite: "strict",
8
+ path: "/",
9
+ maxAge: 60 * 60,
10
+ };
11
+ }
@@ -0,0 +1,84 @@
1
+ import { AppError, getAuthSecret } from "@techstream/quark-core";
2
+ import { jwtVerify, SignJWT } from "jose";
3
+
4
+ const ISSUER = "quark-mobile";
5
+ const ACCESS_TOKEN_EXPIRY = "1h";
6
+ const REFRESH_TOKEN_EXPIRY = "30d";
7
+
8
+ /**
9
+ * Get the shared secret key for signing/verifying mobile JWTs.
10
+ * Returns null if NEXTAUTH_SECRET is not configured.
11
+ */
12
+ function getSecretKey() {
13
+ const secret = getAuthSecret();
14
+ if (!secret) return null;
15
+ return new TextEncoder().encode(secret);
16
+ }
17
+
18
+ /**
19
+ * Issue an access + refresh token pair for a user.
20
+ * @param {{ id: string, email: string, name: string | null, role: string }} user
21
+ * @returns {{ token: string, refreshToken: string, expiresAt: string }}
22
+ */
23
+ export async function issueTokenPair(user) {
24
+ const secretKey = getSecretKey();
25
+ if (!secretKey) throw new AppError("NEXTAUTH_SECRET not configured", 500);
26
+
27
+ const now = Math.floor(Date.now() / 1000);
28
+
29
+ const [token, refreshToken] = await Promise.all([
30
+ new SignJWT({
31
+ sub: user.id,
32
+ email: user.email,
33
+ name: user.name,
34
+ role: user.role,
35
+ })
36
+ .setProtectedHeader({ alg: "HS256" })
37
+ .setIssuedAt(now)
38
+ .setExpirationTime(ACCESS_TOKEN_EXPIRY)
39
+ .setIssuer(ISSUER)
40
+ .sign(secretKey),
41
+ new SignJWT({
42
+ sub: user.id,
43
+ type: "refresh",
44
+ })
45
+ .setProtectedHeader({ alg: "HS256" })
46
+ .setIssuedAt(now)
47
+ .setExpirationTime(REFRESH_TOKEN_EXPIRY)
48
+ .setIssuer(ISSUER)
49
+ .sign(secretKey),
50
+ ]);
51
+
52
+ const expiresAt = new Date(now * 1000 + 60 * 60 * 1000).toISOString();
53
+ return { token, refreshToken, expiresAt };
54
+ }
55
+
56
+ /**
57
+ * Verify a mobile JWT and return its payload.
58
+ * @param {string} token
59
+ * @returns {Promise<import("jose").JWTPayload | null>} payload or null if invalid
60
+ */
61
+ export async function verifyMobileToken(token) {
62
+ const secretKey = getSecretKey();
63
+ if (!secretKey) return null;
64
+
65
+ try {
66
+ const result = await jwtVerify(token, secretKey, { issuer: ISSUER });
67
+ return result.payload;
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Extract and verify a Bearer token from a Request's Authorization header.
75
+ * @param {Request} request
76
+ * @returns {Promise<import("jose").JWTPayload | null>} payload or null if missing/invalid
77
+ */
78
+ export async function extractBearerPayload(request) {
79
+ const authHeader = request.headers.get("authorization");
80
+ if (!authHeader?.startsWith("Bearer ")) return null;
81
+
82
+ const token = authHeader.slice(7);
83
+ return verifyMobileToken(token);
84
+ }
@@ -0,0 +1,35 @@
1
+ import { access } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const CRM_PATHS = [
5
+ "packages/crm/src/config.js",
6
+ "../../packages/crm/src/config.js",
7
+ "../../../../../../packages/crm/src/config.js",
8
+ ];
9
+
10
+ let crmAvailable;
11
+
12
+ async function checkCrmExists() {
13
+ for (const relativePath of CRM_PATHS) {
14
+ try {
15
+ await access(path.resolve(process.cwd(), relativePath));
16
+ return true;
17
+ } catch {}
18
+ }
19
+ return false;
20
+ }
21
+
22
+ export async function loadCrmConfig() {
23
+ if (crmAvailable === undefined) {
24
+ crmAvailable = await checkCrmExists();
25
+ }
26
+ return crmAvailable ? { present: true } : null;
27
+ }
28
+
29
+ export async function hasCrmFeature() {
30
+ return Boolean(await loadCrmConfig());
31
+ }
32
+
33
+ export function resetCrmConfigCache() {
34
+ crmAvailable = undefined;
35
+ }
@@ -0,0 +1,33 @@
1
+ import { NextRequest } from "next/server.js";
2
+
3
+ export function normalizeAuthRequest(request) {
4
+ if (process.env.NODE_ENV !== "development") {
5
+ return request;
6
+ }
7
+
8
+ const forwardedHost = request.headers.get("x-forwarded-host");
9
+ const host = request.headers.get("host");
10
+ const incomingHost = forwardedHost || host;
11
+
12
+ if (!incomingHost) {
13
+ return request;
14
+ }
15
+
16
+ const forwardedProto = request.headers.get("x-forwarded-proto");
17
+ const incomingProtocol = forwardedProto
18
+ ? forwardedProto.split(",")[0].trim()
19
+ : "http";
20
+ const currentUrl = new URL(request.url);
21
+ const currentOrigin = `${currentUrl.protocol}//${currentUrl.host}`;
22
+ const incomingOrigin = `${incomingProtocol}://${incomingHost}`;
23
+
24
+ if (currentOrigin === incomingOrigin) {
25
+ return request;
26
+ }
27
+
28
+ const normalizedUrl = new URL(request.url);
29
+ normalizedUrl.protocol = `${incomingProtocol}:`;
30
+ normalizedUrl.host = incomingHost;
31
+
32
+ return new NextRequest(normalizedUrl, request);
33
+ }
@@ -0,0 +1,80 @@
1
+ import { getAuthSecret } from "@techstream/quark-core/auth";
2
+ import { getToken } from "next-auth/jwt";
3
+
4
+ const STRICT_AUTH_RATE_LIMIT_ROUTES = new Set([
5
+ "/api/auth/callback/credentials",
6
+ "/api/auth/register",
7
+ "/api/auth/signin/credentials",
8
+ ]);
9
+
10
+ function getForwardedProtocol(request) {
11
+ const forwardedProto = request.headers.get("x-forwarded-proto");
12
+ if (!forwardedProto) return null;
13
+
14
+ const [protocol] = forwardedProto
15
+ .split(",")
16
+ .map((value) => value.trim())
17
+ .filter(Boolean);
18
+
19
+ if (!protocol) return null;
20
+ return protocol.endsWith(":") ? protocol : `${protocol}:`;
21
+ }
22
+
23
+ function getFallbackProtocol() {
24
+ const rawUrl =
25
+ process.env.AUTH_URL || process.env.NEXTAUTH_URL || process.env.APP_URL;
26
+
27
+ if (!rawUrl) return "http:";
28
+
29
+ try {
30
+ return new URL(rawUrl).protocol;
31
+ } catch {
32
+ return "http:";
33
+ }
34
+ }
35
+
36
+ export function getRequestProtocol(request) {
37
+ return (
38
+ getForwardedProtocol(request) ||
39
+ request.nextUrl?.protocol ||
40
+ getFallbackProtocol()
41
+ );
42
+ }
43
+
44
+ export function shouldUseSecureAuthCookie(request) {
45
+ const forwardedProtocol = getForwardedProtocol(request);
46
+ if (forwardedProtocol) {
47
+ return forwardedProtocol === "https:";
48
+ }
49
+
50
+ return getRequestProtocol(request) === "https:";
51
+ }
52
+
53
+ export async function getProxyToken(request) {
54
+ const secret = getAuthSecret();
55
+ if (!secret) return null;
56
+
57
+ const secureCookiePreferences = shouldUseSecureAuthCookie(request)
58
+ ? [true, false]
59
+ : [false, true];
60
+
61
+ for (const secureCookie of secureCookiePreferences) {
62
+ const token = await getToken({
63
+ req: request,
64
+ secret,
65
+ secureCookie,
66
+ });
67
+
68
+ if (token) {
69
+ return token;
70
+ }
71
+ }
72
+
73
+ return null;
74
+ }
75
+
76
+ export function getRateLimitBucket(pathname, method = "GET") {
77
+ return method === "POST" && STRICT_AUTH_RATE_LIMIT_ROUTES.has(pathname)
78
+ ? "auth"
79
+ : "api";
80
+ }
@@ -0,0 +1,25 @@
1
+ export function isWebsiteIndexable(env = process.env) {
2
+ const productionEnv = (env.NODE_ENV || "").toLowerCase() === "production";
3
+ const explicitAllow = (env.ALLOW_INDEXING || "").toLowerCase() === "true";
4
+ return productionEnv && explicitAllow;
5
+ }
6
+
7
+ export function getMetadataRobots(env = process.env) {
8
+ if (isWebsiteIndexable(env)) {
9
+ return {
10
+ index: true,
11
+ follow: true,
12
+ };
13
+ }
14
+
15
+ return {
16
+ index: false,
17
+ follow: false,
18
+ nocache: true,
19
+ googleBot: {
20
+ index: false,
21
+ follow: false,
22
+ noimageindex: true,
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,74 @@
1
+ import { config, getAppUrl } from "@techstream/quark-config";
2
+ import { getMetadataRobots } from "./indexing.js";
3
+
4
+ export function getSiteMetadata() {
5
+ const appUrl = getAppUrl();
6
+ const { appName, appDescription } = config;
7
+
8
+ return {
9
+ metadataBase: new URL(appUrl),
10
+ title: {
11
+ default: appName,
12
+ template: `%s | ${appName}`,
13
+ },
14
+ description: appDescription,
15
+ applicationName: appName,
16
+ alternates: {
17
+ canonical: "/",
18
+ },
19
+ openGraph: {
20
+ type: "website",
21
+ url: appUrl,
22
+ title: appName,
23
+ description: appDescription,
24
+ siteName: appName,
25
+ },
26
+ twitter: {
27
+ card: "summary",
28
+ title: appName,
29
+ description: appDescription,
30
+ },
31
+ robots: getMetadataRobots(),
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Build per-page metadata. Titles are rendered as `{ absolute }` so the
37
+ * root layout template (`%s | Brand`) is applied exactly once.
38
+ *
39
+ * Page-title conventions (see the `seo` skill): keyword-first for
40
+ * money/service pages, `Entity | Brand` for detail pages, 50-60 chars.
41
+ * The SEO title may differ from the visible H1 on CMS-driven pages.
42
+ *
43
+ * @param {object} options
44
+ * @param {string} options.title - SEO title (brand suffix appended).
45
+ * @param {string} [options.description] - Meta description (150-160 chars).
46
+ * @param {string} [options.path] - Route path used for the canonical URL.
47
+ * @param {boolean} [options.noIndex=false] - Force noindex/noindex robots.
48
+ */
49
+ export function getPageMetadata({ title, description, path, noIndex = false }) {
50
+ const appUrl = getAppUrl();
51
+ const { appName, appDescription } = config;
52
+ const resolvedDescription = description ?? appDescription;
53
+
54
+ return {
55
+ title: {
56
+ absolute: `${title} | ${appName}`,
57
+ },
58
+ description: resolvedDescription,
59
+ alternates: path ? { canonical: path } : undefined,
60
+ openGraph: {
61
+ type: "website",
62
+ url: path ? new URL(path, appUrl).toString() : appUrl,
63
+ title,
64
+ description: resolvedDescription,
65
+ siteName: appName,
66
+ },
67
+ twitter: {
68
+ card: "summary",
69
+ title,
70
+ description: resolvedDescription,
71
+ },
72
+ robots: noIndex ? { index: false, follow: false } : getMetadataRobots(),
73
+ };
74
+ }
@@ -0,0 +1,22 @@
1
+ export const STATIC_SITEMAP_ROUTES = [
2
+ { path: "/", changeFrequency: "daily", priority: 1 },
3
+ ];
4
+
5
+ export function buildSitemapEntries({
6
+ appUrl,
7
+ staticRoutes = STATIC_SITEMAP_ROUTES,
8
+ publicContentEntries = [],
9
+ }) {
10
+ return [
11
+ ...staticRoutes.map((route) => ({
12
+ url: `${appUrl}${route.path}`,
13
+ changeFrequency: route.changeFrequency,
14
+ priority: route.priority,
15
+ })),
16
+ ...publicContentEntries.map((entry) => ({
17
+ url: `${appUrl}${entry.path}`,
18
+ changeFrequency: entry.changeFrequency,
19
+ priority: entry.priority,
20
+ })),
21
+ ];
22
+ }
@@ -0,0 +1,55 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { loadConfig } from "@techstream/quark-config";
4
+ import { createLogger } from "@techstream/quark-core";
5
+ import { pingDatabase } from "@techstream/quark-db";
6
+
7
+ const logger = createLogger("web-preflight");
8
+
9
+ export const DEFAULT_DEV_PREFLIGHT_TIMEOUT = 1500;
10
+
11
+ export async function runWebPreflight({
12
+ loadAppConfig = () => loadConfig({}, { fresh: true }),
13
+ ping = pingDatabase,
14
+ timeout = DEFAULT_DEV_PREFLIGHT_TIMEOUT,
15
+ } = {}) {
16
+ loadAppConfig();
17
+ return ping({ timeout });
18
+ }
19
+
20
+ export function formatWebPreflightFailure(result) {
21
+ return [
22
+ `Database preflight failed: ${result.message}`,
23
+ "Start PostgreSQL or check DATABASE_URL / POSTGRES_* before running `pnpm dev`.",
24
+ ].join("\n");
25
+ }
26
+
27
+ async function main() {
28
+ if (process.env.QUARK_SKIP_DEV_PREFLIGHT === "1") {
29
+ logger.info("Skipping web dev preflight (QUARK_SKIP_DEV_PREFLIGHT=1)");
30
+ return;
31
+ }
32
+
33
+ logger.info("Running web dev preflight...");
34
+
35
+ try {
36
+ const result = await runWebPreflight();
37
+
38
+ if (result.status === "ok") {
39
+ logger.info(`Database ready (${result.latencyMs}ms)`);
40
+ return;
41
+ }
42
+
43
+ logger.error(formatWebPreflightFailure(result));
44
+ process.exit(1);
45
+ } catch (error) {
46
+ logger.error(error?.message ?? String(error));
47
+ process.exit(1);
48
+ }
49
+ }
50
+
51
+ const __filename = fileURLToPath(import.meta.url);
52
+
53
+ if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
54
+ void main();
55
+ }