@dudousxd/nestjs-media-dashboard 0.16.0 → 0.16.2

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/server/index.ts","../../../../node_modules/.pnpm/tsup@8.3.5_@swc+core@1.15.41_jiti@1.21.7_postcss@8.4.49_typescript@5.9.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js","../../src/server/media-dashboard.module.ts","../../src/server/auth/config.ts","../../src/server/guards.ts","../../src/server/media-console-api.module.ts","../../src/server/media-console-actions.controller.ts","../../src/server/media-console.guard.ts","../../src/server/auth/cookie-header.ts","../../src/server/auth/request.ts","../../src/server/auth/response.ts","../../src/server/auth/session-cookie.ts","../../src/server/auth/session-cookie-io.ts","../../src/server/tokens.ts","../../src/server/media-console.service.ts","../../src/server/object-insights.ts","../../src/server/object-urls.ts","../../src/server/media-console-auth.controller.ts","../../src/server/media-console-read.controller.ts","../../src/server/content-disposition.ts","../../src/server/media-dashboard-ui.controller.ts"],"sourcesContent":["export {\n MediaDashboardModule,\n type MediaDashboardOptions,\n type MediaDashboardAsyncOptions,\n} from './media-dashboard.module.js';\nexport { MediaConsoleApiModule } from './media-console-api.module.js';\nexport type { ObjectUrlConfig, ObjectUrlStrategy } from './object-urls.js';\nexport { MediaConsoleService } from './media-console.service.js';\nexport type {\n ConsoleAuthOptions,\n LoginHook,\n SessionHook,\n} from './auth/config.js';\nexport type { ConsoleSessionUser } from './auth/session-cookie.js';\nexport {\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOAD_SESSIONS,\n} from './tokens.js';\n// The API response types (also published at the `./client` entry) for host reuse.\nexport type * from '../client/types.js';\nexport type {\n ObjectInsight,\n ObjectInsightContext,\n ObjectInsightFact,\n ObjectInsightLink,\n ObjectInsightProvider,\n ObjectInsightsResponse,\n} from './object-insights.js';\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () =>\n typeof document === 'undefined'\n ? new URL(`file:${__filename}`).href\n : (document.currentScript && document.currentScript.src) ||\n new URL('main.js', document.baseURI).href\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import {\n type CanActivate,\n type DynamicModule,\n Module,\n type ModuleMetadata,\n type OptionalFactoryDependency,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport type { InjectionToken } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { type ConsoleAuthOptions, resolveConsoleAuth } from './auth/config.js';\nimport { isGuardClass, stampGuards } from './guards.js';\nimport { MediaConsoleApiModule } from './media-console-api.module.js';\nimport { MediaDashboardUiController } from './media-dashboard-ui.controller.js';\nimport type { ObjectInsightProvider } from './object-insights.js';\nimport type { ObjectUrlStrategy } from './object-urls.js';\nimport {\n MEDIA_CONSOLE_AUTH,\n MEDIA_CONSOLE_OBJECT_INSIGHTS,\n MEDIA_DASHBOARD_API_PATH,\n MEDIA_DASHBOARD_BASE_PATH,\n} from './tokens.js';\n\nexport interface MediaDashboardOptions {\n /**\n * Where the SPA (UI) is served. Default `/media`. A page route — keep it out of an `/api`\n * prefix so it reads as a UI, not an endpoint.\n */\n basePath?: string;\n /**\n * Where the JSON API is mounted (what the SPA fetches). Default `<basePath>/api`. Set it under\n * your app's `/api` prefix — e.g. `/api/media/console` — so the API inherits the app's\n * auth/proxy rules while the UI stays at `basePath`.\n */\n apiBasePath?: string;\n /**\n * Enable the destructive endpoints (delete object/record, copy/move, abort upload). Default\n * `false` — the read API is always available. Front the mount with your own guard either way.\n */\n actions?: boolean;\n /**\n * How the console reports the `url` for an object's bytes — the \"Open ↗\" link, and an image\n * variant's `src`. Default `'auto'`: presigned straight to the object store when the driver can\n * mint one. Set `'proxy'` when the browser has no path to the store (no network route, no CORS\n * grant, a policy against client-to-bucket traffic), and those URLs are routed same-origin\n * through this server instead. See {@link ObjectUrlStrategy}.\n *\n * Downloading is unaffected: `disks/:disk/object/download` is always same-origin.\n */\n objectUrls?: ObjectUrlStrategy;\n /**\n * Gate the console (SPA + API) behind a built-in session-cookie login, telescope-style. Omit to\n * leave the console open (front it with your own guard). When set, the SPA renders a login screen\n * until a valid cookie exists; supply a `login(username, password)` and/or `session(request)`\n * hook that returns a session user (or `null` to deny), and optionally a `revalidate(session)`\n * hook that re-checks the user when the cookie is slid forward — see {@link ConsoleAuthOptions}.\n */\n auth?: ConsoleAuthOptions;\n /**\n * Guard classes (or already-instantiated `CanActivate`s) fronting BOTH the page/asset controller\n * (`MediaDashboardUiController` — a plain REPLACE, since it ships with no guard of its own) and\n * the read/action JSON API controllers (APPENDED to their own built-in `MediaConsoleGuard`\n * session-cookie gate — a request must pass BOTH). Deliberately NOT applied to the auth\n * controller that MINTS that session cookie — see `MediaConsoleApiModule`'s own `guards` doc.\n *\n * These two surfaces (page vs. API) live in the same package here, unlike a split dashboard —\n * but they're still SEPARATE controllers on separate host modules, so `guards` is stamped onto\n * each independently by this one option.\n *\n * Full-page navigations to `basePath` carry only cookies, never an `Authorization` header — a\n * guard passed here must be able to authenticate from a cookie (see the \"Securing the console\"\n * guide) or it will 401 every browser visit to the console, even an already-logged-in admin's.\n *\n * `guards` and `auth` (the built-in cookie login) are independent and compose: with both set, a\n * request must pass the host guard AND (when a route carries `MediaConsoleGuard`) present a valid\n * built-in session cookie too. Configure one, the other, or both.\n *\n * A guard's own DEPENDENCIES resolve from this option's `imports` (see {@link imports}) — the\n * dashboard module has no application context of its own to pull them from otherwise.\n */\n guards?: Array<Type<CanActivate> | CanActivate>;\n /**\n * Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for a class\n * passed to {@link guards} (or any other provider the controllers need reachable). Typically the\n * host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [SessionGuard]`.\n */\n imports?: DynamicModule['imports'];\n /**\n * Host-supplied context about a disk object, rendered in the console's preview — see\n * {@link ObjectInsightProvider}. The console can only describe a file as storage sees it; this is\n * how it learns what the file *means* to your app. Omit for no annotation (the default).\n *\n * Use `forRootAsync`'s `useObjectInsights` when a provider needs injected services.\n */\n objectInsights?: ObjectInsightProvider[];\n}\n\n/**\n * Async variant of {@link MediaDashboardOptions}. The mount paths + `actions` stay static (the\n * router needs them at module-definition time), but `auth` is built by an injected factory — so\n * the `login`/`session` hooks can reach your DB/services (e.g. an EntityManager to validate an\n * admin). Returning `undefined` from the factory leaves the console open.\n */\nexport interface MediaDashboardAsyncOptions {\n basePath?: string;\n apiBasePath?: string;\n actions?: boolean;\n /** See {@link MediaDashboardOptions.objectUrls}. Static like the mount paths and `actions` —\n * it is read at module-definition time, not per request. */\n objectUrls?: ObjectUrlStrategy;\n /** Modules exporting the providers `inject` needs, or a guard class's own dependencies (omit\n * when they're global). Shared with {@link guards}. */\n imports?: DynamicModule['imports'];\n /** Providers injected into `useAuth`, in order. */\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\n /** Build the `auth` config from injected deps (or `undefined` to leave the console open). */\n useAuth: (\n ...deps: any[]\n ) => ConsoleAuthOptions | undefined | Promise<ConsoleAuthOptions | undefined>;\n /**\n * Guard classes (or instances) fronting the console — see {@link MediaDashboardOptions.guards}.\n * Passed here directly (not resolved via `useAuth`'s factory) because guard stamping happens at\n * module-build time, synchronously — the SAME constraint as `basePath`/`apiBasePath` above. A\n * class guard's own dependencies resolve from `imports` above (shared with `useAuth`'s `inject`).\n */\n guards?: Array<Type<CanActivate> | CanActivate>;\n /**\n * Build the {@link MediaDashboardOptions.objectInsights} providers from injected deps — the usual\n * case, since a provider that says anything interesting needs a repository or a service.\n */\n useObjectInsights?: (\n ...deps: any[]\n ) => ObjectInsightProvider[] | Promise<ObjectInsightProvider[]>;\n /**\n * Providers injected into `useObjectInsights`, in order. Defaults to {@link inject} — its own\n * list because the insight providers rarely want the same dependencies the auth hooks do, and\n * sharing one list would force every host to inject the union of both.\n */\n injectObjectInsights?: Array<InjectionToken | OptionalFactoryDependency>;\n}\n\n/** Leading slash, no trailing slash. */\nfunction normalize(path: string): string {\n return `/${path.replace(/^\\/+|\\/+$/g, '')}`;\n}\n\n/**\n * Mounts the /media console: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`\n * (default `<basePath>/api`). Import via `MediaDashboardModule.forRoot(...)` (or `forRootAsync` when\n * the `auth` hooks need injected services) alongside `MediaModule` (global), so it resolves the\n * storage/store/upload tokens. Exclude the UI/API paths from any global `/api` prefix.\n *\n * Front it with your own auth via `guards` (+ `imports` for their dependencies) — see\n * {@link MediaDashboardOptions.guards} — instead of, or alongside, the built-in `auth` cookie login.\n */\n@Module({})\nexport class MediaDashboardModule {\n static forRoot(options: MediaDashboardOptions = {}): DynamicModule {\n const apiBasePath = normalize(\n options.apiBasePath ?? `${normalize(options.basePath ?? '/media')}/api`,\n );\n return MediaDashboardModule.build(\n options,\n apiBasePath,\n { provide: MEDIA_CONSOLE_AUTH, useValue: resolveConsoleAuth(options.auth) },\n { provide: MEDIA_CONSOLE_OBJECT_INSIGHTS, useValue: options.objectInsights ?? [] },\n options.imports,\n );\n }\n\n static forRootAsync(options: MediaDashboardAsyncOptions): DynamicModule {\n const apiBasePath = normalize(\n options.apiBasePath ?? `${normalize(options.basePath ?? '/media')}/api`,\n );\n const authProvider: Provider = {\n provide: MEDIA_CONSOLE_AUTH,\n inject: options.inject ?? [],\n useFactory: async (...deps: any[]) => resolveConsoleAuth(await options.useAuth(...deps)),\n };\n const insightsProvider: Provider = {\n provide: MEDIA_CONSOLE_OBJECT_INSIGHTS,\n inject: options.injectObjectInsights ?? options.inject ?? [],\n useFactory: async (...deps: any[]) =>\n options.useObjectInsights === undefined ? [] : await options.useObjectInsights(...deps),\n };\n return MediaDashboardModule.build(\n options,\n apiBasePath,\n authProvider,\n insightsProvider,\n options.imports,\n );\n }\n\n /** Shared wiring: static routing + the API module, with `auth` supplied by the given provider. */\n private static build(\n options: {\n basePath?: string;\n apiBasePath?: string;\n actions?: boolean;\n objectUrls?: ObjectUrlStrategy;\n guards?: Array<Type<CanActivate> | CanActivate>;\n },\n apiBasePath: string,\n authProvider: Provider,\n insightsProvider: Provider,\n imports?: DynamicModule['imports'],\n ): DynamicModule {\n const basePath = normalize(options.basePath ?? '/media');\n const actions = options.actions === true;\n // MediaDashboardUiController ships with no guard of its own, so this is a plain REPLACE (base\n // `[]`) — matches `MediaConsoleApiModule`'s own `stampGuards` call for the read/action\n // controllers, which DOES have a built-in base to append onto.\n stampGuards(options.guards, [[MediaDashboardUiController, []]]);\n return {\n module: MediaDashboardModule,\n imports: [\n ...(imports ?? []),\n MediaConsoleApiModule.register({\n actions,\n objectUrls: { strategy: options.objectUrls ?? 'auto', apiBasePath },\n // `Path=/`, deliberately NOT scoped to `apiBasePath` (nor to `basePath`): the two mounts\n // are independently configurable and can live at unrelated paths (e.g. a `/media` UI\n // with a `/api/media/console` API). Scoped to the API base, the browser withheld the\n // cookie on the full-page navigation to `basePath`, so the freshly minted session could\n // not open the console it was minted for — the launcher landed on the unauthenticated\n // page. Scoping to `basePath` instead would just move the failure to the API calls.\n // The cookie is HttpOnly + signed + short-lived, so the broader scope is a reasonable\n // trade for being correct in any mount configuration. Matches nestjs-durable-dashboard\n // and nestjs-agent-dashboard; telescope can scope to its mount because its UI and API\n // always share one root.\n cookiePath: '/',\n authProvider,\n insightsProvider,\n imports,\n ...(options.guards ? { guards: options.guards } : {}),\n }),\n RouterModule.register([\n { path: basePath, module: MediaDashboardModule }, // the UI controller below\n { path: apiBasePath, module: MediaConsoleApiModule },\n ]),\n ],\n controllers: [MediaDashboardUiController],\n providers: [\n { provide: MEDIA_DASHBOARD_BASE_PATH, useValue: basePath },\n { provide: MEDIA_DASHBOARD_API_PATH, useValue: apiBasePath },\n // MediaDashboardUiController is hosted HERE, so its guards DI-instantiate from this\n // module — class guards need a provider; an already-instantiated guard needs none.\n ...(options.guards ?? []).filter(isGuardClass),\n ],\n // Re-export the API module so its MediaConsoleService reaches importers if they want it.\n exports: [MediaConsoleApiModule],\n };\n }\n}\n","import type { ConsoleSessionUser } from './session-cookie.js';\n\n/** Host hook for Mode A — validates the host's own auth on the raw request. */\nexport type SessionHook = (\n request: unknown,\n) => Promise<ConsoleSessionUser | null> | ConsoleSessionUser | null;\n\n/** Host hook for Mode B — validates submitted credentials from the built-in login screen. */\nexport type LoginHook = (\n username: string,\n password: string,\n) => Promise<ConsoleSessionUser | null> | ConsoleSessionUser | null;\n\n/**\n * Re-checks a LIVE session when the cookie is slid forward. Runs at most once per `ttl/2` per\n * session, so a DB round-trip here is cheap. Return `false` to revoke (the cookie is cleared and\n * the request denied). Distinct from `session`: that hook reads the host's auth off a fresh\n * request, which a console XHR does not carry — this one receives the already-minted session.\n */\nexport type RevalidateHook = (session: ConsoleSessionUser) => Promise<boolean> | boolean;\n\n/** What `unauthenticatedPage` receives. An object (not positional args) so fields can be added later. */\nexport interface UnauthenticatedPageContext {\n /**\n * The platform-native request — Express' `Request`, Fastify's `FastifyRequest`. Typed `unknown`\n * for the same reason `SessionHook`'s is: this package refuses to depend on either platform.\n */\n request: unknown;\n /**\n * The platform-native response. The hook OWNS it: it must write AND end it. If it returns without\n * writing, the console falls back to serving its own SPA (whose auth screen then renders).\n */\n response: unknown;\n /** Where this console is mounted (e.g. `/media`) — useful for a \"back to the console\" link. */\n basePath: string;\n}\n\n/**\n * Host-owned page for an unauthenticated navigation to the console SPA.\n *\n * Without it, an unauthenticated visitor gets the SPA shell, which then renders the built-in auth\n * screen: a generic \"open this console from your application\" card, generic because the library\n * cannot know who hosts it. This hook replaces that with the host's own page — and, because it is\n * decided BEFORE the shell is served, it also stops the bundle from loading for a visitor who has\n * no session at all.\n *\n * Only consulted under Mode-A-only. With `login` configured the SPA's own login form IS the way in,\n * so gating the shell would lock Mode B hosts out of their own console.\n *\n * Fail-closed by construction: it only ever runs when the request has no valid session. A hook that\n * throws, or returns without writing, falls back to serving the SPA — it cannot let anyone in,\n * because every data route stays behind `MediaConsoleGuard` regardless.\n */\nexport type UnauthenticatedPageHook = (context: UnauthenticatedPageContext) => void | Promise<void>;\n\n/** Author-facing `auth` option on `MediaDashboardModule.forRoot`. Mirrors telescope's dashboardAuth. */\nexport interface ConsoleAuthOptions {\n /** REQUIRED HMAC-SHA256 signing key. Missing/empty => boot error (fail closed). */\n secret: string;\n /** Cookie TTL as a duration string (`'8h'`, `'30m'`, `'7d'`). Default `'8h'`. */\n ttl?: string;\n /** Mode A: validate the host's own auth on the raw request. */\n session?: SessionHook;\n /** Mode B: validate credentials from the built-in login screen. */\n login?: LoginHook;\n /** Re-checks the session on sliding renewal. Not a mode — it cannot mint a session on its own. */\n revalidate?: RevalidateHook;\n /** Renders the host's own page for an unauthenticated navigation, in place of serving the SPA and\n * letting its built-in auth screen render; see `UnauthenticatedPageHook`. Mode-A-only. */\n unauthenticatedPage?: UnauthenticatedPageHook;\n}\n\nexport type AuthMode = 'session' | 'login';\n\n/** Resolved, validated console-auth config shared by the guard, controller, and SPA. */\nexport interface ResolvedConsoleAuth {\n secret: string;\n ttlMs: number;\n modes: AuthMode[];\n session?: SessionHook;\n login?: LoginHook;\n revalidate?: RevalidateHook;\n unauthenticatedPage?: UnauthenticatedPageHook;\n}\n\nconst DEFAULT_TTL_MS = 8 * 60 * 60 * 1000;\nconst DURATION_UNITS: Record<string, number> = {\n s: 1000,\n m: 60 * 1000,\n h: 60 * 60 * 1000,\n d: 24 * 60 * 60 * 1000,\n};\n\n/** Parse a `'<number><s|m|h|d>'` duration to ms; falls back to the 8h default on a bad value. */\nfunction durationToMs(ttl: string | undefined): number {\n if (ttl === undefined) return DEFAULT_TTL_MS;\n const match = /^(\\d+)([smhd])$/.exec(ttl.trim());\n if (!match) return DEFAULT_TTL_MS;\n const unit = DURATION_UNITS[match[2] ?? ''];\n if (unit === undefined) return DEFAULT_TTL_MS;\n return Number(match[1]) * unit;\n}\n\n/**\n * Validate + resolve the `auth` option. Returns `null` when unconfigured (the console stays open —\n * front it with your own guard). Throws at boot (fail closed) when configured but missing a secret\n * or any hook — the host learns immediately rather than shipping an un-mintable gate.\n */\nexport function resolveConsoleAuth(\n options: ConsoleAuthOptions | undefined,\n): ResolvedConsoleAuth | null {\n if (!options) return null;\n if (!options.secret) {\n throw new Error('MediaDashboardModule: auth.secret is required when `auth` is configured.');\n }\n const modes: AuthMode[] = [];\n if (options.session) modes.push('session');\n if (options.login) modes.push('login');\n if (modes.length === 0) {\n throw new Error('MediaDashboardModule: auth needs a `login` and/or `session` hook.');\n }\n if (\n options.unauthenticatedPage !== undefined &&\n typeof options.unauthenticatedPage !== 'function'\n ) {\n throw new Error('MediaDashboardModule: auth.unauthenticatedPage must be a function.');\n }\n return {\n secret: options.secret,\n ttlMs: durationToMs(options.ttl),\n modes,\n ...(options.session ? { session: options.session } : {}),\n ...(options.login ? { login: options.login } : {}),\n ...(options.revalidate ? { revalidate: options.revalidate } : {}),\n ...(options.unauthenticatedPage ? { unauthenticatedPage: options.unauthenticatedPage } : {}),\n };\n}\n","import 'reflect-metadata';\nimport type { CanActivate, Type } from '@nestjs/common';\n\n/**\n * `@nestjs/common`'s own `GUARDS_METADATA` key, INLINED rather than deep-imported from\n * '@nestjs/common/constants' — that subpath has no extension and a strict ESM resolver (which the\n * built dual ESM/CJS output of this package is loaded under) 404s on it. Same convention as\n * `@dudousxd/nestjs-agent`'s `agent-dashboard.module.ts` and `@dudousxd/nestjs-telescope`'s\n * `telescope.module.ts`. A drift spec imports the real constant (via the resolvable\n * `'@nestjs/common/constants.js'` subpath) and asserts this literal stays byte-identical to it.\n */\nexport const GUARDS_METADATA = '__guards__';\n\n/**\n * Narrows a `guards` entry to a class (constructor) as opposed to an already-instantiated\n * `CanActivate`. Only a class needs a DI provider so Nest can instantiate it — an instance is used\n * by the guards consumer as-is.\n */\nexport function isGuardClass(guard: Type<CanActivate> | CanActivate): guard is Type<CanActivate> {\n return typeof guard === 'function';\n}\n\n/**\n * Reads a controller's OWN `@UseGuards` metadata (not the inherited/prototype-chain one), or `[]`\n * when it carries none. MUST be called at module-load time — i.e. assigned to a top-level\n * `const` right after importing the controller — so it captures the pristine, decorator-defined\n * baseline BEFORE any `stampGuards` call below can mutate it.\n */\nexport function baseGuards(controller: Type): Array<Type<CanActivate> | CanActivate> {\n return Reflect.getOwnMetadata(GUARDS_METADATA, controller) ?? [];\n}\n\n/**\n * Stamp host guards onto a console controller — appending to each entry's captured `base` (its\n * own pristine `@UseGuards` metadata, or `[]` when it has none), never to whatever is CURRENTLY\n * stamped on the class. This package does not rebuild its controllers as a fresh subclass per\n * `forRoot`/`forRootAsync` call (unlike `@dudousxd/nestjs-telescope`'s `dynamicController`), so\n * appending onto LIVE metadata would compound across repeated calls against the same static class\n * (e.g. two tests in one file each calling `forRoot` with different `guards`). Recomputing from the\n * captured `base` every time keeps each call's result exactly `[...base, ...guards]`, independent of\n * whatever a prior call stamped.\n *\n * A no-op when `guards` is omitted/empty, leaving every controller's metadata exactly as its\n * decorator (or the absence of one) left it — so omitting `guards` reproduces today's behavior\n * byte-for-byte.\n */\nexport function stampGuards(\n guards: Array<Type<CanActivate> | CanActivate> | undefined,\n entries: Array<[controller: Type, base: Array<Type<CanActivate> | CanActivate>]>,\n): void {\n if (guards === undefined || guards.length === 0) return;\n for (const [controller, base] of entries) {\n Reflect.defineMetadata(GUARDS_METADATA, [...base, ...guards], controller);\n }\n}\n","import {\n type CanActivate,\n type DynamicModule,\n Module,\n type ModuleMetadata,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport { baseGuards, isGuardClass, stampGuards } from './guards.js';\nimport { MediaConsoleActionsController } from './media-console-actions.controller.js';\nimport { MediaConsoleAuthController } from './media-console-auth.controller.js';\nimport { MediaConsoleReadController } from './media-console-read.controller.js';\nimport { MediaConsoleGuard } from './media-console.guard.js';\nimport { MediaConsoleService } from './media-console.service.js';\nimport type { ObjectUrlConfig } from './object-urls.js';\nimport {\n MEDIA_CONSOLE_AUTH,\n MEDIA_CONSOLE_COOKIE_PATH,\n MEDIA_CONSOLE_OBJECT_INSIGHTS,\n MEDIA_CONSOLE_OBJECT_URLS,\n MEDIA_DASHBOARD_ACTIONS,\n} from './tokens.js';\n\n/**\n * `MediaConsoleReadController`/`MediaConsoleActionsController`'s own, pristine\n * `@UseGuards(MediaConsoleGuard)` metadata, captured once at module-load time — the baseline every\n * `register()` call's host `guards` append onto (see `stampGuards` in `./guards.js`).\n */\nconst READ_BASE_GUARDS = baseGuards(MediaConsoleReadController);\nconst ACTIONS_BASE_GUARDS = baseGuards(MediaConsoleActionsController);\n\ninterface ApiModuleOptions {\n actions: boolean;\n /** Cookie `Path` — the JSON API base — so the session cookie rides every console API request. */\n cookiePath: string;\n /** Provider for `MEDIA_CONSOLE_AUTH` — a `useValue` (forRoot) or a `useFactory` (forRootAsync). */\n authProvider: Provider;\n /** Provider for `MEDIA_CONSOLE_OBJECT_INSIGHTS` — the host's object-annotation providers.\n * Omitted binds the empty list, which is the no-op the console renders nothing for. */\n insightsProvider?: Provider;\n /** Resolved object-URL strategy + the API mount it builds proxy URLs against. */\n objectUrls: ObjectUrlConfig;\n /** Extra imports the auth factory's `inject` deps (or a guard class's own deps) live in. */\n imports?: ModuleMetadata['imports'];\n /**\n * Guard classes (or instances) fronting the read + action controllers — APPENDED to their own\n * built-in `MediaConsoleGuard` (session-cookie) gate, so a request must pass BOTH. Deliberately\n * NOT applied to `MediaConsoleAuthController`: it MINTS the session cookie the built-in gate (and\n * any host guard reusing that same cookie) checks for, so it can't be made to require the very\n * auth it grants — mirrors `@dudousxd/nestjs-telescope`'s own auth controller staying outside its\n * `stampGuards` call.\n */\n guards?: Array<Type<CanActivate> | CanActivate>;\n}\n\n/**\n * Holds the console's JSON API: the read controller (always), the actions controller (only when\n * `actions: true`), and the auth controller (always — it mints the session the guard checks, so it\n * is never itself guarded). The read/action controllers carry `MediaConsoleGuard`, a no-op unless\n * the host configured `auth`, plus any host `guards` (see {@link ApiModuleOptions.guards}) — both\n * gates must pass.\n */\n@Module({})\nexport class MediaConsoleApiModule {\n static register(options: ApiModuleOptions): DynamicModule {\n stampGuards(options.guards, [\n [MediaConsoleReadController, READ_BASE_GUARDS],\n [MediaConsoleActionsController, ACTIONS_BASE_GUARDS],\n ]);\n return {\n module: MediaConsoleApiModule,\n imports: options.imports ?? [],\n controllers: [\n MediaConsoleReadController,\n MediaConsoleAuthController,\n ...(options.actions ? [MediaConsoleActionsController] : []),\n ],\n providers: [\n MediaConsoleService,\n MediaConsoleGuard,\n { provide: MEDIA_DASHBOARD_ACTIONS, useValue: options.actions },\n { provide: MEDIA_CONSOLE_COOKIE_PATH, useValue: options.cookiePath },\n { provide: MEDIA_CONSOLE_OBJECT_URLS, useValue: options.objectUrls },\n options.authProvider,\n options.insightsProvider ?? {\n provide: MEDIA_CONSOLE_OBJECT_INSIGHTS,\n useValue: [],\n },\n // Class guards need a DI provider so Nest can instantiate them in THIS module's context\n // (where `imports` above resolves their dependencies). An already-instantiated guard needs\n // none — it's used as-is.\n ...(options.guards ?? []).filter(isGuardClass),\n ],\n // MEDIA_CONSOLE_AUTH is exported (not just provided) because MediaDashboardUiController —\n // hosted in MediaDashboardModule, which imports THIS module — reads it to decide whether to\n // hand an unauthenticated navigation to the host's `unauthenticatedPage`. Exporting keeps the\n // single `authProvider` instance shared; re-providing the same factory in both modules would\n // run a `forRootAsync` host's `useAuth` twice.\n exports: [MediaConsoleService, MEDIA_CONSOLE_AUTH],\n };\n }\n}\n","import type { Readable } from 'node:stream';\nimport {\n Body,\n Controller,\n Delete,\n HttpCode,\n Inject,\n Param,\n Post,\n Query,\n Req,\n UseGuards,\n} from '@nestjs/common';\nimport { MediaConsoleGuard } from './media-console.guard.js';\nimport { MediaConsoleService } from './media-console.service.js';\n\ninterface CopyMoveBody {\n from: string;\n to: string;\n /** Destination disk for a cross-disk copy/move. Omitted → same disk as the source (`:disk`). */\n toDisk?: string;\n}\n\ninterface CreateFolderBody {\n prefix: string;\n}\n\n/**\n * Destructive JSON API for the /media console. Only registered when the host opts in with\n * `MediaDashboardModule.forRoot({ actions: true })` (default off). Bare `@Controller()` — the path\n * prefix comes from `RouterModule`, sharing the API base with the read controller. Gated by\n * `MediaConsoleGuard` (session cookie) when the host configured `auth`.\n */\n@UseGuards(MediaConsoleGuard)\n@Controller()\nexport class MediaConsoleActionsController {\n constructor(@Inject(MediaConsoleService) private readonly service: MediaConsoleService) {}\n\n @Delete('disks/:disk/object')\n @HttpCode(204)\n deleteObject(@Param('disk') disk: string, @Query('key') key: string): Promise<void> {\n return this.service.deleteObject(disk, key);\n }\n\n @Post('disks/:disk/copy')\n @HttpCode(204)\n copyObject(@Param('disk') disk: string, @Body() body: CopyMoveBody): Promise<void> {\n return this.service.copyObject(disk, body.from, body.toDisk ?? disk, body.to);\n }\n\n @Post('disks/:disk/move')\n @HttpCode(204)\n moveObject(@Param('disk') disk: string, @Body() body: CopyMoveBody): Promise<void> {\n return this.service.moveObject(disk, body.from, body.toDisk ?? disk, body.to);\n }\n\n @Post('disks/:disk/move-folder')\n @HttpCode(204)\n moveFolder(@Param('disk') disk: string, @Body() body: CopyMoveBody): Promise<void> {\n return this.service.moveFolder(disk, body.from, body.toDisk ?? disk, body.to);\n }\n\n @Post('disks/:disk/copy-folder')\n @HttpCode(204)\n copyFolder(@Param('disk') disk: string, @Body() body: CopyMoveBody): Promise<void> {\n return this.service.copyFolder(disk, body.from, body.toDisk ?? disk, body.to);\n }\n\n /**\n * Uploads a file to `key` on the disk. The body is the raw bytes sent as `application/octet-stream`\n * so the host's JSON/urlencoded body parsers never consume the stream; the real MIME rides as the\n * `type` query param and becomes the stored object's Content-Type. `@Req()` is the request stream.\n */\n @Post('disks/:disk/upload')\n @HttpCode(204)\n uploadObject(\n @Param('disk') disk: string,\n @Query('key') key: string,\n @Req() request: Readable,\n @Query('type') type?: string,\n ): Promise<void> {\n return this.service.putObject(disk, key, request, type);\n }\n\n @Post('disks/:disk/folder')\n @HttpCode(204)\n createFolder(@Param('disk') disk: string, @Body() body: CreateFolderBody): Promise<void> {\n return this.service.createFolder(disk, body.prefix);\n }\n\n @Delete('disks/:disk/folder')\n @HttpCode(204)\n deleteFolder(@Param('disk') disk: string, @Query('prefix') prefix: string): Promise<void> {\n return this.service.deleteFolder(disk, prefix);\n }\n\n @Post('uploads/:id/abort')\n @HttpCode(204)\n abortUpload(@Param('id') id: string): Promise<void> {\n return this.service.abortUpload(id);\n }\n\n @Delete('library/:id')\n @HttpCode(204)\n deleteLibraryRecord(@Param('id') id: string): Promise<void> {\n return this.service.deleteLibraryRecord(id);\n }\n}\n","import {\n type CanActivate,\n type ExecutionContext,\n Inject,\n Injectable,\n Optional,\n UnauthorizedException,\n} from '@nestjs/common';\nimport type { ResolvedConsoleAuth } from './auth/config.js';\nimport { parseCookieHeader } from './auth/cookie-header.js';\nimport { attachSession, readCookieHeader } from './auth/request.js';\nimport {\n SESSION_COOKIE_NAME,\n clearSessionCookie,\n issueSessionCookie,\n} from './auth/session-cookie-io.js';\nimport {\n type ConsoleSession,\n type ConsoleSessionUser,\n verifySessionCookie,\n} from './auth/session-cookie.js';\nimport { MEDIA_CONSOLE_AUTH, MEDIA_CONSOLE_COOKIE_PATH } from './tokens.js';\n\n/**\n * Gates the console's read + action controllers on a valid session cookie — but ONLY when the host\n * configured `auth`. With no auth configured the resolved value is `null` and the guard is a\n * no-op (the console stays open; front it with your own guard). The auth controller that MINTS the\n * cookie is deliberately NOT decorated with this guard.\n */\n@Injectable()\nexport class MediaConsoleGuard implements CanActivate {\n constructor(\n @Optional() @Inject(MEDIA_CONSOLE_AUTH) private readonly auth: ResolvedConsoleAuth | null,\n @Optional() @Inject(MEDIA_CONSOLE_COOKIE_PATH) private readonly cookiePath: string | null,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n if (!this.auth) return true;\n const http = context.switchToHttp();\n const request = http.getRequest();\n const session = this.verifyRequestSession(request);\n // Absent/invalid/expired cookie => 401 (not 403): the SPA reads this as \"show the login screen\".\n if (!session) throw new UnauthorizedException();\n attachSession(request, session);\n if (!(await this.maybeRenew(http.getResponse(), request, session))) {\n // Revoked mid-session: same 401 as an absent cookie.\n throw new UnauthorizedException();\n }\n return true;\n }\n\n private verifyRequestSession(request: unknown): ConsoleSession | null {\n if (!this.auth) return null;\n const cookieValue = parseCookieHeader(readCookieHeader(request))[SESSION_COOKIE_NAME];\n if (cookieValue === undefined) return null;\n return verifySessionCookie(cookieValue, { secret: this.auth.secret });\n }\n\n /**\n * Sliding renewal + revalidation: when a valid cookie is past half its TTL, re-issue a fresh one\n * so active users never get logged out mid-session — but first let the host's `revalidate` hook\n * re-check the user, so a deactivated or demoted operator loses access instead of riding a\n * self-renewing cookie. Returns `false` when the session was revoked (cookie already cleared).\n */\n private async maybeRenew(\n response: unknown,\n request: unknown,\n session: ConsoleSession,\n ): Promise<boolean> {\n if (!this.auth) return true;\n const now = Date.now();\n if (now - session.iat <= this.auth.ttlMs / 2) return true;\n const user: ConsoleSessionUser = {\n id: session.sub,\n ...(session.name !== undefined ? { name: session.name } : {}),\n roles: session.roles,\n };\n if (this.auth.revalidate) {\n let allowed: boolean;\n try {\n allowed = await this.auth.revalidate(user);\n } catch {\n allowed = false; // Fail closed.\n }\n if (!allowed) {\n clearSessionCookie({ cookiePath: this.cookiePath ?? '/', request, response });\n return false;\n }\n }\n issueSessionCookie(user, {\n auth: this.auth,\n cookiePath: this.cookiePath ?? '/',\n request,\n response,\n now,\n });\n return true;\n }\n}\n","/**\n * Parse a raw `Cookie` request header into a name→value map. Dependency-free so it works on raw\n * Express AND Fastify requests without `cookie-parser`. Values are URL-decoded; malformed segments\n * are skipped, never thrown.\n */\nexport function parseCookieHeader(header: string | undefined): Record<string, string> {\n const cookies: Record<string, string> = {};\n if (typeof header !== 'string' || header === '') return cookies;\n for (const segment of header.split(';')) {\n const eq = segment.indexOf('=');\n if (eq <= 0) continue;\n const name = segment.slice(0, eq).trim();\n if (name === '') continue;\n let rawValue = segment.slice(eq + 1).trim();\n // Strip a single pair of wrapping double-quotes (RFC 6265 quoted form).\n if (rawValue.length >= 2 && rawValue.startsWith('\"') && rawValue.endsWith('\"')) {\n rawValue = rawValue.slice(1, -1);\n }\n // First occurrence wins; don't clobber an earlier value with a later dup.\n if (name in cookies) continue;\n try {\n cookies[name] = decodeURIComponent(rawValue);\n } catch {\n cookies[name] = rawValue;\n }\n }\n return cookies;\n}\n\nexport interface SetCookieOptions {\n path: string;\n maxAgeSeconds: number;\n secure: boolean;\n clear?: boolean;\n}\n\n/**\n * Serialize a `Set-Cookie` header value. Always `HttpOnly` + `SameSite=Lax` (Lax blocks\n * cross-site POSTs carrying the cookie — CSRF coverage for the console's mutation POSTs).\n * Platform-agnostic: just a string.\n */\nexport function serializeSetCookie(name: string, value: string, options: SetCookieOptions): string {\n const parts = [`${name}=${options.clear ? '' : encodeURIComponent(value)}`];\n parts.push(`Path=${options.path}`);\n parts.push('HttpOnly');\n parts.push('SameSite=Lax');\n if (options.secure) parts.push('Secure');\n if (options.clear) {\n parts.push('Max-Age=0');\n parts.push('Expires=Thu, 01 Jan 1970 00:00:00 GMT');\n } else {\n parts.push(`Max-Age=${options.maxAgeSeconds}`);\n }\n return parts.join('; ');\n}\n","import type { ConsoleSession } from './session-cookie.js';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction readHeaders(request: unknown): Record<string, unknown> {\n if (!isRecord(request)) return {};\n const headers = request.headers;\n return isRecord(headers) ? headers : {};\n}\n\n/** Coerce a single header (string or string[]) to a string, else undefined. */\nfunction headerToString(value: unknown): string | undefined {\n if (typeof value === 'string') return value;\n if (Array.isArray(value) && value.every((part) => typeof part === 'string')) {\n return value.join(',');\n }\n return undefined;\n}\n\n/** Read the raw `Cookie` request header (Express and Fastify both expose `headers`). */\nexport function readCookieHeader(request: unknown): string | undefined {\n return headerToString(readHeaders(request).cookie);\n}\n\n/**\n * Decide whether the cookie should carry `Secure`. True when the request is https directly\n * (`request.protocol === 'https'`) OR a proxy forwarded https (`x-forwarded-proto` includes\n * 'https'). Defensive structural reads — never throws, defaults to insecure (works on plain http\n * in dev).\n */\nexport function isHttpsRequest(request: unknown): boolean {\n if (isRecord(request) && request.protocol === 'https') return true;\n const forwarded = headerToString(readHeaders(request)['x-forwarded-proto']);\n if (forwarded === undefined) return false;\n return forwarded\n .split(',')\n .map((proto) => proto.trim().toLowerCase())\n .includes('https');\n}\n\n/** Attach the verified session onto the raw request so downstream code can read it. */\nexport function attachSession(request: unknown, session: ConsoleSession): void {\n if (!isRecord(request)) return;\n (request as { consoleSession?: ConsoleSession }).consoleSession = session;\n}\n","function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\ninterface RawNodeResponse {\n getHeader(name: string): number | string | string[] | undefined;\n setHeader(name: string, value: string | string[]): unknown;\n}\n\nfunction isRawNodeResponse(value: unknown): value is RawNodeResponse {\n return (\n isRecord(value) &&\n typeof value.getHeader === 'function' &&\n typeof value.setHeader === 'function'\n );\n}\n\n/**\n * Resolve the writable Node `ServerResponse` from a platform response. Express' response IS the\n * Node response; Fastify's reply wraps it on `.raw`. Returns the outer response when it already\n * exposes get/setHeader (Express), otherwise the unwrapped `.raw` (Fastify).\n */\nfunction resolveRawResponse(response: unknown): RawNodeResponse | null {\n if (isRawNodeResponse(response)) return response;\n if (isRecord(response) && isRawNodeResponse(response.raw)) return response.raw;\n return null;\n}\n\nfunction existingSetCookies(response: RawNodeResponse): string[] {\n const current = response.getHeader('set-cookie');\n if (Array.isArray(current)) return current;\n if (typeof current === 'string') return [current];\n return [];\n}\n\n/**\n * Append a `Set-Cookie` header to the response WITHOUT clobbering any cookies already queued by\n * the host. Platform-agnostic raw write — no-ops gracefully if the response can't be unwrapped.\n */\nexport function appendSetCookie(response: unknown, cookie: string): void {\n const raw = resolveRawResponse(response);\n if (!raw) return;\n raw.setHeader('set-cookie', [...existingSetCookies(raw), cookie]);\n}\n\ninterface EndableResponse extends RawNodeResponse {\n statusCode: number;\n headersSent?: boolean;\n end(chunk?: string): unknown;\n}\n\nfunction resolveEndableResponse(response: unknown): EndableResponse | null {\n const isEndable = (value: unknown): value is EndableResponse =>\n isRawNodeResponse(value) && typeof (value as EndableResponse).end === 'function';\n if (isEndable(response)) return response;\n if (isRecord(response) && isEndable(response.raw)) return response.raw;\n return null;\n}\n\n/**\n * Did something already write to this response?\n *\n * Used to decide whether a host's `unauthenticatedPage` hook actually produced a page. A hook that\n * returns without writing (an early `return`, a forgotten `await`, a template that resolved to\n * nothing) would otherwise leave the request hanging forever — the browser spins until it times\n * out, with no error anywhere. Checking this lets the caller fall back to serving the SPA.\n */\nexport function responseAlreadyWritten(response: unknown): boolean {\n return resolveEndableResponse(response)?.headersSent === true;\n}\n\n/**\n * Write a full HTML page on the raw response and END it — used to serve the SPA shell from the\n * non-passthrough index route, where the handler owns the whole response so a host's\n * `unauthenticatedPage` can take it over instead.\n */\nexport function sendHtml(response: unknown, status: number, html: string): void {\n const raw = resolveEndableResponse(response);\n if (!raw) return;\n raw.statusCode = status;\n raw.setHeader('content-type', 'text/html; charset=utf-8');\n // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = the classic\n // \"stuck loading after a deploy\"); the unauthenticated page likewise reflects live session state.\n raw.setHeader('cache-control', 'no-store, must-revalidate');\n raw.end(html);\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Clock-skew grace applied to `exp` so a marginally-late clock doesn't bounce a valid cookie. */\nconst EXP_GRACE_MS = 30_000;\n\n/** The validated console session attached to a request once its cookie verifies. */\nexport interface ConsoleSession {\n /** Stable user id (the session user's `id`). */\n sub: string;\n /** Optional display name. */\n name?: string;\n /** Free-form role strings; the console does not interpret them. */\n roles: string[];\n /** Issued-at, epoch milliseconds. */\n iat: number;\n /** Expiry, epoch milliseconds. */\n exp: number;\n}\n\n/** The session user a host `login`/`session` hook returns to mint a cookie. */\nexport interface ConsoleSessionUser {\n id: string;\n name?: string;\n roles?: string[];\n}\n\nexport interface SignOptions {\n secret: string;\n ttlMs: number;\n /** Injectable clock (epoch ms) for deterministic tests. Defaults to `Date.now()`. */\n now?: number;\n}\n\nexport interface VerifyOptions {\n secret: string;\n now?: number;\n}\n\nfunction base64urlEncode(value: string): string {\n return Buffer.from(value, 'utf8').toString('base64url');\n}\n\nfunction base64urlDecode(value: string): string {\n return Buffer.from(value, 'base64url').toString('utf8');\n}\n\nfunction hmac(secret: string, payload: string): string {\n return createHmac('sha256', secret).update(payload).digest('base64url');\n}\n\n/**\n * Sign a session into the cookie value `base64url(payload).base64url(hmac)`. Stateless\n * HMAC-SHA256, no store — `node:crypto` only, no JWT dependency. Mirrors the telescope\n * dashboard's session cookie so the console gate behaves identically.\n */\nexport function signSessionCookie(user: ConsoleSessionUser, options: SignOptions): string {\n const issuedAt = options.now ?? Date.now();\n const session: ConsoleSession = {\n sub: user.id,\n ...(user.name !== undefined ? { name: user.name } : {}),\n roles: user.roles ?? [],\n iat: issuedAt,\n exp: issuedAt + options.ttlMs,\n };\n const encodedPayload = base64urlEncode(JSON.stringify(session));\n return `${encodedPayload}.${hmac(options.secret, encodedPayload)}`;\n}\n\n/** Type guard for the decoded payload shape — defends against tampered/legacy payloads. */\nfunction isSessionPayload(value: unknown): value is ConsoleSession {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Record<string, unknown>;\n if (typeof record.sub !== 'string') return false;\n if (record.name !== undefined && typeof record.name !== 'string') return false;\n if (!Array.isArray(record.roles)) return false;\n if (!record.roles.every((role) => typeof role === 'string')) return false;\n if (typeof record.iat !== 'number' || !Number.isFinite(record.iat)) return false;\n if (typeof record.exp !== 'number' || !Number.isFinite(record.exp)) return false;\n return true;\n}\n\n/**\n * Verify a cookie value and return the session, or `null` for anything tampered, malformed, or\n * expired (past `exp` + a 30s grace). Constant-time signature comparison. NEVER throws — any\n * parse failure yields `null`.\n */\nexport function verifySessionCookie(value: string, options: VerifyOptions): ConsoleSession | null {\n try {\n const dot = value.indexOf('.');\n if (dot <= 0 || dot === value.length - 1) return null;\n const encodedPayload = value.slice(0, dot);\n const provided = Buffer.from(value.slice(dot + 1), 'base64url');\n const expected = Buffer.from(hmac(options.secret, encodedPayload), 'base64url');\n // timingSafeEqual throws on a length mismatch — guard it so a wrong-length (tampered)\n // signature returns null instead of throwing.\n if (provided.length !== expected.length) return null;\n if (!timingSafeEqual(provided, expected)) return null;\n const decoded: unknown = JSON.parse(base64urlDecode(encodedPayload));\n if (!isSessionPayload(decoded)) return null;\n const now = options.now ?? Date.now();\n if (now > decoded.exp + EXP_GRACE_MS) return null;\n return decoded;\n } catch {\n return null;\n }\n}\n","import { serializeSetCookie } from './cookie-header.js';\nimport { isHttpsRequest } from './request.js';\nimport { appendSetCookie } from './response.js';\nimport { type ConsoleSessionUser, signSessionCookie } from './session-cookie.js';\n\n/** Cookie name carrying the signed console session. */\nexport const SESSION_COOKIE_NAME = 'media_console_session';\n\ninterface CookieContext {\n auth: { secret: string; ttlMs: number };\n /** Cookie `Path` — the JSON API base, so the cookie rides every console API request. */\n cookiePath: string;\n request: unknown;\n response: unknown;\n now?: number;\n}\n\n/**\n * Sign a fresh session for `user` and append it as a `Set-Cookie` on the response, scoped to the\n * console API path and `Secure` when the request is https.\n */\nexport function issueSessionCookie(user: ConsoleSessionUser, context: CookieContext): void {\n const value = signSessionCookie(user, {\n secret: context.auth.secret,\n ttlMs: context.auth.ttlMs,\n ...(context.now !== undefined ? { now: context.now } : {}),\n });\n appendSetCookie(\n context.response,\n serializeSetCookie(SESSION_COOKIE_NAME, value, {\n path: context.cookiePath,\n maxAgeSeconds: Math.floor(context.auth.ttlMs / 1000),\n secure: isHttpsRequest(context.request),\n }),\n );\n}\n\n/** Append a cookie-clearing `Set-Cookie` (Max-Age=0) scoped to the console API path. */\nexport function clearSessionCookie(context: Omit<CookieContext, 'auth' | 'now'>): void {\n appendSetCookie(\n context.response,\n serializeSetCookie(SESSION_COOKIE_NAME, '', {\n path: context.cookiePath,\n maxAgeSeconds: 0,\n secure: isHttpsRequest(context.request),\n clear: true,\n }),\n );\n}\n","// Cross-package by-value tokens. These MUST use the exact same `Symbol.for` keys as\n// `@dudousxd/nestjs-media`'s `tokens.ts`. The global symbol registry guarantees identity across\n// packages, so the console resolves the host-provided values without importing the (heavier,\n// controller-carrying) nestjs package — this package depends only on `-core`. Do not change these\n// keys without changing them there too. Mirrors `@dudousxd/nestjs-media-telescope`'s media-tokens.\n\n/** The configured `MediaStore` (or `null`), provided by `MediaModule`. */\nexport const MEDIA_STORE: symbol = Symbol.for('nestjs-media:store');\n/** The configured `UploadSessionStore` (or `null`), provided by `MediaModule`. */\nexport const MEDIA_UPLOAD_SESSIONS: symbol = Symbol.for('nestjs-media:upload-sessions');\n/** The `StorageManager` (alias of `MediaModule`'s internal `MEDIA_STORAGE` token). */\nexport const MEDIA_STORAGE_SHARED: symbol = Symbol.for('nestjs-media:storage');\n\n/** Carries the resolved UI mount base (e.g. `/media`) to the UI controller. */\nexport const MEDIA_DASHBOARD_BASE_PATH: symbol = Symbol('MEDIA_DASHBOARD_BASE_PATH');\n/** Carries the resolved JSON API base (e.g. `/api/media/console`) the SPA fetches from. */\nexport const MEDIA_DASHBOARD_API_PATH: symbol = Symbol('MEDIA_DASHBOARD_API_PATH');\n/** Carries whether destructive action routes were enabled (`options.actions`). */\nexport const MEDIA_DASHBOARD_ACTIONS: symbol = Symbol('MEDIA_DASHBOARD_ACTIONS');\n\n/** Carries the resolved console-auth config (`ResolvedConsoleAuth | null`) to the guard/controller. */\nexport const MEDIA_CONSOLE_AUTH: symbol = Symbol('MEDIA_CONSOLE_AUTH');\n/** Carries the cookie `Path` (the JSON API base) so the session cookie rides every API request. */\nexport const MEDIA_CONSOLE_COOKIE_PATH: symbol = Symbol('MEDIA_CONSOLE_COOKIE_PATH');\n\n/** Carries the host's `ObjectInsightProvider[]` (`options.objectInsights`) to the console service. */\nexport const MEDIA_CONSOLE_OBJECT_INSIGHTS: symbol = Symbol('MEDIA_CONSOLE_OBJECT_INSIGHTS');\n\n/** Carries the resolved `ObjectUrlConfig` (`options.objectUrls` + the API mount) to the console\n * service, which decides from it whether an object's `url` is presigned or same-origin. */\nexport const MEDIA_CONSOLE_OBJECT_URLS: symbol = Symbol('MEDIA_CONSOLE_OBJECT_URLS');\n","import type { Readable } from 'node:stream';\nimport type {\n MediaRecord,\n MediaStore,\n ReadRangeOptions,\n StorageDriver,\n StorageManager,\n UploadSession,\n UploadSessionStore,\n} from '@dudousxd/nestjs-media-core';\nimport {\n BadRequestException,\n HttpException,\n HttpStatus,\n Inject,\n Injectable,\n Logger,\n NotFoundException,\n NotImplementedException,\n Optional,\n PayloadTooLargeException,\n} from '@nestjs/common';\nimport type {\n CollectionsResponse,\n DiskListResponse,\n LibraryDetailResponse,\n LibraryListResponse,\n LibraryRecord,\n ObjectDetailResponse,\n ObjectListResponse,\n Topology,\n UploadDetailResponse,\n UploadInfo,\n UploadListResponse,\n} from '../client/types.js';\nimport type {\n ObjectInsight,\n ObjectInsightProvider,\n ObjectInsightsResponse,\n} from './object-insights.js';\nimport { sanitizeInsight } from './object-insights.js';\nimport type { ObjectUrlConfig } from './object-urls.js';\nimport { objectProxyUrl } from './object-urls.js';\nimport {\n MEDIA_CONSOLE_OBJECT_INSIGHTS,\n MEDIA_CONSOLE_OBJECT_URLS,\n MEDIA_DASHBOARD_ACTIONS,\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOAD_SESSIONS,\n} from './tokens.js';\n\n/** Seconds a presigned object URL stays valid. */\nconst URL_TTL_SECONDS = 300;\nconst DEFAULT_PAGE_LIMIT = 50;\n/** Ceiling for a console (direct) upload — buffered in memory, so bounded to protect the heap. */\nconst MAX_UPLOAD_BYTES = 100 * 1024 * 1024;\n/** Page size when sweeping a folder's objects for a recursive delete. */\nconst DELETE_SWEEP_LIMIT = 1000;\n/** Ceiling for a cross-disk copy/move. The driver has no server-side cross-bucket copy, so the object\n * is streamed through the pod (get→put, buffered) — bounded to protect the heap. Same-disk transfers\n * use the driver's native copy/move and are not subject to this cap. */\nconst MAX_CROSS_DISK_BYTES = 100 * 1024 * 1024;\n\n/** Last path segment of a folder prefix, ignoring a trailing slash. */\nfunction lastSegment(prefix: string): string {\n const trimmed = prefix.replace(/\\/+$/, '');\n const slash = trimmed.lastIndexOf('/');\n return slash === -1 ? trimmed : trimmed.slice(slash + 1);\n}\n\n/**\n * A range as the CLIENT asked for it, before the object's size is known.\n *\n * Either an absolute {@link ReadRangeOptions}, or HTTP's suffix form (`Range: bytes=-500`, \"the last\n * 500 bytes\") — which cannot be made absolute without the size, and the size is `stat`'s to give.\n * Resolving it here rather than in the controller saves a second `stat` on the request path.\n */\nexport type RequestedReadRange = ReadRangeOptions | { suffixLength: number };\n\n/**\n * The requested range cannot be satisfied against this object (start at/past EOF, or start > end).\n *\n * A dedicated class rather than a bare `HttpException` because RFC 9110 requires a 416 to carry an\n * unsatisfied-range `Content-Range` naming the object's real size, so the client learns the size and\n * can retry — and only the controller can set a header. This carries the size across that boundary.\n */\nexport class RangeNotSatisfiableException extends HttpException {\n constructor(readonly size: number) {\n super(\n {\n statusCode: HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE,\n message: `Requested range is not satisfiable for an object of ${size} bytes`,\n error: 'Range Not Satisfiable',\n },\n HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE,\n );\n }\n}\n\n/** What {@link MediaConsoleService.objectStream} hands the controller. `range` is present only when\n * a range was requested AND satisfied, already absolute and clamped to the object — so the\n * controller can write `Content-Range`/`Content-Length` without re-deriving anything. */\nexport interface ObjectStreamResult {\n stream: Readable;\n contentType: string;\n /** Total size of the OBJECT, not of the slice — the `/<size>` in `Content-Range`. */\n size: number;\n /** Absolute inclusive bounds actually being streamed. Absent on a full-body read. */\n range?: { start: number; end: number };\n}\n\n/**\n * Resolve a requested range into absolute, inclusive, in-bounds bounds — or `null` when it is\n * unsatisfiable and the answer has to be a 416.\n *\n * Unsatisfiable means a start at or past EOF (there is no such byte) or a start past the end; a\n * zero-byte object consequently rejects every range, which is what RFC 9110 prescribes. An `end`\n * beyond EOF is NOT unsatisfiable — it is clamped, because \"the next 64 KB from offset N\" is how\n * every paging reader asks for the tail of a file, and erroring there would make the last page of\n * every object unreadable.\n */\nfunction resolveRange(\n range: RequestedReadRange,\n size: number,\n): { start: number; end: number } | null {\n if ('suffixLength' in range) {\n // `bytes=-N`: the last N bytes. A suffix longer than the object is the whole object; `bytes=-0`\n // asks for nothing, which RFC 9110 defines as unsatisfiable rather than as an empty 206.\n if (!Number.isInteger(range.suffixLength) || range.suffixLength <= 0 || size === 0) return null;\n return { start: Math.max(0, size - range.suffixLength), end: size - 1 };\n }\n const { start } = range;\n if (!Number.isInteger(start) || start < 0 || start >= size) return null;\n const end = range.end === undefined ? size - 1 : Math.min(range.end, size - 1);\n if (!Number.isInteger(end) || end < start) return null;\n return { start, end };\n}\n\nfunction mapUpload(session: UploadSession): UploadInfo {\n const percent =\n session.size !== undefined && session.size > 0\n ? Math.min(100, Math.round((session.offset / session.size) * 100))\n : null;\n return {\n id: session.id,\n disk: session.disk,\n key: session.key,\n offset: session.offset,\n size: session.size ?? null,\n percent,\n parts: session.parts,\n multipart: session.multipartUploadId !== undefined,\n ...(session.createdAt ? { createdAt: session.createdAt.toISOString() } : {}),\n };\n}\n\nfunction mapRecord(record: MediaRecord): LibraryRecord {\n return {\n id: record.id,\n ownerType: record.ownerType,\n ownerId: record.ownerId,\n collection: record.collection,\n name: record.name,\n fileName: record.fileName,\n mimeType: record.mimeType,\n size: record.size,\n disk: record.disk,\n path: record.path,\n createdAt: record.createdAt.toISOString(),\n };\n}\n\n/**\n * Read + action logic behind the console's JSON API. Resolves the three by-value media tokens\n * with `@Optional()` so a host without a `MediaStore`/upload store still boots — every method\n * degrades to an empty shape instead of throwing when its capability is absent.\n */\n@Injectable()\nexport class MediaConsoleService {\n constructor(\n @Optional() @Inject(MEDIA_STORAGE_SHARED) private readonly storage: StorageManager | null,\n @Optional() @Inject(MEDIA_STORE) private readonly store: MediaStore | null,\n @Optional() @Inject(MEDIA_UPLOAD_SESSIONS) private readonly uploads: UploadSessionStore | null,\n @Optional() @Inject(MEDIA_DASHBOARD_ACTIONS) private readonly actionsEnabled: boolean | null,\n // Defaulted, not merely `@Optional()`: the console service is constructed directly in unit\n // tests (and by any host wiring it by hand), so a required 5th argument would break every one\n // of those call sites for a feature they do not use.\n @Optional()\n @Inject(MEDIA_CONSOLE_OBJECT_INSIGHTS)\n private readonly insightProviders: ObjectInsightProvider[] | null = null,\n // Strategy and mount path travel as ONE value: a strategy without the path it needs would be a\n // half-configured state that builds `/disks/...` URLs rooted at nothing. `null` means `auto`.\n @Optional()\n @Inject(MEDIA_CONSOLE_OBJECT_URLS)\n private readonly objectUrls: ObjectUrlConfig | null = null,\n ) {}\n\n /**\n * The URL to report for an object's bytes, under the host's {@link ObjectUrlStrategy}. `auto`\n * prefers a presigned URL straight to the store; `proxy` routes through this server, for a host\n * whose browser cannot reach the store at all.\n */\n private objectUrl(driver: StorageDriver, disk: string, key: string): Promise<string> {\n if (this.objectUrls?.strategy === 'proxy') {\n return Promise.resolve(objectProxyUrl(this.objectUrls.apiBasePath, 'raw', disk, key));\n }\n return driver.capabilities.presign\n ? driver.temporaryUrl(key, URL_TTL_SECONDS)\n : driver.url(key);\n }\n\n private readonly logger = new Logger(MediaConsoleService.name);\n\n private diskOrThrow(disk: string): StorageDriver {\n if (!this.storage || !this.storage.diskNames().includes(disk)) {\n throw new NotFoundException(`Unknown disk: ${disk}`);\n }\n return this.storage.disk(disk);\n }\n\n listDisks(): DiskListResponse {\n const storage = this.storage;\n if (!storage) return { disks: [] };\n const defaultDisk = storage.defaultDisk;\n return {\n disks: storage.diskNames().map((name) => ({\n name,\n default: name === defaultDisk,\n capabilities: storage.disk(name).capabilities,\n })),\n };\n }\n\n async listObjects(\n disk: string,\n options: { prefix?: string; cursor?: string; limit?: number },\n ): Promise<ObjectListResponse> {\n const driver = this.diskOrThrow(disk);\n const result = await driver.list(options.prefix ?? '', {\n delimiter: '/',\n ...(options.cursor ? { cursor: options.cursor } : {}),\n limit: options.limit ?? DEFAULT_PAGE_LIMIT,\n });\n return {\n // Drop phantom folders whose name is empty — a CommonPrefix of only slashes (`/`, `//`),\n // produced by a stray key with a leading slash. The S3 driver normalizes such a prefix back to\n // the root, so listing INTO one returns the root again (the phantom included) — a self-reference\n // that infinite-loops the folder tree. Filtering it out removes the trap; those leading-slash\n // keys are unreachable from the console anyway (the driver strips the leading slash).\n folders: result.folders\n .map((prefix) => ({ name: lastSegment(prefix), prefix }))\n .filter((folder) => folder.name !== ''),\n // Drop the zero-byte \"folder marker\" (a key ending in `/`, whose name is empty after the\n // prefix) that `createFolder` writes — it's the folder itself, not a file inside it.\n files: result.files\n .filter((entry) => entry.name !== '')\n .map((entry) => ({\n key: entry.key,\n name: entry.name,\n sizeBytes: entry.sizeBytes,\n lastModified: entry.lastModified ? entry.lastModified.toISOString() : null,\n })),\n ...(result.cursor ? { cursor: result.cursor } : {}),\n };\n }\n\n async objectDetail(disk: string, key: string): Promise<ObjectDetailResponse> {\n const driver = this.diskOrThrow(disk);\n const stat = await driver.stat(key);\n const url = await this.objectUrl(driver, disk, key);\n return {\n key,\n size: stat.size,\n ...(stat.contentType ? { contentType: stat.contentType } : {}),\n ...(stat.lastModified ? { lastModified: stat.lastModified.toISOString() } : {}),\n url,\n };\n }\n\n /**\n * What the HOST knows about this object, from every registered provider.\n *\n * Its own call rather than a field on `objectDetail`, for the same reason the RAG index-status\n * endpoint is separate from its document listing: `objectDetail` is on the path of every preview\n * and every navigation, and providers reach into host databases. Folding them in would put a host\n * query on reads that only wanted a presigned URL.\n *\n * Failures are contained per provider. A provider that throws is logged and dropped, and the\n * others still render — annotation must never be able to stop an admin opening a file, and one\n * host lookup being down says nothing about the rest.\n *\n * Providers run concurrently: they are independent host lookups, and running them in series would\n * make the panel as slow as their sum for no gain.\n */\n async objectInsights(disk: string, key: string): Promise<ObjectInsightsResponse> {\n const providers = this.insightProviders ?? [];\n if (providers.length === 0) {\n return { insights: [] };\n }\n // Validates the disk the same way every other object read does, so an unknown disk 404s here\n // too rather than silently handing host providers a key from nowhere.\n this.diskOrThrow(disk);\n\n const settled = await Promise.all(\n providers.map(async (provider): Promise<ObjectInsight | null> => {\n try {\n return await provider.resolve({ disk, key });\n } catch (error) {\n this.logger.warn(\n `Object-insight provider \"${provider.id}\" failed for ${disk}:${key} — ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return null;\n }\n }),\n );\n\n return {\n insights: settled\n .filter((insight): insight is ObjectInsight => insight !== null)\n .map(sanitizeInsight),\n };\n }\n\n /**\n * The object's raw byte stream plus the metadata the controller needs to serve it inline. Used by\n * the console's same-origin preview proxy so text/PDF render in the browser instead of downloading\n * (and so a CORS-locked bucket is still previewable).\n *\n * With `range`, only that slice — which is what makes previewing a multi-hundred-MB object (a\n * SQLite database read a page at a time) possible at all. The range is resolved HERE, not in the\n * driver, because it has to be checked against the object's size and the `stat` for that is\n * already on this path: the suffix form is made absolute, an overrunning `end` is clamped to the\n * last byte, and anything genuinely unsatisfiable becomes a {@link RangeNotSatisfiableException}\n * (HTTP 416) instead of a surprise empty body.\n *\n * A driver that reports `ranged: false` REFUSES a range request rather than quietly returning the\n * whole object: the caller asked for 64 KB out of 400 MB, and silently answering with all 400 MB\n * — which the caller would then read as if it were the slice — is the exact failure the `ranged`\n * capability exists to make impossible.\n */\n async objectStream(\n disk: string,\n key: string,\n range?: RequestedReadRange,\n ): Promise<ObjectStreamResult> {\n const driver = this.diskOrThrow(disk);\n const stat = await driver.stat(key);\n const contentType = stat.contentType ?? 'application/octet-stream';\n if (!range) {\n return { stream: await driver.stream(key), contentType, size: stat.size };\n }\n if (!driver.capabilities.ranged) {\n throw new NotImplementedException(`Disk \"${disk}\" cannot serve byte ranges`);\n }\n const resolved = resolveRange(range, stat.size);\n if (!resolved) throw new RangeNotSatisfiableException(stat.size);\n return {\n stream: await driver.stream(key, resolved),\n contentType,\n size: stat.size,\n range: resolved,\n };\n }\n\n /** Writes an object from a byte stream (a browser upload). The caller supplies the full key\n * (prefix + filename); an unknown disk 404s via {@link diskOrThrow}. Actions-gated.\n *\n * The request stream is buffered before the write: S3's PutObject needs a known Content-Length,\n * which a raw request stream doesn't carry, so passing the stream straight through fails. Bounded\n * at {@link MAX_UPLOAD_BYTES} so a runaway upload can't exhaust the pod's heap — larger files\n * belong on the resumable upload path, not this console convenience upload. */\n async putObject(disk: string, key: string, body: Readable, contentType?: string): Promise<void> {\n const driver = this.diskOrThrow(disk);\n const chunks: Buffer[] = [];\n let total = 0;\n for await (const chunk of body) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n total += buffer.length;\n if (total > MAX_UPLOAD_BYTES) {\n throw new PayloadTooLargeException(\n `Upload exceeds the ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB console limit.`,\n );\n }\n chunks.push(buffer);\n }\n await driver.put(key, Buffer.concat(chunks), contentType ? { contentType } : undefined);\n }\n\n /** Creates a \"folder\" — a zero-byte marker object at `<prefix>/`, which S3-style listing surfaces\n * as a navigable folder. Normalizes to exactly one trailing slash; rejects an empty prefix. */\n async createFolder(disk: string, prefix: string): Promise<void> {\n const normalized = prefix.replace(/^\\/+/, '').replace(/\\/+$/, '');\n if (normalized === '') throw new BadRequestException('Folder name is required');\n await this.diskOrThrow(disk).put(`${normalized}/`, Buffer.alloc(0));\n }\n\n async deleteObject(disk: string, key: string): Promise<void> {\n await this.diskOrThrow(disk).delete(key);\n }\n\n /** Recursively deletes a \"folder\": every object under `<prefix>/` (nested included) plus the\n * zero-byte marker itself. Passes an empty delimiter so the driver lists keys FLAT — the default\n * `/` delimiter would group nested keys into CommonPrefixes and the sweep would miss them, only\n * deleting direct children. Paginates until the disk reports no more. The marker's own key equals\n * the sweep prefix, which `list` filters out (Key === Prefix), so it's deleted explicitly at the\n * end. Actions-gated. */\n async deleteFolder(disk: string, prefix: string): Promise<void> {\n const driver = this.diskOrThrow(disk);\n const normalized = prefix.replace(/^\\/+/, '').replace(/\\/+$/, '');\n if (normalized === '') throw new BadRequestException('Folder is required');\n const sweepPrefix = `${normalized}/`;\n let cursor: string | undefined;\n do {\n const result = await driver.list(sweepPrefix, {\n delimiter: '',\n limit: DELETE_SWEEP_LIMIT,\n ...(cursor ? { cursor } : {}),\n });\n for (const entry of result.files) {\n await driver.delete(entry.key);\n }\n cursor = result.cursor;\n } while (cursor);\n await driver.delete(sweepPrefix);\n }\n\n async copyObject(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await this.transferKey('copy', fromDisk, from, toDisk, to);\n }\n\n async moveObject(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await this.transferKey('move', fromDisk, from, toDisk, to);\n }\n\n /** Copies or moves a single object, on the same disk or across disks. Same-disk uses the driver's\n * native `copy`/`move` (server-side, no bytes through the pod). Cross-disk has no driver primitive,\n * so the object is streamed through the pod (get→put, buffered), preserving its content type and\n * bounded at {@link MAX_CROSS_DISK_BYTES}; a `move` then deletes the source. */\n private async transferKey(\n op: 'copy' | 'move',\n fromDisk: string,\n from: string,\n toDisk: string,\n to: string,\n ): Promise<void> {\n const fromDriver = this.diskOrThrow(fromDisk);\n if (fromDisk === toDisk) {\n if (op === 'move') await fromDriver.move(from, to);\n else await fromDriver.copy(from, to);\n return;\n }\n const toDriver = this.diskOrThrow(toDisk);\n const stat = await fromDriver.stat(from);\n if (stat.size > MAX_CROSS_DISK_BYTES) {\n throw new PayloadTooLargeException(\n `\"${from}\" is ${Math.round(stat.size / (1024 * 1024))} MB — over the ${\n MAX_CROSS_DISK_BYTES / (1024 * 1024)\n } MB limit for copying across disks from the console.`,\n );\n }\n const bytes = await fromDriver.get(from);\n await toDriver.put(to, bytes, stat.contentType ? { contentType: stat.contentType } : undefined);\n if (op === 'move') await fromDriver.delete(from);\n }\n\n /** Moves a whole \"folder\" (every object under `<from>/`, nested included) to `<to>/`, on the same\n * disk or across disks, preserving each key's path relative to the source. */\n async moveFolder(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await this.transferFolder('move', fromDisk, from, toDisk, to);\n }\n\n /** Copies a whole \"folder\" (every object under `<from>/`, nested included) to `<to>/`, same disk or\n * across disks, preserving each key's path relative to the source. */\n async copyFolder(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await this.transferFolder('copy', fromDisk, from, toDisk, to);\n }\n\n /** Shared engine for {@link moveFolder}/{@link copyFolder}. Same flat-listing sweep as\n * {@link deleteFolder}; each key is relocated via {@link transferKey} (so cross-disk transfers work\n * transparently). On the SAME disk, rejects a destination inside the source (which would recurse\n * forever); across disks that constraint doesn't apply. The destination folder marker is written,\n * and for a move the source marker is removed, so both listings stay consistent. Actions-gated. */\n private async transferFolder(\n op: 'copy' | 'move',\n fromDisk: string,\n from: string,\n toDisk: string,\n to: string,\n ): Promise<void> {\n const fromDriver = this.diskOrThrow(fromDisk);\n const toDriver = this.diskOrThrow(toDisk);\n const fromNormalized = from.replace(/^\\/+/, '').replace(/\\/+$/, '');\n const toNormalized = to.replace(/^\\/+/, '').replace(/\\/+$/, '');\n if (fromNormalized === '') throw new BadRequestException('Source folder is required');\n if (toNormalized === '') throw new BadRequestException('Destination folder is required');\n const fromPrefix = `${fromNormalized}/`;\n const toPrefix = `${toNormalized}/`;\n if (fromDisk === toDisk && (toPrefix === fromPrefix || toPrefix.startsWith(fromPrefix))) {\n throw new BadRequestException(`Cannot ${op} a folder into itself`);\n }\n let cursor: string | undefined;\n do {\n const result = await fromDriver.list(fromPrefix, {\n delimiter: '',\n limit: DELETE_SWEEP_LIMIT,\n ...(cursor ? { cursor } : {}),\n });\n for (const entry of result.files) {\n const destKey = `${toPrefix}${entry.key.slice(fromPrefix.length)}`;\n await this.transferKey(op, fromDisk, entry.key, toDisk, destKey);\n }\n cursor = result.cursor;\n } while (cursor);\n // Relocate/replicate the zero-byte marker (its key equals the sweep prefix, so the sweep skips it):\n // write the destination marker, and for a move drop the source one. Idempotent if there was none.\n await toDriver.put(toPrefix, Buffer.alloc(0));\n if (op === 'move') await fromDriver.delete(fromPrefix);\n }\n\n async listUploads(filter: { disk?: string; prefix?: string }): Promise<UploadListResponse> {\n if (!this.uploads || typeof this.uploads.list !== 'function') return { uploads: [] };\n const sessions = await this.uploads.list({\n ...(filter.disk ? { disk: filter.disk } : {}),\n ...(filter.prefix ? { keyPrefix: filter.prefix } : {}),\n });\n return { uploads: sessions.map(mapUpload) };\n }\n\n async uploadDetail(id: string): Promise<UploadDetailResponse> {\n if (!this.uploads) throw new NotFoundException('No upload store configured');\n const session = await this.uploads.get(id);\n if (!session) throw new NotFoundException(`Unknown upload: ${id}`);\n const parts = this.uploads.listParts ? await this.uploads.listParts(id) : [];\n return { upload: mapUpload(session), parts };\n }\n\n /**\n * Cancels a resumable session by removing its record from the upload store, so it stops\n * appearing as in-progress. NOTE: this does NOT tear down an underlying native multipart upload\n * (e.g. an S3 multipart) or its temporary parts — the decoupled console resolves only the\n * `UploadSessionStore`, not the `ResumableUploadManager` that owns `abort()`. An incomplete\n * multipart is reaped by the bucket's lifecycle policy. Surfaced as \"Cancel session\" in the UI.\n */\n async abortUpload(id: string): Promise<void> {\n if (!this.uploads) throw new NotFoundException('No upload store configured');\n await this.uploads.delete(id);\n }\n\n async listCollections(): Promise<CollectionsResponse> {\n if (!this.store || typeof this.store.aggregate !== 'function') return { collections: [] };\n const buckets = await this.store.aggregate({ groupBy: 'collection', sum: 'size' });\n return { collections: buckets };\n }\n\n async listLibrary(filter: {\n collection?: string;\n disk?: string;\n cursor?: string;\n limit?: number;\n }): Promise<LibraryListResponse> {\n if (!this.store || typeof this.store.list !== 'function') return { records: [] };\n const page = await this.store.list(\n {\n ...(filter.collection ? { collection: filter.collection } : {}),\n ...(filter.disk ? { disk: filter.disk } : {}),\n },\n {\n limit: filter.limit ?? DEFAULT_PAGE_LIMIT,\n ...(filter.cursor ? { cursor: filter.cursor } : {}),\n },\n );\n return {\n records: page.records.map(mapRecord),\n ...(page.cursor ? { cursor: page.cursor } : {}),\n };\n }\n\n async libraryDetail(id: string): Promise<LibraryDetailResponse> {\n if (!this.store) throw new NotFoundException('No media store configured');\n const record = await this.store.find(id);\n if (!record) throw new NotFoundException(`Unknown media record: ${id}`);\n const variants = await Promise.all(\n Object.entries(record.conversions).map(async ([name, conversion]) => {\n const driver = this.storage?.diskNames().includes(conversion.disk)\n ? this.storage.disk(conversion.disk)\n : null;\n const url = driver ? await this.objectUrl(driver, conversion.disk, conversion.path) : '';\n // `disk`/`path` ride along so the UI can reach the object routes (download, and a proxied\n // read) for a variant — `url` alone is opaque, and under `auto` points off this origin.\n return { name, url, disk: conversion.disk, path: conversion.path };\n }),\n );\n return { record: mapRecord(record), variants };\n }\n\n async deleteLibraryRecord(id: string): Promise<void> {\n if (!this.store) throw new NotFoundException('No media store configured');\n await this.store.delete(id);\n }\n\n topology(): Topology {\n return {\n hasStore: this.store !== null && typeof this.store.list === 'function',\n hasUploads: this.uploads !== null && typeof this.uploads.list === 'function',\n disks: this.storage ? this.storage.diskNames().length : 0,\n actions: this.actionsEnabled === true,\n };\n }\n}\n","/**\n * Host-supplied context about a disk object, shown in the console's preview.\n *\n * The console can describe a file only as storage sees it: key, size, content type, last modified.\n * Everything that makes the file *mean* something lives in the host — which work order this scan\n * belongs to, which knowledge base indexed it, whether processing has run. An admin looking at\n * `rag/019af.../handbook.pdf` in the object browser has no way to learn any of that without leaving\n * for another screen and searching by key.\n *\n * This is the seam for handing that back. The host registers a provider; the console asks it when an\n * object is previewed and renders whatever comes back.\n *\n * **Data, not components — and not by preference.** The console ships as a prebuilt SPA bundle, so a\n * host has no way to inject React into it. A provider therefore returns a small structured value the\n * console knows how to render (facts, links, a note), rather than something it draws itself. The\n * cost is a fixed vocabulary; the benefit is that this works at all for a published bundle, and that\n * the console stays ignorant of every domain plugged into it.\n */\n\nimport type { ObjectInsight, ObjectInsightsResponse } from '../client/types.js';\n\nexport type {\n ObjectInsight,\n ObjectInsightFact,\n ObjectInsightLink,\n ObjectInsightsResponse,\n} from '../client/types.js';\n\n/** Which object is being described. */\nexport interface ObjectInsightContext {\n disk: string;\n key: string;\n}\n\n/**\n * A source of {@link ObjectInsight}s, registered by the host on `MediaDashboardModule.forRoot`.\n *\n * `resolve` runs on every preview of every object, so it must be cheap — one indexed lookup, not a\n * scan. Return `null` for an object this provider has nothing to say about (the overwhelmingly\n * common case: most providers care about one key prefix), and the console renders nothing for it.\n *\n * A provider that throws is **skipped**, logged, and the preview renders without it. Annotation is\n * never worth taking the preview down for — a host lookup failing must not stop an admin from\n * opening a file.\n */\nexport interface ObjectInsightProvider {\n /** Stable identifier, used in the log line when this provider fails. */\n id: string;\n resolve(ctx: ObjectInsightContext): Promise<ObjectInsight | null> | ObjectInsight | null;\n}\n\n/** Schemes an {@link ObjectInsightLink} may point at. Anything else is dropped, not rendered. */\nconst SAFE_LINK_SCHEMES = new Set(['http:', 'https:']);\n\n/**\n * Drop links the console must not render.\n *\n * A relative href (`/ctrl/rag/...`) is the normal case and always allowed — it can only ever address\n * the host's own app. An absolute one is parsed and kept only if it is http(s), which refuses\n * `javascript:` and `data:` hrefs. Providers are host code, so this is not a trust boundary so much\n * as a guard against a provider interpolating user-controlled text into a URL and handing the\n * console something that executes when clicked.\n */\nexport function sanitizeInsight(insight: ObjectInsight): ObjectInsight {\n if (insight.links === undefined) {\n return insight;\n }\n const links = insight.links.filter((link) => {\n if (link.href.startsWith('/')) {\n // `//evil.com` is protocol-relative — an absolute URL wearing a relative one's clothes.\n return !link.href.startsWith('//');\n }\n try {\n return SAFE_LINK_SCHEMES.has(new URL(link.href).protocol);\n } catch {\n return false;\n }\n });\n return { ...insight, links };\n}\n","/**\n * How the console hands out the `url` it reports for an object's bytes — the link the UI offers as\n * \"Open ↗\", and the `src` an image variant renders from.\n *\n * - `auto` (default): a presigned, expiring URL straight to the object store when the driver can\n * mint one, else the driver's plain URL. A real link: it survives a copy-paste out of the console\n * and needs nothing from this server to resolve.\n * - `proxy`: the console's own same-origin route. For a host whose browser has no path to the store\n * at all — no network route, no CORS grant, a policy against client-to-bucket traffic — where a\n * presigned URL is a link that cannot be opened. The trade is that these URLs are only meaningful\n * to a session that can reach this server, and every byte travels through it.\n *\n * Downloading is unaffected either way: `disks/:disk/object/download` is always same-origin,\n * because saving a file is the console's own action and has to work wherever the console does.\n */\nexport type ObjectUrlStrategy = 'auto' | 'proxy';\n\n/** The resolved {@link ObjectUrlStrategy} plus what `proxy` needs to build a URL. Travels as one\n * value so the service can never see a strategy without the mount path it depends on. */\nexport interface ObjectUrlConfig {\n strategy: ObjectUrlStrategy;\n /** Where the console's JSON API is mounted — leading slash, no trailing slash. */\n apiBasePath: string;\n}\n\n/** Same-origin URL for one of the console's object-byte routes, rooted at the API mount. */\nexport function objectProxyUrl(\n apiBasePath: string,\n route: 'raw' | 'download',\n disk: string,\n key: string,\n): string {\n const path = `${apiBasePath}/disks/${encodeURIComponent(disk)}/object/${route}`;\n return `${path}?key=${encodeURIComponent(key)}`;\n}\n","import {\n BadRequestException,\n Body,\n Controller,\n Get,\n HttpCode,\n Inject,\n Logger,\n NotFoundException,\n Optional,\n Post,\n Req,\n Res,\n UnauthorizedException,\n} from '@nestjs/common';\nimport type { ConsoleAuthOptions, ResolvedConsoleAuth } from './auth/config.js';\nimport { parseCookieHeader } from './auth/cookie-header.js';\nimport { readCookieHeader } from './auth/request.js';\nimport {\n SESSION_COOKIE_NAME,\n clearSessionCookie,\n issueSessionCookie,\n} from './auth/session-cookie-io.js';\nimport { type ConsoleSessionUser, verifySessionCookie } from './auth/session-cookie.js';\nimport { MEDIA_CONSOLE_AUTH, MEDIA_CONSOLE_COOKIE_PATH } from './tokens.js';\n\ninterface LoginBody {\n username?: unknown;\n password?: unknown;\n}\n\n/** Response of `GET /me`: the console SPA renders the login screen or the console from this. */\ntype MeResponse =\n | { authRequired: false }\n | { user: { id: string; name?: string; roles: string[] } };\n\n/**\n * Mints/clears the console session cookie. Deliberately NOT decorated with `MediaConsoleGuard` —\n * these endpoints CREATE the session the gate checks for. When the host didn't configure `auth`,\n * `GET /me` reports `authRequired: false` (the SPA shows the console) and login/logout 404.\n */\n@Controller()\nexport class MediaConsoleAuthController {\n private readonly logger = new Logger(MediaConsoleAuthController.name);\n /** One warn per hook kind, so a flaky hook doesn't spam logs every request. */\n private readonly warnedHooks = new Set<string>();\n\n constructor(\n @Optional() @Inject(MEDIA_CONSOLE_AUTH) private readonly auth: ResolvedConsoleAuth | null,\n @Optional() @Inject(MEDIA_CONSOLE_COOKIE_PATH) private readonly cookiePath: string | null,\n ) {}\n\n @Get('me')\n me(@Req() request: unknown): MeResponse {\n if (!this.auth) return { authRequired: false };\n const cookieValue = parseCookieHeader(readCookieHeader(request))[SESSION_COOKIE_NAME];\n const session =\n cookieValue !== undefined\n ? verifySessionCookie(cookieValue, { secret: this.auth.secret })\n : null;\n // The UNauthenticated SPA learns which login mode(s) to offer from this 401 body.\n if (!session) throw new UnauthorizedException({ auth: { modes: this.auth.modes } });\n return {\n user: {\n id: session.sub,\n ...(session.name !== undefined ? { name: session.name } : {}),\n roles: session.roles,\n },\n };\n }\n\n @Post('login')\n @HttpCode(200)\n async login(\n @Body() body: LoginBody,\n @Req() request: unknown,\n @Res({ passthrough: true }) response: unknown,\n ): Promise<MeResponse> {\n const auth = this.requireAuth();\n if (!auth.login) throw new NotFoundException();\n if (typeof body?.username !== 'string' || typeof body?.password !== 'string') {\n throw new BadRequestException('Body must include string `username` and `password`.');\n }\n const username = body.username;\n const password = body.password;\n // Uniform 401 for unknown user / bad password — no user-enumeration.\n const user = await this.runHook('login', () => auth.login?.(username, password) ?? null);\n if (!user) throw new UnauthorizedException({ message: 'Invalid credentials' });\n return this.mint(user, request, response);\n }\n\n @Post('session')\n @HttpCode(200)\n async session(\n @Req() request: unknown,\n @Res({ passthrough: true }) response: unknown,\n ): Promise<MeResponse> {\n const auth = this.requireAuth();\n if (!auth.session) throw new NotFoundException();\n const user = await this.runHook('session', () => auth.session?.(request) ?? null);\n if (!user) throw new UnauthorizedException();\n return this.mint(user, request, response);\n }\n\n @Post('logout')\n @HttpCode(204)\n logout(@Req() request: unknown, @Res({ passthrough: true }) response: unknown): void {\n // Best-effort: even without auth configured, clearing is harmless.\n clearSessionCookie({ cookiePath: this.cookiePath ?? '/', request, response });\n }\n\n private requireAuth(): ResolvedConsoleAuth {\n if (!this.auth) throw new NotFoundException();\n return this.auth;\n }\n\n private mint(user: ConsoleSessionUser, request: unknown, response: unknown): MeResponse {\n const auth = this.requireAuth();\n issueSessionCookie(user, {\n auth,\n cookiePath: this.cookiePath ?? '/',\n request,\n response,\n });\n return {\n user: {\n id: user.id,\n ...(user.name !== undefined ? { name: user.name } : {}),\n roles: user.roles ?? [],\n },\n };\n }\n\n /** Run a host hook defensively: a throw is a denial (null), warn-logged once per kind. */\n private async runHook(\n kind: string,\n run: () => Promise<ConsoleSessionUser | null> | ConsoleSessionUser | null,\n ): Promise<ConsoleSessionUser | null> {\n try {\n return (await run()) ?? null;\n } catch (error) {\n if (!this.warnedHooks.has(kind)) {\n this.warnedHooks.add(kind);\n this.logger.warn(`Console auth ${kind} hook threw; treating as denial. ${String(error)}`);\n }\n return null;\n }\n }\n}\n\nexport type { ConsoleAuthOptions };\n","import {\n Controller,\n Get,\n Headers,\n Inject,\n Param,\n Query,\n Res,\n StreamableFile,\n UseGuards,\n} from '@nestjs/common';\nimport type {\n CollectionsResponse,\n DiskListResponse,\n LibraryDetailResponse,\n LibraryListResponse,\n ObjectDetailResponse,\n ObjectListResponse,\n Topology,\n UploadDetailResponse,\n UploadListResponse,\n} from '../client/types.js';\nimport { attachmentDisposition, downloadFilename } from './content-disposition.js';\nimport { MediaConsoleGuard } from './media-console.guard.js';\nimport {\n MediaConsoleService,\n RangeNotSatisfiableException,\n type RequestedReadRange,\n} from './media-console.service.js';\nimport type { ObjectInsightsResponse } from './object-insights.js';\n\n/** Parse an optional numeric query param; undefined when absent or not a finite number. */\nfunction toLimit(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : undefined;\n}\n\n/** `bytes=<start>-<end>`, `bytes=<start>-`, or the suffix `bytes=-<n>`. Nothing else: a multi-range\n * request, a non-`bytes` unit, or junk fails to match and is treated as absent. */\nconst SINGLE_BYTE_RANGE = /^bytes=(\\d*)-(\\d*)$/;\n\n/**\n * Parse a `Range` request header into something the service can resolve against the object's size.\n *\n * Returns `null` for \"no usable range\", which the caller treats as no range at all — a full 200\n * body. That is deliberate and RFC 9110-sanctioned: \"An origin server MUST ignore a Range header\n * field that contains a range unit it does not understand\", and a server may likewise ignore one it\n * cannot parse. Ignoring is safe (the client gets everything it asked for and more); guessing is\n * not, which is why anything outside the single-range grammar above is rejected wholesale rather\n * than partially interpreted.\n */\nexport function parseRangeHeader(header: string | undefined): RequestedReadRange | null {\n if (header === undefined) return null;\n const match = SINGLE_BYTE_RANGE.exec(header.trim());\n if (!match) return null;\n const startText = match[1] ?? '';\n const endText = match[2] ?? '';\n // `bytes=-` names neither bound — meaningless, not a range.\n if (startText === '' && endText === '') return null;\n // `bytes=-N` — the LAST n bytes. Only the size tells us where that starts, so it travels as a\n // suffix and the service (which has the stat) makes it absolute.\n if (startText === '') return { suffixLength: Number(endText) };\n const start = Number(startText);\n return endText === '' ? { start } : { start, end: Number(endText) };\n}\n\n/** The response surface this controller drives, structurally typed: Express' response and Fastify's\n * reply both expose `status()`, and both reach the Node response for `setHeader` (Fastify via\n * `.raw`). Keeps the package free of a platform-specific type dependency, like `auth/response.ts`. */\ninterface ConsoleHttpResponse {\n status?(code: number): unknown;\n statusCode?: number;\n setHeader?(name: string, value: string): unknown;\n raw?: { statusCode?: number; setHeader?(name: string, value: string): unknown };\n}\n\nfunction setHeader(response: ConsoleHttpResponse, name: string, value: string): void {\n if (typeof response.setHeader === 'function') {\n response.setHeader(name, value);\n return;\n }\n if (typeof response.raw?.setHeader === 'function') response.raw.setHeader(name, value);\n}\n\n/** Set the response status without ending the response — Nest still pipes the `StreamableFile`\n * afterwards, and (with `passthrough`) does not re-apply the route's default 200 over this. */\nfunction setStatus(response: ConsoleHttpResponse, code: number): void {\n if (typeof response.status === 'function') {\n response.status(code);\n return;\n }\n if (response.raw) response.raw.statusCode = code;\n else response.statusCode = code;\n}\n\n/**\n * Read-only JSON API for the /media console. Bare `@Controller()` — the path prefix is applied by\n * `RouterModule` (set in `MediaDashboardModule.forRoot({ apiBasePath })`). Always mounted.\n * `MediaConsoleGuard` gates it on a session cookie when the host configured `auth` (else a no-op).\n */\n@UseGuards(MediaConsoleGuard)\n@Controller()\nexport class MediaConsoleReadController {\n constructor(@Inject(MediaConsoleService) private readonly service: MediaConsoleService) {}\n\n @Get('disks')\n disks(): DiskListResponse {\n return this.service.listDisks();\n }\n\n @Get('disks/:disk/objects')\n objects(\n @Param('disk') disk: string,\n @Query('prefix') prefix?: string,\n @Query('cursor') cursor?: string,\n @Query('limit') limit?: string,\n ): Promise<ObjectListResponse> {\n const limitValue = toLimit(limit);\n return this.service.listObjects(disk, {\n ...(prefix ? { prefix } : {}),\n ...(cursor ? { cursor } : {}),\n ...(limitValue !== undefined ? { limit: limitValue } : {}),\n });\n }\n\n @Get('disks/:disk/object')\n object(@Param('disk') disk: string, @Query('key') key: string): Promise<ObjectDetailResponse> {\n return this.service.objectDetail(disk, key);\n }\n\n /** What the host knows about this object, from its registered `objectInsights` providers. Empty\n * when none are registered — the console renders nothing for an empty list. */\n @Get('disks/:disk/object/insights')\n objectInsights(\n @Param('disk') disk: string,\n @Query('key') key: string,\n ): Promise<ObjectInsightsResponse> {\n return this.service.objectInsights(disk, key);\n }\n\n /**\n * Streams the object's bytes inline (Content-Disposition: inline) from the same origin, so the SPA\n * can render text/PDF previews the browser would otherwise download, and read text past CORS.\n */\n @Get('disks/:disk/object/raw')\n objectRaw(\n @Param('disk') disk: string,\n @Query('key') key: string,\n @Headers('range') rangeHeader: string | undefined,\n @Res({ passthrough: true }) response: ConsoleHttpResponse,\n ): Promise<StreamableFile> {\n return this.streamObject(disk, key, rangeHeader, response, 'inline');\n }\n\n /**\n * The same bytes as `/raw`, but as a SAVE rather than a render: `Content-Disposition: attachment`\n * with the object's own name on it.\n *\n * Its own route rather than a flag on `/raw` because `/raw` is load-bearing for every preview —\n * they depend on `inline`, and a disposition that varied by query param would be one wrong link\n * away from a PDF preview turning into a download.\n *\n * Same-origin and unconditional, deliberately: this is the console's own action, so it must work\n * on a host whose browser cannot reach the object store at all (no route to S3, no CORS grant).\n * That is exactly where `objectDetail`'s presigned `url` — a link to share, not a way to fetch —\n * is useless. See the `objectUrls` option.\n */\n @Get('disks/:disk/object/download')\n objectDownload(\n @Param('disk') disk: string,\n @Query('key') key: string,\n @Headers('range') rangeHeader: string | undefined,\n @Res({ passthrough: true }) response: ConsoleHttpResponse,\n ): Promise<StreamableFile> {\n return this.streamObject(\n disk,\n key,\n rangeHeader,\n response,\n attachmentDisposition(downloadFilename(key ?? '')),\n );\n }\n\n /**\n * Stream an object under the given `Content-Disposition`, honouring a single-range `Range` header\n * — which is what lets a reader pull a few KB out of a multi-hundred-MB object (a SQLite page, a\n * ZIP's central directory) instead of the whole file, and what lets a browser resume an\n * interrupted download of one. With no `Range` the response is a plain 200 full body, which the\n * PDF/text/spreadsheet previews depend on.\n */\n private async streamObject(\n disk: string,\n key: string,\n rangeHeader: string | undefined,\n response: ConsoleHttpResponse,\n disposition: string,\n ): Promise<StreamableFile> {\n const requested = parseRangeHeader(rangeHeader);\n // On EVERY response, 200 included: `Accept-Ranges` is how a client discovers that ranged reads\n // work here at all. A client that can't tell will either not try, or try and mis-handle a 200.\n setHeader(response, 'Accept-Ranges', 'bytes');\n\n let result: Awaited<ReturnType<MediaConsoleService['objectStream']>>;\n try {\n result = await this.service.objectStream(disk, key, requested ?? undefined);\n } catch (error) {\n // RFC 9110 requires the 416 to name the object's real size so the client can re-ask. Nest's\n // exception filter serializes the body but cannot know the size, so the header goes on here,\n // before the rethrow — headers already on the response survive the filter's `status().json()`.\n if (error instanceof RangeNotSatisfiableException) {\n setHeader(response, 'Content-Range', `bytes */${error.size}`);\n }\n throw error;\n }\n\n const { stream, contentType, size, range } = result;\n if (!range) {\n return new StreamableFile(stream, {\n type: contentType,\n disposition,\n ...(Number.isFinite(size) ? { length: size } : {}),\n });\n }\n setStatus(response, 206);\n setHeader(response, 'Content-Range', `bytes ${range.start}-${range.end}/${size}`);\n // Content-Length is the SLICE's length, not the object's — inclusive bounds, hence the +1.\n return new StreamableFile(stream, {\n type: contentType,\n disposition,\n length: range.end - range.start + 1,\n });\n }\n\n @Get('uploads')\n uploads(\n @Query('disk') disk?: string,\n @Query('prefix') prefix?: string,\n ): Promise<UploadListResponse> {\n return this.service.listUploads({\n ...(disk ? { disk } : {}),\n ...(prefix ? { prefix } : {}),\n });\n }\n\n @Get('uploads/:id')\n upload(@Param('id') id: string): Promise<UploadDetailResponse> {\n return this.service.uploadDetail(id);\n }\n\n @Get('library/collections')\n collections(): Promise<CollectionsResponse> {\n return this.service.listCollections();\n }\n\n @Get('library')\n library(\n @Query('collection') collection?: string,\n @Query('disk') disk?: string,\n @Query('cursor') cursor?: string,\n @Query('limit') limit?: string,\n ): Promise<LibraryListResponse> {\n const limitValue = toLimit(limit);\n return this.service.listLibrary({\n ...(collection ? { collection } : {}),\n ...(disk ? { disk } : {}),\n ...(cursor ? { cursor } : {}),\n ...(limitValue !== undefined ? { limit: limitValue } : {}),\n });\n }\n\n @Get('library/:id')\n libraryRecord(@Param('id') id: string): Promise<LibraryDetailResponse> {\n return this.service.libraryDetail(id);\n }\n\n @Get('topology')\n topology(): Topology {\n return this.service.topology();\n }\n}\n","/** What a download is called when the key cannot name it — a folder marker, a blank key. */\nconst FALLBACK_FILENAME = 'download';\n\n/**\n * RFC 5987 `attr-char`: the only bytes that may stand unescaped inside an `ext-value`. Everything\n * else is percent-encoded. `encodeURIComponent` alone is not enough — it leaves `'`, `(`, `)` and\n * `*` intact, and a bare `'` closes the ext-value early, truncating the name from that point on.\n */\nconst ATTR_CHAR = /^[A-Za-z0-9!#$&+\\-.^_`|~]$/;\n\n/** Percent-encode a name into an RFC 5987 `ext-value` payload (the part after `UTF-8''`). */\nfunction toExtValue(name: string): string {\n let encoded = '';\n for (const byte of new TextEncoder().encode(name)) {\n const char = String.fromCharCode(byte);\n encoded += ATTR_CHAR.test(char) ? char : `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;\n }\n return encoded;\n}\n\n/** The name to offer for an object key — its last segment, or a fallback when it has none. */\nexport function downloadFilename(key: string): string {\n const segment = key.split('/').pop() ?? '';\n return segment.trim() === '' ? FALLBACK_FILENAME : segment;\n}\n\n/**\n * Build a `Content-Disposition` that makes the browser SAVE the body under `filename`, in both the\n * forms RFC 6266 defines: a quoted ASCII `filename` every user agent understands, and the\n * `filename*` ext-value that carries the real, possibly non-ASCII name for those that prefer it.\n *\n * The quoted form is a header field value, so it is stripped down to what one can legally hold:\n * control characters (which would forge a header break), `\"` and `\\` (which would close the quoted\n * string and let a crafted object key append parameters of its own), and anything outside printable\n * ASCII. That strip is a security boundary, not tidiness — the name reaching here comes from a\n * storage key, which the console does not control.\n */\nexport function attachmentDisposition(filename: string): string {\n const name = filename.trim() === '' ? FALLBACK_FILENAME : filename;\n const ascii = name\n .replace(/[^\\x20-\\x7E]/g, '')\n .replace(/[\"\\\\]/g, '')\n .trim();\n const quoted = ascii === '' ? FALLBACK_FILENAME : ascii;\n return `attachment; filename=\"${quoted}\"; filename*=UTF-8''${toExtValue(name)}`;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { basename, extname, join, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n Controller,\n Get,\n Header,\n Inject,\n Logger,\n NotFoundException,\n Optional,\n Param,\n Req,\n Res,\n StreamableFile,\n} from '@nestjs/common';\nimport type { ResolvedConsoleAuth } from './auth/config.js';\nimport { parseCookieHeader } from './auth/cookie-header.js';\nimport { readCookieHeader } from './auth/request.js';\nimport { responseAlreadyWritten, sendHtml } from './auth/response.js';\nimport { SESSION_COOKIE_NAME } from './auth/session-cookie-io.js';\nimport { verifySessionCookie } from './auth/session-cookie.js';\nimport {\n MEDIA_CONSOLE_AUTH,\n MEDIA_DASHBOARD_API_PATH,\n MEDIA_DASHBOARD_BASE_PATH,\n} from './tokens.js';\n\n/** The base the SPA bundle was built with (Vite `base`); rewritten to the configured base at serve time. */\nconst BUILD_BASE = '/media';\n\n/** dist/server/index.js -> ../spa (the Vite build output). */\nfunction spaDir(): string {\n return fileURLToPath(new URL('../spa', import.meta.url));\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.json': 'application/json; charset=utf-8',\n '.woff2': 'font/woff2',\n '.ico': 'image/x-icon',\n};\n\n/**\n * Serves the bundled /media console SPA at the configured base (+ hashed assets at `<base>/assets`).\n * The path comes from `RouterModule` (set by `MediaDashboardModule.forRoot({ basePath })`), so the\n * controller routes are relative.\n */\n@Controller()\nexport class MediaDashboardUiController {\n private readonly dir = spaDir();\n private readonly logger = new Logger(MediaDashboardUiController.name);\n private warned = false;\n\n constructor(\n @Inject(MEDIA_DASHBOARD_BASE_PATH) private readonly basePath: string,\n @Inject(MEDIA_DASHBOARD_API_PATH) private readonly apiBasePath: string,\n @Optional()\n @Inject(MEDIA_CONSOLE_AUTH)\n private readonly auth: ResolvedConsoleAuth | null = null,\n ) {}\n\n // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = \"stuck\n // loading after a deploy\"). The hashed assets below are immutable. `sendHtml` sets both headers;\n // they are no longer `@Header` decorators because this route is now non-passthrough (see below).\n //\n // Non-passthrough `@Res()` so a host's `auth.unauthenticatedPage` can OWN the response and serve\n // its own page in place of the SPA. Without a hook this behaves exactly as before: the shell is\n // served to everyone, and the SPA's own auth screen renders once its API calls come back 401.\n @Get()\n async index(@Req() req: unknown, @Res() res: unknown): Promise<void> {\n if (await this.serveUnauthenticatedPage(req, res)) return;\n\n const indexPath = join(this.dir, 'index.html');\n if (!existsSync(indexPath)) {\n throw new NotFoundException('Console SPA is not built. Run the package build.');\n }\n // Built with Vite base `/media/`; rewrite asset URLs to the configured base so the SPA loads\n // from `<base>/assets` wherever it's mounted, and tell the client its API base.\n const html = readFileSync(indexPath, 'utf8').replaceAll(\n `=\"${BUILD_BASE}/`,\n `=\"${this.basePath}/`,\n );\n const inject = `<script>window.__MEDIA_BASE__='${this.basePath}';window.__MEDIA_API__='${this.apiBasePath}';</script>`;\n sendHtml(\n res,\n 200,\n html.includes('</head>') ? html.replace('</head>', `${inject}</head>`) : inject + html,\n );\n }\n\n /**\n * Hand the response to the host's `unauthenticatedPage` when this navigation has no valid session.\n * Returns `true` when the host wrote a page (the caller must not also serve the SPA).\n *\n * Mode-A-only, deliberately: with `login` configured the SPA's own login form is the way in, so\n * replacing the shell would lock a Mode B host out of its own console.\n */\n private async serveUnauthenticatedPage(req: unknown, res: unknown): Promise<boolean> {\n const auth = this.auth;\n const hook = auth?.unauthenticatedPage;\n if (!auth || !hook) return false;\n if (auth.modes.includes('login')) return false;\n if (this.hasValidSession(auth, req)) return false;\n\n try {\n await hook({ request: req, response: res, basePath: this.basePath });\n if (responseAlreadyWritten(res)) return true;\n // Returned without writing. Serving the SPA is the only outcome that neither hangs the\n // request nor invents a page the host did not ask for — but it is almost certainly a bug.\n this.warnOnce(\n 'auth.unauthenticatedPage returned without writing a response; serving the console SPA ' +\n 'instead. The hook must write AND end the response it is given.',\n );\n } catch (error) {\n this.warnOnce(\n `auth.unauthenticatedPage threw; serving the console SPA instead. ${String(error)}`,\n );\n // It may have written headers before throwing; writing again would raise\n // ERR_HTTP_HEADERS_SENT on top of the original failure.\n if (responseAlreadyWritten(res)) return true;\n }\n // Falling back to the SPA is safe: every data route stays behind MediaConsoleGuard, so the\n // visitor gets the built-in auth screen, not the console's contents.\n return false;\n }\n\n private hasValidSession(auth: ResolvedConsoleAuth, req: unknown): boolean {\n const cookieValue = parseCookieHeader(readCookieHeader(req))[SESSION_COOKIE_NAME];\n if (cookieValue === undefined) return false;\n return verifySessionCookie(cookieValue, { secret: auth.secret }) !== null;\n }\n\n /** Once per process, not once per request: a broken hook fires on every unauthenticated visit. */\n private warnOnce(message: string): void {\n if (this.warned) return;\n this.warned = true;\n this.logger.warn(message);\n }\n\n @Get('assets/:file')\n @Header('Cache-Control', 'public, max-age=31536000, immutable')\n asset(@Param('file') file: string): StreamableFile {\n const safe = basename(file);\n if (safe !== file) throw new NotFoundException();\n const root = resolve(this.dir, 'assets');\n const assetPath = resolve(root, safe);\n if (!assetPath.startsWith(root + sep) || !existsSync(assetPath)) {\n throw new NotFoundException();\n }\n const type = CONTENT_TYPES[extname(safe)] ?? 'application/octet-stream';\n return new StreamableFile(readFileSync(assetPath), { type });\n }\n}\n"],"mappings":"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","names":["getImportMetaUrl","document","URL","__filename","href","currentScript","src","baseURI","importMetaUrl","import_common","DEFAULT_TTL_MS","DURATION_UNITS","s","m","h","d","durationToMs","ttl","undefined","match","exec","trim","unit","Number","resolveConsoleAuth","options","secret","Error","modes","session","push","login","length","unauthenticatedPage","ttlMs","revalidate","GUARDS_METADATA","isGuardClass","guard","baseGuards","controller","Reflect","getOwnMetadata","stampGuards","guards","entries","undefined","length","base","defineMetadata","import_common","import_common","parseCookieHeader","header","cookies","segment","split","eq","indexOf","name","slice","trim","rawValue","length","startsWith","endsWith","decodeURIComponent","serializeSetCookie","value","options","parts","clear","encodeURIComponent","push","path","secure","maxAgeSeconds","join","isRecord","value","readHeaders","request","headers","headerToString","Array","isArray","every","part","join","undefined","readCookieHeader","cookie","isHttpsRequest","protocol","forwarded","split","map","proto","trim","toLowerCase","includes","attachSession","session","consoleSession","isRecord","value","isRawNodeResponse","getHeader","setHeader","resolveRawResponse","response","raw","existingSetCookies","current","Array","isArray","appendSetCookie","cookie","resolveEndableResponse","isEndable","end","responseAlreadyWritten","headersSent","sendHtml","status","html","statusCode","EXP_GRACE_MS","base64urlEncode","value","Buffer","from","toString","base64urlDecode","hmac","secret","payload","createHmac","update","digest","signSessionCookie","user","options","issuedAt","now","Date","session","sub","id","name","undefined","roles","iat","exp","ttlMs","encodedPayload","JSON","stringify","isSessionPayload","record","Array","isArray","every","role","Number","isFinite","verifySessionCookie","dot","indexOf","length","slice","provided","expected","timingSafeEqual","decoded","parse","SESSION_COOKIE_NAME","issueSessionCookie","user","context","value","signSessionCookie","secret","auth","ttlMs","now","undefined","appendSetCookie","response","serializeSetCookie","path","cookiePath","maxAgeSeconds","Math","floor","secure","isHttpsRequest","request","clearSessionCookie","clear","MEDIA_STORE","Symbol","for","MEDIA_UPLOAD_SESSIONS","MEDIA_STORAGE_SHARED","MEDIA_DASHBOARD_BASE_PATH","MEDIA_DASHBOARD_API_PATH","MEDIA_DASHBOARD_ACTIONS","MEDIA_CONSOLE_AUTH","MEDIA_CONSOLE_COOKIE_PATH","MEDIA_CONSOLE_OBJECT_INSIGHTS","MEDIA_CONSOLE_OBJECT_URLS","MediaConsoleGuard","auth","cookiePath","canActivate","context","http","switchToHttp","request","getRequest","session","verifyRequestSession","UnauthorizedException","attachSession","maybeRenew","getResponse","cookieValue","parseCookieHeader","readCookieHeader","SESSION_COOKIE_NAME","undefined","verifySessionCookie","secret","response","now","Date","iat","ttlMs","user","id","sub","name","roles","revalidate","allowed","clearSessionCookie","issueSessionCookie","import_common","SAFE_LINK_SCHEMES","Set","sanitizeInsight","insight","links","undefined","filter","link","href","startsWith","has","URL","protocol","objectProxyUrl","apiBasePath","route","disk","key","path","encodeURIComponent","URL_TTL_SECONDS","DEFAULT_PAGE_LIMIT","MAX_UPLOAD_BYTES","DELETE_SWEEP_LIMIT","MAX_CROSS_DISK_BYTES","lastSegment","prefix","trimmed","replace","slash","lastIndexOf","slice","RangeNotSatisfiableException","HttpException","size","statusCode","HttpStatus","REQUESTED_RANGE_NOT_SATISFIABLE","message","error","resolveRange","range","Number","isInteger","suffixLength","start","Math","max","end","undefined","min","mapUpload","session","percent","round","offset","id","disk","key","parts","multipart","multipartUploadId","createdAt","toISOString","mapRecord","record","ownerType","ownerId","collection","name","fileName","mimeType","path","MediaConsoleService","storage","store","uploads","actionsEnabled","insightProviders","objectUrls","objectUrl","driver","strategy","Promise","resolve","objectProxyUrl","apiBasePath","capabilities","presign","temporaryUrl","url","logger","Logger","diskOrThrow","diskNames","includes","NotFoundException","listDisks","disks","defaultDisk","map","default","listObjects","options","result","list","delimiter","cursor","limit","folders","filter","folder","files","entry","sizeBytes","lastModified","objectDetail","stat","contentType","objectInsights","providers","length","insights","settled","all","provider","warn","Error","String","insight","sanitizeInsight","objectStream","stream","ranged","NotImplementedException","resolved","putObject","body","chunks","total","chunk","buffer","Buffer","isBuffer","from","PayloadTooLargeException","push","put","concat","createFolder","normalized","BadRequestException","alloc","deleteObject","delete","deleteFolder","sweepPrefix","copyObject","fromDisk","toDisk","to","transferKey","moveObject","op","fromDriver","move","copy","toDriver","bytes","get","moveFolder","transferFolder","copyFolder","fromNormalized","toNormalized","fromPrefix","toPrefix","startsWith","destKey","listUploads","sessions","keyPrefix","uploadDetail","listParts","upload","abortUpload","listCollections","aggregate","collections","buckets","groupBy","sum","listLibrary","records","page","libraryDetail","find","variants","Object","entries","conversions","conversion","deleteLibraryRecord","topology","hasStore","hasUploads","actions","MediaConsoleActionsController","service","deleteObject","disk","key","copyObject","body","from","toDisk","to","moveObject","moveFolder","copyFolder","uploadObject","request","type","putObject","createFolder","prefix","deleteFolder","abortUpload","id","deleteLibraryRecord","import_common","MediaConsoleAuthController","logger","Logger","name","warnedHooks","Set","auth","cookiePath","me","request","authRequired","cookieValue","parseCookieHeader","readCookieHeader","SESSION_COOKIE_NAME","session","undefined","verifySessionCookie","secret","UnauthorizedException","modes","user","id","sub","roles","login","body","response","requireAuth","NotFoundException","username","password","BadRequestException","runHook","message","mint","logout","clearSessionCookie","issueSessionCookie","kind","run","error","has","add","warn","String","passthrough","import_common","FALLBACK_FILENAME","ATTR_CHAR","toExtValue","name","encoded","byte","TextEncoder","encode","char","String","fromCharCode","test","toString","toUpperCase","padStart","downloadFilename","key","segment","split","pop","trim","attachmentDisposition","filename","ascii","replace","quoted","toLimit","value","undefined","parsed","Number","isFinite","Math","floor","SINGLE_BYTE_RANGE","parseRangeHeader","header","match","exec","trim","startText","endText","suffixLength","start","end","setHeader","response","name","raw","setStatus","code","status","statusCode","MediaConsoleReadController","service","disks","listDisks","objects","disk","prefix","cursor","limit","limitValue","listObjects","object","key","objectDetail","objectInsights","objectRaw","rangeHeader","streamObject","objectDownload","attachmentDisposition","downloadFilename","disposition","requested","result","objectStream","error","RangeNotSatisfiableException","size","stream","contentType","range","StreamableFile","type","length","uploads","listUploads","upload","id","uploadDetail","collections","listCollections","library","collection","listLibrary","libraryRecord","libraryDetail","topology","passthrough","READ_BASE_GUARDS","baseGuards","MediaConsoleReadController","ACTIONS_BASE_GUARDS","MediaConsoleActionsController","MediaConsoleApiModule","register","options","stampGuards","guards","module","imports","controllers","MediaConsoleAuthController","actions","providers","MediaConsoleService","MediaConsoleGuard","provide","MEDIA_DASHBOARD_ACTIONS","useValue","MEDIA_CONSOLE_COOKIE_PATH","cookiePath","MEDIA_CONSOLE_OBJECT_URLS","objectUrls","authProvider","insightsProvider","MEDIA_CONSOLE_OBJECT_INSIGHTS","filter","isGuardClass","exports","MEDIA_CONSOLE_AUTH","import_common","BUILD_BASE","spaDir","fileURLToPath","URL","CONTENT_TYPES","MediaDashboardUiController","dir","logger","Logger","name","warned","basePath","apiBasePath","auth","index","req","res","serveUnauthenticatedPage","indexPath","join","existsSync","NotFoundException","html","readFileSync","replaceAll","inject","sendHtml","includes","replace","hook","unauthenticatedPage","modes","hasValidSession","request","response","responseAlreadyWritten","warnOnce","error","String","cookieValue","parseCookieHeader","readCookieHeader","SESSION_COOKIE_NAME","undefined","verifySessionCookie","secret","message","warn","asset","file","safe","basename","root","resolve","assetPath","startsWith","sep","type","extname","StreamableFile","normalize","path","replace","MediaDashboardModule","forRoot","options","apiBasePath","basePath","build","provide","MEDIA_CONSOLE_AUTH","useValue","resolveConsoleAuth","auth","MEDIA_CONSOLE_OBJECT_INSIGHTS","objectInsights","imports","forRootAsync","authProvider","inject","useFactory","deps","useAuth","insightsProvider","injectObjectInsights","useObjectInsights","undefined","actions","stampGuards","guards","MediaDashboardUiController","module","MediaConsoleApiModule","register","objectUrls","strategy","cookiePath","RouterModule","controllers","providers","MEDIA_DASHBOARD_BASE_PATH","MEDIA_DASHBOARD_API_PATH","filter","isGuardClass","exports"]}
1
+ {"version":3,"sources":["../../src/server/index.ts","../../../../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","../../src/server/media-dashboard.module.ts","../../src/server/auth/config.ts","../../src/server/guards.ts","../../src/server/media-console-api.module.ts","../../src/server/media-console-actions.controller.ts","../../src/server/media-console.guard.ts","../../src/server/auth/cookie-header.ts","../../src/server/auth/request.ts","../../src/server/auth/response.ts","../../src/server/auth/session-cookie.ts","../../src/server/auth/session-cookie-io.ts","../../src/server/tokens.ts","../../src/server/media-console.service.ts","../../src/server/object-insights.ts","../../src/server/object-urls.ts","../../src/server/media-console-auth.controller.ts","../../src/server/media-console-read.controller.ts","../../src/server/content-disposition.ts","../../src/server/media-dashboard-ui.controller.ts"],"sourcesContent":["export {\n MediaDashboardModule,\n type MediaDashboardOptions,\n type MediaDashboardAsyncOptions,\n} from './media-dashboard.module.js';\nexport { MediaConsoleApiModule } from './media-console-api.module.js';\nexport type { ObjectUrlConfig, ObjectUrlStrategy } from './object-urls.js';\nexport { MediaConsoleService } from './media-console.service.js';\nexport type {\n ConsoleAuthOptions,\n LoginHook,\n SessionHook,\n} from './auth/config.js';\nexport type { ConsoleSessionUser } from './auth/session-cookie.js';\nexport {\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOAD_SESSIONS,\n} from './tokens.js';\n// The API response types (also published at the `./client` entry) for host reuse.\nexport type * from '../client/types.js';\nexport type {\n ObjectInsight,\n ObjectInsightContext,\n ObjectInsightFact,\n ObjectInsightLink,\n ObjectInsightProvider,\n ObjectInsightsResponse,\n} from './object-insights.js';\n","// Shim globals in cjs bundle\n// There's a weird bug that esbuild will always inject importMetaUrl\n// if we export it as `const importMetaUrl = ... __filename ...`\n// But using a function will not cause this issue\n\nconst getImportMetaUrl = () => \n typeof document === \"undefined\" \n ? new URL(`file:${__filename}`).href \n : (document.currentScript && document.currentScript.tagName.toUpperCase() === 'SCRIPT') \n ? document.currentScript.src \n : new URL(\"main.js\", document.baseURI).href;\n\nexport const importMetaUrl = /* @__PURE__ */ getImportMetaUrl()\n","import {\n type CanActivate,\n type DynamicModule,\n Module,\n type ModuleMetadata,\n type OptionalFactoryDependency,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport type { InjectionToken } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport { type ConsoleAuthOptions, resolveConsoleAuth } from './auth/config.js';\nimport { isGuardClass, stampGuards } from './guards.js';\nimport { MediaConsoleApiModule } from './media-console-api.module.js';\nimport { MediaDashboardUiController } from './media-dashboard-ui.controller.js';\nimport type { ObjectInsightProvider } from './object-insights.js';\nimport type { ObjectUrlStrategy } from './object-urls.js';\nimport {\n MEDIA_CONSOLE_AUTH,\n MEDIA_CONSOLE_OBJECT_INSIGHTS,\n MEDIA_DASHBOARD_API_PATH,\n MEDIA_DASHBOARD_BASE_PATH,\n} from './tokens.js';\n\nexport interface MediaDashboardOptions {\n /**\n * Where the SPA (UI) is served. Default `/media`. A page route — keep it out of an `/api`\n * prefix so it reads as a UI, not an endpoint.\n */\n basePath?: string;\n /**\n * Where the JSON API is mounted (what the SPA fetches). Default `<basePath>/api`. Set it under\n * your app's `/api` prefix — e.g. `/api/media/console` — so the API inherits the app's\n * auth/proxy rules while the UI stays at `basePath`.\n */\n apiBasePath?: string;\n /**\n * Enable the destructive endpoints (delete object/record, copy/move, abort upload). Default\n * `false` — the read API is always available. Front the mount with your own guard either way.\n */\n actions?: boolean;\n /**\n * How the console reports the `url` for an object's bytes — the \"Open ↗\" link, and an image\n * variant's `src`. Default `'auto'`: presigned straight to the object store when the driver can\n * mint one. Set `'proxy'` when the browser has no path to the store (no network route, no CORS\n * grant, a policy against client-to-bucket traffic), and those URLs are routed same-origin\n * through this server instead. See {@link ObjectUrlStrategy}.\n *\n * Downloading is unaffected: `disks/:disk/object/download` is always same-origin.\n */\n objectUrls?: ObjectUrlStrategy;\n /**\n * Gate the console (SPA + API) behind a built-in session-cookie login, telescope-style. Omit to\n * leave the console open (front it with your own guard). When set, the SPA renders a login screen\n * until a valid cookie exists; supply a `login(username, password)` and/or `session(request)`\n * hook that returns a session user (or `null` to deny), and optionally a `revalidate(session)`\n * hook that re-checks the user when the cookie is slid forward — see {@link ConsoleAuthOptions}.\n */\n auth?: ConsoleAuthOptions;\n /**\n * Guard classes (or already-instantiated `CanActivate`s) fronting BOTH the page/asset controller\n * (`MediaDashboardUiController` — a plain REPLACE, since it ships with no guard of its own) and\n * the read/action JSON API controllers (APPENDED to their own built-in `MediaConsoleGuard`\n * session-cookie gate — a request must pass BOTH). Deliberately NOT applied to the auth\n * controller that MINTS that session cookie — see `MediaConsoleApiModule`'s own `guards` doc.\n *\n * These two surfaces (page vs. API) live in the same package here, unlike a split dashboard —\n * but they're still SEPARATE controllers on separate host modules, so `guards` is stamped onto\n * each independently by this one option.\n *\n * Full-page navigations to `basePath` carry only cookies, never an `Authorization` header — a\n * guard passed here must be able to authenticate from a cookie (see the \"Securing the console\"\n * guide) or it will 401 every browser visit to the console, even an already-logged-in admin's.\n *\n * `guards` and `auth` (the built-in cookie login) are independent and compose: with both set, a\n * request must pass the host guard AND (when a route carries `MediaConsoleGuard`) present a valid\n * built-in session cookie too. Configure one, the other, or both.\n *\n * A guard's own DEPENDENCIES resolve from this option's `imports` (see {@link imports}) — the\n * dashboard module has no application context of its own to pull them from otherwise.\n */\n guards?: Array<Type<CanActivate> | CanActivate>;\n /**\n * Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for a class\n * passed to {@link guards} (or any other provider the controllers need reachable). Typically the\n * host's own auth module, e.g. `imports: [AuthModule]` alongside `guards: [SessionGuard]`.\n */\n imports?: DynamicModule['imports'];\n /**\n * Host-supplied context about a disk object, rendered in the console's preview — see\n * {@link ObjectInsightProvider}. The console can only describe a file as storage sees it; this is\n * how it learns what the file *means* to your app. Omit for no annotation (the default).\n *\n * Use `forRootAsync`'s `useObjectInsights` when a provider needs injected services.\n */\n objectInsights?: ObjectInsightProvider[];\n}\n\n/**\n * Async variant of {@link MediaDashboardOptions}. The mount paths + `actions` stay static (the\n * router needs them at module-definition time), but `auth` is built by an injected factory — so\n * the `login`/`session` hooks can reach your DB/services (e.g. an EntityManager to validate an\n * admin). Returning `undefined` from the factory leaves the console open.\n */\nexport interface MediaDashboardAsyncOptions {\n basePath?: string;\n apiBasePath?: string;\n actions?: boolean;\n /** See {@link MediaDashboardOptions.objectUrls}. Static like the mount paths and `actions` —\n * it is read at module-definition time, not per request. */\n objectUrls?: ObjectUrlStrategy;\n /** Modules exporting the providers `inject` needs, or a guard class's own dependencies (omit\n * when they're global). Shared with {@link guards}. */\n imports?: DynamicModule['imports'];\n /** Providers injected into `useAuth`, in order. */\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\n /** Build the `auth` config from injected deps (or `undefined` to leave the console open). */\n useAuth: (\n ...deps: any[]\n ) => ConsoleAuthOptions | undefined | Promise<ConsoleAuthOptions | undefined>;\n /**\n * Guard classes (or instances) fronting the console — see {@link MediaDashboardOptions.guards}.\n * Passed here directly (not resolved via `useAuth`'s factory) because guard stamping happens at\n * module-build time, synchronously — the SAME constraint as `basePath`/`apiBasePath` above. A\n * class guard's own dependencies resolve from `imports` above (shared with `useAuth`'s `inject`).\n */\n guards?: Array<Type<CanActivate> | CanActivate>;\n /**\n * Build the {@link MediaDashboardOptions.objectInsights} providers from injected deps — the usual\n * case, since a provider that says anything interesting needs a repository or a service.\n */\n useObjectInsights?: (\n ...deps: any[]\n ) => ObjectInsightProvider[] | Promise<ObjectInsightProvider[]>;\n /**\n * Providers injected into `useObjectInsights`, in order. Defaults to {@link inject} — its own\n * list because the insight providers rarely want the same dependencies the auth hooks do, and\n * sharing one list would force every host to inject the union of both.\n */\n injectObjectInsights?: Array<InjectionToken | OptionalFactoryDependency>;\n}\n\n/** Leading slash, no trailing slash. */\nfunction normalize(path: string): string {\n return `/${path.replace(/^\\/+|\\/+$/g, '')}`;\n}\n\n/**\n * Mounts the /media console: the bundled React SPA at `basePath` and its JSON API at `apiBasePath`\n * (default `<basePath>/api`). Import via `MediaDashboardModule.forRoot(...)` (or `forRootAsync` when\n * the `auth` hooks need injected services) alongside `MediaModule` (global), so it resolves the\n * storage/store/upload tokens. Exclude the UI/API paths from any global `/api` prefix.\n *\n * Front it with your own auth via `guards` (+ `imports` for their dependencies) — see\n * {@link MediaDashboardOptions.guards} — instead of, or alongside, the built-in `auth` cookie login.\n */\n@Module({})\nexport class MediaDashboardModule {\n static forRoot(options: MediaDashboardOptions = {}): DynamicModule {\n const apiBasePath = normalize(\n options.apiBasePath ?? `${normalize(options.basePath ?? '/media')}/api`,\n );\n return MediaDashboardModule.build(\n options,\n apiBasePath,\n { provide: MEDIA_CONSOLE_AUTH, useValue: resolveConsoleAuth(options.auth) },\n { provide: MEDIA_CONSOLE_OBJECT_INSIGHTS, useValue: options.objectInsights ?? [] },\n options.imports,\n );\n }\n\n static forRootAsync(options: MediaDashboardAsyncOptions): DynamicModule {\n const apiBasePath = normalize(\n options.apiBasePath ?? `${normalize(options.basePath ?? '/media')}/api`,\n );\n const authProvider: Provider = {\n provide: MEDIA_CONSOLE_AUTH,\n inject: options.inject ?? [],\n useFactory: async (...deps: any[]) => resolveConsoleAuth(await options.useAuth(...deps)),\n };\n const insightsProvider: Provider = {\n provide: MEDIA_CONSOLE_OBJECT_INSIGHTS,\n inject: options.injectObjectInsights ?? options.inject ?? [],\n useFactory: async (...deps: any[]) =>\n options.useObjectInsights === undefined ? [] : await options.useObjectInsights(...deps),\n };\n return MediaDashboardModule.build(\n options,\n apiBasePath,\n authProvider,\n insightsProvider,\n options.imports,\n );\n }\n\n /** Shared wiring: static routing + the API module, with `auth` supplied by the given provider. */\n private static build(\n options: {\n basePath?: string;\n apiBasePath?: string;\n actions?: boolean;\n objectUrls?: ObjectUrlStrategy;\n guards?: Array<Type<CanActivate> | CanActivate>;\n },\n apiBasePath: string,\n authProvider: Provider,\n insightsProvider: Provider,\n imports?: DynamicModule['imports'],\n ): DynamicModule {\n const basePath = normalize(options.basePath ?? '/media');\n const actions = options.actions === true;\n // MediaDashboardUiController ships with no guard of its own, so this is a plain REPLACE (base\n // `[]`) — matches `MediaConsoleApiModule`'s own `stampGuards` call for the read/action\n // controllers, which DOES have a built-in base to append onto.\n stampGuards(options.guards, [[MediaDashboardUiController, []]]);\n return {\n module: MediaDashboardModule,\n imports: [\n ...(imports ?? []),\n MediaConsoleApiModule.register({\n actions,\n objectUrls: { strategy: options.objectUrls ?? 'auto', apiBasePath },\n // `Path=/`, deliberately NOT scoped to `apiBasePath` (nor to `basePath`): the two mounts\n // are independently configurable and can live at unrelated paths (e.g. a `/media` UI\n // with a `/api/media/console` API). Scoped to the API base, the browser withheld the\n // cookie on the full-page navigation to `basePath`, so the freshly minted session could\n // not open the console it was minted for — the launcher landed on the unauthenticated\n // page. Scoping to `basePath` instead would just move the failure to the API calls.\n // The cookie is HttpOnly + signed + short-lived, so the broader scope is a reasonable\n // trade for being correct in any mount configuration. Matches nestjs-durable-dashboard\n // and nestjs-agent-dashboard; telescope can scope to its mount because its UI and API\n // always share one root.\n cookiePath: '/',\n authProvider,\n insightsProvider,\n imports,\n ...(options.guards ? { guards: options.guards } : {}),\n }),\n RouterModule.register([\n { path: basePath, module: MediaDashboardModule }, // the UI controller below\n { path: apiBasePath, module: MediaConsoleApiModule },\n ]),\n ],\n controllers: [MediaDashboardUiController],\n providers: [\n { provide: MEDIA_DASHBOARD_BASE_PATH, useValue: basePath },\n { provide: MEDIA_DASHBOARD_API_PATH, useValue: apiBasePath },\n // MediaDashboardUiController is hosted HERE, so its guards DI-instantiate from this\n // module — class guards need a provider; an already-instantiated guard needs none.\n ...(options.guards ?? []).filter(isGuardClass),\n ],\n // Re-export the API module so its MediaConsoleService reaches importers if they want it.\n exports: [MediaConsoleApiModule],\n };\n }\n}\n","import type { ConsoleSessionUser } from './session-cookie.js';\n\n/** Host hook for Mode A — validates the host's own auth on the raw request. */\nexport type SessionHook = (\n request: unknown,\n) => Promise<ConsoleSessionUser | null> | ConsoleSessionUser | null;\n\n/** Host hook for Mode B — validates submitted credentials from the built-in login screen. */\nexport type LoginHook = (\n username: string,\n password: string,\n) => Promise<ConsoleSessionUser | null> | ConsoleSessionUser | null;\n\n/**\n * Re-checks a LIVE session when the cookie is slid forward. Runs at most once per `ttl/2` per\n * session, so a DB round-trip here is cheap. Return `false` to revoke (the cookie is cleared and\n * the request denied). Distinct from `session`: that hook reads the host's auth off a fresh\n * request, which a console XHR does not carry — this one receives the already-minted session.\n */\nexport type RevalidateHook = (session: ConsoleSessionUser) => Promise<boolean> | boolean;\n\n/** What `unauthenticatedPage` receives. An object (not positional args) so fields can be added later. */\nexport interface UnauthenticatedPageContext {\n /**\n * The platform-native request — Express' `Request`, Fastify's `FastifyRequest`. Typed `unknown`\n * for the same reason `SessionHook`'s is: this package refuses to depend on either platform.\n */\n request: unknown;\n /**\n * The platform-native response. The hook OWNS it: it must write AND end it. If it returns without\n * writing, the console falls back to serving its own SPA (whose auth screen then renders).\n */\n response: unknown;\n /** Where this console is mounted (e.g. `/media`) — useful for a \"back to the console\" link. */\n basePath: string;\n}\n\n/**\n * Host-owned page for an unauthenticated navigation to the console SPA.\n *\n * Without it, an unauthenticated visitor gets the SPA shell, which then renders the built-in auth\n * screen: a generic \"open this console from your application\" card, generic because the library\n * cannot know who hosts it. This hook replaces that with the host's own page — and, because it is\n * decided BEFORE the shell is served, it also stops the bundle from loading for a visitor who has\n * no session at all.\n *\n * Only consulted under Mode-A-only. With `login` configured the SPA's own login form IS the way in,\n * so gating the shell would lock Mode B hosts out of their own console.\n *\n * Fail-closed by construction: it only ever runs when the request has no valid session. A hook that\n * throws, or returns without writing, falls back to serving the SPA — it cannot let anyone in,\n * because every data route stays behind `MediaConsoleGuard` regardless.\n */\nexport type UnauthenticatedPageHook = (context: UnauthenticatedPageContext) => void | Promise<void>;\n\n/** Author-facing `auth` option on `MediaDashboardModule.forRoot`. Mirrors telescope's dashboardAuth. */\nexport interface ConsoleAuthOptions {\n /** REQUIRED HMAC-SHA256 signing key. Missing/empty => boot error (fail closed). */\n secret: string;\n /** Cookie TTL as a duration string (`'8h'`, `'30m'`, `'7d'`). Default `'8h'`. */\n ttl?: string;\n /** Mode A: validate the host's own auth on the raw request. */\n session?: SessionHook;\n /** Mode B: validate credentials from the built-in login screen. */\n login?: LoginHook;\n /** Re-checks the session on sliding renewal. Not a mode — it cannot mint a session on its own. */\n revalidate?: RevalidateHook;\n /** Renders the host's own page for an unauthenticated navigation, in place of serving the SPA and\n * letting its built-in auth screen render; see `UnauthenticatedPageHook`. Mode-A-only. */\n unauthenticatedPage?: UnauthenticatedPageHook;\n}\n\nexport type AuthMode = 'session' | 'login';\n\n/** Resolved, validated console-auth config shared by the guard, controller, and SPA. */\nexport interface ResolvedConsoleAuth {\n secret: string;\n ttlMs: number;\n modes: AuthMode[];\n session?: SessionHook;\n login?: LoginHook;\n revalidate?: RevalidateHook;\n unauthenticatedPage?: UnauthenticatedPageHook;\n}\n\nconst DEFAULT_TTL_MS = 8 * 60 * 60 * 1000;\nconst DURATION_UNITS: Record<string, number> = {\n s: 1000,\n m: 60 * 1000,\n h: 60 * 60 * 1000,\n d: 24 * 60 * 60 * 1000,\n};\n\n/** Parse a `'<number><s|m|h|d>'` duration to ms; falls back to the 8h default on a bad value. */\nfunction durationToMs(ttl: string | undefined): number {\n if (ttl === undefined) return DEFAULT_TTL_MS;\n const match = /^(\\d+)([smhd])$/.exec(ttl.trim());\n if (!match) return DEFAULT_TTL_MS;\n const unit = DURATION_UNITS[match[2] ?? ''];\n if (unit === undefined) return DEFAULT_TTL_MS;\n return Number(match[1]) * unit;\n}\n\n/**\n * Validate + resolve the `auth` option. Returns `null` when unconfigured (the console stays open —\n * front it with your own guard). Throws at boot (fail closed) when configured but missing a secret\n * or any hook — the host learns immediately rather than shipping an un-mintable gate.\n */\nexport function resolveConsoleAuth(\n options: ConsoleAuthOptions | undefined,\n): ResolvedConsoleAuth | null {\n if (!options) return null;\n if (!options.secret) {\n throw new Error('MediaDashboardModule: auth.secret is required when `auth` is configured.');\n }\n const modes: AuthMode[] = [];\n if (options.session) modes.push('session');\n if (options.login) modes.push('login');\n if (modes.length === 0) {\n throw new Error('MediaDashboardModule: auth needs a `login` and/or `session` hook.');\n }\n if (\n options.unauthenticatedPage !== undefined &&\n typeof options.unauthenticatedPage !== 'function'\n ) {\n throw new Error('MediaDashboardModule: auth.unauthenticatedPage must be a function.');\n }\n return {\n secret: options.secret,\n ttlMs: durationToMs(options.ttl),\n modes,\n ...(options.session ? { session: options.session } : {}),\n ...(options.login ? { login: options.login } : {}),\n ...(options.revalidate ? { revalidate: options.revalidate } : {}),\n ...(options.unauthenticatedPage ? { unauthenticatedPage: options.unauthenticatedPage } : {}),\n };\n}\n","import 'reflect-metadata';\nimport type { CanActivate, Type } from '@nestjs/common';\n\n/**\n * `@nestjs/common`'s own `GUARDS_METADATA` key, INLINED rather than deep-imported from\n * '@nestjs/common/constants' — that subpath has no extension and a strict ESM resolver (which the\n * built dual ESM/CJS output of this package is loaded under) 404s on it. Same convention as\n * `@dudousxd/nestjs-agent`'s `agent-dashboard.module.ts` and `@dudousxd/nestjs-telescope`'s\n * `telescope.module.ts`. A drift spec imports the real constant (via the resolvable\n * `'@nestjs/common/constants.js'` subpath) and asserts this literal stays byte-identical to it.\n */\nexport const GUARDS_METADATA = '__guards__';\n\n/**\n * Narrows a `guards` entry to a class (constructor) as opposed to an already-instantiated\n * `CanActivate`. Only a class needs a DI provider so Nest can instantiate it — an instance is used\n * by the guards consumer as-is.\n */\nexport function isGuardClass(guard: Type<CanActivate> | CanActivate): guard is Type<CanActivate> {\n return typeof guard === 'function';\n}\n\n/**\n * Reads a controller's OWN `@UseGuards` metadata (not the inherited/prototype-chain one), or `[]`\n * when it carries none. MUST be called at module-load time — i.e. assigned to a top-level\n * `const` right after importing the controller — so it captures the pristine, decorator-defined\n * baseline BEFORE any `stampGuards` call below can mutate it.\n */\nexport function baseGuards(controller: Type): Array<Type<CanActivate> | CanActivate> {\n return Reflect.getOwnMetadata(GUARDS_METADATA, controller) ?? [];\n}\n\n/**\n * Stamp host guards onto a console controller — appending to each entry's captured `base` (its\n * own pristine `@UseGuards` metadata, or `[]` when it has none), never to whatever is CURRENTLY\n * stamped on the class. This package does not rebuild its controllers as a fresh subclass per\n * `forRoot`/`forRootAsync` call (unlike `@dudousxd/nestjs-telescope`'s `dynamicController`), so\n * appending onto LIVE metadata would compound across repeated calls against the same static class\n * (e.g. two tests in one file each calling `forRoot` with different `guards`). Recomputing from the\n * captured `base` every time keeps each call's result exactly `[...base, ...guards]`, independent of\n * whatever a prior call stamped.\n *\n * A no-op when `guards` is omitted/empty, leaving every controller's metadata exactly as its\n * decorator (or the absence of one) left it — so omitting `guards` reproduces today's behavior\n * byte-for-byte.\n */\nexport function stampGuards(\n guards: Array<Type<CanActivate> | CanActivate> | undefined,\n entries: Array<[controller: Type, base: Array<Type<CanActivate> | CanActivate>]>,\n): void {\n if (guards === undefined || guards.length === 0) return;\n for (const [controller, base] of entries) {\n Reflect.defineMetadata(GUARDS_METADATA, [...base, ...guards], controller);\n }\n}\n","import {\n type CanActivate,\n type DynamicModule,\n Module,\n type ModuleMetadata,\n type Provider,\n type Type,\n} from '@nestjs/common';\nimport { baseGuards, isGuardClass, stampGuards } from './guards.js';\nimport { MediaConsoleActionsController } from './media-console-actions.controller.js';\nimport { MediaConsoleAuthController } from './media-console-auth.controller.js';\nimport { MediaConsoleReadController } from './media-console-read.controller.js';\nimport { MediaConsoleGuard } from './media-console.guard.js';\nimport { MediaConsoleService } from './media-console.service.js';\nimport type { ObjectUrlConfig } from './object-urls.js';\nimport {\n MEDIA_CONSOLE_AUTH,\n MEDIA_CONSOLE_COOKIE_PATH,\n MEDIA_CONSOLE_OBJECT_INSIGHTS,\n MEDIA_CONSOLE_OBJECT_URLS,\n MEDIA_DASHBOARD_ACTIONS,\n} from './tokens.js';\n\n/**\n * `MediaConsoleReadController`/`MediaConsoleActionsController`'s own, pristine\n * `@UseGuards(MediaConsoleGuard)` metadata, captured once at module-load time — the baseline every\n * `register()` call's host `guards` append onto (see `stampGuards` in `./guards.js`).\n */\nconst READ_BASE_GUARDS = baseGuards(MediaConsoleReadController);\nconst ACTIONS_BASE_GUARDS = baseGuards(MediaConsoleActionsController);\n\ninterface ApiModuleOptions {\n actions: boolean;\n /** Cookie `Path` — the JSON API base — so the session cookie rides every console API request. */\n cookiePath: string;\n /** Provider for `MEDIA_CONSOLE_AUTH` — a `useValue` (forRoot) or a `useFactory` (forRootAsync). */\n authProvider: Provider;\n /** Provider for `MEDIA_CONSOLE_OBJECT_INSIGHTS` — the host's object-annotation providers.\n * Omitted binds the empty list, which is the no-op the console renders nothing for. */\n insightsProvider?: Provider;\n /** Resolved object-URL strategy + the API mount it builds proxy URLs against. */\n objectUrls: ObjectUrlConfig;\n /** Extra imports the auth factory's `inject` deps (or a guard class's own deps) live in. */\n imports?: ModuleMetadata['imports'];\n /**\n * Guard classes (or instances) fronting the read + action controllers — APPENDED to their own\n * built-in `MediaConsoleGuard` (session-cookie) gate, so a request must pass BOTH. Deliberately\n * NOT applied to `MediaConsoleAuthController`: it MINTS the session cookie the built-in gate (and\n * any host guard reusing that same cookie) checks for, so it can't be made to require the very\n * auth it grants — mirrors `@dudousxd/nestjs-telescope`'s own auth controller staying outside its\n * `stampGuards` call.\n */\n guards?: Array<Type<CanActivate> | CanActivate>;\n}\n\n/**\n * Holds the console's JSON API: the read controller (always), the actions controller (only when\n * `actions: true`), and the auth controller (always — it mints the session the guard checks, so it\n * is never itself guarded). The read/action controllers carry `MediaConsoleGuard`, a no-op unless\n * the host configured `auth`, plus any host `guards` (see {@link ApiModuleOptions.guards}) — both\n * gates must pass.\n */\n@Module({})\nexport class MediaConsoleApiModule {\n static register(options: ApiModuleOptions): DynamicModule {\n stampGuards(options.guards, [\n [MediaConsoleReadController, READ_BASE_GUARDS],\n [MediaConsoleActionsController, ACTIONS_BASE_GUARDS],\n ]);\n return {\n module: MediaConsoleApiModule,\n imports: options.imports ?? [],\n controllers: [\n MediaConsoleReadController,\n MediaConsoleAuthController,\n ...(options.actions ? [MediaConsoleActionsController] : []),\n ],\n providers: [\n MediaConsoleService,\n MediaConsoleGuard,\n { provide: MEDIA_DASHBOARD_ACTIONS, useValue: options.actions },\n { provide: MEDIA_CONSOLE_COOKIE_PATH, useValue: options.cookiePath },\n { provide: MEDIA_CONSOLE_OBJECT_URLS, useValue: options.objectUrls },\n options.authProvider,\n options.insightsProvider ?? {\n provide: MEDIA_CONSOLE_OBJECT_INSIGHTS,\n useValue: [],\n },\n // Class guards need a DI provider so Nest can instantiate them in THIS module's context\n // (where `imports` above resolves their dependencies). An already-instantiated guard needs\n // none — it's used as-is.\n ...(options.guards ?? []).filter(isGuardClass),\n ],\n // MEDIA_CONSOLE_AUTH is exported (not just provided) because MediaDashboardUiController —\n // hosted in MediaDashboardModule, which imports THIS module — reads it to decide whether to\n // hand an unauthenticated navigation to the host's `unauthenticatedPage`. Exporting keeps the\n // single `authProvider` instance shared; re-providing the same factory in both modules would\n // run a `forRootAsync` host's `useAuth` twice.\n exports: [MediaConsoleService, MEDIA_CONSOLE_AUTH],\n };\n }\n}\n","import type { Readable } from 'node:stream';\nimport {\n Body,\n Controller,\n Delete,\n HttpCode,\n Inject,\n Param,\n Post,\n Query,\n Req,\n UseGuards,\n} from '@nestjs/common';\nimport { MediaConsoleGuard } from './media-console.guard.js';\nimport { MediaConsoleService } from './media-console.service.js';\n\ninterface CopyMoveBody {\n from: string;\n to: string;\n /** Destination disk for a cross-disk copy/move. Omitted → same disk as the source (`:disk`). */\n toDisk?: string;\n}\n\ninterface CreateFolderBody {\n prefix: string;\n}\n\n/**\n * Destructive JSON API for the /media console. Only registered when the host opts in with\n * `MediaDashboardModule.forRoot({ actions: true })` (default off). Bare `@Controller()` — the path\n * prefix comes from `RouterModule`, sharing the API base with the read controller. Gated by\n * `MediaConsoleGuard` (session cookie) when the host configured `auth`.\n */\n@UseGuards(MediaConsoleGuard)\n@Controller()\nexport class MediaConsoleActionsController {\n constructor(@Inject(MediaConsoleService) private readonly service: MediaConsoleService) {}\n\n @Delete('disks/:disk/object')\n @HttpCode(204)\n deleteObject(@Param('disk') disk: string, @Query('key') key: string): Promise<void> {\n return this.service.deleteObject(disk, key);\n }\n\n @Post('disks/:disk/copy')\n @HttpCode(204)\n copyObject(@Param('disk') disk: string, @Body() body: CopyMoveBody): Promise<void> {\n return this.service.copyObject(disk, body.from, body.toDisk ?? disk, body.to);\n }\n\n @Post('disks/:disk/move')\n @HttpCode(204)\n moveObject(@Param('disk') disk: string, @Body() body: CopyMoveBody): Promise<void> {\n return this.service.moveObject(disk, body.from, body.toDisk ?? disk, body.to);\n }\n\n @Post('disks/:disk/move-folder')\n @HttpCode(204)\n moveFolder(@Param('disk') disk: string, @Body() body: CopyMoveBody): Promise<void> {\n return this.service.moveFolder(disk, body.from, body.toDisk ?? disk, body.to);\n }\n\n @Post('disks/:disk/copy-folder')\n @HttpCode(204)\n copyFolder(@Param('disk') disk: string, @Body() body: CopyMoveBody): Promise<void> {\n return this.service.copyFolder(disk, body.from, body.toDisk ?? disk, body.to);\n }\n\n /**\n * Uploads a file to `key` on the disk. The body is the raw bytes sent as `application/octet-stream`\n * so the host's JSON/urlencoded body parsers never consume the stream; the real MIME rides as the\n * `type` query param and becomes the stored object's Content-Type. `@Req()` is the request stream.\n */\n @Post('disks/:disk/upload')\n @HttpCode(204)\n uploadObject(\n @Param('disk') disk: string,\n @Query('key') key: string,\n @Req() request: Readable,\n @Query('type') type?: string,\n ): Promise<void> {\n return this.service.putObject(disk, key, request, type);\n }\n\n @Post('disks/:disk/folder')\n @HttpCode(204)\n createFolder(@Param('disk') disk: string, @Body() body: CreateFolderBody): Promise<void> {\n return this.service.createFolder(disk, body.prefix);\n }\n\n @Delete('disks/:disk/folder')\n @HttpCode(204)\n deleteFolder(@Param('disk') disk: string, @Query('prefix') prefix: string): Promise<void> {\n return this.service.deleteFolder(disk, prefix);\n }\n\n @Post('uploads/:id/abort')\n @HttpCode(204)\n abortUpload(@Param('id') id: string): Promise<void> {\n return this.service.abortUpload(id);\n }\n\n @Delete('library/:id')\n @HttpCode(204)\n deleteLibraryRecord(@Param('id') id: string): Promise<void> {\n return this.service.deleteLibraryRecord(id);\n }\n}\n","import {\n type CanActivate,\n type ExecutionContext,\n Inject,\n Injectable,\n Optional,\n UnauthorizedException,\n} from '@nestjs/common';\nimport type { ResolvedConsoleAuth } from './auth/config.js';\nimport { parseCookieHeader } from './auth/cookie-header.js';\nimport { attachSession, readCookieHeader } from './auth/request.js';\nimport {\n SESSION_COOKIE_NAME,\n clearSessionCookie,\n issueSessionCookie,\n} from './auth/session-cookie-io.js';\nimport {\n type ConsoleSession,\n type ConsoleSessionUser,\n verifySessionCookie,\n} from './auth/session-cookie.js';\nimport { MEDIA_CONSOLE_AUTH, MEDIA_CONSOLE_COOKIE_PATH } from './tokens.js';\n\n/**\n * Gates the console's read + action controllers on a valid session cookie — but ONLY when the host\n * configured `auth`. With no auth configured the resolved value is `null` and the guard is a\n * no-op (the console stays open; front it with your own guard). The auth controller that MINTS the\n * cookie is deliberately NOT decorated with this guard.\n */\n@Injectable()\nexport class MediaConsoleGuard implements CanActivate {\n constructor(\n @Optional() @Inject(MEDIA_CONSOLE_AUTH) private readonly auth: ResolvedConsoleAuth | null,\n @Optional() @Inject(MEDIA_CONSOLE_COOKIE_PATH) private readonly cookiePath: string | null,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n if (!this.auth) return true;\n const http = context.switchToHttp();\n const request = http.getRequest();\n const session = this.verifyRequestSession(request);\n // Absent/invalid/expired cookie => 401 (not 403): the SPA reads this as \"show the login screen\".\n if (!session) throw new UnauthorizedException();\n attachSession(request, session);\n if (!(await this.maybeRenew(http.getResponse(), request, session))) {\n // Revoked mid-session: same 401 as an absent cookie.\n throw new UnauthorizedException();\n }\n return true;\n }\n\n private verifyRequestSession(request: unknown): ConsoleSession | null {\n if (!this.auth) return null;\n const cookieValue = parseCookieHeader(readCookieHeader(request))[SESSION_COOKIE_NAME];\n if (cookieValue === undefined) return null;\n return verifySessionCookie(cookieValue, { secret: this.auth.secret });\n }\n\n /**\n * Sliding renewal + revalidation: when a valid cookie is past half its TTL, re-issue a fresh one\n * so active users never get logged out mid-session — but first let the host's `revalidate` hook\n * re-check the user, so a deactivated or demoted operator loses access instead of riding a\n * self-renewing cookie. Returns `false` when the session was revoked (cookie already cleared).\n */\n private async maybeRenew(\n response: unknown,\n request: unknown,\n session: ConsoleSession,\n ): Promise<boolean> {\n if (!this.auth) return true;\n const now = Date.now();\n if (now - session.iat <= this.auth.ttlMs / 2) return true;\n const user: ConsoleSessionUser = {\n id: session.sub,\n ...(session.name !== undefined ? { name: session.name } : {}),\n roles: session.roles,\n };\n if (this.auth.revalidate) {\n let allowed: boolean;\n try {\n allowed = await this.auth.revalidate(user);\n } catch {\n allowed = false; // Fail closed.\n }\n if (!allowed) {\n clearSessionCookie({ cookiePath: this.cookiePath ?? '/', request, response });\n return false;\n }\n }\n issueSessionCookie(user, {\n auth: this.auth,\n cookiePath: this.cookiePath ?? '/',\n request,\n response,\n now,\n });\n return true;\n }\n}\n","/**\n * Parse a raw `Cookie` request header into a name→value map. Dependency-free so it works on raw\n * Express AND Fastify requests without `cookie-parser`. Values are URL-decoded; malformed segments\n * are skipped, never thrown.\n */\nexport function parseCookieHeader(header: string | undefined): Record<string, string> {\n const cookies: Record<string, string> = {};\n if (typeof header !== 'string' || header === '') return cookies;\n for (const segment of header.split(';')) {\n const eq = segment.indexOf('=');\n if (eq <= 0) continue;\n const name = segment.slice(0, eq).trim();\n if (name === '') continue;\n let rawValue = segment.slice(eq + 1).trim();\n // Strip a single pair of wrapping double-quotes (RFC 6265 quoted form).\n if (rawValue.length >= 2 && rawValue.startsWith('\"') && rawValue.endsWith('\"')) {\n rawValue = rawValue.slice(1, -1);\n }\n // First occurrence wins; don't clobber an earlier value with a later dup.\n if (name in cookies) continue;\n try {\n cookies[name] = decodeURIComponent(rawValue);\n } catch {\n cookies[name] = rawValue;\n }\n }\n return cookies;\n}\n\nexport interface SetCookieOptions {\n path: string;\n maxAgeSeconds: number;\n secure: boolean;\n clear?: boolean;\n}\n\n/**\n * Serialize a `Set-Cookie` header value. Always `HttpOnly` + `SameSite=Lax` (Lax blocks\n * cross-site POSTs carrying the cookie — CSRF coverage for the console's mutation POSTs).\n * Platform-agnostic: just a string.\n */\nexport function serializeSetCookie(name: string, value: string, options: SetCookieOptions): string {\n const parts = [`${name}=${options.clear ? '' : encodeURIComponent(value)}`];\n parts.push(`Path=${options.path}`);\n parts.push('HttpOnly');\n parts.push('SameSite=Lax');\n if (options.secure) parts.push('Secure');\n if (options.clear) {\n parts.push('Max-Age=0');\n parts.push('Expires=Thu, 01 Jan 1970 00:00:00 GMT');\n } else {\n parts.push(`Max-Age=${options.maxAgeSeconds}`);\n }\n return parts.join('; ');\n}\n","import type { ConsoleSession } from './session-cookie.js';\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction readHeaders(request: unknown): Record<string, unknown> {\n if (!isRecord(request)) return {};\n const headers = request.headers;\n return isRecord(headers) ? headers : {};\n}\n\n/** Coerce a single header (string or string[]) to a string, else undefined. */\nfunction headerToString(value: unknown): string | undefined {\n if (typeof value === 'string') return value;\n if (Array.isArray(value) && value.every((part) => typeof part === 'string')) {\n return value.join(',');\n }\n return undefined;\n}\n\n/** Read the raw `Cookie` request header (Express and Fastify both expose `headers`). */\nexport function readCookieHeader(request: unknown): string | undefined {\n return headerToString(readHeaders(request).cookie);\n}\n\n/**\n * Decide whether the cookie should carry `Secure`. True when the request is https directly\n * (`request.protocol === 'https'`) OR a proxy forwarded https (`x-forwarded-proto` includes\n * 'https'). Defensive structural reads — never throws, defaults to insecure (works on plain http\n * in dev).\n */\nexport function isHttpsRequest(request: unknown): boolean {\n if (isRecord(request) && request.protocol === 'https') return true;\n const forwarded = headerToString(readHeaders(request)['x-forwarded-proto']);\n if (forwarded === undefined) return false;\n return forwarded\n .split(',')\n .map((proto) => proto.trim().toLowerCase())\n .includes('https');\n}\n\n/** Attach the verified session onto the raw request so downstream code can read it. */\nexport function attachSession(request: unknown, session: ConsoleSession): void {\n if (!isRecord(request)) return;\n (request as { consoleSession?: ConsoleSession }).consoleSession = session;\n}\n","function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\ninterface RawNodeResponse {\n getHeader(name: string): number | string | string[] | undefined;\n setHeader(name: string, value: string | string[]): unknown;\n}\n\nfunction isRawNodeResponse(value: unknown): value is RawNodeResponse {\n return (\n isRecord(value) &&\n typeof value.getHeader === 'function' &&\n typeof value.setHeader === 'function'\n );\n}\n\n/**\n * Resolve the writable Node `ServerResponse` from a platform response. Express' response IS the\n * Node response; Fastify's reply wraps it on `.raw`. Returns the outer response when it already\n * exposes get/setHeader (Express), otherwise the unwrapped `.raw` (Fastify).\n */\nfunction resolveRawResponse(response: unknown): RawNodeResponse | null {\n if (isRawNodeResponse(response)) return response;\n if (isRecord(response) && isRawNodeResponse(response.raw)) return response.raw;\n return null;\n}\n\nfunction existingSetCookies(response: RawNodeResponse): string[] {\n const current = response.getHeader('set-cookie');\n if (Array.isArray(current)) return current;\n if (typeof current === 'string') return [current];\n return [];\n}\n\n/**\n * Append a `Set-Cookie` header to the response WITHOUT clobbering any cookies already queued by\n * the host. Platform-agnostic raw write — no-ops gracefully if the response can't be unwrapped.\n */\nexport function appendSetCookie(response: unknown, cookie: string): void {\n const raw = resolveRawResponse(response);\n if (!raw) return;\n raw.setHeader('set-cookie', [...existingSetCookies(raw), cookie]);\n}\n\ninterface EndableResponse extends RawNodeResponse {\n statusCode: number;\n headersSent?: boolean;\n end(chunk?: string): unknown;\n}\n\nfunction resolveEndableResponse(response: unknown): EndableResponse | null {\n const isEndable = (value: unknown): value is EndableResponse =>\n isRawNodeResponse(value) && typeof (value as EndableResponse).end === 'function';\n if (isEndable(response)) return response;\n if (isRecord(response) && isEndable(response.raw)) return response.raw;\n return null;\n}\n\n/**\n * Did something already write to this response?\n *\n * Used to decide whether a host's `unauthenticatedPage` hook actually produced a page. A hook that\n * returns without writing (an early `return`, a forgotten `await`, a template that resolved to\n * nothing) would otherwise leave the request hanging forever — the browser spins until it times\n * out, with no error anywhere. Checking this lets the caller fall back to serving the SPA.\n */\nexport function responseAlreadyWritten(response: unknown): boolean {\n return resolveEndableResponse(response)?.headersSent === true;\n}\n\n/**\n * Write a full HTML page on the raw response and END it — used to serve the SPA shell from the\n * non-passthrough index route, where the handler owns the whole response so a host's\n * `unauthenticatedPage` can take it over instead.\n */\nexport function sendHtml(response: unknown, status: number, html: string): void {\n const raw = resolveEndableResponse(response);\n if (!raw) return;\n raw.statusCode = status;\n raw.setHeader('content-type', 'text/html; charset=utf-8');\n // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = the classic\n // \"stuck loading after a deploy\"); the unauthenticated page likewise reflects live session state.\n raw.setHeader('cache-control', 'no-store, must-revalidate');\n raw.end(html);\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Clock-skew grace applied to `exp` so a marginally-late clock doesn't bounce a valid cookie. */\nconst EXP_GRACE_MS = 30_000;\n\n/** The validated console session attached to a request once its cookie verifies. */\nexport interface ConsoleSession {\n /** Stable user id (the session user's `id`). */\n sub: string;\n /** Optional display name. */\n name?: string;\n /** Free-form role strings; the console does not interpret them. */\n roles: string[];\n /** Issued-at, epoch milliseconds. */\n iat: number;\n /** Expiry, epoch milliseconds. */\n exp: number;\n}\n\n/** The session user a host `login`/`session` hook returns to mint a cookie. */\nexport interface ConsoleSessionUser {\n id: string;\n name?: string;\n roles?: string[];\n}\n\nexport interface SignOptions {\n secret: string;\n ttlMs: number;\n /** Injectable clock (epoch ms) for deterministic tests. Defaults to `Date.now()`. */\n now?: number;\n}\n\nexport interface VerifyOptions {\n secret: string;\n now?: number;\n}\n\nfunction base64urlEncode(value: string): string {\n return Buffer.from(value, 'utf8').toString('base64url');\n}\n\nfunction base64urlDecode(value: string): string {\n return Buffer.from(value, 'base64url').toString('utf8');\n}\n\nfunction hmac(secret: string, payload: string): string {\n return createHmac('sha256', secret).update(payload).digest('base64url');\n}\n\n/**\n * Sign a session into the cookie value `base64url(payload).base64url(hmac)`. Stateless\n * HMAC-SHA256, no store — `node:crypto` only, no JWT dependency. Mirrors the telescope\n * dashboard's session cookie so the console gate behaves identically.\n */\nexport function signSessionCookie(user: ConsoleSessionUser, options: SignOptions): string {\n const issuedAt = options.now ?? Date.now();\n const session: ConsoleSession = {\n sub: user.id,\n ...(user.name !== undefined ? { name: user.name } : {}),\n roles: user.roles ?? [],\n iat: issuedAt,\n exp: issuedAt + options.ttlMs,\n };\n const encodedPayload = base64urlEncode(JSON.stringify(session));\n return `${encodedPayload}.${hmac(options.secret, encodedPayload)}`;\n}\n\n/** Type guard for the decoded payload shape — defends against tampered/legacy payloads. */\nfunction isSessionPayload(value: unknown): value is ConsoleSession {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Record<string, unknown>;\n if (typeof record.sub !== 'string') return false;\n if (record.name !== undefined && typeof record.name !== 'string') return false;\n if (!Array.isArray(record.roles)) return false;\n if (!record.roles.every((role) => typeof role === 'string')) return false;\n if (typeof record.iat !== 'number' || !Number.isFinite(record.iat)) return false;\n if (typeof record.exp !== 'number' || !Number.isFinite(record.exp)) return false;\n return true;\n}\n\n/**\n * Verify a cookie value and return the session, or `null` for anything tampered, malformed, or\n * expired (past `exp` + a 30s grace). Constant-time signature comparison. NEVER throws — any\n * parse failure yields `null`.\n */\nexport function verifySessionCookie(value: string, options: VerifyOptions): ConsoleSession | null {\n try {\n const dot = value.indexOf('.');\n if (dot <= 0 || dot === value.length - 1) return null;\n const encodedPayload = value.slice(0, dot);\n const provided = Buffer.from(value.slice(dot + 1), 'base64url');\n const expected = Buffer.from(hmac(options.secret, encodedPayload), 'base64url');\n // timingSafeEqual throws on a length mismatch — guard it so a wrong-length (tampered)\n // signature returns null instead of throwing.\n if (provided.length !== expected.length) return null;\n if (!timingSafeEqual(provided, expected)) return null;\n const decoded: unknown = JSON.parse(base64urlDecode(encodedPayload));\n if (!isSessionPayload(decoded)) return null;\n const now = options.now ?? Date.now();\n if (now > decoded.exp + EXP_GRACE_MS) return null;\n return decoded;\n } catch {\n return null;\n }\n}\n","import { serializeSetCookie } from './cookie-header.js';\nimport { isHttpsRequest } from './request.js';\nimport { appendSetCookie } from './response.js';\nimport { type ConsoleSessionUser, signSessionCookie } from './session-cookie.js';\n\n/** Cookie name carrying the signed console session. */\nexport const SESSION_COOKIE_NAME = 'media_console_session';\n\ninterface CookieContext {\n auth: { secret: string; ttlMs: number };\n /** Cookie `Path` — the JSON API base, so the cookie rides every console API request. */\n cookiePath: string;\n request: unknown;\n response: unknown;\n now?: number;\n}\n\n/**\n * Sign a fresh session for `user` and append it as a `Set-Cookie` on the response, scoped to the\n * console API path and `Secure` when the request is https.\n */\nexport function issueSessionCookie(user: ConsoleSessionUser, context: CookieContext): void {\n const value = signSessionCookie(user, {\n secret: context.auth.secret,\n ttlMs: context.auth.ttlMs,\n ...(context.now !== undefined ? { now: context.now } : {}),\n });\n appendSetCookie(\n context.response,\n serializeSetCookie(SESSION_COOKIE_NAME, value, {\n path: context.cookiePath,\n maxAgeSeconds: Math.floor(context.auth.ttlMs / 1000),\n secure: isHttpsRequest(context.request),\n }),\n );\n}\n\n/** Append a cookie-clearing `Set-Cookie` (Max-Age=0) scoped to the console API path. */\nexport function clearSessionCookie(context: Omit<CookieContext, 'auth' | 'now'>): void {\n appendSetCookie(\n context.response,\n serializeSetCookie(SESSION_COOKIE_NAME, '', {\n path: context.cookiePath,\n maxAgeSeconds: 0,\n secure: isHttpsRequest(context.request),\n clear: true,\n }),\n );\n}\n","// Cross-package by-value tokens. These MUST use the exact same `Symbol.for` keys as\n// `@dudousxd/nestjs-media`'s `tokens.ts`. The global symbol registry guarantees identity across\n// packages, so the console resolves the host-provided values without importing the (heavier,\n// controller-carrying) nestjs package — this package depends only on `-core`. Do not change these\n// keys without changing them there too. Mirrors `@dudousxd/nestjs-media-telescope`'s media-tokens.\n\n/** The configured `MediaStore` (or `null`), provided by `MediaModule`. */\nexport const MEDIA_STORE: symbol = Symbol.for('nestjs-media:store');\n/** The configured `UploadSessionStore` (or `null`), provided by `MediaModule`. */\nexport const MEDIA_UPLOAD_SESSIONS: symbol = Symbol.for('nestjs-media:upload-sessions');\n/** The `StorageManager` (alias of `MediaModule`'s internal `MEDIA_STORAGE` token). */\nexport const MEDIA_STORAGE_SHARED: symbol = Symbol.for('nestjs-media:storage');\n\n/** Carries the resolved UI mount base (e.g. `/media`) to the UI controller. */\nexport const MEDIA_DASHBOARD_BASE_PATH: symbol = Symbol('MEDIA_DASHBOARD_BASE_PATH');\n/** Carries the resolved JSON API base (e.g. `/api/media/console`) the SPA fetches from. */\nexport const MEDIA_DASHBOARD_API_PATH: symbol = Symbol('MEDIA_DASHBOARD_API_PATH');\n/** Carries whether destructive action routes were enabled (`options.actions`). */\nexport const MEDIA_DASHBOARD_ACTIONS: symbol = Symbol('MEDIA_DASHBOARD_ACTIONS');\n\n/** Carries the resolved console-auth config (`ResolvedConsoleAuth | null`) to the guard/controller. */\nexport const MEDIA_CONSOLE_AUTH: symbol = Symbol('MEDIA_CONSOLE_AUTH');\n/** Carries the cookie `Path` (the JSON API base) so the session cookie rides every API request. */\nexport const MEDIA_CONSOLE_COOKIE_PATH: symbol = Symbol('MEDIA_CONSOLE_COOKIE_PATH');\n\n/** Carries the host's `ObjectInsightProvider[]` (`options.objectInsights`) to the console service. */\nexport const MEDIA_CONSOLE_OBJECT_INSIGHTS: symbol = Symbol('MEDIA_CONSOLE_OBJECT_INSIGHTS');\n\n/** Carries the resolved `ObjectUrlConfig` (`options.objectUrls` + the API mount) to the console\n * service, which decides from it whether an object's `url` is presigned or same-origin. */\nexport const MEDIA_CONSOLE_OBJECT_URLS: symbol = Symbol('MEDIA_CONSOLE_OBJECT_URLS');\n","import type { Readable } from 'node:stream';\nimport type {\n MediaRecord,\n MediaStore,\n ReadRangeOptions,\n StorageDriver,\n StorageManager,\n UploadSession,\n UploadSessionStore,\n} from '@dudousxd/nestjs-media-core';\nimport {\n BadRequestException,\n HttpException,\n HttpStatus,\n Inject,\n Injectable,\n Logger,\n NotFoundException,\n NotImplementedException,\n Optional,\n PayloadTooLargeException,\n} from '@nestjs/common';\nimport type {\n CollectionsResponse,\n DiskListResponse,\n LibraryDetailResponse,\n LibraryListResponse,\n LibraryRecord,\n ObjectDetailResponse,\n ObjectListResponse,\n Topology,\n UploadDetailResponse,\n UploadInfo,\n UploadListResponse,\n} from '../client/types.js';\nimport type {\n ObjectInsight,\n ObjectInsightProvider,\n ObjectInsightsResponse,\n} from './object-insights.js';\nimport { sanitizeInsight } from './object-insights.js';\nimport type { ObjectUrlConfig } from './object-urls.js';\nimport { objectProxyUrl } from './object-urls.js';\nimport {\n MEDIA_CONSOLE_OBJECT_INSIGHTS,\n MEDIA_CONSOLE_OBJECT_URLS,\n MEDIA_DASHBOARD_ACTIONS,\n MEDIA_STORAGE_SHARED,\n MEDIA_STORE,\n MEDIA_UPLOAD_SESSIONS,\n} from './tokens.js';\n\n/** Seconds a presigned object URL stays valid. */\nconst URL_TTL_SECONDS = 300;\nconst DEFAULT_PAGE_LIMIT = 50;\n/** Ceiling for a console (direct) upload — buffered in memory, so bounded to protect the heap. */\nconst MAX_UPLOAD_BYTES = 100 * 1024 * 1024;\n/** Page size when sweeping a folder's objects for a recursive delete. */\nconst DELETE_SWEEP_LIMIT = 1000;\n/** Ceiling for a cross-disk copy/move. The driver has no server-side cross-bucket copy, so the object\n * is streamed through the pod (get→put, buffered) — bounded to protect the heap. Same-disk transfers\n * use the driver's native copy/move and are not subject to this cap. */\nconst MAX_CROSS_DISK_BYTES = 100 * 1024 * 1024;\n\n/** Last path segment of a folder prefix, ignoring a trailing slash. */\nfunction lastSegment(prefix: string): string {\n const trimmed = prefix.replace(/\\/+$/, '');\n const slash = trimmed.lastIndexOf('/');\n return slash === -1 ? trimmed : trimmed.slice(slash + 1);\n}\n\n/**\n * A range as the CLIENT asked for it, before the object's size is known.\n *\n * Either an absolute {@link ReadRangeOptions}, or HTTP's suffix form (`Range: bytes=-500`, \"the last\n * 500 bytes\") — which cannot be made absolute without the size, and the size is `stat`'s to give.\n * Resolving it here rather than in the controller saves a second `stat` on the request path.\n */\nexport type RequestedReadRange = ReadRangeOptions | { suffixLength: number };\n\n/**\n * The requested range cannot be satisfied against this object (start at/past EOF, or start > end).\n *\n * A dedicated class rather than a bare `HttpException` because RFC 9110 requires a 416 to carry an\n * unsatisfied-range `Content-Range` naming the object's real size, so the client learns the size and\n * can retry — and only the controller can set a header. This carries the size across that boundary.\n */\nexport class RangeNotSatisfiableException extends HttpException {\n constructor(readonly size: number) {\n super(\n {\n statusCode: HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE,\n message: `Requested range is not satisfiable for an object of ${size} bytes`,\n error: 'Range Not Satisfiable',\n },\n HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE,\n );\n }\n}\n\n/** What {@link MediaConsoleService.objectStream} hands the controller. `range` is present only when\n * a range was requested AND satisfied, already absolute and clamped to the object — so the\n * controller can write `Content-Range`/`Content-Length` without re-deriving anything. */\nexport interface ObjectStreamResult {\n stream: Readable;\n contentType: string;\n /** Total size of the OBJECT, not of the slice — the `/<size>` in `Content-Range`. */\n size: number;\n /** Absolute inclusive bounds actually being streamed. Absent on a full-body read. */\n range?: { start: number; end: number };\n}\n\n/**\n * Resolve a requested range into absolute, inclusive, in-bounds bounds — or `null` when it is\n * unsatisfiable and the answer has to be a 416.\n *\n * Unsatisfiable means a start at or past EOF (there is no such byte) or a start past the end; a\n * zero-byte object consequently rejects every range, which is what RFC 9110 prescribes. An `end`\n * beyond EOF is NOT unsatisfiable — it is clamped, because \"the next 64 KB from offset N\" is how\n * every paging reader asks for the tail of a file, and erroring there would make the last page of\n * every object unreadable.\n */\nfunction resolveRange(\n range: RequestedReadRange,\n size: number,\n): { start: number; end: number } | null {\n if ('suffixLength' in range) {\n // `bytes=-N`: the last N bytes. A suffix longer than the object is the whole object; `bytes=-0`\n // asks for nothing, which RFC 9110 defines as unsatisfiable rather than as an empty 206.\n if (!Number.isInteger(range.suffixLength) || range.suffixLength <= 0 || size === 0) return null;\n return { start: Math.max(0, size - range.suffixLength), end: size - 1 };\n }\n const { start } = range;\n if (!Number.isInteger(start) || start < 0 || start >= size) return null;\n const end = range.end === undefined ? size - 1 : Math.min(range.end, size - 1);\n if (!Number.isInteger(end) || end < start) return null;\n return { start, end };\n}\n\nfunction mapUpload(session: UploadSession): UploadInfo {\n const percent =\n session.size !== undefined && session.size > 0\n ? Math.min(100, Math.round((session.offset / session.size) * 100))\n : null;\n return {\n id: session.id,\n disk: session.disk,\n key: session.key,\n offset: session.offset,\n size: session.size ?? null,\n percent,\n parts: session.parts,\n multipart: session.multipartUploadId !== undefined,\n ...(session.createdAt ? { createdAt: session.createdAt.toISOString() } : {}),\n };\n}\n\nfunction mapRecord(record: MediaRecord): LibraryRecord {\n return {\n id: record.id,\n ownerType: record.ownerType,\n ownerId: record.ownerId,\n collection: record.collection,\n name: record.name,\n fileName: record.fileName,\n mimeType: record.mimeType,\n size: record.size,\n disk: record.disk,\n path: record.path,\n createdAt: record.createdAt.toISOString(),\n };\n}\n\n/**\n * Read + action logic behind the console's JSON API. Resolves the three by-value media tokens\n * with `@Optional()` so a host without a `MediaStore`/upload store still boots — every method\n * degrades to an empty shape instead of throwing when its capability is absent.\n */\n@Injectable()\nexport class MediaConsoleService {\n constructor(\n @Optional() @Inject(MEDIA_STORAGE_SHARED) private readonly storage: StorageManager | null,\n @Optional() @Inject(MEDIA_STORE) private readonly store: MediaStore | null,\n @Optional() @Inject(MEDIA_UPLOAD_SESSIONS) private readonly uploads: UploadSessionStore | null,\n @Optional() @Inject(MEDIA_DASHBOARD_ACTIONS) private readonly actionsEnabled: boolean | null,\n // Defaulted, not merely `@Optional()`: the console service is constructed directly in unit\n // tests (and by any host wiring it by hand), so a required 5th argument would break every one\n // of those call sites for a feature they do not use.\n @Optional()\n @Inject(MEDIA_CONSOLE_OBJECT_INSIGHTS)\n private readonly insightProviders: ObjectInsightProvider[] | null = null,\n // Strategy and mount path travel as ONE value: a strategy without the path it needs would be a\n // half-configured state that builds `/disks/...` URLs rooted at nothing. `null` means `auto`.\n @Optional()\n @Inject(MEDIA_CONSOLE_OBJECT_URLS)\n private readonly objectUrls: ObjectUrlConfig | null = null,\n ) {}\n\n /**\n * The URL to report for an object's bytes, under the host's {@link ObjectUrlStrategy}. `auto`\n * prefers a presigned URL straight to the store; `proxy` routes through this server, for a host\n * whose browser cannot reach the store at all.\n */\n private objectUrl(driver: StorageDriver, disk: string, key: string): Promise<string> {\n if (this.objectUrls?.strategy === 'proxy') {\n return Promise.resolve(objectProxyUrl(this.objectUrls.apiBasePath, 'raw', disk, key));\n }\n return driver.capabilities.presign\n ? driver.temporaryUrl(key, URL_TTL_SECONDS)\n : driver.url(key);\n }\n\n private readonly logger = new Logger(MediaConsoleService.name);\n\n private diskOrThrow(disk: string): StorageDriver {\n if (!this.storage || !this.storage.diskNames().includes(disk)) {\n throw new NotFoundException(`Unknown disk: ${disk}`);\n }\n return this.storage.disk(disk);\n }\n\n listDisks(): DiskListResponse {\n const storage = this.storage;\n if (!storage) return { disks: [] };\n const defaultDisk = storage.defaultDisk;\n return {\n disks: storage.diskNames().map((name) => ({\n name,\n default: name === defaultDisk,\n capabilities: storage.disk(name).capabilities,\n })),\n };\n }\n\n async listObjects(\n disk: string,\n options: { prefix?: string; cursor?: string; limit?: number },\n ): Promise<ObjectListResponse> {\n const driver = this.diskOrThrow(disk);\n const result = await driver.list(options.prefix ?? '', {\n delimiter: '/',\n ...(options.cursor ? { cursor: options.cursor } : {}),\n limit: options.limit ?? DEFAULT_PAGE_LIMIT,\n });\n return {\n // Drop phantom folders whose name is empty — a CommonPrefix of only slashes (`/`, `//`),\n // produced by a stray key with a leading slash. The S3 driver normalizes such a prefix back to\n // the root, so listing INTO one returns the root again (the phantom included) — a self-reference\n // that infinite-loops the folder tree. Filtering it out removes the trap; those leading-slash\n // keys are unreachable from the console anyway (the driver strips the leading slash).\n folders: result.folders\n .map((prefix) => ({ name: lastSegment(prefix), prefix }))\n .filter((folder) => folder.name !== ''),\n // Drop the zero-byte \"folder marker\" (a key ending in `/`, whose name is empty after the\n // prefix) that `createFolder` writes — it's the folder itself, not a file inside it.\n files: result.files\n .filter((entry) => entry.name !== '')\n .map((entry) => ({\n key: entry.key,\n name: entry.name,\n sizeBytes: entry.sizeBytes,\n lastModified: entry.lastModified ? entry.lastModified.toISOString() : null,\n })),\n ...(result.cursor ? { cursor: result.cursor } : {}),\n };\n }\n\n async objectDetail(disk: string, key: string): Promise<ObjectDetailResponse> {\n const driver = this.diskOrThrow(disk);\n const stat = await driver.stat(key);\n const url = await this.objectUrl(driver, disk, key);\n return {\n key,\n size: stat.size,\n ...(stat.contentType ? { contentType: stat.contentType } : {}),\n ...(stat.lastModified ? { lastModified: stat.lastModified.toISOString() } : {}),\n url,\n };\n }\n\n /**\n * What the HOST knows about this object, from every registered provider.\n *\n * Its own call rather than a field on `objectDetail`, for the same reason the RAG index-status\n * endpoint is separate from its document listing: `objectDetail` is on the path of every preview\n * and every navigation, and providers reach into host databases. Folding them in would put a host\n * query on reads that only wanted a presigned URL.\n *\n * Failures are contained per provider. A provider that throws is logged and dropped, and the\n * others still render — annotation must never be able to stop an admin opening a file, and one\n * host lookup being down says nothing about the rest.\n *\n * Providers run concurrently: they are independent host lookups, and running them in series would\n * make the panel as slow as their sum for no gain.\n */\n async objectInsights(disk: string, key: string): Promise<ObjectInsightsResponse> {\n const providers = this.insightProviders ?? [];\n if (providers.length === 0) {\n return { insights: [] };\n }\n // Validates the disk the same way every other object read does, so an unknown disk 404s here\n // too rather than silently handing host providers a key from nowhere.\n this.diskOrThrow(disk);\n\n const settled = await Promise.all(\n providers.map(async (provider): Promise<ObjectInsight | null> => {\n try {\n return await provider.resolve({ disk, key });\n } catch (error) {\n this.logger.warn(\n `Object-insight provider \"${provider.id}\" failed for ${disk}:${key} — ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return null;\n }\n }),\n );\n\n return {\n insights: settled\n .filter((insight): insight is ObjectInsight => insight !== null)\n .map(sanitizeInsight),\n };\n }\n\n /**\n * The object's raw byte stream plus the metadata the controller needs to serve it inline. Used by\n * the console's same-origin preview proxy so text/PDF render in the browser instead of downloading\n * (and so a CORS-locked bucket is still previewable).\n *\n * With `range`, only that slice — which is what makes previewing a multi-hundred-MB object (a\n * SQLite database read a page at a time) possible at all. The range is resolved HERE, not in the\n * driver, because it has to be checked against the object's size and the `stat` for that is\n * already on this path: the suffix form is made absolute, an overrunning `end` is clamped to the\n * last byte, and anything genuinely unsatisfiable becomes a {@link RangeNotSatisfiableException}\n * (HTTP 416) instead of a surprise empty body.\n *\n * A driver that reports `ranged: false` REFUSES a range request rather than quietly returning the\n * whole object: the caller asked for 64 KB out of 400 MB, and silently answering with all 400 MB\n * — which the caller would then read as if it were the slice — is the exact failure the `ranged`\n * capability exists to make impossible.\n */\n async objectStream(\n disk: string,\n key: string,\n range?: RequestedReadRange,\n ): Promise<ObjectStreamResult> {\n const driver = this.diskOrThrow(disk);\n const stat = await driver.stat(key);\n const contentType = stat.contentType ?? 'application/octet-stream';\n if (!range) {\n return { stream: await driver.stream(key), contentType, size: stat.size };\n }\n if (!driver.capabilities.ranged) {\n throw new NotImplementedException(`Disk \"${disk}\" cannot serve byte ranges`);\n }\n const resolved = resolveRange(range, stat.size);\n if (!resolved) throw new RangeNotSatisfiableException(stat.size);\n return {\n stream: await driver.stream(key, resolved),\n contentType,\n size: stat.size,\n range: resolved,\n };\n }\n\n /** Writes an object from a byte stream (a browser upload). The caller supplies the full key\n * (prefix + filename); an unknown disk 404s via {@link diskOrThrow}. Actions-gated.\n *\n * The request stream is buffered before the write: S3's PutObject needs a known Content-Length,\n * which a raw request stream doesn't carry, so passing the stream straight through fails. Bounded\n * at {@link MAX_UPLOAD_BYTES} so a runaway upload can't exhaust the pod's heap — larger files\n * belong on the resumable upload path, not this console convenience upload. */\n async putObject(disk: string, key: string, body: Readable, contentType?: string): Promise<void> {\n const driver = this.diskOrThrow(disk);\n const chunks: Buffer[] = [];\n let total = 0;\n for await (const chunk of body) {\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);\n total += buffer.length;\n if (total > MAX_UPLOAD_BYTES) {\n throw new PayloadTooLargeException(\n `Upload exceeds the ${MAX_UPLOAD_BYTES / (1024 * 1024)} MB console limit.`,\n );\n }\n chunks.push(buffer);\n }\n await driver.put(key, Buffer.concat(chunks), contentType ? { contentType } : undefined);\n }\n\n /** Creates a \"folder\" — a zero-byte marker object at `<prefix>/`, which S3-style listing surfaces\n * as a navigable folder. Normalizes to exactly one trailing slash; rejects an empty prefix. */\n async createFolder(disk: string, prefix: string): Promise<void> {\n const normalized = prefix.replace(/^\\/+/, '').replace(/\\/+$/, '');\n if (normalized === '') throw new BadRequestException('Folder name is required');\n await this.diskOrThrow(disk).put(`${normalized}/`, Buffer.alloc(0));\n }\n\n async deleteObject(disk: string, key: string): Promise<void> {\n await this.diskOrThrow(disk).delete(key);\n }\n\n /** Recursively deletes a \"folder\": every object under `<prefix>/` (nested included) plus the\n * zero-byte marker itself. Passes an empty delimiter so the driver lists keys FLAT — the default\n * `/` delimiter would group nested keys into CommonPrefixes and the sweep would miss them, only\n * deleting direct children. Paginates until the disk reports no more. The marker's own key equals\n * the sweep prefix, which `list` filters out (Key === Prefix), so it's deleted explicitly at the\n * end. Actions-gated. */\n async deleteFolder(disk: string, prefix: string): Promise<void> {\n const driver = this.diskOrThrow(disk);\n const normalized = prefix.replace(/^\\/+/, '').replace(/\\/+$/, '');\n if (normalized === '') throw new BadRequestException('Folder is required');\n const sweepPrefix = `${normalized}/`;\n let cursor: string | undefined;\n do {\n const result = await driver.list(sweepPrefix, {\n delimiter: '',\n limit: DELETE_SWEEP_LIMIT,\n ...(cursor ? { cursor } : {}),\n });\n const keys = result.files.map((entry) => entry.key);\n if (driver.deleteMany) await driver.deleteMany(keys);\n else for (const key of keys) await driver.delete(key);\n cursor = result.cursor;\n } while (cursor);\n await driver.delete(sweepPrefix);\n }\n\n async copyObject(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await this.transferKey('copy', fromDisk, from, toDisk, to);\n }\n\n async moveObject(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await this.transferKey('move', fromDisk, from, toDisk, to);\n }\n\n /** Copies or moves a single object, on the same disk or across disks. Same-disk uses the driver's\n * native `copy`/`move` (server-side, no bytes through the pod). Cross-disk has no driver primitive,\n * so the object is streamed through the pod (get→put, buffered), preserving its content type and\n * bounded at {@link MAX_CROSS_DISK_BYTES}; a `move` then deletes the source. */\n private async transferKey(\n op: 'copy' | 'move',\n fromDisk: string,\n from: string,\n toDisk: string,\n to: string,\n ): Promise<void> {\n const fromDriver = this.diskOrThrow(fromDisk);\n if (fromDisk === toDisk) {\n if (op === 'move') await fromDriver.move(from, to);\n else await fromDriver.copy(from, to);\n return;\n }\n const toDriver = this.diskOrThrow(toDisk);\n const stat = await fromDriver.stat(from);\n if (stat.size > MAX_CROSS_DISK_BYTES) {\n throw new PayloadTooLargeException(\n `\"${from}\" is ${Math.round(stat.size / (1024 * 1024))} MB — over the ${\n MAX_CROSS_DISK_BYTES / (1024 * 1024)\n } MB limit for copying across disks from the console.`,\n );\n }\n const bytes = await fromDriver.get(from);\n await toDriver.put(to, bytes, stat.contentType ? { contentType: stat.contentType } : undefined);\n if (op === 'move') await fromDriver.delete(from);\n }\n\n /** Moves a whole \"folder\" (every object under `<from>/`, nested included) to `<to>/`, on the same\n * disk or across disks, preserving each key's path relative to the source. */\n async moveFolder(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await this.transferFolder('move', fromDisk, from, toDisk, to);\n }\n\n /** Copies a whole \"folder\" (every object under `<from>/`, nested included) to `<to>/`, same disk or\n * across disks, preserving each key's path relative to the source. */\n async copyFolder(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await this.transferFolder('copy', fromDisk, from, toDisk, to);\n }\n\n /** Shared engine for {@link moveFolder}/{@link copyFolder}. Same flat-listing sweep as\n * {@link deleteFolder}; each key is relocated via {@link transferKey} (so cross-disk transfers work\n * transparently). On the SAME disk, rejects a destination inside the source (which would recurse\n * forever); across disks that constraint doesn't apply. The destination folder marker is written,\n * and for a move the source marker is removed, so both listings stay consistent. Actions-gated. */\n private async transferFolder(\n op: 'copy' | 'move',\n fromDisk: string,\n from: string,\n toDisk: string,\n to: string,\n ): Promise<void> {\n const fromDriver = this.diskOrThrow(fromDisk);\n const toDriver = this.diskOrThrow(toDisk);\n const fromNormalized = from.replace(/^\\/+/, '').replace(/\\/+$/, '');\n const toNormalized = to.replace(/^\\/+/, '').replace(/\\/+$/, '');\n if (fromNormalized === '') throw new BadRequestException('Source folder is required');\n if (toNormalized === '') throw new BadRequestException('Destination folder is required');\n const fromPrefix = `${fromNormalized}/`;\n const toPrefix = `${toNormalized}/`;\n if (fromDisk === toDisk && (toPrefix === fromPrefix || toPrefix.startsWith(fromPrefix))) {\n throw new BadRequestException(`Cannot ${op} a folder into itself`);\n }\n let cursor: string | undefined;\n do {\n const result = await fromDriver.list(fromPrefix, {\n delimiter: '',\n limit: DELETE_SWEEP_LIMIT,\n ...(cursor ? { cursor } : {}),\n });\n for (const entry of result.files) {\n const destKey = `${toPrefix}${entry.key.slice(fromPrefix.length)}`;\n await this.transferKey(op, fromDisk, entry.key, toDisk, destKey);\n }\n cursor = result.cursor;\n } while (cursor);\n // Relocate/replicate the zero-byte marker (its key equals the sweep prefix, so the sweep skips it):\n // write the destination marker, and for a move drop the source one. Idempotent if there was none.\n await toDriver.put(toPrefix, Buffer.alloc(0));\n if (op === 'move') await fromDriver.delete(fromPrefix);\n }\n\n async listUploads(filter: { disk?: string; prefix?: string }): Promise<UploadListResponse> {\n if (!this.uploads || typeof this.uploads.list !== 'function') return { uploads: [] };\n const sessions = await this.uploads.list({\n ...(filter.disk ? { disk: filter.disk } : {}),\n ...(filter.prefix ? { keyPrefix: filter.prefix } : {}),\n });\n return { uploads: sessions.map(mapUpload) };\n }\n\n async uploadDetail(id: string): Promise<UploadDetailResponse> {\n if (!this.uploads) throw new NotFoundException('No upload store configured');\n const session = await this.uploads.get(id);\n if (!session) throw new NotFoundException(`Unknown upload: ${id}`);\n const parts = this.uploads.listParts ? await this.uploads.listParts(id) : [];\n return { upload: mapUpload(session), parts };\n }\n\n /**\n * Cancels a resumable session by removing its record from the upload store, so it stops\n * appearing as in-progress. NOTE: this does NOT tear down an underlying native multipart upload\n * (e.g. an S3 multipart) or its temporary parts — the decoupled console resolves only the\n * `UploadSessionStore`, not the `ResumableUploadManager` that owns `abort()`. An incomplete\n * multipart is reaped by the bucket's lifecycle policy. Surfaced as \"Cancel session\" in the UI.\n */\n async abortUpload(id: string): Promise<void> {\n if (!this.uploads) throw new NotFoundException('No upload store configured');\n await this.uploads.delete(id);\n }\n\n async listCollections(): Promise<CollectionsResponse> {\n if (!this.store || typeof this.store.aggregate !== 'function') return { collections: [] };\n const buckets = await this.store.aggregate({ groupBy: 'collection', sum: 'size' });\n return { collections: buckets };\n }\n\n async listLibrary(filter: {\n collection?: string;\n disk?: string;\n cursor?: string;\n limit?: number;\n }): Promise<LibraryListResponse> {\n if (!this.store || typeof this.store.list !== 'function') return { records: [] };\n const page = await this.store.list(\n {\n ...(filter.collection ? { collection: filter.collection } : {}),\n ...(filter.disk ? { disk: filter.disk } : {}),\n },\n {\n limit: filter.limit ?? DEFAULT_PAGE_LIMIT,\n ...(filter.cursor ? { cursor: filter.cursor } : {}),\n },\n );\n return {\n records: page.records.map(mapRecord),\n ...(page.cursor ? { cursor: page.cursor } : {}),\n };\n }\n\n async libraryDetail(id: string): Promise<LibraryDetailResponse> {\n if (!this.store) throw new NotFoundException('No media store configured');\n const record = await this.store.find(id);\n if (!record) throw new NotFoundException(`Unknown media record: ${id}`);\n const variants = await Promise.all(\n Object.entries(record.conversions).map(async ([name, conversion]) => {\n const driver = this.storage?.diskNames().includes(conversion.disk)\n ? this.storage.disk(conversion.disk)\n : null;\n const url = driver ? await this.objectUrl(driver, conversion.disk, conversion.path) : '';\n // `disk`/`path` ride along so the UI can reach the object routes (download, and a proxied\n // read) for a variant — `url` alone is opaque, and under `auto` points off this origin.\n return { name, url, disk: conversion.disk, path: conversion.path };\n }),\n );\n return { record: mapRecord(record), variants };\n }\n\n async deleteLibraryRecord(id: string): Promise<void> {\n if (!this.store) throw new NotFoundException('No media store configured');\n await this.store.delete(id);\n }\n\n topology(): Topology {\n return {\n hasStore: this.store !== null && typeof this.store.list === 'function',\n hasUploads: this.uploads !== null && typeof this.uploads.list === 'function',\n disks: this.storage ? this.storage.diskNames().length : 0,\n actions: this.actionsEnabled === true,\n };\n }\n}\n","/**\n * Host-supplied context about a disk object, shown in the console's preview.\n *\n * The console can describe a file only as storage sees it: key, size, content type, last modified.\n * Everything that makes the file *mean* something lives in the host — which work order this scan\n * belongs to, which knowledge base indexed it, whether processing has run. An admin looking at\n * `rag/019af.../handbook.pdf` in the object browser has no way to learn any of that without leaving\n * for another screen and searching by key.\n *\n * This is the seam for handing that back. The host registers a provider; the console asks it when an\n * object is previewed and renders whatever comes back.\n *\n * **Data, not components — and not by preference.** The console ships as a prebuilt SPA bundle, so a\n * host has no way to inject React into it. A provider therefore returns a small structured value the\n * console knows how to render (facts, links, a note), rather than something it draws itself. The\n * cost is a fixed vocabulary; the benefit is that this works at all for a published bundle, and that\n * the console stays ignorant of every domain plugged into it.\n */\n\nimport type { ObjectInsight, ObjectInsightsResponse } from '../client/types.js';\n\nexport type {\n ObjectInsight,\n ObjectInsightFact,\n ObjectInsightLink,\n ObjectInsightsResponse,\n} from '../client/types.js';\n\n/** Which object is being described. */\nexport interface ObjectInsightContext {\n disk: string;\n key: string;\n}\n\n/**\n * A source of {@link ObjectInsight}s, registered by the host on `MediaDashboardModule.forRoot`.\n *\n * `resolve` runs on every preview of every object, so it must be cheap — one indexed lookup, not a\n * scan. Return `null` for an object this provider has nothing to say about (the overwhelmingly\n * common case: most providers care about one key prefix), and the console renders nothing for it.\n *\n * A provider that throws is **skipped**, logged, and the preview renders without it. Annotation is\n * never worth taking the preview down for — a host lookup failing must not stop an admin from\n * opening a file.\n */\nexport interface ObjectInsightProvider {\n /** Stable identifier, used in the log line when this provider fails. */\n id: string;\n resolve(ctx: ObjectInsightContext): Promise<ObjectInsight | null> | ObjectInsight | null;\n}\n\n/** Schemes an {@link ObjectInsightLink} may point at. Anything else is dropped, not rendered. */\nconst SAFE_LINK_SCHEMES = new Set(['http:', 'https:']);\n\n/**\n * Drop links the console must not render.\n *\n * A relative href (`/ctrl/rag/...`) is the normal case and always allowed — it can only ever address\n * the host's own app. An absolute one is parsed and kept only if it is http(s), which refuses\n * `javascript:` and `data:` hrefs. Providers are host code, so this is not a trust boundary so much\n * as a guard against a provider interpolating user-controlled text into a URL and handing the\n * console something that executes when clicked.\n */\nexport function sanitizeInsight(insight: ObjectInsight): ObjectInsight {\n if (insight.links === undefined) {\n return insight;\n }\n const links = insight.links.filter((link) => {\n if (link.href.startsWith('/')) {\n // `//evil.com` is protocol-relative — an absolute URL wearing a relative one's clothes.\n return !link.href.startsWith('//');\n }\n try {\n return SAFE_LINK_SCHEMES.has(new URL(link.href).protocol);\n } catch {\n return false;\n }\n });\n return { ...insight, links };\n}\n","/**\n * How the console hands out the `url` it reports for an object's bytes — the link the UI offers as\n * \"Open ↗\", and the `src` an image variant renders from.\n *\n * - `auto` (default): a presigned, expiring URL straight to the object store when the driver can\n * mint one, else the driver's plain URL. A real link: it survives a copy-paste out of the console\n * and needs nothing from this server to resolve.\n * - `proxy`: the console's own same-origin route. For a host whose browser has no path to the store\n * at all — no network route, no CORS grant, a policy against client-to-bucket traffic — where a\n * presigned URL is a link that cannot be opened. The trade is that these URLs are only meaningful\n * to a session that can reach this server, and every byte travels through it.\n *\n * Downloading is unaffected either way: `disks/:disk/object/download` is always same-origin,\n * because saving a file is the console's own action and has to work wherever the console does.\n */\nexport type ObjectUrlStrategy = 'auto' | 'proxy';\n\n/** The resolved {@link ObjectUrlStrategy} plus what `proxy` needs to build a URL. Travels as one\n * value so the service can never see a strategy without the mount path it depends on. */\nexport interface ObjectUrlConfig {\n strategy: ObjectUrlStrategy;\n /** Where the console's JSON API is mounted — leading slash, no trailing slash. */\n apiBasePath: string;\n}\n\n/** Same-origin URL for one of the console's object-byte routes, rooted at the API mount. */\nexport function objectProxyUrl(\n apiBasePath: string,\n route: 'raw' | 'download',\n disk: string,\n key: string,\n): string {\n const path = `${apiBasePath}/disks/${encodeURIComponent(disk)}/object/${route}`;\n return `${path}?key=${encodeURIComponent(key)}`;\n}\n","import {\n BadRequestException,\n Body,\n Controller,\n Get,\n HttpCode,\n Inject,\n Logger,\n NotFoundException,\n Optional,\n Post,\n Req,\n Res,\n UnauthorizedException,\n} from '@nestjs/common';\nimport type { ConsoleAuthOptions, ResolvedConsoleAuth } from './auth/config.js';\nimport { parseCookieHeader } from './auth/cookie-header.js';\nimport { readCookieHeader } from './auth/request.js';\nimport {\n SESSION_COOKIE_NAME,\n clearSessionCookie,\n issueSessionCookie,\n} from './auth/session-cookie-io.js';\nimport { type ConsoleSessionUser, verifySessionCookie } from './auth/session-cookie.js';\nimport { MEDIA_CONSOLE_AUTH, MEDIA_CONSOLE_COOKIE_PATH } from './tokens.js';\n\ninterface LoginBody {\n username?: unknown;\n password?: unknown;\n}\n\n/** Response of `GET /me`: the console SPA renders the login screen or the console from this. */\ntype MeResponse =\n | { authRequired: false }\n | { user: { id: string; name?: string; roles: string[] } };\n\n/**\n * Mints/clears the console session cookie. Deliberately NOT decorated with `MediaConsoleGuard` —\n * these endpoints CREATE the session the gate checks for. When the host didn't configure `auth`,\n * `GET /me` reports `authRequired: false` (the SPA shows the console) and login/logout 404.\n */\n@Controller()\nexport class MediaConsoleAuthController {\n private readonly logger = new Logger(MediaConsoleAuthController.name);\n /** One warn per hook kind, so a flaky hook doesn't spam logs every request. */\n private readonly warnedHooks = new Set<string>();\n\n constructor(\n @Optional() @Inject(MEDIA_CONSOLE_AUTH) private readonly auth: ResolvedConsoleAuth | null,\n @Optional() @Inject(MEDIA_CONSOLE_COOKIE_PATH) private readonly cookiePath: string | null,\n ) {}\n\n @Get('me')\n me(@Req() request: unknown): MeResponse {\n if (!this.auth) return { authRequired: false };\n const cookieValue = parseCookieHeader(readCookieHeader(request))[SESSION_COOKIE_NAME];\n const session =\n cookieValue !== undefined\n ? verifySessionCookie(cookieValue, { secret: this.auth.secret })\n : null;\n // The UNauthenticated SPA learns which login mode(s) to offer from this 401 body.\n if (!session) throw new UnauthorizedException({ auth: { modes: this.auth.modes } });\n return {\n user: {\n id: session.sub,\n ...(session.name !== undefined ? { name: session.name } : {}),\n roles: session.roles,\n },\n };\n }\n\n @Post('login')\n @HttpCode(200)\n async login(\n @Body() body: LoginBody,\n @Req() request: unknown,\n @Res({ passthrough: true }) response: unknown,\n ): Promise<MeResponse> {\n const auth = this.requireAuth();\n if (!auth.login) throw new NotFoundException();\n if (typeof body?.username !== 'string' || typeof body?.password !== 'string') {\n throw new BadRequestException('Body must include string `username` and `password`.');\n }\n const username = body.username;\n const password = body.password;\n // Uniform 401 for unknown user / bad password — no user-enumeration.\n const user = await this.runHook('login', () => auth.login?.(username, password) ?? null);\n if (!user) throw new UnauthorizedException({ message: 'Invalid credentials' });\n return this.mint(user, request, response);\n }\n\n @Post('session')\n @HttpCode(200)\n async session(\n @Req() request: unknown,\n @Res({ passthrough: true }) response: unknown,\n ): Promise<MeResponse> {\n const auth = this.requireAuth();\n if (!auth.session) throw new NotFoundException();\n const user = await this.runHook('session', () => auth.session?.(request) ?? null);\n if (!user) throw new UnauthorizedException();\n return this.mint(user, request, response);\n }\n\n @Post('logout')\n @HttpCode(204)\n logout(@Req() request: unknown, @Res({ passthrough: true }) response: unknown): void {\n // Best-effort: even without auth configured, clearing is harmless.\n clearSessionCookie({ cookiePath: this.cookiePath ?? '/', request, response });\n }\n\n private requireAuth(): ResolvedConsoleAuth {\n if (!this.auth) throw new NotFoundException();\n return this.auth;\n }\n\n private mint(user: ConsoleSessionUser, request: unknown, response: unknown): MeResponse {\n const auth = this.requireAuth();\n issueSessionCookie(user, {\n auth,\n cookiePath: this.cookiePath ?? '/',\n request,\n response,\n });\n return {\n user: {\n id: user.id,\n ...(user.name !== undefined ? { name: user.name } : {}),\n roles: user.roles ?? [],\n },\n };\n }\n\n /** Run a host hook defensively: a throw is a denial (null), warn-logged once per kind. */\n private async runHook(\n kind: string,\n run: () => Promise<ConsoleSessionUser | null> | ConsoleSessionUser | null,\n ): Promise<ConsoleSessionUser | null> {\n try {\n return (await run()) ?? null;\n } catch (error) {\n if (!this.warnedHooks.has(kind)) {\n this.warnedHooks.add(kind);\n this.logger.warn(`Console auth ${kind} hook threw; treating as denial. ${String(error)}`);\n }\n return null;\n }\n }\n}\n\nexport type { ConsoleAuthOptions };\n","import {\n Controller,\n Get,\n Headers,\n Inject,\n Param,\n Query,\n Res,\n StreamableFile,\n UseGuards,\n} from '@nestjs/common';\nimport type {\n CollectionsResponse,\n DiskListResponse,\n LibraryDetailResponse,\n LibraryListResponse,\n ObjectDetailResponse,\n ObjectListResponse,\n Topology,\n UploadDetailResponse,\n UploadListResponse,\n} from '../client/types.js';\nimport { attachmentDisposition, downloadFilename } from './content-disposition.js';\nimport { MediaConsoleGuard } from './media-console.guard.js';\nimport {\n MediaConsoleService,\n RangeNotSatisfiableException,\n type RequestedReadRange,\n} from './media-console.service.js';\nimport type { ObjectInsightsResponse } from './object-insights.js';\n\n/** Parse an optional numeric query param; undefined when absent or not a finite number. */\nfunction toLimit(value: string | undefined): number | undefined {\n if (value === undefined) return undefined;\n const parsed = Number(value);\n return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : undefined;\n}\n\n/** `bytes=<start>-<end>`, `bytes=<start>-`, or the suffix `bytes=-<n>`. Nothing else: a multi-range\n * request, a non-`bytes` unit, or junk fails to match and is treated as absent. */\nconst SINGLE_BYTE_RANGE = /^bytes=(\\d*)-(\\d*)$/;\n\n/**\n * Parse a `Range` request header into something the service can resolve against the object's size.\n *\n * Returns `null` for \"no usable range\", which the caller treats as no range at all — a full 200\n * body. That is deliberate and RFC 9110-sanctioned: \"An origin server MUST ignore a Range header\n * field that contains a range unit it does not understand\", and a server may likewise ignore one it\n * cannot parse. Ignoring is safe (the client gets everything it asked for and more); guessing is\n * not, which is why anything outside the single-range grammar above is rejected wholesale rather\n * than partially interpreted.\n */\nexport function parseRangeHeader(header: string | undefined): RequestedReadRange | null {\n if (header === undefined) return null;\n const match = SINGLE_BYTE_RANGE.exec(header.trim());\n if (!match) return null;\n const startText = match[1] ?? '';\n const endText = match[2] ?? '';\n // `bytes=-` names neither bound — meaningless, not a range.\n if (startText === '' && endText === '') return null;\n // `bytes=-N` — the LAST n bytes. Only the size tells us where that starts, so it travels as a\n // suffix and the service (which has the stat) makes it absolute.\n if (startText === '') return { suffixLength: Number(endText) };\n const start = Number(startText);\n return endText === '' ? { start } : { start, end: Number(endText) };\n}\n\n/** The response surface this controller drives, structurally typed: Express' response and Fastify's\n * reply both expose `status()`, and both reach the Node response for `setHeader` (Fastify via\n * `.raw`). Keeps the package free of a platform-specific type dependency, like `auth/response.ts`. */\ninterface ConsoleHttpResponse {\n status?(code: number): unknown;\n statusCode?: number;\n setHeader?(name: string, value: string): unknown;\n raw?: { statusCode?: number; setHeader?(name: string, value: string): unknown };\n}\n\nfunction setHeader(response: ConsoleHttpResponse, name: string, value: string): void {\n if (typeof response.setHeader === 'function') {\n response.setHeader(name, value);\n return;\n }\n if (typeof response.raw?.setHeader === 'function') response.raw.setHeader(name, value);\n}\n\n/** Set the response status without ending the response — Nest still pipes the `StreamableFile`\n * afterwards, and (with `passthrough`) does not re-apply the route's default 200 over this. */\nfunction setStatus(response: ConsoleHttpResponse, code: number): void {\n if (typeof response.status === 'function') {\n response.status(code);\n return;\n }\n if (response.raw) response.raw.statusCode = code;\n else response.statusCode = code;\n}\n\n/**\n * Read-only JSON API for the /media console. Bare `@Controller()` — the path prefix is applied by\n * `RouterModule` (set in `MediaDashboardModule.forRoot({ apiBasePath })`). Always mounted.\n * `MediaConsoleGuard` gates it on a session cookie when the host configured `auth` (else a no-op).\n */\n@UseGuards(MediaConsoleGuard)\n@Controller()\nexport class MediaConsoleReadController {\n constructor(@Inject(MediaConsoleService) private readonly service: MediaConsoleService) {}\n\n @Get('disks')\n disks(): DiskListResponse {\n return this.service.listDisks();\n }\n\n @Get('disks/:disk/objects')\n objects(\n @Param('disk') disk: string,\n @Query('prefix') prefix?: string,\n @Query('cursor') cursor?: string,\n @Query('limit') limit?: string,\n ): Promise<ObjectListResponse> {\n const limitValue = toLimit(limit);\n return this.service.listObjects(disk, {\n ...(prefix ? { prefix } : {}),\n ...(cursor ? { cursor } : {}),\n ...(limitValue !== undefined ? { limit: limitValue } : {}),\n });\n }\n\n @Get('disks/:disk/object')\n object(@Param('disk') disk: string, @Query('key') key: string): Promise<ObjectDetailResponse> {\n return this.service.objectDetail(disk, key);\n }\n\n /** What the host knows about this object, from its registered `objectInsights` providers. Empty\n * when none are registered — the console renders nothing for an empty list. */\n @Get('disks/:disk/object/insights')\n objectInsights(\n @Param('disk') disk: string,\n @Query('key') key: string,\n ): Promise<ObjectInsightsResponse> {\n return this.service.objectInsights(disk, key);\n }\n\n /**\n * Streams the object's bytes inline (Content-Disposition: inline) from the same origin, so the SPA\n * can render text/PDF previews the browser would otherwise download, and read text past CORS.\n */\n @Get('disks/:disk/object/raw')\n objectRaw(\n @Param('disk') disk: string,\n @Query('key') key: string,\n @Headers('range') rangeHeader: string | undefined,\n @Res({ passthrough: true }) response: ConsoleHttpResponse,\n ): Promise<StreamableFile> {\n return this.streamObject(disk, key, rangeHeader, response, 'inline');\n }\n\n /**\n * The same bytes as `/raw`, but as a SAVE rather than a render: `Content-Disposition: attachment`\n * with the object's own name on it.\n *\n * Its own route rather than a flag on `/raw` because `/raw` is load-bearing for every preview —\n * they depend on `inline`, and a disposition that varied by query param would be one wrong link\n * away from a PDF preview turning into a download.\n *\n * Same-origin and unconditional, deliberately: this is the console's own action, so it must work\n * on a host whose browser cannot reach the object store at all (no route to S3, no CORS grant).\n * That is exactly where `objectDetail`'s presigned `url` — a link to share, not a way to fetch —\n * is useless. See the `objectUrls` option.\n */\n @Get('disks/:disk/object/download')\n objectDownload(\n @Param('disk') disk: string,\n @Query('key') key: string,\n @Headers('range') rangeHeader: string | undefined,\n @Res({ passthrough: true }) response: ConsoleHttpResponse,\n ): Promise<StreamableFile> {\n return this.streamObject(\n disk,\n key,\n rangeHeader,\n response,\n attachmentDisposition(downloadFilename(key ?? '')),\n );\n }\n\n /**\n * Stream an object under the given `Content-Disposition`, honouring a single-range `Range` header\n * — which is what lets a reader pull a few KB out of a multi-hundred-MB object (a SQLite page, a\n * ZIP's central directory) instead of the whole file, and what lets a browser resume an\n * interrupted download of one. With no `Range` the response is a plain 200 full body, which the\n * PDF/text/spreadsheet previews depend on.\n */\n private async streamObject(\n disk: string,\n key: string,\n rangeHeader: string | undefined,\n response: ConsoleHttpResponse,\n disposition: string,\n ): Promise<StreamableFile> {\n const requested = parseRangeHeader(rangeHeader);\n // On EVERY response, 200 included: `Accept-Ranges` is how a client discovers that ranged reads\n // work here at all. A client that can't tell will either not try, or try and mis-handle a 200.\n setHeader(response, 'Accept-Ranges', 'bytes');\n\n let result: Awaited<ReturnType<MediaConsoleService['objectStream']>>;\n try {\n result = await this.service.objectStream(disk, key, requested ?? undefined);\n } catch (error) {\n // RFC 9110 requires the 416 to name the object's real size so the client can re-ask. Nest's\n // exception filter serializes the body but cannot know the size, so the header goes on here,\n // before the rethrow — headers already on the response survive the filter's `status().json()`.\n if (error instanceof RangeNotSatisfiableException) {\n setHeader(response, 'Content-Range', `bytes */${error.size}`);\n }\n throw error;\n }\n\n const { stream, contentType, size, range } = result;\n if (!range) {\n return new StreamableFile(stream, {\n type: contentType,\n disposition,\n ...(Number.isFinite(size) ? { length: size } : {}),\n });\n }\n setStatus(response, 206);\n setHeader(response, 'Content-Range', `bytes ${range.start}-${range.end}/${size}`);\n // Content-Length is the SLICE's length, not the object's — inclusive bounds, hence the +1.\n return new StreamableFile(stream, {\n type: contentType,\n disposition,\n length: range.end - range.start + 1,\n });\n }\n\n @Get('uploads')\n uploads(\n @Query('disk') disk?: string,\n @Query('prefix') prefix?: string,\n ): Promise<UploadListResponse> {\n return this.service.listUploads({\n ...(disk ? { disk } : {}),\n ...(prefix ? { prefix } : {}),\n });\n }\n\n @Get('uploads/:id')\n upload(@Param('id') id: string): Promise<UploadDetailResponse> {\n return this.service.uploadDetail(id);\n }\n\n @Get('library/collections')\n collections(): Promise<CollectionsResponse> {\n return this.service.listCollections();\n }\n\n @Get('library')\n library(\n @Query('collection') collection?: string,\n @Query('disk') disk?: string,\n @Query('cursor') cursor?: string,\n @Query('limit') limit?: string,\n ): Promise<LibraryListResponse> {\n const limitValue = toLimit(limit);\n return this.service.listLibrary({\n ...(collection ? { collection } : {}),\n ...(disk ? { disk } : {}),\n ...(cursor ? { cursor } : {}),\n ...(limitValue !== undefined ? { limit: limitValue } : {}),\n });\n }\n\n @Get('library/:id')\n libraryRecord(@Param('id') id: string): Promise<LibraryDetailResponse> {\n return this.service.libraryDetail(id);\n }\n\n @Get('topology')\n topology(): Topology {\n return this.service.topology();\n }\n}\n","/** What a download is called when the key cannot name it — a folder marker, a blank key. */\nconst FALLBACK_FILENAME = 'download';\n\n/**\n * RFC 5987 `attr-char`: the only bytes that may stand unescaped inside an `ext-value`. Everything\n * else is percent-encoded. `encodeURIComponent` alone is not enough — it leaves `'`, `(`, `)` and\n * `*` intact, and a bare `'` closes the ext-value early, truncating the name from that point on.\n */\nconst ATTR_CHAR = /^[A-Za-z0-9!#$&+\\-.^_`|~]$/;\n\n/** Percent-encode a name into an RFC 5987 `ext-value` payload (the part after `UTF-8''`). */\nfunction toExtValue(name: string): string {\n let encoded = '';\n for (const byte of new TextEncoder().encode(name)) {\n const char = String.fromCharCode(byte);\n encoded += ATTR_CHAR.test(char) ? char : `%${byte.toString(16).toUpperCase().padStart(2, '0')}`;\n }\n return encoded;\n}\n\n/** The name to offer for an object key — its last segment, or a fallback when it has none. */\nexport function downloadFilename(key: string): string {\n const segment = key.split('/').pop() ?? '';\n return segment.trim() === '' ? FALLBACK_FILENAME : segment;\n}\n\n/**\n * Build a `Content-Disposition` that makes the browser SAVE the body under `filename`, in both the\n * forms RFC 6266 defines: a quoted ASCII `filename` every user agent understands, and the\n * `filename*` ext-value that carries the real, possibly non-ASCII name for those that prefer it.\n *\n * The quoted form is a header field value, so it is stripped down to what one can legally hold:\n * control characters (which would forge a header break), `\"` and `\\` (which would close the quoted\n * string and let a crafted object key append parameters of its own), and anything outside printable\n * ASCII. That strip is a security boundary, not tidiness — the name reaching here comes from a\n * storage key, which the console does not control.\n */\nexport function attachmentDisposition(filename: string): string {\n const name = filename.trim() === '' ? FALLBACK_FILENAME : filename;\n const ascii = name\n .replace(/[^\\x20-\\x7E]/g, '')\n .replace(/[\"\\\\]/g, '')\n .trim();\n const quoted = ascii === '' ? FALLBACK_FILENAME : ascii;\n return `attachment; filename=\"${quoted}\"; filename*=UTF-8''${toExtValue(name)}`;\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { basename, extname, join, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n Controller,\n Get,\n Header,\n Inject,\n Logger,\n NotFoundException,\n Optional,\n Param,\n Req,\n Res,\n StreamableFile,\n} from '@nestjs/common';\nimport type { ResolvedConsoleAuth } from './auth/config.js';\nimport { parseCookieHeader } from './auth/cookie-header.js';\nimport { readCookieHeader } from './auth/request.js';\nimport { responseAlreadyWritten, sendHtml } from './auth/response.js';\nimport { SESSION_COOKIE_NAME } from './auth/session-cookie-io.js';\nimport { verifySessionCookie } from './auth/session-cookie.js';\nimport {\n MEDIA_CONSOLE_AUTH,\n MEDIA_DASHBOARD_API_PATH,\n MEDIA_DASHBOARD_BASE_PATH,\n} from './tokens.js';\n\n/** The base the SPA bundle was built with (Vite `base`); rewritten to the configured base at serve time. */\nconst BUILD_BASE = '/media';\n\n/** dist/server/index.js -> ../spa (the Vite build output). */\nfunction spaDir(): string {\n return fileURLToPath(new URL('../spa', import.meta.url));\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.json': 'application/json; charset=utf-8',\n '.woff2': 'font/woff2',\n '.ico': 'image/x-icon',\n};\n\n/**\n * Serves the bundled /media console SPA at the configured base (+ hashed assets at `<base>/assets`).\n * The path comes from `RouterModule` (set by `MediaDashboardModule.forRoot({ basePath })`), so the\n * controller routes are relative.\n */\n@Controller()\nexport class MediaDashboardUiController {\n private readonly dir = spaDir();\n private readonly logger = new Logger(MediaDashboardUiController.name);\n private warned = false;\n\n constructor(\n @Inject(MEDIA_DASHBOARD_BASE_PATH) private readonly basePath: string,\n @Inject(MEDIA_DASHBOARD_API_PATH) private readonly apiBasePath: string,\n @Optional()\n @Inject(MEDIA_CONSOLE_AUTH)\n private readonly auth: ResolvedConsoleAuth | null = null,\n ) {}\n\n // index.html references hash-named bundles, so it MUST NOT be cached (stale bundle = \"stuck\n // loading after a deploy\"). The hashed assets below are immutable. `sendHtml` sets both headers;\n // they are no longer `@Header` decorators because this route is now non-passthrough (see below).\n //\n // Non-passthrough `@Res()` so a host's `auth.unauthenticatedPage` can OWN the response and serve\n // its own page in place of the SPA. Without a hook this behaves exactly as before: the shell is\n // served to everyone, and the SPA's own auth screen renders once its API calls come back 401.\n @Get()\n async index(@Req() req: unknown, @Res() res: unknown): Promise<void> {\n if (await this.serveUnauthenticatedPage(req, res)) return;\n\n const indexPath = join(this.dir, 'index.html');\n if (!existsSync(indexPath)) {\n throw new NotFoundException('Console SPA is not built. Run the package build.');\n }\n // Built with Vite base `/media/`; rewrite asset URLs to the configured base so the SPA loads\n // from `<base>/assets` wherever it's mounted, and tell the client its API base.\n const html = readFileSync(indexPath, 'utf8').replaceAll(\n `=\"${BUILD_BASE}/`,\n `=\"${this.basePath}/`,\n );\n const inject = `<script>window.__MEDIA_BASE__='${this.basePath}';window.__MEDIA_API__='${this.apiBasePath}';</script>`;\n sendHtml(\n res,\n 200,\n html.includes('</head>') ? html.replace('</head>', `${inject}</head>`) : inject + html,\n );\n }\n\n /**\n * Hand the response to the host's `unauthenticatedPage` when this navigation has no valid session.\n * Returns `true` when the host wrote a page (the caller must not also serve the SPA).\n *\n * Mode-A-only, deliberately: with `login` configured the SPA's own login form is the way in, so\n * replacing the shell would lock a Mode B host out of its own console.\n */\n private async serveUnauthenticatedPage(req: unknown, res: unknown): Promise<boolean> {\n const auth = this.auth;\n const hook = auth?.unauthenticatedPage;\n if (!auth || !hook) return false;\n if (auth.modes.includes('login')) return false;\n if (this.hasValidSession(auth, req)) return false;\n\n try {\n await hook({ request: req, response: res, basePath: this.basePath });\n if (responseAlreadyWritten(res)) return true;\n // Returned without writing. Serving the SPA is the only outcome that neither hangs the\n // request nor invents a page the host did not ask for — but it is almost certainly a bug.\n this.warnOnce(\n 'auth.unauthenticatedPage returned without writing a response; serving the console SPA ' +\n 'instead. The hook must write AND end the response it is given.',\n );\n } catch (error) {\n this.warnOnce(\n `auth.unauthenticatedPage threw; serving the console SPA instead. ${String(error)}`,\n );\n // It may have written headers before throwing; writing again would raise\n // ERR_HTTP_HEADERS_SENT on top of the original failure.\n if (responseAlreadyWritten(res)) return true;\n }\n // Falling back to the SPA is safe: every data route stays behind MediaConsoleGuard, so the\n // visitor gets the built-in auth screen, not the console's contents.\n return false;\n }\n\n private hasValidSession(auth: ResolvedConsoleAuth, req: unknown): boolean {\n const cookieValue = parseCookieHeader(readCookieHeader(req))[SESSION_COOKIE_NAME];\n if (cookieValue === undefined) return false;\n return verifySessionCookie(cookieValue, { secret: auth.secret }) !== null;\n }\n\n /** Once per process, not once per request: a broken hook fires on every unauthenticated visit. */\n private warnOnce(message: string): void {\n if (this.warned) return;\n this.warned = true;\n this.logger.warn(message);\n }\n\n @Get('assets/:file')\n @Header('Cache-Control', 'public, max-age=31536000, immutable')\n asset(@Param('file') file: string): StreamableFile {\n const safe = basename(file);\n if (safe !== file) throw new NotFoundException();\n const root = resolve(this.dir, 'assets');\n const assetPath = resolve(root, safe);\n if (!assetPath.startsWith(root + sep) || !existsSync(assetPath)) {\n throw new NotFoundException();\n }\n const type = CONTENT_TYPES[extname(safe)] ?? 'application/octet-stream';\n return new StreamableFile(readFileSync(assetPath), { type });\n }\n}\n"],"mappings":"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","names":["getImportMetaUrl","document","URL","__filename","href","currentScript","tagName","toUpperCase","src","baseURI","importMetaUrl","import_common","DEFAULT_TTL_MS","DURATION_UNITS","s","m","h","d","durationToMs","ttl","undefined","match","exec","trim","unit","Number","resolveConsoleAuth","options","secret","Error","modes","session","push","login","length","unauthenticatedPage","ttlMs","revalidate","GUARDS_METADATA","isGuardClass","guard","baseGuards","controller","Reflect","getOwnMetadata","stampGuards","guards","entries","undefined","length","base","defineMetadata","import_common","import_common","parseCookieHeader","header","cookies","segment","split","eq","indexOf","name","slice","trim","rawValue","length","startsWith","endsWith","decodeURIComponent","serializeSetCookie","value","options","parts","clear","encodeURIComponent","push","path","secure","maxAgeSeconds","join","isRecord","value","readHeaders","request","headers","headerToString","Array","isArray","every","part","join","undefined","readCookieHeader","cookie","isHttpsRequest","protocol","forwarded","split","map","proto","trim","toLowerCase","includes","attachSession","session","consoleSession","isRecord","value","isRawNodeResponse","getHeader","setHeader","resolveRawResponse","response","raw","existingSetCookies","current","Array","isArray","appendSetCookie","cookie","resolveEndableResponse","isEndable","end","responseAlreadyWritten","headersSent","sendHtml","status","html","statusCode","EXP_GRACE_MS","base64urlEncode","value","Buffer","from","toString","base64urlDecode","hmac","secret","payload","createHmac","update","digest","signSessionCookie","user","options","issuedAt","now","Date","session","sub","id","name","undefined","roles","iat","exp","ttlMs","encodedPayload","JSON","stringify","isSessionPayload","record","Array","isArray","every","role","Number","isFinite","verifySessionCookie","dot","indexOf","length","slice","provided","expected","timingSafeEqual","decoded","parse","SESSION_COOKIE_NAME","issueSessionCookie","user","context","value","signSessionCookie","secret","auth","ttlMs","now","undefined","appendSetCookie","response","serializeSetCookie","path","cookiePath","maxAgeSeconds","Math","floor","secure","isHttpsRequest","request","clearSessionCookie","clear","MEDIA_STORE","Symbol","for","MEDIA_UPLOAD_SESSIONS","MEDIA_STORAGE_SHARED","MEDIA_DASHBOARD_BASE_PATH","MEDIA_DASHBOARD_API_PATH","MEDIA_DASHBOARD_ACTIONS","MEDIA_CONSOLE_AUTH","MEDIA_CONSOLE_COOKIE_PATH","MEDIA_CONSOLE_OBJECT_INSIGHTS","MEDIA_CONSOLE_OBJECT_URLS","MediaConsoleGuard","auth","cookiePath","canActivate","context","http","switchToHttp","request","getRequest","session","verifyRequestSession","UnauthorizedException","attachSession","maybeRenew","getResponse","cookieValue","parseCookieHeader","readCookieHeader","SESSION_COOKIE_NAME","undefined","verifySessionCookie","secret","response","now","Date","iat","ttlMs","user","id","sub","name","roles","revalidate","allowed","clearSessionCookie","issueSessionCookie","import_common","SAFE_LINK_SCHEMES","Set","sanitizeInsight","insight","links","undefined","filter","link","href","startsWith","has","URL","protocol","objectProxyUrl","apiBasePath","route","disk","key","path","encodeURIComponent","URL_TTL_SECONDS","DEFAULT_PAGE_LIMIT","MAX_UPLOAD_BYTES","DELETE_SWEEP_LIMIT","MAX_CROSS_DISK_BYTES","lastSegment","prefix","trimmed","replace","slash","lastIndexOf","slice","RangeNotSatisfiableException","HttpException","size","statusCode","HttpStatus","REQUESTED_RANGE_NOT_SATISFIABLE","message","error","resolveRange","range","Number","isInteger","suffixLength","start","Math","max","end","undefined","min","mapUpload","session","percent","round","offset","id","disk","key","parts","multipart","multipartUploadId","createdAt","toISOString","mapRecord","record","ownerType","ownerId","collection","name","fileName","mimeType","path","MediaConsoleService","storage","store","uploads","actionsEnabled","insightProviders","objectUrls","objectUrl","driver","strategy","Promise","resolve","objectProxyUrl","apiBasePath","capabilities","presign","temporaryUrl","url","logger","Logger","diskOrThrow","diskNames","includes","NotFoundException","listDisks","disks","defaultDisk","map","default","listObjects","options","result","list","delimiter","cursor","limit","folders","filter","folder","files","entry","sizeBytes","lastModified","objectDetail","stat","contentType","objectInsights","providers","length","insights","settled","all","provider","warn","Error","String","insight","sanitizeInsight","objectStream","stream","ranged","NotImplementedException","resolved","putObject","body","chunks","total","chunk","buffer","Buffer","isBuffer","from","PayloadTooLargeException","push","put","concat","createFolder","normalized","BadRequestException","alloc","deleteObject","delete","deleteFolder","sweepPrefix","keys","deleteMany","copyObject","fromDisk","toDisk","to","transferKey","moveObject","op","fromDriver","move","copy","toDriver","bytes","get","moveFolder","transferFolder","copyFolder","fromNormalized","toNormalized","fromPrefix","toPrefix","startsWith","destKey","listUploads","sessions","keyPrefix","uploadDetail","listParts","upload","abortUpload","listCollections","aggregate","collections","buckets","groupBy","sum","listLibrary","records","page","libraryDetail","find","variants","Object","entries","conversions","conversion","deleteLibraryRecord","topology","hasStore","hasUploads","actions","MediaConsoleActionsController","service","deleteObject","disk","key","copyObject","body","from","toDisk","to","moveObject","moveFolder","copyFolder","uploadObject","request","type","putObject","createFolder","prefix","deleteFolder","abortUpload","id","deleteLibraryRecord","import_common","MediaConsoleAuthController","logger","Logger","name","warnedHooks","Set","auth","cookiePath","me","request","authRequired","cookieValue","parseCookieHeader","readCookieHeader","SESSION_COOKIE_NAME","session","undefined","verifySessionCookie","secret","UnauthorizedException","modes","user","id","sub","roles","login","body","response","requireAuth","NotFoundException","username","password","BadRequestException","runHook","message","mint","logout","clearSessionCookie","issueSessionCookie","kind","run","error","has","add","warn","String","passthrough","import_common","FALLBACK_FILENAME","ATTR_CHAR","toExtValue","name","encoded","byte","TextEncoder","encode","char","String","fromCharCode","test","toString","toUpperCase","padStart","downloadFilename","key","segment","split","pop","trim","attachmentDisposition","filename","ascii","replace","quoted","toLimit","value","undefined","parsed","Number","isFinite","Math","floor","SINGLE_BYTE_RANGE","parseRangeHeader","header","match","exec","trim","startText","endText","suffixLength","start","end","setHeader","response","name","raw","setStatus","code","status","statusCode","MediaConsoleReadController","service","disks","listDisks","objects","disk","prefix","cursor","limit","limitValue","listObjects","object","key","objectDetail","objectInsights","objectRaw","rangeHeader","streamObject","objectDownload","attachmentDisposition","downloadFilename","disposition","requested","result","objectStream","error","RangeNotSatisfiableException","size","stream","contentType","range","StreamableFile","type","length","uploads","listUploads","upload","id","uploadDetail","collections","listCollections","library","collection","listLibrary","libraryRecord","libraryDetail","topology","passthrough","READ_BASE_GUARDS","baseGuards","MediaConsoleReadController","ACTIONS_BASE_GUARDS","MediaConsoleActionsController","MediaConsoleApiModule","register","options","stampGuards","guards","module","imports","controllers","MediaConsoleAuthController","actions","providers","MediaConsoleService","MediaConsoleGuard","provide","MEDIA_DASHBOARD_ACTIONS","useValue","MEDIA_CONSOLE_COOKIE_PATH","cookiePath","MEDIA_CONSOLE_OBJECT_URLS","objectUrls","authProvider","insightsProvider","MEDIA_CONSOLE_OBJECT_INSIGHTS","filter","isGuardClass","exports","MEDIA_CONSOLE_AUTH","import_common","BUILD_BASE","spaDir","fileURLToPath","URL","CONTENT_TYPES","MediaDashboardUiController","dir","logger","Logger","name","warned","basePath","apiBasePath","auth","index","req","res","serveUnauthenticatedPage","indexPath","join","existsSync","NotFoundException","html","readFileSync","replaceAll","inject","sendHtml","includes","replace","hook","unauthenticatedPage","modes","hasValidSession","request","response","responseAlreadyWritten","warnOnce","error","String","cookieValue","parseCookieHeader","readCookieHeader","SESSION_COOKIE_NAME","undefined","verifySessionCookie","secret","message","warn","asset","file","safe","basename","root","resolve","assetPath","startsWith","sep","type","extname","StreamableFile","normalize","path","replace","MediaDashboardModule","forRoot","options","apiBasePath","basePath","build","provide","MEDIA_CONSOLE_AUTH","useValue","resolveConsoleAuth","auth","MEDIA_CONSOLE_OBJECT_INSIGHTS","objectInsights","imports","forRootAsync","authProvider","inject","useFactory","deps","useAuth","insightsProvider","injectObjectInsights","useObjectInsights","undefined","actions","stampGuards","guards","MediaDashboardUiController","module","MediaConsoleApiModule","register","objectUrls","strategy","cookiePath","RouterModule","controllers","providers","MEDIA_DASHBOARD_BASE_PATH","MEDIA_DASHBOARD_API_PATH","filter","isGuardClass","exports"]}