@podosoft/podokit 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) 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 +56 -2
  19. package/dist/deploy-schema.js +130 -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 +28 -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 +5 -1
  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 +118 -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 +29 -0
  52. package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +39 -2
  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 +12 -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 +40 -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/package.json +2 -2
  93. package/dist/templates/fullstack/tests/playwright.config.ts +7 -3
  94. package/dist/templates/fullstack/tests/playwright.extensions.ts +88 -1
  95. package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
  96. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
  97. package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
  98. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
  99. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
  100. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
  101. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
  102. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +39 -0
  103. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
  104. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
  105. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +11 -1
  106. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
  107. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
  108. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
  109. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
  110. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
  111. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
  112. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
  113. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
  114. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
  115. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
  116. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
  117. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
  118. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
  119. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
  120. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
  121. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
  122. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
  123. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
  124. package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
  125. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
  126. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
  127. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
  128. package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
  129. package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
  130. package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
  131. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +3 -1
  132. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
  133. package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
  134. package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +41 -11
  135. package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
  136. package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
  137. package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
  138. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +11 -5
  139. package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
  140. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +55 -55
  141. package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
  142. package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
  143. package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
  144. package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
  145. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
  146. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
  147. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
  148. package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
  149. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
  150. package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
  151. package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
  152. package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
  153. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
  154. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
  155. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
  156. package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
  157. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
  158. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
  159. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
  160. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +8 -7
  161. package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +29 -0
  162. package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
  163. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
  164. package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
  165. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
  166. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +17 -0
  167. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +18 -0
  168. package/dist/templates/modules/audit-log/module.manifest.json +15 -3
  169. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
  170. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
  171. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
  172. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
  173. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +2 -0
  174. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
  175. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +7 -5
  176. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
  177. package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
  178. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
  179. package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
  180. package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
  181. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
  182. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
  183. package/dist/templates/modules/auth/module.manifest.json +8 -2
  184. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +57 -1
  185. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +93 -9
  186. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
  187. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
  188. package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
  189. package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
  190. package/dist/templates/modules/bullmq/module.manifest.json +5 -1
  191. package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
  192. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +87 -1
  193. package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +93 -3
  194. package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
  195. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
  196. package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
  197. package/dist/templates/modules/mailer/module.manifest.json +1 -1
  198. package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +1 -1
  199. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
  200. package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +1 -1
  201. package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +45 -20
  202. package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
  203. package/dist/templates/todo/AGENTS.md +5 -1
  204. package/dist/templates/todo/README.md +6 -4
  205. package/dist/text-diff.d.ts +12 -0
  206. package/dist/text-diff.js +111 -0
  207. package/dist/update.d.ts +61 -3
  208. package/dist/update.js +152 -27
  209. package/package.json +2 -2
  210. package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
  211. package/dist/templates/modules/admin-dashboard/files/tests/helpers/exclusive.ts +0 -80
@@ -0,0 +1,44 @@
1
+ /** The shortest time between two rebuilds while Better Auth reports a schema mismatch. */
2
+ export const SCHEMA_RECHECK_INTERVAL_MS = 2_000;
3
+
4
+ export interface SchemaCheckedAuth {
5
+ $context: Promise<{ checkSchema?: (() => Promise<void> | undefined) | undefined }>;
6
+ }
7
+
8
+ export interface SchemaRecheckOptions {
9
+ intervalMs?: number;
10
+ now?: () => number;
11
+ }
12
+
13
+ export function isSchemaMismatch(error: unknown): boolean {
14
+ return typeof error === "object" && error !== null && (error as { code?: unknown }).code === "SCHEMA_MISMATCH";
15
+ }
16
+
17
+ /**
18
+ * Better Auth keeps a schema mismatch it found until a migration run by the
19
+ * same process clears it. An API that started before `migrate:all` ran in
20
+ * another process, such as a separate container, therefore failed every
21
+ * sign-in until it restarted. The returned function calls `rebuild` when
22
+ * `auth` holds a mismatch, at most once per interval; the new instance checks
23
+ * the database again, so the first request after the migration succeeds.
24
+ */
25
+ export function createSchemaRecheck(
26
+ options: SchemaRecheckOptions = {},
27
+ ): (auth: SchemaCheckedAuth, rebuild: () => void) => Promise<void> {
28
+ const intervalMs = options.intervalMs ?? SCHEMA_RECHECK_INTERVAL_MS;
29
+ const now = options.now ?? Date.now;
30
+ let rebuiltAt = Number.NEGATIVE_INFINITY;
31
+ return async (auth, rebuild) => {
32
+ // Nothing is pending once the schema is known to be clean.
33
+ const pending = (await auth.$context).checkSchema?.();
34
+ if (!pending) return;
35
+ try {
36
+ await pending;
37
+ } catch (error) {
38
+ // A failure to reach the database is not kept, so only a mismatch needs a rebuild.
39
+ if (!isSchemaMismatch(error) || now() - rebuiltAt < intervalMs) return;
40
+ rebuiltAt = now();
41
+ rebuild();
42
+ }
43
+ };
44
+ }
@@ -11,6 +11,7 @@ import {
11
11
  SUPPORTED_SOCIAL_PROVIDERS,
12
12
  } from "@podosoft/podokit-auth";
13
13
  import type { DatabaseProviderName } from "@podosoft/podokit-runtime";
14
+ import { AUTH_CONFIG_DEFAULTS } from "../auth/auth-config-store";
14
15
  import { refreshAuthNow } from "../auth/auth-provider";
15
16
  import {
16
17
  readAuthConfigRow,
@@ -123,7 +124,7 @@ export class AuthConfigService {
123
124
  async describe(): Promise<AuthConfigView> {
124
125
  const rows = await this.rows();
125
126
  const byKey = new Map(rows.map((row) => [row.key, row]));
126
- const environment = envAuthConfig();
127
+ const environment = envAuthConfig(AUTH_CONFIG_DEFAULTS);
127
128
  const social: Record<string, SocialProviderView> = {};
128
129
 
129
130
  for (const row of rows) {
@@ -10,6 +10,7 @@ import {
10
10
  migrateSqliteLegacyAccountIssuers,
11
11
  postgresAccountIssuerMigrationDatabase,
12
12
  } from "./auth/account-issuer-migration";
13
+ import { assertMigrationsLoaded } from "./database/migration-files";
13
14
  import { runSqliteMigrations } from "./database/sqlite-migrator";
14
15
 
15
16
  async function runMigrations(): Promise<void> {
@@ -24,9 +25,10 @@ async function runMigrations(): Promise<void> {
24
25
  if (sqliteDatabase) {
25
26
  await runSqliteMigrations(sqliteDatabase);
26
27
  } else {
27
- const { appDataSource: dataSource } = await import("./database/data-source.js");
28
+ const { appDataSource: dataSource, appMigrations } = await import("./database/data-source.js");
28
29
  await dataSource.initialize();
29
30
  try {
31
+ assertMigrationsLoaded(dataSource.migrations, appMigrations);
30
32
  await dataSource.runMigrations();
31
33
  } finally {
32
34
  await dataSource.destroy();
@@ -0,0 +1,9 @@
1
+ // This owned file is never overwritten by `podo update`. Declare the
2
+ // application's on/off settings and their defaults here. They are stored with
3
+ // PodoKit's feature flags in app_setting: administrators change them through
4
+ // PUT /account/settings, the API reads them with SettingsService.getBool(), and
5
+ // GET /account/capabilities returns them as `appFlags`, so do not put anything
6
+ // secret in a name. A missing row keeps the default. For example:
7
+ //
8
+ // export const APP_FLAG_DEFAULTS = { reportsEnabled: true } satisfies Record<string, boolean>;
9
+ export const APP_FLAG_DEFAULTS = {} satisfies Record<string, boolean>;
@@ -0,0 +1,43 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { copyFileSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { FLAG_DEFAULTS, mergeFlagDefaults } from "./flag-defaults";
5
+
6
+ describe("feature flag defaults", () => {
7
+ test("adds the application's flags after PodoKit's", () => {
8
+ const merged = mergeFlagDefaults(FLAG_DEFAULTS, { reportsEnabled: true, betaSearch: false });
9
+ expect(merged).toMatchObject({ twoFactor: true, phoneNumber: false, reportsEnabled: true, betaSearch: false });
10
+ });
11
+
12
+ test("rejects an application flag that reuses a PodoKit name", () => {
13
+ expect(() => mergeFlagDefaults(FLAG_DEFAULTS, { passkey: false })).toThrow(
14
+ "app-flags.ts redefines PodoKit feature flags: passkey",
15
+ );
16
+ });
17
+
18
+ test("reads stored application flags and keeps their defaults otherwise", async () => {
19
+ // A copy next to an app-flags.ts of the test's own, in the API directory.
20
+ const root = mkdtempSync(join(__dirname, ".app-flags-"));
21
+ try {
22
+ copyFileSync(join(__dirname, "flag-defaults.ts"), join(root, "flag-defaults.ts"));
23
+ writeFileSync(
24
+ join(root, "app-flags.ts"),
25
+ "export const APP_FLAG_DEFAULTS = { reportsEnabled: true, betaSearch: false };\n",
26
+ );
27
+ writeFileSync(join(root, "check.ts"), [
28
+ 'import { APP_FLAGS, FEATURE_FLAGS, flagsFrom } from "./flag-defaults";',
29
+ 'const flags = flagsFrom(new Map([["betaSearch", "true"], ["magicLink", "false"]]));',
30
+ "console.log(JSON.stringify([APP_FLAGS, FEATURE_FLAGS.includes(\"reportsEnabled\"), flags.reportsEnabled, flags.betaSearch, flags.magicLink]));",
31
+ "",
32
+ ].join("\n"));
33
+ const child = Bun.spawn([process.execPath, join(root, "check.ts")], { stdout: "pipe", stderr: "pipe" });
34
+ const [stdout, code] = await Promise.all([new Response(child.stdout).text(), child.exited]);
35
+ expect({ code, stdout: stdout.trim() }).toEqual({
36
+ code: 0,
37
+ stdout: '[["reportsEnabled","betaSearch"],true,true,true,false]',
38
+ });
39
+ } finally {
40
+ rmSync(root, { recursive: true, force: true });
41
+ }
42
+ });
43
+ });
@@ -1,14 +1,22 @@
1
- /** Admin-managed auth feature flags and their shipped defaults.
1
+ import { APP_FLAG_DEFAULTS } from "./app-flags";
2
+
3
+ /** Admin-managed auth feature flags and their shipped defaults, followed by the
4
+ * application's flags from the owned app-flags.ts.
2
5
  *
3
6
  * Kept free of framework/ORM imports so both SettingsService and the Better
4
7
  * Auth feature gate (loaded by auth.ts, including when
5
8
  * the better-auth CLI evaluates auth.ts for migrations) can import it safely.
6
9
  *
7
- * Must match the rows seeded by the InitAppSettings migration. phoneNumber is
8
- * off by default because real delivery needs an SMS provider. */
9
- export type FeatureFlag = "twoFactor" | "magicLink" | "emailOtp" | "username" | "multiSession" | "phoneNumber" | "apiKey" | "passkey" | "organization" | "oidcProvider" | "require2fa";
10
+ * The PodoKit defaults must match the rows seeded by the InitAppSettings
11
+ * migration. phoneNumber is off by default because real delivery needs an SMS
12
+ * provider. */
13
+ export type PodokitFeatureFlag = "twoFactor" | "magicLink" | "emailOtp" | "username" | "multiSession" | "phoneNumber" | "apiKey" | "passkey" | "organization" | "oidcProvider" | "require2fa";
14
+
15
+ export type AppFlag = keyof typeof APP_FLAG_DEFAULTS;
10
16
 
11
- export const FLAG_DEFAULTS: Record<FeatureFlag, boolean> = {
17
+ export type FeatureFlag = PodokitFeatureFlag | AppFlag;
18
+
19
+ const PODOKIT_FLAG_DEFAULTS: Record<PodokitFeatureFlag, boolean> = {
12
20
  twoFactor: true,
13
21
  magicLink: true,
14
22
  emailOtp: true,
@@ -26,8 +34,25 @@ export const FLAG_DEFAULTS: Record<FeatureFlag, boolean> = {
26
34
  require2fa: false,
27
35
  };
28
36
 
37
+ /** PodoKit's defaults with the application's; an application flag may not reuse a PodoKit name. */
38
+ export function mergeFlagDefaults<App extends Record<string, boolean>>(
39
+ podokit: Record<PodokitFeatureFlag, boolean>,
40
+ app: App,
41
+ ): Record<PodokitFeatureFlag | keyof App, boolean> {
42
+ const redefined = Object.keys(app).filter((flag) => flag in podokit);
43
+ if (redefined.length > 0) {
44
+ throw new Error(`app-flags.ts redefines PodoKit feature flags: ${redefined.join(", ")}`);
45
+ }
46
+ return { ...podokit, ...app };
47
+ }
48
+
49
+ export const FLAG_DEFAULTS: Record<FeatureFlag, boolean> = mergeFlagDefaults(PODOKIT_FLAG_DEFAULTS, APP_FLAG_DEFAULTS);
50
+
29
51
  export const FEATURE_FLAGS = Object.keys(FLAG_DEFAULTS) as FeatureFlag[];
30
52
 
53
+ /** The application's flags, which the capabilities endpoint returns as `appFlags`. */
54
+ export const APP_FLAGS = Object.keys(APP_FLAG_DEFAULTS) as AppFlag[];
55
+
31
56
  /** Stored values override the shipped defaults; a missing row keeps the default. */
32
57
  export function flagsFrom(settings: ReadonlyMap<string, string>): Record<FeatureFlag, boolean> {
33
58
  const flags = { ...FLAG_DEFAULTS };
@@ -6,9 +6,12 @@
6
6
  "managedOverrides": [
7
7
  ".claude/skills/podokit-configure-auth/**"
8
8
  ],
9
+ "ownedGlobs": [
10
+ "apps/api/src/settings/app-flags.ts"
11
+ ],
9
12
  "dependencies": {
10
- "@podosoft/podokit-auth": "^0.5.0",
11
- "@podosoft/podokit-contracts": "^0.5.0",
13
+ "@podosoft/podokit-auth": "^0.6.0",
14
+ "@podosoft/podokit-contracts": "^0.6.0",
12
15
  "better-auth": "^1.7.7",
13
16
  "pg": "^8.12.0",
14
17
  "@better-auth/api-key": "^1.7.7",
@@ -28,6 +31,9 @@
28
31
  "# makes previously saved DB secrets undecryptable.",
29
32
  "BETTER_AUTH_SECRET=change-me-in-production-min-32-characters",
30
33
  "BETTER_AUTH_URL=http://localhost:5002",
34
+ "# The name authenticator apps and passkey prompts show. Unset, it is the site",
35
+ "# name from the admin Settings, then the project name.",
36
+ "# AUTH_APP_NAME=",
31
37
  "# Everything below is optional. OAuth providers, SMTP, and server-enforced",
32
38
  "# toggles are configured on the admin Settings page and applied without restart.",
33
39
  "# Email/SMTP is provided by the mailer module (see its .env block).",
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "bun:test";
2
- import type { ConnectionOptions } from "bullmq";
2
+ import { Queue, type ConnectionOptions } from "bullmq";
3
3
  import { BullMqJobQueue, externalJobId, parseExternalJobId } from "./bullmq-job.queue";
4
4
  import { producerConnection, reconnectDelay, redisConnection } from "./queue";
5
5
 
@@ -128,3 +128,59 @@ describe("BullMqJobQueue while Redis is unavailable", () => {
128
128
  }
129
129
  });
130
130
  });
131
+
132
+ describe("BullMqJobQueue schedules", () => {
133
+ it("rejects an invalid schedule before keeping it", async () => {
134
+ const queue = new BullMqJobQueue(producerAt(closedPort()));
135
+ try {
136
+ await expect(queue.schedule("bad", "report", {}, { cron: "61 * * * *" })).rejects.toThrow();
137
+ await expect(queue.schedule("bad", "report", {}, { cron: "0 3 * * *", timezone: "Mars/Olympus" })).rejects.toThrow();
138
+ await expect(queue.schedule("bad", "report", {}, { everyMs: 10 })).rejects.toThrow("at least 1000");
139
+ } finally {
140
+ await queue.close();
141
+ }
142
+ });
143
+
144
+ it("keeps a schedule while Redis is unreachable instead of failing", async () => {
145
+ const queue = new BullMqJobQueue(producerAt(closedPort()));
146
+ try {
147
+ await expect(queue.schedule("nightly", "report", {}, { cron: "0 3 * * *" })).resolves.toBeUndefined();
148
+ await expect(queue.unschedule("nightly")).resolves.toBeUndefined();
149
+ } finally {
150
+ await queue.close();
151
+ }
152
+ });
153
+ });
154
+
155
+ // Runs against the Redis the app's environment names (REDIS_URL or REDIS_HOST, as
156
+ // in .env); skipped when neither is set.
157
+ describe.skipIf(!process.env.REDIS_URL && !process.env.REDIS_HOST)("BullMqJobQueue schedules in Redis", () => {
158
+ const REDIS = process.env;
159
+ it("applies schedules on connect, again after Redis lost them, and removes them", async () => {
160
+ const name = `schedule-test-${crypto.randomUUID()}`;
161
+ const queue = new BullMqJobQueue(producerConnection(REDIS));
162
+ const inspect = new Queue(name, { connection: redisConnection(REDIS) });
163
+ try {
164
+ await queue.schedule("hourly", name, { scope: "all" }, { everyMs: 3_600_000 });
165
+ await queue.schedule("nightly", name, {}, { cron: "0 3 * * *", timezone: "Asia/Seoul" });
166
+ expect(await inspect.getJobScheduler("hourly")).toBeUndefined();
167
+
168
+ await queue.connect();
169
+ expect(await inspect.getJobScheduler("hourly")).toMatchObject({ every: 3_600_000, name });
170
+ expect(await inspect.getJobScheduler("nightly")).toMatchObject({ pattern: "0 3 * * *", tz: "Asia/Seoul" });
171
+
172
+ // A Redis that lost its data gets the schedules back on the next connect.
173
+ await inspect.removeJobScheduler("hourly");
174
+ await queue.connect();
175
+ expect(await inspect.getJobScheduler("hourly")).toMatchObject({ every: 3_600_000 });
176
+
177
+ await queue.unschedule("nightly");
178
+ expect(await inspect.getJobScheduler("nightly")).toBeUndefined();
179
+ expect(await inspect.getJobSchedulersCount()).toBe(1);
180
+ } finally {
181
+ await queue.close();
182
+ await inspect.obliterate({ force: true });
183
+ await inspect.close();
184
+ }
185
+ });
186
+ });
@@ -1,13 +1,16 @@
1
1
  import { AppException } from "@podosoft/podokit-contracts";
2
2
  import { Queue, RedisConnection, Worker, type ConnectionOptions, type RedisClient } from "bullmq";
3
- import type {
4
- EnqueueOptions,
5
- JobHandler,
6
- JobQueue,
7
- JobReference,
8
- JobSnapshot,
9
- JobStatus,
10
- Unsubscribe,
3
+ import cronParser from "cron-parser";
4
+ import {
5
+ assertRepeatOptions,
6
+ type EnqueueOptions,
7
+ type JobHandler,
8
+ type JobQueue,
9
+ type JobReference,
10
+ type JobSnapshot,
11
+ type JobStatus,
12
+ type RepeatOptions,
13
+ type Unsubscribe,
11
14
  } from "@podosoft/podokit-runtime";
12
15
  import type { SupervisedConnection } from "../core/services";
13
16
  import { producerConnection, redisConnection } from "./queue";
@@ -45,6 +48,24 @@ export function jobsUnavailable(): AppException {
45
48
  return new AppException("JOBS_UNAVAILABLE", "Background jobs are temporarily unavailable.", 503);
46
49
  }
47
50
 
51
+ function isJobsUnavailable(error: unknown): boolean {
52
+ return error instanceof AppException && error.code === "JOBS_UNAVAILABLE";
53
+ }
54
+
55
+ interface Schedule {
56
+ name: string;
57
+ payload: unknown;
58
+ repeat: RepeatOptions;
59
+ }
60
+
61
+ /** Rejects a schedule BullMQ could not run, before it is kept. */
62
+ function assertSchedule(repeat: RepeatOptions): void {
63
+ assertRepeatOptions(repeat);
64
+ if (repeat.cron !== undefined) {
65
+ cronParser.parseExpression(repeat.cron, { tz: repeat.timezone ?? "UTC" }).next();
66
+ }
67
+ }
68
+
48
69
  /**
49
70
  * Adds and reads jobs over one producer connection that the service registry
50
71
  * supervises, and runs in-process handlers on BullMQ workers, which keep their
@@ -53,6 +74,9 @@ export function jobsUnavailable(): AppException {
53
74
  export class BullMqJobQueue implements JobQueue, SupervisedConnection {
54
75
  private readonly queues = new Map<string, Queue>();
55
76
  private readonly workers = new Map<string, Worker>();
77
+ private readonly schedules = new Map<string, Schedule>();
78
+ // Schedules to delete from Redis once it is reachable: id to queue name.
79
+ private readonly removals = new Map<string, string>();
56
80
  private producer: RedisConnection;
57
81
  private lostListener?: (reason: unknown) => void;
58
82
  private closed = false;
@@ -65,10 +89,17 @@ export class BullMqJobQueue implements JobQueue, SupervisedConnection {
65
89
  this.lostListener = listener;
66
90
  }
67
91
 
68
- /** Opens the producer connection. */
92
+ /**
93
+ * Opens the producer connection and applies every schedule, so a Redis that
94
+ * lost them, such as one restarted without persistence, gets them back.
95
+ */
69
96
  async connect(): Promise<void> {
70
97
  const client = await this.producer.client;
71
98
  if (client.status !== "ready") await client.connect();
99
+ await this.command(async (ready) => {
100
+ for (const id of [...this.removals.keys()]) await this.removeSchedule(ready, id);
101
+ for (const id of this.schedules.keys()) await this.applySchedule(ready, id);
102
+ });
72
103
  }
73
104
 
74
105
  /** Rejects while Redis is unreachable or slower to answer than `REDIS_COMMAND_TIMEOUT_MS`. */
@@ -110,6 +141,27 @@ export class BullMqJobQueue implements JobQueue, SupervisedConnection {
110
141
  };
111
142
  }
112
143
 
144
+ async schedule(id: string, name: string, payload: unknown, repeat: RepeatOptions): Promise<void> {
145
+ assertSchedule(repeat);
146
+ const previous = this.schedules.get(id);
147
+ // A scheduler lives in its queue, so a schedule that moves leaves the old one.
148
+ if (previous && previous.name !== name) this.removals.set(id, previous.name);
149
+ if (this.removals.get(id) === name) this.removals.delete(id);
150
+ this.schedules.set(id, { name, payload, repeat });
151
+ await this.whenConnected(async (client) => {
152
+ if (this.removals.has(id)) await this.removeSchedule(client, id);
153
+ await this.applySchedule(client, id);
154
+ });
155
+ }
156
+
157
+ async unschedule(id: string): Promise<void> {
158
+ const schedule = this.schedules.get(id);
159
+ if (!schedule) return;
160
+ this.schedules.delete(id);
161
+ this.removals.set(id, schedule.name);
162
+ await this.whenConnected((client) => this.removeSchedule(client, id));
163
+ }
164
+
113
165
  process(name: string, handler: JobHandler): Promise<Unsubscribe> {
114
166
  if (this.workers.has(name)) throw new Error(`A handler is already registered for job ${name}`);
115
167
  const worker = new Worker(
@@ -140,6 +192,38 @@ export class BullMqJobQueue implements JobQueue, SupervisedConnection {
140
192
  await this.producer.close(true);
141
193
  }
142
194
 
195
+ private async applySchedule(client: RedisClient, id: string): Promise<void> {
196
+ const schedule = this.schedules.get(id);
197
+ if (!schedule) return;
198
+ const { cron, everyMs, timezone } = schedule.repeat;
199
+ await this.queue(schedule.name, client).upsertJobScheduler(
200
+ id,
201
+ cron !== undefined ? { pattern: cron, tz: timezone ?? "UTC" } : { every: everyMs },
202
+ { name: schedule.name, data: schedule.payload },
203
+ );
204
+ }
205
+
206
+ private async removeSchedule(client: RedisClient, id: string): Promise<void> {
207
+ const name = this.removals.get(id);
208
+ if (name === undefined) return;
209
+ await this.queue(name, client).removeJobScheduler(id);
210
+ if (this.removals.get(id) === name) this.removals.delete(id);
211
+ }
212
+
213
+ /**
214
+ * Runs `send` while the producer is connected. Otherwise, or when Redis goes
215
+ * away during it, the next connect() applies the change instead.
216
+ */
217
+ private async whenConnected(send: (client: RedisClient) => Promise<void>): Promise<void> {
218
+ const client = await this.producer.client;
219
+ if (client.status !== "ready") return;
220
+ try {
221
+ await this.command(send);
222
+ } catch (error) {
223
+ if (!isJobsUnavailable(error)) throw error;
224
+ }
225
+ }
226
+
143
227
  /**
144
228
  * Runs `send` on the producer connection and fails it with JOBS_UNAVAILABLE
145
229
  * while the connection is closed or when Redis does not answer in time.
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { Processor } from "bullmq";
3
+ import { DEMO_QUEUE } from "./queue";
4
+ import { collectWorkers, type WorkerDefinition } from "./worker.module";
5
+
6
+ const processor: Processor = async () => undefined;
7
+
8
+ describe("worker definitions", () => {
9
+ it("adds the application's queues after PodoKit's and releases them last first", async () => {
10
+ const released: string[] = [];
11
+ const collected = await collectWorkers(async (setup) => {
12
+ setup.onClose(() => { released.push("database"); });
13
+ setup.onClose(async () => { released.push("cache"); });
14
+ return [{ queue: "reports", processor, options: { concurrency: 2 } }];
15
+ });
16
+ expect(collected.definitions.map((definition) => definition.queue)).toEqual([DEMO_QUEUE, "reports"]);
17
+ expect(collected.definitions[1]?.options).toEqual({ concurrency: 2 });
18
+ await collected.release();
19
+ expect(released).toEqual(["cache", "database"]);
20
+ });
21
+
22
+ it("refuses two workers for one queue and releases what was opened", async () => {
23
+ const released: string[] = [];
24
+ const duplicate: WorkerDefinition = { queue: DEMO_QUEUE, processor };
25
+ await expect(collectWorkers(async (setup) => {
26
+ setup.onClose(() => { released.push("database"); });
27
+ return [duplicate];
28
+ })).rejects.toThrow(`More than one worker for queue ${DEMO_QUEUE}`);
29
+ expect(released).toEqual(["database"]);
30
+ });
31
+
32
+ it("releases what was opened when the application setup fails", async () => {
33
+ const released: string[] = [];
34
+ await expect(collectWorkers(async (setup) => {
35
+ setup.onClose(() => { released.push("database"); });
36
+ throw new Error("no database");
37
+ })).rejects.toThrow("no database");
38
+ expect(released).toEqual(["database"]);
39
+ });
40
+ });
@@ -1,12 +1,25 @@
1
- import { Worker, type Processor } from "bullmq";
1
+ import { Worker, type Processor, type WorkerOptions } from "bullmq";
2
+ import { configureWorkers } from "../worker.extensions";
2
3
  import { processDemoJob } from "./demo.processor";
3
4
  import { DEMO_QUEUE, redisConnection } from "./queue";
4
5
  // podokit:begin:worker-imports
5
6
  // podokit:end:worker-imports
6
7
 
7
- interface WorkerDefinition {
8
+ export interface WorkerDefinition {
8
9
  queue: string;
9
10
  processor: Processor;
11
+ /** BullMQ worker options other than the connection, such as `concurrency`. */
12
+ options?: Omit<WorkerOptions, "connection">;
13
+ }
14
+
15
+ export interface WorkerSetup {
16
+ /** Runs `close` when the worker process stops, after its workers; the last one registered runs first. */
17
+ onClose(close: () => void | Promise<void>): void;
18
+ }
19
+
20
+ export interface WorkerRuntime {
21
+ workers: Worker[];
22
+ close(): Promise<void>;
10
23
  }
11
24
 
12
25
  const definitions: WorkerDefinition[] = [
@@ -15,8 +28,42 @@ const definitions: WorkerDefinition[] = [
15
28
  // podokit:end:worker-processors
16
29
  ];
17
30
 
18
- export function startWorkers(): Worker[] {
19
- return definitions.map(({ queue, processor }) =>
20
- new Worker(queue, processor, { connection: redisConnection() })
31
+ export interface CollectedWorkers {
32
+ definitions: WorkerDefinition[];
33
+ /** Releases what `configure` opened. */
34
+ release(): Promise<void>;
35
+ }
36
+
37
+ /** PodoKit's queues followed by the application's from worker.extensions.ts; one worker per queue. */
38
+ export async function collectWorkers(
39
+ configure: (setup: WorkerSetup) => Promise<WorkerDefinition[]> = configureWorkers,
40
+ ): Promise<CollectedWorkers> {
41
+ const closers: Array<() => void | Promise<void>> = [];
42
+ const release = async (): Promise<void> => {
43
+ for (const close of closers.splice(0).reverse()) await close();
44
+ };
45
+ try {
46
+ const all = [...definitions, ...await configure({ onClose: (close) => closers.push(close) })];
47
+ const queues = all.map((definition) => definition.queue);
48
+ const repeated = queues.filter((queue, index) => queues.indexOf(queue) !== index);
49
+ if (repeated.length > 0) throw new Error(`More than one worker for queue ${[...new Set(repeated)].join(", ")}`);
50
+ return { definitions: all, release };
51
+ } catch (error) {
52
+ await release();
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ export async function startWorkers(): Promise<WorkerRuntime> {
58
+ const { definitions: all, release } = await collectWorkers();
59
+ const workers = all.map(({ queue, processor, options }) =>
60
+ new Worker(queue, processor, { ...options, connection: redisConnection() })
21
61
  );
62
+ return {
63
+ workers,
64
+ close: async (): Promise<void> => {
65
+ await Promise.all(workers.map((worker) => worker.close()));
66
+ await release();
67
+ },
68
+ };
22
69
  }
@@ -6,16 +6,18 @@ if (PROVIDERS.jobs !== "bullmq") {
6
6
  throw new Error("The BullMQ worker cannot start unless jobs=bullmq is selected");
7
7
  }
8
8
 
9
- const workers = startWorkers();
9
+ const runtime = startWorkers();
10
10
 
11
- for (const worker of workers) {
12
- worker.on("failed", (job, error) => {
13
- process.stderr.write(`Process job failed: ${job?.id ?? "unknown"} ${error.message}\n`);
14
- });
15
- }
11
+ void runtime.then(({ workers }) => {
12
+ for (const worker of workers) {
13
+ worker.on("failed", (job, error) => {
14
+ process.stderr.write(`Process job failed: ${job?.id ?? "unknown"} ${error.message}\n`);
15
+ });
16
+ }
17
+ });
16
18
 
17
19
  for (const signal of ["SIGINT", "SIGTERM"] as const) {
18
20
  process.once(signal, () => {
19
- void Promise.all(workers.map((worker) => worker.close())).finally(() => process.exit(0));
21
+ void runtime.then((started) => started.close()).finally(() => process.exit(0));
20
22
  });
21
23
  }
@@ -0,0 +1,13 @@
1
+ import type { WorkerDefinition, WorkerSetup } from "./jobs/worker.module";
2
+
3
+ // This owned file is never overwritten by `podo update`. Return the queues the
4
+ // BullMQ worker process (`main-worker.ts`) consumes for the application, next to
5
+ // PodoKit's. Open what the processors need here and release it with
6
+ // `setup.onClose`, which runs after the workers stop. For example:
7
+ //
8
+ // const database = new Database(validateEnv(process.env));
9
+ // setup.onClose(() => database.close());
10
+ // return [{ queue: "reports", processor: (job) => buildReport(database, job.data), options: { concurrency: 2 } }];
11
+ export async function configureWorkers(_setup: WorkerSetup): Promise<WorkerDefinition[]> {
12
+ return [];
13
+ }
@@ -16,9 +16,13 @@
16
16
  "apps/api/src/jobs/**",
17
17
  "apps/api/src/main-worker.ts"
18
18
  ],
19
+ "ownedGlobs": [
20
+ "apps/api/src/worker.extensions.ts"
21
+ ],
19
22
  "targetApp": "api",
20
23
  "dependencies": {
21
- "bullmq": "^5.57.0"
24
+ "bullmq": "^5.57.0",
25
+ "cron-parser": "^4.9.0"
22
26
  },
23
27
  "scripts": {
24
28
  "start:worker": "bun dist/main-worker.js",
@@ -41,6 +41,8 @@ function fakeQueue(): FakeQueue {
41
41
  if (!handler) throw new Error(`no handler for ${job.name}`);
42
42
  await handler(payload, job);
43
43
  },
44
+ schedule: async () => undefined,
45
+ unschedule: async () => undefined,
44
46
  close: () => undefined,
45
47
  };
46
48
  }