bunderstack 0.17.0-beta.2 → 0.17.0-beta.3

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 +227 -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 +76 -0
  60. package/dist/config.d.ts.map +1 -0
  61. package/dist/config.js +81 -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 +477 -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
@@ -1,308 +0,0 @@
1
- // src/storage/buckets.ts
2
- import type { OperationRule, ScopeResolver } from '../access'
3
-
4
- // ---------------------------------------------------------------------------
5
- // Input types (developer-facing config)
6
- // ---------------------------------------------------------------------------
7
-
8
- export type BucketBackendInput =
9
- | {
10
- s3: {
11
- bucket: string
12
- region?: string
13
- endpoint?: string
14
- publicUrl?: string
15
- }
16
- }
17
- | { local: string }
18
-
19
- export type BucketConfigInput = {
20
- visibility?: 'public' | 'private'
21
- access?: {
22
- create?: OperationRule
23
- get?: OperationRule
24
- delete?: OperationRule
25
- }
26
- upload?: { maxSize?: string | number; accept?: string[] }
27
- transforms?: boolean
28
- scope?: {
29
- read?: ScopeResolver
30
- write?: ScopeResolver
31
- }
32
- quota?: { perUser?: string | number; perScope?: string | number }
33
- } & Partial<BucketBackendInput>
34
-
35
- export type StorageConfigInput = {
36
- s3?: true | { endpoint?: string }
37
- local?: true | string
38
- defaultBucket?: string
39
- buckets?: Record<string, BucketConfigInput>
40
- }
41
-
42
- // ---------------------------------------------------------------------------
43
- // Resolved types (internal)
44
- // ---------------------------------------------------------------------------
45
-
46
- export type ResolvedBackend =
47
- | { type: 'local'; path: string }
48
- | {
49
- type: 's3'
50
- bucket: string
51
- region: string
52
- endpoint?: string
53
- accessKeyId: string
54
- secretAccessKey: string
55
- publicUrl?: string
56
- }
57
-
58
- export type ResolvedBucket = {
59
- name: string
60
- backend: ResolvedBackend
61
- visibility: 'public' | 'private'
62
- access: { create: OperationRule; get: OperationRule; delete: OperationRule }
63
- upload?: { maxSizeBytes?: number; accept?: string[] }
64
- transforms: boolean
65
- readScope?: ScopeResolver
66
- writeScope?: ScopeResolver
67
- quota?: { perUserBytes?: number; perScopeBytes?: number }
68
- }
69
-
70
- export type ResolvedStorageBuckets = {
71
- defaultBucket: string
72
- buckets: Map<string, ResolvedBucket>
73
- }
74
-
75
- // ---------------------------------------------------------------------------
76
- // parseSize
77
- // ---------------------------------------------------------------------------
78
-
79
- const SIZE_UNITS: Record<string, number> = {
80
- b: 1,
81
- kb: 1024,
82
- mb: 1024 * 1024,
83
- gb: 1024 * 1024 * 1024,
84
- }
85
-
86
- export function parseSize(value: string | number): number {
87
- if (typeof value === 'number') {
88
- if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) {
89
- throw new Error(`[bunderstack] invalid size "${value}"`)
90
- }
91
- return value
92
- }
93
-
94
- const trimmed = value.trim()
95
- if (trimmed === '') throw new Error(`[bunderstack] invalid size "${value}"`)
96
-
97
- const match = /^(\d+(?:\.\d+)?)\s*([a-z]*)$/i.exec(trimmed)
98
- if (!match) throw new Error(`[bunderstack] invalid size "${value}"`)
99
-
100
- const numStr = match[1]
101
- if (numStr === undefined)
102
- throw new Error(`[bunderstack] invalid size "${value}"`)
103
- const num = parseFloat(numStr)
104
- const unit = (match[2] ?? '').toLowerCase()
105
-
106
- if (unit === '') return Math.floor(num)
107
-
108
- const multiplier = SIZE_UNITS[unit]
109
- if (multiplier === undefined)
110
- throw new Error(`[bunderstack] invalid size "${value}"`)
111
-
112
- return Math.floor(num * multiplier)
113
- }
114
-
115
- // ---------------------------------------------------------------------------
116
- // Platform override (Bunderhost & co.)
117
- // ---------------------------------------------------------------------------
118
-
119
- /**
120
- * A deployment platform that injects BUNDERSTACK_S3_ENDPOINT forces every
121
- * bucket onto that backend — code-level `local`/per-bucket `s3` blocks are
122
- * ignored so apps deploy unchanged. Logical buckets already prefix object
123
- * keys ("<bucket>/<uuid>"), so one physical bucket per environment suffices.
124
- */
125
- function platformS3Backend(
126
- env: Record<string, string | undefined>,
127
- ): ResolvedBackend | undefined {
128
- const endpoint = env['BUNDERSTACK_S3_ENDPOINT']
129
- if (!endpoint) return undefined
130
- return {
131
- type: 's3',
132
- bucket: env['BUNDERSTACK_S3_BUCKET'] ?? '',
133
- region: env['BUNDERSTACK_S3_REGION'] ?? 'auto',
134
- endpoint,
135
- accessKeyId: env['BUNDERSTACK_S3_ACCESS_KEY_ID'] ?? '',
136
- secretAccessKey: env['BUNDERSTACK_S3_SECRET_ACCESS_KEY'] ?? '',
137
- publicUrl: env['BUNDERSTACK_S3_PUBLIC_URL'],
138
- }
139
- }
140
-
141
- // ---------------------------------------------------------------------------
142
- // Shared backend resolution
143
- // ---------------------------------------------------------------------------
144
-
145
- function resolveSharedBackend(
146
- input: StorageConfigInput | undefined,
147
- env: Record<string, string | undefined>,
148
- ): ResolvedBackend {
149
- if (!input) return { type: 'local', path: './uploads' }
150
-
151
- if ('local' in input && input.local !== undefined) {
152
- return {
153
- type: 'local',
154
- path: input.local === true ? './uploads' : input.local,
155
- }
156
- }
157
-
158
- if ('s3' in input && input.s3 !== undefined) {
159
- const s3Cfg = typeof input.s3 === 'object' ? input.s3 : {}
160
- return {
161
- type: 's3',
162
- bucket: env['S3_BUCKET'] ?? '',
163
- region: env['S3_REGION'] ?? 'us-east-1',
164
- endpoint: s3Cfg.endpoint ?? env['S3_ENDPOINT'],
165
- accessKeyId: env['S3_ACCESS_KEY_ID'] ?? '',
166
- secretAccessKey: env['S3_SECRET_ACCESS_KEY'] ?? '',
167
- }
168
- }
169
-
170
- return { type: 'local', path: './uploads' }
171
- }
172
-
173
- // ---------------------------------------------------------------------------
174
- // Per-bucket backend resolution
175
- // ---------------------------------------------------------------------------
176
-
177
- function resolveBucketBackend(
178
- bucketInput: BucketConfigInput,
179
- sharedBackend: ResolvedBackend,
180
- env: Record<string, string | undefined>,
181
- ): ResolvedBackend {
182
- // Platform override active → sharedBackend IS the platform backend and
183
- // code-level per-bucket backends are ignored.
184
- if (env['BUNDERSTACK_S3_ENDPOINT']) return sharedBackend
185
- if ('s3' in bucketInput && bucketInput.s3 !== undefined) {
186
- const block = bucketInput.s3
187
- return {
188
- type: 's3',
189
- bucket: block.bucket,
190
- region: block.region ?? env['S3_REGION'] ?? 'us-east-1',
191
- endpoint: block.endpoint ?? env['S3_ENDPOINT'],
192
- accessKeyId: env['S3_ACCESS_KEY_ID'] ?? '',
193
- secretAccessKey: env['S3_SECRET_ACCESS_KEY'] ?? '',
194
- publicUrl: block.publicUrl,
195
- }
196
- }
197
-
198
- if ('local' in bucketInput && bucketInput.local !== undefined) {
199
- return { type: 'local', path: bucketInput.local }
200
- }
201
-
202
- return sharedBackend
203
- }
204
-
205
- // ---------------------------------------------------------------------------
206
- // Bucket resolution
207
- // ---------------------------------------------------------------------------
208
-
209
- function resolveSingleBucket(
210
- name: string,
211
- input: BucketConfigInput,
212
- sharedBackend: ResolvedBackend,
213
- env: Record<string, string | undefined>,
214
- ): ResolvedBucket {
215
- const visibility = input.visibility ?? 'private'
216
- const backend = resolveBucketBackend(input, sharedBackend, env)
217
-
218
- const defaultGet: OperationRule = visibility === 'public' ? 'public' : 'owner'
219
-
220
- const access: ResolvedBucket['access'] = {
221
- create: input.access?.create ?? 'authenticated',
222
- get: input.access?.get ?? defaultGet,
223
- delete: input.access?.delete ?? 'owner',
224
- }
225
-
226
- let upload: ResolvedBucket['upload'] | undefined = undefined
227
- if (input.upload !== undefined) {
228
- upload = {}
229
- if (input.upload.maxSize !== undefined) {
230
- upload.maxSizeBytes = parseSize(input.upload.maxSize)
231
- }
232
- if (input.upload.accept !== undefined) {
233
- upload.accept = input.upload.accept
234
- }
235
- }
236
-
237
- let quota: ResolvedBucket['quota'] | undefined = undefined
238
- if (input.quota !== undefined) {
239
- quota = {}
240
- if (input.quota.perUser !== undefined) {
241
- quota.perUserBytes = parseSize(input.quota.perUser)
242
- }
243
- if (input.quota.perScope !== undefined) {
244
- quota.perScopeBytes = parseSize(input.quota.perScope)
245
- }
246
- }
247
-
248
- return {
249
- name,
250
- backend,
251
- visibility,
252
- access,
253
- upload,
254
- transforms: input.transforms ?? false,
255
- readScope: input.scope?.read,
256
- writeScope: input.scope?.write,
257
- quota,
258
- }
259
- }
260
-
261
- // ---------------------------------------------------------------------------
262
- // resolveBuckets
263
- // ---------------------------------------------------------------------------
264
-
265
- export function resolveBuckets(
266
- input: StorageConfigInput | undefined,
267
- env: Record<string, string | undefined> = process.env,
268
- ): ResolvedStorageBuckets {
269
- const sharedBackend =
270
- platformS3Backend(env) ?? resolveSharedBackend(input, env)
271
- const bucketsInput = input?.buckets
272
-
273
- const declaredNames = bucketsInput ? Object.keys(bucketsInput) : []
274
- const defaultBucketName =
275
- input?.defaultBucket ??
276
- // A single declared bucket is unambiguous — treat it as the default.
277
- (declaredNames.length === 1 ? declaredNames[0]! : 'default')
278
-
279
- const buckets = new Map<string, ResolvedBucket>()
280
-
281
- if (!bucketsInput || Object.keys(bucketsInput).length === 0) {
282
- // Synthesize a single default bucket
283
- const defaultBucket = resolveSingleBucket(
284
- defaultBucketName,
285
- {},
286
- sharedBackend,
287
- env,
288
- )
289
- buckets.set(defaultBucketName, defaultBucket)
290
- return { defaultBucket: defaultBucketName, buckets }
291
- }
292
-
293
- // Validate defaultBucket is declared
294
- if (!(defaultBucketName in bucketsInput)) {
295
- throw new Error(
296
- `[bunderstack] defaultBucket "${defaultBucketName}" is not a declared bucket`,
297
- )
298
- }
299
-
300
- for (const [name, bucketInput] of Object.entries(bucketsInput)) {
301
- buckets.set(
302
- name,
303
- resolveSingleBucket(name, bucketInput, sharedBackend, env),
304
- )
305
- }
306
-
307
- return { defaultBucket: defaultBucketName, buckets }
308
- }
@@ -1,26 +0,0 @@
1
- // src/storage/delete.ts
2
- import type { AnyDb } from '../dialect'
3
- import type { StorageAdapter } from './index'
4
-
5
- import { deleteFileMetaRow } from './file-meta'
6
-
7
- /**
8
- * Delete a logical file: its transform-cache derivatives (under
9
- * `${fileId}__transforms/`), the original object, and the file-meta row.
10
- *
11
- * All deletion goes through this helper so the model stays addable to a future
12
- * blob/file split (see file-meta.ts header) — never inline `adapter.delete`.
13
- */
14
- export async function deleteFileWithDerivatives(
15
- adapter: StorageAdapter,
16
- db: AnyDb,
17
- fileId: string,
18
- ): Promise<void> {
19
- if (adapter.list) {
20
- const derivs = await adapter.list(`${fileId}__transforms/`)
21
- // Ignore individual derivative-delete errors — orphan sweep is the backstop.
22
- await Promise.all(derivs.map((k) => adapter.delete(k).catch(() => {})))
23
- }
24
- await adapter.delete(fileId)
25
- await deleteFileMetaRow(db, fileId)
26
- }
@@ -1,222 +0,0 @@
1
- /*
2
- * DEFERRED — logical/physical blob/file split
3
- *
4
- * The current model is 1:1 logical-file = physical-object: one row in
5
- * bunderstack_file_meta maps to exactly one object in storage. A future
6
- * blob/file split would introduce:
7
- *
8
- * bunderstackBlob { key, bucket, size, contentType, refcount, createdAt }
9
- * bunderstackFile { id, blobKey, ownerId, scopeJson, status, filename,
10
- * createdAt, confirmedAt, deletedAt }
11
- *
12
- * This would enable:
13
- * - Soft/shadow delete (file.deletedAt + per-bucket retention policy)
14
- * - Transfer ownership/scope (mutate file.ownerId / scopeJson, no byte movement)
15
- * - Duplicate/share to many users or orgs by reference (refcount++) so sharing
16
- * to N users stores bytes once; or by-copy via S3 CopyObject.
17
- *
18
- * Deletion would decrement refcount; bytes removed only at 0; orphan sweep also
19
- * reaps refcount=0 blobs (self-heals crash mid-decrement).
20
- *
21
- * Deferred because refcount correctness is a real cost vs. current need.
22
- *
23
- * Two forward-compat invariants keep this addable without breaking the public API:
24
- * 1. fileId is opaque — clients never parse it.
25
- * 2. All deletion goes through the storage delete helper, never inline.
26
- *
27
- * See docs/plans/2026-06-28-multi-bucket-storage-design.md §6 for full rationale.
28
- */
29
-
30
- import { eq, and, lt, sql } from 'drizzle-orm'
31
-
32
- import type { ScopeMap } from '../access'
33
- import type { AnyDb } from '../dialect'
34
-
35
- import { rowMatchesScope } from '../access'
36
- import { bunderstackFiles, filesTableFor } from '../internal-tables'
37
-
38
- export type FileMetaRow = typeof bunderstackFiles.$inferSelect
39
-
40
- // ─── CRUD ─────────────────────────────────────────────────────────────────────
41
-
42
- export async function insertPendingFile(
43
- db: AnyDb,
44
- input: {
45
- fileId: string
46
- bucket: string
47
- ownerId: string | null
48
- scopeJson: string | null
49
- filename: string | null
50
- contentType: string | null
51
- },
52
- ): Promise<void> {
53
- const files = filesTableFor(db)
54
- await db.insert(files).values({
55
- fileId: input.fileId,
56
- bucket: input.bucket,
57
- ownerId: input.ownerId,
58
- scopeJson: input.scopeJson,
59
- filename: input.filename,
60
- contentType: input.contentType,
61
- status: 'pending',
62
- createdAt: Date.now(),
63
- confirmedAt: null,
64
- size: null,
65
- })
66
- }
67
-
68
- export async function insertReadyFile(
69
- db: AnyDb,
70
- input: {
71
- fileId: string
72
- bucket: string
73
- ownerId: string | null
74
- scopeJson: string | null
75
- filename: string | null
76
- contentType: string | null
77
- size: number | null
78
- },
79
- ): Promise<void> {
80
- const files = filesTableFor(db)
81
- const now = Date.now()
82
- await db.insert(files).values({
83
- fileId: input.fileId,
84
- bucket: input.bucket,
85
- ownerId: input.ownerId,
86
- scopeJson: input.scopeJson,
87
- filename: input.filename,
88
- contentType: input.contentType,
89
- size: input.size,
90
- status: 'ready',
91
- createdAt: now,
92
- confirmedAt: now,
93
- })
94
- }
95
-
96
- export async function markFileReady(
97
- db: AnyDb,
98
- fileId: string,
99
- patch: { size: number | null; contentType: string | null },
100
- ): Promise<void> {
101
- const files = filesTableFor(db)
102
- await db
103
- .update(files)
104
- .set({
105
- status: 'ready',
106
- confirmedAt: Date.now(),
107
- size: patch.size,
108
- contentType: patch.contentType,
109
- })
110
- .where(eq(files.fileId, fileId))
111
- }
112
-
113
- export async function getFileMeta(
114
- db: AnyDb,
115
- fileId: string,
116
- ): Promise<FileMetaRow | null> {
117
- const files = filesTableFor(db)
118
- const rows = await db
119
- .select()
120
- .from(files)
121
- .where(eq(files.fileId, fileId))
122
- .limit(1)
123
- return (rows[0] as FileMetaRow | undefined) ?? null
124
- }
125
-
126
- export async function deleteFileMetaRow(
127
- db: AnyDb,
128
- fileId: string,
129
- ): Promise<void> {
130
- const files = filesTableFor(db)
131
- await db.delete(files).where(eq(files.fileId, fileId))
132
- }
133
-
134
- // ─── Sweep ────────────────────────────────────────────────────────────────────
135
-
136
- export async function listStalePendingFiles(
137
- db: AnyDb,
138
- olderThanMs: number,
139
- ): Promise<FileMetaRow[]> {
140
- const files = filesTableFor(db)
141
- return db
142
- .select()
143
- .from(files)
144
- .where(and(eq(files.status, 'pending'), lt(files.createdAt, olderThanMs)))
145
- }
146
-
147
- // ─── Quota ────────────────────────────────────────────────────────────────────
148
-
149
- export async function sumReadySize(
150
- db: AnyDb,
151
- q: { bucket: string; ownerId?: string; scopeJson?: string },
152
- ): Promise<number> {
153
- const files = filesTableFor(db)
154
- const conditions = [eq(files.status, 'ready'), eq(files.bucket, q.bucket)]
155
- if (q.ownerId !== undefined) {
156
- conditions.push(eq(files.ownerId, q.ownerId))
157
- }
158
- if (q.scopeJson !== undefined) {
159
- conditions.push(eq(files.scopeJson, q.scopeJson))
160
- }
161
-
162
- const rows = await db
163
- .select({
164
- total: sql<number>`coalesce(sum(${files.size}), 0)`,
165
- })
166
- .from(files)
167
- .where(and(...conditions))
168
-
169
- const raw = rows[0]?.total ?? 0
170
- return Number(raw)
171
- }
172
-
173
- // ─── Scope helpers ────────────────────────────────────────────────────────────
174
-
175
- export function scopeToJson(scope: ScopeMap | undefined | null): string | null {
176
- if (scope == null) return null
177
- const keys = Object.keys(scope)
178
- if (keys.length === 0) return null
179
- const sorted = keys.sort()
180
- const ordered: Record<string, string | string[]> = {}
181
- for (const k of sorted) {
182
- const value = scope[k]
183
- if (value !== undefined) ordered[k] = value
184
- }
185
- return JSON.stringify(ordered)
186
- }
187
-
188
- function isScopeMap(value: unknown): value is ScopeMap {
189
- if (value === null || typeof value !== 'object' || Array.isArray(value)) {
190
- return false
191
- }
192
- for (const item of Object.values(value)) {
193
- if (typeof item === 'string') continue
194
- if (Array.isArray(item) && item.every((entry) => typeof entry === 'string'))
195
- continue
196
- return false
197
- }
198
- return true
199
- }
200
-
201
- export function parseScopeJson(json: string | null): ScopeMap | null {
202
- if (json == null) return null
203
- try {
204
- const parsed = JSON.parse(json)
205
- return isScopeMap(parsed) ? parsed : null
206
- } catch {
207
- // Corrupt/legacy scope_json should not crash a read; treat as no scope.
208
- return null
209
- }
210
- }
211
-
212
- export function fileMatchesScope(
213
- row: FileMetaRow,
214
- requesterScope: ScopeMap | undefined,
215
- ): boolean {
216
- if (requesterScope == null || Object.keys(requesterScope).length === 0) {
217
- return true
218
- }
219
- if (row.scopeJson == null) return false
220
- const parsed = parseScopeJson(row.scopeJson) ?? {}
221
- return rowMatchesScope(parsed, requesterScope)
222
- }
@@ -1,39 +0,0 @@
1
- // src/storage/index.ts
2
- import { LocalStorageAdapter } from './local'
3
- import { S3StorageAdapter } from './s3'
4
-
5
- export type { LocalStorageAdapter, S3StorageAdapter }
6
- export { createStorageOperations } from './operations'
7
- export type {
8
- PrepareUploadResult,
9
- StorageDownload,
10
- StorageExecutionContext,
11
- StorageOperations,
12
- StorageOperationsOptions,
13
- } from './operations'
14
-
15
- export interface PresignPutOptions {
16
- contentType?: string
17
- expiresIn: number
18
- }
19
- export interface PresignGetOptions {
20
- expiresIn: number
21
- }
22
-
23
- export interface StorageAdapter {
24
- upload(
25
- fileId: string,
26
- data: Blob | ArrayBuffer,
27
- contentType: string,
28
- ): Promise<void>
29
- get(fileId: string): Promise<Response>
30
- delete(fileId: string): Promise<void>
31
- exists(fileId: string): Promise<boolean>
32
- // Optional — present on S3, absent on local (router uses proxy path for local)
33
- presignPut?(key: string, opts: PresignPutOptions): Promise<string>
34
- presignGet?(key: string, opts: PresignGetOptions): Promise<string>
35
- stat?(key: string): Promise<{ size: number; contentType: string } | null>
36
- publicUrlFor?(key: string): string | undefined
37
- /** List full object keys under a prefix (used to reap transform derivatives). */
38
- list?(prefix: string): Promise<string[]>
39
- }
@@ -1,69 +0,0 @@
1
- // src/storage/local.ts
2
- import { mkdir, readdir } from 'node:fs/promises'
3
- import { join } from 'node:path'
4
-
5
- export class LocalStorageAdapter {
6
- private readonly basePath: string
7
- constructor(basePath: string) {
8
- this.basePath = basePath
9
- }
10
-
11
- private filePath(fileId: string) {
12
- return join(this.basePath, fileId)
13
- }
14
-
15
- async upload(
16
- fileId: string,
17
- data: Blob | ArrayBuffer,
18
- contentType: string,
19
- ): Promise<void> {
20
- await mkdir(this.basePath, { recursive: true })
21
- const bytes = data instanceof Blob ? await data.arrayBuffer() : data
22
- await Bun.write(
23
- Bun.file(this.filePath(fileId), { type: contentType }),
24
- bytes,
25
- )
26
- }
27
-
28
- async get(fileId: string): Promise<Response> {
29
- const file = Bun.file(this.filePath(fileId))
30
- if (!(await file.exists()))
31
- return new Response('Not found', { status: 404 })
32
- return new Response(file, {
33
- headers: {
34
- 'Content-Type': file.type,
35
- 'Cache-Control': 'public, max-age=31536000',
36
- },
37
- })
38
- }
39
-
40
- async delete(fileId: string): Promise<void> {
41
- const file = Bun.file(this.filePath(fileId))
42
- if (await file.exists()) await file.unlink()
43
- }
44
-
45
- async exists(fileId: string): Promise<boolean> {
46
- return Bun.file(this.filePath(fileId)).exists()
47
- }
48
-
49
- async stat(
50
- key: string,
51
- ): Promise<{ size: number; contentType: string } | null> {
52
- const file = Bun.file(this.filePath(key))
53
- if (!(await file.exists())) return null
54
- return { size: file.size, contentType: file.type }
55
- }
56
-
57
- async list(prefix: string): Promise<string[]> {
58
- const cleanPrefix = prefix.replace(/\/$/, '')
59
- const dir = join(this.basePath, cleanPrefix)
60
- let entries: string[]
61
- try {
62
- entries = await readdir(dir)
63
- } catch {
64
- // Missing dir (or prefix isn't a directory) → no derivatives.
65
- return []
66
- }
67
- return entries.map((name) => `${cleanPrefix}/${name}`)
68
- }
69
- }