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