iec-builder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (337) hide show
  1. package/.claude/settings.local.json +111 -0
  2. package/.iec.yaml +5 -0
  3. package/CLAUDE.md +174 -0
  4. package/Dockerfile +34 -0
  5. package/README.md +84 -0
  6. package/catalog-info.yaml +11 -0
  7. package/dist/config/env.d.ts +219 -0
  8. package/dist/config/env.d.ts.map +1 -0
  9. package/dist/config/env.js +89 -0
  10. package/dist/config/env.js.map +1 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +148 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/middleware/auth.d.ts +43 -0
  16. package/dist/middleware/auth.d.ts.map +1 -0
  17. package/dist/middleware/auth.js +217 -0
  18. package/dist/middleware/auth.js.map +1 -0
  19. package/dist/middleware/org-access.d.ts +28 -0
  20. package/dist/middleware/org-access.d.ts.map +1 -0
  21. package/dist/middleware/org-access.js +102 -0
  22. package/dist/middleware/org-access.js.map +1 -0
  23. package/dist/models/types.d.ts +254 -0
  24. package/dist/models/types.d.ts.map +1 -0
  25. package/dist/models/types.js +2 -0
  26. package/dist/models/types.js.map +1 -0
  27. package/dist/routes/ai.d.ts +2 -0
  28. package/dist/routes/ai.d.ts.map +1 -0
  29. package/dist/routes/ai.js +77 -0
  30. package/dist/routes/ai.js.map +1 -0
  31. package/dist/routes/audit.d.ts +2 -0
  32. package/dist/routes/audit.d.ts.map +1 -0
  33. package/dist/routes/audit.js +102 -0
  34. package/dist/routes/audit.js.map +1 -0
  35. package/dist/routes/builds.d.ts +2 -0
  36. package/dist/routes/builds.d.ts.map +1 -0
  37. package/dist/routes/builds.js +262 -0
  38. package/dist/routes/builds.js.map +1 -0
  39. package/dist/routes/cluster.d.ts +2 -0
  40. package/dist/routes/cluster.d.ts.map +1 -0
  41. package/dist/routes/cluster.js +181 -0
  42. package/dist/routes/cluster.js.map +1 -0
  43. package/dist/routes/config.d.ts +2 -0
  44. package/dist/routes/config.d.ts.map +1 -0
  45. package/dist/routes/config.js +291 -0
  46. package/dist/routes/config.js.map +1 -0
  47. package/dist/routes/databases.d.ts +2 -0
  48. package/dist/routes/databases.d.ts.map +1 -0
  49. package/dist/routes/databases.js +161 -0
  50. package/dist/routes/databases.js.map +1 -0
  51. package/dist/routes/db-whitelist.d.ts +2 -0
  52. package/dist/routes/db-whitelist.d.ts.map +1 -0
  53. package/dist/routes/db-whitelist.js +148 -0
  54. package/dist/routes/db-whitelist.js.map +1 -0
  55. package/dist/routes/domains.d.ts +2 -0
  56. package/dist/routes/domains.d.ts.map +1 -0
  57. package/dist/routes/domains.js +449 -0
  58. package/dist/routes/domains.js.map +1 -0
  59. package/dist/routes/oauth.d.ts +2 -0
  60. package/dist/routes/oauth.d.ts.map +1 -0
  61. package/dist/routes/oauth.js +180 -0
  62. package/dist/routes/oauth.js.map +1 -0
  63. package/dist/routes/observability.d.ts +2 -0
  64. package/dist/routes/observability.d.ts.map +1 -0
  65. package/dist/routes/observability.js +167 -0
  66. package/dist/routes/observability.js.map +1 -0
  67. package/dist/routes/orgs.d.ts +2 -0
  68. package/dist/routes/orgs.d.ts.map +1 -0
  69. package/dist/routes/orgs.js +270 -0
  70. package/dist/routes/orgs.js.map +1 -0
  71. package/dist/routes/platform.d.ts +2 -0
  72. package/dist/routes/platform.d.ts.map +1 -0
  73. package/dist/routes/platform.js +107 -0
  74. package/dist/routes/platform.js.map +1 -0
  75. package/dist/routes/push.d.ts +2 -0
  76. package/dist/routes/push.d.ts.map +1 -0
  77. package/dist/routes/push.js +233 -0
  78. package/dist/routes/push.js.map +1 -0
  79. package/dist/routes/rotation.d.ts +3 -0
  80. package/dist/routes/rotation.d.ts.map +1 -0
  81. package/dist/routes/rotation.js +154 -0
  82. package/dist/routes/rotation.js.map +1 -0
  83. package/dist/routes/services.d.ts +2 -0
  84. package/dist/routes/services.d.ts.map +1 -0
  85. package/dist/routes/services.js +246 -0
  86. package/dist/routes/services.js.map +1 -0
  87. package/dist/routes/storage.d.ts +2 -0
  88. package/dist/routes/storage.d.ts.map +1 -0
  89. package/dist/routes/storage.js +118 -0
  90. package/dist/routes/storage.js.map +1 -0
  91. package/dist/routes/users.d.ts +2 -0
  92. package/dist/routes/users.d.ts.map +1 -0
  93. package/dist/routes/users.js +183 -0
  94. package/dist/routes/users.js.map +1 -0
  95. package/dist/routes/versions.d.ts +2 -0
  96. package/dist/routes/versions.d.ts.map +1 -0
  97. package/dist/routes/versions.js +195 -0
  98. package/dist/routes/versions.js.map +1 -0
  99. package/dist/routes/webhooks.d.ts +2 -0
  100. package/dist/routes/webhooks.d.ts.map +1 -0
  101. package/dist/routes/webhooks.js +334 -0
  102. package/dist/routes/webhooks.js.map +1 -0
  103. package/dist/services/__tests__/deploy-pipeline.integration.test.d.ts +2 -0
  104. package/dist/services/__tests__/deploy-pipeline.integration.test.d.ts.map +1 -0
  105. package/dist/services/__tests__/deploy-pipeline.integration.test.js +482 -0
  106. package/dist/services/__tests__/deploy-pipeline.integration.test.js.map +1 -0
  107. package/dist/services/bio-client.d.ts +68 -0
  108. package/dist/services/bio-client.d.ts.map +1 -0
  109. package/dist/services/bio-client.js +110 -0
  110. package/dist/services/bio-client.js.map +1 -0
  111. package/dist/services/build-queue.d.ts +7 -0
  112. package/dist/services/build-queue.d.ts.map +1 -0
  113. package/dist/services/build-queue.js +114 -0
  114. package/dist/services/build-queue.js.map +1 -0
  115. package/dist/services/builder.d.ts +7 -0
  116. package/dist/services/builder.d.ts.map +1 -0
  117. package/dist/services/builder.js +1384 -0
  118. package/dist/services/builder.js.map +1 -0
  119. package/dist/services/catalog.d.ts +177 -0
  120. package/dist/services/catalog.d.ts.map +1 -0
  121. package/dist/services/catalog.js +805 -0
  122. package/dist/services/catalog.js.map +1 -0
  123. package/dist/services/catalog.test.d.ts +2 -0
  124. package/dist/services/catalog.test.d.ts.map +1 -0
  125. package/dist/services/catalog.test.js +467 -0
  126. package/dist/services/catalog.test.js.map +1 -0
  127. package/dist/services/cloudflare.d.ts +43 -0
  128. package/dist/services/cloudflare.d.ts.map +1 -0
  129. package/dist/services/cloudflare.js +182 -0
  130. package/dist/services/cloudflare.js.map +1 -0
  131. package/dist/services/config-validator.d.ts +28 -0
  132. package/dist/services/config-validator.d.ts.map +1 -0
  133. package/dist/services/config-validator.js +68 -0
  134. package/dist/services/config-validator.js.map +1 -0
  135. package/dist/services/config-validator.test.d.ts +2 -0
  136. package/dist/services/config-validator.test.d.ts.map +1 -0
  137. package/dist/services/config-validator.test.js +151 -0
  138. package/dist/services/config-validator.test.js.map +1 -0
  139. package/dist/services/crypto.d.ts +19 -0
  140. package/dist/services/crypto.d.ts.map +1 -0
  141. package/dist/services/crypto.js +63 -0
  142. package/dist/services/crypto.js.map +1 -0
  143. package/dist/services/database.d.ts +26 -0
  144. package/dist/services/database.d.ts.map +1 -0
  145. package/dist/services/database.js +100 -0
  146. package/dist/services/database.js.map +1 -0
  147. package/dist/services/db-credential-manager.d.ts +73 -0
  148. package/dist/services/db-credential-manager.d.ts.map +1 -0
  149. package/dist/services/db-credential-manager.js +342 -0
  150. package/dist/services/db-credential-manager.js.map +1 -0
  151. package/dist/services/db-provisioner.d.ts +57 -0
  152. package/dist/services/db-provisioner.d.ts.map +1 -0
  153. package/dist/services/db-provisioner.js +400 -0
  154. package/dist/services/db-provisioner.js.map +1 -0
  155. package/dist/services/db-provisioner.test.d.ts +2 -0
  156. package/dist/services/db-provisioner.test.d.ts.map +1 -0
  157. package/dist/services/db-provisioner.test.js +141 -0
  158. package/dist/services/db-provisioner.test.js.map +1 -0
  159. package/dist/services/db-whitelist.d.ts +58 -0
  160. package/dist/services/db-whitelist.d.ts.map +1 -0
  161. package/dist/services/db-whitelist.js +379 -0
  162. package/dist/services/db-whitelist.js.map +1 -0
  163. package/dist/services/dependency-resolver.d.ts +58 -0
  164. package/dist/services/dependency-resolver.d.ts.map +1 -0
  165. package/dist/services/dependency-resolver.js +180 -0
  166. package/dist/services/dependency-resolver.js.map +1 -0
  167. package/dist/services/dependency-resolver.test.d.ts +2 -0
  168. package/dist/services/dependency-resolver.test.d.ts.map +1 -0
  169. package/dist/services/dependency-resolver.test.js +195 -0
  170. package/dist/services/dependency-resolver.test.js.map +1 -0
  171. package/dist/services/deploy-gate.d.ts +19 -0
  172. package/dist/services/deploy-gate.d.ts.map +1 -0
  173. package/dist/services/deploy-gate.js +56 -0
  174. package/dist/services/deploy-gate.js.map +1 -0
  175. package/dist/services/deploy-gate.test.d.ts +2 -0
  176. package/dist/services/deploy-gate.test.d.ts.map +1 -0
  177. package/dist/services/deploy-gate.test.js +199 -0
  178. package/dist/services/deploy-gate.test.js.map +1 -0
  179. package/dist/services/dockerfile-generator.d.ts +31 -0
  180. package/dist/services/dockerfile-generator.d.ts.map +1 -0
  181. package/dist/services/dockerfile-generator.js +544 -0
  182. package/dist/services/dockerfile-generator.js.map +1 -0
  183. package/dist/services/dockerfile-generator.test.d.ts +2 -0
  184. package/dist/services/dockerfile-generator.test.d.ts.map +1 -0
  185. package/dist/services/dockerfile-generator.test.js +144 -0
  186. package/dist/services/dockerfile-generator.test.js.map +1 -0
  187. package/dist/services/forgejo.d.ts +58 -0
  188. package/dist/services/forgejo.d.ts.map +1 -0
  189. package/dist/services/forgejo.js +131 -0
  190. package/dist/services/forgejo.js.map +1 -0
  191. package/dist/services/koko.d.ts +153 -0
  192. package/dist/services/koko.d.ts.map +1 -0
  193. package/dist/services/koko.js +260 -0
  194. package/dist/services/koko.js.map +1 -0
  195. package/dist/services/kubernetes.d.ts +16 -0
  196. package/dist/services/kubernetes.d.ts.map +1 -0
  197. package/dist/services/kubernetes.js +102 -0
  198. package/dist/services/kubernetes.js.map +1 -0
  199. package/dist/services/oauth-provisioner.d.ts +30 -0
  200. package/dist/services/oauth-provisioner.d.ts.map +1 -0
  201. package/dist/services/oauth-provisioner.js +182 -0
  202. package/dist/services/oauth-provisioner.js.map +1 -0
  203. package/dist/services/oauth-provisioner.test.d.ts +2 -0
  204. package/dist/services/oauth-provisioner.test.d.ts.map +1 -0
  205. package/dist/services/oauth-provisioner.test.js +349 -0
  206. package/dist/services/oauth-provisioner.test.js.map +1 -0
  207. package/dist/services/pod-diagnostics.d.ts +11 -0
  208. package/dist/services/pod-diagnostics.d.ts.map +1 -0
  209. package/dist/services/pod-diagnostics.js +201 -0
  210. package/dist/services/pod-diagnostics.js.map +1 -0
  211. package/dist/services/rotation-scheduler.d.ts +2 -0
  212. package/dist/services/rotation-scheduler.d.ts.map +1 -0
  213. package/dist/services/rotation-scheduler.js +215 -0
  214. package/dist/services/rotation-scheduler.js.map +1 -0
  215. package/dist/services/storage-credential-manager.d.ts +43 -0
  216. package/dist/services/storage-credential-manager.d.ts.map +1 -0
  217. package/dist/services/storage-credential-manager.js +159 -0
  218. package/dist/services/storage-credential-manager.js.map +1 -0
  219. package/dist/services/storage-provisioner.d.ts +32 -0
  220. package/dist/services/storage-provisioner.d.ts.map +1 -0
  221. package/dist/services/storage-provisioner.js +136 -0
  222. package/dist/services/storage-provisioner.js.map +1 -0
  223. package/dist/services/storage.d.ts +65 -0
  224. package/dist/services/storage.d.ts.map +1 -0
  225. package/dist/services/storage.js +204 -0
  226. package/dist/services/storage.js.map +1 -0
  227. package/dist/services/troubleshooter.d.ts +22 -0
  228. package/dist/services/troubleshooter.d.ts.map +1 -0
  229. package/dist/services/troubleshooter.js +168 -0
  230. package/dist/services/troubleshooter.js.map +1 -0
  231. package/dist/services/vault-client.d.ts +114 -0
  232. package/dist/services/vault-client.d.ts.map +1 -0
  233. package/dist/services/vault-client.js +411 -0
  234. package/dist/services/vault-client.js.map +1 -0
  235. package/dist/utils/logger.d.ts +2 -0
  236. package/dist/utils/logger.d.ts.map +1 -0
  237. package/dist/utils/logger.js +6 -0
  238. package/dist/utils/logger.js.map +1 -0
  239. package/dist/utils/response.d.ts +13 -0
  240. package/dist/utils/response.d.ts.map +1 -0
  241. package/dist/utils/response.js +12 -0
  242. package/dist/utils/response.js.map +1 -0
  243. package/docs/registry-migration.md +301 -0
  244. package/docs/registry-quickstart.md +169 -0
  245. package/ecosystem.config.cjs +14 -0
  246. package/findings.md +168 -0
  247. package/helm/default-service/Chart.yaml +6 -0
  248. package/helm/default-service/templates/deployment.yaml +97 -0
  249. package/helm/default-service/templates/ingress.yaml +43 -0
  250. package/helm/default-service/templates/service.yaml +17 -0
  251. package/helm/default-service/values.yaml +82 -0
  252. package/helm/services/iec-builder/Chart.yaml +6 -0
  253. package/helm/services/iec-builder/templates/_helpers.tpl +61 -0
  254. package/helm/services/iec-builder/templates/deployment.yaml +73 -0
  255. package/helm/services/iec-builder/templates/service.yaml +15 -0
  256. package/helm/services/iec-builder/templates/serviceaccount.yaml +12 -0
  257. package/helm/services/iec-builder/values.yaml +56 -0
  258. package/helm/vault-values.yaml +127 -0
  259. package/package.json +45 -0
  260. package/progress.md +156 -0
  261. package/scripts/.vault-init-keys.json +23 -0
  262. package/scripts/backfill-ownership.ts +113 -0
  263. package/scripts/finalize-mongo-auth.sh +212 -0
  264. package/scripts/setup-ipset.sh +107 -0
  265. package/scripts/setup-mongo-auth.sh +163 -0
  266. package/scripts/setup-neo4j-auth.sh +62 -0
  267. package/scripts/setup-redis-auth.sh +55 -0
  268. package/scripts/setup-registry-secret.sh +71 -0
  269. package/scripts/setup-vault.sh +308 -0
  270. package/src/config/env.ts +117 -0
  271. package/src/index.ts +153 -0
  272. package/src/middleware/auth.ts +294 -0
  273. package/src/middleware/org-access.ts +126 -0
  274. package/src/models/types.ts +288 -0
  275. package/src/routes/ai.ts +115 -0
  276. package/src/routes/audit.ts +121 -0
  277. package/src/routes/builds.ts +320 -0
  278. package/src/routes/cluster.ts +235 -0
  279. package/src/routes/config.ts +369 -0
  280. package/src/routes/databases.ts +201 -0
  281. package/src/routes/db-whitelist.ts +204 -0
  282. package/src/routes/domains.ts +547 -0
  283. package/src/routes/oauth.ts +195 -0
  284. package/src/routes/observability.ts +205 -0
  285. package/src/routes/orgs.ts +330 -0
  286. package/src/routes/platform.ts +134 -0
  287. package/src/routes/rotation.ts +191 -0
  288. package/src/routes/services.ts +290 -0
  289. package/src/routes/storage.ts +153 -0
  290. package/src/routes/users.ts +235 -0
  291. package/src/routes/webhooks.ts +384 -0
  292. package/src/services/__tests__/catalog-storage.test.ts +186 -0
  293. package/src/services/__tests__/deploy-pipeline.integration.test.ts +624 -0
  294. package/src/services/__tests__/pod-diagnostics.test.ts +332 -0
  295. package/src/services/__tests__/storage-credential-manager.test.ts +129 -0
  296. package/src/services/__tests__/storage-provisioner.test.ts +166 -0
  297. package/src/services/__tests__/troubleshooter.test.ts +329 -0
  298. package/src/services/bio-client.ts +189 -0
  299. package/src/services/build-queue.ts +137 -0
  300. package/src/services/builder.ts +1800 -0
  301. package/src/services/catalog.test.ts +1389 -0
  302. package/src/services/catalog.ts +1187 -0
  303. package/src/services/cloudflare.ts +259 -0
  304. package/src/services/config-validator.test.ts +190 -0
  305. package/src/services/config-validator.ts +108 -0
  306. package/src/services/crypto.ts +78 -0
  307. package/src/services/database.ts +122 -0
  308. package/src/services/db-credential-manager.test.ts +101 -0
  309. package/src/services/db-credential-manager.ts +447 -0
  310. package/src/services/db-provisioner.test.ts +602 -0
  311. package/src/services/db-provisioner.ts +589 -0
  312. package/src/services/db-whitelist.test.ts +671 -0
  313. package/src/services/db-whitelist.ts +496 -0
  314. package/src/services/dependency-resolver.test.ts +677 -0
  315. package/src/services/dependency-resolver.ts +319 -0
  316. package/src/services/deploy-gate.test.ts +247 -0
  317. package/src/services/deploy-gate.ts +75 -0
  318. package/src/services/dockerfile-generator.test.ts +401 -0
  319. package/src/services/dockerfile-generator.ts +606 -0
  320. package/src/services/forgejo.ts +212 -0
  321. package/src/services/koko.ts +492 -0
  322. package/src/services/kubernetes.ts +141 -0
  323. package/src/services/oauth-provisioner.test.ts +477 -0
  324. package/src/services/oauth-provisioner.ts +286 -0
  325. package/src/services/pod-diagnostics.ts +261 -0
  326. package/src/services/rotation-scheduler.ts +293 -0
  327. package/src/services/storage-credential-manager.ts +223 -0
  328. package/src/services/storage-provisioner.ts +216 -0
  329. package/src/services/storage.ts +274 -0
  330. package/src/services/troubleshooter.ts +208 -0
  331. package/src/services/vault-client.test.ts +272 -0
  332. package/src/services/vault-client.ts +587 -0
  333. package/src/utils/logger.ts +6 -0
  334. package/src/utils/response.ts +23 -0
  335. package/task_plan.md +171 -0
  336. package/tsconfig.json +20 -0
  337. package/vitest.config.ts +19 -0
@@ -0,0 +1,587 @@
1
+ import { execSync } from 'child_process'
2
+ import { env } from '../config/env.js'
3
+ import { logger } from '../utils/logger.js'
4
+
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // Types
7
+ // ─────────────────────────────────────────────────────────────────────────────
8
+
9
+ export interface VaultDatabaseRoleConfig {
10
+ readonly serviceName: string
11
+ readonly environment: string
12
+ readonly databaseName: string
13
+ readonly role: 'readWrite' | 'read'
14
+ readonly dbType: 'mongodb' | 'redis' | 'neo4j'
15
+ }
16
+
17
+ export interface VaultAnnotations {
18
+ readonly [key: string]: string
19
+ }
20
+
21
+ interface VaultHealthResponse {
22
+ readonly initialized: boolean
23
+ readonly sealed: boolean
24
+ readonly version: string
25
+ }
26
+
27
+ // ─────────────────────────────────────────────────────────────────────────────
28
+ // Health & Feature Flag
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+
31
+ /**
32
+ * Check if Vault integration is enabled via feature flag.
33
+ */
34
+ export function isVaultEnabled(): boolean {
35
+ return env.VAULT_ENABLED === 'true' && env.VAULT_ADDR !== ''
36
+ }
37
+
38
+ /**
39
+ * Check if Vault is healthy (initialized, unsealed, reachable).
40
+ * Returns false on any error — builder falls back to Phase 1 static credentials.
41
+ */
42
+ export async function isVaultHealthy(): Promise<boolean> {
43
+ if (!isVaultEnabled()) {
44
+ return false
45
+ }
46
+
47
+ try {
48
+ const response = await fetch(`${env.VAULT_ADDR}/v1/sys/health`, {
49
+ signal: AbortSignal.timeout(5000),
50
+ })
51
+
52
+ if (!response.ok) {
53
+ // 429 = standby, 472 = DR secondary, 473 = performance standby — all "healthy enough"
54
+ // 501 = not initialized, 503 = sealed — not healthy
55
+ const unhealthy = [501, 503]
56
+ if (unhealthy.includes(response.status)) {
57
+ logger.warn({ status: response.status }, 'Vault is not healthy')
58
+ return false
59
+ }
60
+ }
61
+
62
+ const data = await response.json() as VaultHealthResponse
63
+ return data.initialized && !data.sealed
64
+ } catch (error) {
65
+ const message = error instanceof Error ? error.message : 'Unknown error'
66
+ logger.warn({ error: message }, 'Vault health check failed — falling back to static credentials')
67
+ return false
68
+ }
69
+ }
70
+
71
+ // ─────────────────────────────────────────────────────────────────────────────
72
+ // Vault API helpers
73
+ // ─────────────────────────────────────────────────────────────────────────────
74
+
75
+ async function vaultRequest(
76
+ method: string,
77
+ path: string,
78
+ body?: Record<string, unknown>
79
+ ): Promise<Response> {
80
+ const url = `${env.VAULT_ADDR}/v1/${path}`
81
+ const headers: Record<string, string> = {
82
+ 'X-Vault-Token': env.VAULT_TOKEN,
83
+ }
84
+
85
+ if (body) {
86
+ headers['Content-Type'] = 'application/json'
87
+ }
88
+
89
+ const response = await fetch(url, {
90
+ method,
91
+ headers,
92
+ body: body ? JSON.stringify(body) : undefined,
93
+ signal: AbortSignal.timeout(10000),
94
+ })
95
+
96
+ return response
97
+ }
98
+
99
+ async function vaultWrite(path: string, data: Record<string, unknown>): Promise<void> {
100
+ const response = await vaultRequest('POST', path, data)
101
+
102
+ if (!response.ok && response.status !== 204) {
103
+ const errorBody = await response.text()
104
+ throw new Error(`Vault write to ${path} failed (${response.status}): ${errorBody}`)
105
+ }
106
+ }
107
+
108
+ async function vaultDelete(path: string): Promise<void> {
109
+ const response = await vaultRequest('DELETE', path)
110
+
111
+ if (!response.ok && response.status !== 204 && response.status !== 404) {
112
+ const errorBody = await response.text()
113
+ throw new Error(`Vault delete at ${path} failed (${response.status}): ${errorBody}`)
114
+ }
115
+ }
116
+
117
+ async function vaultRead(path: string): Promise<Record<string, unknown> | null> {
118
+ const response = await vaultRequest('GET', path)
119
+
120
+ if (response.status === 404) {
121
+ return null
122
+ }
123
+
124
+ if (!response.ok) {
125
+ const errorBody = await response.text()
126
+ throw new Error(`Vault read from ${path} failed (${response.status}): ${errorBody}`)
127
+ }
128
+
129
+ const json = await response.json() as { data: Record<string, unknown> }
130
+ return json.data
131
+ }
132
+
133
+ // ─────────────────────────────────────────────────────────────────────────────
134
+ // Vault Role Names
135
+ // ─────────────────────────────────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Build the Vault database role name for a service's own database.
139
+ */
140
+ export function buildVaultRoleName(serviceName: string, environment: string, dbType: string): string {
141
+ return `svc-${serviceName}-${environment}-${dbType}`
142
+ }
143
+
144
+ /**
145
+ * Build the Vault database role name for a shared/consumed database.
146
+ */
147
+ export function buildSharedVaultRoleName(
148
+ consumerName: string,
149
+ ownerName: string,
150
+ environment: string,
151
+ dbType: string
152
+ ): string {
153
+ return `svc-${consumerName}-on-${ownerName}-${environment}-${dbType}`
154
+ }
155
+
156
+ /**
157
+ * Build the Vault K8s auth role name for a service.
158
+ */
159
+ function buildKubeAuthRoleName(serviceName: string, environment: string): string {
160
+ return `svc-${serviceName}-${environment}`
161
+ }
162
+
163
+ /**
164
+ * Build the Vault policy name for a service.
165
+ */
166
+ function buildPolicyName(serviceName: string, environment: string): string {
167
+ return `svc-${serviceName}-${environment}`
168
+ }
169
+
170
+ // ─────────────────────────────────────────────────────────────────────────────
171
+ // Database Secrets Engine — Role Management
172
+ // ─────────────────────────────────────────────────────────────────────────────
173
+
174
+ /**
175
+ * Ensure a MongoDB database connection is configured in Vault's database engine.
176
+ * Idempotent — updates if exists.
177
+ */
178
+ export async function ensureMongoDbConnection(connectionName: string, mongoUri: string): Promise<void> {
179
+ logger.info({ connectionName }, 'Configuring Vault MongoDB connection')
180
+
181
+ await vaultWrite(`database/config/${connectionName}`, {
182
+ plugin_name: 'mongodb-database-plugin',
183
+ allowed_roles: 'svc-*',
184
+ connection_url: mongoUri,
185
+ })
186
+ }
187
+
188
+ /**
189
+ * Ensure a Vault database role exists for a service's MongoDB database.
190
+ * Idempotent — updates if exists.
191
+ */
192
+ export async function ensureVaultMongoRole(config: VaultDatabaseRoleConfig): Promise<void> {
193
+ const roleName = buildVaultRoleName(config.serviceName, config.environment, 'mongodb')
194
+ const connectionName = 'mongodb-tawa' // Single MongoDB instance — matches database/config/mongodb-tawa
195
+
196
+ logger.info(
197
+ { roleName, databaseName: config.databaseName, role: config.role },
198
+ 'Ensuring Vault MongoDB role'
199
+ )
200
+
201
+ const creationStatements = JSON.stringify({
202
+ db: config.databaseName,
203
+ roles: [{ role: config.role, db: config.databaseName }],
204
+ })
205
+
206
+ await vaultWrite(`database/roles/${roleName}`, {
207
+ db_name: connectionName,
208
+ creation_statements: [creationStatements],
209
+ default_ttl: '1h',
210
+ max_ttl: '720h',
211
+ })
212
+ }
213
+
214
+ /**
215
+ * Ensure a Vault database role exists for a service's Redis ACL.
216
+ * Idempotent — updates if exists.
217
+ */
218
+ export async function ensureVaultRedisRole(config: VaultDatabaseRoleConfig): Promise<void> {
219
+ const roleName = buildVaultRoleName(config.serviceName, config.environment, 'redis')
220
+ const connectionName = 'redis-tawa' // TODO Phase 2c: configure redis connection in Vault
221
+
222
+ logger.info(
223
+ { roleName, role: config.role },
224
+ 'Ensuring Vault Redis role'
225
+ )
226
+
227
+ // Redis ACL creation template — key prefix isolation
228
+ const keyPattern = `~${config.serviceName}-${config.environment}:*`
229
+ const aclRule = config.role === 'readWrite'
230
+ ? `${keyPattern} +@all`
231
+ : `${keyPattern} +@read`
232
+
233
+ await vaultWrite(`database/roles/${roleName}`, {
234
+ db_name: connectionName,
235
+ creation_statements: [JSON.stringify([aclRule])],
236
+ default_ttl: '1h',
237
+ max_ttl: '720h',
238
+ })
239
+ }
240
+
241
+ /**
242
+ * Ensure a Vault database role exists for Neo4j.
243
+ * Uses database plugin if available, otherwise KV v2 static rotation.
244
+ */
245
+ export async function ensureVaultNeo4jRole(config: VaultDatabaseRoleConfig): Promise<void> {
246
+ const roleName = buildVaultRoleName(config.serviceName, config.environment, 'neo4j')
247
+
248
+ logger.info(
249
+ { roleName, databaseName: config.databaseName, role: config.role },
250
+ 'Ensuring Vault Neo4j role'
251
+ )
252
+
253
+ // Try database engine first (community plugin)
254
+ try {
255
+ const connectionName = 'neo4j-tawa' // TODO Phase 2d: configure neo4j connection in Vault
256
+
257
+ await vaultWrite(`database/roles/${roleName}`, {
258
+ db_name: connectionName,
259
+ creation_statements: [JSON.stringify({
260
+ roles: [{ role: config.role === 'readWrite' ? 'admin' : 'reader' }],
261
+ })],
262
+ default_ttl: '1h',
263
+ max_ttl: '720h',
264
+ })
265
+ } catch (error) {
266
+ // Fallback: store in KV v2 for static rotation
267
+ const message = error instanceof Error ? error.message : 'Unknown error'
268
+ logger.warn(
269
+ { roleName, error: message },
270
+ 'Neo4j database plugin not available, using KV v2 static rotation'
271
+ )
272
+ // KV v2 path — credentials stored as static secrets, rotated by external script
273
+ // The Vault Agent template reads from this path instead of database/creds/
274
+ }
275
+ }
276
+
277
+ // ─────────────────────────────────────────────────────────────────────────────
278
+ // Kubernetes Auth — Role + Policy Management
279
+ // ─────────────────────────────────────────────────────────────────────────────
280
+
281
+ /**
282
+ * Create a Vault policy scoped to a service's database credentials.
283
+ * Least privilege — service can only read its own credentials.
284
+ */
285
+ export async function ensureVaultPolicy(
286
+ serviceName: string,
287
+ environment: string,
288
+ dbTypes: ReadonlyArray<string>
289
+ ): Promise<void> {
290
+ const policyName = buildPolicyName(serviceName, environment)
291
+
292
+ // Build policy rules for each database type
293
+ const rules = dbTypes.map((dbType) => {
294
+ const roleName = buildVaultRoleName(serviceName, environment, dbType)
295
+ return `path "database/creds/${roleName}" {\n capabilities = ["read"]\n}`
296
+ })
297
+
298
+ // Add KV v2 access for Neo4j fallback
299
+ if (dbTypes.includes('neo4j')) {
300
+ rules.push(
301
+ `path "secret/data/neo4j/svc-${serviceName}-${environment}" {\n capabilities = ["read"]\n}`
302
+ )
303
+ }
304
+
305
+ const policy = rules.join('\n\n')
306
+
307
+ logger.info({ policyName, dbTypes }, 'Ensuring Vault policy')
308
+
309
+ await vaultWrite(`sys/policy/${policyName}`, { policy })
310
+ }
311
+
312
+ /**
313
+ * Bind a K8s ServiceAccount to a Vault role via K8s auth method.
314
+ * The service's pod authenticates to Vault using its ServiceAccount token.
315
+ */
316
+ export async function ensureKubeAuthRole(
317
+ serviceName: string,
318
+ environment: string,
319
+ namespace: string
320
+ ): Promise<void> {
321
+ const roleName = buildKubeAuthRoleName(serviceName, environment)
322
+ const policyName = buildPolicyName(serviceName, environment)
323
+
324
+ logger.info(
325
+ { roleName, namespace, serviceAccount: serviceName },
326
+ 'Ensuring Vault K8s auth role'
327
+ )
328
+
329
+ await vaultWrite(`auth/kubernetes/role/${roleName}`, {
330
+ bound_service_account_names: [serviceName],
331
+ bound_service_account_namespaces: [namespace],
332
+ policies: [policyName],
333
+ ttl: '1h',
334
+ })
335
+ }
336
+
337
+ /**
338
+ * Ensure a K8s ServiceAccount exists in the service namespace.
339
+ * Uses kubectl — idempotent via dry-run + apply.
340
+ */
341
+ export function ensureServiceAccount(serviceName: string, namespace: string): void {
342
+ const command = [
343
+ `kubectl create serviceaccount ${serviceName}`,
344
+ `--namespace ${namespace}`,
345
+ '--dry-run=client -o yaml | kubectl apply -f -',
346
+ ].join(' ')
347
+
348
+ execSync(command, {
349
+ encoding: 'utf8',
350
+ shell: '/bin/bash',
351
+ })
352
+
353
+ logger.info({ serviceName, namespace }, 'K8s ServiceAccount ensured')
354
+ }
355
+
356
+ // ─────────────────────────────────────────────────────────────────────────────
357
+ // MinIO — Vault Secrets Engine (custom plugin: vault-plugin-secrets-minio)
358
+ //
359
+ // Dynamic credentials via our custom Vault secrets engine.
360
+ // Plugin creates ephemeral MinIO users + scoped IAM policies on demand.
361
+ // Vault manages the full lifecycle: create on read, revoke on lease expiry.
362
+ // ─────────────────────────────────────────────────────────────────────────────
363
+
364
+ export interface MinioAdminCreds {
365
+ readonly accessKey: string
366
+ readonly secretKey: string
367
+ readonly endpoint: string
368
+ }
369
+
370
+ /**
371
+ * Build the Vault MinIO role name for a service's storage.
372
+ */
373
+ export function buildMinioRoleName(serviceName: string, environment: string): string {
374
+ return `svc-${serviceName}-${environment}-storage`
375
+ }
376
+
377
+ /**
378
+ * Build the Vault secrets engine creds path for a service.
379
+ * Pods read from this path via Vault Agent sidecar — Vault generates
380
+ * ephemeral MinIO credentials on each read.
381
+ */
382
+ export function buildMinioCredsPath(serviceName: string, environment: string): string {
383
+ return `minio/creds/${buildMinioRoleName(serviceName, environment)}`
384
+ }
385
+
386
+ /**
387
+ * Read MinIO admin credentials from Vault KV (secret/minio/admin).
388
+ * These are used by the builder to create buckets via MinIO SDK — never stored on disk.
389
+ * The custom plugin handles user/policy creation, but bucket creation still needs admin creds.
390
+ */
391
+ export async function getMinioAdminCreds(): Promise<MinioAdminCreds> {
392
+ const data = await vaultRead('secret/data/minio/admin')
393
+
394
+ if (!data || !data.data) {
395
+ throw new Error(
396
+ 'MinIO admin credentials not found in Vault KV at secret/minio/admin. ' +
397
+ 'Run: vault kv put secret/minio/admin access_key=... secret_key=... endpoint=...'
398
+ )
399
+ }
400
+
401
+ const inner = data.data as Record<string, string>
402
+
403
+ if (!inner.access_key || !inner.secret_key) {
404
+ throw new Error(
405
+ 'MinIO admin credentials in Vault KV are missing access_key or secret_key'
406
+ )
407
+ }
408
+
409
+ return {
410
+ accessKey: inner.access_key,
411
+ secretKey: inner.secret_key,
412
+ endpoint: inner.endpoint || `http://127.0.0.1:9000`,
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Configure a Vault MinIO role for a service.
418
+ * The role defines the inline IAM policy (scoped to declared buckets).
419
+ * When pods read from `minio/creds/{role}`, Vault creates an ephemeral
420
+ * MinIO user with this policy attached.
421
+ *
422
+ * Idempotent — updates if role exists.
423
+ */
424
+ export async function configureMinioVaultRole(
425
+ serviceName: string,
426
+ environment: string,
427
+ policyJson: string,
428
+ ): Promise<void> {
429
+ const roleName = buildMinioRoleName(serviceName, environment)
430
+
431
+ logger.info({ roleName, serviceName, environment }, 'Configuring Vault MinIO role')
432
+
433
+ await vaultWrite(`minio/roles/${roleName}`, {
434
+ policy: policyJson,
435
+ user_name_prefix: `svc-${serviceName}-${environment}-`,
436
+ default_ttl: '1h',
437
+ max_ttl: '720h',
438
+ })
439
+ }
440
+
441
+ /**
442
+ * Revoke all active MinIO leases for a service (credential rotation).
443
+ * Forces Vault to delete the ephemeral MinIO users + canned policies.
444
+ * New credentials are generated on next pod startup.
445
+ */
446
+ export async function revokeMinioLeases(
447
+ serviceName: string,
448
+ environment: string,
449
+ ): Promise<void> {
450
+ const prefix = `minio/creds/${buildMinioRoleName(serviceName, environment)}`
451
+
452
+ logger.info({ serviceName, environment, prefix }, 'Revoking MinIO leases')
453
+
454
+ await vaultWrite(`sys/leases/revoke-prefix/${prefix}`, {})
455
+ }
456
+
457
+ /**
458
+ * Add MinIO creds path to a service's Vault policy.
459
+ * Call this after ensureVaultPolicy to add storage access rules.
460
+ */
461
+ export async function addMinioPolicyRules(
462
+ serviceName: string,
463
+ environment: string,
464
+ ): Promise<void> {
465
+ const policyName = buildPolicyName(serviceName, environment)
466
+ const credsPath = buildMinioCredsPath(serviceName, environment)
467
+
468
+ // Read existing policy
469
+ const existing = await vaultRead(`sys/policy/${policyName}`)
470
+ const existingPolicy = existing?.rules as string || ''
471
+
472
+ // Only add if not already present
473
+ const minioRule = `path "${credsPath}" {\n capabilities = ["read"]\n}`
474
+ if (existingPolicy.includes(credsPath)) {
475
+ return
476
+ }
477
+
478
+ const updatedPolicy = existingPolicy
479
+ ? `${existingPolicy}\n\n${minioRule}`
480
+ : minioRule
481
+
482
+ await vaultWrite(`sys/policy/${policyName}`, { policy: updatedPolicy })
483
+
484
+ logger.info({ policyName, credsPath }, 'Added MinIO creds rules to Vault policy')
485
+ }
486
+
487
+ // ─────────────────────────────────────────────────────────────────────────────
488
+ // Vault Agent Annotations
489
+ // ─────────────────────────────────────────────────────────────────────────────
490
+
491
+ interface DatabaseAnnotationConfig {
492
+ readonly dbType: 'mongodb' | 'redis' | 'neo4j'
493
+ readonly roleName: string
494
+ readonly templateContent: string
495
+ }
496
+
497
+ /**
498
+ * Build the Vault Agent template that renders a MongoDB connection string.
499
+ */
500
+ function buildMongoTemplate(roleName: string, databaseName: string): string {
501
+ const host = env.DB_MONGODB_PUBLIC_HOST || env.DB_MONGODB_HOST
502
+ const port = env.DB_MONGODB_PORT
503
+ return [
504
+ `{{- with secret "database/creds/${roleName}" -}}`,
505
+ `MONGODB_URI=mongodb://{{ .Data.username }}:{{ .Data.password }}@${host}:${port}/${databaseName}?authSource=${databaseName}`,
506
+ '{{- end -}}',
507
+ ].join('\n')
508
+ }
509
+
510
+ /**
511
+ * Build the Vault Agent template that renders a Redis URL.
512
+ */
513
+ function buildRedisTemplate(roleName: string): string {
514
+ const host = env.DB_REDIS_HOST
515
+ const port = env.DB_REDIS_PORT
516
+ return [
517
+ `{{- with secret "database/creds/${roleName}" -}}`,
518
+ `REDIS_URL=redis://{{ .Data.username }}:{{ .Data.password }}@${host}:${port}/0`,
519
+ '{{- end -}}',
520
+ ].join('\n')
521
+ }
522
+
523
+ /**
524
+ * Build the Vault Agent template that renders Neo4j credentials.
525
+ */
526
+ function buildNeo4jTemplate(roleName: string): string {
527
+ const host = env.DB_NEO4J_HOST
528
+ const port = env.DB_NEO4J_PORT
529
+ return [
530
+ `{{- with secret "database/creds/${roleName}" -}}`,
531
+ `NEO4J_URI=bolt://${host}:${port}`,
532
+ 'NEO4J_USERNAME={{ .Data.username }}',
533
+ 'NEO4J_PASSWORD={{ .Data.password }}',
534
+ '{{- end -}}',
535
+ ].join('\n')
536
+ }
537
+
538
+ /**
539
+ * Generate Vault Agent pod annotations for a service.
540
+ *
541
+ * These annotations are injected via `--set-json podAnnotations='{...}'` in the Helm command.
542
+ * The Vault Agent Injector webhook reads them and injects init + sidecar containers.
543
+ */
544
+ export function getVaultAgentAnnotations(
545
+ serviceName: string,
546
+ environment: string,
547
+ databases: ReadonlyArray<{ type: 'mongodb' | 'redis' | 'neo4j'; name?: string }>
548
+ ): VaultAnnotations {
549
+ if (databases.length === 0) {
550
+ return {}
551
+ }
552
+
553
+ const kubeRoleName = buildKubeAuthRoleName(serviceName, environment)
554
+
555
+ const annotations: Record<string, string> = {
556
+ 'vault.hashicorp.com/agent-inject': 'true',
557
+ 'vault.hashicorp.com/role': kubeRoleName,
558
+ 'vault.hashicorp.com/agent-pre-populate-only': 'false',
559
+ }
560
+
561
+ // Build per-database annotations
562
+ const dbConfigs: DatabaseAnnotationConfig[] = databases.map((db) => {
563
+ const databaseName = db.name || `${serviceName}-${environment}`
564
+ const roleName = buildVaultRoleName(serviceName, environment, db.type)
565
+
566
+ switch (db.type) {
567
+ case 'mongodb':
568
+ return { dbType: db.type, roleName, templateContent: buildMongoTemplate(roleName, databaseName) }
569
+ case 'redis':
570
+ return { dbType: db.type, roleName, templateContent: buildRedisTemplate(roleName) }
571
+ case 'neo4j':
572
+ return { dbType: db.type, roleName, templateContent: buildNeo4jTemplate(roleName) }
573
+ }
574
+ })
575
+
576
+ // Each database type gets its own secret file, all rendered to /vault/secrets/
577
+ // The command annotation triggers when credentials rotate at max_ttl —
578
+ // it removes the liveness signal file so K8s restarts the container with fresh creds.
579
+ for (const config of dbConfigs) {
580
+ const suffix = config.dbType
581
+ annotations[`vault.hashicorp.com/agent-inject-secret-${suffix}`] = `database/creds/${config.roleName}`
582
+ annotations[`vault.hashicorp.com/agent-inject-template-${suffix}`] = config.templateContent
583
+ annotations[`vault.hashicorp.com/agent-inject-command-${suffix}`] = 'rm -f /vault/signals/alive'
584
+ }
585
+
586
+ return annotations
587
+ }
@@ -0,0 +1,6 @@
1
+ import { pino } from 'pino'
2
+ import { env } from '../config/env.js'
3
+
4
+ export const logger = pino({
5
+ level: env.LOG_LEVEL,
6
+ })
@@ -0,0 +1,23 @@
1
+ export interface ApiResponse<T> {
2
+ success: boolean
3
+ data?: T
4
+ error?: {
5
+ code: string
6
+ message: string
7
+ details?: unknown
8
+ }
9
+ }
10
+
11
+ export function apiResponse<T>(data: T): ApiResponse<T> {
12
+ return { success: true, data }
13
+ }
14
+
15
+ // Alias for apiResponse
16
+ export const apiSuccess = apiResponse
17
+
18
+ export function apiError(code: string, message: string, details?: unknown): ApiResponse<never> {
19
+ return {
20
+ success: false,
21
+ error: { code, message, details }
22
+ }
23
+ }