@decohere/scaffold 0.0.1-dev.126 → 0.0.1-dev.18

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 (264) hide show
  1. package/package.json +9 -19
  2. package/src/add-api.action.ts +25 -0
  3. package/src/add-api.command.ts +31 -0
  4. package/src/add.command.ts +20 -0
  5. package/src/add.module.ts +11 -0
  6. package/src/cli.ts +4 -0
  7. package/src/create.command.ts +20 -0
  8. package/src/create.module.ts +11 -0
  9. package/src/domain/prompt.ts +50 -0
  10. package/{types/src/index.d.ts → src/index.ts} +0 -4
  11. package/src/models/compose.model.ts +205 -0
  12. package/src/models/config-yaml.model.ts +83 -0
  13. package/src/models/const.ts +20 -0
  14. package/src/models/dev-container.model.ts +136 -0
  15. package/src/models/docker-image.model.ts +343 -0
  16. package/src/models/env-file.model.ts +45 -0
  17. package/src/models/gitignore.model.ts +35 -0
  18. package/src/models/launch-json.model.ts +60 -0
  19. package/src/models/package-json.model.ts +169 -0
  20. package/src/models/project.model.ts +141 -0
  21. package/{types/src/models/source-file.d.ts → src/models/source-file.ts} +6 -4
  22. package/src/models/tsconfig.model.ts +29 -0
  23. package/src/models/vitest-config.model.ts +26 -0
  24. package/src/models/vscode-settings.model.ts +24 -0
  25. package/src/program.ts +63 -0
  26. package/{types/src/resolve-target-dir.d.ts → src/resolve-target-dir.ts} +4 -1
  27. package/src/services/scaffold.service.ts +89 -0
  28. package/src/version.ts +56 -0
  29. package/src/webapi/api-name.ts +73 -0
  30. package/src/webapi/cli-options.ts +56 -0
  31. package/{types/src/webapi/const.d.ts → src/webapi/const.ts} +21 -9
  32. package/src/webapi/create-api.ts +275 -0
  33. package/src/webapi/devcontainer/host-scripts.ts +35 -0
  34. package/src/webapi/features/auth.feature.ts +454 -0
  35. package/src/webapi/features/auth0-cli.ts +421 -0
  36. package/src/webapi/features/biome-format.ts +34 -0
  37. package/src/webapi/features/database.feature.ts +522 -0
  38. package/src/webapi/features/finalize.feature.ts +112 -0
  39. package/src/webapi/features/postman.feature.ts +287 -0
  40. package/src/webapi/models/api-package-json.model.ts +60 -0
  41. package/src/webapi/models/auth-config.model.ts +36 -0
  42. package/src/webapi/models/controller-source-file.model.ts +232 -0
  43. package/{es6/src/webapi/models/github-ci.model.js → src/webapi/models/github-ci.model.ts} +29 -27
  44. package/src/webapi/models/main-source-file.model.ts +208 -0
  45. package/src/webapi/models/production-dockerfile.model.ts +162 -0
  46. package/src/webapi/models/sample-files.model.ts +45 -0
  47. package/src/webapi/models/webapi-image.model.ts +21 -0
  48. package/src/webapi/postman/hello-openapi.ts +92 -0
  49. package/src/webapi/postman/local-view.ts +195 -0
  50. package/src/webapi/postman/openapi-to-postman.ts +208 -0
  51. package/src/webapi/workspace-readme.ts +192 -0
  52. package/src/webapi.action.ts +22 -0
  53. package/src/webapi.command.ts +41 -0
  54. package/tests/auth-postman.wiring.spec.ts +263 -0
  55. package/tests/auth0-cli.spec.ts +232 -0
  56. package/tests/biome-format.spec.ts +49 -0
  57. package/tests/cli.integration.spec.ts +658 -0
  58. package/tests/compose.model.spec.ts +51 -0
  59. package/tests/container-image.model.spec.ts +162 -0
  60. package/tests/dev-container.model.spec.ts +29 -0
  61. package/tests/devcontainer-base-image.spec.ts +32 -0
  62. package/tests/hello-di-inject.scaffold.spec.ts +18 -0
  63. package/tests/hello-route.contract.spec.ts +94 -0
  64. package/tests/package-json.order.spec.ts +59 -0
  65. package/tests/postman-from-openapi.spec.ts +197 -0
  66. package/tests/seed-devcontainer-templates.ts +40 -0
  67. package/tests/webapi/devcontainer/codebase-rag-mcp.spec.ts +96 -0
  68. package/LICENSE +0 -15
  69. package/commonjs/src/add-api.action.js +0 -37
  70. package/commonjs/src/add-api.command.js +0 -39
  71. package/commonjs/src/add.command.js +0 -22
  72. package/commonjs/src/add.module.js +0 -24
  73. package/commonjs/src/auth0-configure.action.js +0 -28
  74. package/commonjs/src/auth0-configure.command.js +0 -24
  75. package/commonjs/src/auth0-login.action.js +0 -28
  76. package/commonjs/src/auth0-login.command.js +0 -23
  77. package/commonjs/src/auth0.command.js +0 -22
  78. package/commonjs/src/auth0.module.js +0 -28
  79. package/commonjs/src/cli.js +0 -8
  80. package/commonjs/src/create.command.js +0 -22
  81. package/commonjs/src/create.module.js +0 -24
  82. package/commonjs/src/domain/prompt.js +0 -62
  83. package/commonjs/src/index.js +0 -48
  84. package/commonjs/src/models/compose.model.js +0 -258
  85. package/commonjs/src/models/config-yaml.model.js +0 -86
  86. package/commonjs/src/models/const.js +0 -30
  87. package/commonjs/src/models/dev-container.model.js +0 -172
  88. package/commonjs/src/models/docker-image.model.js +0 -277
  89. package/commonjs/src/models/env-file.model.js +0 -48
  90. package/commonjs/src/models/gitignore.model.js +0 -48
  91. package/commonjs/src/models/launch-json.model.js +0 -70
  92. package/commonjs/src/models/package-json.model.js +0 -156
  93. package/commonjs/src/models/project.model.js +0 -132
  94. package/commonjs/src/models/source-file.js +0 -4
  95. package/commonjs/src/models/tsconfig.model.js +0 -32
  96. package/commonjs/src/models/vitest-config.model.js +0 -33
  97. package/commonjs/src/models/vscode-settings.model.js +0 -29
  98. package/commonjs/src/program.js +0 -64
  99. package/commonjs/src/resolve-target-dir.js +0 -17
  100. package/commonjs/src/services/scaffold.service.js +0 -60
  101. package/commonjs/src/version.js +0 -55
  102. package/commonjs/src/webapi/api-name.js +0 -93
  103. package/commonjs/src/webapi/auth0-commands.js +0 -250
  104. package/commonjs/src/webapi/authorize-scopes.js +0 -79
  105. package/commonjs/src/webapi/cli-options.js +0 -99
  106. package/commonjs/src/webapi/const.js +0 -54
  107. package/commonjs/src/webapi/create-api.js +0 -265
  108. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.js +0 -164
  109. package/commonjs/src/webapi/devcontainer/codebase-rag-mcp.ts +0 -182
  110. package/commonjs/src/webapi/devcontainer/host-scripts.js +0 -50
  111. package/commonjs/src/webapi/devcontainer/templates/codebase-rag.env.example +0 -3
  112. package/commonjs/src/webapi/devcontainer/templates/initialize.sh +0 -169
  113. package/commonjs/src/webapi/devcontainer/templates/post-create.sh +0 -35
  114. package/commonjs/src/webapi/devcontainer/templates/post-start.sh +0 -65
  115. package/commonjs/src/webapi/devcontainer/templates/tty-prompt.sh +0 -38
  116. package/commonjs/src/webapi/features/auth.feature.js +0 -408
  117. package/commonjs/src/webapi/features/auth0-cli.js +0 -472
  118. package/commonjs/src/webapi/features/biome-format.js +0 -47
  119. package/commonjs/src/webapi/features/database.feature.js +0 -433
  120. package/commonjs/src/webapi/features/finalize.feature.js +0 -93
  121. package/commonjs/src/webapi/features/postman.feature.js +0 -322
  122. package/commonjs/src/webapi/models/api-package-json.model.js +0 -58
  123. package/commonjs/src/webapi/models/auth-config.model.js +0 -24
  124. package/commonjs/src/webapi/models/controller-source-file.model.js +0 -190
  125. package/commonjs/src/webapi/models/github-ci.model.js +0 -193
  126. package/commonjs/src/webapi/models/main-source-file.model.js +0 -165
  127. package/commonjs/src/webapi/models/production-dockerfile.model.js +0 -75
  128. package/commonjs/src/webapi/models/sample-files.model.js +0 -58
  129. package/commonjs/src/webapi/models/webapi-image.model.js +0 -25
  130. package/commonjs/src/webapi/postman/hello-openapi.js +0 -173
  131. package/commonjs/src/webapi/postman/local-view.js +0 -182
  132. package/commonjs/src/webapi/postman/openapi-to-postman.js +0 -238
  133. package/commonjs/src/webapi/workspace-readme.js +0 -109
  134. package/commonjs/src/webapi.action.js +0 -28
  135. package/commonjs/src/webapi.command.js +0 -43
  136. package/es6/src/add-api.action.js +0 -27
  137. package/es6/src/add-api.command.js +0 -29
  138. package/es6/src/add.command.js +0 -12
  139. package/es6/src/add.module.js +0 -14
  140. package/es6/src/auth0-configure.action.js +0 -18
  141. package/es6/src/auth0-configure.command.js +0 -14
  142. package/es6/src/auth0-login.action.js +0 -18
  143. package/es6/src/auth0-login.command.js +0 -13
  144. package/es6/src/auth0.command.js +0 -12
  145. package/es6/src/auth0.module.js +0 -18
  146. package/es6/src/cli.js +0 -4
  147. package/es6/src/create.command.js +0 -12
  148. package/es6/src/create.module.js +0 -14
  149. package/es6/src/domain/prompt.js +0 -41
  150. package/es6/src/index.js +0 -9
  151. package/es6/src/models/compose.model.js +0 -254
  152. package/es6/src/models/config-yaml.model.js +0 -74
  153. package/es6/src/models/const.js +0 -20
  154. package/es6/src/models/dev-container.model.js +0 -162
  155. package/es6/src/models/docker-image.model.js +0 -235
  156. package/es6/src/models/env-file.model.js +0 -44
  157. package/es6/src/models/gitignore.model.js +0 -38
  158. package/es6/src/models/launch-json.model.js +0 -60
  159. package/es6/src/models/package-json.model.js +0 -141
  160. package/es6/src/models/project.model.js +0 -117
  161. package/es6/src/models/source-file.js +0 -1
  162. package/es6/src/models/tsconfig.model.js +0 -22
  163. package/es6/src/models/vitest-config.model.js +0 -23
  164. package/es6/src/models/vscode-settings.model.js +0 -19
  165. package/es6/src/program.js +0 -50
  166. package/es6/src/resolve-target-dir.js +0 -7
  167. package/es6/src/services/scaffold.service.js +0 -50
  168. package/es6/src/version.js +0 -34
  169. package/es6/src/webapi/api-name.js +0 -57
  170. package/es6/src/webapi/auth0-commands.js +0 -185
  171. package/es6/src/webapi/authorize-scopes.js +0 -55
  172. package/es6/src/webapi/cli-options.js +0 -34
  173. package/es6/src/webapi/const.js +0 -21
  174. package/es6/src/webapi/create-api.js +0 -245
  175. package/es6/src/webapi/devcontainer/codebase-rag-mcp.js +0 -154
  176. package/es6/src/webapi/devcontainer/codebase-rag-mcp.ts +0 -182
  177. package/es6/src/webapi/devcontainer/host-scripts.js +0 -29
  178. package/es6/src/webapi/devcontainer/templates/codebase-rag.env.example +0 -3
  179. package/es6/src/webapi/devcontainer/templates/initialize.sh +0 -169
  180. package/es6/src/webapi/devcontainer/templates/post-create.sh +0 -35
  181. package/es6/src/webapi/devcontainer/templates/post-start.sh +0 -65
  182. package/es6/src/webapi/devcontainer/templates/tty-prompt.sh +0 -38
  183. package/es6/src/webapi/features/auth.feature.js +0 -357
  184. package/es6/src/webapi/features/auth0-cli.js +0 -406
  185. package/es6/src/webapi/features/biome-format.js +0 -40
  186. package/es6/src/webapi/features/database.feature.js +0 -423
  187. package/es6/src/webapi/features/finalize.feature.js +0 -83
  188. package/es6/src/webapi/features/postman.feature.js +0 -256
  189. package/es6/src/webapi/models/api-package-json.model.js +0 -53
  190. package/es6/src/webapi/models/auth-config.model.js +0 -20
  191. package/es6/src/webapi/models/controller-source-file.model.js +0 -181
  192. package/es6/src/webapi/models/main-source-file.model.js +0 -161
  193. package/es6/src/webapi/models/production-dockerfile.model.js +0 -66
  194. package/es6/src/webapi/models/sample-files.model.js +0 -40
  195. package/es6/src/webapi/models/webapi-image.model.js +0 -18
  196. package/es6/src/webapi/postman/hello-openapi.js +0 -166
  197. package/es6/src/webapi/postman/local-view.js +0 -167
  198. package/es6/src/webapi/postman/openapi-to-postman.js +0 -179
  199. package/es6/src/webapi/workspace-readme.js +0 -88
  200. package/es6/src/webapi.action.js +0 -18
  201. package/es6/src/webapi.command.js +0 -33
  202. package/types/src/add-api.action.d.ts +0 -13
  203. package/types/src/add-api.command.d.ts +0 -7
  204. package/types/src/add.command.d.ts +0 -6
  205. package/types/src/add.module.d.ts +0 -14
  206. package/types/src/auth0-configure.action.d.ts +0 -7
  207. package/types/src/auth0-configure.command.d.ts +0 -6
  208. package/types/src/auth0-login.action.d.ts +0 -4
  209. package/types/src/auth0-login.command.d.ts +0 -6
  210. package/types/src/auth0.command.d.ts +0 -6
  211. package/types/src/auth0.module.d.ts +0 -18
  212. package/types/src/cli.d.ts +0 -2
  213. package/types/src/create.command.d.ts +0 -6
  214. package/types/src/create.module.d.ts +0 -14
  215. package/types/src/domain/prompt.d.ts +0 -4
  216. package/types/src/models/compose.model.d.ts +0 -73
  217. package/types/src/models/config-yaml.model.d.ts +0 -27
  218. package/types/src/models/const.d.ts +0 -20
  219. package/types/src/models/dev-container.model.d.ts +0 -49
  220. package/types/src/models/docker-image.model.d.ts +0 -90
  221. package/types/src/models/env-file.model.d.ts +0 -16
  222. package/types/src/models/gitignore.model.d.ts +0 -9
  223. package/types/src/models/launch-json.model.d.ts +0 -7
  224. package/types/src/models/package-json.model.d.ts +0 -36
  225. package/types/src/models/project.model.d.ts +0 -60
  226. package/types/src/models/tsconfig.model.d.ts +0 -5
  227. package/types/src/models/vitest-config.model.d.ts +0 -5
  228. package/types/src/models/vscode-settings.model.d.ts +0 -7
  229. package/types/src/program.d.ts +0 -8
  230. package/types/src/services/scaffold.service.d.ts +0 -22
  231. package/types/src/version.d.ts +0 -6
  232. package/types/src/webapi/api-name.d.ts +0 -14
  233. package/types/src/webapi/auth0-commands.d.ts +0 -11
  234. package/types/src/webapi/authorize-scopes.d.ts +0 -7
  235. package/types/src/webapi/cli-options.d.ts +0 -6
  236. package/types/src/webapi/create-api.d.ts +0 -15
  237. package/types/src/webapi/devcontainer/codebase-rag-mcp.d.ts +0 -32
  238. package/types/src/webapi/devcontainer/codebase-rag-mcp.ts +0 -182
  239. package/types/src/webapi/devcontainer/host-scripts.d.ts +0 -7
  240. package/types/src/webapi/devcontainer/templates/codebase-rag.env.example +0 -3
  241. package/types/src/webapi/devcontainer/templates/initialize.sh +0 -169
  242. package/types/src/webapi/devcontainer/templates/post-create.sh +0 -35
  243. package/types/src/webapi/devcontainer/templates/post-start.sh +0 -65
  244. package/types/src/webapi/devcontainer/templates/tty-prompt.sh +0 -38
  245. package/types/src/webapi/features/auth.feature.d.ts +0 -10
  246. package/types/src/webapi/features/auth0-cli.d.ts +0 -84
  247. package/types/src/webapi/features/biome-format.d.ts +0 -5
  248. package/types/src/webapi/features/database.feature.d.ts +0 -7
  249. package/types/src/webapi/features/finalize.feature.d.ts +0 -2
  250. package/types/src/webapi/features/postman.feature.d.ts +0 -16
  251. package/types/src/webapi/models/api-package-json.model.d.ts +0 -18
  252. package/types/src/webapi/models/auth-config.model.d.ts +0 -26
  253. package/types/src/webapi/models/controller-source-file.model.d.ts +0 -46
  254. package/types/src/webapi/models/github-ci.model.d.ts +0 -9
  255. package/types/src/webapi/models/main-source-file.model.d.ts +0 -54
  256. package/types/src/webapi/models/production-dockerfile.model.d.ts +0 -18
  257. package/types/src/webapi/models/sample-files.model.d.ts +0 -7
  258. package/types/src/webapi/models/webapi-image.model.d.ts +0 -12
  259. package/types/src/webapi/postman/hello-openapi.d.ts +0 -19
  260. package/types/src/webapi/postman/local-view.d.ts +0 -17
  261. package/types/src/webapi/postman/openapi-to-postman.d.ts +0 -62
  262. package/types/src/webapi/workspace-readme.d.ts +0 -18
  263. package/types/src/webapi.action.d.ts +0 -14
  264. package/types/src/webapi.command.d.ts +0 -11
@@ -0,0 +1,454 @@
1
+ import { ask, pick } from "../../domain/prompt.js";
2
+ import type { ConfigYaml } from "../../models/config-yaml.model.js";
3
+ import { WELL_KNOWN_FILES } from "../../models/const.js";
4
+ import type { EnvFile } from "../../models/env-file.model.js";
5
+ import type { WorkspaceScaffold } from "../../models/project.model.js";
6
+ import type { Feature } from "../../services/scaffold.service.js";
7
+ import { AuthConfigModel } from "../models/auth-config.model.js";
8
+ import { ControllerSourceFile } from "../models/controller-source-file.model.js";
9
+ import type { MainSourceFile } from "../models/main-source-file.model.js";
10
+ import * as auth0Cli from "./auth0-cli.js";
11
+
12
+ export type AuthOptions = {
13
+ authProvider?: "auth0" | "none";
14
+ issuerBaseUrl?: string;
15
+ audience?: string;
16
+ databaseProvider?: "none" | "postgres/typeorm";
17
+ adminUsername?: string;
18
+ adminPassword?: string;
19
+ };
20
+
21
+ const ADMIN_ROLE = "admin";
22
+
23
+ const USER_ENTITY_TS = `import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
24
+ import type { Entity as IEntity } from "@decohere/codex";
25
+
26
+ @Entity()
27
+ export class UserEntity implements IEntity {
28
+ \t@PrimaryGeneratedColumn()
29
+ \tpublic id!: number;
30
+
31
+ \t@Column({ unique: true })
32
+ \tpublic sub!: string;
33
+
34
+ \t@Column({ type: "varchar", nullable: true })
35
+ \tpublic email!: string | null;
36
+
37
+ \t@Column()
38
+ \tpublic displayName!: string;
39
+
40
+ \t/** App-owned roles. The \`admin\` role grants CASL \`manage\`/\`all\` (wildcard). */
41
+ \t@Column({ type: "simple-json", default: "[]" })
42
+ \tpublic roles!: string[];
43
+ }
44
+ `;
45
+
46
+ const USER_REPOSITORY_TS = `import { injectable } from "@decohere/crucible/decorators";
47
+ import { injectUnitOfWork } from "@decohere/codex";
48
+ import { TypeOrmRepository, TypeOrmUnitOfWork } from "@decohere/codex-typeorm";
49
+ import { UserEntity } from "../entities/user.entity.js";
50
+
51
+ @injectable()
52
+ export class UserEntityRepository extends TypeOrmRepository<UserEntity> {
53
+ \tpublic constructor(@injectUnitOfWork() unitOfWork: TypeOrmUnitOfWork) {
54
+ \t\tsuper(unitOfWork, UserEntity);
55
+ \t}
56
+
57
+ \tpublic findBySub(sub: string): Promise<UserEntity | null> {
58
+ \t\treturn this.firstOrDefault({ where: { sub } });
59
+ \t}
60
+ }
61
+ `;
62
+
63
+ const USER_SERVICE_TS = `import { injectable } from "@decohere/crucible/decorators";
64
+ import type { Principal } from "@decohere/cortex";
65
+ import { UserEntity } from "../entities/user.entity.js";
66
+ import { UserEntityRepository } from "../repositories/user.repository.js";
67
+
68
+ function claimString(principal: Principal | undefined, key: string): string | undefined {
69
+ \tconst value = principal?.[key];
70
+ \treturn typeof value === "string" && value.length > 0 ? value : undefined;
71
+ }
72
+
73
+ @injectable()
74
+ export class UserService {
75
+ \tpublic constructor(private readonly users: UserEntityRepository) {}
76
+
77
+ \t/**
78
+ \t * Upsert a local profile keyed by JWT \`sub\`.
79
+ \t * Profile fields from the token fill gaps; seeded rows keep displayName/email/roles.
80
+ \t */
81
+ \tpublic async ensureFromPrincipal(principal: Principal | undefined): Promise<UserEntity> {
82
+ \t\tconst sub = principal?.sub;
83
+ \t\tif (!sub) {
84
+ \t\t\tthrow new Error("Authenticated request is missing principal.sub");
85
+ \t\t}
86
+
87
+ \t\tconst email =
88
+ \t\t\tclaimString(principal, "email") ?? claimString(principal, "preferred_username");
89
+ \t\tconst displayFromToken =
90
+ \t\t\tclaimString(principal, "name") ??
91
+ \t\t\tclaimString(principal, "nickname") ??
92
+ \t\t\temail;
93
+
94
+ \t\tconst existing = await this.users.findBySub(sub);
95
+ \t\tif (existing) {
96
+ \t\t\tlet dirty = false;
97
+ \t\t\tif (!existing.email && email) {
98
+ \t\t\t\texisting.email = email;
99
+ \t\t\t\tdirty = true;
100
+ \t\t\t}
101
+ \t\t\tif (existing.displayName === existing.sub && displayFromToken) {
102
+ \t\t\t\texisting.displayName = displayFromToken;
103
+ \t\t\t\tdirty = true;
104
+ \t\t\t}
105
+ \t\t\treturn dirty ? this.users.save(existing) : existing;
106
+ \t\t}
107
+
108
+ \t\tconst created = new UserEntity();
109
+ \t\tcreated.sub = sub;
110
+ \t\tcreated.email = email ?? null;
111
+ \t\tcreated.displayName = displayFromToken ?? sub;
112
+ \t\tcreated.roles = Array.isArray(principal.roles)
113
+ \t\t\t? principal.roles.filter((r): r is string => typeof r === "string")
114
+ \t\t\t: [];
115
+ \t\treturn this.users.save(created);
116
+ \t}
117
+ }
118
+ `;
119
+
120
+ const ABILITY_FACTORY_TS = `import type { Principal } from "@decohere/cortex";
121
+ import {
122
+ \tADMIN_ROLE,
123
+ \tcreatePrincipalFromPayload,
124
+ \tdefineAbilitiesFor as defaultDefineAbilitiesFor,
125
+ } from "@decohere/cortex";
126
+
127
+ /**
128
+ * Customize CASL authorization here (product-specific rules).
129
+ *
130
+ * Default delegates to cortex: \`admin\` role → \`manage\`/\`all\`; IdP scopes
131
+ * (\`action:Subject\`) → abilities. Replace the body when you need app rules
132
+ * beyond that — this file is the intentional extension point, not framework
133
+ * infrastructure to copy.
134
+ */
135
+ export function defineAbilitiesFor(principal: Principal) {
136
+ \treturn defaultDefineAbilitiesFor(principal);
137
+ }
138
+
139
+ /**
140
+ * Customize JWT → Principal mapping here (IdP-specific claims).
141
+ *
142
+ * Elevates \`AUTH0_ADMIN_USER_SUB\` to the admin role so the seeded admin works
143
+ * before/without roles on the access token.
144
+ */
145
+ export function mapPrincipal(payload: Record<string, unknown>): Principal {
146
+ \tconst principal = createPrincipalFromPayload(payload);
147
+ \tconst sub = principal.sub;
148
+ \tif (sub && sub === process.env.AUTH0_ADMIN_USER_SUB) {
149
+ \t\treturn {
150
+ \t\t\t...principal,
151
+ \t\t\troles: [...new Set([ADMIN_ROLE, ...(principal.roles ?? [])])],
152
+ \t\t};
153
+ \t}
154
+ \treturn principal;
155
+ }
156
+ `;
157
+
158
+ async function getOptions(
159
+ workspace: WorkspaceScaffold,
160
+ cliOptions: Partial<AuthOptions>,
161
+ ): Promise<Partial<AuthOptions>> {
162
+ if (cliOptions.authProvider === "auth0" && !cliOptions.issuerBaseUrl) {
163
+ return getAuth0Options(workspace, cliOptions);
164
+ }
165
+ if (cliOptions.authProvider != null) return {};
166
+
167
+ const choices = [{ label: "none" as const }, { label: "auth0" as const }];
168
+ const selected = await pick("\n Auth provider: ", choices, (c) => c.label);
169
+ const provider = selected?.label ?? "none";
170
+
171
+ if (provider !== "auth0") return { authProvider: "none" };
172
+
173
+ return getAuth0Options(workspace, cliOptions);
174
+ }
175
+
176
+ async function getAuth0Options(
177
+ workspace: WorkspaceScaffold,
178
+ cliOptions: Partial<AuthOptions>,
179
+ ): Promise<Partial<AuthOptions>> {
180
+ if (!auth0Cli.isInstalled()) {
181
+ console.error(
182
+ "\n The Auth0 CLI is required for automated setup.\n" +
183
+ " Install: brew install auth0/auth0-cli/auth0\n" +
184
+ " Or visit: https://github.com/auth0/auth0-cli\n",
185
+ );
186
+ process.exit(1);
187
+ }
188
+
189
+ process.stdout.write("\n Checking Auth0 credentials...");
190
+ let tenants = auth0Cli.listTenants();
191
+ if (tenants.length === 0) {
192
+ process.stdout.write(" not logged in.\n");
193
+ console.log(" Logging in to Auth0 (your browser will open)...");
194
+ if (!auth0Cli.login()) {
195
+ console.error("\n Auth0 login failed.");
196
+ process.exit(1);
197
+ }
198
+ tenants = auth0Cli.listTenants();
199
+ }
200
+
201
+ try {
202
+ auth0Cli.listApis();
203
+ process.stdout.write(" ok.\n");
204
+ } catch (err) {
205
+ if (err instanceof auth0Cli.Auth0AuthError) {
206
+ process.stdout.write(" token expired.\n");
207
+ console.log(" Re-authenticating (your browser will open)...");
208
+ if (!auth0Cli.login()) {
209
+ console.error("\n Auth0 login failed.");
210
+ process.exit(1);
211
+ }
212
+ }
213
+ }
214
+
215
+ const active = tenants.find((t) => t.active) ?? tenants[0];
216
+ const issuerBaseUrl = `https://${active.name}`;
217
+ console.log(`\n ✓ Auth0 tenant: ${issuerBaseUrl}`);
218
+
219
+ process.stdout.write("\n Fetching Auth0 APIs...");
220
+ const userApis = auth0Cli.listApis().filter((a) => !a.name.includes("Management API"));
221
+ process.stdout.write(` found ${userApis.length}.\n`);
222
+
223
+ type ApiChoice = { label: string; existing: string | null };
224
+ const projectName = workspace.name;
225
+ const apiChoices: ApiChoice[] = [
226
+ { label: `<new> https://api.${projectName}`, existing: null },
227
+ ...userApis.map((a) => ({ label: `${a.name} — ${a.identifier}`, existing: a.identifier })),
228
+ ];
229
+
230
+ const apiSelected = await pick("\n Select Auth0 API: ", apiChoices, (c) => c.label);
231
+
232
+ let audience: string;
233
+ if (!apiSelected || apiSelected.existing === null) {
234
+ const defaultId = `https://api.${projectName}`;
235
+ audience = await ask(` API Identifier [${defaultId}]: `, defaultId);
236
+ console.log("\n Creating Auth0 API...");
237
+ audience = auth0Cli.createApi(`${projectName} API`, audience).identifier;
238
+ console.log(` \u2713 API created: ${audience}`);
239
+ } else {
240
+ audience = apiSelected.existing;
241
+ console.log(` \u2713 Selected: ${audience}`);
242
+ try {
243
+ auth0Cli.updateApiScopes(audience);
244
+ console.log(` \u2713 API scopes ensured (${auth0Cli.HELLO_API_SCOPES.join(", ")})`);
245
+ } catch {
246
+ console.error(
247
+ " ✗ Failed to update API scopes — add Hello scopes in the Auth0 dashboard.",
248
+ );
249
+ }
250
+ }
251
+
252
+ const withLocalUsers = cliOptions.databaseProvider === "postgres/typeorm";
253
+ let adminUsername: string | undefined;
254
+ let adminPassword: string | undefined;
255
+ if (withLocalUsers) {
256
+ adminUsername =
257
+ cliOptions.adminUsername ?? (await ask("\n Admin username [admin]: ", "admin"));
258
+ adminPassword = cliOptions.adminPassword;
259
+ if (adminPassword == null) {
260
+ adminPassword = await ask(" Admin password (leave blank to generate): ");
261
+ if (!adminPassword) {
262
+ adminPassword = auth0Cli.generateAdminPassword();
263
+ console.log(` ✓ Generated admin password (written to .env): ${adminPassword}`);
264
+ }
265
+ }
266
+ }
267
+
268
+ return {
269
+ authProvider: "auth0",
270
+ issuerBaseUrl,
271
+ audience,
272
+ ...(withLocalUsers ? { adminUsername, adminPassword } : {}),
273
+ };
274
+ }
275
+
276
+ function adminEmail(workspaceName: string, username: string): string {
277
+ if (username.includes("@")) return username;
278
+ const slug = workspaceName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
279
+ return `${username}@${slug}.example.com`;
280
+ }
281
+
282
+ function createAuth0AdminUser(
283
+ workspace: WorkspaceScaffold,
284
+ options: AuthOptions,
285
+ authConfig: AuthConfigModel,
286
+ ): void {
287
+ const username = options.adminUsername ?? "admin";
288
+ const password = options.adminPassword;
289
+ if (!password) return;
290
+
291
+ const email = adminEmail(workspace.name, username);
292
+ try {
293
+ const admin = auth0Cli.createAdminDatabaseUser({ email, password, username });
294
+ authConfig.setAdminUser({
295
+ sub: admin.user_id,
296
+ email: admin.email,
297
+ password: admin.password,
298
+ username: admin.username,
299
+ });
300
+ workspace
301
+ .getModel<EnvFile>(WELL_KNOWN_FILES.ENV_FILE)
302
+ ?.addEntry("AUTH0_ADMIN_USER_SUB", admin.user_id)
303
+ .addEntry("AUTH0_ADMIN_USER_EMAIL", admin.email)
304
+ .addEntry("AUTH0_ADMIN_USER_PASSWORD", admin.password)
305
+ .addEntry("AUTH0_ADMIN_USERNAME", admin.username);
306
+ console.log(` ✓ Auth0 admin user ready: ${admin.email} (role: ${ADMIN_ROLE})`);
307
+ console.log(` Login with EMAIL (not username): ${admin.email}`);
308
+ console.log(` Password: see AUTH0_ADMIN_USER_PASSWORD in .env`);
309
+ } catch (err) {
310
+ console.error(
311
+ ` ✗ Failed to ensure Auth0 admin user (${email}): ${err instanceof Error ? err.message : err}`,
312
+ );
313
+ }
314
+ }
315
+
316
+ function scaffoldLocalUserProfile(workspace: WorkspaceScaffold): void {
317
+ for (const project of workspace.getProjects()) {
318
+ project
319
+ .getModel<MainSourceFile>(WELL_KNOWN_FILES.MAIN_TS)
320
+ ?.addEntity({ className: "UserEntity", importPath: "./entities/user.entity.js" })
321
+ .addRepository({
322
+ className: "UserEntityRepository",
323
+ importPath: "./repositories/user.repository.js",
324
+ })
325
+ .addService({
326
+ className: "UserService",
327
+ importPath: "./services/user.service.js",
328
+ scope: "request",
329
+ })
330
+ .addImport("defineAbilitiesFor", "./auth/ability.factory.js")
331
+ .addImport("mapPrincipal", "./auth/ability.factory.js")
332
+ .setTypeOrmSeed(
333
+ [
334
+ "const sub = process.env.AUTH0_ADMIN_USER_SUB;",
335
+ "if (!sub) return;",
336
+ "const repo = dataSource.getRepository(UserEntity);",
337
+ "const existing = await repo.findOne({ where: { sub } });",
338
+ "if (existing) return;",
339
+ "await repo.save(repo.create({",
340
+ " sub,",
341
+ " email: process.env.AUTH0_ADMIN_USER_EMAIL ?? null,",
342
+ ' displayName: process.env.AUTH0_ADMIN_USERNAME ?? "admin",',
343
+ ' roles: ["admin"],',
344
+ "}));",
345
+ ].join(" "),
346
+ );
347
+
348
+ project.addFile({
349
+ files: () => [
350
+ { filePath: "src/entities/user.entity.ts", content: USER_ENTITY_TS },
351
+ { filePath: "src/repositories/user.repository.ts", content: USER_REPOSITORY_TS },
352
+ { filePath: "src/services/user.service.ts", content: USER_SERVICE_TS },
353
+ { filePath: "src/auth/ability.factory.ts", content: ABILITY_FACTORY_TS },
354
+ ],
355
+ });
356
+ }
357
+
358
+ for (const controller of workspace.getModels(ControllerSourceFile)) {
359
+ controller
360
+ .addImport("UserService", "../services/user.service.js")
361
+ .addImport("inject", "@decohere/crucible/decorators")
362
+ .addConstructorParam('@inject("UserService") private readonly users: UserService');
363
+ controller.getMethod("getOne").setAsync(true).setBody([
364
+ "const user = await this.users.ensureFromPrincipal(this.principalProvider.currentPrincipal);",
365
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: template literal in generated code
366
+ "return { id, message: `Hello ${user.displayName}!`, username: user.displayName };",
367
+ ]);
368
+ }
369
+ }
370
+
371
+ function scaffold(workspace: WorkspaceScaffold, options: AuthOptions): void {
372
+ if (options.authProvider !== "auth0") return;
373
+
374
+ const resource = "Hello";
375
+ const withLocalUsers = options.databaseProvider === "postgres/typeorm";
376
+
377
+ for (const controller of workspace.getModels(ControllerSourceFile)) {
378
+ controller
379
+ .addDecorator(`@Authorize("read", "${resource}")`)
380
+ .addCortexImport("AllowAnonymous")
381
+ .addCortexImport("Authorize")
382
+ .addCortexImport("IPrincipalProviderToken")
383
+ .addCortexImport("IPrincipalProvider", true)
384
+ .addImport("inject", "@decohere/crucible/decorators")
385
+ .addConstructorParam(
386
+ `@inject(IPrincipalProviderToken) private readonly principalProvider: IPrincipalProvider`,
387
+ );
388
+ controller.getMethod("getAll").addDecorator("@AllowAnonymous()");
389
+ if (!withLocalUsers) {
390
+ controller.getMethod("getOne").setBody([
391
+ "const principal = this.principalProvider.currentPrincipal;",
392
+ "const username =",
393
+ '\t(typeof principal?.name === "string" && principal.name) ||',
394
+ '\t(typeof principal?.nickname === "string" && principal.nickname) ||',
395
+ '\t(typeof principal?.email === "string" && principal.email) ||',
396
+ '\t(typeof principal?.preferred_username === "string" && principal.preferred_username) ||',
397
+ "\tprincipal?.sub ||",
398
+ '\t"unknown";',
399
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: template literal in generated code
400
+ "return { id, message: `Hello ${username}!`, username };",
401
+ ]);
402
+ }
403
+ controller.getMethod("create").addDecorator(`@Authorize("create", "${resource}")`);
404
+ controller.getMethod("update").addDecorator(`@Authorize("update", "${resource}")`);
405
+ controller.getMethod("remove").addDecorator(`@Authorize("delete", "${resource}")`);
406
+ }
407
+
408
+ for (const project of workspace.getProjects()) {
409
+ project
410
+ .getModel<MainSourceFile>(WELL_KNOWN_FILES.MAIN_TS)
411
+ ?.addSideEffectImport("@decohere/cortex/auth");
412
+ }
413
+
414
+ if (withLocalUsers) {
415
+ scaffoldLocalUserProfile(workspace);
416
+ for (const project of workspace.getProjects()) {
417
+ project
418
+ .getModel<MainSourceFile>(WELL_KNOWN_FILES.MAIN_TS)
419
+ ?.addBuilderCall(".withAuth(defineAbilitiesFor, { principalMapper: mapPrincipal })");
420
+ }
421
+ } else {
422
+ for (const project of workspace.getProjects()) {
423
+ project
424
+ .getModel<MainSourceFile>(WELL_KNOWN_FILES.MAIN_TS)
425
+ ?.addBuilderCall(".withAuth()");
426
+ }
427
+ }
428
+
429
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: intentional YAML env-var placeholders
430
+ const issuerPlaceholder = "${AUTH0_ISSUER_BASE_URL}";
431
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: intentional YAML env-var placeholders
432
+ const audPlaceholder = "${AUTH0_AUDIENCE}";
433
+
434
+ const configSection = `auth:\n discoveryUrl: ${issuerPlaceholder}\n audience: ${audPlaceholder}\n`;
435
+
436
+ workspace.getModel<ConfigYaml>(WELL_KNOWN_FILES.CONFIG_YAML)?.addSection(configSection);
437
+
438
+ workspace
439
+ .getModel<EnvFile>(WELL_KNOWN_FILES.ENV_FILE)
440
+ ?.addEntry("AUTH0_ISSUER_BASE_URL", options.issuerBaseUrl ?? "")
441
+ .addEntry("AUTH0_AUDIENCE", options.audience ?? "");
442
+
443
+ const authConfig = new AuthConfigModel({
444
+ issuerBaseUrl: options.issuerBaseUrl ?? "",
445
+ audience: options.audience ?? "",
446
+ });
447
+ workspace.addModel(authConfig);
448
+
449
+ if (withLocalUsers && options.adminPassword) {
450
+ createAuth0AdminUser(workspace, options, authConfig);
451
+ }
452
+ }
453
+
454
+ export const AuthFeature: Feature<AuthOptions> = { getOptions, scaffold };