Skip to content

Commit 5496465

Browse files
r8a5claude
andcommitted
Hide installer + marketing surface on user-facing installs
A pages-seo install is a product, not a clone of the maintainer's demo. On a user's domain they shouldn't see /install or /update (those only make sense on the upstream) and they shouldn't get the maintainer's marketing index page either — it talks about "pages-seo" and shows screenshots that aren't their content. New behaviour, gated by `is_maintainer`: Maintainer-only routes (404 on user installs): /install (browser installer page) /install/run.{sh,py,js} (terminal installer scripts) /install/* (anything else under install) /update (browser updater page) /update/* (anything else under update) /api/install/* (provisioner + check endpoints) /api/update/* (diff + OAuth + sync + rebuild) Root (/) on user installs: Serves /sign-in.html — a minimal branded card with one button that links to /admin. No marketing copy, no maintainer branding. Maintainer's deployment (seo.benjaminb.xyz): Everything stays as it was. /install, /update, /, /admin all fully reachable. Implementation: - functions/_middleware.js — runs on every request. Detects is_maintainer via env.IS_MAINTAINER==='1' or settings.is_maintainer==='1'. If false, intercepts installer surface (404) and root (serves /sign-in.html via ASSETS). - functions/_lib/maintainer.js — single helper isMaintainer(env) with the precedence rules. Cheap; loadSettings already caches per request. - functions/_lib/settings.js — adds the is_maintainer key (empty by default). - public/sign-in.html — minimal landing, dark theme matching /admin, single Sign-in button. Upstream flag set: wrangler d1 execute … "INSERT INTO settings (key, value, ...) VALUES ('is_maintainer', '1', ...) ON CONFLICT DO UPDATE SET value='1'..." Verified after deploy: seo.benjaminb.xyz/ still serves the marketing page, /install + /update return 200, /admin works, blog works. A user install of the same code (no env or settings flag) will 404 on /install + /update and serve sign-in.html at /. New installs start with the flag unset, so the protection is on by default — neither the installer nor /api/setup touch it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9941271 commit 5496465

4 files changed

Lines changed: 187 additions & 0 deletions

File tree

functions/_lib/maintainer.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Is this deployment the upstream maintainer's, or a user's install?
2+
//
3+
// On the upstream demo (seo.benjaminb.xyz) we want /install, /update,
4+
// /install/run.*, and the marketing index page to all work — it's
5+
// what makes the project discoverable. On a user's install of the
6+
// same code, none of that makes sense; their site is a content
7+
// product, not a clone of the demo.
8+
//
9+
// The flag lives in two places, in priority order:
10+
// 1. env.IS_MAINTAINER === '1' (Pages secret; survives a fresh
11+
// clone if the maintainer sets it manually)
12+
// 2. settings.is_maintainer === '1' (D1 row; written on the
13+
// upstream by the maintainer at deploy time)
14+
//
15+
// Both default to falsy. A user's install never sees either set.
16+
17+
import { loadSettings } from './settings.js';
18+
19+
export async function isMaintainer(env) {
20+
if (env?.IS_MAINTAINER === '1' || env?.IS_MAINTAINER === 'true') return true;
21+
if (!env?.DB) return false;
22+
try {
23+
const s = await loadSettings(env);
24+
return s?.is_maintainer === '1' || s?.is_maintainer === 'true';
25+
} catch {
26+
return false;
27+
}
28+
}

functions/_lib/settings.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@ const FALLBACK = {
4949
// Update-check metadata. Written when the operator installs from
5050
// /install (browser path) so the Updates admin tab can compare the
5151
// installed commit to upstream HEAD and offer a one-click rebuild.
52+
// Set to '1' on the upstream maintainer's deployment so the
53+
// installer / updater pages (/install, /update, /install/run.*) stay
54+
// accessible there. Every fresh user install starts with this
55+
// empty, so those routes 404 on user-facing domains. Read via
56+
// is_maintainer(env) in _lib/maintainer.js.
57+
is_maintainer: () => '',
5258
install_method: () => '', // 'browser' | 'cli' | ''
5359
installed_sha: () => '', // upstream main commit SHA at install time
5460
install_repo_owner: () => '', // user's GitHub fork owner

functions/_middleware.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Top-level middleware. Two jobs on every request:
2+
//
3+
// 1. Lock down installer/updater routes on user installs.
4+
// /install, /install/*, /update, /update/*, and the
5+
// installer-API namespace (/api/install/*, /api/update/*) only
6+
// make sense on the upstream maintainer's deployment. On a
7+
// user's install of the same code they're noise that exposes
8+
// the maintainer-only surface area to their visitors. We 404
9+
// those routes when isMaintainer(env) is false.
10+
//
11+
// 2. Rewrite the root `/` on user installs to a minimal sign-in
12+
// landing instead of the maintainer's marketing page. The
13+
// marketing page in public/index.html only makes sense on the
14+
// upstream — on a user's domain it leaks the maintainer's
15+
// branding + screenshots and confuses their visitors.
16+
//
17+
// Maintainer detection is via env.IS_MAINTAINER==='1' or
18+
// settings.is_maintainer==='1'. See _lib/maintainer.js. Cached on
19+
// env after the first read.
20+
21+
import { isMaintainer } from './_lib/maintainer.js';
22+
23+
const INSTALLER_RX = /^\/(install|update)(\/.*)?$/;
24+
const INSTALLER_API_RX = /^\/api\/(install|update)(\/.*)?$/;
25+
26+
export const onRequest = async ({ request, env, next }) => {
27+
const url = new URL(request.url);
28+
const path = url.pathname;
29+
30+
const isInstallerSurface = INSTALLER_RX.test(path) || INSTALLER_API_RX.test(path);
31+
const isRoot = path === '/' || path === '/index.html';
32+
33+
if (!isInstallerSurface && !isRoot) {
34+
return next();
35+
}
36+
37+
const maintainer = await isMaintainer(env);
38+
if (maintainer) {
39+
return next();
40+
}
41+
42+
// User install: gate.
43+
if (isInstallerSurface) {
44+
return new Response('Not Found', {
45+
status: 404,
46+
headers: { 'content-type': 'text/plain; charset=utf-8' },
47+
});
48+
}
49+
50+
// Root → minimal sign-in landing. We serve the dedicated file
51+
// public/sign-in.html via the ASSETS binding rather than
52+
// redirecting, so the URL stays clean.
53+
const signin = await env.ASSETS.fetch(new URL('/sign-in.html', url));
54+
if (signin.ok) {
55+
const headers = new Headers(signin.headers);
56+
headers.set('Cache-Control', 'public, max-age=300');
57+
return new Response(signin.body, { status: signin.status, headers });
58+
}
59+
// Fallback if the file is missing for any reason — redirect to /admin.
60+
return Response.redirect(new URL('/admin', url).toString(), 302);
61+
};

public/sign-in.html

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width,initial-scale=1" />
6+
<meta name="robots" content="noindex,nofollow" />
7+
<title>Sign in</title>
8+
<link rel="preconnect" href="https://fonts.googleapis.com" />
9+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Instrument+Serif:ital@0;1&display=swap" />
11+
<style>
12+
:root {
13+
--bg: #06080d;
14+
--bg-card: #0f1520;
15+
--ink: #e8edf2;
16+
--ink-dim: #a3aec0;
17+
--ink-faint: #6a7484;
18+
--accent: #7afff0;
19+
--accent-soft: rgba(122,255,240,0.10);
20+
--line-2: rgba(255,255,255,0.18);
21+
}
22+
*, *::before, *::after { box-sizing: border-box; }
23+
html, body {
24+
margin: 0; padding: 0;
25+
background:
26+
radial-gradient(1000px 500px at 50% -10%, var(--accent-soft), transparent 60%)
27+
var(--bg);
28+
color: var(--ink); font-family: 'Inter', system-ui, sans-serif;
29+
min-height: 100vh; display: flex; align-items: center; justify-content: center;
30+
padding: 32px 20px;
31+
}
32+
.card {
33+
width: min(420px, 100%);
34+
background: var(--bg-card);
35+
border: 1px solid var(--line-2);
36+
border-radius: 16px;
37+
padding: 36px 32px 28px;
38+
text-align: center;
39+
}
40+
.dot {
41+
display: inline-block;
42+
width: 10px; height: 10px; border-radius: 50%;
43+
background: var(--accent);
44+
box-shadow: 0 0 0 5px rgba(122,255,240,0.15);
45+
margin-bottom: 18px;
46+
}
47+
h1 {
48+
margin: 0 0 8px;
49+
font-family: 'Instrument Serif', Georgia, serif;
50+
font-weight: 400;
51+
font-size: 28px;
52+
letter-spacing: -0.01em;
53+
}
54+
h1 em { font-style: italic; color: var(--accent); }
55+
p {
56+
margin: 0 0 24px;
57+
color: var(--ink-dim);
58+
font-size: 14px;
59+
line-height: 1.55;
60+
}
61+
.btn {
62+
display: inline-block;
63+
padding: 12px 24px;
64+
border-radius: 10px;
65+
background: var(--accent);
66+
color: #06080d;
67+
font-weight: 600;
68+
font-size: 14px;
69+
text-decoration: none;
70+
transition: transform .12s;
71+
}
72+
.btn:hover { transform: translateY(-1px); }
73+
footer {
74+
margin-top: 22px;
75+
font-size: 11px;
76+
color: var(--ink-faint);
77+
letter-spacing: .04em;
78+
text-transform: uppercase;
79+
font-family: ui-monospace, monospace;
80+
}
81+
</style>
82+
</head>
83+
<body>
84+
<div class="card">
85+
<span class="dot" aria-hidden="true"></span>
86+
<h1><em>Welcome.</em></h1>
87+
<p>This site is managed by its admin. If that's you, sign in to make changes.</p>
88+
<a class="btn" href="/admin">Sign in</a>
89+
<footer>Powered by pages-seo</footer>
90+
</div>
91+
</body>
92+
</html>

0 commit comments

Comments
 (0)