From 0ff8b42b4af47e20298228f2386f7fa05ba7a535 Mon Sep 17 00:00:00 2001 From: yusufipk Date: Thu, 20 Aug 2026 15:40:21 +0300 Subject: [PATCH] fix(bunny): read the CDN host from runtime config so Docker images can play video NEXT_PUBLIC_BUNNY_CDN_URL is inlined into the client bundle at build time, and the published image is built by CI without it, so the browser had no host to build a playlist URL from no matter what the operator set in .env.docker. The player read the empty URL as a stream that had not finished encoding and sat on 'Video Is Processing', retrying forever. The server knows the value on every request, so the root layout now serialises the public settings into a JSON script tag and the browser reads them from there, falling back to the build-time variable for source builds. The direct-download allow list came through the same broken path and moves with it. Closes #60 --- .env.docker.example | 2 + README.md | 2 +- app/layout.tsx | 14 ++ .../video-page/hooks/use-download-actions.ts | 9 +- lib/bunny-cdn.ts | 15 +- lib/runtime-public-config.ts | 74 ++++++++++ .../hooks/use-download-actions.test.ts | 3 + .../hooks/use-version-actions.test.ts | 3 + tests/component/runtime-public-config.test.ts | 134 ++++++++++++++++++ tests/unit/lib/bunny-cdn.test.ts | 8 +- 10 files changed, 254 insertions(+), 10 deletions(-) create mode 100644 lib/runtime-public-config.ts create mode 100644 tests/component/runtime-public-config.test.ts diff --git a/.env.docker.example b/.env.docker.example index cab4e31..349e68b 100644 --- a/.env.docker.example +++ b/.env.docker.example @@ -70,5 +70,7 @@ STRIPE_WEBHOOK_SECRET="" BUNNY_STREAM_API_KEY="" BUNNY_STREAM_LIBRARY_ID="" 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="" NEXT_PUBLIC_BUNNY_CDN_URL="" diff --git a/README.md b/README.md index eb6c6b7..f6a4bd6 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ OPENFRAME_REQUIRE_INVITE_CODE=false Behavior when disabled: - `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_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. diff --git a/app/layout.tsx b/app/layout.tsx index c16359f..4f311c6 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,10 @@ import type { Metadata } from 'next'; import { Geist_Mono, JetBrains_Mono } from 'next/font/google'; import { Toaster } from 'sonner'; import { ThemeProvider } from '@/components/theme-provider'; +import { + buildRuntimePublicConfig, + RUNTIME_PUBLIC_CONFIG_ELEMENT_ID, +} from '@/lib/runtime-public-config'; import { seoConfig } from '@/lib/seo'; import './globals.css'; @@ -120,6 +124,16 @@ export default function RootLayout({ suppressHydrationWarning > + {/* 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. */} +