@podosoft/podokit 1.5.0 → 1.6.1

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 (215) 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 -18
  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 +27 -21
  18. package/dist/deploy-schema.d.ts +60 -2
  19. package/dist/deploy-schema.js +141 -14
  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 +74 -4
  30. package/dist/provider.js +10 -5
  31. package/dist/remove.js +1 -1
  32. package/dist/template-files.d.ts +42 -0
  33. package/dist/template-files.js +102 -0
  34. package/dist/templates/fullstack/AGENTS.md +30 -4
  35. package/dist/templates/fullstack/README.md +8 -7
  36. package/dist/templates/fullstack/apps/api/package.json +4 -4
  37. package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
  38. package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
  39. package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
  40. package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +7 -2
  41. package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
  42. package/dist/templates/fullstack/apps/api/src/app.ts +41 -25
  43. package/dist/templates/fullstack/apps/api/src/build.spec.ts +121 -0
  44. package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
  45. package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
  46. package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +18 -3
  47. package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
  48. package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
  49. package/dist/templates/fullstack/apps/api/src/config/providers.ts +7 -3
  50. package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
  51. package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +72 -4
  52. package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +78 -6
  53. package/dist/templates/fullstack/apps/api/src/core/services.ts +13 -0
  54. package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
  55. package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
  56. package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
  57. package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
  58. package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
  59. package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
  60. package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
  61. package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
  62. package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
  63. package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
  64. package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
  65. package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
  66. package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
  67. package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
  68. package/dist/templates/fullstack/apps/web/package.json +1 -1
  69. package/dist/templates/fullstack/apps/web/src/app.css +3 -0
  70. package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
  71. package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
  72. package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
  73. package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
  74. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
  75. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +55 -1
  76. package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +2 -1
  77. package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
  78. package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
  79. package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
  80. package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
  81. package/dist/templates/fullstack/compose.dev.yaml +7 -4
  82. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
  83. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +13 -3
  84. package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
  85. package/dist/templates/fullstack/dot-env.docker.example +5 -2
  86. package/dist/templates/fullstack/dot-env.example +4 -0
  87. package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
  88. package/dist/templates/fullstack/infra/k3s/configmap.yaml +1 -0
  89. package/dist/templates/fullstack/tests/README.md +50 -1
  90. package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
  91. package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
  92. package/dist/templates/fullstack/tests/helpers/seeded-admin.ts +15 -0
  93. package/dist/templates/fullstack/tests/package.json +3 -3
  94. package/dist/templates/fullstack/tests/playwright.config.ts +10 -4
  95. package/dist/templates/fullstack/tests/playwright.extensions.ts +149 -1
  96. package/dist/templates/fullstack/tests/run-e2e.ts +28 -0
  97. package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
  98. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
  99. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
  100. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
  101. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
  102. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
  103. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
  104. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +53 -0
  105. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
  106. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
  107. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +11 -1
  108. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
  109. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
  110. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
  111. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
  112. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
  113. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
  114. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
  115. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
  116. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
  117. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/theme-toggle.svelte +1 -1
  118. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
  119. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
  120. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
  121. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
  122. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
  123. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
  124. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
  125. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
  126. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
  127. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
  128. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
  129. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
  130. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
  131. package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
  132. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
  133. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
  134. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +3 -1
  135. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
  136. package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
  137. package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +41 -11
  138. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
  139. package/dist/templates/modules/admin-dashboard/files/tests/helpers/hydration.ts +13 -4
  140. package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
  141. package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
  142. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +14 -6
  143. package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
  144. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +55 -55
  145. package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
  146. package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
  147. package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
  148. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
  149. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
  150. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
  151. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
  152. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
  153. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
  154. package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
  155. package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
  156. package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
  157. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
  158. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
  159. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
  160. package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
  161. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
  162. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
  163. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
  164. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +8 -7
  165. package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +35 -0
  166. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
  167. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
  168. package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
  169. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
  170. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +17 -0
  171. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +18 -0
  172. package/dist/templates/modules/audit-log/module.manifest.json +15 -3
  173. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
  174. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
  175. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
  176. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
  177. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +2 -0
  178. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
  179. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +7 -5
  180. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
  181. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
  182. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
  183. package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
  184. package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
  185. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
  186. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
  187. package/dist/templates/modules/auth/module.manifest.json +8 -2
  188. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +107 -1
  189. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +113 -9
  190. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
  191. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
  192. package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
  193. package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
  194. package/dist/templates/modules/bullmq/module.manifest.json +5 -1
  195. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
  196. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +174 -1
  197. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +150 -3
  198. package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
  199. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
  200. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
  201. package/dist/templates/modules/mailer/module.manifest.json +1 -1
  202. package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +3 -6
  203. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
  204. package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +3 -6
  205. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +45 -20
  206. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +3 -6
  207. package/dist/templates/todo/AGENTS.md +5 -1
  208. package/dist/templates/todo/README.md +6 -4
  209. package/dist/text-diff.d.ts +12 -0
  210. package/dist/text-diff.js +111 -0
  211. package/dist/update.d.ts +61 -3
  212. package/dist/update.js +152 -27
  213. package/package.json +2 -2
  214. package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
  215. 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,10 +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
- TRUSTED_PROXY_HOPS: "1",
361
- TRUSTED_PROXY_HEADER: "x-forwarded-for",
362
369
  },
363
370
  verification: {
364
371
  baseUrl: `https://${host}`,
@@ -412,6 +419,8 @@ function initializeComposeProfile(projectRoot, name, options) {
412
419
  draft.runtimeConfig = {
413
420
  ...draft.runtimeConfig,
414
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" } : {}),
415
424
  ...(modules.has("rate-limit")
416
425
  ? {
417
426
  RATE_LIMIT_TTL: "60",
@@ -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,11 +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}`,
40
- ...(0, deploy_schema_1.idleTimeoutConfig)(profile.runtimeConfig),
42
+ ...(0, deploy_schema_1.idleTimeoutConfig)(profile.runtimeConfig, proxyTimeouts),
43
+ ...composeProxyTrust(profile),
41
44
  ...profile.runtimeConfig,
42
45
  };
43
46
  if (profile.dependencies.postgres.mode === "managed") {
@@ -55,6 +58,18 @@ function derivedRuntimeConfig(profile) {
55
58
  }
56
59
  return result;
57
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
+ }
58
73
  function sortedEnvironment(config) {
59
74
  return Object.entries(config).sort(([left], [right]) => left.localeCompare(right));
60
75
  }
@@ -74,10 +89,10 @@ function environmentBlock(config, indent) {
74
89
  * secret or a changed runtime config a definition change, so `up` actually rolls
75
90
  * the containers instead of leaving stale values running.
76
91
  */
77
- function rolloutStateDigest(profile, secretIdentities) {
92
+ function rolloutStateDigest(profile, secretIdentities, proxyTimeouts) {
78
93
  return digest([
79
94
  "compose-rollout-state",
80
- JSON.stringify(sortedEnvironment(derivedRuntimeConfig(profile))),
95
+ JSON.stringify(sortedEnvironment(derivedRuntimeConfig(profile, proxyTimeouts))),
81
96
  JSON.stringify(Object.entries(secretIdentities).sort(([l], [r]) => l.localeCompare(r))),
82
97
  ]);
83
98
  }
@@ -121,8 +136,13 @@ function dependencyService(profile, role, dependency, image, extra) {
121
136
  extra.healthcheck);
122
137
  }
123
138
  function postgresService(profile, image) {
139
+ const { maxConnections } = profile.dependencies.postgres;
124
140
  return dependencyService(profile, "postgres", profile.dependencies.postgres, image, {
125
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}`)}]` }),
126
146
  healthcheck: ` healthcheck:\n` +
127
147
  ` test: ["CMD-SHELL", "pg_isready -U \\"$$POSTGRES_USER\\" -d \\"$$POSTGRES_DB\\""]\n` +
128
148
  ` interval: 10s\n timeout: 5s\n retries: 10\n start_period: 20s\n`,
@@ -196,7 +216,7 @@ function objectStorageInitService(profile, clientImage) {
196
216
  ` entrypoint: ["sh", "-c", ${quote(script)}]\n` +
197
217
  ` networks: [${profile.target.project}]\n`);
198
218
  }
199
- function applicationService(profile, role, image, release, stateDigest, options) {
219
+ function applicationService(profile, role, image, release, stateDigest, environment, options) {
200
220
  const workload = role === "worker" ? profile.workloads.worker : profile.workloads[role];
201
221
  if (!workload)
202
222
  return "";
@@ -222,7 +242,7 @@ function applicationService(profile, role, image, release, stateDigest, options)
222
242
  ` restart: unless-stopped\n` +
223
243
  (options.command ? ` command: ${options.command}\n` : "") +
224
244
  (envFile ? ` env_file:\n - ${quote(envFile.path)}\n` : "") +
225
- environmentBlock(derivedRuntimeConfig(profile), " ") +
245
+ environmentBlock(environment, " ") +
226
246
  (dependsOn.length
227
247
  ? ` depends_on:\n${dependsOn
228
248
  .map(([name, condition]) => ` ${name}:\n condition: ${condition}\n`)
@@ -253,10 +273,17 @@ function gatewayService(profile, image, release, stateDigest) {
253
273
  `
254
274
  : "";
255
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`;
256
283
  const webSocketRoutes = profile.exposure.webSocketPaths.length
257
284
  ? ` @api_websockets path ${profile.exposure.webSocketPaths.join(" ")}\n` +
258
285
  ` handle @api_websockets {\n` +
259
- ` reverse_proxy ${api}\n` +
286
+ reverseProxy(api) +
260
287
  ` }\n`
261
288
  : "";
262
289
  const apiPathRoutes = profile.exposure.apiPaths.length
@@ -265,7 +292,7 @@ function gatewayService(profile, image, release, stateDigest) {
265
292
  .join(" ")}\n` +
266
293
  ` handle @api_paths {\n` +
267
294
  ` uri strip_prefix ${api_paths_1.API_PATH_PREFIX}\n` +
268
- ` reverse_proxy ${api}\n` +
295
+ reverseProxy(api) +
269
296
  ` }\n`
270
297
  : "";
271
298
  const caddyfile = globalOptions +
@@ -273,7 +300,7 @@ function gatewayService(profile, image, release, stateDigest) {
273
300
  webSocketRoutes +
274
301
  apiPathRoutes +
275
302
  ` handle {\n` +
276
- ` reverse_proxy ${serviceName(profile, "web")}:${CONTAINER_PORT}\n` +
303
+ reverseProxy(`${serviceName(profile, "web")}:${CONTAINER_PORT}`) +
277
304
  ` }\n` +
278
305
  `}\n`;
279
306
  const start = "printf '%s' \"$${CADDYFILE_CONTENT}\" > /tmp/Caddyfile\n" +
@@ -293,7 +320,8 @@ function gatewayService(profile, image, release, stateDigest) {
293
320
  ` interval: 10s\n timeout: 5s\n retries: 6\n start_period: 10s\n` +
294
321
  labelsBlock(profile, release, stateDigest, " "));
295
322
  }
296
- 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);
297
325
  const services = [];
298
326
  const hasGateway = (0, deploy_compose_profile_1.composeHasGateway)(profile);
299
327
  if (profile.dependencies.postgres.mode === "managed" && images.postgres) {
@@ -308,10 +336,10 @@ function renderComposeDocument(profile, release, images, stateDigest, workerComm
308
336
  services.push(objectStorageInitService(profile, images.objectStorageClient));
309
337
  }
310
338
  }
311
- services.push(applicationService(profile, "api", images.api, release, stateDigest, {
339
+ services.push(applicationService(profile, "api", images.api, release, stateDigest, environment, {
312
340
  healthcheckPath: "/health/ready",
313
341
  }));
314
- services.push(applicationService(profile, "web", images.web, release, stateDigest, {
342
+ services.push(applicationService(profile, "web", images.web, release, stateDigest, environment, {
315
343
  // The web server's own route: an API outage must not mark it unhealthy.
316
344
  healthcheckPath: "/healthz",
317
345
  publish: !hasGateway,
@@ -325,7 +353,7 @@ function renderComposeDocument(profile, release, images, stateDigest, workerComm
325
353
  if (profile.workloads.worker) {
326
354
  // The worker runs the API image with a different entry point, exactly as the
327
355
  // Kubernetes driver does.
328
- services.push(applicationService(profile, "worker", images.api, release, stateDigest, {
356
+ services.push(applicationService(profile, "worker", images.api, release, stateDigest, environment, {
329
357
  command: `[${workerCommand.map((part) => quote(part)).join(", ")}]`,
330
358
  }));
331
359
  }
@@ -350,7 +378,7 @@ function renderComposeDocument(profile, release, images, stateDigest, workerComm
350
378
  * The migration runs as its own Compose project so it can be started, waited on,
351
379
  * and removed without touching the running application definition.
352
380
  */
353
- function renderMigrationDocument(profile, release, images, defaultCommand = ["bun", "run", "migrate:all"]) {
381
+ function renderMigrationDocument(profile, release, images, proxyTimeouts, defaultCommand = ["bun", "run", "migrate:all"]) {
354
382
  const command = profile.migration?.command ?? defaultCommand;
355
383
  return (`# Generated by PodoKit. One-shot migration for ${release}.\n` +
356
384
  `name: ${profile.target.project}-migrate\n` +
@@ -360,7 +388,7 @@ function renderMigrationDocument(profile, release, images, defaultCommand = ["bu
360
388
  ` restart: "no"\n` +
361
389
  ` command: [${command.map((part) => quote(part)).join(", ")}]\n` +
362
390
  ` env_file:\n - ${quote(profile.secrets.api.path)}\n` +
363
- environmentBlock(derivedRuntimeConfig(profile), " ") +
391
+ environmentBlock(derivedRuntimeConfig(profile, proxyTimeouts), " ") +
364
392
  ` networks: [${profile.target.project}]\n` +
365
393
  `networks:\n ${profile.target.project}:\n name: ${profile.target.project}\n external: true\n`);
366
394
  }
@@ -384,13 +412,14 @@ function defaultComposeImages(profile, release) {
384
412
  function renderComposeDeployment(projectRoot, profileName, profile, release, resolvedImages, stateDigest) {
385
413
  (0, deploy_schema_1.profilePath)(projectRoot, profileName);
386
414
  const images = resolvedImages ?? defaultComposeImages(profile, release);
387
- const effectiveStateDigest = stateDigest ?? rolloutStateDigest(profile, {});
415
+ const proxyTimeouts = (0, app_proxy_timeouts_1.readAppProxyTimeouts)(projectRoot);
416
+ const effectiveStateDigest = stateDigest ?? rolloutStateDigest(profile, {}, proxyTimeouts);
388
417
  const root = (0, node_path_1.join)((0, node_path_1.resolve)(projectRoot), ".podokit", "runtime", "deploy", profileName);
389
418
  (0, node_fs_1.rmSync)(root, { recursive: true, force: true });
390
419
  (0, node_fs_1.mkdirSync)(root, { recursive: true });
391
420
  const toolchain = (0, lockfile_1.readManifest)(projectRoot)?.toolchain ?? (0, toolchain_1.resolveToolchain)();
392
- const composeDocument = renderComposeDocument(profile, release, images, effectiveStateDigest, (0, toolchain_1.toolchainWorkerCommand)(toolchain));
393
- 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));
394
423
  const composeFile = (0, node_path_1.join)(root, "compose.yaml");
395
424
  const migrationFile = (0, node_path_1.join)(root, "compose.migrate.yaml");
396
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,11 +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
- TRUSTED_PROXY_HOPS: "1",
401
- TRUSTED_PROXY_HEADER: "x-forwarded-for",
402
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" } : {}),
403
407
  ...(modules.has("rate-limit")
404
408
  ? {
405
409
  RATE_LIMIT_TTL: "60",
@@ -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,13 @@
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");
9
11
  const deploy_schema_1 = require("./deploy-schema");
10
12
  const lockfile_1 = require("./lockfile");
11
13
  const toolchain_1 = require("./toolchain");
@@ -112,7 +114,7 @@ ${labels(name, 8)} spec:
112
114
  - name: postgres
113
115
  image: ${quote(image ?? dependency.image)}
114
116
  imagePullPolicy: IfNotPresent
115
- env:
117
+ ${dependency.maxConnections === undefined ? "" : ` args: ["-c", ${quote(`max_connections=${dependency.maxConnections}`)}]\n`} env:
116
118
  - name: POSTGRES_USER
117
119
  valueFrom:
118
120
  secretKeyRef: { name: ${dependency.secretName}, key: POSTGRES_USER }
@@ -351,13 +353,16 @@ function dependencyManifest(profile, images, rolloutStateDigest) {
351
353
  .filter(Boolean)
352
354
  .join("\n---\n");
353
355
  }
354
- 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) {
355
361
  const result = {
356
362
  PORT: "3000",
357
363
  BACKEND_INTERNAL_URL: `http://${profile.target.release}-api:3000`,
358
- PROTOCOL_HEADER: "x-forwarded-proto",
359
- HOST_HEADER: "x-forwarded-host",
360
- ...(0, deploy_schema_1.idleTimeoutConfig)(profile.runtimeConfig),
364
+ ...(0, deploy_schema_1.idleTimeoutConfig)(profile.runtimeConfig, proxyTimeouts),
365
+ ...kubernetesProxyTrust(profile),
361
366
  ...profile.runtimeConfig,
362
367
  };
363
368
  if (profile.dependencies.postgres.mode === "inCluster") {
@@ -375,12 +380,11 @@ function derivedRuntimeConfig(profile) {
375
380
  }
376
381
  return result;
377
382
  }
378
- function derivedRuntimeConfigDigest(profile) {
379
- const entries = Object.entries(derivedRuntimeConfig(profile)).sort(([left], [right]) => left.localeCompare(right));
380
- return digest(["runtime-config", JSON.stringify(entries)]);
383
+ function sortedRuntimeConfig(runtimeConfig) {
384
+ return Object.entries(runtimeConfig).sort(([left], [right]) => left.localeCompare(right));
381
385
  }
382
- function configMap(profile) {
383
- const entries = Object.entries(derivedRuntimeConfig(profile)).sort(([left], [right]) => left.localeCompare(right));
386
+ function configMap(profile, runtimeConfig) {
387
+ const entries = sortedRuntimeConfig(runtimeConfig);
384
388
  return `apiVersion: v1
385
389
  kind: ConfigMap
386
390
  metadata:
@@ -628,10 +632,10 @@ ${webSocketPaths} - path: /
628
632
  port: { number: 3000 }
629
633
  ${apiPathIngress(profile, tls)}`;
630
634
  }
631
- function applicationManifest(profile, images, rolloutStateDigest, workerCommand) {
632
- const runtimeConfigDigest = derivedRuntimeConfigDigest(profile);
635
+ function applicationManifest(profile, images, rolloutStateDigest, workerCommand, runtimeConfig) {
636
+ const runtimeConfigDigest = digest(["runtime-config", JSON.stringify(sortedRuntimeConfig(runtimeConfig))]);
633
637
  return [
634
- configMap(profile),
638
+ configMap(profile, runtimeConfig),
635
639
  deployment(profile, "api", profile.workloads.api, images.api, rolloutStateDigest, runtimeConfigDigest),
636
640
  deployment(profile, "web", profile.workloads.web, images.web, rolloutStateDigest, runtimeConfigDigest),
637
641
  workerDeployment(profile, images.api, rolloutStateDigest, runtimeConfigDigest, workerCommand),
@@ -643,14 +647,13 @@ function applicationManifest(profile, images, rolloutStateDigest, workerCommand)
643
647
  .filter(Boolean)
644
648
  .join("\n---\n");
645
649
  }
646
- function migrationRuntimeEnv(profile) {
647
- return Object.entries(derivedRuntimeConfig(profile))
648
- .sort(([left], [right]) => left.localeCompare(right))
650
+ function migrationRuntimeEnv(runtimeConfig) {
651
+ return sortedRuntimeConfig(runtimeConfig)
649
652
  .map(([key, value]) => ` - name: ${key}
650
653
  value: ${quote(value)}`)
651
654
  .join("\n");
652
655
  }
653
- function migrationManifest(profile, release, images, defaultCommand) {
656
+ function migrationManifest(profile, release, images, defaultCommand, runtimeConfig) {
654
657
  const releaseSlug = release.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
655
658
  const command = profile.migration?.command ?? defaultCommand;
656
659
  return `apiVersion: batch/v1
@@ -680,7 +683,7 @@ ${labels(`${profile.target.release}-migration`, 8)} spec:
680
683
  imagePullPolicy: IfNotPresent
681
684
  command: ${yamlStringSequence(command)}
682
685
  env:
683
- ${migrationRuntimeEnv(profile)}
686
+ ${migrationRuntimeEnv(runtimeConfig)}
684
687
  envFrom:
685
688
  - secretRef: { name: ${profile.secrets.api.name} }
686
689
  securityContext:
@@ -694,7 +697,9 @@ function chart(directory, name, manifest) {
694
697
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(directory, "resources.yaml"), manifest || "# No resources\n");
695
698
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(directory, "templates", "resources.yaml"), '{{ .Files.Get "resources.yaml" }}\n');
696
699
  }
697
- 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)) {
698
703
  (0, deploy_profile_1.profilePath)(projectRoot, profileName);
699
704
  const images = resolvedImages ?? {
700
705
  api: `${profile.release.apiRepository}:${release}`,
@@ -719,8 +724,9 @@ function renderDeployment(projectRoot, profileName, profile, release, resolvedIm
719
724
  const effectiveRolloutStateDigest = rolloutStateDigest ?? offlineRolloutStateDigest(profile);
720
725
  const dependencies = dependencyManifest(profile, images, effectiveRolloutStateDigest);
721
726
  const toolchain = (0, lockfile_1.readManifest)(projectRoot)?.toolchain ?? (0, toolchain_1.resolveToolchain)();
722
- const application = applicationManifest(profile, images, effectiveRolloutStateDigest, (0, toolchain_1.toolchainWorkerCommand)(toolchain));
723
- 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);
724
730
  chart(dependencyChart, "podokit-dependencies", dependencies);
725
731
  chart(applicationChart, "podokit-application", application);
726
732
  (0, node_fs_1.writeFileSync)((0, node_path_1.join)(root, "migration.yaml"), migration);