@dudousxd/nestjs-media-dashboard 0.16.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.d.ts +1 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -4
- package/dist/client/index.js.map +1 -1
- package/dist/client/media-console-client.d.ts +14 -23
- package/dist/client/media-console-client.d.ts.map +1 -1
- package/dist/client/media-console-client.js +53 -46
- package/dist/client/media-console-client.js.map +1 -1
- package/dist/client/types.d.ts +10 -6
- package/dist/client/types.d.ts.map +1 -1
- package/dist/server/index.cjs +805 -1235
- package/dist/server/index.cjs.map +1 -1
- package/dist/server/index.d.cts +362 -239
- package/dist/server/index.d.ts +362 -239
- package/dist/server/index.js +790 -1235
- package/dist/server/index.js.map +1 -1
- package/dist/spa/assets/{ArchivePreview-h5wYCDJT.js → ArchivePreview-DacpA4QQ.js} +1 -1
- package/dist/spa/assets/CertificatePreview-CEfJ-ORW.js +127 -0
- package/dist/spa/assets/MarkdownPreview-47FMx48l.js +74 -0
- package/dist/spa/assets/ParquetPreview-DHvs5kV-.js +973 -0
- package/dist/spa/assets/{SheetPreview-Bfq7dI0a.js → SheetPreview-AT23RGVb.js} +1 -1
- package/dist/spa/assets/{SqlitePreview-JTQDjJJ3.js → SqlitePreview-D7KZuyZD.js} +1 -1
- package/dist/spa/assets/{index-Cjax4szq.js → index-BmejPQYi.js} +1 -1
- package/dist/spa/assets/{preview-BjGG6sSu.js → preview-DVKz8AwF.js} +2 -2
- package/dist/spa/assets/styles-CG_mruF7.css +1 -0
- package/dist/spa/assets/styles-CTvG1yXb.js +61 -0
- package/dist/spa/index.html +3 -3
- package/dist/spa/preview.html +3 -3
- package/package.json +25 -30
- package/dist/client/console-session.d.ts +0 -69
- package/dist/client/console-session.d.ts.map +0 -1
- package/dist/client/console-session.js +0 -120
- package/dist/client/console-session.js.map +0 -1
- package/dist/react/index.d.ts +0 -4
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -15
- package/dist/react/index.js.map +0 -1
- package/dist/react/open-console-button.d.ts +0 -26
- package/dist/react/open-console-button.d.ts.map +0 -1
- package/dist/react/open-console-button.js +0 -29
- package/dist/react/open-console-button.js.map +0 -1
- package/dist/react/use-open-console.d.ts +0 -45
- package/dist/react/use-open-console.d.ts.map +0 -1
- package/dist/react/use-open-console.js +0 -75
- package/dist/react/use-open-console.js.map +0 -1
- package/dist/spa/assets/CertificatePreview-Bm4NkGaO.js +0 -127
- package/dist/spa/assets/MarkdownPreview-Cai2BrTH.js +0 -70
- package/dist/spa/assets/ParquetPreview-DfDTL_6f.js +0 -973
- package/dist/spa/assets/styles-Cui6Yy9Y.js +0 -61
- package/dist/spa/assets/styles-FRkIQ83o.css +0 -1
|
@@ -1 +1 @@
|
|
|
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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/server/index.ts","../../../../node_modules/.pnpm/tsup@8.5.1_@swc+core@1.16.21_jiti@1.21.7_postcss@8.5.32_typescript@5.9.3_yaml@2.9.0/node_modules/tsup/assets/cjs_shims.js","../../src/server/media-dashboard.module.ts","../../src/server/controller-decorators.ts","../../src/server/console-authorize.ts","../../src/server/media-console-api.module.ts","../../src/server/media-console-actions.controller.ts","../../src/server/media-console.service.ts","../../src/server/object-insights.ts","../../src/server/object-urls.ts","../../src/server/tokens.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 {\n MediaConsoleApiModule,\n type MediaConsoleApiModuleOptions,\n} from './media-console-api.module.js';\n// Console access: the `authorize` contract (same names and shapes in every Aviary console).\nexport {\n type ConsoleAuthorize,\n type ConsoleAuthorizeResult,\n type ConsoleAuthorizer,\n type ConsoleAuthorizerClass,\n type ConsolePolicyCheck,\n type ConsoleDecision,\n type ConsoleDecide,\n type ConsoleGate,\n CONSOLE_GATE,\n CONSOLE_AUTHORIZE_DECIDE,\n CONSOLE_AUTHORIZE_MISSING_WARNING,\n CONSOLE_PAGE_METADATA,\n ConsolePage,\n normalizeConsoleResult,\n authorizeByToken,\n resolveConsoleAuthorize,\n ConsoleAuthorizeGuard,\n ConsoleAuthorizeFilter,\n ConsoleAccessDenied,\n UseConsoleAuthorize,\n} from './console-authorize.js';\nexport type { ObjectUrlConfig, ObjectUrlStrategy } from './object-urls.js';\nexport { MediaConsoleService } from './media-console.service.js';\n// The controllers, for a host that mounts the console itself (`routes: false`) by extending them.\nexport { MediaDashboardUiController } from './media-dashboard-ui.controller.js';\nexport { MediaConsoleReadController } from './media-console-read.controller.js';\nexport { MediaConsoleActionsController } from './media-console-actions.controller.js';\nexport {\n MEDIA_MULTIPART,\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 DynamicModule,\n Module,\n type OptionalFactoryDependency,\n type Provider,\n} from '@nestjs/common';\nimport type { InjectionToken } from '@nestjs/common';\nimport { RouterModule } from '@nestjs/core';\nimport type { ConsoleAuthorize } from './console-authorize.js';\nimport { consoleController } from './controller-decorators.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_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 * 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.\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 * Extra `imports` merged into the dashboard's dynamic module — the DI resolution path for the\n * dependencies of a guard passed through {@link decorators} (e.g. `UseGuards(AuthenticationGuard)`),\n * or any other provider the controllers need reachable.\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 * Who may use the console: decided on EVERY console request (the page, its assets and the JSON\n * API) by a guard that runs after your `decorators`, so your authentication guards set\n * `request.user` first. Five forms:\n *\n * ```ts\n * authorize: (ctx) => ctx.switchToHttp().getRequest().user?.isAdmin === true,\n * authorize: ConsoleAccess, // @Injectable() implements ConsoleAuthorizer\n * authorize: () => import('./console-access.js'), // lazy, default export\n * authorize: { policy: ConsolePolicy, ability: 'view' }, // @nestjs/authorization policy\n * authorize: 'console.media', // a gate from @dudousxd/nestjs-authz\n * ```\n *\n * Answer `true`/`{ allowed: true }` to allow, `false`/`{ allowed: false }` for 403,\n * `{ allowed: false, reason: 'unauthenticated' }` for 401, or `{ redirectTo: '/login' }` to send a\n * page navigation there (API calls get 401 with `redirectTo`). A throw is a 403.\n *\n * Omitted: open outside production; in production every request answers 403, and one warning is\n * logged at boot.\n *\n * This is the console's access. It is not `MediaModule`'s `authorize(user, action, subject)` hook,\n * which governs the product upload routes.\n */\n authorize?: ConsoleAuthorize;\n /**\n * Class decorators applied to EVERY controller the console mounts — the page/asset controller\n * and the read and action APIs — e.g.\n * `[UseGuards(AuthenticationGuard), Can(MediaPolicy, 'console')]` with the first-party\n * `@nestjs/authentication` / `@nestjs/authorization`. Default: none.\n *\n * Every console controller is marked `@Can.Anyone()`-open (`'authorization:can-anyone'`) so\n * `@nestjs/authorization`'s deny-by-default global guard doesn't refuse the console; a `Can(...)`\n * passed here is still checked (the guard treats `can-anyone` as open only when no requirement is\n * declared).\n *\n * `decorators` don't decide access on their own: use them for authentication, throttling and the\n * like, and {@link authorize} for who may use the console.\n *\n * Full-page navigations to `basePath` carry only cookies, never an `Authorization` header — a\n * guard here must be able to authenticate the page from a cookie, or every browser visit 401s.\n *\n * Each registration decorates fresh subclasses of the controllers, so one registration's\n * decorators never reach another.\n */\n decorators?: ClassDecorator[];\n /**\n * `false` mounts NO controller — no page, no JSON API — while every provider still works and is\n * exported (`MediaConsoleService`, the resolved paths), so you serve\n * the console from controllers of your own by extending the exported controller classes. Apply\n * `UseConsoleAuthorize()` to them, ABOVE your authentication guard (class decorators apply\n * bottom-up, so authentication runs first), so {@link authorize} still decides access. Keep\n * `basePath`/`apiBasePath` equal to where you mount them: the SPA reads both.\n *\n * ```ts\n * @UseConsoleAuthorize()\n * @UseGuards(AuthGuard)\n * @Controller('media/api')\n * export class ConsoleApiController extends MediaConsoleReadController {}\n * ```\n *\n * Default `true`.\n */\n routes?: boolean;\n}\n\n/**\n * Async variant of {@link MediaDashboardOptions}. The mount paths, `actions` and `decorators` stay\n * static (the router needs them at module-definition time); the `objectInsights` providers are\n * built by an injected factory, so they can reach your repositories and services.\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 decorator guard's own dependencies (omit\n * when they're global). */\n imports?: DynamicModule['imports'];\n /** Providers injected into `useObjectInsights`, in order. */\n inject?: Array<InjectionToken | OptionalFactoryDependency>;\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 * See {@link MediaDashboardOptions.decorators}. Static (not built by a factory): the decorators\n * shape the routes when the module is built.\n */\n decorators?: ClassDecorator[];\n /** See {@link MediaDashboardOptions.authorize}. Static, like `decorators`. */\n authorize?: ConsoleAuthorize;\n /** See {@link MediaDashboardOptions.routes}. Static, like `decorators`. */\n routes?: boolean;\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 `objectInsights` providers need injected services) alongside `MediaModule` (global), so it\n * resolves the storage/store/upload tokens. Exclude the UI/API paths from any global `/api` prefix.\n *\n * Decide who may use it with {@link MediaDashboardOptions.authorize}; without it the console is open\n * outside production and answers 403 to everything in production.\n */\n@Module({})\nexport class MediaDashboardModule {\n static forRoot(options: MediaDashboardOptions = {}): DynamicModule {\n return MediaDashboardModule.build('MediaDashboardModule.forRoot()', options, {\n provide: MEDIA_CONSOLE_OBJECT_INSIGHTS,\n useValue: options.objectInsights ?? [],\n });\n }\n\n static forRootAsync(options: MediaDashboardAsyncOptions = {}): DynamicModule {\n return MediaDashboardModule.build('MediaDashboardModule.forRootAsync()', options, {\n provide: MEDIA_CONSOLE_OBJECT_INSIGHTS,\n inject: options.inject ?? [],\n useFactory: async (...deps: any[]) =>\n options.useObjectInsights === undefined ? [] : await options.useObjectInsights(...deps),\n });\n }\n\n /** Shared wiring: static routing + the API module, with the insights from the given provider. */\n private static build(\n name: string,\n options: {\n basePath?: string;\n apiBasePath?: string;\n actions?: boolean;\n objectUrls?: ObjectUrlStrategy;\n imports?: DynamicModule['imports'];\n authorize?: ConsoleAuthorize;\n decorators?: ClassDecorator[];\n routes?: boolean;\n },\n insightsProvider: Provider,\n ): DynamicModule {\n const basePath = normalize(options.basePath ?? '/media');\n const apiBasePath = normalize(options.apiBasePath ?? `${basePath}/api`);\n const actions = options.actions === true;\n const mount = options.routes !== false;\n const imports = options.imports;\n return {\n module: MediaDashboardModule,\n imports: [\n ...(imports ?? []),\n MediaConsoleApiModule.register({\n actions,\n objectUrls: { strategy: options.objectUrls ?? 'auto', apiBasePath },\n insightsProvider,\n imports,\n ...(options.authorize !== undefined ? { authorize: options.authorize } : {}),\n moduleName: name,\n ...(options.decorators ? { decorators: options.decorators } : {}),\n routes: mount,\n }),\n ...(mount\n ? [\n RouterModule.register([\n { path: basePath, module: MediaDashboardModule }, // the UI controller below\n { path: apiBasePath, module: MediaConsoleApiModule },\n ]),\n ]\n : []),\n ],\n controllers: mount ? [consoleController(MediaDashboardUiController, options.decorators)] : [],\n providers: [\n { provide: MEDIA_DASHBOARD_BASE_PATH, useValue: basePath },\n { provide: MEDIA_DASHBOARD_API_PATH, useValue: apiBasePath },\n ],\n // Re-export the API module so its MediaConsoleService reaches importers if they want it; the\n // resolved paths too, so a host mounting the controllers itself (`routes: false`) can extend\n // `MediaDashboardUiController` in a module of its own.\n exports: [MediaConsoleApiModule, MEDIA_DASHBOARD_BASE_PATH, MEDIA_DASHBOARD_API_PATH],\n };\n }\n}\n","import 'reflect-metadata';\nimport type { Type } from '@nestjs/common';\nimport { UseConsoleAuthorize } from './console-authorize.js';\n\n/**\n * `@nestjs/authorization`'s `CAN_ANYONE_METADATA` key (what `@Can.Anyone()` writes), inlined as a\n * literal: that package doesn't export the constant, and this library doesn't depend on it. A spec\n * asserts the real `Can.Anyone()` writes this exact key, so upstream drift fails loudly.\n */\nexport const CAN_ANYONE_METADATA = 'authorization:can-anyone';\n\n/**\n * Marks a controller open to `@nestjs/authorization`'s global `AuthorizationGuard`, the way\n * `@Can.Anyone()` would, without a dependency on that package. That guard denies a route that\n * declares no requirement, so without this mark every route this library mounts would be refused\n * in an app that adopts it. Who reaches these routes is decided by the console's `authorize` (and\n * any `decorators`): the guard treats `can-anyone` as open only when the route declares no `@Can()`,\n * so a `Can(...)` passed through `decorators` is still checked.\n */\nexport function OpenToAuthorization(): ClassDecorator {\n return (target) => {\n Reflect.defineMetadata(CAN_ANYONE_METADATA, true, target);\n };\n}\n\n/**\n * The controller class to mount for one console registration: a FRESH subclass per call, decorated\n * with the host's `decorators` and then with the console's own `authorize` guard and denial filter\n * (so the host's authentication guards run first and `request.user` is set). A fresh subclass,\n * like `@nestjs/mcp`'s `createMcpController`, because decorators like `UseGuards`/`Can` append to\n * the metadata of the class they're given: applying them to the shared class would leak one\n * registration's decorators into the next. The subclass inherits every route, parameter and DI\n * metadata from the original and keeps its name (for logs and `AuthorizationGuard`'s\n * `Class.handler` in denial events).\n */\nexport function consoleController<T extends Type>(\n controller: T,\n decorators: ClassDecorator[] | undefined,\n): T {\n const Decorated = class extends (controller as Type) {} as unknown as T;\n Object.defineProperty(Decorated, 'name', { value: controller.name });\n // `Reflect.decorate` applies the list bottom-up, like stacked `@A @B class` decorators.\n if (decorators && decorators.length > 0) Reflect.decorate(decorators, Decorated);\n Reflect.decorate([UseConsoleAuthorize()], Decorated);\n return Decorated;\n}\n","import 'reflect-metadata';\nimport { createHash, timingSafeEqual } from 'node:crypto';\nimport {\n type ArgumentsHost,\n type CanActivate,\n Catch,\n type ExceptionFilter,\n type ExecutionContext,\n HttpException,\n Inject,\n Injectable,\n Logger,\n type Type,\n UseFilters,\n UseGuards,\n applyDecorators,\n} from '@nestjs/common';\nimport { HttpAdapterHost, ModuleRef } from '@nestjs/core';\n\n// ── The contract (same names and shapes in every Aviary console) ─────────────────────────────────\n\nexport type ConsoleAuthorizeResult =\n | boolean\n | { allowed: true }\n | { allowed: false; reason?: 'unauthenticated' | 'forbidden' }\n | { redirectTo: string };\n\n/** Class form: an `@Injectable()` provider (constructor injection works). */\nexport interface ConsoleAuthorizer {\n authorize(context: ExecutionContext): ConsoleAuthorizeResult | Promise<ConsoleAuthorizeResult>;\n}\nexport type ConsoleAuthorizerClass = Type<ConsoleAuthorizer>;\n\n/** `@nestjs/authorization` policy check: `policy[ability](user)`. */\nexport interface ConsolePolicyCheck {\n policy: Type<unknown>;\n ability: string;\n}\n\nexport type ConsoleAuthorize =\n | ((context: ExecutionContext) => ConsoleAuthorizeResult | Promise<ConsoleAuthorizeResult>)\n | ConsoleAuthorizerClass\n | (() => Promise<{ default: ConsoleAuthorizerClass }>)\n | ConsolePolicyCheck\n | string;\n\nexport type ConsoleDecision =\n | { allowed: true }\n | { allowed: false; reason: 'unauthenticated' | 'forbidden'; redirectTo?: string };\n\n/** The provider `@dudousxd/nestjs-authz`'s `AuthzModule` registers to resolve gate names. */\nexport const CONSOLE_GATE: symbol = Symbol.for('@dudousxd/aviary:console-gate');\n\n/** What `@dudousxd/nestjs-authz` provides under {@link CONSOLE_GATE}. */\nexport interface ConsoleGate {\n allows(name: string, context: ExecutionContext): Promise<ConsoleAuthorizeResult>;\n}\n\n/** What a resolved `authorize` is: called once per request, never throws. */\nexport type ConsoleDecide = (context: ExecutionContext) => Promise<ConsoleDecision>;\n\nconst PACKAGE = '@dudousxd/nestjs-media-dashboard';\nconst MODULE = 'MediaDashboardModule';\n\n/** The one boot warning for a console with no `authorize`. Same wording in every Aviary console. */\nexport const CONSOLE_AUTHORIZE_MISSING_WARNING = consoleAuthorizeMissingWarning(\n `${MODULE}.forRoot()`,\n);\n\n/** The boot warning naming the registration (`MediaConsoleApiModule.register()` when used alone). */\nexport function consoleAuthorizeMissingWarning(registration: string): string {\n return `[${PACKAGE}] console has no \\`authorize\\` configured: it is open outside production and denies every request in production. Set \\`authorize\\` in ${registration}.`;\n}\n\n// Every decision is a fresh object: a caller may mutate what it gets back.\nconst allow = (): ConsoleDecision => ({ allowed: true });\nconst forbidden = (): ConsoleDecision => ({ allowed: false, reason: 'forbidden' });\nconst unauthenticated = (): ConsoleDecision => ({ allowed: false, reason: 'unauthenticated' });\n\nfunction isProduction(): boolean {\n return (process.env.NODE_ENV ?? '').trim().toLowerCase() === 'production';\n}\n\n/**\n * A class with `authorize()`: on the prototype, or (an instance-property arrow function,\n * `authorize = (ctx) => ...`) only on instances, so any `class`.\n */\nfunction isAuthorizerClass(value: unknown): value is ConsoleAuthorizerClass {\n if (typeof value !== 'function') return false;\n if (\n typeof (value as { prototype?: { authorize?: unknown } }).prototype?.authorize === 'function'\n ) {\n return true;\n }\n return /^class[\\s{]/.test(Function.prototype.toString.call(value));\n}\n\nfunction isPolicyCheck(value: unknown): value is ConsolePolicyCheck {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as ConsolePolicyCheck).policy === 'function' &&\n typeof (value as ConsolePolicyCheck).ability === 'string'\n );\n}\n\nfunction isLazyModule(value: unknown): value is { default: ConsoleAuthorizerClass } {\n return (\n typeof value === 'object' &&\n value !== null &&\n isAuthorizerClass((value as { default?: unknown }).default)\n );\n}\n\n/** Turns any {@link ConsoleAuthorizeResult} into a {@link ConsoleDecision}. */\nexport function normalizeConsoleResult(result: unknown): ConsoleDecision {\n if (result === true) return allow();\n if (typeof result !== 'object' || result === null) return forbidden();\n const shape = result as { allowed?: unknown; reason?: unknown; redirectTo?: unknown };\n if (typeof shape.redirectTo === 'string' && shape.allowed !== true) {\n return { allowed: false, reason: 'unauthenticated', redirectTo: shape.redirectTo };\n }\n if (shape.allowed === true) return allow();\n return shape.reason === 'unauthenticated' ? unauthenticated() : forbidden();\n}\n\n/** `null`, `undefined` and `false` mean \"nobody signed in\", like `@nestjs/authorization`'s guard. */\nfunction hasUser(user: unknown): boolean {\n return user !== null && user !== undefined && user !== false;\n}\n\n/** A dynamic import the bundler leaves alone, so the optional peer is never statically required. */\nconst importOptional = (specifier: string): Promise<Record<string, unknown>> =>\n import(/* @vite-ignore */ specifier);\n\nasync function policyDecider(\n check: ConsolePolicyCheck,\n moduleRef: ModuleRef,\n): Promise<ConsoleDecide> {\n let pkg: Record<string, unknown>;\n try {\n pkg = await importOptional('@nestjs/authorization');\n } catch {\n throw new Error(\n '`authorize: { policy, ability }` needs @nestjs/authorization installed (npm i @nestjs/authorization); or pass a function, a class, or a gate name',\n );\n }\n type Service = { can(policy: Type<unknown>, ability: string, user: unknown): Promise<boolean> };\n let service: Service | undefined;\n try {\n service = moduleRef.get<Service>(pkg.AuthorizationService as Type<Service>, { strict: false });\n } catch {\n service = undefined;\n }\n if (!service) {\n throw new Error(\n '`authorize: { policy, ability }` needs AuthorizationModule.forRoot() from @nestjs/authorization imported in the app',\n );\n }\n const found = service;\n const defaultGetUser = pkg.defaultGetUser as (context: ExecutionContext) => unknown;\n let getUser = defaultGetUser;\n try {\n const options = moduleRef.get<{ getUser?: typeof getUser } | undefined>(\n pkg.AUTHORIZATION_MODULE_OPTIONS as string,\n { strict: false },\n );\n if (typeof options?.getUser === 'function') getUser = options.getUser;\n } catch {\n // No module options registered: `defaultGetUser` reads `request.user`.\n }\n const { policy, ability } = check;\n return async (context) => {\n // `null`, `false` and `undefined` are a guest, as for `@nestjs/authorization`'s own guard.\n const user = await getUser(context);\n const signedIn = hasUser(user);\n if (await found.can(policy, ability, signedIn ? user : null)) return allow();\n return signedIn ? forbidden() : unauthenticated();\n };\n}\n\nfunction gateDecider(name: string, moduleRef: ModuleRef): ConsoleDecide {\n let gate: ConsoleGate | undefined;\n try {\n gate = moduleRef.get<ConsoleGate>(CONSOLE_GATE, { strict: false });\n } catch {\n gate = undefined;\n }\n if (!gate) {\n throw new Error(\n `\\`authorize: '${name}'\\` needs @dudousxd/nestjs-authz's AuthzModule (it resolves gate names); or pass a function, a class, or { policy, ability }`,\n );\n }\n const found = gate;\n return async (context) => normalizeConsoleResult(await found.allows(name, context));\n}\n\nfunction classDecider(moduleRef: ModuleRef): (Class: ConsoleAuthorizerClass) => ConsoleDecide {\n const instances = new Map<ConsoleAuthorizerClass, Promise<ConsoleAuthorizer>>();\n const instanceOf = (Class: ConsoleAuthorizerClass): Promise<ConsoleAuthorizer> => {\n const cached = instances.get(Class);\n if (cached) return cached;\n const pending = (async (): Promise<ConsoleAuthorizer> => {\n try {\n // A registered provider is used as is; otherwise one instance per registration.\n const registered = moduleRef.get(Class, { strict: false });\n if (registered) return registered;\n } catch {\n // Not a provider: create it below.\n }\n return moduleRef.create(Class);\n })();\n // A failed construction is retried on the next request instead of denying forever.\n pending.catch(() => instances.delete(Class));\n instances.set(Class, pending);\n return pending;\n };\n return (Class) => async (context) =>\n normalizeConsoleResult(await (await instanceOf(Class)).authorize(context));\n}\n\n/**\n * Resolves `authorize` into one function the guard calls per request. Configuration mistakes (a gate\n * name without nestjs-authz, a policy without @nestjs/authorization and its module) throw here, so\n * call it at boot. The returned function never throws: an error is a `forbidden`.\n *\n * `authorize` omitted: open outside production, `forbidden` for every request in production, and one\n * warning logged (unless `warnIfMissing: false`).\n */\nexport async function resolveConsoleAuthorize(\n authorize: ConsoleAuthorize | undefined,\n moduleRef: ModuleRef,\n options: { logger?: Logger; warnIfMissing?: boolean; registration?: string } = {},\n): Promise<ConsoleDecide> {\n const logger = options.logger ?? new Logger(MODULE);\n if (authorize === undefined) {\n if (options.warnIfMissing !== false) {\n logger.warn(\n options.registration\n ? consoleAuthorizeMissingWarning(options.registration)\n : CONSOLE_AUTHORIZE_MISSING_WARNING,\n );\n }\n const production = isProduction();\n return async () => (production ? forbidden() : allow());\n }\n\n let decide: ConsoleDecide;\n if (typeof authorize === 'string') {\n decide = gateDecider(authorize, moduleRef);\n } else if (isPolicyCheck(authorize)) {\n decide = await policyDecider(authorize, moduleRef);\n } else if (isAuthorizerClass(authorize)) {\n decide = classDecider(moduleRef)(authorize);\n } else if (typeof authorize === 'function') {\n const fromClass = classDecider(moduleRef);\n let lazy: ConsoleDecide | undefined;\n const fn = authorize as (context: ExecutionContext) => unknown;\n decide = async (context) => {\n if (lazy) return lazy(context);\n const result = await fn(context);\n if (!isLazyModule(result)) return normalizeConsoleResult(result);\n // A lazy import: remember the class, so the import runs once per registration.\n lazy = fromClass(result.default);\n return lazy(context);\n };\n } else {\n throw new Error(\n `${MODULE}: \\`authorize\\` must be a function, a class with authorize(), a lazy import, { policy, ability } or a gate name.`,\n );\n }\n\n return async (context) => {\n try {\n return await decide(context);\n } catch (error) {\n logger.warn(\n `console \\`authorize\\` threw, denying the request: ${error instanceof Error ? error.message : String(error)}`,\n );\n return forbidden();\n }\n };\n}\n\nconst sha256 = (value: string): Buffer => createHash('sha256').update(value).digest();\n\n/**\n * Allows a request carrying `Authorization: Bearer <token>` (compared timing-safe); anything else\n * is `unauthenticated`. Header only: no query string, no custom header. Throws when `token` is\n * empty, so a missing env var fails at config time instead of opening the console.\n */\nexport function authorizeByToken(options: { token: string | undefined }): ConsoleAuthorize {\n const token = options.token;\n if (typeof token !== 'string' || token.length === 0) {\n throw new Error('authorizeByToken: `token` is empty; set it (e.g. from an env var).');\n }\n const expected = sha256(token);\n return (context: ExecutionContext): ConsoleAuthorizeResult => {\n const header = context.switchToHttp().getRequest<{ headers?: Record<string, unknown> }>()\n ?.headers?.authorization;\n const match = typeof header === 'string' ? /^Bearer\\s+(.+)$/i.exec(header.trim()) : null;\n if (match?.[1] && timingSafeEqual(sha256(match[1]), expected)) return true;\n return { allowed: false, reason: 'unauthenticated' };\n };\n}\n\n// ── Enforcement: a guard (decides) + a filter scoped to the console controllers (answers) ─────────\n\n/** DI token of the resolved {@link ConsoleDecide} the guard runs. */\nexport const CONSOLE_AUTHORIZE_DECIDE: symbol = Symbol(`${PACKAGE}:console-authorize`);\n\n/** Marks a controller as serving pages: a denied `GET` that accepts HTML gets a page, not JSON. */\nexport const CONSOLE_PAGE_METADATA = `${PACKAGE}:console-page`;\nexport const ConsolePage = (): ClassDecorator => (target) => {\n Reflect.defineMetadata(CONSOLE_PAGE_METADATA, true, target);\n};\n\n/**\n * The denial the guard throws. An `HttpException` with the JSON body, so it still answers correctly\n * where {@link ConsoleAuthorizeFilter} is not applied; the filter adds the page answers.\n */\nexport class ConsoleAccessDenied extends HttpException {\n constructor(\n readonly decision: Extract<ConsoleDecision, { allowed: false }>,\n /** Whether the denied request was a page navigation (answered with a redirect or a page). */\n readonly page = false,\n ) {\n const isUnauthenticated = decision.reason === 'unauthenticated';\n super(\n isUnauthenticated\n ? {\n error: 'unauthenticated',\n ...(decision.redirectTo ? { redirectTo: decision.redirectTo } : {}),\n }\n : { error: 'forbidden' },\n isUnauthenticated ? 401 : 403,\n );\n }\n}\n\n/**\n * Runs the console's `authorize` on every request to a console controller. The dashboard applies it\n * (after your `decorators`, so your authentication guards set `request.user` first); on controllers\n * you mount yourself (`routes: false`) apply {@link UseConsoleAuthorize}.\n */\n@Injectable()\nexport class ConsoleAuthorizeGuard implements CanActivate {\n constructor(@Inject(CONSOLE_AUTHORIZE_DECIDE) private readonly decide: ConsoleDecide) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const decision = await this.decide(context);\n if (decision.allowed) return true;\n throw new ConsoleAccessDenied(decision, isPageNavigation(context));\n }\n}\n\n/** A `GET` asking for HTML on a console page controller: a browser navigation, not an API call. */\nfunction isPageNavigation(context: ExecutionContext): boolean {\n if (context.getType() !== 'http') return false;\n if (Reflect.getMetadata(CONSOLE_PAGE_METADATA, context.getClass()) !== true) return false;\n const request = context\n .switchToHttp()\n .getRequest<{ method?: string; headers?: Record<string, unknown> }>();\n const accept = request?.headers?.accept;\n return request?.method === 'GET' && typeof accept === 'string' && accept.includes('text/html');\n}\n\nconst page = (title: string, message: string): string =>\n `<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>${title}</title></head><body style=\"font-family:system-ui,sans-serif;display:grid;place-items:center;min-height:100vh;margin:0\"><main style=\"text-align:center\"><h1>${title}</h1><p>${message}</p></main></body></html>`;\n\nconst DENIED_PAGE = page('Access denied', 'You do not have access to this console.');\nconst SIGN_IN_PAGE = page('Sign in required', 'Sign in to open this console.');\n\n/**\n * Answers a console denial: JSON for API calls; for a page navigation a 302 to `redirectTo`, an\n * \"Access denied\" page (403) or a \"Sign in required\" page (401).\n */\n@Catch(ConsoleAccessDenied)\nexport class ConsoleAuthorizeFilter implements ExceptionFilter {\n constructor(@Inject(HttpAdapterHost) private readonly adapterHost: HttpAdapterHost) {}\n\n catch(exception: ConsoleAccessDenied, host: ArgumentsHost): void {\n const adapter = this.adapterHost.httpAdapter;\n const response = host.switchToHttp().getResponse();\n const { decision, page: isPage } = exception;\n if (isPage && decision.redirectTo) {\n adapter.redirect(response, 302, decision.redirectTo);\n return;\n }\n if (isPage) {\n const unauthenticated = decision.reason === 'unauthenticated';\n adapter.setHeader(response, 'Content-Type', 'text/html; charset=utf-8');\n adapter.reply(\n response,\n unauthenticated ? SIGN_IN_PAGE : DENIED_PAGE,\n unauthenticated ? 401 : 403,\n );\n return;\n }\n adapter.reply(response, exception.getResponse(), exception.getStatus());\n }\n}\n\n/**\n * The guard and its filter as one class decorator, for a console you mount yourself\n * (`routes: false`). List it ABOVE your authentication guards, so it runs after them.\n */\nexport const UseConsoleAuthorize = (): ClassDecorator & MethodDecorator =>\n applyDecorators(UseGuards(ConsoleAuthorizeGuard), UseFilters(ConsoleAuthorizeFilter));\n\n/**\n * The provider of {@link CONSOLE_AUTHORIZE_DECIDE} for one registration: `authorize` resolved at boot\n * (one warning when it is omitted, whether or not the routes are mounted).\n */\nexport function consoleAuthorizeProvider(\n authorize: ConsoleAuthorize | undefined,\n registration: string,\n): {\n provide: symbol;\n inject: [typeof ModuleRef];\n useFactory: (moduleRef: ModuleRef) => Promise<ConsoleDecide>;\n} {\n return {\n provide: CONSOLE_AUTHORIZE_DECIDE,\n inject: [ModuleRef],\n useFactory: (moduleRef) => resolveConsoleAuthorize(authorize, moduleRef, { registration }),\n };\n}\n","import { type DynamicModule, Module, type ModuleMetadata, type Provider } from '@nestjs/common';\nimport {\n CONSOLE_AUTHORIZE_DECIDE,\n type ConsoleAuthorize,\n consoleAuthorizeProvider,\n} from './console-authorize.js';\nimport { consoleController } from './controller-decorators.js';\nimport { MediaConsoleActionsController } from './media-console-actions.controller.js';\nimport { MediaConsoleReadController } from './media-console-read.controller.js';\nimport { MediaConsoleService } from './media-console.service.js';\nimport type { ObjectUrlConfig } from './object-urls.js';\nimport {\n MEDIA_CONSOLE_OBJECT_INSIGHTS,\n MEDIA_CONSOLE_OBJECT_URLS,\n MEDIA_DASHBOARD_ACTIONS,\n} from './tokens.js';\n\nexport interface MediaConsoleApiModuleOptions {\n actions: boolean;\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 insights factory's `inject` deps (or a decorator's guards' deps) live in. */\n imports?: ModuleMetadata['imports'];\n /**\n * Who may use the console API — see `MediaDashboardOptions.authorize`. Omitted: open outside\n * production, every request answers 403 in production, and one warning is logged at boot.\n */\n authorize?: ConsoleAuthorize;\n /** The registration named in the boot warning when `authorize` is omitted. Default\n * `'MediaConsoleApiModule.register()'`. */\n moduleName?: string;\n /**\n * Class decorators applied to EVERY controller this module mounts (read, actions) — see\n * `MediaDashboardOptions.decorators`. Each call decorates fresh subclasses, never the shared\n * classes, so decorators never leak from one registration into the next.\n */\n decorators?: ClassDecorator[];\n /**\n * `false` registers no controller, only the providers (exported), for a host that mounts the\n * controllers itself — see `MediaDashboardOptions.routes`. Default `true`.\n */\n routes?: boolean;\n}\n\n/**\n * Holds the console's JSON API: the read controller (always) and the actions controller (only when\n * `actions: true`). Who reaches them is decided by `authorize` (`ConsoleAuthorizeGuard`).\n */\n@Module({})\nexport class MediaConsoleApiModule {\n static register(options: MediaConsoleApiModuleOptions): DynamicModule {\n return {\n module: MediaConsoleApiModule,\n imports: options.imports ?? [],\n controllers:\n options.routes === false\n ? []\n : [\n consoleController(MediaConsoleReadController, options.decorators),\n ...(options.actions\n ? [consoleController(MediaConsoleActionsController, options.decorators)]\n : []),\n ],\n providers: [\n MediaConsoleService,\n consoleAuthorizeProvider(\n options.authorize,\n options.moduleName ?? 'MediaConsoleApiModule.register()',\n ),\n { provide: MEDIA_DASHBOARD_ACTIONS, useValue: options.actions },\n { provide: MEDIA_CONSOLE_OBJECT_URLS, useValue: options.objectUrls },\n options.insightsProvider ?? {\n provide: MEDIA_CONSOLE_OBJECT_INSIGHTS,\n useValue: [],\n },\n ],\n // For a host mounting the controllers itself (`routes: false`): its subclasses inject these, and\n // `ConsoleAuthorizeGuard` injects the resolved `authorize` check.\n exports: [MediaConsoleService, MEDIA_DASHBOARD_ACTIONS, CONSOLE_AUTHORIZE_DECIDE],\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} from '@nestjs/common';\nimport { OpenToAuthorization } from './controller-decorators.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.\n */\n// Open to `@nestjs/authorization`'s deny-by-default guard; the console's `authorize` decides access.\n@OpenToAuthorization()\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 * streamed onto the disk (no size cap, nothing buffered).\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 & { headers?: Record<string, string | string[] | undefined> },\n @Query('type') type?: string,\n ): Promise<void> {\n const declared = Number(request.headers?.['content-length']);\n return this.service.putObject(\n disk,\n key,\n request,\n type,\n Number.isSafeInteger(declared) && declared >= 0 ? declared : undefined,\n );\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 type { Readable } from 'node:stream';\nimport {\n type MediaRecord,\n type MediaStore,\n type MultipartDrivers,\n type UploadSession,\n type UploadSessionStore,\n assertMediaKey,\n canPresign,\n diskCapabilities,\n folderPrefix,\n hasPublicUrl,\n listFolderLevel,\n multipartFor,\n} from '@dudousxd/nestjs-media-core';\nimport {\n BadRequestException,\n HttpException,\n Inject,\n Injectable,\n Logger,\n NotFoundException,\n Optional,\n type StreamableFile,\n} from '@nestjs/common';\nimport {\n type ServeFileOptions,\n Storage,\n type StorageDisk,\n StorageError,\n StorageFileNotFoundError,\n serveFile,\n} from '@nestjs/storage';\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_MULTIPART,\n MEDIA_STORE,\n MEDIA_UPLOAD_SESSIONS,\n} from './tokens.js';\n\n/** Seconds a signed object URL stays valid. */\nconst URL_TTL_SECONDS = 300;\nconst DEFAULT_PAGE_LIMIT = 50;\n/** Page size when sweeping a folder's objects for a recursive delete/copy/move. */\nconst SWEEP_LIMIT = 1000;\n/**\n * The zero-byte file `createFolder` writes so an empty folder exists: `@nestjs/storage` refuses keys\n * ending in `/`, so the S3-style `prefix/` marker can't be written. Hidden from listings.\n */\nexport const FOLDER_KEEP_FILE = '.keep';\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/** A folder path from the client (`a/b`, `a/b/`, `/a/b`) as `a/b/`; 400 when it can't be a key. */\nfunction clientFolder(prefix: string, what: string): string {\n const trimmed = prefix.trim().replace(/^\\/+/, '');\n let normalized: string;\n try {\n normalized = folderPrefix(trimmed);\n } catch (error) {\n throw new BadRequestException(`${what}: ${(error as Error).message}`);\n }\n if (normalized === '') throw new BadRequestException(`${what} is required`);\n return normalized;\n}\n\n/** Whether `key` is one `@nestjs/storage` can address (a legacy `prefix/` marker isn't). */\nfunction isAddressable(key: string): boolean {\n try {\n assertMediaKey(key);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Core errors that carry a client `status` (400). */\nconst CORE_CLIENT_ERRORS = new Set(['InvalidMediaKeyError', 'InvalidListingCursorError']);\n\n/**\n * Storage errors as HTTP: `@nestjs/storage`'s errors carry a `status` (404 missing file, 400\n * invalid key, 409 key conflict, 416 range) that Nest's exception filter doesn't read on its own;\n * so do the core's `InvalidMediaKeyError` and `InvalidListingCursorError` (400).\n * An object store failure (`StorageServiceError`, no status) stays a 500.\n */\nfunction toHttp(error: unknown): unknown {\n if (error instanceof HttpException) return error;\n if (error instanceof StorageFileNotFoundError) return new NotFoundException(error.message);\n const status = (error as { status?: unknown } | null)?.status;\n if (\n (error instanceof StorageError ||\n CORE_CLIENT_ERRORS.has((error as Error | null)?.name ?? '')) &&\n typeof status === 'number'\n ) {\n return new HttpException((error as Error).message, status);\n }\n return error;\n}\n\nasync function http<T>(run: () => Promise<T>): Promise<T> {\n try {\n return await run();\n } catch (error) {\n throw toHttp(error);\n }\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, on the disks of `@nestjs/storage` (`Storage`).\n * Resolves the store, upload store and multipart side-cars with `@Optional()` so a host without\n * them still boots — every method degrades to an empty shape instead of throwing.\n */\n@Injectable()\nexport class MediaConsoleService {\n constructor(\n @Optional() @Inject(Storage) private readonly storage: Storage | 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).\n @Optional()\n @Inject(MEDIA_CONSOLE_OBJECT_INSIGHTS)\n private readonly insightProviders: ObjectInsightProvider[] | null = null,\n // Strategy and mount path travel as ONE value. `null` means `auto`.\n @Optional()\n @Inject(MEDIA_CONSOLE_OBJECT_URLS)\n private readonly objectUrls: ObjectUrlConfig | null = null,\n /** `MediaModuleOptions.multipart`: native folder listing and the multipart badge. */\n @Optional()\n @Inject(MEDIA_MULTIPART)\n private readonly multipart: MultipartDrivers | null = null,\n ) {}\n\n private readonly logger = new Logger(MediaConsoleService.name);\n\n /**\n * The URL to report for an object's bytes, under the host's {@link ObjectUrlStrategy}. `auto`\n * prefers the disk's public URL, then a signed one (S3 presigns; a local disk signs with\n * `signedUrls`), and falls back to this server's own route for a disk that can do neither.\n * `proxy` always routes through this server.\n */\n private async objectUrl(disk: StorageDisk, name: string, key: string): Promise<string> {\n const proxy = () => objectProxyUrl(this.objectUrls?.apiBasePath ?? '', 'raw', name, key);\n if (this.objectUrls?.strategy === 'proxy') return proxy();\n if (hasPublicUrl(disk)) return disk.url(key);\n if (await canPresign(disk)) return disk.signedUrl(key, { expiresIn: URL_TTL_SECONDS * 1000 });\n return proxy();\n }\n\n private diskOrThrow(disk: string): StorageDisk {\n if (!this.storage || !this.storage.names().includes(disk)) {\n throw new NotFoundException(`Unknown disk: ${disk}`);\n }\n return this.storage.disk(disk);\n }\n\n async listDisks(): Promise<DiskListResponse> {\n const storage = this.storage;\n if (!storage) return { disks: [] };\n return {\n disks: await Promise.all(\n storage.names().map(async (name) => ({\n name,\n default: name === storage.defaultDisk,\n capabilities: await diskCapabilities(\n storage.disk(name),\n multipartFor(this.multipart ?? undefined, name),\n ),\n })),\n ),\n };\n }\n\n /**\n * One folder level. `StorageDisk.list()` is flat; a disk whose multipart side-car lists with a\n * delimiter (S3) asks the store for folders, any other disk folds its flat listing.\n */\n async listObjects(\n disk: string,\n options: { prefix?: string; cursor?: string; limit?: number },\n ): Promise<ObjectListResponse> {\n const store = this.diskOrThrow(disk);\n const raw = (options.prefix ?? '').replace(/^\\/+/, '');\n let prefix: string;\n try {\n prefix = folderPrefix(raw);\n } catch (error) {\n throw new BadRequestException((error as Error).message);\n }\n const level = await http(() =>\n listFolderLevel(\n store,\n prefix,\n {\n limit: options.limit ?? DEFAULT_PAGE_LIMIT,\n ...(options.cursor ? { cursor: options.cursor } : {}),\n },\n multipartFor(this.multipart ?? undefined, disk),\n ),\n );\n return {\n folders: level.folders\n .map((folder) => ({ name: lastSegment(folder), prefix: folder }))\n .filter((folder) => folder.name !== ''),\n files: level.files\n .map((entry) => ({ entry, name: entry.key.slice(prefix.length) }))\n .filter(({ name }) => name !== '' && name !== FOLDER_KEEP_FILE)\n .map(({ entry, name }) => ({\n key: entry.key,\n name,\n sizeBytes: entry.size,\n lastModified: entry.lastModified.toISOString(),\n })),\n ...(level.cursor ? { cursor: level.cursor } : {}),\n };\n }\n\n async objectDetail(disk: string, key: string): Promise<ObjectDetailResponse> {\n const store = this.diskOrThrow(disk);\n return http(async () => {\n const stat = await store.stat(key);\n return {\n key,\n size: stat.size,\n contentType: stat.contentType,\n lastModified: stat.lastModified.toISOString(),\n url: await this.objectUrl(store, disk, key),\n };\n });\n }\n\n /**\n * What the HOST knows about this object, from every registered provider. Its own call rather\n * than a field on `objectDetail`, so host lookups stay off the preview path. A provider that\n * throws is logged and dropped; providers run concurrently.\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 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 * Streams an object as the response through `@nestjs/storage`'s `serveFile()`: a single `Range`\n * gets a 206 with `Content-Range` (how a SQLite/ZIP/Parquet preview reads a few KB out of a huge\n * object), `If-None-Match` a 304, a range past the end a 416, a missing file a 404. `inline`\n * applies only to types that can't run script in this origin; others download.\n */\n serveObject(\n disk: string,\n key: string,\n options: ServeFileOptions,\n ): Promise<StreamableFile | undefined> {\n return http(() => serveFile(this.diskOrThrow(disk), key, options));\n }\n\n /**\n * Writes an object from a byte stream (a browser upload), streamed onto the disk: S3Disk turns a\n * body of unknown length into a multipart upload itself, so nothing is buffered here. With the\n * request's `Content-Length`, a body that ends short fails and nothing is written.\n */\n async putObject(\n disk: string,\n key: string,\n body: Readable,\n contentType?: string,\n contentLength?: number,\n ): Promise<void> {\n const store = this.diskOrThrow(disk);\n await http(() =>\n store.put(key, body, {\n ...(contentType ? { contentType } : {}),\n ...(contentLength !== undefined ? { contentLength } : {}),\n }),\n );\n }\n\n /**\n * Creates a folder: a zero-byte `<prefix>/.keep` file (hidden from listings), since\n * `@nestjs/storage` keys can't end with `/`. Listings show the folder because a key lives in it.\n */\n async createFolder(disk: string, prefix: string): Promise<void> {\n const folder = clientFolder(prefix, 'Folder name');\n await http(() => this.diskOrThrow(disk).put(`${folder}${FOLDER_KEEP_FILE}`, ''));\n }\n\n async deleteObject(disk: string, key: string): Promise<void> {\n await http(() => this.diskOrThrow(disk).delete(key));\n }\n\n /**\n * Recursively deletes a folder: every file under `<prefix>/`, nested included (its `.keep`\n * too), a batch of keys per `delete()`. A legacy `prefix/` marker object written by another tool\n * can't be addressed through `@nestjs/storage`; it is skipped.\n */\n async deleteFolder(disk: string, prefix: string): Promise<void> {\n const store = this.diskOrThrow(disk);\n const folder = clientFolder(prefix, 'Folder');\n await http(async () => {\n const keys: string[] = [];\n for await (const entry of store.listAll({ prefix: folder, limit: SWEEP_LIMIT })) {\n if (isAddressable(entry.key)) keys.push(entry.key);\n }\n for (let start = 0; start < keys.length; start += SWEEP_LIMIT) {\n await store.delete(keys.slice(start, start + SWEEP_LIMIT));\n }\n });\n }\n\n async copyObject(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await http(() => this.transferKey('copy', fromDisk, from, toDisk, to));\n }\n\n async moveObject(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await http(() => this.transferKey('move', fromDisk, from, toDisk, to));\n }\n\n /**\n * Copies or moves one object. Same disk: the disk's own `copy`/`move` (server-side on S3, a\n * rename on a local disk). Across disks: the source's body streams into the destination's\n * `put()` (no size limit, nothing buffered), keeping its content type, cache control,\n * disposition and metadata; a `move` then deletes the source.\n */\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 source = this.diskOrThrow(fromDisk);\n if (fromDisk === toDisk) {\n if (op === 'move') await source.move(from, to);\n else await source.copy(from, to);\n return;\n }\n const target = this.diskOrThrow(toDisk);\n const download = await source.get(from);\n try {\n await target.put(to, download.body, {\n contentType: download.contentType,\n contentLength: download.size,\n ...(download.cacheControl ? { cacheControl: download.cacheControl } : {}),\n ...(download.contentDisposition ? { contentDisposition: download.contentDisposition } : {}),\n metadata: download.metadata,\n });\n } finally {\n download.body.destroy();\n }\n if (op === 'move') await source.delete(from);\n }\n\n /** Moves a whole folder (every file under `<from>/`, nested included) to `<to>/`. */\n async moveFolder(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await http(() => this.transferFolder('move', fromDisk, from, toDisk, to));\n }\n\n /** Copies a whole folder (every file under `<from>/`, nested included) to `<to>/`. */\n async copyFolder(fromDisk: string, from: string, toDisk: string, to: string): Promise<void> {\n await http(() => this.transferFolder('copy', fromDisk, from, toDisk, to));\n }\n\n /**\n * Shared engine for {@link moveFolder}/{@link copyFolder}: a flat sweep of the source, each key\n * relocated via {@link transferKey}, keeping its path relative to the folder (an empty folder's\n * `.keep` goes along). On the SAME disk, a destination inside the source is refused (it would\n * recurse forever).\n */\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 source = this.diskOrThrow(fromDisk);\n this.diskOrThrow(toDisk);\n const fromPrefix = clientFolder(from, 'Source folder');\n const toPrefix = clientFolder(to, 'Destination folder');\n if (fromDisk === toDisk && toPrefix.startsWith(fromPrefix)) {\n throw new BadRequestException(`Cannot ${op} a folder into itself`);\n }\n // The keys first, then the transfers: a move changes the listing it would be paging through.\n const keys: string[] = [];\n for await (const entry of source.listAll({ prefix: fromPrefix, limit: SWEEP_LIMIT })) {\n if (isAddressable(entry.key)) keys.push(entry.key);\n }\n for (const key of keys) {\n await this.transferKey(\n op,\n fromDisk,\n key,\n toDisk,\n `${toPrefix}${key.slice(fromPrefix.length)}`,\n );\n }\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 disk = this.storage?.names().includes(conversion.disk)\n ? this.storage.disk(conversion.disk)\n : null;\n const url = disk\n ? await this.objectUrl(disk, conversion.disk, conversion.path).catch(() => '')\n : '';\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.names().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","// 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. The disks themselves come from `@nestjs/storage`'s `Storage`. 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 multipart side-cars by disk name (`MediaModuleOptions.multipart`), provided by `MediaModule`. */\nexport const MEDIA_MULTIPART: symbol = Symbol.for('nestjs-media:multipart');\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 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 {\n Controller,\n Get,\n Inject,\n Param,\n Query,\n Req,\n Res,\n type StreamableFile,\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 { downloadFilename } from './content-disposition.js';\nimport { OpenToAuthorization } from './controller-decorators.js';\nimport { MediaConsoleService } 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/**\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 */\n// Open to `@nestjs/authorization`'s deny-by-default guard; the console's `authorize` decides access.\n@OpenToAuthorization()\n@Controller()\nexport class MediaConsoleReadController {\n constructor(@Inject(MediaConsoleService) private readonly service: MediaConsoleService) {}\n\n @Get('disks')\n disks(): Promise<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 from the same origin (`serveFile()`), so the SPA can render previews\n * past CORS: `inline` for types that can't run script here (images, PDF, audio, video, plain\n * text), `attachment` for the rest — the text previews read the body with `fetch`, which doesn't\n * care. A single `Range` gets a 206, which is how SQLite/ZIP/Parquet previews read a few KB out\n * of a multi-hundred-MB object.\n */\n @Get('disks/:disk/object/raw')\n objectRaw(\n @Param('disk') disk: string,\n @Query('key') key: string,\n @Req() req: unknown,\n @Res({ passthrough: true }) res: unknown,\n ): Promise<StreamableFile | undefined> {\n return this.service.serveObject(disk, key, { req, res, disposition: 'inline' });\n }\n\n /**\n * The same bytes as `/raw`, but as a SAVE: `Content-Disposition: attachment` with the object's\n * own name. Same-origin and unconditional, so it works where the browser can't reach the store.\n */\n @Get('disks/:disk/object/download')\n objectDownload(\n @Param('disk') disk: string,\n @Query('key') key: string,\n @Req() req: unknown,\n @Res({ passthrough: true }) res: unknown,\n ): Promise<StreamableFile | undefined> {\n return this.service.serveObject(disk, key, {\n req,\n res,\n disposition: 'attachment',\n filename: downloadFilename(key ?? ''),\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 blank key. */\nconst FALLBACK_FILENAME = 'download';\n\n/**\n * The name to offer for an object key — its last segment, or a fallback when it has none. The\n * `Content-Disposition` itself (RFC 6266, with the `filename*` form for non-ASCII names) is built\n * by `@nestjs/storage`'s `serveFile()`.\n */\nexport function downloadFilename(key: string): string {\n const segment = key.split('/').pop() ?? '';\n return segment.trim() === '' ? FALLBACK_FILENAME : segment;\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 NotFoundException,\n Param,\n StreamableFile,\n} from '@nestjs/common';\nimport { ConsolePage } from './console-authorize.js';\nimport { OpenToAuthorization } from './controller-decorators.js';\nimport { MEDIA_DASHBOARD_API_PATH, MEDIA_DASHBOARD_BASE_PATH } 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// Open to `@nestjs/authorization`'s deny-by-default guard; the console's `authorize` decides access.\n// Marked as the page controller: a denied browser navigation here gets a redirect or a page, not JSON.\n@OpenToAuthorization()\n@ConsolePage()\n@Controller()\nexport class MediaDashboardUiController {\n private readonly dir = spaDir();\n\n constructor(\n @Inject(MEDIA_DASHBOARD_BASE_PATH) private readonly basePath: string,\n @Inject(MEDIA_DASHBOARD_API_PATH) private readonly apiBasePath: string,\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.\n @Get()\n @Header('Content-Type', 'text/html; charset=utf-8')\n @Header('Cache-Control', 'no-store, must-revalidate')\n index(): string {\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 return html.includes('</head>') ? html.replace('</head>', `${inject}</head>`) : inject + html;\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","import_core","import_reflect_metadata","CONSOLE_GATE","Symbol","for","PACKAGE","MODULE","CONSOLE_AUTHORIZE_MISSING_WARNING","consoleAuthorizeMissingWarning","registration","allow","allowed","forbidden","reason","unauthenticated","isProduction","process","env","NODE_ENV","trim","toLowerCase","isAuthorizerClass","value","prototype","authorize","test","Function","toString","call","isPolicyCheck","policy","ability","isLazyModule","default","normalizeConsoleResult","result","shape","redirectTo","hasUser","user","undefined","importOptional","specifier","policyDecider","check","moduleRef","pkg","Error","service","get","AuthorizationService","strict","found","defaultGetUser","getUser","options","AUTHORIZATION_MODULE_OPTIONS","context","signedIn","can","gateDecider","name","gate","allows","classDecider","instances","Map","instanceOf","Class","cached","pending","registered","create","catch","delete","set","resolveConsoleAuthorize","logger","Logger","warnIfMissing","warn","production","decide","fromClass","lazy","fn","error","message","String","sha256","createHash","update","digest","authorizeByToken","token","length","expected","header","switchToHttp","getRequest","headers","authorization","match","exec","timingSafeEqual","CONSOLE_AUTHORIZE_DECIDE","CONSOLE_PAGE_METADATA","ConsolePage","target","Reflect","defineMetadata","ConsoleAccessDenied","HttpException","decision","page","isUnauthenticated","ConsoleAuthorizeGuard","canActivate","isPageNavigation","getType","getMetadata","getClass","request","accept","method","includes","title","DENIED_PAGE","SIGN_IN_PAGE","ConsoleAuthorizeFilter","adapterHost","exception","host","adapter","httpAdapter","response","getResponse","isPage","redirect","setHeader","reply","getStatus","UseConsoleAuthorize","applyDecorators","UseGuards","UseFilters","consoleAuthorizeProvider","provide","inject","ModuleRef","useFactory","CAN_ANYONE_METADATA","OpenToAuthorization","target","Reflect","defineMetadata","consoleController","controller","decorators","Decorated","Object","defineProperty","value","name","length","decorate","UseConsoleAuthorize","import_common","import_common","import_common","SAFE_LINK_SCHEMES","Set","sanitizeInsight","insight","links","undefined","filter","link","href","startsWith","has","URL","protocol","objectProxyUrl","apiBasePath","route","disk","key","path","encodeURIComponent","MEDIA_STORE","Symbol","for","MEDIA_UPLOAD_SESSIONS","MEDIA_MULTIPART","MEDIA_DASHBOARD_BASE_PATH","MEDIA_DASHBOARD_API_PATH","MEDIA_DASHBOARD_ACTIONS","MEDIA_CONSOLE_OBJECT_INSIGHTS","MEDIA_CONSOLE_OBJECT_URLS","URL_TTL_SECONDS","DEFAULT_PAGE_LIMIT","SWEEP_LIMIT","FOLDER_KEEP_FILE","lastSegment","prefix","trimmed","replace","slash","lastIndexOf","slice","clientFolder","what","trim","normalized","folderPrefix","error","BadRequestException","message","isAddressable","key","assertMediaKey","CORE_CLIENT_ERRORS","Set","toHttp","HttpException","StorageFileNotFoundError","NotFoundException","status","StorageError","has","name","http","run","mapUpload","session","percent","size","undefined","Math","min","round","offset","id","disk","parts","multipart","multipartUploadId","createdAt","toISOString","mapRecord","record","ownerType","ownerId","collection","fileName","mimeType","path","MediaConsoleService","storage","store","uploads","actionsEnabled","insightProviders","objectUrls","logger","Logger","objectUrl","proxy","objectProxyUrl","apiBasePath","strategy","hasPublicUrl","url","canPresign","signedUrl","expiresIn","diskOrThrow","names","includes","listDisks","disks","Promise","all","map","default","defaultDisk","capabilities","diskCapabilities","multipartFor","listObjects","options","raw","level","listFolderLevel","limit","cursor","folders","folder","filter","files","entry","length","sizeBytes","lastModified","objectDetail","stat","contentType","objectInsights","providers","insights","settled","provider","resolve","warn","Error","String","insight","sanitizeInsight","serveObject","serveFile","putObject","body","contentLength","put","createFolder","deleteObject","delete","deleteFolder","keys","listAll","push","start","copyObject","fromDisk","from","toDisk","to","transferKey","moveObject","op","source","move","copy","target","download","get","cacheControl","contentDisposition","metadata","destroy","moveFolder","transferFolder","copyFolder","fromPrefix","toPrefix","startsWith","listUploads","list","sessions","keyPrefix","uploadDetail","listParts","upload","abortUpload","listCollections","aggregate","collections","buckets","groupBy","sum","listLibrary","records","page","libraryDetail","find","variants","Object","entries","conversions","conversion","catch","deleteLibraryRecord","topology","hasStore","hasUploads","actions","MediaConsoleActionsController","service","deleteObject","disk","key","copyObject","body","from","toDisk","to","moveObject","moveFolder","copyFolder","uploadObject","request","type","declared","Number","headers","putObject","isSafeInteger","undefined","createFolder","prefix","deleteFolder","abortUpload","id","deleteLibraryRecord","import_common","FALLBACK_FILENAME","downloadFilename","key","segment","split","pop","trim","toLimit","value","undefined","parsed","Number","isFinite","Math","floor","MediaConsoleReadController","service","disks","listDisks","objects","disk","prefix","cursor","limit","limitValue","listObjects","object","key","objectDetail","objectInsights","objectRaw","req","res","serveObject","disposition","objectDownload","filename","downloadFilename","uploads","listUploads","upload","id","uploadDetail","collections","listCollections","library","collection","listLibrary","libraryRecord","libraryDetail","topology","passthrough","MediaConsoleApiModule","register","options","module","imports","controllers","routes","consoleController","MediaConsoleReadController","decorators","actions","MediaConsoleActionsController","providers","MediaConsoleService","consoleAuthorizeProvider","authorize","moduleName","provide","MEDIA_DASHBOARD_ACTIONS","useValue","MEDIA_CONSOLE_OBJECT_URLS","objectUrls","insightsProvider","MEDIA_CONSOLE_OBJECT_INSIGHTS","exports","CONSOLE_AUTHORIZE_DECIDE","import_common","BUILD_BASE","spaDir","fileURLToPath","URL","CONTENT_TYPES","MediaDashboardUiController","dir","basePath","apiBasePath","index","indexPath","join","existsSync","NotFoundException","html","readFileSync","replaceAll","inject","includes","replace","asset","file","safe","basename","root","resolve","assetPath","startsWith","sep","type","extname","StreamableFile","normalize","path","replace","MediaDashboardModule","forRoot","options","build","provide","MEDIA_CONSOLE_OBJECT_INSIGHTS","useValue","objectInsights","forRootAsync","inject","useFactory","deps","useObjectInsights","undefined","name","insightsProvider","basePath","apiBasePath","actions","mount","routes","imports","module","MediaConsoleApiModule","register","objectUrls","strategy","authorize","moduleName","decorators","RouterModule","controllers","consoleController","MediaDashboardUiController","providers","MEDIA_DASHBOARD_BASE_PATH","MEDIA_DASHBOARD_API_PATH","exports"]}
|