@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.
- package/README.md +2 -1
- package/dist/app-proxy-timeouts.d.ts +27 -0
- package/dist/app-proxy-timeouts.js +180 -0
- package/dist/assemble.d.ts +5 -2
- package/dist/assemble.js +14 -4
- package/dist/deploy-checks.d.ts +44 -0
- package/dist/deploy-checks.js +267 -0
- package/dist/deploy-compose-profile.d.ts +6 -2
- package/dist/deploy-compose-profile.js +15 -6
- package/dist/deploy-compose-render.d.ts +12 -4
- package/dist/deploy-compose-render.js +47 -17
- package/dist/deploy-compose.d.ts +2 -5
- package/dist/deploy-compose.js +7 -2
- package/dist/deploy-profile.d.ts +6 -2
- package/dist/deploy-profile.js +11 -7
- package/dist/deploy-render.d.ts +4 -1
- package/dist/deploy-render.js +28 -20
- package/dist/deploy-schema.d.ts +61 -0
- package/dist/deploy-schema.js +157 -1
- package/dist/deploy.d.ts +2 -5
- package/dist/deploy.js +5 -2
- package/dist/dev.d.ts +21 -0
- package/dist/dev.js +78 -3
- package/dist/eject.js +2 -0
- package/dist/index.js +85 -5
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +5 -2
- package/dist/lockfile.d.ts +24 -0
- package/dist/lockfile.js +77 -5
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +12 -4
- package/dist/remove.js +1 -1
- package/dist/template-files.d.ts +42 -0
- package/dist/template-files.js +102 -0
- package/dist/templates/fullstack/AGENTS.md +44 -7
- package/dist/templates/fullstack/README.md +8 -7
- package/dist/templates/fullstack/apps/api/package.json +4 -4
- package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
- package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
- package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +5 -1
- package/dist/templates/fullstack/apps/api/src/app.extensions.ts +7 -4
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +47 -28
- package/dist/templates/fullstack/apps/api/src/build.spec.ts +118 -0
- package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +46 -3
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
- package/dist/templates/fullstack/apps/api/src/config/providers.ts +8 -2
- package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +17 -6
- package/dist/templates/fullstack/apps/api/src/config/server-options.ts +5 -8
- package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +29 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +39 -2
- package/dist/templates/fullstack/apps/api/src/core/client-address.spec.ts +82 -0
- package/dist/templates/fullstack/apps/api/src/core/client-address.ts +68 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +43 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
- package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
- package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
- package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
- package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
- package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
- package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
- package/dist/templates/fullstack/apps/web/Dockerfile +4 -0
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/app.css +3 -0
- package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
- package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +370 -36
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +250 -41
- package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +5 -2
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +23 -0
- package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
- package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
- package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
- package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
- package/dist/templates/fullstack/compose.dev.yaml +7 -4
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +12 -3
- package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
- package/dist/templates/fullstack/dot-env.docker.example +7 -2
- package/dist/templates/fullstack/dot-env.example +21 -1
- package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
- package/dist/templates/fullstack/infra/k3s/configmap.yaml +4 -0
- package/dist/templates/fullstack/tests/README.md +40 -1
- package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
- package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
- package/dist/templates/fullstack/tests/package.json +2 -2
- package/dist/templates/fullstack/tests/playwright.config.ts +7 -3
- package/dist/templates/fullstack/tests/playwright.extensions.ts +88 -1
- package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.spec.ts +152 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.ts +112 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +18 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +36 -13
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +13 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.test.ts +33 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.ts +34 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +73 -39
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/account-sessions.api.spec.ts +55 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +29 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +55 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +11 -5
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +3 -10
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-sessions.ui.spec.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +62 -59
- package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
- package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +33 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
- package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +33 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +20 -1
- package/dist/templates/modules/audit-log/module.manifest.json +15 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +6 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +54 -6
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -8
- package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.spec.ts +56 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.ts +45 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
- package/dist/templates/modules/auth/module.manifest.json +8 -2
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +57 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +93 -9
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
- package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
- package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
- package/dist/templates/modules/bullmq/module.manifest.json +5 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +87 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +93 -3
- package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +1 -1
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +1 -1
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +2 -59
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +0 -65
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +82 -21
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +54 -15
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +43 -5
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +10 -2
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +61 -3
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +48 -15
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +63 -27
- package/dist/templates/modules/rate-limit/module.manifest.json +6 -9
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
- package/dist/templates/todo/AGENTS.md +46 -6
- package/dist/templates/todo/README.md +6 -4
- package/dist/text-diff.d.ts +12 -0
- package/dist/text-diff.js +111 -0
- package/dist/update.d.ts +61 -3
- package/dist/update.js +152 -27
- package/package.json +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/exclusive.ts +0 -80
package/README.md
CHANGED
|
@@ -85,11 +85,12 @@ on restart; files and the database-backed local jobs persist.
|
|
|
85
85
|
| `podo provider list` | Show active database, cache, object storage, event, and job providers. |
|
|
86
86
|
| `podo provider set <capability> <provider> [--apply]` | Preview or apply one provider change. |
|
|
87
87
|
| `podo status` | Show the PodoKit version, modules, ownership tiers, and local edits. |
|
|
88
|
-
| `podo diff` | List edited or deleted managed files. |
|
|
88
|
+
| `podo diff [path...]` | List edited or deleted managed files, or show a file's diff against its template. |
|
|
89
89
|
| `podo doctor` | Check Bun and framework versions. |
|
|
90
90
|
| `podo locale <command>` | Manage JSON locales. |
|
|
91
91
|
| `podo update [--apply]` | Preview or apply managed template and module updates. |
|
|
92
92
|
| `podo eject <path...>` | Transfer managed files to application ownership. |
|
|
93
|
+
| `podo restore <path...>` | Rewrite managed files from the template; an ejected file becomes managed again. |
|
|
93
94
|
| `podo dev <action>` | Manage the generated development containers. |
|
|
94
95
|
| `podo deploy <action>` | Plan, apply, verify, or roll back an immutable Kubernetes or Docker Compose release. |
|
|
95
96
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The application's own `/api` proxy deadlines, read for deployment.
|
|
3
|
+
*
|
|
4
|
+
* The web server loads `APP_PROXY_TIMEOUTS` from an application-owned module, and
|
|
5
|
+
* the idle timeouts a deployment renders have to outlast its longest entry. The
|
|
6
|
+
* CLI must not run application code to learn them, so it reads the array literal
|
|
7
|
+
* with a small tokenizer that accepts exactly the shape PodoKit generates and
|
|
8
|
+
* documents: object literals with a string `pathPrefix` and a `timeoutMs` that is
|
|
9
|
+
* `null` or a number, optionally a product such as `5 * 60_000`. Anything else is
|
|
10
|
+
* reported, never guessed.
|
|
11
|
+
*/
|
|
12
|
+
export declare const APP_PROXY_TIMEOUTS_PATH = "apps/web/src/lib/server/proxy-timeouts.ts";
|
|
13
|
+
export interface AppProxyTimeout {
|
|
14
|
+
pathPrefix: string;
|
|
15
|
+
timeoutMs: number | null;
|
|
16
|
+
}
|
|
17
|
+
/** The entries, or why they could not be read without running the module. */
|
|
18
|
+
export type AppProxyTimeouts = {
|
|
19
|
+
entries: AppProxyTimeout[];
|
|
20
|
+
} | {
|
|
21
|
+
error: string;
|
|
22
|
+
};
|
|
23
|
+
export declare const NO_APP_PROXY_TIMEOUTS: AppProxyTimeouts;
|
|
24
|
+
/** Parses the `APP_PROXY_TIMEOUTS` array literal from the module's source text. */
|
|
25
|
+
export declare function parseAppProxyTimeouts(source: string): AppProxyTimeout[];
|
|
26
|
+
/** Reads the application's deadlines; an app without the module has none. */
|
|
27
|
+
export declare function readAppProxyTimeouts(projectRoot: string): AppProxyTimeouts;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NO_APP_PROXY_TIMEOUTS = exports.APP_PROXY_TIMEOUTS_PATH = void 0;
|
|
4
|
+
exports.parseAppProxyTimeouts = parseAppProxyTimeouts;
|
|
5
|
+
exports.readAppProxyTimeouts = readAppProxyTimeouts;
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
/**
|
|
9
|
+
* The application's own `/api` proxy deadlines, read for deployment.
|
|
10
|
+
*
|
|
11
|
+
* The web server loads `APP_PROXY_TIMEOUTS` from an application-owned module, and
|
|
12
|
+
* the idle timeouts a deployment renders have to outlast its longest entry. The
|
|
13
|
+
* CLI must not run application code to learn them, so it reads the array literal
|
|
14
|
+
* with a small tokenizer that accepts exactly the shape PodoKit generates and
|
|
15
|
+
* documents: object literals with a string `pathPrefix` and a `timeoutMs` that is
|
|
16
|
+
* `null` or a number, optionally a product such as `5 * 60_000`. Anything else is
|
|
17
|
+
* reported, never guessed.
|
|
18
|
+
*/
|
|
19
|
+
exports.APP_PROXY_TIMEOUTS_PATH = "apps/web/src/lib/server/proxy-timeouts.ts";
|
|
20
|
+
exports.NO_APP_PROXY_TIMEOUTS = { entries: [] };
|
|
21
|
+
function tokenize(source) {
|
|
22
|
+
const tokens = [];
|
|
23
|
+
let line = 1;
|
|
24
|
+
let index = 0;
|
|
25
|
+
const fail = (message) => {
|
|
26
|
+
throw new Error(`${message} on line ${line}`);
|
|
27
|
+
};
|
|
28
|
+
while (index < source.length) {
|
|
29
|
+
const character = source[index];
|
|
30
|
+
if (character === "\n") {
|
|
31
|
+
line += 1;
|
|
32
|
+
index += 1;
|
|
33
|
+
}
|
|
34
|
+
else if (/\s/.test(character)) {
|
|
35
|
+
index += 1;
|
|
36
|
+
}
|
|
37
|
+
else if (source.startsWith("//", index)) {
|
|
38
|
+
const end = source.indexOf("\n", index);
|
|
39
|
+
index = end === -1 ? source.length : end;
|
|
40
|
+
}
|
|
41
|
+
else if (source.startsWith("/*", index)) {
|
|
42
|
+
const end = source.indexOf("*/", index + 2);
|
|
43
|
+
if (end === -1)
|
|
44
|
+
fail("an unterminated comment");
|
|
45
|
+
line += source.slice(index, end).split("\n").length - 1;
|
|
46
|
+
index = end + 2;
|
|
47
|
+
}
|
|
48
|
+
else if (character === '"' || character === "'" || character === "`") {
|
|
49
|
+
let value = "";
|
|
50
|
+
let cursor = index + 1;
|
|
51
|
+
for (;;) {
|
|
52
|
+
const next = source[cursor];
|
|
53
|
+
if (next === undefined || next === "\n")
|
|
54
|
+
fail("an unterminated string");
|
|
55
|
+
if (next === character)
|
|
56
|
+
break;
|
|
57
|
+
if (next === "\\") {
|
|
58
|
+
const escaped = source[cursor + 1];
|
|
59
|
+
if (escaped === undefined || !"\\'\"`/".includes(escaped))
|
|
60
|
+
fail("an unsupported string escape");
|
|
61
|
+
value += escaped;
|
|
62
|
+
cursor += 2;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (character === "`" && next === "$" && source[cursor + 1] === "{")
|
|
66
|
+
fail("a template literal with an expression");
|
|
67
|
+
value += next;
|
|
68
|
+
cursor += 1;
|
|
69
|
+
}
|
|
70
|
+
tokens.push({ kind: "string", value, line });
|
|
71
|
+
index = cursor + 1;
|
|
72
|
+
}
|
|
73
|
+
else if (/[0-9]/.test(character)) {
|
|
74
|
+
const match = /^[0-9][0-9_]*(?:\.[0-9][0-9_]*)?(?:[eE][+-]?[0-9]+)?/.exec(source.slice(index));
|
|
75
|
+
tokens.push({ kind: "number", value: match[0], line });
|
|
76
|
+
index += match[0].length;
|
|
77
|
+
}
|
|
78
|
+
else if (/[A-Za-z_$]/.test(character)) {
|
|
79
|
+
const match = /^[A-Za-z_$][A-Za-z0-9_$]*/.exec(source.slice(index));
|
|
80
|
+
tokens.push({ kind: "identifier", value: match[0], line });
|
|
81
|
+
index += match[0].length;
|
|
82
|
+
}
|
|
83
|
+
else if (source.startsWith("=>", index) || source.startsWith("...", index)) {
|
|
84
|
+
const value = source.startsWith("=>", index) ? "=>" : "...";
|
|
85
|
+
tokens.push({ kind: "punctuation", value, line });
|
|
86
|
+
index += value.length;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
tokens.push({ kind: "punctuation", value: character, line });
|
|
90
|
+
index += 1;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return tokens;
|
|
94
|
+
}
|
|
95
|
+
function numberValue(token) {
|
|
96
|
+
const value = Number(token.value.replace(/_/g, ""));
|
|
97
|
+
if (!Number.isFinite(value) || /_$|_\.|\._|__/.test(token.value)) {
|
|
98
|
+
throw new Error(`an invalid number ${token.value} on line ${token.line}`);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
/** Parses the `APP_PROXY_TIMEOUTS` array literal from the module's source text. */
|
|
103
|
+
function parseAppProxyTimeouts(source) {
|
|
104
|
+
const tokens = tokenize(source);
|
|
105
|
+
const declaration = tokens.findIndex((token, index) => token.kind === "identifier" &&
|
|
106
|
+
token.value === "APP_PROXY_TIMEOUTS" &&
|
|
107
|
+
tokens[index - 1]?.value === "const");
|
|
108
|
+
if (declaration === -1)
|
|
109
|
+
throw new Error("no `const APP_PROXY_TIMEOUTS` declaration");
|
|
110
|
+
let index = declaration + 1;
|
|
111
|
+
while (index < tokens.length && tokens[index].value !== "=")
|
|
112
|
+
index += 1;
|
|
113
|
+
index += 1;
|
|
114
|
+
/** Consumes the next token, which must satisfy `accepts`. */
|
|
115
|
+
const take = (expected, accepts) => {
|
|
116
|
+
const token = tokens[index];
|
|
117
|
+
if (!token)
|
|
118
|
+
throw new Error(`${expected} expected at the end of the file`);
|
|
119
|
+
if (!accepts(token)) {
|
|
120
|
+
throw new Error(`${expected} expected, found ${JSON.stringify(token.value)} on line ${token.line}`);
|
|
121
|
+
}
|
|
122
|
+
index += 1;
|
|
123
|
+
return token;
|
|
124
|
+
};
|
|
125
|
+
const punctuation = (...values) => (token) => token.kind === "punctuation" && values.includes(token.value);
|
|
126
|
+
take("an array literal", punctuation("["));
|
|
127
|
+
const entries = [];
|
|
128
|
+
while (take("`{` or `]`", punctuation("{", "]")).value === "{") {
|
|
129
|
+
const entry = {};
|
|
130
|
+
const property = (token) => punctuation("}")(token) || token.kind === "identifier" || token.kind === "string";
|
|
131
|
+
let key = take("pathPrefix, timeoutMs, or `}`", property);
|
|
132
|
+
while (key.kind !== "punctuation") {
|
|
133
|
+
if ((key.value !== "pathPrefix" && key.value !== "timeoutMs") || key.value in entry) {
|
|
134
|
+
throw new Error(`pathPrefix or timeoutMs expected, found ${JSON.stringify(key.value)} on line ${key.line}`);
|
|
135
|
+
}
|
|
136
|
+
take("`:`", punctuation(":"));
|
|
137
|
+
if (key.value === "pathPrefix") {
|
|
138
|
+
entry.pathPrefix = take("a string literal", (token) => token.kind === "string").value;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const value = take("null or a number", (token) => token.kind === "number" || (token.kind === "identifier" && token.value === "null"));
|
|
142
|
+
let product = value.kind === "number" ? numberValue(value) : null;
|
|
143
|
+
while (product !== null && tokens[index]?.value === "*") {
|
|
144
|
+
index += 1;
|
|
145
|
+
product *= numberValue(take("a number", (token) => token.kind === "number"));
|
|
146
|
+
}
|
|
147
|
+
entry.timeoutMs = product;
|
|
148
|
+
}
|
|
149
|
+
if (take("`,` or `}`", punctuation(",", "}")).value === "}")
|
|
150
|
+
break;
|
|
151
|
+
key = take("pathPrefix, timeoutMs, or `}`", property);
|
|
152
|
+
}
|
|
153
|
+
if (entry.pathPrefix === undefined || entry.timeoutMs === undefined) {
|
|
154
|
+
throw new Error(`an entry without pathPrefix or timeoutMs before line ${tokens[index - 1].line}`);
|
|
155
|
+
}
|
|
156
|
+
if (entry.timeoutMs !== null && (!Number.isInteger(entry.timeoutMs) || entry.timeoutMs < 1)) {
|
|
157
|
+
throw new Error(`timeoutMs ${entry.timeoutMs} for ${entry.pathPrefix} is not a positive whole number of milliseconds`);
|
|
158
|
+
}
|
|
159
|
+
entries.push({ pathPrefix: entry.pathPrefix, timeoutMs: entry.timeoutMs });
|
|
160
|
+
if (take("`,` or `]`", punctuation(",", "]")).value === "]")
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
return entries;
|
|
164
|
+
}
|
|
165
|
+
/** Reads the application's deadlines; an app without the module has none. */
|
|
166
|
+
function readAppProxyTimeouts(projectRoot) {
|
|
167
|
+
const path = (0, node_path_1.join)(projectRoot, exports.APP_PROXY_TIMEOUTS_PATH);
|
|
168
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
169
|
+
return exports.NO_APP_PROXY_TIMEOUTS;
|
|
170
|
+
try {
|
|
171
|
+
return { entries: parseAppProxyTimeouts((0, node_fs_1.readFileSync)(path, "utf8")) };
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
return {
|
|
175
|
+
error: `Cannot read APP_PROXY_TIMEOUTS from ${exports.APP_PROXY_TIMEOUTS_PATH} without running it: ` +
|
|
176
|
+
`${error.message}. Write each entry as { pathPrefix: "/api/...", timeoutMs: <milliseconds> | null } ` +
|
|
177
|
+
"with literal values, or set CONNECTION_IDLE_TIMEOUT and API_IDLE_TIMEOUT in runtimeConfig yourself.",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
package/dist/assemble.d.ts
CHANGED
|
@@ -15,5 +15,8 @@ export interface AssembleOptions {
|
|
|
15
15
|
}
|
|
16
16
|
/** Assemble the project tree for a template + ordered modules, in memory. */
|
|
17
17
|
export declare function assembleProject(options: AssembleOptions): VfsTree;
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Render a Bun-native project template. Files that name the development host
|
|
20
|
+
* render it from the project's `.podokit/dev.json` when there is one.
|
|
21
|
+
*/
|
|
22
|
+
export declare function renderProjectTemplate(templatesDir: string, template: string, projectAnswers: TemplateVars, projectRoot?: string): VfsTree;
|
package/dist/assemble.js
CHANGED
|
@@ -6,6 +6,7 @@ const node_fs_1 = require("node:fs");
|
|
|
6
6
|
const node_path_1 = require("node:path");
|
|
7
7
|
const podokit_template_engine_1 = require("@podosoft/podokit-template-engine");
|
|
8
8
|
const add_1 = require("./add");
|
|
9
|
+
const dev_1 = require("./dev");
|
|
9
10
|
function textOf(tree, path) {
|
|
10
11
|
return String(tree.get(path)?.content ?? "");
|
|
11
12
|
}
|
|
@@ -70,14 +71,16 @@ function preserveExternalPackageDependency(tree, projectRoot, packageName) {
|
|
|
70
71
|
? { ...current }
|
|
71
72
|
: {};
|
|
72
73
|
dependencies[packageName] = version;
|
|
73
|
-
|
|
74
|
+
// Bun and npm write dependency sections sorted by name; insertion order
|
|
75
|
+
// would leave the file looking edited after `bun add` a second module.
|
|
76
|
+
rootPackage[section] = Object.fromEntries(Object.entries(dependencies).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
74
77
|
setText(tree, "package.json", `${JSON.stringify(rootPackage, null, 2)}\n`);
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
/** Assemble the project tree for a template + ordered modules, in memory. */
|
|
79
82
|
function assembleProject(options) {
|
|
80
|
-
const tree = renderProjectTemplate(options.templatesDir, options.template, options.answers);
|
|
83
|
+
const tree = renderProjectTemplate(options.templatesDir, options.template, options.answers, options.projectRoot);
|
|
81
84
|
for (const mod of options.modules) {
|
|
82
85
|
const name = typeof mod === "string" ? mod : (mod.packageName ?? mod.name);
|
|
83
86
|
const resolved = (0, add_1.resolveModule)(name, (0, node_path_1.join)(options.templatesDir, "modules"), options.projectRoot ?? options.templatesDir);
|
|
@@ -93,8 +96,15 @@ function assembleProject(options) {
|
|
|
93
96
|
}
|
|
94
97
|
return tree;
|
|
95
98
|
}
|
|
96
|
-
/**
|
|
97
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Render a Bun-native project template. Files that name the development host
|
|
101
|
+
* render it from the project's `.podokit/dev.json` when there is one.
|
|
102
|
+
*/
|
|
103
|
+
function renderProjectTemplate(templatesDir, template, projectAnswers, projectRoot) {
|
|
104
|
+
const answers = {
|
|
105
|
+
...projectAnswers,
|
|
106
|
+
...(0, dev_1.devTemplateVars)(projectAnswers.projectName ?? "app", projectRoot),
|
|
107
|
+
};
|
|
98
108
|
const tree = template === "todo"
|
|
99
109
|
? (0, podokit_template_engine_1.renderTemplate)((0, node_path_1.join)(templatesDir, "fullstack"), answers)
|
|
100
110
|
: (0, podokit_template_engine_1.renderTemplate)((0, node_path_1.join)(templatesDir, template), answers);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AnyDeployProfile } from "./deploy-driver";
|
|
2
|
+
/**
|
|
3
|
+
* Read-only checks of a deployment profile against the application it deploys.
|
|
4
|
+
*
|
|
5
|
+
* Both drivers run them before they look at the target, because they need
|
|
6
|
+
* nothing but the project directory: a profile that misconfigures the
|
|
7
|
+
* application fails or warns the same way on a cluster and on a Docker host.
|
|
8
|
+
*/
|
|
9
|
+
/** One doctor result. A warning still passes, so apply runs, but the CLI flags it. */
|
|
10
|
+
export interface DeployFinding {
|
|
11
|
+
code: string;
|
|
12
|
+
ok: boolean;
|
|
13
|
+
message: string;
|
|
14
|
+
warning?: true;
|
|
15
|
+
}
|
|
16
|
+
export declare function installedModules(projectRoot: string): Set<string>;
|
|
17
|
+
/** PostgreSQL's default max_connections, which a profile can change. */
|
|
18
|
+
export declare const DEFAULT_MAX_CONNECTIONS = 100;
|
|
19
|
+
export interface ConnectionBudget {
|
|
20
|
+
maxConnections: number;
|
|
21
|
+
available: number;
|
|
22
|
+
/** Connections one API or worker process can hold, and what they are. */
|
|
23
|
+
perProcess: number;
|
|
24
|
+
perProcessTerms: string[];
|
|
25
|
+
steadyProcesses: number;
|
|
26
|
+
steady: number;
|
|
27
|
+
/** Processes and connections while a rollout runs old and new pods side by side. */
|
|
28
|
+
rolloutProcesses: number;
|
|
29
|
+
rollout: number;
|
|
30
|
+
migration: number;
|
|
31
|
+
peak: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The most PostgreSQL connections the deployment can open: every API and worker
|
|
35
|
+
* process with each pool full, the Kubernetes rollout's surge pod per workload
|
|
36
|
+
* (the rendered strategy allows one), and the migration that runs beside the
|
|
37
|
+
* release still serving. Compose replaces containers without a surge. Null when
|
|
38
|
+
* the deployment uses no PostgreSQL.
|
|
39
|
+
*/
|
|
40
|
+
export declare function connectionBudget(profile: AnyDeployProfile, modules: Set<string>): ConnectionBudget | null;
|
|
41
|
+
/** The plan warning for a budget over the limit, or null. */
|
|
42
|
+
export declare function connectionBudgetWarning(profile: AnyDeployProfile, modules: Set<string>): string | null;
|
|
43
|
+
export declare function profileFindings(projectRoot: string, profile: AnyDeployProfile): DeployFinding[];
|
|
44
|
+
export declare function formatFinding(finding: DeployFinding): string;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_MAX_CONNECTIONS = void 0;
|
|
4
|
+
exports.installedModules = installedModules;
|
|
5
|
+
exports.connectionBudget = connectionBudget;
|
|
6
|
+
exports.connectionBudgetWarning = connectionBudgetWarning;
|
|
7
|
+
exports.profileFindings = profileFindings;
|
|
8
|
+
exports.formatFinding = formatFinding;
|
|
9
|
+
const app_proxy_timeouts_1 = require("./app-proxy-timeouts");
|
|
10
|
+
const deploy_compose_profile_1 = require("./deploy-compose-profile");
|
|
11
|
+
const deploy_compose_render_1 = require("./deploy-compose-render");
|
|
12
|
+
const deploy_render_1 = require("./deploy-render");
|
|
13
|
+
const deploy_schema_1 = require("./deploy-schema");
|
|
14
|
+
const lockfile_1 = require("./lockfile");
|
|
15
|
+
function installedModules(projectRoot) {
|
|
16
|
+
return new Set(((0, lockfile_1.readManifest)(projectRoot)?.modules ?? []).map((module) => module.name));
|
|
17
|
+
}
|
|
18
|
+
function auditLogFinding(runtimeConfig) {
|
|
19
|
+
const value = runtimeConfig.AUDIT_LOG_ENABLED;
|
|
20
|
+
if (value === "true") {
|
|
21
|
+
return {
|
|
22
|
+
code: "audit-log",
|
|
23
|
+
ok: true,
|
|
24
|
+
message: "AUDIT_LOG_ENABLED is true, so the audit log records from the first request.",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
code: "audit-log",
|
|
29
|
+
ok: true,
|
|
30
|
+
warning: true,
|
|
31
|
+
message: value === undefined
|
|
32
|
+
? 'runtimeConfig does not set AUDIT_LOG_ENABLED, so until an administrator saves the audit setting the API\'s built-in default decides whether anything is recorded; set it to "true".'
|
|
33
|
+
: `AUDIT_LOG_ENABLED is ${JSON.stringify(value)}, so the audit log records nothing until an administrator turns it on in Settings; set it to "true".`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether the rendered idle timeouts outlast the /api proxy's longest wait. A web
|
|
38
|
+
* server whose CONNECTION_IDLE_TIMEOUT cannot refuses to start, so that fails; a
|
|
39
|
+
* short API_IDLE_TIMEOUT only warns, because the API may lift its idle timeout
|
|
40
|
+
* for the long routes itself with `server.timeout(request, 0)`.
|
|
41
|
+
*/
|
|
42
|
+
function idleTimeoutFinding(runtimeConfig, proxyTimeouts) {
|
|
43
|
+
const code = "idle-timeouts";
|
|
44
|
+
const explicit = deploy_schema_1.IDLE_TIMEOUT_KEYS.filter((key) => runtimeConfig[key] !== undefined);
|
|
45
|
+
if ("error" in proxyTimeouts) {
|
|
46
|
+
return explicit.length === deploy_schema_1.IDLE_TIMEOUT_KEYS.length
|
|
47
|
+
? { code, ok: true, warning: true, message: `${proxyTimeouts.error} The idle timeouts were not checked.` }
|
|
48
|
+
: { code, ok: false, message: proxyTimeouts.error };
|
|
49
|
+
}
|
|
50
|
+
let rendered;
|
|
51
|
+
try {
|
|
52
|
+
rendered = (0, deploy_schema_1.idleTimeoutConfig)(runtimeConfig, proxyTimeouts);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return { code, ok: false, message: error.message };
|
|
56
|
+
}
|
|
57
|
+
const wait = (0, deploy_schema_1.longestProxyWait)(runtimeConfig, proxyTimeouts.entries);
|
|
58
|
+
const problems = [];
|
|
59
|
+
for (const key of explicit) {
|
|
60
|
+
const value = rendered[key];
|
|
61
|
+
const seconds = Number(value);
|
|
62
|
+
if (!/^\d+$/.test(value) || seconds > deploy_schema_1.MAX_IDLE_TIMEOUT_SECONDS) {
|
|
63
|
+
problems.push({ key, blocking: true, message: `${key} "${value}" is not 0 to ${deploy_schema_1.MAX_IDLE_TIMEOUT_SECONDS} seconds.` });
|
|
64
|
+
}
|
|
65
|
+
else if (!(0, deploy_schema_1.idleTimeoutCovers)(seconds, wait)) {
|
|
66
|
+
problems.push({
|
|
67
|
+
key,
|
|
68
|
+
blocking: key === "CONNECTION_IDLE_TIMEOUT",
|
|
69
|
+
message: key === "CONNECTION_IDLE_TIMEOUT"
|
|
70
|
+
? `CONNECTION_IDLE_TIMEOUT "${value}" closes a silent connection before the /api proxy's longest wait (${(0, deploy_schema_1.describeProxyWait)(wait)}), so the web server refuses to start; remove it from runtimeConfig to derive it.`
|
|
71
|
+
: `API_IDLE_TIMEOUT "${value}" closes a silent connection before the /api proxy's longest wait (${(0, deploy_schema_1.describeProxyWait)(wait)}), unless the API lifts it for that route; remove it from runtimeConfig to derive it.`,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (problems.length) {
|
|
76
|
+
const blocking = problems.some((problem) => problem.blocking);
|
|
77
|
+
return {
|
|
78
|
+
code,
|
|
79
|
+
ok: !blocking,
|
|
80
|
+
...(blocking ? {} : { warning: true }),
|
|
81
|
+
message: problems.map((problem) => problem.message).join(" "),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
code,
|
|
86
|
+
ok: true,
|
|
87
|
+
message: `CONNECTION_IDLE_TIMEOUT ${rendered.CONNECTION_IDLE_TIMEOUT} and API_IDLE_TIMEOUT ${rendered.API_IDLE_TIMEOUT} ` +
|
|
88
|
+
`outlast the /api proxy's longest wait (${(0, deploy_schema_1.describeProxyWait)(wait)}).`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** PostgreSQL's default max_connections, which a profile can change. */
|
|
92
|
+
exports.DEFAULT_MAX_CONNECTIONS = 100;
|
|
93
|
+
/** PostgreSQL's default superuser_reserved_connections, unavailable to the application. */
|
|
94
|
+
const RESERVED_CONNECTIONS = 3;
|
|
95
|
+
/** The migration process: Better Auth's schema check, then the application migrations. */
|
|
96
|
+
const MIGRATION_CONNECTIONS = 2;
|
|
97
|
+
function poolSize(runtimeConfig, key, fallback) {
|
|
98
|
+
const value = runtimeConfig[key];
|
|
99
|
+
if (value === undefined)
|
|
100
|
+
return fallback;
|
|
101
|
+
if (!/^[1-9][0-9]*$/.test(value))
|
|
102
|
+
throw new Error(`${key} "${value}" is not a positive integer.`);
|
|
103
|
+
return Number(value);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* The most PostgreSQL connections the deployment can open: every API and worker
|
|
107
|
+
* process with each pool full, the Kubernetes rollout's surge pod per workload
|
|
108
|
+
* (the rendered strategy allows one), and the migration that runs beside the
|
|
109
|
+
* release still serving. Compose replaces containers without a surge. Null when
|
|
110
|
+
* the deployment uses no PostgreSQL.
|
|
111
|
+
*/
|
|
112
|
+
function connectionBudget(profile, modules) {
|
|
113
|
+
const postgres = profile.dependencies.postgres;
|
|
114
|
+
if (postgres.mode === "disabled")
|
|
115
|
+
return null;
|
|
116
|
+
const config = profile.runtimeConfig;
|
|
117
|
+
const queryPool = poolSize(config, "DB_POOL_MAX", 20);
|
|
118
|
+
// The Bun SQL pool opens one more connection to check a server that stopped answering.
|
|
119
|
+
const perProcessTerms = [`DB_POOL_MAX ${queryPool} + 1`];
|
|
120
|
+
let perProcess = queryPool + 1;
|
|
121
|
+
if (modules.has("auth")) {
|
|
122
|
+
const authPool = poolSize(config, "DB_AUTH_POOL_MAX", 10);
|
|
123
|
+
perProcessTerms.push(`DB_AUTH_POOL_MAX ${authPool}`);
|
|
124
|
+
perProcess += authPool;
|
|
125
|
+
}
|
|
126
|
+
for (const pool of postgres.appPools ?? []) {
|
|
127
|
+
const size = poolSize(config, pool.sizeKey, 0);
|
|
128
|
+
perProcessTerms.push(`${pool.sizeKey} ${size}${pool.extraConnections ? ` + ${pool.extraConnections}` : ""}`);
|
|
129
|
+
perProcess += size + pool.extraConnections;
|
|
130
|
+
}
|
|
131
|
+
const worker = profile.workloads.worker;
|
|
132
|
+
const steadyProcesses = profile.workloads.api.replicas + (worker?.replicas ?? 0);
|
|
133
|
+
const rolloutProcesses = steadyProcesses + (profile.driver === "kubernetes-helm" ? 1 + (worker ? 1 : 0) : 0);
|
|
134
|
+
const maxConnections = postgres.maxConnections ?? exports.DEFAULT_MAX_CONNECTIONS;
|
|
135
|
+
const steady = steadyProcesses * perProcess;
|
|
136
|
+
const rollout = rolloutProcesses * perProcess;
|
|
137
|
+
return {
|
|
138
|
+
maxConnections,
|
|
139
|
+
available: maxConnections - RESERVED_CONNECTIONS,
|
|
140
|
+
perProcess,
|
|
141
|
+
perProcessTerms,
|
|
142
|
+
steadyProcesses,
|
|
143
|
+
steady,
|
|
144
|
+
rolloutProcesses,
|
|
145
|
+
rollout,
|
|
146
|
+
migration: MIGRATION_CONNECTIONS,
|
|
147
|
+
peak: Math.max(rollout, steady + MIGRATION_CONNECTIONS),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function budgetSummary(budget) {
|
|
151
|
+
return (`each API or worker process holds up to ${budget.perProcess} (${budget.perProcessTerms.join(" + ")}); ` +
|
|
152
|
+
`${budget.steadyProcesses} process(es) hold ${budget.steady}` +
|
|
153
|
+
(budget.rolloutProcesses > budget.steadyProcesses
|
|
154
|
+
? `, ${budget.rolloutProcesses} during a rollout hold ${budget.rollout}`
|
|
155
|
+
: "") +
|
|
156
|
+
`, and the migration adds ${budget.migration} to the ${budget.steady}`);
|
|
157
|
+
}
|
|
158
|
+
const BUDGET_FIX = "Lower DB_POOL_MAX or DB_AUTH_POOL_MAX in runtimeConfig, run fewer replicas, or raise dependencies.postgres.maxConnections.";
|
|
159
|
+
/** The plan warning for a budget over the limit, or null. */
|
|
160
|
+
function connectionBudgetWarning(profile, modules) {
|
|
161
|
+
let budget;
|
|
162
|
+
try {
|
|
163
|
+
budget = connectionBudget(profile, modules);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
return `Cannot count PostgreSQL connections: ${error.message}`;
|
|
167
|
+
}
|
|
168
|
+
if (!budget || budget.peak <= budget.available)
|
|
169
|
+
return null;
|
|
170
|
+
return (`PostgreSQL connections can reach ${budget.peak}, above the ${budget.available} that max_connections ` +
|
|
171
|
+
`${budget.maxConnections} leaves after ${RESERVED_CONNECTIONS} reserved: ${budgetSummary(budget)}. ${BUDGET_FIX}`);
|
|
172
|
+
}
|
|
173
|
+
function connectionBudgetFinding(profile, modules) {
|
|
174
|
+
const code = "database-connections";
|
|
175
|
+
let budget;
|
|
176
|
+
try {
|
|
177
|
+
budget = connectionBudget(profile, modules);
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
return { code, ok: false, message: `Cannot count PostgreSQL connections: ${error.message}` };
|
|
181
|
+
}
|
|
182
|
+
if (!budget)
|
|
183
|
+
return null;
|
|
184
|
+
const limit = `max_connections ${budget.maxConnections} leaves ${budget.available} after ${RESERVED_CONNECTIONS} reserved`;
|
|
185
|
+
if (budget.steady > budget.available) {
|
|
186
|
+
return {
|
|
187
|
+
code,
|
|
188
|
+
ok: false,
|
|
189
|
+
message: `PostgreSQL connections exceed the limit even between rollouts: ${limit}, and ${budgetSummary(budget)}. ${BUDGET_FIX}`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (budget.peak > budget.available) {
|
|
193
|
+
return {
|
|
194
|
+
code,
|
|
195
|
+
ok: true,
|
|
196
|
+
warning: true,
|
|
197
|
+
message: `PostgreSQL connections can reach ${budget.peak} during a deployment: ${limit}, and ${budgetSummary(budget)}. ${BUDGET_FIX}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
code,
|
|
202
|
+
ok: true,
|
|
203
|
+
message: `PostgreSQL connections stay within the limit, at most ${budget.peak}: ${limit}, and ${budgetSummary(budget)}.`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Whether the trusted chain the profile describes matches the topology PodoKit
|
|
208
|
+
* renders, so web and API record the client rather than a proxy in front.
|
|
209
|
+
*/
|
|
210
|
+
function clientAddressFinding(profile) {
|
|
211
|
+
const code = "client-address";
|
|
212
|
+
const edge = profile.exposure.edgeProxies;
|
|
213
|
+
const problems = [];
|
|
214
|
+
let rendered;
|
|
215
|
+
if (profile.driver === "docker-compose") {
|
|
216
|
+
rendered = (0, deploy_compose_render_1.composeProxyTrust)(profile);
|
|
217
|
+
const cidrs = profile.exposure.trustedProxyCidrs;
|
|
218
|
+
if ((0, deploy_compose_profile_1.composeHasGateway)(profile) && edge > 0 && cidrs.length === 0) {
|
|
219
|
+
problems.push(`exposure.edgeProxies is ${edge} but exposure.trustedProxyCidrs is empty, so the gateway sends the ` +
|
|
220
|
+
"nearest edge proxy's address as every request's client; list that proxy's CIDR.");
|
|
221
|
+
}
|
|
222
|
+
else if ((0, deploy_compose_profile_1.composeHasGateway)(profile) && edge === 0 && cidrs.length > 0) {
|
|
223
|
+
problems.push("exposure.trustedProxyCidrs trusts forwarded headers from a proxy, but exposure.edgeProxies is 0; " +
|
|
224
|
+
"set it to the number of proxies in front of the published port.");
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
rendered = (0, deploy_render_1.kubernetesProxyTrust)(profile);
|
|
229
|
+
const direct = [...profile.exposure.webSocketPaths, ...profile.exposure.apiPaths];
|
|
230
|
+
if (profile.exposure.mode === "ingress" && edge > 0 && direct.length > 0) {
|
|
231
|
+
problems.push(`With exposure.edgeProxies ${edge}, the Ingress forwards ${edge + 1} addresses on ${direct.join(", ")}, ` +
|
|
232
|
+
"where requests skip the web server, and the API, which trusts the one address the web server sends, " +
|
|
233
|
+
"records the nearest edge proxy for them. Pass the client address to the Ingress controller with " +
|
|
234
|
+
"the PROXY protocol instead, so the controller sees the client as its peer, and set edgeProxies to 0.");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const depth = rendered.XFF_DEPTH;
|
|
238
|
+
const defaultOrigin = "keeps its default origin, https and the Host header,";
|
|
239
|
+
const web = !depth
|
|
240
|
+
? `trusts no forwarded header: it ${defaultOrigin} and takes the client address from the connection`
|
|
241
|
+
: `${rendered.PROTOCOL_HEADER ? "takes its origin from X-Forwarded-Proto and X-Forwarded-Host" : defaultOrigin} ` +
|
|
242
|
+
`and takes the client from the X-Forwarded-For entry ${depth === "1" ? "its peer wrote" : `${depth} hops away`} ` +
|
|
243
|
+
`(XFF_DEPTH ${depth})`;
|
|
244
|
+
const summary = `the web server ${web}, and the API trusts the one address the web server sends (TRUSTED_PROXY_HOPS 1)`;
|
|
245
|
+
if (problems.length)
|
|
246
|
+
return { code, ok: true, warning: true, message: `${problems.join(" ")} Rendered: ${summary}.` };
|
|
247
|
+
const traefik = profile.driver === "kubernetes-helm" && profile.exposure.mode === "ingress" && edge > 0
|
|
248
|
+
? " The Ingress controller must trust the edge proxies' forwarded headers, such as Traefik's forwardedHeaders.trustedIPs."
|
|
249
|
+
: "";
|
|
250
|
+
return { code, ok: true, message: `With exposure.edgeProxies ${edge}, ${summary}.${traefik}` };
|
|
251
|
+
}
|
|
252
|
+
function profileFindings(projectRoot, profile) {
|
|
253
|
+
const modules = installedModules(projectRoot);
|
|
254
|
+
const findings = [
|
|
255
|
+
clientAddressFinding(profile),
|
|
256
|
+
idleTimeoutFinding(profile.runtimeConfig, (0, app_proxy_timeouts_1.readAppProxyTimeouts)(projectRoot)),
|
|
257
|
+
];
|
|
258
|
+
const budget = connectionBudgetFinding(profile, modules);
|
|
259
|
+
if (budget)
|
|
260
|
+
findings.push(budget);
|
|
261
|
+
if (modules.has("audit-log"))
|
|
262
|
+
findings.push(auditLogFinding(profile.runtimeConfig));
|
|
263
|
+
return findings;
|
|
264
|
+
}
|
|
265
|
+
function formatFinding(finding) {
|
|
266
|
+
return `${finding.ok ? (finding.warning ? "warn" : "ok ") : "FAIL"} ${finding.message}`;
|
|
267
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MigrationProfile, ReleaseProfile, VerificationProfile } from "./deploy-schema";
|
|
1
|
+
import { MigrationProfile, PostgresBudgetProfile, ReleaseProfile, VerificationProfile } from "./deploy-schema";
|
|
2
2
|
/**
|
|
3
3
|
* The Docker Compose deployment driver.
|
|
4
4
|
*
|
|
@@ -32,6 +32,8 @@ export interface ComposeDependencyProfile {
|
|
|
32
32
|
requiredKeys: string[];
|
|
33
33
|
volume: string;
|
|
34
34
|
}
|
|
35
|
+
export interface ComposePostgresProfile extends ComposeDependencyProfile, PostgresBudgetProfile {
|
|
36
|
+
}
|
|
35
37
|
export interface ComposeObjectStorageProfile extends ComposeDependencyProfile {
|
|
36
38
|
bucket: string;
|
|
37
39
|
clientImage: string;
|
|
@@ -71,6 +73,8 @@ export interface DockerComposeProfileV1 {
|
|
|
71
73
|
webSocketPaths: string[];
|
|
72
74
|
/** Path prefixes under /api/ that the gateway sends to the API with /api removed. */
|
|
73
75
|
apiPaths: string[];
|
|
76
|
+
/** Reverse proxies in front of the published port that append to X-Forwarded-For. */
|
|
77
|
+
edgeProxies: number;
|
|
74
78
|
trustedProxyCidrs: string[];
|
|
75
79
|
gatewayImage: string;
|
|
76
80
|
};
|
|
@@ -80,7 +84,7 @@ export interface DockerComposeProfileV1 {
|
|
|
80
84
|
worker: ComposeWorkloadProfile | null;
|
|
81
85
|
};
|
|
82
86
|
dependencies: {
|
|
83
|
-
postgres:
|
|
87
|
+
postgres: ComposePostgresProfile;
|
|
84
88
|
redis: ComposeDependencyProfile;
|
|
85
89
|
objectStorage: ComposeObjectStorageProfile;
|
|
86
90
|
};
|