feat: implement US-only availability check for production instance in middleware and privacy page
Some checks failed
Create and publish Docker images / prepare (push) Has been cancelled
Version Docs on Tag / version-docs (push) Has been cancelled
Create and publish Docker images / build (amd64, linux/amd64, ubuntu-24.04) (push) Has been cancelled
Create and publish Docker images / build (arm64, linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Create and publish Docker images / merge (push) Has been cancelled

This commit is contained in:
Richard R 2026-02-19 12:28:30 -07:00
parent ffcd120f68
commit bc4c888866
2 changed files with 83 additions and 3 deletions

View file

@ -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.
</div>
{isRichardrDevProductionInstance ? (
<div className="privacy-highlight">
<strong>US-only availability:</strong> 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.
</div>
) : null}
<div className="privacy-highlight">
<strong>OpenReader does not sell your personal information.</strong> 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() {
</p>
<div className="privacy-card landing-panel">
<div className="privacy-card-label">Service Providers (Sub-processors)</div>
{process.env.RICHARDRDEV_PRODUCTION === 'true' ? (
{isRichardrDevProductionInstance ? (
<ul className="grid gap-2 sm:grid-cols-2 mt-2">
<li className="text-sm"><strong>Vercel:</strong> Hosting, Edge Functions &amp; Analytics</li>
<li className="text-sm"><strong>Neon (PostgreSQL):</strong> Database Storage</li>
@ -217,7 +227,7 @@ export default async function PrivacyPage() {
<li className="text-sm"><strong>Hosting Provider:</strong> Application Hosting &amp; Logs</li>
<li className="text-sm"><strong>Database Service:</strong> Relational Database Storage</li>
<li className="text-sm"><strong>Object Storage:</strong> Encrypted File Storage (Documents)</li>
<li className="text-sm"><strong>TTS Provider:</strong> Text-to-Speech Processing (Optional)</li>
<li className="text-sm"><strong>TTS Provider:</strong> Text-to-Speech Processing (Based on your configured provider)</li>
</ul>
)}
</div>
@ -266,7 +276,27 @@ export default async function PrivacyPage() {
<section>
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent"></span>
6. Contact Us
6. US Processing &amp; Location
</h2>
{isRichardrDevProductionInstance ? (
<p className="mb-4 text-sm leading-relaxed text-foreground/90">
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.
</p>
) : (
<p className="mb-4 text-sm leading-relaxed text-foreground/90">
Data processing location depends on your deployment and hosting
provider configuration.
</p>
)}
</section>
<section>
<h2 className="text-xl font-bold mb-4 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-accent"></span>
7. Contact Us
</h2>
<p className="mb-4 text-sm leading-relaxed text-foreground/90">
If you have questions or concerns about this Privacy Policy, please contact the instance administrator via the repository:

View file

@ -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();