@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
|
@@ -6,19 +6,25 @@ import {
|
|
|
6
6
|
backendReachable,
|
|
7
7
|
bodyLimitFor,
|
|
8
8
|
checkBackend,
|
|
9
|
+
checkIdleTimeout,
|
|
10
|
+
configuredPublicOrigins,
|
|
9
11
|
envMilliseconds,
|
|
10
12
|
IN_MEMORY_RESPONSE_BYTES,
|
|
11
13
|
proxyRequest,
|
|
14
|
+
proxyTimeoutFor,
|
|
12
15
|
reportBackendAnswer,
|
|
13
16
|
rawSubpath,
|
|
14
17
|
resolveClientIp,
|
|
15
18
|
sortedBodyLimits,
|
|
19
|
+
sortedProxyTimeouts,
|
|
20
|
+
upstreamFetchOptions,
|
|
16
21
|
withAppHeaders,
|
|
17
22
|
} from "./backend-proxy";
|
|
18
23
|
|
|
19
24
|
describe("backend proxy client address handling", () => {
|
|
20
25
|
afterEach(() => {
|
|
21
26
|
vi.unstubAllGlobals();
|
|
27
|
+
vi.unstubAllEnvs();
|
|
22
28
|
});
|
|
23
29
|
|
|
24
30
|
it("returns the normalized address from a trusted proxy chain", () => {
|
|
@@ -71,6 +77,48 @@ describe("backend proxy client address handling", () => {
|
|
|
71
77
|
expect(upstreamHeaders?.get("x-forwarded-for")).toBe("203.0.113.9");
|
|
72
78
|
});
|
|
73
79
|
|
|
80
|
+
it("forwards the public origin a request came in on when CORS_ORIGIN lists it", async () => {
|
|
81
|
+
let upstreamHeaders: Headers | undefined;
|
|
82
|
+
vi.stubGlobal("fetch", async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
83
|
+
upstreamHeaders = new Headers(init?.headers);
|
|
84
|
+
return new Response(null, { status: 204 });
|
|
85
|
+
});
|
|
86
|
+
vi.stubEnv("CORS_ORIGIN", "https://example.com,https://mirror.example.com");
|
|
87
|
+
|
|
88
|
+
// The adapter builds the request URL from the gateway's forwarded host and
|
|
89
|
+
// protocol; a caller's own forwarded headers never reach the API.
|
|
90
|
+
const request = new Request("https://mirror.example.com/api/mcp/info", {
|
|
91
|
+
headers: { "x-forwarded-host": "attacker.example", "x-forwarded-proto": "http" },
|
|
92
|
+
});
|
|
93
|
+
await proxyRequest(request, "http://api:5002/mcp/info");
|
|
94
|
+
|
|
95
|
+
expect(upstreamHeaders?.get("x-forwarded-host")).toBe("mirror.example.com");
|
|
96
|
+
expect(upstreamHeaders?.get("x-forwarded-proto")).toBe("https");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("forwards no origin that CORS_ORIGIN does not list", async () => {
|
|
100
|
+
let upstreamHeaders: Headers | undefined;
|
|
101
|
+
vi.stubGlobal("fetch", async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
102
|
+
upstreamHeaders = new Headers(init?.headers);
|
|
103
|
+
return new Response(null, { status: 204 });
|
|
104
|
+
});
|
|
105
|
+
vi.stubEnv("CORS_ORIGIN", "https://example.com");
|
|
106
|
+
|
|
107
|
+
const request = new Request("http://attacker.example/api/mcp/info", {
|
|
108
|
+
headers: { "x-forwarded-host": "example.com", "x-forwarded-proto": "https" },
|
|
109
|
+
});
|
|
110
|
+
await proxyRequest(request, "http://api:5002/mcp/info");
|
|
111
|
+
|
|
112
|
+
expect(upstreamHeaders?.has("x-forwarded-host")).toBe(false);
|
|
113
|
+
expect(upstreamHeaders?.has("x-forwarded-proto")).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("reads absolute CORS_ORIGIN entries as origins and skips patterns", () => {
|
|
117
|
+
expect([...configuredPublicOrigins(" https://example.com/ , *.example.com, not a url, http://localhost:5001")])
|
|
118
|
+
.toEqual(["https://example.com", "http://localhost:5001"]);
|
|
119
|
+
expect(configuredPublicOrigins("").size).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
74
122
|
// Not forwarding this does not leave the header unset: fetch substitutes its own
|
|
75
123
|
// default, so the API records "node" for every session and the account page shows
|
|
76
124
|
// that as the device for all of them.
|
|
@@ -550,53 +598,110 @@ describe("backend proxy application body limits", () => {
|
|
|
550
598
|
expect(bodyLimitFor("/other", limits)).toBeUndefined();
|
|
551
599
|
});
|
|
552
600
|
|
|
601
|
+
it("matches :name parameters against one non-empty segment, most specific rule first", () => {
|
|
602
|
+
const limits = sortedBodyLimits([
|
|
603
|
+
{ pathPrefix: "/api/hosts", maxBytes: 100 },
|
|
604
|
+
{ pathPrefix: "/api/hosts/:hostId/file-transfers", maxBytes: 1_048_576 },
|
|
605
|
+
{ pathPrefix: "/api/hosts/local/:section", maxBytes: 10 },
|
|
606
|
+
{ pathPrefix: "/api/hosts/:hostId/:section/:item", maxBytes: 20 },
|
|
607
|
+
]);
|
|
608
|
+
|
|
609
|
+
expect(bodyLimitFor("/api/hosts/h1/file-transfers", limits)).toBe(1_048_576);
|
|
610
|
+
expect(bodyLimitFor("/api/hosts/h%31/file-transfers", limits)).toBe(1_048_576);
|
|
611
|
+
// More segments win, then a literal segment over a parameter from the left.
|
|
612
|
+
expect(bodyLimitFor("/api/hosts/h1/file-transfers/t1/chunk", limits)).toBe(20);
|
|
613
|
+
expect(bodyLimitFor("/api/hosts/local/file-transfers", limits)).toBe(10);
|
|
614
|
+
expect(bodyLimitFor("/api/hosts/h1/file-transfers-old", limits)).toBe(100);
|
|
615
|
+
expect(bodyLimitFor("/api/hosts/file-transfers", limits)).toBe(100);
|
|
616
|
+
expect(bodyLimitFor("/api/hosts//file-transfers", limits)).toBe(100);
|
|
617
|
+
expect(bodyLimitFor("/api/other", limits)).toBeUndefined();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it("caps the body of a route with a path parameter", async () => {
|
|
621
|
+
const proxy = await proxyWithBodyLimits([{ pathPrefix: "/api/hosts/:hostId/file-transfers", maxBytes: 8 }]);
|
|
622
|
+
const record = recordingFetch();
|
|
623
|
+
|
|
624
|
+
const capped = await proxy(
|
|
625
|
+
new Request("http://app.localhost/api/hosts/h1/file-transfers/t1/chunk", { method: "POST", body: "0123456789" }),
|
|
626
|
+
"http://api:5002/hosts/h1/file-transfers/t1/chunk",
|
|
627
|
+
);
|
|
628
|
+
const other = await proxy(
|
|
629
|
+
new Request("http://app.localhost/api/hosts/h1/notes", { method: "POST", body: "0123456789" }),
|
|
630
|
+
"http://api:5002/hosts/h1/notes",
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
expect(capped.status).toBe(413);
|
|
634
|
+
expect(other.status).toBe(204);
|
|
635
|
+
expect(record.bodies.map((body) => body.byteLength)).toEqual([10]);
|
|
636
|
+
});
|
|
637
|
+
|
|
553
638
|
it.each<[{ pathPrefix: string; maxBytes: number }[], string]>([
|
|
554
|
-
[[{ pathPrefix: "/feedback/", maxBytes: 10 }], "must be a
|
|
555
|
-
[[{ pathPrefix: "/api/files/*", maxBytes: 10 }], "must be a
|
|
556
|
-
[[{ pathPrefix: "/api//files", maxBytes: 10 }], "must be a
|
|
557
|
-
[[{ pathPrefix: "/api/files/../admin", maxBytes: 10 }], "must be a
|
|
639
|
+
[[{ pathPrefix: "/feedback/", maxBytes: 10 }], "must be a path under /api/"],
|
|
640
|
+
[[{ pathPrefix: "/api/files/*", maxBytes: 10 }], "must be a path under /api/"],
|
|
641
|
+
[[{ pathPrefix: "/api//files", maxBytes: 10 }], "must be a path under /api/"],
|
|
642
|
+
[[{ pathPrefix: "/api/files/../admin", maxBytes: 10 }], "must be a path under /api/"],
|
|
558
643
|
[[{ pathPrefix: "/api/files", maxBytes: 0 }], "positive whole number"],
|
|
559
644
|
[[{ pathPrefix: "/api/files", maxBytes: 1.5 }], "positive whole number"],
|
|
560
|
-
[[{ pathPrefix: "/
|
|
645
|
+
[[{ pathPrefix: "/apix/files", maxBytes: 10 }], "must be a path under /api/"],
|
|
646
|
+
[[{ pathPrefix: "/api/files/:", maxBytes: 10 }], "must be a path under /api/"],
|
|
647
|
+
[[{ pathPrefix: "/api/files/:1st", maxBytes: 10 }], "must be a path under /api/"],
|
|
648
|
+
[[{ pathPrefix: "/api/files/:file-id", maxBytes: 10 }], "must be a path under /api/"],
|
|
649
|
+
[[{ pathPrefix: "/api/files/:a:b", maxBytes: 10 }], "must be a path under /api/"],
|
|
650
|
+
[[{ pathPrefix: "/api/files", maxBytes: 10 }, { pathPrefix: "/api/files/", maxBytes: 20 }], "lists the same paths twice"],
|
|
651
|
+
[
|
|
652
|
+
[{ pathPrefix: "/api/hosts/:hostId", maxBytes: 10 }, { pathPrefix: "/api/hosts/:id/", maxBytes: 20 }],
|
|
653
|
+
'lists the same paths twice: "/api/hosts/:hostId" and "/api/hosts/:id/"',
|
|
654
|
+
],
|
|
561
655
|
])("rejects an invalid body limit list %#", (limits, message) => {
|
|
562
656
|
expect(() => sortedBodyLimits(limits)).toThrow(message);
|
|
563
657
|
});
|
|
564
658
|
|
|
565
659
|
it("refuses to load with an invalid application body limit", async () => {
|
|
566
660
|
await expect(proxyWithBodyLimits([{ pathPrefix: "/upload", maxBytes: 10 }])).rejects.toThrow(
|
|
567
|
-
"APP_BODY_LIMITS pathPrefix must be a
|
|
661
|
+
"APP_BODY_LIMITS pathPrefix must be a path under /api/",
|
|
568
662
|
);
|
|
569
663
|
});
|
|
570
664
|
});
|
|
571
665
|
|
|
572
|
-
|
|
573
|
-
type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
666
|
+
type Fetch = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
574
667
|
|
|
668
|
+
// Never answers on its own, like a blackholed connection or a hung API, and
|
|
669
|
+
// rejects when its signal aborts.
|
|
670
|
+
const hang: Fetch = (_input, init) =>
|
|
671
|
+
new Promise((_settle, reject) => {
|
|
672
|
+
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason));
|
|
673
|
+
});
|
|
674
|
+
const refuse: Fetch = async () => {
|
|
675
|
+
throw new TypeError("Unable to connect. Is the computer able to access the url?");
|
|
676
|
+
};
|
|
677
|
+
// Answers after a delay unless its signal aborts first, like a slow endpoint.
|
|
678
|
+
const answerAfter =
|
|
679
|
+
(ms: number): Fetch =>
|
|
680
|
+
(_input, init) =>
|
|
681
|
+
new Promise((settle, reject) => {
|
|
682
|
+
const timer = setTimeout(() => settle(Response.json({ done: true })), ms);
|
|
683
|
+
init?.signal?.addEventListener("abort", () => {
|
|
684
|
+
clearTimeout(timer);
|
|
685
|
+
reject(init.signal?.reason);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// Routes the background GET /health check and the proxied request separately.
|
|
690
|
+
function stubApi(health: Fetch, upstream: Fetch): ReturnType<typeof vi.fn<Fetch>> {
|
|
691
|
+
const fetch = vi.fn<Fetch>((input, init) =>
|
|
692
|
+
String(input).endsWith("/health") ? health(input, init) : upstream(input, init),
|
|
693
|
+
);
|
|
694
|
+
vi.stubGlobal("fetch", fetch);
|
|
695
|
+
return fetch;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
describe("backend proxy outages", () => {
|
|
575
699
|
afterEach(() => {
|
|
576
700
|
reportBackendAnswer();
|
|
577
701
|
vi.unstubAllGlobals();
|
|
578
702
|
vi.unstubAllEnvs();
|
|
579
703
|
});
|
|
580
704
|
|
|
581
|
-
// Never answers on its own, like a blackholed connection or a hung API, and
|
|
582
|
-
// rejects when its signal aborts.
|
|
583
|
-
const hang: Fetch = (_input, init) =>
|
|
584
|
-
new Promise((_settle, reject) => {
|
|
585
|
-
init?.signal?.addEventListener("abort", () => reject(init.signal?.reason));
|
|
586
|
-
});
|
|
587
|
-
const refuse: Fetch = async () => {
|
|
588
|
-
throw new TypeError("Unable to connect. Is the computer able to access the url?");
|
|
589
|
-
};
|
|
590
|
-
|
|
591
|
-
// Routes the background GET /health check and the proxied request separately.
|
|
592
|
-
function stubApi(health: Fetch, upstream: Fetch): ReturnType<typeof vi.fn<Fetch>> {
|
|
593
|
-
const fetch = vi.fn<Fetch>((input, init) =>
|
|
594
|
-
String(input).endsWith("/health") ? health(input, init) : upstream(input, init),
|
|
595
|
-
);
|
|
596
|
-
vi.stubGlobal("fetch", fetch);
|
|
597
|
-
return fetch;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
705
|
async function takeApiDown(): Promise<void> {
|
|
601
706
|
stubApi(refuse, refuse);
|
|
602
707
|
await checkBackend();
|
|
@@ -645,25 +750,37 @@ describe("backend proxy outages", () => {
|
|
|
645
750
|
expect(backendReachable()).toBe(false);
|
|
646
751
|
});
|
|
647
752
|
|
|
648
|
-
it("keeps waiting for a slow endpoint while the API answers its health
|
|
753
|
+
it("keeps waiting for a slow endpoint while the API answers its health checks", async () => {
|
|
649
754
|
vi.stubEnv("BACKEND_INTERNAL_URL", "http://api:5002");
|
|
650
755
|
vi.stubEnv("BACKEND_TIMEOUT_MS", "20");
|
|
651
|
-
const fetch = stubApi(
|
|
652
|
-
async () => new Response("ok"),
|
|
653
|
-
() => new Promise((settle) => setTimeout(() => settle(Response.json({ done: true })), 80)),
|
|
654
|
-
);
|
|
756
|
+
const fetch = stubApi(async () => new Response("ok"), answerAfter(100));
|
|
655
757
|
|
|
656
758
|
const response = await proxyRequest(new Request("http://app.localhost/api/report"), "http://api:5002/report");
|
|
657
759
|
|
|
658
760
|
expect(response.status).toBe(200);
|
|
659
761
|
expect(await response.json()).toEqual({ done: true });
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
]);
|
|
762
|
+
const [first, ...checks] = fetch.mock.calls.map(([input]) => String(input));
|
|
763
|
+
expect(first).toBe("http://api:5002/report");
|
|
764
|
+
expect(checks.length).toBeGreaterThanOrEqual(2);
|
|
765
|
+
expect(new Set(checks)).toEqual(new Set(["http://api:5002/health"]));
|
|
664
766
|
expect(backendReachable()).toBe(true);
|
|
665
767
|
});
|
|
666
768
|
|
|
769
|
+
it("fails a waiting request when the API stops answering after an earlier check", async () => {
|
|
770
|
+
vi.stubEnv("BACKEND_TIMEOUT_MS", "20");
|
|
771
|
+
vi.stubEnv("BACKEND_PROXY_TIMEOUT_MS", "10000");
|
|
772
|
+
let checks = 0;
|
|
773
|
+
stubApi((input, init) => (++checks <= 2 ? Promise.resolve(new Response("ok")) : hang(input, init)), hang);
|
|
774
|
+
|
|
775
|
+
const started = Date.now();
|
|
776
|
+
const response = await proxyRequest(new Request("http://app.localhost/api/report"), "http://api:5002/report");
|
|
777
|
+
|
|
778
|
+
expect(response.status).toBe(503);
|
|
779
|
+
expect(await response.json()).toMatchObject({ error: { code: "BACKEND_UNAVAILABLE" } });
|
|
780
|
+
expect(checks).toBeGreaterThanOrEqual(3);
|
|
781
|
+
expect(Date.now() - started).toBeLessThan(5_000);
|
|
782
|
+
});
|
|
783
|
+
|
|
667
784
|
it("answers at once without contacting the API while it is unreachable", async () => {
|
|
668
785
|
await takeApiDown();
|
|
669
786
|
const fetch = stubApi(refuse, async () => new Response(null, { status: 204 }));
|
|
@@ -739,3 +856,220 @@ describe("backend proxy outages", () => {
|
|
|
739
856
|
expect(envMilliseconds("BACKEND_TIMEOUT_MS", 2_000)).toBe(1_500);
|
|
740
857
|
});
|
|
741
858
|
});
|
|
859
|
+
|
|
860
|
+
describe("backend proxy application timeouts", () => {
|
|
861
|
+
afterEach(() => {
|
|
862
|
+
vi.unstubAllGlobals();
|
|
863
|
+
vi.unstubAllEnvs();
|
|
864
|
+
vi.doUnmock("./proxy-timeouts");
|
|
865
|
+
vi.resetModules();
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
async function proxyWithTimeouts(
|
|
869
|
+
timeouts: ReadonlyArray<{ pathPrefix: string; timeoutMs: number | null }>,
|
|
870
|
+
): Promise<typeof proxyRequest> {
|
|
871
|
+
vi.resetModules();
|
|
872
|
+
vi.doMock("./proxy-timeouts", () => ({ APP_PROXY_TIMEOUTS: timeouts }));
|
|
873
|
+
return (await import("./backend-proxy")).proxyRequest;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
it("waits longer on a route with a longer deadline", async () => {
|
|
877
|
+
vi.stubEnv("BACKEND_PROXY_TIMEOUT_MS", "20");
|
|
878
|
+
const proxy = await proxyWithTimeouts([{ pathPrefix: "/api/exports/", timeoutMs: 2_000 }]);
|
|
879
|
+
stubApi(async () => new Response("ok"), answerAfter(80));
|
|
880
|
+
|
|
881
|
+
const listed = await proxy(new Request("http://app.localhost/api/exports/42"), "http://api:5002/exports/42");
|
|
882
|
+
const other = await proxy(new Request("http://app.localhost/api/report"), "http://api:5002/report");
|
|
883
|
+
|
|
884
|
+
expect(listed.status).toBe(200);
|
|
885
|
+
expect(await listed.json()).toEqual({ done: true });
|
|
886
|
+
expect(other.status).toBe(504);
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it("answers 504 at a route's shorter deadline", async () => {
|
|
890
|
+
const proxy = await proxyWithTimeouts([{ pathPrefix: "/api/quick", timeoutMs: 20 }]);
|
|
891
|
+
stubApi(async () => new Response("ok"), hang);
|
|
892
|
+
|
|
893
|
+
const started = Date.now();
|
|
894
|
+
const response = await proxy(new Request("http://app.localhost/api/quick/check"), "http://api:5002/quick/check");
|
|
895
|
+
|
|
896
|
+
expect(response.status).toBe(504);
|
|
897
|
+
expect(await response.json()).toMatchObject({ error: { code: "BACKEND_TIMEOUT", path: "/api/quick/check" } });
|
|
898
|
+
expect(Date.now() - started).toBeLessThan(5_000);
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it("waits without a deadline on a null route until the API answers", async () => {
|
|
902
|
+
vi.stubEnv("BACKEND_TIMEOUT_MS", "20");
|
|
903
|
+
vi.stubEnv("BACKEND_PROXY_TIMEOUT_MS", "20");
|
|
904
|
+
const proxy = await proxyWithTimeouts([{ pathPrefix: "/api/file-transfers/", timeoutMs: null }]);
|
|
905
|
+
stubApi(async () => new Response("ok"), answerAfter(150));
|
|
906
|
+
|
|
907
|
+
const response = await proxy(
|
|
908
|
+
new Request("http://app.localhost/api/file-transfers/7/commit", { method: "POST", body: "{}" }),
|
|
909
|
+
"http://api:5002/file-transfers/7/commit",
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
expect(response.status).toBe(200);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it("still answers 503 on a null route once the API is found unreachable", async () => {
|
|
916
|
+
vi.stubEnv("BACKEND_TIMEOUT_MS", "20");
|
|
917
|
+
const proxy = await proxyWithTimeouts([{ pathPrefix: "/api/file-transfers/", timeoutMs: null }]);
|
|
918
|
+
let checks = 0;
|
|
919
|
+
stubApi((input, init) => (++checks <= 2 ? Promise.resolve(new Response("ok")) : hang(input, init)), hang);
|
|
920
|
+
|
|
921
|
+
const response = await proxy(
|
|
922
|
+
new Request("http://app.localhost/api/file-transfers/7/commit", { method: "POST", body: "{}" }),
|
|
923
|
+
"http://api:5002/file-transfers/7/commit",
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
expect(response.status).toBe(503);
|
|
927
|
+
expect(response.headers.get("retry-after")).toBe("2");
|
|
928
|
+
expect(await response.json()).toMatchObject({
|
|
929
|
+
error: { code: "BACKEND_UNAVAILABLE", path: "/api/file-transfers/7/commit" },
|
|
930
|
+
});
|
|
931
|
+
expect(checks).toBeGreaterThanOrEqual(3);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
it("turns off Bun's fetch idle timeout on a route without a deadline only", async () => {
|
|
935
|
+
const proxy = await proxyWithTimeouts([{ pathPrefix: "/api/hosts/:hostId/file-transfers", timeoutMs: null }]);
|
|
936
|
+
const fetch = stubApi(async () => new Response("ok"), async () => Response.json({ done: true }));
|
|
937
|
+
|
|
938
|
+
await proxy(
|
|
939
|
+
new Request("http://app.localhost/api/hosts/h1/file-transfers/t1/commit", { method: "POST", body: "{}" }),
|
|
940
|
+
"http://api:5002/hosts/h1/file-transfers/t1/commit",
|
|
941
|
+
);
|
|
942
|
+
await proxy(new Request("http://app.localhost/api/hosts/h1/notes"), "http://api:5002/hosts/h1/notes");
|
|
943
|
+
|
|
944
|
+
const upstream = fetch.mock.calls.filter(([input]) => !String(input).endsWith("/health"));
|
|
945
|
+
const [commit, notes] = upstream.map(([, init]) => init as RequestInit & { timeout?: unknown });
|
|
946
|
+
expect(upstream).toHaveLength(2);
|
|
947
|
+
expect(commit?.timeout).toBe(false);
|
|
948
|
+
expect(notes).not.toHaveProperty("timeout");
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
it("turns off Bun's fetch idle timeout for a deadline it would cut first", async () => {
|
|
952
|
+
expect(upstreamFetchOptions(null)).toEqual({ timeout: false });
|
|
953
|
+
expect(upstreamFetchOptions(600_000)).toEqual({ timeout: false });
|
|
954
|
+
expect(upstreamFetchOptions(300_000)).toEqual({ timeout: false });
|
|
955
|
+
expect(upstreamFetchOptions(299_999)).toEqual({});
|
|
956
|
+
expect(upstreamFetchOptions(30_000)).toEqual({});
|
|
957
|
+
|
|
958
|
+
vi.stubEnv("BACKEND_PROXY_TIMEOUT_MS", "300000");
|
|
959
|
+
vi.stubEnv("CONNECTION_IDLE_TIMEOUT", "0");
|
|
960
|
+
const proxy = await proxyWithTimeouts([]);
|
|
961
|
+
const fetch = stubApi(async () => new Response("ok"), async () => Response.json({ done: true }));
|
|
962
|
+
|
|
963
|
+
await proxy(new Request("http://app.localhost/api/report"), "http://api:5002/report");
|
|
964
|
+
|
|
965
|
+
expect(fetch.mock.calls[0]?.[1]).toHaveProperty("timeout", false);
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
it("waits without a deadline on a route with a path parameter", async () => {
|
|
969
|
+
vi.stubEnv("BACKEND_PROXY_TIMEOUT_MS", "20");
|
|
970
|
+
const proxy = await proxyWithTimeouts([{ pathPrefix: "/api/hosts/:hostId/file-transfers", timeoutMs: null }]);
|
|
971
|
+
stubApi(async () => new Response("ok"), answerAfter(80));
|
|
972
|
+
|
|
973
|
+
const commit = await proxy(
|
|
974
|
+
new Request("http://app.localhost/api/hosts/h1/file-transfers/t1/commit", { method: "POST", body: "{}" }),
|
|
975
|
+
"http://api:5002/hosts/h1/file-transfers/t1/commit",
|
|
976
|
+
);
|
|
977
|
+
const other = await proxy(new Request("http://app.localhost/api/hosts/h1/notes"), "http://api:5002/hosts/h1/notes");
|
|
978
|
+
|
|
979
|
+
expect(commit.status).toBe(200);
|
|
980
|
+
expect(other.status).toBe(504);
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
it("applies the longest matching prefix on the decoded path and the default elsewhere", () => {
|
|
984
|
+
vi.stubEnv("BACKEND_PROXY_TIMEOUT_MS", "1234");
|
|
985
|
+
const timeouts = sortedProxyTimeouts([
|
|
986
|
+
{ pathPrefix: "/api/", timeoutMs: 5_000 },
|
|
987
|
+
{ pathPrefix: "/api/file-transfers/", timeoutMs: null },
|
|
988
|
+
{ pathPrefix: "/api/file-transfers/status", timeoutMs: 100 },
|
|
989
|
+
]);
|
|
990
|
+
|
|
991
|
+
expect(proxyTimeoutFor("/api/file-transfers/7/commit", timeouts)).toBeNull();
|
|
992
|
+
expect(proxyTimeoutFor("/api/file-transfers", timeouts)).toBeNull();
|
|
993
|
+
expect(proxyTimeoutFor("/api/file-transfers/status/7", timeouts)).toBe(100);
|
|
994
|
+
expect(proxyTimeoutFor("/api/%66ile-transfers/7", timeouts)).toBeNull();
|
|
995
|
+
expect(proxyTimeoutFor("/api/file-transfers-old/7", timeouts)).toBe(5_000);
|
|
996
|
+
expect(proxyTimeoutFor("/api/auth/get-session", sortedProxyTimeouts([]))).toBe(1234);
|
|
997
|
+
expect(proxyTimeoutFor("/other", timeouts)).toBe(1234);
|
|
998
|
+
|
|
999
|
+
const byHost = sortedProxyTimeouts([
|
|
1000
|
+
{ pathPrefix: "/api/hosts/:hostId/file-transfers", timeoutMs: null },
|
|
1001
|
+
{ pathPrefix: "/api/hosts/local/file-transfers", timeoutMs: 60_000 },
|
|
1002
|
+
]);
|
|
1003
|
+
expect(proxyTimeoutFor("/api/hosts/h1/file-transfers/t1/commit", byHost)).toBeNull();
|
|
1004
|
+
expect(proxyTimeoutFor("/api/hosts/local/file-transfers/t1/commit", byHost)).toBe(60_000);
|
|
1005
|
+
expect(proxyTimeoutFor("/api/hosts/file-transfers", byHost)).toBe(1234);
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it.each<[{ pathPrefix: string; timeoutMs: number | null }[], string]>([
|
|
1009
|
+
[[{ pathPrefix: "/exports/", timeoutMs: 10 }], "must be a path under /api/"],
|
|
1010
|
+
[[{ pathPrefix: "/api/exports/*", timeoutMs: 10 }], "must be a path under /api/"],
|
|
1011
|
+
[[{ pathPrefix: "/api/exports/../auth", timeoutMs: 10 }], "must be a path under /api/"],
|
|
1012
|
+
[[{ pathPrefix: "/api/exports", timeoutMs: 0 }], "must be null or a whole number of milliseconds"],
|
|
1013
|
+
[[{ pathPrefix: "/api/exports", timeoutMs: 1.5 }], "must be null or a whole number of milliseconds"],
|
|
1014
|
+
[[{ pathPrefix: "/api/exports", timeoutMs: 2_147_483_648 }], "must be null or a whole number of milliseconds"],
|
|
1015
|
+
[[{ pathPrefix: "/api/exports/:", timeoutMs: 10 }], "must be a path under /api/"],
|
|
1016
|
+
[[{ pathPrefix: "/api/exports", timeoutMs: null }, { pathPrefix: "/api/exports/", timeoutMs: 10 }], "lists the same paths twice"],
|
|
1017
|
+
[[{ pathPrefix: "/api/:a/x", timeoutMs: null }, { pathPrefix: "/api/:b/x", timeoutMs: 10 }], "lists the same paths twice"],
|
|
1018
|
+
])("rejects an invalid timeout list %#", (timeouts, message) => {
|
|
1019
|
+
expect(() => sortedProxyTimeouts(timeouts)).toThrow(message);
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
it.each([undefined, "0", "35", "soon"])("accepts CONNECTION_IDLE_TIMEOUT=%s with the default deadline", (idle) => {
|
|
1023
|
+
expect(() => checkIdleTimeout(idle, 30_000, [])).not.toThrow();
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it("requires the idle timeout to outlast the longest deadline by Bun's check interval", () => {
|
|
1027
|
+
expect(() => checkIdleTimeout("34", 30_000, [])).toThrow(
|
|
1028
|
+
"CONNECTION_IDLE_TIMEOUT=34 lets the web server close a silent connection before the /api proxy stops " +
|
|
1029
|
+
"waiting for response headers (30000 ms, BACKEND_PROXY_TIMEOUT_MS)",
|
|
1030
|
+
);
|
|
1031
|
+
expect(() => checkIdleTimeout("10", 30_000, [])).toThrow("Set CONNECTION_IDLE_TIMEOUT to at least 35, or to 0");
|
|
1032
|
+
expect(() => checkIdleTimeout("35", 30_500, [])).not.toThrow();
|
|
1033
|
+
expect(() => checkIdleTimeout("35", 31_000, [])).toThrow("at least 36");
|
|
1034
|
+
|
|
1035
|
+
const routes = sortedProxyTimeouts([
|
|
1036
|
+
{ pathPrefix: "/api/quick/", timeoutMs: 5_000 },
|
|
1037
|
+
{ pathPrefix: "/api/exports/", timeoutMs: 120_000 },
|
|
1038
|
+
]);
|
|
1039
|
+
expect(() => checkIdleTimeout("35", 30_000, routes)).toThrow(
|
|
1040
|
+
"(120000 ms, APP_PROXY_TIMEOUTS for /api/exports); Bun closes idle connections up to 4 seconds early. " +
|
|
1041
|
+
"Set CONNECTION_IDLE_TIMEOUT to at least 125, or to 0 to turn the idle timeout off.",
|
|
1042
|
+
);
|
|
1043
|
+
expect(() => checkIdleTimeout("125", 30_000, routes)).not.toThrow();
|
|
1044
|
+
expect(() => checkIdleTimeout("0", 30_000, routes)).not.toThrow();
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
it("requires the idle timeout off for a route without a deadline or beyond Bun's limit", () => {
|
|
1048
|
+
const unbounded = sortedProxyTimeouts([
|
|
1049
|
+
{ pathPrefix: "/api/exports/", timeoutMs: 120_000 },
|
|
1050
|
+
{ pathPrefix: "/api/file-transfers/", timeoutMs: null },
|
|
1051
|
+
]);
|
|
1052
|
+
expect(() => checkIdleTimeout("255", 30_000, unbounded)).toThrow(
|
|
1053
|
+
"(no deadline, APP_PROXY_TIMEOUTS for /api/file-transfers); Bun closes idle connections up to 4 seconds " +
|
|
1054
|
+
"early. Set CONNECTION_IDLE_TIMEOUT to 0 to turn the idle timeout off, since Bun allows at most 255 seconds.",
|
|
1055
|
+
);
|
|
1056
|
+
expect(() => checkIdleTimeout("0", 30_000, unbounded)).not.toThrow();
|
|
1057
|
+
expect(() => checkIdleTimeout("255", 250_999, [])).not.toThrow();
|
|
1058
|
+
expect(() => checkIdleTimeout("255", 251_000, [])).toThrow("Set CONNECTION_IDLE_TIMEOUT to 0");
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
it("refuses to load when CONNECTION_IDLE_TIMEOUT cannot cover a route's deadline", async () => {
|
|
1062
|
+
vi.stubEnv("CONNECTION_IDLE_TIMEOUT", "35");
|
|
1063
|
+
await expect(proxyWithTimeouts([{ pathPrefix: "/api/file-transfers/", timeoutMs: null }])).rejects.toThrow(
|
|
1064
|
+
"CONNECTION_IDLE_TIMEOUT=35 lets the web server close a silent connection",
|
|
1065
|
+
);
|
|
1066
|
+
vi.stubEnv("CONNECTION_IDLE_TIMEOUT", "0");
|
|
1067
|
+
await expect(proxyWithTimeouts([{ pathPrefix: "/api/file-transfers/", timeoutMs: null }])).resolves.toBeDefined();
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
it("refuses to load with an invalid application timeout", async () => {
|
|
1071
|
+
await expect(proxyWithTimeouts([{ pathPrefix: "/api/exports", timeoutMs: -1 }])).rejects.toThrow(
|
|
1072
|
+
"APP_PROXY_TIMEOUTS timeoutMs for /api/exports must be null",
|
|
1073
|
+
);
|
|
1074
|
+
});
|
|
1075
|
+
});
|