nuxt-media-library 0.0.49 → 0.0.50

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 +45 -0
  2. package/dist/module.d.mts +14 -1
  3. package/dist/module.d.ts +14 -1
  4. package/dist/module.json +1 -1
  5. package/dist/module.mjs +11 -2
  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
@@ -153,6 +153,51 @@ addMediaGrants(lesson.mediaGrant, profile.mediaGrant) // "mlg_…" or { grant, e
153
153
  grants are added.
154
154
  - Grants live in the browser only, never in server-side state.
155
155
 
156
+ ### Uploads started by your backend services
157
+
158
+ When a backend service starts the upload (the Python client's
159
+ `create_upload_router`), the service creates the session and returns a grant
160
+ for it, and `useMLServiceUpload` sends the parts. Where the parts go is the
161
+ app's decision, not the service's:
162
+
163
+ ```ts
164
+ // nuxt.config.ts
165
+ export default defineNuxtConfig({
166
+ mediaLibrary: {
167
+ baseURL: process.env.MEDIA_LIBRARY_URL, // server side, e.g. http://media-library.internal/v1
168
+ serviceUploads: { proxy: true }, // parts go through /api/media-library/service-uploads
169
+ // or, direct to the media library (needs CORS for the site):
170
+ // serviceUploads: { baseURL: 'https://media.example.com/v1' },
171
+ },
172
+ })
173
+ ```
174
+
175
+ ```ts
176
+ const { upload, progress } = useMLServiceUpload()
177
+ const { sessionId, assetId } = await upload(file, {
178
+ start: '/api/videos/uploads',
179
+ body: { profile: 'lesson-video', context: { lessonId } },
180
+ })
181
+ await $fetch(`/api/videos/lessons/${lessonId}/video`, { method: 'PUT', body: { sessionId, assetId } })
182
+ ```
183
+
184
+ - The upload base is, in order: `upload(file, { baseURL })`,
185
+ `useMLServiceUpload({ baseURL })`, `serviceUploads.baseURL`
186
+ (`NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BASE_URL`), the pass-through
187
+ route when `serviceUploads.proxy` is on, and last the `uploadBaseUrl` an
188
+ older service returns. With none, `upload` fails before any part is sent.
189
+ - The pass-through route (`serviceUploads.proxy: true`) forwards only a
190
+ session's `GET`, parts `PUT`, `finalize` and `abort` to the private
191
+ `mediaLibrary.baseURL`. It requires the browser's `Bearer mlg_…` grant and
192
+ forwards it and `X-Tenant` untouched (the static `X-Tenant` when the
193
+ browser sends none). It never adds the service token, static auth headers
194
+ or a Nuxt-issued grant, drops cookies, and streams request bodies. Mount it
195
+ yourself with `createMLServiceUploadsProxy()` from
196
+ `nuxt-media-library/server` for another path.
197
+ - Don't send these uploads through `createMLCatchAllProxy` with `authorize`:
198
+ grant mode replaces the browser's grant with its own, and the service's
199
+ `claim_upload` rejects the session.
200
+
156
201
  ### Vue installation (non-Nuxt)
157
202
 
158
203
  ```bash
package/dist/module.d.mts CHANGED
@@ -24,8 +24,21 @@ interface ModuleOptions {
24
24
  final?: boolean;
25
25
  };
26
26
  headers?: Record<string, string> | (() => Record<string, string>);
27
+ /**
28
+ * Uploads started by the app's backend services (`useMLServiceUpload`).
29
+ * `proxy: true` adds `/api/media-library/service-uploads/**`, a same-origin
30
+ * pass-through that forwards the browser's media grant and nothing else.
31
+ * `baseURL` is where the browser sends those uploads (the media library's
32
+ * public API root with `/v1`, or a proxy route); with `proxy` it defaults
33
+ * to the pass-through route.
34
+ */
35
+ serviceUploads?: {
36
+ proxy?: boolean;
37
+ baseURL?: string;
38
+ };
27
39
  }
40
+ declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
28
41
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
29
42
 
30
- export { _default as default };
43
+ export { SERVICE_UPLOADS_ROUTE, _default as default };
31
44
  export type { ChunkUploaderRoutesConfig, ModuleOptions };
package/dist/module.d.ts CHANGED
@@ -24,8 +24,21 @@ interface ModuleOptions {
24
24
  final?: boolean;
25
25
  };
26
26
  headers?: Record<string, string> | (() => Record<string, string>);
27
+ /**
28
+ * Uploads started by the app's backend services (`useMLServiceUpload`).
29
+ * `proxy: true` adds `/api/media-library/service-uploads/**`, a same-origin
30
+ * pass-through that forwards the browser's media grant and nothing else.
31
+ * `baseURL` is where the browser sends those uploads (the media library's
32
+ * public API root with `/v1`, or a proxy route); with `proxy` it defaults
33
+ * to the pass-through route.
34
+ */
35
+ serviceUploads?: {
36
+ proxy?: boolean;
37
+ baseURL?: string;
38
+ };
27
39
  }
40
+ declare const SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
28
41
  declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
29
42
 
30
- export { _default as default };
43
+ export { SERVICE_UPLOADS_ROUTE, _default as default };
31
44
  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.50",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -4,6 +4,7 @@ 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 SERVICE_UPLOADS_ROUTE = "/api/media-library/service-uploads";
7
8
  const module$1 = defineNuxtModule({
8
9
  meta: {
9
10
  name: "media-library",
@@ -89,6 +90,12 @@ const module$1 = defineNuxtModule({
89
90
  route: "/api/media-library/manage/processing/**",
90
91
  handler: resolver.resolve("./runtime/server/processing-proxy")
91
92
  });
93
+ if (options.serviceUploads?.proxy) {
94
+ addServerHandler({
95
+ route: `${SERVICE_UPLOADS_ROUTE}/**`,
96
+ handler: resolver.resolve("./runtime/server/service-uploads-proxy")
97
+ });
98
+ }
92
99
  nuxt.options.runtimeConfig.mediaLibrary = {
93
100
  ...existingPrivate,
94
101
  baseURL: existingPrivate.baseURL ?? envBaseURL ?? options.baseURL ?? "",
@@ -110,7 +117,9 @@ const module$1 = defineNuxtModule({
110
117
  perPart: existingPublic.checksum?.perPart ?? options.checksum?.perPart ?? true,
111
118
  final: existingPublic.checksum?.final ?? options.checksum?.final ?? true
112
119
  },
113
- headers: existingPublic.headers ?? options.headers ?? {}
120
+ headers: existingPublic.headers ?? options.headers ?? {},
121
+ // Where useMLServiceUpload sends parts (NUXT_PUBLIC_MEDIA_LIBRARY_SERVICE_UPLOAD_BASE_URL).
122
+ serviceUploadBaseURL: existingPublic.serviceUploadBaseURL || options.serviceUploads?.baseURL || (options.serviceUploads?.proxy ? SERVICE_UPLOADS_ROUTE : "")
114
123
  };
115
124
  addPlugin({
116
125
  src: resolver.resolve("./runtime/plugins/config")
@@ -118,4 +127,4 @@ const module$1 = defineNuxtModule({
118
127
  }
119
128
  });
120
129
 
121
- export { module$1 as default };
130
+ 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.50",
4
4
  "description": "Nuxt module for media library management — chunked uploads, asset previews, and resource policies",
5
5
  "repository": {
6
6
  "type": "git",