Skip to content

Commit 3f22a86

Browse files
w9autowyf9
andauthored
feat(captcha): switchable multi-provider set + GeeTest v4 + Cap (#30)
* feat: allow skipping 2FA on GPG-verified login * feat(captcha): switchable multi-provider set + GeeTest v4 + Cap Replace the single site-wide captcha provider with an ordered, switchable set: - captcha_providers[] — element 0 is the default, the rest are alternates a visitor can switch to (manually, on failure, or after a configurable timeout). The chosen provider is remembered in localStorage. - Each provider carries its own credentials; submissions name their provider and the server verifies against it, rejecting providers outside the enabled set. - Read-time migration seeds captcha_providers + per-provider keys from the legacy captcha_provider / shared key pair, so existing deployments keep working. New providers: - GeeTest v4 (SenseBot) — lib/geetest.ts, HMAC-signed server validation with a configurable fail-open/closed posture on GeeTest outage. - Cap (trycap.dev) — lib/cap.ts, embedded via capjs-core (KV-backed replay guard + redeem tokens, secret derived from the JWT secret) or external Cap Standalone. Aliases esbuild/javascript-obfuscator to a stub since capjs-core lazily imports them for instrumentation levels the embedded path never uses. Also: AdminSettings provider-set editor (default + alternates with reordering + per-provider panels), new config keys wired through allowlist/redaction/secret encryption/log redaction, EN+zh docs, CONTEXT.md glossary and ADR 0001. * fix(captcha): address Sourcery review - geetest: reject missing/malformed output unconditionally; only apply fail-open to a confirmed GeeTest transport/service outage, closing a bypass where an empty geetest object passed when fail-open was enabled (security). - cap: make the embedded single-use checks atomic via the D1 replay-claim table (claimReplayValue) instead of racy KV get-then-put — the challenge nonce and the redeem token are now consumed exactly once under concurrency. KV still proves token authenticity/expiry; the atomic claim is the single-use gate. - geetest widget: mount with product "popup" + appendTo(container) so the widget actually renders and onReady fires; removed the disconnected manual verify button. * Delete docs/adr/0001-switchable-captcha-set.md i dont want adr for now * docs: complete CONTEXT.md project glossary; point AGENTS.md to it Expand CONTEXT.md from the captcha-only glossary into the full project vocabulary — identity (user, team-as-user, restricted account), team structure (sub-team vs member group, effective role, site floor), apps & providers (OAuth app vs source, first-party/official/verified/trusted, public client), step-up/sudo, scopes & tokens, invitations, domains, captcha, and governance (audit log, notice, notification ruleset). Add an AGENTS.md 'Domain model' section directing agents to read and keep CONTEXT.md current. --------- Co-authored-by: wyf9 <wyf9@siiway.org>
1 parent 9758405 commit 3f22a86

44 files changed

Lines changed: 2417 additions & 459 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@
1212
- Run docs dev server: bun run docs:dev
1313
- Lint: bun run lint
1414

15+
## Domain model
16+
17+
- `CONTEXT.md` (repo root) is the project glossary — the canonical meaning of
18+
Prism-specific terms (User, Team-as-user, OAuth source vs OAuth app, step-up
19+
2FA, captcha enabled set, etc.). Read it before writing code so you use the
20+
project's vocabulary, and use it to disambiguate overloaded words.
21+
- When you introduce, rename, or sharpen a domain term, update `CONTEXT.md` in
22+
the same change. It is a glossary only: definitions, not implementation
23+
details or specs. An out-of-date glossary is a bug.
24+
1525
## Documentation
1626

1727
- After making functional changes (new features, changed behavior, deprecated

CONTEXT.md

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
# Prism
2+
3+
A self-hosted OAuth 2.0 / OpenID Connect identity platform on Cloudflare
4+
Workers. This file is the project glossary: the canonical meaning of terms that
5+
are specific to Prism's domain. It is not a spec and holds no implementation
6+
details — when a term is ambiguous in conversation or code, this is the tie-break.
7+
8+
## Identity
9+
10+
**User**:
11+
A real human identity — a row in `users` with `kind = "user"`. Can log in, hold
12+
factors, own apps, and belong to teams.
13+
_Avoid_: account, member (both are overloaded — see below).
14+
15+
**Team**:
16+
A shared owner of OAuth apps and verified domains, with its own members, roles,
17+
and optional public profile.
18+
19+
**Team-as-user**:
20+
A synthetic `users` row (`kind = "team"`, id equal to the team's id) that exists
21+
only so a team can own apps through the same owner column a user does. It has no
22+
password, no session, and never logs in.
23+
_Avoid_: team account.
24+
25+
**Restricted account**:
26+
A user created through a team's invite-link registration. It is confined to its
27+
origin team's subtree and is deactivated (then reaped) when that team dissolves,
28+
until it is converted to a full account.
29+
_Avoid_: limited user, guest.
30+
31+
**Origin team**:
32+
The team whose invite minted a restricted account; it bounds the subtree that
33+
account may act within.
34+
35+
## Team structure
36+
37+
**Sub-team**:
38+
A team nested under a parent (`parent_team_id`). Membership and verified domains
39+
cascade down the parent chain to it (subject to the inheritance switches).
40+
_Avoid_: child team, group.
41+
42+
**Member group**:
43+
A team-defined label attached to members *within a single team* — a
44+
many-to-many tag, not a nested team. A member can hold several.
45+
_Avoid_: role, team group, sub-team.
46+
47+
**Effective member / effective role**:
48+
A user's standing on a team computed by walking up the parent chain and taking
49+
the highest role held anywhere on it — as opposed to a **direct** membership row
50+
on that exact team.
51+
52+
**Role**:
53+
A member's standing within a team: `owner`, `co-owner`, `admin`, or `member`.
54+
55+
**Site floor**:
56+
A site-wide *minimum* join requirement (2FA, verified email) that every team
57+
must enforce. Teams may require more, never less.
58+
_Avoid_: default requirement.
59+
60+
## Applications & providers
61+
62+
**OAuth app**:
63+
A relying party registered by a user or team that obtains tokens from Prism.
64+
_Avoid_: client (only use "client" for the OAuth-protocol role), integration.
65+
66+
**OAuth source**:
67+
An external identity provider Prism federates *from* for social/federated login
68+
(GitHub, Google, Microsoft, Discord, Telegram, X, Generic OIDC, Generic OAuth 2).
69+
Multiple sources of the same kind may coexist.
70+
_Avoid_: provider (reserved for captcha), connection.
71+
72+
**Social connection**:
73+
A user's link to one specific OAuth source account.
74+
75+
**First-party app**:
76+
An OAuth app flagged as run by the instance operator; it skips the consent
77+
screen.
78+
79+
**Official app**:
80+
An OAuth app the operator marks as officially endorsed (a trust badge). Distinct
81+
from first-party: an app can be endorsed without being operator-run.
82+
83+
**Verified app**:
84+
An OAuth app an admin has reviewed. Unverified external apps show a warning on
85+
the consent screen.
86+
87+
**Trusted source**:
88+
An OAuth source whose account emails are treated as already verified at
89+
registration. Untrusted sources still force email verification afterwards.
90+
91+
**Public client**:
92+
An OAuth app with no server to keep a secret — must use PKCE and has no client
93+
secret. (This is the OAuth-protocol sense of "client".)
94+
_Avoid_: browser app.
95+
96+
## Authentication & step-up
97+
98+
**Step-up 2FA**:
99+
A server-initiated, one-time re-confirmation of a sensitive action via TOTP or
100+
passkey. It grants no new access — the result is a one-time proof the user
101+
re-confirmed.
102+
_Avoid_: re-auth, MFA prompt.
103+
104+
**Action-pinned**:
105+
The property that a step-up challenge's action text and redirect URI are fixed
106+
at the server-to-server step, so an attacker who only controls a URL cannot
107+
forge the confirmation prompt.
108+
109+
**Sudo mode**:
110+
A grace window opened by a successful step-up during which further confirmations
111+
for the same `(user, session, app)` skip the TOTP/passkey prompt.
112+
_Avoid_: elevated session.
113+
114+
**Passkey**:
115+
A WebAuthn / FIDO2 credential used as a login or step-up factor.
116+
117+
**Backup code**:
118+
A single-use recovery code that substitutes for a 2FA factor.
119+
120+
## Scopes & tokens
121+
122+
**Platform scope**:
123+
A scope from the fixed vocabulary in `shared/scopes.ts` (`openid`, `profile`,
124+
`teams:read`, …).
125+
126+
**Bound team scope**:
127+
A `team:<id>:…` scope validated against the one team a token is bound to — as
128+
opposed to the aggregate `teams:*` family that spans every team the user is in.
129+
130+
**Cross-app scope**:
131+
A named permission one app publishes for *other* apps to request through the
132+
standard consent screen.
133+
_Avoid_: exported permission (that's the code's internal term).
134+
135+
**Personal access token (PAT)**:
136+
A long-lived, user-minted API token (prefix `prism_pat_`) carrying
137+
user-grantable scopes.
138+
139+
## Invitations & registration
140+
141+
**Site invite**:
142+
An admin-issued invite that lets someone register on an invite-only instance.
143+
144+
**Team invite**:
145+
A team-issued invite to join an existing team. It may optionally also mint a new
146+
account (see invite-link registration).
147+
148+
**Invite-link registration**:
149+
Account creation through a team invite. It is what lets a team owner mint
150+
accounts, and requires a per-team grant from the site admin; accounts so created
151+
are restricted accounts.
152+
153+
## Domains
154+
155+
**Domain verification**:
156+
Proving ownership of a domain via DNS TXT, HTML meta, or a `.well-known` file, so
157+
the domain may back OAuth redirect URIs. The method chosen is reused on
158+
re-verification.
159+
160+
## Bot protection (captcha)
161+
162+
**Captcha provider**:
163+
A single bot-protection mechanism: `turnstile`, `hcaptcha`, `recaptcha`, `pow`
164+
(built-in proof-of-work), `geetest` (GeeTest v4), or `cap`
165+
([Cap](https://trycap.dev)). `none` is not a provider — it is the absence of one.
166+
_Avoid_: source (reserved for OAuth).
167+
168+
**Enabled set**:
169+
The ordered list of providers a site has turned on (`captcha_providers`). The
170+
moderator chooses both membership and order. An empty set means captcha is off.
171+
_Avoid_: provider list, captcha config.
172+
173+
**Default provider**:
174+
Element 0 of the enabled set — the provider rendered first to a visitor.
175+
_Avoid_: primary provider.
176+
177+
**Alternate**:
178+
Any non-default member of the enabled set. A visitor may switch to it.
179+
_Avoid_: fallback, secondary.
180+
181+
**Switch**:
182+
A visitor swapping the rendered widget from the current provider to an alternate.
183+
Offered manually, revealed on a verification failure, and revealed after
184+
`captcha_switch_timeout_seconds`. It never happens automatically — the visitor
185+
chooses — and the choice is remembered in the browser.
186+
187+
**Cap mode**:
188+
Where Cap runs: `embedded` (in-Worker via `capjs-core`, KV-backed) or `external`
189+
(a self-hosted Cap Standalone server).
190+
191+
**Fail open / fail closed**:
192+
The posture when a third-party captcha service (GeeTest) is unreachable: accept
193+
the visitor (fail open) or reject them (fail closed). Prism defaults to fail
194+
closed so an outage does not silently drop bot protection.
195+
196+
## Governance & communication
197+
198+
**Audit log (Transparent Control)**:
199+
The append-only record of significant actions, scoped to a user, a team, or the
200+
platform, and fanned out to scoped Discord / Telegram / general webhooks.
201+
_Avoid_: request log (that is separate operational telemetry).
202+
203+
**Notice**:
204+
An operator-authored announcement shown inside the product within a time window,
205+
dismissible per reader.
206+
_Avoid_: banner, alert, announcement.
207+
208+
**Notification ruleset**:
209+
A named, ordered set of match / action / stop rules that route a user's
210+
per-event email and Telegram notifications.

0 commit comments

Comments
 (0)