bunderstack 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/crud.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  type AuthSessionResolver,
15
15
  type CrudOperation,
16
16
  type ResolvedAccess,
17
+ tableEntryForName,
17
18
  type ResolvedTableAccess,
18
19
  type ScopeMap,
19
20
  type ScopeResolver,
@@ -38,15 +39,6 @@ export type CrudRouterOptions<
38
39
  realtime?: RealtimeFacade<TSchema>
39
40
  }
40
41
 
41
- function tableEntryForName(
42
- access: ResolvedAccess,
43
- tableName: string,
44
- ): ResolvedTableAccess | undefined {
45
- for (const entry of access.values()) {
46
- if (entry.tableName === tableName) return entry
47
- }
48
- return undefined
49
- }
50
42
 
51
43
  function isRecord(value: unknown): value is Record<string, unknown> {
52
44
  return value !== null && typeof value === 'object' && !Array.isArray(value)
@@ -163,7 +155,11 @@ export function buildCrudRouter<TSchema extends Record<string, unknown>>(
163
155
  )
164
156
  }
165
157
 
166
- const scope = scopeFor(tableAccess.readScope, { user, session, request: c.req.raw })
158
+ const scope = scopeFor(tableAccess.readScope, {
159
+ user,
160
+ session,
161
+ request: c.req.raw,
162
+ })
167
163
  if (
168
164
  scope &&
169
165
  !rowMatchesScope(rows[0] as Record<string, unknown>, scope)
@@ -240,7 +236,12 @@ export function buildCrudRouter<TSchema extends Record<string, unknown>>(
240
236
  user?.id ?? null,
241
237
  )
242
238
 
243
- const scope = scopeFor(tableAccess.writeScope, { user, session, request: c.req.raw, body: body as Record<string, unknown> })
239
+ const scope = scopeFor(tableAccess.writeScope, {
240
+ user,
241
+ session,
242
+ request: c.req.raw,
243
+ body: body as Record<string, unknown>,
244
+ })
244
245
  const stamped = scope ? stampScope(values, scope) : values
245
246
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
246
247
  const rows = await (db as any).insert(table).values(stamped).returning()
@@ -279,7 +280,11 @@ export function buildCrudRouter<TSchema extends Record<string, unknown>>(
279
280
  return apiError(c, ErrorCode.NOT_FOUND, 'Not found', 404)
280
281
  }
281
282
 
282
- const readScope = scopeFor(tableAccess.readScope, { user, session, request: c.req.raw })
283
+ const readScope = scopeFor(tableAccess.readScope, {
284
+ user,
285
+ session,
286
+ request: c.req.raw,
287
+ })
283
288
  if (
284
289
  readScope &&
285
290
  !rowMatchesScope(existing[0] as Record<string, unknown>, readScope)
@@ -319,7 +324,12 @@ export function buildCrudRouter<TSchema extends Record<string, unknown>>(
319
324
  user?.id ?? null,
320
325
  )
321
326
 
322
- const writeScope = scopeFor(tableAccess.writeScope, { user, session, request: c.req.raw, body: body as Record<string, unknown> })
327
+ const writeScope = scopeFor(tableAccess.writeScope, {
328
+ user,
329
+ session,
330
+ request: c.req.raw,
331
+ body: body as Record<string, unknown>,
332
+ })
323
333
  const stamped = writeScope ? stampScope(values, writeScope) : values
324
334
 
325
335
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -352,7 +362,11 @@ export function buildCrudRouter<TSchema extends Record<string, unknown>>(
352
362
  return apiError(c, ErrorCode.NOT_FOUND, 'Not found', 404)
353
363
  }
354
364
 
355
- const scope = scopeFor(tableAccess.readScope, { user, session, request: c.req.raw })
365
+ const scope = scopeFor(tableAccess.readScope, {
366
+ user,
367
+ session,
368
+ request: c.req.raw,
369
+ })
356
370
  if (
357
371
  scope &&
358
372
  !rowMatchesScope(existing[0] as Record<string, unknown>, scope)
package/src/env.ts CHANGED
@@ -10,6 +10,10 @@ export type EnvConfigInput = {
10
10
  runtimeEnv?: Record<string, unknown>
11
11
  }
12
12
 
13
+ export type BunderstackRole = 'all' | 'web' | 'worker'
14
+
15
+ const ROLES: readonly BunderstackRole[] = ['all', 'web', 'worker']
16
+
13
17
  /** Vars bunderstack itself consumes, always validated. */
14
18
  export type BaseEnv = {
15
19
  NODE_ENV?: string
@@ -19,12 +23,13 @@ export type BaseEnv = {
19
23
  REDIS_URL?: string
20
24
  RESEND_API_KEY?: string
21
25
  SMTP_URL?: string
22
- BUNDERSTACK_CRON_SECRET?: string
26
+ BUNDERSTACK_ROLE: BunderstackRole
23
27
  }
24
28
 
25
- type InferVars<T> = T extends Record<string, ZodType>
26
- ? { [K in keyof T]: z.output<T[K]> }
27
- : unknown
29
+ type InferVars<T> =
30
+ T extends Record<string, ZodType>
31
+ ? { [K in keyof T]: z.output<T[K]> }
32
+ : unknown
28
33
 
29
34
  // Non-distributive so `ValidatedEnv<undefined>` is BaseEnv, not `never`.
30
35
  export type ValidatedEnv<TEnv extends EnvConfigInput | undefined> = [
@@ -52,8 +57,6 @@ export type ValidateEnvOptions = {
52
57
  source?: Record<string, string | undefined>
53
58
  /** Dialect-aware DATABASE_URL fallback; createBunderstack passes it. */
54
59
  defaultDatabaseUrl?: string
55
- /** Require the platform schedule secret for an application with cron work. */
56
- cronConfigured?: boolean
57
60
  }
58
61
 
59
62
  const DEV_AUTH_SECRET = 'dev-secret-change-in-prod'
@@ -108,13 +111,18 @@ export function validateEnv<TEnv extends EnvConfigInput | undefined>(
108
111
  REDIS_URL: source.REDIS_URL,
109
112
  RESEND_API_KEY: source.RESEND_API_KEY,
110
113
  SMTP_URL: source.SMTP_URL,
111
- BUNDERSTACK_CRON_SECRET: source.BUNDERSTACK_CRON_SECRET,
114
+ BUNDERSTACK_ROLE: (source.BUNDERSTACK_ROLE ?? 'all') as BunderstackRole,
112
115
  }
113
116
  if (isProduction && !source.AUTH_SECRET) {
114
117
  issues.push('AUTH_SECRET: required in production')
115
118
  }
116
- if (isProduction && options.cronConfigured && !source.BUNDERSTACK_CRON_SECRET) {
117
- issues.push('BUNDERSTACK_CRON_SECRET: required when cron is configured in production')
119
+ if (
120
+ source.BUNDERSTACK_ROLE !== undefined &&
121
+ !ROLES.includes(source.BUNDERSTACK_ROLE as BunderstackRole)
122
+ ) {
123
+ issues.push(
124
+ `BUNDERSTACK_ROLE: must be one of ${ROLES.join(', ')} (got "${String(source.BUNDERSTACK_ROLE)}")`,
125
+ )
118
126
  }
119
127
  if (options.emailProvider === 'resend' && !source.RESEND_API_KEY) {
120
128
  issues.push("RESEND_API_KEY: required when email provider is 'resend'")
package/src/handler.ts CHANGED
@@ -4,11 +4,11 @@ import { Hono } from 'hono'
4
4
  import { createRateLimiter, type RateLimitConfig } from './rate-limit'
5
5
 
6
6
  interface HandlerParts {
7
+ customRouter?: Hono
7
8
  crudRouter: Hono
8
9
  authHandler?: (req: Request) => Promise<Response>
9
10
  storageRouter?: Hono
10
11
  realtimeRouter?: Hono
11
- cronRouter?: Hono
12
12
  trpcHandler?: (req: Request) => Promise<Response>
13
13
  rateLimit?: boolean | RateLimitConfig
14
14
  }
@@ -20,16 +20,16 @@ export function buildHandler(parts: HandlerParts): {
20
20
  const app = new Hono()
21
21
  const checkRateLimit = createRateLimiter(parts.rateLimit)
22
22
 
23
+ // Registered ahead of everything so custom routes can sit in front of the
24
+ // core app. Collisions are rejected at construction, not silently shadowed.
25
+ if (parts.customRouter) app.route('/', parts.customRouter)
26
+
23
27
  const health = (c: { json: (data: unknown) => Response }) =>
24
28
  c.json({ status: 'ok' })
25
29
  app.get('/health', health)
26
30
  app.get('/api/health', health)
27
31
  app.route('/api', parts.crudRouter)
28
32
 
29
- if (parts.cronRouter) {
30
- app.route('/api/_bunderstack', parts.cronRouter)
31
- }
32
-
33
33
  if (parts.authHandler) {
34
34
  app.all('/api/auth/*', (c) => parts.authHandler!(c.req.raw))
35
35
  }
package/src/index.ts CHANGED
@@ -4,6 +4,8 @@ import type { Hono as HonoType } from 'hono'
4
4
 
5
5
  import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
6
6
 
7
+ import { getTableName, isTable } from 'drizzle-orm'
8
+
7
9
  import type { TableAccessInput } from './access'
8
10
  import type { DbFor } from './db'
9
11
  import type {
@@ -11,15 +13,17 @@ import type {
11
13
  EnqueueOptions,
12
14
  JobsDefs,
13
15
  JobsFacade,
14
- LocalCronScheduler,
15
- LocalCronSchedulerOptions,
16
16
  StartWorkerOptions,
17
17
  WorkerHandle,
18
18
  } from './jobs/index'
19
19
  import type { StorageConfigInput } from './storage/buckets'
20
20
  import type { StorageAdapter } from './storage/index'
21
21
 
22
- import { resolveAccessUser, validateAndResolveAccess } from './access'
22
+ import {
23
+ resolveAccessUser,
24
+ tableEntryForName,
25
+ validateAndResolveAccess,
26
+ } from './access'
23
27
  import {
24
28
  createAuth,
25
29
  toAuthSessionResolver,
@@ -37,10 +41,7 @@ import { withInternalTables } from './internal-tables'
37
41
  import {
38
42
  createJobsBuilder,
39
43
  createJobRunner,
40
- buildCronRouter,
41
44
  enqueueJob,
42
- runCronSlot,
43
- startLocalCronScheduler,
44
45
  startJobWorker,
45
46
  validateJobsDefs,
46
47
  } from './jobs/index'
@@ -57,6 +58,7 @@ import {
57
58
  } from './realtime/facade'
58
59
  import { createRealtimeBroker, buildRealtimeRouter } from './realtime/index'
59
60
  import { createRedisRealtimeBroker } from './realtime/redis'
61
+ import { createRouteContext, validateCustomRoutes } from './routes'
60
62
  import { deleteFileWithDerivatives } from './storage/delete'
61
63
  import { deleteFileMetaRow, insertReadyFile } from './storage/file-meta'
62
64
  import { createBucketStorages } from './storage/registry'
@@ -64,7 +66,8 @@ import { buildBucketStorageRouter } from './storage/router'
64
66
  import { sweepOrphans } from './storage/sweep'
65
67
  import { createTRPC, type BunderstackTRPC } from './trpc'
66
68
 
67
- type AuthInstance = ReturnType<typeof createAuth>
69
+ export type AuthInstance = ReturnType<typeof createAuth>
70
+
68
71
 
69
72
  function waitForWorkerShutdown(
70
73
  signal: AbortSignal,
@@ -138,11 +141,6 @@ export type AppRunWorkerOptions = AppStartWorkerOptions & {
138
141
  */
139
142
  allowProcessLocalRealtime?: boolean
140
143
  }
141
- export type AppStartCronSchedulerOptions = Pick<
142
- LocalCronSchedulerOptions,
143
- 'onError'
144
- >
145
-
146
144
  /** Bucket names declared in a storage config; `string` when unknowable. */
147
145
  export type BucketNamesOf<TStorage> = TStorage extends {
148
146
  buckets: infer B extends Record<string, unknown>
@@ -178,11 +176,9 @@ export type BunderstackApp<
178
176
  startWorker(options?: AppStartWorkerOptions): Promise<WorkerHandle>
179
177
  /** Run a queue worker until aborted, then close the application. */
180
178
  runWorker(options?: AppRunWorkerOptions): Promise<void>
181
- /** Start local delivery for declared cron tasks (for development only). */
182
- startCronScheduler(
183
- options?: AppStartCronSchedulerOptions,
184
- ): Promise<LocalCronScheduler>
185
179
  close(): Promise<void>
180
+ /** True when this process is running the background tick loop. */
181
+ readonly backgroundRunning: boolean
186
182
  readonly status: LifecycleStatus
187
183
  readonly signal: AbortSignal
188
184
  /** Deploy-time introspection: what this app needs provisioned. */
@@ -345,11 +341,9 @@ export async function createBunderstack<
345
341
  emailProvider: emailProviderTag(options.email),
346
342
  defaultDatabaseUrl:
347
343
  dialect === 'pg' ? 'file:./data.pglite' : 'file:./data.db',
348
- // Storage orphan cleanup is also a signed platform schedule, so every
349
- // production app has scheduled delivery even without user-defined cron.
350
- cronConfigured: true,
344
+ source: options.processEnv,
351
345
  })
352
- const config = resolveConfig(options, env)
346
+ const config = resolveConfig(options, env, options.processEnv)
353
347
  // Adapters use Drizzle mocks during deployment introspection, so the database
354
348
  // and Redis below never touch external services.
355
349
  const introspect = process.env.BUNDERSTACK_INTROSPECT === '1'
@@ -518,25 +512,41 @@ export async function createBunderstack<
518
512
  })
519
513
  },
520
514
  }
521
- const jobRunner = jobsDefs
515
+ const storageConfigured = Boolean(options.storage)
516
+ // The storage sweep used to be a hardcoded maintenance route. It is an
517
+ // ordinary cron now, so it inherits retries, timeout and onFailed.
518
+ const resolvedDefs: JobsDefs | undefined = storageConfigured
519
+ ? {
520
+ ...(jobsDefs ?? {}),
521
+ 'bunderstack:storage-sweep': {
522
+ kind: 'cron',
523
+ schedule: '0 4 * * *',
524
+ handler: async () => {
525
+ await storage.sweep()
526
+ },
527
+ },
528
+ }
529
+ : jobsDefs
530
+
531
+ const jobRunner = resolvedDefs
522
532
  ? createJobRunner({
523
533
  db,
524
- defs: jobsDefs,
534
+ defs: resolvedDefs,
525
535
  ctx: { db: userDb, env, email, storage, realtime },
526
536
  })
527
537
  : undefined
528
538
  const jobs = {
529
539
  async enqueue(name: string, input?: unknown, opts?: EnqueueOptions) {
530
- if (!jobsDefs) {
540
+ if (!resolvedDefs) {
531
541
  throw new Error(
532
542
  '[bunderstack] no jobs configured — add a `jobs` key to createBunderstack',
533
543
  )
534
544
  }
535
- const result = await enqueueJob(db, jobsDefs, name, input, opts)
545
+ const result = await enqueueJob(db, resolvedDefs, name, input, opts)
536
546
  return result
537
547
  },
538
548
  tick(now?: number) {
539
- return jobRunner ? jobRunner.tick(now) : Promise.resolve()
549
+ return jobRunner ? jobRunner.tick(now) : Promise.resolve({ claimed: 0, ran: 0, failed: 0 })
540
550
  },
541
551
  }
542
552
  if (jobRunner) jobRunner.setJobsFacade(jobs)
@@ -558,54 +568,16 @@ export async function createBunderstack<
558
568
  const handle = startJobWorker({
559
569
  ...options,
560
570
  signal,
561
- tick: (now) => jobRunner.tick(now),
571
+ // The runtime loop only cares that a tick completed; TickResult is for
572
+ // callers that invoke tick() directly.
573
+ tick: async (now) => {
574
+ await jobRunner.tick(now)
575
+ },
562
576
  })
563
577
  const unregister = lifecycle.add(() => handle.close())
564
578
  void handle.closed.finally(unregister)
565
579
  return handle
566
580
  }
567
- const startCronScheduler = async (
568
- options: AppStartCronSchedulerOptions = {},
569
- ): Promise<LocalCronScheduler> => {
570
- if (introspect) {
571
- return { tick: async () => {}, close: async () => {} }
572
- }
573
- const cron = Object.entries(jobsDefs ?? {}).flatMap(
574
- ([name, definition]) =>
575
- definition.kind === 'cron'
576
- ? [{ name, schedule: definition.schedule }]
577
- : [],
578
- )
579
- if (cron.length === 0) {
580
- throw new Error('[bunderstack] no cron tasks configured')
581
- }
582
- if (lifecycle.status !== 'ready') {
583
- throw new Error('[bunderstack] application lifecycle is closed')
584
- }
585
- const scheduler = startLocalCronScheduler({
586
- cron,
587
- onError: options.onError,
588
- runSlot: async (name, slot) => {
589
- await runCronSlot({
590
- db,
591
- defs: jobsDefs!,
592
- ctx: { db: userDb, env, email, storage, realtime },
593
- name,
594
- slot,
595
- now: Date.now(),
596
- })
597
- },
598
- })
599
- const unregister = lifecycle.add(() => scheduler.close())
600
- try {
601
- await scheduler.tick()
602
- } catch (error) {
603
- unregister()
604
- await scheduler.close()
605
- throw error
606
- }
607
- return scheduler
608
- }
609
581
  const runWorker = async (
610
582
  options: AppRunWorkerOptions = {},
611
583
  ): Promise<void> => {
@@ -655,25 +627,52 @@ export async function createBunderstack<
655
627
  }),
656
628
  })
657
629
  : undefined
658
- const cronRouter = env.BUNDERSTACK_CRON_SECRET
659
- ? buildCronRouter({
660
- db,
661
- defs: jobsDefs ?? {},
662
- ctx: { db: userDb, env, email, storage, realtime },
663
- secret: env.BUNDERSTACK_CRON_SECRET,
664
- storage,
665
- })
630
+ const customRouter = options.routes
631
+ ? (() => {
632
+ const routeCtx = createRouteContext({
633
+ db: userDb,
634
+ env,
635
+ storage,
636
+ email,
637
+ jobs,
638
+ realtime,
639
+ auth,
640
+ authResolver,
641
+ })
642
+ const built = (
643
+ options.routes as (ctx: unknown) => import('hono').Hono
644
+ )(routeCtx)
645
+ const enabledTables = Object.values(options.schema)
646
+ .filter((table) => isTable(table))
647
+ .map((table) => getTableName(table))
648
+ .filter((name) => tableEntryForName(resolvedAccess, name)?.enabled)
649
+ validateCustomRoutes(built.routes, enabledTables)
650
+ return built
651
+ })()
666
652
  : undefined
667
653
  const { handler, router } = buildHandler({
654
+ customRouter,
668
655
  crudRouter,
669
656
  authHandler: (req) => auth.handler(req),
670
657
  storageRouter,
671
658
  realtimeRouter,
672
659
  trpcHandler,
673
- cronRouter,
674
660
  rateLimit: options.rateLimit,
675
661
  })
676
662
 
663
+ // Topology is a deployment concern: the role decides whether this process
664
+ // runs background work, so application code never has to.
665
+ const roleWantsWorker =
666
+ env.BUNDERSTACK_ROLE === 'all' || env.BUNDERSTACK_ROLE === 'worker'
667
+ const autoStart =
668
+ options.background?.autoStart ??
669
+ (roleWantsWorker && !introspect && resolvedDefs !== undefined)
670
+ let backgroundRunning = false
671
+ if (autoStart) {
672
+ await startWorker()
673
+ backgroundRunning = true
674
+ }
675
+
677
676
  const app: BunderstackApp<
678
677
  TSchema,
679
678
  TAccess,
@@ -697,8 +696,8 @@ export async function createBunderstack<
697
696
  jobs: jobs as never,
698
697
  startWorker,
699
698
  runWorker,
700
- startCronScheduler,
701
699
  close: () => lifecycle.close(),
700
+ backgroundRunning,
702
701
  get status() {
703
702
  return lifecycle.status
704
703
  },
@@ -712,7 +711,7 @@ export async function createBunderstack<
712
711
  envConfig: options.env as EnvConfigInput | undefined,
713
712
  emailProvider: emailProviderTag(options.email),
714
713
  realtime: Boolean(config.realtime),
715
- jobs: jobsDefs,
714
+ jobs: resolvedDefs,
716
715
  }),
717
716
  }
718
717
 
@@ -763,11 +762,7 @@ export type {
763
762
  } from './email'
764
763
  export { createTRPC } from './trpc'
765
764
  export type { BunderstackTRPC, TRPCContext } from './trpc'
766
- export {
767
- createJobsBuilder,
768
- signScheduleRequest,
769
- verifyScheduleRequest,
770
- } from './jobs/index'
765
+ export { createJobsBuilder } from './jobs/index'
771
766
  export type {
772
767
  BunderstackJobContext,
773
768
  BunderstackJobsBuilder,
@@ -783,8 +778,6 @@ export type {
783
778
  JobsRuntimeFacade,
784
779
  QueueJobDefinition,
785
780
  QueueJobKeys,
786
- LocalCronScheduler,
787
- LocalCronSchedulerOptions,
788
781
  RunWorkerOptions,
789
782
  StartWorkerOptions,
790
783
  WorkerHandle,
@@ -831,3 +824,10 @@ export type {
831
824
  RealtimeTransport,
832
825
  SchemaTable,
833
826
  } from './realtime/facade'
827
+
828
+ export type {
829
+ BunderstackRouteContext,
830
+ RouteContext,
831
+ RoutesBuilder,
832
+ } from './routes'
833
+
@@ -67,20 +67,4 @@ export const bunderstackJobsPg = pgTable(
67
67
  ],
68
68
  )
69
69
 
70
- export const bunderstackCronRunsPg = pgTable(
71
- '_bunderstack_cron_runs',
72
- {
73
- taskId: text('task_id').notNull(),
74
- scheduledAt: bigint('scheduled_at', { mode: 'number' }).notNull(),
75
- status: text('status').notNull(),
76
- attempts: integer('attempts').notNull().default(0),
77
- lockedUntil: bigint('locked_until', { mode: 'number' }),
78
- lastError: text('last_error'),
79
- startedAt: bigint('started_at', { mode: 'number' }),
80
- finishedAt: bigint('finished_at', { mode: 'number' }),
81
- },
82
- (t) => [
83
- primaryKey({ columns: [t.taskId, t.scheduledAt] }),
84
- index('bcr_claim').on(t.status, t.lockedUntil),
85
- ],
86
- )
70
+
@@ -12,7 +12,6 @@ import {
12
12
  import { detectDialect } from './dialect'
13
13
  import {
14
14
  bunderstackFilesPg,
15
- bunderstackCronRunsPg,
16
15
  bunderstackIdempotencyPg,
17
16
  bunderstackJobsPg,
18
17
  } from './internal-tables-pg'
@@ -74,43 +73,22 @@ export const bunderstackJobs = sqliteTable(
74
73
  ],
75
74
  )
76
75
 
77
- export const bunderstackCronRuns = sqliteTable(
78
- '_bunderstack_cron_runs',
79
- {
80
- taskId: text('task_id').notNull(),
81
- scheduledAt: integer('scheduled_at').notNull(),
82
- status: text('status').notNull(),
83
- attempts: integer('attempts').notNull().default(0),
84
- lockedUntil: integer('locked_until'),
85
- lastError: text('last_error'),
86
- startedAt: integer('started_at'),
87
- finishedAt: integer('finished_at'),
88
- },
89
- (t) => [
90
- primaryKey({ columns: [t.taskId, t.scheduledAt] }),
91
- index('bcr_claim').on(t.status, t.lockedUntil),
92
- ],
93
- )
94
-
95
76
  export const INTERNAL_TABLES = {
96
77
  bunderstackFiles,
97
78
  bunderstackIdempotency,
98
79
  bunderstackJobs,
99
- bunderstackCronRuns,
100
80
  } as const
101
81
 
102
82
  export const INTERNAL_TABLE_NAMES: ReadonlySet<string> = new Set([
103
83
  'bunderstack_file_meta',
104
84
  '_bunderstack_idempotency',
105
85
  '_bunderstack_jobs',
106
- '_bunderstack_cron_runs',
107
86
  ])
108
87
 
109
88
  export const INTERNAL_TABLES_PG = {
110
89
  bunderstackFiles: bunderstackFilesPg,
111
90
  bunderstackIdempotency: bunderstackIdempotencyPg,
112
91
  bunderstackJobs: bunderstackJobsPg,
113
- bunderstackCronRuns: bunderstackCronRunsPg,
114
92
  } as const
115
93
 
116
94
  // Both dialect twins count as "ours" for the re-export identity check.
@@ -121,7 +99,6 @@ const INTERNAL_TABLE_CANDIDATES = new Map<string, readonly unknown[]>([
121
99
  [bunderstackIdempotency, bunderstackIdempotencyPg],
122
100
  ],
123
101
  [getTableName(bunderstackJobs), [bunderstackJobs, bunderstackJobsPg]],
124
- [getTableName(bunderstackCronRuns), [bunderstackCronRuns, bunderstackCronRunsPg]],
125
102
  ])
126
103
 
127
104
  /** Internal file-meta table matching the db's dialect. */
@@ -139,11 +116,6 @@ export function jobsTableFor(db: unknown) {
139
116
  return is(db, PgDatabase) ? bunderstackJobsPg : bunderstackJobs
140
117
  }
141
118
 
142
- /** Internal cron-run table matching the db's dialect. */
143
- export function cronRunsTableFor(db: unknown) {
144
- return is(db, PgDatabase) ? bunderstackCronRunsPg : bunderstackCronRuns
145
- }
146
-
147
119
  export function withInternalTables<TSchema extends Record<string, unknown>>(
148
120
  schema: TSchema,
149
121
  ): TSchema & typeof INTERNAL_TABLES {