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,180 @@
1
+ import { Router } from 'express';
2
+ import { z } from 'zod';
3
+ import { apiResponse, apiError } from '../utils/response.js';
4
+ import { getBioClient, extractRequestMeta } from '../services/bio-client.js';
5
+ import { logger } from '../utils/logger.js';
6
+ export const oauthRouter = Router();
7
+ // Zod schemas for first-pass validation
8
+ const CreateClientSchema = z.object({
9
+ name: z.string().min(1).max(100),
10
+ description: z.string().optional(),
11
+ redirectUris: z.array(z.string().url()).min(1),
12
+ allowedScopes: z.array(z.string()).default(['openid', 'profile', 'email']),
13
+ allowedGrantTypes: z.array(z.string()).default(['authorization_code', 'refresh_token']),
14
+ isConfidential: z.boolean().default(true),
15
+ accessTokenTtl: z.number().int().min(60).max(86400).optional(),
16
+ refreshTokenTtl: z.number().int().min(3600).max(2592000).optional(),
17
+ });
18
+ const UpdateClientSchema = z.object({
19
+ name: z.string().min(1).max(100).optional(),
20
+ description: z.string().optional(),
21
+ redirectUris: z.array(z.string().url()).optional(),
22
+ allowedScopes: z.array(z.string()).optional(),
23
+ allowedGrantTypes: z.array(z.string()).optional(),
24
+ isActive: z.boolean().optional(),
25
+ accessTokenTtl: z.number().int().min(60).max(86400).optional(),
26
+ refreshTokenTtl: z.number().int().min(3600).max(2592000).optional(),
27
+ });
28
+ function handleUpstreamResponse(res, result, successStatus = 200) {
29
+ if (result.success) {
30
+ res.status(successStatus).json(apiResponse(result.data));
31
+ }
32
+ else if (result.error?.code === 'UPSTREAM_ERROR') {
33
+ res.status(502).json(apiError('UPSTREAM_ERROR', result.error.message));
34
+ }
35
+ else {
36
+ const status = result.error?.code === 'NOT_FOUND' ? 404
37
+ : result.error?.code === 'CONFLICT' ? 409
38
+ : result.error?.code === 'UNAUTHORIZED' ? 401
39
+ : result.error?.code === 'FORBIDDEN' ? 403
40
+ : result.error?.code === 'VALIDATION_ERROR' ? 400
41
+ : 500;
42
+ res.status(status).json(apiError(result.error?.code || 'ERROR', result.error?.message || 'Unknown error'));
43
+ }
44
+ }
45
+ // List all OAuth clients
46
+ oauthRouter.get('/clients', async (req, res, next) => {
47
+ try {
48
+ const bio = getBioClient();
49
+ const meta = extractRequestMeta(req);
50
+ const result = await bio.listClients(meta);
51
+ handleUpstreamResponse(res, result);
52
+ }
53
+ catch (err) {
54
+ next(err);
55
+ }
56
+ });
57
+ // Get single OAuth client
58
+ oauthRouter.get('/clients/:clientId', async (req, res, next) => {
59
+ try {
60
+ const { clientId } = req.params;
61
+ const bio = getBioClient();
62
+ const meta = extractRequestMeta(req);
63
+ const result = await bio.getClient(clientId, meta);
64
+ handleUpstreamResponse(res, result);
65
+ }
66
+ catch (err) {
67
+ next(err);
68
+ }
69
+ });
70
+ // Create new OAuth client
71
+ oauthRouter.post('/clients', async (req, res, next) => {
72
+ try {
73
+ const parsed = CreateClientSchema.safeParse(req.body);
74
+ if (!parsed.success) {
75
+ res.status(400).json(apiError('VALIDATION_ERROR', 'Invalid request', parsed.error.flatten()));
76
+ return;
77
+ }
78
+ const bio = getBioClient();
79
+ const meta = extractRequestMeta(req);
80
+ const result = await bio.createClient(parsed.data, meta);
81
+ logger.info({ name: parsed.data.name }, 'OAuth client creation proxied to bio-id');
82
+ handleUpstreamResponse(res, result, 201);
83
+ }
84
+ catch (err) {
85
+ next(err);
86
+ }
87
+ });
88
+ // Update OAuth client
89
+ oauthRouter.patch('/clients/:clientId', async (req, res, next) => {
90
+ try {
91
+ const { clientId } = req.params;
92
+ const parsed = UpdateClientSchema.safeParse(req.body);
93
+ if (!parsed.success) {
94
+ res.status(400).json(apiError('VALIDATION_ERROR', 'Invalid request', parsed.error.flatten()));
95
+ return;
96
+ }
97
+ const bio = getBioClient();
98
+ const meta = extractRequestMeta(req);
99
+ const result = await bio.updateClient(clientId, parsed.data, meta);
100
+ logger.info({ clientId }, 'OAuth client update proxied to bio-id');
101
+ handleUpstreamResponse(res, result);
102
+ }
103
+ catch (err) {
104
+ next(err);
105
+ }
106
+ });
107
+ // Add redirect URI to client
108
+ oauthRouter.post('/clients/:clientId/redirect-uris', async (req, res, next) => {
109
+ try {
110
+ const { clientId } = req.params;
111
+ const { uri } = req.body;
112
+ if (!uri || typeof uri !== 'string') {
113
+ res.status(400).json(apiError('VALIDATION_ERROR', 'URI is required'));
114
+ return;
115
+ }
116
+ try {
117
+ new URL(uri);
118
+ }
119
+ catch {
120
+ res.status(400).json(apiError('VALIDATION_ERROR', 'Invalid URI format'));
121
+ return;
122
+ }
123
+ const bio = getBioClient();
124
+ const meta = extractRequestMeta(req);
125
+ const result = await bio.addRedirectUri(clientId, uri, meta);
126
+ logger.info({ clientId, uri }, 'Redirect URI add proxied to bio-id');
127
+ handleUpstreamResponse(res, result);
128
+ }
129
+ catch (err) {
130
+ next(err);
131
+ }
132
+ });
133
+ // Remove redirect URI from client
134
+ oauthRouter.delete('/clients/:clientId/redirect-uris', async (req, res, next) => {
135
+ try {
136
+ const { clientId } = req.params;
137
+ const { uri } = req.body;
138
+ if (!uri || typeof uri !== 'string') {
139
+ res.status(400).json(apiError('VALIDATION_ERROR', 'URI is required'));
140
+ return;
141
+ }
142
+ const bio = getBioClient();
143
+ const meta = extractRequestMeta(req);
144
+ const result = await bio.removeRedirectUri(clientId, uri, meta);
145
+ logger.info({ clientId, uri }, 'Redirect URI remove proxied to bio-id');
146
+ handleUpstreamResponse(res, result);
147
+ }
148
+ catch (err) {
149
+ next(err);
150
+ }
151
+ });
152
+ // Regenerate client secret
153
+ oauthRouter.post('/clients/:clientId/regenerate-secret', async (req, res, next) => {
154
+ try {
155
+ const { clientId } = req.params;
156
+ const bio = getBioClient();
157
+ const meta = extractRequestMeta(req);
158
+ const result = await bio.regenerateSecret(clientId, meta);
159
+ logger.info({ clientId }, 'OAuth client secret regeneration proxied to bio-id');
160
+ handleUpstreamResponse(res, result);
161
+ }
162
+ catch (err) {
163
+ next(err);
164
+ }
165
+ });
166
+ // Delete OAuth client
167
+ oauthRouter.delete('/clients/:clientId', async (req, res, next) => {
168
+ try {
169
+ const { clientId } = req.params;
170
+ const bio = getBioClient();
171
+ const meta = extractRequestMeta(req);
172
+ const result = await bio.deleteClient(clientId, meta);
173
+ logger.info({ clientId }, 'OAuth client deletion proxied to bio-id');
174
+ handleUpstreamResponse(res, result);
175
+ }
176
+ catch (err) {
177
+ next(err);
178
+ }
179
+ });
180
+ //# sourceMappingURL=oauth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/routes/oauth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const observabilityRouter: import("express-serve-static-core").Router;
2
+ //# sourceMappingURL=observability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../src/routes/observability.ts"],"names":[],"mappings":"AAWA,eAAO,MAAM,mBAAmB,4CAAW,CAAA"}
@@ -0,0 +1,167 @@
1
+ import { Router } from 'express';
2
+ import { spawn } from 'child_process';
3
+ import { z } from 'zod';
4
+ import { apiResponse, apiError } from '../utils/response.js';
5
+ import { requireOrgAccess } from '../middleware/org-access.js';
6
+ import { logger } from '../utils/logger.js';
7
+ import { getPods } from '../services/pod-diagnostics.js';
8
+ import { troubleshoot } from '../services/troubleshooter.js';
9
+ import { getServicesCollection } from '../services/database.js';
10
+ export const observabilityRouter = Router();
11
+ const EnvironmentSchema = z.object({
12
+ environment: z.enum(['prod', 'sandbox', 'uat']).default('sandbox'),
13
+ });
14
+ const K8S_NAME_REGEX = /^[a-z0-9][a-z0-9._-]{0,252}$/;
15
+ const LogQuerySchema = z.object({
16
+ environment: z.enum(['prod', 'sandbox', 'uat']).default('sandbox'),
17
+ previous: z.enum(['true', 'false']).default('false'),
18
+ tail: z.string().default('100').transform(Number).pipe(z.number().int().min(1).max(1000)),
19
+ container: z.string().regex(K8S_NAME_REGEX).optional(),
20
+ });
21
+ async function resolveService(req) {
22
+ if (req.service)
23
+ return req.service;
24
+ const serviceName = req.params.serviceName;
25
+ if (!serviceName)
26
+ return null;
27
+ const services = getServicesCollection();
28
+ return services.findOne({ $or: [{ id: serviceName }, { name: serviceName }] });
29
+ }
30
+ /**
31
+ * GET /services/:serviceName/pods
32
+ * Returns pod list with status, restartCount, containerStatuses, recent events.
33
+ */
34
+ observabilityRouter.get('/:serviceName/pods', requireOrgAccess(), async (req, res, next) => {
35
+ try {
36
+ const service = await resolveService(req);
37
+ if (!service) {
38
+ res.status(404).json(apiError('NOT_FOUND', 'Service not found'));
39
+ return;
40
+ }
41
+ const query = EnvironmentSchema.safeParse(req.query);
42
+ if (!query.success) {
43
+ res.status(400).json(apiError('VALIDATION_ERROR', 'Invalid query', query.error.flatten()));
44
+ return;
45
+ }
46
+ const namespace = service.namespace || `${service.name}-${query.data.environment}`;
47
+ const pods = await getPods(namespace, service.name);
48
+ res.json(apiResponse({
49
+ service: service.name,
50
+ namespace,
51
+ environment: query.data.environment,
52
+ pods,
53
+ count: pods.length,
54
+ }));
55
+ }
56
+ catch (err) {
57
+ next(err);
58
+ }
59
+ });
60
+ /**
61
+ * GET /services/:serviceName/pods/logs
62
+ * SSE stream of pod logs via kubectl logs --follow.
63
+ */
64
+ observabilityRouter.get('/:serviceName/pods/logs', requireOrgAccess(), async (req, res, next) => {
65
+ try {
66
+ const service = await resolveService(req);
67
+ if (!service) {
68
+ res.status(404).json(apiError('NOT_FOUND', 'Service not found'));
69
+ return;
70
+ }
71
+ const query = LogQuerySchema.safeParse(req.query);
72
+ if (!query.success) {
73
+ res.status(400).json(apiError('VALIDATION_ERROR', 'Invalid query', query.error.flatten()));
74
+ return;
75
+ }
76
+ const namespace = service.namespace || `${service.name}-${query.data.environment}`;
77
+ const usePrevious = query.data.previous === 'true';
78
+ const tail = Math.min(Math.max(query.data.tail, 1), 1000);
79
+ const args = ['logs', '-n', namespace, '-l', `app=${service.name}`, `--tail=${tail}`];
80
+ if (!usePrevious) {
81
+ args.push('--follow');
82
+ }
83
+ if (usePrevious) {
84
+ args.push('--previous');
85
+ }
86
+ if (query.data.container) {
87
+ args.push('-c', query.data.container);
88
+ }
89
+ res.writeHead(200, {
90
+ 'Content-Type': 'text/event-stream',
91
+ 'Cache-Control': 'no-cache',
92
+ 'Connection': 'keep-alive',
93
+ 'X-Accel-Buffering': 'no',
94
+ });
95
+ res.write(': connected\n\n');
96
+ const kubectl = spawn('kubectl', args);
97
+ // Keepalive every 30 seconds to prevent proxy timeouts
98
+ const keepalive = setInterval(() => {
99
+ res.write(': keepalive\n\n');
100
+ }, 30_000);
101
+ // Safety timeout: close after 10 minutes
102
+ const timeout = setTimeout(() => {
103
+ kubectl.kill('SIGTERM');
104
+ res.write(`data: ${JSON.stringify({ event: 'timeout', message: 'Stream closed after 10 minutes' })}\n\n`);
105
+ res.end();
106
+ }, 10 * 60 * 1000);
107
+ const cleanup = () => {
108
+ clearInterval(keepalive);
109
+ clearTimeout(timeout);
110
+ };
111
+ kubectl.stdout.on('data', (chunk) => {
112
+ const lines = chunk.toString().split('\n').filter((l) => l.trim());
113
+ for (const line of lines) {
114
+ res.write(`data: ${JSON.stringify({ log: line, timestamp: new Date().toISOString() })}\n\n`);
115
+ }
116
+ });
117
+ kubectl.stderr.on('data', (chunk) => {
118
+ const msg = chunk.toString().trim();
119
+ if (msg) {
120
+ res.write(`data: ${JSON.stringify({ error: msg })}\n\n`);
121
+ }
122
+ });
123
+ kubectl.on('close', (code) => {
124
+ cleanup();
125
+ res.write(`data: ${JSON.stringify({ event: 'close', code })}\n\n`);
126
+ res.end();
127
+ });
128
+ kubectl.on('error', (err) => {
129
+ logger.warn({ err, namespace, service: service.name }, 'kubectl logs spawn error');
130
+ cleanup();
131
+ res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
132
+ res.end();
133
+ });
134
+ req.on('close', () => {
135
+ cleanup();
136
+ kubectl.kill('SIGTERM');
137
+ });
138
+ }
139
+ catch (err) {
140
+ next(err);
141
+ }
142
+ });
143
+ /**
144
+ * GET /services/:serviceName/troubleshoot
145
+ * Structured diagnostic analysis with actionable suggestions.
146
+ */
147
+ observabilityRouter.get('/:serviceName/troubleshoot', requireOrgAccess(), async (req, res, next) => {
148
+ try {
149
+ const service = await resolveService(req);
150
+ if (!service) {
151
+ res.status(404).json(apiError('NOT_FOUND', 'Service not found'));
152
+ return;
153
+ }
154
+ const query = EnvironmentSchema.safeParse(req.query);
155
+ if (!query.success) {
156
+ res.status(400).json(apiError('VALIDATION_ERROR', 'Invalid query', query.error.flatten()));
157
+ return;
158
+ }
159
+ const namespace = service.namespace || `${service.name}-${query.data.environment}`;
160
+ const result = await troubleshoot(service.name, namespace, query.data.environment);
161
+ res.json(apiResponse(result));
162
+ }
163
+ catch (err) {
164
+ next(err);
165
+ }
166
+ });
167
+ //# sourceMappingURL=observability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/routes/observability.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const orgsRouter: import("express-serve-static-core").Router;
2
+ //# sourceMappingURL=orgs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orgs.d.ts","sourceRoot":"","sources":["../../src/routes/orgs.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,UAAU,4CAAW,CAAA"}
@@ -0,0 +1,270 @@
1
+ import { Router } from 'express';
2
+ import { z } from 'zod';
3
+ import { randomBytes } from 'crypto';
4
+ import { apiResponse, apiError } from '../utils/response.js';
5
+ import { env } from '../config/env.js';
6
+ import { logger } from '../utils/logger.js';
7
+ import { deriveOrgRole } from '../middleware/auth.js';
8
+ import { getOrgInvitesCollection } from '../services/database.js';
9
+ import { forgejoAdminRequest, deriveUsername, ensureOrgMembership, } from '../services/forgejo.js';
10
+ export const orgsRouter = Router();
11
+ /**
12
+ * Generate a human-readable invite code: WORD-WORD-WORD-WORD
13
+ */
14
+ function generateInviteCode() {
15
+ const words = [
16
+ 'alpha', 'bravo', 'cedar', 'delta', 'eagle', 'flame', 'grove', 'harbor',
17
+ 'ivory', 'jasper', 'karma', 'lunar', 'maple', 'noble', 'orbit', 'prism',
18
+ 'quartz', 'ridge', 'solar', 'terra', 'ultra', 'vivid', 'wave', 'xenon',
19
+ 'yield', 'zenith', 'atlas', 'blade', 'coral', 'drift', 'ember', 'forge',
20
+ ];
21
+ const parts = Array.from({ length: 4 }, () => words[randomBytes(1)[0] % words.length]);
22
+ return parts.join('-').toUpperCase();
23
+ }
24
+ const CreateInviteSchema = z.object({
25
+ maxUses: z.number().int().min(0).max(1000).default(0),
26
+ expiresInDays: z.number().int().min(1).max(90).default(7),
27
+ });
28
+ const JoinOrgSchema = z.object({
29
+ code: z.string().min(1).max(100),
30
+ });
31
+ /**
32
+ * POST /orgs/:org/invites
33
+ * Create an invite code for an org. Only org admins can create invites.
34
+ */
35
+ orgsRouter.post('/:org/invites', async (req, res, next) => {
36
+ try {
37
+ const authReq = req;
38
+ const { org } = req.params;
39
+ if (!authReq.user) {
40
+ res.status(401).json(apiError('UNAUTHORIZED', 'Authentication required'));
41
+ return;
42
+ }
43
+ // Must be admin of the target org (or platform service)
44
+ const role = deriveOrgRole(authReq.user.roles);
45
+ const isOwnOrg = authReq.user.org === org;
46
+ if (role !== 'admin' && !authReq.user.roles.includes('service')) {
47
+ res.status(403).json(apiError('FORBIDDEN', 'Only org admins can create invites'));
48
+ return;
49
+ }
50
+ if (!isOwnOrg && !authReq.user.roles.includes('service') && !authReq.user.roles.includes('super_admin')) {
51
+ res.status(403).json(apiError('FORBIDDEN', 'Cannot create invites for other orgs'));
52
+ return;
53
+ }
54
+ const parsed = CreateInviteSchema.safeParse(req.body);
55
+ if (!parsed.success) {
56
+ res.status(400).json(apiError('VALIDATION_ERROR', 'Invalid request body', parsed.error.flatten()));
57
+ return;
58
+ }
59
+ const { maxUses, expiresInDays } = parsed.data;
60
+ const now = new Date();
61
+ const expiresAt = new Date(now.getTime() + expiresInDays * 24 * 60 * 60 * 1000);
62
+ const invite = {
63
+ id: randomBytes(16).toString('hex'),
64
+ org,
65
+ code: generateInviteCode(),
66
+ createdBy: authReq.user.email,
67
+ createdAt: now.toISOString(),
68
+ expiresAt: expiresAt.toISOString(),
69
+ maxUses,
70
+ uses: 0,
71
+ usedBy: [],
72
+ };
73
+ const collection = getOrgInvitesCollection();
74
+ await collection.insertOne(invite);
75
+ logger.info({ org, code: invite.code, createdBy: invite.createdBy, expiresInDays }, 'Org invite created');
76
+ res.status(201).json(apiResponse({
77
+ code: invite.code,
78
+ org: invite.org,
79
+ expiresAt: invite.expiresAt,
80
+ maxUses: invite.maxUses,
81
+ }));
82
+ }
83
+ catch (err) {
84
+ next(err);
85
+ }
86
+ });
87
+ /**
88
+ * GET /orgs/:org/invites
89
+ * List active invites for an org. Only org admins.
90
+ */
91
+ orgsRouter.get('/:org/invites', async (req, res, next) => {
92
+ try {
93
+ const authReq = req;
94
+ const { org } = req.params;
95
+ if (!authReq.user) {
96
+ res.status(401).json(apiError('UNAUTHORIZED', 'Authentication required'));
97
+ return;
98
+ }
99
+ const role = deriveOrgRole(authReq.user.roles);
100
+ const isOwnOrg = authReq.user.org === org;
101
+ if (role !== 'admin' && !authReq.user.roles.includes('service')) {
102
+ res.status(403).json(apiError('FORBIDDEN', 'Only org admins can list invites'));
103
+ return;
104
+ }
105
+ if (!isOwnOrg && !authReq.user.roles.includes('service') && !authReq.user.roles.includes('super_admin')) {
106
+ res.status(403).json(apiError('FORBIDDEN', 'Cannot list invites for other orgs'));
107
+ return;
108
+ }
109
+ const collection = getOrgInvitesCollection();
110
+ const invites = await collection
111
+ .find({ org, expiresAt: { $gt: new Date().toISOString() } })
112
+ .sort({ createdAt: -1 })
113
+ .toArray();
114
+ const sanitized = invites.map(inv => ({
115
+ code: inv.code,
116
+ org: inv.org,
117
+ createdBy: inv.createdBy,
118
+ createdAt: inv.createdAt,
119
+ expiresAt: inv.expiresAt,
120
+ maxUses: inv.maxUses,
121
+ uses: inv.uses,
122
+ }));
123
+ res.json(apiResponse(sanitized));
124
+ }
125
+ catch (err) {
126
+ next(err);
127
+ }
128
+ });
129
+ /**
130
+ * DELETE /orgs/:org/invites/:code
131
+ * Revoke an invite. Only org admins.
132
+ */
133
+ orgsRouter.delete('/:org/invites/:code', async (req, res, next) => {
134
+ try {
135
+ const authReq = req;
136
+ const { org, code } = req.params;
137
+ if (!authReq.user) {
138
+ res.status(401).json(apiError('UNAUTHORIZED', 'Authentication required'));
139
+ return;
140
+ }
141
+ const role = deriveOrgRole(authReq.user.roles);
142
+ const isOwnOrg = authReq.user.org === org;
143
+ if (role !== 'admin' && !authReq.user.roles.includes('service')) {
144
+ res.status(403).json(apiError('FORBIDDEN', 'Only org admins can revoke invites'));
145
+ return;
146
+ }
147
+ if (!isOwnOrg && !authReq.user.roles.includes('service') && !authReq.user.roles.includes('super_admin')) {
148
+ res.status(403).json(apiError('FORBIDDEN', 'Cannot revoke invites for other orgs'));
149
+ return;
150
+ }
151
+ const collection = getOrgInvitesCollection();
152
+ const result = await collection.deleteOne({ org, code: code.toUpperCase() });
153
+ if (result.deletedCount === 0) {
154
+ res.status(404).json(apiError('NOT_FOUND', 'Invite not found'));
155
+ return;
156
+ }
157
+ logger.info({ org, code, revokedBy: authReq.user.email }, 'Org invite revoked');
158
+ res.json(apiResponse({ revoked: true }));
159
+ }
160
+ catch (err) {
161
+ next(err);
162
+ }
163
+ });
164
+ /**
165
+ * POST /orgs/join
166
+ * Accept an invite code and join the org in Forgejo.
167
+ * Requires an existing Forgejo account (run provision-git first).
168
+ */
169
+ orgsRouter.post('/join', async (req, res, next) => {
170
+ try {
171
+ const authReq = req;
172
+ if (!authReq.user) {
173
+ res.status(401).json(apiError('UNAUTHORIZED', 'Authentication required'));
174
+ return;
175
+ }
176
+ if (!env.FORGEJO_TOKEN) {
177
+ res.status(503).json(apiError('SERVICE_UNAVAILABLE', 'Git provisioning is not configured'));
178
+ return;
179
+ }
180
+ const parsed = JoinOrgSchema.safeParse(req.body);
181
+ if (!parsed.success) {
182
+ res.status(400).json(apiError('VALIDATION_ERROR', 'Invalid request body', parsed.error.flatten()));
183
+ return;
184
+ }
185
+ const code = parsed.data.code.toUpperCase();
186
+ const collection = getOrgInvitesCollection();
187
+ // Find and validate the invite
188
+ const invite = await collection.findOne({ code });
189
+ if (!invite) {
190
+ res.status(404).json(apiError('INVITE_NOT_FOUND', 'Invalid or expired invite code'));
191
+ return;
192
+ }
193
+ if (new Date(invite.expiresAt) < new Date()) {
194
+ res.status(410).json(apiError('INVITE_EXPIRED', 'This invite has expired'));
195
+ return;
196
+ }
197
+ if (invite.maxUses > 0 && invite.uses >= invite.maxUses) {
198
+ res.status(410).json(apiError('INVITE_EXHAUSTED', 'This invite has reached its usage limit'));
199
+ return;
200
+ }
201
+ if (invite.usedBy.includes(authReq.user.email)) {
202
+ res.status(409).json(apiError('ALREADY_JOINED', 'You have already used this invite'));
203
+ return;
204
+ }
205
+ // Resolve the user's Forgejo username
206
+ const username = deriveUsername(authReq.user.email);
207
+ const userCheck = await forgejoAdminRequest('GET', `/users/${encodeURIComponent(username)}`);
208
+ if (userCheck.status !== 200 || !userCheck.data) {
209
+ res.status(400).json(apiError('NO_GIT_ACCOUNT', 'No Forgejo account found. Run `tawa login` first.'));
210
+ return;
211
+ }
212
+ const forgejoUsername = userCheck.data.login;
213
+ // Add user to the org
214
+ const membership = await ensureOrgMembership(invite.org, forgejoUsername);
215
+ if (!membership) {
216
+ res.status(500).json(apiError('JOIN_FAILED', 'Failed to add you to the org'));
217
+ return;
218
+ }
219
+ // Record usage
220
+ await collection.updateOne({ code }, {
221
+ $inc: { uses: 1 },
222
+ $push: { usedBy: authReq.user.email },
223
+ });
224
+ logger.info({ org: invite.org, username: forgejoUsername, email: authReq.user.email, code }, 'User joined org via invite');
225
+ res.json(apiResponse({
226
+ org: invite.org,
227
+ username: forgejoUsername,
228
+ role: membership.role,
229
+ }));
230
+ }
231
+ catch (err) {
232
+ next(err);
233
+ }
234
+ });
235
+ /**
236
+ * GET /orgs/:org/members
237
+ * List members of a Forgejo org. Org members can view.
238
+ */
239
+ orgsRouter.get('/:org/members', async (req, res, next) => {
240
+ try {
241
+ const authReq = req;
242
+ const { org } = req.params;
243
+ if (!authReq.user) {
244
+ res.status(401).json(apiError('UNAUTHORIZED', 'Authentication required'));
245
+ return;
246
+ }
247
+ if (!env.FORGEJO_TOKEN) {
248
+ res.status(503).json(apiError('SERVICE_UNAVAILABLE', 'Git provisioning is not configured'));
249
+ return;
250
+ }
251
+ const result = await forgejoAdminRequest('GET', `/orgs/${encodeURIComponent(org)}/members`);
252
+ if (result.status === 404) {
253
+ res.status(404).json(apiError('NOT_FOUND', 'Org not found'));
254
+ return;
255
+ }
256
+ if (result.status !== 200 || !result.data) {
257
+ res.status(502).json(apiError('UPSTREAM_ERROR', 'Failed to fetch org members'));
258
+ return;
259
+ }
260
+ const members = result.data.map(m => ({
261
+ username: m.login,
262
+ id: m.id,
263
+ }));
264
+ res.json(apiResponse(members));
265
+ }
266
+ catch (err) {
267
+ next(err);
268
+ }
269
+ });
270
+ //# sourceMappingURL=orgs.js.map