A plan view in the corner, a night you can actually see, and three kinds of visitor
The right half of the screen was empty sky. It holds the board now, drawn flat,
with the footprint of the camera's own frustum on it — the one part of a minimap
that earns its place, because it answers "where am I looking from" without
leaving the shot. Click it, drag it, scroll it. It is a 2D canvas rather than a
second WebGL context, cached per city and redrawn only when something moved.
Night was black. Not dark — black: at 3 a.m. the coastline, the hills and the
bay were one shape, and the frame read as a failed render rather than as
darkness. The sky already had a floor for exactly this reason and nothing did
the equivalent for the ground, so the ground has one now. The moon still has to
be worth computing, so the gap between a moonlit night and a moonless one is
preserved rather than filled in.
Three tiers, resolved once in the new src/access.ts: anonymous, signed in,
admin. Anonymous gets the map and a public office — the shell, the furniture,
the named viewpoints, nobody home — built without the private objects rather
than with them hidden, because scene.traverse makes hiding a leak with a bow on
it. The time scrubber and the debug readouts are admin only, and admin is
granted by TERA_ADMIN_SUBJECTS on the server and inferred nowhere else. An
unreachable API means member, never god: the promise is "clone it and it works",
not "clone it and you are an administrator of a deployment you did not
configure".
Three things this run found and fixed rather than shipped:
- entryUrl came off the wire and went straight into an href with no scheme
check, and a CSP of script-src 'self' 'unsafe-inline' does not stop a
javascript: URL from navigating. One rejection point in access.ts now.
- A 5xx from /health was the same null as "no API at all" and therefore the
opposite conclusion. Eight seconds of tera-api restarting would have told
every anonymous visitor they were a member. A 5xx is an answer; it fails
closed.
- decodeURIComponent in cookieToken was the one path in auth/index.ts that
threw rather than returning ANONYMOUS, so one malformed cookie header from
an unauthenticated caller turned /api/v1/session into a 500.
Also: keyboard shortcuts, focus rings, a boot state instead of a blank 2.3
seconds, a collapsible panel under 900px, and no horizontal overflow at 375,
768, 1440 or 2560.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,6 +76,18 @@ export interface PasswordLogin {
|
||||
rateWindowSeconds: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Who holds the god tier, resolved from `TERA_ADMIN_SUBJECTS`. See `loadAdmins`
|
||||
* for the rules; the shape exists so that "everyone" is a state the type system
|
||||
* knows about rather than a magic string left sitting in `subjects`.
|
||||
*/
|
||||
export interface AdminGrant {
|
||||
/** `TERA_ADMIN_SUBJECTS=*`. Development only; see `loadAdmins`. */
|
||||
everyone: boolean;
|
||||
/** Exact subject ids, trimmed. Empty with `everyone: false` means no admins. */
|
||||
subjects: string[];
|
||||
}
|
||||
|
||||
export interface AuthConfig {
|
||||
mode: AuthMode;
|
||||
/** Where a browser sends someone to sign in. `sso` mode only. */
|
||||
@@ -93,6 +105,8 @@ export interface AuthConfig {
|
||||
audience: string;
|
||||
/** Set only by `TERA_AUTH_MODE=password`; see `PasswordLogin` and `loadPasswordLogin`. */
|
||||
passwordLogin: PasswordLogin | null;
|
||||
/** Subjects the server will call admins. Unset means nobody; see `loadAdmins`. */
|
||||
admins: AdminGrant;
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
@@ -352,9 +366,79 @@ function loadAuth(env: Env, degraded: string[]): AuthConfig {
|
||||
issuer: str(env, "TERA_AUTH_JWT_ISSUER", ""),
|
||||
audience: str(env, "TERA_AUTH_JWT_AUDIENCE", ""),
|
||||
passwordLogin,
|
||||
// Last, because it wants the mode *after* every demotion above has run: a
|
||||
// list of admins on a box that just demoted to mode=none is worth a
|
||||
// sentence, and the sentence is only true once `mode` has settled.
|
||||
admins: loadAdmins(env, mode, degraded),
|
||||
};
|
||||
}
|
||||
|
||||
/** `TERA_ADMIN_SUBJECTS=*`; see `loadAdmins` for why this is a dev switch. */
|
||||
const ADMIN_WILDCARD = "*";
|
||||
|
||||
/**
|
||||
* Who gets the god tier — the time controls, the debug panel, and whatever else
|
||||
* ends up behind `Viewer.admin`.
|
||||
*
|
||||
* `TERA_ADMIN_SUBJECTS` is a comma-separated list of **subject ids**: the `sub`
|
||||
* claim this box verifies, not an email and not a display name. Under
|
||||
* `TERA_AUTH_MODE=password` the subject is `TERA_AUTH_PASSWORD_USER`, so the
|
||||
* single self-hosted account becomes an admin by naming it here — and only by
|
||||
* naming it here. Password mode is deliberately not auto-admin: one grant path,
|
||||
* written down in the environment, is the property worth having.
|
||||
*
|
||||
* **Unset means no admins, and that is the safe default.** A box handed nothing
|
||||
* serves the public map to everyone and the god tier to nobody. Matching is
|
||||
* exact after trimming, and case-sensitive: subject ids are opaque strings an
|
||||
* issuer minted, `user_01H…` and `USER_01H…` can be two different accounts, and
|
||||
* folding case here would silently widen a grant to an id nobody configured.
|
||||
*
|
||||
* The one wildcard is `*`, which makes **every authenticated subject an admin**.
|
||||
* It is here so a self-hoster poking at this on a laptop does not have to go
|
||||
* find their own subject id first. It is a development switch, it must never
|
||||
* reach a deployment env file, and it pushes a line into `degraded` so that
|
||||
* `/api/v1/health` says out loud that the box is handing out godmode.
|
||||
*
|
||||
* That loudness is the whole point, and it is paid for. lumbridge-v4 shipped
|
||||
* `ADMIN_EMAILS` with `admin@lumbridgecorp.com` as a committed default while
|
||||
* nobody had ever registered that address — a standing offer of admin to
|
||||
* whoever signed up for it first, invisible because nothing anywhere announced
|
||||
* it. A grant nobody can see is a grant nobody revokes. Hence: no committed
|
||||
* defaults, no implicit grants, and the one blanket switch reports itself.
|
||||
*
|
||||
* Note what is *not* here: no count and no list ever reaches the wire.
|
||||
* `routes/health.ts` serves `degraded`, and these sentences name the variable,
|
||||
* never its contents.
|
||||
*/
|
||||
function loadAdmins(env: Env, mode: AuthMode, degraded: string[]): AdminGrant {
|
||||
const configured = list(env, "TERA_ADMIN_SUBJECTS");
|
||||
const everyone = configured.includes(ADMIN_WILDCARD);
|
||||
const subjects = configured.filter((subject) => subject !== ADMIN_WILDCARD);
|
||||
|
||||
if (everyone) {
|
||||
degraded.push(
|
||||
"TERA_ADMIN_SUBJECTS=* grants the admin tier to every authenticated " +
|
||||
"subject on this box, time controls and debug included. That is a " +
|
||||
"development switch; anywhere reachable from outside, list the subject " +
|
||||
"ids instead.",
|
||||
);
|
||||
}
|
||||
|
||||
// Not a demotion of this setting — nothing falls back — but a line worth
|
||||
// printing, because admin is a property of an *authenticated* viewer and
|
||||
// mode=none never produces one. An operator who listed admins here and reads
|
||||
// `degraded` learns in one sentence why nobody is getting them.
|
||||
if ((everyone || subjects.length > 0) && mode === "none") {
|
||||
degraded.push(
|
||||
"TERA_ADMIN_SUBJECTS is set, but authentication resolved to mode=none: " +
|
||||
"nobody can sign in, so nobody is an admin. Set TERA_AUTH_MODE, and " +
|
||||
"check the lines above for an auth demotion that got you here.",
|
||||
);
|
||||
}
|
||||
|
||||
return { everyone, subjects };
|
||||
}
|
||||
|
||||
/**
|
||||
* The credential for `TERA_AUTH_MODE=password`, or `null` with a loud line if
|
||||
* the environment did not supply a usable one.
|
||||
|
||||
Reference in New Issue
Block a user