@podosoft/podokit 1.4.2 → 1.6.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 (245) hide show
  1. package/README.md +2 -1
  2. package/dist/app-proxy-timeouts.d.ts +27 -0
  3. package/dist/app-proxy-timeouts.js +180 -0
  4. package/dist/assemble.d.ts +5 -2
  5. package/dist/assemble.js +14 -4
  6. package/dist/deploy-checks.d.ts +44 -0
  7. package/dist/deploy-checks.js +267 -0
  8. package/dist/deploy-compose-profile.d.ts +6 -2
  9. package/dist/deploy-compose-profile.js +15 -6
  10. package/dist/deploy-compose-render.d.ts +12 -4
  11. package/dist/deploy-compose-render.js +47 -17
  12. package/dist/deploy-compose.d.ts +2 -5
  13. package/dist/deploy-compose.js +7 -2
  14. package/dist/deploy-profile.d.ts +6 -2
  15. package/dist/deploy-profile.js +11 -7
  16. package/dist/deploy-render.d.ts +4 -1
  17. package/dist/deploy-render.js +28 -20
  18. package/dist/deploy-schema.d.ts +61 -0
  19. package/dist/deploy-schema.js +157 -1
  20. package/dist/deploy.d.ts +2 -5
  21. package/dist/deploy.js +5 -2
  22. package/dist/dev.d.ts +21 -0
  23. package/dist/dev.js +78 -3
  24. package/dist/eject.js +2 -0
  25. package/dist/index.js +85 -5
  26. package/dist/lib.d.ts +2 -0
  27. package/dist/lib.js +5 -2
  28. package/dist/lockfile.d.ts +24 -0
  29. package/dist/lockfile.js +77 -5
  30. package/dist/provider.d.ts +1 -0
  31. package/dist/provider.js +12 -4
  32. package/dist/remove.js +1 -1
  33. package/dist/template-files.d.ts +42 -0
  34. package/dist/template-files.js +102 -0
  35. package/dist/templates/fullstack/AGENTS.md +44 -7
  36. package/dist/templates/fullstack/README.md +8 -7
  37. package/dist/templates/fullstack/apps/api/package.json +4 -4
  38. package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
  39. package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
  40. package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
  41. package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +5 -1
  42. package/dist/templates/fullstack/apps/api/src/app.extensions.ts +7 -4
  43. package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
  44. package/dist/templates/fullstack/apps/api/src/app.ts +47 -28
  45. package/dist/templates/fullstack/apps/api/src/build.spec.ts +118 -0
  46. package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
  47. package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
  48. package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +46 -3
  49. package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
  50. package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
  51. package/dist/templates/fullstack/apps/api/src/config/providers.ts +8 -2
  52. package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +17 -6
  53. package/dist/templates/fullstack/apps/api/src/config/server-options.ts +5 -8
  54. package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
  55. package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +29 -0
  56. package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +39 -2
  57. package/dist/templates/fullstack/apps/api/src/core/client-address.spec.ts +82 -0
  58. package/dist/templates/fullstack/apps/api/src/core/client-address.ts +68 -0
  59. package/dist/templates/fullstack/apps/api/src/core/services.ts +43 -0
  60. package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
  61. package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
  62. package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
  63. package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
  64. package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
  65. package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
  66. package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
  67. package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
  68. package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
  69. package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
  70. package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
  71. package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
  72. package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
  73. package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
  74. package/dist/templates/fullstack/apps/web/Dockerfile +4 -0
  75. package/dist/templates/fullstack/apps/web/package.json +1 -1
  76. package/dist/templates/fullstack/apps/web/src/app.css +3 -0
  77. package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
  78. package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
  79. package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
  80. package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
  81. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +370 -36
  82. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +250 -41
  83. package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +5 -2
  84. package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +23 -0
  85. package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
  86. package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
  87. package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
  88. package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
  89. package/dist/templates/fullstack/compose.dev.yaml +7 -4
  90. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
  91. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +12 -3
  92. package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
  93. package/dist/templates/fullstack/dot-env.docker.example +7 -2
  94. package/dist/templates/fullstack/dot-env.example +21 -1
  95. package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
  96. package/dist/templates/fullstack/infra/k3s/configmap.yaml +4 -0
  97. package/dist/templates/fullstack/tests/README.md +40 -1
  98. package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
  99. package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
  100. package/dist/templates/fullstack/tests/package.json +2 -2
  101. package/dist/templates/fullstack/tests/playwright.config.ts +7 -3
  102. package/dist/templates/fullstack/tests/playwright.extensions.ts +88 -1
  103. package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
  104. package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.spec.ts +152 -0
  105. package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.ts +112 -0
  106. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +24 -0
  107. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +18 -0
  108. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +36 -13
  109. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +13 -1
  110. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
  111. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
  112. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.test.ts +33 -0
  113. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.ts +34 -0
  114. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
  115. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
  116. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
  117. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
  118. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +39 -0
  119. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
  120. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
  121. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +73 -39
  122. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
  123. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
  124. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
  125. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
  126. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
  127. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
  128. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
  129. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
  130. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
  131. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +3 -1
  132. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
  133. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
  134. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
  135. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
  136. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
  137. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
  138. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
  139. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
  140. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
  141. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
  142. package/dist/templates/modules/admin-dashboard/files/tests/api/account-sessions.api.spec.ts +55 -0
  143. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
  144. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
  145. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
  146. package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
  147. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
  148. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
  149. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +29 -1
  150. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
  151. package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
  152. package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +55 -0
  153. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
  154. package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
  155. package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
  156. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +11 -5
  157. package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +3 -10
  158. package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
  159. package/dist/templates/modules/admin-dashboard/files/tests/ui/account-sessions.ui.spec.ts +46 -0
  160. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +62 -59
  161. package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
  162. package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
  163. package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
  164. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
  165. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
  166. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
  167. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
  168. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
  169. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
  170. package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
  171. package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
  172. package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
  173. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
  174. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
  175. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
  176. package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
  177. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
  178. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
  179. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
  180. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +33 -7
  181. package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +29 -0
  182. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
  183. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
  184. package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
  185. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -1
  186. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
  187. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +33 -0
  188. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +20 -1
  189. package/dist/templates/modules/audit-log/module.manifest.json +15 -3
  190. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
  191. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
  192. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
  193. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
  194. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +6 -1
  195. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
  196. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +54 -6
  197. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -4
  198. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -8
  199. package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.spec.ts +56 -0
  200. package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.ts +45 -0
  201. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
  202. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
  203. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
  204. package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
  205. package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
  206. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
  207. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
  208. package/dist/templates/modules/auth/module.manifest.json +8 -2
  209. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +57 -1
  210. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +93 -9
  211. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
  212. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
  213. package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
  214. package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
  215. package/dist/templates/modules/bullmq/module.manifest.json +5 -1
  216. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
  217. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +87 -1
  218. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +93 -3
  219. package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
  220. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
  221. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
  222. package/dist/templates/modules/mailer/module.manifest.json +1 -1
  223. package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +1 -1
  224. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
  225. package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +1 -1
  226. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +2 -59
  227. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +0 -65
  228. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +82 -21
  229. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +54 -15
  230. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +43 -5
  231. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +10 -2
  232. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +61 -3
  233. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +48 -15
  234. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +63 -27
  235. package/dist/templates/modules/rate-limit/module.manifest.json +6 -9
  236. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
  237. package/dist/templates/todo/AGENTS.md +46 -6
  238. package/dist/templates/todo/README.md +6 -4
  239. package/dist/text-diff.d.ts +12 -0
  240. package/dist/text-diff.js +111 -0
  241. package/dist/update.d.ts +61 -3
  242. package/dist/update.js +152 -27
  243. package/package.json +2 -2
  244. package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
  245. package/dist/templates/modules/admin-dashboard/files/tests/helpers/exclusive.ts +0 -80
@@ -170,6 +170,7 @@ function parseComposeProfile(value) {
170
170
  "port",
171
171
  "webSocketPaths",
172
172
  "apiPaths",
173
+ "edgeProxies",
173
174
  "trustedProxyCidrs",
174
175
  "gatewayImage",
175
176
  ], "exposure");
@@ -201,6 +202,8 @@ function parseComposeProfile(value) {
201
202
  throw new Error("Deployment exposure.port must be a TCP port.");
202
203
  const webSocketPaths = (0, websocket_paths_1.parseExactWebSocketPaths)(exposure.webSocketPaths ?? [], "exposure.webSocketPaths");
203
204
  const apiPaths = (0, api_paths_1.parseApiPaths)(exposure.apiPaths ?? [], "exposure.apiPaths", webSocketPaths);
205
+ // A reverse proxy on the host is the expected front door, as bindAddress assumes.
206
+ const edgeProxies = (0, deploy_schema_1.parseEdgeProxies)(exposure, 1);
204
207
  const trustedProxyCidrs = parseTrustedProxyCidrs(exposure.trustedProxyCidrs ?? [], "exposure.trustedProxyCidrs");
205
208
  if (trustedProxyCidrs.length > 0 && webSocketPaths.length === 0 && apiPaths.length === 0) {
206
209
  throw new Error("Deployment exposure.trustedProxyCidrs requires at least one exposure.webSocketPaths or exposure.apiPaths entry.");
@@ -231,6 +234,12 @@ function parseComposeProfile(value) {
231
234
  if (typeof secrets.registryLogin !== "boolean") {
232
235
  throw new Error("Deployment secrets.registryLogin must be a boolean.");
233
236
  }
237
+ const runtimeConfig = (0, deploy_schema_1.parseRuntimeConfig)((0, deploy_schema_1.requiredRecord)(value, "runtimeConfig"));
238
+ const postgresRecord = (0, deploy_schema_1.requiredRecord)(dependencies, "postgres");
239
+ const postgres = {
240
+ ...parseComposeDependency(postgresRecord, "dependencies.postgres", ["maxConnections", "appPools"]),
241
+ ...(0, deploy_schema_1.parsePostgresBudget)(postgresRecord, "dependencies.postgres", runtimeConfig),
242
+ };
234
243
  const profile = {
235
244
  schemaVersion: 1,
236
245
  driver: "docker-compose",
@@ -243,6 +252,7 @@ function parseComposeProfile(value) {
243
252
  port,
244
253
  webSocketPaths,
245
254
  apiPaths,
255
+ edgeProxies,
246
256
  trustedProxyCidrs,
247
257
  gatewayImage,
248
258
  },
@@ -254,7 +264,7 @@ function parseComposeProfile(value) {
254
264
  : parseComposeWorkload((0, deploy_schema_1.requiredRecord)(workloads, "worker"), "workloads.worker"),
255
265
  },
256
266
  dependencies: {
257
- postgres: parseComposeDependency((0, deploy_schema_1.requiredRecord)(dependencies, "postgres"), "dependencies.postgres"),
267
+ postgres,
258
268
  redis: parseComposeDependency((0, deploy_schema_1.requiredRecord)(dependencies, "redis"), "dependencies.redis"),
259
269
  objectStorage,
260
270
  },
@@ -267,7 +277,7 @@ function parseComposeProfile(value) {
267
277
  ? { migration: { command: (0, deploy_schema_1.parseCommand)(migration.command, "migration.command") } }
268
278
  : {}),
269
279
  ...(sync ? { sync: parseSyncProfile(sync, "sync") } : {}),
270
- runtimeConfig: (0, deploy_schema_1.parseRuntimeConfig)((0, deploy_schema_1.requiredRecord)(value, "runtimeConfig")),
280
+ runtimeConfig,
271
281
  verification: (0, deploy_schema_1.parseVerification)((0, deploy_schema_1.requiredRecord)(value, "verification")),
272
282
  };
273
283
  return profile;
@@ -308,6 +318,7 @@ function buildDefaultComposeProfile(projectName, options) {
308
318
  port: 8080,
309
319
  webSocketPaths: [],
310
320
  apiPaths: [],
321
+ edgeProxies: 1,
311
322
  trustedProxyCidrs: [],
312
323
  gatewayImage: "caddy:2.10-alpine",
313
324
  },
@@ -355,8 +366,6 @@ function buildDefaultComposeProfile(projectName, options) {
355
366
  runtimeConfig: {
356
367
  NODE_ENV: "production",
357
368
  CORS_ORIGIN: `https://${host}`,
358
- ADDRESS_HEADER: "x-forwarded-for",
359
- XFF_DEPTH: "1",
360
369
  },
361
370
  verification: {
362
371
  baseUrl: `https://${host}`,
@@ -410,6 +419,8 @@ function initializeComposeProfile(projectRoot, name, options) {
410
419
  draft.runtimeConfig = {
411
420
  ...draft.runtimeConfig,
412
421
  ...(usesAuth ? { BETTER_AUTH_URL: `https://${host}` } : {}),
422
+ // The API's fallback until an administrator saves the setting.
423
+ ...(modules.has("audit-log") ? { AUDIT_LOG_ENABLED: "true" } : {}),
413
424
  ...(modules.has("rate-limit")
414
425
  ? {
415
426
  RATE_LIMIT_TTL: "60",
@@ -418,8 +429,6 @@ function initializeComposeProfile(projectRoot, name, options) {
418
429
  RATE_LIMIT_AUTH_TTL: "60",
419
430
  RATE_LIMIT_AUTH_MAX: "20",
420
431
  RATE_LIMIT_RUNTIME_MAX: "1000",
421
- RATE_LIMIT_TRUSTED_PROXY_HOPS: "1",
422
- RATE_LIMIT_PROXY_HEADER: "x-forwarded-for",
423
432
  RATE_LIMIT_STORAGE_TIMEOUT_MS: "1000",
424
433
  RATE_LIMIT_UNAVAILABLE_RETRY_AFTER: "1",
425
434
  }
@@ -1,4 +1,5 @@
1
1
  import { type DockerComposeProfileV1 } from "./deploy-compose-profile";
2
+ import { type AppProxyTimeouts } from "./app-proxy-timeouts";
2
3
  /**
3
4
  * Compose project rendering.
4
5
  *
@@ -29,7 +30,14 @@ export declare function serviceName(profile: DockerComposeProfileV1, role: strin
29
30
  * Configuration PodoKit derives rather than accepts, so a profile cannot point the
30
31
  * app at the wrong service or port.
31
32
  */
32
- export declare function derivedRuntimeConfig(profile: DockerComposeProfileV1): Record<string, string>;
33
+ export declare function derivedRuntimeConfig(profile: DockerComposeProfileV1, proxyTimeouts: AppProxyTimeouts): Record<string, string>;
34
+ /**
35
+ * The gateway replaces X-Forwarded-For with the one address it resolved. It keeps
36
+ * a trusted proxy's X-Forwarded-Proto and X-Forwarded-Host and otherwise writes its
37
+ * own, which name its plain-HTTP listener: behind an edge proxy it does not trust,
38
+ * the web server keeps its default origin (https and the Host header) instead.
39
+ */
40
+ export declare function composeProxyTrust(profile: DockerComposeProfileV1): Record<string, string>;
33
41
  /**
34
42
  * Identity of everything the containers read but Compose cannot see a change in.
35
43
  *
@@ -38,12 +46,12 @@ export declare function derivedRuntimeConfig(profile: DockerComposeProfileV1): R
38
46
  * secret or a changed runtime config a definition change, so `up` actually rolls
39
47
  * the containers instead of leaving stale values running.
40
48
  */
41
- export declare function rolloutStateDigest(profile: DockerComposeProfileV1, secretIdentities: Record<string, string>): string;
42
- export declare function renderComposeDocument(profile: DockerComposeProfileV1, release: string, images: ComposeImages, stateDigest: string, workerCommand?: string[]): string;
49
+ export declare function rolloutStateDigest(profile: DockerComposeProfileV1, secretIdentities: Record<string, string>, proxyTimeouts: AppProxyTimeouts): string;
50
+ export declare function renderComposeDocument(profile: DockerComposeProfileV1, release: string, images: ComposeImages, stateDigest: string, proxyTimeouts: AppProxyTimeouts, workerCommand?: string[]): string;
43
51
  /**
44
52
  * The migration runs as its own Compose project so it can be started, waited on,
45
53
  * and removed without touching the running application definition.
46
54
  */
47
- export declare function renderMigrationDocument(profile: DockerComposeProfileV1, release: string, images: ComposeImages, defaultCommand?: string[]): string;
55
+ export declare function renderMigrationDocument(profile: DockerComposeProfileV1, release: string, images: ComposeImages, proxyTimeouts: AppProxyTimeouts, defaultCommand?: string[]): string;
48
56
  export declare function defaultComposeImages(profile: DockerComposeProfileV1, release: string): ComposeImages;
49
57
  export declare function renderComposeDeployment(projectRoot: string, profileName: string, profile: DockerComposeProfileV1, release: string, resolvedImages?: ComposeImages, stateDigest?: string): ComposeRuntime;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.serviceName = serviceName;
4
4
  exports.derivedRuntimeConfig = derivedRuntimeConfig;
5
+ exports.composeProxyTrust = composeProxyTrust;
5
6
  exports.rolloutStateDigest = rolloutStateDigest;
6
7
  exports.renderComposeDocument = renderComposeDocument;
7
8
  exports.renderMigrationDocument = renderMigrationDocument;
@@ -12,6 +13,7 @@ const node_fs_1 = require("node:fs");
12
13
  const node_path_1 = require("node:path");
13
14
  const api_paths_1 = require("./api-paths");
14
15
  const deploy_compose_profile_1 = require("./deploy-compose-profile");
16
+ const app_proxy_timeouts_1 = require("./app-proxy-timeouts");
15
17
  const deploy_schema_1 = require("./deploy-schema");
16
18
  const lockfile_1 = require("./lockfile");
17
19
  const toolchain_1 = require("./toolchain");
@@ -33,10 +35,12 @@ function serviceName(profile, role) {
33
35
  * Configuration PodoKit derives rather than accepts, so a profile cannot point the
34
36
  * app at the wrong service or port.
35
37
  */
36
- function derivedRuntimeConfig(profile) {
38
+ function derivedRuntimeConfig(profile, proxyTimeouts) {
37
39
  const result = {
38
40
  PORT: String(CONTAINER_PORT),
39
41
  BACKEND_INTERNAL_URL: `http://${serviceName(profile, "api")}:${CONTAINER_PORT}`,
42
+ ...(0, deploy_schema_1.idleTimeoutConfig)(profile.runtimeConfig, proxyTimeouts),
43
+ ...composeProxyTrust(profile),
40
44
  ...profile.runtimeConfig,
41
45
  };
42
46
  if (profile.dependencies.postgres.mode === "managed") {
@@ -54,6 +58,18 @@ function derivedRuntimeConfig(profile) {
54
58
  }
55
59
  return result;
56
60
  }
61
+ /**
62
+ * The gateway replaces X-Forwarded-For with the one address it resolved. It keeps
63
+ * a trusted proxy's X-Forwarded-Proto and X-Forwarded-Host and otherwise writes its
64
+ * own, which name its plain-HTTP listener: behind an edge proxy it does not trust,
65
+ * the web server keeps its default origin (https and the Host header) instead.
66
+ */
67
+ function composeProxyTrust(profile) {
68
+ const { edgeProxies, trustedProxyCidrs } = profile.exposure;
69
+ return (0, deploy_compose_profile_1.composeHasGateway)(profile)
70
+ ? (0, deploy_schema_1.proxyTrustConfig)(1, edgeProxies === 0 || trustedProxyCidrs.length > 0)
71
+ : (0, deploy_schema_1.proxyTrustConfig)(edgeProxies);
72
+ }
57
73
  function sortedEnvironment(config) {
58
74
  return Object.entries(config).sort(([left], [right]) => left.localeCompare(right));
59
75
  }
@@ -73,10 +89,10 @@ function environmentBlock(config, indent) {
73
89
  * secret or a changed runtime config a definition change, so `up` actually rolls
74
90
  * the containers instead of leaving stale values running.
75
91
  */
76
- function rolloutStateDigest(profile, secretIdentities) {
92
+ function rolloutStateDigest(profile, secretIdentities, proxyTimeouts) {
77
93
  return digest([
78
94
  "compose-rollout-state",
79
- JSON.stringify(sortedEnvironment(derivedRuntimeConfig(profile))),
95
+ JSON.stringify(sortedEnvironment(derivedRuntimeConfig(profile, proxyTimeouts))),
80
96
  JSON.stringify(Object.entries(secretIdentities).sort(([l], [r]) => l.localeCompare(r))),
81
97
  ]);
82
98
  }
@@ -120,8 +136,13 @@ function dependencyService(profile, role, dependency, image, extra) {
120
136
  extra.healthcheck);
121
137
  }
122
138
  function postgresService(profile, image) {
139
+ const { maxConnections } = profile.dependencies.postgres;
123
140
  return dependencyService(profile, "postgres", profile.dependencies.postgres, image, {
124
141
  dataPath: "/var/lib/postgresql/data",
142
+ // The image's entry point runs postgres with arguments that start with "-".
143
+ ...(maxConnections === undefined
144
+ ? {}
145
+ : { command: `["-c", ${quote(`max_connections=${maxConnections}`)}]` }),
125
146
  healthcheck: ` healthcheck:\n` +
126
147
  ` test: ["CMD-SHELL", "pg_isready -U \\"$$POSTGRES_USER\\" -d \\"$$POSTGRES_DB\\""]\n` +
127
148
  ` interval: 10s\n timeout: 5s\n retries: 10\n start_period: 20s\n`,
@@ -195,7 +216,7 @@ function objectStorageInitService(profile, clientImage) {
195
216
  ` entrypoint: ["sh", "-c", ${quote(script)}]\n` +
196
217
  ` networks: [${profile.target.project}]\n`);
197
218
  }
198
- function applicationService(profile, role, image, release, stateDigest, options) {
219
+ function applicationService(profile, role, image, release, stateDigest, environment, options) {
199
220
  const workload = role === "worker" ? profile.workloads.worker : profile.workloads[role];
200
221
  if (!workload)
201
222
  return "";
@@ -221,7 +242,7 @@ function applicationService(profile, role, image, release, stateDigest, options)
221
242
  ` restart: unless-stopped\n` +
222
243
  (options.command ? ` command: ${options.command}\n` : "") +
223
244
  (envFile ? ` env_file:\n - ${quote(envFile.path)}\n` : "") +
224
- environmentBlock(derivedRuntimeConfig(profile), " ") +
245
+ environmentBlock(environment, " ") +
225
246
  (dependsOn.length
226
247
  ? ` depends_on:\n${dependsOn
227
248
  .map(([name, condition]) => ` ${name}:\n condition: ${condition}\n`)
@@ -252,10 +273,17 @@ function gatewayService(profile, image, release, stateDigest) {
252
273
  `
253
274
  : "";
254
275
  const api = `${serviceName(profile, "api")}:${CONTAINER_PORT}`;
276
+ // Caddy appends the peer to X-Forwarded-For, so behind a trusted proxy web and
277
+ // API would get "client, proxy" and record the proxy at one trusted hop. Send
278
+ // the client address Caddy resolved instead: the client behind a trusted proxy,
279
+ // the peer otherwise.
280
+ const reverseProxy = (upstream) => ` reverse_proxy ${upstream} {\n` +
281
+ ` header_up X-Forwarded-For {client_ip}\n` +
282
+ ` }\n`;
255
283
  const webSocketRoutes = profile.exposure.webSocketPaths.length
256
284
  ? ` @api_websockets path ${profile.exposure.webSocketPaths.join(" ")}\n` +
257
285
  ` handle @api_websockets {\n` +
258
- ` reverse_proxy ${api}\n` +
286
+ reverseProxy(api) +
259
287
  ` }\n`
260
288
  : "";
261
289
  const apiPathRoutes = profile.exposure.apiPaths.length
@@ -264,7 +292,7 @@ function gatewayService(profile, image, release, stateDigest) {
264
292
  .join(" ")}\n` +
265
293
  ` handle @api_paths {\n` +
266
294
  ` uri strip_prefix ${api_paths_1.API_PATH_PREFIX}\n` +
267
- ` reverse_proxy ${api}\n` +
295
+ reverseProxy(api) +
268
296
  ` }\n`
269
297
  : "";
270
298
  const caddyfile = globalOptions +
@@ -272,7 +300,7 @@ function gatewayService(profile, image, release, stateDigest) {
272
300
  webSocketRoutes +
273
301
  apiPathRoutes +
274
302
  ` handle {\n` +
275
- ` reverse_proxy ${serviceName(profile, "web")}:${CONTAINER_PORT}\n` +
303
+ reverseProxy(`${serviceName(profile, "web")}:${CONTAINER_PORT}`) +
276
304
  ` }\n` +
277
305
  `}\n`;
278
306
  const start = "printf '%s' \"$${CADDYFILE_CONTENT}\" > /tmp/Caddyfile\n" +
@@ -292,7 +320,8 @@ function gatewayService(profile, image, release, stateDigest) {
292
320
  ` interval: 10s\n timeout: 5s\n retries: 6\n start_period: 10s\n` +
293
321
  labelsBlock(profile, release, stateDigest, " "));
294
322
  }
295
- function renderComposeDocument(profile, release, images, stateDigest, workerCommand = ["bun", "dist/main-worker.js"]) {
323
+ function renderComposeDocument(profile, release, images, stateDigest, proxyTimeouts, workerCommand = ["bun", "dist/main-worker.js"]) {
324
+ const environment = derivedRuntimeConfig(profile, proxyTimeouts);
296
325
  const services = [];
297
326
  const hasGateway = (0, deploy_compose_profile_1.composeHasGateway)(profile);
298
327
  if (profile.dependencies.postgres.mode === "managed" && images.postgres) {
@@ -307,10 +336,10 @@ function renderComposeDocument(profile, release, images, stateDigest, workerComm
307
336
  services.push(objectStorageInitService(profile, images.objectStorageClient));
308
337
  }
309
338
  }
310
- services.push(applicationService(profile, "api", images.api, release, stateDigest, {
339
+ services.push(applicationService(profile, "api", images.api, release, stateDigest, environment, {
311
340
  healthcheckPath: "/health/ready",
312
341
  }));
313
- services.push(applicationService(profile, "web", images.web, release, stateDigest, {
342
+ services.push(applicationService(profile, "web", images.web, release, stateDigest, environment, {
314
343
  // The web server's own route: an API outage must not mark it unhealthy.
315
344
  healthcheckPath: "/healthz",
316
345
  publish: !hasGateway,
@@ -324,7 +353,7 @@ function renderComposeDocument(profile, release, images, stateDigest, workerComm
324
353
  if (profile.workloads.worker) {
325
354
  // The worker runs the API image with a different entry point, exactly as the
326
355
  // Kubernetes driver does.
327
- services.push(applicationService(profile, "worker", images.api, release, stateDigest, {
356
+ services.push(applicationService(profile, "worker", images.api, release, stateDigest, environment, {
328
357
  command: `[${workerCommand.map((part) => quote(part)).join(", ")}]`,
329
358
  }));
330
359
  }
@@ -349,7 +378,7 @@ function renderComposeDocument(profile, release, images, stateDigest, workerComm
349
378
  * The migration runs as its own Compose project so it can be started, waited on,
350
379
  * and removed without touching the running application definition.
351
380
  */
352
- function renderMigrationDocument(profile, release, images, defaultCommand = ["bun", "run", "migrate:all"]) {
381
+ function renderMigrationDocument(profile, release, images, proxyTimeouts, defaultCommand = ["bun", "run", "migrate:all"]) {
353
382
  const command = profile.migration?.command ?? defaultCommand;
354
383
  return (`# Generated by PodoKit. One-shot migration for ${release}.\n` +
355
384
  `name: ${profile.target.project}-migrate\n` +
@@ -359,7 +388,7 @@ function renderMigrationDocument(profile, release, images, defaultCommand = ["bu
359
388
  ` restart: "no"\n` +
360
389
  ` command: [${command.map((part) => quote(part)).join(", ")}]\n` +
361
390
  ` env_file:\n - ${quote(profile.secrets.api.path)}\n` +
362
- environmentBlock(derivedRuntimeConfig(profile), " ") +
391
+ environmentBlock(derivedRuntimeConfig(profile, proxyTimeouts), " ") +
363
392
  ` networks: [${profile.target.project}]\n` +
364
393
  `networks:\n ${profile.target.project}:\n name: ${profile.target.project}\n external: true\n`);
365
394
  }
@@ -383,13 +412,14 @@ function defaultComposeImages(profile, release) {
383
412
  function renderComposeDeployment(projectRoot, profileName, profile, release, resolvedImages, stateDigest) {
384
413
  (0, deploy_schema_1.profilePath)(projectRoot, profileName);
385
414
  const images = resolvedImages ?? defaultComposeImages(profile, release);
386
- const effectiveStateDigest = stateDigest ?? rolloutStateDigest(profile, {});
415
+ const proxyTimeouts = (0, app_proxy_timeouts_1.readAppProxyTimeouts)(projectRoot);
416
+ const effectiveStateDigest = stateDigest ?? rolloutStateDigest(profile, {}, proxyTimeouts);
387
417
  const root = (0, node_path_1.join)((0, node_path_1.resolve)(projectRoot), ".podokit", "runtime", "deploy", profileName);
388
418
  (0, node_fs_1.rmSync)(root, { recursive: true, force: true });
389
419
  (0, node_fs_1.mkdirSync)(root, { recursive: true });
390
420
  const toolchain = (0, lockfile_1.readManifest)(projectRoot)?.toolchain ?? (0, toolchain_1.resolveToolchain)();
391
- const composeDocument = renderComposeDocument(profile, release, images, effectiveStateDigest, (0, toolchain_1.toolchainWorkerCommand)(toolchain));
392
- const migrationDocument = renderMigrationDocument(profile, release, images, (0, toolchain_1.toolchainMigrationCommand)(toolchain));
421
+ const composeDocument = renderComposeDocument(profile, release, images, effectiveStateDigest, proxyTimeouts, (0, toolchain_1.toolchainWorkerCommand)(toolchain));
422
+ const migrationDocument = renderMigrationDocument(profile, release, images, proxyTimeouts, (0, toolchain_1.toolchainMigrationCommand)(toolchain));
393
423
  const composeFile = (0, node_path_1.join)(root, "compose.yaml");
394
424
  const migrationFile = (0, node_path_1.join)(root, "compose.migrate.yaml");
395
425
  (0, node_fs_1.writeFileSync)(composeFile, composeDocument);
@@ -1,3 +1,4 @@
1
+ import { type DeployFinding } from "./deploy-checks";
1
2
  import type { CommandRunner } from "./dev";
2
3
  import { type ComposeSyncDrift } from "./deploy-compose-sync";
3
4
  import { type ComposeImages } from "./deploy-compose-render";
@@ -10,11 +11,7 @@ import { type ComposeImages } from "./deploy-compose-render";
10
11
  * lock file in a state volume replaces the Kubernetes Lease, and a release ledger in
11
12
  * that same volume replaces Helm revisions.
12
13
  */
13
- export interface ComposeDoctorFinding {
14
- code: string;
15
- ok: boolean;
16
- message: string;
17
- }
14
+ export type ComposeDoctorFinding = DeployFinding;
18
15
  export interface ComposePlanAction {
19
16
  order: number;
20
17
  kind: "dependencies" | "migration" | "application" | "verification" | "rollback";
@@ -12,6 +12,8 @@ exports.composeRuntimeRoot = composeRuntimeRoot;
12
12
  const node_crypto_1 = require("node:crypto");
13
13
  const node_path_1 = require("node:path");
14
14
  const api_paths_1 = require("./api-paths");
15
+ const app_proxy_timeouts_1 = require("./app-proxy-timeouts");
16
+ const deploy_checks_1 = require("./deploy-checks");
15
17
  const deploy_driver_1 = require("./deploy-driver");
16
18
  const deploy_compose_exec_1 = require("./deploy-compose-exec");
17
19
  const deploy_compose_sync_1 = require("./deploy-compose-sync");
@@ -171,7 +173,7 @@ function managedDependencyServices(profile) {
171
173
  }
172
174
  function doctorComposeDeployment(projectRoot, profileName, runner = deploy_compose_exec_1.defaultRunner, release) {
173
175
  const profile = (0, deploy_driver_1.loadComposeProfile)(projectRoot, profileName);
174
- const findings = [];
176
+ const findings = (0, deploy_checks_1.profileFindings)(projectRoot, profile);
175
177
  const record = (code, ok, message) => {
176
178
  findings.push({ code, ok, message });
177
179
  };
@@ -351,7 +353,7 @@ function planComposeDeployment(projectRoot, profileName, release, runner = deplo
351
353
  ]));
352
354
  secretIdentities[label] = output.trim();
353
355
  }
354
- const stateDigest = (0, deploy_compose_render_1.rolloutStateDigest)(profile, secretIdentities);
356
+ const stateDigest = (0, deploy_compose_render_1.rolloutStateDigest)(profile, secretIdentities, (0, app_proxy_timeouts_1.readAppProxyTimeouts)(projectRoot));
355
357
  const runtime = (0, deploy_compose_render_1.renderComposeDeployment)(projectRoot, profileName, profile, release, images, stateDigest);
356
358
  const composeDocumentDigest = digest(["compose-document", runtime.composeDocument]);
357
359
  const psOutput = composePs(profile, runner);
@@ -386,6 +388,9 @@ function planComposeDeployment(projectRoot, profileName, release, runner = deplo
386
388
  warnings.push(`Release ${release} is already the current revision; apply will recreate containers whose definition changed.`);
387
389
  }
388
390
  warnings.push("The migration runs before the new containers start, so it must be compatible with the release currently serving traffic.");
391
+ const budgetWarning = (0, deploy_checks_1.connectionBudgetWarning)(profile, (0, deploy_checks_1.installedModules)(projectRoot));
392
+ if (budgetWarning)
393
+ warnings.push(budgetWarning);
389
394
  const profileDigest = (0, deploy_compose_profile_1.composeProfileDigest)(profile);
390
395
  const planHash = digest([
391
396
  "compose-plan",
@@ -1,4 +1,4 @@
1
- import { MigrationProfile, VerificationCheckProfile } from "./deploy-schema";
1
+ import { MigrationProfile, PostgresBudgetProfile, VerificationCheckProfile } from "./deploy-schema";
2
2
  export { profileDirectory, profilePath } from "./deploy-schema";
3
3
  export type { MigrationProfile, VerificationCheckProfile } from "./deploy-schema";
4
4
  export type DependencyMode = "inCluster" | "external" | "disabled";
@@ -20,6 +20,8 @@ export interface StatefulDependencyProfile {
20
20
  storageSize: string;
21
21
  }
22
22
  export type RedisDependencyProfile = StatefulDependencyProfile;
23
+ export interface PostgresDependencyProfile extends StatefulDependencyProfile, PostgresBudgetProfile {
24
+ }
23
25
  export interface ObjectStorageDependencyProfile extends StatefulDependencyProfile {
24
26
  bucket: string;
25
27
  clientImage: string;
@@ -52,6 +54,8 @@ export interface DeployProfileV1 {
52
54
  webSocketPaths: string[];
53
55
  /** Path prefixes under /api/ that the Ingress sends to the API with /api removed. */
54
56
  apiPaths: string[];
57
+ /** Reverse proxies in front of the Ingress controller or NodePort that append to X-Forwarded-For. */
58
+ edgeProxies: number;
55
59
  };
56
60
  workloads: {
57
61
  api: WorkloadProfile;
@@ -59,7 +63,7 @@ export interface DeployProfileV1 {
59
63
  worker: WorkloadProfile | null;
60
64
  };
61
65
  dependencies: {
62
- postgres: StatefulDependencyProfile;
66
+ postgres: PostgresDependencyProfile;
63
67
  redis: RedisDependencyProfile;
64
68
  objectStorage: ObjectStorageDependencyProfile;
65
69
  };
@@ -119,7 +119,7 @@ function parseDeploymentProfile(value) {
119
119
  const runtimeConfig = (0, deploy_schema_1.requiredRecord)(value, "runtimeConfig");
120
120
  (0, deploy_schema_1.assertOnlyKeys)(target, ["context", "clusterFingerprint", "namespace", "release", "createNamespace"], "target");
121
121
  (0, deploy_schema_1.assertOnlyKeys)(release, ["strategy", "tagPattern", "apiRepository", "webRepository"], "release");
122
- (0, deploy_schema_1.assertOnlyKeys)(exposure, ["mode", "host", "ingressClassName", "tlsSecretName", "webSocketPaths", "apiPaths"], "exposure");
122
+ (0, deploy_schema_1.assertOnlyKeys)(exposure, ["mode", "host", "ingressClassName", "tlsSecretName", "webSocketPaths", "apiPaths", "edgeProxies"], "exposure");
123
123
  (0, deploy_schema_1.assertOnlyKeys)(workloads, ["api", "web", "worker"], "workloads");
124
124
  (0, deploy_schema_1.assertOnlyKeys)(dependencies, ["postgres", "redis", "objectStorage"], "dependencies");
125
125
  (0, deploy_schema_1.assertOnlyKeys)(secrets, ["api", "web", "imagePull"], "secrets");
@@ -173,7 +173,12 @@ function parseDeploymentProfile(value) {
173
173
  if (exposureMode === "nodePort" && apiPaths.length > 0) {
174
174
  throw new Error("Deployment exposure.apiPaths requires exposure.mode ingress.");
175
175
  }
176
- const postgres = parseStatefulDependency((0, deploy_schema_1.requiredRecord)(dependencies, "postgres"), "dependencies.postgres");
176
+ const parsedConfig = (0, deploy_schema_1.parseRuntimeConfig)(runtimeConfig);
177
+ const postgresRecord = (0, deploy_schema_1.requiredRecord)(dependencies, "postgres");
178
+ const postgres = {
179
+ ...parseStatefulDependency(postgresRecord, "dependencies.postgres", ["maxConnections", "appPools"]),
180
+ ...(0, deploy_schema_1.parsePostgresBudget)(postgresRecord, "dependencies.postgres", parsedConfig),
181
+ };
177
182
  const redis = parseRedisDependency((0, deploy_schema_1.requiredRecord)(dependencies, "redis"), "dependencies.redis");
178
183
  const objectStorageRecord = (0, deploy_schema_1.requiredRecord)(dependencies, "objectStorage");
179
184
  (0, deploy_schema_1.assertOnlyKeys)(objectStorageRecord, [
@@ -192,7 +197,6 @@ function parseDeploymentProfile(value) {
192
197
  };
193
198
  (0, deploy_schema_1.validateDnsLabel)(objectStorage.bucket, "dependencies.objectStorage.bucket");
194
199
  (0, deploy_schema_1.validateImage)(objectStorage.clientImage, "dependencies.objectStorage.clientImage");
195
- const parsedConfig = (0, deploy_schema_1.parseRuntimeConfig)(runtimeConfig);
196
200
  const { baseUrl, checks } = (0, deploy_schema_1.parseVerification)(verification);
197
201
  const profile = {
198
202
  schemaVersion: 1,
@@ -217,6 +221,7 @@ function parseDeploymentProfile(value) {
217
221
  tlsSecretName,
218
222
  webSocketPaths,
219
223
  apiPaths,
224
+ edgeProxies: (0, deploy_schema_1.parseEdgeProxies)(exposure, 0),
220
225
  },
221
226
  workloads: {
222
227
  api: parseWorkload((0, deploy_schema_1.requiredRecord)(workloads, "api"), "workloads.api"),
@@ -327,6 +332,7 @@ function initializeDeploymentProfile(projectRoot, name, options) {
327
332
  tlsSecretName: null,
328
333
  webSocketPaths: [],
329
334
  apiPaths: (0, api_paths_1.installedApiPaths)(manifest.modules),
335
+ edgeProxies: 0,
330
336
  },
331
337
  workloads: {
332
338
  api: {
@@ -395,9 +401,9 @@ function initializeDeploymentProfile(projectRoot, name, options) {
395
401
  runtimeConfig: {
396
402
  NODE_ENV: "production",
397
403
  CORS_ORIGIN: `https://${host}`,
398
- ADDRESS_HEADER: "x-forwarded-for",
399
- XFF_DEPTH: "1",
400
404
  ...(usesAuth ? { BETTER_AUTH_URL: `https://${host}` } : {}),
405
+ // The API's fallback until an administrator saves the setting.
406
+ ...(modules.has("audit-log") ? { AUDIT_LOG_ENABLED: "true" } : {}),
401
407
  ...(modules.has("rate-limit")
402
408
  ? {
403
409
  RATE_LIMIT_TTL: "60",
@@ -406,8 +412,6 @@ function initializeDeploymentProfile(projectRoot, name, options) {
406
412
  RATE_LIMIT_AUTH_TTL: "60",
407
413
  RATE_LIMIT_AUTH_MAX: "20",
408
414
  RATE_LIMIT_RUNTIME_MAX: "1000",
409
- RATE_LIMIT_TRUSTED_PROXY_HOPS: "1",
410
- RATE_LIMIT_PROXY_HEADER: "x-forwarded-for",
411
415
  RATE_LIMIT_STORAGE_TIMEOUT_MS: "1000",
412
416
  RATE_LIMIT_UNAVAILABLE_RETRY_AFTER: "1",
413
417
  }
@@ -1,4 +1,5 @@
1
1
  import type { DeployProfileV1 } from "./deploy-profile";
2
+ import { type AppProxyTimeouts } from "./app-proxy-timeouts";
2
3
  export interface DeploymentRuntime {
3
4
  root: string;
4
5
  dependencyChart: string;
@@ -15,4 +16,6 @@ export interface DeploymentImages {
15
16
  objectStorage: string | null;
16
17
  objectStorageClient: string | null;
17
18
  }
18
- export declare function renderDeployment(projectRoot: string, profileName: string, profile: DeployProfileV1, release: string, resolvedImages?: DeploymentImages, rolloutStateDigest?: string): DeploymentRuntime;
19
+ /** The Ingress controller appends its peer: the client, or the last edge proxy. */
20
+ export declare function kubernetesProxyTrust(profile: DeployProfileV1): Record<string, string>;
21
+ export declare function renderDeployment(projectRoot: string, profileName: string, profile: DeployProfileV1, release: string, resolvedImages?: DeploymentImages, rolloutStateDigest?: string, proxyTimeouts?: AppProxyTimeouts): DeploymentRuntime;
@@ -1,11 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.kubernetesProxyTrust = kubernetesProxyTrust;
3
4
  exports.renderDeployment = renderDeployment;
4
5
  const node_fs_1 = require("node:fs");
5
6
  const node_crypto_1 = require("node:crypto");
6
7
  const node_path_1 = require("node:path");
7
8
  const deploy_profile_1 = require("./deploy-profile");
8
9
  const api_paths_1 = require("./api-paths");
10
+ const app_proxy_timeouts_1 = require("./app-proxy-timeouts");
11
+ const deploy_schema_1 = require("./deploy-schema");
9
12
  const lockfile_1 = require("./lockfile");
10
13
  const toolchain_1 = require("./toolchain");
11
14
  function quote(value) {
@@ -111,7 +114,7 @@ ${labels(name, 8)} spec:
111
114
  - name: postgres
112
115
  image: ${quote(image ?? dependency.image)}
113
116
  imagePullPolicy: IfNotPresent
114
- env:
117
+ ${dependency.maxConnections === undefined ? "" : ` args: ["-c", ${quote(`max_connections=${dependency.maxConnections}`)}]\n`} env:
115
118
  - name: POSTGRES_USER
116
119
  valueFrom:
117
120
  secretKeyRef: { name: ${dependency.secretName}, key: POSTGRES_USER }
@@ -350,12 +353,16 @@ function dependencyManifest(profile, images, rolloutStateDigest) {
350
353
  .filter(Boolean)
351
354
  .join("\n---\n");
352
355
  }
353
- function derivedRuntimeConfig(profile) {
356
+ /** The Ingress controller appends its peer: the client, or the last edge proxy. */
357
+ function kubernetesProxyTrust(profile) {
358
+ return (0, deploy_schema_1.proxyTrustConfig)(profile.exposure.edgeProxies + (profile.exposure.mode === "ingress" ? 1 : 0));
359
+ }
360
+ function derivedRuntimeConfig(profile, proxyTimeouts) {
354
361
  const result = {
355
362
  PORT: "3000",
356
363
  BACKEND_INTERNAL_URL: `http://${profile.target.release}-api:3000`,
357
- PROTOCOL_HEADER: "x-forwarded-proto",
358
- HOST_HEADER: "x-forwarded-host",
364
+ ...(0, deploy_schema_1.idleTimeoutConfig)(profile.runtimeConfig, proxyTimeouts),
365
+ ...kubernetesProxyTrust(profile),
359
366
  ...profile.runtimeConfig,
360
367
  };
361
368
  if (profile.dependencies.postgres.mode === "inCluster") {
@@ -373,12 +380,11 @@ function derivedRuntimeConfig(profile) {
373
380
  }
374
381
  return result;
375
382
  }
376
- function derivedRuntimeConfigDigest(profile) {
377
- const entries = Object.entries(derivedRuntimeConfig(profile)).sort(([left], [right]) => left.localeCompare(right));
378
- return digest(["runtime-config", JSON.stringify(entries)]);
383
+ function sortedRuntimeConfig(runtimeConfig) {
384
+ return Object.entries(runtimeConfig).sort(([left], [right]) => left.localeCompare(right));
379
385
  }
380
- function configMap(profile) {
381
- const entries = Object.entries(derivedRuntimeConfig(profile)).sort(([left], [right]) => left.localeCompare(right));
386
+ function configMap(profile, runtimeConfig) {
387
+ const entries = sortedRuntimeConfig(runtimeConfig);
382
388
  return `apiVersion: v1
383
389
  kind: ConfigMap
384
390
  metadata:
@@ -626,10 +632,10 @@ ${webSocketPaths} - path: /
626
632
  port: { number: 3000 }
627
633
  ${apiPathIngress(profile, tls)}`;
628
634
  }
629
- function applicationManifest(profile, images, rolloutStateDigest, workerCommand) {
630
- const runtimeConfigDigest = derivedRuntimeConfigDigest(profile);
635
+ function applicationManifest(profile, images, rolloutStateDigest, workerCommand, runtimeConfig) {
636
+ const runtimeConfigDigest = digest(["runtime-config", JSON.stringify(sortedRuntimeConfig(runtimeConfig))]);
631
637
  return [
632
- configMap(profile),
638
+ configMap(profile, runtimeConfig),
633
639
  deployment(profile, "api", profile.workloads.api, images.api, rolloutStateDigest, runtimeConfigDigest),
634
640
  deployment(profile, "web", profile.workloads.web, images.web, rolloutStateDigest, runtimeConfigDigest),
635
641
  workerDeployment(profile, images.api, rolloutStateDigest, runtimeConfigDigest, workerCommand),
@@ -641,14 +647,13 @@ function applicationManifest(profile, images, rolloutStateDigest, workerCommand)
641
647
  .filter(Boolean)
642
648
  .join("\n---\n");
643
649
  }
644
- function migrationRuntimeEnv(profile) {
645
- return Object.entries(derivedRuntimeConfig(profile))
646
- .sort(([left], [right]) => left.localeCompare(right))
650
+ function migrationRuntimeEnv(runtimeConfig) {
651
+ return sortedRuntimeConfig(runtimeConfig)
647
652
  .map(([key, value]) => ` - name: ${key}
648
653
  value: ${quote(value)}`)
649
654
  .join("\n");
650
655
  }
651
- function migrationManifest(profile, release, images, defaultCommand) {
656
+ function migrationManifest(profile, release, images, defaultCommand, runtimeConfig) {
652
657
  const releaseSlug = release.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
653
658
  const command = profile.migration?.command ?? defaultCommand;
654
659
  return `apiVersion: batch/v1
@@ -678,7 +683,7 @@ ${labels(`${profile.target.release}-migration`, 8)} spec:
678
683
  imagePullPolicy: IfNotPresent
679
684
  command: ${yamlStringSequence(command)}
680
685
  env:
681
- ${migrationRuntimeEnv(profile)}
686
+ ${migrationRuntimeEnv(runtimeConfig)}
682
687
  envFrom:
683
688
  - secretRef: { name: ${profile.secrets.api.name} }
684
689
  securityContext:
@@ -692,7 +697,9 @@ function chart(directory, name, manifest) {
692
697
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(directory, "resources.yaml"), manifest || "# No resources\n");
693
698
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(directory, "templates", "resources.yaml"), '{{ .Files.Get "resources.yaml" }}\n');
694
699
  }
695
- function renderDeployment(projectRoot, profileName, profile, release, resolvedImages, rolloutStateDigest) {
700
+ function renderDeployment(projectRoot, profileName, profile, release, resolvedImages, rolloutStateDigest,
701
+ // Planning renders into a scratch directory, so it reads these from the project.
702
+ proxyTimeouts = (0, app_proxy_timeouts_1.readAppProxyTimeouts)(projectRoot)) {
696
703
  (0, deploy_profile_1.profilePath)(projectRoot, profileName);
697
704
  const images = resolvedImages ?? {
698
705
  api: `${profile.release.apiRepository}:${release}`,
@@ -717,8 +724,9 @@ function renderDeployment(projectRoot, profileName, profile, release, resolvedIm
717
724
  const effectiveRolloutStateDigest = rolloutStateDigest ?? offlineRolloutStateDigest(profile);
718
725
  const dependencies = dependencyManifest(profile, images, effectiveRolloutStateDigest);
719
726
  const toolchain = (0, lockfile_1.readManifest)(projectRoot)?.toolchain ?? (0, toolchain_1.resolveToolchain)();
720
- const application = applicationManifest(profile, images, effectiveRolloutStateDigest, (0, toolchain_1.toolchainWorkerCommand)(toolchain));
721
- const migration = migrationManifest(profile, release, images, (0, toolchain_1.toolchainMigrationCommand)(toolchain));
727
+ const runtimeConfig = derivedRuntimeConfig(profile, proxyTimeouts);
728
+ const application = applicationManifest(profile, images, effectiveRolloutStateDigest, (0, toolchain_1.toolchainWorkerCommand)(toolchain), runtimeConfig);
729
+ const migration = migrationManifest(profile, release, images, (0, toolchain_1.toolchainMigrationCommand)(toolchain), runtimeConfig);
722
730
  chart(dependencyChart, "podokit-dependencies", dependencies);
723
731
  chart(applicationChart, "podokit-application", application);
724
732
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, "migration.yaml"), migration);