@podosoft/podokit 1.1.6 → 1.3.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/dist/add.d.ts +3 -1
- package/dist/add.js +116 -14
- package/dist/deploy-render.js +5 -0
- package/dist/index.js +8 -0
- package/dist/lib.d.ts +2 -2
- package/dist/lib.js +3 -2
- package/dist/lockfile.d.ts +3 -1
- package/dist/lockfile.js +17 -6
- package/dist/templates/fullstack/apps/api/package.json +2 -2
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +81 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +53 -5
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +6 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +10 -1
- package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +30 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +19 -3
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +15 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +3 -0
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +149 -4
- package/dist/templates/fullstack/apps/api/src/database/database.ts +66 -13
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +86 -0
- package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +4 -9
- package/dist/templates/fullstack/apps/api/src/health/readiness.service.spec.ts +141 -1
- package/dist/templates/fullstack/apps/api/src/health/readiness.service.ts +86 -12
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +188 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +30 -0
- package/dist/templates/fullstack/apps/web/src/routes/api/[...path]/+server.ts +3 -3
- package/dist/templates/fullstack/apps/web/src/routes/api/auth/[...all]/+server.ts +3 -3
- package/dist/templates/fullstack/compose.dev.yaml +21 -0
- package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +8 -1
- package/dist/templates/fullstack/dot-env.docker +3 -0
- package/dist/templates/fullstack/dot-env.example +14 -0
- package/dist/templates/fullstack/infra/k3s/api-deployment.yaml +2 -0
- package/dist/templates/fullstack/tests/playwright.extensions.ts +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +21 -26
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.migration.spec.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.spec.ts +25 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.spec.ts +122 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +187 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +160 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.spec.ts +76 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.spec.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.ts +153 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +22 -28
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +60 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/migrations/1792060000000-IndexSessionCreatedAt.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +3 -5
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +6 -17
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.spec.ts +167 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.ts +50 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.spec.ts +158 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +78 -28
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +3 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +19 -11
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-data.server.ts +6 -32
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.test.ts +74 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.ts +73 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +102 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +141 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.test.ts +68 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.ts +108 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +14 -8
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/session-idle-timeout.svelte +74 -49
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.test.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +23 -6
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.test.ts +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.test.ts +98 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.ts +54 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.test.ts +23 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +2 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts +7 -50
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/public-paths.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/audit/+page.svelte +88 -45
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +76 -75
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.server.ts +15 -14
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +2 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +79 -37
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +10 -17
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.ts +10 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +4 -3
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.svelte +14 -8
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/admin-sessions.api.spec.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/admin-users.api.spec.ts +86 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +75 -12
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +1 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +35 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +7 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +72 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +36 -2
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -13
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +0 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +16 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +45 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +64 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +8 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +32 -1
- package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/README.md +46 -5
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-events.ts +19 -7
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +126 -3
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +13 -11
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.spec.ts +115 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.ts +44 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +421 -43
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +382 -35
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +57 -0
- package/dist/templates/modules/audit-log/module.manifest.json +7 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +20 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +24 -29
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +28 -13
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.spec.ts +56 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.ts +22 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +81 -5
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -9
- package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.spec.ts +38 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.ts +43 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +24 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +31 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.spec.ts +58 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +46 -57
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.spec.ts +53 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.ts +65 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +21 -37
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.spec.ts +23 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.spec.ts +33 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.ts +29 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.spec.ts +81 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +37 -34
- package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
- package/dist/templates/modules/auth/module.manifest.json +2 -2
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +136 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +34 -13
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.spec.ts +60 -0
- package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.ts +51 -0
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +70 -9
- package/dist/templates/modules/file-upload/module.manifest.json +9 -3
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +26 -32
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +16 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +31 -4
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +145 -0
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.ts +37 -0
- package/dist/templates/modules/job-progress/files/tests/api/progress.api.spec.ts +94 -4
- package/dist/templates/modules/job-progress/module.manifest.json +3 -3
- package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.spec.ts +54 -0
- package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.ts +47 -25
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.spec.ts +86 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +159 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-demo.ts +38 -0
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +172 -4
- package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +110 -33
- package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +88 -0
- package/dist/templates/modules/object-storage-local/module.manifest.json +6 -1
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +80 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +13 -7
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +156 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.ts +38 -0
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +39 -21
- package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +101 -2
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +84 -9
- package/dist/templates/modules/object-storage-s3/module.manifest.json +8 -3
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +9 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +7 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +139 -21
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +80 -11
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +12 -6
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +15 -2
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +46 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +4 -3
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +125 -0
- package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +27 -13
- package/dist/templates/modules/sse/files/apps/api/src/events/event-envelope.ts +123 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +191 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/event-topics.ts +64 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +250 -0
- package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +121 -15
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +407 -18
- package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +153 -40
- package/dist/templates/modules/sse/files/apps/web/src/lib/events.test.ts +216 -0
- package/dist/templates/modules/sse/files/apps/web/src/lib/events.ts +207 -0
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +227 -70
- package/dist/templates/modules/sse/module.manifest.json +20 -5
- package/dist/update.d.ts +17 -0
- package/dist/update.js +33 -8
- package/package.json +2 -2
|
@@ -1,30 +1,104 @@
|
|
|
1
1
|
export type ReadinessStatus = "up" | "down";
|
|
2
2
|
export type ReadinessCheck = () => Promise<void>;
|
|
3
|
+
export type ReadinessResults = Record<string, ReadinessStatus>;
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_READINESS_CHECK_TIMEOUT_MS = 800;
|
|
6
|
+
export const DEFAULT_READINESS_CACHE_MS = 2_000;
|
|
7
|
+
|
|
8
|
+
/** The core database check. The health endpoint reports it as `db`, apart from module checks. */
|
|
9
|
+
export const DATABASE_READINESS_CHECK = "database";
|
|
10
|
+
|
|
11
|
+
export interface ReadinessOptions {
|
|
12
|
+
/** Longest one check may run before it is reported down. */
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
/** How long a finished evaluation is reused. `0` evaluates on every run. */
|
|
15
|
+
cacheMs?: number;
|
|
16
|
+
/** Receives the reason for each check that is down, once per evaluation. */
|
|
17
|
+
onCheckFailure?: (name: string, reason: string) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class ReadinessTimeoutError extends Error {
|
|
21
|
+
constructor(name: string, timeoutMs: number) {
|
|
22
|
+
super(`Readiness check "${name}" timed out after ${timeoutMs} ms`);
|
|
23
|
+
this.name = "ReadinessTimeoutError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
3
26
|
|
|
4
27
|
export class ReadinessService {
|
|
5
28
|
private readonly checks = new Map<string, ReadinessCheck>();
|
|
29
|
+
private readonly timeoutMs: number;
|
|
30
|
+
private readonly cacheMs: number;
|
|
31
|
+
private readonly onCheckFailure: ((name: string, reason: string) => void) | undefined;
|
|
32
|
+
// Bumped whenever the registered checks change, so an evaluation that started
|
|
33
|
+
// earlier is neither shared nor cached afterwards.
|
|
34
|
+
private generation = 0;
|
|
35
|
+
private inFlight?: Promise<ReadinessResults>;
|
|
36
|
+
private cached?: { results: ReadinessResults; expiresAt: number };
|
|
37
|
+
|
|
38
|
+
constructor(options: ReadinessOptions = {}) {
|
|
39
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_READINESS_CHECK_TIMEOUT_MS;
|
|
40
|
+
this.cacheMs = options.cacheMs ?? DEFAULT_READINESS_CACHE_MS;
|
|
41
|
+
this.onCheckFailure = options.onCheckFailure;
|
|
42
|
+
}
|
|
6
43
|
|
|
7
44
|
register(name: string, check: ReadinessCheck): () => void {
|
|
8
45
|
if (this.checks.has(name)) {
|
|
9
46
|
throw new Error(`Readiness check "${name}" is already registered`);
|
|
10
47
|
}
|
|
11
48
|
this.checks.set(name, check);
|
|
49
|
+
this.invalidate();
|
|
12
50
|
return () => {
|
|
13
51
|
this.checks.delete(name);
|
|
52
|
+
this.invalidate();
|
|
14
53
|
};
|
|
15
54
|
}
|
|
16
55
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Runs every check in parallel, each bounded by the timeout. Concurrent callers
|
|
58
|
+
* share one evaluation, and a finished one is reused for the cache window.
|
|
59
|
+
*/
|
|
60
|
+
async run(): Promise<ReadinessResults> {
|
|
61
|
+
if (this.cached && Date.now() < this.cached.expiresAt) return { ...this.cached.results };
|
|
62
|
+
this.inFlight ??= this.evaluate(this.generation);
|
|
63
|
+
return { ...(await this.inFlight) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private invalidate(): void {
|
|
67
|
+
this.generation += 1;
|
|
68
|
+
this.inFlight = undefined;
|
|
69
|
+
this.cached = undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private async evaluate(generation: number): Promise<ReadinessResults> {
|
|
73
|
+
try {
|
|
74
|
+
const entries = await Promise.all(
|
|
75
|
+
[...this.checks].map(
|
|
76
|
+
async ([name, check]): Promise<[string, ReadinessStatus]> => [name, await this.probe(name, check)],
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
const results: ReadinessResults = Object.fromEntries(entries);
|
|
80
|
+
if (generation === this.generation && this.cacheMs > 0) {
|
|
81
|
+
this.cached = { results, expiresAt: Date.now() + this.cacheMs };
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
} finally {
|
|
85
|
+
if (generation === this.generation) this.inFlight = undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private async probe(name: string, check: ReadinessCheck): Promise<ReadinessStatus> {
|
|
90
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
91
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
92
|
+
timer = setTimeout(() => reject(new ReadinessTimeoutError(name, this.timeoutMs)), this.timeoutMs);
|
|
93
|
+
});
|
|
94
|
+
try {
|
|
95
|
+
await Promise.race([check(), timeout]);
|
|
96
|
+
return "up";
|
|
97
|
+
} catch (error) {
|
|
98
|
+
this.onCheckFailure?.(name, error instanceof Error ? error.message : String(error));
|
|
99
|
+
return "down";
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
}
|
|
29
103
|
}
|
|
30
104
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import type { AddressInfo } from "node:net";
|
|
3
|
+
import { gzipSync } from "node:zlib";
|
|
1
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { proxyRequest, resolveClientIp } from "./backend-proxy";
|
|
5
|
+
import { proxyRequest, rawSubpath, resolveClientIp } from "./backend-proxy";
|
|
3
6
|
|
|
4
7
|
describe("backend proxy client address handling", () => {
|
|
5
8
|
afterEach(() => {
|
|
@@ -77,6 +80,23 @@ describe("backend proxy client address handling", () => {
|
|
|
77
80
|
expect(upstreamHeaders?.get("user-agent")).toBe(agent);
|
|
78
81
|
});
|
|
79
82
|
|
|
83
|
+
it("aborts the upstream request when the client disconnects", async () => {
|
|
84
|
+
let upstreamSignal: AbortSignal | undefined;
|
|
85
|
+
vi.stubGlobal("fetch", async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
86
|
+
upstreamSignal = init?.signal ?? undefined;
|
|
87
|
+
return new Response(null, { status: 204 });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const client = new AbortController();
|
|
91
|
+
const request = new Request("http://app.localhost/api/events", { signal: client.signal });
|
|
92
|
+
await proxyRequest(request, "http://api:5002/events");
|
|
93
|
+
|
|
94
|
+
expect(upstreamSignal).toBeDefined();
|
|
95
|
+
expect(upstreamSignal?.aborted).toBe(false);
|
|
96
|
+
client.abort();
|
|
97
|
+
expect(upstreamSignal?.aborted).toBe(true);
|
|
98
|
+
});
|
|
99
|
+
|
|
80
100
|
it("relays rate-limit response headers to browser clients", async () => {
|
|
81
101
|
vi.stubGlobal("fetch", async () => new Response(
|
|
82
102
|
JSON.stringify({ success: false }),
|
|
@@ -101,4 +121,171 @@ describe("backend proxy client address handling", () => {
|
|
|
101
121
|
expect(response.headers.get("ratelimit-limit")).toBe("300");
|
|
102
122
|
expect(response.headers.get("ratelimit-remaining")).toBe("0");
|
|
103
123
|
});
|
|
124
|
+
|
|
125
|
+
it("relays the next-page cursor of a cursor-paged list", async () => {
|
|
126
|
+
vi.stubGlobal("fetch", async () => new Response("[]", {
|
|
127
|
+
headers: { "content-type": "application/json", "x-next-cursor": "WyIyMDI2Il0" },
|
|
128
|
+
}));
|
|
129
|
+
|
|
130
|
+
const response = await proxyRequest(
|
|
131
|
+
new Request("http://app.localhost/api/audit-logs?limit=15"),
|
|
132
|
+
"http://api:5002/audit-logs?limit=15",
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(response.headers.get("x-next-cursor")).toBe("WyIyMDI2Il0");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("relays cache validator and download headers", async () => {
|
|
139
|
+
vi.stubGlobal("fetch", async () => new Response("report", {
|
|
140
|
+
headers: {
|
|
141
|
+
"content-type": "text/csv",
|
|
142
|
+
etag: '"report-v3"',
|
|
143
|
+
"last-modified": "Wed, 14 Oct 2026 08:00:00 GMT",
|
|
144
|
+
"content-disposition": 'attachment; filename="report.csv"',
|
|
145
|
+
vary: "accept-encoding, cookie",
|
|
146
|
+
"x-content-type-options": "nosniff",
|
|
147
|
+
},
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
const response = await proxyRequest(
|
|
151
|
+
new Request("http://app.localhost/api/reports/3"),
|
|
152
|
+
"http://api:5002/reports/3",
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(response.headers.get("etag")).toBe('"report-v3"');
|
|
156
|
+
expect(response.headers.get("last-modified")).toBe("Wed, 14 Oct 2026 08:00:00 GMT");
|
|
157
|
+
expect(response.headers.get("content-disposition")).toBe('attachment; filename="report.csv"');
|
|
158
|
+
expect(response.headers.get("vary")).toBe("accept-encoding, cookie");
|
|
159
|
+
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("relays content-length for an uncompressed upstream body", async () => {
|
|
163
|
+
vi.stubGlobal("fetch", async () => new Response("0123456789", {
|
|
164
|
+
headers: { "content-length": "10" },
|
|
165
|
+
}));
|
|
166
|
+
|
|
167
|
+
const response = await proxyRequest(
|
|
168
|
+
new Request("http://app.localhost/api/files/1"),
|
|
169
|
+
"http://api:5002/files/1",
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
expect(response.headers.get("content-length")).toBe("10");
|
|
173
|
+
expect(await response.text()).toBe("0123456789");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("drops content-length when the upstream body was compressed", async () => {
|
|
177
|
+
vi.stubGlobal("fetch", async () => new Response("decoded body", {
|
|
178
|
+
headers: { "content-encoding": "gzip", "content-length": "4" },
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
const response = await proxyRequest(
|
|
182
|
+
new Request("http://app.localhost/api/files/1"),
|
|
183
|
+
"http://api:5002/files/1",
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(response.headers.get("content-length")).toBeNull();
|
|
187
|
+
expect(response.headers.get("content-encoding")).toBeNull();
|
|
188
|
+
expect(await response.text()).toBe("decoded body");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("delivers a gzip-encoded upstream body in full", async () => {
|
|
192
|
+
const payload = `${"podokit ".repeat(4096)}end`;
|
|
193
|
+
const compressed = gzipSync(payload);
|
|
194
|
+
const server = createServer((_request, response) => {
|
|
195
|
+
response.writeHead(200, {
|
|
196
|
+
"content-type": "text/plain",
|
|
197
|
+
"content-encoding": "gzip",
|
|
198
|
+
"content-length": String(compressed.byteLength),
|
|
199
|
+
});
|
|
200
|
+
response.end(compressed);
|
|
201
|
+
});
|
|
202
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
203
|
+
try {
|
|
204
|
+
const { port } = server.address() as AddressInfo;
|
|
205
|
+
const response = await proxyRequest(
|
|
206
|
+
new Request("http://app.localhost/api/export"),
|
|
207
|
+
`http://127.0.0.1:${String(port)}/export`,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
expect(response.headers.get("content-length")).toBeNull();
|
|
211
|
+
expect(await response.text()).toBe(payload);
|
|
212
|
+
} finally {
|
|
213
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("forwards conditional and range request headers", async () => {
|
|
218
|
+
let upstreamHeaders: Headers | undefined;
|
|
219
|
+
vi.stubGlobal("fetch", async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
220
|
+
upstreamHeaders = new Headers(init?.headers);
|
|
221
|
+
return new Response(null, { status: 204 });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const request = new Request("http://app.localhost/api/files/1", {
|
|
225
|
+
headers: {
|
|
226
|
+
"if-none-match": '"file-v1"',
|
|
227
|
+
"if-modified-since": "Wed, 14 Oct 2026 08:00:00 GMT",
|
|
228
|
+
range: "bytes=0-99",
|
|
229
|
+
"if-range": '"file-v1"',
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
await proxyRequest(request, "http://api:5002/files/1");
|
|
233
|
+
|
|
234
|
+
expect(upstreamHeaders?.get("if-none-match")).toBe('"file-v1"');
|
|
235
|
+
expect(upstreamHeaders?.get("if-modified-since")).toBe("Wed, 14 Oct 2026 08:00:00 GMT");
|
|
236
|
+
expect(upstreamHeaders?.get("range")).toBe("bytes=0-99");
|
|
237
|
+
expect(upstreamHeaders?.get("if-range")).toBe('"file-v1"');
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("passes a 304 Not Modified response through without a body", async () => {
|
|
241
|
+
vi.stubGlobal("fetch", async () => new Response(null, {
|
|
242
|
+
status: 304,
|
|
243
|
+
headers: { etag: '"file-v1"', "cache-control": "private, no-cache" },
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
const response = await proxyRequest(
|
|
247
|
+
new Request("http://app.localhost/api/files/1", {
|
|
248
|
+
headers: { "if-none-match": '"file-v1"' },
|
|
249
|
+
}),
|
|
250
|
+
"http://api:5002/files/1",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(response.status).toBe(304);
|
|
254
|
+
expect(response.headers.get("etag")).toBe('"file-v1"');
|
|
255
|
+
expect(await response.text()).toBe("");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("passes a 206 Partial Content response through with its range", async () => {
|
|
259
|
+
vi.stubGlobal("fetch", async () => new Response("0123", {
|
|
260
|
+
status: 206,
|
|
261
|
+
headers: {
|
|
262
|
+
"content-range": "bytes 0-3/10",
|
|
263
|
+
"accept-ranges": "bytes",
|
|
264
|
+
"content-length": "4",
|
|
265
|
+
},
|
|
266
|
+
}));
|
|
267
|
+
|
|
268
|
+
const response = await proxyRequest(
|
|
269
|
+
new Request("http://app.localhost/api/files/1", { headers: { range: "bytes=0-3" } }),
|
|
270
|
+
"http://api:5002/files/1",
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(response.status).toBe(206);
|
|
274
|
+
expect(response.headers.get("content-range")).toBe("bytes 0-3/10");
|
|
275
|
+
expect(response.headers.get("accept-ranges")).toBe("bytes");
|
|
276
|
+
expect(response.headers.get("content-length")).toBe("4");
|
|
277
|
+
expect(await response.text()).toBe("0123");
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("backend proxy raw path", () => {
|
|
282
|
+
it("keeps encoded separators inside a path segment", () => {
|
|
283
|
+
const url = new URL("http://localhost/api/files/uploads%2Fu1%2Fa%3Fb.png/url?x=1");
|
|
284
|
+
expect(rawSubpath(url, "/api/")).toBe("files/uploads%2Fu1%2Fa%3Fb.png/url");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("strips only the given prefix", () => {
|
|
288
|
+
expect(rawSubpath(new URL("http://localhost/api/auth/get-session"), "/api/auth/")).toBe("get-session");
|
|
289
|
+
expect(rawSubpath(new URL("http://localhost/other"), "/api/")).toBe("/other");
|
|
290
|
+
});
|
|
104
291
|
});
|
|
@@ -15,16 +15,36 @@ const FORWARDED_HEADERS = [
|
|
|
15
15
|
"origin",
|
|
16
16
|
"referer",
|
|
17
17
|
"user-agent",
|
|
18
|
+
"if-none-match",
|
|
19
|
+
"if-modified-since",
|
|
20
|
+
"range",
|
|
21
|
+
"if-range",
|
|
18
22
|
];
|
|
19
23
|
const RELAYED_RESPONSE_HEADERS = [
|
|
20
24
|
"content-type",
|
|
25
|
+
"content-disposition",
|
|
26
|
+
"content-range",
|
|
27
|
+
"accept-ranges",
|
|
21
28
|
"location",
|
|
22
29
|
"cache-control",
|
|
30
|
+
"etag",
|
|
31
|
+
"last-modified",
|
|
32
|
+
"vary",
|
|
33
|
+
"x-content-type-options",
|
|
34
|
+
"content-security-policy",
|
|
23
35
|
"retry-after",
|
|
24
36
|
"ratelimit-limit",
|
|
25
37
|
"ratelimit-remaining",
|
|
38
|
+
// Cursor-paged lists (such as the audit log) return the next page's cursor here.
|
|
39
|
+
"x-next-cursor",
|
|
26
40
|
];
|
|
27
41
|
|
|
42
|
+
// SvelteKit decodes rest params, so an encoded "%2F" or "%3F" inside a path
|
|
43
|
+
// segment would reach the API as "/" or "?". Forward the raw pathname instead.
|
|
44
|
+
export function rawSubpath(url: URL, prefix: string): string {
|
|
45
|
+
return url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : url.pathname;
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
export function backendBaseUrl(): string {
|
|
29
49
|
return process.env.BACKEND_INTERNAL_URL ?? "http://localhost:5002";
|
|
30
50
|
}
|
|
@@ -71,6 +91,10 @@ export async function proxyRequest(
|
|
|
71
91
|
// them as text corrupts bytes that are not valid UTF-8 before forwarding.
|
|
72
92
|
body: hasBody ? await request.arrayBuffer() : undefined,
|
|
73
93
|
redirect: "manual",
|
|
94
|
+
// Abort the upstream request when the browser disconnects. Without this, every
|
|
95
|
+
// closed EventSource or aborted request keeps its API socket open until Bun's
|
|
96
|
+
// fetch pool is exhausted and the web process stops serving requests.
|
|
97
|
+
signal: request.signal,
|
|
74
98
|
});
|
|
75
99
|
|
|
76
100
|
const responseHeaders = new Headers();
|
|
@@ -78,6 +102,12 @@ export async function proxyRequest(
|
|
|
78
102
|
const value = upstream.headers.get(name);
|
|
79
103
|
if (value) responseHeaders.set(name, value);
|
|
80
104
|
}
|
|
105
|
+
// Bun's fetch decodes a compressed body but keeps the upstream content-encoding
|
|
106
|
+
// and compressed content-length, which would truncate the decoded body.
|
|
107
|
+
const contentLength = upstream.headers.get("content-length");
|
|
108
|
+
if (contentLength && !upstream.headers.has("content-encoding")) {
|
|
109
|
+
responseHeaders.set("content-length", contentLength);
|
|
110
|
+
}
|
|
81
111
|
// Relay Set-Cookie so auth session cookies reach the browser.
|
|
82
112
|
for (const cookie of upstream.headers.getSetCookie()) {
|
|
83
113
|
responseHeaders.append("set-cookie", cookie);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { RequestHandler } from "@sveltejs/kit";
|
|
2
|
-
import { backendBaseUrl, proxyRequest, resolveClientIp } from "#lib/server/backend-proxy.js";
|
|
2
|
+
import { backendBaseUrl, proxyRequest, rawSubpath, resolveClientIp } from "#lib/server/backend-proxy.js";
|
|
3
3
|
|
|
4
4
|
// Proxy the app's REST API, stripping the /api prefix (backend routes are at root).
|
|
5
|
-
const handler: RequestHandler = ({ request,
|
|
5
|
+
const handler: RequestHandler = ({ request, url, getClientAddress }) =>
|
|
6
6
|
proxyRequest(
|
|
7
7
|
request,
|
|
8
|
-
`${backendBaseUrl()}/${
|
|
8
|
+
`${backendBaseUrl()}/${rawSubpath(url, "/api/")}${url.search}`,
|
|
9
9
|
resolveClientIp(getClientAddress),
|
|
10
10
|
);
|
|
11
11
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { RequestHandler } from "@sveltejs/kit";
|
|
2
|
-
import { backendBaseUrl, proxyRequest, resolveClientIp } from "#lib/server/backend-proxy.js";
|
|
2
|
+
import { backendBaseUrl, proxyRequest, rawSubpath, resolveClientIp } from "#lib/server/backend-proxy.js";
|
|
3
3
|
|
|
4
4
|
// Proxy the better-auth handler, keeping the /api/auth prefix.
|
|
5
|
-
const handler: RequestHandler = ({ request,
|
|
5
|
+
const handler: RequestHandler = ({ request, url, getClientAddress }) =>
|
|
6
6
|
proxyRequest(
|
|
7
7
|
request,
|
|
8
|
-
`${backendBaseUrl()}/api/auth/${
|
|
8
|
+
`${backendBaseUrl()}/api/auth/${rawSubpath(url, "/api/auth/")}${url.search}`,
|
|
9
9
|
resolveClientIp(getClientAddress),
|
|
10
10
|
);
|
|
11
11
|
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# Containerized Bun development environment.
|
|
2
|
+
#
|
|
3
|
+
# Every service has a memory limit equal to its swap limit, so a runaway process
|
|
4
|
+
# is OOM-killed inside its container instead of exhausting the host. Override a
|
|
5
|
+
# limit with the matching *_MEM_LIMIT variable from the shell or the project's
|
|
6
|
+
# .env file (not .env.docker), e.g. API_MEM_LIMIT=3g podo dev watch.
|
|
2
7
|
name: {{projectName}}-dev
|
|
3
8
|
|
|
4
9
|
services:
|
|
5
10
|
traefik:
|
|
11
|
+
mem_limit: ${TRAEFIK_MEM_LIMIT:-256m}
|
|
12
|
+
memswap_limit: ${TRAEFIK_MEM_LIMIT:-256m}
|
|
6
13
|
profiles: [podokit-legacy-proxy]
|
|
7
14
|
image: traefik:v3.5
|
|
8
15
|
command:
|
|
@@ -19,6 +26,8 @@ services:
|
|
|
19
26
|
networks: [podokit]
|
|
20
27
|
|
|
21
28
|
web:
|
|
29
|
+
mem_limit: ${WEB_MEM_LIMIT:-2g}
|
|
30
|
+
memswap_limit: ${WEB_MEM_LIMIT:-2g}
|
|
22
31
|
build:
|
|
23
32
|
context: .
|
|
24
33
|
dockerfile: Dockerfile.dev
|
|
@@ -42,6 +51,8 @@ services:
|
|
|
42
51
|
- { action: rebuild, path: ./apps/web/package.json }
|
|
43
52
|
|
|
44
53
|
api:
|
|
54
|
+
mem_limit: ${API_MEM_LIMIT:-2g}
|
|
55
|
+
memswap_limit: ${API_MEM_LIMIT:-2g}
|
|
45
56
|
build:
|
|
46
57
|
context: .
|
|
47
58
|
dockerfile: Dockerfile.dev
|
|
@@ -61,6 +72,8 @@ services:
|
|
|
61
72
|
- { action: rebuild, path: ./apps/api/package.json }
|
|
62
73
|
|
|
63
74
|
worker:
|
|
75
|
+
mem_limit: ${WORKER_MEM_LIMIT:-1g}
|
|
76
|
+
memswap_limit: ${WORKER_MEM_LIMIT:-1g}
|
|
64
77
|
profiles: [queue]
|
|
65
78
|
build:
|
|
66
79
|
context: .
|
|
@@ -81,6 +94,8 @@ services:
|
|
|
81
94
|
- { action: rebuild, path: ./apps/api/package.json }
|
|
82
95
|
|
|
83
96
|
postgres:
|
|
97
|
+
mem_limit: ${POSTGRES_MEM_LIMIT:-1g}
|
|
98
|
+
memswap_limit: ${POSTGRES_MEM_LIMIT:-1g}
|
|
84
99
|
image: postgres:16-alpine
|
|
85
100
|
environment:
|
|
86
101
|
POSTGRES_USER: podokit
|
|
@@ -96,11 +111,15 @@ services:
|
|
|
96
111
|
networks: [podokit]
|
|
97
112
|
|
|
98
113
|
redis:
|
|
114
|
+
mem_limit: ${REDIS_MEM_LIMIT:-256m}
|
|
115
|
+
memswap_limit: ${REDIS_MEM_LIMIT:-256m}
|
|
99
116
|
profiles: [cache]
|
|
100
117
|
image: redis:7-alpine
|
|
101
118
|
networks: [podokit]
|
|
102
119
|
|
|
103
120
|
minio:
|
|
121
|
+
mem_limit: ${MINIO_MEM_LIMIT:-512m}
|
|
122
|
+
memswap_limit: ${MINIO_MEM_LIMIT:-512m}
|
|
104
123
|
profiles: [storage]
|
|
105
124
|
image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z
|
|
106
125
|
command: server /data --console-address ":9001"
|
|
@@ -112,6 +131,8 @@ services:
|
|
|
112
131
|
networks: [podokit]
|
|
113
132
|
|
|
114
133
|
minio-init:
|
|
134
|
+
mem_limit: ${MINIO_INIT_MEM_LIMIT:-128m}
|
|
135
|
+
memswap_limit: ${MINIO_INIT_MEM_LIMIT:-128m}
|
|
115
136
|
profiles: [storage]
|
|
116
137
|
image: pgsty/mc:RELEASE.2026-09-16T00-00-00Z
|
|
117
138
|
depends_on:
|
|
@@ -19,5 +19,12 @@ description: Use when adding or changing a SvelteKit page/route in apps/web —
|
|
|
19
19
|
skill, added with the admin-dashboard module).
|
|
20
20
|
5. **i18n**: no hardcoded user-facing strings — add keys to
|
|
21
21
|
`apps/web/src/lib/i18n/messages.ts` for **every** locale.
|
|
22
|
-
6. **
|
|
22
|
+
6. **Public pages** (with the admin-dashboard module): every page outside the
|
|
23
|
+
sign-in flow, `/`, and module pages requires a session and answers 503 while
|
|
24
|
+
the API is unavailable. Add the prefix of a page anonymous visitors should see
|
|
25
|
+
(marketing, pricing, legal, `robots.txt`) to `APP_PUBLIC_PATHS` in the
|
|
26
|
+
app-owned `apps/web/src/lib/server/public-paths.ts`. A prefix covers its
|
|
27
|
+
subpaths. Give such a page's loader a fallback for when the API is down
|
|
28
|
+
instead of throwing. Never edit `PUBLIC_PATHS` in the managed `guards.ts`.
|
|
29
|
+
7. **Verify**: `{{webRun}} build` (includes `svelte-check`)
|
|
23
30
|
and add/adjust a Playwright test in `tests/`.
|
|
@@ -12,6 +12,20 @@ POSTGRES_USER=podokit
|
|
|
12
12
|
POSTGRES_PASSWORD=podokit
|
|
13
13
|
POSTGRES_DB=podokit
|
|
14
14
|
|
|
15
|
+
# PostgreSQL pools. Each API replica opens up to DB_POOL_MAX connections for
|
|
16
|
+
# queries, plus DB_AUTH_POOL_MAX for the auth module when it is installed. Keep
|
|
17
|
+
# replicas x (DB_POOL_MAX + DB_AUTH_POOL_MAX), plus a few for migrations, below
|
|
18
|
+
# the server's max_connections (100 by default). The timeouts are milliseconds.
|
|
19
|
+
# DB_POOL_MAX=20
|
|
20
|
+
# DB_CONNECT_TIMEOUT_MS=10000
|
|
21
|
+
# DB_IDLE_TIMEOUT_MS=30000
|
|
22
|
+
# DB_AUTH_POOL_MAX=10
|
|
23
|
+
|
|
24
|
+
# Readiness (GET /health/ready): the longest one dependency check may run, and how
|
|
25
|
+
# long a result is reused across probes (0 checks on every request).
|
|
26
|
+
# READINESS_CHECK_TIMEOUT_MS=800
|
|
27
|
+
# READINESS_CACHE_MS=2000
|
|
28
|
+
|
|
15
29
|
# Web -> API boundary (server-side only; not exposed to the browser)
|
|
16
30
|
BACKEND_INTERNAL_URL=http://localhost:5002
|
|
17
31
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import type {
|
|
4
|
+
import type { PlaywrightTestConfig } from "@playwright/test";
|
|
5
5
|
|
|
6
|
-
type Projects = NonNullable<
|
|
6
|
+
type Projects = NonNullable<PlaywrightTestConfig["projects"]>;
|
|
7
7
|
|
|
8
8
|
const requireFromTests = createRequire(__filename);
|
|
9
9
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
4
5
|
|
|
6
|
+
const compiledRuntime = new URL("../dist/bootstrap-admin-runtime.js", import.meta.url);
|
|
7
|
+
const sourceRuntime = new URL("../src/bootstrap-admin-runtime.ts", import.meta.url);
|
|
8
|
+
|
|
5
9
|
const args = process.argv.slice(2);
|
|
6
10
|
|
|
7
11
|
function has(flag) {
|
|
@@ -130,34 +134,15 @@ async function main() {
|
|
|
130
134
|
}
|
|
131
135
|
|
|
132
136
|
const input = resolveBootstrapInput();
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const { auth } =
|
|
137
|
-
const { pool } = await import("../dist/auth/db.js");
|
|
138
|
-
const { verifyPassword } = await import("better-auth/crypto");
|
|
137
|
+
// A production image runs the bundled entry; development runs the source.
|
|
138
|
+
const useCompiled = process.env.NODE_ENV === "production" && existsSync(compiledRuntime);
|
|
139
|
+
const runtime = await import((useCompiled ? compiledRuntime : sourceRuntime).href);
|
|
140
|
+
const { auth, findBootstrapUser, verifyPassword, closeAuthDatabase } = runtime;
|
|
139
141
|
|
|
140
142
|
try {
|
|
141
143
|
const result = await bootstrapAdmin(
|
|
142
144
|
{
|
|
143
|
-
findUser:
|
|
144
|
-
const query = await pool.query(
|
|
145
|
-
`SELECT u.id,
|
|
146
|
-
u.email,
|
|
147
|
-
u.role,
|
|
148
|
-
u."emailVerified" AS "emailVerified",
|
|
149
|
-
u."signupApproved" AS "signupApproved",
|
|
150
|
-
a.password AS "passwordHash"
|
|
151
|
-
FROM "user" AS u
|
|
152
|
-
LEFT JOIN "account" AS a
|
|
153
|
-
ON a."userId" = u.id
|
|
154
|
-
AND a."providerId" = 'credential'
|
|
155
|
-
WHERE lower(u.email) = lower($1)
|
|
156
|
-
LIMIT 1`,
|
|
157
|
-
[email],
|
|
158
|
-
);
|
|
159
|
-
return query.rows[0] ?? null;
|
|
160
|
-
},
|
|
145
|
+
findUser: findBootstrapUser,
|
|
161
146
|
createUser: async (user) => auth.api.createUser({ body: user }),
|
|
162
147
|
verifyPassword: async (hash, password) => verifyPassword({ hash, password }),
|
|
163
148
|
},
|
|
@@ -174,14 +159,24 @@ async function main() {
|
|
|
174
159
|
console.log(`Verified existing administrator account ${result.email}.`);
|
|
175
160
|
}
|
|
176
161
|
} finally {
|
|
177
|
-
await
|
|
162
|
+
await closeAuthDatabase();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A refused PostgreSQL connection is an AggregateError with an empty message.
|
|
167
|
+
export function errorMessage(error) {
|
|
168
|
+
if (!(error instanceof Error)) return String(error);
|
|
169
|
+
if (error.message) return error.message;
|
|
170
|
+
if (error instanceof AggregateError && error.errors.length > 0) {
|
|
171
|
+
return error.errors.map(errorMessage).join("; ");
|
|
178
172
|
}
|
|
173
|
+
return error.name;
|
|
179
174
|
}
|
|
180
175
|
|
|
181
176
|
const invokedPath = process.argv[1] ? pathToFileURL(process.argv[1]).href : "";
|
|
182
177
|
if (invokedPath === import.meta.url) {
|
|
183
178
|
main().catch((error) => {
|
|
184
|
-
console.error(
|
|
179
|
+
console.error(errorMessage(error));
|
|
185
180
|
process.exitCode = 1;
|
|
186
181
|
});
|
|
187
182
|
}
|