From bc4c88886692af93536082fdd9d54a430dad8dc6 Mon Sep 17 00:00:00 2001 From: Richard R Date: Thu, 19 Feb 2026 12:28:30 -0700 Subject: [PATCH] feat: implement US-only availability check for production instance in middleware and privacy page --- src/app/(public)/privacy/page.tsx | 36 ++++++++++++++++++++-- src/middleware.ts | 50 +++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/app/(public)/privacy/page.tsx b/src/app/(public)/privacy/page.tsx index b05e9fc..addefc5 100644 --- a/src/app/(public)/privacy/page.tsx +++ b/src/app/(public)/privacy/page.tsx @@ -17,6 +17,8 @@ export const metadata: Metadata = { export default async function PrivacyPage() { const effectiveDate = 'February 17, 2026'; + const isRichardrDevProductionInstance = + process.env.RICHARDRDEV_PRODUCTION === 'true'; const hdrs = await headers(); const host = hdrs.get('host') ?? 'this server'; @@ -146,6 +148,14 @@ export default async function PrivacyPage() { The operator of this service is responsible for handling your information. + {isRichardrDevProductionInstance ? ( +
+ US-only availability: This instance is intended for + users located in the United States. Requests from outside the US, + or requests without reliable country metadata, may be blocked. +
+ ) : null} +
OpenReader does not sell your personal information. We do not sell data to data brokers or third parties. We use data solely to provide and improve the reading experience. @@ -205,7 +215,7 @@ export default async function PrivacyPage() {

Service Providers (Sub-processors)
- {process.env.RICHARDRDEV_PRODUCTION === 'true' ? ( + {isRichardrDevProductionInstance ? (
  • Vercel: Hosting, Edge Functions & Analytics
  • Neon (PostgreSQL): Database Storage
  • @@ -217,7 +227,7 @@ export default async function PrivacyPage() {
  • Hosting Provider: Application Hosting & Logs
  • Database Service: Relational Database Storage
  • Object Storage: Encrypted File Storage (Documents)
  • -
  • TTS Provider: Text-to-Speech Processing (Optional)
  • +
  • TTS Provider: Text-to-Speech Processing (Based on your configured provider)
)}
@@ -266,7 +276,27 @@ export default async function PrivacyPage() {

- 6. Contact Us + 6. US Processing & Location +

+ {isRichardrDevProductionInstance ? ( +

+ For the official OpenReader instance, personal + information is processed and stored in the United States. + By using this service, you acknowledge that your data is handled + in the US and that the service is available only for US users. +

+ ) : ( +

+ Data processing location depends on your deployment and hosting + provider configuration. +

+ )} +
+ +
+

+ + 7. Contact Us

If you have questions or concerns about this Privacy Policy, please contact the instance administrator via the repository: diff --git a/src/middleware.ts b/src/middleware.ts index 55d12c9..28dc91d 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -8,6 +8,13 @@ import type { NextRequest } from 'next/server'; const SESSION_COOKIE = 'better-auth.session_token'; const SECURE_SESSION_COOKIE = '__Secure-better-auth.session_token'; const SESSION_COOKIE_NAMES = [SESSION_COOKIE, SECURE_SESSION_COOKIE]; +const US_COUNTRY_CODE = 'US'; +const COUNTRY_HEADER_NAMES = [ + 'x-vercel-ip-country', + 'cf-ipcountry', + 'x-country-code', + 'x-geo-country', +]; /** * Routes that never require a session cookie. @@ -37,7 +44,50 @@ function isAnonymousAuthEnabled(): boolean { return raw?.trim().toLowerCase() === 'true'; } +function isRichardrDevProductionInstance(): boolean { + return process.env.RICHARDRDEV_PRODUCTION?.trim().toLowerCase() === 'true'; +} + +function getCountryCodeFromHeaders(request: NextRequest): string | null { + for (const headerName of COUNTRY_HEADER_NAMES) { + const rawValue = request.headers.get(headerName); + if (!rawValue) continue; + + const normalized = rawValue.trim().toUpperCase(); + if (normalized) { + return normalized; + } + } + + return null; +} + export function middleware(request: NextRequest) { + if (isRichardrDevProductionInstance()) { + const countryCode = getCountryCodeFromHeaders(request); + const isUnitedStatesRequest = countryCode === US_COUNTRY_CODE; + + // Strict region gate for the official production instance. + if (!isUnitedStatesRequest) { + const { pathname } = request.nextUrl; + + if (pathname.startsWith('/api/')) { + return NextResponse.json( + { error: 'OpenReader is only available in the United States.' }, + { status: 451, headers: { 'Cache-Control': 'no-store' } }, + ); + } + + return new NextResponse('OpenReader is only available in the United States.', { + status: 451, + headers: { + 'Cache-Control': 'no-store', + 'Content-Type': 'text/plain; charset=utf-8', + }, + }); + } + } + // When auth is disabled entirely, let everything through. if (!isAuthEnabled()) { return NextResponse.next();