bunderstack 0.17.0-beta.2 → 0.17.0-beta.4

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 (336) hide show
  1. package/CHANGELOG.md +233 -0
  2. package/README.md +11 -1
  3. package/dist/access.d.ts +116 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +325 -0
  6. package/dist/access.js.map +1 -0
  7. package/dist/api/builder.d.ts +145 -0
  8. package/dist/api/builder.d.ts.map +1 -0
  9. package/dist/api/builder.js +28 -0
  10. package/dist/api/builder.js.map +1 -0
  11. package/dist/api/context.d.ts +34 -0
  12. package/dist/api/context.d.ts.map +1 -0
  13. package/dist/api/context.js +32 -0
  14. package/dist/api/context.js.map +1 -0
  15. package/dist/api/crud-router.d.ts +290 -0
  16. package/dist/api/crud-router.d.ts.map +1 -0
  17. package/dist/api/crud-router.js +249 -0
  18. package/dist/api/crud-router.js.map +1 -0
  19. package/dist/api/openapi.d.ts +6 -0
  20. package/dist/api/openapi.d.ts.map +1 -0
  21. package/dist/api/openapi.js +146 -0
  22. package/dist/api/openapi.js.map +1 -0
  23. package/dist/api/realtime-router.d.ts +76 -0
  24. package/dist/api/realtime-router.d.ts.map +1 -0
  25. package/dist/api/realtime-router.js +41 -0
  26. package/dist/api/realtime-router.js.map +1 -0
  27. package/dist/api/registry.d.ts +30 -0
  28. package/dist/api/registry.d.ts.map +1 -0
  29. package/dist/api/registry.js +239 -0
  30. package/dist/api/registry.js.map +1 -0
  31. package/dist/api/router.d.ts +9 -0
  32. package/dist/api/router.d.ts.map +1 -0
  33. package/dist/api/router.js +19 -0
  34. package/dist/api/router.js.map +1 -0
  35. package/dist/api/storage-router.d.ts +279 -0
  36. package/dist/api/storage-router.d.ts.map +1 -0
  37. package/dist/api/storage-router.js +155 -0
  38. package/dist/api/storage-router.js.map +1 -0
  39. package/dist/api/types.d.ts +55 -0
  40. package/dist/api/types.d.ts.map +1 -0
  41. package/dist/api/types.js +2 -0
  42. package/dist/api/types.js.map +1 -0
  43. package/dist/auth.d.ts +31 -0
  44. package/dist/auth.d.ts.map +1 -0
  45. package/dist/auth.js +102 -0
  46. package/dist/auth.js.map +1 -0
  47. package/dist/blueprint-generator.d.ts +18 -0
  48. package/dist/blueprint-generator.d.ts.map +1 -0
  49. package/dist/blueprint-generator.js +125 -0
  50. package/dist/blueprint-generator.js.map +1 -0
  51. package/dist/blueprint.d.ts +46 -0
  52. package/dist/blueprint.d.ts.map +1 -0
  53. package/dist/blueprint.js +162 -0
  54. package/dist/blueprint.js.map +1 -0
  55. package/dist/cli.d.ts +8 -0
  56. package/dist/cli.d.ts.map +1 -0
  57. package/dist/cli.js +68 -0
  58. package/dist/cli.js.map +1 -0
  59. package/dist/config.d.ts +108 -0
  60. package/dist/config.d.ts.map +1 -0
  61. package/dist/config.js +90 -0
  62. package/dist/config.js.map +1 -0
  63. package/dist/cron.d.ts +4 -0
  64. package/dist/cron.d.ts.map +1 -0
  65. package/dist/cron.js +3 -0
  66. package/dist/cron.js.map +1 -0
  67. package/dist/crud-operations.d.ts +43 -0
  68. package/dist/crud-operations.d.ts.map +1 -0
  69. package/dist/crud-operations.js +238 -0
  70. package/dist/crud-operations.js.map +1 -0
  71. package/dist/database/adapter.d.ts +20 -0
  72. package/dist/database/adapter.d.ts.map +1 -0
  73. package/dist/database/adapter.js +2 -0
  74. package/dist/database/adapter.js.map +1 -0
  75. package/dist/database/bun-sql.d.ts +3 -0
  76. package/dist/database/bun-sql.d.ts.map +1 -0
  77. package/dist/database/bun-sql.js +21 -0
  78. package/dist/database/bun-sql.js.map +1 -0
  79. package/dist/database/libsql.d.ts +3 -0
  80. package/dist/database/libsql.d.ts.map +1 -0
  81. package/dist/database/libsql.js +22 -0
  82. package/dist/database/libsql.js.map +1 -0
  83. package/dist/database/pglite.d.ts +3 -0
  84. package/dist/database/pglite.d.ts.map +1 -0
  85. package/dist/database/pglite.js +27 -0
  86. package/dist/database/pglite.js.map +1 -0
  87. package/dist/database/postgres-js.d.ts +3 -0
  88. package/dist/database/postgres-js.d.ts.map +1 -0
  89. package/dist/database/postgres-js.js +21 -0
  90. package/dist/database/postgres-js.js.map +1 -0
  91. package/dist/db.d.ts +18 -0
  92. package/dist/db.d.ts.map +1 -0
  93. package/dist/db.js +26 -0
  94. package/dist/db.js.map +1 -0
  95. package/dist/dialect.d.ts +17 -0
  96. package/dist/dialect.d.ts.map +1 -0
  97. package/dist/dialect.js +22 -0
  98. package/dist/dialect.js.map +1 -0
  99. package/dist/email/smtp.d.ts +14 -0
  100. package/dist/email/smtp.d.ts.map +1 -0
  101. package/dist/email/smtp.js +27 -0
  102. package/dist/email/smtp.js.map +1 -0
  103. package/dist/email.d.ts +39 -0
  104. package/dist/email.d.ts.map +1 -0
  105. package/dist/email.js +97 -0
  106. package/dist/email.js.map +1 -0
  107. package/dist/env.d.ts +47 -0
  108. package/dist/env.d.ts.map +1 -0
  109. package/dist/env.js +99 -0
  110. package/dist/env.js.map +1 -0
  111. package/dist/errors.d.ts +125 -0
  112. package/dist/errors.d.ts.map +1 -0
  113. package/dist/errors.js +92 -0
  114. package/dist/errors.js.map +1 -0
  115. package/dist/handler.d.ts +9 -0
  116. package/dist/handler.d.ts.map +1 -0
  117. package/dist/handler.js +22 -0
  118. package/dist/handler.js.map +1 -0
  119. package/dist/idempotency.d.ts +17 -0
  120. package/dist/idempotency.d.ts.map +1 -0
  121. package/dist/idempotency.js +65 -0
  122. package/dist/idempotency.js.map +1 -0
  123. package/dist/index.d.ts +134 -0
  124. package/dist/index.d.ts.map +1 -0
  125. package/dist/index.js +480 -0
  126. package/dist/index.js.map +1 -0
  127. package/dist/internal-tables-pg.d.ts +481 -0
  128. package/dist/internal-tables-pg.d.ts.map +1 -0
  129. package/{src/internal-tables-pg.ts → dist/internal-tables-pg.js} +10 -34
  130. package/dist/internal-tables-pg.js.map +1 -0
  131. package/dist/internal-tables.d.ts +2509 -0
  132. package/dist/internal-tables.d.ts.map +1 -0
  133. package/dist/internal-tables.js +108 -0
  134. package/dist/internal-tables.js.map +1 -0
  135. package/dist/jobs/cron.d.ts +15 -0
  136. package/dist/jobs/cron.d.ts.map +1 -0
  137. package/dist/jobs/cron.js +74 -0
  138. package/dist/jobs/cron.js.map +1 -0
  139. package/dist/jobs/define.d.ts +133 -0
  140. package/dist/jobs/define.d.ts.map +1 -0
  141. package/dist/jobs/define.js +74 -0
  142. package/dist/jobs/define.js.map +1 -0
  143. package/dist/jobs/index.d.ts +11 -0
  144. package/dist/jobs/index.d.ts.map +1 -0
  145. package/dist/jobs/index.js +8 -0
  146. package/dist/jobs/index.js.map +1 -0
  147. package/dist/jobs/queue.d.ts +6 -0
  148. package/dist/jobs/queue.d.ts.map +1 -0
  149. package/dist/jobs/queue.js +55 -0
  150. package/dist/jobs/queue.js.map +1 -0
  151. package/dist/jobs/runtime.d.ts +13 -0
  152. package/dist/jobs/runtime.d.ts.map +1 -0
  153. package/dist/jobs/runtime.js +46 -0
  154. package/dist/jobs/runtime.js.map +1 -0
  155. package/dist/jobs/slots.d.ts +25 -0
  156. package/dist/jobs/slots.d.ts.map +1 -0
  157. package/dist/jobs/slots.js +41 -0
  158. package/dist/jobs/slots.js.map +1 -0
  159. package/dist/jobs/worker.d.ts +12 -0
  160. package/dist/jobs/worker.d.ts.map +1 -0
  161. package/dist/jobs/worker.js +311 -0
  162. package/dist/jobs/worker.js.map +1 -0
  163. package/dist/lifecycle.d.ts +10 -0
  164. package/dist/lifecycle.d.ts.map +1 -0
  165. package/dist/lifecycle.js +37 -0
  166. package/dist/lifecycle.js.map +1 -0
  167. package/dist/list-query.d.ts +62 -0
  168. package/dist/list-query.d.ts.map +1 -0
  169. package/dist/list-query.js +194 -0
  170. package/dist/list-query.js.map +1 -0
  171. package/dist/manifest.d.ts +59 -0
  172. package/dist/manifest.d.ts.map +1 -0
  173. package/dist/manifest.js +167 -0
  174. package/dist/manifest.js.map +1 -0
  175. package/dist/provision-internals.d.ts +24 -0
  176. package/dist/provision-internals.d.ts.map +1 -0
  177. package/dist/provision-internals.js +7 -0
  178. package/dist/provision-internals.js.map +1 -0
  179. package/dist/provision.d.ts +23 -0
  180. package/dist/provision.d.ts.map +1 -0
  181. package/dist/provision.js +96 -0
  182. package/dist/provision.js.map +1 -0
  183. package/dist/rate-limit.d.ts +7 -0
  184. package/dist/rate-limit.d.ts.map +1 -0
  185. package/dist/rate-limit.js +49 -0
  186. package/dist/rate-limit.js.map +1 -0
  187. package/dist/realtime/facade.d.ts +18 -0
  188. package/dist/realtime/facade.d.ts.map +1 -0
  189. package/dist/realtime/facade.js +36 -0
  190. package/dist/realtime/facade.js.map +1 -0
  191. package/dist/realtime/filter.d.ts +13 -0
  192. package/dist/realtime/filter.d.ts.map +1 -0
  193. package/dist/realtime/filter.js +48 -0
  194. package/dist/realtime/filter.js.map +1 -0
  195. package/dist/realtime/heartbeat.d.ts +14 -0
  196. package/dist/realtime/heartbeat.d.ts.map +1 -0
  197. package/dist/realtime/heartbeat.js +64 -0
  198. package/dist/realtime/heartbeat.js.map +1 -0
  199. package/dist/realtime/publisher.d.ts +21 -0
  200. package/dist/realtime/publisher.d.ts.map +1 -0
  201. package/dist/realtime/publisher.js +17 -0
  202. package/dist/realtime/publisher.js.map +1 -0
  203. package/dist/schema-export-pg.d.ts +2 -0
  204. package/dist/schema-export-pg.d.ts.map +1 -0
  205. package/dist/schema-export-pg.js +4 -0
  206. package/dist/schema-export-pg.js.map +1 -0
  207. package/dist/schema-export.d.ts +2 -0
  208. package/dist/schema-export.d.ts.map +1 -0
  209. package/dist/schema-export.js +2 -0
  210. package/dist/schema-export.js.map +1 -0
  211. package/dist/scope.d.ts +4 -0
  212. package/dist/scope.d.ts.map +1 -0
  213. package/dist/scope.js +13 -0
  214. package/dist/scope.js.map +1 -0
  215. package/dist/standard-schema.d.ts +13 -0
  216. package/dist/standard-schema.d.ts.map +1 -0
  217. package/dist/standard-schema.js +34 -0
  218. package/dist/standard-schema.js.map +1 -0
  219. package/dist/storage/buckets.d.ts +80 -0
  220. package/dist/storage/buckets.d.ts.map +1 -0
  221. package/dist/storage/buckets.js +178 -0
  222. package/dist/storage/buckets.js.map +1 -0
  223. package/dist/storage/delete.d.ts +11 -0
  224. package/dist/storage/delete.d.ts.map +1 -0
  225. package/dist/storage/delete.js +18 -0
  226. package/dist/storage/delete.js.map +1 -0
  227. package/dist/storage/file-meta.d.ts +37 -0
  228. package/dist/storage/file-meta.d.ts.map +1 -0
  229. package/dist/storage/file-meta.js +166 -0
  230. package/dist/storage/file-meta.js.map +1 -0
  231. package/dist/storage/index.d.ts +28 -0
  232. package/dist/storage/index.d.ts.map +1 -0
  233. package/dist/storage/index.js +5 -0
  234. package/dist/storage/index.js.map +1 -0
  235. package/dist/storage/local.d.ts +15 -0
  236. package/dist/storage/local.d.ts.map +1 -0
  237. package/dist/storage/local.js +56 -0
  238. package/dist/storage/local.js.map +1 -0
  239. package/dist/storage/operations.d.ts +54 -0
  240. package/dist/storage/operations.d.ts.map +1 -0
  241. package/dist/storage/operations.js +250 -0
  242. package/dist/storage/operations.js.map +1 -0
  243. package/dist/storage/registry.d.ts +10 -0
  244. package/dist/storage/registry.d.ts.map +1 -0
  245. package/dist/storage/registry.js +23 -0
  246. package/dist/storage/registry.js.map +1 -0
  247. package/dist/storage/s3.d.ts +28 -0
  248. package/dist/storage/s3.d.ts.map +1 -0
  249. package/dist/storage/s3.js +71 -0
  250. package/dist/storage/s3.js.map +1 -0
  251. package/dist/storage/sweep.d.ts +9 -0
  252. package/dist/storage/sweep.d.ts.map +1 -0
  253. package/dist/storage/sweep.js +17 -0
  254. package/dist/storage/sweep.js.map +1 -0
  255. package/dist/storage/thumbnails.d.ts +11 -0
  256. package/dist/storage/thumbnails.d.ts.map +1 -0
  257. package/dist/storage/thumbnails.js +60 -0
  258. package/dist/storage/thumbnails.js.map +1 -0
  259. package/dist/testing.d.ts +27 -0
  260. package/dist/testing.d.ts.map +1 -0
  261. package/dist/testing.js +11 -0
  262. package/dist/testing.js.map +1 -0
  263. package/dist/typeid-pg.d.ts +19 -0
  264. package/dist/typeid-pg.d.ts.map +1 -0
  265. package/{src/typeid-pg.ts → dist/typeid-pg.js} +10 -13
  266. package/dist/typeid-pg.js.map +1 -0
  267. package/dist/typeid.d.ts +45 -0
  268. package/dist/typeid.d.ts.map +1 -0
  269. package/dist/typeid.js +100 -0
  270. package/dist/typeid.js.map +1 -0
  271. package/package.json +77 -28
  272. package/src/access.ts +0 -532
  273. package/src/api/builder.ts +0 -52
  274. package/src/api/context.ts +0 -83
  275. package/src/api/crud-router.ts +0 -386
  276. package/src/api/openapi.ts +0 -184
  277. package/src/api/realtime-router.ts +0 -75
  278. package/src/api/registry.ts +0 -338
  279. package/src/api/router.ts +0 -34
  280. package/src/api/storage-router.ts +0 -224
  281. package/src/api/types.ts +0 -105
  282. package/src/auth.ts +0 -117
  283. package/src/blueprint-generator.ts +0 -203
  284. package/src/blueprint.ts +0 -264
  285. package/src/cli.ts +0 -90
  286. package/src/config.ts +0 -215
  287. package/src/cron.ts +0 -3
  288. package/src/crud-operations.ts +0 -478
  289. package/src/database/adapter.ts +0 -26
  290. package/src/database/bun-sql.ts +0 -23
  291. package/src/database/libsql.ts +0 -28
  292. package/src/database/pglite.ts +0 -33
  293. package/src/database/postgres-js.ts +0 -33
  294. package/src/db.ts +0 -61
  295. package/src/dialect.ts +0 -38
  296. package/src/email/smtp.ts +0 -45
  297. package/src/email.ts +0 -154
  298. package/src/env.ts +0 -180
  299. package/src/errors.ts +0 -123
  300. package/src/handler.ts +0 -30
  301. package/src/idempotency.ts +0 -97
  302. package/src/index.ts +0 -834
  303. package/src/internal-tables.ts +0 -149
  304. package/src/jobs/cron.ts +0 -87
  305. package/src/jobs/define.ts +0 -283
  306. package/src/jobs/index.ts +0 -37
  307. package/src/jobs/queue.ts +0 -67
  308. package/src/jobs/runtime.ts +0 -58
  309. package/src/jobs/slots.ts +0 -52
  310. package/src/jobs/worker.ts +0 -369
  311. package/src/lifecycle.ts +0 -49
  312. package/src/list-query.ts +0 -333
  313. package/src/manifest.ts +0 -297
  314. package/src/provision-internals.ts +0 -29
  315. package/src/provision.ts +0 -128
  316. package/src/rate-limit.ts +0 -71
  317. package/src/realtime/facade.ts +0 -74
  318. package/src/realtime/filter.ts +0 -81
  319. package/src/realtime/heartbeat.ts +0 -80
  320. package/src/realtime/publisher.ts +0 -46
  321. package/src/schema-export-pg.ts +0 -7
  322. package/src/schema-export.ts +0 -5
  323. package/src/scope.ts +0 -17
  324. package/src/standard-schema.ts +0 -59
  325. package/src/storage/buckets.ts +0 -308
  326. package/src/storage/delete.ts +0 -26
  327. package/src/storage/file-meta.ts +0 -222
  328. package/src/storage/index.ts +0 -39
  329. package/src/storage/local.ts +0 -69
  330. package/src/storage/operations.ts +0 -398
  331. package/src/storage/registry.ts +0 -40
  332. package/src/storage/s3.ts +0 -99
  333. package/src/storage/sweep.ts +0 -25
  334. package/src/storage/thumbnails.ts +0 -65
  335. package/src/testing.ts +0 -31
  336. package/src/typeid.ts +0 -116
package/src/access.ts DELETED
@@ -1,532 +0,0 @@
1
- import { getTableColumns, getTableName, isTable } from 'drizzle-orm'
2
-
3
- import { INTERNAL_TABLE_NAMES } from './internal-tables'
4
-
5
- export const AUTH_TABLE_NAMES = new Set([
6
- 'user',
7
- 'session',
8
- 'account',
9
- 'verification',
10
- ])
11
-
12
- const EXPOSEABLE_AUTH_TABLES = new Set(['user'])
13
-
14
- export type AccessUser = {
15
- id: string
16
- email: string
17
- name?: string
18
- role?: string
19
- }
20
-
21
- export type AccessContext = {
22
- user: AccessUser | null
23
- request: Request
24
- row?: Record<string, unknown>
25
- body?: Record<string, unknown>
26
- session?: { activeOrganizationId: string | null } | null
27
- }
28
-
29
- export type OperationRule =
30
- | 'public'
31
- | 'authenticated'
32
- | 'owner'
33
- | 'deny'
34
- | ((ctx: AccessContext) => boolean | Promise<boolean>)
35
-
36
- export type ScopeMap = Record<string, string | string[]>
37
- export type ScopeResolver = (ctx: AccessContext) => ScopeMap
38
-
39
- export type CrudOperation = 'list' | 'get' | 'create' | 'update' | 'delete'
40
-
41
- export type SortOrder = 'asc' | 'desc'
42
-
43
- export type DefaultSort = {
44
- column: string
45
- order: SortOrder
46
- }
47
-
48
- export type TableAccessInput = {
49
- crud?: boolean
50
- /** Opt the BetterAuth `user` table into auto-CRUD (read profiles, owner-update image). */
51
- exposeAuthTable?: boolean
52
- ownerColumn?: string
53
- list?: OperationRule
54
- get?: OperationRule
55
- create?: OperationRule
56
- update?: OperationRule
57
- delete?: OperationRule
58
- /** Explicit write allowlist. Entries override matching system-readonly
59
- * defaults such as `updatedAt`; `id` remains immutable on update. */
60
- writableColumns?: string[]
61
- readonlyColumns?: string[]
62
- /** Columns matched by `?q=` on list — opt-in; omitted columns are never searched. */
63
- searchableColumns?: string[]
64
- /** Columns filterable via flat `?column=value` query params. */
65
- filterableColumns?: string[]
66
- /** Columns allowed in `?sort=`. Defaults to `['id']`. */
67
- sortableColumns?: string[]
68
- /** Default list ordering when `?sort` is omitted. Defaults to `{ column: 'id', order: 'desc' }`. */
69
- defaultSort?: DefaultSort
70
- scope?: {
71
- read?: ScopeResolver
72
- write?: ScopeResolver
73
- }
74
- }
75
-
76
- export type ResolvedTableAccess = {
77
- tableKey: string
78
- tableName: string
79
- enabled: boolean
80
- ownerColumn?: string
81
- list: OperationRule
82
- get: OperationRule
83
- create: OperationRule
84
- update: OperationRule
85
- delete: OperationRule
86
- writableColumns?: string[]
87
- readonlyColumns: string[]
88
- searchableColumns?: string[]
89
- filterableColumns: string[]
90
- sortableColumns: string[]
91
- defaultSort: DefaultSort
92
- readScope?: ScopeResolver
93
- writeScope?: ScopeResolver
94
- }
95
-
96
- export type ResolvedAccess = Map<string, ResolvedTableAccess>
97
-
98
- /**
99
- * Look up a table's resolved access by its physical table name.
100
- *
101
- * `ResolvedAccess` is keyed by schema export name, not table name, so every
102
- * consumer that starts from a physical name needs this scan. It lives here so
103
- * CRUD, realtime, and route validation cannot drift apart on which tables they
104
- * consider enabled.
105
- */
106
- export function tableEntryForName(
107
- access: ResolvedAccess,
108
- tableName: string,
109
- ): ResolvedTableAccess | undefined {
110
- for (const entry of access.values()) {
111
- if (entry.tableName === tableName) return entry
112
- }
113
- return undefined
114
- }
115
-
116
- const DEFAULT_READONLY = [
117
- 'id',
118
- 'createdAt',
119
- 'updatedAt',
120
- 'created_at',
121
- 'updated_at',
122
- ]
123
-
124
- function getSchemaTables<TSchema extends Record<string, unknown>>(
125
- schema: TSchema,
126
- ) {
127
- const tables: {
128
- key: string
129
- table: Parameters<typeof getTableName>[0]
130
- name: string
131
- columns: string[]
132
- }[] = []
133
- for (const [key, value] of Object.entries(schema)) {
134
- if (!isTable(value)) continue
135
- const name = getTableName(value)
136
- const columns = Object.keys(getTableColumns(value))
137
- tables.push({ key, table: value, name, columns })
138
- }
139
- return tables
140
- }
141
-
142
- function resolveListAccess(
143
- input: TableAccessInput,
144
- columns: string[],
145
- ): Pick<
146
- ResolvedTableAccess,
147
- 'filterableColumns' | 'sortableColumns' | 'defaultSort'
148
- > {
149
- const sortableColumns =
150
- input.sortableColumns ?? (columns.includes('id') ? ['id'] : [])
151
- const defaultSort: DefaultSort = input.defaultSort ?? {
152
- column: sortableColumns[0] ?? 'id',
153
- order: 'desc',
154
- }
155
-
156
- if (!columns.includes(defaultSort.column)) {
157
- throw new Error(
158
- `[bunderstack] defaultSort.column "${defaultSort.column}" is not a column on this table`,
159
- )
160
- }
161
- if (!sortableColumns.includes(defaultSort.column)) {
162
- throw new Error(
163
- `[bunderstack] defaultSort.column "${defaultSort.column}" must be listed in sortableColumns`,
164
- )
165
- }
166
-
167
- const filterableColumns = input.filterableColumns ?? []
168
- for (const col of filterableColumns) {
169
- if (!columns.includes(col)) {
170
- throw new Error(
171
- `[bunderstack] filterableColumns references unknown column "${col}"`,
172
- )
173
- }
174
- }
175
-
176
- for (const col of sortableColumns) {
177
- if (!columns.includes(col)) {
178
- throw new Error(
179
- `[bunderstack] sortableColumns references unknown column "${col}"`,
180
- )
181
- }
182
- }
183
-
184
- return { filterableColumns, sortableColumns, defaultSort }
185
- }
186
-
187
- function resolveDefaults(
188
- input: TableAccessInput,
189
- ownerColumn: string | undefined,
190
- columns: string[],
191
- ): Omit<ResolvedTableAccess, 'tableKey' | 'tableName' | 'enabled'> {
192
- const listAccess = resolveListAccess(input, columns)
193
- const explicitlyWritable = new Set(input.writableColumns ?? [])
194
- return {
195
- ownerColumn,
196
- list: input.list ?? 'public',
197
- get: input.get ?? 'public',
198
- create: input.create ?? 'public',
199
- update: input.update ?? (ownerColumn ? 'owner' : 'deny'),
200
- delete: input.delete ?? (ownerColumn ? 'owner' : 'deny'),
201
- writableColumns: input.writableColumns,
202
- readonlyColumns: [
203
- ...DEFAULT_READONLY.filter(
204
- (column) => column === 'id' || !explicitlyWritable.has(column),
205
- ),
206
- ...(input.readonlyColumns ?? []),
207
- ...(ownerColumn ? [ownerColumn] : []),
208
- ],
209
- searchableColumns: input.searchableColumns,
210
- readScope: input.scope?.read,
211
- writeScope: input.scope?.write,
212
- ...listAccess,
213
- }
214
- }
215
-
216
- function detectOwnerColumn(
217
- columns: string[],
218
- input?: TableAccessInput,
219
- ): string | undefined {
220
- if (input?.ownerColumn) return input.ownerColumn
221
- if (columns.includes('userId')) return 'userId'
222
- return undefined
223
- }
224
-
225
- export function validateAndResolveAccess<
226
- TSchema extends Record<string, unknown>,
227
- >(
228
- schema: TSchema,
229
- accessInput?: Record<string, TableAccessInput>,
230
- ): ResolvedAccess {
231
- const tables = getSchemaTables(schema)
232
- const tableByKey = new Map(tables.map((t) => [t.key, t]))
233
- const resolved: ResolvedAccess = new Map()
234
-
235
- if (accessInput) {
236
- for (const key of Object.keys(accessInput)) {
237
- if (!tableByKey.has(key)) {
238
- throw new Error(
239
- `[bunderstack] access.${key} does not match any table in schema`,
240
- )
241
- }
242
- const tableName = tableByKey.get(key)!.name
243
- const input = accessInput[key]
244
- if (AUTH_TABLE_NAMES.has(tableName)) {
245
- if (input?.crud === false) continue
246
- if (!EXPOSEABLE_AUTH_TABLES.has(tableName) || !input?.exposeAuthTable) {
247
- throw new Error(
248
- `[bunderstack] access.${key} cannot target auth table "${tableName}" — use { crud: false } to silence, or exposeAuthTable on user`,
249
- )
250
- }
251
- }
252
- }
253
- }
254
-
255
- for (const { key, name, columns } of tables) {
256
- const input = accessInput?.[key]
257
- if (input?.crud === false) continue
258
- if (INTERNAL_TABLE_NAMES.has(name)) continue
259
-
260
- if (AUTH_TABLE_NAMES.has(name)) {
261
- if (!input?.exposeAuthTable || !EXPOSEABLE_AUTH_TABLES.has(name)) continue
262
-
263
- const ownerColumn = input.ownerColumn ?? 'id'
264
- if (!columns.includes(ownerColumn)) {
265
- throw new Error(
266
- `[bunderstack] access.${key}.ownerColumn "${ownerColumn}" is not a column on table "${name}"`,
267
- )
268
- }
269
-
270
- const defaults = resolveDefaults(
271
- {
272
- ...input,
273
- create: input.create ?? 'deny',
274
- delete: input.delete ?? 'deny',
275
- },
276
- ownerColumn,
277
- columns,
278
- )
279
-
280
- const existingReadonly = defaults.readonlyColumns.filter((col) =>
281
- columns.includes(col),
282
- )
283
- const resolvedReadonly = [...new Set(existingReadonly)]
284
-
285
- for (const col of [
286
- ...(defaults.writableColumns ?? []),
287
- ...(defaults.searchableColumns ?? []),
288
- ...defaults.filterableColumns,
289
- ...defaults.sortableColumns,
290
- ...resolvedReadonly,
291
- ]) {
292
- if (!columns.includes(col)) {
293
- throw new Error(
294
- `[bunderstack] access.${key} references unknown column "${col}" on table "${name}"`,
295
- )
296
- }
297
- }
298
-
299
- resolved.set(key, {
300
- tableKey: key,
301
- tableName: name,
302
- enabled: true,
303
- ...defaults,
304
- readonlyColumns: resolvedReadonly,
305
- })
306
- continue
307
- }
308
-
309
- const ownerColumn = detectOwnerColumn(columns, input)
310
- const hasExplicitRules = input !== undefined
311
- const hasConventionOwner =
312
- ownerColumn !== undefined &&
313
- input?.ownerColumn === undefined &&
314
- columns.includes('userId')
315
-
316
- if (!hasExplicitRules && !hasConventionOwner) continue
317
- if (
318
- !ownerColumn &&
319
- input?.crud !== true &&
320
- !input?.scope?.read &&
321
- !input?.scope?.write
322
- )
323
- continue
324
-
325
- if (input?.ownerColumn && !columns.includes(input.ownerColumn)) {
326
- throw new Error(
327
- `[bunderstack] access.${key}.ownerColumn "${input.ownerColumn}" is not a column on table "${name}"`,
328
- )
329
- }
330
-
331
- const defaults = resolveDefaults(input ?? {}, ownerColumn, columns)
332
- const existingReadonly = defaults.readonlyColumns.filter((col) =>
333
- columns.includes(col),
334
- )
335
- const resolvedReadonly = [...new Set(existingReadonly)]
336
-
337
- for (const col of [
338
- ...(defaults.writableColumns ?? []),
339
- ...(defaults.searchableColumns ?? []),
340
- ...defaults.filterableColumns,
341
- ...defaults.sortableColumns,
342
- ...resolvedReadonly,
343
- ]) {
344
- if (!columns.includes(col)) {
345
- throw new Error(
346
- `[bunderstack] access.${key} references unknown column "${col}" on table "${name}"`,
347
- )
348
- }
349
- }
350
-
351
- const needsOwner = [defaults.update, defaults.delete].some(
352
- (r) => r === 'owner',
353
- )
354
- if (needsOwner && !ownerColumn) {
355
- throw new Error(
356
- `[bunderstack] access.${key} requires ownerColumn for owner-based update/delete rules`,
357
- )
358
- }
359
-
360
- resolved.set(key, {
361
- tableKey: key,
362
- tableName: name,
363
- enabled: true,
364
- ...defaults,
365
- readonlyColumns: resolvedReadonly,
366
- })
367
- }
368
-
369
- return resolved
370
- }
371
-
372
- export function defineAccess<
373
- TSchema extends Record<string, unknown>,
374
- const TRules extends Record<string, TableAccessInput>,
375
- >(schema: TSchema, rules: TRules): TRules {
376
- validateAndResolveAccess(schema, rules)
377
- return rules
378
- }
379
-
380
- export function rowMatchesScope(
381
- row: Record<string, unknown>,
382
- scope: ScopeMap,
383
- ): boolean {
384
- for (const [col, expected] of Object.entries(scope)) {
385
- const actual = row[col]
386
- if (actual == null) return false
387
- if (Array.isArray(expected)) {
388
- if (!expected.map(String).includes(String(actual))) return false
389
- } else if (String(actual) !== String(expected)) {
390
- return false
391
- }
392
- }
393
- return true
394
- }
395
-
396
- export function stampScope(
397
- values: Record<string, unknown>,
398
- scope: ScopeMap,
399
- ): Record<string, unknown> {
400
- const out = { ...values }
401
- for (const [col, expected] of Object.entries(scope)) {
402
- if (!Array.isArray(expected)) out[col] = expected
403
- }
404
- return out
405
- }
406
-
407
- export function checkAccessSync(
408
- rule: Exclude<
409
- OperationRule,
410
- (ctx: AccessContext) => boolean | Promise<boolean>
411
- >,
412
- ctx: AccessContext,
413
- ownerColumn?: string,
414
- ): { allowed: boolean } {
415
- if (rule === 'deny') return { allowed: false }
416
- if (rule === 'public') return { allowed: true }
417
- if (!ctx.user) return { allowed: false }
418
- if (rule === 'authenticated') return { allowed: true }
419
- if (rule === 'owner') {
420
- if (!ownerColumn) return { allowed: false }
421
- const owner = ctx.row?.[ownerColumn]
422
- return { allowed: owner != null && String(owner) === ctx.user.id }
423
- }
424
- return { allowed: false }
425
- }
426
-
427
- export async function checkAccess(
428
- rule: OperationRule,
429
- ctx: AccessContext,
430
- ownerColumn?: string,
431
- ): Promise<{ allowed: boolean; status: 401 | 403 }> {
432
- if (rule === 'deny') return { allowed: false, status: 403 }
433
-
434
- if (typeof rule === 'function') {
435
- return (await rule(ctx))
436
- ? { allowed: true, status: 403 }
437
- : { allowed: false, status: 403 }
438
- }
439
-
440
- if (rule === 'public') return { allowed: true, status: 403 }
441
-
442
- if (!ctx.user) return { allowed: false, status: 401 }
443
-
444
- if (rule === 'authenticated') return { allowed: true, status: 403 }
445
-
446
- if (rule === 'owner') {
447
- if (!ownerColumn) return { allowed: false, status: 403 }
448
- const rowOwner = ctx.row?.[ownerColumn] ?? ctx.body?.[ownerColumn]
449
- if (rowOwner == null) return { allowed: false, status: 403 }
450
- if (String(rowOwner) !== ctx.user.id) return { allowed: false, status: 403 }
451
- return { allowed: true, status: 403 }
452
- }
453
-
454
- return { allowed: false, status: 403 }
455
- }
456
-
457
- export function sanitizeWriteBody(
458
- body: Record<string, unknown>,
459
- access: ResolvedTableAccess,
460
- mode: 'create' | 'update',
461
- userId: string | null,
462
- ): Record<string, unknown> {
463
- const out: Record<string, unknown> = {}
464
- const readonly = new Set(access.readonlyColumns)
465
-
466
- for (const [key, value] of Object.entries(body)) {
467
- // On create, allow client-supplied `id` even if it appears in readonlyColumns,
468
- // but still respect an explicit writableColumns allowlist if provided.
469
- if (mode === 'create' && key === 'id') {
470
- if (access.writableColumns && !access.writableColumns.includes(key))
471
- continue
472
- out[key] = value
473
- continue
474
- }
475
- if (readonly.has(key)) continue
476
- if (access.writableColumns && !access.writableColumns.includes(key))
477
- continue
478
- out[key] = value
479
- }
480
-
481
- if (mode === 'create' && access.ownerColumn) {
482
- out[access.ownerColumn] = userId ?? null
483
- }
484
-
485
- if (mode === 'update' && access.ownerColumn) {
486
- delete out[access.ownerColumn]
487
- }
488
-
489
- return out
490
- }
491
-
492
- export type AuthSessionResolver = {
493
- api: {
494
- getSession: (opts: { headers: Headers }) => Promise<{
495
- user: { id: string; email: string; name?: string; role?: string } | null
496
- session?: { activeOrganizationId?: string | null } | null
497
- } | null>
498
- }
499
- }
500
-
501
- export async function resolveAccessUser(
502
- auth: AuthSessionResolver | undefined,
503
- headers: Headers,
504
- ): Promise<AccessUser | null> {
505
- if (!auth) return null
506
- const session = await auth.api.getSession({ headers })
507
- if (!session?.user) return null
508
- return {
509
- id: session.user.id,
510
- email: session.user.email,
511
- name: session.user.name,
512
- role: session.user.role,
513
- }
514
- }
515
-
516
- export async function resolveSession(
517
- auth: AuthSessionResolver | undefined,
518
- headers: Headers,
519
- ): Promise<{ user: AccessUser | null; activeOrganizationId: string | null }> {
520
- if (!auth) return { user: null, activeOrganizationId: null }
521
- const session = await auth.api.getSession({ headers })
522
- if (!session?.user) return { user: null, activeOrganizationId: null }
523
- return {
524
- user: {
525
- id: session.user.id,
526
- email: session.user.email,
527
- name: session.user.name,
528
- role: session.user.role,
529
- },
530
- activeOrganizationId: session.session?.activeOrganizationId ?? null,
531
- }
532
- }
@@ -1,52 +0,0 @@
1
- import { os, type AnyRouter } from '@orpc/server'
2
-
3
- import type { ApiContext } from './context'
4
-
5
- import {
6
- BUNDERSTACK_ERRORS,
7
- BunderstackError,
8
- mapBunderstackErrors,
9
- } from '../errors'
10
- export type { ProtectedContextAdditions } from './types'
11
-
12
- export function createApiBuilder<
13
- TSchema extends Record<string, unknown> = Record<string, unknown>,
14
- TEnv = Record<string, unknown>,
15
- >() {
16
- const base = os
17
- .$context<ApiContext<TSchema, TEnv>>()
18
- .errors(BUNDERSTACK_ERRORS)
19
- .use(mapBunderstackErrors)
20
-
21
- const protectedProc = base.use(async ({ context, next }) => {
22
- const session = await context.getSession()
23
- if (!session.user) {
24
- throw new BunderstackError('UNAUTHORIZED', 'Authentication required')
25
- }
26
- return next({
27
- context: {
28
- user: session.user,
29
- session: {
30
- activeOrganizationId: session.activeOrganizationId,
31
- },
32
- },
33
- })
34
- })
35
-
36
- return {
37
- public: base,
38
- protected: protectedProc,
39
- webhook: base,
40
- }
41
- }
42
-
43
- export type BunderstackApiBuilder<
44
- TSchema extends Record<string, unknown>,
45
- TEnv = Record<string, unknown>,
46
- > = ReturnType<typeof createApiBuilder<TSchema, TEnv>>
47
-
48
- export type ApiFactory<
49
- TSchema extends Record<string, unknown>,
50
- TEnv,
51
- TCustomApiRouter extends AnyRouter,
52
- > = (builder: BunderstackApiBuilder<TSchema, TEnv>) => TCustomApiRouter
@@ -1,83 +0,0 @@
1
- import type { AccessUser, AuthSessionResolver } from '../access'
2
- import type { DbFor } from '../db'
3
- import type { EmailFacade } from '../email'
4
- import type { AuthInstance, StorageFacade } from '../index'
5
- import type { JobsRuntimeFacade } from '../jobs/define'
6
- import type { RealtimeFacade } from '../realtime/facade'
7
-
8
- import { resolveSession } from '../access'
9
-
10
- export interface ApiContextDeps<
11
- TSchema extends Record<string, unknown> = Record<string, unknown>,
12
- TEnv = Record<string, unknown>,
13
- > {
14
- db: DbFor<TSchema>
15
- env: TEnv
16
- storage: StorageFacade
17
- email: EmailFacade
18
- jobs: JobsRuntimeFacade
19
- realtime: RealtimeFacade<TSchema>
20
- auth: AuthInstance
21
- authResolver?: AuthSessionResolver
22
- }
23
-
24
- export interface ApiContext<
25
- TSchema extends Record<string, unknown> = Record<string, unknown>,
26
- TEnv = Record<string, unknown>,
27
- > {
28
- db: DbFor<TSchema>
29
- env: TEnv
30
- storage: StorageFacade
31
- email: EmailFacade
32
- jobs: JobsRuntimeFacade
33
- realtime: RealtimeFacade<TSchema>
34
- auth: AuthInstance
35
- request: Request
36
- resHeaders: Headers
37
- getRawBody: () => Promise<string>
38
- getSession: () => Promise<{
39
- user: AccessUser | null
40
- activeOrganizationId: string | null
41
- }>
42
- }
43
-
44
- export function createApiContext<
45
- TSchema extends Record<string, unknown> = Record<string, unknown>,
46
- TEnv = Record<string, unknown>,
47
- >(
48
- deps: ApiContextDeps<TSchema, TEnv>,
49
- request: Request,
50
- ): ApiContext<TSchema, TEnv> {
51
- // Reserve the body stream before a transport codec consumes `request`.
52
- const rawBodyRequest = request.clone()
53
- let rawBodyPromise: Promise<string> | undefined
54
- let sessionPromise:
55
- | Promise<{ user: AccessUser | null; activeOrganizationId: string | null }>
56
- | undefined
57
-
58
- const getSession = () => {
59
- if (!sessionPromise) {
60
- sessionPromise = resolveSession(deps.authResolver, request.headers)
61
- }
62
- return sessionPromise
63
- }
64
-
65
- const getRawBody = () => {
66
- if (!rawBodyPromise) rawBodyPromise = rawBodyRequest.text()
67
- return rawBodyPromise
68
- }
69
-
70
- return {
71
- db: deps.db,
72
- env: deps.env,
73
- storage: deps.storage,
74
- email: deps.email,
75
- jobs: deps.jobs,
76
- realtime: deps.realtime,
77
- auth: deps.auth,
78
- request,
79
- resHeaders: new Headers(),
80
- getRawBody,
81
- getSession,
82
- }
83
- }