@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.
Files changed (203) hide show
  1. package/dist/add.d.ts +3 -1
  2. package/dist/add.js +116 -14
  3. package/dist/deploy-render.js +5 -0
  4. package/dist/index.js +8 -0
  5. package/dist/lib.d.ts +2 -2
  6. package/dist/lib.js +3 -2
  7. package/dist/lockfile.d.ts +3 -1
  8. package/dist/lockfile.js +17 -6
  9. package/dist/templates/fullstack/apps/api/package.json +2 -2
  10. package/dist/templates/fullstack/apps/api/scripts/build.mjs +1 -0
  11. package/dist/templates/fullstack/apps/api/src/app.spec.ts +81 -0
  12. package/dist/templates/fullstack/apps/api/src/app.ts +53 -5
  13. package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +6 -0
  14. package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +10 -1
  15. package/dist/templates/fullstack/apps/api/src/core/services.spec.ts +30 -0
  16. package/dist/templates/fullstack/apps/api/src/core/services.ts +19 -3
  17. package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +15 -0
  18. package/dist/templates/fullstack/apps/api/src/database/data-source.ts +3 -0
  19. package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +149 -4
  20. package/dist/templates/fullstack/apps/api/src/database/database.ts +66 -13
  21. package/dist/templates/fullstack/apps/api/src/database/migrate.ts +1 -1
  22. package/dist/templates/fullstack/apps/api/src/health/health.plugin.spec.ts +86 -0
  23. package/dist/templates/fullstack/apps/api/src/health/health.plugin.ts +4 -9
  24. package/dist/templates/fullstack/apps/api/src/health/readiness.service.spec.ts +141 -1
  25. package/dist/templates/fullstack/apps/api/src/health/readiness.service.ts +86 -12
  26. package/dist/templates/fullstack/apps/web/package.json +1 -1
  27. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +188 -1
  28. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +30 -0
  29. package/dist/templates/fullstack/apps/web/src/routes/api/[...path]/+server.ts +3 -3
  30. package/dist/templates/fullstack/apps/web/src/routes/api/auth/[...all]/+server.ts +3 -3
  31. package/dist/templates/fullstack/compose.dev.yaml +21 -0
  32. package/dist/templates/fullstack/dot-claude/skills/podokit-sveltekit-route/SKILL.md +8 -1
  33. package/dist/templates/fullstack/dot-env.docker +3 -0
  34. package/dist/templates/fullstack/dot-env.example +14 -0
  35. package/dist/templates/fullstack/infra/k3s/api-deployment.yaml +2 -0
  36. package/dist/templates/fullstack/tests/playwright.extensions.ts +2 -2
  37. package/dist/templates/modules/admin-dashboard/files/apps/api/scripts/bootstrap-admin.mjs +21 -26
  38. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.migration.spec.ts +74 -0
  39. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.spec.ts +25 -0
  40. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +26 -0
  41. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.spec.ts +122 -0
  42. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.routes.ts +46 -0
  43. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +187 -0
  44. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +160 -0
  45. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.spec.ts +76 -0
  46. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.routes.ts +41 -0
  47. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.spec.ts +124 -0
  48. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-users/admin-users.service.ts +153 -0
  49. package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +22 -28
  50. package/dist/templates/modules/admin-dashboard/files/apps/api/src/bootstrap-admin-runtime.ts +60 -0
  51. package/dist/templates/modules/admin-dashboard/files/apps/api/src/migrations/1792060000000-IndexSessionCreatedAt.ts +35 -0
  52. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.spec.ts +3 -5
  53. package/dist/templates/modules/admin-dashboard/files/apps/api/src/profile-image/profile-image.service.ts +6 -17
  54. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.spec.ts +167 -0
  55. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-assets/site-assets.routes.ts +50 -0
  56. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.spec.ts +158 -0
  57. package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +78 -28
  58. package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +3 -0
  59. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +124 -0
  60. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +19 -11
  61. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-data.server.ts +6 -32
  62. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.test.ts +74 -0
  63. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/sessions.ts +73 -0
  64. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.test.ts +102 -0
  65. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/users.ts +141 -0
  66. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.test.ts +68 -0
  67. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/audit-log.ts +108 -0
  68. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +14 -8
  69. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/session-idle-timeout.svelte +74 -49
  70. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -1
  71. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -1
  72. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.test.ts +29 -0
  73. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +23 -6
  74. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.test.ts +21 -0
  75. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/oauth-query.ts +15 -0
  76. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.test.ts +98 -0
  77. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/capabilities.ts +54 -0
  78. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.test.ts +23 -0
  79. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +2 -0
  80. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/protected-layout.ts +7 -50
  81. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/public-paths.ts +9 -0
  82. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/audit/+page.svelte +88 -45
  83. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/sessions/+page.svelte +76 -75
  84. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.server.ts +15 -14
  85. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +2 -2
  86. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/users/+page.svelte +79 -37
  87. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +10 -17
  88. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.ts +10 -0
  89. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +4 -3
  90. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.svelte +14 -8
  91. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -1
  92. package/dist/templates/modules/admin-dashboard/files/tests/api/admin-sessions.api.spec.ts +124 -0
  93. package/dist/templates/modules/admin-dashboard/files/tests/api/admin-users.api.spec.ts +86 -0
  94. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +75 -12
  95. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +1 -0
  96. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +35 -1
  97. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +7 -0
  98. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +72 -0
  99. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +36 -2
  100. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -2
  101. package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +41 -0
  102. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -13
  103. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +0 -1
  104. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -3
  105. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +16 -5
  106. package/dist/templates/modules/admin-dashboard/files/tests/ui/request-budget.ui.spec.ts +45 -0
  107. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +64 -1
  108. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +29 -0
  109. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +8 -1
  110. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +32 -1
  111. package/dist/templates/modules/admin-dashboard/module.manifest.json +3 -2
  112. package/dist/templates/modules/audit-log/files/apps/api/src/audit/README.md +46 -5
  113. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-events.ts +19 -7
  114. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -0
  115. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.migration.spec.ts +126 -3
  116. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +13 -11
  117. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.spec.ts +115 -0
  118. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.routes.ts +44 -0
  119. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +421 -43
  120. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +382 -35
  121. package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1792000000000-IndexAuditLogFilters.ts +57 -0
  122. package/dist/templates/modules/audit-log/module.manifest.json +7 -4
  123. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +20 -5
  124. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +24 -29
  125. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +28 -13
  126. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.spec.ts +56 -2
  127. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.openapi.ts +22 -0
  128. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +81 -5
  129. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -2
  130. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -9
  131. package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.spec.ts +38 -0
  132. package/dist/templates/modules/auth/files/apps/api/src/auth/config-events.ts +43 -0
  133. package/dist/templates/modules/auth/files/apps/api/src/auth/db.spec.ts +24 -0
  134. package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +31 -2
  135. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.spec.ts +58 -0
  136. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +46 -57
  137. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.spec.ts +53 -0
  138. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.rows.ts +65 -0
  139. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +21 -37
  140. package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.spec.ts +23 -0
  141. package/dist/templates/modules/auth/files/apps/api/src/auth-config/config-param.ts +10 -0
  142. package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.spec.ts +33 -0
  143. package/dist/templates/modules/auth/files/apps/api/src/auth-config/session-expiry.ts +29 -0
  144. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +10 -0
  145. package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.spec.ts +81 -0
  146. package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +37 -34
  147. package/dist/templates/modules/auth/files/dot-claude/skills/podokit-configure-auth/references/bootstrap-admin.md +2 -0
  148. package/dist/templates/modules/auth/module.manifest.json +2 -2
  149. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.spec.ts +136 -0
  150. package/dist/templates/modules/file-upload/files/apps/api/src/files/files.module.ts +34 -13
  151. package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.spec.ts +60 -0
  152. package/dist/templates/modules/file-upload/files/apps/api/src/files/upload-keys.ts +51 -0
  153. package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +70 -9
  154. package/dist/templates/modules/file-upload/module.manifest.json +9 -3
  155. package/dist/templates/modules/job-progress/files/apps/api/src/progress/job-progress.module.ts +26 -32
  156. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.bridge.spec.ts +16 -1
  157. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.processor.ts +31 -4
  158. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +145 -0
  159. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.ts +37 -0
  160. package/dist/templates/modules/job-progress/files/tests/api/progress.api.spec.ts +94 -4
  161. package/dist/templates/modules/job-progress/module.manifest.json +3 -3
  162. package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.spec.ts +54 -0
  163. package/dist/templates/modules/mailer/files/apps/api/src/mail/mailer.ts +47 -25
  164. package/dist/templates/modules/mailer/module.manifest.json +1 -1
  165. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.spec.ts +86 -0
  166. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/download-url.ts +159 -0
  167. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-demo.ts +38 -0
  168. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.spec.ts +172 -4
  169. package/dist/templates/modules/object-storage-local/files/apps/api/src/storage-local/storage-local.module.ts +110 -33
  170. package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +88 -0
  171. package/dist/templates/modules/object-storage-local/module.manifest.json +6 -1
  172. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.spec.ts +80 -0
  173. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/s3-object.store.ts +13 -7
  174. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.spec.ts +156 -0
  175. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage-demo.ts +38 -0
  176. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.module.ts +39 -21
  177. package/dist/templates/modules/object-storage-s3/files/apps/api/src/storage/storage.service.ts +101 -2
  178. package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +84 -9
  179. package/dist/templates/modules/object-storage-s3/module.manifest.json +8 -3
  180. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +9 -0
  181. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +7 -0
  182. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +139 -21
  183. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +80 -11
  184. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +12 -6
  185. package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +15 -2
  186. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +46 -0
  187. package/dist/templates/modules/rate-limit/module.manifest.json +4 -3
  188. package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.spec.ts +125 -0
  189. package/dist/templates/modules/redis/files/apps/api/src/redis/redis-cache.store.ts +27 -13
  190. package/dist/templates/modules/sse/files/apps/api/src/events/event-envelope.ts +123 -0
  191. package/dist/templates/modules/sse/files/apps/api/src/events/event-stream.ts +191 -0
  192. package/dist/templates/modules/sse/files/apps/api/src/events/event-topics.ts +64 -0
  193. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.spec.ts +250 -0
  194. package/dist/templates/modules/sse/files/apps/api/src/events/events.module.ts +121 -15
  195. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.spec.ts +407 -18
  196. package/dist/templates/modules/sse/files/apps/api/src/events/events.service.ts +153 -40
  197. package/dist/templates/modules/sse/files/apps/web/src/lib/events.test.ts +216 -0
  198. package/dist/templates/modules/sse/files/apps/web/src/lib/events.ts +207 -0
  199. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +227 -70
  200. package/dist/templates/modules/sse/module.manifest.json +20 -5
  201. package/dist/update.d.ts +17 -0
  202. package/dist/update.js +33 -8
  203. 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
- async run(): Promise<Record<string, ReadinessStatus>> {
18
- const results = await Promise.all(
19
- [...this.checks].map(async ([name, check]): Promise<[string, ReadinessStatus]> => {
20
- try {
21
- await check();
22
- return [name, "up"];
23
- } catch {
24
- return [name, "down"];
25
- }
26
- }),
27
- );
28
- return Object.fromEntries(results);
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
  }
@@ -16,7 +16,7 @@
16
16
  "dependencies": {
17
17
  "@internationalized/date": "^3.12.2",
18
18
  "@lucide/svelte": "^1.23.0",
19
- "@podosoft/podokit-api-client": "^0.8.0",
19
+ "@podosoft/podokit-api-client": "^0.8.5",
20
20
  "bits-ui": "^2.18.1",
21
21
  "clsx": "^2.1.1",
22
22
  "mode-watcher": "^1.1.0",
@@ -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, params, url, getClientAddress }) =>
5
+ const handler: RequestHandler = ({ request, url, getClientAddress }) =>
6
6
  proxyRequest(
7
7
  request,
8
- `${backendBaseUrl()}/${params.path}${url.search}`,
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, params, url, getClientAddress }) =>
5
+ const handler: RequestHandler = ({ request, url, getClientAddress }) =>
6
6
  proxyRequest(
7
7
  request,
8
- `${backendBaseUrl()}/api/auth/${params.all}${url.search}`,
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. **Verify**: `{{webRun}} build` (includes `svelte-check`)
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/`.
@@ -1,3 +1,6 @@
1
+ # Container environment for compose.dev.yaml. Compose does not interpolate
2
+ # from this file: set container memory limits such as API_MEM_LIMIT=3g in the
3
+ # shell or the project's .env file instead.
1
4
  NODE_ENV=development
2
5
  PORT=5002
3
6
  POSTGRES_HOST=postgres
@@ -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
 
@@ -24,6 +24,8 @@ spec:
24
24
  readinessProbe:
25
25
  httpGet: { path: /health/ready, port: 3000 }
26
26
  initialDelaySeconds: 5
27
+ timeoutSeconds: 2
27
28
  livenessProbe:
28
29
  httpGet: { path: /health, port: 3000 }
29
30
  initialDelaySeconds: 15
31
+ timeoutSeconds: 2
@@ -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 { TestConfig } from "@playwright/test";
4
+ import type { PlaywrightTestConfig } from "@playwright/test";
5
5
 
6
- type Projects = NonNullable<TestConfig["projects"]>;
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
- // Load the auth graph before importing one of its dependencies directly.
134
- // Concurrent ESM imports can ask Node to link the same Better Auth submodule
135
- // through two graph roots and fail before the command reaches the database.
136
- const { auth } = await import("../dist/auth/auth.js");
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: async (email) => {
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 pool.end();
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(error instanceof Error ? error.message : String(error));
179
+ console.error(errorMessage(error));
185
180
  process.exitCode = 1;
186
181
  });
187
182
  }