@classytic/arc 2.15.3 → 2.16.0

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 (159) hide show
  1. package/README.md +1 -0
  2. package/bin/arc.js +12 -0
  3. package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
  4. package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
  5. package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
  7. package/dist/audit/index.d.mts +1 -1
  8. package/dist/audit/index.mjs +4 -2
  9. package/dist/auth/index.d.mts +4 -4
  10. package/dist/auth/index.mjs +4 -4
  11. package/dist/auth/redis-session.d.mts +1 -1
  12. package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
  13. package/dist/buildHandler-BZX6zzDM.mjs +300 -0
  14. package/dist/cache/index.d.mts +3 -3
  15. package/dist/cache/index.mjs +3 -3
  16. package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
  17. package/dist/cli/commands/describe.d.mts +35 -1
  18. package/dist/cli/commands/describe.mjs +52 -12
  19. package/dist/cli/commands/docs.d.mts +1 -4
  20. package/dist/cli/commands/docs.mjs +4 -16
  21. package/dist/cli/commands/generate.d.mts +2 -20
  22. package/dist/cli/commands/generate.mjs +1 -546
  23. package/dist/cli/commands/init.d.mts +2 -40
  24. package/dist/cli/commands/init.mjs +1 -3036
  25. package/dist/cli/commands/introspect.mjs +53 -64
  26. package/dist/cli/index.d.mts +2 -2
  27. package/dist/cli/index.mjs +2 -2
  28. package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
  29. package/dist/core/index.d.mts +3 -3
  30. package/dist/core/index.mjs +5 -5
  31. package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
  32. package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
  33. package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
  34. package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
  35. package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
  36. package/dist/docs/index.d.mts +2 -2
  37. package/dist/docs/index.mjs +1 -1
  38. package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
  39. package/dist/errors-C1lX_jlm.d.mts +91 -0
  40. package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
  41. package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.d.mts} +1 -1
  42. package/dist/events/index.d.mts +3 -3
  43. package/dist/events/index.mjs +5 -5
  44. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +1 -1
  47. package/dist/factory/index.mjs +2 -2
  48. package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
  49. package/dist/generate-BWFwgcCM.d.mts +38 -0
  50. package/dist/generate-CYac-OLv.mjs +654 -0
  51. package/dist/hooks/index.d.mts +1 -1
  52. package/dist/hooks/index.mjs +1 -1
  53. package/dist/idempotency/index.d.mts +2 -2
  54. package/dist/idempotency/index.mjs +1 -1
  55. package/dist/idempotency/redis.d.mts +1 -1
  56. package/dist/{index-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
  57. package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
  58. package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
  59. package/dist/index.d.mts +6 -6
  60. package/dist/index.mjs +7 -8
  61. package/dist/init-Dv71MsJr.d.mts +71 -0
  62. package/dist/init-HDvoO9L5.mjs +3098 -0
  63. package/dist/integrations/event-gateway.d.mts +2 -2
  64. package/dist/integrations/event-gateway.mjs +1 -1
  65. package/dist/integrations/index.d.mts +2 -2
  66. package/dist/integrations/jobs.mjs +3 -3
  67. package/dist/integrations/mcp/index.d.mts +239 -7
  68. package/dist/integrations/mcp/index.mjs +2 -528
  69. package/dist/integrations/mcp/testing.d.mts +2 -2
  70. package/dist/integrations/mcp/testing.mjs +6 -10
  71. package/dist/integrations/streamline.d.mts +71 -2
  72. package/dist/integrations/streamline.mjs +81 -8
  73. package/dist/integrations/websocket-redis.d.mts +1 -1
  74. package/dist/integrations/websocket.d.mts +1 -1
  75. package/dist/integrations/websocket.mjs +1 -0
  76. package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
  77. package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
  78. package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
  79. package/dist/middleware/index.d.mts +1 -1
  80. package/dist/middleware/index.mjs +1 -1
  81. package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
  82. package/dist/permissions/index.d.mts +2 -2
  83. package/dist/permissions/index.mjs +1 -1
  84. package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
  85. package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
  86. package/dist/pipeline/index.d.mts +1 -1
  87. package/dist/pipeline/index.mjs +1 -1
  88. package/dist/plugins/index.d.mts +5 -5
  89. package/dist/plugins/index.mjs +10 -10
  90. package/dist/plugins/response-cache.mjs +5 -5
  91. package/dist/plugins/tracing-entry.d.mts +1 -1
  92. package/dist/plugins/tracing-entry.mjs +1 -1
  93. package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
  94. package/dist/presets/filesUpload.d.mts +4 -4
  95. package/dist/presets/filesUpload.mjs +2 -2
  96. package/dist/presets/index.d.mts +1 -1
  97. package/dist/presets/index.mjs +1 -1
  98. package/dist/presets/multiTenant.d.mts +1 -1
  99. package/dist/presets/multiTenant.mjs +4 -3
  100. package/dist/presets/search.d.mts +2 -2
  101. package/dist/presets/search.mjs +1 -1
  102. package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
  103. package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
  104. package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
  105. package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
  106. package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
  107. package/dist/registry/index.d.mts +319 -2
  108. package/dist/registry/index.mjs +3 -3
  109. package/dist/registry-BBE23CDj.mjs +576 -0
  110. package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
  111. package/dist/scope/index.d.mts +3 -3
  112. package/dist/scope/index.mjs +3 -3
  113. package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
  114. package/dist/testing/index.d.mts +2 -2
  115. package/dist/testing/index.mjs +16 -7
  116. package/dist/testing/storageContract.d.mts +1 -1
  117. package/dist/types/index.d.mts +5 -5
  118. package/dist/types/storage.d.mts +1 -1
  119. package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
  120. package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
  121. package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
  122. package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
  123. package/dist/utils/index.d.mts +1286 -2
  124. package/dist/utils/index.mjs +1 -1
  125. package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
  126. package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
  127. package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
  128. package/package.json +22 -29
  129. package/skills/arc/SKILL.md +299 -689
  130. package/skills/arc/references/auth.md +19 -7
  131. package/skills/arc-code-review/SKILL.md +1 -1
  132. package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
  133. package/dist/createActionRouter-S3MLVYot.mjs +0 -220
  134. package/dist/index-bRjYu21O.d.mts +0 -1320
  135. package/dist/org/index.d.mts +0 -66
  136. package/dist/org/index.mjs +0 -486
  137. package/dist/org/types.d.mts +0 -82
  138. package/dist/org/types.mjs +0 -1
  139. package/dist/registry-I-ogLgL9.mjs +0 -46
  140. /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
  141. /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
  142. /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
  143. /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
  144. /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
  145. /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
  146. /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
  147. /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
  148. /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
  149. /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
  150. /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
  151. /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
  152. /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
  153. /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
  154. /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
  155. /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
  156. /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
  157. /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
  158. /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
  159. /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BkjeGZRn.d.mts} +0 -0
@@ -1,3037 +1,2 @@
1
- import * as fs from "node:fs/promises";
2
- import * as path from "node:path";
3
- import { accessSync } from "node:fs";
4
- import { execSync, spawn } from "node:child_process";
5
- import * as readline from "node:readline";
6
- //#region src/cli/commands/init.ts
7
- /**
8
- * Arc CLI - Init Command
9
- *
10
- * Scaffolds a new Arc project with clean architecture:
11
- * - MongoKit or Custom adapter
12
- * - Multi-tenant or Single-tenant
13
- * - TypeScript or JavaScript
14
- *
15
- * Automatically installs dependencies using detected package manager.
16
- */
17
- /**
18
- * Initialize a new Arc project
19
- */
20
- async function init(options = {}) {
21
- console.log(`
22
- ╔═══════════════════════════════════════════════════════════════╗
23
- ║ Arc Project Setup ║
24
- ║ Resource-Oriented Backend Framework ║
25
- ╚═══════════════════════════════════════════════════════════════╝
26
- `);
27
- const config = await gatherConfig(options);
28
- console.log(`\nCreating project: ${config.name}`);
29
- console.log(` Adapter: ${config.adapter === "mongokit" ? "MongoKit (MongoDB)" : "Custom / Drizzle-ready"}`);
30
- console.log(` Auth: ${config.auth === "better-auth" ? "Better Auth (recommended)" : "Arc JWT"}`);
31
- if (config.auth === "better-auth") {
32
- console.log(` Session: ${config.session === "cookie" ? "Cookie" : config.session === "bearer" ? "Bearer token" : "Cookie + Bearer"}`);
33
- if (config.apiKey) console.log(` API keys: enabled (@better-auth/api-key)`);
34
- }
35
- console.log(` Tenant: ${config.tenant === "multi" ? "Multi-tenant" : "Single-tenant"}`);
36
- console.log(` Language: ${config.typescript ? "TypeScript" : "JavaScript"}`);
37
- console.log(` Target: ${config.edge ? "Edge/Serverless" : "Node.js Server"}\n`);
38
- const projectPath = path.join(process.cwd(), config.name);
39
- try {
40
- await fs.access(projectPath);
41
- if (!options.force) throw new Error(`Directory "${config.name}" already exists. Use --force to overwrite.`);
42
- } catch (err) {
43
- if (!(err && typeof err === "object" && "code" in err && err.code === "ENOENT")) throw err;
44
- }
45
- const packageManager = detectPackageManager();
46
- console.log(`Using package manager: ${packageManager}\n`);
47
- await createProjectStructure(projectPath, config);
48
- if (!options.skipInstall) {
49
- console.log("\n📥 Installing dependencies...\n");
50
- await installDependencies(projectPath, config, packageManager);
51
- }
52
- printSuccessMessage(config, options.skipInstall);
53
- }
54
- /**
55
- * Detect which package manager to use
56
- * Priority: pnpm > yarn > bun > npm (based on lockfile or global availability)
57
- */
58
- function detectPackageManager() {
59
- try {
60
- const cwd = process.cwd();
61
- if (existsSync$1(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
62
- if (existsSync$1(path.join(cwd, "yarn.lock"))) return "yarn";
63
- if (existsSync$1(path.join(cwd, "bun.lockb"))) return "bun";
64
- if (existsSync$1(path.join(cwd, "package-lock.json"))) return "npm";
65
- } catch {}
66
- if (isCommandAvailable("pnpm")) return "pnpm";
67
- if (isCommandAvailable("yarn")) return "yarn";
68
- if (isCommandAvailable("bun")) return "bun";
69
- return "npm";
70
- }
71
- /**
72
- * Check if a command is available in PATH
73
- */
74
- function isCommandAvailable(command) {
75
- try {
76
- execSync(`${command} --version`, { stdio: "ignore" });
77
- return true;
78
- } catch {
79
- return false;
80
- }
81
- }
82
- /**
83
- * Sync check if file exists (ESM-compatible — no require())
84
- */
85
- function existsSync$1(filePath) {
86
- try {
87
- accessSync(filePath);
88
- return true;
89
- } catch {
90
- return false;
91
- }
92
- }
93
- /**
94
- * Single source of truth for scaffolded project dependencies.
95
- *
96
- * Versions are pinned to the floor each subsystem requires — peer-dep
97
- * minimums on Arc, kit minimums (mongokit ≥ 3.11, repo-core ≥ 0.2,
98
- * mongoose ≥ 9.4.1), and major-version stable for the rest. The carets
99
- * allow minor + patch upgrades without breaking arc's contract, while
100
- * preventing the silent breakage of `@latest` on a kit floor bump.
101
- *
102
- * Used by both `packageJsonTemplate` (declares the deps in the generated
103
- * `package.json` so `npm install` works without a pre-pass) and
104
- * `installDependencies` (runs the package manager's `install` against
105
- * the declared ranges). One source — no drift.
106
- */
107
- const SCAFFOLD_DEP_VERSIONS = {
108
- core: {
109
- "@classytic/arc": "^2.13.0",
110
- "@classytic/primitives": "^0.3.0",
111
- "@classytic/repo-core": "^0.4.0",
112
- "@fastify/cors": "^11.2.0",
113
- "@fastify/helmet": "^13.0.2",
114
- "@fastify/rate-limit": "^10.3.0",
115
- "@fastify/sensible": "^6.0.4",
116
- "@fastify/under-pressure": "^9.0.3",
117
- dotenv: "^17.4.2",
118
- fastify: "^5.8.5"
119
- },
120
- authJwt: {
121
- "@fastify/jwt": "^10.0.0",
122
- bcryptjs: "^3.0.0"
123
- },
124
- authBetterAuth: {
125
- "better-auth": "^1.6.9",
126
- mongodb: "^7.1.0"
127
- },
128
- authBetterAuthApiKey: { "@better-auth/api-key": "^1.6.9" },
129
- adapterMongokit: {
130
- "@classytic/mongokit": "^3.13.0",
131
- mongoose: "^9.6.1"
132
- },
133
- devCommon: {
134
- "mongodb-memory-server": "^11.1.0",
135
- "pino-pretty": "^13.0.0",
136
- vitest: "^4.1.5"
137
- },
138
- devTypescript: {
139
- "@types/node": "^22.10.0",
140
- tsx: "^4.21.0",
141
- typescript: "^5.7.2"
142
- },
143
- typesJwt: { "@types/bcryptjs": "^3.0.0" }
144
- };
145
- /**
146
- * Resolve the dependency manifest for a scaffold configuration.
147
- *
148
- * Returns sorted records (alphabetical by package name) so the generated
149
- * `package.json` is deterministic — diffs across re-runs stay clean.
150
- */
151
- function resolveScaffoldDependencies(config) {
152
- const dependencies = { ...SCAFFOLD_DEP_VERSIONS.core };
153
- const devDependencies = { ...SCAFFOLD_DEP_VERSIONS.devCommon };
154
- if (config.auth === "better-auth") {
155
- Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authBetterAuth);
156
- if (config.apiKey) Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authBetterAuthApiKey);
157
- } else {
158
- Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.authJwt);
159
- if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.typesJwt);
160
- }
161
- if (config.adapter === "mongokit") Object.assign(dependencies, SCAFFOLD_DEP_VERSIONS.adapterMongokit);
162
- if (config.typescript) Object.assign(devDependencies, SCAFFOLD_DEP_VERSIONS.devTypescript);
163
- return {
164
- dependencies: sortByKey(dependencies),
165
- devDependencies: sortByKey(devDependencies)
166
- };
167
- }
168
- /**
169
- * Sort a record alphabetically by key — package.json convention.
170
- */
171
- function sortByKey(record) {
172
- return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
173
- }
174
- /**
175
- * Install dependencies using the detected package manager.
176
- *
177
- * Dependencies are already declared in the generated `package.json` (see
178
- * `packageJsonTemplate`), so a single plain `install` resolves the full
179
- * tree. No two-pass `npm add` flow — the manifest is the source of truth.
180
- */
181
- async function installDependencies(projectPath, _config, pm) {
182
- console.log(` Installing dependencies...`);
183
- await runCommand(getInstallCommand(pm), projectPath);
184
- console.log(`\nDependencies installed successfully.`);
185
- }
186
- /**
187
- * Get the plain `install` command for a package manager. Reads the declared
188
- * dependencies from the project's `package.json`.
189
- */
190
- function getInstallCommand(pm) {
191
- switch (pm) {
192
- case "pnpm": return "pnpm install";
193
- case "yarn": return "yarn install";
194
- case "bun": return "bun install";
195
- default: return "npm install";
196
- }
197
- }
198
- /**
199
- * Run a shell command in a directory
200
- */
201
- function runCommand(command, cwd) {
202
- return new Promise((resolve, reject) => {
203
- const isWindows = process.platform === "win32";
204
- const child = spawn(isWindows ? "cmd" : "/bin/sh", [isWindows ? "/c" : "-c", command], {
205
- cwd,
206
- stdio: "inherit",
207
- env: {
208
- ...process.env,
209
- FORCE_COLOR: "1"
210
- }
211
- });
212
- child.on("close", (code) => {
213
- if (code === 0) resolve();
214
- else reject(/* @__PURE__ */ new Error(`Command failed with exit code ${code}`));
215
- });
216
- child.on("error", reject);
217
- });
218
- }
219
- async function gatherConfig(options) {
220
- const rl = readline.createInterface({
221
- input: process.stdin,
222
- output: process.stdout
223
- });
224
- const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
225
- const nonInteractive = !!options.name;
226
- try {
227
- const name = options.name || await question("Project name: ") || "my-arc-app";
228
- let adapter = options.adapter || "mongokit";
229
- if (!options.adapter && !nonInteractive) adapter = await question("Database adapter [1=MongoKit (recommended), 2=Custom / Drizzle-ready]: ") === "2" ? "custom" : "mongokit";
230
- let auth = options.auth || "better-auth";
231
- if (!options.auth && !nonInteractive) auth = await question("Auth strategy [1=Better Auth (recommended), 2=Arc JWT]: ") === "2" ? "jwt" : "better-auth";
232
- let session = options.session ?? "cookie";
233
- let apiKey = options.apiKey ?? false;
234
- if (auth === "better-auth" && !nonInteractive) {
235
- if (options.session === void 0) {
236
- const sessionChoice = await question("Session strategy [1=Cookie (web app, default), 2=Bearer token (mobile/SPA), 3=Both]: ");
237
- session = sessionChoice === "2" ? "bearer" : sessionChoice === "3" ? "both" : "cookie";
238
- }
239
- if (options.apiKey === void 0) apiKey = (await question("Enable API key plugin (machine-to-machine auth via @better-auth/api-key)? [y/N]: ")).toLowerCase() === "y";
240
- }
241
- let tenant = options.tenant || "single";
242
- if (!options.tenant && !nonInteractive) tenant = await question("Tenant mode [1=Single-tenant, 2=Multi-tenant]: ") === "2" ? "multi" : "single";
243
- let typescript = options.typescript ?? true;
244
- if (options.typescript === void 0 && !nonInteractive) typescript = await question("Language [1=TypeScript (recommended), 2=JavaScript]: ") !== "2";
245
- let edge = options.edge ?? false;
246
- if (options.edge === void 0 && !nonInteractive) edge = await question("Deployment target [1=Node.js Server (default), 2=Edge/Serverless]: ") === "2";
247
- if (edge && adapter === "mongokit" && !nonInteractive) {
248
- console.log("");
249
- console.log(" ⚠ Edge + MongoKit: Mongoose does NOT work on Cloudflare Workers.");
250
- console.log(" MongoDB Atlas works with the raw driver (mongodb 6.15+ with nodejs_compat_v2),");
251
- console.log(" but MongoKit depends on Mongoose. Options:");
252
- console.log(" 1. Use AWS Lambda / Vercel Serverless (Node.js) — Mongoose works normally");
253
- console.log(" 2. Use Cloudflare Hyperdrive + PostgreSQL (wire sqlitekit/Drizzle via custom adapter)");
254
- console.log(" 3. Continue with MongoKit — works on Lambda/Vercel, NOT on Cloudflare Workers");
255
- console.log("");
256
- if ((await question("Continue with MongoKit? [y/N]: ")).toLowerCase() !== "y") {
257
- adapter = "custom";
258
- console.log(" Switched to custom adapter. Wire sqlitekit/Drizzle, prismakit, or any RepositoryLike-conforming repo.");
259
- }
260
- }
261
- return {
262
- name,
263
- adapter,
264
- auth,
265
- tenant,
266
- apiKey,
267
- session,
268
- typescript,
269
- edge
270
- };
271
- } finally {
272
- rl.close();
273
- }
274
- }
275
- async function createProjectStructure(projectPath, config) {
276
- const ext = config.typescript ? "ts" : "js";
277
- const dirs = [
278
- "",
279
- "src",
280
- "src/config",
281
- "src/shared",
282
- "src/shared/presets",
283
- "src/plugins",
284
- "src/resources",
285
- ...config.auth === "jwt" ? ["src/resources/user", "src/resources/auth"] : [],
286
- "src/resources/example",
287
- "tests"
288
- ];
289
- for (const dir of dirs) {
290
- await fs.mkdir(path.join(projectPath, dir), { recursive: true });
291
- console.log(` + Created: ${dir || "/"}`);
292
- }
293
- const files = {
294
- "package.json": packageJsonTemplate(config),
295
- ".gitignore": gitignoreTemplate(),
296
- ".env.example": envExampleTemplate(config),
297
- ".env.dev": envDevTemplate(config),
298
- "README.md": readmeTemplate(config)
299
- };
300
- if (config.typescript) files["tsconfig.json"] = tsconfigTemplate();
301
- files["vitest.config.ts"] = vitestConfigTemplate(config);
302
- files[`src/config/env.${ext}`] = envLoaderTemplate(config);
303
- files[`src/config/index.${ext}`] = configTemplate(config);
304
- files[`src/app.${ext}`] = appTemplate(config);
305
- files[`src/index.${ext}`] = indexTemplate(config);
306
- files[`src/shared/index.${ext}`] = sharedIndexTemplate(config);
307
- files[`src/shared/adapter.${ext}`] = config.adapter === "mongokit" ? createAdapterTemplate(config) : customAdapterTemplate(config);
308
- files[`src/shared/permissions.${ext}`] = permissionsTemplate(config);
309
- if (config.tenant === "multi") {
310
- files[`src/shared/presets/index.${ext}`] = presetsMultiTenantTemplate(config);
311
- files[`src/shared/presets/flexible-multi-tenant.${ext}`] = flexibleMultiTenantPresetTemplate(config);
312
- } else files[`src/shared/presets/index.${ext}`] = presetsSingleTenantTemplate(config);
313
- files[`src/plugins/index.${ext}`] = pluginsIndexTemplate(config);
314
- files[`src/resources/index.${ext}`] = resourcesIndexTemplate(config);
315
- if (config.auth === "better-auth") files[`src/auth.${ext}`] = betterAuthSetupTemplate(config);
316
- else {
317
- files[`src/resources/user/user.model.${ext}`] = userModelTemplate(config);
318
- files[`src/resources/user/user.repository.${ext}`] = userRepositoryTemplate(config);
319
- files[`src/resources/user/user.controller.${ext}`] = userControllerTemplate(config);
320
- files[`src/resources/auth/auth.resource.${ext}`] = authResourceTemplate(config);
321
- files[`src/resources/auth/auth.handlers.${ext}`] = authHandlersTemplate(config);
322
- files[`src/resources/auth/auth.schemas.${ext}`] = authSchemasTemplate(config);
323
- }
324
- files[`src/resources/example/example.model.${ext}`] = exampleModelTemplate(config);
325
- files[`src/resources/example/example.repository.${ext}`] = exampleRepositoryTemplate(config);
326
- files[`src/resources/example/example.resource.${ext}`] = exampleResourceTemplate(config);
327
- files[`src/resources/example/example.controller.${ext}`] = exampleControllerTemplate(config);
328
- files[`src/resources/example/example.schemas.${ext}`] = exampleSchemasTemplate(config);
329
- files[`tests/example.test.${ext}`] = exampleTestTemplate(config);
330
- if (config.auth === "jwt") files[`tests/auth.test.${ext}`] = authTestTemplate(config);
331
- if (!config.edge) {
332
- files.Dockerfile = dockerfileTemplate(config);
333
- files[".dockerignore"] = dockerignoreTemplate();
334
- files["docker-compose.yml"] = dockerComposeTemplate(config);
335
- }
336
- if (config.edge) files["wrangler.toml"] = wranglerTemplate(config);
337
- files[".arcrc"] = `${JSON.stringify({
338
- adapter: config.adapter,
339
- auth: config.auth,
340
- tenant: config.tenant,
341
- typescript: config.typescript
342
- }, null, 2)}\n`;
343
- for (const [filePath, content] of Object.entries(files)) {
344
- const fullPath = path.join(projectPath, filePath);
345
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
346
- await fs.writeFile(fullPath, content);
347
- console.log(` + Created: ${filePath}`);
348
- }
349
- }
350
- function packageJsonTemplate(config) {
351
- const { dependencies, devDependencies } = resolveScaffoldDependencies(config);
352
- const scripts = config.typescript ? config.edge ? {
353
- dev: "tsx watch src/index.ts",
354
- build: "tsc",
355
- start: "node dist/index.js",
356
- deploy: "wrangler deploy",
357
- "deploy:dev": "wrangler dev",
358
- test: "vitest run",
359
- "test:watch": "vitest"
360
- } : {
361
- dev: "tsx watch src/index.ts",
362
- build: "tsc",
363
- start: "node dist/index.js",
364
- test: "vitest run",
365
- "test:watch": "vitest"
366
- } : config.edge ? {
367
- dev: "node --watch src/index.js",
368
- start: "node src/index.js",
369
- deploy: "wrangler deploy",
370
- "deploy:dev": "wrangler dev",
371
- test: "vitest run",
372
- "test:watch": "vitest"
373
- } : {
374
- dev: "node --watch src/index.js",
375
- start: "node src/index.js",
376
- test: "vitest run",
377
- "test:watch": "vitest"
378
- };
379
- return JSON.stringify({
380
- name: config.name,
381
- version: "1.0.0",
382
- type: "module",
383
- main: config.typescript ? "dist/index.js" : "src/index.js",
384
- imports: {
385
- "#config/*": "./src/config/*",
386
- "#shared/*": "./src/shared/*",
387
- "#resources/*": "./src/resources/*",
388
- "#plugins/*": "./src/plugins/*",
389
- "#services/*": "./src/services/*",
390
- "#lib/*": "./src/lib/*",
391
- "#utils/*": "./src/utils/*"
392
- },
393
- scripts,
394
- dependencies,
395
- devDependencies,
396
- engines: { node: ">=22" }
397
- }, null, 2);
398
- }
399
- function tsconfigTemplate() {
400
- return JSON.stringify({
401
- compilerOptions: {
402
- target: "ES2022",
403
- module: "NodeNext",
404
- moduleResolution: "NodeNext",
405
- lib: ["ES2022"],
406
- outDir: "./dist",
407
- rootDir: "./src",
408
- strict: true,
409
- esModuleInterop: true,
410
- skipLibCheck: true,
411
- forceConsistentCasingInFileNames: true,
412
- declaration: true,
413
- declarationMap: true,
414
- sourceMap: true,
415
- resolveJsonModule: true,
416
- paths: {
417
- "#shared/*": ["./src/shared/*"],
418
- "#resources/*": ["./src/resources/*"],
419
- "#config/*": ["./src/config/*"],
420
- "#plugins/*": ["./src/plugins/*"]
421
- }
422
- },
423
- include: ["src/**/*"],
424
- exclude: ["node_modules", "dist"]
425
- }, null, 2);
426
- }
427
- function vitestConfigTemplate(config) {
428
- const srcDir = config.typescript ? "./src" : "./src";
429
- return `import { defineConfig } from 'vitest/config';
430
- import { resolve } from 'path';
431
-
432
- export default defineConfig({
433
- test: {
434
- globals: true,
435
- environment: 'node',
436
- },
437
- resolve: {
438
- alias: {
439
- '#config': resolve(__dirname, '${srcDir}/config'),
440
- '#shared': resolve(__dirname, '${srcDir}/shared'),
441
- '#resources': resolve(__dirname, '${srcDir}/resources'),
442
- '#plugins': resolve(__dirname, '${srcDir}/plugins'),
443
- },
444
- },
445
- });
446
- `;
447
- }
448
- function gitignoreTemplate() {
449
- return `# Dependencies
450
- node_modules/
451
-
452
- # Build
453
- dist/
454
- *.js.map
455
-
456
- # Environment (local overrides — never commit secrets)
457
- .env.local
458
- .env.*.local
459
- # Uncomment if your .env contains secrets:
460
- # .env
461
-
462
- # IDE
463
- .vscode/
464
- .idea/
465
- *.swp
466
- *.swo
467
-
468
- # OS
469
- .DS_Store
470
- Thumbs.db
471
-
472
- # Logs
473
- *.log
474
- npm-debug.log*
475
-
476
- # Test coverage
477
- coverage/
478
- `;
479
- }
480
- function envExampleTemplate(config) {
481
- let content = `# Environment Files (Next.js-style priority):
482
- # .env.local → machine-specific overrides (gitignored)
483
- # .env.production → production defaults
484
- # .env.development → development defaults (or .env.dev)
485
- # .env → shared defaults (fallback)
486
- #
487
- # Tip: Copy this file to .env.local for local development
488
-
489
- # Server
490
- PORT=8040
491
- HOST=0.0.0.0
492
- NODE_ENV=development
493
- `;
494
- if (config.auth === "better-auth") content += `
495
- # Better Auth
496
- BETTER_AUTH_SECRET=your-32-character-minimum-secret-here
497
- FRONTEND_URL=http://localhost:3000
498
-
499
- # Google OAuth (optional)
500
- # GOOGLE_CLIENT_ID=
501
- # GOOGLE_CLIENT_SECRET=
502
- `;
503
- else content += `
504
- # JWT
505
- JWT_SECRET=your-32-character-minimum-secret-here
506
- JWT_EXPIRES_IN=7d
507
- `;
508
- content += `
509
- # CORS - Allowed origins
510
- # Options:
511
- # * = allow all origins (not recommended for production)
512
- # Comma-separated list = specific origins only
513
- CORS_ORIGINS=http://localhost:3000,http://localhost:5173
514
- `;
515
- if (config.adapter === "mongokit") content += `
516
- # MongoDB
517
- MONGODB_URI=mongodb://localhost:27017/${config.name}
518
- `;
519
- if (config.tenant === "multi") content += `
520
- # Multi-tenant
521
- ORG_HEADER=x-organization-id
522
- `;
523
- return content;
524
- }
525
- function readmeTemplate(config) {
526
- const ext = config.typescript ? "ts" : "js";
527
- return `# ${config.name}
528
-
529
- Built with [Arc](https://github.com/classytic/arc) - Resource-Oriented Backend Framework
530
-
531
- ## Quick Start
532
-
533
- \`\`\`bash
534
- # Install dependencies
535
- npm install
536
-
537
- # Start development server (uses .env.dev)
538
- npm run dev
539
-
540
- # Run tests
541
- npm test
542
- \`\`\`
543
-
544
- ## Project Structure
545
-
546
- \`\`\`
547
- src/
548
- ├── config/ # Configuration (loaded first)
549
- │ ├── env.${ext} # Env loader (import first!)
550
- │ └── index.${ext} # App config
551
- ├── shared/ # Shared utilities
552
- │ ├── adapter.${ext} # ${config.adapter === "mongokit" ? "MongoKit adapter factory" : "Custom / Drizzle-ready adapter"}
553
- │ ├── permissions.${ext} # Permission helpers
554
- │ └── presets/ # ${config.tenant === "multi" ? "Multi-tenant presets" : "Standard presets"}
555
- ├── plugins/ # App-specific plugins
556
- │ └── index.${ext} # Plugin registry
557
- ├── resources/ # API Resources
558
- │ ├── index.${ext} # Resource registry
559
- │ └── example/ # Example resource
560
- │ ├── index.${ext} # Resource definition
561
- │ ├── model.${ext} # Mongoose schema
562
- │ └── repository.${ext} # MongoKit repository
563
- ├── app.${ext} # App factory (reusable)
564
- └── index.${ext} # Server entry point
565
- tests/
566
- └── example.test.${ext} # Example tests
567
- \`\`\`
568
-
569
- ## Architecture
570
-
571
- ### Entry Points
572
-
573
- - **\`src/index.${ext}\`** - ${config.edge ? "Edge/serverless fetch handler (Cloudflare Workers, Lambda, Vercel)" : "HTTP server entry point"}
574
- - **\`src/app.${ext}\`** - App factory (import for workers/tests)
575
-
576
- \`\`\`${config.typescript ? "typescript" : "javascript"}
577
- // For workers or custom entry points:
578
- import { createAppInstance } from './app.js';
579
-
580
- const app = await createAppInstance();
581
- // Use app for your worker logic
582
- \`\`\`
583
-
584
- ### Adding Resources
585
-
586
- 1. Create a new folder in \`src/resources/\`:
587
-
588
- \`\`\`
589
- src/resources/product/
590
- ├── index.${ext} # Resource definition
591
- ├── model.${ext} # Mongoose schema
592
- └── repository.${ext} # MongoKit repository
593
- \`\`\`
594
-
595
- 2. Register in \`src/resources/index.${ext}\`:
596
-
597
- \`\`\`${config.typescript ? "typescript" : "javascript"}
598
- import productResource from './product/index.js';
599
-
600
- export const resources = [
601
- exampleResource,
602
- productResource, // Add here
603
- ];
604
- \`\`\`
605
-
606
- ### Adding Plugins
607
-
608
- Add custom plugins in \`src/plugins/index.${ext}\`:
609
-
610
- \`\`\`${config.typescript ? "typescript" : "javascript"}
611
- export async function registerPlugins(app, deps) {
612
- const { config } = deps; // Explicit dependency injection
613
-
614
- await app.register(myCustomPlugin, { ...options });
615
- }
616
- \`\`\`
617
-
618
- ## CLI Commands
619
-
620
- \`\`\`bash
621
- # Generate a new resource
622
- arc generate resource product
623
-
624
- # Introspect existing schema
625
- arc introspect
626
-
627
- # Generate API docs
628
- arc docs
629
- \`\`\`
630
-
631
- ## Environment Files (Next.js-style)
632
-
633
- Priority (first loaded wins):
634
- 1. \`.env.local\` — Machine-specific overrides (gitignored)
635
- 2. \`.env.{environment}\` — e.g., \`.env.production\`, \`.env.development\`, \`.env.test\`
636
- 3. \`.env\` — Shared defaults (fallback)
637
-
638
- Short forms also supported: \`.env.prod\`, \`.env.dev\`, \`.env.test\`
639
-
640
- ## API Documentation
641
-
642
- API documentation is available via Scalar UI:
643
-
644
- - **Interactive UI**: [http://localhost:8040/docs](http://localhost:8040/data)
645
- - **OpenAPI Spec**: [http://localhost:8040/_docs/openapi.json](http://localhost:8040/_docs/openapi.json)
646
-
647
- ## API Endpoints
648
-
649
- | Method | Endpoint | Description |
650
- |--------|----------|-------------|
651
- | GET | /docs | API documentation (Scalar UI) |
652
- | GET | /_docs/openapi.json | OpenAPI 3.0 spec |
653
- | GET | /examples | List all |
654
- | GET | /examples/:id | Get by ID |
655
- | POST | /examples | Create |
656
- | PATCH | /examples/:id | Update |
657
- | DELETE | /examples/:id | Delete |
658
-
659
- ## Docker Deployment
660
-
661
- This project comes ready for containerization:
662
-
663
- \`\`\`bash
664
- # Build the production image
665
- docker build -t ${config.name} .
666
-
667
- # Run the container
668
- docker run -p 8040:8040 --env-file .env ${config.name}
669
- \`\`\`
670
-
671
- If you're using a database (like MongoDB), you can use Docker Compose to spin up the full stack locally:
672
-
673
- \`\`\`bash
674
- docker-compose up -d
675
- \`\`\`
676
- `;
677
- }
678
- function indexTemplate(config) {
679
- const ts = config.typescript;
680
- if (config.edge) return edgeIndexTemplate(config);
681
- return `/**
682
- * ${config.name} - Server Entry Point
683
- * Generated by Arc CLI
684
- *
685
- * This file starts the HTTP server.
686
- * For workers or other entry points, import createAppInstance from './app.js'
687
- */
688
-
689
- // Load environment FIRST (before any other imports)
690
- import '#config/env.js';
691
-
692
- import config from '#config/index.js';
693
- ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';" : ""}
694
- import { createAppInstance } from './app.js';
695
-
696
- async function main()${ts ? ": Promise<void>" : ""} {
697
- console.log(\`Environment: \${config.env}\`);
698
- ${config.adapter === "mongokit" ? `
699
- // Connect to MongoDB
700
- await mongoose.connect(config.database.uri);
701
- console.log('Connected to MongoDB');
702
- ` : ""}
703
- // Create and configure app
704
- const app = await createAppInstance();
705
-
706
- // Start server
707
- await app.listen({ port: config.server.port, host: config.server.host });
708
- console.log(\`Server running at http://\${config.server.host}:\${config.server.port}\`);
709
- }
710
-
711
- main().catch((err) => {
712
- console.error('Failed to start server:', err);
713
- process.exit(1);
714
- });
715
- `;
716
- }
717
- /**
718
- * Edge/serverless entry point — exports a Web Standards fetch handler.
719
- * Works on Cloudflare Workers, AWS Lambda, Vercel Serverless, etc.
720
- */
721
- function edgeIndexTemplate(config) {
722
- const ts = config.typescript;
723
- const dbNote = config.adapter === "mongokit" ? ` *\n * NOTE: Mongoose does NOT work on Cloudflare Workers. This entry point\n * works on AWS Lambda and Vercel Serverless (Node.js runtime) where\n * Mongoose/MongoKit works normally. For Cloudflare Workers, switch to\n * Drizzle + Hyperdrive (PostgreSQL) or the raw mongodb driver.` : "";
724
- return `/**
725
- * ${config.name} - Edge/Serverless Entry Point
726
- * Generated by Arc CLI
727
- *
728
- * Exports a Web Standards fetch handler that works on:
729
- * - Cloudflare Workers (enable nodejs_compat in wrangler.toml)
730
- * - AWS Lambda (via fetch-based adapter)
731
- * - Vercel Serverless Functions
732
- * - Any runtime supporting the Web Standards Request/Response API
733
- *
734
- * No app.listen() — routes through Fastify's .inject() internally.
735
- ${dbNote}
736
- */
737
-
738
- import { toFetchHandler } from '@classytic/arc/factory';
739
- import { createAppInstance } from './app.js';
740
-
741
- const app = await createAppInstance();
742
- const handler = toFetchHandler(app);
743
-
744
- /**
745
- * Cloudflare Workers / generic fetch handler
746
- */
747
- export default {
748
- async fetch(request${ts ? ": Request" : ""})${ts ? ": Promise<Response>" : ""} {
749
- return handler(request);
750
- },
751
- };
752
-
753
- /**
754
- * Named export for platforms that expect it (Vercel, AWS Lambda adapters)
755
- */
756
- export { handler };
757
- `;
758
- }
759
- function appTemplate(config) {
760
- const ts = config.typescript;
761
- const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
762
- const betterAuthImport = config.auth === "better-auth" ? `import { createBetterAuthAdapter } from '@classytic/arc/auth';
763
- import { getAuth } from './auth.js';
764
- ` : "";
765
- const authConfig = config.auth === "better-auth" ? `auth: {
766
- type: 'betterAuth',
767
- betterAuth: createBetterAuthAdapter({ auth: getAuth(), orgContext: true }),
768
- },` : `auth: {
769
- type: 'jwt',
770
- jwt: { secret: config.jwt.secret },
771
- },`;
772
- return `/**
773
- * ${config.name} - App Factory
774
- * Generated by Arc CLI
775
- *
776
- * Creates and configures the Fastify app instance.
777
- * Can be imported by:
778
- * - index.ts (HTTP server via app.listen, or edge handler via toFetchHandler)
779
- * - worker.ts (background workers)
780
- * - tests (integration tests via app.inject)
781
- */
782
-
783
- ${typeImport}import config from '#config/index.js';
784
- import { createApp, loadResources } from '@classytic/arc/factory';
785
- ${betterAuthImport}
786
- // App-specific plugins
787
- import { registerPlugins } from '#plugins/index.js';
788
-
789
- // Resource registry
790
- import { resources, registerResources } from '#resources/index.js';
791
-
792
- /**
793
- * Create a fully configured app instance
794
- *
795
- * @returns Configured Fastify instance ready to use
796
- */
797
- export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : ""} {
798
- // Create Arc app with resources and base configuration. \`resourcePrefix\`
799
- // mounts every resource under \`/api\`, matching the \`apiPrefix\` the
800
- // OpenAPI plugin advertises — keeps docs and routes in agreement.
801
- const app = await createApp({
802
- preset: config.env === 'production' ? (${config.edge ? "'edge'" : "'production'"}) : 'development',
803
- resources,
804
- resourcePrefix: '/api',
805
- ${authConfig}
806
- cors: {
807
- origin: config.cors.origins,
808
- methods: config.cors.methods,
809
- allowedHeaders: config.cors.allowedHeaders,
810
- credentials: config.cors.credentials,
811
- },
812
- trustProxy: true,
813
- arcPlugins: {
814
- metrics: config.env === 'production', // Prometheus /_metrics endpoint
815
- },
816
- });
817
-
818
- // Register app-specific plugins (explicit dependency injection)
819
- await registerPlugins(app, { config });
820
-
821
- return app;
822
- }
823
-
824
- export default createAppInstance;
825
- `;
826
- }
827
- function envLoaderTemplate(config) {
828
- const ts = config.typescript;
829
- return `/**
830
- * Environment Loader
831
- *
832
- * MUST be imported FIRST before any other imports.
833
- * Loads .env files based on NODE_ENV with Next.js-style priority:
834
- *
835
- * .env.local (always loaded first — gitignored, machine-specific overrides)
836
- * .env.{environment} (e.g., .env.production, .env.dev, .env.test)
837
- * .env (fallback defaults)
838
- *
839
- * Supports both long-form (production, development, test) and
840
- * short-form (prod, dev, test) env file names.
841
- *
842
- * Usage:
843
- * import '#config/env.js'; // First line of entry point
844
- */
845
-
846
- import dotenv from 'dotenv';
847
- import { existsSync } from 'node:fs';
848
- import { resolve } from 'node:path';
849
-
850
- ${ts ? "type EnvName = 'prod' | 'dev' | 'test';\n" : ""}const ENV_ALIASES${ts ? ": Record<EnvName, string>" : ""} = {
851
- prod: 'production',
852
- dev: 'development',
853
- test: 'test',
854
- };
855
-
856
- function normalizeEnv(env${ts ? ": string | undefined" : ""})${ts ? ": EnvName" : ""} {
857
- const raw = (env || '').toLowerCase();
858
- if (raw === 'production' || raw === 'prod') return 'prod';
859
- if (raw === 'test' || raw === 'qa') return 'test';
860
- return 'dev';
861
- }
862
-
863
- const env = normalizeEnv(process.env.NODE_ENV);
864
- const longForm = ENV_ALIASES[env];
865
-
866
- // Priority: .env.local → .env.{long} → .env.{short} → .env
867
- // Same convention as Next.js — .env.local always wins, never committed to git
868
- const candidates = [
869
- '.env.local',
870
- \`.env.\${longForm}\`,
871
- \`.env.\${env}\`,
872
- '.env',
873
- ].map((f) => resolve(process.cwd(), f));
874
-
875
- const loaded${ts ? ": string[]" : ""} = [];
876
- for (const file of candidates) {
877
- if (existsSync(file)) {
878
- // override: false means earlier files take priority (first loaded wins)
879
- dotenv.config({ path: file, override: false });
880
- loaded.push(file.split(/[\\\\/]/).pop()${ts ? "!" : ""});
881
- }
882
- }
883
-
884
- // Only log in development (silent in production/test)
885
- if (env === 'dev' && loaded.length > 0) {
886
- console.log(\`env: \${loaded.join(' + ')}\`);
887
- } else if (loaded.length === 0) {
888
- console.warn('No .env file found — using process environment only');
889
- }
890
-
891
- export const ENV = env;
892
- `;
893
- }
894
- function envDevTemplate(config) {
895
- let content = `# Development Environment
896
- NODE_ENV=development
897
-
898
- # Server
899
- PORT=8040
900
- HOST=0.0.0.0
901
- `;
902
- if (config.auth === "better-auth") content += `
903
- # Better Auth
904
- BETTER_AUTH_SECRET=dev-secret-change-in-production-min-32-chars
905
- FRONTEND_URL=http://localhost:3000
906
-
907
- # Google OAuth (optional — leave empty to disable)
908
- GOOGLE_CLIENT_ID=
909
- GOOGLE_CLIENT_SECRET=
910
- `;
911
- else content += `
912
- # JWT
913
- JWT_SECRET=dev-secret-change-in-production-min-32-chars
914
- JWT_EXPIRES_IN=7d
915
- `;
916
- content += `
917
- # CORS - Allowed origins
918
- # Options:
919
- # * = allow all origins (not recommended for production)
920
- # Comma-separated list = specific origins only
921
- CORS_ORIGINS=http://localhost:3000,http://localhost:5173
922
- `;
923
- if (config.adapter === "mongokit") content += `
924
- # MongoDB
925
- MONGODB_URI=mongodb://localhost:27017/${config.name}
926
- `;
927
- if (config.tenant === "multi") content += `
928
- # Multi-tenant
929
- ORG_HEADER=x-organization-id
930
- `;
931
- return content;
932
- }
933
- function pluginsIndexTemplate(config) {
934
- const ts = config.typescript;
935
- const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
936
- const configType = ts ? ": { config: AppConfig }" : "";
937
- const appType = ts ? ": FastifyInstance" : "";
938
- let content = `/**
939
- * App Plugins Registry
940
- *
941
- * Register your app-specific plugins here.
942
- * Dependencies are passed explicitly (no shims, no magic).
943
- */
944
-
945
- ${typeImport}${ts ? "import type { AppConfig } from '../config/index.js';\n" : ""}import { openApiPlugin, scalarPlugin } from '@classytic/arc/docs';
946
- import { errorHandlerPlugin } from '@classytic/arc/plugins';
947
- `;
948
- content += `
949
- /**
950
- * Register all app-specific plugins
951
- *
952
- * @param app - Fastify instance
953
- * @param deps - Explicit dependencies (config, services, etc.)
954
- */
955
- export async function registerPlugins(
956
- app${appType},
957
- deps${configType}
958
- )${ts ? ": Promise<void>" : ""} {
959
- const { config } = deps;
960
-
961
- // Error handling (CastError → 400, validation → 422, duplicate → 409)
962
- await app.register(errorHandlerPlugin, {
963
- includeStack: config.isDev,
964
- });
965
-
966
- // API Documentation (Scalar UI)
967
- // OpenAPI spec: /_docs/openapi.json
968
- // Scalar UI: /docs
969
- await app.register(openApiPlugin, {
970
- title: '${config.name} API',
971
- version: '1.0.0',
972
- description: 'API documentation for ${config.name}',
973
- apiPrefix: '/api',
974
- });
975
- await app.register(scalarPlugin, {
976
- routePrefix: '/docs',
977
- theme: 'default',
978
- });
979
-
980
- // Add your custom plugins here:
981
- // await app.register(myCustomPlugin, { ...options });
982
- }
983
- `;
984
- return content;
985
- }
986
- function resourcesIndexTemplate(config) {
987
- const ts = config.typescript;
988
- const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
989
- const appType = ts ? ": FastifyInstance" : "";
990
- return `/**
991
- * Resources Registry
992
- *
993
- * Central registry for all API resources.
994
- * All resources are mounted under /api prefix via Fastify scoping.
995
- */
996
-
997
- ${typeImport}${config.auth === "jwt" ? `
998
- // Auth resources (register, login, /users/me)
999
- import { authResource, userProfileResource } from './auth/auth.resource.js';
1000
- ` : `
1001
- // Auth is handled by Better Auth — routes at /api/auth/*
1002
- // No manual auth resource needed.
1003
- `}
1004
- // App resources
1005
- import exampleResource from './example/example.resource.js';
1006
-
1007
- // Add more resources here:
1008
- // import productResource from './product/product.resource.js';
1009
-
1010
- /**
1011
- * All registered resources
1012
- */
1013
- export const resources = [
1014
- ${config.auth === "jwt" ? ` authResource,
1015
- userProfileResource,
1016
- ` : ` `}exampleResource,
1017
- ]${ts ? " as const" : ""};
1018
-
1019
- /**
1020
- * Register all resources with the app under a common prefix.
1021
- * Fastify scoping ensures all routes are mounted at /api/*.
1022
- * The apiPrefix option in openApiPlugin keeps OpenAPI docs in sync.
1023
- */
1024
- export async function registerResources(app${appType}, prefix = '/api')${ts ? ": Promise<void>" : ""} {
1025
- await app.register(async (scope) => {
1026
- for (const resource of resources) {
1027
- await scope.register(resource.toPlugin());
1028
- }
1029
- }, { prefix });
1030
- }
1031
- `;
1032
- }
1033
- function sharedIndexTemplate(_config) {
1034
- return `/**
1035
- * Shared Utilities
1036
- *
1037
- * Central exports for resource definitions.
1038
- * Import from here for clean, consistent code.
1039
- */
1040
-
1041
- // Adapter factory
1042
- export { createAdapter } from './adapter.js';
1043
-
1044
- // Core Arc exports
1045
- export { defineResource } from '@classytic/arc';
1046
- export { createMongooseAdapter } from '@classytic/mongokit/adapter';
1047
-
1048
- // Permission helpers (core + application-level)
1049
- export * from './permissions.js';
1050
-
1051
- // Presets
1052
- export * from './presets/index.js';
1053
- `;
1054
- }
1055
- function createAdapterTemplate(config) {
1056
- const ts = config.typescript;
1057
- return `/**
1058
- * MongoKit Adapter Factory
1059
- *
1060
- * Creates Arc adapters using MongoKit repositories.
1061
- * The repository handles query parsing via MongoKit's built-in QueryParser.
1062
- */
1063
-
1064
- import { createMongooseAdapter } from '@classytic/mongokit/adapter';
1065
- import { buildCrudSchemasFromModel } from '@classytic/mongokit';
1066
- ${ts ? "import type { Model } from 'mongoose';\nimport type { Repository } from '@classytic/mongokit';" : ""}
1067
-
1068
- /**
1069
- * Create a MongoKit-powered adapter for a resource.
1070
- *
1071
- * Note: Query parsing is handled by MongoKit's Repository class.
1072
- * \`buildCrudSchemasFromModel\` is the canonical OpenAPI schema generator
1073
- * for arc + Mongoose (arc 2.12+ no longer ships a built-in fallback —
1074
- * passing it explicitly is required for OpenAPI auto-generation).
1075
- */
1076
- export function createAdapter${ts ? "<TDoc = any>" : ""}(
1077
- model${ts ? ": Model<TDoc>" : ""},
1078
- repository${ts ? ": Repository<TDoc>" : ""}
1079
- ) {
1080
- return createMongooseAdapter({
1081
- model,
1082
- repository,
1083
- schemaGenerator: buildCrudSchemasFromModel,
1084
- });
1085
- }
1086
- `;
1087
- }
1088
- function customAdapterTemplate(config) {
1089
- const ts = config.typescript;
1090
- return `/**
1091
- * Custom Adapter Factory
1092
- *
1093
- * Use this for the bring-your-own-repository path — any object that
1094
- * satisfies the \`RepositoryLike\` contract from
1095
- * \`@classytic/repo-core/adapter\` plugs in here. Each classytic kit
1096
- * also ships its own \`/adapter\` subpath; if one of those fits, import
1097
- * its factory directly instead.
1098
- */
1099
-
1100
- ${ts ? "import type { DataAdapter, RepositoryLike } from '@classytic/repo-core/adapter';" : ""}
1101
-
1102
- /**
1103
- * Create a custom adapter for a resource.
1104
- *
1105
- * Pass any object satisfying \`RepositoryLike<TDoc>\` (a 5-method floor:
1106
- * \`getAll\` / \`getById\` / \`create\` / \`update\` / \`delete\`, plus any
1107
- * optional \`StandardRepo\` methods you implement).
1108
- */
1109
- export function createAdapter${ts ? "<TDoc = unknown>" : ""}(
1110
- _source${ts ? ": unknown" : ""},
1111
- repository${ts ? ": RepositoryLike<TDoc>" : ""}
1112
- )${ts ? ": DataAdapter<TDoc>" : ""} {
1113
- return {
1114
- type: 'custom',
1115
- name: 'custom-repository',
1116
- repository,
1117
- };
1118
- }
1119
- `;
1120
- }
1121
- function presetsMultiTenantTemplate(config) {
1122
- return `/**
1123
- * Arc Presets - Multi-Tenant Configuration
1124
- *
1125
- * Pre-configured presets for multi-tenant applications.
1126
- * Includes both strict and flexible tenant isolation options.
1127
- */
1128
-
1129
- import {
1130
- multiTenantPreset,
1131
- ownedByUserPreset,
1132
- softDeletePreset,
1133
- slugLookupPreset,
1134
- } from '@classytic/arc/presets';
1135
-
1136
- // Flexible preset for mixed public/private routes
1137
- export { flexibleMultiTenantPreset } from './flexible-multi-tenant.js';
1138
-
1139
- /**
1140
- * Organization-scoped preset (STRICT)
1141
- * Always requires auth, always filters by organizationId.
1142
- * Use for admin-only resources.
1143
- */
1144
- export const orgScoped = multiTenantPreset({
1145
- tenantField: 'organizationId',
1146
- });
1147
-
1148
- /**
1149
- * Owned by creator preset
1150
- * Filters queries by createdBy field.
1151
- */
1152
- export const ownedByCreator = ownedByUserPreset({
1153
- ownerField: 'createdBy',
1154
- });
1155
-
1156
- /**
1157
- * Owned by user preset
1158
- * For resources where userId references the owner.
1159
- */
1160
- export const ownedByUser = ownedByUserPreset({
1161
- ownerField: 'userId',
1162
- });
1163
-
1164
- /**
1165
- * Soft delete preset
1166
- * Adds deletedAt filtering and restore endpoint.
1167
- */
1168
- export const softDelete = softDeletePreset();
1169
-
1170
- /**
1171
- * Slug lookup preset
1172
- * Enables GET by slug in addition to ID.
1173
- */
1174
- export const slugLookup = slugLookupPreset();
1175
-
1176
- // Export all presets
1177
- export const presets = {
1178
- orgScoped,
1179
- ownedByCreator,
1180
- ownedByUser,
1181
- softDelete,
1182
- slugLookup,
1183
- }${config.typescript ? " as const" : ""};
1184
-
1185
- export default presets;
1186
- `;
1187
- }
1188
- function presetsSingleTenantTemplate(config) {
1189
- return `/**
1190
- * Arc Presets - Single-Tenant Configuration
1191
- *
1192
- * Pre-configured presets for single-tenant applications.
1193
- */
1194
-
1195
- import {
1196
- ownedByUserPreset,
1197
- softDeletePreset,
1198
- slugLookupPreset,
1199
- } from '@classytic/arc/presets';
1200
-
1201
- /**
1202
- * Owned by creator preset
1203
- * Filters queries by createdBy field.
1204
- */
1205
- export const ownedByCreator = ownedByUserPreset({
1206
- ownerField: 'createdBy',
1207
- });
1208
-
1209
- /**
1210
- * Owned by user preset
1211
- * For resources where userId references the owner.
1212
- */
1213
- export const ownedByUser = ownedByUserPreset({
1214
- ownerField: 'userId',
1215
- });
1216
-
1217
- /**
1218
- * Soft delete preset
1219
- * Adds deletedAt filtering and restore endpoint.
1220
- */
1221
- export const softDelete = softDeletePreset();
1222
-
1223
- /**
1224
- * Slug lookup preset
1225
- * Enables GET by slug in addition to ID.
1226
- */
1227
- export const slugLookup = slugLookupPreset();
1228
-
1229
- // Export all presets
1230
- export const presets = {
1231
- ownedByCreator,
1232
- ownedByUser,
1233
- softDelete,
1234
- slugLookup,
1235
- }${config.typescript ? " as const" : ""};
1236
-
1237
- export default presets;
1238
- `;
1239
- }
1240
- function flexibleMultiTenantPresetTemplate(config) {
1241
- const ts = config.typescript;
1242
- return `/**
1243
- * Flexible Multi-Tenant Preset
1244
- *
1245
- * Smarter tenant filtering that works with public + authenticated routes.
1246
- *
1247
- * Philosophy:
1248
- * - No org scope → No filtering (public data, all orgs)
1249
- * - Org scope present → Filter by org
1250
- * - Elevated scope → No filter (platform admin sees all)
1251
- *
1252
- * Uses request.scope (RequestScope) from Arc's scope system.
1253
- */
1254
- ${ts ? `
1255
- import { getOrgId, isElevated, isMember } from '@classytic/arc/scope';
1256
- import type { RequestScope } from '@classytic/arc/scope';
1257
-
1258
- interface FlexibleMultiTenantOptions {
1259
- tenantField?: string;
1260
- }
1261
-
1262
- interface PresetMiddlewares {
1263
- list: ((request: any, reply: any) => Promise<void>)[];
1264
- get: ((request: any, reply: any) => Promise<void>)[];
1265
- create: ((request: any, reply: any) => Promise<void>)[];
1266
- update: ((request: any, reply: any) => Promise<void>)[];
1267
- delete: ((request: any, reply: any) => Promise<void>)[];
1268
- }
1269
-
1270
- interface Preset {
1271
- [key: string]: unknown;
1272
- name: string;
1273
- middlewares: PresetMiddlewares;
1274
- }
1275
- ` : `
1276
- const { getOrgId, isElevated, isMember } = require('@classytic/arc/scope');
1277
- `}
1278
- /**
1279
- * Create flexible tenant filter middleware.
1280
- * Only filters when org context is present.
1281
- */
1282
- function createFlexibleTenantFilter(tenantField${ts ? ": string" : ""}) {
1283
- return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
1284
- const scope${ts ? ": RequestScope" : ""} = request.scope ?? { kind: 'public' };
1285
-
1286
- // Elevated scope — platform admin sees all, no filter
1287
- if (isElevated(scope)) {
1288
- request.log?.debug?.({ msg: 'Elevated scope — no tenant filter' });
1289
- return;
1290
- }
1291
-
1292
- // Member scope — filter by org
1293
- if (isMember(scope)) {
1294
- request.query = request.query ?? {};
1295
- request.query._policyFilters = {
1296
- ...(request.query._policyFilters ?? {}),
1297
- [tenantField]: scope.organizationId,
1298
- };
1299
- request.log?.debug?.({ msg: 'Tenant filter applied', orgId: scope.organizationId, tenantField });
1300
- return;
1301
- }
1302
-
1303
- // Public / authenticated — no org context, show all data (public routes)
1304
- request.log?.debug?.({ msg: 'No org context — showing all data' });
1305
- };
1306
- }
1307
-
1308
- /**
1309
- * Create tenant injection middleware.
1310
- * Injects tenant ID into request body on create.
1311
- */
1312
- function createTenantInjection(tenantField${ts ? ": string" : ""}) {
1313
- return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
1314
- const scope${ts ? ": RequestScope" : ""} = request.scope ?? { kind: 'public' };
1315
- const orgId = getOrgId(scope);
1316
-
1317
- // Fail-closed: Require orgId for create operations
1318
- if (!orgId) {
1319
- return reply.code(403).send({
1320
- error: 'Organization context required to create resources',
1321
- code: 'arc.org_required',
1322
- });
1323
- }
1324
-
1325
- if (request.body) {
1326
- request.body[tenantField] = orgId;
1327
- }
1328
- };
1329
- }
1330
-
1331
- /**
1332
- * Flexible Multi-Tenant Preset
1333
- *
1334
- * @param options.tenantField - Field name in database (default: 'organizationId')
1335
- */
1336
- export function flexibleMultiTenantPreset(options${ts ? ": FlexibleMultiTenantOptions = {}" : " = {}"})${ts ? ": Preset" : ""} {
1337
- const { tenantField = 'organizationId' } = options;
1338
-
1339
- const tenantFilter = createFlexibleTenantFilter(tenantField);
1340
- const tenantInjection = createTenantInjection(tenantField);
1341
-
1342
- return {
1343
- name: 'flexibleMultiTenant',
1344
- middlewares: {
1345
- list: [tenantFilter],
1346
- get: [tenantFilter],
1347
- create: [tenantInjection],
1348
- update: [tenantFilter],
1349
- delete: [tenantFilter],
1350
- },
1351
- };
1352
- }
1353
-
1354
- export default flexibleMultiTenantPreset;
1355
- `;
1356
- }
1357
- function permissionsTemplate(config) {
1358
- const ts = config.typescript;
1359
- const typeImport = ts ? ",\n type PermissionCheck," : "";
1360
- const returnType = ts ? ": PermissionCheck" : "";
1361
- let content = `/**
1362
- * Permission Helpers
1363
- *
1364
- * Clean, type-safe permission definitions for resources.
1365
- */
1366
-
1367
- import {
1368
- requireAuth,
1369
- requireRoles,
1370
- requireOwnership,
1371
- allowPublic,
1372
- roles,
1373
- anyOf,
1374
- allOf,
1375
- denyAll,
1376
- when${typeImport}
1377
- } from '@classytic/arc/permissions';
1378
-
1379
- // Re-export core helpers
1380
- export {
1381
- allowPublic,
1382
- requireAuth,
1383
- requireRoles,
1384
- requireOwnership,
1385
- roles,
1386
- allOf,
1387
- anyOf,
1388
- denyAll,
1389
- when,
1390
- };
1391
-
1392
- // ============================================================================
1393
- // Permission Helpers
1394
- // ============================================================================
1395
-
1396
- /**
1397
- * Require any authenticated user
1398
- */
1399
- export const requireAuthenticated = ()${returnType} =>
1400
- requireRoles(['user', 'admin', 'superadmin']);
1401
-
1402
- /**
1403
- * Require admin or superadmin
1404
- */
1405
- export const requireAdmin = ()${returnType} =>
1406
- requireRoles(['admin', 'superadmin']);
1407
-
1408
- /**
1409
- * Require superadmin only
1410
- */
1411
- export const requireSuperadmin = ()${returnType} =>
1412
- requireRoles(['superadmin']);
1413
- `;
1414
- if (config.tenant === "multi") if (config.auth === "better-auth") content += `
1415
- // ============================================================================
1416
- // Better Auth Organization & Team Permission Helpers
1417
- // ============================================================================
1418
-
1419
- /**
1420
- * Organization-level guards (per-org member.role):
1421
- *
1422
- * - requireRoles('admin') — checks BOTH user.role AND org member.role (recommended)
1423
- * - requireOrgRole(['admin','owner']) — checks member.role in active org ONLY
1424
- * - requireOrgMembership() — just checks if user is in the org (any role)
1425
- * - requireTeamMembership() — checks if user is in the active team
1426
- *
1427
- * RECOMMENDED: Use requireRoles() for most cases. Since Arc 2.7.1 it defaults to
1428
- * checking both platform AND org roles, so a single call covers BA org plugin users
1429
- * with platform-admin overrides. Use requireOrgRole() when you ONLY want org-level
1430
- * checks (and want to explicitly exclude platform admins).
1431
- *
1432
- * Platform superadmin automatically bypasses all org role checks.
1433
- *
1434
- * IMPORTANT: When using Better Auth's Access Control (ac) with custom roles,
1435
- * you MUST define ALL roles (owner, admin, member, + any custom) using the
1436
- * same AC instance. BA's built-in defaults won't cover custom statements.
1437
- * Omitting any role causes BA's hasPermission to fail silently for that role.
1438
- *
1439
- * @see multi-org-betterauth boilerplate (src/shared/access-control.ts) for the recommended pattern.
1440
- */
1441
- import {
1442
- requireOrgMembership,
1443
- requireOrgRole,
1444
- requireTeamMembership,
1445
- } from '@classytic/arc/permissions';
1446
- export { requireOrgMembership, requireOrgRole, requireTeamMembership };
1447
-
1448
- /**
1449
- * Require organization owner (checks member.role, not user.role)
1450
- */
1451
- export const requireOrgOwner = ()${returnType} =>
1452
- requireOrgRole(['owner']);
1453
-
1454
- /**
1455
- * Require organization manager or higher (checks member.role, not user.role)
1456
- */
1457
- export const requireOrgManager = ()${returnType} =>
1458
- requireOrgRole(['manager', 'admin', 'owner']);
1459
-
1460
- /**
1461
- * Require any organization member (any role)
1462
- */
1463
- export const requireOrgStaff = ()${returnType} =>
1464
- requireOrgMembership();
1465
- `;
1466
- else content += `
1467
- /**
1468
- * Require organization owner (elevated scope auto-bypasses)
1469
- */
1470
- export const requireOrgOwner = ()${returnType} =>
1471
- requireRoles(['owner', 'admin', 'superadmin']);
1472
-
1473
- /**
1474
- * Require organization manager or higher
1475
- */
1476
- export const requireOrgManager = ()${returnType} =>
1477
- requireRoles(['owner', 'manager', 'admin', 'superadmin']);
1478
-
1479
- /**
1480
- * Require organization staff (any org member)
1481
- */
1482
- export const requireOrgStaff = ()${returnType} =>
1483
- requireRoles(['owner', 'manager', 'staff', 'admin', 'superadmin']);
1484
- `;
1485
- content += `
1486
- // ============================================================================
1487
- // Standard Permission Sets
1488
- // ============================================================================
1489
-
1490
- /**
1491
- * Public read, authenticated write (default for most resources)
1492
- */
1493
- export const publicReadPermissions = {
1494
- list: allowPublic(),
1495
- get: allowPublic(),
1496
- create: requireAuthenticated(),
1497
- update: requireAuthenticated(),
1498
- delete: requireAuthenticated(),
1499
- };
1500
-
1501
- /**
1502
- * All operations require authentication
1503
- */
1504
- export const authenticatedPermissions = {
1505
- list: requireAuth(),
1506
- get: requireAuth(),
1507
- create: requireAuth(),
1508
- update: requireAuth(),
1509
- delete: requireAuth(),
1510
- };
1511
-
1512
- /**
1513
- * Admin only permissions
1514
- */
1515
- export const adminPermissions = {
1516
- list: requireAdmin(),
1517
- get: requireAdmin(),
1518
- create: requireSuperadmin(),
1519
- update: requireSuperadmin(),
1520
- delete: requireSuperadmin(),
1521
- };
1522
- `;
1523
- if (config.tenant === "multi") {
1524
- content += `
1525
- /**
1526
- * Organization staff permissions
1527
- */
1528
- export const orgStaffPermissions = {
1529
- list: requireOrgStaff(),
1530
- get: requireOrgStaff(),
1531
- create: requireOrgManager(),
1532
- update: requireOrgManager(),
1533
- delete: requireOrgOwner(),
1534
- };
1535
- `;
1536
- if (config.auth === "better-auth") content += `
1537
- /**
1538
- * Team-scoped permissions (requires active team)
1539
- * Uses Better Auth's team membership — flat groups, no team-level roles.
1540
- */
1541
- export const teamScopedPermissions = {
1542
- list: requireTeamMembership(),
1543
- get: requireTeamMembership(),
1544
- create: requireTeamMembership(),
1545
- update: requireTeamMembership(),
1546
- delete: requireOrgOwner(),
1547
- };
1548
- `;
1549
- }
1550
- return content;
1551
- }
1552
- function configTemplate(config) {
1553
- const ts = config.typescript;
1554
- const authTypeBlock = config.auth === "better-auth" ? `
1555
- betterAuth: {
1556
- secret: string;
1557
- };
1558
- frontend: {
1559
- url: string;
1560
- };` : `
1561
- jwt: {
1562
- secret: string;
1563
- expiresIn: string;
1564
- };`;
1565
- let typeDefinition = "";
1566
- if (ts) typeDefinition = `
1567
- export interface AppConfig {
1568
- env: string;
1569
- isDev: boolean;
1570
- isProd: boolean;
1571
- server: {
1572
- port: number;
1573
- host: string;
1574
- };${authTypeBlock}
1575
- cors: {
1576
- origins: string[] | boolean; // true = allow all ('*')
1577
- methods: string[];
1578
- allowedHeaders: string[];
1579
- credentials: boolean;
1580
- };${config.adapter === "mongokit" ? `
1581
- database: {
1582
- uri: string;
1583
- };` : ""}${config.tenant === "multi" ? `
1584
- org: {
1585
- header: string;
1586
- };` : ""}
1587
- }
1588
- `;
1589
- const authConfigBlock = config.auth === "better-auth" ? `
1590
- betterAuth: {
1591
- secret: process.env.BETTER_AUTH_SECRET || 'dev-secret-change-in-production-min-32-chars',
1592
- },
1593
-
1594
- frontend: {
1595
- url: process.env.FRONTEND_URL || 'http://localhost:3000',
1596
- },` : `
1597
- jwt: {
1598
- secret: process.env.JWT_SECRET || 'dev-secret-change-in-production-min-32',
1599
- expiresIn: process.env.JWT_EXPIRES_IN || '7d',
1600
- },`;
1601
- return `/**
1602
- * Application Configuration
1603
- *
1604
- * All config is loaded from environment variables.
1605
- * ENV file is loaded by config/env.ts (imported first in entry points).
1606
- */
1607
- ${typeDefinition}
1608
- const config${ts ? ": AppConfig" : ""} = {
1609
- env: process.env.NODE_ENV || 'development',
1610
- isDev: (process.env.NODE_ENV || 'development') !== 'production',
1611
- isProd: process.env.NODE_ENV === 'production',
1612
-
1613
- server: {
1614
- port: parseInt(process.env.PORT || '8040', 10),
1615
- host: process.env.HOST || '0.0.0.0',
1616
- },
1617
- ${authConfigBlock}
1618
-
1619
- cors: {
1620
- // '*' = allow all origins (true), otherwise comma-separated list
1621
- origins:
1622
- process.env.CORS_ORIGINS === '*'
1623
- ? true
1624
- : (process.env.CORS_ORIGINS || 'http://localhost:3000').split(','),
1625
- methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
1626
- allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id', 'x-request-id'],
1627
- credentials: true,
1628
- },
1629
- ${config.adapter === "mongokit" ? `
1630
- database: {
1631
- uri: process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}',
1632
- },
1633
- ` : ""}${config.tenant === "multi" ? `
1634
- org: {
1635
- header: process.env.ORG_HEADER || 'x-organization-id',
1636
- },
1637
- ` : ""}};
1638
-
1639
- export default config;
1640
- `;
1641
- }
1642
- function exampleModelTemplate(config) {
1643
- const ts = config.typescript;
1644
- const typeExport = ts ? `
1645
- export type ExampleDocument = mongoose.InferSchemaType<typeof exampleSchema>;
1646
- export type ExampleModel = mongoose.Model<ExampleDocument>;
1647
- ` : "";
1648
- return `/**
1649
- * Example Model
1650
- * Generated by Arc CLI
1651
- */
1652
-
1653
- import mongoose from 'mongoose';
1654
-
1655
- const exampleSchema = new mongoose.Schema(
1656
- {
1657
- name: { type: String, required: true, trim: true },
1658
- description: { type: String, trim: true },
1659
- isActive: { type: Boolean, default: true, index: true },
1660
- ${config.tenant === "multi" ? " organizationId: { type: mongoose.Schema.Types.ObjectId, ref: 'Organization', required: true, index: true },\n" : ""} createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', index: true },
1661
- deletedAt: { type: Date, default: null, index: true },
1662
- },
1663
- {
1664
- timestamps: true,
1665
- toJSON: { virtuals: true },
1666
- toObject: { virtuals: true },
1667
- }
1668
- );
1669
-
1670
- // Indexes for common queries
1671
- exampleSchema.index({ name: 1 });
1672
- exampleSchema.index({ deletedAt: 1, isActive: 1 });
1673
- ${config.tenant === "multi" ? "exampleSchema.index({ organizationId: 1, deletedAt: 1 });\n" : ""}${typeExport}
1674
- const Example = mongoose.model${ts ? "<ExampleDocument>" : ""}('Example', exampleSchema);
1675
-
1676
- export default Example;
1677
- `;
1678
- }
1679
- function exampleRepositoryTemplate(config) {
1680
- const ts = config.typescript;
1681
- return `/**
1682
- * Example Repository
1683
- * Generated by Arc CLI
1684
- *
1685
- * MongoKit repository with plugins for:
1686
- * - Soft delete (deletedAt filtering)
1687
- * - Custom business logic methods
1688
- */
1689
-
1690
- import {
1691
- Repository,
1692
- softDeletePlugin,
1693
- methodRegistryPlugin,
1694
- mongoOperationsPlugin,
1695
- } from '@classytic/mongokit';
1696
- ${ts ? "import type { ExampleDocument } from './example.model.js';\n" : ""}import Example from './example.model.js';
1697
-
1698
- class ExampleRepository extends Repository${ts ? "<ExampleDocument>" : ""} {
1699
- constructor() {
1700
- super(Example, [
1701
- methodRegistryPlugin(),
1702
- softDeletePlugin(),
1703
- mongoOperationsPlugin(),
1704
- ]);
1705
- }
1706
-
1707
- /**
1708
- * Find all active (non-deleted) records
1709
- */
1710
- async findActive() {
1711
- return this.Model.find({ isActive: true, deletedAt: null }).lean();
1712
- }
1713
- ${config.tenant === "multi" ? `
1714
- /**
1715
- * Find active records for an organization
1716
- */
1717
- async findActiveByOrg(organizationId${ts ? ": string" : ""}) {
1718
- return this.Model.find({
1719
- organizationId,
1720
- isActive: true,
1721
- deletedAt: null,
1722
- }).lean();
1723
- }
1724
- ` : ""}
1725
- // Note: softDeletePlugin provides restore() and getDeleted() methods automatically
1726
- }
1727
-
1728
- const exampleRepository = new ExampleRepository();
1729
-
1730
- export default exampleRepository;
1731
- export { ExampleRepository };
1732
- `;
1733
- }
1734
- function exampleResourceTemplate(config) {
1735
- const ts = config.typescript;
1736
- return `/**
1737
- * Example Resource
1738
- * Generated by Arc CLI
1739
- *
1740
- * A complete resource with:
1741
- * - Model (Mongoose schema)
1742
- * - Repository (MongoKit with plugins)
1743
- * - Permissions (role-based access)
1744
- * - Presets (soft delete${config.tenant === "multi" ? ", multi-tenant" : ""})
1745
- */
1746
-
1747
- import { defineResource } from '@classytic/arc';
1748
- import { QueryParser } from '@classytic/mongokit';
1749
- import { createAdapter } from '#shared/adapter.js';
1750
- import { ${config.tenant === "multi" ? "orgStaffPermissions" : "publicReadPermissions"} } from '#shared/permissions.js';
1751
- ${config.tenant === "multi" ? "import { flexibleMultiTenantPreset } from '#shared/presets/flexible-multi-tenant.js';\n" : ""}import Example${ts ? ", { type ExampleDocument }" : ""} from './example.model.js';
1752
- import exampleRepository from './example.repository.js';
1753
- import exampleController from './example.controller.js';
1754
-
1755
- const queryParser = new QueryParser({
1756
- allowedFilterFields: ['isActive'],
1757
- });
1758
-
1759
- const exampleResource = defineResource${ts ? "<ExampleDocument>" : ""}({
1760
- name: 'example',
1761
- displayName: 'Examples',
1762
- prefix: '/examples',
1763
-
1764
- adapter: createAdapter(Example, exampleRepository),
1765
- controller: exampleController,
1766
- queryParser,
1767
-
1768
- presets: [
1769
- 'softDelete',
1770
- 'bulk',${config.tenant === "multi" ? `
1771
- flexibleMultiTenantPreset({ tenantField: 'organizationId' }),` : ""}
1772
- ],
1773
-
1774
- permissions: ${config.tenant === "multi" ? "orgStaffPermissions" : "publicReadPermissions"},
1775
-
1776
- // Add custom routes here:
1777
- // routes: [
1778
- // {
1779
- // method: 'GET',
1780
- // path: '/custom',
1781
- // summary: 'Custom endpoint',
1782
- // handler: async (request, reply) => { ... },
1783
- // },
1784
- // ],
1785
- });
1786
-
1787
- export default exampleResource;
1788
- `;
1789
- }
1790
- function exampleControllerTemplate(config) {
1791
- config.typescript;
1792
- return `/**
1793
- * Example Controller
1794
- * Generated by Arc CLI
1795
- *
1796
- * BaseController provides CRUD operations with:
1797
- * - Automatic pagination
1798
- * - Query parsing
1799
- * - Validation
1800
- */
1801
-
1802
- import { BaseController } from '@classytic/arc';
1803
- import exampleRepository from './example.repository.js';
1804
- import { exampleSchemaOptions } from './example.schemas.js';
1805
-
1806
- class ExampleController extends BaseController {
1807
- constructor() {
1808
- super(exampleRepository, {
1809
- schemaOptions: exampleSchemaOptions,${config.tenant === "multi" ? `
1810
- tenantField: 'organizationId', // Configurable tenant field for multi-tenant` : `
1811
- // tenantField: 'organizationId', // For multi-tenant apps`}
1812
- });
1813
- }
1814
-
1815
- // Add custom controller methods here:
1816
- // async customAction(request, reply) {
1817
- // // Custom logic
1818
- // }
1819
- }
1820
-
1821
- const exampleController = new ExampleController();
1822
- export default exampleController;
1823
- `;
1824
- }
1825
- function exampleSchemasTemplate(config) {
1826
- const ts = config.typescript;
1827
- const multiTenantFields = config.tenant === "multi";
1828
- return `/**
1829
- * Example Schemas
1830
- * Generated by Arc CLI
1831
- *
1832
- * Schema options for controller validation and query parsing
1833
- */
1834
-
1835
- import Example from './example.model.js';
1836
- import { buildCrudSchemasFromModel } from '@classytic/mongokit/utils';
1837
-
1838
- /**
1839
- * CRUD Schemas with Field Rules
1840
- * Auto-generated from Mongoose model
1841
- */
1842
- const crudSchemas = buildCrudSchemasFromModel(Example, {
1843
- strictAdditionalProperties: true,
1844
- fieldRules: {
1845
- // Framework-injected fields — strip from body + required[]
1846
- // deletedAt: { systemManaged: true },
1847
- // Legitimate null values (Zod .nullable() patterns) — widen JSON-Schema type
1848
- // priceMode: { nullable: true },
1849
- // Elevated-admin override for systemManaged fields (cross-tenant writes)
1850
- // organizationId: { systemManaged: true, preserveForElevated: true },
1851
- },
1852
- query: {
1853
- filterableFields: {
1854
- isActive: 'boolean',${multiTenantFields ? `
1855
- organizationId: 'ObjectId',` : ""}
1856
- createdAt: 'date',
1857
- },
1858
- },
1859
- });
1860
-
1861
- // Schema options for controller
1862
- export const exampleSchemaOptions${ts ? ": any" : ""} = {
1863
- query: {${multiTenantFields ? `
1864
- allowedPopulate: ['organizationId'],` : ""}
1865
- filterableFields: {
1866
- isActive: 'boolean',${multiTenantFields ? `
1867
- organizationId: 'ObjectId',` : ""}
1868
- createdAt: 'date',
1869
- },
1870
- },
1871
- };
1872
-
1873
- export default crudSchemas;
1874
- `;
1875
- }
1876
- function exampleTestTemplate(config) {
1877
- const ts = config.typescript;
1878
- return `/**
1879
- * Example Resource Tests
1880
- * Generated by Arc CLI
1881
- *
1882
- * Run tests: npm test
1883
- * Watch mode: npm run test:watch
1884
- *
1885
- * Uses arc's 2.11 testing surface:
1886
- * - createTestApp — turnkey Fastify + in-memory Mongo + auth + fixtures
1887
- * - expectArc — fluent envelope matchers (.ok, .unauthorized, .forbidden, ...)
1888
- * - ctx.auth — unified TestAuthProvider, register a role once then reuse .headers
1889
- */
1890
-
1891
- import { describe, it, beforeAll, afterAll } from 'vitest';
1892
- import { createTestApp, expectArc } from '@classytic/arc/testing';
1893
- import type { TestAppContext } from '@classytic/arc/testing';
1894
- import { exampleResource } from '../src/resources/example/example.js';
1895
-
1896
- describe('Example Resource', () => {
1897
- let ctx${ts ? ": TestAppContext" : ""};
1898
-
1899
- beforeAll(async () => {
1900
- ctx = await createTestApp({
1901
- resources: [exampleResource],
1902
- authMode: 'jwt',
1903
- ${config.adapter === "mongokit" ? " connectMongoose: true,\n" : ""} });
1904
-
1905
- // Arc's permission engine reads singular user.role — string,
1906
- // comma-separated string, or array all normalise via getUserRoles().
1907
- ctx.auth${ts ? "!" : ""}.register('admin', {
1908
- user: { id: '1', role: 'admin' },
1909
- orgId: 'org-1',
1910
- });
1911
- });
1912
-
1913
- afterAll(() => ctx.close());
1914
-
1915
- describe('GET /examples', () => {
1916
- it('should return a list of examples (public)', async () => {
1917
- const res = await ctx.app.inject({ method: 'GET', url: '/examples' });
1918
- expectArc(res).ok().paginated();
1919
- });
1920
- });
1921
-
1922
- describe('POST /examples', () => {
1923
- it('should require authentication', async () => {
1924
- const res = await ctx.app.inject({
1925
- method: 'POST',
1926
- url: '/examples',
1927
- payload: { name: 'Test Example' },
1928
- });
1929
- expectArc(res).unauthorized();
1930
- });
1931
-
1932
- it('should create when admin is authenticated', async () => {
1933
- const res = await ctx.app.inject({
1934
- method: 'POST',
1935
- url: '/examples',
1936
- headers: ctx.auth${ts ? "!" : ""}.as('admin').headers,
1937
- payload: { name: 'Test Example' },
1938
- });
1939
- expectArc(res).ok();
1940
- });
1941
- });
1942
-
1943
- // Add more tests as needed:
1944
- // - GET /examples/:id (expectArc(res).ok().hasData({ name: '...' }))
1945
- // - PATCH /examples/:id (expectArc(res).ok())
1946
- // - DELETE /examples/:id (expectArc(res).ok())
1947
- // - Custom endpoints
1948
- // - Permission denials (expectArc(res).forbidden().hasError(/reason/))
1949
- // - Field hiding (expectArc(res).hidesField('password'))
1950
- });
1951
- `;
1952
- }
1953
- function userModelTemplate(config) {
1954
- const ts = config.typescript;
1955
- const orgRoles = config.tenant === "multi" ? `
1956
- // Organization roles (for multi-tenant)
1957
- const ORG_ROLES = ['owner', 'manager', 'hr', 'staff', 'contractor'] as const;
1958
- type OrgRole = typeof ORG_ROLES[number];
1959
- ` : "";
1960
- const orgInterface = config.tenant === "multi" ? `
1961
- type UserOrganization = {
1962
- organizationId: Types.ObjectId;
1963
- organizationName: string;
1964
- roles: OrgRole[];
1965
- joinedAt: Date;
1966
- };
1967
- ` : "";
1968
- const orgSchema = config.tenant === "multi" ? `
1969
- // Multi-org support
1970
- organizations: [{
1971
- organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', required: true },
1972
- organizationName: { type: String, required: true },
1973
- roles: { type: [String], enum: ORG_ROLES, default: [] },
1974
- joinedAt: { type: Date, default: () => new Date() },
1975
- }],
1976
- ` : "";
1977
- const orgMethods = config.tenant === "multi" ? `
1978
- // Organization methods
1979
- userSchema.methods.getOrgRoles = function(orgId${ts ? ": Types.ObjectId | string" : ""}) {
1980
- const org = this.organizations.find(o => o.organizationId.toString() === orgId.toString());
1981
- return org?.roles || [];
1982
- };
1983
-
1984
- userSchema.methods.hasOrgAccess = function(orgId${ts ? ": Types.ObjectId | string" : ""}) {
1985
- return this.organizations.some(o => o.organizationId.toString() === orgId.toString());
1986
- };
1987
-
1988
- userSchema.methods.addOrganization = function(
1989
- organizationId${ts ? ": Types.ObjectId" : ""},
1990
- organizationName${ts ? ": string" : ""},
1991
- roles${ts ? ": OrgRole[]" : ""} = []
1992
- ) {
1993
- const existing = this.organizations.find(o => o.organizationId.toString() === organizationId.toString());
1994
- if (existing) {
1995
- existing.organizationName = organizationName;
1996
- existing.roles = [...new Set([...existing.roles, ...roles])];
1997
- } else {
1998
- this.organizations.push({ organizationId, organizationName, roles, joinedAt: new Date() });
1999
- }
2000
- return this;
2001
- };
2002
-
2003
- userSchema.methods.removeOrganization = function(organizationId${ts ? ": Types.ObjectId" : ""}) {
2004
- this.organizations = this.organizations.filter(o => o.organizationId.toString() !== organizationId.toString());
2005
- return this;
2006
- };
2007
-
2008
- // Index for org queries
2009
- userSchema.index({ 'organizations.organizationId': 1 });
2010
- ` : "";
2011
- const userType = ts ? `
2012
- const PLATFORM_ROLES = ['user', 'admin', 'superadmin'] as const;
2013
- type PlatformRole = typeof PLATFORM_ROLES[number];
2014
-
2015
- /**
2016
- * Comma-separated list of platform roles (Better Auth admin-plugin convention).
2017
- * Single role: 'admin'. Multiple: 'admin,trainer'. Arc's permission engine
2018
- * normalises both forms via getUserRoles() — see @classytic/arc/scope.
2019
- */
2020
- type User = {
2021
- name: string;
2022
- email: string;
2023
- password: string;
2024
- role: string;${config.tenant === "multi" ? `
2025
- organizations: UserOrganization[];` : ""}
2026
- resetPasswordToken?: string;
2027
- resetPasswordExpires?: Date;
2028
- };
2029
-
2030
- type UserMethods = {
2031
- matchPassword: (enteredPassword: string) => Promise<boolean>;${config.tenant === "multi" ? `
2032
- getOrgRoles: (orgId: Types.ObjectId | string) => OrgRole[];
2033
- hasOrgAccess: (orgId: Types.ObjectId | string) => boolean;
2034
- addOrganization: (orgId: Types.ObjectId, name: string, roles?: OrgRole[]) => UserDocument;
2035
- removeOrganization: (orgId: Types.ObjectId) => UserDocument;` : ""}
2036
- };
2037
-
2038
- export type UserDocument = HydratedDocument<User, UserMethods>;
2039
- export type UserModel = Model<User, {}, UserMethods>;
2040
- ` : "";
2041
- return `/**
2042
- * User Model
2043
- * Generated by Arc CLI
2044
- */
2045
-
2046
- import bcrypt from 'bcryptjs';
2047
- import mongoose${ts ? ", { type HydratedDocument, type Model, type Types }" : ""} from 'mongoose';
2048
- ${orgRoles}
2049
- const { Schema } = mongoose;
2050
- ${orgInterface}${userType}
2051
- const userSchema = new Schema${ts ? "<User, UserModel, UserMethods>" : ""}(
2052
- {
2053
- name: { type: String, required: true, trim: true },
2054
- email: {
2055
- type: String,
2056
- required: true,
2057
- unique: true,
2058
- lowercase: true,
2059
- trim: true,
2060
- },
2061
- password: { type: String, required: true },
2062
-
2063
- // Platform role — singular field, matches Arc's permission engine
2064
- // (req.user.role) and Better Auth's admin-plugin convention.
2065
- // Comma-separated for multi-role users (e.g. 'admin,trainer');
2066
- // getUserRoles() in @classytic/arc/scope normalises both forms.
2067
- role: {
2068
- type: String,
2069
- required: true,
2070
- default: 'user',
2071
- index: true,
2072
- validate: {
2073
- validator: (v${ts ? ": string" : ""}) =>
2074
- /^(user|admin|superadmin)(,(user|admin|superadmin))*$/.test(v),
2075
- message: (props${ts ? ": { value: string }" : ""}) =>
2076
- \`Invalid role "\${props.value}" — expected one or more of user|admin|superadmin\`,
2077
- },
2078
- },
2079
- ${orgSchema}
2080
- // Password reset
2081
- resetPasswordToken: String,
2082
- resetPasswordExpires: Date,
2083
- },
2084
- { timestamps: true }
2085
- );
2086
-
2087
- // Password hashing
2088
- userSchema.pre('save', async function() {
2089
- if (!this.isModified('password')) return;
2090
- const salt = await bcrypt.genSalt(10);
2091
- this.password = await bcrypt.hash(this.password, salt);
2092
- });
2093
-
2094
- // Password comparison
2095
- userSchema.methods.matchPassword = async function(enteredPassword${ts ? ": string" : ""}) {
2096
- return bcrypt.compare(enteredPassword, this.password);
2097
- };
2098
- ${orgMethods}
2099
- // Exclude password in JSON
2100
- userSchema.set('toJSON', {
2101
- transform: (_doc, ret${ts ? ": any" : ""}) => {
2102
- delete ret.password;
2103
- delete ret.resetPasswordToken;
2104
- delete ret.resetPasswordExpires;
2105
- return ret;
2106
- },
2107
- });
2108
-
2109
- const User = mongoose.models.User${ts ? " as UserModel" : ""} || mongoose.model${ts ? "<User, UserModel>" : ""}('User', userSchema);
2110
- export default User;
2111
- `;
2112
- }
2113
- function userRepositoryTemplate(config) {
2114
- const ts = config.typescript;
2115
- return `/**
2116
- * User Repository
2117
- * Generated by Arc CLI
2118
- *
2119
- * MongoKit repository with plugins for common operations
2120
- */
2121
-
2122
- import {
2123
- Repository,
2124
- methodRegistryPlugin,
2125
- mongoOperationsPlugin,
2126
- } from '@classytic/mongokit';
2127
- ${ts ? "import type { UserDocument } from './user.model.js';\nimport type { ClientSession, Types } from 'mongoose';\n" : ""}import User from './user.model.js';
2128
-
2129
- ${ts ? "type ID = string | Types.ObjectId;\n" : ""}
2130
- class UserRepository extends Repository${ts ? "<UserDocument>" : ""} {
2131
- constructor() {
2132
- super(User${ts ? " as any" : ""}, [
2133
- methodRegistryPlugin(),
2134
- mongoOperationsPlugin(),
2135
- ]);
2136
- }
2137
-
2138
- /**
2139
- * Find user by email
2140
- */
2141
- async findByEmail(email${ts ? ": string" : ""}) {
2142
- return this.Model.findOne({ email: email.toLowerCase().trim() });
2143
- }
2144
-
2145
- /**
2146
- * Find user by reset token
2147
- */
2148
- async findByResetToken(token${ts ? ": string" : ""}) {
2149
- return this.Model.findOne({
2150
- resetPasswordToken: token,
2151
- resetPasswordExpires: { $gt: Date.now() },
2152
- });
2153
- }
2154
-
2155
- /**
2156
- * Check if email exists
2157
- */
2158
- async emailExists(email${ts ? ": string" : ""})${ts ? ": Promise<boolean>" : ""} {
2159
- const result = await this.Model.exists({ email: email.toLowerCase().trim() });
2160
- return !!result;
2161
- }
2162
-
2163
- /**
2164
- * Update user password (triggers hash middleware)
2165
- */
2166
- async updatePassword(userId${ts ? ": ID" : ""}, newPassword${ts ? ": string" : ""}, options${ts ? ": { session?: ClientSession }" : ""} = {}) {
2167
- const user = await this.Model.findById(userId).session(options.session ?? null);
2168
- if (!user) throw new Error('User not found');
2169
-
2170
- user.password = newPassword;
2171
- user.resetPasswordToken = undefined;
2172
- user.resetPasswordExpires = undefined;
2173
- await user.save({ session: options.session ?? undefined });
2174
- return user;
2175
- }
2176
-
2177
- /**
2178
- * Set reset token
2179
- */
2180
- async setResetToken(userId${ts ? ": ID" : ""}, token${ts ? ": string" : ""}, expiresAt${ts ? ": Date" : ""}) {
2181
- return this.Model.findByIdAndUpdate(
2182
- userId,
2183
- { resetPasswordToken: token, resetPasswordExpires: expiresAt },
2184
- { new: true }
2185
- );
2186
- }
2187
- ${config.tenant === "multi" ? `
2188
- /**
2189
- * Find users by organization
2190
- */
2191
- async findByOrganization(organizationId${ts ? ": ID" : ""}) {
2192
- return this.Model.find({ 'organizations.organizationId': organizationId })
2193
- .select('-password -resetPasswordToken -resetPasswordExpires')
2194
- .lean();
2195
- }
2196
- ` : ""}
2197
- }
2198
-
2199
- const userRepository = new UserRepository();
2200
- export default userRepository;
2201
- export { UserRepository };
2202
- `;
2203
- }
2204
- function userControllerTemplate(config) {
2205
- return `/**
2206
- * User Controller
2207
- * Generated by Arc CLI
2208
- *
2209
- * BaseController for user management operations.
2210
- * Used by auth resource for /users/me endpoints.
2211
- */
2212
-
2213
- import { BaseController } from '@classytic/arc';
2214
- import userRepository from './user.repository.js';
2215
-
2216
- class UserController extends BaseController {
2217
- constructor() {
2218
- super(userRepository${config.typescript ? " as any" : ""});
2219
- }
2220
-
2221
- // Custom user operations can be added here
2222
- }
2223
-
2224
- const userController = new UserController();
2225
- export default userController;
2226
- `;
2227
- }
2228
- function authResourceTemplate(config) {
2229
- const ts = config.typescript;
2230
- return `/**
2231
- * Auth Resource
2232
- * Generated by Arc CLI
2233
- *
2234
- * Combined auth + user profile endpoints:
2235
- * - POST /auth/register
2236
- * - POST /auth/login
2237
- * - POST /auth/refresh
2238
- * - POST /auth/forgot-password
2239
- * - POST /auth/reset-password
2240
- * - GET /users/me
2241
- * - PATCH /users/me
2242
- */
2243
-
2244
- import { defineResource } from '@classytic/arc';
2245
- import { allowPublic, requireAuth } from '@classytic/arc/permissions';
2246
- import { createAdapter } from '#shared/adapter.js';
2247
- import User from '../user/user.model.js';
2248
- import userRepository from '../user/user.repository.js';
2249
- import * as handlers from './auth.handlers.js';
2250
- import * as schemas from './auth.schemas.js';
2251
-
2252
- /**
2253
- * Auth Resource - handles authentication
2254
- */
2255
- export const authResource = defineResource({
2256
- name: 'auth',
2257
- displayName: 'Authentication',
2258
- tag: 'Authentication',
2259
- prefix: '/auth',
2260
-
2261
- adapter: createAdapter(User${ts ? " as any" : ""}, userRepository${ts ? " as any" : ""}),
2262
- disableDefaultRoutes: true,
2263
-
2264
- routes: [
2265
- {
2266
- method: 'POST',
2267
- path: '/register',
2268
- summary: 'Register new user',
2269
- permissions: allowPublic(),
2270
- handler: handlers.register,
2271
- raw: true,
2272
- schema: { body: schemas.registerBody, response: { 201: schemas.successResponse } },
2273
- },
2274
- {
2275
- method: 'POST',
2276
- path: '/login',
2277
- summary: 'User login',
2278
- permissions: allowPublic(),
2279
- handler: handlers.login,
2280
- raw: true,
2281
- schema: { body: schemas.loginBody, response: { 200: schemas.loginResponse } },
2282
- },
2283
- {
2284
- method: 'POST',
2285
- path: '/refresh',
2286
- summary: 'Refresh access token',
2287
- permissions: allowPublic(),
2288
- handler: handlers.refreshToken,
2289
- raw: true,
2290
- schema: { body: schemas.refreshBody, response: { 200: schemas.tokenResponse } },
2291
- },
2292
- {
2293
- method: 'POST',
2294
- path: '/forgot-password',
2295
- summary: 'Request password reset',
2296
- permissions: allowPublic(),
2297
- handler: handlers.forgotPassword,
2298
- raw: true,
2299
- schema: { body: schemas.forgotBody, response: { 200: schemas.successResponse } },
2300
- },
2301
- {
2302
- method: 'POST',
2303
- path: '/reset-password',
2304
- summary: 'Reset password with token',
2305
- permissions: allowPublic(),
2306
- handler: handlers.resetPassword,
2307
- raw: true,
2308
- schema: { body: schemas.resetBody, response: { 200: schemas.successResponse } },
2309
- },
2310
- ],
2311
- });
2312
-
2313
- /**
2314
- * User Profile Resource - handles /users/me
2315
- */
2316
- export const userProfileResource = defineResource({
2317
- name: 'user-profile',
2318
- displayName: 'User Profile',
2319
- tag: 'User Profile',
2320
- prefix: '/users',
2321
-
2322
- adapter: createAdapter(User${ts ? " as any" : ""}, userRepository${ts ? " as any" : ""}),
2323
- disableDefaultRoutes: true,
2324
-
2325
- routes: [
2326
- {
2327
- method: 'GET',
2328
- path: '/me',
2329
- summary: 'Get current user profile',
2330
- permissions: requireAuth(),
2331
- handler: handlers.getUserProfile,
2332
- raw: true,
2333
- schema: { response: { 200: schemas.userProfileResponse } },
2334
- },
2335
- {
2336
- method: 'PATCH',
2337
- path: '/me',
2338
- summary: 'Update current user profile',
2339
- permissions: requireAuth(),
2340
- handler: handlers.updateUserProfile,
2341
- raw: true,
2342
- schema: { body: schemas.updateUserBody, response: { 200: schemas.userProfileResponse } },
2343
- },
2344
- ],
2345
- });
2346
-
2347
- export default authResource;
2348
- `;
2349
- }
2350
- function betterAuthSetupTemplate(config) {
2351
- const ts = config.typescript;
2352
- const useMongo = config.adapter === "mongokit";
2353
- const useStubs = useMongo;
2354
- const useOrg = config.tenant === "multi";
2355
- const useBearer = config.session === "bearer" || config.session === "both";
2356
- const useApiKey = config.apiKey;
2357
- const mongoImport = useMongo ? `import mongoose from 'mongoose';
2358
- import { mongodbAdapter } from '@better-auth/mongo-adapter';` : "";
2359
- const stubsImport = useStubs ? `import { registerBetterAuthStubs } from '@classytic/mongokit/better-auth';` : "";
2360
- const pluginImports = [
2361
- useOrg ? `import { organization } from 'better-auth/plugins';` : "",
2362
- useBearer ? `import { bearer } from 'better-auth/plugins';` : "",
2363
- useApiKey ? `import { apiKey } from '@better-auth/api-key';` : ""
2364
- ].filter(Boolean).join("\n");
2365
- const dbAdapter = useMongo ? config.typescript ? `database: mongodbAdapter(mongoose.connection.getClient().db() as any),` : `database: mongodbAdapter(mongoose.connection.getClient().db()),` : `// Configure your database adapter here
2366
- // See: https://www.better-auth.com/docs/concepts/database`;
2367
- const pluginEntries = [
2368
- useBearer ? ` bearer(),` : "",
2369
- useApiKey ? ` apiKey({
2370
- enableMetadata: true,
2371
- rateLimit: { enabled: true, timeWindow: 1000 * 60 * 60 * 24, maxRequests: 10 },
2372
- }),` : "",
2373
- useOrg ? ` organization({
2374
- allowUserToCreateOrganization: true,
2375
- creatorRole: 'owner',
2376
- teams: { enabled: true },
2377
- }),` : ""
2378
- ].filter(Boolean);
2379
- const orgPluginUsage = pluginEntries.length > 0 ? `
2380
- plugins: [
2381
- ${pluginEntries.join("\n")}
2382
- ],` : "";
2383
- const stubPluginsList = [useOrg ? `'organization'` : ""].filter(Boolean).join(", ");
2384
- const stubExtras = useApiKey ? `, extraCollections: ['apikey']` : "";
2385
- return `/**
2386
- * Better Auth Configuration
2387
- * Generated by Arc CLI
2388
- *
2389
- * Better Auth owns auth flows + writes its own tables. Arc reads them as
2390
- * resources via \`@classytic/mongokit/better-auth\`'s overlay (full pagination,
2391
- * queryparser, OpenAPI, audit) without re-implementing CRUD.
2392
- *
2393
- * BA-managed tables: user, session, account, verification${useOrg ? ", organization, member, invitation, team, teamMember" : ""}${useApiKey ? ", apikey" : ""}
2394
- *
2395
- * @see https://www.better-auth.com/docs
2396
- */
2397
-
2398
- import { betterAuth } from 'better-auth';
2399
- ${[
2400
- mongoImport,
2401
- pluginImports,
2402
- stubsImport
2403
- ].filter(Boolean).join("\n")}
2404
- import config from '#config/index.js';
2405
-
2406
- let _auth${ts ? ": ReturnType<typeof betterAuth> | null" : ""} = null;
2407
-
2408
- /**
2409
- * Get the Better Auth instance (lazy singleton)
2410
- *
2411
- * Must be called AFTER database connection is established.
2412
- */
2413
- export function getAuth()${ts ? ": ReturnType<typeof betterAuth>" : ""} {
2414
- if (process.env.NODE_ENV === 'production' && !process.env.BETTER_AUTH_SECRET) {
2415
- throw new Error('BETTER_AUTH_SECRET is required in production (min 32 chars)');
2416
- }
2417
-
2418
- if (!_auth) {
2419
- _auth = betterAuth({
2420
- secret: config.betterAuth.secret,
2421
- baseURL: process.env.BETTER_AUTH_URL || \`http://localhost:\${config.server.port}\`,
2422
- basePath: '/api/auth',
2423
-
2424
- ${dbAdapter}
2425
- ${config.tenant === "multi" ? `
2426
- user: {
2427
- additionalFields: {
2428
- roles: {
2429
- type: 'string[]',
2430
- defaultValue: ['user'],
2431
- required: false,
2432
- input: false, // Cannot be set during signup
2433
- },
2434
- },
2435
- },
2436
- ` : ""}
2437
- emailAndPassword: {
2438
- enabled: true,
2439
- minPasswordLength: 6,
2440
- },
2441
-
2442
- // Google OAuth (enabled when env vars are set)
2443
- ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
2444
- ? {
2445
- socialProviders: {
2446
- google: {
2447
- clientId: process.env.GOOGLE_CLIENT_ID,
2448
- clientSecret: process.env.GOOGLE_CLIENT_SECRET,
2449
- },
2450
- },
2451
- }
2452
- : {}),
2453
- ${orgPluginUsage}
2454
- session: {
2455
- cookieCache: {
2456
- enabled: true,
2457
- maxAge: 5 * 60, // 5 minutes
2458
- },
2459
- },
2460
-
2461
- trustedOrigins: [config.frontend.url],
2462
-
2463
- rateLimit: {
2464
- enabled: process.env.NODE_ENV === 'production',
2465
- },
2466
- });
2467
- ${useStubs ? `
2468
- // Register stub Mongoose models for Better Auth's collections so
2469
- // \`.populate('user')\` / \`ref: 'organization'\` resolve against BA-owned
2470
- // documents app-wide. Idempotent, strict:false, plugin-aware.
2471
- registerBetterAuthStubs(mongoose, {${stubPluginsList ? ` plugins: [${stubPluginsList}]` : ""}${stubExtras} });
2472
- ` : ""} }
2473
-
2474
- return _auth;
2475
- }
2476
-
2477
- export default getAuth;
2478
- `;
2479
- }
2480
- function authHandlersTemplate(config) {
2481
- const ts = config.typescript;
2482
- return `/**
2483
- * Auth Handlers
2484
- * Generated by Arc CLI
2485
- *
2486
- * Uses Arc's built-in JWT utilities via fastify.auth (provided by @fastify/jwt v10).
2487
- * No standalone jsonwebtoken dependency needed.
2488
- */
2489
-
2490
- import userRepository from '../user/user.repository.js';
2491
- ${ts ? `
2492
- import type { FastifyRequest, FastifyReply } from 'fastify';
2493
- // Load Arc auth type augmentations (adds request.server.auth typings)
2494
- import '@classytic/arc/auth';
2495
- ` : ""}
2496
-
2497
- /**
2498
- * Register new user
2499
- */
2500
- export async function register(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2501
- try {
2502
- const { name, email, password } = request.body${ts ? " as any" : ""};
2503
-
2504
- // Check if email exists
2505
- if (await userRepository.emailExists(email)) {
2506
- return reply.code(400).send({ error: 'Email already registered', code: 'arc.email_exists' });
2507
- }
2508
-
2509
- // Create user
2510
- await userRepository.create({ name, email, password, role: 'user' });
2511
-
2512
- return reply.code(201).send({ message: 'User registered successfully' });
2513
- } catch (error) {
2514
- request.log.error({ err: error }, 'Register error');
2515
- return reply.code(500).send({ error: 'Registration failed', code: 'arc.internal' });
2516
- }
2517
- }
2518
-
2519
- /**
2520
- * Login user
2521
- */
2522
- export async function login(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2523
- try {
2524
- const { email, password } = request.body${ts ? " as any" : ""};
2525
-
2526
- const user = await userRepository.findByEmail(email);
2527
- if (!user || !(await user.matchPassword(password))) {
2528
- return reply.code(401).send({ error: 'Invalid credentials', code: 'arc.unauthorized' });
2529
- }
2530
-
2531
- const tokens = request.server.auth.issueTokens({ id: user._id.toString(), role: user.role });
2532
-
2533
- return reply.send({
2534
- user: { id: user._id, name: user.name, email: user.email, role: user.role },
2535
- ...tokens,
2536
- });
2537
- } catch (error) {
2538
- request.log.error({ err: error }, 'Login error');
2539
- return reply.code(500).send({ error: 'Login failed', code: 'arc.internal' });
2540
- }
2541
- }
2542
-
2543
- /**
2544
- * Refresh access token
2545
- */
2546
- export async function refreshToken(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2547
- try {
2548
- const { token } = request.body${ts ? " as any" : ""};
2549
- if (!token) {
2550
- return reply.code(401).send({ error: 'Refresh token required', code: 'arc.unauthorized' });
2551
- }
2552
-
2553
- const decoded = request.server.auth.verifyRefreshToken(token)${ts ? " as { id: string }" : ""};
2554
- const tokens = request.server.auth.issueTokens({ id: decoded.id });
2555
-
2556
- return reply.send(tokens);
2557
- } catch {
2558
- return reply.code(401).send({ error: 'Invalid refresh token', code: 'arc.unauthorized' });
2559
- }
2560
- }
2561
-
2562
- /**
2563
- * Forgot password
2564
- */
2565
- export async function forgotPassword(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2566
- try {
2567
- const { email } = request.body${ts ? " as any" : ""};
2568
- const user = await userRepository.findByEmail(email);
2569
-
2570
- if (user) {
2571
- const { randomBytes } = await import('node:crypto');
2572
- const token = randomBytes(32).toString('hex');
2573
- const expires = new Date(Date.now() + 3600000); // 1 hour
2574
- await userRepository.setResetToken(user._id, token, expires);
2575
- // SCAFFOLD: Integrate your email provider to send the reset link
2576
- request.log.info(\`Password reset requested for \${email}\`);
2577
- }
2578
-
2579
- // Always 200 to prevent email enumeration
2580
- return reply.send({ message: 'If email exists, reset link sent' });
2581
- } catch (error) {
2582
- request.log.error({ err: error }, 'Forgot password error');
2583
- return reply.code(500).send({ error: 'Failed to process request', code: 'arc.internal' });
2584
- }
2585
- }
2586
-
2587
- /**
2588
- * Reset password
2589
- */
2590
- export async function resetPassword(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2591
- try {
2592
- const { token, newPassword } = request.body${ts ? " as any" : ""};
2593
- const user = await userRepository.findByResetToken(token);
2594
-
2595
- if (!user) {
2596
- return reply.code(400).send({ error: 'Invalid or expired token', code: 'arc.bad_request' });
2597
- }
2598
-
2599
- await userRepository.updatePassword(user._id, newPassword);
2600
- return reply.send({ message: 'Password has been reset' });
2601
- } catch (error) {
2602
- request.log.error({ err: error }, 'Reset password error');
2603
- return reply.code(500).send({ error: 'Failed to reset password', code: 'arc.internal' });
2604
- }
2605
- }
2606
-
2607
- /**
2608
- * Get current user profile
2609
- */
2610
- export async function getUserProfile(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2611
- try {
2612
- const userId = (request${ts ? " as any" : ""}).user?._id || (request${ts ? " as any" : ""}).user?.id;
2613
- const user = await userRepository.getById(userId);
2614
-
2615
- if (!user) {
2616
- return reply.code(404).send({ error: 'User not found', code: 'arc.not_found' });
2617
- }
2618
-
2619
- return reply.send(user);
2620
- } catch (error) {
2621
- request.log.error({ err: error }, 'Get profile error');
2622
- return reply.code(500).send({ error: 'Failed to get profile', code: 'arc.internal' });
2623
- }
2624
- }
2625
-
2626
- /**
2627
- * Update current user profile
2628
- */
2629
- export async function updateUserProfile(request${ts ? ": FastifyRequest" : ""}, reply${ts ? ": FastifyReply" : ""}) {
2630
- try {
2631
- const userId = (request${ts ? " as any" : ""}).user?._id || (request${ts ? " as any" : ""}).user?.id;
2632
- const updates = { ...request.body${ts ? " as any" : ""} };
2633
-
2634
- // Prevent updating protected fields — auth-managed only
2635
- delete updates.password;
2636
- delete updates.role;
2637
- delete updates.organizations;
2638
-
2639
- const user = await userRepository.Model.findByIdAndUpdate(userId, updates, { new: true });
2640
- return reply.send(user);
2641
- } catch (error) {
2642
- request.log.error({ err: error }, 'Update profile error');
2643
- return reply.code(500).send({ error: 'Failed to update profile', code: 'arc.internal' });
2644
- }
2645
- }
2646
- `;
2647
- }
2648
- function authSchemasTemplate(_config) {
2649
- return `/**
2650
- * Auth Schemas
2651
- * Generated by Arc CLI
2652
- */
2653
-
2654
- export const registerBody = {
2655
- type: 'object',
2656
- required: ['name', 'email', 'password'],
2657
- properties: {
2658
- name: { type: 'string', minLength: 2 },
2659
- email: { type: 'string', format: 'email' },
2660
- password: { type: 'string', minLength: 6 },
2661
- },
2662
- };
2663
-
2664
- export const loginBody = {
2665
- type: 'object',
2666
- required: ['email', 'password'],
2667
- properties: {
2668
- email: { type: 'string', format: 'email' },
2669
- password: { type: 'string' },
2670
- },
2671
- };
2672
-
2673
- export const refreshBody = {
2674
- type: 'object',
2675
- required: ['token'],
2676
- properties: {
2677
- token: { type: 'string' },
2678
- },
2679
- };
2680
-
2681
- export const forgotBody = {
2682
- type: 'object',
2683
- required: ['email'],
2684
- properties: {
2685
- email: { type: 'string', format: 'email' },
2686
- },
2687
- };
2688
-
2689
- export const resetBody = {
2690
- type: 'object',
2691
- required: ['token', 'newPassword'],
2692
- properties: {
2693
- token: { type: 'string' },
2694
- newPassword: { type: 'string', minLength: 6 },
2695
- },
2696
- };
2697
-
2698
- export const updateUserBody = {
2699
- type: 'object',
2700
- properties: {
2701
- name: { type: 'string', minLength: 2 },
2702
- email: { type: 'string', format: 'email' },
2703
- },
2704
- };
2705
-
2706
- // Response schemas (enables fast-json-stringify serialization)
2707
-
2708
- export const successResponse = {
2709
- type: 'object',
2710
- properties: {
2711
- success: { type: 'boolean' },
2712
- message: { type: 'string' },
2713
- },
2714
- };
2715
-
2716
- export const loginResponse = {
2717
- type: 'object',
2718
- properties: {
2719
- success: { type: 'boolean' },
2720
- user: {
2721
- type: 'object',
2722
- properties: {
2723
- id: { type: 'string' },
2724
- name: { type: 'string' },
2725
- email: { type: 'string' },
2726
- role: { type: 'string' },
2727
- },
2728
- },
2729
- accessToken: { type: 'string' },
2730
- refreshToken: { type: 'string' },
2731
- },
2732
- };
2733
-
2734
- export const tokenResponse = {
2735
- type: 'object',
2736
- properties: {
2737
- success: { type: 'boolean' },
2738
- accessToken: { type: 'string' },
2739
- refreshToken: { type: 'string' },
2740
- },
2741
- };
2742
-
2743
- export const userProfileResponse = {
2744
- type: 'object',
2745
- properties: {
2746
- success: { type: 'boolean' },
2747
- data: { type: 'object', additionalProperties: true },
2748
- },
2749
- };
2750
- `;
2751
- }
2752
- function authTestTemplate(config) {
2753
- const ts = config.typescript;
2754
- return `/**
2755
- * Auth Tests
2756
- * Generated by Arc CLI
2757
- */
2758
-
2759
- import { describe, it, expect, beforeAll, afterAll } from 'vitest';
2760
- ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';\n" : ""}import { createAppInstance } from '../src/app.js';
2761
- ${ts ? "import type { FastifyInstance } from 'fastify';\n" : ""}
2762
- describe('Auth', () => {
2763
- let app${ts ? ": FastifyInstance" : ""};
2764
- const testUser = {
2765
- name: 'Test User',
2766
- email: 'test@example.com',
2767
- password: 'password123',
2768
- };
2769
-
2770
- beforeAll(async () => {
2771
- ${config.adapter === "mongokit" ? ` const testDbUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.name}-test';
2772
- await mongoose.connect(testDbUri);
2773
- // Clean up test data
2774
- await mongoose.connection.collection('users').deleteMany({ email: testUser.email });
2775
- ` : ""}
2776
- app = await createAppInstance();
2777
- await app.ready();
2778
- });
2779
-
2780
- afterAll(async () => {
2781
- ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('users').deleteMany({ email: testUser.email });
2782
- await mongoose.connection.close();
2783
- ` : ""} await app.close();
2784
- });
2785
-
2786
- describe('POST /auth/register', () => {
2787
- it('should register a new user', async () => {
2788
- const response = await app.inject({
2789
- method: 'POST',
2790
- url: '/auth/register',
2791
- payload: testUser,
2792
- });
2793
-
2794
- expect(response.statusCode).toBe(201);
2795
- const body = JSON.parse(response.body);
2796
- expect(body.success).toBe(true);
2797
- });
2798
-
2799
- it('should reject duplicate email', async () => {
2800
- const response = await app.inject({
2801
- method: 'POST',
2802
- url: '/auth/register',
2803
- payload: testUser,
2804
- });
2805
-
2806
- expect(response.statusCode).toBe(400);
2807
- });
2808
- });
2809
-
2810
- describe('POST /auth/login', () => {
2811
- it('should login with valid credentials', async () => {
2812
- const response = await app.inject({
2813
- method: 'POST',
2814
- url: '/auth/login',
2815
- payload: { email: testUser.email, password: testUser.password },
2816
- });
2817
-
2818
- expect(response.statusCode).toBe(200);
2819
- const body = JSON.parse(response.body);
2820
- expect(body.success).toBe(true);
2821
- expect(body.accessToken).toBeDefined();
2822
- expect(body.refreshToken).toBeDefined();
2823
- });
2824
-
2825
- it('should reject invalid credentials', async () => {
2826
- const response = await app.inject({
2827
- method: 'POST',
2828
- url: '/auth/login',
2829
- payload: { email: testUser.email, password: 'wrongpassword' },
2830
- });
2831
-
2832
- expect(response.statusCode).toBe(401);
2833
- });
2834
- });
2835
-
2836
- describe('GET /users/me', () => {
2837
- it('should require authentication', async () => {
2838
- const response = await app.inject({
2839
- method: 'GET',
2840
- url: '/users/me',
2841
- });
2842
-
2843
- expect(response.statusCode).toBe(401);
2844
- });
2845
- });
2846
- });
2847
- `;
2848
- }
2849
- function printSuccessMessage(config, skipInstall) {
2850
- const installStep = skipInstall ? ` npm install\n` : "";
2851
- const ext = config.typescript ? "ts" : "js";
2852
- const authInfo = config.auth === "better-auth" ? `
2853
- Auth (Better Auth):
2854
-
2855
- Auth routes: http://localhost:8040/api/auth/*
2856
- Better Auth handles: registration, login, sessions, OAuth
2857
- Config file: src/auth.${ext}
2858
- ` : `
2859
- Auth (JWT):
2860
-
2861
- POST /auth/register # Register
2862
- POST /auth/login # Login (returns JWT)
2863
- POST /auth/refresh # Refresh token
2864
- GET /users/me # Current user profile
2865
- `;
2866
- console.log(`
2867
- ╔═══════════════════════════════════════════════════════════════╗
2868
- ║ Project Created ║
2869
- ╚═══════════════════════════════════════════════════════════════╝
2870
-
2871
- Next steps:
2872
-
2873
- cd ${config.name}
2874
- ${installStep} npm run dev # Uses .env.dev automatically
2875
- ${authInfo}
2876
- API Documentation:
2877
-
2878
- http://localhost:8040/docs # Scalar UI
2879
- http://localhost:8040/_docs/openapi.json # OpenAPI spec
2880
-
2881
- Run tests:
2882
-
2883
- npm test # Run once
2884
- npm run test:watch # Watch mode
2885
-
2886
- Add resources:
2887
-
2888
- arc generate resource product
2889
-
2890
- Project structure:
2891
-
2892
- src/
2893
- ├── app.${ext} # App factory (for workers/tests)
2894
- ├── index.${ext} # Server entry${config.auth === "better-auth" ? `\n ├── auth.${ext} # Better Auth config` : ""}
2895
- ├── config/ # Configuration
2896
- ├── shared/ # Adapters, presets, permissions
2897
- ├── plugins/ # App plugins (DI pattern)
2898
- └── resources/ # API resources
2899
-
2900
- Documentation:
2901
- https://github.com/classytic/arc
2902
- `);
2903
- }
2904
- function dockerignoreTemplate() {
2905
- return `node_modules
2906
- dist
2907
- .env
2908
- .env.*
2909
- .git
2910
- .vscode
2911
- .idea
2912
- Dockerfile
2913
- docker-compose.yml
2914
- coverage
2915
- npm-debug.log*
2916
- .DS_Store
2917
- `;
2918
- }
2919
- function dockerfileTemplate(config) {
2920
- return `# Multi-stage Dockerfile for Arc + Fastify
2921
- # Optimized for production and caching
2922
-
2923
- # 1. Build Stage
2924
- FROM node:22-alpine AS builder
2925
- WORKDIR /app
2926
- COPY package*.json ./
2927
- ${config.typescript ? "COPY tsconfig*.json ./" : ""}
2928
- # If using pnpm, bun, or yarn, adjust the lockfile here
2929
- RUN npm ci
2930
-
2931
- COPY . .
2932
- ${config.typescript ? "RUN npm run build" : ""}
2933
-
2934
- # 2. Production Stage
2935
- FROM node:22-alpine AS runner
2936
- WORKDIR /app
2937
- ENV NODE_ENV=production
2938
-
2939
- COPY package*.json ./
2940
- RUN npm ci --only=production
2941
-
2942
- ${config.typescript ? "COPY --from=builder /app/dist ./dist" : "COPY src ./src"}
2943
-
2944
- EXPOSE 8040
2945
- CMD ["npm", "start"]
2946
- `;
2947
- }
2948
- function dockerComposeTemplate(config) {
2949
- let content = `version: '3.8'
2950
-
2951
- services:
2952
- api:
2953
- build:
2954
- context: .
2955
- dockerfile: Dockerfile
2956
- ports:
2957
- - "8040:8040"
2958
- environment:
2959
- - NODE_ENV=development
2960
- - PORT=8040
2961
- - HOST=0.0.0.0`;
2962
- if (config.adapter === "mongokit") content += `
2963
- - MONGODB_URI=mongodb://mongo:27017/${config.name}
2964
- depends_on:
2965
- - mongo
2966
-
2967
- mongo:
2968
- image: mongo:7
2969
- ports:
2970
- - "27017:27017"
2971
- volumes:
2972
- - mongo-data:/data/db`;
2973
- content += `
2974
-
2975
- volumes:`;
2976
- if (config.adapter === "mongokit") content += `
2977
- mongo-data:
2978
- `;
2979
- return content;
2980
- }
2981
- function wranglerTemplate(config) {
2982
- const entry = config.typescript ? "dist/index.js" : "src/index.js";
2983
- const compatFlag = config.adapter === "mongokit" ? "nodejs_compat_v2" : "nodejs_compat";
2984
- let dbConfig = "";
2985
- if (config.adapter === "mongokit") dbConfig = `
2986
- # MongoDB Atlas — store URI as a secret:
2987
- # npx wrangler secret put MONGODB_URI
2988
- #
2989
- # IMPORTANT: Mongoose does NOT work on Workers. Use the raw mongodb driver (6.15+).
2990
- # For Lambda/Vercel (Node.js), Mongoose works normally.
2991
- `;
2992
- else dbConfig = `
2993
- # Database options for Cloudflare Workers:
2994
- #
2995
- # PostgreSQL via Hyperdrive (recommended — connection pooling + caching):
2996
- # npx wrangler hyperdrive create my-db --connection-string="postgres://user:pass@host:5432/db"
2997
- # Then uncomment:
2998
- # [[hyperdrive]]
2999
- # binding = "HYPERDRIVE"
3000
- # id = "<your-hyperdrive-id>"
3001
- #
3002
- # Turso (edge SQLite):
3003
- # npx wrangler secret put TURSO_URL
3004
- # npx wrangler secret put TURSO_AUTH_TOKEN
3005
- #
3006
- # Neon (serverless PostgreSQL via HTTP):
3007
- # npx wrangler secret put DATABASE_URL
3008
- #
3009
- # D1 (Cloudflare's native SQLite):
3010
- # [[d1_databases]]
3011
- # binding = "DB"
3012
- # database_name = "${config.name}-db"
3013
- # database_id = "<run: npx wrangler d1 create ${config.name}-db>"
3014
- `;
3015
- return `# Cloudflare Workers configuration
3016
- # Generated by Arc CLI — see https://developers.cloudflare.com/workers/
3017
-
3018
- name = "${config.name}"
3019
- main = "${entry}"
3020
- compatibility_date = "2025-03-20"
3021
-
3022
- # Required for Arc — enables node:crypto and AsyncLocalStorage
3023
- compatibility_flags = ["${compatFlag}"]
3024
-
3025
- [vars]
3026
- NODE_ENV = "production"
3027
-
3028
- # Secrets (never commit these — use wrangler secret put):
3029
- # npx wrangler secret put JWT_SECRET
3030
- ${dbConfig}
3031
- # Custom domain:
3032
- # [routes]
3033
- # { pattern = "api.example.com/*", zone_name = "example.com" }
3034
- `;
3035
- }
3036
- //#endregion
1
+ import { t as init } from "../../init-HDvoO9L5.mjs";
3037
2
  export { init };