@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.
- package/CHANGELOG.md +44 -0
- package/LICENSE +75 -0
- package/README.md +82 -0
- package/dist/attachments.d.mts +66 -0
- package/dist/attachments.mjs +118 -0
- package/dist/config/media.config.d.mts +96 -0
- package/dist/config/media.config.mjs +82 -0
- package/dist/config/media.defaults.d.mts +131 -0
- package/dist/config/media.defaults.mjs +145 -0
- package/dist/index.d.mts +243 -0
- package/dist/index.mjs +628 -0
- package/dist/providers/media-provider.config.d.mts +66 -0
- package/dist/providers/media-provider.config.mjs +97 -0
- package/dist/providers/memory.provider.d.mts +5 -0
- package/dist/providers/memory.provider.mjs +68 -0
- package/dist/resources/media/media.buffered-upload.d.mts +47 -0
- package/dist/resources/media/media.buffered-upload.mjs +76 -0
- package/dist/resources/media/media.routes.d.mts +28 -0
- package/dist/resources/media/media.routes.mjs +203 -0
- package/dist/resources/media/media.schema.d.mts +62 -0
- package/dist/resources/media/media.schema.mjs +54 -0
- package/package.json +119 -0
|
@@ -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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|