@dudousxd/nestjs-media-dashboard 0.16.2 → 0.18.0

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 (50) hide show
  1. package/dist/client/index.d.ts +1 -2
  2. package/dist/client/index.d.ts.map +1 -1
  3. package/dist/client/index.js +1 -4
  4. package/dist/client/index.js.map +1 -1
  5. package/dist/client/media-console-client.d.ts +14 -23
  6. package/dist/client/media-console-client.d.ts.map +1 -1
  7. package/dist/client/media-console-client.js +53 -46
  8. package/dist/client/media-console-client.js.map +1 -1
  9. package/dist/client/types.d.ts +10 -6
  10. package/dist/client/types.d.ts.map +1 -1
  11. package/dist/server/index.cjs +805 -1235
  12. package/dist/server/index.cjs.map +1 -1
  13. package/dist/server/index.d.cts +362 -239
  14. package/dist/server/index.d.ts +362 -239
  15. package/dist/server/index.js +790 -1235
  16. package/dist/server/index.js.map +1 -1
  17. package/dist/spa/assets/{ArchivePreview-h5wYCDJT.js → ArchivePreview-DacpA4QQ.js} +1 -1
  18. package/dist/spa/assets/CertificatePreview-CEfJ-ORW.js +127 -0
  19. package/dist/spa/assets/MarkdownPreview-47FMx48l.js +74 -0
  20. package/dist/spa/assets/ParquetPreview-DHvs5kV-.js +973 -0
  21. package/dist/spa/assets/{SheetPreview-Bfq7dI0a.js → SheetPreview-AT23RGVb.js} +1 -1
  22. package/dist/spa/assets/{SqlitePreview-JTQDjJJ3.js → SqlitePreview-D7KZuyZD.js} +1 -1
  23. package/dist/spa/assets/{index-Cjax4szq.js → index-BmejPQYi.js} +1 -1
  24. package/dist/spa/assets/{preview-BjGG6sSu.js → preview-DVKz8AwF.js} +2 -2
  25. package/dist/spa/assets/styles-CG_mruF7.css +1 -0
  26. package/dist/spa/assets/styles-CTvG1yXb.js +61 -0
  27. package/dist/spa/index.html +3 -3
  28. package/dist/spa/preview.html +3 -3
  29. package/package.json +25 -30
  30. package/dist/client/console-session.d.ts +0 -69
  31. package/dist/client/console-session.d.ts.map +0 -1
  32. package/dist/client/console-session.js +0 -120
  33. package/dist/client/console-session.js.map +0 -1
  34. package/dist/react/index.d.ts +0 -4
  35. package/dist/react/index.d.ts.map +0 -1
  36. package/dist/react/index.js +0 -15
  37. package/dist/react/index.js.map +0 -1
  38. package/dist/react/open-console-button.d.ts +0 -26
  39. package/dist/react/open-console-button.d.ts.map +0 -1
  40. package/dist/react/open-console-button.js +0 -29
  41. package/dist/react/open-console-button.js.map +0 -1
  42. package/dist/react/use-open-console.d.ts +0 -45
  43. package/dist/react/use-open-console.d.ts.map +0 -1
  44. package/dist/react/use-open-console.js +0 -75
  45. package/dist/react/use-open-console.js.map +0 -1
  46. package/dist/spa/assets/CertificatePreview-Bm4NkGaO.js +0 -127
  47. package/dist/spa/assets/MarkdownPreview-Cai2BrTH.js +0 -70
  48. package/dist/spa/assets/ParquetPreview-DfDTL_6f.js +0 -973
  49. package/dist/spa/assets/styles-Cui6Yy9Y.js +0 -61
  50. package/dist/spa/assets/styles-FRkIQ83o.css +0 -1
@@ -21,338 +21,46 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/server/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
- MEDIA_STORAGE_SHARED: () => MEDIA_STORAGE_SHARED,
24
+ CONSOLE_AUTHORIZE_DECIDE: () => CONSOLE_AUTHORIZE_DECIDE,
25
+ CONSOLE_AUTHORIZE_MISSING_WARNING: () => CONSOLE_AUTHORIZE_MISSING_WARNING,
26
+ CONSOLE_GATE: () => CONSOLE_GATE,
27
+ CONSOLE_PAGE_METADATA: () => CONSOLE_PAGE_METADATA,
28
+ ConsoleAccessDenied: () => ConsoleAccessDenied,
29
+ ConsoleAuthorizeFilter: () => ConsoleAuthorizeFilter,
30
+ ConsoleAuthorizeGuard: () => ConsoleAuthorizeGuard,
31
+ ConsolePage: () => ConsolePage,
32
+ MEDIA_MULTIPART: () => MEDIA_MULTIPART,
25
33
  MEDIA_STORE: () => MEDIA_STORE,
26
34
  MEDIA_UPLOAD_SESSIONS: () => MEDIA_UPLOAD_SESSIONS,
35
+ MediaConsoleActionsController: () => MediaConsoleActionsController,
27
36
  MediaConsoleApiModule: () => MediaConsoleApiModule,
37
+ MediaConsoleReadController: () => MediaConsoleReadController,
28
38
  MediaConsoleService: () => MediaConsoleService,
29
- MediaDashboardModule: () => MediaDashboardModule
39
+ MediaDashboardModule: () => MediaDashboardModule,
40
+ MediaDashboardUiController: () => MediaDashboardUiController,
41
+ UseConsoleAuthorize: () => UseConsoleAuthorize,
42
+ authorizeByToken: () => authorizeByToken,
43
+ normalizeConsoleResult: () => normalizeConsoleResult,
44
+ resolveConsoleAuthorize: () => resolveConsoleAuthorize
30
45
  });
31
46
  module.exports = __toCommonJS(index_exports);
32
47
 
33
- // ../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.16.20_jiti@1.21.7_postcss@8.5.31_typescript@5.9.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
48
+ // ../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.16.21_jiti@1.21.7_postcss@8.5.32_typescript@5.9.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js
34
49
  var getImportMetaUrl = /* @__PURE__ */ __name(() => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href, "getImportMetaUrl");
35
50
  var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
36
51
 
37
52
  // src/server/media-dashboard.module.ts
38
- var import_common8 = require("@nestjs/common");
39
- var import_core = require("@nestjs/core");
53
+ var import_common7 = require("@nestjs/common");
54
+ var import_core2 = require("@nestjs/core");
40
55
 
41
- // src/server/auth/config.ts
42
- var DEFAULT_TTL_MS = 8 * 60 * 60 * 1e3;
43
- var DURATION_UNITS = {
44
- s: 1e3,
45
- m: 60 * 1e3,
46
- h: 60 * 60 * 1e3,
47
- d: 24 * 60 * 60 * 1e3
48
- };
49
- function durationToMs(ttl) {
50
- if (ttl === void 0) return DEFAULT_TTL_MS;
51
- const match = /^(\d+)([smhd])$/.exec(ttl.trim());
52
- if (!match) return DEFAULT_TTL_MS;
53
- const unit = DURATION_UNITS[match[2] ?? ""];
54
- if (unit === void 0) return DEFAULT_TTL_MS;
55
- return Number(match[1]) * unit;
56
- }
57
- __name(durationToMs, "durationToMs");
58
- function resolveConsoleAuth(options) {
59
- if (!options) return null;
60
- if (!options.secret) {
61
- throw new Error("MediaDashboardModule: auth.secret is required when `auth` is configured.");
62
- }
63
- const modes = [];
64
- if (options.session) modes.push("session");
65
- if (options.login) modes.push("login");
66
- if (modes.length === 0) {
67
- throw new Error("MediaDashboardModule: auth needs a `login` and/or `session` hook.");
68
- }
69
- if (options.unauthenticatedPage !== void 0 && typeof options.unauthenticatedPage !== "function") {
70
- throw new Error("MediaDashboardModule: auth.unauthenticatedPage must be a function.");
71
- }
72
- return {
73
- secret: options.secret,
74
- ttlMs: durationToMs(options.ttl),
75
- modes,
76
- ...options.session ? {
77
- session: options.session
78
- } : {},
79
- ...options.login ? {
80
- login: options.login
81
- } : {},
82
- ...options.revalidate ? {
83
- revalidate: options.revalidate
84
- } : {},
85
- ...options.unauthenticatedPage ? {
86
- unauthenticatedPage: options.unauthenticatedPage
87
- } : {}
88
- };
89
- }
90
- __name(resolveConsoleAuth, "resolveConsoleAuth");
56
+ // src/server/controller-decorators.ts
57
+ var import_reflect_metadata2 = require("reflect-metadata");
91
58
 
92
- // src/server/guards.ts
59
+ // src/server/console-authorize.ts
93
60
  var import_reflect_metadata = require("reflect-metadata");
94
- var GUARDS_METADATA = "__guards__";
95
- function isGuardClass(guard) {
96
- return typeof guard === "function";
97
- }
98
- __name(isGuardClass, "isGuardClass");
99
- function baseGuards(controller) {
100
- return Reflect.getOwnMetadata(GUARDS_METADATA, controller) ?? [];
101
- }
102
- __name(baseGuards, "baseGuards");
103
- function stampGuards(guards, entries) {
104
- if (guards === void 0 || guards.length === 0) return;
105
- for (const [controller, base] of entries) {
106
- Reflect.defineMetadata(GUARDS_METADATA, [
107
- ...base,
108
- ...guards
109
- ], controller);
110
- }
111
- }
112
- __name(stampGuards, "stampGuards");
113
-
114
- // src/server/media-console-api.module.ts
115
- var import_common6 = require("@nestjs/common");
116
-
117
- // src/server/media-console-actions.controller.ts
118
- var import_common3 = require("@nestjs/common");
119
-
120
- // src/server/media-console.guard.ts
121
- var import_common = require("@nestjs/common");
122
-
123
- // src/server/auth/cookie-header.ts
124
- function parseCookieHeader(header) {
125
- const cookies = {};
126
- if (typeof header !== "string" || header === "") return cookies;
127
- for (const segment of header.split(";")) {
128
- const eq = segment.indexOf("=");
129
- if (eq <= 0) continue;
130
- const name = segment.slice(0, eq).trim();
131
- if (name === "") continue;
132
- let rawValue = segment.slice(eq + 1).trim();
133
- if (rawValue.length >= 2 && rawValue.startsWith('"') && rawValue.endsWith('"')) {
134
- rawValue = rawValue.slice(1, -1);
135
- }
136
- if (name in cookies) continue;
137
- try {
138
- cookies[name] = decodeURIComponent(rawValue);
139
- } catch {
140
- cookies[name] = rawValue;
141
- }
142
- }
143
- return cookies;
144
- }
145
- __name(parseCookieHeader, "parseCookieHeader");
146
- function serializeSetCookie(name, value, options) {
147
- const parts = [
148
- `${name}=${options.clear ? "" : encodeURIComponent(value)}`
149
- ];
150
- parts.push(`Path=${options.path}`);
151
- parts.push("HttpOnly");
152
- parts.push("SameSite=Lax");
153
- if (options.secure) parts.push("Secure");
154
- if (options.clear) {
155
- parts.push("Max-Age=0");
156
- parts.push("Expires=Thu, 01 Jan 1970 00:00:00 GMT");
157
- } else {
158
- parts.push(`Max-Age=${options.maxAgeSeconds}`);
159
- }
160
- return parts.join("; ");
161
- }
162
- __name(serializeSetCookie, "serializeSetCookie");
163
-
164
- // src/server/auth/request.ts
165
- function isRecord(value) {
166
- return typeof value === "object" && value !== null;
167
- }
168
- __name(isRecord, "isRecord");
169
- function readHeaders(request) {
170
- if (!isRecord(request)) return {};
171
- const headers = request.headers;
172
- return isRecord(headers) ? headers : {};
173
- }
174
- __name(readHeaders, "readHeaders");
175
- function headerToString(value) {
176
- if (typeof value === "string") return value;
177
- if (Array.isArray(value) && value.every((part) => typeof part === "string")) {
178
- return value.join(",");
179
- }
180
- return void 0;
181
- }
182
- __name(headerToString, "headerToString");
183
- function readCookieHeader(request) {
184
- return headerToString(readHeaders(request).cookie);
185
- }
186
- __name(readCookieHeader, "readCookieHeader");
187
- function isHttpsRequest(request) {
188
- if (isRecord(request) && request.protocol === "https") return true;
189
- const forwarded = headerToString(readHeaders(request)["x-forwarded-proto"]);
190
- if (forwarded === void 0) return false;
191
- return forwarded.split(",").map((proto) => proto.trim().toLowerCase()).includes("https");
192
- }
193
- __name(isHttpsRequest, "isHttpsRequest");
194
- function attachSession(request, session) {
195
- if (!isRecord(request)) return;
196
- request.consoleSession = session;
197
- }
198
- __name(attachSession, "attachSession");
199
-
200
- // src/server/auth/response.ts
201
- function isRecord2(value) {
202
- return typeof value === "object" && value !== null;
203
- }
204
- __name(isRecord2, "isRecord");
205
- function isRawNodeResponse(value) {
206
- return isRecord2(value) && typeof value.getHeader === "function" && typeof value.setHeader === "function";
207
- }
208
- __name(isRawNodeResponse, "isRawNodeResponse");
209
- function resolveRawResponse(response) {
210
- if (isRawNodeResponse(response)) return response;
211
- if (isRecord2(response) && isRawNodeResponse(response.raw)) return response.raw;
212
- return null;
213
- }
214
- __name(resolveRawResponse, "resolveRawResponse");
215
- function existingSetCookies(response) {
216
- const current = response.getHeader("set-cookie");
217
- if (Array.isArray(current)) return current;
218
- if (typeof current === "string") return [
219
- current
220
- ];
221
- return [];
222
- }
223
- __name(existingSetCookies, "existingSetCookies");
224
- function appendSetCookie(response, cookie) {
225
- const raw = resolveRawResponse(response);
226
- if (!raw) return;
227
- raw.setHeader("set-cookie", [
228
- ...existingSetCookies(raw),
229
- cookie
230
- ]);
231
- }
232
- __name(appendSetCookie, "appendSetCookie");
233
- function resolveEndableResponse(response) {
234
- const isEndable = /* @__PURE__ */ __name((value) => isRawNodeResponse(value) && typeof value.end === "function", "isEndable");
235
- if (isEndable(response)) return response;
236
- if (isRecord2(response) && isEndable(response.raw)) return response.raw;
237
- return null;
238
- }
239
- __name(resolveEndableResponse, "resolveEndableResponse");
240
- function responseAlreadyWritten(response) {
241
- return resolveEndableResponse(response)?.headersSent === true;
242
- }
243
- __name(responseAlreadyWritten, "responseAlreadyWritten");
244
- function sendHtml(response, status, html) {
245
- const raw = resolveEndableResponse(response);
246
- if (!raw) return;
247
- raw.statusCode = status;
248
- raw.setHeader("content-type", "text/html; charset=utf-8");
249
- raw.setHeader("cache-control", "no-store, must-revalidate");
250
- raw.end(html);
251
- }
252
- __name(sendHtml, "sendHtml");
253
-
254
- // src/server/auth/session-cookie.ts
255
61
  var import_node_crypto = require("crypto");
256
- var EXP_GRACE_MS = 3e4;
257
- function base64urlEncode(value) {
258
- return Buffer.from(value, "utf8").toString("base64url");
259
- }
260
- __name(base64urlEncode, "base64urlEncode");
261
- function base64urlDecode(value) {
262
- return Buffer.from(value, "base64url").toString("utf8");
263
- }
264
- __name(base64urlDecode, "base64urlDecode");
265
- function hmac(secret, payload) {
266
- return (0, import_node_crypto.createHmac)("sha256", secret).update(payload).digest("base64url");
267
- }
268
- __name(hmac, "hmac");
269
- function signSessionCookie(user, options) {
270
- const issuedAt = options.now ?? Date.now();
271
- const session = {
272
- sub: user.id,
273
- ...user.name !== void 0 ? {
274
- name: user.name
275
- } : {},
276
- roles: user.roles ?? [],
277
- iat: issuedAt,
278
- exp: issuedAt + options.ttlMs
279
- };
280
- const encodedPayload = base64urlEncode(JSON.stringify(session));
281
- return `${encodedPayload}.${hmac(options.secret, encodedPayload)}`;
282
- }
283
- __name(signSessionCookie, "signSessionCookie");
284
- function isSessionPayload(value) {
285
- if (typeof value !== "object" || value === null) return false;
286
- const record = value;
287
- if (typeof record.sub !== "string") return false;
288
- if (record.name !== void 0 && typeof record.name !== "string") return false;
289
- if (!Array.isArray(record.roles)) return false;
290
- if (!record.roles.every((role) => typeof role === "string")) return false;
291
- if (typeof record.iat !== "number" || !Number.isFinite(record.iat)) return false;
292
- if (typeof record.exp !== "number" || !Number.isFinite(record.exp)) return false;
293
- return true;
294
- }
295
- __name(isSessionPayload, "isSessionPayload");
296
- function verifySessionCookie(value, options) {
297
- try {
298
- const dot = value.indexOf(".");
299
- if (dot <= 0 || dot === value.length - 1) return null;
300
- const encodedPayload = value.slice(0, dot);
301
- const provided = Buffer.from(value.slice(dot + 1), "base64url");
302
- const expected = Buffer.from(hmac(options.secret, encodedPayload), "base64url");
303
- if (provided.length !== expected.length) return null;
304
- if (!(0, import_node_crypto.timingSafeEqual)(provided, expected)) return null;
305
- const decoded = JSON.parse(base64urlDecode(encodedPayload));
306
- if (!isSessionPayload(decoded)) return null;
307
- const now = options.now ?? Date.now();
308
- if (now > decoded.exp + EXP_GRACE_MS) return null;
309
- return decoded;
310
- } catch {
311
- return null;
312
- }
313
- }
314
- __name(verifySessionCookie, "verifySessionCookie");
315
-
316
- // src/server/auth/session-cookie-io.ts
317
- var SESSION_COOKIE_NAME = "media_console_session";
318
- function issueSessionCookie(user, context) {
319
- const value = signSessionCookie(user, {
320
- secret: context.auth.secret,
321
- ttlMs: context.auth.ttlMs,
322
- ...context.now !== void 0 ? {
323
- now: context.now
324
- } : {}
325
- });
326
- appendSetCookie(context.response, serializeSetCookie(SESSION_COOKIE_NAME, value, {
327
- path: context.cookiePath,
328
- maxAgeSeconds: Math.floor(context.auth.ttlMs / 1e3),
329
- secure: isHttpsRequest(context.request)
330
- }));
331
- }
332
- __name(issueSessionCookie, "issueSessionCookie");
333
- function clearSessionCookie(context) {
334
- appendSetCookie(context.response, serializeSetCookie(SESSION_COOKIE_NAME, "", {
335
- path: context.cookiePath,
336
- maxAgeSeconds: 0,
337
- secure: isHttpsRequest(context.request),
338
- clear: true
339
- }));
340
- }
341
- __name(clearSessionCookie, "clearSessionCookie");
342
-
343
- // src/server/tokens.ts
344
- var MEDIA_STORE = /* @__PURE__ */ Symbol.for("nestjs-media:store");
345
- var MEDIA_UPLOAD_SESSIONS = /* @__PURE__ */ Symbol.for("nestjs-media:upload-sessions");
346
- var MEDIA_STORAGE_SHARED = /* @__PURE__ */ Symbol.for("nestjs-media:storage");
347
- var MEDIA_DASHBOARD_BASE_PATH = /* @__PURE__ */ Symbol("MEDIA_DASHBOARD_BASE_PATH");
348
- var MEDIA_DASHBOARD_API_PATH = /* @__PURE__ */ Symbol("MEDIA_DASHBOARD_API_PATH");
349
- var MEDIA_DASHBOARD_ACTIONS = /* @__PURE__ */ Symbol("MEDIA_DASHBOARD_ACTIONS");
350
- var MEDIA_CONSOLE_AUTH = /* @__PURE__ */ Symbol("MEDIA_CONSOLE_AUTH");
351
- var MEDIA_CONSOLE_COOKIE_PATH = /* @__PURE__ */ Symbol("MEDIA_CONSOLE_COOKIE_PATH");
352
- var MEDIA_CONSOLE_OBJECT_INSIGHTS = /* @__PURE__ */ Symbol("MEDIA_CONSOLE_OBJECT_INSIGHTS");
353
- var MEDIA_CONSOLE_OBJECT_URLS = /* @__PURE__ */ Symbol("MEDIA_CONSOLE_OBJECT_URLS");
354
-
355
- // src/server/media-console.guard.ts
62
+ var import_common = require("@nestjs/common");
63
+ var import_core = require("@nestjs/core");
356
64
  function _ts_decorate(decorators, target, key, desc) {
357
65
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
358
66
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
@@ -379,94 +87,339 @@ function _ts_param(paramIndex, decorator) {
379
87
  };
380
88
  }
381
89
  __name(_ts_param, "_ts_param");
382
- var MediaConsoleGuard = class {
383
- static {
384
- __name(this, "MediaConsoleGuard");
90
+ var CONSOLE_GATE = /* @__PURE__ */ Symbol.for("@dudousxd/aviary:console-gate");
91
+ var PACKAGE = "@dudousxd/nestjs-media-dashboard";
92
+ var MODULE = "MediaDashboardModule";
93
+ var CONSOLE_AUTHORIZE_MISSING_WARNING = consoleAuthorizeMissingWarning(`${MODULE}.forRoot()`);
94
+ function consoleAuthorizeMissingWarning(registration) {
95
+ return `[${PACKAGE}] console has no \`authorize\` configured: it is open outside production and denies every request in production. Set \`authorize\` in ${registration}.`;
96
+ }
97
+ __name(consoleAuthorizeMissingWarning, "consoleAuthorizeMissingWarning");
98
+ var allow = /* @__PURE__ */ __name(() => ({
99
+ allowed: true
100
+ }), "allow");
101
+ var forbidden = /* @__PURE__ */ __name(() => ({
102
+ allowed: false,
103
+ reason: "forbidden"
104
+ }), "forbidden");
105
+ var unauthenticated = /* @__PURE__ */ __name(() => ({
106
+ allowed: false,
107
+ reason: "unauthenticated"
108
+ }), "unauthenticated");
109
+ function isProduction() {
110
+ return (process.env.NODE_ENV ?? "").trim().toLowerCase() === "production";
111
+ }
112
+ __name(isProduction, "isProduction");
113
+ function isAuthorizerClass(value) {
114
+ if (typeof value !== "function") return false;
115
+ if (typeof value.prototype?.authorize === "function") {
116
+ return true;
385
117
  }
386
- auth;
387
- cookiePath;
388
- constructor(auth, cookiePath) {
389
- this.auth = auth;
390
- this.cookiePath = cookiePath;
118
+ return /^class[\s{]/.test(Function.prototype.toString.call(value));
119
+ }
120
+ __name(isAuthorizerClass, "isAuthorizerClass");
121
+ function isPolicyCheck(value) {
122
+ return typeof value === "object" && value !== null && typeof value.policy === "function" && typeof value.ability === "string";
123
+ }
124
+ __name(isPolicyCheck, "isPolicyCheck");
125
+ function isLazyModule(value) {
126
+ return typeof value === "object" && value !== null && isAuthorizerClass(value.default);
127
+ }
128
+ __name(isLazyModule, "isLazyModule");
129
+ function normalizeConsoleResult(result) {
130
+ if (result === true) return allow();
131
+ if (typeof result !== "object" || result === null) return forbidden();
132
+ const shape = result;
133
+ if (typeof shape.redirectTo === "string" && shape.allowed !== true) {
134
+ return {
135
+ allowed: false,
136
+ reason: "unauthenticated",
137
+ redirectTo: shape.redirectTo
138
+ };
391
139
  }
392
- async canActivate(context) {
393
- if (!this.auth) return true;
394
- const http = context.switchToHttp();
395
- const request = http.getRequest();
396
- const session = this.verifyRequestSession(request);
397
- if (!session) throw new import_common.UnauthorizedException();
398
- attachSession(request, session);
399
- if (!await this.maybeRenew(http.getResponse(), request, session)) {
400
- throw new import_common.UnauthorizedException();
401
- }
402
- return true;
140
+ if (shape.allowed === true) return allow();
141
+ return shape.reason === "unauthenticated" ? unauthenticated() : forbidden();
142
+ }
143
+ __name(normalizeConsoleResult, "normalizeConsoleResult");
144
+ function hasUser(user) {
145
+ return user !== null && user !== void 0 && user !== false;
146
+ }
147
+ __name(hasUser, "hasUser");
148
+ var importOptional = /* @__PURE__ */ __name((specifier) => import(
149
+ /* @vite-ignore */
150
+ specifier
151
+ ), "importOptional");
152
+ async function policyDecider(check, moduleRef) {
153
+ let pkg;
154
+ try {
155
+ pkg = await importOptional("@nestjs/authorization");
156
+ } catch {
157
+ throw new Error("`authorize: { policy, ability }` needs @nestjs/authorization installed (npm i @nestjs/authorization); or pass a function, a class, or a gate name");
403
158
  }
404
- verifyRequestSession(request) {
405
- if (!this.auth) return null;
406
- const cookieValue = parseCookieHeader(readCookieHeader(request))[SESSION_COOKIE_NAME];
407
- if (cookieValue === void 0) return null;
408
- return verifySessionCookie(cookieValue, {
409
- secret: this.auth.secret
159
+ let service;
160
+ try {
161
+ service = moduleRef.get(pkg.AuthorizationService, {
162
+ strict: false
410
163
  });
164
+ } catch {
165
+ service = void 0;
411
166
  }
412
- /**
413
- * Sliding renewal + revalidation: when a valid cookie is past half its TTL, re-issue a fresh one
414
- * so active users never get logged out mid-session — but first let the host's `revalidate` hook
415
- * re-check the user, so a deactivated or demoted operator loses access instead of riding a
416
- * self-renewing cookie. Returns `false` when the session was revoked (cookie already cleared).
417
- */
418
- async maybeRenew(response, request, session) {
419
- if (!this.auth) return true;
420
- const now = Date.now();
421
- if (now - session.iat <= this.auth.ttlMs / 2) return true;
422
- const user = {
423
- id: session.sub,
424
- ...session.name !== void 0 ? {
425
- name: session.name
426
- } : {},
427
- roles: session.roles
428
- };
429
- if (this.auth.revalidate) {
430
- let allowed;
167
+ if (!service) {
168
+ throw new Error("`authorize: { policy, ability }` needs AuthorizationModule.forRoot() from @nestjs/authorization imported in the app");
169
+ }
170
+ const found = service;
171
+ const defaultGetUser = pkg.defaultGetUser;
172
+ let getUser = defaultGetUser;
173
+ try {
174
+ const options = moduleRef.get(pkg.AUTHORIZATION_MODULE_OPTIONS, {
175
+ strict: false
176
+ });
177
+ if (typeof options?.getUser === "function") getUser = options.getUser;
178
+ } catch {
179
+ }
180
+ const { policy, ability } = check;
181
+ return async (context) => {
182
+ const user = await getUser(context);
183
+ const signedIn = hasUser(user);
184
+ if (await found.can(policy, ability, signedIn ? user : null)) return allow();
185
+ return signedIn ? forbidden() : unauthenticated();
186
+ };
187
+ }
188
+ __name(policyDecider, "policyDecider");
189
+ function gateDecider(name, moduleRef) {
190
+ let gate;
191
+ try {
192
+ gate = moduleRef.get(CONSOLE_GATE, {
193
+ strict: false
194
+ });
195
+ } catch {
196
+ gate = void 0;
197
+ }
198
+ if (!gate) {
199
+ throw new Error(`\`authorize: '${name}'\` needs @dudousxd/nestjs-authz's AuthzModule (it resolves gate names); or pass a function, a class, or { policy, ability }`);
200
+ }
201
+ const found = gate;
202
+ return async (context) => normalizeConsoleResult(await found.allows(name, context));
203
+ }
204
+ __name(gateDecider, "gateDecider");
205
+ function classDecider(moduleRef) {
206
+ const instances = /* @__PURE__ */ new Map();
207
+ const instanceOf = /* @__PURE__ */ __name((Class) => {
208
+ const cached = instances.get(Class);
209
+ if (cached) return cached;
210
+ const pending = (async () => {
431
211
  try {
432
- allowed = await this.auth.revalidate(user);
433
- } catch {
434
- allowed = false;
435
- }
436
- if (!allowed) {
437
- clearSessionCookie({
438
- cookiePath: this.cookiePath ?? "/",
439
- request,
440
- response
212
+ const registered = moduleRef.get(Class, {
213
+ strict: false
441
214
  });
442
- return false;
215
+ if (registered) return registered;
216
+ } catch {
443
217
  }
218
+ return moduleRef.create(Class);
219
+ })();
220
+ pending.catch(() => instances.delete(Class));
221
+ instances.set(Class, pending);
222
+ return pending;
223
+ }, "instanceOf");
224
+ return (Class) => async (context) => normalizeConsoleResult(await (await instanceOf(Class)).authorize(context));
225
+ }
226
+ __name(classDecider, "classDecider");
227
+ async function resolveConsoleAuthorize(authorize, moduleRef, options = {}) {
228
+ const logger = options.logger ?? new import_common.Logger(MODULE);
229
+ if (authorize === void 0) {
230
+ if (options.warnIfMissing !== false) {
231
+ logger.warn(options.registration ? consoleAuthorizeMissingWarning(options.registration) : CONSOLE_AUTHORIZE_MISSING_WARNING);
444
232
  }
445
- issueSessionCookie(user, {
446
- auth: this.auth,
447
- cookiePath: this.cookiePath ?? "/",
448
- request,
449
- response,
450
- now
451
- });
452
- return true;
233
+ const production = isProduction();
234
+ return async () => production ? forbidden() : allow();
235
+ }
236
+ let decide;
237
+ if (typeof authorize === "string") {
238
+ decide = gateDecider(authorize, moduleRef);
239
+ } else if (isPolicyCheck(authorize)) {
240
+ decide = await policyDecider(authorize, moduleRef);
241
+ } else if (isAuthorizerClass(authorize)) {
242
+ decide = classDecider(moduleRef)(authorize);
243
+ } else if (typeof authorize === "function") {
244
+ const fromClass = classDecider(moduleRef);
245
+ let lazy;
246
+ const fn = authorize;
247
+ decide = /* @__PURE__ */ __name(async (context) => {
248
+ if (lazy) return lazy(context);
249
+ const result = await fn(context);
250
+ if (!isLazyModule(result)) return normalizeConsoleResult(result);
251
+ lazy = fromClass(result.default);
252
+ return lazy(context);
253
+ }, "decide");
254
+ } else {
255
+ throw new Error(`${MODULE}: \`authorize\` must be a function, a class with authorize(), a lazy import, { policy, ability } or a gate name.`);
256
+ }
257
+ return async (context) => {
258
+ try {
259
+ return await decide(context);
260
+ } catch (error) {
261
+ logger.warn(`console \`authorize\` threw, denying the request: ${error instanceof Error ? error.message : String(error)}`);
262
+ return forbidden();
263
+ }
264
+ };
265
+ }
266
+ __name(resolveConsoleAuthorize, "resolveConsoleAuthorize");
267
+ var sha256 = /* @__PURE__ */ __name((value) => (0, import_node_crypto.createHash)("sha256").update(value).digest(), "sha256");
268
+ function authorizeByToken(options) {
269
+ const token = options.token;
270
+ if (typeof token !== "string" || token.length === 0) {
271
+ throw new Error("authorizeByToken: `token` is empty; set it (e.g. from an env var).");
272
+ }
273
+ const expected = sha256(token);
274
+ return (context) => {
275
+ const header = context.switchToHttp().getRequest()?.headers?.authorization;
276
+ const match = typeof header === "string" ? /^Bearer\s+(.+)$/i.exec(header.trim()) : null;
277
+ if (match?.[1] && (0, import_node_crypto.timingSafeEqual)(sha256(match[1]), expected)) return true;
278
+ return {
279
+ allowed: false,
280
+ reason: "unauthenticated"
281
+ };
282
+ };
283
+ }
284
+ __name(authorizeByToken, "authorizeByToken");
285
+ var CONSOLE_AUTHORIZE_DECIDE = /* @__PURE__ */ Symbol(`${PACKAGE}:console-authorize`);
286
+ var CONSOLE_PAGE_METADATA = `${PACKAGE}:console-page`;
287
+ var ConsolePage = /* @__PURE__ */ __name(() => (target) => {
288
+ Reflect.defineMetadata(CONSOLE_PAGE_METADATA, true, target);
289
+ }, "ConsolePage");
290
+ var ConsoleAccessDenied = class extends import_common.HttpException {
291
+ static {
292
+ __name(this, "ConsoleAccessDenied");
293
+ }
294
+ decision;
295
+ page;
296
+ constructor(decision, page2 = false) {
297
+ const isUnauthenticated = decision.reason === "unauthenticated";
298
+ super(isUnauthenticated ? {
299
+ error: "unauthenticated",
300
+ ...decision.redirectTo ? {
301
+ redirectTo: decision.redirectTo
302
+ } : {}
303
+ } : {
304
+ error: "forbidden"
305
+ }, isUnauthenticated ? 401 : 403), this.decision = decision, this.page = page2;
306
+ }
307
+ };
308
+ var ConsoleAuthorizeGuard = class {
309
+ static {
310
+ __name(this, "ConsoleAuthorizeGuard");
311
+ }
312
+ decide;
313
+ constructor(decide) {
314
+ this.decide = decide;
315
+ }
316
+ async canActivate(context) {
317
+ const decision = await this.decide(context);
318
+ if (decision.allowed) return true;
319
+ throw new ConsoleAccessDenied(decision, isPageNavigation(context));
453
320
  }
454
321
  };
455
- MediaConsoleGuard = _ts_decorate([
322
+ ConsoleAuthorizeGuard = _ts_decorate([
456
323
  (0, import_common.Injectable)(),
457
- _ts_param(0, (0, import_common.Optional)()),
458
- _ts_param(0, (0, import_common.Inject)(MEDIA_CONSOLE_AUTH)),
459
- _ts_param(1, (0, import_common.Optional)()),
460
- _ts_param(1, (0, import_common.Inject)(MEDIA_CONSOLE_COOKIE_PATH)),
324
+ _ts_param(0, (0, import_common.Inject)(CONSOLE_AUTHORIZE_DECIDE)),
461
325
  _ts_metadata("design:type", Function),
462
326
  _ts_metadata("design:paramtypes", [
463
- Object,
464
- Object
327
+ typeof ConsoleDecide === "undefined" ? Object : ConsoleDecide
465
328
  ])
466
- ], MediaConsoleGuard);
329
+ ], ConsoleAuthorizeGuard);
330
+ function isPageNavigation(context) {
331
+ if (context.getType() !== "http") return false;
332
+ if (Reflect.getMetadata(CONSOLE_PAGE_METADATA, context.getClass()) !== true) return false;
333
+ const request = context.switchToHttp().getRequest();
334
+ const accept = request?.headers?.accept;
335
+ return request?.method === "GET" && typeof accept === "string" && accept.includes("text/html");
336
+ }
337
+ __name(isPageNavigation, "isPageNavigation");
338
+ var page = /* @__PURE__ */ __name((title, message) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${title}</title></head><body style="font-family:system-ui,sans-serif;display:grid;place-items:center;min-height:100vh;margin:0"><main style="text-align:center"><h1>${title}</h1><p>${message}</p></main></body></html>`, "page");
339
+ var DENIED_PAGE = page("Access denied", "You do not have access to this console.");
340
+ var SIGN_IN_PAGE = page("Sign in required", "Sign in to open this console.");
341
+ var ConsoleAuthorizeFilter = class {
342
+ static {
343
+ __name(this, "ConsoleAuthorizeFilter");
344
+ }
345
+ adapterHost;
346
+ constructor(adapterHost) {
347
+ this.adapterHost = adapterHost;
348
+ }
349
+ catch(exception, host) {
350
+ const adapter = this.adapterHost.httpAdapter;
351
+ const response = host.switchToHttp().getResponse();
352
+ const { decision, page: isPage } = exception;
353
+ if (isPage && decision.redirectTo) {
354
+ adapter.redirect(response, 302, decision.redirectTo);
355
+ return;
356
+ }
357
+ if (isPage) {
358
+ const unauthenticated2 = decision.reason === "unauthenticated";
359
+ adapter.setHeader(response, "Content-Type", "text/html; charset=utf-8");
360
+ adapter.reply(response, unauthenticated2 ? SIGN_IN_PAGE : DENIED_PAGE, unauthenticated2 ? 401 : 403);
361
+ return;
362
+ }
363
+ adapter.reply(response, exception.getResponse(), exception.getStatus());
364
+ }
365
+ };
366
+ ConsoleAuthorizeFilter = _ts_decorate([
367
+ (0, import_common.Catch)(ConsoleAccessDenied),
368
+ _ts_param(0, (0, import_common.Inject)(import_core.HttpAdapterHost)),
369
+ _ts_metadata("design:type", Function),
370
+ _ts_metadata("design:paramtypes", [
371
+ typeof import_core.HttpAdapterHost === "undefined" ? Object : import_core.HttpAdapterHost
372
+ ])
373
+ ], ConsoleAuthorizeFilter);
374
+ var UseConsoleAuthorize = /* @__PURE__ */ __name(() => (0, import_common.applyDecorators)((0, import_common.UseGuards)(ConsoleAuthorizeGuard), (0, import_common.UseFilters)(ConsoleAuthorizeFilter)), "UseConsoleAuthorize");
375
+ function consoleAuthorizeProvider(authorize, registration) {
376
+ return {
377
+ provide: CONSOLE_AUTHORIZE_DECIDE,
378
+ inject: [
379
+ import_core.ModuleRef
380
+ ],
381
+ useFactory: /* @__PURE__ */ __name((moduleRef) => resolveConsoleAuthorize(authorize, moduleRef, {
382
+ registration
383
+ }), "useFactory")
384
+ };
385
+ }
386
+ __name(consoleAuthorizeProvider, "consoleAuthorizeProvider");
387
+
388
+ // src/server/controller-decorators.ts
389
+ var CAN_ANYONE_METADATA = "authorization:can-anyone";
390
+ function OpenToAuthorization() {
391
+ return (target) => {
392
+ Reflect.defineMetadata(CAN_ANYONE_METADATA, true, target);
393
+ };
394
+ }
395
+ __name(OpenToAuthorization, "OpenToAuthorization");
396
+ function consoleController(controller, decorators) {
397
+ const Decorated = class extends controller {
398
+ static {
399
+ __name(this, "Decorated");
400
+ }
401
+ };
402
+ Object.defineProperty(Decorated, "name", {
403
+ value: controller.name
404
+ });
405
+ if (decorators && decorators.length > 0) Reflect.decorate(decorators, Decorated);
406
+ Reflect.decorate([
407
+ UseConsoleAuthorize()
408
+ ], Decorated);
409
+ return Decorated;
410
+ }
411
+ __name(consoleController, "consoleController");
412
+
413
+ // src/server/media-console-api.module.ts
414
+ var import_common5 = require("@nestjs/common");
415
+
416
+ // src/server/media-console-actions.controller.ts
417
+ var import_common3 = require("@nestjs/common");
467
418
 
468
419
  // src/server/media-console.service.ts
420
+ var import_nestjs_media_core = require("@dudousxd/nestjs-media-core");
469
421
  var import_common2 = require("@nestjs/common");
422
+ var import_storage = require("@nestjs/storage");
470
423
 
471
424
  // src/server/object-insights.ts
472
425
  var SAFE_LINK_SCHEMES = /* @__PURE__ */ new Set([
@@ -501,6 +454,16 @@ function objectProxyUrl(apiBasePath, route, disk, key) {
501
454
  }
502
455
  __name(objectProxyUrl, "objectProxyUrl");
503
456
 
457
+ // src/server/tokens.ts
458
+ var MEDIA_STORE = /* @__PURE__ */ Symbol.for("nestjs-media:store");
459
+ var MEDIA_UPLOAD_SESSIONS = /* @__PURE__ */ Symbol.for("nestjs-media:upload-sessions");
460
+ var MEDIA_MULTIPART = /* @__PURE__ */ Symbol.for("nestjs-media:multipart");
461
+ var MEDIA_DASHBOARD_BASE_PATH = /* @__PURE__ */ Symbol("MEDIA_DASHBOARD_BASE_PATH");
462
+ var MEDIA_DASHBOARD_API_PATH = /* @__PURE__ */ Symbol("MEDIA_DASHBOARD_API_PATH");
463
+ var MEDIA_DASHBOARD_ACTIONS = /* @__PURE__ */ Symbol("MEDIA_DASHBOARD_ACTIONS");
464
+ var MEDIA_CONSOLE_OBJECT_INSIGHTS = /* @__PURE__ */ Symbol("MEDIA_CONSOLE_OBJECT_INSIGHTS");
465
+ var MEDIA_CONSOLE_OBJECT_URLS = /* @__PURE__ */ Symbol("MEDIA_CONSOLE_OBJECT_URLS");
466
+
504
467
  // src/server/media-console.service.ts
505
468
  function _ts_decorate2(decorators, target, key, desc) {
506
469
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
@@ -530,46 +493,57 @@ function _ts_param2(paramIndex, decorator) {
530
493
  __name(_ts_param2, "_ts_param");
531
494
  var URL_TTL_SECONDS = 300;
532
495
  var DEFAULT_PAGE_LIMIT = 50;
533
- var MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
534
- var DELETE_SWEEP_LIMIT = 1e3;
535
- var MAX_CROSS_DISK_BYTES = 100 * 1024 * 1024;
496
+ var SWEEP_LIMIT = 1e3;
497
+ var FOLDER_KEEP_FILE = ".keep";
536
498
  function lastSegment(prefix) {
537
499
  const trimmed = prefix.replace(/\/+$/, "");
538
500
  const slash = trimmed.lastIndexOf("/");
539
501
  return slash === -1 ? trimmed : trimmed.slice(slash + 1);
540
502
  }
541
503
  __name(lastSegment, "lastSegment");
542
- var RangeNotSatisfiableException = class extends import_common2.HttpException {
543
- static {
544
- __name(this, "RangeNotSatisfiableException");
504
+ function clientFolder(prefix, what) {
505
+ const trimmed = prefix.trim().replace(/^\/+/, "");
506
+ let normalized;
507
+ try {
508
+ normalized = (0, import_nestjs_media_core.folderPrefix)(trimmed);
509
+ } catch (error) {
510
+ throw new import_common2.BadRequestException(`${what}: ${error.message}`);
545
511
  }
546
- size;
547
- constructor(size) {
548
- super({
549
- statusCode: import_common2.HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE,
550
- message: `Requested range is not satisfiable for an object of ${size} bytes`,
551
- error: "Range Not Satisfiable"
552
- }, import_common2.HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE), this.size = size;
512
+ if (normalized === "") throw new import_common2.BadRequestException(`${what} is required`);
513
+ return normalized;
514
+ }
515
+ __name(clientFolder, "clientFolder");
516
+ function isAddressable(key) {
517
+ try {
518
+ (0, import_nestjs_media_core.assertMediaKey)(key);
519
+ return true;
520
+ } catch {
521
+ return false;
553
522
  }
554
- };
555
- function resolveRange(range, size) {
556
- if ("suffixLength" in range) {
557
- if (!Number.isInteger(range.suffixLength) || range.suffixLength <= 0 || size === 0) return null;
558
- return {
559
- start: Math.max(0, size - range.suffixLength),
560
- end: size - 1
561
- };
523
+ }
524
+ __name(isAddressable, "isAddressable");
525
+ var CORE_CLIENT_ERRORS = /* @__PURE__ */ new Set([
526
+ "InvalidMediaKeyError",
527
+ "InvalidListingCursorError"
528
+ ]);
529
+ function toHttp(error) {
530
+ if (error instanceof import_common2.HttpException) return error;
531
+ if (error instanceof import_storage.StorageFileNotFoundError) return new import_common2.NotFoundException(error.message);
532
+ const status = error?.status;
533
+ if ((error instanceof import_storage.StorageError || CORE_CLIENT_ERRORS.has(error?.name ?? "")) && typeof status === "number") {
534
+ return new import_common2.HttpException(error.message, status);
535
+ }
536
+ return error;
537
+ }
538
+ __name(toHttp, "toHttp");
539
+ async function http(run) {
540
+ try {
541
+ return await run();
542
+ } catch (error) {
543
+ throw toHttp(error);
562
544
  }
563
- const { start } = range;
564
- if (!Number.isInteger(start) || start < 0 || start >= size) return null;
565
- const end = range.end === void 0 ? size - 1 : Math.min(range.end, size - 1);
566
- if (!Number.isInteger(end) || end < start) return null;
567
- return {
568
- start,
569
- end
570
- };
571
545
  }
572
- __name(resolveRange, "resolveRange");
546
+ __name(http, "http");
573
547
  function mapUpload(session) {
574
548
  const percent = session.size !== void 0 && session.size > 0 ? Math.min(100, Math.round(session.offset / session.size * 100)) : null;
575
549
  return {
@@ -613,108 +587,106 @@ var MediaConsoleService = class _MediaConsoleService {
613
587
  actionsEnabled;
614
588
  insightProviders;
615
589
  objectUrls;
616
- constructor(storage, store, uploads, actionsEnabled, insightProviders = null, objectUrls = null) {
590
+ multipart;
591
+ constructor(storage, store, uploads, actionsEnabled, insightProviders = null, objectUrls = null, multipart = null) {
617
592
  this.storage = storage;
618
593
  this.store = store;
619
594
  this.uploads = uploads;
620
595
  this.actionsEnabled = actionsEnabled;
621
596
  this.insightProviders = insightProviders;
622
597
  this.objectUrls = objectUrls;
598
+ this.multipart = multipart;
623
599
  }
600
+ logger = new import_common2.Logger(_MediaConsoleService.name);
624
601
  /**
625
602
  * The URL to report for an object's bytes, under the host's {@link ObjectUrlStrategy}. `auto`
626
- * prefers a presigned URL straight to the store; `proxy` routes through this server, for a host
627
- * whose browser cannot reach the store at all.
603
+ * prefers the disk's public URL, then a signed one (S3 presigns; a local disk signs with
604
+ * `signedUrls`), and falls back to this server's own route for a disk that can do neither.
605
+ * `proxy` always routes through this server.
628
606
  */
629
- objectUrl(driver, disk, key) {
630
- if (this.objectUrls?.strategy === "proxy") {
631
- return Promise.resolve(objectProxyUrl(this.objectUrls.apiBasePath, "raw", disk, key));
632
- }
633
- return driver.capabilities.presign ? driver.temporaryUrl(key, URL_TTL_SECONDS) : driver.url(key);
607
+ async objectUrl(disk, name, key) {
608
+ const proxy = /* @__PURE__ */ __name(() => objectProxyUrl(this.objectUrls?.apiBasePath ?? "", "raw", name, key), "proxy");
609
+ if (this.objectUrls?.strategy === "proxy") return proxy();
610
+ if ((0, import_nestjs_media_core.hasPublicUrl)(disk)) return disk.url(key);
611
+ if (await (0, import_nestjs_media_core.canPresign)(disk)) return disk.signedUrl(key, {
612
+ expiresIn: URL_TTL_SECONDS * 1e3
613
+ });
614
+ return proxy();
634
615
  }
635
- logger = new import_common2.Logger(_MediaConsoleService.name);
636
616
  diskOrThrow(disk) {
637
- if (!this.storage || !this.storage.diskNames().includes(disk)) {
617
+ if (!this.storage || !this.storage.names().includes(disk)) {
638
618
  throw new import_common2.NotFoundException(`Unknown disk: ${disk}`);
639
619
  }
640
620
  return this.storage.disk(disk);
641
621
  }
642
- listDisks() {
622
+ async listDisks() {
643
623
  const storage = this.storage;
644
624
  if (!storage) return {
645
625
  disks: []
646
626
  };
647
- const defaultDisk = storage.defaultDisk;
648
627
  return {
649
- disks: storage.diskNames().map((name) => ({
628
+ disks: await Promise.all(storage.names().map(async (name) => ({
650
629
  name,
651
- default: name === defaultDisk,
652
- capabilities: storage.disk(name).capabilities
653
- }))
630
+ default: name === storage.defaultDisk,
631
+ capabilities: await (0, import_nestjs_media_core.diskCapabilities)(storage.disk(name), (0, import_nestjs_media_core.multipartFor)(this.multipart ?? void 0, name))
632
+ })))
654
633
  };
655
634
  }
635
+ /**
636
+ * One folder level. `StorageDisk.list()` is flat; a disk whose multipart side-car lists with a
637
+ * delimiter (S3) asks the store for folders, any other disk folds its flat listing.
638
+ */
656
639
  async listObjects(disk, options) {
657
- const driver = this.diskOrThrow(disk);
658
- const result = await driver.list(options.prefix ?? "", {
659
- delimiter: "/",
640
+ const store = this.diskOrThrow(disk);
641
+ const raw = (options.prefix ?? "").replace(/^\/+/, "");
642
+ let prefix;
643
+ try {
644
+ prefix = (0, import_nestjs_media_core.folderPrefix)(raw);
645
+ } catch (error) {
646
+ throw new import_common2.BadRequestException(error.message);
647
+ }
648
+ const level = await http(() => (0, import_nestjs_media_core.listFolderLevel)(store, prefix, {
649
+ limit: options.limit ?? DEFAULT_PAGE_LIMIT,
660
650
  ...options.cursor ? {
661
651
  cursor: options.cursor
662
- } : {},
663
- limit: options.limit ?? DEFAULT_PAGE_LIMIT
664
- });
652
+ } : {}
653
+ }, (0, import_nestjs_media_core.multipartFor)(this.multipart ?? void 0, disk)));
665
654
  return {
666
- // Drop phantom folders whose name is empty — a CommonPrefix of only slashes (`/`, `//`),
667
- // produced by a stray key with a leading slash. The S3 driver normalizes such a prefix back to
668
- // the root, so listing INTO one returns the root again (the phantom included) — a self-reference
669
- // that infinite-loops the folder tree. Filtering it out removes the trap; those leading-slash
670
- // keys are unreachable from the console anyway (the driver strips the leading slash).
671
- folders: result.folders.map((prefix) => ({
672
- name: lastSegment(prefix),
673
- prefix
655
+ folders: level.folders.map((folder) => ({
656
+ name: lastSegment(folder),
657
+ prefix: folder
674
658
  })).filter((folder) => folder.name !== ""),
675
- // Drop the zero-byte "folder marker" (a key ending in `/`, whose name is empty after the
676
- // prefix) that `createFolder` writes — it's the folder itself, not a file inside it.
677
- files: result.files.filter((entry) => entry.name !== "").map((entry) => ({
659
+ files: level.files.map((entry) => ({
660
+ entry,
661
+ name: entry.key.slice(prefix.length)
662
+ })).filter(({ name }) => name !== "" && name !== FOLDER_KEEP_FILE).map(({ entry, name }) => ({
678
663
  key: entry.key,
679
- name: entry.name,
680
- sizeBytes: entry.sizeBytes,
681
- lastModified: entry.lastModified ? entry.lastModified.toISOString() : null
664
+ name,
665
+ sizeBytes: entry.size,
666
+ lastModified: entry.lastModified.toISOString()
682
667
  })),
683
- ...result.cursor ? {
684
- cursor: result.cursor
668
+ ...level.cursor ? {
669
+ cursor: level.cursor
685
670
  } : {}
686
671
  };
687
672
  }
688
673
  async objectDetail(disk, key) {
689
- const driver = this.diskOrThrow(disk);
690
- const stat = await driver.stat(key);
691
- const url = await this.objectUrl(driver, disk, key);
692
- return {
693
- key,
694
- size: stat.size,
695
- ...stat.contentType ? {
696
- contentType: stat.contentType
697
- } : {},
698
- ...stat.lastModified ? {
699
- lastModified: stat.lastModified.toISOString()
700
- } : {},
701
- url
702
- };
674
+ const store = this.diskOrThrow(disk);
675
+ return http(async () => {
676
+ const stat = await store.stat(key);
677
+ return {
678
+ key,
679
+ size: stat.size,
680
+ contentType: stat.contentType,
681
+ lastModified: stat.lastModified.toISOString(),
682
+ url: await this.objectUrl(store, disk, key)
683
+ };
684
+ });
703
685
  }
704
686
  /**
705
- * What the HOST knows about this object, from every registered provider.
706
- *
707
- * Its own call rather than a field on `objectDetail`, for the same reason the RAG index-status
708
- * endpoint is separate from its document listing: `objectDetail` is on the path of every preview
709
- * and every navigation, and providers reach into host databases. Folding them in would put a host
710
- * query on reads that only wanted a presigned URL.
711
- *
712
- * Failures are contained per provider. A provider that throws is logged and dropped, and the
713
- * others still render — annotation must never be able to stop an admin opening a file, and one
714
- * host lookup being down says nothing about the rest.
715
- *
716
- * Providers run concurrently: they are independent host lookups, and running them in series would
717
- * make the panel as slow as their sum for no gain.
687
+ * What the HOST knows about this object, from every registered provider. Its own call rather
688
+ * than a field on `objectDetail`, so host lookups stay off the preview path. A provider that
689
+ * throws is logged and dropped; providers run concurrently.
718
690
  */
719
691
  async objectInsights(disk, key) {
720
692
  const providers = this.insightProviders ?? [];
@@ -740,177 +712,132 @@ var MediaConsoleService = class _MediaConsoleService {
740
712
  };
741
713
  }
742
714
  /**
743
- * The object's raw byte stream plus the metadata the controller needs to serve it inline. Used by
744
- * the console's same-origin preview proxy so text/PDF render in the browser instead of downloading
745
- * (and so a CORS-locked bucket is still previewable).
746
- *
747
- * With `range`, only that slice — which is what makes previewing a multi-hundred-MB object (a
748
- * SQLite database read a page at a time) possible at all. The range is resolved HERE, not in the
749
- * driver, because it has to be checked against the object's size and the `stat` for that is
750
- * already on this path: the suffix form is made absolute, an overrunning `end` is clamped to the
751
- * last byte, and anything genuinely unsatisfiable becomes a {@link RangeNotSatisfiableException}
752
- * (HTTP 416) instead of a surprise empty body.
753
- *
754
- * A driver that reports `ranged: false` REFUSES a range request rather than quietly returning the
755
- * whole object: the caller asked for 64 KB out of 400 MB, and silently answering with all 400 MB
756
- * — which the caller would then read as if it were the slice — is the exact failure the `ranged`
757
- * capability exists to make impossible.
715
+ * Streams an object as the response through `@nestjs/storage`'s `serveFile()`: a single `Range`
716
+ * gets a 206 with `Content-Range` (how a SQLite/ZIP/Parquet preview reads a few KB out of a huge
717
+ * object), `If-None-Match` a 304, a range past the end a 416, a missing file a 404. `inline`
718
+ * applies only to types that can't run script in this origin; others download.
758
719
  */
759
- async objectStream(disk, key, range) {
760
- const driver = this.diskOrThrow(disk);
761
- const stat = await driver.stat(key);
762
- const contentType = stat.contentType ?? "application/octet-stream";
763
- if (!range) {
764
- return {
765
- stream: await driver.stream(key),
766
- contentType,
767
- size: stat.size
768
- };
769
- }
770
- if (!driver.capabilities.ranged) {
771
- throw new import_common2.NotImplementedException(`Disk "${disk}" cannot serve byte ranges`);
772
- }
773
- const resolved = resolveRange(range, stat.size);
774
- if (!resolved) throw new RangeNotSatisfiableException(stat.size);
775
- return {
776
- stream: await driver.stream(key, resolved),
777
- contentType,
778
- size: stat.size,
779
- range: resolved
780
- };
720
+ serveObject(disk, key, options) {
721
+ return http(() => (0, import_storage.serveFile)(this.diskOrThrow(disk), key, options));
781
722
  }
782
- /** Writes an object from a byte stream (a browser upload). The caller supplies the full key
783
- * (prefix + filename); an unknown disk 404s via {@link diskOrThrow}. Actions-gated.
784
- *
785
- * The request stream is buffered before the write: S3's PutObject needs a known Content-Length,
786
- * which a raw request stream doesn't carry, so passing the stream straight through fails. Bounded
787
- * at {@link MAX_UPLOAD_BYTES} so a runaway upload can't exhaust the pod's heap — larger files
788
- * belong on the resumable upload path, not this console convenience upload. */
789
- async putObject(disk, key, body, contentType) {
790
- const driver = this.diskOrThrow(disk);
791
- const chunks = [];
792
- let total = 0;
793
- for await (const chunk of body) {
794
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
795
- total += buffer.length;
796
- if (total > MAX_UPLOAD_BYTES) {
797
- throw new import_common2.PayloadTooLargeException(`Upload exceeds the ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB console limit.`);
798
- }
799
- chunks.push(buffer);
800
- }
801
- await driver.put(key, Buffer.concat(chunks), contentType ? {
802
- contentType
803
- } : void 0);
723
+ /**
724
+ * Writes an object from a byte stream (a browser upload), streamed onto the disk: S3Disk turns a
725
+ * body of unknown length into a multipart upload itself, so nothing is buffered here. With the
726
+ * request's `Content-Length`, a body that ends short fails and nothing is written.
727
+ */
728
+ async putObject(disk, key, body, contentType, contentLength) {
729
+ const store = this.diskOrThrow(disk);
730
+ await http(() => store.put(key, body, {
731
+ ...contentType ? {
732
+ contentType
733
+ } : {},
734
+ ...contentLength !== void 0 ? {
735
+ contentLength
736
+ } : {}
737
+ }));
804
738
  }
805
- /** Creates a "folder" — a zero-byte marker object at `<prefix>/`, which S3-style listing surfaces
806
- * as a navigable folder. Normalizes to exactly one trailing slash; rejects an empty prefix. */
739
+ /**
740
+ * Creates a folder: a zero-byte `<prefix>/.keep` file (hidden from listings), since
741
+ * `@nestjs/storage` keys can't end with `/`. Listings show the folder because a key lives in it.
742
+ */
807
743
  async createFolder(disk, prefix) {
808
- const normalized = prefix.replace(/^\/+/, "").replace(/\/+$/, "");
809
- if (normalized === "") throw new import_common2.BadRequestException("Folder name is required");
810
- await this.diskOrThrow(disk).put(`${normalized}/`, Buffer.alloc(0));
744
+ const folder = clientFolder(prefix, "Folder name");
745
+ await http(() => this.diskOrThrow(disk).put(`${folder}${FOLDER_KEEP_FILE}`, ""));
811
746
  }
812
747
  async deleteObject(disk, key) {
813
- await this.diskOrThrow(disk).delete(key);
814
- }
815
- /** Recursively deletes a "folder": every object under `<prefix>/` (nested included) plus the
816
- * zero-byte marker itself. Passes an empty delimiter so the driver lists keys FLAT — the default
817
- * `/` delimiter would group nested keys into CommonPrefixes and the sweep would miss them, only
818
- * deleting direct children. Paginates until the disk reports no more. The marker's own key equals
819
- * the sweep prefix, which `list` filters out (Key === Prefix), so it's deleted explicitly at the
820
- * end. Actions-gated. */
748
+ await http(() => this.diskOrThrow(disk).delete(key));
749
+ }
750
+ /**
751
+ * Recursively deletes a folder: every file under `<prefix>/`, nested included (its `.keep`
752
+ * too), a batch of keys per `delete()`. A legacy `prefix/` marker object written by another tool
753
+ * can't be addressed through `@nestjs/storage`; it is skipped.
754
+ */
821
755
  async deleteFolder(disk, prefix) {
822
- const driver = this.diskOrThrow(disk);
823
- const normalized = prefix.replace(/^\/+/, "").replace(/\/+$/, "");
824
- if (normalized === "") throw new import_common2.BadRequestException("Folder is required");
825
- const sweepPrefix = `${normalized}/`;
826
- let cursor;
827
- do {
828
- const result = await driver.list(sweepPrefix, {
829
- delimiter: "",
830
- limit: DELETE_SWEEP_LIMIT,
831
- ...cursor ? {
832
- cursor
833
- } : {}
834
- });
835
- const keys = result.files.map((entry) => entry.key);
836
- if (driver.deleteMany) await driver.deleteMany(keys);
837
- else for (const key of keys) await driver.delete(key);
838
- cursor = result.cursor;
839
- } while (cursor);
840
- await driver.delete(sweepPrefix);
756
+ const store = this.diskOrThrow(disk);
757
+ const folder = clientFolder(prefix, "Folder");
758
+ await http(async () => {
759
+ const keys = [];
760
+ for await (const entry of store.listAll({
761
+ prefix: folder,
762
+ limit: SWEEP_LIMIT
763
+ })) {
764
+ if (isAddressable(entry.key)) keys.push(entry.key);
765
+ }
766
+ for (let start = 0; start < keys.length; start += SWEEP_LIMIT) {
767
+ await store.delete(keys.slice(start, start + SWEEP_LIMIT));
768
+ }
769
+ });
841
770
  }
842
771
  async copyObject(fromDisk, from, toDisk, to) {
843
- await this.transferKey("copy", fromDisk, from, toDisk, to);
772
+ await http(() => this.transferKey("copy", fromDisk, from, toDisk, to));
844
773
  }
845
774
  async moveObject(fromDisk, from, toDisk, to) {
846
- await this.transferKey("move", fromDisk, from, toDisk, to);
775
+ await http(() => this.transferKey("move", fromDisk, from, toDisk, to));
847
776
  }
848
- /** Copies or moves a single object, on the same disk or across disks. Same-disk uses the driver's
849
- * native `copy`/`move` (server-side, no bytes through the pod). Cross-disk has no driver primitive,
850
- * so the object is streamed through the pod (get→put, buffered), preserving its content type and
851
- * bounded at {@link MAX_CROSS_DISK_BYTES}; a `move` then deletes the source. */
777
+ /**
778
+ * Copies or moves one object. Same disk: the disk's own `copy`/`move` (server-side on S3, a
779
+ * rename on a local disk). Across disks: the source's body streams into the destination's
780
+ * `put()` (no size limit, nothing buffered), keeping its content type, cache control,
781
+ * disposition and metadata; a `move` then deletes the source.
782
+ */
852
783
  async transferKey(op, fromDisk, from, toDisk, to) {
853
- const fromDriver = this.diskOrThrow(fromDisk);
784
+ const source = this.diskOrThrow(fromDisk);
854
785
  if (fromDisk === toDisk) {
855
- if (op === "move") await fromDriver.move(from, to);
856
- else await fromDriver.copy(from, to);
786
+ if (op === "move") await source.move(from, to);
787
+ else await source.copy(from, to);
857
788
  return;
858
789
  }
859
- const toDriver = this.diskOrThrow(toDisk);
860
- const stat = await fromDriver.stat(from);
861
- if (stat.size > MAX_CROSS_DISK_BYTES) {
862
- throw new import_common2.PayloadTooLargeException(`"${from}" is ${Math.round(stat.size / (1024 * 1024))} MB \u2014 over the ${MAX_CROSS_DISK_BYTES / (1024 * 1024)} MB limit for copying across disks from the console.`);
790
+ const target = this.diskOrThrow(toDisk);
791
+ const download = await source.get(from);
792
+ try {
793
+ await target.put(to, download.body, {
794
+ contentType: download.contentType,
795
+ contentLength: download.size,
796
+ ...download.cacheControl ? {
797
+ cacheControl: download.cacheControl
798
+ } : {},
799
+ ...download.contentDisposition ? {
800
+ contentDisposition: download.contentDisposition
801
+ } : {},
802
+ metadata: download.metadata
803
+ });
804
+ } finally {
805
+ download.body.destroy();
863
806
  }
864
- const bytes = await fromDriver.get(from);
865
- await toDriver.put(to, bytes, stat.contentType ? {
866
- contentType: stat.contentType
867
- } : void 0);
868
- if (op === "move") await fromDriver.delete(from);
869
- }
870
- /** Moves a whole "folder" (every object under `<from>/`, nested included) to `<to>/`, on the same
871
- * disk or across disks, preserving each key's path relative to the source. */
807
+ if (op === "move") await source.delete(from);
808
+ }
809
+ /** Moves a whole folder (every file under `<from>/`, nested included) to `<to>/`. */
872
810
  async moveFolder(fromDisk, from, toDisk, to) {
873
- await this.transferFolder("move", fromDisk, from, toDisk, to);
811
+ await http(() => this.transferFolder("move", fromDisk, from, toDisk, to));
874
812
  }
875
- /** Copies a whole "folder" (every object under `<from>/`, nested included) to `<to>/`, same disk or
876
- * across disks, preserving each key's path relative to the source. */
813
+ /** Copies a whole folder (every file under `<from>/`, nested included) to `<to>/`. */
877
814
  async copyFolder(fromDisk, from, toDisk, to) {
878
- await this.transferFolder("copy", fromDisk, from, toDisk, to);
815
+ await http(() => this.transferFolder("copy", fromDisk, from, toDisk, to));
879
816
  }
880
- /** Shared engine for {@link moveFolder}/{@link copyFolder}. Same flat-listing sweep as
881
- * {@link deleteFolder}; each key is relocated via {@link transferKey} (so cross-disk transfers work
882
- * transparently). On the SAME disk, rejects a destination inside the source (which would recurse
883
- * forever); across disks that constraint doesn't apply. The destination folder marker is written,
884
- * and for a move the source marker is removed, so both listings stay consistent. Actions-gated. */
817
+ /**
818
+ * Shared engine for {@link moveFolder}/{@link copyFolder}: a flat sweep of the source, each key
819
+ * relocated via {@link transferKey}, keeping its path relative to the folder (an empty folder's
820
+ * `.keep` goes along). On the SAME disk, a destination inside the source is refused (it would
821
+ * recurse forever).
822
+ */
885
823
  async transferFolder(op, fromDisk, from, toDisk, to) {
886
- const fromDriver = this.diskOrThrow(fromDisk);
887
- const toDriver = this.diskOrThrow(toDisk);
888
- const fromNormalized = from.replace(/^\/+/, "").replace(/\/+$/, "");
889
- const toNormalized = to.replace(/^\/+/, "").replace(/\/+$/, "");
890
- if (fromNormalized === "") throw new import_common2.BadRequestException("Source folder is required");
891
- if (toNormalized === "") throw new import_common2.BadRequestException("Destination folder is required");
892
- const fromPrefix = `${fromNormalized}/`;
893
- const toPrefix = `${toNormalized}/`;
894
- if (fromDisk === toDisk && (toPrefix === fromPrefix || toPrefix.startsWith(fromPrefix))) {
824
+ const source = this.diskOrThrow(fromDisk);
825
+ this.diskOrThrow(toDisk);
826
+ const fromPrefix = clientFolder(from, "Source folder");
827
+ const toPrefix = clientFolder(to, "Destination folder");
828
+ if (fromDisk === toDisk && toPrefix.startsWith(fromPrefix)) {
895
829
  throw new import_common2.BadRequestException(`Cannot ${op} a folder into itself`);
896
830
  }
897
- let cursor;
898
- do {
899
- const result = await fromDriver.list(fromPrefix, {
900
- delimiter: "",
901
- limit: DELETE_SWEEP_LIMIT,
902
- ...cursor ? {
903
- cursor
904
- } : {}
905
- });
906
- for (const entry of result.files) {
907
- const destKey = `${toPrefix}${entry.key.slice(fromPrefix.length)}`;
908
- await this.transferKey(op, fromDisk, entry.key, toDisk, destKey);
909
- }
910
- cursor = result.cursor;
911
- } while (cursor);
912
- await toDriver.put(toPrefix, Buffer.alloc(0));
913
- if (op === "move") await fromDriver.delete(fromPrefix);
831
+ const keys = [];
832
+ for await (const entry of source.listAll({
833
+ prefix: fromPrefix,
834
+ limit: SWEEP_LIMIT
835
+ })) {
836
+ if (isAddressable(entry.key)) keys.push(entry.key);
837
+ }
838
+ for (const key of keys) {
839
+ await this.transferKey(op, fromDisk, key, toDisk, `${toPrefix}${key.slice(fromPrefix.length)}`);
840
+ }
914
841
  }
915
842
  async listUploads(filter) {
916
843
  if (!this.uploads || typeof this.uploads.list !== "function") return {
@@ -965,7 +892,7 @@ var MediaConsoleService = class _MediaConsoleService {
965
892
  if (!this.store || typeof this.store.list !== "function") return {
966
893
  records: []
967
894
  };
968
- const page = await this.store.list({
895
+ const page2 = await this.store.list({
969
896
  ...filter.collection ? {
970
897
  collection: filter.collection
971
898
  } : {},
@@ -979,9 +906,9 @@ var MediaConsoleService = class _MediaConsoleService {
979
906
  } : {}
980
907
  });
981
908
  return {
982
- records: page.records.map(mapRecord),
983
- ...page.cursor ? {
984
- cursor: page.cursor
909
+ records: page2.records.map(mapRecord),
910
+ ...page2.cursor ? {
911
+ cursor: page2.cursor
985
912
  } : {}
986
913
  };
987
914
  }
@@ -990,8 +917,8 @@ var MediaConsoleService = class _MediaConsoleService {
990
917
  const record = await this.store.find(id);
991
918
  if (!record) throw new import_common2.NotFoundException(`Unknown media record: ${id}`);
992
919
  const variants = await Promise.all(Object.entries(record.conversions).map(async ([name, conversion]) => {
993
- const driver = this.storage?.diskNames().includes(conversion.disk) ? this.storage.disk(conversion.disk) : null;
994
- const url = driver ? await this.objectUrl(driver, conversion.disk, conversion.path) : "";
920
+ const disk = this.storage?.names().includes(conversion.disk) ? this.storage.disk(conversion.disk) : null;
921
+ const url = disk ? await this.objectUrl(disk, conversion.disk, conversion.path).catch(() => "") : "";
995
922
  return {
996
923
  name,
997
924
  url,
@@ -1012,7 +939,7 @@ var MediaConsoleService = class _MediaConsoleService {
1012
939
  return {
1013
940
  hasStore: this.store !== null && typeof this.store.list === "function",
1014
941
  hasUploads: this.uploads !== null && typeof this.uploads.list === "function",
1015
- disks: this.storage ? this.storage.diskNames().length : 0,
942
+ disks: this.storage ? this.storage.names().length : 0,
1016
943
  actions: this.actionsEnabled === true
1017
944
  };
1018
945
  }
@@ -1020,7 +947,7 @@ var MediaConsoleService = class _MediaConsoleService {
1020
947
  MediaConsoleService = _ts_decorate2([
1021
948
  (0, import_common2.Injectable)(),
1022
949
  _ts_param2(0, (0, import_common2.Optional)()),
1023
- _ts_param2(0, (0, import_common2.Inject)(MEDIA_STORAGE_SHARED)),
950
+ _ts_param2(0, (0, import_common2.Inject)(import_storage.Storage)),
1024
951
  _ts_param2(1, (0, import_common2.Optional)()),
1025
952
  _ts_param2(1, (0, import_common2.Inject)(MEDIA_STORE)),
1026
953
  _ts_param2(2, (0, import_common2.Optional)()),
@@ -1031,6 +958,8 @@ MediaConsoleService = _ts_decorate2([
1031
958
  _ts_param2(4, (0, import_common2.Inject)(MEDIA_CONSOLE_OBJECT_INSIGHTS)),
1032
959
  _ts_param2(5, (0, import_common2.Optional)()),
1033
960
  _ts_param2(5, (0, import_common2.Inject)(MEDIA_CONSOLE_OBJECT_URLS)),
961
+ _ts_param2(6, (0, import_common2.Optional)()),
962
+ _ts_param2(6, (0, import_common2.Inject)(MEDIA_MULTIPART)),
1034
963
  _ts_metadata2("design:type", Function),
1035
964
  _ts_metadata2("design:paramtypes", [
1036
965
  Object,
@@ -1038,6 +967,7 @@ MediaConsoleService = _ts_decorate2([
1038
967
  Object,
1039
968
  Object,
1040
969
  Object,
970
+ Object,
1041
971
  Object
1042
972
  ])
1043
973
  ], MediaConsoleService);
@@ -1095,10 +1025,12 @@ var MediaConsoleActionsController = class {
1095
1025
  /**
1096
1026
  * Uploads a file to `key` on the disk. The body is the raw bytes sent as `application/octet-stream`
1097
1027
  * so the host's JSON/urlencoded body parsers never consume the stream; the real MIME rides as the
1098
- * `type` query param and becomes the stored object's Content-Type. `@Req()` is the request stream.
1028
+ * `type` query param and becomes the stored object's Content-Type. `@Req()` is the request stream,
1029
+ * streamed onto the disk (no size cap, nothing buffered).
1099
1030
  */
1100
1031
  uploadObject(disk, key, request, type) {
1101
- return this.service.putObject(disk, key, request, type);
1032
+ const declared = Number(request.headers?.["content-length"]);
1033
+ return this.service.putObject(disk, key, request, type, Number.isSafeInteger(declared) && declared >= 0 ? declared : void 0);
1102
1034
  }
1103
1035
  createFolder(disk, body) {
1104
1036
  return this.service.createFolder(disk, body.prefix);
@@ -1184,7 +1116,7 @@ _ts_decorate3([
1184
1116
  _ts_metadata3("design:paramtypes", [
1185
1117
  String,
1186
1118
  String,
1187
- typeof Readable === "undefined" ? Object : Readable,
1119
+ Object,
1188
1120
  String
1189
1121
  ]),
1190
1122
  _ts_metadata3("design:returntype", typeof Promise === "undefined" ? Object : Promise)
@@ -1234,7 +1166,7 @@ _ts_decorate3([
1234
1166
  _ts_metadata3("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1235
1167
  ], MediaConsoleActionsController.prototype, "deleteLibraryRecord", null);
1236
1168
  MediaConsoleActionsController = _ts_decorate3([
1237
- (0, import_common3.UseGuards)(MediaConsoleGuard),
1169
+ OpenToAuthorization(),
1238
1170
  (0, import_common3.Controller)(),
1239
1171
  _ts_param3(0, (0, import_common3.Inject)(MediaConsoleService)),
1240
1172
  _ts_metadata3("design:type", Function),
@@ -1243,229 +1175,19 @@ MediaConsoleActionsController = _ts_decorate3([
1243
1175
  ])
1244
1176
  ], MediaConsoleActionsController);
1245
1177
 
1246
- // src/server/media-console-auth.controller.ts
1247
- var import_common4 = require("@nestjs/common");
1248
- function _ts_decorate4(decorators, target, key, desc) {
1249
- var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1250
- if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
1251
- r = Reflect.decorate(decorators, target, key, desc);
1252
- } else {
1253
- for (var i = decorators.length - 1; i >= 0; i--) {
1254
- if (d = decorators[i]) {
1255
- r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
1256
- }
1257
- }
1258
- }
1259
- return c > 3 && r && Object.defineProperty(target, key, r), r;
1260
- }
1261
- __name(_ts_decorate4, "_ts_decorate");
1262
- function _ts_metadata4(metadataKey, metadataValue) {
1263
- if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
1264
- return Reflect.metadata(metadataKey, metadataValue);
1265
- }
1266
- }
1267
- __name(_ts_metadata4, "_ts_metadata");
1268
- function _ts_param4(paramIndex, decorator) {
1269
- return function(target, key) {
1270
- decorator(target, key, paramIndex);
1271
- };
1272
- }
1273
- __name(_ts_param4, "_ts_param");
1274
- var MediaConsoleAuthController = class _MediaConsoleAuthController {
1275
- static {
1276
- __name(this, "MediaConsoleAuthController");
1277
- }
1278
- auth;
1279
- cookiePath;
1280
- logger = new import_common4.Logger(_MediaConsoleAuthController.name);
1281
- /** One warn per hook kind, so a flaky hook doesn't spam logs every request. */
1282
- warnedHooks = /* @__PURE__ */ new Set();
1283
- constructor(auth, cookiePath) {
1284
- this.auth = auth;
1285
- this.cookiePath = cookiePath;
1286
- }
1287
- me(request) {
1288
- if (!this.auth) return {
1289
- authRequired: false
1290
- };
1291
- const cookieValue = parseCookieHeader(readCookieHeader(request))[SESSION_COOKIE_NAME];
1292
- const session = cookieValue !== void 0 ? verifySessionCookie(cookieValue, {
1293
- secret: this.auth.secret
1294
- }) : null;
1295
- if (!session) throw new import_common4.UnauthorizedException({
1296
- auth: {
1297
- modes: this.auth.modes
1298
- }
1299
- });
1300
- return {
1301
- user: {
1302
- id: session.sub,
1303
- ...session.name !== void 0 ? {
1304
- name: session.name
1305
- } : {},
1306
- roles: session.roles
1307
- }
1308
- };
1309
- }
1310
- async login(body, request, response) {
1311
- const auth = this.requireAuth();
1312
- if (!auth.login) throw new import_common4.NotFoundException();
1313
- if (typeof body?.username !== "string" || typeof body?.password !== "string") {
1314
- throw new import_common4.BadRequestException("Body must include string `username` and `password`.");
1315
- }
1316
- const username = body.username;
1317
- const password = body.password;
1318
- const user = await this.runHook("login", () => auth.login?.(username, password) ?? null);
1319
- if (!user) throw new import_common4.UnauthorizedException({
1320
- message: "Invalid credentials"
1321
- });
1322
- return this.mint(user, request, response);
1323
- }
1324
- async session(request, response) {
1325
- const auth = this.requireAuth();
1326
- if (!auth.session) throw new import_common4.NotFoundException();
1327
- const user = await this.runHook("session", () => auth.session?.(request) ?? null);
1328
- if (!user) throw new import_common4.UnauthorizedException();
1329
- return this.mint(user, request, response);
1330
- }
1331
- logout(request, response) {
1332
- clearSessionCookie({
1333
- cookiePath: this.cookiePath ?? "/",
1334
- request,
1335
- response
1336
- });
1337
- }
1338
- requireAuth() {
1339
- if (!this.auth) throw new import_common4.NotFoundException();
1340
- return this.auth;
1341
- }
1342
- mint(user, request, response) {
1343
- const auth = this.requireAuth();
1344
- issueSessionCookie(user, {
1345
- auth,
1346
- cookiePath: this.cookiePath ?? "/",
1347
- request,
1348
- response
1349
- });
1350
- return {
1351
- user: {
1352
- id: user.id,
1353
- ...user.name !== void 0 ? {
1354
- name: user.name
1355
- } : {},
1356
- roles: user.roles ?? []
1357
- }
1358
- };
1359
- }
1360
- /** Run a host hook defensively: a throw is a denial (null), warn-logged once per kind. */
1361
- async runHook(kind, run) {
1362
- try {
1363
- return await run() ?? null;
1364
- } catch (error) {
1365
- if (!this.warnedHooks.has(kind)) {
1366
- this.warnedHooks.add(kind);
1367
- this.logger.warn(`Console auth ${kind} hook threw; treating as denial. ${String(error)}`);
1368
- }
1369
- return null;
1370
- }
1371
- }
1372
- };
1373
- _ts_decorate4([
1374
- (0, import_common4.Get)("me"),
1375
- _ts_param4(0, (0, import_common4.Req)()),
1376
- _ts_metadata4("design:type", Function),
1377
- _ts_metadata4("design:paramtypes", [
1378
- Object
1379
- ]),
1380
- _ts_metadata4("design:returntype", typeof MeResponse === "undefined" ? Object : MeResponse)
1381
- ], MediaConsoleAuthController.prototype, "me", null);
1382
- _ts_decorate4([
1383
- (0, import_common4.Post)("login"),
1384
- (0, import_common4.HttpCode)(200),
1385
- _ts_param4(0, (0, import_common4.Body)()),
1386
- _ts_param4(1, (0, import_common4.Req)()),
1387
- _ts_param4(2, (0, import_common4.Res)({
1388
- passthrough: true
1389
- })),
1390
- _ts_metadata4("design:type", Function),
1391
- _ts_metadata4("design:paramtypes", [
1392
- typeof LoginBody === "undefined" ? Object : LoginBody,
1393
- Object,
1394
- Object
1395
- ]),
1396
- _ts_metadata4("design:returntype", Promise)
1397
- ], MediaConsoleAuthController.prototype, "login", null);
1398
- _ts_decorate4([
1399
- (0, import_common4.Post)("session"),
1400
- (0, import_common4.HttpCode)(200),
1401
- _ts_param4(0, (0, import_common4.Req)()),
1402
- _ts_param4(1, (0, import_common4.Res)({
1403
- passthrough: true
1404
- })),
1405
- _ts_metadata4("design:type", Function),
1406
- _ts_metadata4("design:paramtypes", [
1407
- Object,
1408
- Object
1409
- ]),
1410
- _ts_metadata4("design:returntype", Promise)
1411
- ], MediaConsoleAuthController.prototype, "session", null);
1412
- _ts_decorate4([
1413
- (0, import_common4.Post)("logout"),
1414
- (0, import_common4.HttpCode)(204),
1415
- _ts_param4(0, (0, import_common4.Req)()),
1416
- _ts_param4(1, (0, import_common4.Res)({
1417
- passthrough: true
1418
- })),
1419
- _ts_metadata4("design:type", Function),
1420
- _ts_metadata4("design:paramtypes", [
1421
- Object,
1422
- Object
1423
- ]),
1424
- _ts_metadata4("design:returntype", void 0)
1425
- ], MediaConsoleAuthController.prototype, "logout", null);
1426
- MediaConsoleAuthController = _ts_decorate4([
1427
- (0, import_common4.Controller)(),
1428
- _ts_param4(0, (0, import_common4.Optional)()),
1429
- _ts_param4(0, (0, import_common4.Inject)(MEDIA_CONSOLE_AUTH)),
1430
- _ts_param4(1, (0, import_common4.Optional)()),
1431
- _ts_param4(1, (0, import_common4.Inject)(MEDIA_CONSOLE_COOKIE_PATH)),
1432
- _ts_metadata4("design:type", Function),
1433
- _ts_metadata4("design:paramtypes", [
1434
- Object,
1435
- Object
1436
- ])
1437
- ], MediaConsoleAuthController);
1438
-
1439
1178
  // src/server/media-console-read.controller.ts
1440
- var import_common5 = require("@nestjs/common");
1179
+ var import_common4 = require("@nestjs/common");
1441
1180
 
1442
1181
  // src/server/content-disposition.ts
1443
1182
  var FALLBACK_FILENAME = "download";
1444
- var ATTR_CHAR = /^[A-Za-z0-9!#$&+\-.^_`|~]$/;
1445
- function toExtValue(name) {
1446
- let encoded = "";
1447
- for (const byte of new TextEncoder().encode(name)) {
1448
- const char = String.fromCharCode(byte);
1449
- encoded += ATTR_CHAR.test(char) ? char : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
1450
- }
1451
- return encoded;
1452
- }
1453
- __name(toExtValue, "toExtValue");
1454
1183
  function downloadFilename(key) {
1455
1184
  const segment = key.split("/").pop() ?? "";
1456
1185
  return segment.trim() === "" ? FALLBACK_FILENAME : segment;
1457
1186
  }
1458
1187
  __name(downloadFilename, "downloadFilename");
1459
- function attachmentDisposition(filename) {
1460
- const name = filename.trim() === "" ? FALLBACK_FILENAME : filename;
1461
- const ascii = name.replace(/[^\x20-\x7E]/g, "").replace(/["\\]/g, "").trim();
1462
- const quoted = ascii === "" ? FALLBACK_FILENAME : ascii;
1463
- return `attachment; filename="${quoted}"; filename*=UTF-8''${toExtValue(name)}`;
1464
- }
1465
- __name(attachmentDisposition, "attachmentDisposition");
1466
1188
 
1467
1189
  // src/server/media-console-read.controller.ts
1468
- function _ts_decorate5(decorators, target, key, desc) {
1190
+ function _ts_decorate4(decorators, target, key, desc) {
1469
1191
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1470
1192
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
1471
1193
  r = Reflect.decorate(decorators, target, key, desc);
@@ -1478,62 +1200,25 @@ function _ts_decorate5(decorators, target, key, desc) {
1478
1200
  }
1479
1201
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1480
1202
  }
1481
- __name(_ts_decorate5, "_ts_decorate");
1482
- function _ts_metadata5(metadataKey, metadataValue) {
1203
+ __name(_ts_decorate4, "_ts_decorate");
1204
+ function _ts_metadata4(metadataKey, metadataValue) {
1483
1205
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
1484
1206
  return Reflect.metadata(metadataKey, metadataValue);
1485
1207
  }
1486
1208
  }
1487
- __name(_ts_metadata5, "_ts_metadata");
1488
- function _ts_param5(paramIndex, decorator) {
1209
+ __name(_ts_metadata4, "_ts_metadata");
1210
+ function _ts_param4(paramIndex, decorator) {
1489
1211
  return function(target, key) {
1490
1212
  decorator(target, key, paramIndex);
1491
1213
  };
1492
1214
  }
1493
- __name(_ts_param5, "_ts_param");
1215
+ __name(_ts_param4, "_ts_param");
1494
1216
  function toLimit(value) {
1495
1217
  if (value === void 0) return void 0;
1496
1218
  const parsed = Number(value);
1497
1219
  return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : void 0;
1498
1220
  }
1499
1221
  __name(toLimit, "toLimit");
1500
- var SINGLE_BYTE_RANGE = /^bytes=(\d*)-(\d*)$/;
1501
- function parseRangeHeader(header) {
1502
- if (header === void 0) return null;
1503
- const match = SINGLE_BYTE_RANGE.exec(header.trim());
1504
- if (!match) return null;
1505
- const startText = match[1] ?? "";
1506
- const endText = match[2] ?? "";
1507
- if (startText === "" && endText === "") return null;
1508
- if (startText === "") return {
1509
- suffixLength: Number(endText)
1510
- };
1511
- const start = Number(startText);
1512
- return endText === "" ? {
1513
- start
1514
- } : {
1515
- start,
1516
- end: Number(endText)
1517
- };
1518
- }
1519
- __name(parseRangeHeader, "parseRangeHeader");
1520
- function setHeader(response, name, value) {
1521
- if (typeof response.setHeader === "function") {
1522
- response.setHeader(name, value);
1523
- return;
1524
- }
1525
- if (typeof response.raw?.setHeader === "function") response.raw.setHeader(name, value);
1526
- }
1527
- __name(setHeader, "setHeader");
1528
- function setStatus(response, code) {
1529
- if (typeof response.status === "function") {
1530
- response.status(code);
1531
- return;
1532
- }
1533
- if (response.raw) response.raw.statusCode = code;
1534
- else response.statusCode = code;
1535
- }
1536
- __name(setStatus, "setStatus");
1537
1222
  var MediaConsoleReadController = class {
1538
1223
  static {
1539
1224
  __name(this, "MediaConsoleReadController");
@@ -1568,63 +1253,29 @@ var MediaConsoleReadController = class {
1568
1253
  return this.service.objectInsights(disk, key);
1569
1254
  }
1570
1255
  /**
1571
- * Streams the object's bytes inline (Content-Disposition: inline) from the same origin, so the SPA
1572
- * can render text/PDF previews the browser would otherwise download, and read text past CORS.
1256
+ * Streams the object's bytes from the same origin (`serveFile()`), so the SPA can render previews
1257
+ * past CORS: `inline` for types that can't run script here (images, PDF, audio, video, plain
1258
+ * text), `attachment` for the rest — the text previews read the body with `fetch`, which doesn't
1259
+ * care. A single `Range` gets a 206, which is how SQLite/ZIP/Parquet previews read a few KB out
1260
+ * of a multi-hundred-MB object.
1573
1261
  */
1574
- objectRaw(disk, key, rangeHeader, response) {
1575
- return this.streamObject(disk, key, rangeHeader, response, "inline");
1576
- }
1577
- /**
1578
- * The same bytes as `/raw`, but as a SAVE rather than a render: `Content-Disposition: attachment`
1579
- * with the object's own name on it.
1580
- *
1581
- * Its own route rather than a flag on `/raw` because `/raw` is load-bearing for every preview —
1582
- * they depend on `inline`, and a disposition that varied by query param would be one wrong link
1583
- * away from a PDF preview turning into a download.
1584
- *
1585
- * Same-origin and unconditional, deliberately: this is the console's own action, so it must work
1586
- * on a host whose browser cannot reach the object store at all (no route to S3, no CORS grant).
1587
- * That is exactly where `objectDetail`'s presigned `url` — a link to share, not a way to fetch —
1588
- * is useless. See the `objectUrls` option.
1589
- */
1590
- objectDownload(disk, key, rangeHeader, response) {
1591
- return this.streamObject(disk, key, rangeHeader, response, attachmentDisposition(downloadFilename(key ?? "")));
1262
+ objectRaw(disk, key, req, res) {
1263
+ return this.service.serveObject(disk, key, {
1264
+ req,
1265
+ res,
1266
+ disposition: "inline"
1267
+ });
1592
1268
  }
1593
1269
  /**
1594
- * Stream an object under the given `Content-Disposition`, honouring a single-range `Range` header
1595
- * — which is what lets a reader pull a few KB out of a multi-hundred-MB object (a SQLite page, a
1596
- * ZIP's central directory) instead of the whole file, and what lets a browser resume an
1597
- * interrupted download of one. With no `Range` the response is a plain 200 full body, which the
1598
- * PDF/text/spreadsheet previews depend on.
1270
+ * The same bytes as `/raw`, but as a SAVE: `Content-Disposition: attachment` with the object's
1271
+ * own name. Same-origin and unconditional, so it works where the browser can't reach the store.
1599
1272
  */
1600
- async streamObject(disk, key, rangeHeader, response, disposition) {
1601
- const requested = parseRangeHeader(rangeHeader);
1602
- setHeader(response, "Accept-Ranges", "bytes");
1603
- let result;
1604
- try {
1605
- result = await this.service.objectStream(disk, key, requested ?? void 0);
1606
- } catch (error) {
1607
- if (error instanceof RangeNotSatisfiableException) {
1608
- setHeader(response, "Content-Range", `bytes */${error.size}`);
1609
- }
1610
- throw error;
1611
- }
1612
- const { stream, contentType, size, range } = result;
1613
- if (!range) {
1614
- return new import_common5.StreamableFile(stream, {
1615
- type: contentType,
1616
- disposition,
1617
- ...Number.isFinite(size) ? {
1618
- length: size
1619
- } : {}
1620
- });
1621
- }
1622
- setStatus(response, 206);
1623
- setHeader(response, "Content-Range", `bytes ${range.start}-${range.end}/${size}`);
1624
- return new import_common5.StreamableFile(stream, {
1625
- type: contentType,
1626
- disposition,
1627
- length: range.end - range.start + 1
1273
+ objectDownload(disk, key, req, res) {
1274
+ return this.service.serveObject(disk, key, {
1275
+ req,
1276
+ res,
1277
+ disposition: "attachment",
1278
+ filename: downloadFilename(key ?? "")
1628
1279
  });
1629
1280
  }
1630
1281
  uploads(disk, prefix) {
@@ -1667,151 +1318,151 @@ var MediaConsoleReadController = class {
1667
1318
  return this.service.topology();
1668
1319
  }
1669
1320
  };
1670
- _ts_decorate5([
1671
- (0, import_common5.Get)("disks"),
1672
- _ts_metadata5("design:type", Function),
1673
- _ts_metadata5("design:paramtypes", []),
1674
- _ts_metadata5("design:returntype", typeof DiskListResponse === "undefined" ? Object : DiskListResponse)
1321
+ _ts_decorate4([
1322
+ (0, import_common4.Get)("disks"),
1323
+ _ts_metadata4("design:type", Function),
1324
+ _ts_metadata4("design:paramtypes", []),
1325
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1675
1326
  ], MediaConsoleReadController.prototype, "disks", null);
1676
- _ts_decorate5([
1677
- (0, import_common5.Get)("disks/:disk/objects"),
1678
- _ts_param5(0, (0, import_common5.Param)("disk")),
1679
- _ts_param5(1, (0, import_common5.Query)("prefix")),
1680
- _ts_param5(2, (0, import_common5.Query)("cursor")),
1681
- _ts_param5(3, (0, import_common5.Query)("limit")),
1682
- _ts_metadata5("design:type", Function),
1683
- _ts_metadata5("design:paramtypes", [
1327
+ _ts_decorate4([
1328
+ (0, import_common4.Get)("disks/:disk/objects"),
1329
+ _ts_param4(0, (0, import_common4.Param)("disk")),
1330
+ _ts_param4(1, (0, import_common4.Query)("prefix")),
1331
+ _ts_param4(2, (0, import_common4.Query)("cursor")),
1332
+ _ts_param4(3, (0, import_common4.Query)("limit")),
1333
+ _ts_metadata4("design:type", Function),
1334
+ _ts_metadata4("design:paramtypes", [
1684
1335
  String,
1685
1336
  String,
1686
1337
  String,
1687
1338
  String
1688
1339
  ]),
1689
- _ts_metadata5("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1340
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1690
1341
  ], MediaConsoleReadController.prototype, "objects", null);
1691
- _ts_decorate5([
1692
- (0, import_common5.Get)("disks/:disk/object"),
1693
- _ts_param5(0, (0, import_common5.Param)("disk")),
1694
- _ts_param5(1, (0, import_common5.Query)("key")),
1695
- _ts_metadata5("design:type", Function),
1696
- _ts_metadata5("design:paramtypes", [
1342
+ _ts_decorate4([
1343
+ (0, import_common4.Get)("disks/:disk/object"),
1344
+ _ts_param4(0, (0, import_common4.Param)("disk")),
1345
+ _ts_param4(1, (0, import_common4.Query)("key")),
1346
+ _ts_metadata4("design:type", Function),
1347
+ _ts_metadata4("design:paramtypes", [
1697
1348
  String,
1698
1349
  String
1699
1350
  ]),
1700
- _ts_metadata5("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1351
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1701
1352
  ], MediaConsoleReadController.prototype, "object", null);
1702
- _ts_decorate5([
1703
- (0, import_common5.Get)("disks/:disk/object/insights"),
1704
- _ts_param5(0, (0, import_common5.Param)("disk")),
1705
- _ts_param5(1, (0, import_common5.Query)("key")),
1706
- _ts_metadata5("design:type", Function),
1707
- _ts_metadata5("design:paramtypes", [
1353
+ _ts_decorate4([
1354
+ (0, import_common4.Get)("disks/:disk/object/insights"),
1355
+ _ts_param4(0, (0, import_common4.Param)("disk")),
1356
+ _ts_param4(1, (0, import_common4.Query)("key")),
1357
+ _ts_metadata4("design:type", Function),
1358
+ _ts_metadata4("design:paramtypes", [
1708
1359
  String,
1709
1360
  String
1710
1361
  ]),
1711
- _ts_metadata5("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1362
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1712
1363
  ], MediaConsoleReadController.prototype, "objectInsights", null);
1713
- _ts_decorate5([
1714
- (0, import_common5.Get)("disks/:disk/object/raw"),
1715
- _ts_param5(0, (0, import_common5.Param)("disk")),
1716
- _ts_param5(1, (0, import_common5.Query)("key")),
1717
- _ts_param5(2, (0, import_common5.Headers)("range")),
1718
- _ts_param5(3, (0, import_common5.Res)({
1364
+ _ts_decorate4([
1365
+ (0, import_common4.Get)("disks/:disk/object/raw"),
1366
+ _ts_param4(0, (0, import_common4.Param)("disk")),
1367
+ _ts_param4(1, (0, import_common4.Query)("key")),
1368
+ _ts_param4(2, (0, import_common4.Req)()),
1369
+ _ts_param4(3, (0, import_common4.Res)({
1719
1370
  passthrough: true
1720
1371
  })),
1721
- _ts_metadata5("design:type", Function),
1722
- _ts_metadata5("design:paramtypes", [
1372
+ _ts_metadata4("design:type", Function),
1373
+ _ts_metadata4("design:paramtypes", [
1723
1374
  String,
1724
1375
  String,
1725
1376
  Object,
1726
- typeof ConsoleHttpResponse === "undefined" ? Object : ConsoleHttpResponse
1377
+ Object
1727
1378
  ]),
1728
- _ts_metadata5("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1379
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1729
1380
  ], MediaConsoleReadController.prototype, "objectRaw", null);
1730
- _ts_decorate5([
1731
- (0, import_common5.Get)("disks/:disk/object/download"),
1732
- _ts_param5(0, (0, import_common5.Param)("disk")),
1733
- _ts_param5(1, (0, import_common5.Query)("key")),
1734
- _ts_param5(2, (0, import_common5.Headers)("range")),
1735
- _ts_param5(3, (0, import_common5.Res)({
1381
+ _ts_decorate4([
1382
+ (0, import_common4.Get)("disks/:disk/object/download"),
1383
+ _ts_param4(0, (0, import_common4.Param)("disk")),
1384
+ _ts_param4(1, (0, import_common4.Query)("key")),
1385
+ _ts_param4(2, (0, import_common4.Req)()),
1386
+ _ts_param4(3, (0, import_common4.Res)({
1736
1387
  passthrough: true
1737
1388
  })),
1738
- _ts_metadata5("design:type", Function),
1739
- _ts_metadata5("design:paramtypes", [
1389
+ _ts_metadata4("design:type", Function),
1390
+ _ts_metadata4("design:paramtypes", [
1740
1391
  String,
1741
1392
  String,
1742
1393
  Object,
1743
- typeof ConsoleHttpResponse === "undefined" ? Object : ConsoleHttpResponse
1394
+ Object
1744
1395
  ]),
1745
- _ts_metadata5("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1396
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1746
1397
  ], MediaConsoleReadController.prototype, "objectDownload", null);
1747
- _ts_decorate5([
1748
- (0, import_common5.Get)("uploads"),
1749
- _ts_param5(0, (0, import_common5.Query)("disk")),
1750
- _ts_param5(1, (0, import_common5.Query)("prefix")),
1751
- _ts_metadata5("design:type", Function),
1752
- _ts_metadata5("design:paramtypes", [
1398
+ _ts_decorate4([
1399
+ (0, import_common4.Get)("uploads"),
1400
+ _ts_param4(0, (0, import_common4.Query)("disk")),
1401
+ _ts_param4(1, (0, import_common4.Query)("prefix")),
1402
+ _ts_metadata4("design:type", Function),
1403
+ _ts_metadata4("design:paramtypes", [
1753
1404
  String,
1754
1405
  String
1755
1406
  ]),
1756
- _ts_metadata5("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1407
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1757
1408
  ], MediaConsoleReadController.prototype, "uploads", null);
1758
- _ts_decorate5([
1759
- (0, import_common5.Get)("uploads/:id"),
1760
- _ts_param5(0, (0, import_common5.Param)("id")),
1761
- _ts_metadata5("design:type", Function),
1762
- _ts_metadata5("design:paramtypes", [
1409
+ _ts_decorate4([
1410
+ (0, import_common4.Get)("uploads/:id"),
1411
+ _ts_param4(0, (0, import_common4.Param)("id")),
1412
+ _ts_metadata4("design:type", Function),
1413
+ _ts_metadata4("design:paramtypes", [
1763
1414
  String
1764
1415
  ]),
1765
- _ts_metadata5("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1416
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1766
1417
  ], MediaConsoleReadController.prototype, "upload", null);
1767
- _ts_decorate5([
1768
- (0, import_common5.Get)("library/collections"),
1769
- _ts_metadata5("design:type", Function),
1770
- _ts_metadata5("design:paramtypes", []),
1771
- _ts_metadata5("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1418
+ _ts_decorate4([
1419
+ (0, import_common4.Get)("library/collections"),
1420
+ _ts_metadata4("design:type", Function),
1421
+ _ts_metadata4("design:paramtypes", []),
1422
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1772
1423
  ], MediaConsoleReadController.prototype, "collections", null);
1773
- _ts_decorate5([
1774
- (0, import_common5.Get)("library"),
1775
- _ts_param5(0, (0, import_common5.Query)("collection")),
1776
- _ts_param5(1, (0, import_common5.Query)("disk")),
1777
- _ts_param5(2, (0, import_common5.Query)("cursor")),
1778
- _ts_param5(3, (0, import_common5.Query)("limit")),
1779
- _ts_metadata5("design:type", Function),
1780
- _ts_metadata5("design:paramtypes", [
1424
+ _ts_decorate4([
1425
+ (0, import_common4.Get)("library"),
1426
+ _ts_param4(0, (0, import_common4.Query)("collection")),
1427
+ _ts_param4(1, (0, import_common4.Query)("disk")),
1428
+ _ts_param4(2, (0, import_common4.Query)("cursor")),
1429
+ _ts_param4(3, (0, import_common4.Query)("limit")),
1430
+ _ts_metadata4("design:type", Function),
1431
+ _ts_metadata4("design:paramtypes", [
1781
1432
  String,
1782
1433
  String,
1783
1434
  String,
1784
1435
  String
1785
1436
  ]),
1786
- _ts_metadata5("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1437
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1787
1438
  ], MediaConsoleReadController.prototype, "library", null);
1788
- _ts_decorate5([
1789
- (0, import_common5.Get)("library/:id"),
1790
- _ts_param5(0, (0, import_common5.Param)("id")),
1791
- _ts_metadata5("design:type", Function),
1792
- _ts_metadata5("design:paramtypes", [
1439
+ _ts_decorate4([
1440
+ (0, import_common4.Get)("library/:id"),
1441
+ _ts_param4(0, (0, import_common4.Param)("id")),
1442
+ _ts_metadata4("design:type", Function),
1443
+ _ts_metadata4("design:paramtypes", [
1793
1444
  String
1794
1445
  ]),
1795
- _ts_metadata5("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1446
+ _ts_metadata4("design:returntype", typeof Promise === "undefined" ? Object : Promise)
1796
1447
  ], MediaConsoleReadController.prototype, "libraryRecord", null);
1797
- _ts_decorate5([
1798
- (0, import_common5.Get)("topology"),
1799
- _ts_metadata5("design:type", Function),
1800
- _ts_metadata5("design:paramtypes", []),
1801
- _ts_metadata5("design:returntype", typeof Topology === "undefined" ? Object : Topology)
1448
+ _ts_decorate4([
1449
+ (0, import_common4.Get)("topology"),
1450
+ _ts_metadata4("design:type", Function),
1451
+ _ts_metadata4("design:paramtypes", []),
1452
+ _ts_metadata4("design:returntype", typeof Topology === "undefined" ? Object : Topology)
1802
1453
  ], MediaConsoleReadController.prototype, "topology", null);
1803
- MediaConsoleReadController = _ts_decorate5([
1804
- (0, import_common5.UseGuards)(MediaConsoleGuard),
1805
- (0, import_common5.Controller)(),
1806
- _ts_param5(0, (0, import_common5.Inject)(MediaConsoleService)),
1807
- _ts_metadata5("design:type", Function),
1808
- _ts_metadata5("design:paramtypes", [
1454
+ MediaConsoleReadController = _ts_decorate4([
1455
+ OpenToAuthorization(),
1456
+ (0, import_common4.Controller)(),
1457
+ _ts_param4(0, (0, import_common4.Inject)(MediaConsoleService)),
1458
+ _ts_metadata4("design:type", Function),
1459
+ _ts_metadata4("design:paramtypes", [
1809
1460
  typeof MediaConsoleService === "undefined" ? Object : MediaConsoleService
1810
1461
  ])
1811
1462
  ], MediaConsoleReadController);
1812
1463
 
1813
1464
  // src/server/media-console-api.module.ts
1814
- function _ts_decorate6(decorators, target, key, desc) {
1465
+ function _ts_decorate5(decorators, target, key, desc) {
1815
1466
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1816
1467
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
1817
1468
  r = Reflect.decorate(decorators, target, key, desc);
@@ -1824,81 +1475,57 @@ function _ts_decorate6(decorators, target, key, desc) {
1824
1475
  }
1825
1476
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1826
1477
  }
1827
- __name(_ts_decorate6, "_ts_decorate");
1828
- var READ_BASE_GUARDS = baseGuards(MediaConsoleReadController);
1829
- var ACTIONS_BASE_GUARDS = baseGuards(MediaConsoleActionsController);
1478
+ __name(_ts_decorate5, "_ts_decorate");
1830
1479
  var MediaConsoleApiModule = class _MediaConsoleApiModule {
1831
1480
  static {
1832
1481
  __name(this, "MediaConsoleApiModule");
1833
1482
  }
1834
1483
  static register(options) {
1835
- stampGuards(options.guards, [
1836
- [
1837
- MediaConsoleReadController,
1838
- READ_BASE_GUARDS
1839
- ],
1840
- [
1841
- MediaConsoleActionsController,
1842
- ACTIONS_BASE_GUARDS
1843
- ]
1844
- ]);
1845
1484
  return {
1846
1485
  module: _MediaConsoleApiModule,
1847
1486
  imports: options.imports ?? [],
1848
- controllers: [
1849
- MediaConsoleReadController,
1850
- MediaConsoleAuthController,
1487
+ controllers: options.routes === false ? [] : [
1488
+ consoleController(MediaConsoleReadController, options.decorators),
1851
1489
  ...options.actions ? [
1852
- MediaConsoleActionsController
1490
+ consoleController(MediaConsoleActionsController, options.decorators)
1853
1491
  ] : []
1854
1492
  ],
1855
1493
  providers: [
1856
1494
  MediaConsoleService,
1857
- MediaConsoleGuard,
1495
+ consoleAuthorizeProvider(options.authorize, options.moduleName ?? "MediaConsoleApiModule.register()"),
1858
1496
  {
1859
1497
  provide: MEDIA_DASHBOARD_ACTIONS,
1860
1498
  useValue: options.actions
1861
1499
  },
1862
- {
1863
- provide: MEDIA_CONSOLE_COOKIE_PATH,
1864
- useValue: options.cookiePath
1865
- },
1866
1500
  {
1867
1501
  provide: MEDIA_CONSOLE_OBJECT_URLS,
1868
1502
  useValue: options.objectUrls
1869
1503
  },
1870
- options.authProvider,
1871
1504
  options.insightsProvider ?? {
1872
1505
  provide: MEDIA_CONSOLE_OBJECT_INSIGHTS,
1873
1506
  useValue: []
1874
- },
1875
- // Class guards need a DI provider so Nest can instantiate them in THIS module's context
1876
- // (where `imports` above resolves their dependencies). An already-instantiated guard needs
1877
- // none — it's used as-is.
1878
- ...(options.guards ?? []).filter(isGuardClass)
1507
+ }
1879
1508
  ],
1880
- // MEDIA_CONSOLE_AUTH is exported (not just provided) because MediaDashboardUiController —
1881
- // hosted in MediaDashboardModule, which imports THIS module — reads it to decide whether to
1882
- // hand an unauthenticated navigation to the host's `unauthenticatedPage`. Exporting keeps the
1883
- // single `authProvider` instance shared; re-providing the same factory in both modules would
1884
- // run a `forRootAsync` host's `useAuth` twice.
1509
+ // For a host mounting the controllers itself (`routes: false`): its subclasses inject these, and
1510
+ // `ConsoleAuthorizeGuard` injects the resolved `authorize` check.
1885
1511
  exports: [
1886
1512
  MediaConsoleService,
1887
- MEDIA_CONSOLE_AUTH
1513
+ MEDIA_DASHBOARD_ACTIONS,
1514
+ CONSOLE_AUTHORIZE_DECIDE
1888
1515
  ]
1889
1516
  };
1890
1517
  }
1891
1518
  };
1892
- MediaConsoleApiModule = _ts_decorate6([
1893
- (0, import_common6.Module)({})
1519
+ MediaConsoleApiModule = _ts_decorate5([
1520
+ (0, import_common5.Module)({})
1894
1521
  ], MediaConsoleApiModule);
1895
1522
 
1896
1523
  // src/server/media-dashboard-ui.controller.ts
1897
1524
  var import_node_fs = require("fs");
1898
1525
  var import_node_path = require("path");
1899
1526
  var import_node_url = require("url");
1900
- var import_common7 = require("@nestjs/common");
1901
- function _ts_decorate7(decorators, target, key, desc) {
1527
+ var import_common6 = require("@nestjs/common");
1528
+ function _ts_decorate6(decorators, target, key, desc) {
1902
1529
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1903
1530
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
1904
1531
  r = Reflect.decorate(decorators, target, key, desc);
@@ -1911,19 +1538,19 @@ function _ts_decorate7(decorators, target, key, desc) {
1911
1538
  }
1912
1539
  return c > 3 && r && Object.defineProperty(target, key, r), r;
1913
1540
  }
1914
- __name(_ts_decorate7, "_ts_decorate");
1915
- function _ts_metadata6(metadataKey, metadataValue) {
1541
+ __name(_ts_decorate6, "_ts_decorate");
1542
+ function _ts_metadata5(metadataKey, metadataValue) {
1916
1543
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") {
1917
1544
  return Reflect.metadata(metadataKey, metadataValue);
1918
1545
  }
1919
1546
  }
1920
- __name(_ts_metadata6, "_ts_metadata");
1921
- function _ts_param6(paramIndex, decorator) {
1547
+ __name(_ts_metadata5, "_ts_metadata");
1548
+ function _ts_param5(paramIndex, decorator) {
1922
1549
  return function(target, key) {
1923
1550
  decorator(target, key, paramIndex);
1924
1551
  };
1925
1552
  }
1926
- __name(_ts_param6, "_ts_param");
1553
+ __name(_ts_param5, "_ts_param");
1927
1554
  var BUILD_BASE = "/media";
1928
1555
  function spaDir() {
1929
1556
  return (0, import_node_url.fileURLToPath)(new URL("../spa", importMetaUrl));
@@ -1938,129 +1565,75 @@ var CONTENT_TYPES = {
1938
1565
  ".woff2": "font/woff2",
1939
1566
  ".ico": "image/x-icon"
1940
1567
  };
1941
- var MediaDashboardUiController = class _MediaDashboardUiController {
1568
+ var MediaDashboardUiController = class {
1942
1569
  static {
1943
1570
  __name(this, "MediaDashboardUiController");
1944
1571
  }
1945
1572
  basePath;
1946
1573
  apiBasePath;
1947
- auth;
1948
1574
  dir = spaDir();
1949
- logger = new import_common7.Logger(_MediaDashboardUiController.name);
1950
- warned = false;
1951
- constructor(basePath, apiBasePath, auth = null) {
1575
+ constructor(basePath, apiBasePath) {
1952
1576
  this.basePath = basePath;
1953
1577
  this.apiBasePath = apiBasePath;
1954
- this.auth = auth;
1955
1578
  }
1956
1579
  // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = "stuck
1957
- // loading after a deploy"). The hashed assets below are immutable. `sendHtml` sets both headers;
1958
- // they are no longer `@Header` decorators because this route is now non-passthrough (see below).
1959
- //
1960
- // Non-passthrough `@Res()` so a host's `auth.unauthenticatedPage` can OWN the response and serve
1961
- // its own page in place of the SPA. Without a hook this behaves exactly as before: the shell is
1962
- // served to everyone, and the SPA's own auth screen renders once its API calls come back 401.
1963
- async index(req, res) {
1964
- if (await this.serveUnauthenticatedPage(req, res)) return;
1580
+ // loading after a deploy"). The hashed assets below are immutable.
1581
+ index() {
1965
1582
  const indexPath = (0, import_node_path.join)(this.dir, "index.html");
1966
1583
  if (!(0, import_node_fs.existsSync)(indexPath)) {
1967
- throw new import_common7.NotFoundException("Console SPA is not built. Run the package build.");
1584
+ throw new import_common6.NotFoundException("Console SPA is not built. Run the package build.");
1968
1585
  }
1969
1586
  const html = (0, import_node_fs.readFileSync)(indexPath, "utf8").replaceAll(`="${BUILD_BASE}/`, `="${this.basePath}/`);
1970
1587
  const inject = `<script>window.__MEDIA_BASE__='${this.basePath}';window.__MEDIA_API__='${this.apiBasePath}';</script>`;
1971
- sendHtml(res, 200, html.includes("</head>") ? html.replace("</head>", `${inject}</head>`) : inject + html);
1972
- }
1973
- /**
1974
- * Hand the response to the host's `unauthenticatedPage` when this navigation has no valid session.
1975
- * Returns `true` when the host wrote a page (the caller must not also serve the SPA).
1976
- *
1977
- * Mode-A-only, deliberately: with `login` configured the SPA's own login form is the way in, so
1978
- * replacing the shell would lock a Mode B host out of its own console.
1979
- */
1980
- async serveUnauthenticatedPage(req, res) {
1981
- const auth = this.auth;
1982
- const hook = auth?.unauthenticatedPage;
1983
- if (!auth || !hook) return false;
1984
- if (auth.modes.includes("login")) return false;
1985
- if (this.hasValidSession(auth, req)) return false;
1986
- try {
1987
- await hook({
1988
- request: req,
1989
- response: res,
1990
- basePath: this.basePath
1991
- });
1992
- if (responseAlreadyWritten(res)) return true;
1993
- this.warnOnce("auth.unauthenticatedPage returned without writing a response; serving the console SPA instead. The hook must write AND end the response it is given.");
1994
- } catch (error) {
1995
- this.warnOnce(`auth.unauthenticatedPage threw; serving the console SPA instead. ${String(error)}`);
1996
- if (responseAlreadyWritten(res)) return true;
1997
- }
1998
- return false;
1999
- }
2000
- hasValidSession(auth, req) {
2001
- const cookieValue = parseCookieHeader(readCookieHeader(req))[SESSION_COOKIE_NAME];
2002
- if (cookieValue === void 0) return false;
2003
- return verifySessionCookie(cookieValue, {
2004
- secret: auth.secret
2005
- }) !== null;
2006
- }
2007
- /** Once per process, not once per request: a broken hook fires on every unauthenticated visit. */
2008
- warnOnce(message) {
2009
- if (this.warned) return;
2010
- this.warned = true;
2011
- this.logger.warn(message);
1588
+ return html.includes("</head>") ? html.replace("</head>", `${inject}</head>`) : inject + html;
2012
1589
  }
2013
1590
  asset(file) {
2014
1591
  const safe = (0, import_node_path.basename)(file);
2015
- if (safe !== file) throw new import_common7.NotFoundException();
1592
+ if (safe !== file) throw new import_common6.NotFoundException();
2016
1593
  const root = (0, import_node_path.resolve)(this.dir, "assets");
2017
1594
  const assetPath = (0, import_node_path.resolve)(root, safe);
2018
1595
  if (!assetPath.startsWith(root + import_node_path.sep) || !(0, import_node_fs.existsSync)(assetPath)) {
2019
- throw new import_common7.NotFoundException();
1596
+ throw new import_common6.NotFoundException();
2020
1597
  }
2021
1598
  const type = CONTENT_TYPES[(0, import_node_path.extname)(safe)] ?? "application/octet-stream";
2022
- return new import_common7.StreamableFile((0, import_node_fs.readFileSync)(assetPath), {
1599
+ return new import_common6.StreamableFile((0, import_node_fs.readFileSync)(assetPath), {
2023
1600
  type
2024
1601
  });
2025
1602
  }
2026
1603
  };
2027
- _ts_decorate7([
2028
- (0, import_common7.Get)(),
2029
- _ts_param6(0, (0, import_common7.Req)()),
2030
- _ts_param6(1, (0, import_common7.Res)()),
2031
- _ts_metadata6("design:type", Function),
2032
- _ts_metadata6("design:paramtypes", [
2033
- Object,
2034
- Object
2035
- ]),
2036
- _ts_metadata6("design:returntype", Promise)
1604
+ _ts_decorate6([
1605
+ (0, import_common6.Get)(),
1606
+ (0, import_common6.Header)("Content-Type", "text/html; charset=utf-8"),
1607
+ (0, import_common6.Header)("Cache-Control", "no-store, must-revalidate"),
1608
+ _ts_metadata5("design:type", Function),
1609
+ _ts_metadata5("design:paramtypes", []),
1610
+ _ts_metadata5("design:returntype", String)
2037
1611
  ], MediaDashboardUiController.prototype, "index", null);
2038
- _ts_decorate7([
2039
- (0, import_common7.Get)("assets/:file"),
2040
- (0, import_common7.Header)("Cache-Control", "public, max-age=31536000, immutable"),
2041
- _ts_param6(0, (0, import_common7.Param)("file")),
2042
- _ts_metadata6("design:type", Function),
2043
- _ts_metadata6("design:paramtypes", [
1612
+ _ts_decorate6([
1613
+ (0, import_common6.Get)("assets/:file"),
1614
+ (0, import_common6.Header)("Cache-Control", "public, max-age=31536000, immutable"),
1615
+ _ts_param5(0, (0, import_common6.Param)("file")),
1616
+ _ts_metadata5("design:type", Function),
1617
+ _ts_metadata5("design:paramtypes", [
2044
1618
  String
2045
1619
  ]),
2046
- _ts_metadata6("design:returntype", typeof import_common7.StreamableFile === "undefined" ? Object : import_common7.StreamableFile)
1620
+ _ts_metadata5("design:returntype", typeof import_common6.StreamableFile === "undefined" ? Object : import_common6.StreamableFile)
2047
1621
  ], MediaDashboardUiController.prototype, "asset", null);
2048
- MediaDashboardUiController = _ts_decorate7([
2049
- (0, import_common7.Controller)(),
2050
- _ts_param6(0, (0, import_common7.Inject)(MEDIA_DASHBOARD_BASE_PATH)),
2051
- _ts_param6(1, (0, import_common7.Inject)(MEDIA_DASHBOARD_API_PATH)),
2052
- _ts_param6(2, (0, import_common7.Optional)()),
2053
- _ts_param6(2, (0, import_common7.Inject)(MEDIA_CONSOLE_AUTH)),
2054
- _ts_metadata6("design:type", Function),
2055
- _ts_metadata6("design:paramtypes", [
2056
- String,
1622
+ MediaDashboardUiController = _ts_decorate6([
1623
+ OpenToAuthorization(),
1624
+ ConsolePage(),
1625
+ (0, import_common6.Controller)(),
1626
+ _ts_param5(0, (0, import_common6.Inject)(MEDIA_DASHBOARD_BASE_PATH)),
1627
+ _ts_param5(1, (0, import_common6.Inject)(MEDIA_DASHBOARD_API_PATH)),
1628
+ _ts_metadata5("design:type", Function),
1629
+ _ts_metadata5("design:paramtypes", [
2057
1630
  String,
2058
- Object
1631
+ String
2059
1632
  ])
2060
1633
  ], MediaDashboardUiController);
2061
1634
 
2062
1635
  // src/server/media-dashboard.module.ts
2063
- function _ts_decorate8(decorators, target, key, desc) {
1636
+ function _ts_decorate7(decorators, target, key, desc) {
2064
1637
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
2065
1638
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") {
2066
1639
  r = Reflect.decorate(decorators, target, key, desc);
@@ -2073,7 +1646,7 @@ function _ts_decorate8(decorators, target, key, desc) {
2073
1646
  }
2074
1647
  return c > 3 && r && Object.defineProperty(target, key, r), r;
2075
1648
  }
2076
- __name(_ts_decorate8, "_ts_decorate");
1649
+ __name(_ts_decorate7, "_ts_decorate");
2077
1650
  function normalize(path) {
2078
1651
  return `/${path.replace(/^\/+|\/+$/g, "")}`;
2079
1652
  }
@@ -2083,39 +1656,25 @@ var MediaDashboardModule = class _MediaDashboardModule {
2083
1656
  __name(this, "MediaDashboardModule");
2084
1657
  }
2085
1658
  static forRoot(options = {}) {
2086
- const apiBasePath = normalize(options.apiBasePath ?? `${normalize(options.basePath ?? "/media")}/api`);
2087
- return _MediaDashboardModule.build(options, apiBasePath, {
2088
- provide: MEDIA_CONSOLE_AUTH,
2089
- useValue: resolveConsoleAuth(options.auth)
2090
- }, {
1659
+ return _MediaDashboardModule.build("MediaDashboardModule.forRoot()", options, {
2091
1660
  provide: MEDIA_CONSOLE_OBJECT_INSIGHTS,
2092
1661
  useValue: options.objectInsights ?? []
2093
- }, options.imports);
1662
+ });
2094
1663
  }
2095
- static forRootAsync(options) {
2096
- const apiBasePath = normalize(options.apiBasePath ?? `${normalize(options.basePath ?? "/media")}/api`);
2097
- const authProvider = {
2098
- provide: MEDIA_CONSOLE_AUTH,
2099
- inject: options.inject ?? [],
2100
- useFactory: /* @__PURE__ */ __name(async (...deps) => resolveConsoleAuth(await options.useAuth(...deps)), "useFactory")
2101
- };
2102
- const insightsProvider = {
1664
+ static forRootAsync(options = {}) {
1665
+ return _MediaDashboardModule.build("MediaDashboardModule.forRootAsync()", options, {
2103
1666
  provide: MEDIA_CONSOLE_OBJECT_INSIGHTS,
2104
- inject: options.injectObjectInsights ?? options.inject ?? [],
1667
+ inject: options.inject ?? [],
2105
1668
  useFactory: /* @__PURE__ */ __name(async (...deps) => options.useObjectInsights === void 0 ? [] : await options.useObjectInsights(...deps), "useFactory")
2106
- };
2107
- return _MediaDashboardModule.build(options, apiBasePath, authProvider, insightsProvider, options.imports);
1669
+ });
2108
1670
  }
2109
- /** Shared wiring: static routing + the API module, with `auth` supplied by the given provider. */
2110
- static build(options, apiBasePath, authProvider, insightsProvider, imports) {
1671
+ /** Shared wiring: static routing + the API module, with the insights from the given provider. */
1672
+ static build(name, options, insightsProvider) {
2111
1673
  const basePath = normalize(options.basePath ?? "/media");
1674
+ const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);
2112
1675
  const actions = options.actions === true;
2113
- stampGuards(options.guards, [
2114
- [
2115
- MediaDashboardUiController,
2116
- []
2117
- ]
2118
- ]);
1676
+ const mount = options.routes !== false;
1677
+ const imports = options.imports;
2119
1678
  return {
2120
1679
  module: _MediaDashboardModule,
2121
1680
  imports: [
@@ -2126,38 +1685,33 @@ var MediaDashboardModule = class _MediaDashboardModule {
2126
1685
  strategy: options.objectUrls ?? "auto",
2127
1686
  apiBasePath
2128
1687
  },
2129
- // `Path=/`, deliberately NOT scoped to `apiBasePath` (nor to `basePath`): the two mounts
2130
- // are independently configurable and can live at unrelated paths (e.g. a `/media` UI
2131
- // with a `/api/media/console` API). Scoped to the API base, the browser withheld the
2132
- // cookie on the full-page navigation to `basePath`, so the freshly minted session could
2133
- // not open the console it was minted for — the launcher landed on the unauthenticated
2134
- // page. Scoping to `basePath` instead would just move the failure to the API calls.
2135
- // The cookie is HttpOnly + signed + short-lived, so the broader scope is a reasonable
2136
- // trade for being correct in any mount configuration. Matches nestjs-durable-dashboard
2137
- // and nestjs-agent-dashboard; telescope can scope to its mount because its UI and API
2138
- // always share one root.
2139
- cookiePath: "/",
2140
- authProvider,
2141
1688
  insightsProvider,
2142
1689
  imports,
2143
- ...options.guards ? {
2144
- guards: options.guards
2145
- } : {}
1690
+ ...options.authorize !== void 0 ? {
1691
+ authorize: options.authorize
1692
+ } : {},
1693
+ moduleName: name,
1694
+ ...options.decorators ? {
1695
+ decorators: options.decorators
1696
+ } : {},
1697
+ routes: mount
2146
1698
  }),
2147
- import_core.RouterModule.register([
2148
- {
2149
- path: basePath,
2150
- module: _MediaDashboardModule
2151
- },
2152
- {
2153
- path: apiBasePath,
2154
- module: MediaConsoleApiModule
2155
- }
2156
- ])
2157
- ],
2158
- controllers: [
2159
- MediaDashboardUiController
1699
+ ...mount ? [
1700
+ import_core2.RouterModule.register([
1701
+ {
1702
+ path: basePath,
1703
+ module: _MediaDashboardModule
1704
+ },
1705
+ {
1706
+ path: apiBasePath,
1707
+ module: MediaConsoleApiModule
1708
+ }
1709
+ ])
1710
+ ] : []
2160
1711
  ],
1712
+ controllers: mount ? [
1713
+ consoleController(MediaDashboardUiController, options.decorators)
1714
+ ] : [],
2161
1715
  providers: [
2162
1716
  {
2163
1717
  provide: MEDIA_DASHBOARD_BASE_PATH,
@@ -2166,28 +1720,44 @@ var MediaDashboardModule = class _MediaDashboardModule {
2166
1720
  {
2167
1721
  provide: MEDIA_DASHBOARD_API_PATH,
2168
1722
  useValue: apiBasePath
2169
- },
2170
- // MediaDashboardUiController is hosted HERE, so its guards DI-instantiate from this
2171
- // module — class guards need a provider; an already-instantiated guard needs none.
2172
- ...(options.guards ?? []).filter(isGuardClass)
1723
+ }
2173
1724
  ],
2174
- // Re-export the API module so its MediaConsoleService reaches importers if they want it.
1725
+ // Re-export the API module so its MediaConsoleService reaches importers if they want it; the
1726
+ // resolved paths too, so a host mounting the controllers itself (`routes: false`) can extend
1727
+ // `MediaDashboardUiController` in a module of its own.
2175
1728
  exports: [
2176
- MediaConsoleApiModule
1729
+ MediaConsoleApiModule,
1730
+ MEDIA_DASHBOARD_BASE_PATH,
1731
+ MEDIA_DASHBOARD_API_PATH
2177
1732
  ]
2178
1733
  };
2179
1734
  }
2180
1735
  };
2181
- MediaDashboardModule = _ts_decorate8([
2182
- (0, import_common8.Module)({})
1736
+ MediaDashboardModule = _ts_decorate7([
1737
+ (0, import_common7.Module)({})
2183
1738
  ], MediaDashboardModule);
2184
1739
  // Annotate the CommonJS export names for ESM import in node:
2185
1740
  0 && (module.exports = {
2186
- MEDIA_STORAGE_SHARED,
1741
+ CONSOLE_AUTHORIZE_DECIDE,
1742
+ CONSOLE_AUTHORIZE_MISSING_WARNING,
1743
+ CONSOLE_GATE,
1744
+ CONSOLE_PAGE_METADATA,
1745
+ ConsoleAccessDenied,
1746
+ ConsoleAuthorizeFilter,
1747
+ ConsoleAuthorizeGuard,
1748
+ ConsolePage,
1749
+ MEDIA_MULTIPART,
2187
1750
  MEDIA_STORE,
2188
1751
  MEDIA_UPLOAD_SESSIONS,
1752
+ MediaConsoleActionsController,
2189
1753
  MediaConsoleApiModule,
1754
+ MediaConsoleReadController,
2190
1755
  MediaConsoleService,
2191
- MediaDashboardModule
1756
+ MediaDashboardModule,
1757
+ MediaDashboardUiController,
1758
+ UseConsoleAuthorize,
1759
+ authorizeByToken,
1760
+ normalizeConsoleResult,
1761
+ resolveConsoleAuthorize
2192
1762
  });
2193
1763
  //# sourceMappingURL=index.cjs.map