@spinekit/media 0.1.1

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.
@@ -0,0 +1,131 @@
1
+ //#region src/config/media.defaults.d.ts
2
+ /**
3
+ * Commerce media DEFAULTS — folders, size variants, aspect ratios, processing.
4
+ *
5
+ * These are opinionated defaults for a commerce catalog, not universal truths, so every
6
+ * one is overridable. A fork that sells services instead of goods drops `products`; a
7
+ * fork that needs a `hero` ratio adds one. Nobody should have to edit package source to
8
+ * change a folder list.
9
+ *
10
+ * They live here rather than in a host because the SHAPE is shared: any commerce
11
+ * deployment wants product/category/banner/brand imagery with bounded variants. Only the
12
+ * values vary, and values are what `mergeMediaDefaults` takes.
13
+ */
14
+ /**
15
+ * `branding` holds document branding assets (invoice/report logos) — deliberately separate
16
+ * from `brands` (product manufacturer imagery), so a bill-logo picker can scope to it
17
+ * without wading through product marketing media.
18
+ */
19
+ declare const BASE_FOLDERS: readonly ['general', 'products', 'categories', 'blog', 'users', 'banners', 'brands', 'branding'];
20
+ /**
21
+ * Variants auto-generated per uploaded image. `width`/`height` are MAX BOUNDS — the aspect
22
+ * ratio is decided by the presets below, not here.
23
+ */
24
+ declare const SIZE_VARIANTS: readonly [{
25
+ readonly name: 'thumbnail';
26
+ readonly width: 200;
27
+ readonly height: 200;
28
+ readonly quality: 80;
29
+ readonly format: 'avif';
30
+ }, {
31
+ readonly name: 'medium';
32
+ readonly width: 800;
33
+ readonly height: 800;
34
+ readonly quality: 80;
35
+ readonly format: 'avif';
36
+ }];
37
+ /**
38
+ * Aspect-ratio presets by content type.
39
+ *
40
+ * `preserveRatio: true` scales to fit within the bounds and never crops or distorts.
41
+ * `aspectRatio` + `fit: 'cover'` crops to an exact ratio — used only for avatars, where a
42
+ * square is expected and a letterboxed face looks broken.
43
+ */
44
+ declare const ASPECT_RATIO_PRESETS: {
45
+ readonly default: {
46
+ readonly preserveRatio: true;
47
+ };
48
+ readonly product: {
49
+ readonly preserveRatio: true;
50
+ };
51
+ readonly category: {
52
+ readonly preserveRatio: true;
53
+ };
54
+ readonly banner: {
55
+ readonly preserveRatio: true;
56
+ };
57
+ readonly brand: {
58
+ readonly preserveRatio: true;
59
+ };
60
+ readonly avatar: {
61
+ readonly aspectRatio: 1;
62
+ readonly fit: 'cover';
63
+ };
64
+ };
65
+ /** Folder → content-type mapping for auto-detection when no `contentType` is supplied. */
66
+ declare const FOLDER_CONTENT_TYPE_MAP: {
67
+ readonly product: readonly ['products', 'product'];
68
+ readonly category: readonly ['categories', 'category'];
69
+ readonly banner: readonly ['banners', 'banner'];
70
+ readonly avatar: readonly ['users', 'avatars'];
71
+ readonly brand: readonly ['brands', 'brand'];
72
+ };
73
+ declare const IMAGE_SETTINGS: {
74
+ readonly defaultMaxWidth: 3840;
75
+ readonly quality: {
76
+ readonly jpeg: 85;
77
+ readonly webp: 85;
78
+ readonly avif: 80;
79
+ readonly png: 100;
80
+ };
81
+ readonly format: 'avif';
82
+ readonly generateAlt: {
83
+ readonly enabled: true;
84
+ readonly strategy: 'filename';
85
+ readonly fallback: 'Image';
86
+ };
87
+ /**
88
+ * What may be STORED. Permissive by default — the host owns this list.
89
+ *
90
+ * SVG is included. It is worth knowing what that means so the choice is an
91
+ * informed one rather than an accident: an SVG is executable markup, so one
92
+ * served from an origin that shares cookies with the app can run script with
93
+ * the viewer's session. The presigned path cannot rule it out either — the
94
+ * server never sees the bytes, and the MIME it cross-checks against storage
95
+ * is the one the same client supplied during its signed PUT.
96
+ *
97
+ * That is a deployment decision, not a package one, and it has ordinary
98
+ * answers: serve media from a cookie-free origin, send
99
+ * `Content-Disposition: attachment`, or sanitise on upload. A deployment that
100
+ * wants a stricter set overrides `imageSettings.allowedMimeTypes` — brand
101
+ * marks and icon sets are commonly SVG, so removing it here would break real
102
+ * uploads to enforce a posture the host may already handle elsewhere.
103
+ *
104
+ * SEPARATE and NOT a policy: whether a format can be re-encoded without
105
+ * destroying it. SVG and animated GIF cannot (rasterising loses the vector;
106
+ * a one-frame re-encode loses the animation), so both are excluded from the
107
+ * client TRANSFORM path in `@classytic/media-transform/policy`. That is a
108
+ * fact about the format, which the package owns — being storable and being
109
+ * transformable are different questions.
110
+ */
111
+ readonly allowedMimeTypes: readonly ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/avif'];
112
+ readonly maxSize: number;
113
+ };
114
+ interface MediaDefaults {
115
+ baseFolders: readonly string[];
116
+ sizeVariants: readonly Record<string, unknown>[];
117
+ aspectRatioPresets: Record<string, unknown>;
118
+ folderContentTypeMap: Record<string, readonly string[]>;
119
+ imageSettings: Record<string, unknown>;
120
+ }
121
+ declare const MEDIA_DEFAULTS: MediaDefaults;
122
+ /**
123
+ * Merge a deployment's overrides over the defaults.
124
+ *
125
+ * Shallow PER KEY on purpose: a fork that supplies `baseFolders` means *these folders*,
126
+ * not "these in addition to mine". Deep-merging a list would make removing a default
127
+ * folder impossible, which is the main reason a fork overrides it at all.
128
+ */
129
+ declare function mergeMediaDefaults(overrides?: Partial<MediaDefaults>): MediaDefaults;
130
+ //#endregion
131
+ export { ASPECT_RATIO_PRESETS, BASE_FOLDERS, FOLDER_CONTENT_TYPE_MAP, IMAGE_SETTINGS, MEDIA_DEFAULTS, MediaDefaults, SIZE_VARIANTS, mergeMediaDefaults };
@@ -0,0 +1,145 @@
1
+ //#region src/config/media.defaults.ts
2
+ /**
3
+ * Commerce media DEFAULTS — folders, size variants, aspect ratios, processing.
4
+ *
5
+ * These are opinionated defaults for a commerce catalog, not universal truths, so every
6
+ * one is overridable. A fork that sells services instead of goods drops `products`; a
7
+ * fork that needs a `hero` ratio adds one. Nobody should have to edit package source to
8
+ * change a folder list.
9
+ *
10
+ * They live here rather than in a host because the SHAPE is shared: any commerce
11
+ * deployment wants product/category/banner/brand imagery with bounded variants. Only the
12
+ * values vary, and values are what `mergeMediaDefaults` takes.
13
+ */
14
+ /**
15
+ * `branding` holds document branding assets (invoice/report logos) — deliberately separate
16
+ * from `brands` (product manufacturer imagery), so a bill-logo picker can scope to it
17
+ * without wading through product marketing media.
18
+ */
19
+ const BASE_FOLDERS = [
20
+ "general",
21
+ "products",
22
+ "categories",
23
+ "blog",
24
+ "users",
25
+ "banners",
26
+ "brands",
27
+ "branding"
28
+ ];
29
+ /**
30
+ * Variants auto-generated per uploaded image. `width`/`height` are MAX BOUNDS — the aspect
31
+ * ratio is decided by the presets below, not here.
32
+ */
33
+ const SIZE_VARIANTS = [{
34
+ name: "thumbnail",
35
+ width: 200,
36
+ height: 200,
37
+ quality: 80,
38
+ format: "avif"
39
+ }, {
40
+ name: "medium",
41
+ width: 800,
42
+ height: 800,
43
+ quality: 80,
44
+ format: "avif"
45
+ }];
46
+ /**
47
+ * Aspect-ratio presets by content type.
48
+ *
49
+ * `preserveRatio: true` scales to fit within the bounds and never crops or distorts.
50
+ * `aspectRatio` + `fit: 'cover'` crops to an exact ratio — used only for avatars, where a
51
+ * square is expected and a letterboxed face looks broken.
52
+ */
53
+ const ASPECT_RATIO_PRESETS = {
54
+ default: { preserveRatio: true },
55
+ product: { preserveRatio: true },
56
+ category: { preserveRatio: true },
57
+ banner: { preserveRatio: true },
58
+ brand: { preserveRatio: true },
59
+ avatar: {
60
+ aspectRatio: 1,
61
+ fit: "cover"
62
+ }
63
+ };
64
+ /** Folder → content-type mapping for auto-detection when no `contentType` is supplied. */
65
+ const FOLDER_CONTENT_TYPE_MAP = {
66
+ product: ["products", "product"],
67
+ category: ["categories", "category"],
68
+ banner: ["banners", "banner"],
69
+ avatar: ["users", "avatars"],
70
+ brand: ["brands", "brand"]
71
+ };
72
+ const IMAGE_SETTINGS = {
73
+ defaultMaxWidth: 3840,
74
+ quality: {
75
+ jpeg: 85,
76
+ webp: 85,
77
+ avif: 80,
78
+ png: 100
79
+ },
80
+ format: "avif",
81
+ generateAlt: {
82
+ enabled: true,
83
+ strategy: "filename",
84
+ fallback: "Image"
85
+ },
86
+ /**
87
+ * What may be STORED. Permissive by default — the host owns this list.
88
+ *
89
+ * SVG is included. It is worth knowing what that means so the choice is an
90
+ * informed one rather than an accident: an SVG is executable markup, so one
91
+ * served from an origin that shares cookies with the app can run script with
92
+ * the viewer's session. The presigned path cannot rule it out either — the
93
+ * server never sees the bytes, and the MIME it cross-checks against storage
94
+ * is the one the same client supplied during its signed PUT.
95
+ *
96
+ * That is a deployment decision, not a package one, and it has ordinary
97
+ * answers: serve media from a cookie-free origin, send
98
+ * `Content-Disposition: attachment`, or sanitise on upload. A deployment that
99
+ * wants a stricter set overrides `imageSettings.allowedMimeTypes` — brand
100
+ * marks and icon sets are commonly SVG, so removing it here would break real
101
+ * uploads to enforce a posture the host may already handle elsewhere.
102
+ *
103
+ * SEPARATE and NOT a policy: whether a format can be re-encoded without
104
+ * destroying it. SVG and animated GIF cannot (rasterising loses the vector;
105
+ * a one-frame re-encode loses the animation), so both are excluded from the
106
+ * client TRANSFORM path in `@classytic/media-transform/policy`. That is a
107
+ * fact about the format, which the package owns — being storable and being
108
+ * transformable are different questions.
109
+ */
110
+ allowedMimeTypes: [
111
+ "image/jpeg",
112
+ "image/jpg",
113
+ "image/png",
114
+ "image/webp",
115
+ "image/gif",
116
+ "image/svg+xml",
117
+ "image/avif"
118
+ ],
119
+ maxSize: 52428800
120
+ };
121
+ const MEDIA_DEFAULTS = {
122
+ baseFolders: BASE_FOLDERS,
123
+ sizeVariants: SIZE_VARIANTS,
124
+ aspectRatioPresets: ASPECT_RATIO_PRESETS,
125
+ folderContentTypeMap: FOLDER_CONTENT_TYPE_MAP,
126
+ imageSettings: IMAGE_SETTINGS
127
+ };
128
+ /**
129
+ * Merge a deployment's overrides over the defaults.
130
+ *
131
+ * Shallow PER KEY on purpose: a fork that supplies `baseFolders` means *these folders*,
132
+ * not "these in addition to mine". Deep-merging a list would make removing a default
133
+ * folder impossible, which is the main reason a fork overrides it at all.
134
+ */
135
+ function mergeMediaDefaults(overrides = {}) {
136
+ return {
137
+ baseFolders: overrides.baseFolders ?? MEDIA_DEFAULTS.baseFolders,
138
+ sizeVariants: overrides.sizeVariants ?? MEDIA_DEFAULTS.sizeVariants,
139
+ aspectRatioPresets: overrides.aspectRatioPresets ?? MEDIA_DEFAULTS.aspectRatioPresets,
140
+ folderContentTypeMap: overrides.folderContentTypeMap ?? MEDIA_DEFAULTS.folderContentTypeMap,
141
+ imageSettings: overrides.imageSettings ?? MEDIA_DEFAULTS.imageSettings
142
+ };
143
+ }
144
+ //#endregion
145
+ export { ASPECT_RATIO_PRESETS, BASE_FOLDERS, FOLDER_CONTENT_TYPE_MAP, IMAGE_SETTINGS, MEDIA_DEFAULTS, SIZE_VARIANTS, mergeMediaDefaults };
@@ -0,0 +1,243 @@
1
+ import { ResourceSeams } from "@classytic/arc";
2
+ import { EngineSlot } from "@spinekit/kit/engine-slot";
3
+ import { MediaTransformPolicy } from "@classytic/media-transform/policy";
4
+ import { CompleteMultipartInput, ConfirmUploadInput, IMediaDocument, MediaContext, MediaEngine, MultipartUploadSession, MultipartUploadSession as MultipartUploadSession$1, PresignedUploadResult, PresignedUploadResult as PresignedUploadResult$1 } from "@classytic/media-kit";
5
+ import { ArcModule } from "@classytic/arc/factory";
6
+ import { ScheduleDefinition } from "@classytic/arc/plugins";
7
+ import { CleanupStep } from "@classytic/repo-core/cleanup";
8
+ //#region src/cleanup.d.ts
9
+ interface MediaCleanupStepsOptions {
10
+ /**
11
+ * Cutoff for the soft-delete purge. Omitted → the kernel's own default
12
+ * (`softDelete.ttlDays`, 30 days). Passing a NARROWER window is the
13
+ * supported way to bound a single run's blast radius.
14
+ */
15
+ deletedOlderThan?: Date;
16
+ /** Cutoff for stale `pending`/`deleting` rows. Omitted → kernel default. */
17
+ stalePendingOlderThan?: Date;
18
+ /** Expiry horizon. Omitted → "now" (everything already expired). */
19
+ expiredBefore?: Date;
20
+ /** Tenant/actor context handed to the kernel verbs. */
21
+ ctx?: MediaContext;
22
+ }
23
+ /**
24
+ * The three media purge steps, in the order a full cleanup should run them:
25
+ * stale uploads first (cheapest, unblocks nothing), then expired assets, then
26
+ * the soft-delete backlog.
27
+ */
28
+ declare function mediaCleanupSteps(engine: Pick<MediaEngine, 'repositories'>, options?: MediaCleanupStepsOptions): CleanupStep[];
29
+ //#endregion
30
+ //#region src/index.d.ts
31
+ /** An arc permission gate — sync/async, boolean or PermissionResult. */
32
+ type PermissionGate = import('@classytic/arc/permissions').PermissionCheck;
33
+ interface MediaPermissions {
34
+ /** List/read media + mint signed read URLs. */
35
+ view: PermissionGate;
36
+ /** Start/complete uploads (the two-phase flow). */
37
+ upload: PermissionGate;
38
+ /** Delete (soft per engine config) and other mutations. */
39
+ manage: PermissionGate;
40
+ }
41
+ declare function createMediaRolePermissions(input: {
42
+ view: readonly string[];
43
+ upload: readonly string[];
44
+ manage: readonly string[];
45
+ }): MediaPermissions;
46
+ declare function createMediaEngineSlot<TEngine extends MediaEngineLike = MediaEngineLike>(): EngineSlot<TEngine>;
47
+ /**
48
+ * The slice of a `@classytic/media-kit` engine this module needs — typed
49
+ * with the kernel's OWN bags (the kernel is already a peer, so this adds no
50
+ * new coupling; the 2.21 kernel-port standard across the spine).
51
+ */
52
+ interface MediaEngineLike {
53
+ /**
54
+ * The ONE teardown name (STANDARDIZATION-PLAN §6.4). Declared so `closeOwned`
55
+ * cannot be written against an engine with no way to be closed.
56
+ */
57
+ close(): void | Promise<void>;
58
+ config: Pick<MediaEngine['config'], 'tenant'>;
59
+ models: MediaEngine['models'];
60
+ repositories: MediaEngine['repositories'];
61
+ }
62
+ /** Wire body for `POST /media/start-write`. */
63
+ interface StartWriteBody {
64
+ filename: string;
65
+ contentType: string;
66
+ /** Total bytes — drives the multipart threshold decision. */
67
+ size?: number;
68
+ folder?: string;
69
+ /**
70
+ * Client-computed content hash (e.g. media-transform's `sha256Hex`).
71
+ * A tenant-scoped hash hit short-circuits: no upload happens at all.
72
+ */
73
+ sha256?: string;
74
+ /** Force a multipart/resumable session regardless of size. */
75
+ multipart?: boolean;
76
+ /** Sign all parts up-front (S3 multipart). */
77
+ partCount?: number;
78
+ /** Presigned URL validity in seconds. */
79
+ expiresIn?: number;
80
+ /**
81
+ * Sizes the client produced from the SAME decode and intends to upload.
82
+ *
83
+ * Requested here rather than in a second round trip because the client
84
+ * already knows them: the transform policy told it which to make, and it made
85
+ * them before asking for anywhere to put them.
86
+ */
87
+ derivatives?: Array<{
88
+ name: string;
89
+ contentType: string;
90
+ size?: number;
91
+ }>;
92
+ }
93
+ /** Discriminated result of `POST /media/start-write`. */
94
+ type StartWriteResult = {
95
+ kind: 'dedup';
96
+ media: IMediaDocument;
97
+ } | {
98
+ kind: 'presigned';
99
+ upload: PresignedUploadResult$1;
100
+ /**
101
+ * One presigned PUT per requested derivative, keyed by variant name.
102
+ * Absent when none were requested. Keys are DERIVED server-side from the
103
+ * primary's — a client never names its own storage location.
104
+ */
105
+ derivativeUploads?: Array<{
106
+ name: string;
107
+ upload: PresignedUploadResult$1;
108
+ }>;
109
+ } | {
110
+ kind: 'multipart' | 'resumable';
111
+ session: MultipartUploadSession$1;
112
+ };
113
+ /**
114
+ * Explicit engine ownership (STANDARDIZATION-PLAN §7.3) — identical vocabulary
115
+ * to `@spinekit/catalog`, deliberately: two spine modules inventing two ways to
116
+ * say "who closes this" is how a fleet ends up with two lifecycle stories.
117
+ */
118
+ type MediaEngineSource<TEngine> = {
119
+ readonly kind: 'blueprint';
120
+ readonly blueprint: {
121
+ bind(connection: unknown, runtime: never): TEngine | Promise<TEngine>;
122
+ };
123
+ readonly connection: unknown;
124
+ /** Runtime collaborators, or a thunk — building a storage driver is live work. */
125
+ readonly runtime: unknown | (() => unknown | Promise<unknown>);
126
+ } | {
127
+ readonly kind: 'external';
128
+ readonly engine: TEngine;
129
+ };
130
+ interface MediaResourceDeps<TEngine extends MediaEngineLike = MediaEngineLike> {
131
+ /**
132
+ * The bound engine.
133
+ *
134
+ * Required when building a resource directly; the MODULE supplies it from
135
+ * its slot, so a module caller passes `source` instead.
136
+ */
137
+ engine: TEngine;
138
+ permissions: MediaPermissions;
139
+ /**
140
+ * The client-transform policy served at `GET {prefix}/transform-policy`.
141
+ *
142
+ * Defaults to `buildTransformPolicy()`, which DERIVES it from this package's
143
+ * media shape (size variants, avatar folders, image settings) so the rule the
144
+ * client follows and the variants the server declares cannot drift apart.
145
+ * Pass one to state deployment specifics — most importantly
146
+ * `untouchedFolders`, since only the deployment knows which of its folders
147
+ * hold documents rather than photographs.
148
+ */
149
+ transformPolicy?: MediaTransformPolicy;
150
+ /** Route prefix. Default `/media`. */
151
+ prefix?: string;
152
+ /**
153
+ * `start-write` returns a multipart/resumable session (instead of a single
154
+ * presigned PUT) when `size` meets this threshold. Default 100 MiB.
155
+ */
156
+ multipartThresholdBytes?: number;
157
+ /**
158
+ * Host seams merged via arc's slot-aware `mergeResourceConfig` (2.21) — a
159
+ * value, or a FACTORY taking the bound engine.
160
+ *
161
+ * The factory form exists because host seams routinely wrap the engine's own
162
+ * repositories (be-prod's extension routes take `repositories.media`), and
163
+ * under a `blueprint` source the engine does not exist at composition. Arc
164
+ * evaluates this in the resource phase.
165
+ */
166
+ seams?: ResourceSeams | ((engine: TEngine) => ResourceSeams);
167
+ }
168
+ interface MediaModuleDeps<TEngine extends MediaEngineLike = MediaEngineLike> extends Omit<MediaResourceDeps<TEngine>, 'engine'> {
169
+ /**
170
+ * Where the engine comes from, and therefore WHO CLOSES IT. Prefer
171
+ * `blueprint` — the module then binds at bootstrap and the host keeps no
172
+ * engine singleton at all.
173
+ */
174
+ source?: MediaEngineSource<TEngine>;
175
+ /** Pre-built engine (equivalent to `source: { kind: 'external' }`). */
176
+ engine?: TEngine;
177
+ /**
178
+ * Slot shared with the host, so host code can read the bound engine without
179
+ * a hand-rolled ensure/get/clear trio. spine-kit owns the primitive.
180
+ */
181
+ slot?: EngineSlot<TEngine>;
182
+ /** Post-registration wiring the HOST owns (event subscriptions, etc.). */
183
+ /**
184
+ * Derived from arc rather than re-declared. Arc's `afterResources` may return
185
+ * a `ModuleDisposer` so the wiring it sets up can be torn down; a hand-written
186
+ * `=> void` signature silently stops matching the moment that contract moves,
187
+ * which is exactly how this drifted.
188
+ */
189
+ afterResources?: ArcModule['afterResources'];
190
+ /**
191
+ * Maintenance sweep intervals. Omit for the sane defaults (hourly stale-pending
192
+ * + expired, daily soft-delete purge). Pass `false` to opt OUT of the arm
193
+ * entirely (a host running its own maintenance runner). See
194
+ * `mediaMaintenanceSchedules`.
195
+ */
196
+ maintenance?: MediaMaintenanceOptions | false;
197
+ dependsOn?: readonly string[];
198
+ }
199
+ declare function createMediaResource(deps: MediaResourceDeps): import("@classytic/arc").ResourceDefinition<import("@classytic/arc").AnyRecord>;
200
+ declare function createMediaModule<TEngine extends MediaEngineLike>(deps: MediaModuleDeps<TEngine>): ArcModule<unknown>;
201
+ interface MediaMaintenanceOptions {
202
+ /** Sweep pending docs older than the kernel's 24h cutoff. Default hourly. */
203
+ stalePendingEvery?: number;
204
+ /** Purge soft-deleted docs past their TTL. Default daily. */
205
+ purgeDeletedEvery?: number;
206
+ /** Purge time-bound (`expiresAt`) assets. Default hourly. */
207
+ purgeExpiredEvery?: number;
208
+ }
209
+ /**
210
+ * Maintenance schedule definitions for arc's `schedulesPlugin` — drop into
211
+ * the host's schedule table (with a `lock` for multi-replica leader safety):
212
+ *
213
+ * ```ts
214
+ * await app.register(schedulesPlugin, {
215
+ * lock: createMongoLockAdapter({ connection }),
216
+ * schedules: [...mediaMaintenanceSchedules(engine), ...otherJobs],
217
+ * });
218
+ * ```
219
+ *
220
+ * ## What these sweeps do NOT cover — read before relying on them
221
+ *
222
+ * An abandoned TWO-PHASE upload leaves **no database row at all**. `start-write`
223
+ * only signs a URL; the record is created by `complete-write`. So a client that
224
+ * presigns and then never confirms leaves an object in the bucket that nothing
225
+ * here can see, let alone reclaim.
226
+ *
227
+ * This docblock previously said those uploads "leave `pending` docs that these
228
+ * sweeps reclaim", and treated bucket lifecycle as belt-and-braces. That was
229
+ * backwards, and it is the dangerous direction to be wrong in: a deployment
230
+ * reading it would skip the lifecycle rule and accumulate unreferenced objects
231
+ * it is still paying to store, with a maintenance job reporting success the
232
+ * whole time.
233
+ *
234
+ * **A bucket lifecycle expiry on incomplete/unreferenced keys is REQUIRED, not
235
+ * optional.** See media-kit's README ("Orphaned storage objects").
236
+ *
237
+ * What these sweeps genuinely reclaim: rows that DO exist and are stuck —
238
+ * `pending` records from the buffered upload path, soft-deleted docs past their
239
+ * retention window, and expired assets.
240
+ */
241
+ declare function mediaMaintenanceSchedules(engine: Pick<MediaEngineLike, 'repositories'>, options?: MediaMaintenanceOptions): ScheduleDefinition[];
242
+ //#endregion
243
+ export { type CompleteMultipartInput, type ConfirmUploadInput, type MediaCleanupStepsOptions, MediaEngineLike, MediaEngineSource, MediaMaintenanceOptions, MediaModuleDeps, MediaPermissions, MediaResourceDeps, type MultipartUploadSession, PermissionGate, type PresignedUploadResult, StartWriteBody, StartWriteResult, createMediaEngineSlot, createMediaModule, createMediaResource, createMediaRolePermissions, mediaCleanupSteps, mediaMaintenanceSchedules };