nuxt-media-library 0.0.49 → 0.0.51

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 (39) hide show
  1. package/README.md +58 -3
  2. package/dist/module.d.mts +24 -1
  3. package/dist/module.d.ts +24 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +40 -5
  6. package/dist/runtime/components/MLMedia/Dropzone.d.vue.ts +3 -3
  7. package/dist/runtime/components/MLMedia/Dropzone.vue.d.ts +3 -3
  8. package/dist/runtime/components/MLMedia/Field.d.vue.ts +4 -4
  9. package/dist/runtime/components/MLMedia/Field.vue.d.ts +4 -4
  10. package/dist/runtime/components/MLMedia/FilePicker.d.vue.ts +1 -1
  11. package/dist/runtime/components/MLMedia/FilePicker.vue.d.ts +1 -1
  12. package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +1 -1
  13. package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +1 -1
  14. package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +3 -3
  15. package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +3 -3
  16. package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +5 -5
  17. package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +5 -5
  18. package/dist/runtime/components/MLMedia/KindBadge.d.vue.ts +1 -1
  19. package/dist/runtime/components/MLMedia/KindBadge.vue.d.ts +1 -1
  20. package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +2 -2
  21. package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +2 -2
  22. package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +3 -3
  23. package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +3 -3
  24. package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +2 -2
  25. package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +2 -2
  26. package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +1 -1
  27. package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +1 -1
  28. package/dist/runtime/components/MLMedia/StatusBadge.d.vue.ts +1 -1
  29. package/dist/runtime/components/MLMedia/StatusBadge.vue.d.ts +1 -1
  30. package/dist/runtime/composables/useMLServiceUpload.d.ts +10 -2
  31. package/dist/runtime/composables/useMLServiceUpload.js +4 -1
  32. package/dist/runtime/server/index.d.ts +2 -0
  33. package/dist/runtime/server/index.js +1 -0
  34. package/dist/runtime/server/service-uploads-proxy.d.ts +31 -0
  35. package/dist/runtime/server/service-uploads-proxy.js +61 -0
  36. package/dist/runtime/utils/uploader/serviceUpload.d.ts +16 -2
  37. package/dist/runtime/utils/uploader/serviceUpload.js +5 -1
  38. package/dist/types.d.mts +1 -1
  39. package/package.json +1 -1
package/README.md CHANGED
@@ -47,8 +47,11 @@ export default defineNuxtConfig({
47
47
  mediaLibrary: {
48
48
  // Auto-register Vue components from src/runtime/components
49
49
  components: true,
50
- // Chunk uploader config (used by MlFileUpload)
51
- baseURL: 'https://api.example.com',
50
+ // Where the Nuxt server reaches the media library (with /v1). Server-side
51
+ // only, never sent to browsers; prefer NUXT_MEDIA_LIBRARY_BASE_URL.
52
+ baseURL: 'http://media-library.internal/v1',
53
+ // Only for direct browser uploads (proxyUploads: false): a public address.
54
+ // publicBaseURL: 'https://media.example.com/v1',
52
55
  // Uploads always go through Nuxt server (/api/media-library/uploads/*)
53
56
  routes: {
54
57
  createSession: '/uploads/sessions',
@@ -60,11 +63,18 @@ export default defineNuxtConfig({
60
63
  partSize: 8 * 1024 * 1024,
61
64
  concurrency: 3,
62
65
  checksum: { algorithm: 'SHA-256', perPart: true, final: true },
63
- headers: {},
66
+ // Sent to browsers too, except credentials: Authorization, Cookie,
67
+ // Proxy-Authorization and X-Api-Key stay server-side (the proxies add them).
68
+ headers: { 'X-Tenant': 'acme' },
64
69
  },
65
70
  })
66
71
  ```
67
72
 
73
+ Nuxt puts the public runtime config in every page, so the module never copies
74
+ `baseURL` or credential headers there. Set the media library's internal
75
+ address and any service token only server-side
76
+ (`NUXT_MEDIA_LIBRARY_BASE_URL`, `NUXT_MEDIA_LIBRARY_SERVICE_TOKEN`).
77
+
68
78
  ### Nuxt usage
69
79
 
70
80
  ```vue
@@ -153,6 +163,51 @@ addMediaGrants(lesson.mediaGrant, profile.mediaGrant) // "mlg_…" or { grant, e
153
163
  grants are added.
154
164
  - Grants live in the browser only, never in server-side state.
155
165
 
166
+ ### Uploads started by your backend services
167
+
168
+ When a backend service starts the upload (the Python client's
169
+ `create_upload_router`), the service creates the session and returns a grant
170
+ for it, and `useMLServiceUpload` sends the parts. Where the parts go is the
171
+ app's decision, not the service's:
172
+
173
+ ```ts
174
+ // nuxt.config.ts
175
+ export default defineNuxtConfig({
176
+ mediaLibrary: {
177
+ // server side only: set NUXT_MEDIA_LIBRARY_BASE_URL (e.g. http://media-library.internal/v1)
178
+ serviceUploads: { proxy: true }, // parts go through /api/media-library/service-uploads
179
+ // or, direct to the media library (needs CORS for the site):
180
+ // serviceUploads: { baseURL: 'https://media.example.com/v1' },
181
+ },
182
+ })
183
+ ```
184
+
185
+ ```ts
186
+ const { upload, progress } = useMLServiceUpload()
187
+ const { sessionId, assetId } = await upload(file, {
188
+ start: '/api/videos/uploads',
189
+ body: { profile: 'lesson-video', context: { lessonId } },
190
+ })
191
+ await $fetch(`/api/videos/lessons/${lessonId}/video`, { method: 'PUT', body: { sessionId, assetId } })
192
+ ```
193
+
194
+ - The upload base is, in order: `upload(file, { baseURL })`,
195
+ `useMLServiceUpload({ baseURL })`, `serviceUploads.baseURL`
196
+ (`NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BASE_URL`), the pass-through
197
+ route when `serviceUploads.proxy` is on, and last the `uploadBaseUrl` an
198
+ older service returns. With none, `upload` fails before any part is sent.
199
+ - The pass-through route (`serviceUploads.proxy: true`) forwards only a
200
+ session's `GET`, parts `PUT`, `finalize` and `abort` to the private
201
+ `mediaLibrary.baseURL`. It requires the browser's `Bearer mlg_…` grant and
202
+ forwards it and `X-Tenant` untouched (the static `X-Tenant` when the
203
+ browser sends none). It never adds the service token, static auth headers
204
+ or a Nuxt-issued grant, drops cookies, and streams request bodies. Mount it
205
+ yourself with `createMLServiceUploadsProxy()` from
206
+ `nuxt-media-library/server` for another path.
207
+ - Don't send these uploads through `createMLCatchAllProxy` with `authorize`:
208
+ grant mode replaces the browser's grant with its own, and the service's
209
+ `claim_upload` rejects the session.
210
+
156
211
  ### Vue installation (non-Nuxt)
157
212
 
158
213
  ```bash
package/dist/module.d.mts CHANGED
@@ -14,7 +14,17 @@ interface ChunkUploaderRoutesConfig {
14
14
  }
15
15
  interface ModuleOptions {
16
16
  components?: boolean;
17
+ /**
18
+ * Where the Nuxt server reaches the media library, with `/v1` (often an
19
+ * internal address). Server-side only: never sent to browsers. Prefer
20
+ * NUXT_MEDIA_LIBRARY_BASE_URL at runtime.
21
+ */
17
22
  baseURL?: string;
23
+ /**
24
+ * The media library address browsers use for direct uploads
25
+ * (`proxyUploads: false`), with `/v1`. Public: only set a public address.
26
+ */
27
+ publicBaseURL?: string;
18
28
  routes?: ChunkUploaderRoutesConfig;
19
29
  partSize?: number;
20
30
  concurrency?: number;
@@ -24,8 +34,21 @@ interface ModuleOptions {
24
34
  final?: boolean;
25
35
  };
26
36
  headers?: Record<string, string> | (() => Record<string, string>);
37
+ /**
38
+ * Uploads started by the app's backend services (`useMLServiceUpload`).
39
+ * `proxy: true` adds `/api/media-library/service-uploads/**`, a same-origin
40
+ * pass-through that forwards the browser's media grant and nothing else.
41
+ * `baseURL` is where the browser sends those uploads (the media library's
42
+ * public API root with `/v1`, or a proxy route); with `proxy` it defaults
43
+ * to the pass-through route.
44
+ */
45
+ serviceUploads?: {
46
+ proxy?: boolean;
47
+ baseURL?: string;
48
+ };
27
49
  }
50
+ declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
28
51
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
29
52
 
30
- export { _default as default };
53
+ export { SERVICE_UPLOADS_ROUTE, _default as default };
31
54
  export type { ChunkUploaderRoutesConfig, ModuleOptions };
package/dist/module.d.ts CHANGED
@@ -14,7 +14,17 @@ interface ChunkUploaderRoutesConfig {
14
14
  }
15
15
  interface ModuleOptions {
16
16
  components?: boolean;
17
+ /**
18
+ * Where the Nuxt server reaches the media library, with `/v1` (often an
19
+ * internal address). Server-side only: never sent to browsers. Prefer
20
+ * NUXT_MEDIA_LIBRARY_BASE_URL at runtime.
21
+ */
17
22
  baseURL?: string;
23
+ /**
24
+ * The media library address browsers use for direct uploads
25
+ * (`proxyUploads: false`), with `/v1`. Public: only set a public address.
26
+ */
27
+ publicBaseURL?: string;
18
28
  routes?: ChunkUploaderRoutesConfig;
19
29
  partSize?: number;
20
30
  concurrency?: number;
@@ -24,8 +34,21 @@ interface ModuleOptions {
24
34
  final?: boolean;
25
35
  };
26
36
  headers?: Record<string, string> | (() => Record<string, string>);
37
+ /**
38
+ * Uploads started by the app's backend services (`useMLServiceUpload`).
39
+ * `proxy: true` adds `/api/media-library/service-uploads/**`, a same-origin
40
+ * pass-through that forwards the browser's media grant and nothing else.
41
+ * `baseURL` is where the browser sends those uploads (the media library's
42
+ * public API root with `/v1`, or a proxy route); with `proxy` it defaults
43
+ * to the pass-through route.
44
+ */
45
+ serviceUploads?: {
46
+ proxy?: boolean;
47
+ baseURL?: string;
48
+ };
27
49
  }
50
+ declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
28
51
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
29
52
 
30
- export { _default as default };
53
+ export { SERVICE_UPLOADS_ROUTE, _default as default };
31
54
  export type { ChunkUploaderRoutesConfig, ModuleOptions };
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "media-library",
3
3
  "configKey": "mediaLibrary",
4
- "version": "0.0.49",
4
+ "version": "0.0.51",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,9 +1,25 @@
1
- import { defineNuxtModule, createResolver, addImportsDir, addComponentsDir, addServerHandler, addPlugin } from '@nuxt/kit';
1
+ import { defineNuxtModule, createResolver, addImportsDir, addComponentsDir, addServerHandler, useLogger, addPlugin } from '@nuxt/kit';
2
2
  export * from '../dist/runtime/types/resourcePolicy.js';
3
3
  export * from '../dist/runtime/types/uploader.js';
4
4
  export * from '../dist/runtime/types/preview.js';
5
5
  export * from '../dist/runtime/types/asset.js';
6
6
 
7
+ const CREDENTIAL_HEADERS = ["authorization", "proxy-authorization", "cookie", "x-api-key"];
8
+ function browserHeaders(headers) {
9
+ if (!headers || typeof headers !== "object") return { headers: {}, dropped: [] };
10
+ const kept = {};
11
+ const dropped = [];
12
+ for (const [name, value] of Object.entries(headers)) {
13
+ if (CREDENTIAL_HEADERS.includes(name.toLowerCase())) dropped.push(name);
14
+ else kept[name] = value;
15
+ }
16
+ return { headers: kept, dropped };
17
+ }
18
+ function browserBaseURL(existingPublic, publicBaseURL) {
19
+ return existingPublic ?? publicBaseURL ?? "";
20
+ }
21
+
22
+ const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
7
23
  const module$1 = defineNuxtModule({
8
24
  meta: {
9
25
  name: "media-library",
@@ -37,7 +53,10 @@ const module$1 = defineNuxtModule({
37
53
  const envTenantHeader = process.env.NUXT_MEDIA_LIBRARY_HEADERS_X_TENANT;
38
54
  const envHeaders = envTenantHeader ? { "X-tenant": envTenantHeader } : {};
39
55
  const existingHeaders = typeof existingPrivate.headers === "object" ? existingPrivate.headers : {};
40
- const resolvedHeaders = { ...existingHeaders };
56
+ const optionCredentials = Object.fromEntries(
57
+ browserHeaders(options.headers).dropped.map((name) => [name, options.headers[name]])
58
+ );
59
+ const resolvedHeaders = { ...optionCredentials, ...existingHeaders };
41
60
  for (const [key, value] of Object.entries(envHeaders)) {
42
61
  if (!resolvedHeaders[key]) {
43
62
  resolvedHeaders[key] = value;
@@ -89,6 +108,12 @@ const module$1 = defineNuxtModule({
89
108
  route: "/api/media-library/manage/processing/**",
90
109
  handler: resolver.resolve("./runtime/server/processing-proxy")
91
110
  });
111
+ if (options.serviceUploads?.proxy) {
112
+ addServerHandler({
113
+ route: `${SERVICE_UPLOADS_ROUTE}/**`,
114
+ handler: resolver.resolve("./runtime/server/service-uploads-proxy")
115
+ });
116
+ }
92
117
  nuxt.options.runtimeConfig.mediaLibrary = {
93
118
  ...existingPrivate,
94
119
  baseURL: existingPrivate.baseURL ?? envBaseURL ?? options.baseURL ?? "",
@@ -98,9 +123,17 @@ const module$1 = defineNuxtModule({
98
123
  // grants). Declared so NUXT_MEDIA_LIBRARY_SERVICE_TOKEN applies.
99
124
  serviceToken: existingPrivate.serviceToken ?? ""
100
125
  };
126
+ const logger = useLogger("media-library");
127
+ const publicHeaders = browserHeaders(existingPublic.headers ?? options.headers);
128
+ if (publicHeaders.dropped.length) {
129
+ logger.warn(`mediaLibrary.headers: ${publicHeaders.dropped.join(", ")} kept server-side, not sent to browsers.`);
130
+ }
131
+ if (existingPublic.proxyUploads === false && !existingPublic.baseURL && !options.publicBaseURL) {
132
+ logger.warn("proxyUploads is false but no public media library address is set: set mediaLibrary.publicBaseURL. baseURL is server-side only.");
133
+ }
101
134
  nuxt.options.runtimeConfig.public.mediaLibrary = {
102
135
  ...existingPublic,
103
- baseURL: existingPublic.baseURL ?? options.baseURL ?? "",
136
+ baseURL: browserBaseURL(existingPublic.baseURL, options.publicBaseURL),
104
137
  routes: existingPublic.routes ?? routes,
105
138
  proxyUploads: existingPublic.proxyUploads ?? true,
106
139
  partSize: existingPublic.partSize ?? options.partSize ?? 8 * 1024 * 1024,
@@ -110,7 +143,9 @@ const module$1 = defineNuxtModule({
110
143
  perPart: existingPublic.checksum?.perPart ?? options.checksum?.perPart ?? true,
111
144
  final: existingPublic.checksum?.final ?? options.checksum?.final ?? true
112
145
  },
113
- headers: existingPublic.headers ?? options.headers ?? {}
146
+ headers: publicHeaders.headers,
147
+ // Where useMLServiceUpload sends parts (NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BASE_URL).
148
+ serviceUploadBaseURL: existingPublic.serviceUploadBaseURL || options.serviceUploads?.baseURL || (options.serviceUploads?.proxy ? SERVICE_UPLOADS_ROUTE : "")
114
149
  };
115
150
  addPlugin({
116
151
  src: resolver.resolve("./runtime/plugins/config")
@@ -118,4 +153,4 @@ const module$1 = defineNuxtModule({
118
153
  }
119
154
  });
120
155
 
121
- export { module$1 as default };
156
+ export { SERVICE_UPLOADS_ROUTE, module$1 as default };
@@ -39,13 +39,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
39
39
  onFiles?: ((files: File[]) => any) | undefined;
40
40
  onRejected?: ((files: RejectedFile[]) => any) | undefined;
41
41
  }>, {
42
- title: string;
43
- accept: string;
44
- maxSize: number | null;
45
42
  variant: "area" | "overlay";
46
43
  compact: boolean;
47
44
  disabled: boolean;
48
45
  multiple: boolean;
46
+ title: string;
47
+ accept: string;
48
+ maxSize: number | null;
49
49
  maxFiles: number | null;
50
50
  hint: string;
51
51
  buttonLabel: string;
@@ -39,13 +39,13 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
39
39
  onFiles?: ((files: File[]) => any) | undefined;
40
40
  onRejected?: ((files: RejectedFile[]) => any) | undefined;
41
41
  }>, {
42
- title: string;
43
- accept: string;
44
- maxSize: number | null;
45
42
  variant: "area" | "overlay";
46
43
  compact: boolean;
47
44
  disabled: boolean;
48
45
  multiple: boolean;
46
+ title: string;
47
+ accept: string;
48
+ maxSize: number | null;
49
49
  maxFiles: number | null;
50
50
  hint: string;
51
51
  buttonLabel: string;
@@ -41,19 +41,19 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
41
41
  "onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
42
42
  "onUpdate:alt"?: ((value: string | undefined) => any) | undefined;
43
43
  }>, {
44
- visibility: "private" | "restricted" | "public";
44
+ disabled: boolean;
45
+ visibility: "private" | "public" | "restricted";
45
46
  label: string;
46
47
  accept: readonly string[] | string;
47
48
  maxSize: number | null;
49
+ required: boolean;
50
+ removeLabel: string;
48
51
  processingConfig: {
49
52
  stepOptions?: Record<string, Record<string, unknown>> | null;
50
53
  useFilenameAsAssetId?: boolean;
51
54
  enableTranscription?: boolean;
52
55
  };
53
56
  collectionName: string | null;
54
- disabled: boolean;
55
- required: boolean;
56
- removeLabel: string;
57
57
  hint: string;
58
58
  missingAlt: boolean;
59
59
  picker: boolean;
@@ -41,19 +41,19 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
41
41
  "onUpdate:modelValue"?: ((value: string | null) => any) | undefined;
42
42
  "onUpdate:alt"?: ((value: string | undefined) => any) | undefined;
43
43
  }>, {
44
- visibility: "private" | "restricted" | "public";
44
+ disabled: boolean;
45
+ visibility: "private" | "public" | "restricted";
45
46
  label: string;
46
47
  accept: readonly string[] | string;
47
48
  maxSize: number | null;
49
+ required: boolean;
50
+ removeLabel: string;
48
51
  processingConfig: {
49
52
  stepOptions?: Record<string, Record<string, unknown>> | null;
50
53
  useFilenameAsAssetId?: boolean;
51
54
  enableTranscription?: boolean;
52
55
  };
53
56
  collectionName: string | null;
54
- disabled: boolean;
55
- required: boolean;
56
- removeLabel: string;
57
57
  hint: string;
58
58
  missingAlt: boolean;
59
59
  picker: boolean;
@@ -14,9 +14,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
14
14
  }>, {
15
15
  title: string;
16
16
  accept: readonly string[] | string;
17
- maxSize: number | null;
18
17
  max: number;
19
18
  selected: readonly string[];
19
+ maxSize: number | null;
20
20
  upload: MediaUploadOptions;
21
21
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
22
  declare const _default: typeof __VLS_export;
@@ -14,9 +14,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
14
14
  }>, {
15
15
  title: string;
16
16
  accept: readonly string[] | string;
17
- maxSize: number | null;
18
17
  max: number;
19
18
  selected: readonly string[];
19
+ maxSize: number | null;
20
20
  upload: MediaUploadOptions;
21
21
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
22
22
  declare const _default: typeof __VLS_export;
@@ -23,9 +23,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
23
23
  }>, {
24
24
  title: string;
25
25
  accept: readonly string[] | string;
26
- maxSize: number | null;
27
26
  max: number;
28
27
  selected: readonly string[];
28
+ maxSize: number | null;
29
29
  upload: MediaUploadOptions;
30
30
  bodyClass: string;
31
31
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -23,9 +23,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
23
23
  }>, {
24
24
  title: string;
25
25
  accept: readonly string[] | string;
26
- maxSize: number | null;
27
26
  max: number;
28
27
  selected: readonly string[];
28
+ maxSize: number | null;
29
29
  upload: MediaUploadOptions;
30
30
  bodyClass: string;
31
31
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -51,10 +51,12 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
51
51
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
52
52
  "onUpdate:profiles"?: ((profiles: MediaGalleryProfiles) => any) | undefined;
53
53
  }>, {
54
- visibility: "private" | "restricted" | "public";
54
+ disabled: boolean;
55
55
  title: string;
56
+ visibility: "private" | "public" | "restricted";
56
57
  profile: string | null;
57
58
  accept: readonly string[] | string;
59
+ max: number | null;
58
60
  maxSize: number | null;
59
61
  processingConfig: {
60
62
  stepOptions?: Record<string, Record<string, unknown>> | null;
@@ -62,8 +64,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
62
64
  enableTranscription?: boolean;
63
65
  };
64
66
  collectionName: string | null;
65
- disabled: boolean;
66
- max: number | null;
67
67
  hint: string;
68
68
  picker: boolean;
69
69
  onPick: () => void;
@@ -51,10 +51,12 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
51
51
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
52
52
  "onUpdate:profiles"?: ((profiles: MediaGalleryProfiles) => any) | undefined;
53
53
  }>, {
54
- visibility: "private" | "restricted" | "public";
54
+ disabled: boolean;
55
55
  title: string;
56
+ visibility: "private" | "public" | "restricted";
56
57
  profile: string | null;
57
58
  accept: readonly string[] | string;
59
+ max: number | null;
58
60
  maxSize: number | null;
59
61
  processingConfig: {
60
62
  stepOptions?: Record<string, Record<string, unknown>> | null;
@@ -62,8 +64,6 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
62
64
  enableTranscription?: boolean;
63
65
  };
64
66
  collectionName: string | null;
65
- disabled: boolean;
66
- max: number | null;
67
67
  hint: string;
68
68
  picker: boolean;
69
69
  onPick: () => void;
@@ -66,21 +66,21 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
66
66
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
67
67
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
68
68
  }>, {
69
- visibility: "private" | "restricted" | "public";
69
+ disabled: boolean;
70
+ highlight: ReadonlySet<string> | null;
70
71
  title: string;
72
+ visibility: "private" | "public" | "restricted";
71
73
  profile: string | null;
72
74
  accept: readonly string[] | string;
75
+ max: number | null;
73
76
  maxSize: number | null;
77
+ removeLabel: string;
74
78
  processingConfig: {
75
79
  stepOptions?: Record<string, Record<string, unknown>> | null;
76
80
  useFilenameAsAssetId?: boolean;
77
81
  enableTranscription?: boolean;
78
82
  };
79
83
  collectionName: string | null;
80
- disabled: boolean;
81
- highlight: ReadonlySet<string> | null;
82
- removeLabel: string;
83
- max: number | null;
84
84
  onAdd: (assetId: string, tag: string | undefined) => void;
85
85
  hint: string;
86
86
  picker: boolean;
@@ -66,21 +66,21 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
66
66
  "onUpdate:modelValue"?: ((value: string[]) => any) | undefined;
67
67
  "onUpdate:alt"?: ((value: Record<string, string> | undefined) => any) | undefined;
68
68
  }>, {
69
- visibility: "private" | "restricted" | "public";
69
+ disabled: boolean;
70
+ highlight: ReadonlySet<string> | null;
70
71
  title: string;
72
+ visibility: "private" | "public" | "restricted";
71
73
  profile: string | null;
72
74
  accept: readonly string[] | string;
75
+ max: number | null;
73
76
  maxSize: number | null;
77
+ removeLabel: string;
74
78
  processingConfig: {
75
79
  stepOptions?: Record<string, Record<string, unknown>> | null;
76
80
  useFilenameAsAssetId?: boolean;
77
81
  enableTranscription?: boolean;
78
82
  };
79
83
  collectionName: string | null;
80
- disabled: boolean;
81
- highlight: ReadonlySet<string> | null;
82
- removeLabel: string;
83
- max: number | null;
84
84
  onAdd: (assetId: string, tag: string | undefined) => void;
85
85
  hint: string;
86
86
  picker: boolean;
@@ -9,7 +9,7 @@ type __VLS_Props = {
9
9
  variant?: BadgeProps['variant'];
10
10
  };
11
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
12
- size: "md" | "xs" | "sm" | "lg" | "xl";
12
+ size: "xs" | "sm" | "md" | "lg" | "xl";
13
13
  variant: "solid" | "outline" | "soft" | "subtle";
14
14
  icon: boolean;
15
15
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -9,7 +9,7 @@ type __VLS_Props = {
9
9
  variant?: BadgeProps['variant'];
10
10
  };
11
11
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
12
- size: "md" | "xs" | "sm" | "lg" | "xl";
12
+ size: "xs" | "sm" | "md" | "lg" | "xl";
13
13
  variant: "solid" | "outline" | "soft" | "subtle";
14
14
  icon: boolean;
15
15
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -40,10 +40,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
40
40
  asset: AssetDetail | DeliveryAsset | null;
41
41
  src: string | null;
42
42
  progress: number | null;
43
- name: string;
43
+ disabled: boolean;
44
44
  kind: string | null;
45
+ name: string;
45
46
  phase: ProgressVeilPhase | null;
46
- disabled: boolean;
47
47
  selected: boolean;
48
48
  missingAlt: boolean;
49
49
  onOpen: () => void;
@@ -40,10 +40,10 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
40
40
  asset: AssetDetail | DeliveryAsset | null;
41
41
  src: string | null;
42
42
  progress: number | null;
43
- name: string;
43
+ disabled: boolean;
44
44
  kind: string | null;
45
+ name: string;
45
46
  phase: ProgressVeilPhase | null;
46
- disabled: boolean;
47
47
  selected: boolean;
48
48
  missingAlt: boolean;
49
49
  onOpen: () => void;
@@ -24,11 +24,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
24
24
  }>, {
25
25
  asset: AssetDetail | DeliveryAsset | null;
26
26
  src: string | null;
27
- name: string;
28
- kind: string | null;
29
27
  disabled: boolean;
30
- meta: string;
28
+ kind: string | null;
29
+ name: string;
31
30
  selected: boolean;
31
+ meta: string;
32
32
  disabledReason: string;
33
33
  onPreview: () => void;
34
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -24,11 +24,11 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {},
24
24
  }>, {
25
25
  asset: AssetDetail | DeliveryAsset | null;
26
26
  src: string | null;
27
- name: string;
28
- kind: string | null;
29
27
  disabled: boolean;
30
- meta: string;
28
+ kind: string | null;
29
+ name: string;
31
30
  selected: boolean;
31
+ meta: string;
32
32
  disabledReason: string;
33
33
  onPreview: () => void;
34
34
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -20,9 +20,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
20
20
  size: string;
21
21
  asset: AssetDetail | DeliveryAsset | null;
22
22
  src: string | null;
23
- name: string;
24
- kind: string | null;
25
23
  disabled: boolean;
24
+ kind: string | null;
25
+ name: string;
26
26
  selected: boolean;
27
27
  date: string;
28
28
  disabledReason: string;
@@ -20,9 +20,9 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
20
20
  size: string;
21
21
  asset: AssetDetail | DeliveryAsset | null;
22
22
  src: string | null;
23
- name: string;
24
- kind: string | null;
25
23
  disabled: boolean;
24
+ kind: string | null;
25
+ name: string;
26
26
  selected: boolean;
27
27
  date: string;
28
28
  disabledReason: string;
@@ -15,8 +15,8 @@ type __VLS_Props = {
15
15
  };
16
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
17
17
  progress: number | null;
18
- label: string;
19
18
  compact: boolean;
19
+ label: string;
20
20
  onCancel: () => void;
21
21
  onRetry: () => void;
22
22
  detail: string;
@@ -15,8 +15,8 @@ type __VLS_Props = {
15
15
  };
16
16
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
17
17
  progress: number | null;
18
- label: string;
19
18
  compact: boolean;
19
+ label: string;
20
20
  onCancel: () => void;
21
21
  onRetry: () => void;
22
22
  detail: string;
@@ -8,7 +8,7 @@ type __VLS_Props = {
8
8
  variant?: BadgeProps['variant'];
9
9
  };
10
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
- size: "md" | "xs" | "sm" | "lg" | "xl";
11
+ size: "xs" | "sm" | "md" | "lg" | "xl";
12
12
  progress: number | null;
13
13
  variant: "solid" | "outline" | "soft" | "subtle";
14
14
  icon: boolean;
@@ -8,7 +8,7 @@ type __VLS_Props = {
8
8
  variant?: BadgeProps['variant'];
9
9
  };
10
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
11
- size: "md" | "xs" | "sm" | "lg" | "xl";
11
+ size: "xs" | "sm" | "md" | "lg" | "xl";
12
12
  progress: number | null;
13
13
  variant: "solid" | "outline" | "soft" | "subtle";
14
14
  icon: boolean;
@@ -16,10 +16,18 @@ export type { ServiceUploadOptions, ServiceUploadResult } from '../utils/uploade
16
16
  * await $fetch(`/api/videos/lessons/${lessonId}/video`, { method: 'PUT', body: { sessionId, assetId } })
17
17
  * ```
18
18
  *
19
+ * Parts go to, in order: `upload(file, { baseURL })`, `useMLServiceUpload({
20
+ * baseURL })`, the app's `mediaLibrary.serviceUploads` (`baseURL`, or the
21
+ * pass-through route with `proxy: true`), then the URL the service returned.
22
+ * Direct calls need the media library to allow the site's origin (CORS);
23
+ * the pass-through route does not.
24
+ *
19
25
  * One upload at a time per call of the composable; call it once per
20
- * upload field. The media library must allow the site's origin (CORS).
26
+ * upload field.
21
27
  */
22
- export declare function useMLServiceUpload(): {
28
+ export declare function useMLServiceUpload(defaults?: {
29
+ baseURL?: string;
30
+ }): {
23
31
  upload: (file: File, options: ServiceUploadOptions) => Promise<ServiceUploadResult>;
24
32
  abort: () => void;
25
33
  uploading: import("vue").Ref<boolean, boolean>;
@@ -1,11 +1,14 @@
1
1
  import { ref } from "vue";
2
+ import { useRuntimeConfig } from "#imports";
2
3
  import { ChunkUploader } from "../utils/uploader/uploader.js";
3
4
  import { createServiceUpload } from "../utils/uploader/serviceUpload.js";
4
- export function useMLServiceUpload() {
5
+ export function useMLServiceUpload(defaults = {}) {
5
6
  const uploading = ref(false);
6
7
  const progress = ref(null);
7
8
  const error = ref(null);
9
+ const configured = useRuntimeConfig().public?.mediaLibrary?.serviceUploadBaseURL;
8
10
  const runner = createServiceUpload({
11
+ baseURL: defaults.baseURL || configured || void 0,
9
12
  fetch: (url, opts) => $fetch(url, opts),
10
13
  createUploader: (cfg) => new ChunkUploader(cfg)
11
14
  });
@@ -1,4 +1,6 @@
1
1
  export { createMLProxy, createMLCatchAllProxy } from './create-ml-proxy.js';
2
+ export { createMLServiceUploadsProxy } from './service-uploads-proxy.js';
3
+ export type { MLServiceUploadsProxyOptions } from './service-uploads-proxy.js';
2
4
  export type { MLProxyType, MLProxyOptions } from './create-ml-proxy.js';
3
5
  export { cachedIssuer, grantNeed, MLGrantIssueError, serviceTokenIssuer, specFromNeed, } from './grants.js';
4
6
  export type { MLAuthorize, MLGrantNeed, MLGrantProfile, MLGrantScope, MLGrantSpec, MLIssueGrant, MLIssuedGrant, } from './grants.js';
@@ -1,4 +1,5 @@
1
1
  export { createMLProxy, createMLCatchAllProxy } from "./create-ml-proxy.js";
2
+ export { createMLServiceUploadsProxy } from "./service-uploads-proxy.js";
2
3
  export {
3
4
  cachedIssuer,
4
5
  grantNeed,
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Same-origin pass-through for uploads started by the app's backend services
3
+ * (py_client `create_upload_router`, `useMLServiceUpload`).
4
+ *
5
+ * The service created the session and gave the browser a media grant for it;
6
+ * this route only carries the browser's own requests to the media library:
7
+ *
8
+ * - only `GET …/sessions/{id}`, `PUT …/sessions/{id}/parts/{n}`,
9
+ * `POST …/sessions/{id}/finalize` and `POST …/sessions/{id}/abort`
10
+ * (never session creation: the service does that);
11
+ * - the browser's `Authorization: Bearer mlg_…` is required and forwarded
12
+ * untouched, with its `X-Tenant`; no service token, static auth header or
13
+ * Nuxt-issued grant is ever added, and cookies are not forwarded;
14
+ * - request bodies are streamed (parts are megabytes, files gigabytes).
15
+ */
16
+ export interface MLServiceUploadsProxyOptions {
17
+ /** The media library API root with `/v1`. Default: private `mediaLibrary.baseURL`. */
18
+ baseURL?: string;
19
+ /** `X-Tenant` when the browser sends none. Default: the static `X-Tenant` header. */
20
+ tenant?: string;
21
+ }
22
+ /** The upstream path and method for a request, or null when it is not a session route. */
23
+ export declare function serviceUploadRoute(pathname: string, method: string): {
24
+ path: string;
25
+ method: string;
26
+ } | null;
27
+ /** The headers sent upstream: the grant, the tenant, and the upload headers. */
28
+ export declare function serviceUploadHeaders(incoming: Record<string, string | undefined>, tenant?: string): Record<string, string> | null;
29
+ export declare function createMLServiceUploadsProxy(options?: MLServiceUploadsProxyOptions): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
30
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
31
+ export default _default;
@@ -0,0 +1,61 @@
1
+ import { createError, defineEventHandler, getRequestHeaders, getRequestURL, getRequestWebStream, sendProxy } from "h3";
2
+ import { useRuntimeConfig } from "#imports";
3
+ const SESSION_PATH = /\/manage\/uploads\/sessions\/([^/]+)(\/parts\/\d+|\/finalize|\/abort)?\/?$/;
4
+ const GRANT = /^Bearer mlg_[\w.-]+$/;
5
+ const FORWARDED = ["content-type", "content-length", "content-range", "x-part-sha256", "accept"];
6
+ export function serviceUploadRoute(pathname, method) {
7
+ const match = pathname.match(SESSION_PATH);
8
+ if (!match) return null;
9
+ const [, sessionId, action = ""] = match;
10
+ const expected = action.startsWith("/parts/") ? "PUT" : action ? "POST" : "GET";
11
+ if (method !== expected) return null;
12
+ return { path: `/manage/uploads/sessions/${sessionId}${action}`, method };
13
+ }
14
+ export function serviceUploadHeaders(incoming, tenant) {
15
+ const lower = Object.fromEntries(Object.entries(incoming).map(([k, v]) => [k.toLowerCase(), v]));
16
+ const authorization = lower.authorization;
17
+ if (!authorization || !GRANT.test(authorization)) return null;
18
+ const headers = { authorization };
19
+ const xTenant = lower["x-tenant"] || tenant;
20
+ if (xTenant) headers["x-tenant"] = xTenant;
21
+ for (const name of FORWARDED) {
22
+ const value = lower[name];
23
+ if (value) headers[name] = value;
24
+ }
25
+ return headers;
26
+ }
27
+ export function createMLServiceUploadsProxy(options = {}) {
28
+ return defineEventHandler((event) => proxyServiceUpload(event, options));
29
+ }
30
+ async function proxyServiceUpload(event, options) {
31
+ const method = (event.node.req.method || "GET").toUpperCase();
32
+ const reqUrl = getRequestURL(event);
33
+ const route = serviceUploadRoute(reqUrl.pathname, method);
34
+ if (!route) throw createError({ statusCode: 404, statusMessage: "Not found" });
35
+ const config = useRuntimeConfig().mediaLibrary || {};
36
+ const baseURL = options.baseURL ?? config.baseURL;
37
+ if (!baseURL || !/^https?:\/\//.test(baseURL)) {
38
+ throw createError({ statusCode: 500, statusMessage: "mediaLibrary.baseURL must be an absolute http(s) URL." });
39
+ }
40
+ const headers = serviceUploadHeaders(getRequestHeaders(event), options.tenant ?? staticTenant(config.headers));
41
+ if (!headers) throw createError({ statusCode: 401, statusMessage: "A media grant (Bearer mlg_\u2026) is required" });
42
+ const upstream = new URL(`${baseURL.replace(/\/+$/, "")}${route.path}`);
43
+ upstream.search = reqUrl.search;
44
+ const fetchOptions = { method, headers };
45
+ if (method !== "GET") {
46
+ fetchOptions.body = getRequestWebStream(event);
47
+ fetchOptions.duplex = "half";
48
+ }
49
+ const result = await sendProxy(event, upstream.toString(), { fetchOptions });
50
+ if (event.node.res.statusCode >= 400) {
51
+ console.error(`[media-library] service-uploads-proxy upstream responded ${event.node.res.statusCode} for ${method} ${upstream}`);
52
+ }
53
+ return result;
54
+ }
55
+ function staticTenant(headers) {
56
+ const resolved = typeof headers === "function" ? headers() : headers;
57
+ if (!resolved || typeof resolved !== "object") return void 0;
58
+ const entry = Object.entries(resolved).find(([k]) => k.toLowerCase() === "x-tenant");
59
+ return entry?.[1] || void 0;
60
+ }
61
+ export default createMLServiceUploadsProxy();
@@ -11,7 +11,12 @@ export interface ServiceUploadStart {
11
11
  sessionId: string;
12
12
  grant: string;
13
13
  expiresAt: string;
14
- uploadBaseUrl: string;
14
+ /**
15
+ * Where the service says to upload. Used only when the app configures no
16
+ * upload base (`baseURL` option, or `mediaLibrary.serviceUploads`); newer
17
+ * services leave it out.
18
+ */
19
+ uploadBaseUrl?: string;
15
20
  tenant: string;
16
21
  partSizeBytes?: number;
17
22
  }
@@ -24,6 +29,13 @@ export interface ServiceUploadGrant {
24
29
  export interface ServiceUploadOptions {
25
30
  /** The service's start endpoint, e.g. `/api/videos/uploads`. */
26
31
  start: string;
32
+ /**
33
+ * Where to send the parts: the media library's public API root with
34
+ * `/v1`, or a same-origin pass-through route such as
35
+ * `/api/media-library/service-uploads`. Default: the app's configured
36
+ * upload base, then the service's `uploadBaseUrl`.
37
+ */
38
+ baseURL?: string;
27
39
  /**
28
40
  * Extra fields for the start request, e.g. `{ profile, context }`.
29
41
  * `mimeType`, `sizeBytes` and `filename` are taken from the file
@@ -59,12 +71,14 @@ interface UploaderLike {
59
71
  }
60
72
  export interface ServiceUploadDeps {
61
73
  fetch: FetchLike;
74
+ /** The app's configured upload base (used when an upload passes no `baseURL`). */
75
+ baseURL?: string;
62
76
  createUploader: (cfg: ChunkUploaderConfig) => UploaderLike;
63
77
  now?: () => number;
64
78
  setTimer?: (fn: () => void, ms: number) => unknown;
65
79
  clearTimer?: (handle: unknown) => void;
66
80
  }
67
- /** The media library routes a session grant allows (relative to `uploadBaseUrl`). */
81
+ /** The media library routes a session grant allows (relative to the upload base). */
68
82
  export declare const serviceUploadRoutes: {
69
83
  createSession: string;
70
84
  getSession: (id: string) => string;
@@ -41,6 +41,10 @@ export function createServiceUpload(deps) {
41
41
  }
42
42
  });
43
43
  if (run.aborted) throw abortError();
44
+ const baseURL = (options.baseURL || deps.baseURL || started.uploadBaseUrl || "").replace(/\/+$/, "");
45
+ if (!baseURL) {
46
+ throw new Error("No upload base URL: pass baseURL, or set mediaLibrary.serviceUploads (baseURL or proxy) in nuxt.config.");
47
+ }
44
48
  const sessionId = started.sessionId;
45
49
  const refreshUrl = typeof options.refresh === "function" ? options.refresh(sessionId) : options.refresh ?? `${start}/${encodeURIComponent(sessionId)}/grant`;
46
50
  let grant = started.grant;
@@ -73,7 +77,7 @@ export function createServiceUpload(deps) {
73
77
  timer = null;
74
78
  };
75
79
  const uploader = deps.createUploader({
76
- baseURL: started.uploadBaseUrl.replace(/\/+$/, ""),
80
+ baseURL,
77
81
  routes: serviceUploadRoutes,
78
82
  partSize: started.partSizeBytes ?? DEFAULT_PART_SIZE,
79
83
  concurrency: options.concurrency ?? 3,
package/dist/types.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { default } from './module.mjs'
1
+ export { type SERVICE_UPLOADS_ROUTE, default } from './module.mjs'
2
2
 
3
3
  export { type ChunkUploaderRoutesConfig, type ModuleOptions } from './module.mjs'
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-media-library",
3
- "version": "0.0.49",
3
+ "version": "0.0.51",
4
4
  "description": "Nuxt module for media library management — chunked uploads, asset previews, and resource policies",
5
5
  "repository": {
6
6
  "type": "git",