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.
- package/README.md +183 -57
- package/dist/module.d.mts +12 -6
- package/dist/module.d.ts +12 -6
- package/dist/module.json +1 -1
- package/dist/module.mjs +12 -17
- package/dist/runtime/components/MLMedia/Editor.d.vue.ts +1 -0
- package/dist/runtime/components/MLMedia/Editor.vue +3 -1
- package/dist/runtime/components/MLMedia/Editor.vue.d.ts +1 -0
- package/dist/runtime/components/MLMedia/EditorPanel.d.vue.ts +3 -0
- package/dist/runtime/components/MLMedia/EditorPanel.vue +3 -2
- package/dist/runtime/components/MLMedia/EditorPanel.vue.d.ts +3 -0
- package/dist/runtime/components/MLMedia/Field.d.vue.ts +2 -2
- package/dist/runtime/components/MLMedia/Field.vue +2 -1
- package/dist/runtime/components/MLMedia/Field.vue.d.ts +2 -2
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue +1 -1
- package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +2 -2
- package/dist/runtime/components/MLMedia/Gallery.vue +4 -1
- package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +2 -2
- package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/GallerySection.vue +2 -1
- package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/UploadStep.vue +4 -3
- package/dist/runtime/components/MLPreviewProvider.vue +2 -0
- package/dist/runtime/components/MLProcessing/PipelineStepItem.vue +7 -9
- package/dist/runtime/components/MLProcessing/StepperItem.vue +3 -3
- package/dist/runtime/composables/useMLAssetPreview.d.ts +1 -0
- package/dist/runtime/composables/useMLAssetPreview.js +2 -0
- package/dist/runtime/composables/useMLMediaAssets.d.ts +11 -3
- package/dist/runtime/composables/useMLMediaAssets.js +29 -5
- package/dist/runtime/composables/useMLMediaEditor.d.ts +2 -0
- package/dist/runtime/composables/useMLMediaField.js +1 -1
- package/dist/runtime/composables/useMLMediaGallery.js +1 -1
- package/dist/runtime/composables/useMLMediaRenewal.d.ts +23 -0
- package/dist/runtime/composables/useMLMediaRenewal.js +27 -0
- package/dist/runtime/composables/useMLMediaUpload.d.ts +3 -3
- package/dist/runtime/composables/useMLMediaUpload.js +2 -2
- package/dist/runtime/composables/useMLPermalinks.d.ts +20 -0
- package/dist/runtime/composables/useMLPermalinks.js +42 -0
- package/dist/runtime/composables/useMLProcessingPipeline.js +2 -1
- package/dist/runtime/composables/useMLProcessingStrategies.js +29 -17
- package/dist/runtime/composables/useMLUploadQueue.js +7 -3
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +6 -0
- package/dist/runtime/plugins/asset-direcetive-client.js +2 -0
- package/dist/runtime/server/create-ml-proxy.d.ts +1 -17
- package/dist/runtime/server/create-ml-proxy.js +35 -114
- package/dist/runtime/server/delivery-proxy.js +4 -3
- package/dist/runtime/server/index.d.ts +2 -2
- package/dist/runtime/server/index.js +1 -7
- package/dist/runtime/server/permalinks-proxy.d.ts +2 -0
- package/dist/runtime/server/permalinks-proxy.js +2 -0
- package/dist/runtime/types/asset.d.ts +12 -4
- package/dist/runtime/types/embeddedMedia.d.ts +25 -0
- package/dist/runtime/types/embeddedMedia.js +0 -0
- package/dist/runtime/types/permalink.d.ts +12 -0
- package/dist/runtime/types/permalink.js +0 -0
- package/dist/runtime/utils/backends.d.ts +28 -2
- package/dist/runtime/utils/backends.js +20 -3
- package/dist/runtime/utils/credentialHeaders.d.ts +19 -0
- package/dist/runtime/utils/credentialHeaders.js +26 -0
- package/dist/runtime/utils/deprecation.d.ts +2 -0
- package/dist/runtime/utils/deprecation.js +7 -0
- package/dist/runtime/utils/embeddedMedia.d.ts +20 -0
- package/dist/runtime/utils/embeddedMedia.js +15 -0
- package/dist/runtime/utils/permalinks.d.ts +11 -0
- package/dist/runtime/utils/permalinks.js +16 -0
- package/dist/runtime/utils/processingSteps.d.ts +14 -3
- package/dist/runtime/utils/processingSteps.js +15 -12
- package/dist/runtime/utils/streamUrls.d.ts +7 -0
- package/dist/runtime/utils/streamUrls.js +6 -4
- package/package.json +2 -2
- package/dist/runtime/server/grants.d.ts +0 -124
- 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
|
-
// (
|
|
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,
|
|
70
|
-
// Proxy-Authorization and X-Api-Key stay server-side (the
|
|
71
|
-
|
|
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
|
|
79
|
-
|
|
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
|
-
###
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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
|
|
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
|
-
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
`
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
222
|
-
|
|
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', //
|
|
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
|
|
292
|
-
|
|
293
|
-
user
|
|
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({
|
|
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
|
|
62
|
-
*
|
|
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? }`,
|
|
71
|
-
* (attach a finished upload, `/…/:sessionId/attach`)
|
|
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({
|
|
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
|
|
62
|
-
*
|
|
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? }`,
|
|
71
|
-
* (attach a finished upload, `/…/:sessionId/attach`)
|
|
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
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({
|
|
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" | "
|
|
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" | "
|
|
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" | "
|
|
79
|
+
visibility: "private" | "restricted" | "public";
|
|
80
80
|
disabled: boolean;
|
|
81
81
|
backend: string | null;
|
|
82
|
-
service: string | import("
|
|
82
|
+
service: string | import("#imports").MediaServiceUpload | null;
|
|
83
83
|
profile: string | null;
|
|
84
84
|
processingConfig: {
|
|
85
85
|
stepOptions?: Record<string, Record<string, unknown>> | null;
|