bunderstack 0.10.0 → 0.12.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 +1 -1
- package/src/access.ts +10 -2
- package/src/auth.ts +18 -6
- package/src/config.ts +7 -1
- package/src/index.ts +26 -1
- package/src/jobs/define.ts +5 -0
- package/src/jobs/index.ts +1 -0
- package/src/testing.ts +31 -0
- package/src/trpc.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bunderstack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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 (
|
|
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(
|
|
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 (
|
|
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
|
+
config.storage.defaultBucket
|
|
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({
|
|
@@ -591,6 +613,7 @@ export async function createBunderstack<
|
|
|
591
613
|
email,
|
|
592
614
|
jobs,
|
|
593
615
|
realtime,
|
|
616
|
+
storage,
|
|
594
617
|
req,
|
|
595
618
|
}),
|
|
596
619
|
})
|
|
@@ -708,6 +731,7 @@ export {
|
|
|
708
731
|
verifyScheduleRequest,
|
|
709
732
|
} from './jobs/index'
|
|
710
733
|
export type {
|
|
734
|
+
BunderstackJobContext,
|
|
711
735
|
BunderstackJobsBuilder,
|
|
712
736
|
BackgroundDefinition,
|
|
713
737
|
BackgroundDefs,
|
|
@@ -760,6 +784,7 @@ export type {
|
|
|
760
784
|
} from './storage/buckets'
|
|
761
785
|
// StorageFacade is declared+exported inline above.
|
|
762
786
|
export type { TransformSpec } from './storage/thumbnails'
|
|
787
|
+
export { mockAuthSession } from './testing'
|
|
763
788
|
|
|
764
789
|
export type { RealtimeAction } from './realtime/index'
|
|
765
790
|
export { createRealtimeFacade } from './realtime/facade'
|
package/src/jobs/define.ts
CHANGED
|
@@ -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
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
|
+
}
|
package/src/trpc.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { DbFor } from './db'
|
|
|
7
7
|
import type { EmailFacade } from './email'
|
|
8
8
|
import type { JobsRuntimeFacade } from './jobs/index'
|
|
9
9
|
import type { RealtimeFacade } from './realtime/facade'
|
|
10
|
+
import type { StorageFacade } from './index'
|
|
10
11
|
|
|
11
12
|
export type TRPCContext<
|
|
12
13
|
TSchema extends Record<string, unknown>,
|
|
@@ -18,6 +19,7 @@ export type TRPCContext<
|
|
|
18
19
|
email: EmailFacade
|
|
19
20
|
jobs: JobsRuntimeFacade
|
|
20
21
|
realtime: RealtimeFacade<TSchema>
|
|
22
|
+
storage: StorageFacade
|
|
21
23
|
req: Request
|
|
22
24
|
}
|
|
23
25
|
|