mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +00:00
Merge pull request #66 from yusufipk/fix/bunny-cdn-runtime-config
fix(bunny): read the CDN host from runtime config so Docker images can play video
This commit is contained in:
@@ -70,5 +70,7 @@ STRIPE_WEBHOOK_SECRET=""
|
|||||||
BUNNY_STREAM_API_KEY=""
|
BUNNY_STREAM_API_KEY=""
|
||||||
BUNNY_STREAM_LIBRARY_ID=""
|
BUNNY_STREAM_LIBRARY_ID=""
|
||||||
BUNNY_API_KEY=""
|
BUNNY_API_KEY=""
|
||||||
|
# Playback host for Bunny versions. Set BUNNY_CDN_URL: the app reads it at request
|
||||||
|
# time, while NEXT_PUBLIC_BUNNY_CDN_URL only reaches the browser in a source build.
|
||||||
BUNNY_CDN_URL=""
|
BUNNY_CDN_URL=""
|
||||||
NEXT_PUBLIC_BUNNY_CDN_URL=""
|
NEXT_PUBLIC_BUNNY_CDN_URL=""
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ OPENFRAME_REQUIRE_INVITE_CODE=false
|
|||||||
Behavior when disabled:
|
Behavior when disabled:
|
||||||
|
|
||||||
- `OPENFRAME_ENABLE_STRIPE=false` disables Stripe checkout and customer portal flows and removes billing-based workspace restrictions.
|
- `OPENFRAME_ENABLE_STRIPE=false` disables Stripe checkout and customer portal flows and removes billing-based workspace restrictions.
|
||||||
- `OPENFRAME_ENABLE_BUNNY_UPLOADS=false` hides Bunny direct-upload entry points. URL-based providers such as YouTube remain available.
|
- `OPENFRAME_ENABLE_BUNNY_UPLOADS=false` hides Bunny direct-upload entry points. URL-based providers such as YouTube remain available. When enabling it, set `BUNNY_CDN_URL` (not only `NEXT_PUBLIC_BUNNY_CDN_URL`): it is read at request time, so a published image picks up the playback host without a rebuild.
|
||||||
- `OPENFRAME_ENABLE_S3_VIDEO_UPLOADS=true` (with `R2_*` configured) enables presigned uploads to your own S3-compatible storage. Set `OPENFRAME_ENABLE_BUNNY_UPLOADS=false` — only one direct-upload backend can be active. The bucket must allow CORS `PUT` from your app origin (for example `http://localhost:3000` in dev and your production URL). For Docker + MinIO, keep `R2_ENDPOINT=http://minio:9000` (app-internal) and set `R2_PRESIGN_ENDPOINT` to the browser-reachable MinIO origin (for example `http://localhost:9000` locally, or `https://minio.example.com` when MinIO is behind a reverse proxy). Use the origin only — no path suffix. The app's Content-Security-Policy is generated from runtime env at request time, so published Docker images pick up custom `R2_PRESIGN_ENDPOINT` values without rebuilding or editing `next.config.ts`.
|
- `OPENFRAME_ENABLE_S3_VIDEO_UPLOADS=true` (with `R2_*` configured) enables presigned uploads to your own S3-compatible storage. Set `OPENFRAME_ENABLE_BUNNY_UPLOADS=false` — only one direct-upload backend can be active. The bucket must allow CORS `PUT` from your app origin (for example `http://localhost:3000` in dev and your production URL). For Docker + MinIO, keep `R2_ENDPOINT=http://minio:9000` (app-internal) and set `R2_PRESIGN_ENDPOINT` to the browser-reachable MinIO origin (for example `http://localhost:9000` locally, or `https://minio.example.com` when MinIO is behind a reverse proxy). Use the origin only — no path suffix. The app's Content-Security-Policy is generated from runtime env at request time, so published Docker images pick up custom `R2_PRESIGN_ENDPOINT` values without rebuilding or editing `next.config.ts`.
|
||||||
- `OPENFRAME_REQUIRE_INVITE_CODE=false` allows open registration while keeping invitation-link registration intact.
|
- `OPENFRAME_REQUIRE_INVITE_CODE=false` allows open registration while keeping invitation-link registration intact.
|
||||||
- `OPENFRAME_ENABLE_ANALYTICS=true` records first-touch attribution and funnel events into your own database, readable on `/admin/growth`, or as JSON on `/api/admin/growth` by a script sending `Authorization: Bearer $OPENFRAME_ADMIN_API_TOKEN` (at least 32 characters, unset by default, in which case an admin session is the only way in). Off by default, and nothing leaves the instance either way.
|
- `OPENFRAME_ENABLE_ANALYTICS=true` records first-touch attribution and funnel events into your own database, readable on `/admin/growth`, or as JSON on `/api/admin/growth` by a script sending `Authorization: Bearer $OPENFRAME_ADMIN_API_TOKEN` (at least 32 characters, unset by default, in which case an admin session is the only way in). Off by default, and nothing leaves the instance either way.
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import type { Metadata } from 'next';
|
|||||||
import { Geist_Mono, JetBrains_Mono } from 'next/font/google';
|
import { Geist_Mono, JetBrains_Mono } from 'next/font/google';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
import { ThemeProvider } from '@/components/theme-provider';
|
import { ThemeProvider } from '@/components/theme-provider';
|
||||||
|
import {
|
||||||
|
buildRuntimePublicConfig,
|
||||||
|
RUNTIME_PUBLIC_CONFIG_ELEMENT_ID,
|
||||||
|
} from '@/lib/runtime-public-config';
|
||||||
import { seoConfig } from '@/lib/seo';
|
import { seoConfig } from '@/lib/seo';
|
||||||
import './globals.css';
|
import './globals.css';
|
||||||
|
|
||||||
@@ -120,6 +124,16 @@ export default function RootLayout({
|
|||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<body className="antialiased min-h-screen bg-background font-sans">
|
<body className="antialiased min-h-screen bg-background font-sans">
|
||||||
|
{/* Not executed, only parsed by readRuntimePublicConfig(). It carries the
|
||||||
|
public settings the browser cannot get from NEXT_PUBLIC_* variables,
|
||||||
|
which are frozen into the bundle when the image is built. */}
|
||||||
|
<script
|
||||||
|
id={RUNTIME_PUBLIC_CONFIG_ELEMENT_ID}
|
||||||
|
type="application/json"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: JSON.stringify(buildRuntimePublicConfig()).replace(/</g, '\\u003c'),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/* One script per object: single-object payloads with a top-level
|
{/* One script per object: single-object payloads with a top-level
|
||||||
@context survive naive JSON-LD consumers that choke on arrays. */}
|
@context survive naive JSON-LD consumers that choke on arrays. */}
|
||||||
{structuredData.map((data) => (
|
{structuredData.map((data) => (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
VideoData,
|
VideoData,
|
||||||
} from '@/components/video-page/types';
|
} from '@/components/video-page/types';
|
||||||
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||||
|
import { resolvePublicDirectDownloadAllowedHosts } from '@/lib/runtime-public-config';
|
||||||
import {
|
import {
|
||||||
downloadNamedFile,
|
downloadNamedFile,
|
||||||
downloadProgressLabel,
|
downloadProgressLabel,
|
||||||
@@ -32,11 +33,9 @@ function sanitizeDownloadFileName(value: string): string {
|
|||||||
function getAllowedHosts() {
|
function getAllowedHosts() {
|
||||||
const bunnyCdnHostname = resolvePublicBunnyCdnHostname();
|
const bunnyCdnHostname = resolvePublicBunnyCdnHostname();
|
||||||
return [
|
return [
|
||||||
...(bunnyCdnHostname ? [bunnyCdnHostname] : []),
|
...(bunnyCdnHostname ? [bunnyCdnHostname.trim().toLowerCase()] : []),
|
||||||
...(process.env.NEXT_PUBLIC_DIRECT_DOWNLOAD_ALLOWED_HOSTS ?? '').split(','),
|
...resolvePublicDirectDownloadAllowedHosts(),
|
||||||
]
|
].filter(Boolean);
|
||||||
.map((host) => host.trim().toLowerCase())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSafeDirectDownloadUrl(rawUrl: string): string | null {
|
function getSafeDirectDownloadUrl(rawUrl: string): string | null {
|
||||||
|
|||||||
+14
-1
@@ -1,3 +1,5 @@
|
|||||||
|
import { readRuntimePublicConfig } from '@/lib/runtime-public-config';
|
||||||
|
|
||||||
function normalizeBunnyCdnHostname(raw: string | null | undefined): string | null {
|
function normalizeBunnyCdnHostname(raw: string | null | undefined): string | null {
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
@@ -17,6 +19,17 @@ export function resolveServerBunnyCdnHostname(): string | null {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browser-side hostname. Prefers the config the server injects at request time,
|
||||||
|
* because `NEXT_PUBLIC_BUNNY_CDN_URL` is inlined when the bundle is built and the
|
||||||
|
* published Docker image is built without it. Falls back to the build-time variable,
|
||||||
|
* which is what a source build sets, and to the server-only variable during the SSR
|
||||||
|
* pass of a client component, where it is still readable and has to produce the same
|
||||||
|
* hostname the browser will read or hydration diverges.
|
||||||
|
*/
|
||||||
export function resolvePublicBunnyCdnHostname(): string | null {
|
export function resolvePublicBunnyCdnHostname(): string | null {
|
||||||
return normalizeBunnyCdnHostname(process.env.NEXT_PUBLIC_BUNNY_CDN_URL);
|
return (
|
||||||
|
normalizeBunnyCdnHostname(readRuntimePublicConfig()?.bunnyCdnUrl) ??
|
||||||
|
resolveServerBunnyCdnHostname()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Public configuration the browser needs but cannot read from the environment.
|
||||||
|
*
|
||||||
|
* Next inlines `NEXT_PUBLIC_*` into the client bundle at build time. The published
|
||||||
|
* Docker image is built by CI with none of these set, so every browser-side reader
|
||||||
|
* gets an empty string no matter what the operator puts in `.env.docker`, and the
|
||||||
|
* Bunny player ends up with no CDN host to build a playlist URL from. The server
|
||||||
|
* knows the real values on every request, so it serialises them into a JSON script
|
||||||
|
* tag in the root layout and the browser reads them back from the DOM. Same reason
|
||||||
|
* the CSP is built per request in lib/content-security-policy.ts.
|
||||||
|
*
|
||||||
|
* Caveat: a prerendered route bakes the values it had at build time into its HTML,
|
||||||
|
* and a client-side navigation away from one keeps that copy of the root layout. The
|
||||||
|
* prerendered routes are the legal and marketing pages, none of which play video or
|
||||||
|
* download media, so nothing reads a stale copy today. A new prerendered route that
|
||||||
|
* needs either has to opt into per-request rendering.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const RUNTIME_PUBLIC_CONFIG_ELEMENT_ID = 'openframe-runtime-public-config';
|
||||||
|
|
||||||
|
export interface RuntimePublicConfig {
|
||||||
|
/** Raw value, not a hostname: lib/bunny-cdn.ts owns the normalisation. */
|
||||||
|
bunnyCdnUrl: string;
|
||||||
|
/** Comma-separated hostnames, as the environment variable spells them. */
|
||||||
|
directDownloadAllowedHosts: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Server-side: the values as configured for this deployment, read at request time. */
|
||||||
|
export function buildRuntimePublicConfig(): RuntimePublicConfig {
|
||||||
|
return {
|
||||||
|
bunnyCdnUrl: process.env.BUNNY_CDN_URL || process.env.NEXT_PUBLIC_BUNNY_CDN_URL || '',
|
||||||
|
directDownloadAllowedHosts: process.env.NEXT_PUBLIC_DIRECT_DOWNLOAD_ALLOWED_HOSTS || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browser-side: the injected values, or null when there is no document (the SSR pass
|
||||||
|
* of a client component) or no script tag (a page rendered before this existed).
|
||||||
|
* Callers fall back to the build-time environment in both cases.
|
||||||
|
*/
|
||||||
|
export function readRuntimePublicConfig(): RuntimePublicConfig | null {
|
||||||
|
if (typeof document === 'undefined') return null;
|
||||||
|
|
||||||
|
const element = document.getElementById(RUNTIME_PUBLIC_CONFIG_ELEMENT_ID);
|
||||||
|
if (!element?.textContent) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed: unknown = JSON.parse(element.textContent);
|
||||||
|
if (!parsed || typeof parsed !== 'object') return null;
|
||||||
|
const { bunnyCdnUrl, directDownloadAllowedHosts } = parsed as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
bunnyCdnUrl: typeof bunnyCdnUrl === 'string' ? bunnyCdnUrl : '',
|
||||||
|
directDownloadAllowedHosts:
|
||||||
|
typeof directDownloadAllowedHosts === 'string' ? directDownloadAllowedHosts : '',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePublicDirectDownloadAllowedHosts(): string[] {
|
||||||
|
// An injected value that is empty means the same as an absent one: nothing was
|
||||||
|
// configured here, so keep whatever the bundle was built with rather than
|
||||||
|
// narrowing a deployment that already worked.
|
||||||
|
const configured =
|
||||||
|
readRuntimePublicConfig()?.directDownloadAllowedHosts ||
|
||||||
|
process.env.NEXT_PUBLIC_DIRECT_DOWNLOAD_ALLOWED_HOSTS ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
return configured
|
||||||
|
.split(',')
|
||||||
|
.map((host) => host.trim().toLowerCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
@@ -125,6 +125,9 @@ function urlsFetched(): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
// The runtime resolver reads BUNNY_CDN_URL first, so pin it rather than
|
||||||
|
// inheriting the value a developer has in .env.
|
||||||
|
vi.stubEnv('BUNNY_CDN_URL', undefined);
|
||||||
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', `https://${BUNNY_HOST}`);
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', `https://${BUNNY_HOST}`);
|
||||||
vi.stubEnv('NEXT_PUBLIC_DIRECT_DOWNLOAD_ALLOWED_HOSTS', ALLOWED_DIRECT_HOST);
|
vi.stubEnv('NEXT_PUBLIC_DIRECT_DOWNLOAD_ALLOWED_HOSTS', ALLOWED_DIRECT_HOST);
|
||||||
clicked = [];
|
clicked = [];
|
||||||
|
|||||||
@@ -173,6 +173,9 @@ function makeFile(name = 'my clip.mp4') {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tusUploads.length = 0;
|
tusUploads.length = 0;
|
||||||
tusFailure = null;
|
tusFailure = null;
|
||||||
|
// The runtime resolver reads BUNNY_CDN_URL first, so pin it rather than
|
||||||
|
// inheriting the value a developer has in .env.
|
||||||
|
vi.stubEnv('BUNNY_CDN_URL', undefined);
|
||||||
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', 'https://cdn.example.test');
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', 'https://cdn.example.test');
|
||||||
fetchMock = vi.fn((url: string) => {
|
fetchMock = vi.fn((url: string) => {
|
||||||
if (url === BUNNY_INIT_URL) {
|
if (url === BUNNY_INIT_URL) {
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { resolvePublicBunnyCdnHostname } from '@/lib/bunny-cdn';
|
||||||
|
import {
|
||||||
|
buildRuntimePublicConfig,
|
||||||
|
readRuntimePublicConfig,
|
||||||
|
resolvePublicDirectDownloadAllowedHosts,
|
||||||
|
RUNTIME_PUBLIC_CONFIG_ELEMENT_ID,
|
||||||
|
} from '@/lib/runtime-public-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These run in jsdom because that is the only place the bug shows: on the server
|
||||||
|
* the environment is readable at request time, in the browser it is whatever was
|
||||||
|
* inlined when the bundle was built, which for the published Docker image is
|
||||||
|
* nothing.
|
||||||
|
*/
|
||||||
|
function injectConfig(payload: string): void {
|
||||||
|
const element = document.createElement('script');
|
||||||
|
element.id = RUNTIME_PUBLIC_CONFIG_ELEMENT_ID;
|
||||||
|
element.type = 'application/json';
|
||||||
|
element.textContent = payload;
|
||||||
|
document.body.appendChild(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.stubEnv('BUNNY_CDN_URL', undefined);
|
||||||
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', undefined);
|
||||||
|
vi.stubEnv('NEXT_PUBLIC_DIRECT_DOWNLOAD_ALLOWED_HOSTS', undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
document.getElementById(RUNTIME_PUBLIC_CONFIG_ELEMENT_ID)?.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildRuntimePublicConfig', () => {
|
||||||
|
it('prefers the server variable over the public one', () => {
|
||||||
|
vi.stubEnv('BUNNY_CDN_URL', 'https://server.b-cdn.net');
|
||||||
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', 'https://public.b-cdn.net');
|
||||||
|
|
||||||
|
expect(buildRuntimePublicConfig().bunnyCdnUrl).toBe('https://server.b-cdn.net');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('emits empty strings rather than undefined when nothing is configured', () => {
|
||||||
|
expect(buildRuntimePublicConfig()).toEqual({
|
||||||
|
bunnyCdnUrl: '',
|
||||||
|
directDownloadAllowedHosts: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('readRuntimePublicConfig', () => {
|
||||||
|
it('returns null when the page carries no config', () => {
|
||||||
|
expect(readRuntimePublicConfig()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for a payload that is not valid JSON', () => {
|
||||||
|
injectConfig('{ not json');
|
||||||
|
|
||||||
|
expect(readRuntimePublicConfig()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('coerces missing or non-string fields to empty strings', () => {
|
||||||
|
injectConfig(JSON.stringify({ bunnyCdnUrl: 42 }));
|
||||||
|
|
||||||
|
expect(readRuntimePublicConfig()).toEqual({
|
||||||
|
bunnyCdnUrl: '',
|
||||||
|
directDownloadAllowedHosts: '',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolvePublicBunnyCdnHostname in the browser', () => {
|
||||||
|
it('uses the injected hostname when the build-time variable is empty', () => {
|
||||||
|
// The published Docker image, where the operator configured BUNNY_CDN_URL at
|
||||||
|
// runtime and the bundle was built without it.
|
||||||
|
injectConfig(JSON.stringify({ bunnyCdnUrl: 'https://vz-runtime.b-cdn.net' }));
|
||||||
|
|
||||||
|
expect(resolvePublicBunnyCdnHostname()).toBe('vz-runtime.b-cdn.net');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers the injected hostname over the one inlined at build time', () => {
|
||||||
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', 'https://vz-build.b-cdn.net');
|
||||||
|
injectConfig(JSON.stringify({ bunnyCdnUrl: 'https://vz-runtime.b-cdn.net' }));
|
||||||
|
|
||||||
|
expect(resolvePublicBunnyCdnHostname()).toBe('vz-runtime.b-cdn.net');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the build-time variable when the injected value is empty', () => {
|
||||||
|
vi.stubEnv('NEXT_PUBLIC_BUNNY_CDN_URL', 'https://vz-build.b-cdn.net');
|
||||||
|
injectConfig(JSON.stringify({ bunnyCdnUrl: '' }));
|
||||||
|
|
||||||
|
expect(resolvePublicBunnyCdnHostname()).toBe('vz-build.b-cdn.net');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null when neither source is configured', () => {
|
||||||
|
injectConfig(JSON.stringify({ bunnyCdnUrl: '' }));
|
||||||
|
|
||||||
|
expect(resolvePublicBunnyCdnHostname()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolvePublicDirectDownloadAllowedHosts', () => {
|
||||||
|
it('splits, trims and lowercases the injected list', () => {
|
||||||
|
injectConfig(
|
||||||
|
JSON.stringify({ directDownloadAllowedHosts: ' Files.Example.com , cdn.example.com ,, ' })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(resolvePublicDirectDownloadAllowedHosts()).toEqual([
|
||||||
|
'files.example.com',
|
||||||
|
'cdn.example.com',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the build-time variable when no config is injected', () => {
|
||||||
|
vi.stubEnv('NEXT_PUBLIC_DIRECT_DOWNLOAD_ALLOWED_HOSTS', 'files.example.com');
|
||||||
|
|
||||||
|
expect(resolvePublicDirectDownloadAllowedHosts()).toEqual(['files.example.com']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the build-time list when the injected one is empty', () => {
|
||||||
|
// An empty injected value means nothing was configured on this deployment, so
|
||||||
|
// it must not narrow a source build that already had a list.
|
||||||
|
vi.stubEnv('NEXT_PUBLIC_DIRECT_DOWNLOAD_ALLOWED_HOSTS', 'files.example.com');
|
||||||
|
injectConfig(JSON.stringify({ directDownloadAllowedHosts: '' }));
|
||||||
|
|
||||||
|
expect(resolvePublicDirectDownloadAllowedHosts()).toEqual(['files.example.com']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty list when neither source is configured', () => {
|
||||||
|
injectConfig(JSON.stringify({ directDownloadAllowedHosts: '' }));
|
||||||
|
|
||||||
|
expect(resolvePublicDirectDownloadAllowedHosts()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -103,11 +103,13 @@ describe('resolvePublicBunnyCdnHostname', () => {
|
|||||||
expect(resolvePublicBunnyCdnHostname()).toBeNull();
|
expect(resolvePublicBunnyCdnHostname()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reads only the public variable, ignoring the server-only one', () => {
|
it('falls back to the server variable when no config has been injected', () => {
|
||||||
// This runs in the browser bundle, where BUNNY_CDN_URL is never inlined.
|
// No document here, which is the SSR pass of a client component: the
|
||||||
|
// server-only variable is readable and has to yield the same hostname the
|
||||||
|
// browser will read out of the injected config, or hydration diverges.
|
||||||
vi.stubEnv('BUNNY_CDN_URL', 'https://server.b-cdn.net');
|
vi.stubEnv('BUNNY_CDN_URL', 'https://server.b-cdn.net');
|
||||||
|
|
||||||
expect(resolvePublicBunnyCdnHostname()).toBeNull();
|
expect(resolvePublicBunnyCdnHostname()).toBe('server.b-cdn.net');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reduces the configured public url to its hostname', () => {
|
it('reduces the configured public url to its hostname', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user