@podosoft/podokit 1.5.0 → 1.6.1

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 (215) hide show
  1. package/README.md +2 -1
  2. package/dist/app-proxy-timeouts.d.ts +27 -0
  3. package/dist/app-proxy-timeouts.js +180 -0
  4. package/dist/assemble.d.ts +5 -2
  5. package/dist/assemble.js +14 -4
  6. package/dist/deploy-checks.d.ts +44 -0
  7. package/dist/deploy-checks.js +267 -0
  8. package/dist/deploy-compose-profile.d.ts +6 -2
  9. package/dist/deploy-compose-profile.js +15 -6
  10. package/dist/deploy-compose-render.d.ts +12 -4
  11. package/dist/deploy-compose-render.js +47 -18
  12. package/dist/deploy-compose.d.ts +2 -5
  13. package/dist/deploy-compose.js +7 -2
  14. package/dist/deploy-profile.d.ts +6 -2
  15. package/dist/deploy-profile.js +11 -7
  16. package/dist/deploy-render.d.ts +4 -1
  17. package/dist/deploy-render.js +27 -21
  18. package/dist/deploy-schema.d.ts +60 -2
  19. package/dist/deploy-schema.js +141 -14
  20. package/dist/deploy.d.ts +2 -5
  21. package/dist/deploy.js +5 -2
  22. package/dist/dev.d.ts +21 -0
  23. package/dist/dev.js +78 -3
  24. package/dist/eject.js +2 -0
  25. package/dist/index.js +85 -5
  26. package/dist/lib.d.ts +2 -0
  27. package/dist/lib.js +5 -2
  28. package/dist/lockfile.d.ts +24 -0
  29. package/dist/lockfile.js +74 -4
  30. package/dist/provider.js +10 -5
  31. package/dist/remove.js +1 -1
  32. package/dist/template-files.d.ts +42 -0
  33. package/dist/template-files.js +102 -0
  34. package/dist/templates/fullstack/AGENTS.md +30 -4
  35. package/dist/templates/fullstack/README.md +8 -7
  36. package/dist/templates/fullstack/apps/api/package.json +4 -4
  37. package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
  38. package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
  39. package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
  40. package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +7 -2
  41. package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
  42. package/dist/templates/fullstack/apps/api/src/app.ts +41 -25
  43. package/dist/templates/fullstack/apps/api/src/build.spec.ts +121 -0
  44. package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
  45. package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
  46. package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +18 -3
  47. package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
  48. package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
  49. package/dist/templates/fullstack/apps/api/src/config/providers.ts +7 -3
  50. package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
  51. package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +72 -4
  52. package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +78 -6
  53. package/dist/templates/fullstack/apps/api/src/core/services.ts +13 -0
  54. package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
  55. package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
  56. package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
  57. package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
  58. package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
  59. package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
  60. package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
  61. package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
  62. package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
  63. package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
  64. package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
  65. package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
  66. package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
  67. package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
  68. package/dist/templates/fullstack/apps/web/package.json +1 -1
  69. package/dist/templates/fullstack/apps/web/src/app.css +3 -0
  70. package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
  71. package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
  72. package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
  73. package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
  74. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +79 -0
  75. package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +55 -1
  76. package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +2 -1
  77. package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
  78. package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
  79. package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
  80. package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
  81. package/dist/templates/fullstack/compose.dev.yaml +7 -4
  82. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
  83. package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +13 -3
  84. package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
  85. package/dist/templates/fullstack/dot-env.docker.example +5 -2
  86. package/dist/templates/fullstack/dot-env.example +4 -0
  87. package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
  88. package/dist/templates/fullstack/infra/k3s/configmap.yaml +1 -0
  89. package/dist/templates/fullstack/tests/README.md +50 -1
  90. package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
  91. package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
  92. package/dist/templates/fullstack/tests/helpers/seeded-admin.ts +15 -0
  93. package/dist/templates/fullstack/tests/package.json +3 -3
  94. package/dist/templates/fullstack/tests/playwright.config.ts +10 -4
  95. package/dist/templates/fullstack/tests/playwright.extensions.ts +149 -1
  96. package/dist/templates/fullstack/tests/run-e2e.ts +28 -0
  97. package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
  98. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
  99. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
  100. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
  101. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
  102. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
  103. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
  104. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +53 -0
  105. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
  106. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
  107. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +11 -1
  108. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
  109. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
  110. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
  111. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
  112. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
  113. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
  114. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
  115. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
  116. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
  117. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/theme-toggle.svelte +1 -1
  118. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
  119. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
  120. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
  121. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
  122. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
  123. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
  124. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
  125. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
  126. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
  127. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
  128. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
  129. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
  130. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
  131. package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
  132. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
  133. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
  134. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +3 -1
  135. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
  136. package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
  137. package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +41 -11
  138. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
  139. package/dist/templates/modules/admin-dashboard/files/tests/helpers/hydration.ts +13 -4
  140. package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
  141. package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
  142. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +14 -6
  143. package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
  144. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +55 -55
  145. package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
  146. package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
  147. package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
  148. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
  149. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
  150. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
  151. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
  152. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
  153. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
  154. package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
  155. package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
  156. package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
  157. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
  158. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
  159. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
  160. package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
  161. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
  162. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
  163. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
  164. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +8 -7
  165. package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +35 -0
  166. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
  167. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
  168. package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
  169. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
  170. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +17 -0
  171. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +18 -0
  172. package/dist/templates/modules/audit-log/module.manifest.json +15 -3
  173. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
  174. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
  175. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
  176. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
  177. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +2 -0
  178. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
  179. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +7 -5
  180. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
  181. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
  182. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
  183. package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
  184. package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
  185. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
  186. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
  187. package/dist/templates/modules/auth/module.manifest.json +8 -2
  188. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +107 -1
  189. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +113 -9
  190. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
  191. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
  192. package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
  193. package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
  194. package/dist/templates/modules/bullmq/module.manifest.json +5 -1
  195. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
  196. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +174 -1
  197. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +150 -3
  198. package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
  199. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
  200. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
  201. package/dist/templates/modules/mailer/module.manifest.json +1 -1
  202. package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +3 -6
  203. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
  204. package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +3 -6
  205. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +45 -20
  206. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +3 -6
  207. package/dist/templates/todo/AGENTS.md +5 -1
  208. package/dist/templates/todo/README.md +6 -4
  209. package/dist/text-diff.d.ts +12 -0
  210. package/dist/text-diff.js +111 -0
  211. package/dist/update.d.ts +61 -3
  212. package/dist/update.js +152 -27
  213. package/package.json +2 -2
  214. package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
  215. package/dist/templates/modules/admin-dashboard/files/tests/helpers/exclusive.ts +0 -80
@@ -358,6 +358,23 @@ export function proxyTimeoutFor(
358
358
  return entry ? entry.timeoutMs : backendProxyTimeoutMs();
359
359
  }
360
360
 
361
+ /**
362
+ * Bun's fetch fails a request whose connection sends nothing for its idle
363
+ * timeout, 300 seconds unless BUN_CONFIG_HTTP_IDLE_TIMEOUT sets another, even
364
+ * while the proxy still waits for response headers.
365
+ */
366
+ const FETCH_IDLE_TIMEOUT_MS = 300_000;
367
+
368
+ /**
369
+ * Bun's fetch options for a request that waits up to timeoutMs for response
370
+ * headers. A wait without a deadline, or one Bun's idle timeout would cut
371
+ * first, turns that timeout off, so only the deadline, the health checks, and
372
+ * the browser end it. Shorter waits keep Bun's default.
373
+ */
374
+ export function upstreamFetchOptions(timeoutMs: number | null): { timeout?: false } {
375
+ return timeoutMs === null || timeoutMs >= FETCH_IDLE_TIMEOUT_MS ? { timeout: false } : {};
376
+ }
377
+
361
378
  /** Bun closes silent connections on a 4-second tick, up to 4 seconds before their idle timeout. */
362
379
  const IDLE_TICK_SECONDS = 4;
363
380
  /** The longest idle timeout Bun accepts; 0 turns it off. */
@@ -509,6 +526,40 @@ export function normalizeClientIp(address: string | undefined): string | undefin
509
526
  return address;
510
527
  }
511
528
 
529
+ let publicOriginsSource: string | undefined;
530
+ let publicOrigins: ReadonlySet<string> = new Set();
531
+
532
+ /**
533
+ * The public web origins listed in CORS_ORIGIN. Wildcard patterns name no single
534
+ * origin and are skipped, as are entries that are not absolute URLs.
535
+ */
536
+ export function configuredPublicOrigins(value = process.env.CORS_ORIGIN): ReadonlySet<string> {
537
+ if (value !== publicOriginsSource) {
538
+ publicOriginsSource = value;
539
+ publicOrigins = new Set(
540
+ (value ?? "")
541
+ .split(",")
542
+ .map((entry) => entry.trim())
543
+ .filter((entry) => !entry.includes("*") && URL.canParse(entry))
544
+ .map((entry) => new URL(entry).origin),
545
+ );
546
+ }
547
+ return publicOrigins;
548
+ }
549
+
550
+ /**
551
+ * Tell the API which public origin a request came in on. SvelteKit resolves the
552
+ * request URL from the Host header, or from the gateway's forwarded headers that
553
+ * HOST_HEADER and PROTOCOL_HEADER name, so the value is sent only when that origin
554
+ * is listed in CORS_ORIGIN. The API can then build links for the origin a browser
555
+ * used without trusting a header a caller wrote.
556
+ */
557
+ export function setForwardedOrigin(headers: Headers, url: URL): void {
558
+ if (!configuredPublicOrigins().has(url.origin)) return;
559
+ headers.set("x-forwarded-host", url.host);
560
+ headers.set("x-forwarded-proto", url.protocol.slice(0, -1));
561
+ }
562
+
512
563
  export function resolveClientIp(getClientAddress: () => string): string | undefined {
513
564
  try {
514
565
  return normalizeClientIp(getClientAddress());
@@ -531,8 +582,10 @@ export async function proxyRequest(
531
582
  // Forward the resolved client IP so the API (better-auth) can record it.
532
583
  const forwardedIp = normalizeClientIp(clientAddress);
533
584
  if (forwardedIp) headers.set("x-forwarded-for", forwardedIp);
585
+ const requestUrl = new URL(request.url);
586
+ setForwardedOrigin(headers, requestUrl);
534
587
 
535
- const { pathname } = new URL(request.url);
588
+ const { pathname } = requestUrl;
536
589
  if (!backendReachable()) {
537
590
  request.body?.cancel().catch(() => undefined);
538
591
  return backendFailure(pathname, 503);
@@ -578,6 +631,7 @@ export async function proxyRequest(
578
631
  // closed EventSource or aborted request keeps its API socket open until Bun's
579
632
  // fetch pool is exhausted and the web process stops serving requests.
580
633
  signal: AbortSignal.any([request.signal, waiting.signal]),
634
+ ...upstreamFetchOptions(timeoutMs),
581
635
  });
582
636
  } catch (cause: unknown) {
583
637
  // Nobody waits for an answer to a request the browser abandoned.
@@ -10,7 +10,8 @@
10
10
  * headers before it answers 504 BACKEND_TIMEOUT; null waits as long as the API
11
11
  * takes. Either way the proxy answers 503 BACKEND_UNAVAILABLE once the API's
12
12
  * GET /health stops answering, and the deadline ends when the response headers
13
- * arrive.
13
+ * arrive. For null or 300 seconds and more, the proxy also turns off Bun's fetch
14
+ * idle timeout, which would otherwise end a silent wait after 300 seconds.
14
15
  *
15
16
  * Bun closes a silent connection up to 4 seconds before its idle timeout, so a
16
17
  * longer wait also needs the API's API_IDLE_TIMEOUT and the web server's
@@ -6,5 +6,5 @@
6
6
  "esModuleInterop": true,
7
7
  "skipLibCheck": true
8
8
  },
9
- "include": ["src", "vite.config.ts"]
9
+ "include": ["src", "vite.config.ts", "vite.extensions.ts"]
10
10
  }
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { webSocketProxy } from "./vite.config.ts";
3
+
4
+ describe("dev-server WebSocket proxy", () => {
5
+ it("sends each exact path in .podokit/dev.json to the API", () => {
6
+ const proxy = webSocketProxy(
7
+ JSON.stringify({ hostname: "app.localhost", webSocketPaths: ["/events/ws", "/agent.v1/ws"] }),
8
+ "http://api:5002",
9
+ );
10
+
11
+ expect(proxy).toEqual({
12
+ "^/events/ws(?:\\?.*)?$": { target: "http://api:5002", ws: true, xfwd: true },
13
+ "^/agent\\.v1/ws(?:\\?.*)?$": { target: "http://api:5002", ws: true, xfwd: true },
14
+ });
15
+ const [events] = Object.keys(proxy).map((key) => new RegExp(key));
16
+ expect(events?.test("/events/ws")).toBe(true);
17
+ expect(events?.test("/events/ws?token=1")).toBe(true);
18
+ expect(events?.test("/events/ws/other")).toBe(false);
19
+ expect(events?.test("/events/wsx")).toBe(false);
20
+ });
21
+
22
+ it("proxies nothing without the file or its paths", () => {
23
+ expect(webSocketProxy(null)).toEqual({});
24
+ expect(webSocketProxy(JSON.stringify({ hostname: "app.localhost" }))).toEqual({});
25
+ });
26
+
27
+ it("rejects a path the gateway would reject", () => {
28
+ expect(() => webSocketProxy(JSON.stringify({ webSocketPaths: ["events/ws"] }))).toThrow(
29
+ 'invalid path: "events/ws"',
30
+ );
31
+ expect(() => webSocketProxy(JSON.stringify({ webSocketPaths: "/events/ws" }))).toThrow("must be an array");
32
+ });
33
+ });
@@ -1,8 +1,10 @@
1
+ import { readFileSync } from "node:fs";
1
2
  import adapter from "@sveltejs/adapter-bun";
2
3
  import { sveltekit } from "@sveltejs/kit/vite";
3
4
  import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
4
5
  import tailwindcss from "@tailwindcss/vite";
5
- import { defineConfig } from "vite";
6
+ import { defineConfig, mergeConfig, type ProxyOptions } from "vite";
7
+ import { appSvelteKitConfig, appViteConfig } from "./vite.extensions.ts";
6
8
 
7
9
  // Inside the dev container, bind to all interfaces and accept local gateway or
8
10
  // HTTPS tunnel hostnames. Vite derives HMR's protocol, host, and port from the
@@ -10,16 +12,56 @@ import { defineConfig } from "vite";
10
12
  const inDocker = process.env.VITE_DOCKER === "1";
11
13
  const publicOrigin = process.env.PODOKIT_BUILD_ORIGIN;
12
14
 
13
- export default defineConfig({
14
- plugins: [
15
- tailwindcss(),
16
- sveltekit({
17
- adapter: adapter(),
18
- preprocess: vitePreprocess(),
19
- ...(publicOrigin ? { paths: { origin: publicOrigin } } : {}),
15
+ /**
16
+ * Dev-server proxies for the exact WebSocket paths in `.podokit/dev.json`. A
17
+ * SvelteKit route cannot answer an upgrade, so `bun run dev` sends these paths to
18
+ * the API the way the `podo dev` gateway and a deployment's edge do. The API must
19
+ * authenticate each upgrade itself.
20
+ */
21
+ export function webSocketProxy(
22
+ devConfig: string | null,
23
+ target = process.env.BACKEND_INTERNAL_URL ?? "http://localhost:5002",
24
+ ): Record<string, ProxyOptions> {
25
+ if (devConfig === null) return {};
26
+ const { webSocketPaths = [] } = JSON.parse(devConfig) as { webSocketPaths?: unknown };
27
+ if (!Array.isArray(webSocketPaths)) throw new Error(".podokit/dev.json webSocketPaths must be an array of paths");
28
+ return Object.fromEntries(
29
+ webSocketPaths.map((path: unknown) => {
30
+ if (typeof path !== "string" || !/^\/[A-Za-z0-9._~!$&'()+,;=:@/-]+$/.test(path)) {
31
+ throw new Error(`.podokit/dev.json webSocketPaths has an invalid path: ${JSON.stringify(path)}`);
32
+ }
33
+ const exact = `^${path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\?.*)?$`;
34
+ return [exact, { target, ws: true, xfwd: true }];
20
35
  }),
21
- ],
22
- server: {
23
- ...(inDocker ? { host: true, port: 5001, allowedHosts: true } : {}),
24
- },
25
- });
36
+ );
37
+ }
38
+
39
+ function readDevConfig(): string | null {
40
+ try {
41
+ return readFileSync(new URL("../../.podokit/dev.json", import.meta.url), "utf8");
42
+ } catch (cause) {
43
+ if ((cause as { code?: unknown }).code === "ENOENT") return null;
44
+ throw cause;
45
+ }
46
+ }
47
+
48
+ export default defineConfig((env) =>
49
+ mergeConfig(
50
+ {
51
+ plugins: [
52
+ tailwindcss(),
53
+ sveltekit({
54
+ adapter: adapter(),
55
+ preprocess: vitePreprocess(),
56
+ ...appSvelteKitConfig,
57
+ ...(publicOrigin ? { paths: { ...appSvelteKitConfig.paths, origin: publicOrigin } } : {}),
58
+ }),
59
+ ],
60
+ server: {
61
+ ...(inDocker ? { host: true, port: 5001, allowedHosts: true } : {}),
62
+ ...(env.command === "serve" ? { proxy: webSocketProxy(readDevConfig()) } : {}),
63
+ },
64
+ },
65
+ typeof appViteConfig === "function" ? appViteConfig(env) : appViteConfig,
66
+ ),
67
+ );
@@ -0,0 +1,23 @@
1
+ import type { Config as SvelteKitConfig } from "@sveltejs/kit/vite";
2
+ import type { UserConfig, UserConfigFnObject } from "vite";
3
+
4
+ /**
5
+ * Application-owned additions to the managed vite.config.ts, combined with Vite's
6
+ * `mergeConfig`: lists such as `plugins` are appended and objects are merged key
7
+ * by key. Use it for dev-server warmup, dependency optimization, `define`
8
+ * constants, extra plugins, or extra allowed hosts instead of editing
9
+ * vite.config.ts. A function receives Vite's `{ command, mode }`. WebSocket paths
10
+ * listed in `.podokit/dev.json` already reach the API through the dev server.
11
+ * PodoKit creates this file once and never overwrites it.
12
+ */
13
+ export const appViteConfig: UserConfig | UserConfigFnObject = {
14
+ // Transform the pages most sessions open first as soon as the dev server starts:
15
+ // server: { warmup: { clientFiles: ["./src/routes/+layout.svelte", "./src/routes/+page.svelte"] } },
16
+ };
17
+
18
+ /**
19
+ * Application-owned SvelteKit options, such as `alias`, `csrf`, or `prerender`.
20
+ * PodoKit sets `adapter` and `preprocess`, and `paths.origin` from
21
+ * PODOKIT_BUILD_ORIGIN at build time.
22
+ */
23
+ export const appSvelteKitConfig: SvelteKitConfig = {};
@@ -48,6 +48,7 @@ services:
48
48
  - { action: sync, path: ./apps/web/src, target: /app/apps/web/src }
49
49
  - { action: sync, path: ./apps/web/static, target: /app/apps/web/static }
50
50
  - { action: sync+restart, path: ./apps/web/vite.config.ts, target: /app/apps/web/vite.config.ts }
51
+ - { action: sync+restart, path: ./apps/web/vite.extensions.ts, target: /app/apps/web/vite.extensions.ts }
51
52
  - { action: rebuild, path: ./apps/web/package.json }
52
53
 
53
54
  api:
@@ -121,7 +122,7 @@ services:
121
122
  mem_limit: ${MINIO_MEM_LIMIT:-512m}
122
123
  memswap_limit: ${MINIO_MEM_LIMIT:-512m}
123
124
  profiles: [storage]
124
- image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z
125
+ image: pgsty/silo:RELEASE.2026-09-16T00-00-00Z@sha256:635197cb9f36d01bee221d34d1c7d7960f6a95c48b0b6c01d99cd13bdae51a46
125
126
  command: server /data --console-address ":9001"
126
127
  environment:
127
128
  MINIO_ROOT_USER: podokit
@@ -134,15 +135,17 @@ services:
134
135
  mem_limit: ${MINIO_INIT_MEM_LIMIT:-128m}
135
136
  memswap_limit: ${MINIO_INIT_MEM_LIMIT:-128m}
136
137
  profiles: [storage]
137
- image: pgsty/mc:RELEASE.2026-09-16T00-00-00Z
138
+ image: pgsty/mc:RELEASE.2026-09-16T00-00-00Z@sha256:cfc83108c3abb371f8fb84d99c1fdc88f8c237e022409b0081fb7c0a3be634dd
138
139
  depends_on:
139
140
  minio: { condition: service_started }
141
+ # -e stops at the first failing command, so a bucket that cannot be
142
+ # created fails this service instead of leaving uploads to fail later.
140
143
  entrypoint:
141
144
  - sh
142
- - -c
145
+ - -ec
143
146
  - |
144
147
  until mc alias set loc http://minio:9000 podokit podokitsecret; do sleep 1; done
145
- mc mb -p loc/podokit || true
148
+ mc mb --ignore-existing loc/podokit
146
149
  networks: [podokit]
147
150
 
148
151
  networks:
@@ -16,9 +16,9 @@ Read the profile's `driver` first and say which one you are operating.
16
16
  1. Run `{{packageExecutor}} @podosoft/podokit status`, `{{packageExecutor}} @podosoft/podokit diff`, and `{{packageExecutor}} @podosoft/podokit doctor`. Stop if managed-file conflicts or unsupported framework versions could make the image unreliable.
17
17
  2. Read [references/profile.md](references/profile.md) before creating or changing a deployment profile.
18
18
  3. Make sure the images exist for that tag. Read [references/release.md](references/release.md): tagging `vX.Y.Z` runs the release workflow, which builds and pushes both images and stops there. Never apply a release whose images were not published.
19
- 4. Run `{{packageExecutor}} @podosoft/podokit deploy doctor --profile <name>`. Resolve every failure without printing secret values.
19
+ 4. Run `{{packageExecutor}} @podosoft/podokit deploy doctor --profile <name>`. Resolve every `FAIL` line and show the user every `warn` line, without printing secret values.
20
20
  5. Run `{{packageExecutor}} @podosoft/podokit deploy plan --profile <name> --release <stable-semver> --json`.
21
- 6. Show the user the driver, the target (context plus namespace and Helm release, or Docker context plus Compose project), the fingerprint result, the exact image digests, the current revision, the actions, the migration warning, and the plan hash.
21
+ 6. Show the user the driver, the target (context plus namespace and Helm release, or Docker context plus Compose project), the fingerprint result, the exact image digests, the current revision, the actions, every plan warning, and the plan hash.
22
22
  7. Do not apply until the user explicitly approves that exact plan.
23
23
  8. Apply with `{{packageExecutor}} @podosoft/podokit deploy apply --profile <name> --release <stable-semver> --confirm <plan-hash>`.
24
24
  9. Run `{{packageExecutor}} @podosoft/podokit deploy status --profile <name> --json` and `{{packageExecutor}} @podosoft/podokit deploy verify --profile <name> --json`.
@@ -24,9 +24,17 @@ Review these fields before planning:
24
24
  `stripPrefix` Middleware sends them to API without `/api`. It needs a
25
25
  Traefik-served IngressClass, and `/api/auth` is rejected. `podo deploy init`
26
26
  and `podo add` fill in the paths installed modules declare (file-upload:
27
- `/api/files/uploads/`); keep them.
27
+ `/api/files/uploads/`); keep them. `edgeProxies` counts the reverse proxies in
28
+ front of the Ingress controller or NodePort (default 0); the web server then
29
+ trusts that many plus the controller in `X-Forwarded-For`. Never set
30
+ `XFF_DEPTH`, `ADDRESS_HEADER`, `TRUSTED_PROXY_HOPS`, or `TRUSTED_PROXY_HEADER`
31
+ in `runtimeConfig`; the profile refuses them.
28
32
  - `workloads`: API/web replicas and resources plus an optional worker
29
- - `dependencies`: PostgreSQL, Redis, and object storage mode, image, Secret name, and storage
33
+ - `dependencies`: PostgreSQL, Redis, and object storage mode, image, Secret name, and storage.
34
+ PostgreSQL also takes `maxConnections` (default 100) and `appPools` for pools the
35
+ application opens itself, each sized by a `runtimeConfig` key (`sizeKey`) or a
36
+ fixed `size` when the code sets it; the doctor and the plan check the connection budget
37
+ against them, so lower `DB_POOL_MAX`/`DB_AUTH_POOL_MAX` or replicas when they warn.
30
38
  - `secrets`: API/optional web Secret names and required key names, plus image-pull Secret
31
39
  - `runtimeConfig`: non-sensitive environment values only
32
40
  - `verification`: HTTPS origin and checks with exact statuses and optional JSON fields
@@ -72,7 +80,9 @@ The fields that differ from the Kubernetes driver:
72
80
  it, sends exact WebSocket paths to API, sends `apiPaths` prefixes to API with
73
81
  `/api` removed, and sends every other request to web. Forwarding headers are trusted
74
82
  only from the direct proxy CIDRs in `trustedProxyCidrs`; leave it empty without an
75
- external proxy. The bind address defaults to `127.0.0.1`, because the expected
83
+ external proxy. `edgeProxies` counts the reverse proxies in front of the published
84
+ port (default 1); with the gateway it must match `trustedProxyCidrs`, which the
85
+ doctor checks. The bind address defaults to `127.0.0.1`, because the expected
76
86
  front door is a trusted reverse proxy — binding every interface publishes the app
77
87
  before TLS exists.
78
88
  - `workloads`: replicas plus Compose `cpus` and `memory` limits
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: podokit-update
3
- description: Use when the user wants to update a Bun-based project to a newer PodoKit v1 version, pull in template/module improvements, check local edits, or take ownership of a managed file. Covers podo status/diff/doctor/update/eject.
3
+ description: Use when the user wants to update a Bun-based project to a newer PodoKit v1 version, pull in template/module improvements, check local edits, or take ownership of a managed file. Covers podo status/diff/doctor/update/eject/restore.
4
4
  ---
5
5
 
6
6
  # Keep a PodoKit project up to date
@@ -11,15 +11,18 @@ its own files while keeping the user's changes.
11
11
  ```bash
12
12
  podo status # version, modules, file tiers, and how many managed files were edited
13
13
  podo diff # list the PodoKit-managed files you've edited (owned files ignored)
14
+ podo diff <path> # line diff of one file against the template (owned files too)
14
15
  podo doctor # check framework versions vs. the supported ranges
15
16
  podo update # dry-run: preview what a newer version would change (writes nothing)
16
17
  podo update --apply # apply it — clean updates are written; your edits are 3-way merged
17
18
  podo eject <path> # take ownership of a managed file so update skips it
19
+ podo restore <path> # rewrite a managed file from the template; re-manages an ejected file
18
20
  ```
19
21
 
20
22
  Rules to respect:
21
23
  - **owned** files (routes, your components, `AGENTS.md`, shadcn `ui/**`) are never
22
- touched by `update`.
24
+ touched by `update`. When their template changes, the plan lists them as
25
+ `review`; compare with `podo diff <path>` and copy what the app needs.
23
26
  - **assembled** files are recomputed from the module set — keep edits **outside**
24
27
  the `// podokit:begin…end` fences.
25
28
  - Run `podo update` (dry-run) first and read the plan before `--apply`. Commit or
@@ -18,11 +18,14 @@ TRUSTED_PROXY_HOPS=1
18
18
  # response headers.
19
19
  # BACKEND_TIMEOUT_MS=2000
20
20
  # BACKEND_PROXY_TIMEOUT_MS=30000
21
- CORS_ORIGIN=http://{{projectName}}.localhost,http://localhost
21
+ # podo dev sets CORS_ORIGIN and BETTER_AUTH_URL for web and api from
22
+ # .podokit/dev.json. These two serve compose.dev.yaml run without podo dev,
23
+ # such as from the devcontainer.
24
+ CORS_ORIGIN={{devCorsOrigin}}
22
25
  REDIS_HOST=redis
23
26
  REDIS_PORT=6379
24
27
  BETTER_AUTH_SECRET=
25
- BETTER_AUTH_URL=http://{{projectName}}.localhost
28
+ BETTER_AUTH_URL={{devAuthUrl}}
26
29
  ADMIN_EMAILS=admin@example.com
27
30
  AUDIT_LOG_ENABLED=true
28
31
  RATE_LIMIT_TTL=60
@@ -3,9 +3,13 @@
3
3
  # ── Required (runtime) ───────────────────────────────────────────────
4
4
  NODE_ENV=development
5
5
  PORT=5002
6
+ # Address the API listens on (default 0.0.0.0); 127.0.0.1 keeps it off the network.
7
+ # HOST=0.0.0.0
6
8
 
7
9
  # Canonical database connection. Set this for PostgreSQL or SQLite; POSTGRES_* remains a fallback.
8
10
  # DATABASE_URL=postgres://podokit:podokit@localhost:5432/podokit
11
+ # With SQLite and no DATABASE_URL, the database file, relative to apps/api.
12
+ # SQLITE_PATH=./data/podokit.sqlite
9
13
  POSTGRES_HOST=localhost
10
14
  POSTGRES_PORT=5432
11
15
  POSTGRES_USER=podokit
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "hostname": "{{projectName}}.localhost",
3
+ "hostname": "{{devHostname}}",
4
4
  "webSocketPaths": [],
5
5
  "apiPaths": []
6
6
  }
@@ -16,3 +16,4 @@ data:
16
16
  HOST_HEADER: "x-forwarded-host"
17
17
  ADDRESS_HEADER: "x-forwarded-for"
18
18
  XFF_DEPTH: "1"
19
+ # podokit:end:runtime-config
@@ -13,9 +13,58 @@ The suite runs against a live stack (web on `E2E_BASE_URL`, proxying `/api/*` to
13
13
  the API). With the recommended containerized loop, start `podo dev watch` and run:
14
14
 
15
15
  ```bash
16
- E2E_BASE_URL=http://{{projectName}}.localhost {{rootRun}} test:e2e
16
+ E2E_BASE_URL=http://{{devHostname}} {{rootRun}} test:e2e
17
17
  ```
18
18
 
19
19
  The default `http://localhost:5001` is for the alternative `{{rootRun}} dev`
20
20
  host-process loop. Files ending in `*.api.spec.ts` run in the request-only `api`
21
21
  project; `*.ui.spec.ts` run in the chromium `ui` project.
22
+
23
+ ## Workers and exclusive tests
24
+
25
+ The suite runs on one worker unless `E2E_WORKERS` says otherwise. For the whole
26
+ suite, more workers finish sooner:
27
+
28
+ ```bash
29
+ E2E_WORKERS=2 {{rootRun}} test:e2e
30
+ ```
31
+
32
+ Set the count with `E2E_WORKERS`, not `--workers`: the global setup refuses more
33
+ than one worker when `E2E_WORKERS` is unset.
34
+
35
+ Every test shares one API, database, mail catcher, and SMS sink. A test that
36
+ changes state other tests rely on takes the `@exclusive` tag:
37
+
38
+ ```ts
39
+ test("maintenance mode holds visitors back", { tag: "@exclusive" }, async ({ page }) => {
40
+ // ...
41
+ });
42
+ ```
43
+
44
+ Tag a test when it changes a server-wide setting (a feature flag, the
45
+ authentication configuration such as SMTP, social providers, or sign-up
46
+ approval, the site settings, or a module's settings) or the profile of a seeded
47
+ account. Each project has an `<name>-exclusive` twin, such as `ui-exclusive`,
48
+ that runs only the tagged tests, one at a time. Never empty the shared mail
49
+ catcher or SMS sink in a test: send to an address or number of the test's own
50
+ instead.
51
+
52
+ The `test:e2e` scripts run `run-e2e.ts`, which hands its arguments (a file,
53
+ `--grep`, `--project`) to Playwright. With more than one worker it runs
54
+ Playwright twice: first every test not tagged `@exclusive` on those workers,
55
+ then the tagged tests on one worker, even when a test failed in the first run.
56
+ The suite fails when either run fails. The second run reports to
57
+ `playwright-report/exclusive` and `test-results/exclusive`:
58
+
59
+ ```bash
60
+ bunx playwright show-report playwright-report/exclusive
61
+ ```
62
+
63
+ A plain `bunx playwright test` with `E2E_WORKERS` above 1 makes one run in which
64
+ the twins depend on every other test: a failing test there leaves the exclusive
65
+ tests not run, and selecting an exclusive test runs every other test first. Use
66
+ `{{rootRun}} test:e2e` on more workers, and do not combine `--no-deps` with
67
+ `E2E_WORKERS` above 1.
68
+
69
+ Application-owned projects in `playwright.projects.cjs` get twins too. A project
70
+ that another project depends on is a setup project and keeps a single copy.
@@ -0,0 +1,18 @@
1
+ import type { AppPaths } from "./paths";
2
+
3
+ /**
4
+ * Where this application's screens differ from the generated ones, for the
5
+ * managed e2e specs. PodoKit creates this file once and never overwrites it.
6
+ * Set only what differs; `paths.ts` documents each entry and its default. For
7
+ * example, an app whose first screen is a signed-in workspace without a
8
+ * heading, and which shows no public footer:
9
+ *
10
+ * ```ts
11
+ * export const appPaths: Partial<AppPaths> = {
12
+ * homeReady: (page) => page.getByTestId("workspace"),
13
+ * indexable: [],
14
+ * footer: null,
15
+ * };
16
+ * ```
17
+ */
18
+ export const appPaths: Partial<AppPaths> = {};
@@ -0,0 +1,33 @@
1
+ import type { Locator, Page } from "@playwright/test";
2
+ import { appPaths } from "./app-paths";
3
+
4
+ /**
5
+ * The application screens managed specs visit. The defaults describe the
6
+ * generated pages; `app-paths.ts`, which the application owns, overrides any
7
+ * of them, so a managed spec never needs editing for an app's own screens.
8
+ */
9
+ export interface AppPaths {
10
+ /** The first screen a visitor opens. */
11
+ home: string;
12
+ /** Visible once the first screen has rendered. */
13
+ homeReady: (page: Page) => Locator;
14
+ /** Public pages search engines may index: they carry no `X-Robots-Tag`. */
15
+ indexable: string[];
16
+ /** A public page that renders the managed `SiteFooter`, or null when none does. */
17
+ footer: string | null;
18
+ /**
19
+ * A page that renders the managed `LanguageSwitch`, `ThemeToggle`, and
20
+ * `AccountMenu` for a signed-in user, or null when none does.
21
+ */
22
+ accountMenu: string | null;
23
+ }
24
+
25
+ const defaults: AppPaths = {
26
+ home: "/",
27
+ homeReady: (page) => page.getByRole("heading", { level: 1 }),
28
+ indexable: ["/"],
29
+ footer: "/login",
30
+ accountMenu: "/",
31
+ };
32
+
33
+ export const paths: AppPaths = { ...defaults, ...appPaths };
@@ -0,0 +1,15 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ /**
5
+ * The administrator the admin-dashboard Playwright setup seeds, with the address
6
+ * and password `accounts.ts` reads from `E2E_*` variables and the application's
7
+ * `app-accounts.ts`, or null in an app without that module. Specs of modules
8
+ * that do not require admin-dashboard sign in with this instead of importing
9
+ * `accounts.ts`, which only admin-dashboard installs.
10
+ */
11
+ export async function seededAdmin(): Promise<{ email: string; password: string } | null> {
12
+ if (!existsSync(join(__dirname, "accounts.ts"))) return null;
13
+ const { ADMIN } = await import("./accounts");
14
+ return { email: ADMIN.email, password: ADMIN.password };
15
+ }
@@ -3,9 +3,9 @@
3
3
  "version": "0.0.0",
4
4
  "private": true,
5
5
  "scripts": {
6
- "test:e2e": "bunx playwright test",
7
- "test:e2e:ui": "bunx playwright test --project=ui",
8
- "test:e2e:api": "bunx playwright test --project=api",
6
+ "test:e2e": "bun run-e2e.ts",
7
+ "test:e2e:ui": "bun run-e2e.ts --project=ui --project=ui-exclusive",
8
+ "test:e2e:api": "bun run-e2e.ts --project=api --project=api-exclusive",
9
9
  "test:e2e:report": "bunx playwright show-report"
10
10
  },
11
11
  "devDependencies": {
@@ -1,11 +1,15 @@
1
1
  import { defineConfig, devices } from "@playwright/test";
2
- import { loadPlaywrightProjects } from "./playwright.extensions";
2
+ import { e2eRunSettings, loadPlaywrightProjects, withExclusivePhase } from "./playwright.extensions";
3
3
 
4
4
  // The suite runs against a live stack: web on E2E_BASE_URL (proxying /api/* to
5
5
  // the API). Start it first (see docs/testing.md) — locally via the dev harness,
6
6
  // in CI via scripts/e2e-ci.mjs. Two projects: `api` (request-only, backend e2e)
7
7
  // and `ui` (chromium, pages). Files are routed by suffix (*.api/*.ui.spec.ts).
8
+ // E2E_WORKERS sets the worker count (default 1); tests tagged @exclusive run in
9
+ // `<project>-exclusive` twins after the others, in a second run when run-e2e.ts
10
+ // starts the suite on more workers (see playwright.extensions.ts).
8
11
  const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:5001";
12
+ const run = e2eRunSettings();
9
13
  const coreProjects = [
10
14
  { name: "api", testMatch: /.*\.api\.spec\.ts/ },
11
15
  { name: "ui", testMatch: /.*\.ui\.spec\.ts/, use: { ...devices["Desktop Chrome"] } },
@@ -16,12 +20,14 @@ export default defineConfig({
16
20
  fullyParallel: true,
17
21
  forbidOnly: !!process.env.CI,
18
22
  retries: process.env.CI ? 2 : 0,
19
- workers: process.env.CI ? 1 : undefined,
20
- reporter: [["list"], ["html", { open: "never" }]],
23
+ workers: run.workers,
24
+ outputDir: run.outputDir,
25
+ globalSetup: "./playwright.extensions.ts",
26
+ reporter: [["list"], ["html", { open: "never", outputFolder: run.reportFolder }]],
21
27
  use: {
22
28
  baseURL,
23
29
  trace: "on-first-retry",
24
30
  screenshot: "only-on-failure",
25
31
  },
26
- projects: [...coreProjects, ...loadPlaywrightProjects(coreProjects)],
32
+ projects: withExclusivePhase([...coreProjects, ...loadPlaywrightProjects(coreProjects)], run.workers, run.phase),
27
33
  });