@adonis-agora/media 0.12.1 → 0.13.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.
@@ -0,0 +1,334 @@
1
+ ---
2
+ name: media-library-collections
3
+ description: >-
4
+ Attach files to owner collections with @adonis-agora/media — media.library.attach /
5
+ attachExisting / for(ownerType, ownerId), the MediaCollectionConfig policy layer
6
+ (acceptsMimeTypes exact-match whitelist plus magic-byte signature verification,
7
+ single:true atomic replace, append-only ordering, per-collection disk), eager vs lazy
8
+ ConversionPresets (width/height/fit/format/quality, webp default), ensureConversion,
9
+ url/signedUrl resolution, delete semantics, deterministic id retries, customProperties,
10
+ and the MediaRecord.conversions map shared with transformers. Use when uploading files
11
+ to entities, defining collections, generating thumbnails, or fixing
12
+ MimeNotAllowedError / ConversionNotDefinedError / ImageProcessorMissingError /
13
+ TransformNotReadyError.
14
+ license: MIT
15
+ metadata:
16
+ type: core
17
+ library: "@adonis-agora/media"
18
+ library_version: "0.12.1"
19
+ framework: adonisjs
20
+ sources:
21
+ - "DavideCarvalho/adonis-media:docs/getting-started.mdx"
22
+ - "DavideCarvalho/adonis-media:docs/collections-and-conversions.mdx"
23
+ - "DavideCarvalho/adonis-media:docs/transformers.mdx"
24
+ - "DavideCarvalho/adonis-media:packages/adonis/src/media_library.ts"
25
+ ---
26
+
27
+ # Media library: collections, policy, and conversions
28
+
29
+ `media.library` is the spatie/laravel-media-library shape: files belong to an owner
30
+ (`ownerType` + `ownerId`, both just stored strings — the library never loads your models)
31
+ inside named collections that carry the policy. Attach writes bytes to a Drive disk and a
32
+ metadata record to the configured `MediaStore`.
33
+
34
+ ## Setup
35
+
36
+ ```ts title="app/controllers/post_images_controller.ts"
37
+ import { createReadStream } from 'node:fs'
38
+ import Post from '#models/post'
39
+ import media from '@adonis-agora/media/services/main'
40
+ import type { HttpContext } from '@adonisjs/core/http'
41
+
42
+ export default class PostImagesController {
43
+ async store({ request, response, params }: HttpContext) {
44
+ const post = await Post.findOrFail(params.id)
45
+
46
+ const image = request.file('image', { size: '5mb', extnames: ['jpg', 'png', 'webp'] })
47
+ if (!image) return response.badRequest({ error: 'An "image" file is required' })
48
+
49
+ const record = await media.library.attach({
50
+ ownerType: 'Post',
51
+ ownerId: post.id, // string | number — coerced for you
52
+ collection: 'gallery',
53
+ fileName: image.clientName,
54
+ mimeType: image.type!,
55
+ contents: createReadStream(image.tmpPath!), // Buffer | Readable
56
+ })
57
+
58
+ return response.created({ id: record.id, url: await media.library.url(record.id) })
59
+ }
60
+ }
61
+ ```
62
+
63
+ The file lands at `${ownerType}/${ownerId}/${collection}/${id}/${fileName}` on the resolved
64
+ disk (per-call `disk` → collection `disk` → storage default). Eager conversions are
65
+ generated synchronously inside `attach`; lazy ones wait for the first `url(id, name)`.
66
+
67
+ Source: `docs/getting-started.mdx` (Step 3).
68
+
69
+ ## Core patterns
70
+
71
+ ### Pattern 1 — declare the collection policy in config
72
+
73
+ Collections are opt-in constraints: an undeclared collection is permissive (no whitelist,
74
+ not single, no conversions). Declare one to constrain it:
75
+
76
+ ```ts title="config/media.ts"
77
+ defineConfig({
78
+ collections: [
79
+ { name: 'avatar', single: true, acceptsMimeTypes: ['image/png', 'image/jpeg'] },
80
+ { name: 'invoices', disk: 'private-s3', acceptsMimeTypes: ['application/pdf'] },
81
+ {
82
+ name: 'gallery',
83
+ conversions: [
84
+ { name: 'thumb', width: 200, height: 200, fit: 'cover' },
85
+ { name: 'card', width: 600, format: 'webp', quality: 80 },
86
+ { name: 'og', width: 1200, height: 630, eager: true },
87
+ ],
88
+ },
89
+ ],
90
+ })
91
+ ```
92
+
93
+ `acceptsMimeTypes` is checked **before anything is written** (no orphan files on rejection)
94
+ and is an exact string match — list each type. When a whitelist is present the library also
95
+ verifies the file's magic-byte signature: a mismatch throws `ContentTypeMismatchError`, and
96
+ on a **closed** whitelist (every accepted type is signature-detectable, e.g.
97
+ `['application/pdf']`) unrecognizable bytes throw `ContentSignatureUnrecognizedError`. An
98
+ **open** whitelist (any type without a signature, e.g. `['application/pdf', 'image/svg+xml']`)
99
+ falls back to the declared type.
100
+
101
+ Source: `docs/collections-and-conversions.mdx`.
102
+
103
+ ### Pattern 2 — eager vs lazy conversions
104
+
105
+ `ConversionPreset` maps straight onto sharp. `eager: true` generates synchronously on
106
+ `attach`; the default is lazy — generated on the **first** `url(id, name)` and then cached on
107
+ the record, so every later call is a pure lookup.
108
+
109
+ ```ts
110
+ const m = await media.library.attach({ ...input, collection: 'gallery' })
111
+
112
+ await media.library.url(m.id, 'og') // eager → already on disk, pure lookup
113
+ await media.library.url(m.id, 'thumb') // lazy → generated NOW, then cached
114
+ await media.library.ensureConversion(m.id, 'thumb') // force it ahead of time (idempotent)
115
+ ```
116
+
117
+ Conversions are written under `conversions/` next to the original, recorded on
118
+ `record.conversions` keyed by preset name, and removed by `delete()` together with the
119
+ original. `format` defaults to `webp`; `fit` defaults to `cover`.
120
+
121
+ Source: `docs/collections-and-conversions.mdx` (Eager vs lazy).
122
+
123
+ ### Pattern 3 — bind an owner once with `for()`, adopt existing objects with `attachExisting`
124
+
125
+ ```ts
126
+ const gallery = media.library.for('Post', post.id)
127
+
128
+ for (const image of images) {
129
+ await gallery.attach({ collection: 'gallery', fileName: image.clientName, mimeType: image.type!, contents })
130
+ }
131
+
132
+ // zero-copy adoption of an object ALREADY on the disk (finished TUS / direct upload):
133
+ await gallery.attachExisting({
134
+ collection: 'gallery',
135
+ key: 'uploads/9f2a/clip.mp4',
136
+ fileName: 'clip.mp4',
137
+ mimeType: 'video/mp4',
138
+ })
139
+ ```
140
+
141
+ `attachExisting` never reads or rewrites bytes; the size comes from a disk HEAD when
142
+ omitted, and the collection's MIME + signature checks still run against a short head read.
143
+ Pass `moveIntoLayout: true` to relocate into the canonical layout using the disk's native
144
+ server-side move.
145
+
146
+ Source: `docs/getting-started.mdx` (Bind an owner once), `docs/uploads/resumable-tus.mdx`.
147
+
148
+ ### Pattern 4 — deterministic `id` for retry-safe overwrites
149
+
150
+ ```ts
151
+ await media.library.attach({
152
+ ownerType: 'Invoice',
153
+ ownerId: invoice.id,
154
+ collection: 'pdf',
155
+ id: `invoice-${invoice.id}`, // fixed key → retry-safe overwrite, no orphaned UUIDs
156
+ fileName: 'invoice.pdf',
157
+ mimeType: 'application/pdf',
158
+ contents: renderedPdf,
159
+ })
160
+ ```
161
+
162
+ The `id` is used verbatim as the record key and the storage-key segment — you own
163
+ uniqueness. Retries overwrite in place instead of minting a fresh UUID path.
164
+
165
+ Source: `docs/getting-started.mdx` (Idempotent overwrites).
166
+
167
+ ## Common mistakes
168
+
169
+ ### [CRITICAL] Assuming attach replaces an existing file without single:true
170
+
171
+ Wrong:
172
+
173
+ ```ts
174
+ // 'avatar' declared WITHOUT single: true — each call appends another file
175
+ await media.library.attach({ ownerType: 'User', ownerId: '1', collection: 'avatar', ... }) // v1
176
+ await media.library.attach({ ownerType: 'User', ownerId: '1', collection: 'avatar', ... }) // v2
177
+ await media.library.list('User', '1', 'avatar') // [v1, v2] — "the avatar" is now a list
178
+ ```
179
+
180
+ Correct:
181
+
182
+ ```ts
183
+ // config/media.ts
184
+ defineConfig({ collections: [{ name: 'avatar', single: true, acceptsMimeTypes: ['image/png'] }] })
185
+ ```
186
+
187
+ Mechanism: replacement is a property of the **collection**, not the call. `single: true`
188
+ deletes the previous file (original + conversions) only AFTER the new one is safely
189
+ persisted, so a failed upload leaves the old media intact; without it ordering is
190
+ append-only and every attach takes the next slot.
191
+ Source: `docs/collections-and-conversions.mdx` (Single-file collections), `docs/single-file.mdx` (warn callout).
192
+
193
+ ### [HIGH] Using wildcard MIME types in acceptsMimeTypes
194
+
195
+ Wrong:
196
+
197
+ ```ts
198
+ defineConfig({
199
+ collections: [{ name: 'images', acceptsMimeTypes: ['image/*'] }],
200
+ })
201
+ ```
202
+
203
+ Correct:
204
+
205
+ ```ts
206
+ defineConfig({
207
+ collections: [{ name: 'images', acceptsMimeTypes: ['image/png', 'image/jpeg', 'image/webp'] }],
208
+ })
209
+ ```
210
+
211
+ Mechanism: the whitelist is an exact string match against the declared `mimeType` — there is
212
+ no wildcard expansion, so `'image/*'` matches nothing and every attach throws
213
+ `MimeNotAllowedError` (`E_MEDIA_MIME_NOT_ALLOWED`) before anything is written.
214
+ Source: `docs/collections-and-conversions.mdx` (MIME whitelist).
215
+
216
+ ### [HIGH] Requesting conversions with no imageProcessor configured
217
+
218
+ Wrong:
219
+
220
+ ```ts
221
+ defineConfig({
222
+ collections: [{ name: 'gallery', conversions: [{ name: 'thumb', width: 200 }] }],
223
+ // no imageProcessor
224
+ })
225
+ ```
226
+
227
+ Correct:
228
+
229
+ ```ts
230
+ import { defineConfig, processors } from '@adonis-agora/media'
231
+
232
+ defineConfig({
233
+ imageProcessor: processors.sharp(),
234
+ collections: [{ name: 'gallery', conversions: [{ name: 'thumb', width: 200 }] }],
235
+ })
236
+ ```
237
+
238
+ Mechanism: `imageProcessor` is required only when conversions exist, but asking for one
239
+ without a configured processor throws `ImageProcessorMissingError`
240
+ (`E_MEDIA_IMAGE_PROCESSOR_MISSING`) instead of silently skipping.
241
+ Source: `docs/configuration.mdx` (`imageProcessor`), `docs/errors.mdx`.
242
+
243
+ ### [CRITICAL] Reading url() on a transformer conversion expecting lazy generation
244
+
245
+ Wrong:
246
+
247
+ ```ts
248
+ // 'videos' collection declares transformers.hls()
249
+ const url = await media.library.url(videoId, 'hls') // throws before any transform ran
250
+ ```
251
+
252
+ Correct:
253
+
254
+ ```ts
255
+ // run the transform explicitly (usually from a job), then read
256
+ await media.library.transform(videoId, 'hls') // idempotent — retries skip through
257
+ const record = await media.library.find(videoId)
258
+ record!.conversions.hls // { path, prefix, files, meta }
259
+ ```
260
+
261
+ Mechanism: image presets generate lazily on first read, but reads NEVER generate transforms —
262
+ `url()`/`deliver()` throw `TransformNotReadyError` so a request never stalls behind a remux.
263
+ The split is deliberate.
264
+ Source: `docs/transformers.mdx` (Reads never generate a transform).
265
+
266
+ ### [HIGH] Declaring conversion presets on a non-image collection
267
+
268
+ Wrong:
269
+
270
+ ```ts
271
+ defineConfig({
272
+ collections: [
273
+ { name: 'videos', acceptsMimeTypes: ['video/mp4'], conversions: [{ name: 'thumb', width: 320 }] },
274
+ ],
275
+ })
276
+ ```
277
+
278
+ Correct:
279
+
280
+ ```ts
281
+ import { defineConfig, transformers } from '@adonis-agora/media'
282
+
283
+ defineConfig({
284
+ collections: [
285
+ {
286
+ name: 'videos',
287
+ acceptsMimeTypes: ['video/mp4'],
288
+ transformers: [transformers.probe({ eager: true })], // non-image derivations
289
+ },
290
+ ],
291
+ })
292
+ ```
293
+
294
+ Mechanism: presets decode the original as an image through sharp; requesting one on a video
295
+ or PDF fails inside sharp. Transformers are the seam for everything beyond images and
296
+ persist into the same `record.conversions` namespace (name collisions throw
297
+ `TransformerConflictError` at boot).
298
+ Source: `docs/collections-and-conversions.mdx` (warn callout), `docs/transformers.mdx`.
299
+
300
+ ### [HIGH] Trusting the declared mimeType as the real content type
301
+
302
+ Wrong:
303
+
304
+ ```ts
305
+ // client renames payload.png → report.pdf and declares 'application/pdf'
306
+ await media.library.attach({
307
+ ownerType: 'Patient', ownerId: '7', collection: 'exams',
308
+ fileName: 'scan.pdf', mimeType: 'application/pdf', contents, // bytes are PNG
309
+ })
310
+ ```
311
+
312
+ Correct:
313
+
314
+ ```ts
315
+ try {
316
+ await media.library.attach({ ownerType: 'Patient', ownerId: '7', collection: 'exams',
317
+ fileName: 'scan.pdf', mimeType: 'application/pdf', contents })
318
+ } catch (error) {
319
+ if (error instanceof ContentTypeMismatchError) {
320
+ // error.declaredMimeType vs error.detectedMimeType — reject with a 4xx
321
+ }
322
+ throw error
323
+ }
324
+ ```
325
+
326
+ Mechanism: with `acceptsMimeTypes` present the library reads the file's magic-byte signature
327
+ (first 189 bytes, replayed for streams) and validates the REAL content — a renamed file is a
328
+ `ContentTypeMismatchError` even though the declared type is whitelisted. Only the head is
329
+ read; the payload stays streaming.
330
+ Source: `docs/collections-and-conversions.mdx` (The content is checked too).
331
+
332
+ See also: `media-attachments-single-file/SKILL.md` — the column-attachment shape for
333
+ single-file fields; `media-uploads-resumable/SKILL.md` — landing large uploads as records
334
+ via `attachExisting`; `media-stores-delivery/SKILL.md` — serving with delivery modes.
@@ -0,0 +1,253 @@
1
+ ---
2
+ name: media-setup
3
+ description: >-
4
+ Install and configure @adonis-agora/media in an AdonisJS app. Covers node ace
5
+ configure (media_provider + dashboard_provider registration), config/media.ts via
6
+ defineConfig with stores/processors/disks/uploadSessions factory maps, the
7
+ media singleton from @adonis-agora/media/services/main, disk resolution precedence
8
+ (per-call > collection > top-level disk > Drive default), lazy optional peers
9
+ (@adonisjs/lucid, sharp, @aws-sdk/client-s3, mediabunny), StoreNotConfiguredError,
10
+ DriveNotReadyError, and the embedded console provider. Use when setting up media,
11
+ choosing a MediaStore, wiring disks, or fixing boot errors.
12
+ license: MIT
13
+ metadata:
14
+ type: core
15
+ library: "@adonis-agora/media"
16
+ library_version: "0.12.1"
17
+ framework: adonisjs
18
+ sources:
19
+ - "DavideCarvalho/adonis-media:README.md"
20
+ - "DavideCarvalho/adonis-media:docs/getting-started.mdx"
21
+ - "DavideCarvalho/adonis-media:docs/configuration.mdx"
22
+ - "DavideCarvalho/adonis-media:packages/adonis/src/define_config.ts"
23
+ - "DavideCarvalho/adonis-media:packages/adonis/src/media_manager.ts"
24
+ ---
25
+
26
+ # Setting up @adonis-agora/media
27
+
28
+ `@adonis-agora/media` is a media library for AdonisJS on top of `@adonisjs/drive`: owner
29
+ collections with policy, column attachments, image conversions, content transformers,
30
+ large-file uploads, configurable delivery, and an embedded management console. Storage is
31
+ delegated entirely to Drive — this package never reimplements disk drivers.
32
+
33
+ ## Setup
34
+
35
+ Install Drive (the storage backend) plus the package, then run the configure codemod:
36
+
37
+ ```bash
38
+ node ace add @adonisjs/drive # required — media owns no disk drivers
39
+ npm i @adonis-agora/media
40
+ node ace configure @adonis-agora/media
41
+ ```
42
+
43
+ `configure` registers **both** providers in `adonisrc.ts`, publishes `config/media.ts`
44
+ and `config/media_dashboard.ts`, and publishes two Lucid migrations (one for the optional
45
+ `lucid` media store, one for the upload-session store). Delete the migrations you don't
46
+ need; the in-memory stores need none.
47
+
48
+ ```ts title="config/media.ts"
49
+ import { defineConfig, stores, processors } from '@adonis-agora/media'
50
+
51
+ export default defineConfig({
52
+ // disk: 's3', // omit to use Drive's default disk
53
+ store: 'memory',
54
+ stores: {
55
+ memory: stores.memory(),
56
+ lucid: stores.lucid({ connection: 'pg' }),
57
+ },
58
+ imageProcessor: processors.sharp(),
59
+ collections: [
60
+ { name: 'avatar', single: true, acceptsMimeTypes: ['image/png', 'image/jpeg'] },
61
+ {
62
+ name: 'gallery',
63
+ conversions: [
64
+ { name: 'thumb', width: 200, height: 200 },
65
+ { name: 'og', width: 1200, eager: true },
66
+ ],
67
+ },
68
+ ],
69
+ })
70
+ ```
71
+
72
+ Reach the singleton manager anywhere:
73
+
74
+ ```ts
75
+ import media from '@adonis-agora/media/services/main'
76
+
77
+ const url = await media.library.url(recordId)
78
+ ```
79
+
80
+ Source: `docs/getting-started.mdx`, `docs/configuration.mdx`.
81
+
82
+ ## Core patterns
83
+
84
+ ### Pattern 1 — select drivers by name from lazy factory maps
85
+
86
+ The Agora idiom: drivers live in the core package (`stores`, `processors`, `disks`,
87
+ `uploadSessions`, `transformers`), you select them **by name**, and each heavy optional peer
88
+ (`@adonisjs/lucid`, `sharp`, `@aws-sdk/client-s3`, `mediabunny`) is imported only when its
89
+ driver is selected at boot.
90
+
91
+ ```ts
92
+ import { defineConfig, stores, processors, disks } from '@adonis-agora/media'
93
+
94
+ export default defineConfig({
95
+ store: 'lucid',
96
+ stores: { lucid: stores.lucid({ connection: 'pg' }) },
97
+ imageProcessor: processors.sharp(),
98
+ disks: { s3: disks.s3({ bucket: 'my-bucket', region: 'us-east-1' }) },
99
+ disk: 's3',
100
+ })
101
+ ```
102
+
103
+ Listing a driver you never select costs nothing — the thunk imports nothing until built.
104
+ An empty `defineConfig({})` gives the in-memory store on Drive's default disk, no
105
+ conversions, no upload routes, `auto` delivery, diagnostics on.
106
+
107
+ Source: `docs/configuration.mdx`.
108
+
109
+ ### Pattern 2 — disk precedence
110
+
111
+ Which disk a write targets resolves per-call first, then collection, then top-level, then
112
+ Drive's default:
113
+
114
+ ```ts
115
+ defineConfig({
116
+ disk: 'public-s3', // 3rd choice
117
+ collections: [
118
+ { name: 'invoices', disk: 'private-s3' }, // 2nd choice
119
+ ],
120
+ })
121
+
122
+ // per-call wins over everything:
123
+ await media.library.attach({ ..., disk: 'temp-fs' }) // 1st choice
124
+ ```
125
+
126
+ Where the NAME resolves from has separate precedence: the config's own `disks` map first,
127
+ then `config/drive.ts`. A record stores which disk it lives on, so moving a collection to a
128
+ new disk never strands existing files.
129
+
130
+ Source: `docs/configuration.mdx` (`disk` — where files live).
131
+
132
+ ### Pattern 3 — DI instead of the service import
133
+
134
+ The provider binds one singleton `MediaManager`. Prefer it through the container when you
135
+ want testability:
136
+
137
+ ```ts
138
+ import { MediaManager } from '@adonis-agora/media'
139
+
140
+ export default class PostImagesController {
141
+ constructor(protected media: MediaManager) {}
142
+ }
143
+ // or: const media = await app.container.make(MediaManager)
144
+ ```
145
+
146
+ Accessors: `media.library` (collections), `media.attachments` (column attachments),
147
+ `media.disk(name?)` (raw Drive disk escape hatch), `media.store` (cross-owner reads),
148
+ and the opt-in coordinators `media.uploads` / `media.resumable` / `media.direct`.
149
+
150
+ Source: `docs/getting-started.mdx` (Step 2), `packages/adonis/src/media_manager.ts`.
151
+
152
+ ## Common mistakes
153
+
154
+ ### [CRITICAL] Calling the media singleton at module scope before Drive boots
155
+
156
+ Wrong:
157
+
158
+ ```ts
159
+ // module scope — runs before the app (and Drive) has booted
160
+ import media from '@adonis-agora/media/services/main'
161
+ export const DEFAULT_URL = await media.library.url('seed-image')
162
+ ```
163
+
164
+ Correct:
165
+
166
+ ```ts
167
+ import media from '@adonis-agora/media/services/main'
168
+
169
+ export default class LogoController {
170
+ async show() {
171
+ const url = await media.library.url('seed-image') // inside request lifecycle
172
+ }
173
+ }
174
+ ```
175
+
176
+ Mechanism: a disk resolution reaching `@adonisjs/drive` before its manager has booted throws
177
+ `DriveNotReadyError` (`E_MEDIA_DRIVE_NOT_READY`) — almost always a media call at module scope
178
+ instead of inside a request or `app.booted`.
179
+ Source: `docs/errors.mdx` (Storage and configuration).
180
+
181
+ ### [HIGH] Naming a `store` that has no matching factory entry
182
+
183
+ Wrong:
184
+
185
+ ```ts
186
+ defineConfig({
187
+ store: 'luicd', // typo
188
+ stores: { lucid: stores.lucid() },
189
+ })
190
+ ```
191
+
192
+ Correct:
193
+
194
+ ```ts
195
+ defineConfig({
196
+ store: 'lucid',
197
+ stores: { lucid: stores.lucid() },
198
+ })
199
+ ```
200
+
201
+ Mechanism: the provider throws `StoreNotConfiguredError` at boot — deliberately NOT a silent
202
+ fallback to in-memory, because a typo quietly swapping durable persistence for a process-local
203
+ Map is discovered only after data loss. Only the zero-config path (no `store` named at all)
204
+ resolves to in-memory. Same rule for upload-session stores.
205
+ Source: `docs/configuration.mdx` (How the provider builds it), `docs/errors.mdx`.
206
+
207
+ ### [HIGH] Selecting lucid/sharp without installing the peer or running the migration
208
+
209
+ Wrong:
210
+
211
+ ```sh
212
+ npm i @adonis-agora/media # no @adonisjs/lucid, no sharp, no migration:run
213
+ # config selects store: 'lucid' + conversions → fails at boot / first attach
214
+ ```
215
+
216
+ Correct:
217
+
218
+ ```sh
219
+ node ace add @adonisjs/lucid # or ensure it exists
220
+ npm i sharp
221
+ node ace migration:run # published by configure for the lucid stores
222
+ ```
223
+
224
+ Mechanism: the factory thunks import their peers lazily at boot-time selection; selecting
225
+ `lucid` without the published `media` table fails on the first store write, not at
226
+ `configure` time.
227
+ Source: `docs/getting-started.mdx` (Prerequisites, Callout on migrations).
228
+
229
+ ### [MEDIUM] Importing the dashboard's defineConfig into config/media.ts
230
+
231
+ Wrong:
232
+
233
+ ```ts
234
+ // config/media.ts
235
+ import { defineConfig } from '@adonis-agora/media/dashboard' // wrong file's helper
236
+ export default defineConfig({ collections: [{ name: 'gallery' }] })
237
+ ```
238
+
239
+ Correct:
240
+
241
+ ```ts
242
+ // config/media.ts
243
+ import { defineConfig } from '@adonis-agora/media'
244
+ // config/media_dashboard.ts uses:
245
+ // import { defineConfig } from '@adonis-agora/media/dashboard'
246
+ ```
247
+
248
+ Mechanism: both packages export a `defineConfig`; the wrong one types the wrong file and
249
+ silently drops unknown keys instead of failing.
250
+ Source: README.md (subpath exports table), `docs/dashboard/index.mdx`.
251
+
252
+ See also: `media-library-collections/SKILL.md` — attaching to collections after setup;
253
+ `media-stores-delivery/SKILL.md` — writing custom stores/processors/disks.