@classytic/arc 1.1.0 → 2.1.2

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 (322) hide show
  1. package/README.md +247 -794
  2. package/bin/arc.js +91 -52
  3. package/dist/EventTransport-BD2U0BTc.d.mts +100 -0
  4. package/dist/EventTransport-BD2U0BTc.d.mts.map +1 -0
  5. package/dist/HookSystem-BsGV-j2l.mjs +405 -0
  6. package/dist/HookSystem-BsGV-j2l.mjs.map +1 -0
  7. package/dist/ResourceRegistry-DsN4KJjV.mjs +250 -0
  8. package/dist/ResourceRegistry-DsN4KJjV.mjs.map +1 -0
  9. package/dist/adapters/index.d.mts +5 -0
  10. package/dist/adapters/index.mjs +3 -0
  11. package/dist/audit/index.d.mts +82 -0
  12. package/dist/audit/index.d.mts.map +1 -0
  13. package/dist/audit/index.mjs +276 -0
  14. package/dist/audit/index.mjs.map +1 -0
  15. package/dist/audit/mongodb.d.mts +5 -0
  16. package/dist/audit/mongodb.mjs +3 -0
  17. package/dist/audited-C3T5DTUx.mjs +141 -0
  18. package/dist/audited-C3T5DTUx.mjs.map +1 -0
  19. package/dist/auth/index.d.mts +189 -0
  20. package/dist/auth/index.d.mts.map +1 -0
  21. package/dist/auth/index.mjs +1102 -0
  22. package/dist/auth/index.mjs.map +1 -0
  23. package/dist/auth/redis-session.d.mts +44 -0
  24. package/dist/auth/redis-session.d.mts.map +1 -0
  25. package/dist/auth/redis-session.mjs +76 -0
  26. package/dist/auth/redis-session.mjs.map +1 -0
  27. package/dist/betterAuthOpenApi-BrHKeSAx.mjs +250 -0
  28. package/dist/betterAuthOpenApi-BrHKeSAx.mjs.map +1 -0
  29. package/dist/cache/index.d.mts +146 -0
  30. package/dist/cache/index.d.mts.map +1 -0
  31. package/dist/cache/index.mjs +92 -0
  32. package/dist/cache/index.mjs.map +1 -0
  33. package/dist/caching-Bl28lYsR.mjs +94 -0
  34. package/dist/caching-Bl28lYsR.mjs.map +1 -0
  35. package/dist/chunk-C7Uep-_p.mjs +20 -0
  36. package/dist/circuitBreaker-DeY4FCjs.mjs +1097 -0
  37. package/dist/circuitBreaker-DeY4FCjs.mjs.map +1 -0
  38. package/dist/cli/commands/describe.d.mts +19 -0
  39. package/dist/cli/commands/describe.d.mts.map +1 -0
  40. package/dist/cli/commands/describe.mjs +239 -0
  41. package/dist/cli/commands/describe.mjs.map +1 -0
  42. package/dist/cli/commands/docs.d.mts +14 -0
  43. package/dist/cli/commands/docs.d.mts.map +1 -0
  44. package/dist/cli/commands/docs.mjs +53 -0
  45. package/dist/cli/commands/docs.mjs.map +1 -0
  46. package/dist/cli/commands/{generate.d.ts → generate.d.mts} +3 -1
  47. package/dist/cli/commands/generate.d.mts.map +1 -0
  48. package/dist/cli/commands/generate.mjs +358 -0
  49. package/dist/cli/commands/generate.mjs.map +1 -0
  50. package/dist/cli/commands/{init.d.ts → init.d.mts} +12 -8
  51. package/dist/cli/commands/init.d.mts.map +1 -0
  52. package/dist/cli/commands/{init.js → init.mjs} +807 -616
  53. package/dist/cli/commands/init.mjs.map +1 -0
  54. package/dist/cli/commands/introspect.d.mts +11 -0
  55. package/dist/cli/commands/introspect.d.mts.map +1 -0
  56. package/dist/cli/commands/introspect.mjs +76 -0
  57. package/dist/cli/commands/introspect.mjs.map +1 -0
  58. package/dist/cli/index.d.mts +17 -0
  59. package/dist/cli/index.d.mts.map +1 -0
  60. package/dist/cli/index.mjs +157 -0
  61. package/dist/cli/index.mjs.map +1 -0
  62. package/dist/constants-DdXFXQtN.mjs +85 -0
  63. package/dist/constants-DdXFXQtN.mjs.map +1 -0
  64. package/dist/core/index.d.mts +5 -0
  65. package/dist/core/index.mjs +4 -0
  66. package/dist/createApp-CUgNqegw.mjs +560 -0
  67. package/dist/createApp-CUgNqegw.mjs.map +1 -0
  68. package/dist/defineResource-k0_BDn8v.mjs +2197 -0
  69. package/dist/defineResource-k0_BDn8v.mjs.map +1 -0
  70. package/dist/discovery/index.d.mts +47 -0
  71. package/dist/discovery/index.d.mts.map +1 -0
  72. package/dist/discovery/index.mjs +110 -0
  73. package/dist/discovery/index.mjs.map +1 -0
  74. package/dist/docs/index.d.mts +163 -0
  75. package/dist/docs/index.d.mts.map +1 -0
  76. package/dist/docs/index.mjs +73 -0
  77. package/dist/docs/index.mjs.map +1 -0
  78. package/dist/elevation-BRy3yFWT.mjs +113 -0
  79. package/dist/elevation-BRy3yFWT.mjs.map +1 -0
  80. package/dist/elevation-B_2dRLVP.d.mts +88 -0
  81. package/dist/elevation-B_2dRLVP.d.mts.map +1 -0
  82. package/dist/errorHandler-BbcgBmIH.d.mts +73 -0
  83. package/dist/errorHandler-BbcgBmIH.d.mts.map +1 -0
  84. package/dist/errorHandler-C1okiriz.mjs +109 -0
  85. package/dist/errorHandler-C1okiriz.mjs.map +1 -0
  86. package/dist/errors-B9bZok84.mjs +212 -0
  87. package/dist/errors-B9bZok84.mjs.map +1 -0
  88. package/dist/errors-ChKiFz62.d.mts +125 -0
  89. package/dist/errors-ChKiFz62.d.mts.map +1 -0
  90. package/dist/eventPlugin-CTrLH3mt.d.mts +125 -0
  91. package/dist/eventPlugin-CTrLH3mt.d.mts.map +1 -0
  92. package/dist/eventPlugin-DGR_B2on.mjs +230 -0
  93. package/dist/eventPlugin-DGR_B2on.mjs.map +1 -0
  94. package/dist/events/index.d.mts +54 -0
  95. package/dist/events/index.d.mts.map +1 -0
  96. package/dist/events/index.mjs +52 -0
  97. package/dist/events/index.mjs.map +1 -0
  98. package/dist/events/transports/redis-stream-entry.d.mts +2 -0
  99. package/dist/events/transports/redis-stream-entry.mjs +178 -0
  100. package/dist/events/transports/redis-stream-entry.mjs.map +1 -0
  101. package/dist/events/transports/redis.d.mts +77 -0
  102. package/dist/events/transports/redis.d.mts.map +1 -0
  103. package/dist/events/transports/redis.mjs +125 -0
  104. package/dist/events/transports/redis.mjs.map +1 -0
  105. package/dist/externalPaths-DlINfKbP.d.mts +51 -0
  106. package/dist/externalPaths-DlINfKbP.d.mts.map +1 -0
  107. package/dist/factory/index.d.mts +64 -0
  108. package/dist/factory/index.d.mts.map +1 -0
  109. package/dist/factory/index.mjs +3 -0
  110. package/dist/fastifyAdapter-BkrGrlFi.d.mts +217 -0
  111. package/dist/fastifyAdapter-BkrGrlFi.d.mts.map +1 -0
  112. package/dist/fields-DyaDVX4J.d.mts +110 -0
  113. package/dist/fields-DyaDVX4J.d.mts.map +1 -0
  114. package/dist/fields-iagOozy0.mjs +115 -0
  115. package/dist/fields-iagOozy0.mjs.map +1 -0
  116. package/dist/hooks/index.d.mts +4 -0
  117. package/dist/hooks/index.mjs +3 -0
  118. package/dist/idempotency/index.d.mts +97 -0
  119. package/dist/idempotency/index.d.mts.map +1 -0
  120. package/dist/idempotency/index.mjs +320 -0
  121. package/dist/idempotency/index.mjs.map +1 -0
  122. package/dist/idempotency/mongodb.d.mts +2 -0
  123. package/dist/idempotency/mongodb.mjs +115 -0
  124. package/dist/idempotency/mongodb.mjs.map +1 -0
  125. package/dist/idempotency/redis.d.mts +2 -0
  126. package/dist/idempotency/redis.mjs +104 -0
  127. package/dist/idempotency/redis.mjs.map +1 -0
  128. package/dist/index.d.mts +261 -0
  129. package/dist/index.d.mts.map +1 -0
  130. package/dist/index.mjs +105 -0
  131. package/dist/index.mjs.map +1 -0
  132. package/dist/integrations/event-gateway.d.mts +47 -0
  133. package/dist/integrations/event-gateway.d.mts.map +1 -0
  134. package/dist/integrations/event-gateway.mjs +44 -0
  135. package/dist/integrations/event-gateway.mjs.map +1 -0
  136. package/dist/integrations/index.d.mts +5 -0
  137. package/dist/integrations/index.mjs +1 -0
  138. package/dist/integrations/jobs.d.mts +104 -0
  139. package/dist/integrations/jobs.d.mts.map +1 -0
  140. package/dist/integrations/jobs.mjs +124 -0
  141. package/dist/integrations/jobs.mjs.map +1 -0
  142. package/dist/integrations/streamline.d.mts +61 -0
  143. package/dist/integrations/streamline.d.mts.map +1 -0
  144. package/dist/integrations/streamline.mjs +126 -0
  145. package/dist/integrations/streamline.mjs.map +1 -0
  146. package/dist/integrations/websocket.d.mts +83 -0
  147. package/dist/integrations/websocket.d.mts.map +1 -0
  148. package/dist/integrations/websocket.mjs +289 -0
  149. package/dist/integrations/websocket.mjs.map +1 -0
  150. package/dist/interface-B01JvPVc.d.mts +78 -0
  151. package/dist/interface-B01JvPVc.d.mts.map +1 -0
  152. package/dist/interface-CZe8IkMf.d.mts +55 -0
  153. package/dist/interface-CZe8IkMf.d.mts.map +1 -0
  154. package/dist/interface-Ch8HU9uM.d.mts +1098 -0
  155. package/dist/interface-Ch8HU9uM.d.mts.map +1 -0
  156. package/dist/introspectionPlugin-rFdO8ZUa.mjs +54 -0
  157. package/dist/introspectionPlugin-rFdO8ZUa.mjs.map +1 -0
  158. package/dist/keys-BqNejWup.mjs +43 -0
  159. package/dist/keys-BqNejWup.mjs.map +1 -0
  160. package/dist/logger-Df2O2WsW.mjs +79 -0
  161. package/dist/logger-Df2O2WsW.mjs.map +1 -0
  162. package/dist/memory-cQgelFOj.mjs +144 -0
  163. package/dist/memory-cQgelFOj.mjs.map +1 -0
  164. package/dist/migrations/index.d.mts +157 -0
  165. package/dist/migrations/index.d.mts.map +1 -0
  166. package/dist/migrations/index.mjs +261 -0
  167. package/dist/migrations/index.mjs.map +1 -0
  168. package/dist/mongodb-BfJVlUJH.mjs +94 -0
  169. package/dist/mongodb-BfJVlUJH.mjs.map +1 -0
  170. package/dist/mongodb-CGzRbfAK.d.mts +119 -0
  171. package/dist/mongodb-CGzRbfAK.d.mts.map +1 -0
  172. package/dist/mongodb-JN-9JA7K.d.mts +72 -0
  173. package/dist/mongodb-JN-9JA7K.d.mts.map +1 -0
  174. package/dist/openapi-G3Cw7XuM.mjs +524 -0
  175. package/dist/openapi-G3Cw7XuM.mjs.map +1 -0
  176. package/dist/org/index.d.mts +69 -0
  177. package/dist/org/index.d.mts.map +1 -0
  178. package/dist/org/index.mjs +514 -0
  179. package/dist/org/index.mjs.map +1 -0
  180. package/dist/org/types.d.mts +83 -0
  181. package/dist/org/types.d.mts.map +1 -0
  182. package/dist/org/types.mjs +1 -0
  183. package/dist/permissions/index.d.mts +279 -0
  184. package/dist/permissions/index.d.mts.map +1 -0
  185. package/dist/permissions/index.mjs +579 -0
  186. package/dist/permissions/index.mjs.map +1 -0
  187. package/dist/plugins/index.d.mts +173 -0
  188. package/dist/plugins/index.d.mts.map +1 -0
  189. package/dist/plugins/index.mjs +523 -0
  190. package/dist/plugins/index.mjs.map +1 -0
  191. package/dist/plugins/response-cache.d.mts +88 -0
  192. package/dist/plugins/response-cache.d.mts.map +1 -0
  193. package/dist/plugins/response-cache.mjs +284 -0
  194. package/dist/plugins/response-cache.mjs.map +1 -0
  195. package/dist/plugins/tracing-entry.d.mts +2 -0
  196. package/dist/plugins/tracing-entry.mjs +186 -0
  197. package/dist/plugins/tracing-entry.mjs.map +1 -0
  198. package/dist/pluralize-CEweyOEm.mjs +87 -0
  199. package/dist/pluralize-CEweyOEm.mjs.map +1 -0
  200. package/dist/policies/{index.d.ts → index.d.mts} +204 -169
  201. package/dist/policies/index.d.mts.map +1 -0
  202. package/dist/policies/index.mjs +322 -0
  203. package/dist/policies/index.mjs.map +1 -0
  204. package/dist/presets/{index.d.ts → index.d.mts} +63 -131
  205. package/dist/presets/index.d.mts.map +1 -0
  206. package/dist/presets/index.mjs +144 -0
  207. package/dist/presets/index.mjs.map +1 -0
  208. package/dist/presets/multiTenant.d.mts +25 -0
  209. package/dist/presets/multiTenant.d.mts.map +1 -0
  210. package/dist/presets/multiTenant.mjs +114 -0
  211. package/dist/presets/multiTenant.mjs.map +1 -0
  212. package/dist/presets-BITljm96.mjs +120 -0
  213. package/dist/presets-BITljm96.mjs.map +1 -0
  214. package/dist/presets-DzSMwlKj.d.mts +58 -0
  215. package/dist/presets-DzSMwlKj.d.mts.map +1 -0
  216. package/dist/prisma-DJbMt3yf.mjs +628 -0
  217. package/dist/prisma-DJbMt3yf.mjs.map +1 -0
  218. package/dist/prisma-Dg9GoVdj.d.mts +275 -0
  219. package/dist/prisma-Dg9GoVdj.d.mts.map +1 -0
  220. package/dist/queryCachePlugin-7THaI5mt.d.mts +72 -0
  221. package/dist/queryCachePlugin-7THaI5mt.d.mts.map +1 -0
  222. package/dist/queryCachePlugin-DMBnp2Q0.mjs +139 -0
  223. package/dist/queryCachePlugin-DMBnp2Q0.mjs.map +1 -0
  224. package/dist/redis-D-JAeLtm.d.mts +50 -0
  225. package/dist/redis-D-JAeLtm.d.mts.map +1 -0
  226. package/dist/redis-stream-Bdh_vUU8.d.mts +104 -0
  227. package/dist/redis-stream-Bdh_vUU8.d.mts.map +1 -0
  228. package/dist/registry/index.d.mts +12 -0
  229. package/dist/registry/index.d.mts.map +1 -0
  230. package/dist/registry/index.mjs +4 -0
  231. package/dist/requestContext-QQD6ROJc.mjs +56 -0
  232. package/dist/requestContext-QQD6ROJc.mjs.map +1 -0
  233. package/dist/schemaConverter-BwrmWroW.mjs +99 -0
  234. package/dist/schemaConverter-BwrmWroW.mjs.map +1 -0
  235. package/dist/schemas/index.d.mts +64 -0
  236. package/dist/schemas/index.d.mts.map +1 -0
  237. package/dist/schemas/index.mjs +83 -0
  238. package/dist/schemas/index.mjs.map +1 -0
  239. package/dist/scope/index.d.mts +22 -0
  240. package/dist/scope/index.d.mts.map +1 -0
  241. package/dist/scope/index.mjs +66 -0
  242. package/dist/scope/index.mjs.map +1 -0
  243. package/dist/sessionManager-jPKLbHE0.d.mts +187 -0
  244. package/dist/sessionManager-jPKLbHE0.d.mts.map +1 -0
  245. package/dist/sse-B3c3_yZp.mjs +124 -0
  246. package/dist/sse-B3c3_yZp.mjs.map +1 -0
  247. package/dist/testing/index.d.mts +908 -0
  248. package/dist/testing/index.d.mts.map +1 -0
  249. package/dist/testing/index.mjs +1977 -0
  250. package/dist/testing/index.mjs.map +1 -0
  251. package/dist/tracing-Cc7vVQPp.d.mts +71 -0
  252. package/dist/tracing-Cc7vVQPp.d.mts.map +1 -0
  253. package/dist/typeGuards-DhMNLuvU.mjs +10 -0
  254. package/dist/typeGuards-DhMNLuvU.mjs.map +1 -0
  255. package/dist/types/index.d.mts +947 -0
  256. package/dist/types/index.d.mts.map +1 -0
  257. package/dist/types/index.mjs +15 -0
  258. package/dist/types/index.mjs.map +1 -0
  259. package/dist/types-Beqn1Un7.mjs +39 -0
  260. package/dist/types-Beqn1Un7.mjs.map +1 -0
  261. package/dist/types-CIgB7UUl.d.mts +446 -0
  262. package/dist/types-CIgB7UUl.d.mts.map +1 -0
  263. package/dist/types-aYB4V7uN.d.mts +87 -0
  264. package/dist/types-aYB4V7uN.d.mts.map +1 -0
  265. package/dist/utils/index.d.mts +748 -0
  266. package/dist/utils/index.d.mts.map +1 -0
  267. package/dist/utils/index.mjs +6 -0
  268. package/package.json +194 -68
  269. package/dist/BaseController-DVAiHxEQ.d.ts +0 -233
  270. package/dist/adapters/index.d.ts +0 -237
  271. package/dist/adapters/index.js +0 -668
  272. package/dist/arcCorePlugin-CsShQdyP.d.ts +0 -273
  273. package/dist/audit/index.d.ts +0 -195
  274. package/dist/audit/index.js +0 -319
  275. package/dist/auth/index.d.ts +0 -47
  276. package/dist/auth/index.js +0 -174
  277. package/dist/cli/commands/docs.d.ts +0 -11
  278. package/dist/cli/commands/docs.js +0 -474
  279. package/dist/cli/commands/generate.js +0 -334
  280. package/dist/cli/commands/introspect.d.ts +0 -8
  281. package/dist/cli/commands/introspect.js +0 -338
  282. package/dist/cli/index.d.ts +0 -4
  283. package/dist/cli/index.js +0 -3269
  284. package/dist/core/index.d.ts +0 -220
  285. package/dist/core/index.js +0 -2786
  286. package/dist/createApp-Ce9wl8W9.d.ts +0 -77
  287. package/dist/docs/index.d.ts +0 -166
  288. package/dist/docs/index.js +0 -658
  289. package/dist/errors-8WIxGS_6.d.ts +0 -122
  290. package/dist/events/index.d.ts +0 -117
  291. package/dist/events/index.js +0 -89
  292. package/dist/factory/index.d.ts +0 -38
  293. package/dist/factory/index.js +0 -1652
  294. package/dist/hooks/index.d.ts +0 -4
  295. package/dist/hooks/index.js +0 -199
  296. package/dist/idempotency/index.d.ts +0 -323
  297. package/dist/idempotency/index.js +0 -500
  298. package/dist/index-B4t03KQ0.d.ts +0 -1366
  299. package/dist/index.d.ts +0 -135
  300. package/dist/index.js +0 -4756
  301. package/dist/migrations/index.d.ts +0 -185
  302. package/dist/migrations/index.js +0 -274
  303. package/dist/org/index.d.ts +0 -129
  304. package/dist/org/index.js +0 -220
  305. package/dist/permissions/index.d.ts +0 -144
  306. package/dist/permissions/index.js +0 -103
  307. package/dist/plugins/index.d.ts +0 -46
  308. package/dist/plugins/index.js +0 -1069
  309. package/dist/policies/index.js +0 -196
  310. package/dist/presets/index.js +0 -384
  311. package/dist/presets/multiTenant.d.ts +0 -39
  312. package/dist/presets/multiTenant.js +0 -112
  313. package/dist/registry/index.d.ts +0 -16
  314. package/dist/registry/index.js +0 -253
  315. package/dist/testing/index.d.ts +0 -618
  316. package/dist/testing/index.js +0 -48020
  317. package/dist/types/index.d.ts +0 -4
  318. package/dist/types/index.js +0 -8
  319. package/dist/types-B99TBmFV.d.ts +0 -76
  320. package/dist/types-BvckRbs2.d.ts +0 -143
  321. package/dist/utils/index.d.ts +0 -679
  322. package/dist/utils/index.js +0 -931
@@ -1,325 +1,317 @@
1
- import * as fs from 'fs/promises';
2
- import * as path from 'path';
3
- import * as readline from 'readline';
4
- import { execSync, spawn } from 'child_process';
5
-
6
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
7
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
8
- }) : x)(function(x) {
9
- if (typeof require !== "undefined") return require.apply(this, arguments);
10
- throw Error('Dynamic require of "' + x + '" is not supported');
11
- });
1
+ import { accessSync } from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as fs from "node:fs/promises";
4
+ import * as readline from "node:readline";
5
+ import { execSync, spawn } from "node:child_process";
6
+
7
+ //#region src/cli/commands/init.ts
8
+ /**
9
+ * Arc CLI - Init Command
10
+ *
11
+ * Scaffolds a new Arc project with clean architecture:
12
+ * - MongoKit or Custom adapter
13
+ * - Multi-tenant or Single-tenant
14
+ * - TypeScript or JavaScript
15
+ *
16
+ * Automatically installs dependencies using detected package manager.
17
+ */
18
+ /**
19
+ * Initialize a new Arc project
20
+ */
12
21
  async function init(options = {}) {
13
- console.log(`
22
+ console.log(`
14
23
  ╔═══════════════════════════════════════════════════════════════╗
15
- 🔥 Arc Project Setup
24
+ ║ Arc Project Setup
16
25
  ║ Resource-Oriented Backend Framework ║
17
26
  ╚═══════════════════════════════════════════════════════════════╝
18
27
  `);
19
- const config = await gatherConfig(options);
20
- console.log(`
21
- 📦 Creating project: ${config.name}`);
22
- console.log(` Adapter: ${config.adapter === "mongokit" ? "MongoKit (MongoDB)" : "Custom"}`);
23
- console.log(` Tenant: ${config.tenant === "multi" ? "Multi-tenant" : "Single-tenant"}`);
24
- console.log(` Language: ${config.typescript ? "TypeScript" : "JavaScript"}
25
- `);
26
- const projectPath = path.join(process.cwd(), config.name);
27
- try {
28
- await fs.access(projectPath);
29
- if (!options.force) {
30
- console.error(`❌ Directory "${config.name}" already exists. Use --force to overwrite.`);
31
- process.exit(1);
32
- }
33
- } catch {
34
- }
35
- const packageManager = detectPackageManager();
36
- console.log(`📦 Using package manager: ${packageManager}
37
- `);
38
- await createProjectStructure(projectPath, config);
39
- if (!options.skipInstall) {
40
- console.log("\n📥 Installing dependencies...\n");
41
- await installDependencies(projectPath, config, packageManager);
42
- }
43
- printSuccessMessage(config, options.skipInstall);
28
+ const config = await gatherConfig(options);
29
+ console.log(`\nCreating project: ${config.name}`);
30
+ console.log(` Adapter: ${config.adapter === "mongokit" ? "MongoKit (MongoDB)" : "Custom"}`);
31
+ console.log(` Auth: ${config.auth === "better-auth" ? "Better Auth (recommended)" : "Arc JWT"}`);
32
+ console.log(` Tenant: ${config.tenant === "multi" ? "Multi-tenant" : "Single-tenant"}`);
33
+ console.log(` Language: ${config.typescript ? "TypeScript" : "JavaScript"}`);
34
+ console.log(` Target: ${config.edge ? "Edge/Serverless" : "Node.js Server"}\n`);
35
+ const projectPath = path.join(process.cwd(), config.name);
36
+ try {
37
+ await fs.access(projectPath);
38
+ if (!options.force) throw new Error(`Directory "${config.name}" already exists. Use --force to overwrite.`);
39
+ } catch (err) {
40
+ if (!(err && typeof err === "object" && "code" in err && err.code === "ENOENT")) throw err;
41
+ }
42
+ const packageManager = detectPackageManager();
43
+ console.log(`Using package manager: ${packageManager}\n`);
44
+ await createProjectStructure(projectPath, config);
45
+ if (!options.skipInstall) {
46
+ console.log("\n📥 Installing dependencies...\n");
47
+ await installDependencies(projectPath, config, packageManager);
48
+ }
49
+ printSuccessMessage(config, options.skipInstall);
44
50
  }
51
+ /**
52
+ * Detect which package manager to use
53
+ * Priority: pnpm > yarn > bun > npm (based on lockfile or global availability)
54
+ */
45
55
  function detectPackageManager() {
46
- try {
47
- const cwd = process.cwd();
48
- if (existsSync(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
49
- if (existsSync(path.join(cwd, "yarn.lock"))) return "yarn";
50
- if (existsSync(path.join(cwd, "bun.lockb"))) return "bun";
51
- if (existsSync(path.join(cwd, "package-lock.json"))) return "npm";
52
- } catch {
53
- }
54
- if (isCommandAvailable("pnpm")) return "pnpm";
55
- if (isCommandAvailable("yarn")) return "yarn";
56
- if (isCommandAvailable("bun")) return "bun";
57
- return "npm";
56
+ try {
57
+ const cwd = process.cwd();
58
+ if (existsSync$1(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
59
+ if (existsSync$1(path.join(cwd, "yarn.lock"))) return "yarn";
60
+ if (existsSync$1(path.join(cwd, "bun.lockb"))) return "bun";
61
+ if (existsSync$1(path.join(cwd, "package-lock.json"))) return "npm";
62
+ } catch {}
63
+ if (isCommandAvailable("pnpm")) return "pnpm";
64
+ if (isCommandAvailable("yarn")) return "yarn";
65
+ if (isCommandAvailable("bun")) return "bun";
66
+ return "npm";
58
67
  }
68
+ /**
69
+ * Check if a command is available in PATH
70
+ */
59
71
  function isCommandAvailable(command) {
60
- try {
61
- execSync(`${command} --version`, { stdio: "ignore" });
62
- return true;
63
- } catch {
64
- return false;
65
- }
72
+ try {
73
+ execSync(`${command} --version`, { stdio: "ignore" });
74
+ return true;
75
+ } catch {
76
+ return false;
77
+ }
66
78
  }
67
- function existsSync(filePath) {
68
- try {
69
- __require("fs").accessSync(filePath);
70
- return true;
71
- } catch {
72
- return false;
73
- }
79
+ /**
80
+ * Sync check if file exists (ESM-compatible — no require())
81
+ */
82
+ function existsSync$1(filePath) {
83
+ try {
84
+ accessSync(filePath);
85
+ return true;
86
+ } catch {
87
+ return false;
88
+ }
74
89
  }
90
+ /**
91
+ * Install dependencies using the detected package manager
92
+ */
75
93
  async function installDependencies(projectPath, config, pm) {
76
- const deps = [
77
- "@classytic/arc@latest",
78
- "fastify@latest",
79
- "@fastify/cors@latest",
80
- "@fastify/helmet@latest",
81
- "@fastify/jwt@latest",
82
- "@fastify/rate-limit@latest",
83
- "@fastify/sensible@latest",
84
- "@fastify/under-pressure@latest",
85
- "bcryptjs@latest",
86
- "dotenv@latest",
87
- "jsonwebtoken@latest"
88
- ];
89
- if (config.adapter === "mongokit") {
90
- deps.push("@classytic/mongokit@latest", "mongoose@latest");
91
- }
92
- const devDeps = [
93
- "vitest@latest",
94
- "pino-pretty@latest"
95
- ];
96
- if (config.typescript) {
97
- devDeps.push(
98
- "typescript@latest",
99
- "@types/node@latest",
100
- "@types/jsonwebtoken@latest",
101
- "tsx@latest"
102
- );
103
- }
104
- const installCmd = getInstallCommand(pm, deps, false);
105
- const installDevCmd = getInstallCommand(pm, devDeps, true);
106
- console.log(` Installing dependencies...`);
107
- await runCommand(installCmd, projectPath);
108
- console.log(` Installing dev dependencies...`);
109
- await runCommand(installDevCmd, projectPath);
110
- console.log(`
111
- ✅ Dependencies installed successfully!`);
94
+ const deps = [
95
+ "@classytic/arc@latest",
96
+ "fastify@latest",
97
+ "@fastify/cors@latest",
98
+ "@fastify/helmet@latest",
99
+ "@fastify/rate-limit@latest",
100
+ "@fastify/sensible@latest",
101
+ "@fastify/under-pressure@latest",
102
+ "dotenv@latest"
103
+ ];
104
+ if (config.auth === "better-auth") deps.push("better-auth@latest", "mongodb@latest");
105
+ else deps.push("@fastify/jwt@latest", "bcryptjs@latest");
106
+ if (config.adapter === "mongokit") deps.push("@classytic/mongokit@latest", "mongoose@latest");
107
+ const devDeps = ["vitest@latest", "pino-pretty@latest"];
108
+ if (config.typescript) devDeps.push("typescript@latest", "@types/node@latest", "tsx@latest");
109
+ const installCmd = getInstallCommand(pm, deps, false);
110
+ const installDevCmd = getInstallCommand(pm, devDeps, true);
111
+ console.log(` Installing dependencies...`);
112
+ await runCommand(installCmd, projectPath);
113
+ console.log(` Installing dev dependencies...`);
114
+ await runCommand(installDevCmd, projectPath);
115
+ console.log(`\nDependencies installed successfully.`);
112
116
  }
117
+ /**
118
+ * Get the install command for a package manager
119
+ */
113
120
  function getInstallCommand(pm, packages, isDev) {
114
- const pkgList = packages.join(" ");
115
- switch (pm) {
116
- case "pnpm":
117
- return `pnpm add ${isDev ? "-D" : ""} ${pkgList}`;
118
- case "yarn":
119
- return `yarn add ${isDev ? "-D" : ""} ${pkgList}`;
120
- case "bun":
121
- return `bun add ${isDev ? "-d" : ""} ${pkgList}`;
122
- case "npm":
123
- default:
124
- return `npm install ${isDev ? "--save-dev" : ""} ${pkgList}`;
125
- }
121
+ const pkgList = packages.join(" ");
122
+ switch (pm) {
123
+ case "pnpm": return `pnpm add ${isDev ? "-D" : ""} ${pkgList}`;
124
+ case "yarn": return `yarn add ${isDev ? "-D" : ""} ${pkgList}`;
125
+ case "bun": return `bun add ${isDev ? "-d" : ""} ${pkgList}`;
126
+ default: return `npm install ${isDev ? "--save-dev" : ""} ${pkgList}`;
127
+ }
126
128
  }
129
+ /**
130
+ * Run a shell command in a directory
131
+ */
127
132
  function runCommand(command, cwd) {
128
- return new Promise((resolve, reject) => {
129
- const isWindows = process.platform === "win32";
130
- const shell = isWindows ? "cmd" : "/bin/sh";
131
- const shellFlag = isWindows ? "/c" : "-c";
132
- const child = spawn(shell, [shellFlag, command], {
133
- cwd,
134
- stdio: "inherit",
135
- env: { ...process.env, FORCE_COLOR: "1" }
136
- });
137
- child.on("close", (code) => {
138
- if (code === 0) {
139
- resolve();
140
- } else {
141
- reject(new Error(`Command failed with exit code ${code}`));
142
- }
143
- });
144
- child.on("error", reject);
145
- });
133
+ return new Promise((resolve, reject) => {
134
+ const isWindows = process.platform === "win32";
135
+ const child = spawn(isWindows ? "cmd" : "/bin/sh", [isWindows ? "/c" : "-c", command], {
136
+ cwd,
137
+ stdio: "inherit",
138
+ env: {
139
+ ...process.env,
140
+ FORCE_COLOR: "1"
141
+ }
142
+ });
143
+ child.on("close", (code) => {
144
+ if (code === 0) resolve();
145
+ else reject(/* @__PURE__ */ new Error(`Command failed with exit code ${code}`));
146
+ });
147
+ child.on("error", reject);
148
+ });
146
149
  }
147
150
  async function gatherConfig(options) {
148
- const rl = readline.createInterface({
149
- input: process.stdin,
150
- output: process.stdout
151
- });
152
- const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
153
- try {
154
- const name = options.name || await question("📁 Project name: ") || "my-arc-app";
155
- let adapter = options.adapter || "mongokit";
156
- if (!options.adapter) {
157
- const adapterChoice = await question("🗄️ Database adapter [1=MongoKit (recommended), 2=Custom]: ");
158
- adapter = adapterChoice === "2" ? "custom" : "mongokit";
159
- }
160
- let tenant = options.tenant || "single";
161
- if (!options.tenant) {
162
- const tenantChoice = await question("🏢 Tenant mode [1=Single-tenant, 2=Multi-tenant]: ");
163
- tenant = tenantChoice === "2" ? "multi" : "single";
164
- }
165
- let typescript = options.typescript ?? true;
166
- if (options.typescript === void 0) {
167
- const tsChoice = await question("�� Language [1=TypeScript (recommended), 2=JavaScript]: ");
168
- typescript = tsChoice !== "2";
169
- }
170
- return { name, adapter, tenant, typescript };
171
- } finally {
172
- rl.close();
173
- }
151
+ const rl = readline.createInterface({
152
+ input: process.stdin,
153
+ output: process.stdout
154
+ });
155
+ const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
156
+ const nonInteractive = !!options.name;
157
+ try {
158
+ const name = options.name || await question("Project name: ") || "my-arc-app";
159
+ let adapter = options.adapter || "mongokit";
160
+ if (!options.adapter && !nonInteractive) adapter = await question("Database adapter [1=MongoKit (recommended), 2=Custom]: ") === "2" ? "custom" : "mongokit";
161
+ let auth = options.auth || "better-auth";
162
+ if (!options.auth && !nonInteractive) auth = await question("Auth strategy [1=Better Auth (recommended), 2=Arc JWT]: ") === "2" ? "jwt" : "better-auth";
163
+ let tenant = options.tenant || "single";
164
+ if (!options.tenant && !nonInteractive) tenant = await question("Tenant mode [1=Single-tenant, 2=Multi-tenant]: ") === "2" ? "multi" : "single";
165
+ let typescript = options.typescript ?? true;
166
+ if (options.typescript === void 0 && !nonInteractive) typescript = await question("Language [1=TypeScript (recommended), 2=JavaScript]: ") !== "2";
167
+ let edge = options.edge ?? false;
168
+ if (options.edge === void 0 && !nonInteractive) edge = await question("Deployment target [1=Node.js Server (default), 2=Edge/Serverless]: ") === "2";
169
+ return {
170
+ name,
171
+ adapter,
172
+ auth,
173
+ tenant,
174
+ typescript,
175
+ edge
176
+ };
177
+ } finally {
178
+ rl.close();
179
+ }
174
180
  }
175
181
  async function createProjectStructure(projectPath, config) {
176
- const ext = config.typescript ? "ts" : "js";
177
- const dirs = [
178
- "",
179
- "src",
180
- "src/config",
181
- // Config & env loading (import first!)
182
- "src/shared",
183
- // Shared utilities (adapters, presets, permissions)
184
- "src/shared/presets",
185
- // Preset definitions
186
- "src/plugins",
187
- // App-specific plugins
188
- "src/resources",
189
- // Resource definitions
190
- "src/resources/user",
191
- // User resource (user.model, user.repository, etc.)
192
- "src/resources/auth",
193
- // Auth resource (auth.resource, auth.handlers, etc.)
194
- "src/resources/example",
195
- // Example resource
196
- "tests"
197
- ];
198
- for (const dir of dirs) {
199
- await fs.mkdir(path.join(projectPath, dir), { recursive: true });
200
- console.log(` 📁 Created: ${dir || "/"}`);
201
- }
202
- const files = {
203
- "package.json": packageJsonTemplate(config),
204
- ".gitignore": gitignoreTemplate(),
205
- ".env.example": envExampleTemplate(config),
206
- ".env.dev": envDevTemplate(config),
207
- "README.md": readmeTemplate(config)
208
- };
209
- if (config.typescript) {
210
- files["tsconfig.json"] = tsconfigTemplate();
211
- }
212
- files["vitest.config.ts"] = vitestConfigTemplate(config);
213
- files[`src/config/env.${ext}`] = envLoaderTemplate(config);
214
- files[`src/config/index.${ext}`] = configTemplate(config);
215
- files[`src/app.${ext}`] = appTemplate(config);
216
- files[`src/index.${ext}`] = indexTemplate(config);
217
- files[`src/shared/index.${ext}`] = sharedIndexTemplate(config);
218
- files[`src/shared/adapter.${ext}`] = config.adapter === "mongokit" ? createAdapterTemplate(config) : customAdapterTemplate(config);
219
- files[`src/shared/permissions.${ext}`] = permissionsTemplate(config);
220
- if (config.tenant === "multi") {
221
- files[`src/shared/presets/index.${ext}`] = presetsMultiTenantTemplate(config);
222
- files[`src/shared/presets/flexible-multi-tenant.${ext}`] = flexibleMultiTenantPresetTemplate(config);
223
- } else {
224
- files[`src/shared/presets/index.${ext}`] = presetsSingleTenantTemplate(config);
225
- }
226
- files[`src/plugins/index.${ext}`] = pluginsIndexTemplate(config);
227
- files[`src/resources/index.${ext}`] = resourcesIndexTemplate(config);
228
- files[`src/resources/user/user.model.${ext}`] = userModelTemplate(config);
229
- files[`src/resources/user/user.repository.${ext}`] = userRepositoryTemplate(config);
230
- files[`src/resources/user/user.controller.${ext}`] = userControllerTemplate(config);
231
- files[`src/resources/auth/auth.resource.${ext}`] = authResourceTemplate(config);
232
- files[`src/resources/auth/auth.handlers.${ext}`] = authHandlersTemplate(config);
233
- files[`src/resources/auth/auth.schemas.${ext}`] = authSchemasTemplate();
234
- files[`src/resources/example/example.model.${ext}`] = exampleModelTemplate(config);
235
- files[`src/resources/example/example.repository.${ext}`] = exampleRepositoryTemplate(config);
236
- files[`src/resources/example/example.resource.${ext}`] = exampleResourceTemplate(config);
237
- files[`src/resources/example/example.controller.${ext}`] = exampleControllerTemplate(config);
238
- files[`src/resources/example/example.schemas.${ext}`] = exampleSchemasTemplate(config);
239
- files[`tests/example.test.${ext}`] = exampleTestTemplate(config);
240
- files[`tests/auth.test.${ext}`] = authTestTemplate(config);
241
- for (const [filePath, content] of Object.entries(files)) {
242
- const fullPath = path.join(projectPath, filePath);
243
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
244
- await fs.writeFile(fullPath, content);
245
- console.log(` ✅ Created: ${filePath}`);
246
- }
182
+ const ext = config.typescript ? "ts" : "js";
183
+ const dirs = [
184
+ "",
185
+ "src",
186
+ "src/config",
187
+ "src/shared",
188
+ "src/shared/presets",
189
+ "src/plugins",
190
+ "src/resources",
191
+ ...config.auth === "jwt" ? ["src/resources/user", "src/resources/auth"] : [],
192
+ "src/resources/example",
193
+ "tests"
194
+ ];
195
+ for (const dir of dirs) {
196
+ await fs.mkdir(path.join(projectPath, dir), { recursive: true });
197
+ console.log(` + Created: ${dir || "/"}`);
198
+ }
199
+ const files = {
200
+ "package.json": packageJsonTemplate(config),
201
+ ".gitignore": gitignoreTemplate(),
202
+ ".env.example": envExampleTemplate(config),
203
+ ".env.dev": envDevTemplate(config),
204
+ "README.md": readmeTemplate(config)
205
+ };
206
+ if (config.typescript) files["tsconfig.json"] = tsconfigTemplate();
207
+ files["vitest.config.ts"] = vitestConfigTemplate(config);
208
+ files[`src/config/env.${ext}`] = envLoaderTemplate(config);
209
+ files[`src/config/index.${ext}`] = configTemplate(config);
210
+ files[`src/app.${ext}`] = appTemplate(config);
211
+ files[`src/index.${ext}`] = indexTemplate(config);
212
+ files[`src/shared/index.${ext}`] = sharedIndexTemplate(config);
213
+ files[`src/shared/adapter.${ext}`] = config.adapter === "mongokit" ? createAdapterTemplate(config) : customAdapterTemplate(config);
214
+ files[`src/shared/permissions.${ext}`] = permissionsTemplate(config);
215
+ if (config.tenant === "multi") {
216
+ files[`src/shared/presets/index.${ext}`] = presetsMultiTenantTemplate(config);
217
+ files[`src/shared/presets/flexible-multi-tenant.${ext}`] = flexibleMultiTenantPresetTemplate(config);
218
+ } else files[`src/shared/presets/index.${ext}`] = presetsSingleTenantTemplate(config);
219
+ files[`src/plugins/index.${ext}`] = pluginsIndexTemplate(config);
220
+ files[`src/resources/index.${ext}`] = resourcesIndexTemplate(config);
221
+ if (config.auth === "better-auth") files[`src/auth.${ext}`] = betterAuthSetupTemplate(config);
222
+ else {
223
+ files[`src/resources/user/user.model.${ext}`] = userModelTemplate(config);
224
+ files[`src/resources/user/user.repository.${ext}`] = userRepositoryTemplate(config);
225
+ files[`src/resources/user/user.controller.${ext}`] = userControllerTemplate(config);
226
+ files[`src/resources/auth/auth.resource.${ext}`] = authResourceTemplate(config);
227
+ files[`src/resources/auth/auth.handlers.${ext}`] = authHandlersTemplate(config);
228
+ files[`src/resources/auth/auth.schemas.${ext}`] = authSchemasTemplate(config);
229
+ }
230
+ files[`src/resources/example/example.model.${ext}`] = exampleModelTemplate(config);
231
+ files[`src/resources/example/example.repository.${ext}`] = exampleRepositoryTemplate(config);
232
+ files[`src/resources/example/example.resource.${ext}`] = exampleResourceTemplate(config);
233
+ files[`src/resources/example/example.controller.${ext}`] = exampleControllerTemplate(config);
234
+ files[`src/resources/example/example.schemas.${ext}`] = exampleSchemasTemplate(config);
235
+ files[`tests/example.test.${ext}`] = exampleTestTemplate(config);
236
+ if (config.auth === "jwt") files[`tests/auth.test.${ext}`] = authTestTemplate(config);
237
+ files[".arcrc"] = JSON.stringify({
238
+ adapter: config.adapter,
239
+ auth: config.auth,
240
+ tenant: config.tenant,
241
+ typescript: config.typescript
242
+ }, null, 2) + "\n";
243
+ for (const [filePath, content] of Object.entries(files)) {
244
+ const fullPath = path.join(projectPath, filePath);
245
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
246
+ await fs.writeFile(fullPath, content);
247
+ console.log(` + Created: ${filePath}`);
248
+ }
247
249
  }
248
250
  function packageJsonTemplate(config) {
249
- const scripts = config.typescript ? {
250
- dev: "tsx watch src/index.ts",
251
- build: "tsc",
252
- start: "node dist/index.js",
253
- test: "vitest run",
254
- "test:watch": "vitest"
255
- } : {
256
- dev: "node --watch src/index.js",
257
- start: "node src/index.js",
258
- test: "vitest run",
259
- "test:watch": "vitest"
260
- };
261
- const imports = config.typescript ? {
262
- "#config/*": "./dist/config/*",
263
- "#shared/*": "./dist/shared/*",
264
- "#resources/*": "./dist/resources/*",
265
- "#plugins/*": "./dist/plugins/*"
266
- } : {
267
- "#config/*": "./src/config/*",
268
- "#shared/*": "./src/shared/*",
269
- "#resources/*": "./src/resources/*",
270
- "#plugins/*": "./src/plugins/*"
271
- };
272
- return JSON.stringify(
273
- {
274
- name: config.name,
275
- version: "1.0.0",
276
- type: "module",
277
- main: config.typescript ? "dist/index.js" : "src/index.js",
278
- imports,
279
- scripts,
280
- engines: {
281
- node: ">=20"
282
- }
283
- },
284
- null,
285
- 2
286
- );
251
+ const scripts = config.typescript ? {
252
+ dev: "tsx watch src/index.ts",
253
+ build: "tsc",
254
+ start: "node dist/index.js",
255
+ test: "vitest run",
256
+ "test:watch": "vitest"
257
+ } : {
258
+ dev: "node --watch src/index.js",
259
+ start: "node src/index.js",
260
+ test: "vitest run",
261
+ "test:watch": "vitest"
262
+ };
263
+ const imports = config.typescript ? {
264
+ "#config/*": "./dist/config/*",
265
+ "#shared/*": "./dist/shared/*",
266
+ "#resources/*": "./dist/resources/*",
267
+ "#plugins/*": "./dist/plugins/*"
268
+ } : {
269
+ "#config/*": "./src/config/*",
270
+ "#shared/*": "./src/shared/*",
271
+ "#resources/*": "./src/resources/*",
272
+ "#plugins/*": "./src/plugins/*"
273
+ };
274
+ return JSON.stringify({
275
+ name: config.name,
276
+ version: "1.0.0",
277
+ type: "module",
278
+ main: config.typescript ? "dist/index.js" : "src/index.js",
279
+ imports,
280
+ scripts,
281
+ engines: { node: ">=20" }
282
+ }, null, 2);
287
283
  }
288
284
  function tsconfigTemplate() {
289
- return JSON.stringify(
290
- {
291
- compilerOptions: {
292
- target: "ES2022",
293
- module: "NodeNext",
294
- moduleResolution: "NodeNext",
295
- lib: ["ES2022"],
296
- outDir: "./dist",
297
- rootDir: "./src",
298
- strict: true,
299
- esModuleInterop: true,
300
- skipLibCheck: true,
301
- forceConsistentCasingInFileNames: true,
302
- declaration: true,
303
- declarationMap: true,
304
- sourceMap: true,
305
- resolveJsonModule: true,
306
- paths: {
307
- "#shared/*": ["./src/shared/*"],
308
- "#resources/*": ["./src/resources/*"],
309
- "#config/*": ["./src/config/*"],
310
- "#plugins/*": ["./src/plugins/*"]
311
- }
312
- },
313
- include: ["src/**/*"],
314
- exclude: ["node_modules", "dist"]
315
- },
316
- null,
317
- 2
318
- );
285
+ return JSON.stringify({
286
+ compilerOptions: {
287
+ target: "ES2022",
288
+ module: "NodeNext",
289
+ moduleResolution: "NodeNext",
290
+ lib: ["ES2022"],
291
+ outDir: "./dist",
292
+ rootDir: "./src",
293
+ strict: true,
294
+ esModuleInterop: true,
295
+ skipLibCheck: true,
296
+ forceConsistentCasingInFileNames: true,
297
+ declaration: true,
298
+ declarationMap: true,
299
+ sourceMap: true,
300
+ resolveJsonModule: true,
301
+ paths: {
302
+ "#shared/*": ["./src/shared/*"],
303
+ "#resources/*": ["./src/resources/*"],
304
+ "#config/*": ["./src/config/*"],
305
+ "#plugins/*": ["./src/plugins/*"]
306
+ }
307
+ },
308
+ include: ["src/**/*"],
309
+ exclude: ["node_modules", "dist"]
310
+ }, null, 2);
319
311
  }
320
312
  function vitestConfigTemplate(config) {
321
- const srcDir = config.typescript ? "./src" : "./src";
322
- return `import { defineConfig } from 'vitest/config';
313
+ const srcDir = config.typescript ? "./src" : "./src";
314
+ return `import { defineConfig } from 'vitest/config';
323
315
  import { resolve } from 'path';
324
316
 
325
317
  export default defineConfig({
@@ -339,7 +331,7 @@ export default defineConfig({
339
331
  `;
340
332
  }
341
333
  function gitignoreTemplate() {
342
- return `# Dependencies
334
+ return `# Dependencies
343
335
  node_modules/
344
336
 
345
337
  # Build
@@ -370,31 +362,45 @@ coverage/
370
362
  `;
371
363
  }
372
364
  function envExampleTemplate(config) {
373
- let content = `# Server
365
+ let content = `# Server
374
366
  PORT=8040
375
367
  HOST=0.0.0.0
376
368
  NODE_ENV=development
377
-
369
+ `;
370
+ if (config.auth === "better-auth") content += `
371
+ # Better Auth
372
+ BETTER_AUTH_SECRET=your-32-character-minimum-secret-here
373
+ FRONTEND_URL=http://localhost:3000
374
+
375
+ # Google OAuth (optional)
376
+ # GOOGLE_CLIENT_ID=
377
+ # GOOGLE_CLIENT_SECRET=
378
+ `;
379
+ else content += `
378
380
  # JWT
379
381
  JWT_SECRET=your-32-character-minimum-secret-here
382
+ JWT_EXPIRES_IN=7d
380
383
  `;
381
- if (config.adapter === "mongokit") {
382
- content += `
384
+ content += `
385
+ # CORS - Allowed origins
386
+ # Options:
387
+ # * = allow all origins (not recommended for production)
388
+ # Comma-separated list = specific origins only
389
+ CORS_ORIGINS=http://localhost:3000,http://localhost:5173
390
+ `;
391
+ if (config.adapter === "mongokit") content += `
383
392
  # MongoDB
384
393
  MONGODB_URI=mongodb://localhost:27017/${config.name}
385
394
  `;
386
- }
387
- if (config.tenant === "multi") {
388
- content += `
395
+ if (config.tenant === "multi") content += `
389
396
  # Multi-tenant
390
- DEFAULT_ORG_ID=
397
+ ORG_HEADER=x-organization-id
391
398
  `;
392
- }
393
- return content;
399
+ return content;
394
400
  }
395
401
  function readmeTemplate(config) {
396
- const ext = config.typescript ? "ts" : "js";
397
- return `# ${config.name}
402
+ const ext = config.typescript ? "ts" : "js";
403
+ return `# ${config.name}
398
404
 
399
405
  Built with [Arc](https://github.com/classytic/arc) - Resource-Oriented Backend Framework
400
406
 
@@ -500,9 +506,9 @@ arc docs
500
506
 
501
507
  ## Environment Files
502
508
 
503
- - \`.env.dev\` - Development (default)
504
- - \`.env.test\` - Testing
505
- - \`.env.prod\` - Production
509
+ - \`.env.development\` / \`.env.dev\` - Development (default)
510
+ - \`.env.test\` / \`.env.qa\` - Testing / QA
511
+ - \`.env.production\` / \`.env.prod\` - Production
506
512
  - \`.env\` - Fallback
507
513
 
508
514
  ## API Documentation
@@ -526,8 +532,8 @@ API documentation is available via Scalar UI:
526
532
  `;
527
533
  }
528
534
  function indexTemplate(config) {
529
- const ts = config.typescript;
530
- return `/**
535
+ const ts = config.typescript;
536
+ return `/**
531
537
  * ${config.name} - Server Entry Point
532
538
  * Generated by Arc CLI
533
539
  *
@@ -543,30 +549,37 @@ ${config.adapter === "mongokit" ? "import mongoose from 'mongoose';" : ""}
543
549
  import { createAppInstance } from './app.js';
544
550
 
545
551
  async function main()${ts ? ": Promise<void>" : ""} {
546
- console.log(\`🔧 Environment: \${config.env}\`);
552
+ console.log(\`Environment: \${config.env}\`);
547
553
  ${config.adapter === "mongokit" ? `
548
554
  // Connect to MongoDB
549
555
  await mongoose.connect(config.database.uri);
550
- console.log('📦 Connected to MongoDB');
556
+ console.log('Connected to MongoDB');
551
557
  ` : ""}
552
558
  // Create and configure app
553
559
  const app = await createAppInstance();
554
560
 
555
561
  // Start server
556
562
  await app.listen({ port: config.server.port, host: config.server.host });
557
- console.log(\`🚀 Server running at http://\${config.server.host}:\${config.server.port}\`);
563
+ console.log(\`Server running at http://\${config.server.host}:\${config.server.port}\`);
558
564
  }
559
565
 
560
566
  main().catch((err) => {
561
- console.error('Failed to start server:', err);
567
+ console.error('Failed to start server:', err);
562
568
  process.exit(1);
563
569
  });
564
570
  `;
565
571
  }
566
572
  function appTemplate(config) {
567
- const ts = config.typescript;
568
- const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
569
- return `/**
573
+ const ts = config.typescript;
574
+ const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
575
+ const betterAuthImport = config.auth === "better-auth" ? `import { createBetterAuthAdapter } from '@classytic/arc/auth';
576
+ import { getAuth } from './auth.js';
577
+ ` : "";
578
+ const authConfig = config.auth === "better-auth" ? config.tenant === "multi" ? `auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth: getAuth(), orgContext: true }) },` : `auth: { type: 'betterAuth', betterAuth: createBetterAuthAdapter({ auth: getAuth() }) },` : `auth: {
579
+ type: 'jwt',
580
+ jwt: { secret: config.jwt.secret },
581
+ },`;
582
+ return `/**
570
583
  * ${config.name} - App Factory
571
584
  * Generated by Arc CLI
572
585
  *
@@ -579,7 +592,7 @@ function appTemplate(config) {
579
592
 
580
593
  ${typeImport}import config from '#config/index.js';
581
594
  import { createApp } from '@classytic/arc/factory';
582
-
595
+ ${betterAuthImport}
583
596
  // App-specific plugins
584
597
  import { registerPlugins } from '#plugins/index.js';
585
598
 
@@ -594,16 +607,15 @@ import { registerResources } from '#resources/index.js';
594
607
  export async function createAppInstance()${ts ? ": Promise<FastifyInstance>" : ""} {
595
608
  // Create Arc app with base configuration
596
609
  const app = await createApp({
597
- preset: config.env === 'production' ? 'production' : 'development',
598
- auth: {
599
- jwt: { secret: config.jwt.secret },
600
- },
610
+ preset: config.env === 'production' ? (${config.edge ? "'edge'" : "'production'"}) : 'development',
611
+ ${authConfig}
601
612
  cors: {
602
613
  origin: config.cors.origins,
603
614
  methods: config.cors.methods,
604
615
  allowedHeaders: config.cors.allowedHeaders,
605
616
  credentials: config.cors.credentials,
606
617
  },
618
+ trustProxy: true,
607
619
  });
608
620
 
609
621
  // Register app-specific plugins (explicit dependency injection)
@@ -619,8 +631,8 @@ export default createAppInstance;
619
631
  `;
620
632
  }
621
633
  function envLoaderTemplate(config) {
622
- const ts = config.typescript;
623
- return `/**
634
+ const ts = config.typescript;
635
+ return `/**
624
636
  * Environment Loader
625
637
  *
626
638
  * MUST be imported FIRST before any other imports.
@@ -653,12 +665,12 @@ const defaultEnvFile = resolve(process.cwd(), '.env');
653
665
 
654
666
  if (existsSync(envFile)) {
655
667
  dotenv.config({ path: envFile });
656
- console.log(\`📄 Loaded: .env.\${env}\`);
668
+ console.log(\`Loaded: .env.\${env}\`);
657
669
  } else if (existsSync(defaultEnvFile)) {
658
670
  dotenv.config({ path: defaultEnvFile });
659
- console.log('📄 Loaded: .env');
671
+ console.log('Loaded: .env');
660
672
  } else {
661
- console.warn('⚠️ No .env file found');
673
+ console.warn('Warning: No .env file found');
662
674
  }
663
675
 
664
676
  // Export for reference
@@ -666,43 +678,50 @@ export const ENV = env;
666
678
  `;
667
679
  }
668
680
  function envDevTemplate(config) {
669
- let content = `# Development Environment
681
+ let content = `# Development Environment
670
682
  NODE_ENV=development
671
683
 
672
684
  # Server
673
685
  PORT=8040
674
686
  HOST=0.0.0.0
675
-
687
+ `;
688
+ if (config.auth === "better-auth") content += `
689
+ # Better Auth
690
+ BETTER_AUTH_SECRET=dev-secret-change-in-production-min-32-chars
691
+ FRONTEND_URL=http://localhost:3000
692
+
693
+ # Google OAuth (optional — leave empty to disable)
694
+ GOOGLE_CLIENT_ID=
695
+ GOOGLE_CLIENT_SECRET=
696
+ `;
697
+ else content += `
676
698
  # JWT
677
699
  JWT_SECRET=dev-secret-change-in-production-min-32-chars
678
700
  JWT_EXPIRES_IN=7d
679
-
701
+ `;
702
+ content += `
680
703
  # CORS - Allowed origins
681
704
  # Options:
682
705
  # * = allow all origins (not recommended for production)
683
706
  # Comma-separated list = specific origins only
684
707
  CORS_ORIGINS=http://localhost:3000,http://localhost:5173
685
708
  `;
686
- if (config.adapter === "mongokit") {
687
- content += `
709
+ if (config.adapter === "mongokit") content += `
688
710
  # MongoDB
689
711
  MONGODB_URI=mongodb://localhost:27017/${config.name}
690
712
  `;
691
- }
692
- if (config.tenant === "multi") {
693
- content += `
713
+ if (config.tenant === "multi") content += `
694
714
  # Multi-tenant
695
715
  ORG_HEADER=x-organization-id
696
716
  `;
697
- }
698
- return content;
717
+ return content;
699
718
  }
700
719
  function pluginsIndexTemplate(config) {
701
- const ts = config.typescript;
702
- const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
703
- const configType = ts ? ": { config: AppConfig }" : "";
704
- const appType = ts ? ": FastifyInstance" : "";
705
- let content = `/**
720
+ const ts = config.typescript;
721
+ const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
722
+ const configType = ts ? ": { config: AppConfig }" : "";
723
+ const appType = ts ? ": FastifyInstance" : "";
724
+ let content = `/**
706
725
  * App Plugins Registry
707
726
  *
708
727
  * Register your app-specific plugins here.
@@ -710,12 +729,9 @@ function pluginsIndexTemplate(config) {
710
729
  */
711
730
 
712
731
  ${typeImport}${ts ? "import type { AppConfig } from '../config/index.js';\n" : ""}import { openApiPlugin, scalarPlugin } from '@classytic/arc/docs';
732
+ import { errorHandlerPlugin } from '@classytic/arc/plugins';
713
733
  `;
714
- if (config.tenant === "multi") {
715
- content += `import { orgScopePlugin } from '@classytic/arc/org';
716
- `;
717
- }
718
- content += `
734
+ content += `
719
735
  /**
720
736
  * Register all app-specific plugins
721
737
  *
@@ -728,6 +744,11 @@ export async function registerPlugins(
728
744
  )${ts ? ": Promise<void>" : ""} {
729
745
  const { config } = deps;
730
746
 
747
+ // Error handling (CastError → 400, validation → 422, duplicate → 409)
748
+ await app.register(errorHandlerPlugin, {
749
+ includeStack: config.isDev,
750
+ });
751
+
731
752
  // API Documentation (Scalar UI)
732
753
  // OpenAPI spec: /_docs/openapi.json
733
754
  // Scalar UI: /docs
@@ -735,43 +756,37 @@ export async function registerPlugins(
735
756
  title: '${config.name} API',
736
757
  version: '1.0.0',
737
758
  description: 'API documentation for ${config.name}',
759
+ apiPrefix: '/api',
738
760
  });
739
761
  await app.register(scalarPlugin, {
740
762
  routePrefix: '/docs',
741
763
  theme: 'default',
742
764
  });
743
- `;
744
- if (config.tenant === "multi") {
745
- content += `
746
- // Multi-tenant org scope
747
- await app.register(orgScopePlugin, {
748
- header: config.org?.header || 'x-organization-id',
749
- bypassRoles: ['superadmin', 'admin'],
750
- });
751
- `;
752
- }
753
- content += `
765
+
754
766
  // Add your custom plugins here:
755
767
  // await app.register(myCustomPlugin, { ...options });
756
768
  }
757
769
  `;
758
- return content;
770
+ return content;
759
771
  }
760
772
  function resourcesIndexTemplate(config) {
761
- const ts = config.typescript;
762
- const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
763
- const appType = ts ? ": FastifyInstance" : "";
764
- return `/**
773
+ const ts = config.typescript;
774
+ const typeImport = ts ? "import type { FastifyInstance } from 'fastify';\n" : "";
775
+ const appType = ts ? ": FastifyInstance" : "";
776
+ return `/**
765
777
  * Resources Registry
766
778
  *
767
779
  * Central registry for all API resources.
768
- * Flat structure - no barrels, direct imports.
780
+ * All resources are mounted under /api prefix via Fastify scoping.
769
781
  */
770
782
 
771
- ${typeImport}
783
+ ${typeImport}${config.auth === "jwt" ? `
772
784
  // Auth resources (register, login, /users/me)
773
785
  import { authResource, userProfileResource } from './auth/auth.resource.js';
774
-
786
+ ` : `
787
+ // Auth is handled by Better Auth — routes at /api/auth/*
788
+ // No manual auth resource needed.
789
+ `}
775
790
  // App resources
776
791
  import exampleResource from './example/example.resource.js';
777
792
 
@@ -782,24 +797,27 @@ import exampleResource from './example/example.resource.js';
782
797
  * All registered resources
783
798
  */
784
799
  export const resources = [
785
- authResource,
800
+ ${config.auth === "jwt" ? ` authResource,
786
801
  userProfileResource,
787
- exampleResource,
802
+ ` : ` `}exampleResource,
788
803
  ]${ts ? " as const" : ""};
789
804
 
790
805
  /**
791
- * Register all resources with the app
792
- */
793
- export async function registerResources(app${appType})${ts ? ": Promise<void>" : ""} {
794
- for (const resource of resources) {
795
- await app.register(resource.toPlugin());
796
- }
806
+ * Register all resources with the app under a common prefix.
807
+ * Fastify scoping ensures all routes are mounted at /api/*.
808
+ * The apiPrefix option in openApiPlugin keeps OpenAPI docs in sync.
809
+ */
810
+ export async function registerResources(app${appType}, prefix = '/api')${ts ? ": Promise<void>" : ""} {
811
+ await app.register(async (scope) => {
812
+ for (const resource of resources) {
813
+ await scope.register(resource.toPlugin());
814
+ }
815
+ }, { prefix });
797
816
  }
798
817
  `;
799
818
  }
800
- function sharedIndexTemplate(config) {
801
- const ts = config.typescript;
802
- return `/**
819
+ function sharedIndexTemplate(_config) {
820
+ return `/**
803
821
  * Shared Utilities
804
822
  *
805
823
  * Central exports for resource definitions.
@@ -812,19 +830,7 @@ export { createAdapter } from './adapter.js';
812
830
  // Core Arc exports
813
831
  export { createMongooseAdapter, defineResource } from '@classytic/arc';
814
832
 
815
- // Permission helpers
816
- export {
817
- allowPublic,
818
- requireAuth,
819
- requireRoles,
820
- requireOwnership,
821
- allOf,
822
- anyOf,
823
- denyAll,
824
- when,${ts ? "\n type PermissionCheck," : ""}
825
- } from '@classytic/arc/permissions';
826
-
827
- // Application permissions
833
+ // Permission helpers (core + application-level)
828
834
  export * from './permissions.js';
829
835
 
830
836
  // Presets
@@ -832,8 +838,8 @@ export * from './presets/index.js';
832
838
  `;
833
839
  }
834
840
  function createAdapterTemplate(config) {
835
- const ts = config.typescript;
836
- return `/**
841
+ const ts = config.typescript;
842
+ return `/**
837
843
  * MongoKit Adapter Factory
838
844
  *
839
845
  * Creates Arc adapters using MongoKit repositories.
@@ -849,10 +855,10 @@ ${ts ? "import type { Model } from 'mongoose';\nimport type { Repository } from
849
855
  * Note: Query parsing is handled by MongoKit's Repository class.
850
856
  * Just pass the model and repository - Arc handles the rest.
851
857
  */
852
- export function createAdapter${ts ? "<TDoc, TRepo extends Repository<TDoc>>" : ""}(
858
+ export function createAdapter${ts ? "<TDoc = any>" : ""}(
853
859
  model${ts ? ": Model<TDoc>" : ""},
854
- repository${ts ? ": TRepo" : ""}
855
- )${ts ? ": ReturnType<typeof createMongooseAdapter>" : ""} {
860
+ repository${ts ? ": Repository<TDoc>" : ""}
861
+ ) {
856
862
  return createMongooseAdapter({
857
863
  model,
858
864
  repository,
@@ -861,8 +867,8 @@ export function createAdapter${ts ? "<TDoc, TRepo extends Repository<TDoc>>" : "
861
867
  `;
862
868
  }
863
869
  function customAdapterTemplate(config) {
864
- const ts = config.typescript;
865
- return `/**
870
+ const ts = config.typescript;
871
+ return `/**
866
872
  * Custom Adapter Factory
867
873
  *
868
874
  * Implement your own database adapter here.
@@ -883,7 +889,7 @@ export function createAdapter${ts ? "<TDoc>" : ""}(
883
889
  model${ts ? ": Model<TDoc>" : ""},
884
890
  repository${ts ? ": any" : ""}
885
891
  )${ts ? ": ReturnType<typeof createMongooseAdapter>" : ""} {
886
- // TODO: Implement your custom adapter
892
+ // SCAFFOLD: Replace with your custom adapter implementation
887
893
  return createMongooseAdapter({
888
894
  model,
889
895
  repository,
@@ -892,8 +898,7 @@ export function createAdapter${ts ? "<TDoc>" : ""}(
892
898
  `;
893
899
  }
894
900
  function presetsMultiTenantTemplate(config) {
895
- const ts = config.typescript;
896
- return `/**
901
+ return `/**
897
902
  * Arc Presets - Multi-Tenant Configuration
898
903
  *
899
904
  * Pre-configured presets for multi-tenant applications.
@@ -917,7 +922,6 @@ export { flexibleMultiTenantPreset } from './flexible-multi-tenant.js';
917
922
  */
918
923
  export const orgScoped = multiTenantPreset({
919
924
  tenantField: 'organizationId',
920
- bypassRoles: ['superadmin', 'admin'],
921
925
  });
922
926
 
923
927
  /**
@@ -955,14 +959,13 @@ export const presets = {
955
959
  ownedByUser,
956
960
  softDelete,
957
961
  slugLookup,
958
- }${ts ? " as const" : ""};
962
+ }${config.typescript ? " as const" : ""};
959
963
 
960
964
  export default presets;
961
965
  `;
962
966
  }
963
967
  function presetsSingleTenantTemplate(config) {
964
- const ts = config.typescript;
965
- return `/**
968
+ return `/**
966
969
  * Arc Presets - Single-Tenant Configuration
967
970
  *
968
971
  * Pre-configured presets for single-tenant applications.
@@ -1008,18 +1011,31 @@ export const presets = {
1008
1011
  ownedByUser,
1009
1012
  softDelete,
1010
1013
  slugLookup,
1011
- }${ts ? " as const" : ""};
1014
+ }${config.typescript ? " as const" : ""};
1012
1015
 
1013
1016
  export default presets;
1014
1017
  `;
1015
1018
  }
1016
1019
  function flexibleMultiTenantPresetTemplate(config) {
1017
- const ts = config.typescript;
1018
- const typeAnnotations = ts ? `
1020
+ const ts = config.typescript;
1021
+ return `/**
1022
+ * Flexible Multi-Tenant Preset
1023
+ *
1024
+ * Smarter tenant filtering that works with public + authenticated routes.
1025
+ *
1026
+ * Philosophy:
1027
+ * - No org scope → No filtering (public data, all orgs)
1028
+ * - Org scope present → Filter by org
1029
+ * - Elevated scope → No filter (platform admin sees all)
1030
+ *
1031
+ * Uses request.scope (RequestScope) from Arc's scope system.
1032
+ */
1033
+ ${ts ? `
1034
+ import { getOrgId, isElevated, isMember } from '@classytic/arc/scope';
1035
+ import type { RequestScope } from '@classytic/arc/scope';
1036
+
1019
1037
  interface FlexibleMultiTenantOptions {
1020
1038
  tenantField?: string;
1021
- bypassRoles?: string[];
1022
- extractOrganizationId?: (request: any) => string | null;
1023
1039
  }
1024
1040
 
1025
1041
  interface PresetMiddlewares {
@@ -1035,101 +1051,47 @@ interface Preset {
1035
1051
  name: string;
1036
1052
  middlewares: PresetMiddlewares;
1037
1053
  }
1038
- ` : "";
1039
- return `/**
1040
- * Flexible Multi-Tenant Preset
1041
- *
1042
- * Smarter tenant filtering that works with public + authenticated routes.
1043
- *
1044
- * Philosophy:
1045
- * - No org header → No filtering (public data, all orgs)
1046
- * - Org header present → Require auth, filter by org
1047
- *
1048
- * This differs from Arc's strict multiTenant which always requires auth.
1049
- */
1050
- ${typeAnnotations}
1054
+ ` : `
1055
+ const { getOrgId, isElevated, isMember } = require('@classytic/arc/scope');
1056
+ `}
1051
1057
  /**
1052
- * Default organization ID extractor
1053
- * Tries multiple sources in order of priority
1058
+ * Create flexible tenant filter middleware.
1059
+ * Only filters when org context is present.
1054
1060
  */
1055
- function defaultExtractOrganizationId(request${ts ? ": any" : ""})${ts ? ": string | null" : ""} {
1056
- // Priority 1: Explicit context (set by org-scope plugin)
1057
- if (request.context?.organizationId) {
1058
- return String(request.context.organizationId);
1059
- }
1060
-
1061
- // Priority 2: User's organizationId field
1062
- if (request.user?.organizationId) {
1063
- return String(request.user.organizationId);
1064
- }
1065
-
1066
- // Priority 3: User's organization object (nested)
1067
- if (request.user?.organization) {
1068
- const org = request.user.organization;
1069
- return String(org._id || org.id || org);
1070
- }
1071
-
1072
- return null;
1073
- }
1074
-
1075
- /**
1076
- * Create flexible tenant filter middleware
1077
- * Only filters when org context is present
1078
- */
1079
- function createFlexibleTenantFilter(
1080
- tenantField${ts ? ": string" : ""},
1081
- bypassRoles${ts ? ": string[]" : ""},
1082
- extractOrganizationId${ts ? ": (request: any) => string | null" : ""}
1083
- ) {
1061
+ function createFlexibleTenantFilter(tenantField${ts ? ": string" : ""}) {
1084
1062
  return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
1085
- const user = request.user;
1086
- const orgId = extractOrganizationId(request);
1063
+ const scope${ts ? ": RequestScope" : ""} = request.scope ?? { kind: 'public' };
1087
1064
 
1088
- // No org context - allow through (public data, no filtering)
1089
- if (!orgId) {
1090
- request.log?.debug?.({ msg: 'No org context - showing all data' });
1065
+ // Elevated scope platform admin sees all, no filter
1066
+ if (isElevated(scope)) {
1067
+ request.log?.debug?.({ msg: 'Elevated scope no tenant filter' });
1091
1068
  return;
1092
1069
  }
1093
1070
 
1094
- // Org context present - auth should already be handled by org-scope plugin
1095
- // But double-check for safety
1096
- if (!user) {
1097
- request.log?.warn?.({ msg: 'Org context present but no user - should not happen' });
1098
- return reply.code(401).send({
1099
- success: false,
1100
- error: 'Unauthorized',
1101
- message: 'Authentication required for organization-scoped data',
1102
- });
1103
- }
1104
-
1105
- // Bypass roles skip filter (superadmin sees all)
1106
- const userRoles = Array.isArray(user.roles) ? user.roles : [];
1107
- if (bypassRoles.some((r${ts ? ": string" : ""}) => userRoles.includes(r))) {
1108
- request.log?.debug?.({ msg: 'Bypass role - no tenant filter' });
1071
+ // Member scope filter by org
1072
+ if (isMember(scope)) {
1073
+ request.query = request.query ?? {};
1074
+ request.query._policyFilters = {
1075
+ ...(request.query._policyFilters ?? {}),
1076
+ [tenantField]: scope.organizationId,
1077
+ };
1078
+ request.log?.debug?.({ msg: 'Tenant filter applied', orgId: scope.organizationId, tenantField });
1109
1079
  return;
1110
1080
  }
1111
1081
 
1112
- // Apply tenant filter to query
1113
- request.query = request.query ?? {};
1114
- request.query._policyFilters = {
1115
- ...(request.query._policyFilters ?? {}),
1116
- [tenantField]: orgId,
1117
- };
1118
-
1119
- request.log?.debug?.({ msg: 'Tenant filter applied', orgId, tenantField });
1082
+ // Public / authenticated no org context, show all data (public routes)
1083
+ request.log?.debug?.({ msg: 'No org context — showing all data' });
1120
1084
  };
1121
1085
  }
1122
1086
 
1123
1087
  /**
1124
- * Create tenant injection middleware
1125
- * Injects tenant ID into request body on create
1088
+ * Create tenant injection middleware.
1089
+ * Injects tenant ID into request body on create.
1126
1090
  */
1127
- function createTenantInjection(
1128
- tenantField${ts ? ": string" : ""},
1129
- extractOrganizationId${ts ? ": (request: any) => string | null" : ""}
1130
- ) {
1091
+ function createTenantInjection(tenantField${ts ? ": string" : ""}) {
1131
1092
  return async (request${ts ? ": any" : ""}, reply${ts ? ": any" : ""}) => {
1132
- const orgId = extractOrganizationId(request);
1093
+ const scope${ts ? ": RequestScope" : ""} = request.scope ?? { kind: 'public' };
1094
+ const orgId = getOrgId(scope);
1133
1095
 
1134
1096
  // Fail-closed: Require orgId for create operations
1135
1097
  if (!orgId) {
@@ -1150,18 +1112,12 @@ function createTenantInjection(
1150
1112
  * Flexible Multi-Tenant Preset
1151
1113
  *
1152
1114
  * @param options.tenantField - Field name in database (default: 'organizationId')
1153
- * @param options.bypassRoles - Roles that bypass tenant isolation (default: ['superadmin'])
1154
- * @param options.extractOrganizationId - Custom org ID extractor function
1155
1115
  */
1156
1116
  export function flexibleMultiTenantPreset(options${ts ? ": FlexibleMultiTenantOptions = {}" : " = {}"})${ts ? ": Preset" : ""} {
1157
- const {
1158
- tenantField = 'organizationId',
1159
- bypassRoles = ['superadmin'],
1160
- extractOrganizationId = defaultExtractOrganizationId,
1161
- } = options;
1117
+ const { tenantField = 'organizationId' } = options;
1162
1118
 
1163
- const tenantFilter = createFlexibleTenantFilter(tenantField, bypassRoles, extractOrganizationId);
1164
- const tenantInjection = createTenantInjection(tenantField, extractOrganizationId);
1119
+ const tenantFilter = createFlexibleTenantFilter(tenantField);
1120
+ const tenantInjection = createTenantInjection(tenantField);
1165
1121
 
1166
1122
  return {
1167
1123
  name: 'flexibleMultiTenant',
@@ -1179,10 +1135,10 @@ export default flexibleMultiTenantPreset;
1179
1135
  `;
1180
1136
  }
1181
1137
  function permissionsTemplate(config) {
1182
- const ts = config.typescript;
1183
- const typeImport = ts ? ",\n type PermissionCheck," : "";
1184
- const returnType = ts ? ": PermissionCheck" : "";
1185
- let content = `/**
1138
+ const ts = config.typescript;
1139
+ const typeImport = ts ? ",\n type PermissionCheck," : "";
1140
+ const returnType = ts ? ": PermissionCheck" : "";
1141
+ let content = `/**
1186
1142
  * Permission Helpers
1187
1143
  *
1188
1144
  * Clean, type-safe permission definitions for resources.
@@ -1233,28 +1189,73 @@ export const requireAdmin = ()${returnType} =>
1233
1189
  export const requireSuperadmin = ()${returnType} =>
1234
1190
  requireRoles(['superadmin']);
1235
1191
  `;
1236
- if (config.tenant === "multi") {
1237
- content += `
1192
+ if (config.tenant === "multi") if (config.auth === "better-auth") content += `
1193
+ // ============================================================================
1194
+ // Better Auth Organization & Team Permission Helpers
1195
+ // ============================================================================
1196
+
1197
+ /**
1198
+ * Organization-level guards (per-org member.role):
1199
+ *
1200
+ * - requireOrgRole(['admin','owner']) — checks member.role in active org
1201
+ * - requireOrgMembership() — just checks if user is in the org (any role)
1202
+ * - requireTeamMembership() — checks if user is in the active team
1203
+ *
1204
+ * These are DIFFERENT from platform-level helpers above (requireRoles checks user.roles).
1205
+ * Platform superadmin automatically bypasses all org role checks.
1206
+ *
1207
+ * IMPORTANT: When using Better Auth's Access Control (ac) with custom roles,
1208
+ * you MUST define ALL roles (owner, admin, member, + any custom) using the
1209
+ * same AC instance. BA's built-in defaults won't cover custom statements.
1210
+ * Omitting any role causes BA's hasPermission to fail silently for that role.
1211
+ *
1212
+ * @see multi-org-betterauth boilerplate (src/shared/access-control.ts) for the recommended pattern.
1213
+ */
1214
+ import {
1215
+ requireOrgMembership,
1216
+ requireOrgRole,
1217
+ requireTeamMembership,
1218
+ } from '@classytic/arc/permissions';
1219
+ export { requireOrgMembership, requireOrgRole, requireTeamMembership };
1220
+
1221
+ /**
1222
+ * Require organization owner (checks member.role, not user.roles)
1223
+ */
1224
+ export const requireOrgOwner = ()${returnType} =>
1225
+ requireOrgRole(['owner']);
1226
+
1227
+ /**
1228
+ * Require organization manager or higher (checks member.role, not user.roles)
1229
+ */
1230
+ export const requireOrgManager = ()${returnType} =>
1231
+ requireOrgRole(['manager', 'admin', 'owner']);
1232
+
1233
+ /**
1234
+ * Require any organization member (any role)
1235
+ */
1236
+ export const requireOrgStaff = ()${returnType} =>
1237
+ requireOrgMembership();
1238
+ `;
1239
+ else content += `
1238
1240
  /**
1239
- * Require organization owner
1241
+ * Require organization owner (elevated scope auto-bypasses)
1240
1242
  */
1241
1243
  export const requireOrgOwner = ()${returnType} =>
1242
- requireRoles(['owner'], { bypassRoles: ['admin', 'superadmin'] });
1244
+ requireRoles(['owner', 'admin', 'superadmin']);
1243
1245
 
1244
1246
  /**
1245
1247
  * Require organization manager or higher
1246
1248
  */
1247
1249
  export const requireOrgManager = ()${returnType} =>
1248
- requireRoles(['owner', 'manager'], { bypassRoles: ['admin', 'superadmin'] });
1250
+ requireRoles(['owner', 'manager', 'admin', 'superadmin']);
1249
1251
 
1250
1252
  /**
1251
1253
  * Require organization staff (any org member)
1252
1254
  */
1253
1255
  export const requireOrgStaff = ()${returnType} =>
1254
- requireRoles(['owner', 'manager', 'staff'], { bypassRoles: ['admin', 'superadmin'] });
1256
+ requireRoles(['owner', 'manager', 'staff', 'admin', 'superadmin']);
1255
1257
  `;
1256
- }
1257
- content += `
1258
+ content += `
1258
1259
  // ============================================================================
1259
1260
  // Standard Permission Sets
1260
1261
  // ============================================================================
@@ -1292,8 +1293,8 @@ export const adminPermissions = {
1292
1293
  delete: requireSuperadmin(),
1293
1294
  };
1294
1295
  `;
1295
- if (config.tenant === "multi") {
1296
- content += `
1296
+ if (config.tenant === "multi") {
1297
+ content += `
1297
1298
  /**
1298
1299
  * Organization staff permissions
1299
1300
  */
@@ -1305,24 +1306,45 @@ export const orgStaffPermissions = {
1305
1306
  delete: requireOrgOwner(),
1306
1307
  };
1307
1308
  `;
1308
- }
1309
- return content;
1309
+ if (config.auth === "better-auth") content += `
1310
+ /**
1311
+ * Team-scoped permissions (requires active team)
1312
+ * Uses Better Auth's team membership — flat groups, no team-level roles.
1313
+ */
1314
+ export const teamScopedPermissions = {
1315
+ list: requireTeamMembership(),
1316
+ get: requireTeamMembership(),
1317
+ create: requireTeamMembership(),
1318
+ update: requireTeamMembership(),
1319
+ delete: requireOrgOwner(),
1320
+ };
1321
+ `;
1322
+ }
1323
+ return content;
1310
1324
  }
1311
1325
  function configTemplate(config) {
1312
- const ts = config.typescript;
1313
- let typeDefinition = "";
1314
- if (ts) {
1315
- typeDefinition = `
1326
+ const ts = config.typescript;
1327
+ const authTypeBlock = config.auth === "better-auth" ? `
1328
+ betterAuth: {
1329
+ secret: string;
1330
+ };
1331
+ frontend: {
1332
+ url: string;
1333
+ };` : `
1334
+ jwt: {
1335
+ secret: string;
1336
+ expiresIn: string;
1337
+ };`;
1338
+ let typeDefinition = "";
1339
+ if (ts) typeDefinition = `
1316
1340
  export interface AppConfig {
1317
1341
  env: string;
1342
+ isDev: boolean;
1343
+ isProd: boolean;
1318
1344
  server: {
1319
1345
  port: number;
1320
1346
  host: string;
1321
- };
1322
- jwt: {
1323
- secret: string;
1324
- expiresIn: string;
1325
- };
1347
+ };${authTypeBlock}
1326
1348
  cors: {
1327
1349
  origins: string[] | boolean; // true = allow all ('*')
1328
1350
  methods: string[];
@@ -1332,13 +1354,24 @@ export interface AppConfig {
1332
1354
  database: {
1333
1355
  uri: string;
1334
1356
  };` : ""}${config.tenant === "multi" ? `
1335
- org?: {
1357
+ org: {
1336
1358
  header: string;
1337
1359
  };` : ""}
1338
1360
  }
1339
1361
  `;
1340
- }
1341
- return `/**
1362
+ const authConfigBlock = config.auth === "better-auth" ? `
1363
+ betterAuth: {
1364
+ secret: process.env.BETTER_AUTH_SECRET || 'dev-secret-change-in-production-min-32-chars',
1365
+ },
1366
+
1367
+ frontend: {
1368
+ url: process.env.FRONTEND_URL || 'http://localhost:3000',
1369
+ },` : `
1370
+ jwt: {
1371
+ secret: process.env.JWT_SECRET || 'dev-secret-change-in-production-min-32',
1372
+ expiresIn: process.env.JWT_EXPIRES_IN || '7d',
1373
+ },`;
1374
+ return `/**
1342
1375
  * Application Configuration
1343
1376
  *
1344
1377
  * All config is loaded from environment variables.
@@ -1347,16 +1380,14 @@ export interface AppConfig {
1347
1380
  ${typeDefinition}
1348
1381
  const config${ts ? ": AppConfig" : ""} = {
1349
1382
  env: process.env.NODE_ENV || 'development',
1383
+ isDev: (process.env.NODE_ENV || 'development') !== 'production',
1384
+ isProd: process.env.NODE_ENV === 'production',
1350
1385
 
1351
1386
  server: {
1352
1387
  port: parseInt(process.env.PORT || '8040', 10),
1353
1388
  host: process.env.HOST || '0.0.0.0',
1354
1389
  },
1355
-
1356
- jwt: {
1357
- secret: process.env.JWT_SECRET || 'dev-secret-change-in-production-min-32',
1358
- expiresIn: process.env.JWT_EXPIRES_IN || '7d',
1359
- },
1390
+ ${authConfigBlock}
1360
1391
 
1361
1392
  cors: {
1362
1393
  // '*' = allow all origins (true), otherwise comma-separated list
@@ -1382,12 +1413,12 @@ export default config;
1382
1413
  `;
1383
1414
  }
1384
1415
  function exampleModelTemplate(config) {
1385
- const ts = config.typescript;
1386
- const typeExport = ts ? `
1416
+ const ts = config.typescript;
1417
+ const typeExport = ts ? `
1387
1418
  export type ExampleDocument = mongoose.InferSchemaType<typeof exampleSchema>;
1388
1419
  export type ExampleModel = mongoose.Model<ExampleDocument>;
1389
1420
  ` : "";
1390
- return `/**
1421
+ return `/**
1391
1422
  * Example Model
1392
1423
  * Generated by Arc CLI
1393
1424
  */
@@ -1419,10 +1450,8 @@ export default Example;
1419
1450
  `;
1420
1451
  }
1421
1452
  function exampleRepositoryTemplate(config) {
1422
- const ts = config.typescript;
1423
- const typeImport = ts ? "import type { ExampleDocument } from './example.model.js';\n" : "";
1424
- const generic = ts ? "<ExampleDocument>" : "";
1425
- return `/**
1453
+ const ts = config.typescript;
1454
+ return `/**
1426
1455
  * Example Repository
1427
1456
  * Generated by Arc CLI
1428
1457
  *
@@ -1436,9 +1465,9 @@ import {
1436
1465
  softDeletePlugin,
1437
1466
  methodRegistryPlugin,
1438
1467
  } from '@classytic/mongokit';
1439
- ${typeImport}import Example from './example.model.js';
1468
+ ${ts ? "import type { ExampleDocument } from './example.model.js';\n" : ""}import Example from './example.model.js';
1440
1469
 
1441
- class ExampleRepository extends Repository${generic} {
1470
+ class ExampleRepository extends Repository${ts ? "<ExampleDocument>" : ""} {
1442
1471
  constructor() {
1443
1472
  super(Example, [
1444
1473
  methodRegistryPlugin(), // Required for plugin method registration
@@ -1474,9 +1503,8 @@ export { ExampleRepository };
1474
1503
  `;
1475
1504
  }
1476
1505
  function exampleResourceTemplate(config) {
1477
- config.typescript;
1478
- config.tenant === "multi" ? "['softDelete', 'flexibleMultiTenant']" : "['softDelete']";
1479
- return `/**
1506
+ const ts = config.typescript;
1507
+ return `/**
1480
1508
  * Example Resource
1481
1509
  * Generated by Arc CLI
1482
1510
  *
@@ -1489,12 +1517,12 @@ function exampleResourceTemplate(config) {
1489
1517
 
1490
1518
  import { defineResource } from '@classytic/arc';
1491
1519
  import { createAdapter } from '#shared/adapter.js';
1492
- import { publicReadPermissions } from '#shared/permissions.js';
1493
- ${config.tenant === "multi" ? "import { flexibleMultiTenantPreset } from '#shared/presets/flexible-multi-tenant.js';\n" : ""}import Example from './example.model.js';
1520
+ import { ${config.tenant === "multi" ? "orgStaffPermissions" : "publicReadPermissions"} } from '#shared/permissions.js';
1521
+ ${config.tenant === "multi" ? "import { flexibleMultiTenantPreset } from '#shared/presets/flexible-multi-tenant.js';\n" : ""}import Example${ts ? ", { type ExampleDocument }" : ""} from './example.model.js';
1494
1522
  import exampleRepository from './example.repository.js';
1495
1523
  import exampleController from './example.controller.js';
1496
1524
 
1497
- const exampleResource = defineResource({
1525
+ const exampleResource = defineResource${ts ? "<ExampleDocument>" : ""}({
1498
1526
  name: 'example',
1499
1527
  displayName: 'Examples',
1500
1528
  prefix: '/examples',
@@ -1507,7 +1535,7 @@ const exampleResource = defineResource({
1507
1535
  flexibleMultiTenantPreset({ tenantField: 'organizationId' }),` : ""}
1508
1536
  ],
1509
1537
 
1510
- permissions: publicReadPermissions,
1538
+ permissions: ${config.tenant === "multi" ? "orgStaffPermissions" : "publicReadPermissions"},
1511
1539
 
1512
1540
  // Add custom routes here:
1513
1541
  // additionalRoutes: [
@@ -1524,8 +1552,7 @@ export default exampleResource;
1524
1552
  `;
1525
1553
  }
1526
1554
  function exampleControllerTemplate(config) {
1527
- const ts = config.typescript;
1528
- return `/**
1555
+ return `/**
1529
1556
  * Example Controller
1530
1557
  * Generated by Arc CLI
1531
1558
  *
@@ -1541,7 +1568,11 @@ import { exampleSchemaOptions } from './example.schemas.js';
1541
1568
 
1542
1569
  class ExampleController extends BaseController {
1543
1570
  constructor() {
1544
- super(exampleRepository${ts ? " as any" : ""}, { schemaOptions: exampleSchemaOptions });
1571
+ super(exampleRepository${config.typescript ? " as any" : ""}, {
1572
+ schemaOptions: exampleSchemaOptions,${config.tenant === "multi" ? `
1573
+ tenantField: 'organizationId', // Configurable tenant field for multi-tenant` : `
1574
+ // tenantField: 'organizationId', // For multi-tenant apps`}
1575
+ });
1545
1576
  }
1546
1577
 
1547
1578
  // Add custom controller methods here:
@@ -1555,9 +1586,9 @@ export default exampleController;
1555
1586
  `;
1556
1587
  }
1557
1588
  function exampleSchemasTemplate(config) {
1558
- const ts = config.typescript;
1559
- const multiTenantFields = config.tenant === "multi";
1560
- return `/**
1589
+ const ts = config.typescript;
1590
+ const multiTenantFields = config.tenant === "multi";
1591
+ return `/**
1561
1592
  * Example Schemas
1562
1593
  * Generated by Arc CLI
1563
1594
  *
@@ -1602,8 +1633,8 @@ export default crudSchemas;
1602
1633
  `;
1603
1634
  }
1604
1635
  function exampleTestTemplate(config) {
1605
- const ts = config.typescript;
1606
- return `/**
1636
+ const ts = config.typescript;
1637
+ return `/**
1607
1638
  * Example Resource Tests
1608
1639
  * Generated by Arc CLI
1609
1640
  *
@@ -1668,13 +1699,13 @@ ${config.adapter === "mongokit" ? " await mongoose.connection.close();" : ""}
1668
1699
  `;
1669
1700
  }
1670
1701
  function userModelTemplate(config) {
1671
- const ts = config.typescript;
1672
- const orgRoles = config.tenant === "multi" ? `
1702
+ const ts = config.typescript;
1703
+ const orgRoles = config.tenant === "multi" ? `
1673
1704
  // Organization roles (for multi-tenant)
1674
1705
  const ORG_ROLES = ['owner', 'manager', 'hr', 'staff', 'contractor'] as const;
1675
1706
  type OrgRole = typeof ORG_ROLES[number];
1676
1707
  ` : "";
1677
- const orgInterface = config.tenant === "multi" ? `
1708
+ const orgInterface = config.tenant === "multi" ? `
1678
1709
  type UserOrganization = {
1679
1710
  organizationId: Types.ObjectId;
1680
1711
  organizationName: string;
@@ -1682,7 +1713,7 @@ type UserOrganization = {
1682
1713
  joinedAt: Date;
1683
1714
  };
1684
1715
  ` : "";
1685
- const orgSchema = config.tenant === "multi" ? `
1716
+ const orgSchema = config.tenant === "multi" ? `
1686
1717
  // Multi-org support
1687
1718
  organizations: [{
1688
1719
  organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', required: true },
@@ -1691,7 +1722,7 @@ type UserOrganization = {
1691
1722
  joinedAt: { type: Date, default: () => new Date() },
1692
1723
  }],
1693
1724
  ` : "";
1694
- const orgMethods = config.tenant === "multi" ? `
1725
+ const orgMethods = config.tenant === "multi" ? `
1695
1726
  // Organization methods
1696
1727
  userSchema.methods.getOrgRoles = function(orgId${ts ? ": Types.ObjectId | string" : ""}) {
1697
1728
  const org = this.organizations.find(o => o.organizationId.toString() === orgId.toString());
@@ -1725,7 +1756,7 @@ userSchema.methods.removeOrganization = function(organizationId${ts ? ": Types.O
1725
1756
  // Index for org queries
1726
1757
  userSchema.index({ 'organizations.organizationId': 1 });
1727
1758
  ` : "";
1728
- const userType = ts ? `
1759
+ const userType = ts ? `
1729
1760
  type PlatformRole = 'user' | 'admin' | 'superadmin';
1730
1761
 
1731
1762
  type User = {
@@ -1749,7 +1780,7 @@ type UserMethods = {
1749
1780
  export type UserDocument = HydratedDocument<User, UserMethods>;
1750
1781
  export type UserModel = Model<User, {}, UserMethods>;
1751
1782
  ` : "";
1752
- return `/**
1783
+ return `/**
1753
1784
  * User Model
1754
1785
  * Generated by Arc CLI
1755
1786
  */
@@ -1812,9 +1843,8 @@ export default User;
1812
1843
  `;
1813
1844
  }
1814
1845
  function userRepositoryTemplate(config) {
1815
- const ts = config.typescript;
1816
- const typeImport = ts ? "import type { UserDocument } from './user.model.js';\nimport type { ClientSession, Types } from 'mongoose';\n" : "";
1817
- return `/**
1846
+ const ts = config.typescript;
1847
+ return `/**
1818
1848
  * User Repository
1819
1849
  * Generated by Arc CLI
1820
1850
  *
@@ -1826,7 +1856,7 @@ import {
1826
1856
  methodRegistryPlugin,
1827
1857
  mongoOperationsPlugin,
1828
1858
  } from '@classytic/mongokit';
1829
- ${typeImport}import User from './user.model.js';
1859
+ ${ts ? "import type { UserDocument } from './user.model.js';\nimport type { ClientSession, Types } from 'mongoose';\n" : ""}import User from './user.model.js';
1830
1860
 
1831
1861
  ${ts ? "type ID = string | Types.ObjectId;\n" : ""}
1832
1862
  class UserRepository extends Repository${ts ? "<UserDocument>" : ""} {
@@ -1904,8 +1934,7 @@ export { UserRepository };
1904
1934
  `;
1905
1935
  }
1906
1936
  function userControllerTemplate(config) {
1907
- const ts = config.typescript;
1908
- return `/**
1937
+ return `/**
1909
1938
  * User Controller
1910
1939
  * Generated by Arc CLI
1911
1940
  *
@@ -1918,7 +1947,7 @@ import userRepository from './user.repository.js';
1918
1947
 
1919
1948
  class UserController extends BaseController {
1920
1949
  constructor() {
1921
- super(userRepository${ts ? " as any" : ""});
1950
+ super(userRepository${config.typescript ? " as any" : ""});
1922
1951
  }
1923
1952
 
1924
1953
  // Custom user operations can be added here
@@ -1929,8 +1958,8 @@ export default userController;
1929
1958
  `;
1930
1959
  }
1931
1960
  function authResourceTemplate(config) {
1932
- const ts = config.typescript;
1933
- return `/**
1961
+ const ts = config.typescript;
1962
+ return `/**
1934
1963
  * Auth Resource
1935
1964
  * Generated by Arc CLI
1936
1965
  *
@@ -1972,7 +2001,7 @@ export const authResource = defineResource({
1972
2001
  permissions: allowPublic(),
1973
2002
  handler: handlers.register,
1974
2003
  wrapHandler: false,
1975
- schema: { body: schemas.registerBody },
2004
+ schema: { body: schemas.registerBody, response: { 201: schemas.successResponse } },
1976
2005
  },
1977
2006
  {
1978
2007
  method: 'POST',
@@ -1981,7 +2010,7 @@ export const authResource = defineResource({
1981
2010
  permissions: allowPublic(),
1982
2011
  handler: handlers.login,
1983
2012
  wrapHandler: false,
1984
- schema: { body: schemas.loginBody },
2013
+ schema: { body: schemas.loginBody, response: { 200: schemas.loginResponse } },
1985
2014
  },
1986
2015
  {
1987
2016
  method: 'POST',
@@ -1990,7 +2019,7 @@ export const authResource = defineResource({
1990
2019
  permissions: allowPublic(),
1991
2020
  handler: handlers.refreshToken,
1992
2021
  wrapHandler: false,
1993
- schema: { body: schemas.refreshBody },
2022
+ schema: { body: schemas.refreshBody, response: { 200: schemas.tokenResponse } },
1994
2023
  },
1995
2024
  {
1996
2025
  method: 'POST',
@@ -1999,7 +2028,7 @@ export const authResource = defineResource({
1999
2028
  permissions: allowPublic(),
2000
2029
  handler: handlers.forgotPassword,
2001
2030
  wrapHandler: false,
2002
- schema: { body: schemas.forgotBody },
2031
+ schema: { body: schemas.forgotBody, response: { 200: schemas.successResponse } },
2003
2032
  },
2004
2033
  {
2005
2034
  method: 'POST',
@@ -2008,7 +2037,7 @@ export const authResource = defineResource({
2008
2037
  permissions: allowPublic(),
2009
2038
  handler: handlers.resetPassword,
2010
2039
  wrapHandler: false,
2011
- schema: { body: schemas.resetBody },
2040
+ schema: { body: schemas.resetBody, response: { 200: schemas.successResponse } },
2012
2041
  },
2013
2042
  ],
2014
2043
  });
@@ -2033,6 +2062,7 @@ export const userProfileResource = defineResource({
2033
2062
  permissions: requireAuth(),
2034
2063
  handler: handlers.getUserProfile,
2035
2064
  wrapHandler: false,
2065
+ schema: { response: { 200: schemas.userProfileResponse } },
2036
2066
  },
2037
2067
  {
2038
2068
  method: 'PATCH',
@@ -2041,7 +2071,7 @@ export const userProfileResource = defineResource({
2041
2071
  permissions: requireAuth(),
2042
2072
  handler: handlers.updateUserProfile,
2043
2073
  wrapHandler: false,
2044
- schema: { body: schemas.updateUserBody },
2074
+ schema: { body: schemas.updateUserBody, response: { 200: schemas.userProfileResponse } },
2045
2075
  },
2046
2076
  ],
2047
2077
  });
@@ -2049,26 +2079,128 @@ export const userProfileResource = defineResource({
2049
2079
  export default authResource;
2050
2080
  `;
2051
2081
  }
2082
+ function betterAuthSetupTemplate(config) {
2083
+ const ts = config.typescript;
2084
+ const mongoImport = config.adapter === "mongokit" ? `import mongoose from 'mongoose';
2085
+ import { mongodbAdapter } from 'better-auth/adapters/mongodb';` : "";
2086
+ const dbAdapter = config.adapter === "mongokit" ? config.typescript ? `database: mongodbAdapter(mongoose.connection.getClient().db() as any),` : `database: mongodbAdapter(mongoose.connection.getClient().db()),` : `// Configure your database adapter here
2087
+ // See: https://www.better-auth.com/docs/concepts/database`;
2088
+ const orgPlugin = config.tenant === "multi" ? `
2089
+ import { organization } from 'better-auth/plugins/organization';
2090
+ import { bearer } from 'better-auth/plugins/bearer';` : "";
2091
+ const orgPluginUsage = config.tenant === "multi" ? `
2092
+ plugins: [
2093
+ bearer(),
2094
+ organization({
2095
+ allowUserToCreateOrganization: true,
2096
+ creatorRole: 'owner',
2097
+ teams: {
2098
+ enabled: true,
2099
+ },
2100
+ }),
2101
+ ],` : "";
2102
+ return `/**
2103
+ * Better Auth Configuration
2104
+ * Generated by Arc CLI
2105
+ *
2106
+ * Authentication is handled entirely by Better Auth.
2107
+ * Routes are registered automatically at /api/auth/*
2108
+ *
2109
+ * Better Auth manages these collections:
2110
+ * - user, session, account${config.tenant === "multi" ? ", organization, member, invitation, team, teamMember" : ""}
2111
+ *
2112
+ * @see https://www.better-auth.com/docs
2113
+ */
2114
+
2115
+ import { betterAuth } from 'better-auth';
2116
+ ${mongoImport}${orgPlugin}
2117
+ import config from '#config/index.js';
2118
+
2119
+ let _auth${ts ? ": ReturnType<typeof betterAuth> | null" : ""} = null;
2120
+
2121
+ /**
2122
+ * Get the Better Auth instance (lazy singleton)
2123
+ *
2124
+ * Must be called AFTER database connection is established.
2125
+ */
2126
+ export function getAuth()${ts ? ": ReturnType<typeof betterAuth>" : ""} {
2127
+ if (process.env.NODE_ENV === 'production' && !process.env.BETTER_AUTH_SECRET) {
2128
+ throw new Error('BETTER_AUTH_SECRET is required in production (min 32 chars)');
2129
+ }
2130
+
2131
+ if (!_auth) {
2132
+ _auth = betterAuth({
2133
+ secret: config.betterAuth.secret,
2134
+ baseURL: process.env.BETTER_AUTH_URL || \`http://localhost:\${config.server.port}\`,
2135
+ basePath: '/api/auth',
2136
+
2137
+ ${dbAdapter}
2138
+ ${config.tenant === "multi" ? `
2139
+ user: {
2140
+ additionalFields: {
2141
+ roles: {
2142
+ type: 'string[]',
2143
+ defaultValue: ['user'],
2144
+ required: false,
2145
+ input: false, // Cannot be set during signup
2146
+ },
2147
+ },
2148
+ },
2149
+ ` : ""}
2150
+ emailAndPassword: {
2151
+ enabled: true,
2152
+ minPasswordLength: 6,
2153
+ },
2154
+
2155
+ // Google OAuth (enabled when env vars are set)
2156
+ ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
2157
+ ? {
2158
+ socialProviders: {
2159
+ google: {
2160
+ clientId: process.env.GOOGLE_CLIENT_ID,
2161
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
2162
+ },
2163
+ },
2164
+ }
2165
+ : {}),
2166
+ ${orgPluginUsage}
2167
+ session: {
2168
+ cookieCache: {
2169
+ enabled: true,
2170
+ maxAge: 5 * 60, // 5 minutes
2171
+ },
2172
+ },
2173
+
2174
+ trustedOrigins: [config.frontend.url],
2175
+
2176
+ rateLimit: {
2177
+ enabled: process.env.NODE_ENV === 'production',
2178
+ },
2179
+ });
2180
+ }
2181
+
2182
+ return _auth;
2183
+ }
2184
+
2185
+ export default getAuth;
2186
+ `;
2187
+ }
2052
2188
  function authHandlersTemplate(config) {
2053
- const ts = config.typescript;
2054
- const typeAnnotations = ts ? `
2055
- import type { FastifyRequest, FastifyReply } from 'fastify';
2056
- ` : "";
2057
- return `/**
2189
+ const ts = config.typescript;
2190
+ return `/**
2058
2191
  * Auth Handlers
2059
2192
  * Generated by Arc CLI
2193
+ *
2194
+ * Uses Arc's built-in JWT utilities via fastify.auth (provided by @fastify/jwt v10).
2195
+ * No standalone jsonwebtoken dependency needed.
2060
2196
  */
2061
2197
 
2062
- import jwt from 'jsonwebtoken';
2063
- import config from '#config/index.js';
2064
2198
  import userRepository from '../user/user.repository.js';
2065
- ${typeAnnotations}
2066
- // Token helpers
2067
- function generateTokens(userId${ts ? ": string" : ""}) {
2068
- const accessToken = jwt.sign({ id: userId }, config.jwt.secret, { expiresIn: '15m' });
2069
- const refreshToken = jwt.sign({ id: userId }, config.jwt.secret, { expiresIn: '7d' });
2070
- return { accessToken, refreshToken };
2071
- }
2199
+ ${ts ? `
2200
+ import type { FastifyRequest, FastifyReply } from 'fastify';
2201
+ // Load Arc auth type augmentations (adds request.server.auth typings)
2202
+ import '@classytic/arc/auth';
2203
+ ` : ""}
2072
2204
 
2073
2205
  /**
2074
2206
  * Register new user
@@ -2104,7 +2236,7 @@ export async function login(request${ts ? ": FastifyRequest" : ""}, reply${ts ?
2104
2236
  return reply.code(401).send({ success: false, message: 'Invalid credentials' });
2105
2237
  }
2106
2238
 
2107
- const tokens = generateTokens(user._id.toString());
2239
+ const tokens = request.server.auth.issueTokens({ id: user._id.toString(), roles: user.roles });
2108
2240
 
2109
2241
  return reply.send({
2110
2242
  success: true,
@@ -2127,8 +2259,8 @@ export async function refreshToken(request${ts ? ": FastifyRequest" : ""}, reply
2127
2259
  return reply.code(401).send({ success: false, message: 'Refresh token required' });
2128
2260
  }
2129
2261
 
2130
- const decoded = jwt.verify(token, config.jwt.secret)${ts ? " as { id: string }" : ""};
2131
- const tokens = generateTokens(decoded.id);
2262
+ const decoded = request.server.auth.verifyRefreshToken(token)${ts ? " as { id: string }" : ""};
2263
+ const tokens = request.server.auth.issueTokens({ id: decoded.id });
2132
2264
 
2133
2265
  return reply.send({ success: true, ...tokens });
2134
2266
  } catch {
@@ -2145,11 +2277,12 @@ export async function forgotPassword(request${ts ? ": FastifyRequest" : ""}, rep
2145
2277
  const user = await userRepository.findByEmail(email);
2146
2278
 
2147
2279
  if (user) {
2148
- const token = Math.random().toString(36).slice(2) + Date.now().toString(36);
2280
+ const { randomBytes } = await import('node:crypto');
2281
+ const token = randomBytes(32).toString('hex');
2149
2282
  const expires = new Date(Date.now() + 3600000); // 1 hour
2150
2283
  await userRepository.setResetToken(user._id, token, expires);
2151
- // TODO: Send email with reset link
2152
- request.log.info(\`Password reset token for \${email}: \${token}\`);
2284
+ // SCAFFOLD: Integrate your email provider to send the reset link
2285
+ request.log.info(\`Password reset requested for \${email}\`);
2153
2286
  }
2154
2287
 
2155
2288
  // Always return success to prevent email enumeration
@@ -2221,8 +2354,8 @@ export async function updateUserProfile(request${ts ? ": FastifyRequest" : ""},
2221
2354
  }
2222
2355
  `;
2223
2356
  }
2224
- function authSchemasTemplate(config) {
2225
- return `/**
2357
+ function authSchemasTemplate(_config) {
2358
+ return `/**
2226
2359
  * Auth Schemas
2227
2360
  * Generated by Arc CLI
2228
2361
  */
@@ -2278,11 +2411,56 @@ export const updateUserBody = {
2278
2411
  email: { type: 'string', format: 'email' },
2279
2412
  },
2280
2413
  };
2414
+
2415
+ // Response schemas (enables fast-json-stringify serialization)
2416
+
2417
+ export const successResponse = {
2418
+ type: 'object',
2419
+ properties: {
2420
+ success: { type: 'boolean' },
2421
+ message: { type: 'string' },
2422
+ },
2423
+ };
2424
+
2425
+ export const loginResponse = {
2426
+ type: 'object',
2427
+ properties: {
2428
+ success: { type: 'boolean' },
2429
+ user: {
2430
+ type: 'object',
2431
+ properties: {
2432
+ id: { type: 'string' },
2433
+ name: { type: 'string' },
2434
+ email: { type: 'string' },
2435
+ roles: { type: 'array', items: { type: 'string' } },
2436
+ },
2437
+ },
2438
+ accessToken: { type: 'string' },
2439
+ refreshToken: { type: 'string' },
2440
+ },
2441
+ };
2442
+
2443
+ export const tokenResponse = {
2444
+ type: 'object',
2445
+ properties: {
2446
+ success: { type: 'boolean' },
2447
+ accessToken: { type: 'string' },
2448
+ refreshToken: { type: 'string' },
2449
+ },
2450
+ };
2451
+
2452
+ export const userProfileResponse = {
2453
+ type: 'object',
2454
+ properties: {
2455
+ success: { type: 'boolean' },
2456
+ data: { type: 'object', additionalProperties: true },
2457
+ },
2458
+ };
2281
2459
  `;
2282
2460
  }
2283
2461
  function authTestTemplate(config) {
2284
- const ts = config.typescript;
2285
- return `/**
2462
+ const ts = config.typescript;
2463
+ return `/**
2286
2464
  * Auth Tests
2287
2465
  * Generated by Arc CLI
2288
2466
  */
@@ -2378,18 +2556,32 @@ ${config.adapter === "mongokit" ? ` await mongoose.connection.collection('use
2378
2556
  `;
2379
2557
  }
2380
2558
  function printSuccessMessage(config, skipInstall) {
2381
- const installStep = skipInstall ? ` npm install
2382
- ` : "";
2383
- console.log(`
2559
+ const installStep = skipInstall ? ` npm install\n` : "";
2560
+ const ext = config.typescript ? "ts" : "js";
2561
+ const authInfo = config.auth === "better-auth" ? `
2562
+ Auth (Better Auth):
2563
+
2564
+ Auth routes: http://localhost:8040/api/auth/*
2565
+ Better Auth handles: registration, login, sessions, OAuth
2566
+ Config file: src/auth.${ext}
2567
+ ` : `
2568
+ Auth (JWT):
2569
+
2570
+ POST /auth/register # Register
2571
+ POST /auth/login # Login (returns JWT)
2572
+ POST /auth/refresh # Refresh token
2573
+ GET /users/me # Current user profile
2574
+ `;
2575
+ console.log(`
2384
2576
  ╔═══════════════════════════════════════════════════════════════╗
2385
- Project Created!
2577
+ ║ Project Created
2386
2578
  ╚═══════════════════════════════════════════════════════════════╝
2387
2579
 
2388
2580
  Next steps:
2389
2581
 
2390
2582
  cd ${config.name}
2391
2583
  ${installStep} npm run dev # Uses .env.dev automatically
2392
-
2584
+ ${authInfo}
2393
2585
  API Documentation:
2394
2586
 
2395
2587
  http://localhost:8040/docs # Scalar UI
@@ -2402,15 +2594,13 @@ Run tests:
2402
2594
 
2403
2595
  Add resources:
2404
2596
 
2405
- 1. Create folder: src/resources/product/
2406
- 2. Add: index.${config.typescript ? "ts" : "js"}, model.${config.typescript ? "ts" : "js"}, repository.${config.typescript ? "ts" : "js"}
2407
- 3. Register in src/resources/index.${config.typescript ? "ts" : "js"}
2597
+ arc generate resource product
2408
2598
 
2409
2599
  Project structure:
2410
2600
 
2411
2601
  src/
2412
- ├── app.${config.typescript ? "ts" : "js"} # App factory (for workers/tests)
2413
- ├── index.${config.typescript ? "ts" : "js"} # Server entry
2602
+ ├── app.${ext} # App factory (for workers/tests)
2603
+ ├── index.${ext} # Server entry${config.auth === "better-auth" ? `\n ├── auth.${ext} # Better Auth config` : ""}
2414
2604
  ├── config/ # Configuration
2415
2605
  ├── shared/ # Adapters, presets, permissions
2416
2606
  ├── plugins/ # App plugins (DI pattern)
@@ -2420,6 +2610,7 @@ Documentation:
2420
2610
  https://github.com/classytic/arc
2421
2611
  `);
2422
2612
  }
2423
- var init_default = init;
2424
2613
 
2425
- export { init_default as default, init };
2614
+ //#endregion
2615
+ export { init as default, init };
2616
+ //# sourceMappingURL=init.mjs.map