@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.
- package/README.md +210 -0
- package/package.json +56 -0
- package/src/dependabot-config.test.js +128 -0
- package/src/deploy/adapters/iac.js +203 -0
- package/src/deploy/adapters/index.js +27 -0
- package/src/deploy/adapters/railway.js +508 -0
- package/src/deploy/adapters/railway.test.js +318 -0
- package/src/deploy/contract.js +59 -0
- package/src/deploy/deploy.integration.test.js +183 -0
- package/src/deploy/deploy.js +430 -0
- package/src/deploy/deploy.test.js +105 -0
- package/src/deploy/discovery.js +115 -0
- package/src/deploy/discovery.test.js +141 -0
- package/src/deploy/index.js +38 -0
- package/src/deploy/inspect.js +130 -0
- package/src/index.js +2920 -0
- package/src/mobile-template.test.js +232 -0
- package/src/prepare-hooks.test.js +535 -0
- package/src/scaffold-guards.test.js +140 -0
- package/src/scaffold-output.test.js +228 -0
- package/src/template-scripts.test.js +160 -0
- package/src/utils.js +77 -0
- package/src/utils.test.js +95 -0
- package/src/worker-dockerfile.test.js +85 -0
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.dockerignore +14 -0
- package/templates/base-project/.env.railway.example +87 -0
- package/templates/base-project/.github/copilot-instructions.md +65 -0
- package/templates/base-project/.github/dependabot.yml +31 -0
- package/templates/base-project/.github/skills/project-context/SKILL.md +109 -0
- package/templates/base-project/.github/workflows/ci.yml +116 -0
- package/templates/base-project/.github/workflows/dependabot-auto-merge.yml +22 -0
- package/templates/base-project/.github/workflows/install-windows.yml +27 -0
- package/templates/base-project/.github/workflows/release.yml +67 -0
- package/templates/base-project/.railway/railway.ts +40 -0
- package/templates/base-project/CLAUDE.md +163 -0
- package/templates/base-project/MAIN.md +21 -0
- package/templates/base-project/README.md +164 -0
- package/templates/base-project/apps/web/.env.example +99 -0
- package/templates/base-project/apps/web/Dockerfile +38 -0
- package/templates/base-project/apps/web/biome.json +8 -0
- package/templates/base-project/apps/web/jsconfig.json +9 -0
- package/templates/base-project/apps/web/next.config.js +154 -0
- package/templates/base-project/apps/web/package.json +42 -0
- package/templates/base-project/apps/web/postcss.config.mjs +7 -0
- package/templates/base-project/apps/web/public/.well-known/security.txt +3 -0
- package/templates/base-project/apps/web/public/quark.svg +47 -0
- package/templates/base-project/apps/web/scripts/prepare-standalone.mjs +71 -0
- package/templates/base-project/apps/web/src/app/_components/AsciiPanel.js +103 -0
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/_components/SyncedPanels.js +212 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiReplayRecorder.js +198 -0
- package/templates/base-project/apps/web/src/app/_components/UmamiWebVitals.js +39 -0
- package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +7 -0
- package/templates/base-project/apps/web/src/app/api/auth/apple/route.js +82 -0
- package/templates/base-project/apps/web/src/app/api/auth/google/route.js +59 -0
- package/templates/base-project/apps/web/src/app/api/auth/refresh/route.js +48 -0
- package/templates/base-project/apps/web/src/app/api/auth/register/route.js +63 -0
- package/templates/base-project/apps/web/src/app/api/auth/token/route.js +44 -0
- package/templates/base-project/apps/web/src/app/api/csrf/route.js +40 -0
- package/templates/base-project/apps/web/src/app/api/device/register/route.js +57 -0
- package/templates/base-project/apps/web/src/app/api/error-handler.js +23 -0
- package/templates/base-project/apps/web/src/app/api/files/[id]/route.js +89 -0
- package/templates/base-project/apps/web/src/app/api/files/route.js +120 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +160 -0
- package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
- package/templates/base-project/apps/web/src/app/api/users/me/route.js +67 -0
- package/templates/base-project/apps/web/src/app/api/users/route.js +45 -0
- package/templates/base-project/apps/web/src/app/auth/error/page.js +83 -0
- package/templates/base-project/apps/web/src/app/auth/forgot-password/page.js +58 -0
- package/templates/base-project/apps/web/src/app/auth/provider-loading.js +25 -0
- package/templates/base-project/apps/web/src/app/auth/register/RegisterPageClient.js +340 -0
- package/templates/base-project/apps/web/src/app/auth/register/page.js +17 -0
- package/templates/base-project/apps/web/src/app/auth/signin/SignInPageClient.js +230 -0
- package/templates/base-project/apps/web/src/app/auth/signin/page.js +10 -0
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +58 -0
- package/templates/base-project/apps/web/src/app/globals.css +1166 -0
- package/templates/base-project/apps/web/src/app/icon.svg +47 -0
- package/templates/base-project/apps/web/src/app/layout.js +57 -0
- package/templates/base-project/apps/web/src/app/manifest.js +12 -0
- package/templates/base-project/apps/web/src/app/not-found.js +39 -0
- package/templates/base-project/apps/web/src/app/page.js +262 -0
- package/templates/base-project/apps/web/src/app/robots.js +23 -0
- package/templates/base-project/apps/web/src/app/sitemap.js +14 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-before-send.js +29 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-config.js +95 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-csp.js +24 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-marketing.js +131 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-replay.js +204 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami-web-vitals.js +237 -0
- package/templates/base-project/apps/web/src/lib/analytics/umami.js +57 -0
- package/templates/base-project/apps/web/src/lib/auth-middleware.js +82 -0
- package/templates/base-project/apps/web/src/lib/auth-signup.js +5 -0
- package/templates/base-project/apps/web/src/lib/auth.js +191 -0
- package/templates/base-project/apps/web/src/lib/csrf-cookie.js +11 -0
- package/templates/base-project/apps/web/src/lib/jwt.js +84 -0
- package/templates/base-project/apps/web/src/lib/load-crm-config.js +35 -0
- package/templates/base-project/apps/web/src/lib/normalize-auth-request.js +33 -0
- package/templates/base-project/apps/web/src/lib/proxy-auth.js +80 -0
- package/templates/base-project/apps/web/src/lib/seo/indexing.js +25 -0
- package/templates/base-project/apps/web/src/lib/seo/site-metadata.js +74 -0
- package/templates/base-project/apps/web/src/lib/sitemap-entries.js +22 -0
- package/templates/base-project/apps/web/src/preflight.js +55 -0
- package/templates/base-project/apps/web/src/proxy.js +328 -0
- package/templates/base-project/biome.json +52 -0
- package/templates/base-project/docker-compose.override.yml +17 -0
- package/templates/base-project/docker-compose.yml +37 -0
- package/templates/base-project/docs/FIRST_FEATURE.md +74 -0
- package/templates/base-project/docs/START_HERE.md +70 -0
- package/templates/base-project/docs/adr/README.md +42 -0
- package/templates/base-project/package.json +58 -0
- package/templates/base-project/packages/db/package.json +38 -0
- package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +202 -0
- package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
- package/templates/base-project/packages/db/prisma/schema.prisma +161 -0
- package/templates/base-project/packages/db/prisma/seed.js +152 -0
- package/templates/base-project/packages/db/prisma.config.js +27 -0
- package/templates/base-project/packages/db/src/client.js +75 -0
- package/templates/base-project/packages/db/src/connection.js +44 -0
- package/templates/base-project/packages/db/src/context.js +275 -0
- package/templates/base-project/packages/db/src/index.js +5 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/packages/db/src/queries.js +292 -0
- package/templates/base-project/packages/db/src/schemas.js +30 -0
- package/templates/base-project/packages/db/src/umami.js +299 -0
- package/templates/base-project/pnpm-workspace.yaml +20 -0
- package/templates/base-project/scripts/check-loading.mjs +49 -0
- package/templates/base-project/scripts/check-standards.mjs +186 -0
- package/templates/base-project/scripts/clean-workspace.mjs +479 -0
- package/templates/base-project/scripts/dev-preflight.mjs +48 -0
- package/templates/base-project/scripts/doctor.js +408 -0
- package/templates/base-project/scripts/prepare.js +490 -0
- package/templates/base-project/scripts/run-tests.mjs +71 -0
- package/templates/base-project/skills/add-dashboard/SKILL.md +28 -0
- package/templates/base-project/skills/add-endpoint/SKILL.md +29 -0
- package/templates/base-project/skills/add-model/SKILL.md +28 -0
- package/templates/base-project/skills/ecommerce/SKILL.md +224 -0
- package/templates/base-project/skills/ecommerce-cart/SKILL.md +193 -0
- package/templates/base-project/skills/ecommerce-catalog/SKILL.md +179 -0
- package/templates/base-project/skills/ecommerce-checkout/SKILL.md +195 -0
- package/templates/base-project/skills/i18n/SKILL.md +211 -0
- package/templates/base-project/skills/payment/SKILL.md +280 -0
- package/templates/base-project/skills/quark-skills/SKILL.md +37 -0
- package/templates/base-project/turbo.json +143 -0
- package/templates/config/package.json +20 -0
- package/templates/config/src/app-url.js +134 -0
- package/templates/config/src/environment.js +284 -0
- package/templates/config/src/index.js +16 -0
- package/templates/config/src/load-config.js +157 -0
- package/templates/config/src/rate-limit.js +136 -0
- package/templates/config/src/redis.js +43 -0
- package/templates/config/src/validate-env.js +532 -0
- package/templates/jobs/README.md +52 -0
- package/templates/jobs/package.json +9 -0
- package/templates/jobs/src/definitions.js +13 -0
- package/templates/jobs/src/index.js +1 -0
- package/templates/mobile/.env.example +16 -0
- package/templates/mobile/CHECKLIST.md +40 -0
- package/templates/mobile/README.md +130 -0
- package/templates/mobile/app/(app)/_layout.tsx +83 -0
- package/templates/mobile/app/(app)/index.tsx +147 -0
- package/templates/mobile/app/(app)/profile.tsx +204 -0
- package/templates/mobile/app/(app)/settings.tsx +80 -0
- package/templates/mobile/app/(auth)/_layout.tsx +10 -0
- package/templates/mobile/app/(auth)/sign-in.tsx +140 -0
- package/templates/mobile/app/(auth)/sign-up.tsx +178 -0
- package/templates/mobile/app/+not-found.tsx +32 -0
- package/templates/mobile/app/_layout.tsx +18 -0
- package/templates/mobile/app.json +47 -0
- package/templates/mobile/assets/adaptive-icon.png +0 -0
- package/templates/mobile/assets/icon.png +0 -0
- package/templates/mobile/assets/notification-icon.png +0 -0
- package/templates/mobile/assets/splash.png +0 -0
- package/templates/mobile/biome.json +27 -0
- package/templates/mobile/components/AuthGate.tsx +19 -0
- package/templates/mobile/components/ThemeProvider.tsx +23 -0
- package/templates/mobile/eas.json +29 -0
- package/templates/mobile/hooks/use-auth.ts +127 -0
- package/templates/mobile/lib/api-client.ts +138 -0
- package/templates/mobile/lib/api.ts +83 -0
- package/templates/mobile/lib/auth-providers-google.ts +74 -0
- package/templates/mobile/lib/auth-providers.ts +74 -0
- package/templates/mobile/lib/auth.ts +100 -0
- package/templates/mobile/lib/config.ts +14 -0
- package/templates/mobile/lib/errors.ts +46 -0
- package/templates/mobile/lib/notifications.ts +77 -0
- package/templates/mobile/lib/storage.ts +25 -0
- package/templates/mobile/lib/theme-tokens.ts +16 -0
- package/templates/mobile/package.json +37 -0
- package/templates/mobile/tsconfig.json +10 -0
- package/templates/pwa/app/_components/PWARegister.js +15 -0
- package/templates/pwa/app/manifest.js +32 -0
- package/templates/pwa/public/sw.js +72 -0
- package/templates/ui/README.md +44 -0
- package/templates/ui/package.json +21 -0
- package/templates/ui/src/badge.js +23 -0
- package/templates/ui/src/button.js +111 -0
- package/templates/ui/src/card.js +128 -0
- package/templates/ui/src/checkbox.js +20 -0
- package/templates/ui/src/container.js +13 -0
- package/templates/ui/src/dialog.js +137 -0
- package/templates/ui/src/error-banner.js +15 -0
- package/templates/ui/src/footer.js +239 -0
- package/templates/ui/src/form-field.integration-test.js +142 -0
- package/templates/ui/src/form-field.js +50 -0
- package/templates/ui/src/index.js +24 -0
- package/templates/ui/src/input.js +11 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/lightbox.integration-test.js +167 -0
- package/templates/ui/src/lightbox.js +243 -0
- package/templates/ui/src/logo.js +52 -0
- package/templates/ui/src/navbar.js +602 -0
- package/templates/ui/src/password-input.js +31 -0
- package/templates/ui/src/rich-text.js +514 -0
- package/templates/ui/src/select.js +317 -0
- package/templates/ui/src/skeleton.js +10 -0
- package/templates/ui/src/spinner.js +31 -0
- package/templates/ui/src/table.js +169 -0
- package/templates/ui/src/textarea.js +11 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +196 -0
- package/templates/ui/src/toast.js +206 -0
- package/templates/worker/Dockerfile +64 -0
- package/templates/worker/README.md +56 -0
- package/templates/worker/package.json +36 -0
- package/templates/worker/src/config/pricing.js +46 -0
- package/templates/worker/src/handlers/email-job-validation.js +36 -0
- package/templates/worker/src/handlers/email.js +98 -0
- package/templates/worker/src/handlers/files.js +59 -0
- package/templates/worker/src/handlers/index.js +20 -0
- package/templates/worker/src/handlers/push-notification.js +234 -0
- package/templates/worker/src/index.js +505 -0
- package/templates/worker/src/lib/railway.js +438 -0
- package/templates/worker/src/lib/tokens.js +81 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js Proxy
|
|
3
|
+
* Handles auth guards, rate limiting, CORS, and security headers.
|
|
4
|
+
*
|
|
5
|
+
* Guards (in order):
|
|
6
|
+
* 1. Metrics guard - optionally protects /api/metrics with a bearer
|
|
7
|
+
* token. Set METRICS_TOKEN in env to enable.
|
|
8
|
+
* 2. Rate limiting, CORS, security headers for all API routes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { getAllowedOrigins } from "@techstream/quark-config/app-url";
|
|
12
|
+
import { NextResponse } from "next/server";
|
|
13
|
+
|
|
14
|
+
import { buildContentSecurityPolicy } from "./lib/analytics/umami-csp.js";
|
|
15
|
+
import { getRateLimitBucket } from "./lib/proxy-auth";
|
|
16
|
+
|
|
17
|
+
// ─── 1. Metrics guard ────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function metricsGuard(request) {
|
|
20
|
+
const { pathname } = request.nextUrl;
|
|
21
|
+
if (pathname !== "/api/metrics") return null;
|
|
22
|
+
|
|
23
|
+
// If METRICS_TOKEN is not configured, the endpoint is unprotected (existing
|
|
24
|
+
// behaviour preserved). Set METRICS_TOKEN in env to enable protection.
|
|
25
|
+
const expectedToken = process.env.METRICS_TOKEN;
|
|
26
|
+
if (!expectedToken) return null;
|
|
27
|
+
|
|
28
|
+
const authHeader = request.headers.get("authorization") ?? "";
|
|
29
|
+
const providedToken = authHeader.startsWith("Bearer ")
|
|
30
|
+
? authHeader.slice(7)
|
|
31
|
+
: (request.headers.get("x-metrics-token") ?? "");
|
|
32
|
+
|
|
33
|
+
if (providedToken !== expectedToken) {
|
|
34
|
+
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return null; // authorised - continue
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Simple in-memory rate limiter (use Redis for production)
|
|
41
|
+
const rateLimit = new Map();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve the client IP for rate-limit keying.
|
|
45
|
+
*
|
|
46
|
+
* `NextRequest.ip` was removed in Next 15, so `request.ip` is always
|
|
47
|
+
* `undefined` here and every caller collapsed onto the literal key
|
|
48
|
+
* "unknown" — one shared bucket for the whole server. That let a single
|
|
49
|
+
* burst of 5 credential posts (the `auth` bucket) lock out sign-in for
|
|
50
|
+
* everyone for 15 minutes.
|
|
51
|
+
*
|
|
52
|
+
* Trust `x-forwarded-for`/`x-real-ip` only when you are actually behind a
|
|
53
|
+
* reverse proxy (Railway, Docker, nginx) that overwrites them; otherwise a
|
|
54
|
+
* client can spoof them to evade the limiter.
|
|
55
|
+
*
|
|
56
|
+
* @param {import("next/server").NextRequest} request
|
|
57
|
+
* @returns {string}
|
|
58
|
+
*/
|
|
59
|
+
function getClientIp(request) {
|
|
60
|
+
const forwardedFor = request.headers.get("x-forwarded-for");
|
|
61
|
+
if (forwardedFor) {
|
|
62
|
+
// Left-most entry is the originating client; later entries are proxies.
|
|
63
|
+
const client = forwardedFor
|
|
64
|
+
.split(",")
|
|
65
|
+
.map((value) => value.trim())
|
|
66
|
+
.filter(Boolean)[0];
|
|
67
|
+
if (client) return client;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const realIp = request.headers.get("x-real-ip");
|
|
71
|
+
if (realIp) return realIp.trim();
|
|
72
|
+
|
|
73
|
+
return "unknown";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const RATE_LIMIT_CONFIG = {
|
|
77
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
78
|
+
maxRequests: {
|
|
79
|
+
api: 100, // 100 requests per 15 minutes for general API
|
|
80
|
+
auth: 5, // 5 requests per 15 minutes for auth endpoints
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Rate limiting implementation
|
|
86
|
+
*/
|
|
87
|
+
function checkRateLimit(ip, path, method) {
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
const key = `${ip}:${path}`;
|
|
90
|
+
|
|
91
|
+
const maxRequests =
|
|
92
|
+
RATE_LIMIT_CONFIG.maxRequests[getRateLimitBucket(path, method)];
|
|
93
|
+
|
|
94
|
+
// Get or create rate limit record
|
|
95
|
+
const record = rateLimit.get(key) || {
|
|
96
|
+
count: 0,
|
|
97
|
+
resetTime: now + RATE_LIMIT_CONFIG.windowMs,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Reset if window has passed
|
|
101
|
+
if (now > record.resetTime) {
|
|
102
|
+
record.count = 0;
|
|
103
|
+
record.resetTime = now + RATE_LIMIT_CONFIG.windowMs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check if limit exceeded
|
|
107
|
+
if (record.count >= maxRequests) {
|
|
108
|
+
return {
|
|
109
|
+
limited: true,
|
|
110
|
+
resetTime: record.resetTime,
|
|
111
|
+
remaining: 0,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Increment counter
|
|
116
|
+
record.count++;
|
|
117
|
+
rateLimit.set(key, record);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
limited: false,
|
|
121
|
+
resetTime: record.resetTime,
|
|
122
|
+
remaining: maxRequests - record.count,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Clean up old rate limit records periodically.
|
|
128
|
+
* `unref()` so the timer never holds the Node process open on shutdown.
|
|
129
|
+
*/
|
|
130
|
+
const rateLimitCleanup = setInterval(() => {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
for (const [key, record] of rateLimit.entries()) {
|
|
133
|
+
if (now > record.resetTime) {
|
|
134
|
+
rateLimit.delete(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}, 60 * 1000); // Clean up every minute
|
|
138
|
+
if (typeof rateLimitCleanup.unref === "function") rateLimitCleanup.unref();
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* CORS configuration
|
|
142
|
+
*/
|
|
143
|
+
const CORS_CONFIG = {
|
|
144
|
+
allowedOrigins: getAllowedOrigins(),
|
|
145
|
+
allowedMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
|
|
146
|
+
allowedHeaders: ["Content-Type", "Authorization", "X-CSRF-Token"],
|
|
147
|
+
exposedHeaders: [
|
|
148
|
+
"X-RateLimit-Limit",
|
|
149
|
+
"X-RateLimit-Remaining",
|
|
150
|
+
"X-RateLimit-Reset",
|
|
151
|
+
],
|
|
152
|
+
credentials: true,
|
|
153
|
+
maxAge: 86400, // 24 hours
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Security headers configuration
|
|
158
|
+
*
|
|
159
|
+
* In development, Turbopack's hot-reload runtime uses eval() for module
|
|
160
|
+
* evaluation. 'unsafe-eval' is therefore added to script-src only when
|
|
161
|
+
* NODE_ENV is not 'production' - it must never reach a production build.
|
|
162
|
+
*/
|
|
163
|
+
const SECURITY_HEADERS = {
|
|
164
|
+
"X-DNS-Prefetch-Control": "on",
|
|
165
|
+
"Strict-Transport-Security": "max-age=63072000; includeSubDomains",
|
|
166
|
+
"X-Frame-Options": "SAMEORIGIN",
|
|
167
|
+
"X-Content-Type-Options": "nosniff",
|
|
168
|
+
"Referrer-Policy": "strict-origin-when-cross-origin",
|
|
169
|
+
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
|
170
|
+
"Content-Security-Policy": buildContentSecurityPolicy(),
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Request size limits (in bytes)
|
|
175
|
+
*/
|
|
176
|
+
const REQUEST_SIZE_LIMITS = {
|
|
177
|
+
api: parseInt(process.env.API_BODY_SIZE_LIMIT || "2097152", 10), // 2MB default
|
|
178
|
+
upload: parseInt(process.env.UPLOAD_SIZE_LIMIT || "10485760", 10), // 10MB for uploads
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export async function proxy(request) {
|
|
182
|
+
const metricsResponse = metricsGuard(request);
|
|
183
|
+
if (metricsResponse) return metricsResponse;
|
|
184
|
+
|
|
185
|
+
const { pathname } = request.nextUrl;
|
|
186
|
+
const origin = request.headers.get("origin") || "";
|
|
187
|
+
|
|
188
|
+
// Create response
|
|
189
|
+
const response = NextResponse.next();
|
|
190
|
+
|
|
191
|
+
// Check request body size for API routes
|
|
192
|
+
if (
|
|
193
|
+
pathname.startsWith("/api/") &&
|
|
194
|
+
["POST", "PUT", "PATCH"].includes(request.method)
|
|
195
|
+
) {
|
|
196
|
+
const contentLength = request.headers.get("content-length");
|
|
197
|
+
if (contentLength) {
|
|
198
|
+
const size = parseInt(contentLength, 10);
|
|
199
|
+
const limit = pathname.startsWith("/api/upload")
|
|
200
|
+
? REQUEST_SIZE_LIMITS.upload
|
|
201
|
+
: REQUEST_SIZE_LIMITS.api;
|
|
202
|
+
|
|
203
|
+
if (size > limit) {
|
|
204
|
+
return new NextResponse(
|
|
205
|
+
JSON.stringify({
|
|
206
|
+
error: "Payload too large",
|
|
207
|
+
message: `Request body size exceeds limit of ${Math.round(limit / 1024 / 1024)}MB`,
|
|
208
|
+
maxSize: limit,
|
|
209
|
+
}),
|
|
210
|
+
{
|
|
211
|
+
status: 413,
|
|
212
|
+
headers: {
|
|
213
|
+
"Content-Type": "application/json",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Apply rate limiting to API routes only
|
|
222
|
+
if (pathname.startsWith("/api/")) {
|
|
223
|
+
const ip = getClientIp(request);
|
|
224
|
+
const rateLimitBucket = getRateLimitBucket(pathname, request.method);
|
|
225
|
+
const maxRequests = RATE_LIMIT_CONFIG.maxRequests[rateLimitBucket];
|
|
226
|
+
const rateLimitResult = checkRateLimit(ip, pathname, request.method);
|
|
227
|
+
|
|
228
|
+
if (rateLimitResult.limited) {
|
|
229
|
+
const retryAfter = Math.ceil(
|
|
230
|
+
(rateLimitResult.resetTime - Date.now()) / 1000,
|
|
231
|
+
);
|
|
232
|
+
return new NextResponse(
|
|
233
|
+
JSON.stringify({
|
|
234
|
+
error: "Too many requests",
|
|
235
|
+
message: "You have exceeded the rate limit. Please try again later.",
|
|
236
|
+
retryAfter,
|
|
237
|
+
}),
|
|
238
|
+
{
|
|
239
|
+
status: 429,
|
|
240
|
+
headers: {
|
|
241
|
+
"Content-Type": "application/json",
|
|
242
|
+
"Retry-After": retryAfter.toString(),
|
|
243
|
+
"X-RateLimit-Limit": maxRequests.toString(),
|
|
244
|
+
"X-RateLimit-Remaining": "0",
|
|
245
|
+
"X-RateLimit-Reset": new Date(
|
|
246
|
+
rateLimitResult.resetTime,
|
|
247
|
+
).toISOString(),
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Add rate limit headers to response
|
|
254
|
+
response.headers.set("X-RateLimit-Limit", maxRequests.toString());
|
|
255
|
+
response.headers.set(
|
|
256
|
+
"X-RateLimit-Remaining",
|
|
257
|
+
rateLimitResult.remaining.toString(),
|
|
258
|
+
);
|
|
259
|
+
response.headers.set(
|
|
260
|
+
"X-RateLimit-Reset",
|
|
261
|
+
new Date(rateLimitResult.resetTime).toISOString(),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Handle CORS for API routes
|
|
266
|
+
if (pathname.startsWith("/api/")) {
|
|
267
|
+
// Check if origin is allowed
|
|
268
|
+
const isAllowedOrigin =
|
|
269
|
+
CORS_CONFIG.allowedOrigins.includes("*") ||
|
|
270
|
+
CORS_CONFIG.allowedOrigins.includes(origin);
|
|
271
|
+
|
|
272
|
+
if (isAllowedOrigin || !origin) {
|
|
273
|
+
response.headers.set(
|
|
274
|
+
"Access-Control-Allow-Origin",
|
|
275
|
+
origin || CORS_CONFIG.allowedOrigins[0],
|
|
276
|
+
);
|
|
277
|
+
response.headers.set(
|
|
278
|
+
"Access-Control-Allow-Methods",
|
|
279
|
+
CORS_CONFIG.allowedMethods.join(", "),
|
|
280
|
+
);
|
|
281
|
+
response.headers.set(
|
|
282
|
+
"Access-Control-Allow-Headers",
|
|
283
|
+
CORS_CONFIG.allowedHeaders.join(", "),
|
|
284
|
+
);
|
|
285
|
+
response.headers.set(
|
|
286
|
+
"Access-Control-Expose-Headers",
|
|
287
|
+
CORS_CONFIG.exposedHeaders.join(", "),
|
|
288
|
+
);
|
|
289
|
+
response.headers.set(
|
|
290
|
+
"Access-Control-Max-Age",
|
|
291
|
+
CORS_CONFIG.maxAge.toString(),
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
if (CORS_CONFIG.credentials) {
|
|
295
|
+
response.headers.set("Access-Control-Allow-Credentials", "true");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Handle preflight requests
|
|
300
|
+
if (request.method === "OPTIONS") {
|
|
301
|
+
return new NextResponse(null, {
|
|
302
|
+
status: 204,
|
|
303
|
+
headers: response.headers,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Apply security headers
|
|
309
|
+
for (const [key, value] of Object.entries(SECURITY_HEADERS)) {
|
|
310
|
+
response.headers.set(key, value);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return response;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Configure which routes the proxy runs on
|
|
317
|
+
export const config = {
|
|
318
|
+
matcher: [
|
|
319
|
+
/*
|
|
320
|
+
* Match all request paths except:
|
|
321
|
+
* - _next/static (static files)
|
|
322
|
+
* - _next/image (image optimization files)
|
|
323
|
+
* - favicon.ico (favicon file)
|
|
324
|
+
* - public folder
|
|
325
|
+
*/
|
|
326
|
+
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
|
|
327
|
+
],
|
|
328
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": false,
|
|
10
|
+
"includes": [
|
|
11
|
+
"**",
|
|
12
|
+
"!**/.next",
|
|
13
|
+
"!**/dist",
|
|
14
|
+
"!**/.turbo",
|
|
15
|
+
"!**/coverage",
|
|
16
|
+
"!packages/db/src/generated"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"formatter": {
|
|
20
|
+
"enabled": true,
|
|
21
|
+
"indentStyle": "tab"
|
|
22
|
+
},
|
|
23
|
+
"linter": {
|
|
24
|
+
"enabled": true,
|
|
25
|
+
"rules": {
|
|
26
|
+
"preset": "recommended"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"javascript": {
|
|
30
|
+
"formatter": {
|
|
31
|
+
"quoteStyle": "double"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"css": {
|
|
35
|
+
"parser": {
|
|
36
|
+
"cssModules": true,
|
|
37
|
+
"tailwindDirectives": true
|
|
38
|
+
},
|
|
39
|
+
"linter": {
|
|
40
|
+
"enabled": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"assist": {
|
|
44
|
+
"enabled": true,
|
|
45
|
+
"actions": {
|
|
46
|
+
"source": {
|
|
47
|
+
"organizeImports": "on"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"root": false
|
|
52
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Dev-only services - auto-merged by `docker compose up`.
|
|
2
|
+
# Not read by Railway, Kubernetes, or other deployment platforms.
|
|
3
|
+
services:
|
|
4
|
+
# --- Mailpit (Local SMTP Server) ---
|
|
5
|
+
mailpit:
|
|
6
|
+
image: ghcr.io/axllent/mailpit
|
|
7
|
+
restart: always
|
|
8
|
+
ports:
|
|
9
|
+
# SMTP port (used by application to send mail)
|
|
10
|
+
- "${MAIL_SMTP_PORT:-1025}:1025"
|
|
11
|
+
# Web UI port (to view sent emails)
|
|
12
|
+
- "${MAIL_UI_PORT:-8025}:8025"
|
|
13
|
+
healthcheck:
|
|
14
|
+
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8025"]
|
|
15
|
+
interval: 10s
|
|
16
|
+
timeout: 5s
|
|
17
|
+
retries: 3
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
services:
|
|
2
|
+
# --- 1. PostgreSQL Database ---
|
|
3
|
+
postgres:
|
|
4
|
+
image: postgres:16-alpine
|
|
5
|
+
restart: always
|
|
6
|
+
ports:
|
|
7
|
+
- "${POSTGRES_PORT:-5432}:5432"
|
|
8
|
+
environment:
|
|
9
|
+
POSTGRES_USER: ${POSTGRES_USER:-quark_user}
|
|
10
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-CHANGE_ME_IN_ENV_FILE}
|
|
11
|
+
POSTGRES_DB: ${POSTGRES_DB:-quark_dev}
|
|
12
|
+
volumes:
|
|
13
|
+
- postgres_data:/var/lib/postgresql/data
|
|
14
|
+
healthcheck:
|
|
15
|
+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-quark_user}"]
|
|
16
|
+
interval: 5s
|
|
17
|
+
timeout: 5s
|
|
18
|
+
retries: 5
|
|
19
|
+
|
|
20
|
+
# --- 2. Redis Cache & Job Queue ---
|
|
21
|
+
redis:
|
|
22
|
+
image: redis:7-alpine
|
|
23
|
+
restart: always
|
|
24
|
+
ports:
|
|
25
|
+
- "${REDIS_PORT:-6379}:6379"
|
|
26
|
+
command: redis-server --appendonly yes
|
|
27
|
+
volumes:
|
|
28
|
+
- redis_data:/data
|
|
29
|
+
healthcheck:
|
|
30
|
+
test: ["CMD", "redis-cli", "ping"]
|
|
31
|
+
interval: 5s
|
|
32
|
+
timeout: 5s
|
|
33
|
+
retries: 5
|
|
34
|
+
|
|
35
|
+
volumes:
|
|
36
|
+
postgres_data:
|
|
37
|
+
redis_data:
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# First Feature
|
|
2
|
+
|
|
3
|
+
Use this guide when you are ready to add the first real feature to your project.
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Prove the full workflow once:
|
|
8
|
+
|
|
9
|
+
1. add a Prisma model
|
|
10
|
+
2. run the migration
|
|
11
|
+
3. add query helpers
|
|
12
|
+
4. build a page or mutation flow
|
|
13
|
+
5. add a test near the changed code
|
|
14
|
+
|
|
15
|
+
## 1. Add the model
|
|
16
|
+
|
|
17
|
+
Edit `packages/db/prisma/schema.prisma`.
|
|
18
|
+
|
|
19
|
+
Every model should include:
|
|
20
|
+
|
|
21
|
+
- `id String @id @default(cuid())`
|
|
22
|
+
- `createdAt DateTime @default(now())`
|
|
23
|
+
- `updatedAt DateTime @updatedAt`
|
|
24
|
+
|
|
25
|
+
If the new data belongs to a signed-in user, relate it to `User`.
|
|
26
|
+
|
|
27
|
+
## 2. Generate and migrate
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm db:generate
|
|
31
|
+
pnpm db:migrate
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 3. Add query helpers
|
|
35
|
+
|
|
36
|
+
Edit `packages/db/src/queries.js`.
|
|
37
|
+
|
|
38
|
+
Start with the smallest useful surface:
|
|
39
|
+
|
|
40
|
+
- `findAll`
|
|
41
|
+
- `findById`
|
|
42
|
+
- `create`
|
|
43
|
+
|
|
44
|
+
Add `update` and `delete` when the feature actually needs them.
|
|
45
|
+
|
|
46
|
+
## 4. Build the web flow
|
|
47
|
+
|
|
48
|
+
Use `apps/web/src/app/` for the first route.
|
|
49
|
+
|
|
50
|
+
Typical first files:
|
|
51
|
+
|
|
52
|
+
- `apps/web/src/app/<feature>/page.js`
|
|
53
|
+
- `apps/web/src/app/<feature>/_actions/*.js` for Server Actions
|
|
54
|
+
- `apps/web/src/app/api/<feature>/route.js` if this feature needs an API surface
|
|
55
|
+
|
|
56
|
+
If the project includes `ui`, use components from `@yourapp/ui`.
|
|
57
|
+
|
|
58
|
+
## 5. Add one test
|
|
59
|
+
|
|
60
|
+
Start in the layer you changed first:
|
|
61
|
+
|
|
62
|
+
- query helper test
|
|
63
|
+
- route test
|
|
64
|
+
- form helper test
|
|
65
|
+
|
|
66
|
+
Do not wait for a perfect end-to-end suite before writing the first useful test.
|
|
67
|
+
|
|
68
|
+
## Optional next step
|
|
69
|
+
|
|
70
|
+
If the feature needs more infrastructure, add it only when necessary:
|
|
71
|
+
|
|
72
|
+
- `ui` for reusable components
|
|
73
|
+
- `jobs` for async work
|
|
74
|
+
- `cms` for editorial content workflows
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Start Here
|
|
2
|
+
|
|
3
|
+
This is the shortest current path through a newly scaffolded Quark project.
|
|
4
|
+
|
|
5
|
+
## Read these first
|
|
6
|
+
|
|
7
|
+
1. `README.md` - quick start, included features, and first files to edit
|
|
8
|
+
2. `docs/FIRST_FEATURE.md` - the canonical first product feature walkthrough
|
|
9
|
+
3. `CLAUDE.md` - AI tool context for code generation and refactors
|
|
10
|
+
|
|
11
|
+
## Know the project contract
|
|
12
|
+
|
|
13
|
+
Every Quark scaffold gives you:
|
|
14
|
+
|
|
15
|
+
- `apps/web` - the Next.js app
|
|
16
|
+
- `packages/db` - Prisma schema, client, and query helpers
|
|
17
|
+
- `packages/config` - validated environment config
|
|
18
|
+
- optional local packages like `ui` and `jobs`
|
|
19
|
+
|
|
20
|
+
The package READMEs explain optional features if they are present:
|
|
21
|
+
|
|
22
|
+
- `packages/ui/README.md`
|
|
23
|
+
- `packages/jobs/README.md`
|
|
24
|
+
- `packages/cms/README.md`
|
|
25
|
+
|
|
26
|
+
## Best order to learn the codebase
|
|
27
|
+
|
|
28
|
+
1. `packages/db/prisma/schema.prisma`
|
|
29
|
+
2. `packages/db/src/queries.js`
|
|
30
|
+
3. `apps/web/src/app/`
|
|
31
|
+
4. `apps/web/src/lib/auth.js`
|
|
32
|
+
5. `apps/worker/src/handlers/` if your project includes background jobs
|
|
33
|
+
|
|
34
|
+
## Build next
|
|
35
|
+
|
|
36
|
+
Follow `docs/FIRST_FEATURE.md` and ship one thin vertical slice:
|
|
37
|
+
|
|
38
|
+
1. add a model
|
|
39
|
+
2. migrate the database
|
|
40
|
+
3. add query helpers
|
|
41
|
+
4. build the page or mutation
|
|
42
|
+
5. add one focused test
|
|
43
|
+
|
|
44
|
+
That is the fastest way to move from "starter" to "product code."
|
|
45
|
+
|
|
46
|
+
## Keep your scaffold visible
|
|
47
|
+
|
|
48
|
+
Before taking Quark infrastructure updates, review scaffold-managed drift from the project root:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npx @techstream/quark-create-app update --scaffold-check
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If you want CI to fail when scaffold-managed files drift, use:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx @techstream/quark-create-app update --scaffold-check --fail-on-drift
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Recover disk space
|
|
61
|
+
|
|
62
|
+
When local build artifacts pile up, run these from the project root:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pnpm clean:check # Preview reclaimable build artifacts
|
|
66
|
+
pnpm clean # Remove .turbo, .next, build, dist, and coverage output
|
|
67
|
+
pnpm clean:deep # Also remove repo-local temp workspaces
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`pnpm dev` also auto-cleans stale oversized local artifacts at most once per day. Set `QUARK_SKIP_AUTO_CLEAN=1` if you want to skip that behavior for a run.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Architecture Decision Records
|
|
2
|
+
|
|
3
|
+
This directory contains Architecture Decision Records (ADRs) for __QUARK_PROJECT_NAME__.
|
|
4
|
+
|
|
5
|
+
An ADR documents a significant architectural decision: the context, the decision made, and its consequences.
|
|
6
|
+
|
|
7
|
+
## Index
|
|
8
|
+
|
|
9
|
+
| ID | Title | Status |
|
|
10
|
+
|----|-------|--------|
|
|
11
|
+
|
|
12
|
+
<!-- Add entries here as you make architectural decisions. -->
|
|
13
|
+
<!-- Example: | [ADR-001](./001-example-decision.md) | Why we chose X over Y | Accepted | -->
|
|
14
|
+
|
|
15
|
+
## Format
|
|
16
|
+
|
|
17
|
+
Each ADR uses this structure:
|
|
18
|
+
|
|
19
|
+
- **Status:** Accepted / Superseded / Deprecated
|
|
20
|
+
- **Context:** Why a decision was needed
|
|
21
|
+
- **Decision:** What was decided
|
|
22
|
+
- **Consequences:** Trade-offs and implications
|
|
23
|
+
|
|
24
|
+
## Template
|
|
25
|
+
|
|
26
|
+
```markdown
|
|
27
|
+
# ADR-XXX: Title
|
|
28
|
+
|
|
29
|
+
**Status:** Accepted
|
|
30
|
+
|
|
31
|
+
## Context
|
|
32
|
+
|
|
33
|
+
Why was this decision needed?
|
|
34
|
+
|
|
35
|
+
## Decision
|
|
36
|
+
|
|
37
|
+
What was decided?
|
|
38
|
+
|
|
39
|
+
## Consequences
|
|
40
|
+
|
|
41
|
+
What are the trade-offs?
|
|
42
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@myquark/root",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "My Quark project",
|
|
7
|
+
"main": "index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"clean": "node scripts/clean-workspace.mjs",
|
|
10
|
+
"clean:check": "node scripts/clean-workspace.mjs --check",
|
|
11
|
+
"clean:deep": "node scripts/clean-workspace.mjs --deep",
|
|
12
|
+
"build": "dotenv -- turbo run build",
|
|
13
|
+
"dev": "dotenv -- node scripts/dev-preflight.mjs && dotenv -- turbo run dev",
|
|
14
|
+
"dev:mobile": "pnpm --filter @myquark/mobile start",
|
|
15
|
+
"build:mobile": "pnpm --filter @myquark/mobile export",
|
|
16
|
+
"lint": "turbo run lint",
|
|
17
|
+
"standards": "node scripts/check-standards.mjs",
|
|
18
|
+
"check:loading": "node scripts/check-loading.mjs",
|
|
19
|
+
"test": "turbo run test",
|
|
20
|
+
"docker:up": "docker compose up -d",
|
|
21
|
+
"docker:down": "docker compose down",
|
|
22
|
+
"docker:clean": "docker compose down -v",
|
|
23
|
+
"db:generate": "turbo run db:generate",
|
|
24
|
+
"db:migrate": "pnpm --filter ./packages/db db:migrate",
|
|
25
|
+
"db:migrate:deploy": "pnpm --filter ./packages/db db:migrate:deploy",
|
|
26
|
+
"db:push": "turbo run db:push",
|
|
27
|
+
"db:seed": "turbo run db:seed",
|
|
28
|
+
"db:studio": "turbo run db:studio",
|
|
29
|
+
"doctor": "node scripts/doctor.js",
|
|
30
|
+
"doctor:fix": "node scripts/doctor.js --fix",
|
|
31
|
+
"doctor:ci": "node scripts/doctor.js --ci",
|
|
32
|
+
"prepare": "node scripts/prepare.js"
|
|
33
|
+
},
|
|
34
|
+
"simple-git-hooks": {
|
|
35
|
+
"pre-commit": "pnpm nano-staged",
|
|
36
|
+
"pre-push": "pnpm check:loading && pnpm test"
|
|
37
|
+
},
|
|
38
|
+
"nano-staged": {
|
|
39
|
+
"**/*.{js,jsx,mjs,json,css}": [
|
|
40
|
+
"biome format --no-errors-on-unmatched --write",
|
|
41
|
+
"biome check --no-errors-on-unmatched --write"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"keywords": [],
|
|
45
|
+
"author": "",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"packageManager": "pnpm@10.12.1",
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@biomejs/biome": "^2.4.0",
|
|
50
|
+
"@types/node": "^24.10.9",
|
|
51
|
+
"dotenv-cli": "^11.0.0",
|
|
52
|
+
"nano-staged": "^0.9.0",
|
|
53
|
+
"simple-git-hooks": "^2.13.1",
|
|
54
|
+
"tsx": "^4.21.0",
|
|
55
|
+
"turbo": "^2.8.1"
|
|
56
|
+
},
|
|
57
|
+
"pnpm": {}
|
|
58
|
+
}
|