nuxt-media-library 0.0.60 → 0.0.62

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 (81) hide show
  1. package/README.md +183 -57
  2. package/dist/module.d.mts +12 -6
  3. package/dist/module.d.ts +12 -6
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +12 -17
  6. package/dist/runtime/components/MLMedia/Editor.d.vue.ts +1 -0
  7. package/dist/runtime/components/MLMedia/Editor.vue +3 -1
  8. package/dist/runtime/components/MLMedia/Editor.vue.d.ts +1 -0
  9. package/dist/runtime/components/MLMedia/EditorPanel.d.vue.ts +3 -0
  10. package/dist/runtime/components/MLMedia/EditorPanel.vue +3 -2
  11. package/dist/runtime/components/MLMedia/EditorPanel.vue.d.ts +3 -0
  12. package/dist/runtime/components/MLMedia/Field.d.vue.ts +2 -2
  13. package/dist/runtime/components/MLMedia/Field.vue +2 -1
  14. package/dist/runtime/components/MLMedia/Field.vue.d.ts +2 -2
  15. package/dist/runtime/components/MLMedia/FilePickerPanel.vue +1 -1
  16. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +2 -2
  17. package/dist/runtime/components/MLMedia/Gallery.vue +4 -1
  18. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +2 -2
  19. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +1 -1
  20. package/dist/runtime/components/MLMedia/GallerySection.vue +2 -1
  21. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +1 -1
  22. package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +1 -1
  23. package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +1 -1
  24. package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +1 -1
  25. package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +1 -1
  26. package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +1 -1
  27. package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +1 -1
  28. package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +1 -1
  29. package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +1 -1
  30. package/dist/runtime/components/MLMedia/UploadStep.vue +4 -3
  31. package/dist/runtime/components/MLPreviewProvider.vue +2 -0
  32. package/dist/runtime/components/MLProcessing/PipelineStepItem.vue +7 -9
  33. package/dist/runtime/components/MLProcessing/StepperItem.vue +3 -3
  34. package/dist/runtime/composables/useMLAssetPreview.d.ts +1 -0
  35. package/dist/runtime/composables/useMLAssetPreview.js +2 -0
  36. package/dist/runtime/composables/useMLMediaAssets.d.ts +11 -3
  37. package/dist/runtime/composables/useMLMediaAssets.js +29 -5
  38. package/dist/runtime/composables/useMLMediaEditor.d.ts +2 -0
  39. package/dist/runtime/composables/useMLMediaField.js +1 -1
  40. package/dist/runtime/composables/useMLMediaGallery.js +1 -1
  41. package/dist/runtime/composables/useMLMediaRenewal.d.ts +23 -0
  42. package/dist/runtime/composables/useMLMediaRenewal.js +27 -0
  43. package/dist/runtime/composables/useMLMediaUpload.d.ts +3 -3
  44. package/dist/runtime/composables/useMLMediaUpload.js +2 -2
  45. package/dist/runtime/composables/useMLPermalinks.d.ts +20 -0
  46. package/dist/runtime/composables/useMLPermalinks.js +42 -0
  47. package/dist/runtime/composables/useMLProcessingPipeline.js +2 -1
  48. package/dist/runtime/composables/useMLProcessingStrategies.js +29 -17
  49. package/dist/runtime/composables/useMLUploadQueue.js +7 -3
  50. package/dist/runtime/index.d.ts +6 -0
  51. package/dist/runtime/index.js +6 -0
  52. package/dist/runtime/plugins/asset-direcetive-client.js +2 -0
  53. package/dist/runtime/server/create-ml-proxy.d.ts +1 -17
  54. package/dist/runtime/server/create-ml-proxy.js +35 -114
  55. package/dist/runtime/server/delivery-proxy.js +4 -3
  56. package/dist/runtime/server/index.d.ts +2 -2
  57. package/dist/runtime/server/index.js +1 -7
  58. package/dist/runtime/server/permalinks-proxy.d.ts +2 -0
  59. package/dist/runtime/server/permalinks-proxy.js +2 -0
  60. package/dist/runtime/types/asset.d.ts +12 -4
  61. package/dist/runtime/types/embeddedMedia.d.ts +25 -0
  62. package/dist/runtime/types/embeddedMedia.js +0 -0
  63. package/dist/runtime/types/permalink.d.ts +12 -0
  64. package/dist/runtime/types/permalink.js +0 -0
  65. package/dist/runtime/utils/backends.d.ts +28 -2
  66. package/dist/runtime/utils/backends.js +20 -3
  67. package/dist/runtime/utils/credentialHeaders.d.ts +19 -0
  68. package/dist/runtime/utils/credentialHeaders.js +26 -0
  69. package/dist/runtime/utils/deprecation.d.ts +2 -0
  70. package/dist/runtime/utils/deprecation.js +7 -0
  71. package/dist/runtime/utils/embeddedMedia.d.ts +20 -0
  72. package/dist/runtime/utils/embeddedMedia.js +15 -0
  73. package/dist/runtime/utils/permalinks.d.ts +11 -0
  74. package/dist/runtime/utils/permalinks.js +16 -0
  75. package/dist/runtime/utils/processingSteps.d.ts +14 -3
  76. package/dist/runtime/utils/processingSteps.js +15 -12
  77. package/dist/runtime/utils/streamUrls.d.ts +7 -0
  78. package/dist/runtime/utils/streamUrls.js +6 -4
  79. package/package.json +2 -2
  80. package/dist/runtime/server/grants.d.ts +0 -124
  81. package/dist/runtime/server/grants.js +0 -164
package/README.md CHANGED
@@ -53,7 +53,7 @@ export default defineNuxtConfig({
53
53
  // Only for direct browser uploads (proxyUploads: false): a public address.
54
54
  // publicBaseURL: 'https://media.example.com/v1',
55
55
  // false when your own server/api/media-library/[...].ts serves /manage
56
- // (media grants, below). Default true: the module's /manage proxies.
56
+ // (below). Default true: the module's /manage proxies.
57
57
  // manageProxy: false,
58
58
  // Uploads always go through Nuxt server (/api/media-library/uploads/*)
59
59
  routes: {
@@ -66,17 +66,20 @@ export default defineNuxtConfig({
66
66
  partSize: 8 * 1024 * 1024,
67
67
  concurrency: 3,
68
68
  checksum: { algorithm: 'SHA-256', perPart: true, final: true },
69
- // Sent to browsers too, except credentials: Authorization, Cookie,
70
- // Proxy-Authorization and X-Api-Key stay server-side (the proxies add them).
71
- headers: { 'X-Tenant': 'acme' },
69
+ // Optional. Sent to browsers too, except credentials: Authorization,
70
+ // Cookie, Proxy-Authorization and X-Api-Key stay server-side (only the
71
+ // /manage proxies add them, never /delivery). X-Tenant is not needed
72
+ // (the media library ignores it); it is still sent when set.
73
+ // headers: { 'X-Tenant': 'acme' },
72
74
  },
73
75
  })
74
76
  ```
75
77
 
76
78
  Nuxt puts the public runtime config in every page, so the module never copies
77
79
  `baseURL` or credential headers there. Set the media library's internal
78
- address and any service token only server-side
79
- (`NUXT_MEDIA_LIBRARY_BASE_URL`, `NUXT_MEDIA_LIBRARY_SERVICE_TOKEN`).
80
+ address only server-side (`NUXT_MEDIA_LIBRARY_BASE_URL`). The Nuxt server
81
+ issues no media grants and needs no service token: the MLMedia widgets read
82
+ through each service's `resolve` backend (below).
80
83
 
81
84
  ### Nuxt usage
82
85
 
@@ -95,21 +98,63 @@ If you use the uploader component in Nuxt, provide config via the Nuxt plugin
95
98
  </template>
96
99
  ```
97
100
 
98
- ### Authorizing `/manage` with media grants
101
+ ### Reading files through your services (`resolve` backends)
102
+
103
+ The MLMedia widgets (`MLMediaGallery`, `MLMediaField`, the file picker, the
104
+ editor) and the upload queue read files through the service that owns them.
105
+ The service decides who may see what; the Nuxt server holds no media
106
+ credential and issues no grants.
107
+
108
+ ```ts
109
+ // nuxt.config.ts (or the service's layer)
110
+ export default defineNuxtConfig({
111
+ mediaLibrary: {
112
+ backends: {
113
+ videos: {
114
+ upload: { start: '/api/videos/uploads' },
115
+ resolve: '/api/videos/media/resolve',
116
+ },
117
+ },
118
+ },
119
+ })
120
+ ```
121
+
122
+ ```vue
123
+ <MLMediaGallery v-model="lesson.mediaIds" backend="videos" />
124
+ ```
125
+
126
+ ```python
127
+ # the videos service (pkg_media[fastapi])
128
+ gate = AccessGate(client, can_access=lambda user, ids: videos.visible_media(user, ids))
129
+ app.include_router(create_media_router(gate, get_current_user, prefix="/videos/media"))
130
+ ```
131
+
132
+ - Request: `POST {resolve}` from the browser, same origin, with the user's
133
+ session (cookies), body `{ "assetIds": ["…"] }`. At most 100 ids per
134
+ request; the widgets split longer lists.
135
+ - Response: `{ "items": [{ "assetId", "status", "asset", "error" }] }`, the
136
+ shape of the media library's `/manage/assets/resolve`. `status` is `ok`,
137
+ `not_found`, `forbidden` (the service's `can_access` refused) or `error`;
138
+ `asset` is set when `ok`. HLS/DASH links are reloaded before
139
+ `streamUrlExpiresIn` runs out, as before.
140
+ - `backend` picks the route: the field, the gallery (also when it places
141
+ picked files by kind), the picker's already-attached files, the editor
142
+ (`useMLMediaEditor().open(id, { backend })`), `useMLMediaAssets(ids, { backend })`
143
+ and the upload queue's status polling all read there.
144
+ - Without `backend`, or for a backend without `resolve`, they read through
145
+ Nuxt's own `/api/media-library/manage/assets/resolve` as before. An
146
+ unknown name shows the files as errors with a message naming it.
147
+ - Still through Nuxt's own `/manage` proxy: the picker's library list and
148
+ its preview of listed files, and the processing stream.
149
+
150
+ ### Serving `/manage` yourself
99
151
 
100
152
  The module's server proxies forward `/api/media-library/manage/*` to the
101
- media library. Without an `authorize` hook they forward the browser's
102
- credentials (or your static headers) as before. With one, every `/manage`
103
- request is checked by your rules and sent upstream with a short-lived
104
- **media grant** scoped to exactly that request (server:
105
- `docs/security/media-grants.md`). Your app never re-implements the
106
- endpoints, and no long-lived credential reaches the media library from the
107
- browser.
108
-
109
- Turn off the module's built-in `/manage` proxies so your handler gets the
110
- requests. Their routes (`/api/media-library/manage/assets/**`…) are more
111
- specific than a catch-all file and would otherwise answer first, without
112
- `authorize` (the module warns at startup when it sees a shadowed catch-all):
153
+ media library with the browser's credentials and your static `headers`.
154
+ To add your own per-request credential, turn off the built-in `/manage`
155
+ proxies (their routes are more specific than a catch-all file and would
156
+ answer first; the module warns at startup when it sees a shadowed
157
+ catch-all) and serve them from one file:
113
158
 
114
159
  ```ts
115
160
  // nuxt.config.ts
@@ -120,46 +165,108 @@ export default defineNuxtConfig({
120
165
 
121
166
  ```ts
122
167
  // server/api/media-library/[...].ts
123
- import { createMLCatchAllProxy, specFromNeed } from '@fritill-team/nuxt-media-library/server'
124
-
125
- const COVER = { kind: 'image', visibility: 'public', allowedMimeTypes: ['image/*'], maxSizeBytes: 5_000_000 }
168
+ import { createMLCatchAllProxy } from 'nuxt-media-library/server'
126
169
 
127
170
  export default createMLCatchAllProxy({
128
- async authorize(event, need) {
129
- const user = await requireUser(event) // your session
130
- const subject = String(user.id)
131
- if (need.scope === 'upload' && !need.sessionId) { // creating an upload
132
- return user.canUpload ? { scopes: ['upload'], profile: COVER, subject } : false
133
- }
134
- if (need.taskId) return false // or look up the task's asset
135
- if (need.scope === 'manage:*') return user.isAdmin // tenant-wide list, stream
136
- return (need.assetIds ?? []).every(id => user.canEdit(id))
137
- ? { ...specFromNeed(need), subject }
138
- : false
139
- },
171
+ requestHeaders: async event => ({ Authorization: `Bearer ${await getAccessToken(event)}` }),
140
172
  })
141
173
  ```
142
174
 
143
- - `need`: `{ scope, method, path, assetIds?, taskId?, sessionId?, upload? }`
144
- (`upload` is the create body: MIME type, size, file name).
145
- - Return `false` (403), `true` (the need as is), or a spec
146
- `{ scopes, assetIds?, sessionIds?, profile?, subject?, ttlSeconds? }`.
147
- Creating an upload needs a `profile`: kind, visibility and processing come
148
- from it, not from the browser. `manage:*` is only accepted for requests
149
- that need it.
150
- - Grants are issued with the private runtime config
151
- `mediaLibrary.serviceToken` (`NUXT_MEDIA_LIBRARY_SERVICE_TOKEN`), or pass
152
- `issueGrant(spec)` to get them from your backend (e.g. the Python
153
- client's `create_grant_router`). They are cached per spec until shortly
154
- before they expire; an upload's parts, finalize and abort reuse the grant
155
- that created the session.
156
- - `/delivery` and the processing metadata routes are not affected.
175
+ - Grant mode is gone: `createMLCatchAllProxy` and `createMLProxy` no longer
176
+ take `authorize` or `issueGrant`, and the private `mediaLibrary.serviceToken`
177
+ config is not read. Passing either option throws, so an app that relied
178
+ on them fails loudly instead of forwarding unchecked. Read files through
179
+ `resolve` backends instead.
180
+ - `/delivery` never gets a credential from any source: not the static
181
+ credentials (a service token in `headers`), not the browser's own
182
+ `Authorization`, `Cookie`, `Proxy-Authorization` or `X-Api-Key` (a
183
+ signed-in user's token, which the media library refuses), and not one a
184
+ `requestHeaders` factory adds. Grants travel in `X-Media-Grants`; other
185
+ non-credential headers such as `X-Tenant` pass. `requestHeaders` still
186
+ sees the request's cookies, and `/manage` is unchanged.
157
187
  - With `manageProxy: false` the module still serves `/api/media-library/delivery/**`
158
188
  and, when `serviceUploads.proxy` is on, the service-upload pass-through.
159
189
  Your catch-all handles everything else under `/api/media-library`.
160
190
 
191
+ ### Showing media on public pages (embedded media)
192
+
193
+ Public pages never resolve an asset id. Each service puts the media into its
194
+ own response (Python: `pkg_media`'s `MediaEmbedder`, or `StrawberryMedia` for
195
+ GraphQL), having decided who may see what. The page renders the fields it gets:
196
+
197
+ ```ts
198
+ interface EmbeddedMedia { // from the service's GraphQL schema (codegen)
199
+ assetId: string
200
+ kind: 'image' | 'video' | 'audio' | 'document' | 'other'
201
+ url: string | null // the file; null when denied or not ready
202
+ poster: string | null // the one image to show; set even when denied
203
+ sources: { url: string, width: number, height?: number | null }[] // poster sizes
204
+ streams: { hls?: string | null, dash?: string | null } | null
205
+ width?: number | null; height?: number | null; alt?: string | null
206
+ expiresIn: number | null // seconds until the first link expires; null = never
207
+ public: boolean // every link tokenless (cache-safe)
208
+ }
209
+ ```
210
+
211
+ The module auto-imports the helpers that read it (a structural type,
212
+ `MLEmbeddedMedia`, so any codegen type fits):
213
+
214
+ | Helper | Gives |
215
+ |---|---|
216
+ | `mlMediaPoster(media, fallback?)` | the image to show, else `fallback` |
217
+ | `mlMediaSrcset(media)` | `srcset` from `media.sources`, or `undefined` |
218
+ | `mlPlayableSource(media, { preferFile? })` | `{ src, type: 'hls' \| 'progressive' }`: the HLS ladder, else the file; `null` when locked or not ready |
219
+ | `mlIsPlayableVideo(media)` | a video the viewer can play now |
220
+ | `useMLMediaRenewal(media, renew, { marginSeconds? })` | calls `renew` 5 minutes (default) before tokened links expire; browser only |
221
+
222
+ ```vue
223
+ <script setup lang="ts">
224
+ const { data: item } = await useAsyncData('article', fetchArticle)
225
+ // Re-fetch before the links expire and swap in only `media`.
226
+ useMLMediaRenewal(() => item.value?.media, async () => {
227
+ const fresh = await fetchArticle()
228
+ if (item.value && fresh?.media) item.value = { ...item.value, media: fresh.media }
229
+ })
230
+ const source = computed(() => mlPlayableSource(item.value?.media))
231
+ </script>
232
+
233
+ <template>
234
+ <!-- `sizes` is the box's widest rendered width. -->
235
+ <img
236
+ :src="mlMediaPoster(item?.media, '/placeholder.jpg')"
237
+ :srcset="mlMediaSrcset(item?.media)"
238
+ sizes="(min-width: 1024px) 420px, 100vw"
239
+ alt=""
240
+ loading="lazy"
241
+ >
242
+ <!-- A video: the app's own player (hls.js for type 'hls'). -->
243
+ <AppVideoPlayer v-if="source" :src="source.src" :type="source.type" :poster="item?.media?.poster" />
244
+ </template>
245
+ ```
246
+
247
+ - **Locked or missing:** `url` and `streams` are null; show `poster` (and a
248
+ paywall). No call to the media library can do better.
249
+ - **Renewal:** tokened links expire (`expiresIn`). `useMLMediaRenewal` fetches the
250
+ record again before then; swap in only the new `media` and resume the player at
251
+ its playhead (key the player on its source).
252
+ - **SEO:** pass `media.poster` (and the service's embedded share image) to your
253
+ SEO helper as URLs; the HTML rendered on the server then carries them.
254
+ - **Caching:** a response with a tokened link is per user (`public: false`);
255
+ don't cache it at the edge.
256
+ - `useMLAssetPreview`, `<MLPreviewProvider>` and `v-asset` resolve ids in the
257
+ browser through `/delivery`. They are **deprecated** (a warning in dev) and go
258
+ with `/delivery`; so do read grants (below).
259
+
260
+ Design and rulings: [`docs/design/embedded-media.md`](docs/design/embedded-media.md).
261
+ Moving a service and its layer over, step by step:
262
+ [`docs/design/embedded-media-migration-guide.md`](docs/design/embedded-media-migration-guide.md).
263
+
161
264
  ### Showing assets of several services: read grants
162
265
 
266
+ > **Deprecated** with `/delivery`: public pages show embedded media instead
267
+ > (see above), and no grant is needed.
268
+
269
+
163
270
  Every link comes from the media library; each service decides only for its
164
271
  own assets by returning a read grant (`mlg_…`, e.g. the Python client's
165
272
  `client.grants.read(ids)`) with the data that references them. Hand the
@@ -218,13 +325,10 @@ await $fetch(`/api/videos/lessons/${lessonId}/video`, { method: 'PUT', body: { s
218
325
  session's `GET`, parts `PUT`, `finalize` and `abort` to the private
219
326
  `mediaLibrary.baseURL`. It requires the browser's `Bearer mlg_…` grant and
220
327
  forwards it and `X-Tenant` untouched (the static `X-Tenant` when the
221
- browser sends none). It never adds the service token, static auth headers
222
- or a Nuxt-issued grant, drops cookies, and streams request bodies. Mount it
328
+ browser sends none, and only if one is configured). It never adds static
329
+ auth headers, drops cookies, and streams request bodies. Mount it
223
330
  yourself with `createMLServiceUploadsProxy()` from
224
331
  `nuxt-media-library/server` for another path.
225
- - Don't send these uploads through `createMLCatchAllProxy` with `authorize`:
226
- grant mode replaces the browser's grant with its own, and the service's
227
- `claim_upload` rejects the session.
228
332
 
229
333
  #### One layer per service, widgets by backend name
230
334
 
@@ -240,7 +344,8 @@ export default defineNuxtConfig({
240
344
  backends: {
241
345
  videos: {
242
346
  upload: { start: '/api/videos/uploads' }, // refresh defaults to `${start}/:sessionId/grant`
243
- claim: '/api/videos/uploads/:sessionId/attach', // py_client on_attach
347
+ claim: '/api/videos/uploads/:sessionId/attach', // pkg_media on_attach
348
+ resolve: '/api/videos/media/resolve', // pkg_media create_media_router
244
349
  },
245
350
  },
246
351
  },
@@ -288,9 +393,10 @@ export default defineNuxtConfig({
288
393
  `(assetId, role, sessionId)`, field: `(assetId, sessionId)`), including
289
394
  files uploaded from the picker and chosen. Send both to the service's
290
395
  attach endpoint: its `claim_upload` needs them.
291
- - Processing progress uses `/api/media-library/manage/assets/resolve` and
292
- the processing stream, so the app's proxy must allow `read` on a file the
293
- user just uploaded.
396
+ - Processing progress uses the processing stream and, when it is not
397
+ available, polls the backend's `resolve` route (Nuxt's own route without
398
+ one), so the service's `can_access` must let the user see a file they
399
+ just uploaded.
294
400
  - There is no default backend: each layer's widgets name theirs. The module
295
401
  only stores the paths; each layer's own routes reach its service.
296
402
 
@@ -316,6 +422,26 @@ name, extension, MIME type, kind, size, duration, width, height and pages.
316
422
  has none yet.
317
423
  - `useMLMediaMeta(ids, getAsset)` gives the same map for your own lists.
318
424
 
425
+ ### Permalinks
426
+
427
+ A permalink is a stable public address `{media host}/p/{name}` for a
428
+ public asset (server `docs/api/permalinks.md`). Store the name, e.g.
429
+ `users/42/avatar`, and re-point it when the file changes:
430
+
431
+ ```ts
432
+ const { set, get, remove } = useMLPermalinks()
433
+ const link = await set('users/42/avatar', assetId) // { name, assetId, url, createdAt, updatedAt }
434
+ await get('users/42/avatar') // null when unknown
435
+ ```
436
+
437
+ - The media library takes a **service token only**. The module's
438
+ `/api/media-library/manage/permalinks/{name}` proxy works when
439
+ `mediaLibrary.headers` carries one. Otherwise call
440
+ `PUT/GET/DELETE {baseURL}/manage/permalinks/{name}` from your server
441
+ (`permalinkPath(name)` from `/server`).
442
+ - `isValidPermalinkName` checks a name the way the server does;
443
+ `permalinkUrl(mediaHost, name)` builds the public address.
444
+
319
445
  ### Error messages, in the editor's language
320
446
 
321
447
  When a backend refuses a request it answers `{ detail, reason }`: `reason`
package/dist/module.d.mts CHANGED
@@ -36,6 +36,11 @@ interface ModuleOptions {
36
36
  perPart?: boolean;
37
37
  final?: boolean;
38
38
  };
39
+ /**
40
+ * Static headers the `/manage` proxies add upstream. Optional. Credential
41
+ * headers stay server-side and never go to `/delivery`. `X-Tenant` is not
42
+ * needed (the media library ignores it); it is still sent when set.
43
+ */
39
44
  headers?: Record<string, string> | (() => Record<string, string>);
40
45
  /**
41
46
  * Uploads started by the app's backend services (`useMLServiceUpload`).
@@ -56,19 +61,20 @@ interface ModuleOptions {
56
61
  /**
57
62
  * The built-in `/api/media-library/manage/**` proxies (default `true`).
58
63
  * Set `false` when the app serves `/manage` itself, e.g. with
59
- * `createMLCatchAllProxy({ authorize })` in
64
+ * `createMLCatchAllProxy({ requestHeaders })` in
60
65
  * `server/api/media-library/[...].ts`: the built-in routes are more
61
- * specific than an app's catch-all and would otherwise answer first, so
62
- * `authorize` would never run. `/delivery/**` and the service-upload
63
- * pass-through are still registered.
66
+ * specific than an app's catch-all and would otherwise answer first.
67
+ * `/delivery/**` and the service-upload pass-through are still registered.
64
68
  */
65
69
  manageProxy?: boolean;
66
70
  /**
67
71
  * The backend registry: named places widgets upload to and read from
68
72
  * (`<MLMediaGallery backend="videos" />`). Each entry has a `kind`
69
73
  * (`media-library`, the default, or `media-kit`) and its routes: `upload`
70
- * (`{ start, refresh? }`, py_client `create_upload_router`), `claim`
71
- * (attach a finished upload, `/…/:sessionId/attach`). Each service's Nuxt
74
+ * (`{ start, refresh? }`, pkg_media `create_upload_router`), `claim`
75
+ * (attach a finished upload, `/…/:sessionId/attach`) and `resolve` (where
76
+ * widgets read its assets: `POST { assetIds }` with the user's session,
77
+ * pkg_media `create_media_router`). Each service's Nuxt
72
78
  * layer registers its own; Nuxt merges them. Paths only: each layer serves
73
79
  * its routes.
74
80
  */
package/dist/module.d.ts CHANGED
@@ -36,6 +36,11 @@ interface ModuleOptions {
36
36
  perPart?: boolean;
37
37
  final?: boolean;
38
38
  };
39
+ /**
40
+ * Static headers the `/manage` proxies add upstream. Optional. Credential
41
+ * headers stay server-side and never go to `/delivery`. `X-Tenant` is not
42
+ * needed (the media library ignores it); it is still sent when set.
43
+ */
39
44
  headers?: Record<string, string> | (() => Record<string, string>);
40
45
  /**
41
46
  * Uploads started by the app's backend services (`useMLServiceUpload`).
@@ -56,19 +61,20 @@ interface ModuleOptions {
56
61
  /**
57
62
  * The built-in `/api/media-library/manage/**` proxies (default `true`).
58
63
  * Set `false` when the app serves `/manage` itself, e.g. with
59
- * `createMLCatchAllProxy({ authorize })` in
64
+ * `createMLCatchAllProxy({ requestHeaders })` in
60
65
  * `server/api/media-library/[...].ts`: the built-in routes are more
61
- * specific than an app's catch-all and would otherwise answer first, so
62
- * `authorize` would never run. `/delivery/**` and the service-upload
63
- * pass-through are still registered.
66
+ * specific than an app's catch-all and would otherwise answer first.
67
+ * `/delivery/**` and the service-upload pass-through are still registered.
64
68
  */
65
69
  manageProxy?: boolean;
66
70
  /**
67
71
  * The backend registry: named places widgets upload to and read from
68
72
  * (`<MLMediaGallery backend="videos" />`). Each entry has a `kind`
69
73
  * (`media-library`, the default, or `media-kit`) and its routes: `upload`
70
- * (`{ start, refresh? }`, py_client `create_upload_router`), `claim`
71
- * (attach a finished upload, `/…/:sessionId/attach`). Each service's Nuxt
74
+ * (`{ start, refresh? }`, pkg_media `create_upload_router`), `claim`
75
+ * (attach a finished upload, `/…/:sessionId/attach`) and `resolve` (where
76
+ * widgets read its assets: `POST { assetIds }` with the user's session,
77
+ * pkg_media `create_media_router`). Each service's Nuxt
72
78
  * layer registers its own; Nuxt merges them. Paths only: each layer serves
73
79
  * its routes.
74
80
  */
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "media-library",
3
3
  "configKey": "mediaLibrary",
4
- "version": "0.0.60",
4
+ "version": "0.0.62",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,23 +1,13 @@
1
1
  import { existsSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { defineNuxtModule, createResolver, addImportsDir, addComponentsDir, addServerHandler, useLogger, addPlugin } from '@nuxt/kit';
3
+ import { defineNuxtModule, createResolver, addImportsDir, addImports, addComponentsDir, addServerHandler, useLogger, addPlugin } from '@nuxt/kit';
4
+ import { browserHeaders } from '../dist/runtime/utils/credentialHeaders.js';
4
5
  import { normalizeBackends } from '../dist/runtime/utils/backends.js';
5
6
  export * from '../dist/runtime/types/resourcePolicy.js';
6
7
  export * from '../dist/runtime/types/uploader.js';
7
8
  export * from '../dist/runtime/types/preview.js';
8
9
  export * from '../dist/runtime/types/asset.js';
9
10
 
10
- const CREDENTIAL_HEADERS = ["authorization", "proxy-authorization", "cookie", "x-api-key"];
11
- function browserHeaders(headers) {
12
- if (!headers || typeof headers !== "object") return { headers: {}, dropped: [] };
13
- const kept = {};
14
- const dropped = [];
15
- for (const [name, value] of Object.entries(headers)) {
16
- if (CREDENTIAL_HEADERS.includes(name.toLowerCase())) dropped.push(name);
17
- else kept[name] = value;
18
- }
19
- return { headers: kept, dropped };
20
- }
21
11
  function browserBaseURL(existingPublic, publicBaseURL) {
22
12
  return existingPublic ?? publicBaseURL ?? "";
23
13
  }
@@ -63,6 +53,10 @@ const module$1 = defineNuxtModule({
63
53
  const resolver = createResolver(import.meta.url);
64
54
  const runtimeDir = resolver.resolve("./runtime");
65
55
  addImportsDir(resolver.resolve("runtime/composables"));
56
+ addImports(["mlMediaPoster", "mlMediaSrcset", "mlPlayableSource", "mlIsPlayableVideo"].map((name) => ({
57
+ name,
58
+ from: resolver.resolve("runtime/utils/embeddedMedia")
59
+ })));
66
60
  nuxt.options.build.transpile ||= [];
67
61
  nuxt.options.build.transpile.push(runtimeDir);
68
62
  const existingPrivate = nuxt.options.runtimeConfig.mediaLibrary || {};
@@ -128,9 +122,13 @@ const module$1 = defineNuxtModule({
128
122
  route: "/api/media-library/manage/processing/**",
129
123
  handler: resolver.resolve("./runtime/server/processing-proxy")
130
124
  });
125
+ addServerHandler({
126
+ route: "/api/media-library/manage/permalinks/**",
127
+ handler: resolver.resolve("./runtime/server/permalinks-proxy")
128
+ });
131
129
  const shadowed = appCatchAlls(nuxt.options._layers);
132
130
  if (shadowed.length) {
133
- logger.warn(`${shadowed.join(", ")} will not receive /api/media-library/manage requests: the module's own /manage routes answer first. Set mediaLibrary.manageProxy: false to serve /manage from your handler (e.g. createMLCatchAllProxy({ authorize })).`);
131
+ logger.warn(`${shadowed.join(", ")} will not receive /api/media-library/manage requests: the module's own /manage routes answer first. Set mediaLibrary.manageProxy: false to serve /manage from your handler (e.g. createMLCatchAllProxy({ requestHeaders })).`);
134
132
  }
135
133
  }
136
134
  if (options.serviceUploads?.proxy) {
@@ -143,10 +141,7 @@ const module$1 = defineNuxtModule({
143
141
  ...existingPrivate,
144
142
  baseURL: existingPrivate.baseURL ?? envBaseURL ?? options.baseURL ?? "",
145
143
  routes: existingPrivate.routes ?? options.routes ?? routes,
146
- headers: resolvedHeaders,
147
- // Only read when a proxy has `authorize` and no `issueGrant` (media
148
- // grants). Declared so NUXT_MEDIA_LIBRARY_SERVICE_TOKEN applies.
149
- serviceToken: existingPrivate.serviceToken ?? ""
144
+ headers: resolvedHeaders
150
145
  };
151
146
  const publicHeaders = browserHeaders(existingPublic.headers ?? options.headers);
152
147
  if (publicHeaders.dropped.length) {
@@ -13,6 +13,7 @@ type __VLS_Props = {
13
13
  readonly?: boolean;
14
14
  removeLabel?: string;
15
15
  usages?: Readonly<Record<string, MediaUsage[]>>;
16
+ backend?: string | null;
16
17
  };
17
18
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
18
19
  close: (result: MediaEditorResult | null) => any;
@@ -5,7 +5,8 @@ defineProps({
5
5
  alt: { type: Object, required: false },
6
6
  readonly: { type: Boolean, required: false },
7
7
  removeLabel: { type: String, required: false },
8
- usages: { type: Object, required: false }
8
+ usages: { type: Object, required: false },
9
+ backend: { type: [String, null], required: false }
9
10
  });
10
11
  const emit = defineEmits(["close"]);
11
12
  </script>
@@ -25,6 +26,7 @@ const emit = defineEmits(["close"]);
25
26
  :readonly="readonly"
26
27
  :remove-label="removeLabel"
27
28
  :usages="usages"
29
+ :backend="backend"
28
30
  @save="({ alt: a }) => emit('close', { action: 'save', alt: a })"
29
31
  @remove="(id) => emit('close', { action: 'remove', assetId: id })"
30
32
  @cancel="emit('close', null)"
@@ -13,6 +13,7 @@ type __VLS_Props = {
13
13
  readonly?: boolean;
14
14
  removeLabel?: string;
15
15
  usages?: Readonly<Record<string, MediaUsage[]>>;
16
+ backend?: string | null;
16
17
  };
17
18
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
18
19
  close: (result: MediaEditorResult | null) => any;
@@ -12,6 +12,8 @@ type __VLS_Props = {
12
12
  removeLabel?: string;
13
13
  /** Where each file is used, per asset ID. Omit to hide the section. */
14
14
  usages?: Readonly<Record<string, MediaUsage[]>>;
15
+ /** The backend (`mediaLibrary.backends`) whose `resolve` route reads the files. */
16
+ backend?: string | null;
15
17
  };
16
18
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
17
19
  currentId: import("vue").ComputedRef<string>;
@@ -28,6 +30,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
28
30
  alt: Record<string, string>;
29
31
  }) => any) | undefined;
30
32
  }>, {
33
+ backend: string | null;
31
34
  usages: Readonly<Record<string, MediaUsage[]>>;
32
35
  siblings: readonly string[];
33
36
  alt: Readonly<Record<string, string>>;
@@ -8,7 +8,8 @@ const props = defineProps({
8
8
  alt: { type: Object, required: false, default: () => ({}) },
9
9
  readonly: { type: Boolean, required: false, default: false },
10
10
  removeLabel: { type: String, required: false, default: "" },
11
- usages: { type: Object, required: false, default: void 0 }
11
+ usages: { type: Object, required: false, default: void 0 },
12
+ backend: { type: [String, null], required: false, default: null }
12
13
  });
13
14
  const emit = defineEmits(["save", "remove", "cancel"]);
14
15
  const list = computed(() => {
@@ -17,7 +18,7 @@ const list = computed(() => {
17
18
  });
18
19
  const index = ref(Math.max(0, list.value.indexOf(props.assetId)));
19
20
  const currentId = computed(() => list.value[index.value] ?? props.assetId);
20
- const library = useMLMediaAssets(list);
21
+ const library = useMLMediaAssets(list, { backend: () => props.backend });
21
22
  const asset = computed(() => library.get(currentId.value));
22
23
  const lookup = computed(() => library.lookup(currentId.value));
23
24
  const name = computed(() => asset.value ? assetName(asset.value) : "");
@@ -12,6 +12,8 @@ type __VLS_Props = {
12
12
  removeLabel?: string;
13
13
  /** Where each file is used, per asset ID. Omit to hide the section. */
14
14
  usages?: Readonly<Record<string, MediaUsage[]>>;
15
+ /** The backend (`mediaLibrary.backends`) whose `resolve` route reads the files. */
16
+ backend?: string | null;
15
17
  };
16
18
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
17
19
  currentId: import("vue").ComputedRef<string>;
@@ -28,6 +30,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
28
30
  alt: Record<string, string>;
29
31
  }) => any) | undefined;
30
32
  }>, {
33
+ backend: string | null;
31
34
  usages: Readonly<Record<string, MediaUsage[]>>;
32
35
  siblings: readonly string[];
33
36
  alt: Readonly<Record<string, string>>;
@@ -63,9 +63,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
63
63
  "onUpdate:alt"?: ((value: string | undefined) => any) | undefined;
64
64
  "onUpdate:meta"?: ((value: MediaFileMeta | null) => any) | undefined;
65
65
  }>, {
66
- visibility: "private" | "public" | "restricted";
67
- disabled: boolean;
66
+ visibility: "private" | "restricted" | "public";
68
67
  label: string;
68
+ disabled: boolean;
69
69
  backend: string | null;
70
70
  service: string | import("../../index.js").MediaServiceUpload | null;
71
71
  profile: string | null;
@@ -120,7 +120,8 @@ async function openEditor() {
120
120
  const result = await mediaEditor.open(id, {
121
121
  alt: alt.value ?? "",
122
122
  readonly: props.disabled,
123
- removeLabel: props.removeLabel
123
+ removeLabel: props.removeLabel,
124
+ backend: props.backend
124
125
  });
125
126
  if (result?.action === "save") alt.value = result.alt[id] ?? "";
126
127
  else if (result?.action === "remove") field.clear();
@@ -63,9 +63,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
63
63
  "onUpdate:alt"?: ((value: string | undefined) => any) | undefined;
64
64
  "onUpdate:meta"?: ((value: MediaFileMeta | null) => any) | undefined;
65
65
  }>, {
66
- visibility: "private" | "public" | "restricted";
67
- disabled: boolean;
66
+ visibility: "private" | "restricted" | "public";
68
67
  label: string;
68
+ disabled: boolean;
69
69
  backend: string | null;
70
70
  service: string | import("../../index.js").MediaServiceUpload | null;
71
71
  profile: string | null;
@@ -62,7 +62,7 @@ function toggle(id) {
62
62
  choose(id);
63
63
  }
64
64
  }
65
- const pinned = useMLMediaAssets(() => initial);
65
+ const pinned = useMLMediaAssets(() => initial, { backend: () => props.upload?.backend });
66
66
  const pinnedAssets = computed(() => initial.map((id) => pinned.get(id)).filter((a) => !!a));
67
67
  const uploader = useMLMediaUpload(props.upload);
68
68
  const rejectNote = ref(null);
@@ -76,10 +76,10 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
76
76
  "onUpdate:profiles"?: ((profiles: MediaGalleryProfiles) => any) | undefined;
77
77
  }>, {
78
78
  title: string;
79
- visibility: "private" | "public" | "restricted";
79
+ visibility: "private" | "restricted" | "public";
80
80
  disabled: boolean;
81
81
  backend: string | null;
82
- service: string | import("../../index.js").MediaServiceUpload | null;
82
+ service: string | import("#imports").MediaServiceUpload | null;
83
83
  profile: string | null;
84
84
  processingConfig: {
85
85
  stepOptions?: Record<string, Record<string, unknown>> | null;