appos 0.2.1 → 0.2.3-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.
Files changed (232) hide show
  1. package/dist/bin/auth-schema-7KeUwlcd.mjs +2 -0
  2. package/dist/bin/concurrently.mjs +2 -0
  3. package/dist/bin/event-v2sCJkNd.mjs +2 -0
  4. package/dist/bin/extract-blob-metadata-TqNd9w-6.mjs +2 -0
  5. package/dist/bin/generate-image-variant-D8H9FxgD.mjs +2 -0
  6. package/dist/bin/generate-preview-5jLZLX6I.mjs +2 -0
  7. package/dist/bin/main.mjs +362 -0
  8. package/dist/bin/purge-attachment-CMlJMNOk.mjs +2 -0
  9. package/dist/bin/purge-audit-logs-hd6q6vnR.mjs +2 -0
  10. package/dist/bin/purge-unattached-blobs-BYv5b9R9.mjs +2 -0
  11. package/dist/bin/track-db-changes-q0Vl7Htm.mjs +2 -0
  12. package/dist/bin/vite.mjs +2 -0
  13. package/dist/bin/vitest.mjs +2 -0
  14. package/dist/bin/workflow-BagSlsMp.mjs +2 -0
  15. package/dist/bin/youch-handler-Jj6i1XIT.mjs +2 -0
  16. package/dist/exports/api/_virtual/rolldown_runtime.mjs +1 -0
  17. package/dist/exports/api/app-context.d.mts +115 -0
  18. package/dist/exports/api/app-context.mjs +1 -0
  19. package/dist/exports/api/auth-schema.d.mts +4248 -0
  20. package/dist/exports/api/auth-schema.mjs +1 -0
  21. package/dist/exports/api/auth.d.mts +398 -0
  22. package/dist/exports/api/auth.mjs +1 -0
  23. package/dist/exports/api/cache.d.mts +44 -0
  24. package/dist/exports/api/cache.mjs +1 -0
  25. package/dist/exports/api/config.d.mts +28 -0
  26. package/dist/exports/api/config.mjs +1 -0
  27. package/dist/exports/api/container.d.mts +210 -0
  28. package/dist/exports/api/container.mjs +1 -0
  29. package/dist/exports/api/database.d.mts +99 -0
  30. package/dist/exports/api/database.mjs +1 -0
  31. package/dist/exports/api/event.d.mts +235 -0
  32. package/dist/exports/api/event.mjs +1 -0
  33. package/dist/exports/api/i18n.d.mts +34 -0
  34. package/dist/exports/api/i18n.mjs +1 -0
  35. package/dist/exports/api/index.d.mts +21 -0
  36. package/dist/exports/api/index.mjs +1 -0
  37. package/dist/exports/api/logger.d.mts +21 -0
  38. package/dist/exports/api/logger.mjs +1 -0
  39. package/dist/exports/api/mailer.d.mts +70 -0
  40. package/dist/exports/api/mailer.mjs +1 -0
  41. package/dist/exports/api/middleware/request-logger.d.mts +24 -0
  42. package/dist/exports/api/middleware.d.mts +39 -0
  43. package/dist/exports/api/middleware.mjs +1 -0
  44. package/dist/exports/api/node_modules/.bun/change-case@5.4.4/node_modules/change-case/dist/index.mjs +1 -0
  45. package/dist/exports/api/openapi.d.mts +271 -0
  46. package/dist/exports/api/openapi.mjs +1 -0
  47. package/dist/exports/api/orm.d.mts +13 -0
  48. package/dist/exports/api/orm.mjs +1 -0
  49. package/dist/exports/api/otel.d.mts +40 -0
  50. package/dist/exports/api/otel.mjs +1 -0
  51. package/dist/exports/api/packages/appos/src/constants.mjs +1 -0
  52. package/dist/exports/api/packages/appos/src/instrumentation.d.mts +7 -0
  53. package/dist/exports/api/packages/appos/src/instrumentation.mjs +1 -0
  54. package/dist/exports/api/packages/appos/src/web/auth.mjs +1 -0
  55. package/dist/exports/api/redis.d.mts +34 -0
  56. package/dist/exports/api/redis.mjs +1 -0
  57. package/dist/exports/api/storage-schema.d.mts +707 -0
  58. package/dist/exports/api/storage-schema.mjs +1 -0
  59. package/dist/exports/api/storage.d.mts +506 -0
  60. package/dist/exports/api/storage.mjs +1 -0
  61. package/dist/exports/api/workflow.d.mts +250 -0
  62. package/dist/exports/api/workflow.mjs +1 -0
  63. package/dist/exports/api/workflows/_virtual/rolldown_runtime.mjs +1 -0
  64. package/dist/exports/api/workflows/auth-schema.mjs +1 -0
  65. package/dist/exports/api/workflows/auth.d.mts +375 -0
  66. package/dist/exports/api/workflows/cache.d.mts +44 -0
  67. package/dist/exports/api/workflows/config.d.mts +18 -0
  68. package/dist/exports/api/workflows/container.d.mts +167 -0
  69. package/dist/exports/api/workflows/database.d.mts +46 -0
  70. package/dist/exports/api/workflows/event.d.mts +68 -0
  71. package/dist/exports/api/workflows/event.mjs +1 -0
  72. package/dist/exports/api/workflows/extract-blob-metadata.mjs +1 -0
  73. package/dist/exports/api/workflows/generate-image-variant.d.mts +63 -0
  74. package/dist/exports/api/workflows/generate-image-variant.mjs +1 -0
  75. package/dist/exports/api/workflows/generate-preview.mjs +1 -0
  76. package/dist/exports/api/workflows/index.d.mts +2 -0
  77. package/dist/exports/api/workflows/index.mjs +1 -0
  78. package/dist/exports/api/workflows/logger.d.mts +21 -0
  79. package/dist/exports/api/workflows/mailer.d.mts +70 -0
  80. package/dist/exports/api/workflows/orm.d.mts +13 -0
  81. package/dist/exports/api/workflows/purge-attachment.mjs +1 -0
  82. package/dist/exports/api/workflows/purge-audit-logs.mjs +1 -0
  83. package/dist/exports/api/workflows/purge-unattached-blobs.mjs +1 -0
  84. package/dist/exports/api/workflows/redis.mjs +1 -0
  85. package/dist/exports/api/workflows/storage-schema.d.mts +699 -0
  86. package/dist/exports/api/workflows/storage.d.mts +396 -0
  87. package/dist/exports/api/workflows/track-db-changes.d.mts +72 -0
  88. package/dist/exports/api/workflows/track-db-changes.mjs +1 -0
  89. package/dist/exports/api/workflows/workflow.d.mts +24 -0
  90. package/dist/exports/api/workflows/workflow.mjs +1 -0
  91. package/dist/exports/cli/_virtual/rolldown_runtime.mjs +1 -0
  92. package/dist/exports/cli/api/auth-schema.mjs +1 -0
  93. package/dist/exports/cli/api/auth.d.mts +375 -0
  94. package/dist/exports/cli/api/cache.d.mts +44 -0
  95. package/dist/exports/cli/api/config.d.mts +18 -0
  96. package/dist/exports/cli/api/container.d.mts +167 -0
  97. package/dist/exports/cli/api/database.d.mts +46 -0
  98. package/dist/exports/cli/api/event.d.mts +68 -0
  99. package/dist/exports/cli/api/event.mjs +1 -0
  100. package/dist/exports/cli/api/logger.d.mts +21 -0
  101. package/dist/exports/cli/api/mailer.d.mts +70 -0
  102. package/dist/exports/cli/api/orm.d.mts +13 -0
  103. package/dist/exports/cli/api/redis.mjs +1 -0
  104. package/dist/exports/cli/api/storage-schema.d.mts +699 -0
  105. package/dist/exports/cli/api/storage.d.mts +396 -0
  106. package/dist/exports/cli/api/workflow.d.mts +2 -0
  107. package/dist/exports/cli/api/workflow.mjs +1 -0
  108. package/dist/exports/cli/api/workflows/extract-blob-metadata.mjs +1 -0
  109. package/dist/exports/cli/api/workflows/generate-image-variant.d.mts +63 -0
  110. package/dist/exports/cli/api/workflows/generate-image-variant.mjs +1 -0
  111. package/dist/exports/cli/api/workflows/generate-preview.mjs +1 -0
  112. package/dist/exports/cli/api/workflows/purge-attachment.mjs +1 -0
  113. package/dist/exports/cli/api/workflows/purge-audit-logs.mjs +1 -0
  114. package/dist/exports/cli/api/workflows/purge-unattached-blobs.mjs +1 -0
  115. package/dist/exports/cli/api/workflows/track-db-changes.mjs +1 -0
  116. package/dist/exports/cli/command.d.mts +54 -0
  117. package/dist/exports/cli/command.mjs +1 -0
  118. package/dist/exports/cli/context.d.mts +170 -0
  119. package/dist/exports/cli/index.d.mts +3 -0
  120. package/dist/exports/cli/index.mjs +1 -0
  121. package/dist/exports/devtools/index.d.ts +3 -0
  122. package/dist/exports/devtools/index.js +1 -0
  123. package/dist/exports/instrumentation.d.mts +1 -0
  124. package/dist/exports/instrumentation.mjs +1 -0
  125. package/dist/exports/tests/_virtual/rolldown_runtime.mjs +1 -0
  126. package/dist/exports/tests/api.d.mts +86 -0
  127. package/dist/exports/tests/api.mjs +1 -0
  128. package/dist/exports/tests/mock.d.mts +1 -0
  129. package/dist/exports/tests/mock.mjs +1 -0
  130. package/dist/exports/tests/node_modules/.bun/change-case@5.4.4/node_modules/change-case/dist/index.mjs +1 -0
  131. package/dist/exports/tests/node_modules/.bun/rate-limit-redis@4.3.1_f1fa5524233c9c60/node_modules/rate-limit-redis/dist/index.mjs +25 -0
  132. package/dist/exports/tests/packages/appos/src/api/app-context.d.mts +115 -0
  133. package/dist/exports/tests/packages/appos/src/api/auth-schema.d.mts +4248 -0
  134. package/dist/exports/tests/packages/appos/src/api/auth-schema.mjs +1 -0
  135. package/dist/exports/tests/packages/appos/src/api/auth.d.mts +398 -0
  136. package/dist/exports/tests/packages/appos/src/api/cache.d.mts +44 -0
  137. package/dist/exports/tests/packages/appos/src/api/config.d.mts +28 -0
  138. package/dist/exports/tests/packages/appos/src/api/container.d.mts +210 -0
  139. package/dist/exports/tests/packages/appos/src/api/database.d.mts +99 -0
  140. package/dist/exports/tests/packages/appos/src/api/database.mjs +1 -0
  141. package/dist/exports/tests/packages/appos/src/api/event.d.mts +235 -0
  142. package/dist/exports/tests/packages/appos/src/api/event.mjs +1 -0
  143. package/dist/exports/tests/packages/appos/src/api/i18n.d.mts +34 -0
  144. package/dist/exports/tests/packages/appos/src/api/index.d.mts +27 -0
  145. package/dist/exports/tests/packages/appos/src/api/logger.d.mts +21 -0
  146. package/dist/exports/tests/packages/appos/src/api/mailer.d.mts +70 -0
  147. package/dist/exports/tests/packages/appos/src/api/middleware/error-handler.mjs +1 -0
  148. package/dist/exports/tests/packages/appos/src/api/middleware/health.mjs +1 -0
  149. package/dist/exports/tests/packages/appos/src/api/middleware/i18n.mjs +1 -0
  150. package/dist/exports/tests/packages/appos/src/api/middleware/request-logger.d.mts +24 -0
  151. package/dist/exports/tests/packages/appos/src/api/middleware/request-logger.mjs +1 -0
  152. package/dist/exports/tests/packages/appos/src/api/middleware/shutdown.mjs +1 -0
  153. package/dist/exports/tests/packages/appos/src/api/middleware/timeout.mjs +1 -0
  154. package/dist/exports/tests/packages/appos/src/api/middleware/youch-handler.mjs +1 -0
  155. package/dist/exports/tests/packages/appos/src/api/middleware.d.mts +39 -0
  156. package/dist/exports/tests/packages/appos/src/api/middleware.mjs +1 -0
  157. package/dist/exports/tests/packages/appos/src/api/openapi.d.mts +271 -0
  158. package/dist/exports/tests/packages/appos/src/api/orm.d.mts +13 -0
  159. package/dist/exports/tests/packages/appos/src/api/otel.d.mts +40 -0
  160. package/dist/exports/tests/packages/appos/src/api/redis.d.mts +34 -0
  161. package/dist/exports/tests/packages/appos/src/api/redis.mjs +1 -0
  162. package/dist/exports/tests/packages/appos/src/api/server.mjs +1 -0
  163. package/dist/exports/tests/packages/appos/src/api/storage-schema.d.mts +707 -0
  164. package/dist/exports/tests/packages/appos/src/api/storage.d.mts +506 -0
  165. package/dist/exports/tests/packages/appos/src/api/workflow.d.mts +250 -0
  166. package/dist/exports/tests/packages/appos/src/api/workflow.mjs +1 -0
  167. package/dist/exports/tests/packages/appos/src/api/workflows/extract-blob-metadata.mjs +1 -0
  168. package/dist/exports/tests/packages/appos/src/api/workflows/generate-image-variant.d.mts +99 -0
  169. package/dist/exports/tests/packages/appos/src/api/workflows/generate-image-variant.mjs +1 -0
  170. package/dist/exports/tests/packages/appos/src/api/workflows/generate-preview.mjs +1 -0
  171. package/dist/exports/tests/packages/appos/src/api/workflows/purge-attachment.mjs +1 -0
  172. package/dist/exports/tests/packages/appos/src/api/workflows/purge-audit-logs.mjs +1 -0
  173. package/dist/exports/tests/packages/appos/src/api/workflows/purge-unattached-blobs.mjs +1 -0
  174. package/dist/exports/tests/packages/appos/src/api/workflows/track-db-changes.mjs +1 -0
  175. package/dist/exports/tests/packages/appos/src/constants.mjs +1 -0
  176. package/dist/exports/tests/packages/appos/src/instrumentation.d.mts +7 -0
  177. package/dist/exports/tests/packages/appos/src/instrumentation.mjs +1 -0
  178. package/dist/exports/tests/react.d.mts +2 -0
  179. package/dist/exports/tests/react.mjs +1 -0
  180. package/dist/exports/tests/setup.d.mts +1 -0
  181. package/dist/exports/tests/setup.mjs +1 -0
  182. package/dist/exports/vendors/date.js +1 -0
  183. package/dist/exports/vendors/toolkit.js +1 -0
  184. package/dist/exports/vendors/zod.d.ts +1 -0
  185. package/dist/exports/vendors/zod.js +1 -0
  186. package/dist/exports/vite/index.d.mts +19 -0
  187. package/dist/exports/vite/index.mjs +1 -0
  188. package/dist/exports/vitest/config.d.mts +1 -0
  189. package/dist/exports/vitest/config.mjs +1 -0
  190. package/dist/exports/vitest/globals.d.mts +1 -0
  191. package/dist/exports/vitest/globals.mjs +1 -0
  192. package/dist/exports/vitest/index.d.mts +1 -0
  193. package/dist/exports/vitest/index.mjs +1 -0
  194. package/dist/exports/web/api/auth.d.ts +125 -0
  195. package/dist/exports/web/api/database.d.ts +4 -0
  196. package/dist/exports/web/api/logger.d.ts +1 -0
  197. package/dist/exports/web/auth.d.ts +2388 -0
  198. package/dist/exports/web/auth.js +1 -0
  199. package/dist/exports/web/i18n.d.ts +42 -0
  200. package/dist/exports/web/i18n.js +1 -0
  201. package/dist/exports/web/index.d.ts +6 -0
  202. package/dist/exports/web/index.js +1 -0
  203. package/package.json +138 -98
  204. package/build/bin/main.mjs +0 -2
  205. package/build/exports/cli/index.d.mts +0 -325
  206. package/build/exports/cli/index.mjs +0 -1
  207. package/build/exports/instrumentation/execAsync-DaIUcs6_.mjs +0 -1
  208. package/build/exports/instrumentation/getMachineId-bsd-bB6ipDhm.mjs +0 -1
  209. package/build/exports/instrumentation/getMachineId-darwin-D1Bx5aCe.mjs +0 -2
  210. package/build/exports/instrumentation/getMachineId-linux-D_R9Tla0.mjs +0 -1
  211. package/build/exports/instrumentation/getMachineId-unsupported-BZKPE_Ev.mjs +0 -1
  212. package/build/exports/instrumentation/getMachineId-win-CmPvIqHL.mjs +0 -1
  213. package/build/exports/instrumentation/instrumentation.d.mts +0 -1
  214. package/build/exports/instrumentation/instrumentation.mjs +0 -80
  215. package/build/exports/server/index.d.mts +0 -327
  216. package/build/exports/server/index.mjs +0 -219
  217. package/build/exports/server/react-gPO8Jsy-.mjs +0 -13
  218. package/build/exports/server/server.node-D_9RYjm9.mjs +0 -210
  219. package/build/exports/store/index.d.mts +0 -58
  220. package/build/exports/store/index.mjs +0 -15
  221. package/build/exports/support/datetime.js +0 -1
  222. package/build/exports/support/utils.js +0 -1
  223. package/build/exports/support/zod.d.ts +0 -2
  224. package/build/exports/support/zod.js +0 -23
  225. package/build/exports/test/dist-DAsoCGWk.mjs +0 -348
  226. package/build/exports/test/index.d.mts +0 -3
  227. package/build/exports/test/index.mjs +0 -1
  228. package/build/exports/test/magic-string.es-BWgiB2kd.mjs +0 -14
  229. package/build/exports/test/setup.d.mts +0 -1
  230. package/build/exports/test/setup.mjs +0 -329
  231. /package/{build/exports/support/datetime.d.ts → dist/exports/vendors/date.d.ts} +0 -0
  232. /package/{build/exports/support/utils.d.ts → dist/exports/vendors/toolkit.d.ts} +0 -0
@@ -0,0 +1,506 @@
1
+ import { StorageBlob, StorageRelationsConfig, StorageTables } from "./storage-schema.mjs";
2
+ import { ImageTransformations, ResizeOptions, resizeSchema, transformationsSchema } from "./workflows/generate-image-variant.mjs";
3
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
+ import { Pool } from "pg";
5
+ import { DriveManager } from "flydrive";
6
+ import { FSDriverOptions } from "flydrive/drivers/fs/types";
7
+ import { S3DriverOptions } from "flydrive/drivers/s3/types";
8
+
9
+ //#region src/api/storage.d.ts
10
+
11
+ /**
12
+ * Options for defining an S3 disk.
13
+ */
14
+ interface DefineS3DiskOptions {
15
+ /**
16
+ * The S3 bucket name.
17
+ */
18
+ bucket: string;
19
+ /**
20
+ * Optional credentials. If not provided, uses default AWS credential chain.
21
+ */
22
+ credentials?: {
23
+ accessKeyId: string;
24
+ secretAccessKey: string;
25
+ };
26
+ /**
27
+ * Optional S3 endpoint for S3-compatible services (e.g., MinIO, LocalStack).
28
+ */
29
+ endpoint?: string;
30
+ /**
31
+ * The AWS region.
32
+ */
33
+ region: string;
34
+ /**
35
+ * Bucket visibility (private or public).
36
+ * @default "private"
37
+ */
38
+ visibility?: "private" | "public";
39
+ }
40
+ /**
41
+ * Creates an S3 disk configuration with common boilerplate handled.
42
+ *
43
+ * Algorithm:
44
+ * 1. Build base config with bucket, region, visibility
45
+ * 2. If endpoint provided, add endpoint + forcePathStyle
46
+ * 3. If credentials provided, add credentials object
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * defineS3Disk({
51
+ * bucket: "my-bucket",
52
+ * region: "us-east-1",
53
+ * endpoint: "http://localhost:4566",
54
+ * credentials: {
55
+ * accessKeyId: "test",
56
+ * secretAccessKey: "test",
57
+ * },
58
+ * })
59
+ * ```
60
+ */
61
+ declare function defineS3Disk(opts: DefineS3DiskOptions): S3DriverOptions;
62
+ /**
63
+ * Database type with storage schema and relations.
64
+ */
65
+ type DatabaseWithStorage = NodePgDatabase<StorageTables, StorageRelationsConfig> & {
66
+ $client: Pool;
67
+ };
68
+ /**
69
+ * Storage service configuration.
70
+ * @template TDisks - Record of disk configurations keyed by disk name
71
+ * @template TDb - Database type for table name extraction
72
+ * @template TAttachments - Mapping of table names to valid attachment names
73
+ */
74
+ interface DefineStorageOptions<TDisks extends Record<string, FSDriverOptions | S3DriverOptions>, TDb extends NodePgDatabase<any, any> = DatabaseWithStorage, TAttachments extends Partial<Record<ExtractTableNames<TDb>, readonly string[]>> = Record<never, never>> {
75
+ /**
76
+ * Type-safe attachment name configuration.
77
+ * Maps table names to valid attachment names for that table.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * attachments: {
82
+ * users: ["avatar", "documents"] as const,
83
+ * posts: ["images", "cover"] as const,
84
+ * }
85
+ * ```
86
+ */
87
+ attachments?: TAttachments;
88
+ /**
89
+ * The database instance with storage schema and relations.
90
+ */
91
+ database: TDb;
92
+ /**
93
+ * The default disk name.
94
+ */
95
+ default?: keyof TDisks;
96
+ /**
97
+ * The disk configurations.
98
+ */
99
+ disks?: TDisks;
100
+ /**
101
+ * Optional public endpoint for signed URLs (for S3 disks).
102
+ *
103
+ * If provided, signed URLs will use this endpoint to be browser-accessible.
104
+ */
105
+ publicEndpoint?: string;
106
+ /**
107
+ * Cron expression for unattached blob purge schedule.
108
+ * @default "0 0 * * *" (midnight daily)
109
+ */
110
+ purgeCron?: string;
111
+ /**
112
+ * Secret key for signing blob IDs. Required for signedId() / findSigned() methods.
113
+ */
114
+ secret?: string;
115
+ }
116
+ /**
117
+ * Type of the storage service instance.
118
+ */
119
+ type Storage<TDisks extends Record<string, FSDriverOptions | S3DriverOptions> = Record<string, FSDriverOptions | S3DriverOptions>, TTableNames extends string = string, TAttachments extends Partial<Record<TTableNames, readonly string[]>> = Record<never, never>> = StorageService<Extract<keyof TDisks, string>, TTableNames, TAttachments>;
120
+ /**
121
+ * Extract table names from a database type.
122
+ */
123
+ type ExtractTableNames<TDb> = TDb extends NodePgDatabase<infer TTables, any> ? keyof TTables extends string ? keyof TTables : never : never;
124
+ /**
125
+ * Define storage service with FlyDrive.
126
+ * @template TDisks - Record of disk configurations keyed by disk name
127
+ * @template TDb - Database type for table name extraction
128
+ * @template TAttachments - Mapping of table names to valid attachment names
129
+ */
130
+ declare function defineStorage<TDisks extends Record<string, FSDriverOptions | S3DriverOptions>, TDb extends NodePgDatabase<any, any> = DatabaseWithStorage, TAttachments extends Partial<Record<ExtractTableNames<TDb>, readonly string[]>> = Record<never, never>>(opts: DefineStorageOptions<TDisks, TDb, TAttachments>): StorageService<Extract<keyof TDisks, string>, ExtractTableNames<TDb>, TAttachments>;
131
+ /**
132
+ * Helper type to extract attachment name for a specific table.
133
+ */
134
+ type AttachmentName<TAttachments extends Partial<Record<string, readonly string[]>>, TTable extends string> = TTable extends keyof TAttachments ? TAttachments[TTable] extends readonly (infer N)[] ? N : string : string;
135
+ /**
136
+ * Storage service for blob operations.
137
+ *
138
+ * This service uses a dual-drive architecture when configured:
139
+ * - `drive`: For actual file operations (upload, delete, read) using internal endpoints
140
+ * - `signedUrlDrive`: For generating browser-accessible signed URLs with public endpoints
141
+ *
142
+ * This allows containers to use fast internal network for operations while
143
+ * generating URLs that work in end-user browsers.
144
+ *
145
+ * @template TDiskNames - Union of disk names (e.g., "private" | "public")
146
+ * @template TTableNames - Union of table names for type-safe attachment methods
147
+ * @template TAttachments - Mapping of table names to valid attachment names
148
+ */
149
+ declare class StorageService<TDiskNames extends string = string, TTableNames extends string = string, TAttachments extends Partial<Record<TTableNames, readonly string[]>> = Record<never, never>> {
150
+ /**
151
+ * The drive manager for file operations.
152
+ */
153
+ readonly drive: DriveManager<any>;
154
+ /**
155
+ * The database instance with storage schema and relations.
156
+ */
157
+ readonly db: DatabaseWithStorage;
158
+ /**
159
+ * The default disk name.
160
+ */
161
+ readonly defaultDisk: TDiskNames;
162
+ /**
163
+ * Cron expression for unattached blob purge schedule.
164
+ *
165
+ * @default "0 0 * * *"
166
+ */
167
+ readonly purgeCron?: string;
168
+ /**
169
+ * Secret key for signing blob IDs.
170
+ */
171
+ readonly secret?: string;
172
+ /**
173
+ * Drive manager for generating signed URLs (public endpoint).
174
+ */
175
+ readonly signedUrlDrive: DriveManager<any>;
176
+ constructor(drive: DriveManager<any>, db: DatabaseWithStorage, defaultDisk: TDiskNames, signedUrlDrive: DriveManager<any>, purgeCron?: string, secret?: string);
177
+ /**
178
+ * Create a blob record and upload file.
179
+ */
180
+ createBlob(file: Buffer | Uint8Array, options: {
181
+ filename: string;
182
+ contentType?: string;
183
+ metadata?: Record<string, any>;
184
+ serviceName?: TDiskNames;
185
+ prefix?: string;
186
+ }): Promise<StorageBlob>;
187
+ /**
188
+ * Get blob by ID.
189
+ */
190
+ getBlob(id: string): Promise<StorageBlob | null>;
191
+ /**
192
+ * Download blob content.
193
+ */
194
+ downloadBlob(id: string): Promise<Buffer | null>;
195
+ /**
196
+ * Delete blob and its content.
197
+ */
198
+ deleteBlob(id: string): Promise<boolean>;
199
+ /**
200
+ * Get signed URL for blob. If blob is on public service, returns permanent
201
+ * URL instead.
202
+ */
203
+ getSignedUrl(id: string, options?: {
204
+ expiresIn?: number;
205
+ disposition?: "inline" | "attachment";
206
+ filename?: string;
207
+ }): Promise<string | null>;
208
+ /**
209
+ * Get permanent public URL (no expiration).
210
+ * Works for blobs on public storage service.
211
+ */
212
+ getPublicUrl(id: string): Promise<string | null>;
213
+ /**
214
+ * Create attachment between a record and blob.
215
+ *
216
+ * @param recordType - The table name (e.g., 'users')
217
+ * @param recordId - The record ID
218
+ * @param blobId - The blob ID to attach
219
+ * @param name - The attachment name (e.g., 'avatar')
220
+ * @param replaceExisting - If true, replaces existing attachment with same name (for one-to-one)
221
+ */
222
+ createAttachment(recordType: string, recordId: string, blobId: string, name: string, replaceExisting?: boolean): Promise<{
223
+ id: string;
224
+ createdAt: string;
225
+ name: string;
226
+ recordType: string;
227
+ recordId: string;
228
+ blobId: string;
229
+ }>;
230
+ /**
231
+ * Get attachments for a record with their associated blobs.
232
+ */
233
+ getAttachments(recordType: string, recordId: string, name?: string): Promise<{
234
+ id: string;
235
+ createdAt: string;
236
+ name: string;
237
+ recordType: string;
238
+ recordId: string;
239
+ blobId: string;
240
+ blob: {
241
+ id: string;
242
+ createdAt: string;
243
+ key: string;
244
+ metadata: unknown;
245
+ filename: string;
246
+ contentType: string | null;
247
+ serviceName: string;
248
+ byteSize: number;
249
+ checksum: string | null;
250
+ } | null;
251
+ }[]>;
252
+ /**
253
+ * Get attachments by their IDs with associated blobs.
254
+ *
255
+ * @param attachmentIds - Array of attachment IDs to fetch.
256
+ */
257
+ getAttachmentsByIds(attachmentIds: string[]): Promise<{
258
+ id: string;
259
+ createdAt: string;
260
+ name: string;
261
+ recordType: string;
262
+ recordId: string;
263
+ blobId: string;
264
+ blob: {
265
+ id: string;
266
+ createdAt: string;
267
+ key: string;
268
+ metadata: unknown;
269
+ filename: string;
270
+ contentType: string | null;
271
+ serviceName: string;
272
+ byteSize: number;
273
+ checksum: string | null;
274
+ } | null;
275
+ }[]>;
276
+ /**
277
+ * Delete a single attachment by ID.
278
+ */
279
+ deleteAttachment(attachmentId: string): Promise<boolean>;
280
+ /**
281
+ * Delete attachments for a record (without deleting the blobs).
282
+ */
283
+ deleteAttachments(recordType: string, recordId: string, name?: string): Promise<number>;
284
+ /**
285
+ * Get direct upload credentials (for S3). Also creates a pending blob record
286
+ * that will be finalized after upload.
287
+ */
288
+ getDirectUploadUrl(options: {
289
+ contentType?: string;
290
+ expiresIn?: number;
291
+ filename: string;
292
+ metadata?: Record<string, any>;
293
+ serviceName?: TDiskNames;
294
+ }): Promise<{
295
+ blobId: string;
296
+ headers?: Record<string, string>;
297
+ key: string;
298
+ url: string;
299
+ } | null>;
300
+ /**
301
+ * Finalize a direct upload by updating blob metadata. Call this after the
302
+ * client has uploaded to S3.
303
+ */
304
+ finalizeDirectUpload(blobId: string, actualSize: number): Promise<void>;
305
+ /**
306
+ * Update blob metadata (for automatic extraction).
307
+ */
308
+ updateBlobMetadata(blobId: string, metadata: Record<string, any>): Promise<void>;
309
+ /**
310
+ * Get existing variant or return null.
311
+ */
312
+ getVariant(blobId: string, transformations: ImageTransformations): Promise<StorageBlob | null>;
313
+ /**
314
+ * Create variant blob and record.
315
+ */
316
+ createVariant(blobId: string, transformations: ImageTransformations, variantBuffer: Buffer): Promise<StorageBlob>;
317
+ /**
318
+ * Get blobs that have no attachments (orphaned). Useful for cleanup jobs.
319
+ */
320
+ getUnattachedBlobs(options?: {
321
+ olderThan?: string;
322
+ limit?: number;
323
+ }): Promise<StorageBlob[]>;
324
+ /**
325
+ * Get pending blobs that were never finalized (stuck direct uploads).
326
+ */
327
+ getPendingBlobs(olderThan?: string): Promise<StorageBlob[]>;
328
+ /**
329
+ * Purge unattached blobs.
330
+ */
331
+ purgeUnattached(olderThan?: string): Promise<number>;
332
+ /**
333
+ * Create a signed, tamper-proof reference to a blob.
334
+ * Use findSigned() to resolve back to blob.
335
+ *
336
+ * Algorithm:
337
+ * 1. Create payload with blobId and expiration timestamp
338
+ * 2. JSON stringify and base64url encode the payload
339
+ * 3. Create HMAC-SHA256 signature of the encoded payload
340
+ * 4. Return payload.signature format
341
+ */
342
+ signedId(blobId: string, expiresIn?: number): string;
343
+ /**
344
+ * Find blob by signed ID. Returns null if invalid or expired.
345
+ * Uses constant-time comparison to prevent timing attacks.
346
+ *
347
+ * Algorithm:
348
+ * 1. Split signed ID into payload and signature
349
+ * 2. Decode and recompute expected signature
350
+ * 3. Use timingSafeEqual for constant-time comparison
351
+ * 4. Check expiration timestamp
352
+ * 5. Return blob if valid, null otherwise
353
+ */
354
+ findSigned(signedId: string): Promise<StorageBlob | null>;
355
+ /**
356
+ * Get a single attachment handle (one-to-one relationship).
357
+ * Similar to Rails' `has_one_attached :avatar`.
358
+ *
359
+ * Algorithm:
360
+ * Returns an object with methods to manage a single attachment:
361
+ * - attach(): Replace existing attachment with new blob
362
+ * - get(): Get attached blob or null
363
+ * - url(): Get signed URL
364
+ * - variant(): Get/generate image variant
365
+ * - purge(): Delete attachment and blob
366
+ *
367
+ * @template TTable - The table name (must be in TTableNames)
368
+ */
369
+ one<TTable extends TTableNames>(recordType: TTable, recordId: string, name: AttachmentName<TAttachments, TTable>): {
370
+ /**
371
+ * Attach a blob to this record (replaces existing).
372
+ * Auto-triggers extractBlobMetadata workflow.
373
+ */
374
+ attach(blobId: string): Promise<{
375
+ id: string;
376
+ createdAt: string;
377
+ name: string;
378
+ recordType: string;
379
+ recordId: string;
380
+ blobId: string;
381
+ }>;
382
+ /** Get the attached blob or null. */
383
+ get(): Promise<StorageBlob | null>;
384
+ /** Check if a blob is attached. */
385
+ attached(): Promise<boolean>;
386
+ /** Get signed URL or null. */
387
+ url(options?: {
388
+ expiresIn?: number;
389
+ disposition?: "inline" | "attachment";
390
+ }): Promise<string | null>;
391
+ /** Get permanent public URL or null. */
392
+ publicUrl(): Promise<string | null>;
393
+ /** Get blob metadata or null. */
394
+ metadata(): Promise<Record<string, unknown> | null>;
395
+ /** Check if metadata has been extracted. */
396
+ analyzed(): Promise<boolean>;
397
+ /** Check if blob supports preview/variant generation. */
398
+ representable(): Promise<boolean>;
399
+ /**
400
+ * Get variant URL. Auto-enqueues generation if not ready.
401
+ * Returns URL if variant exists, null if generating.
402
+ */
403
+ variant(transformations: ImageTransformations, expiresIn?: number): Promise<string | null>;
404
+ /**
405
+ * Get preview URL. Auto-enqueues generation if not ready.
406
+ * Returns URL if preview exists, null if generating.
407
+ */
408
+ preview(expiresIn?: number, timeInSeconds?: number): Promise<string | null>;
409
+ /** Detach blob from record (keeps blob for reuse). */
410
+ detach(): Promise<boolean>;
411
+ /** Delete attachment AND blob immediately. */
412
+ purge(): Promise<boolean>;
413
+ /** Enqueue attachment and blob deletion in background. */
414
+ purgeLater(): Promise<boolean>;
415
+ /** Download blob content as Buffer. */
416
+ download(): Promise<Buffer | null>;
417
+ /** Download to temp file, run callback, auto-cleanup. */
418
+ open<T>(callback: (filePath: string) => Promise<T> | T): Promise<T | null>;
419
+ /**
420
+ * Smart representation - variant for images, preview for videos/PDFs.
421
+ * Auto-enqueues generation if not ready.
422
+ */
423
+ representation(transformations: ImageTransformations, expiresIn?: number): Promise<string | null>;
424
+ /** Get blob's byte size. */
425
+ byteSize(): Promise<number | null>;
426
+ /** Get blob's content type. */
427
+ contentType(): Promise<string | null>;
428
+ /** Get blob's filename. */
429
+ filename(): Promise<string | null>;
430
+ /** Get signed, tamper-proof ID for the blob. */
431
+ signedId(expiresIn?: number): Promise<string | null>;
432
+ };
433
+ /**
434
+ * Get a multiple attachment handle (one-to-many relationship).
435
+ * Similar to Rails' `has_many_attached :images`.
436
+ *
437
+ * Algorithm:
438
+ * Returns an object with methods to manage multiple attachments:
439
+ * - attach(): Add blobs (doesn't replace existing)
440
+ * - list(): Get all attached blobs
441
+ * - urls(): Get signed URLs for all
442
+ * - variants(): Get/generate variants for all
443
+ * - purge(): Delete all or specific attachment
444
+ *
445
+ * @template TTable - The table name (must be in TTableNames)
446
+ */
447
+ many<TTable extends TTableNames>(recordType: TTable, recordId: string, name: AttachmentName<TAttachments, TTable>): {
448
+ /**
449
+ * Attach one or more blobs (adds to existing, doesn't replace).
450
+ * Auto-triggers extractBlobMetadata for each blob.
451
+ */
452
+ attach(blobIds: string | string[]): Promise<{
453
+ id: string;
454
+ createdAt: string;
455
+ name: string;
456
+ recordType: string;
457
+ recordId: string;
458
+ blobId: string;
459
+ }[]>;
460
+ /** Get all attached blobs. */
461
+ list(): Promise<StorageBlob[]>;
462
+ /** Count attached blobs. */
463
+ count(): Promise<number>;
464
+ /** Get signed URLs for all blobs. */
465
+ urls(options?: {
466
+ expiresIn?: number;
467
+ }): Promise<string[]>;
468
+ /** Get public URLs for all blobs. */
469
+ publicUrls(): Promise<(string | null)[]>;
470
+ /** Get metadata for all blobs. */
471
+ metadata(): Promise<(Record<string, unknown> | null)[]>;
472
+ /** Check which blobs have been analyzed. */
473
+ analyzed(): Promise<boolean[]>;
474
+ /** Check which blobs support preview/variants. */
475
+ representable(): Promise<boolean[]>;
476
+ /** Get variant URLs for all blobs. Auto-enqueues if not ready. */
477
+ variants(transformations: ImageTransformations, expiresIn?: number): Promise<(string | null)[]>;
478
+ /** Get preview URLs for all blobs. Auto-enqueues if not ready. */
479
+ previews(expiresIn?: number, timeInSeconds?: number): Promise<(string | null)[]>;
480
+ /** Detach specific blob or all blobs (keeps blobs for reuse). */
481
+ detach(blobId?: string): Promise<number>;
482
+ /** Delete specific or all attachments AND blobs immediately. */
483
+ purge(blobId?: string): Promise<number>;
484
+ /** Enqueue specific or all attachments for background deletion. */
485
+ purgeLater(blobId?: string): Promise<number>;
486
+ /** Download all blobs as Buffers. */
487
+ download(): Promise<Buffer[]>;
488
+ /** Download each blob to temp file, run callback for each. */
489
+ open<T>(callback: (filePath: string, blob: StorageBlob) => Promise<T> | T): Promise<T[]>;
490
+ /**
491
+ * Smart representations - variant for images, preview for videos/PDFs.
492
+ * Auto-enqueues generation if not ready.
493
+ */
494
+ representations(transformations: ImageTransformations, expiresIn?: number): Promise<(string | null)[]>;
495
+ /** Get byte sizes for all blobs. */
496
+ byteSizes(): Promise<number[]>;
497
+ /** Get content types for all blobs. */
498
+ contentTypes(): Promise<(string | null)[]>;
499
+ /** Get filenames for all blobs. */
500
+ filenames(): Promise<string[]>;
501
+ /** Get signed IDs for all blobs. */
502
+ signedIds(expiresIn?: number): Promise<string[]>;
503
+ };
504
+ }
505
+ //#endregion
506
+ export { DatabaseWithStorage, DefineS3DiskOptions, DefineStorageOptions, Storage, StorageService, defineS3Disk, defineStorage };
@@ -0,0 +1 @@
1
+ import{orm_exports as e}from"./orm.mjs";import{extractBlobMetadata as t}from"./workflows/extract-blob-metadata.mjs";import{generateImageVariant as n,resizeSchema as r,transformationsSchema as i}from"./workflows/generate-image-variant.mjs";import{generatePreview as a}from"./workflows/generate-preview.mjs";import{purgeAttachment as o}from"./workflows/purge-attachment.mjs";import{join as s}from"node:path";import{mkdtemp as c,rm as l,writeFile as u}from"node:fs/promises";import{createHash as d,createHmac as f,randomBytes as p,timingSafeEqual as m}from"node:crypto";import{tmpdir as h}from"node:os";import{DriveManager as g}from"flydrive";import{FSDriver as _}from"flydrive/drivers/fs";import{S3Driver as v}from"flydrive/drivers/s3";function y(e){return{bucket:e.bucket,region:e.region,visibility:e.visibility??`private`,...e.endpoint&&{endpoint:e.endpoint,forcePathStyle:!0},...e.credentials&&{credentials:e.credentials}}}function b(e){return d(`md5`).update(e).digest(`hex`)}function x(e,t){return`variants/${e}/${d(`sha256`).update(JSON.stringify({blobId:e,transformations:t})).digest(`hex`)}`}function S(e){let t=`${Date.now()}-${p(16).toString(`hex`)}`;return e?`${e}/${t}`:t}function C(e){if(!e.default)throw Error(`Storage: 'default' disk must be specified`);if(!e.disks||Object.keys(e.disks).length===0)throw Error(`Storage: At least one disk must be configured`);let t=e.default,n={},r={};for(let[t,i]of Object.entries(e.disks)){let a=i;if(`driver`in a)if(a.driver===`fs`){let e=()=>new _(a);n[t]=e,r[t]=e}else a.driver===`s3`&&(n[t]=()=>new v(a),e.publicEndpoint?r[t]=()=>new v({...a,endpoint:e.publicEndpoint}):r[t]=()=>new v(a));else if(`location`in a){let e=()=>new _(a);n[t]=e,r[t]=e}else if(`bucket`in a)if(n[t]=()=>new v(a),e.publicEndpoint){let n={...a,endpoint:e.publicEndpoint};r[t]=()=>new v(n)}else r[t]=()=>new v(a)}let i=new g({default:t,services:n}),a=new g({default:t,services:r});return new w(i,e.database,t,a,e.purgeCron,e.secret)}var w=class{drive;db;defaultDisk;purgeCron;secret;signedUrlDrive;constructor(e,t,n,r,i,a){this.drive=e,this.db=t,this.defaultDisk=n,this.signedUrlDrive=r,this.purgeCron=i,this.secret=a}async createBlob(e,t){let n=S(t.prefix),r=Buffer.from(e),i=b(r),a=t.serviceName||this.defaultDisk;await(t.serviceName?this.drive.use(t.serviceName):this.drive.use()).put(n,r,{contentType:t.contentType});let[o]=await this.db.insert(this.db._.fullSchema.storageBlobs).values({key:n,filename:t.filename,contentType:t.contentType,metadata:t.metadata,serviceName:a,byteSize:r.byteLength,checksum:i}).returning();return o}async getBlob(t){let[n]=await this.db.select().from(this.db._.fullSchema.storageBlobs).where((0,e.eq)(this.db._.fullSchema.storageBlobs.id,t));return n||null}async downloadBlob(e){let t=await this.getBlob(e);if(!t)return null;let n=await this.drive.use(t.serviceName).getBytes(t.key);return Buffer.from(n)}async deleteBlob(t){let n=await this.getBlob(t);return n?(await this.drive.use(n.serviceName).delete(n.key),await this.db.delete(this.db._.fullSchema.storageBlobs).where((0,e.eq)(this.db._.fullSchema.storageBlobs.id,t)),!0):!1}async getSignedUrl(e,t={}){let n=await this.getBlob(e);if(!n)return null;if(n.serviceName===`public`)return this.getPublicUrl(e);let r=this.signedUrlDrive.use(n.serviceName);try{let e={expiresIn:t.expiresIn||3600};if(t.disposition){let r=t.filename||n.filename;e.contentDisposition=t.disposition===`attachment`?`attachment; filename="${r}"`:`inline; filename="${r}"`}return await r.getSignedUrl(n.key,e)}catch{let e=`/storage/${n.id}`;return t.disposition?`${e}?${new URLSearchParams({disposition:t.disposition,...t.filename&&{filename:t.filename}}).toString()}`:e}}async getPublicUrl(e){let t=await this.getBlob(e);if(!t)return null;let n=this.drive.use(t.serviceName);if(t.serviceName===`public`)try{if(`getUrl`in n&&typeof n.getUrl==`function`)return await n.getUrl(t.key)}catch{}return`/storage/${t.id}`}async createAttachment(t,n,r,i,a=!1){if(a){let a=`${t}:${n}:${i}`,o=Array.from(a).reduce((e,t)=>(e<<5)-e+t.charCodeAt(0)|0,0);return await this.db.transaction(async a=>{await a.execute(e.sql`SELECT pg_advisory_xact_lock(${o})`),await a.delete(this.db._.fullSchema.storageAttachments).where((0,e.and)((0,e.eq)(this.db._.fullSchema.storageAttachments.recordType,t),(0,e.eq)(this.db._.fullSchema.storageAttachments.recordId,n),(0,e.eq)(this.db._.fullSchema.storageAttachments.name,i)));let[s]=await a.insert(this.db._.fullSchema.storageAttachments).values({recordType:t,recordId:n,blobId:r,name:i}).returning();return s})}let[o]=await this.db.insert(this.db._.fullSchema.storageAttachments).values({recordType:t,recordId:n,blobId:r,name:i}).returning();return o}async getAttachments(e,t,n){return this.db.query.storageAttachments.findMany({where:{recordType:e,recordId:t,...n&&{name:n}},with:{blob:!0}})}async getAttachmentsByIds(e){return e.length===0?[]:this.db.query.storageAttachments.findMany({where:{id:{in:e}},with:{blob:!0}})}async deleteAttachment(t){return((await this.db.delete(this.db._.fullSchema.storageAttachments).where((0,e.eq)(this.db._.fullSchema.storageAttachments.id,t))).rowCount??0)>0}async deleteAttachments(t,n,r){let i=r?(0,e.and)((0,e.eq)(this.db._.fullSchema.storageAttachments.recordType,t),(0,e.eq)(this.db._.fullSchema.storageAttachments.recordId,n),(0,e.eq)(this.db._.fullSchema.storageAttachments.name,r)):(0,e.and)((0,e.eq)(this.db._.fullSchema.storageAttachments.recordType,t),(0,e.eq)(this.db._.fullSchema.storageAttachments.recordId,n));return(await this.db.delete(this.db._.fullSchema.storageAttachments).where(i)).rowCount??0}async getDirectUploadUrl(e){let t=e.serviceName||this.defaultDisk,n=this.drive.use(t);try{let r=S();if(`putSignedUrl`in n){let i=await n.putSignedUrl(r,{expiresIn:e.expiresIn||3600,contentType:e.contentType}),[a]=await this.db.insert(this.db._.fullSchema.storageBlobs).values({key:r,filename:e.filename,contentType:e.contentType,metadata:{...e.metadata,pending:!0},serviceName:t,byteSize:0,checksum:``}).returning();return{url:i,key:r,blobId:a.id,headers:{"Content-Type":e.contentType||`application/octet-stream`}}}}catch{}return null}async finalizeDirectUpload(t,n){let r=await this.getBlob(t);if(!r)throw Error(`Blob ${t} not found`);let i=r.metadata&&typeof r.metadata==`object`?{...r.metadata}:{};delete i.pending,await this.db.update(this.db._.fullSchema.storageBlobs).set({byteSize:n,metadata:i}).where((0,e.eq)(this.db._.fullSchema.storageBlobs.id,t))}async updateBlobMetadata(t,n){let r=await this.getBlob(t);if(!r)throw Error(`Blob ${t} not found`);let i={...r.metadata&&typeof r.metadata==`object`?r.metadata:{},...n};await this.db.update(this.db._.fullSchema.storageBlobs).set({metadata:i}).where((0,e.eq)(this.db._.fullSchema.storageBlobs.id,t))}async getVariant(e,t){let n=d(`sha256`).update(JSON.stringify(t)).digest(`hex`),[r]=await this.db.query.storageVariantRecords.findMany({where:{blobId:e,variationDigest:n},with:{blob:!0},limit:1});return r?.blob||null}async createVariant(e,t,n){let r=await this.getBlob(e);if(!r)throw Error(`Blob ${e} not found`);let i=x(e,t),a=b(n);await this.drive.use(r.serviceName).put(i,n,{contentType:r.contentType||`application/octet-stream`});let[o]=await this.db.insert(this.db._.fullSchema.storageBlobs).values({key:i,filename:r.filename,contentType:r.contentType,metadata:{sourceBlob:e,transformations:t},serviceName:r.serviceName,byteSize:n.byteLength,checksum:a}).returning(),s=d(`sha256`).update(JSON.stringify(t)).digest(`hex`);return await this.db.insert(this.db._.fullSchema.storageVariantRecords).values({blobId:e,variationDigest:s,id:o.id}),o}async getUnattachedBlobs(t={}){let n=t.olderThan||new Date(Date.now()-2880*60*1e3).toISOString(),r=t.limit||1e3;return(await this.db.select({blob:this.db._.fullSchema.storageBlobs}).from(this.db._.fullSchema.storageBlobs).leftJoin(this.db._.fullSchema.storageAttachments,(0,e.eq)(this.db._.fullSchema.storageBlobs.id,this.db._.fullSchema.storageAttachments.blobId)).where((0,e.and)((0,e.isNull)(this.db._.fullSchema.storageAttachments.id),(0,e.lt)(this.db._.fullSchema.storageBlobs.createdAt,n))).limit(r)).map(e=>e.blob)}async getPendingBlobs(t=new Date(Date.now()-1440*60*1e3).toISOString()){return await this.db.select().from(this.db._.fullSchema.storageBlobs).where((0,e.and)((0,e.lt)(this.db._.fullSchema.storageBlobs.createdAt,t),e.sql`${this.db._.fullSchema.storageBlobs.metadata}->>'pending' = 'true'`))}async purgeUnattached(e){let t=await this.getUnattachedBlobs({olderThan:e}),n=0;for(let e of t)try{await this.deleteBlob(e.id),n++}catch(t){console.error(`Failed to purge blob ${e.id}:`,t)}return n}signedId(e,t=3600){if(!this.secret)throw Error(`Storage secret not configured`);let n={blobId:e,exp:Math.floor(Date.now()/1e3)+t},r=JSON.stringify(n),i=f(`sha256`,this.secret).update(r).digest(`base64url`);return`${Buffer.from(r).toString(`base64url`)}.${i}`}async findSigned(e){if(!this.secret)throw Error(`Storage secret not configured`);try{let[t,n]=e.split(`.`);if(!t||!n)return null;let r=Buffer.from(t,`base64url`).toString(),i=f(`sha256`,this.secret).update(r).digest(`base64url`),a=Buffer.from(n,`base64url`),o=Buffer.from(i,`base64url`);if(a.length!==o.length||!m(a,o))return null;let s=JSON.parse(r);return s.exp&&s.exp<Math.floor(Date.now()/1e3)?null:this.getBlob(s.blobId)}catch{return null}}one(e,r,i){let d=this,f=i;return{async attach(n){if(!await d.getBlob(n))throw Error(`Blob ${n} not found`);let i=await d.createAttachment(e,r,n,f,!0);return await t.start({blobId:n}),i},async get(){return(await d.getAttachments(e,r,f))[0]?.blob??null},async attached(){return await this.get()!==null},async url(e){let t=await this.get();return t?d.getSignedUrl(t.id,e):null},async publicUrl(){let e=await this.get();return e?d.getPublicUrl(e.id):null},async metadata(){return(await this.get())?.metadata??null},async analyzed(){return(await this.metadata())?.analyzed===!0},async representable(){let e=await this.get();return e?.contentType?[`image/`,`video/`,`application/pdf`].some(t=>e.contentType?.startsWith(t)):!1},async variant(e,t=3600){let r=await this.get();if(!r)return null;let i=await d.getVariant(r.id,e);return i?d.getSignedUrl(i.id,{expiresIn:t}):(await n.start({blobId:r.id,transformations:e}),null)},async preview(e=3600,t=1){let n=await this.get();if(!n)return null;let r=await d.getVariant(n.id,{preview:!0});return r?d.getSignedUrl(r.id,{expiresIn:e}):(await a.start({blobId:n.id,timeInSeconds:t}),null)},async detach(){return await d.deleteAttachments(e,r,f)>0},async purge(){let t=await d.getAttachments(e,r,f);if(t.length===0)return!1;let n=t[0].blob;return n?(await d.deleteAttachments(e,r,f),await d.deleteBlob(n.id),!0):!1},async purgeLater(){let t=await d.getAttachments(e,r,f);return t.length===0?!1:(await o.start({attachmentIds:[t[0].id]}),!0)},async download(){let e=await this.get();return e?d.downloadBlob(e.id):null},async open(e){let t=await this.get();if(!t)return null;let n=await d.downloadBlob(t.id);if(!n)return null;let r=await c(s(h(),`attachment-`)),i=s(r,t.filename);try{return await u(i,n),await e(i)}finally{await l(r,{recursive:!0,force:!0})}},async representation(e,t=3600){let n=await this.get();return n?n.contentType?.startsWith(`image/`)?this.variant(e,t):this.preview(t):null},async byteSize(){return(await this.get())?.byteSize??null},async contentType(){return(await this.get())?.contentType??null},async filename(){return(await this.get())?.filename??null},async signedId(e=3600){let t=await this.get();return t?d.signedId(t.id,e):null}}}many(e,r,i){let d=this,f=i;return{async attach(n){let i=Array.isArray(n)?n:[n],a=await Promise.all(i.map(e=>d.getBlob(e))),o=i.filter((e,t)=>!a[t]);if(o.length>0)throw Error(`Blobs not found: ${o.join(`, `)}`);let s=await Promise.all(i.map(t=>d.createAttachment(e,r,t,f)));return await Promise.all(i.map(e=>t.start({blobId:e}))),s},async list(){return(await d.getAttachments(e,r,f)).map(e=>e.blob).filter(e=>e!==null)},async count(){return(await this.list()).length},async urls(e){let t=await this.list();return(await Promise.all(t.map(t=>d.getSignedUrl(t.id,e)))).filter(e=>e!==null)},async publicUrls(){let e=await this.list();return Promise.all(e.map(e=>d.getPublicUrl(e.id)))},async metadata(){return(await this.list()).map(e=>e.metadata??null)},async analyzed(){return(await this.metadata()).map(e=>e?.analyzed===!0)},async representable(){return(await this.list()).map(e=>e.contentType?[`image/`,`video/`,`application/pdf`].some(t=>e.contentType?.startsWith(t)):!1)},async variants(e,t=3600){let r=await this.list();return Promise.all(r.map(async r=>{let i=await d.getVariant(r.id,e);return i?d.getSignedUrl(i.id,{expiresIn:t}):(await n.start({blobId:r.id,transformations:e}),null)}))},async previews(e=3600,t=1){let n=await this.list();return Promise.all(n.map(async n=>{let r=await d.getVariant(n.id,{preview:!0});return r?d.getSignedUrl(r.id,{expiresIn:e}):(await a.start({blobId:n.id,timeInSeconds:t}),null)}))},async detach(t){if(t){let n=(await d.getAttachments(e,r,f)).find(e=>e.blob?.id===t);return n?(await d.deleteAttachment(n.id),1):0}return d.deleteAttachments(e,r,f)},async purge(t){let n=await d.getAttachments(e,r,f);if(t){let e=n.find(e=>e.blob?.id===t);return e?(await d.deleteAttachment(e.id),await d.deleteBlob(t),1):0}let i=0;for(let e of n)await d.deleteAttachment(e.id),e.blob&&(await d.deleteBlob(e.blob.id),i++);return i},async purgeLater(t){let n=await d.getAttachments(e,r,f);if(t){let e=n.find(e=>e.blob?.id===t);return e?(await o.start({attachmentIds:[e.id]}),1):0}return n.length>0&&await o.start({attachmentIds:n.map(e=>e.id)}),n.length},async download(){let e=await this.list(),t=[];for(let n of e){let e=await d.downloadBlob(n.id);e&&t.push(e)}return t},async open(e){let t=await this.list(),n=[];for(let r of t){let t=await d.downloadBlob(r.id);if(!t)continue;let i=await c(s(h(),`attachment-`)),a=s(i,r.filename);try{await u(a,t),n.push(await e(a,r))}finally{await l(i,{recursive:!0,force:!0})}}return n},async representations(e,t=3600){let r=await this.list();return Promise.all(r.map(async r=>{if(r.contentType?.startsWith(`image/`)){let i=await d.getVariant(r.id,e);return i?d.getSignedUrl(i.id,{expiresIn:t}):(await n.start({blobId:r.id,transformations:e}),null)}let i=await d.getVariant(r.id,{preview:!0});return i?d.getSignedUrl(i.id,{expiresIn:t}):(await a.start({blobId:r.id}),null)}))},async byteSizes(){return(await this.list()).map(e=>e.byteSize)},async contentTypes(){return(await this.list()).map(e=>e.contentType)},async filenames(){return(await this.list()).map(e=>e.filename)},async signedIds(e=3600){return(await this.list()).map(t=>d.signedId(t.id,e))}}}};export{w as StorageService,y as defineS3Disk,C as defineStorage};