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.
- package/README.md +58 -3
- package/dist/module.d.mts +24 -1
- package/dist/module.d.ts +24 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +40 -5
- package/dist/runtime/components/MLMedia/Dropzone.d.vue.ts +3 -3
- package/dist/runtime/components/MLMedia/Dropzone.vue.d.ts +3 -3
- package/dist/runtime/components/MLMedia/Field.d.vue.ts +4 -4
- package/dist/runtime/components/MLMedia/Field.vue.d.ts +4 -4
- package/dist/runtime/components/MLMedia/FilePicker.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/FilePicker.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/FilePickerPanel.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/FilePickerPanel.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/Gallery.d.vue.ts +3 -3
- package/dist/runtime/components/MLMedia/Gallery.vue.d.ts +3 -3
- package/dist/runtime/components/MLMedia/GallerySection.d.vue.ts +5 -5
- package/dist/runtime/components/MLMedia/GallerySection.vue.d.ts +5 -5
- package/dist/runtime/components/MLMedia/KindBadge.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/KindBadge.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/MediaTile.d.vue.ts +2 -2
- package/dist/runtime/components/MLMedia/MediaTile.vue.d.ts +2 -2
- package/dist/runtime/components/MLMedia/PickerCard.d.vue.ts +3 -3
- package/dist/runtime/components/MLMedia/PickerCard.vue.d.ts +3 -3
- package/dist/runtime/components/MLMedia/PickerRow.d.vue.ts +2 -2
- package/dist/runtime/components/MLMedia/PickerRow.vue.d.ts +2 -2
- package/dist/runtime/components/MLMedia/ProgressVeil.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/ProgressVeil.vue.d.ts +1 -1
- package/dist/runtime/components/MLMedia/StatusBadge.d.vue.ts +1 -1
- package/dist/runtime/components/MLMedia/StatusBadge.vue.d.ts +1 -1
- package/dist/runtime/composables/useMLServiceUpload.d.ts +10 -2
- package/dist/runtime/composables/useMLServiceUpload.js +4 -1
- package/dist/runtime/server/index.d.ts +2 -0
- package/dist/runtime/server/index.js +1 -0
- package/dist/runtime/server/service-uploads-proxy.d.ts +31 -0
- package/dist/runtime/server/service-uploads-proxy.js +61 -0
- package/dist/runtime/utils/uploader/serviceUpload.d.ts +16 -2
- package/dist/runtime/utils/uploader/serviceUpload.js +5 -1
- package/dist/types.d.mts +1 -1
- 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
|
-
//
|
|
51
|
-
|
|
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
|
-
|
|
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
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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.
|
|
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';
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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