nuxt-media-library 0.0.59 → 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 (78) hide show
  1. package/README.md +142 -57
  2. package/dist/module.d.mts +24 -6
  3. package/dist/module.d.ts +24 -6
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +10 -16
  6. package/dist/runtime/components/MLAssetManager/AssetOverview.vue +3 -2
  7. package/dist/runtime/components/MLMedia/Editor.d.vue.ts +1 -0
  8. package/dist/runtime/components/MLMedia/Editor.vue +3 -1
  9. package/dist/runtime/components/MLMedia/Editor.vue.d.ts +1 -0
  10. package/dist/runtime/components/MLMedia/EditorPanel.d.vue.ts +3 -0
  11. package/dist/runtime/components/MLMedia/EditorPanel.vue +3 -2
  12. package/dist/runtime/components/MLMedia/EditorPanel.vue.d.ts +3 -0
  13. package/dist/runtime/components/MLMedia/Field.d.vue.ts +2 -2
  14. package/dist/runtime/components/MLMedia/Field.vue +2 -1
  15. package/dist/runtime/components/MLMedia/Field.vue.d.ts +2 -2
  16. package/dist/runtime/components/MLMedia/FilePickerPanel.vue +1 -1
  17. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +2 -2
  18. package/dist/runtime/components/MLMedia/Gallery.vue +4 -1
  19. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +2 -2
  20. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +1 -1
  21. package/dist/runtime/components/MLMedia/GallerySection.vue +2 -1
  22. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +1 -1
  23. package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +1 -1
  24. package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +1 -1
  25. package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +1 -1
  26. package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +1 -1
  27. package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +1 -1
  28. package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +1 -1
  29. package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +2 -2
  30. package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +2 -2
  31. package/dist/runtime/components/MLMedia/UploadRow.vue +2 -2
  32. package/dist/runtime/components/MLMedia/UploadStep.vue +4 -3
  33. package/dist/runtime/components/MLMedia/UploadSteps.vue +2 -2
  34. package/dist/runtime/components/MLProcessing/Pipeline.vue +2 -2
  35. package/dist/runtime/components/MLProcessing/PipelineStepItem.vue +7 -9
  36. package/dist/runtime/components/MLProcessing/StepperItem.vue +3 -3
  37. package/dist/runtime/composables/useMLAssetLibrary.js +2 -1
  38. package/dist/runtime/composables/useMLAssetManager.js +2 -1
  39. package/dist/runtime/composables/useMLMediaAssets.d.ts +11 -3
  40. package/dist/runtime/composables/useMLMediaAssets.js +31 -6
  41. package/dist/runtime/composables/useMLMediaBrowse.js +2 -1
  42. package/dist/runtime/composables/useMLMediaEditor.d.ts +2 -0
  43. package/dist/runtime/composables/useMLMediaField.js +1 -1
  44. package/dist/runtime/composables/useMLMediaGallery.js +1 -1
  45. package/dist/runtime/composables/useMLMediaUpload.d.ts +3 -3
  46. package/dist/runtime/composables/useMLMediaUpload.js +2 -2
  47. package/dist/runtime/composables/useMLPermalinks.d.ts +20 -0
  48. package/dist/runtime/composables/useMLPermalinks.js +42 -0
  49. package/dist/runtime/composables/useMLProcessingPipeline.js +2 -1
  50. package/dist/runtime/composables/useMLProcessingStepper.js +2 -1
  51. package/dist/runtime/composables/useMLProcessingStrategies.js +29 -17
  52. package/dist/runtime/composables/useMLUploadQueue.js +12 -5
  53. package/dist/runtime/index.d.ts +4 -0
  54. package/dist/runtime/index.js +4 -0
  55. package/dist/runtime/plugins/config.js +7 -0
  56. package/dist/runtime/server/create-ml-proxy.d.ts +1 -17
  57. package/dist/runtime/server/create-ml-proxy.js +35 -114
  58. package/dist/runtime/server/delivery-proxy.js +4 -3
  59. package/dist/runtime/server/index.d.ts +2 -2
  60. package/dist/runtime/server/index.js +1 -7
  61. package/dist/runtime/server/permalinks-proxy.d.ts +2 -0
  62. package/dist/runtime/server/permalinks-proxy.js +2 -0
  63. package/dist/runtime/types/asset.d.ts +12 -4
  64. package/dist/runtime/types/permalink.d.ts +12 -0
  65. package/dist/runtime/types/permalink.js +0 -0
  66. package/dist/runtime/utils/backends.d.ts +28 -2
  67. package/dist/runtime/utils/backends.js +20 -3
  68. package/dist/runtime/utils/credentialHeaders.d.ts +19 -0
  69. package/dist/runtime/utils/credentialHeaders.js +26 -0
  70. package/dist/runtime/utils/errorMessage.d.ts +42 -0
  71. package/dist/runtime/utils/errorMessage.js +112 -0
  72. package/dist/runtime/utils/permalinks.d.ts +11 -0
  73. package/dist/runtime/utils/permalinks.js +16 -0
  74. package/dist/runtime/utils/processingSteps.d.ts +14 -3
  75. package/dist/runtime/utils/processingSteps.js +15 -12
  76. package/package.json +2 -2
  77. package/dist/runtime/server/grants.d.ts +0 -124
  78. 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,62 @@ 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
+
368
+ ### Error messages, in the editor's language
369
+
370
+ When a backend refuses a request it answers `{ detail, reason }`: `reason`
371
+ is a stable code (py_client `UploadRejected`: `too_large`, `mime_type`,
372
+ `context_mismatch`, …), `detail` English text. The widgets show, first that
373
+ applies:
374
+
375
+ 1. the text for a known `reason`, in the editor's language;
376
+ 2. `detail` (a 422's first `msg`);
377
+ 3. the backend's inner `message`, then the status text;
378
+ 4. the place's own fallback ("Upload failed", "Could not add the file", …).
379
+
380
+ The body is read whether the module's own proxy streamed it (`err.data`) or
381
+ an app proxy rethrew it with h3 `createError({ data })` (`err.data.data`).
382
+ The raw "[POST] url: 400" text is never shown.
383
+
384
+ The language is the app's `@nuxtjs/i18n` locale, else `mediaLibrary.locale`;
385
+ its language part picks the texts (`ar-SA` → `ar`). Built in: `en` and `ar`.
386
+ An app can change any text, or add a language:
387
+
388
+ ```ts
389
+ mediaLibrary: {
390
+ locale: 'ar', // without @nuxtjs/i18n
391
+ errorTexts: {
392
+ ar: {
393
+ reasons: { too_large: 'حجم هذا الملف أكبر من المسموح.' },
394
+ fallbacks: { upload: 'فشل رفع الملف' },
395
+ },
396
+ },
397
+ },
398
+ ```
399
+
400
+ A reason with no text in the editor's language uses the English one, then
401
+ `detail`. `mediaErrorMessage(err, fallback, { locale, texts })` and
402
+ `mlErrorMessage(err, fallback)` are exported for your own pages.
403
+
319
404
  ### Vue installation (non-Nuxt)
320
405
 
321
406
  ```bash
package/dist/module.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
  import { ServiceUploadBackend } from '../dist/runtime/utils/uploader/serviceBackends.js';
3
3
  import { MediaBackends } from '../dist/runtime/utils/backends.js';
4
+ import { MediaErrorTextsByLocale } from '../dist/runtime/utils/errorMessage.js';
4
5
  export * from '../dist/runtime/types/resourcePolicy.js';
5
6
  export * from '../dist/runtime/types/uploader.js';
6
7
  export * from '../dist/runtime/types/preview.js';
@@ -35,6 +36,11 @@ interface ModuleOptions {
35
36
  perPart?: boolean;
36
37
  final?: boolean;
37
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
+ */
38
44
  headers?: Record<string, string> | (() => Record<string, string>);
39
45
  /**
40
46
  * Uploads started by the app's backend services (`useMLServiceUpload`).
@@ -55,23 +61,35 @@ interface ModuleOptions {
55
61
  /**
56
62
  * The built-in `/api/media-library/manage/**` proxies (default `true`).
57
63
  * Set `false` when the app serves `/manage` itself, e.g. with
58
- * `createMLCatchAllProxy({ authorize })` in
64
+ * `createMLCatchAllProxy({ requestHeaders })` in
59
65
  * `server/api/media-library/[...].ts`: the built-in routes are more
60
- * specific than an app's catch-all and would otherwise answer first, so
61
- * `authorize` would never run. `/delivery/**` and the service-upload
62
- * 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.
63
68
  */
64
69
  manageProxy?: boolean;
65
70
  /**
66
71
  * The backend registry: named places widgets upload to and read from
67
72
  * (`<MLMediaGallery backend="videos" />`). Each entry has a `kind`
68
73
  * (`media-library`, the default, or `media-kit`) and its routes: `upload`
69
- * (`{ start, refresh? }`, py_client `create_upload_router`), `claim`
70
- * (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
71
78
  * layer registers its own; Nuxt merges them. Paths only: each layer serves
72
79
  * its routes.
73
80
  */
74
81
  backends?: MediaBackends;
82
+ /**
83
+ * Language of the widgets' error texts when the app has no i18n locale
84
+ * (with @nuxtjs/i18n its active locale wins). Built in: `en`, `ar`.
85
+ */
86
+ locale?: string;
87
+ /**
88
+ * The app's error texts over the built-in ones, by language:
89
+ * `{ ar: { reasons: { too_large: '…' }, fallbacks: { upload: '…' } } }`.
90
+ * `reasons` are backend reason codes (py_client `UploadRejected`).
91
+ */
92
+ errorTexts?: MediaErrorTextsByLocale;
75
93
  }
76
94
  declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
77
95
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
package/dist/module.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
2
  import { ServiceUploadBackend } from '../dist/runtime/utils/uploader/serviceBackends.js';
3
3
  import { MediaBackends } from '../dist/runtime/utils/backends.js';
4
+ import { MediaErrorTextsByLocale } from '../dist/runtime/utils/errorMessage.js';
4
5
  export * from '../dist/runtime/types/resourcePolicy.js';
5
6
  export * from '../dist/runtime/types/uploader.js';
6
7
  export * from '../dist/runtime/types/preview.js';
@@ -35,6 +36,11 @@ interface ModuleOptions {
35
36
  perPart?: boolean;
36
37
  final?: boolean;
37
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
+ */
38
44
  headers?: Record<string, string> | (() => Record<string, string>);
39
45
  /**
40
46
  * Uploads started by the app's backend services (`useMLServiceUpload`).
@@ -55,23 +61,35 @@ interface ModuleOptions {
55
61
  /**
56
62
  * The built-in `/api/media-library/manage/**` proxies (default `true`).
57
63
  * Set `false` when the app serves `/manage` itself, e.g. with
58
- * `createMLCatchAllProxy({ authorize })` in
64
+ * `createMLCatchAllProxy({ requestHeaders })` in
59
65
  * `server/api/media-library/[...].ts`: the built-in routes are more
60
- * specific than an app's catch-all and would otherwise answer first, so
61
- * `authorize` would never run. `/delivery/**` and the service-upload
62
- * 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.
63
68
  */
64
69
  manageProxy?: boolean;
65
70
  /**
66
71
  * The backend registry: named places widgets upload to and read from
67
72
  * (`<MLMediaGallery backend="videos" />`). Each entry has a `kind`
68
73
  * (`media-library`, the default, or `media-kit`) and its routes: `upload`
69
- * (`{ start, refresh? }`, py_client `create_upload_router`), `claim`
70
- * (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
71
78
  * layer registers its own; Nuxt merges them. Paths only: each layer serves
72
79
  * its routes.
73
80
  */
74
81
  backends?: MediaBackends;
82
+ /**
83
+ * Language of the widgets' error texts when the app has no i18n locale
84
+ * (with @nuxtjs/i18n its active locale wins). Built in: `en`, `ar`.
85
+ */
86
+ locale?: string;
87
+ /**
88
+ * The app's error texts over the built-in ones, by language:
89
+ * `{ ar: { reasons: { too_large: '…' }, fallbacks: { upload: '…' } } }`.
90
+ * `reasons` are backend reason codes (py_client `UploadRejected`).
91
+ */
92
+ errorTexts?: MediaErrorTextsByLocale;
75
93
  }
76
94
  declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
77
95
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "media-library",
3
3
  "configKey": "mediaLibrary",
4
- "version": "0.0.59",
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) {
@@ -177,6 +168,9 @@ const module$1 = defineNuxtModule({
177
168
  // The backend registry (NUXT_PUBLIC_MEDIA_LIBRARY_BACKENDS): names widgets pass as `backend`.
178
169
  backends,
179
170
  // Older form, kept for code that reads it: each backend's upload routes.
171
+ // NUXT_PUBLIC_MEDIA_LIBRARY_LOCALE; the app's i18n locale wins at runtime.
172
+ locale: existingPublic.locale || options.locale || "",
173
+ errorTexts: { ...options.errorTexts, ...existingPublic.errorTexts },
180
174
  serviceUploadBackends: Object.fromEntries(
181
175
  Object.entries(backends).flatMap(([name, b]) => b.upload ? [[name, b.upload]] : [])
182
176
  )
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { computed, ref, watch } from "vue";
3
3
  import { useMLResourcePolicy, Visibility } from "../../index";
4
+ import { mlErrorMessage } from "../../utils/errorMessage";
4
5
  const props = defineProps({
5
6
  asset: { type: Object, required: true },
6
7
  manager: { type: Object, required: false }
@@ -71,7 +72,7 @@ async function changeVisibility(next) {
71
72
  try {
72
73
  await props.manager.updateAsset(props.asset.id, { visibility: next });
73
74
  } catch (err) {
74
- visibilityError.value = err instanceof Error ? err.message : "Failed to update visibility";
75
+ visibilityError.value = mlErrorMessage(err, "updateVisibility");
75
76
  } finally {
76
77
  visibilityUpdating.value = false;
77
78
  }
@@ -138,7 +139,7 @@ async function loadPolicy() {
138
139
  try {
139
140
  policy.value = await policyApi.get(id);
140
141
  } catch (err) {
141
- policyError.value = err instanceof Error ? err.message : "Failed to load policy";
142
+ policyError.value = mlErrorMessage(err, "loadPolicy");
142
143
  policy.value = null;
143
144
  } finally {
144
145
  policyLoading.value = false;
@@ -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;