nuxt-media-library 0.0.60 → 0.0.61

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 (67) hide show
  1. package/README.md +106 -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 +7 -16
  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/MLProcessing/PipelineStepItem.vue +7 -9
  32. package/dist/runtime/components/MLProcessing/StepperItem.vue +3 -3
  33. package/dist/runtime/composables/useMLMediaAssets.d.ts +11 -3
  34. package/dist/runtime/composables/useMLMediaAssets.js +29 -5
  35. package/dist/runtime/composables/useMLMediaEditor.d.ts +2 -0
  36. package/dist/runtime/composables/useMLMediaField.js +1 -1
  37. package/dist/runtime/composables/useMLMediaGallery.js +1 -1
  38. package/dist/runtime/composables/useMLMediaUpload.d.ts +3 -3
  39. package/dist/runtime/composables/useMLMediaUpload.js +2 -2
  40. package/dist/runtime/composables/useMLPermalinks.d.ts +20 -0
  41. package/dist/runtime/composables/useMLPermalinks.js +42 -0
  42. package/dist/runtime/composables/useMLProcessingPipeline.js +2 -1
  43. package/dist/runtime/composables/useMLProcessingStrategies.js +29 -17
  44. package/dist/runtime/composables/useMLUploadQueue.js +7 -3
  45. package/dist/runtime/index.d.ts +3 -0
  46. package/dist/runtime/index.js +3 -0
  47. package/dist/runtime/server/create-ml-proxy.d.ts +1 -17
  48. package/dist/runtime/server/create-ml-proxy.js +35 -114
  49. package/dist/runtime/server/delivery-proxy.js +4 -3
  50. package/dist/runtime/server/index.d.ts +2 -2
  51. package/dist/runtime/server/index.js +1 -7
  52. package/dist/runtime/server/permalinks-proxy.d.ts +2 -0
  53. package/dist/runtime/server/permalinks-proxy.js +2 -0
  54. package/dist/runtime/types/asset.d.ts +12 -4
  55. package/dist/runtime/types/permalink.d.ts +12 -0
  56. package/dist/runtime/types/permalink.js +0 -0
  57. package/dist/runtime/utils/backends.d.ts +28 -2
  58. package/dist/runtime/utils/backends.js +20 -3
  59. package/dist/runtime/utils/credentialHeaders.d.ts +19 -0
  60. package/dist/runtime/utils/credentialHeaders.js +26 -0
  61. package/dist/runtime/utils/permalinks.d.ts +11 -0
  62. package/dist/runtime/utils/permalinks.js +16 -0
  63. package/dist/runtime/utils/processingSteps.d.ts +14 -3
  64. package/dist/runtime/utils/processingSteps.js +15 -12
  65. package/package.json +2 -2
  66. package/dist/runtime/server/grants.d.ts +0 -124
  67. 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,40 +165,25 @@ 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`.
@@ -218,13 +248,10 @@ await $fetch(`/api/videos/lessons/${lessonId}/video`, { method: 'PUT', body: { s
218
248
  session's `GET`, parts `PUT`, `finalize` and `abort` to the private
219
249
  `mediaLibrary.baseURL`. It requires the browser's `Bearer mlg_…` grant and
220
250
  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
251
+ browser sends none, and only if one is configured). It never adds static
252
+ auth headers, drops cookies, and streams request bodies. Mount it
223
253
  yourself with `createMLServiceUploadsProxy()` from
224
254
  `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
255
 
229
256
  #### One layer per service, widgets by backend name
230
257
 
@@ -240,7 +267,8 @@ export default defineNuxtConfig({
240
267
  backends: {
241
268
  videos: {
242
269
  upload: { start: '/api/videos/uploads' }, // refresh defaults to `${start}/:sessionId/grant`
243
- claim: '/api/videos/uploads/:sessionId/attach', // py_client on_attach
270
+ claim: '/api/videos/uploads/:sessionId/attach', // pkg_media on_attach
271
+ resolve: '/api/videos/media/resolve', // pkg_media create_media_router
244
272
  },
245
273
  },
246
274
  },
@@ -288,9 +316,10 @@ export default defineNuxtConfig({
288
316
  `(assetId, role, sessionId)`, field: `(assetId, sessionId)`), including
289
317
  files uploaded from the picker and chosen. Send both to the service's
290
318
  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.
319
+ - Processing progress uses the processing stream and, when it is not
320
+ available, polls the backend's `resolve` route (Nuxt's own route without
321
+ one), so the service's `can_access` must let the user see a file they
322
+ just uploaded.
294
323
  - There is no default backend: each layer's widgets name theirs. The module
295
324
  only stores the paths; each layer's own routes reach its service.
296
325
 
@@ -316,6 +345,26 @@ name, extension, MIME type, kind, size, duration, width, height and pages.
316
345
  has none yet.
317
346
  - `useMLMediaMeta(ids, getAsset)` gives the same map for your own lists.
318
347
 
348
+ ### Permalinks
349
+
350
+ A permalink is a stable public address `{media host}/p/{name}` for a
351
+ public asset (server `docs/api/permalinks.md`). Store the name, e.g.
352
+ `users/42/avatar`, and re-point it when the file changes:
353
+
354
+ ```ts
355
+ const { set, get, remove } = useMLPermalinks()
356
+ const link = await set('users/42/avatar', assetId) // { name, assetId, url, createdAt, updatedAt }
357
+ await get('users/42/avatar') // null when unknown
358
+ ```
359
+
360
+ - The media library takes a **service token only**. The module's
361
+ `/api/media-library/manage/permalinks/{name}` proxy works when
362
+ `mediaLibrary.headers` carries one. Otherwise call
363
+ `PUT/GET/DELETE {baseURL}/manage/permalinks/{name}` from your server
364
+ (`permalinkPath(name)` from `/server`).
365
+ - `isValidPermalinkName` checks a name the way the server does;
366
+ `permalinkUrl(mediaHost, name)` builds the public address.
367
+
319
368
  ### Error messages, in the editor's language
320
369
 
321
370
  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.61",
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
3
  import { defineNuxtModule, createResolver, addImportsDir, 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
  }
@@ -128,9 +118,13 @@ const module$1 = defineNuxtModule({
128
118
  route: "/api/media-library/manage/processing/**",
129
119
  handler: resolver.resolve("./runtime/server/processing-proxy")
130
120
  });
121
+ addServerHandler({
122
+ route: "/api/media-library/manage/permalinks/**",
123
+ handler: resolver.resolve("./runtime/server/permalinks-proxy")
124
+ });
131
125
  const shadowed = appCatchAlls(nuxt.options._layers);
132
126
  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 })).`);
127
+ 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
128
  }
135
129
  }
136
130
  if (options.serviceUploads?.proxy) {
@@ -143,10 +137,7 @@ const module$1 = defineNuxtModule({
143
137
  ...existingPrivate,
144
138
  baseURL: existingPrivate.baseURL ?? envBaseURL ?? options.baseURL ?? "",
145
139
  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 ?? ""
140
+ headers: resolvedHeaders
150
141
  };
151
142
  const publicHeaders = browserHeaders(existingPublic.headers ?? options.headers);
152
143
  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;
@@ -1,8 +1,10 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
3
  import { $fetch } from "ofetch";
4
+ import { useRuntimeConfig } from "#imports";
4
5
  import { watchMediaMeta } from "../../composables/useMLMediaMeta";
5
6
  import { kindForFile } from "../../utils/fileAccept";
7
+ import { backendsFromConfig, resolveAssetsUrl } from "../../utils/backends";
6
8
  import { GALLERY_TILE_TYPE } from "../../utils/galleryDrag";
7
9
  import GallerySection from "./GallerySection.vue";
8
10
  const props = defineProps({
@@ -147,11 +149,12 @@ function routeFiles(files) {
147
149
  }
148
150
  if (left.length) notice.value = `${left[0].name}${left.length > 1 ? ` and ${left.length - 1} more` : ""} can't be used in any role here.`;
149
151
  }
152
+ const backends = backendsFromConfig(useRuntimeConfig().public?.mediaLibrary);
150
153
  async function placeByKind(ids) {
151
154
  let kinds = /* @__PURE__ */ new Map();
152
155
  try {
153
156
  const res = await $fetch(
154
- "/api/media-library/manage/assets/resolve",
157
+ resolveAssetsUrl(props.backend, backends),
155
158
  { method: "POST", body: { assetIds: ids } }
156
159
  );
157
160
  kinds = new Map(res.items.map((it, i) => [it.asset?.id ?? it.assetId ?? ids[i], it.asset?.kind?.toLowerCase() ?? 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;
@@ -80,7 +80,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
80
80
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
81
81
  }>, {
82
82
  title: string;
83
- visibility: "private" | "public" | "restricted";
83
+ visibility: "private" | "restricted" | "public";
84
84
  disabled: boolean;
85
85
  highlight: ReadonlySet<string> | null;
86
86
  backend: string | null;
@@ -161,7 +161,8 @@ async function openEditor(id) {
161
161
  siblings: model.value,
162
162
  alt: alt.value ?? {},
163
163
  readonly: props.disabled,
164
- removeLabel: removeLabel.value
164
+ removeLabel: removeLabel.value,
165
+ backend: props.backend
165
166
  });
166
167
  if (result?.action === "save") alt.value = { ...alt.value ?? {}, ...result.alt };
167
168
  else if (result?.action === "remove") gallery.remove([result.assetId]);
@@ -80,7 +80,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
80
80
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
81
81
  }>, {
82
82
  title: string;
83
- visibility: "private" | "public" | "restricted";
83
+ visibility: "private" | "restricted" | "public";
84
84
  disabled: boolean;
85
85
  highlight: ReadonlySet<string> | null;
86
86
  backend: string | null;
@@ -41,9 +41,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
41
41
  asset: AssetDetail | DeliveryAsset | null;
42
42
  src: string | null;
43
43
  progress: number | null;
44
+ name: string;
44
45
  duration: number | null;
45
46
  disabled: boolean;
46
- name: string;
47
47
  phase: ProgressVeilPhase | null;
48
48
  selected: boolean;
49
49
  missingAlt: boolean;
@@ -41,9 +41,9 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
41
41
  asset: AssetDetail | DeliveryAsset | null;
42
42
  src: string | null;
43
43
  progress: number | null;
44
+ name: string;
44
45
  duration: number | null;
45
46
  disabled: boolean;
46
- name: string;
47
47
  phase: ProgressVeilPhase | null;
48
48
  selected: boolean;
49
49
  missingAlt: boolean;
@@ -25,8 +25,8 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
25
25
  kind: string | null;
26
26
  asset: AssetDetail | DeliveryAsset | null;
27
27
  src: string | null;
28
- disabled: boolean;
29
28
  name: string;
29
+ disabled: boolean;
30
30
  meta: string;
31
31
  selected: boolean;
32
32
  disabledReason: string;