@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
@@ -1,3 +1,4 @@
1
+ import type { AppProxyTimeout, AppProxyTimeouts } from "./app-proxy-timeouts";
1
2
  /**
2
3
  * Validation primitives shared by every deployment driver.
3
4
  *
@@ -30,6 +31,23 @@ export declare const SENSITIVE_KEY: RegExp;
30
31
  export declare const STABLE_SEMVER_TAG_PATTERN = "^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$";
31
32
  /** Keys PodoKit derives itself, so a profile may not set them. */
32
33
  export declare const MANAGED_RUNTIME_CONFIG_KEYS: string[];
34
+ /**
35
+ * Keys that decide which forwarded address web and API record. They follow from
36
+ * the proxies in front of the deployment, so a profile states that one number and
37
+ * PodoKit renders the four values that agree with it.
38
+ */
39
+ export declare const CLIENT_ADDRESS_KEYS: string[];
40
+ export declare function parseEdgeProxies(exposure: Record<string, unknown>, fallback: number): number;
41
+ /**
42
+ * What the web server and the API trust, given how many proxies each request to
43
+ * the web server has passed. Behind at least one, the web server takes its origin
44
+ * from X-Forwarded-Proto and X-Forwarded-Host and the client from the
45
+ * X-Forwarded-For entry the outermost trusted proxy wrote (XFF_DEPTH); with none
46
+ * it trusts no forwarded header. It forwards that one address to the API, so the
47
+ * API always trusts one hop: the web proxy, the Compose gateway, or an Ingress
48
+ * that trusts no proxy in front of it.
49
+ */
50
+ export declare function proxyTrustConfig(webProxyHops: number, trustForwardedOrigin?: boolean): Record<string, string>;
33
51
  export declare function containsUrlCredentials(value: string): boolean;
34
52
  export declare function isRecord(value: unknown): value is Record<string, unknown>;
35
53
  export declare function assertOnlyKeys(value: Record<string, unknown>, allowed: string[], field: string): void;
@@ -50,6 +68,49 @@ export declare function validateImage(value: string, field: string): void;
50
68
  export declare function validateImageRepository(value: string, field: string): void;
51
69
  export declare function parseReleaseProfile(release: Record<string, unknown>): ReleaseProfile;
52
70
  export declare function parseRuntimeConfig(runtimeConfig: Record<string, unknown>): Record<string, string>;
71
+ /**
72
+ * An application-owned PostgreSQL pool each API or worker process opens beside
73
+ * PodoKit's own: `sizeKey` names the runtimeConfig key that caps it, and
74
+ * `extraConnections` counts connections it opens beyond that cap, such as one to
75
+ * check a server that stopped answering.
76
+ */
77
+ export interface AppConnectionPool {
78
+ sizeKey: string;
79
+ extraConnections: number;
80
+ }
81
+ /** What the connection budget needs to know about the database, beyond its mode. */
82
+ export interface PostgresBudgetProfile {
83
+ /** The server's max_connections; PodoKit sets it on a database it runs. */
84
+ maxConnections?: number;
85
+ appPools?: AppConnectionPool[];
86
+ }
87
+ export declare function parsePostgresBudget(value: Record<string, unknown>, field: string, runtimeConfig: Record<string, string>): PostgresBudgetProfile;
88
+ /** Bun closes silent connections on a 4-second tick, up to 4 seconds before their idle timeout. */
89
+ export declare const IDLE_TICK_SECONDS = 4;
90
+ export declare const MAX_IDLE_TIMEOUT_SECONDS = 255;
91
+ export declare const IDLE_TIMEOUT_KEYS: readonly ["CONNECTION_IDLE_TIMEOUT", "API_IDLE_TIMEOUT"];
92
+ /** The longest the /api proxy waits for response headers, and what sets it. */
93
+ export interface LongestProxyWait {
94
+ /** Milliseconds, or null for a route without a deadline. */
95
+ timeoutMs: number | null;
96
+ source: string;
97
+ }
98
+ /**
99
+ * BACKEND_PROXY_TIMEOUT_MS, or the application's longest APP_PROXY_TIMEOUTS
100
+ * entry when that waits longer; an entry without a deadline wins outright.
101
+ */
102
+ export declare function longestProxyWait(runtimeConfig: Record<string, string>, entries: AppProxyTimeout[]): LongestProxyWait;
103
+ export declare function describeProxyWait(wait: LongestProxyWait): string;
104
+ /** Whether an idle timeout keeps a silent connection open for the whole wait. */
105
+ export declare function idleTimeoutCovers(seconds: number, wait: LongestProxyWait): boolean;
106
+ /**
107
+ * The idle timeouts of the web server (CONNECTION_IDLE_TIMEOUT) and the API
108
+ * (API_IDLE_TIMEOUT), both run by Bun. Each is the profile's own value, or else
109
+ * the fewest seconds that keep a silent connection open past the /api proxy's
110
+ * longest wait: 35 for the default 30000 ms, and 0 (off) when an application
111
+ * route has no deadline.
112
+ */
113
+ export declare function idleTimeoutConfig(runtimeConfig: Record<string, string>, proxyTimeouts: AppProxyTimeouts): Record<string, string>;
53
114
  export declare function validateHttpsOrigin(value: string): void;
54
115
  export declare function parseVerification(verification: Record<string, unknown>): VerificationProfile;
55
116
  export declare function profileDirectory(projectRoot: string): string;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MANAGED_RUNTIME_CONFIG_KEYS = exports.STABLE_SEMVER_TAG_PATTERN = exports.SENSITIVE_KEY = exports.DNS_LABEL = exports.PROFILE_NAME = void 0;
3
+ exports.IDLE_TIMEOUT_KEYS = exports.MAX_IDLE_TIMEOUT_SECONDS = exports.IDLE_TICK_SECONDS = exports.CLIENT_ADDRESS_KEYS = exports.MANAGED_RUNTIME_CONFIG_KEYS = exports.STABLE_SEMVER_TAG_PATTERN = exports.SENSITIVE_KEY = exports.DNS_LABEL = exports.PROFILE_NAME = void 0;
4
+ exports.parseEdgeProxies = parseEdgeProxies;
5
+ exports.proxyTrustConfig = proxyTrustConfig;
4
6
  exports.containsUrlCredentials = containsUrlCredentials;
5
7
  exports.isRecord = isRecord;
6
8
  exports.assertOnlyKeys = assertOnlyKeys;
@@ -17,6 +19,11 @@ exports.validateImage = validateImage;
17
19
  exports.validateImageRepository = validateImageRepository;
18
20
  exports.parseReleaseProfile = parseReleaseProfile;
19
21
  exports.parseRuntimeConfig = parseRuntimeConfig;
22
+ exports.parsePostgresBudget = parsePostgresBudget;
23
+ exports.longestProxyWait = longestProxyWait;
24
+ exports.describeProxyWait = describeProxyWait;
25
+ exports.idleTimeoutCovers = idleTimeoutCovers;
26
+ exports.idleTimeoutConfig = idleTimeoutConfig;
20
27
  exports.validateHttpsOrigin = validateHttpsOrigin;
21
28
  exports.parseVerification = parseVerification;
22
29
  exports.profileDirectory = profileDirectory;
@@ -35,6 +42,43 @@ exports.MANAGED_RUNTIME_CONFIG_KEYS = [
35
42
  "PROTOCOL_HEADER",
36
43
  "HOST_HEADER",
37
44
  ];
45
+ /**
46
+ * Keys that decide which forwarded address web and API record. They follow from
47
+ * the proxies in front of the deployment, so a profile states that one number and
48
+ * PodoKit renders the four values that agree with it.
49
+ */
50
+ exports.CLIENT_ADDRESS_KEYS = ["ADDRESS_HEADER", "XFF_DEPTH", "TRUSTED_PROXY_HOPS", "TRUSTED_PROXY_HEADER"];
51
+ /** Edge proxies are counted, not configured; more than this is a typo. */
52
+ const MAX_EDGE_PROXIES = 8;
53
+ function parseEdgeProxies(exposure, fallback) {
54
+ const value = exposure.edgeProxies ?? fallback;
55
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 0 || value > MAX_EDGE_PROXIES) {
56
+ throw new Error(`Deployment profile field "exposure.edgeProxies" must be an integer from 0 to ${MAX_EDGE_PROXIES}.`);
57
+ }
58
+ return value;
59
+ }
60
+ /**
61
+ * What the web server and the API trust, given how many proxies each request to
62
+ * the web server has passed. Behind at least one, the web server takes its origin
63
+ * from X-Forwarded-Proto and X-Forwarded-Host and the client from the
64
+ * X-Forwarded-For entry the outermost trusted proxy wrote (XFF_DEPTH); with none
65
+ * it trusts no forwarded header. It forwards that one address to the API, so the
66
+ * API always trusts one hop: the web proxy, the Compose gateway, or an Ingress
67
+ * that trusts no proxy in front of it.
68
+ */
69
+ function proxyTrustConfig(webProxyHops, trustForwardedOrigin = webProxyHops > 0) {
70
+ return {
71
+ ...(trustForwardedOrigin ? { PROTOCOL_HEADER: "x-forwarded-proto", HOST_HEADER: "x-forwarded-host" } : {}),
72
+ ...(webProxyHops > 0 ? { ADDRESS_HEADER: "x-forwarded-for", XFF_DEPTH: String(webProxyHops) } : {}),
73
+ TRUSTED_PROXY_HOPS: "1",
74
+ TRUSTED_PROXY_HEADER: "x-forwarded-for",
75
+ };
76
+ }
77
+ /** Keys the generated API no longer reads; it refuses to start while they are set. */
78
+ const RENAMED_RUNTIME_CONFIG_KEYS = {
79
+ RATE_LIMIT_TRUSTED_PROXY_HOPS: "TRUSTED_PROXY_HOPS",
80
+ RATE_LIMIT_PROXY_HEADER: "TRUSTED_PROXY_HEADER",
81
+ };
38
82
  function containsUrlCredentials(value) {
39
83
  if (!value.includes("://"))
40
84
  return false;
@@ -183,6 +227,14 @@ function parseRuntimeConfig(runtimeConfig) {
183
227
  if (exports.MANAGED_RUNTIME_CONFIG_KEYS.includes(key)) {
184
228
  throw new Error(`Deployment runtimeConfig key "${key}" is managed by PodoKit.`);
185
229
  }
230
+ if (exports.CLIENT_ADDRESS_KEYS.includes(key)) {
231
+ throw new Error(`Deployment runtimeConfig key "${key}" is derived from exposure.edgeProxies, the number of reverse ` +
232
+ "proxies in front of the deployment; remove it and set exposure.edgeProxies instead.");
233
+ }
234
+ const renamed = RENAMED_RUNTIME_CONFIG_KEYS[key];
235
+ if (renamed) {
236
+ throw new Error(`Deployment runtimeConfig key "${key}" was renamed to "${renamed}".`);
237
+ }
186
238
  if (typeof entry !== "string") {
187
239
  throw new Error(`Deployment runtimeConfig value "${key}" must be a string.`);
188
240
  }
@@ -193,6 +245,110 @@ function parseRuntimeConfig(runtimeConfig) {
193
245
  }
194
246
  return parsed;
195
247
  }
248
+ /** PostgreSQL's own ceiling for max_connections. */
249
+ const MAX_POSTGRES_CONNECTIONS = 262_143;
250
+ /** Pools whose sizes the connection budget already reads. */
251
+ const PODOKIT_POOL_KEYS = ["DB_POOL_MAX", "DB_AUTH_POOL_MAX"];
252
+ function parsePostgresBudget(value, field, runtimeConfig) {
253
+ const parsed = {};
254
+ if (value.maxConnections !== undefined) {
255
+ const maxConnections = value.maxConnections;
256
+ if (typeof maxConnections !== "number" ||
257
+ !Number.isInteger(maxConnections) ||
258
+ maxConnections < 4 ||
259
+ maxConnections > MAX_POSTGRES_CONNECTIONS) {
260
+ throw new Error(`Deployment profile field "${field}.maxConnections" must be an integer from 4 to ${MAX_POSTGRES_CONNECTIONS}.`);
261
+ }
262
+ parsed.maxConnections = maxConnections;
263
+ }
264
+ if (value.appPools !== undefined) {
265
+ if (!Array.isArray(value.appPools)) {
266
+ throw new Error(`Deployment profile field "${field}.appPools" must be an array.`);
267
+ }
268
+ const seen = new Set();
269
+ parsed.appPools = value.appPools.map((entry, index) => {
270
+ const entryField = `${field}.appPools[${index}]`;
271
+ if (!isRecord(entry))
272
+ throw new Error(`Deployment profile field "${entryField}" must be an object.`);
273
+ assertOnlyKeys(entry, ["sizeKey", "extraConnections"], entryField);
274
+ const sizeKey = requiredString(entry, "sizeKey");
275
+ if (!/^[A-Z][A-Z0-9_]*$/.test(sizeKey) || PODOKIT_POOL_KEYS.includes(sizeKey) || seen.has(sizeKey)) {
276
+ throw new Error(`Deployment profile field "${entryField}.sizeKey" must name another pool's runtimeConfig key, once; ` +
277
+ `${PODOKIT_POOL_KEYS.join(" and ")} are counted already.`);
278
+ }
279
+ seen.add(sizeKey);
280
+ if (!/^[1-9][0-9]*$/.test(runtimeConfig[sizeKey] ?? "")) {
281
+ throw new Error(`Deployment profile field "${entryField}.sizeKey" names ${sizeKey}, which runtimeConfig must set to a positive integer.`);
282
+ }
283
+ const extraConnections = entry.extraConnections ?? 0;
284
+ if (typeof extraConnections !== "number" || !Number.isInteger(extraConnections) || extraConnections < 0) {
285
+ throw new Error(`Deployment profile field "${entryField}.extraConnections" must be a non-negative integer.`);
286
+ }
287
+ return { sizeKey, extraConnections };
288
+ });
289
+ }
290
+ return parsed;
291
+ }
292
+ /** Bun closes silent connections on a 4-second tick, up to 4 seconds before their idle timeout. */
293
+ exports.IDLE_TICK_SECONDS = 4;
294
+ exports.MAX_IDLE_TIMEOUT_SECONDS = 255;
295
+ const DEFAULT_BACKEND_PROXY_TIMEOUT_MS = 30_000;
296
+ exports.IDLE_TIMEOUT_KEYS = ["CONNECTION_IDLE_TIMEOUT", "API_IDLE_TIMEOUT"];
297
+ /**
298
+ * BACKEND_PROXY_TIMEOUT_MS, or the application's longest APP_PROXY_TIMEOUTS
299
+ * entry when that waits longer; an entry without a deadline wins outright.
300
+ */
301
+ function longestProxyWait(runtimeConfig, entries) {
302
+ const proxyTimeout = runtimeConfig.BACKEND_PROXY_TIMEOUT_MS?.trim();
303
+ let longest = {
304
+ timeoutMs: proxyTimeout && /^\d+$/.test(proxyTimeout) ? Number(proxyTimeout) : DEFAULT_BACKEND_PROXY_TIMEOUT_MS,
305
+ source: "BACKEND_PROXY_TIMEOUT_MS",
306
+ };
307
+ for (const entry of entries) {
308
+ if (longest.timeoutMs !== null && (entry.timeoutMs === null || entry.timeoutMs > longest.timeoutMs)) {
309
+ longest = { timeoutMs: entry.timeoutMs, source: `APP_PROXY_TIMEOUTS entry ${entry.pathPrefix}` };
310
+ }
311
+ }
312
+ return longest;
313
+ }
314
+ function describeProxyWait(wait) {
315
+ return `${wait.timeoutMs === null ? "no deadline" : `${wait.timeoutMs} ms`}, ${wait.source}`;
316
+ }
317
+ /** Whether an idle timeout keeps a silent connection open for the whole wait. */
318
+ function idleTimeoutCovers(seconds, wait) {
319
+ return seconds === 0 || (wait.timeoutMs !== null && (seconds - exports.IDLE_TICK_SECONDS) * 1000 > wait.timeoutMs);
320
+ }
321
+ /**
322
+ * The idle timeouts of the web server (CONNECTION_IDLE_TIMEOUT) and the API
323
+ * (API_IDLE_TIMEOUT), both run by Bun. Each is the profile's own value, or else
324
+ * the fewest seconds that keep a silent connection open past the /api proxy's
325
+ * longest wait: 35 for the default 30000 ms, and 0 (off) when an application
326
+ * route has no deadline.
327
+ */
328
+ function idleTimeoutConfig(runtimeConfig, proxyTimeouts) {
329
+ const config = {};
330
+ for (const name of exports.IDLE_TIMEOUT_KEYS) {
331
+ const configured = runtimeConfig[name];
332
+ if (configured !== undefined) {
333
+ config[name] = configured;
334
+ continue;
335
+ }
336
+ if ("error" in proxyTimeouts)
337
+ throw new Error(proxyTimeouts.error);
338
+ const wait = longestProxyWait(runtimeConfig, proxyTimeouts.entries);
339
+ if (wait.timeoutMs === null) {
340
+ config[name] = "0";
341
+ continue;
342
+ }
343
+ const derived = Math.floor(wait.timeoutMs / 1000) + exports.IDLE_TICK_SECONDS + 1;
344
+ if (derived > exports.MAX_IDLE_TIMEOUT_SECONDS) {
345
+ throw new Error(`The /api proxy waits up to ${wait.timeoutMs} ms (${wait.source}), longer than the longest idle timeout ` +
346
+ `(${exports.MAX_IDLE_TIMEOUT_SECONDS} seconds); set ${name} to "0" to turn it off.`);
347
+ }
348
+ config[name] = String(derived);
349
+ }
350
+ return config;
351
+ }
196
352
  function validateHttpsOrigin(value) {
197
353
  let parsed;
198
354
  try {
package/dist/deploy.d.ts CHANGED
@@ -1,10 +1,7 @@
1
+ import { type DeployFinding } from "./deploy-checks";
1
2
  import type { CommandRunner } from "./dev";
2
3
  import { type DeploymentImages } from "./deploy-render";
3
- export interface DeploymentDoctorFinding {
4
- code: string;
5
- ok: boolean;
6
- message: string;
7
- }
4
+ export type DeploymentDoctorFinding = DeployFinding;
8
5
  export interface DeploymentPlanAction {
9
6
  order: number;
10
7
  kind: "dependencies" | "migration" | "application" | "verification" | "rollback";
package/dist/deploy.js CHANGED
@@ -14,6 +14,8 @@ const node_fs_1 = require("node:fs");
14
14
  const node_os_1 = require("node:os");
15
15
  const node_path_1 = require("node:path");
16
16
  const api_paths_1 = require("./api-paths");
17
+ const app_proxy_timeouts_1 = require("./app-proxy-timeouts");
18
+ const deploy_checks_1 = require("./deploy-checks");
17
19
  const deploy_profile_1 = require("./deploy-profile");
18
20
  const deploy_render_1 = require("./deploy-render");
19
21
  function defaultRunner(command, args, options) {
@@ -143,7 +145,7 @@ function apiSecretRequirements(profile) {
143
145
  }
144
146
  function doctorDeployment(projectRoot, profileName, runner = defaultRunner) {
145
147
  const profile = (0, deploy_profile_1.loadDeploymentProfile)(projectRoot, profileName);
146
- const findings = [];
148
+ const findings = (0, deploy_checks_1.profileFindings)(projectRoot, profile);
147
149
  const fingerprint = inspectClusterFingerprint(profile.target.context, runner);
148
150
  findings.push({
149
151
  code: "cluster-fingerprint",
@@ -496,11 +498,12 @@ function planDeployment(projectRoot, profileName, release, runner = defaultRunne
496
498
  warnings: [
497
499
  "Application rollback does not reverse database migrations.",
498
500
  "PersistentVolumeClaims and Kubernetes Secrets are never deleted by apply or rollback.",
501
+ ...[(0, deploy_checks_1.connectionBudgetWarning)(profile, (0, deploy_checks_1.installedModules)(projectRoot))].filter((warning) => warning !== null),
499
502
  ],
500
503
  };
501
504
  const temporaryRoot = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), "podokit-deploy-plan-"));
502
505
  try {
503
- const runtime = (0, deploy_render_1.renderDeployment)(temporaryRoot, profileName, profile, release, images, state.rolloutStateDigest);
506
+ const runtime = (0, deploy_render_1.renderDeployment)(temporaryRoot, profileName, profile, release, images, state.rolloutStateDigest, (0, app_proxy_timeouts_1.readAppProxyTimeouts)(projectRoot));
504
507
  return { ...base, planHash: planDigest(base, runtime) };
505
508
  }
506
509
  finally {
package/dist/dev.d.ts CHANGED
@@ -9,6 +9,10 @@ export interface DevConfig {
9
9
  export interface DevRuntime {
10
10
  alias: string;
11
11
  config: DevConfig;
12
+ /** Browser origins of the development stack, for CORS_ORIGIN. */
13
+ origins: string[];
14
+ /** BETTER_AUTH_URL for the containers. */
15
+ authUrl: string;
12
16
  projectRoot: string;
13
17
  routeId: string;
14
18
  runtimeComposePath: string;
@@ -22,8 +26,25 @@ export type CommandRunner = (command: string, args: string[], options: {
22
26
  capture: boolean;
23
27
  }) => CommandResult;
24
28
  export declare function readDevConfig(projectRoot: string): DevConfig;
29
+ /**
30
+ * The origins a browser reaches the development stack on: the gateway
31
+ * hostname, the HTTPS tunnel origin when one is declared, and plain localhost
32
+ * for the compose file's own proxy.
33
+ */
34
+ export declare function devOrigins(config: Pick<DevConfig, "hostname" | "publicUrl">): string[];
35
+ /**
36
+ * Template variables for files that name the development host. They follow
37
+ * `.podokit/dev.json` when it exists and is valid, so a renamed host renders
38
+ * the same way it is routed; otherwise the project name's default.
39
+ */
40
+ export declare function devTemplateVars(projectName: string, projectRoot?: string): Record<string, string>;
25
41
  export declare function resolveDevRuntime(projectRoot: string): DevRuntime;
26
42
  export declare function installedDevProfiles(projectRoot: string): string[];
43
+ /**
44
+ * The compose overlay `podo dev` adds. Besides the gateway network it sets
45
+ * CORS_ORIGIN and BETTER_AUTH_URL for web and api from `.podokit/dev.json`, so
46
+ * the browser origins follow the routed hostname whatever `.env.docker` says.
47
+ */
27
48
  export declare function renderRuntimeCompose(runtime: DevRuntime): string;
28
49
  export declare function renderRoute(runtime: DevRuntime): string;
29
50
  /**
package/dist/dev.js CHANGED
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readDevConfig = readDevConfig;
4
+ exports.devOrigins = devOrigins;
5
+ exports.devTemplateVars = devTemplateVars;
4
6
  exports.resolveDevRuntime = resolveDevRuntime;
5
7
  exports.installedDevProfiles = installedDevProfiles;
6
8
  exports.renderRuntimeCompose = renderRuntimeCompose;
@@ -106,6 +108,68 @@ function readDevConfig(projectRoot) {
106
108
  apiPaths: (0, api_paths_1.parseApiPaths)(config.apiPaths ?? [], `${DEV_CONFIG}.apiPaths`, webSocketPaths),
107
109
  };
108
110
  }
111
+ /**
112
+ * The origins a browser reaches the development stack on: the gateway
113
+ * hostname, the HTTPS tunnel origin when one is declared, and plain localhost
114
+ * for the compose file's own proxy.
115
+ */
116
+ function devOrigins(config) {
117
+ return [
118
+ ...new Set([
119
+ `http://${config.hostname}`,
120
+ ...(config.publicUrl ? [new URL(config.publicUrl).origin] : []),
121
+ "http://localhost",
122
+ ]),
123
+ ];
124
+ }
125
+ /**
126
+ * Template variables for files that name the development host. They follow
127
+ * `.podokit/dev.json` when it exists and is valid, so a renamed host renders
128
+ * the same way it is routed; otherwise the project name's default.
129
+ */
130
+ function devTemplateVars(projectName, projectRoot) {
131
+ let config = { hostname: `${projectName}.localhost` };
132
+ const path = projectRoot ? (0, node_path_1.join)(projectRoot, DEV_CONFIG) : undefined;
133
+ if (path && (0, node_fs_1.existsSync)(path)) {
134
+ try {
135
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(path, "utf8"));
136
+ if (typeof parsed.hostname === "string") {
137
+ validateHostname(parsed.hostname);
138
+ if (parsed.publicUrl)
139
+ validatePublicUrl(parsed.publicUrl);
140
+ config = { hostname: parsed.hostname, publicUrl: parsed.publicUrl };
141
+ }
142
+ }
143
+ catch {
144
+ // podo dev reports an unreadable dev.json; rendering keeps the default.
145
+ }
146
+ }
147
+ return {
148
+ devHostname: config.hostname,
149
+ devCorsOrigin: devOrigins(config).join(","),
150
+ devAuthUrl: `http://${config.hostname}`,
151
+ };
152
+ }
153
+ /**
154
+ * BETTER_AUTH_URL for the containers: the gateway origin, or the origin named
155
+ * by PODOKIT_DEV_AUTH_URL for an OAuth round trip through `publicUrl`.
156
+ */
157
+ function devAuthUrl(config, origins) {
158
+ const requested = process.env.PODOKIT_DEV_AUTH_URL?.trim();
159
+ if (!requested)
160
+ return `http://${config.hostname}`;
161
+ let origin;
162
+ try {
163
+ origin = new URL(requested).origin;
164
+ }
165
+ catch {
166
+ origin = "";
167
+ }
168
+ if (!origins.includes(origin) || origin === "http://localhost") {
169
+ throw new Error(`PODOKIT_DEV_AUTH_URL must be http://${config.hostname}${config.publicUrl ? ` or ${new URL(config.publicUrl).origin}` : ""} (from ${DEV_CONFIG}).`);
170
+ }
171
+ return origin;
172
+ }
109
173
  function stableId(projectRoot) {
110
174
  return (0, node_crypto_1.createHash)("sha256").update(projectRoot).digest("hex").slice(0, 10);
111
175
  }
@@ -118,9 +182,12 @@ function resolveDevRuntime(projectRoot) {
118
182
  const config = readDevConfig(root);
119
183
  const id = stableId(root);
120
184
  const projectSlug = slug(config.hostname.replace(/\.localhost$/, ""));
185
+ const origins = devOrigins(config);
121
186
  return {
122
187
  alias: `podokit-${projectSlug}-${id}`,
123
188
  config,
189
+ origins,
190
+ authUrl: devAuthUrl(config, origins),
124
191
  projectRoot: root,
125
192
  routeId: `route-${projectSlug}-${id}`,
126
193
  runtimeComposePath: (0, node_path_1.join)(root, RUNTIME_COMPOSE),
@@ -143,11 +210,19 @@ function routesToApi(config) {
143
210
  function anyOf(matchers) {
144
211
  return matchers.length > 1 ? `(${matchers.join(" || ")})` : (matchers[0] ?? "");
145
212
  }
213
+ /**
214
+ * The compose overlay `podo dev` adds. Besides the gateway network it sets
215
+ * CORS_ORIGIN and BETTER_AUTH_URL for web and api from `.podokit/dev.json`, so
216
+ * the browser origins follow the routed hostname whatever `.env.docker` says.
217
+ */
146
218
  function renderRuntimeCompose(runtime) {
147
- const api = routesToApi(runtime.config)
148
- ? ` api:\n networks:\n ${GATEWAY_NETWORK}:\n aliases: [${runtime.alias}-api]\n`
219
+ const environment = ` environment:\n` +
220
+ ` CORS_ORIGIN: ${JSON.stringify(runtime.origins.join(","))}\n` +
221
+ ` BETTER_AUTH_URL: ${JSON.stringify(runtime.authUrl)}\n`;
222
+ const apiNetworks = routesToApi(runtime.config)
223
+ ? ` networks:\n ${GATEWAY_NETWORK}:\n aliases: [${runtime.alias}-api]\n`
149
224
  : "";
150
- return `# Generated by podo dev. Do not edit.\nservices:\n traefik:\n profiles: [podokit-legacy-proxy]\n web:\n networks:\n ${GATEWAY_NETWORK}:\n aliases: [${runtime.alias}]\n${api}networks:\n ${GATEWAY_NETWORK}:\n external: true\n name: ${GATEWAY_NETWORK}\n`;
225
+ return `# Generated by podo dev. Do not edit.\nservices:\n traefik:\n profiles: [podokit-legacy-proxy]\n web:\n${environment} networks:\n ${GATEWAY_NETWORK}:\n aliases: [${runtime.alias}]\n api:\n${environment}${apiNetworks}networks:\n ${GATEWAY_NETWORK}:\n external: true\n name: ${GATEWAY_NETWORK}\n`;
151
226
  }
152
227
  function renderRoute(runtime) {
153
228
  const { alias, config, routeId } = runtime;
package/dist/eject.js CHANGED
@@ -17,6 +17,8 @@ function eject(projectRoot, paths) {
17
17
  continue;
18
18
  }
19
19
  if (entry.tier !== "owned") {
20
+ // A managed file's baseline is what the template generated.
21
+ entry.templateHash = entry.outHash;
20
22
  entry.tier = "owned";
21
23
  result.ejected.push(path);
22
24
  }
package/dist/index.js CHANGED
@@ -12,12 +12,14 @@ const remove_1 = require("./remove");
12
12
  const inspect_1 = require("./inspect");
13
13
  const update_1 = require("./update");
14
14
  const eject_1 = require("./eject");
15
+ const template_files_1 = require("./template-files");
15
16
  const dev_1 = require("./dev");
16
17
  const locale_1 = require("./locale");
17
18
  const deploy_1 = require("./deploy");
18
19
  const deploy_profile_1 = require("./deploy-profile");
19
20
  const deploy_render_1 = require("./deploy-render");
20
21
  const deploy_driver_1 = require("./deploy-driver");
22
+ const deploy_checks_1 = require("./deploy-checks");
21
23
  const deploy_compose_profile_1 = require("./deploy-compose-profile");
22
24
  const deploy_compose_render_1 = require("./deploy-compose-render");
23
25
  const deploy_compose_1 = require("./deploy-compose");
@@ -33,7 +35,7 @@ Usage:
33
35
  podo provider set <capability> <provider> [--apply]
34
36
  podo remove <module> Un-apply a module (inverse of add)
35
37
  podo status Show version, modules, file tiers, and local edits
36
- podo diff List PodoKit-managed files you have edited
38
+ podo diff [path...] List edited managed files, or diff files against the template
37
39
  podo doctor Check framework versions against supported ranges
38
40
  podo dev <action> [...] Run the shared, portless container development gateway
39
41
  podo deploy <action> Plan, apply, verify, or roll back a deployment
@@ -41,6 +43,7 @@ Usage:
41
43
  podo locale <command> Add, validate, activate, or list JSON locales
42
44
  podo update [--apply] Preview (or apply) what a version update would change
43
45
  podo eject <path...> Take ownership of managed files (update skips them)
46
+ podo restore <path...> Rewrite managed or ejected files from the template
44
47
 
45
48
  Options:
46
49
  --template <t> Template to scaffold (see below)
@@ -220,6 +223,26 @@ function parseArgs(argv) {
220
223
  parsed.positionals = positionals;
221
224
  return parsed;
222
225
  }
226
+ /** Explain owned files whose template changed, with the upstream diff when the previous templates are given. */
227
+ function writeOwnedTemplateNotes(plan, templatesDir, from) {
228
+ if (plan.changes.some((change) => change.action === "review")) {
229
+ if (from) {
230
+ process.stdout.write("\nTemplate changes to files you own (not applied):\n");
231
+ for (const change of (0, update_1.upstreamChanges)(process.cwd(), templatesDir, plan, from)) {
232
+ process.stdout.write(change.diff || `${change.path}: new in this version's template.\n`);
233
+ }
234
+ }
235
+ else {
236
+ process.stdout.write("\nreview: the template changed for a file you own. podo update never writes it; " +
237
+ "podo diff <path> compares it with the new template, and --from <previous templates> shows what changed upstream.\n");
238
+ }
239
+ }
240
+ if (plan.ownedWithoutTemplate.length) {
241
+ process.stdout.write(`\n${plan.ownedWithoutTemplate.length} owned file(s) differ from this version's templates, and the lock does not record ` +
242
+ "the template they came from. podo diff <path> shows the difference; applying this update records the template " +
243
+ "so the next update reports only real template changes.\n");
244
+ }
245
+ }
223
246
  function writeApiPaths(heading, result, verb) {
224
247
  const lines = (0, module_api_paths_1.describeApiPaths)(result, verb);
225
248
  if (lines.length)
@@ -258,7 +281,7 @@ async function runComposeDeploy(action, args, profileName) {
258
281
  const findings = (0, deploy_compose_1.doctorComposeDeployment)(cwd, profileName, undefined, args.release);
259
282
  process.stdout.write(args.json
260
283
  ? `${JSON.stringify(findings, null, 2)}\n`
261
- : `${findings.map((f) => `${f.ok ? "ok " : "FAIL"} ${f.message}`).join("\n")}\n`);
284
+ : `${findings.map(deploy_checks_1.formatFinding).join("\n")}\n`);
262
285
  if (findings.some((finding) => !finding.ok))
263
286
  process.exitCode = 1;
264
287
  return;
@@ -461,7 +484,33 @@ async function main(argv) {
461
484
  return;
462
485
  }
463
486
  if (args.command === "diff") {
487
+ const targets = args.positionals.slice(1);
464
488
  try {
489
+ if (targets.length) {
490
+ const result = (0, template_files_1.templateDiff)(process.cwd(), (0, node_path_1.join)(__dirname, "templates"), targets);
491
+ if (result.projectVersion !== result.templateVersion) {
492
+ process.stdout.write(`Comparing with PodoKit ${result.templateVersion} templates; this project is on ${result.projectVersion}.\n`);
493
+ }
494
+ for (const file of result.files) {
495
+ const tier = file.tier ? ` (${file.tier})` : "";
496
+ if (file.status === "not-generated") {
497
+ process.stdout.write(`${file.path}: PodoKit does not generate this file.\n`);
498
+ }
499
+ else if (file.status === "missing") {
500
+ process.stdout.write(`${file.path}${tier}: missing; podo restore ${file.path} writes the template's version.\n`);
501
+ }
502
+ else if (file.status === "same") {
503
+ process.stdout.write(`${file.path}${tier}: same as the template.\n`);
504
+ }
505
+ else if (file.binary) {
506
+ process.stdout.write(`${file.path}${tier}: binary file differs from the template.\n`);
507
+ }
508
+ else {
509
+ process.stdout.write(file.diff);
510
+ }
511
+ }
512
+ return;
513
+ }
465
514
  const { drifted, missing } = (0, inspect_1.diff)(process.cwd());
466
515
  if (!drifted.length && !missing.length) {
467
516
  process.stdout.write("No local edits to PodoKit-managed files.\n");
@@ -539,7 +588,7 @@ async function main(argv) {
539
588
  const findings = (0, deploy_1.doctorDeployment)(process.cwd(), profileName);
540
589
  process.stdout.write(args.json
541
590
  ? `${JSON.stringify(findings, null, 2)}\n`
542
- : `${findings.map((finding) => `${finding.ok ? "ok " : "FAIL"} ${finding.message}`).join("\n")}\n`);
591
+ : `${findings.map(deploy_checks_1.formatFinding).join("\n")}\n`);
543
592
  if (findings.some((finding) => !finding.ok))
544
593
  process.exitCode = 1;
545
594
  return;
@@ -561,7 +610,9 @@ async function main(argv) {
561
610
  const plan = (0, deploy_1.planDeployment)(process.cwd(), profileName, args.release);
562
611
  process.stdout.write(args.json
563
612
  ? `${JSON.stringify(plan, null, 2)}\n`
564
- : `${plan.actions.map((entry) => `${entry.order}. ${entry.description}`).join("\n")}\nPlan hash: ${plan.planHash}\n`);
613
+ : `${plan.actions.map((entry) => `${entry.order}. ${entry.description}`).join("\n")}\n` +
614
+ `${plan.warnings.map((warning) => `warning: ${warning}`).join("\n")}\n` +
615
+ `Plan hash: ${plan.planHash}\n`);
565
616
  return;
566
617
  }
567
618
  if (action === "apply") {
@@ -672,12 +723,23 @@ async function main(argv) {
672
723
  result.conflicts.map((f) => ` ${f}`).join("\n") +
673
724
  "\n");
674
725
  }
726
+ if (result.review.length) {
727
+ process.stdout.write("\nThe template changed for these files you own; they were left as they are. " +
728
+ "Compare each with podo diff <path>:\n" +
729
+ result.review.map((f) => ` ${f}`).join("\n") +
730
+ "\n");
731
+ }
675
732
  return;
676
733
  }
677
734
  const plan = (0, update_1.planUpdate)(process.cwd(), templatesDir);
678
735
  const counts = (0, update_1.summarize)(plan);
679
736
  const shown = plan.changes.filter((c) => c.action !== "up-to-date" && c.action !== "skip");
680
737
  process.stdout.write(`podo update ${plan.fromVersion} -> ${plan.toVersion} (template: ${plan.template}; modules: ${plan.modules.join(", ") || "none"})\n\n`);
738
+ if (plan.moduleDowngrades.length) {
739
+ process.stdout.write("WARN older external modules are installed than this project applied:\n" +
740
+ `${(0, update_1.describeDowngrades)(plan.moduleDowngrades)}\n` +
741
+ "--apply refuses until you install the recorded versions; the plan below would downgrade them.\n\n");
742
+ }
681
743
  if (plan.addedModules.length) {
682
744
  process.stdout.write("Modules to add:\n" +
683
745
  plan.addedModules.map((module) => ` ${module.name} (${(0, update_1.addedModuleReason)(module)})`).join("\n") +
@@ -696,9 +758,10 @@ async function main(argv) {
696
758
  process.stdout.write(` ${c.action.padEnd(9)} ${path} (${c.note})\n`);
697
759
  }
698
760
  process.stdout.write(`${shown.length ? "\n" : ""}${counts.update} update, ${counts.add} add, ${counts.move} move, ` +
699
- `${counts.remove} remove, ${counts.conflict} conflict. ` +
761
+ `${counts.remove} remove, ${counts.conflict} conflict, ${counts.review} review. ` +
700
762
  `Dry-run — nothing was written. Re-run with --apply to write (use --from <dir> for a 3-way merge).\n`);
701
763
  }
764
+ writeOwnedTemplateNotes(plan, templatesDir, args.from);
702
765
  }
703
766
  catch (err) {
704
767
  fail(err.message);
@@ -727,6 +790,23 @@ async function main(argv) {
727
790
  }
728
791
  return;
729
792
  }
793
+ if (args.command === "restore") {
794
+ const targets = args.positionals.slice(1);
795
+ if (!targets.length) {
796
+ fail("Usage: podo restore <path...>");
797
+ }
798
+ try {
799
+ const result = (0, template_files_1.restore)(process.cwd(), (0, node_path_1.join)(__dirname, "templates"), targets);
800
+ process.stdout.write(`Restored from the template: ${result.restored.join(", ")}\n`);
801
+ if (result.remanaged.length) {
802
+ process.stdout.write(`Managed by PodoKit again: ${result.remanaged.join(", ")}\n`);
803
+ }
804
+ }
805
+ catch (err) {
806
+ fail(err.message);
807
+ }
808
+ return;
809
+ }
730
810
  if (args.command !== "create") {
731
811
  fail(`Unknown command "${args.command}". Run "podo --help".`);
732
812
  }
package/dist/lib.d.ts CHANGED
@@ -11,6 +11,8 @@ export { describeApiPaths } from "./module-api-paths";
11
11
  export { status, diff, doctor, SUPPORTED_FRAMEWORKS } from "./inspect";
12
12
  export type { StatusReport, DoctorFinding } from "./inspect";
13
13
  export { addedModuleReason, planUpdate, summarize } from "./update";
14
+ export { restore, templateDiff } from "./template-files";
15
+ export type { RestoreResult, TemplateDiffResult, TemplateFileDiff } from "./template-files";
14
16
  export type { AddedModule, UpdatePlan, FileChange } from "./update";
15
17
  export { readDevConfig, renderRoute, renderRuntimeCompose, resolveDevRuntime, runDevCommand } from "./dev";
16
18
  export type { CommandRunner, DevConfig, DevRuntime } from "./dev";