@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,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quark-skills
|
|
3
|
+
description: Index of every skill bundled with this Quark project - what each skill is for and when to load it. Start here when deciding which skill applies to a task.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Quark Skills Index
|
|
7
|
+
|
|
8
|
+
Every skill below ships with this project and is always available. Skills are abstract and domain-neutral: they describe framework patterns and workflows, not a specific business. Load the relevant `SKILL.md` before starting work in its area.
|
|
9
|
+
|
|
10
|
+
## Core workflow skills
|
|
11
|
+
|
|
12
|
+
| Skill | Path | Purpose |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| add-model | `add-model/SKILL.md` | Add a new Prisma model, query helpers, and tests to the db package. |
|
|
15
|
+
| add-endpoint | `add-endpoint/SKILL.md` | Add an API route (or Server Action) with Zod validation, CSRF, and role guards. |
|
|
16
|
+
| add-dashboard | `add-dashboard/SKILL.md` | Add a metrics/overview dashboard page with metric cards and loading states. |
|
|
17
|
+
| admin-dashboard | `admin-dashboard/SKILL.md` | Build a full admin/back-office area: guarded routes, CRUD screens, audit logging. |
|
|
18
|
+
|
|
19
|
+
## Domain starters
|
|
20
|
+
|
|
21
|
+
These teach the AI to build a complete vertical on demand. No starter code is scaffolded - the AI generates it from the skill.
|
|
22
|
+
|
|
23
|
+
| Skill | Path | Purpose |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| bookings | `bookings/SKILL.md` | Booking/reservation/appointment/scheduling systems. |
|
|
26
|
+
| crm | `crm/SKILL.md` | Contact/pipeline/customer relationship systems. |
|
|
27
|
+
| cms | `cms/SKILL.md` | Content modeling, authoring, and publishing systems. |
|
|
28
|
+
| ai | `ai/SKILL.md` | AI assistant features (chat, completions) on app data. |
|
|
29
|
+
| payment | `payment/SKILL.md` | Stripe payment integration, checkout, webhooks, fulfillment. |
|
|
30
|
+
| ecommerce | `ecommerce/SKILL.md` | Product catalog, cart, checkout, orders, inventory. Sub-skills: `ecommerce-catalog`, `ecommerce-cart`, `ecommerce-checkout`. |
|
|
31
|
+
| i18n | `i18n/SKILL.md` | Internationalization, locale routing, localized CMS content, hreflang SEO. |
|
|
32
|
+
|
|
33
|
+
## How to use skills
|
|
34
|
+
|
|
35
|
+
1. Match the user's request to a skill above; read its `SKILL.md` first.
|
|
36
|
+
2. Skills assume the Quark conventions: Zod-validated actions/routes, query helpers in `packages/db/src/queries.js`, shared UI imports, `createLogger()` logging, models with `createdAt`/`updatedAt`.
|
|
37
|
+
3. If no skill matches, follow `CLAUDE.md` / project context and the reference patterns in `apps/web/src/app/api/users/route.js` and `apps/web/src/app/page.js`.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://v2-10-12.turborepo.dev/schema.json",
|
|
3
|
+
"ui": "stream",
|
|
4
|
+
"globalEnv": [
|
|
5
|
+
"GITHUB_OUTPUT",
|
|
6
|
+
"npm_execpath",
|
|
7
|
+
"npm_lifecycle_event",
|
|
8
|
+
"QUARK_SKIP_AUTO_CLEAN",
|
|
9
|
+
"QUARK_SKIP_DEV_PREFLIGHT",
|
|
10
|
+
"QUARK_SKIP_GIT_INIT"
|
|
11
|
+
],
|
|
12
|
+
"tasks": {
|
|
13
|
+
"clean": {
|
|
14
|
+
"cache": false
|
|
15
|
+
},
|
|
16
|
+
"build": {
|
|
17
|
+
"dependsOn": ["^build", "db:generate"],
|
|
18
|
+
"outputs": ["dist/**", ".next/**"],
|
|
19
|
+
"env": ["NODE_ENV"],
|
|
20
|
+
"passThroughEnv": [
|
|
21
|
+
"DATABASE_URL",
|
|
22
|
+
"POSTGRES_HOST",
|
|
23
|
+
"POSTGRES_PORT",
|
|
24
|
+
"POSTGRES_USER",
|
|
25
|
+
"POSTGRES_PASSWORD",
|
|
26
|
+
"POSTGRES_DB",
|
|
27
|
+
"APP_URL",
|
|
28
|
+
"ALLOW_INDEXING",
|
|
29
|
+
"PORT",
|
|
30
|
+
"NEXTAUTH_SECRET",
|
|
31
|
+
"AUTH_SECRET"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"lint": {
|
|
35
|
+
"outputs": []
|
|
36
|
+
},
|
|
37
|
+
"test": {
|
|
38
|
+
"dependsOn": ["^build", "db:generate"],
|
|
39
|
+
"outputs": [],
|
|
40
|
+
"passThroughEnv": [
|
|
41
|
+
"DATABASE_URL",
|
|
42
|
+
"POSTGRES_HOST",
|
|
43
|
+
"POSTGRES_PORT",
|
|
44
|
+
"POSTGRES_USER",
|
|
45
|
+
"POSTGRES_PASSWORD",
|
|
46
|
+
"POSTGRES_DB",
|
|
47
|
+
"REDIS_URL",
|
|
48
|
+
"REDIS_HOST",
|
|
49
|
+
"REDIS_PORT",
|
|
50
|
+
"NEXTAUTH_SECRET",
|
|
51
|
+
"AUTH_SECRET",
|
|
52
|
+
"APP_URL",
|
|
53
|
+
"ALLOW_INDEXING",
|
|
54
|
+
"PORT"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
"db:generate": {
|
|
58
|
+
"cache": false,
|
|
59
|
+
"outputs": ["node_modules/.prisma/client"],
|
|
60
|
+
"passThroughEnv": [
|
|
61
|
+
"DATABASE_URL",
|
|
62
|
+
"POSTGRES_HOST",
|
|
63
|
+
"POSTGRES_PORT",
|
|
64
|
+
"POSTGRES_USER",
|
|
65
|
+
"POSTGRES_PASSWORD",
|
|
66
|
+
"POSTGRES_DB"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"db:migrate:deploy": {
|
|
70
|
+
"cache": false,
|
|
71
|
+
"passThroughEnv": [
|
|
72
|
+
"DATABASE_URL",
|
|
73
|
+
"POSTGRES_HOST",
|
|
74
|
+
"POSTGRES_PORT",
|
|
75
|
+
"POSTGRES_USER",
|
|
76
|
+
"POSTGRES_PASSWORD",
|
|
77
|
+
"POSTGRES_DB"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"db:push": {
|
|
81
|
+
"cache": false,
|
|
82
|
+
"passThroughEnv": [
|
|
83
|
+
"DATABASE_URL",
|
|
84
|
+
"POSTGRES_HOST",
|
|
85
|
+
"POSTGRES_PORT",
|
|
86
|
+
"POSTGRES_USER",
|
|
87
|
+
"POSTGRES_PASSWORD",
|
|
88
|
+
"POSTGRES_DB"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"db:seed": {
|
|
92
|
+
"cache": false,
|
|
93
|
+
"passThroughEnv": [
|
|
94
|
+
"DATABASE_URL",
|
|
95
|
+
"POSTGRES_HOST",
|
|
96
|
+
"POSTGRES_PORT",
|
|
97
|
+
"POSTGRES_USER",
|
|
98
|
+
"POSTGRES_PASSWORD",
|
|
99
|
+
"POSTGRES_DB",
|
|
100
|
+
"SEED_PROFILE",
|
|
101
|
+
"ADMIN_EMAIL",
|
|
102
|
+
"ADMIN_PASSWORD",
|
|
103
|
+
"ADMIN_NAME"
|
|
104
|
+
]
|
|
105
|
+
},
|
|
106
|
+
"dev": {
|
|
107
|
+
"cache": false,
|
|
108
|
+
"persistent": true,
|
|
109
|
+
"passThroughEnv": [
|
|
110
|
+
"DATABASE_URL",
|
|
111
|
+
"PORT",
|
|
112
|
+
"POSTGRES_HOST",
|
|
113
|
+
"POSTGRES_PORT",
|
|
114
|
+
"POSTGRES_USER",
|
|
115
|
+
"POSTGRES_PASSWORD",
|
|
116
|
+
"POSTGRES_DB",
|
|
117
|
+
"REDIS_URL",
|
|
118
|
+
"REDIS_HOST",
|
|
119
|
+
"REDIS_PORT",
|
|
120
|
+
"MAIL_HOST",
|
|
121
|
+
"MAIL_SMTP_PORT",
|
|
122
|
+
"MAIL_UI_PORT",
|
|
123
|
+
"NEXTAUTH_URL",
|
|
124
|
+
"NEXTAUTH_SECRET",
|
|
125
|
+
"AUTH_SECRET",
|
|
126
|
+
"APP_URL",
|
|
127
|
+
"ALLOW_INDEXING",
|
|
128
|
+
"WORKER_CONCURRENCY",
|
|
129
|
+
"STORAGE_PROVIDER",
|
|
130
|
+
"STORAGE_LOCAL_DIR",
|
|
131
|
+
"S3_BUCKET",
|
|
132
|
+
"S3_REGION",
|
|
133
|
+
"S3_ENDPOINT",
|
|
134
|
+
"S3_ACCESS_KEY_ID",
|
|
135
|
+
"S3_SECRET_ACCESS_KEY",
|
|
136
|
+
"S3_PUBLIC_URL",
|
|
137
|
+
"UPLOAD_MAX_SIZE",
|
|
138
|
+
"UPLOAD_ALLOWED_TYPES",
|
|
139
|
+
"EXPO_PUBLIC_API_URL"
|
|
140
|
+
]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@myquark/config",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./rate-limit": "./src/rate-limit.js",
|
|
9
|
+
"./app-url": "./src/app-url.js",
|
|
10
|
+
"./environment": "./src/environment.js",
|
|
11
|
+
"./load-config": "./src/load-config.js",
|
|
12
|
+
"./validate-env": "./src/validate-env.js",
|
|
13
|
+
"./redis": "./src/redis.js"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@techstream/quark-core": "^2.0.0",
|
|
17
|
+
"ioredis": "^6.0.0",
|
|
18
|
+
"zod": "^4.6.5"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* APP_URL - Single source of truth for the application's canonical URL.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. APP_URL (canonical origin - set it in every environment)
|
|
6
|
+
* 2. NEXTAUTH_URL (legacy / backward-compat)
|
|
7
|
+
* 3. http://localhost:${PORT || 3000} (local dev fallback)
|
|
8
|
+
*
|
|
9
|
+
* In development PORT is the default source for the web server port, but
|
|
10
|
+
* APP_URL should still be set explicitly whenever the origin you browse on is
|
|
11
|
+
* not exactly localhost:PORT (127.0.0.1, a LAN IP, a tunnel, another port).
|
|
12
|
+
* Auth.js derives its post-sign-in redirect and its client-side base URL from
|
|
13
|
+
* this value, so a mismatch silently drops the session.
|
|
14
|
+
* In production, set APP_URL explicitly (e.g. https://yourdomain.com).
|
|
15
|
+
*
|
|
16
|
+
* Derived values:
|
|
17
|
+
* - NEXTAUTH_URL - always set equal to the resolved APP_URL so
|
|
18
|
+
* NextAuth works without a separate variable.
|
|
19
|
+
* - allowedOrigins - the resolved APP_URL plus any extra origins
|
|
20
|
+
* listed in ALLOWED_ORIGINS (comma-separated).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { ValidationError } from "@techstream/quark-core/errors";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves the canonical application URL.
|
|
27
|
+
* @returns {string} The canonical URL (no trailing slash)
|
|
28
|
+
*/
|
|
29
|
+
export function getAppUrl() {
|
|
30
|
+
const configuredAppUrl = process.env.APP_URL?.trim();
|
|
31
|
+
if (configuredAppUrl) {
|
|
32
|
+
return normalizeAppUrl(configuredAppUrl, "APP_URL");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const configuredNextAuthUrl = process.env.NEXTAUTH_URL?.trim();
|
|
36
|
+
if (configuredNextAuthUrl) {
|
|
37
|
+
return normalizeAppUrl(configuredNextAuthUrl, "NEXTAUTH_URL");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const fallbackPort = resolveLocalPort(process.env.PORT);
|
|
41
|
+
|
|
42
|
+
return normalizeAppUrl(`http://localhost:${fallbackPort}`, "derived APP_URL");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveLocalPort(value) {
|
|
46
|
+
const parsedPort = Number.parseInt(value, 10);
|
|
47
|
+
if (Number.isInteger(parsedPort) && parsedPort > 0) {
|
|
48
|
+
return String(parsedPort);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return "3000";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeAppUrl(rawUrl, label) {
|
|
55
|
+
let parsedUrl;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
parsedUrl = new URL(rawUrl);
|
|
59
|
+
} catch {
|
|
60
|
+
throw new ValidationError(
|
|
61
|
+
`${label} must be an absolute http(s) URL. Received: ${rawUrl}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
|
|
66
|
+
throw new ValidationError(
|
|
67
|
+
`${label} must use http:// or https://. Received: ${rawUrl}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return parsedUrl.toString().replace(/\/+$/, "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns the list of allowed CORS origins.
|
|
76
|
+
*
|
|
77
|
+
* Always includes the canonical APP_URL.
|
|
78
|
+
* If ALLOWED_ORIGINS is set, those are *added* (not replacing) the canonical
|
|
79
|
+
* origin so the primary domain is never accidentally excluded.
|
|
80
|
+
*
|
|
81
|
+
* @returns {string[]} De-duplicated list of allowed origins
|
|
82
|
+
*/
|
|
83
|
+
export function getAllowedOrigins() {
|
|
84
|
+
const canonical = getAppUrl();
|
|
85
|
+
const extras = process.env.ALLOWED_ORIGINS
|
|
86
|
+
? process.env.ALLOWED_ORIGINS.split(",")
|
|
87
|
+
.map((o) => o.trim())
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
: [];
|
|
90
|
+
|
|
91
|
+
// In development, allow the ports the dev server actually listens on plus
|
|
92
|
+
// the loopback aliases people type by hand. `getEnvironmentConfig()` returns
|
|
93
|
+
// the hard-coded default (3000) and ignores `process.env.PORT`, so reading it
|
|
94
|
+
// here silently drifted from the real port whenever PORT was overridden.
|
|
95
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
96
|
+
// resolveLocalPort() returns a string - parse before doing arithmetic or
|
|
97
|
+
// "3000" + 1 silently becomes "30001".
|
|
98
|
+
const devPort = Number.parseInt(resolveLocalPort(process.env.PORT), 10);
|
|
99
|
+
const devOrigins = isDev
|
|
100
|
+
? [
|
|
101
|
+
`http://localhost:${devPort}`,
|
|
102
|
+
`http://localhost:${devPort + 1}`,
|
|
103
|
+
`http://127.0.0.1:${devPort}`,
|
|
104
|
+
`http://127.0.0.1:${devPort + 1}`,
|
|
105
|
+
]
|
|
106
|
+
: [];
|
|
107
|
+
|
|
108
|
+
// Extra LAN/VPN dev hosts accepted by next.config.js `allowedDevOrigins`,
|
|
109
|
+
// so CORS and the Next.js dev host allow-list stay in agreement.
|
|
110
|
+
const devHostExtras = isDev
|
|
111
|
+
? ["NEXT_DEV_ALLOWED_ORIGINS", "ALLOWED_DEV_ORIGINS"].flatMap((key) =>
|
|
112
|
+
(process.env[key] ?? "")
|
|
113
|
+
.split(",")
|
|
114
|
+
.map((host) => host.trim())
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
.map((host) => (host.includes("://") ? host : `http://${host}`)),
|
|
117
|
+
)
|
|
118
|
+
: [];
|
|
119
|
+
|
|
120
|
+
// De-duplicate
|
|
121
|
+
return [...new Set([canonical, ...extras, ...devOrigins, ...devHostExtras])];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Ensures NEXTAUTH_URL is set in process.env so NextAuth picks it up,
|
|
126
|
+
* even when only APP_URL was configured.
|
|
127
|
+
*
|
|
128
|
+
* Call this once at startup (e.g. in your env validation step).
|
|
129
|
+
*/
|
|
130
|
+
export function syncNextAuthUrl() {
|
|
131
|
+
if (!process.env.NEXTAUTH_URL) {
|
|
132
|
+
process.env.NEXTAUTH_URL = getAppUrl();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @techstream/quark-config - Environment Configuration
|
|
3
|
+
* Provides environment-specific defaults for dev, test, staging, and production.
|
|
4
|
+
* Each environment defines sensible defaults that can be overridden via env vars.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {"development" | "test" | "staging" | "production"} Environment
|
|
9
|
+
*
|
|
10
|
+
* @typedef {Object} EnvironmentConfig
|
|
11
|
+
* @property {Environment} environment - Resolved environment name
|
|
12
|
+
* @property {boolean} isProduction - True in production and staging
|
|
13
|
+
* @property {boolean} isDevelopment - True in development
|
|
14
|
+
* @property {boolean} isTest - True in test
|
|
15
|
+
* @property {Object} server - Server configuration
|
|
16
|
+
* @property {number} server.port - HTTP port
|
|
17
|
+
* @property {Object} rateLimit - Rate limiting defaults
|
|
18
|
+
* @property {number} rateLimit.windowMs - Rate limit window in ms
|
|
19
|
+
* @property {number} rateLimit.maxRequests - Max requests per window
|
|
20
|
+
* @property {number} rateLimit.authMaxRequests - Max auth requests per window
|
|
21
|
+
* @property {Object} cache - Cache configuration
|
|
22
|
+
* @property {number} cache.defaultTtl - Default cache TTL in seconds
|
|
23
|
+
* @property {Object} logging - Logging configuration
|
|
24
|
+
* @property {string} logging.level - Minimum log level
|
|
25
|
+
* @property {boolean} logging.json - Use JSON format
|
|
26
|
+
* @property {Object} db - Database configuration
|
|
27
|
+
* @property {number} db.poolMax - Max DB pool connections
|
|
28
|
+
* @property {number} db.poolIdleTimeout - Idle timeout in seconds
|
|
29
|
+
* @property {number} db.connectionTimeout - Connection timeout in seconds
|
|
30
|
+
* @property {Object} email - Email configuration
|
|
31
|
+
* @property {number} email.timeout - SMTP/API timeout in ms
|
|
32
|
+
* @property {Object} security - Security configuration
|
|
33
|
+
* @property {boolean} security.enforceHttps - Require HTTPS
|
|
34
|
+
* @property {boolean} security.trustProxy - Trust proxy headers
|
|
35
|
+
* @property {Object} auth - Authentication configuration
|
|
36
|
+
* @property {boolean} auth.allowSignup - Allow public self-service signup
|
|
37
|
+
* @property {Object} features - Feature flags
|
|
38
|
+
* @property {boolean} features.debugRoutes - Enable debug endpoints
|
|
39
|
+
* @property {boolean} features.seedOnStart - Auto-seed database on startup
|
|
40
|
+
* @property {boolean} features.detailedErrors - Include stack traces in error responses
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** @type {Record<Environment, EnvironmentConfig>} */
|
|
44
|
+
const ENVIRONMENT_CONFIGS = {
|
|
45
|
+
development: {
|
|
46
|
+
environment: "development",
|
|
47
|
+
isProduction: false,
|
|
48
|
+
isDevelopment: true,
|
|
49
|
+
isTest: false,
|
|
50
|
+
server: {
|
|
51
|
+
port: 3000,
|
|
52
|
+
},
|
|
53
|
+
rateLimit: {
|
|
54
|
+
windowMs: 15 * 60 * 1000,
|
|
55
|
+
maxRequests: 1000,
|
|
56
|
+
authMaxRequests: 50,
|
|
57
|
+
},
|
|
58
|
+
cache: {
|
|
59
|
+
defaultTtl: 60,
|
|
60
|
+
},
|
|
61
|
+
logging: {
|
|
62
|
+
level: "debug",
|
|
63
|
+
json: false,
|
|
64
|
+
},
|
|
65
|
+
db: {
|
|
66
|
+
poolMax: 5,
|
|
67
|
+
poolIdleTimeout: 30,
|
|
68
|
+
connectionTimeout: 5,
|
|
69
|
+
},
|
|
70
|
+
email: {
|
|
71
|
+
timeout: 10_000,
|
|
72
|
+
},
|
|
73
|
+
security: {
|
|
74
|
+
enforceHttps: false,
|
|
75
|
+
trustProxy: false,
|
|
76
|
+
},
|
|
77
|
+
auth: {
|
|
78
|
+
allowSignup: true,
|
|
79
|
+
},
|
|
80
|
+
features: {
|
|
81
|
+
debugRoutes: true,
|
|
82
|
+
seedOnStart: false,
|
|
83
|
+
detailedErrors: true,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
test: {
|
|
88
|
+
environment: "test",
|
|
89
|
+
isProduction: false,
|
|
90
|
+
isDevelopment: false,
|
|
91
|
+
isTest: true,
|
|
92
|
+
server: {
|
|
93
|
+
port: 3001,
|
|
94
|
+
},
|
|
95
|
+
rateLimit: {
|
|
96
|
+
windowMs: 15 * 60 * 1000,
|
|
97
|
+
maxRequests: 10_000,
|
|
98
|
+
authMaxRequests: 10_000,
|
|
99
|
+
},
|
|
100
|
+
cache: {
|
|
101
|
+
defaultTtl: 0,
|
|
102
|
+
},
|
|
103
|
+
logging: {
|
|
104
|
+
level: "warn",
|
|
105
|
+
json: false,
|
|
106
|
+
},
|
|
107
|
+
db: {
|
|
108
|
+
poolMax: 3,
|
|
109
|
+
poolIdleTimeout: 10,
|
|
110
|
+
connectionTimeout: 5,
|
|
111
|
+
},
|
|
112
|
+
email: {
|
|
113
|
+
timeout: 5_000,
|
|
114
|
+
},
|
|
115
|
+
security: {
|
|
116
|
+
enforceHttps: false,
|
|
117
|
+
trustProxy: false,
|
|
118
|
+
},
|
|
119
|
+
auth: {
|
|
120
|
+
allowSignup: true,
|
|
121
|
+
},
|
|
122
|
+
features: {
|
|
123
|
+
debugRoutes: true,
|
|
124
|
+
seedOnStart: false,
|
|
125
|
+
detailedErrors: true,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
staging: {
|
|
130
|
+
environment: "staging",
|
|
131
|
+
isProduction: true,
|
|
132
|
+
isDevelopment: false,
|
|
133
|
+
isTest: false,
|
|
134
|
+
server: {
|
|
135
|
+
port: 3000,
|
|
136
|
+
},
|
|
137
|
+
rateLimit: {
|
|
138
|
+
windowMs: 15 * 60 * 1000,
|
|
139
|
+
maxRequests: 100,
|
|
140
|
+
authMaxRequests: 5,
|
|
141
|
+
},
|
|
142
|
+
cache: {
|
|
143
|
+
defaultTtl: 300,
|
|
144
|
+
},
|
|
145
|
+
logging: {
|
|
146
|
+
level: "info",
|
|
147
|
+
json: true,
|
|
148
|
+
},
|
|
149
|
+
db: {
|
|
150
|
+
poolMax: 10,
|
|
151
|
+
poolIdleTimeout: 30,
|
|
152
|
+
connectionTimeout: 5,
|
|
153
|
+
},
|
|
154
|
+
email: {
|
|
155
|
+
timeout: 10_000,
|
|
156
|
+
},
|
|
157
|
+
security: {
|
|
158
|
+
enforceHttps: true,
|
|
159
|
+
trustProxy: true,
|
|
160
|
+
},
|
|
161
|
+
auth: {
|
|
162
|
+
allowSignup: true,
|
|
163
|
+
},
|
|
164
|
+
features: {
|
|
165
|
+
debugRoutes: false,
|
|
166
|
+
seedOnStart: false,
|
|
167
|
+
detailedErrors: false,
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
production: {
|
|
172
|
+
environment: "production",
|
|
173
|
+
isProduction: true,
|
|
174
|
+
isDevelopment: false,
|
|
175
|
+
isTest: false,
|
|
176
|
+
server: {
|
|
177
|
+
port: 3000,
|
|
178
|
+
},
|
|
179
|
+
rateLimit: {
|
|
180
|
+
windowMs: 15 * 60 * 1000,
|
|
181
|
+
maxRequests: 100,
|
|
182
|
+
authMaxRequests: 5,
|
|
183
|
+
},
|
|
184
|
+
cache: {
|
|
185
|
+
defaultTtl: 600,
|
|
186
|
+
},
|
|
187
|
+
logging: {
|
|
188
|
+
level: "info",
|
|
189
|
+
json: true,
|
|
190
|
+
},
|
|
191
|
+
db: {
|
|
192
|
+
poolMax: 10,
|
|
193
|
+
poolIdleTimeout: 30,
|
|
194
|
+
connectionTimeout: 5,
|
|
195
|
+
},
|
|
196
|
+
email: {
|
|
197
|
+
timeout: 10_000,
|
|
198
|
+
},
|
|
199
|
+
security: {
|
|
200
|
+
enforceHttps: true,
|
|
201
|
+
trustProxy: true,
|
|
202
|
+
},
|
|
203
|
+
auth: {
|
|
204
|
+
allowSignup: true,
|
|
205
|
+
},
|
|
206
|
+
features: {
|
|
207
|
+
debugRoutes: false,
|
|
208
|
+
seedOnStart: false,
|
|
209
|
+
detailedErrors: false,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/** Valid environment names */
|
|
215
|
+
export const ENVIRONMENTS = /** @type {const} */ ([
|
|
216
|
+
"development",
|
|
217
|
+
"test",
|
|
218
|
+
"staging",
|
|
219
|
+
"production",
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Resolves the current environment from NODE_ENV.
|
|
224
|
+
* Maps common aliases (e.g. "dev" → "development", "prod" → "production").
|
|
225
|
+
* Defaults to "development" if unset or unrecognized.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} [nodeEnv] - Override for NODE_ENV (defaults to process.env.NODE_ENV)
|
|
228
|
+
* @returns {Environment}
|
|
229
|
+
*/
|
|
230
|
+
export function resolveEnvironment(nodeEnv) {
|
|
231
|
+
const raw = (nodeEnv ?? process.env.NODE_ENV ?? "").toLowerCase().trim();
|
|
232
|
+
|
|
233
|
+
const aliases = {
|
|
234
|
+
dev: "development",
|
|
235
|
+
development: "development",
|
|
236
|
+
test: "test",
|
|
237
|
+
testing: "test",
|
|
238
|
+
staging: "staging",
|
|
239
|
+
stage: "staging",
|
|
240
|
+
prod: "production",
|
|
241
|
+
production: "production",
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return aliases[raw] || "development";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Returns the full environment configuration for a given environment.
|
|
249
|
+
* Unknown environments fall back to development.
|
|
250
|
+
*
|
|
251
|
+
* @param {string} [nodeEnv] - Override for NODE_ENV
|
|
252
|
+
* @returns {EnvironmentConfig}
|
|
253
|
+
*/
|
|
254
|
+
export function getEnvironmentConfig(nodeEnv) {
|
|
255
|
+
const env = resolveEnvironment(nodeEnv);
|
|
256
|
+
return { ...ENVIRONMENT_CONFIGS[env] };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Shallow-merges environment config with user-provided overrides (one level deep).
|
|
261
|
+
* Top-level scalars are replaced; top-level objects are spread-merged.
|
|
262
|
+
* Useful when downstream apps need to adjust defaults per-environment.
|
|
263
|
+
*
|
|
264
|
+
* @param {EnvironmentConfig} base - Base environment config
|
|
265
|
+
* @param {Record<string, unknown>} overrides - Partial overrides to merge
|
|
266
|
+
* @returns {EnvironmentConfig}
|
|
267
|
+
*/
|
|
268
|
+
export function mergeConfig(base, overrides) {
|
|
269
|
+
const result = { ...base };
|
|
270
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
271
|
+
if (
|
|
272
|
+
value != null &&
|
|
273
|
+
typeof value === "object" &&
|
|
274
|
+
!Array.isArray(value) &&
|
|
275
|
+
typeof result[key] === "object" &&
|
|
276
|
+
result[key] != null
|
|
277
|
+
) {
|
|
278
|
+
result[key] = { ...result[key], ...value };
|
|
279
|
+
} else {
|
|
280
|
+
result[key] = value;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return result;
|
|
284
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const config = {
|
|
2
|
+
appName: process.env.APP_NAME || "Quark",
|
|
3
|
+
appDescription: process.env.APP_DESCRIPTION || "A Quark-powered application",
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export { getAllowedOrigins, getAppUrl, syncNextAuthUrl } from "./app-url.js";
|
|
7
|
+
export {
|
|
8
|
+
ENVIRONMENTS,
|
|
9
|
+
getEnvironmentConfig,
|
|
10
|
+
mergeConfig,
|
|
11
|
+
resolveEnvironment,
|
|
12
|
+
} from "./environment.js";
|
|
13
|
+
export { getConfig, loadConfig, resetConfig } from "./load-config.js";
|
|
14
|
+
export { applyRateLimit, rateLimit } from "./rate-limit.js";
|
|
15
|
+
export { closeSharedRedisClient, getSharedRedisClient } from "./redis.js";
|
|
16
|
+
export { loadEnv } from "./validate-env.js";
|