bunderstack 0.10.0 → 0.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunderstack",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Batteries-included backend framework for Bun: CRUD APIs, auth, file storage, realtime, tRPC, email, and validated env from a single Drizzle schema and config object.",
5
5
  "keywords": [
6
6
  "backend",
package/src/access.ts CHANGED
@@ -311,7 +311,13 @@ export function validateAndResolveAccess<
311
311
  columns.includes('userId')
312
312
 
313
313
  if (!hasExplicitRules && !hasConventionOwner) continue
314
- if (!ownerColumn && input?.crud !== true && !input?.scope?.read && !input?.scope?.write) continue
314
+ if (
315
+ !ownerColumn &&
316
+ input?.crud !== true &&
317
+ !input?.scope?.read &&
318
+ !input?.scope?.write
319
+ )
320
+ continue
315
321
 
316
322
  if (input?.ownerColumn && !columns.includes(input.ownerColumn)) {
317
323
  throw new Error(
@@ -483,7 +489,7 @@ export function sanitizeWriteBody(
483
489
  export type AuthSessionResolver = {
484
490
  api: {
485
491
  getSession: (opts: { headers: Headers }) => Promise<{
486
- user: { id: string; email: string; name?: string } | null
492
+ user: { id: string; email: string; name?: string; role?: string } | null
487
493
  session?: { activeOrganizationId?: string | null } | null
488
494
  } | null>
489
495
  }
@@ -500,6 +506,7 @@ export async function resolveAccessUser(
500
506
  id: session.user.id,
501
507
  email: session.user.email,
502
508
  name: session.user.name,
509
+ role: session.user.role,
503
510
  }
504
511
  }
505
512
 
@@ -515,6 +522,7 @@ export async function resolveSession(
515
522
  id: session.user.id,
516
523
  email: session.user.email,
517
524
  name: session.user.name,
525
+ role: session.user.role,
518
526
  },
519
527
  activeOrganizationId: session.session?.activeOrganizationId ?? null,
520
528
  }
package/src/auth.ts CHANGED
@@ -2,16 +2,22 @@
2
2
  import { betterAuth } from 'better-auth'
3
3
  import { drizzleAdapter } from 'better-auth/adapters/drizzle'
4
4
 
5
- import type { BetterAuthConfig } from './config'
6
5
  import type { AuthSessionResolver } from './access'
6
+ import type { BetterAuthConfig } from './config'
7
7
  import type { AnyDb, Dialect } from './dialect'
8
8
  import type { EmailFacade } from './email'
9
9
 
10
- export function createAuth(db: AnyDb, cfg: BetterAuthConfig, dialect: Dialect) {
10
+ export function createAuth(
11
+ db: AnyDb,
12
+ cfg: BetterAuthConfig,
13
+ dialect: Dialect,
14
+ userSchema?: Record<string, unknown>,
15
+ ) {
11
16
  return betterAuth({
12
17
  ...cfg,
13
18
  database: drizzleAdapter(db as Parameters<typeof drizzleAdapter>[0], {
14
19
  provider: dialect === 'pg' ? 'pg' : 'sqlite',
20
+ ...(userSchema ? { schema: userSchema } : {}),
15
21
  }),
16
22
  })
17
23
  }
@@ -39,15 +45,18 @@ export function toAuthSessionResolver(
39
45
  typeof session.activeOrganizationId === 'string'
40
46
  ? session.activeOrganizationId
41
47
  : null
48
+ const role =
49
+ 'role' in result.user && typeof result.user.role === 'string'
50
+ ? result.user.role
51
+ : undefined
42
52
  return {
43
53
  user: {
44
54
  id: result.user.id,
45
55
  email: result.user.email,
46
56
  name: result.user.name,
57
+ ...(role ? { role } : {}),
47
58
  },
48
- session: session
49
- ? { activeOrganizationId }
50
- : null,
59
+ session: session ? { activeOrganizationId } : null,
51
60
  }
52
61
  }
53
62
  return null
@@ -70,7 +79,10 @@ export function withEmailAuthDefaults(
70
79
  if (!emailConfigured) return cfg
71
80
  const out: BetterAuthConfig = { ...cfg }
72
81
 
73
- if (cfg.emailAndPassword?.enabled && !cfg.emailAndPassword.sendResetPassword) {
82
+ if (
83
+ cfg.emailAndPassword?.enabled &&
84
+ !cfg.emailAndPassword.sendResetPassword
85
+ ) {
74
86
  out.emailAndPassword = {
75
87
  ...cfg.emailAndPassword,
76
88
  sendResetPassword: async ({ user, url }) => {
package/src/config.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  import { betterAuth } from 'better-auth'
3
3
  import { z } from 'zod'
4
4
 
5
- import type { TableAccessInput } from './access'
5
+ import type { AuthSessionResolver, TableAccessInput } from './access'
6
6
  import type { DatabaseAdapter } from './database/adapter'
7
7
  import type { EmailConfigInput } from './email'
8
8
  import type { IdempotencyConfig } from './idempotency'
@@ -86,6 +86,7 @@ export type BunderstackConfig<
86
86
  | 'schema'
87
87
  | 'access'
88
88
  | 'auth'
89
+ | 'authResolver'
89
90
  | 'storage'
90
91
  | 'env'
91
92
  | 'email'
@@ -102,6 +103,11 @@ export type BunderstackConfig<
102
103
  migrations?: string
103
104
  }
104
105
  auth?: BetterAuthConfig
106
+ /**
107
+ * Reuse an application-owned session reader for CRUD, realtime, storage,
108
+ * and tRPC while keeping Bunderstack's auth handler available.
109
+ */
110
+ authResolver?: AuthSessionResolver
105
111
  storage?: TStorage
106
112
  env?: TEnv
107
113
  email?: EmailConfigInput
package/src/index.ts CHANGED
@@ -107,6 +107,14 @@ export interface StorageFacade {
107
107
  * the count reaped.
108
108
  */
109
109
  sweep(olderThanMs?: number): Promise<number>
110
+ /**
111
+ * Get a public or presigned download URL for a file key.
112
+ * Returns a presigned S3 URL when S3 is configured, or local proxy route `/api/files/...` in development.
113
+ */
114
+ getUrl(
115
+ key: string,
116
+ opts?: { bucket?: string; expiresIn?: number },
117
+ ): Promise<string>
110
118
  }
111
119
 
112
120
  export type AppStartWorkerOptions = Omit<StartWorkerOptions, 'tick'>
@@ -362,10 +370,11 @@ export async function createBunderstack<
362
370
  db,
363
371
  withEmailAuthDefaults(config.auth, email, Boolean(options.email)),
364
372
  dialect,
373
+ options.schema as Record<string, unknown>,
365
374
  )
366
375
  // Internal routers consume the narrow AuthSessionResolver contract, not the
367
376
  // raw better-auth instance. app.auth still exposes `auth` unchanged.
368
- const authResolver = toAuthSessionResolver(auth)
377
+ const authResolver = options.authResolver ?? toAuthSessionResolver(auth)
369
378
  const resolvedAccess = validateAndResolveAccess(
370
379
  options.schema,
371
380
  options.access,
@@ -465,6 +474,19 @@ export async function createBunderstack<
465
474
  sweep(olderThanMs = DEFAULT_PENDING_TTL_MS) {
466
475
  return sweepOrphans(registry, db, olderThanMs)
467
476
  },
477
+ async getUrl(key, opts = {}) {
478
+ const bucketName =
479
+ opts.bucket ??
480
+ key.split('/')[0] ??
481
+ registry.defaultBucketName
482
+ const adapter = registry.get(bucketName)?.adapter
483
+ if (adapter?.presignGet) {
484
+ return adapter.presignGet(key, {
485
+ expiresIn: opts.expiresIn ?? 3600,
486
+ })
487
+ }
488
+ return `/api/files/${key}`
489
+ },
468
490
  }
469
491
  const jobRunner = jobsDefs
470
492
  ? createJobRunner({
@@ -708,6 +730,7 @@ export {
708
730
  verifyScheduleRequest,
709
731
  } from './jobs/index'
710
732
  export type {
733
+ BunderstackJobContext,
711
734
  BunderstackJobsBuilder,
712
735
  BackgroundDefinition,
713
736
  BackgroundDefs,
@@ -760,6 +783,7 @@ export type {
760
783
  } from './storage/buckets'
761
784
  // StorageFacade is declared+exported inline above.
762
785
  export type { TransformSpec } from './storage/thumbnails'
786
+ export { mockAuthSession } from './testing'
763
787
 
764
788
  export type { RealtimeAction } from './realtime/index'
765
789
  export { createRealtimeFacade } from './realtime/facade'
@@ -49,6 +49,11 @@ export type JobContext<
49
49
  realtime: RealtimeFacade<TSchema>
50
50
  }
51
51
 
52
+ export type BunderstackJobContext<
53
+ TSchema extends Record<string, unknown> = Record<string, unknown>,
54
+ TEnvResult = Record<string, unknown>,
55
+ > = JobContext<TSchema, TEnvResult>
56
+
52
57
  export type QueueJobDefinition<
53
58
  TInput,
54
59
  TSchema extends Record<string, unknown> = Record<string, unknown>,
package/src/jobs/index.ts CHANGED
@@ -10,6 +10,7 @@ export type {
10
10
  AnyJobDefinition,
11
11
  BackgroundDefinition,
12
12
  BackgroundDefs,
13
+ BunderstackJobContext,
13
14
  BunderstackJobsBuilder,
14
15
  EnqueueOptions,
15
16
  JobContext,
package/src/testing.ts ADDED
@@ -0,0 +1,31 @@
1
+ // src/testing.ts — test utilities for Bunderstack applications.
2
+
3
+ export type AuthSessionResolverLike = {
4
+ api: {
5
+ getSession: (opts: { headers: Headers }) => Promise<unknown>
6
+ }
7
+ }
8
+
9
+ export type BunderstackAppLike = {
10
+ auth: unknown
11
+ }
12
+
13
+ /**
14
+ * Mock the auth session resolver on a Bunderstack app instance for unit testing.
15
+ */
16
+ export function mockAuthSession<
17
+ TUser extends { id: string; email: string; name?: string; role?: string },
18
+ >(
19
+ app: BunderstackAppLike,
20
+ resolver: (opts: { headers: Headers }) => Promise<{
21
+ user: TUser
22
+ session?: { activeOrganizationId?: string | null } | null
23
+ } | null>,
24
+ ): void {
25
+ const auth = app.auth as unknown as AuthSessionResolverLike
26
+ if (auth?.api) {
27
+ auth.api.getSession = resolver as unknown as (opts: {
28
+ headers: Headers
29
+ }) => Promise<unknown>
30
+ }
31
+ }