Appearance
Accounts & email
gammonchain has no passwords. Your identity is an Ed25519 key pair. The server knows only the public half, and you prove you hold the private half by signing a fresh challenge on every connection. You can either keep that key yourself (a guest) or let the server keep it for you so that you can sign in from anywhere by email (an account). The difference matters, and this page explains why.
How identity works
On first visit the browser generates an Ed25519 key pair with WebCrypto and stores it in localStorage (public/identity.js). Each WebSocket connection then goes like this:
- The server sends a random 16-byte
challengein itswelcomemessage. - The client signs the challenge and sends
hellowith its name, public key, signature, and the tip of its dice reveal chain. - The server verifies the signature (
identify()insrc/server/index.js). Only a key that proves itself is bound to the session. Otherwise anyone could type in someone else's public key and play on their rating.
A guest also gets a readable generated name on first visit, such as swiftotter42, which the browser keeps. The name is only a label. The key is the identity.
Browsers without Ed25519
If the browser cannot do Ed25519 in WebCrypto (the code comments mention Safari before 17), it can still play, unrated, and the server says so in its identity reply.
Guests and accounts
| guest | account | |
|---|---|---|
| name | a generated one, kept by the browser | your username |
| where the private key lives | only in your browser | in your browser and on the server, with your account |
| play on another device | no | a sign-in link from your inbox (works once, 30 minutes) |
| can the operator sign as you? | no | yes |
The last row is the honest cost of email sign-in, and it is why guests still exist. Without a password, there is no way to get your key onto a new device that does not also hand it to the server on the way.
This affects what a match record proves. The verifier's participation check requires every named player to have signed. For a guest, whose key never leaves their browser, that signature cannot be forged, so the record proves the guest played. For an account, the operator holds the key and could produce the signature. If your signature on a match record needs to mean something the operator cannot forge, stay a guest. See Verification.
Guests: your key is your rating
A guest key lives only in that browser's storage. If you clear site data, the identity and its rating are gone, and nobody can restore them. public/identity.js has methods to export and import the key as a file, but the current client has no button for either.
Registering an account
Registration needs a verified key, a username and an email address. It only works on servers that have a mail provider configured.
- Usernames are 3–20 characters: letters, digits,
-or_. They must be unique. - The browser sends its private key (as a JWK) with the registration. The server stores it with the account. That is exactly the trade-off described above.
- The server emails a sign-in link. Opening the link on any device installs the key in that browser.
- If the address already has an account, the server sends a sign-in link to that address and gives the same reply as for a new registration. Answering "that address is already registered" would tell anyone which addresses have accounts.
Magic sign-in links
To sign in on a new device, request a link by email from the account panel. The flow (login-link over the WebSocket, then POST /api/login-claim):
- The token is 24 random bytes and works once. It is cleared from the database the moment someone tries to redeem it, even if it turns out to have expired.
- It expires after 30 minutes.
- The link has the form
<PUBLIC_URL>/?login=<token>. The client removes the token from the address bar right away, redeems it, and gets the account's key, username and email back. - The reply is always the same whether or not an account exists for the address.
- Each key and each email address may trigger at most 5 emails per hour. This is an in-memory quota in
src/server/index.jsthat stops one socket from making the server send unlimited mail.
PUBLIC_URL must match the site exactly
Sign-in, confirmation and unsubscribe links are built from PUBLIC_URL. If it is wrong (wrong scheme, or left at the http://localhost:8080 default), the links 404 or point at localhost. See Email.
The daily puzzle by email
The daily puzzle is one position a day, the same for everyone, taken from matches the server recorded. A brand-new install with no matches uses self-play instead. The server screens up to 24 candidate positions at 1 ply, evaluates the four sharpest at 2 ply, and picks the one where the best play leads the second-best by the widest margin. It stops early once a margin reaches 0.12 (MIN_GAP * 2). There is no minimum margin: on a day with nothing sharp, the sharpest available position is still used. The server can email it every day, by default at 08:00 UTC (MAIL_HOUR).
The current web client has no subscription form
The server implements the whole opt-in flow over the WebSocket (subscribe, email-prefs, email-status; see WebSocket protocol), and the confirmation and unsubscribe links work. But public/app.js never sends those messages, so gammonchain.com currently has no button to subscribe. The flow below is what the server does when a client asks.
Email is strictly double opt-in:
- A client with a verified key sends an address. The same address cannot be subscribed to two different keys.
- The server emails a confirmation link (
/email/verify?t=...). Nothing else is sent to that address until the link is clicked. Until then the subscription ispending. - Once confirmed, one short email a day arrives, with the position drawn as a monospace text board, which survives email clients that strip images or HTML.
Every puzzle email includes a one-click unsubscribe link and a List-Unsubscribe header. A subscriber can also:
- turn the daily mail off: the subscription is kept, with
dailyset to 0. This is what the unsubscribe link does. - forget my address: the subscription row is deleted.
Sending is idempotent per address per day, and each run is capped at MAIL_DAILY_CAP addresses. Subscriptions belong to a key, not to an account, so a guest can subscribe too.
You get one attempt at each puzzle, and the first answer stands. The puzzle panel on the site works for every verified key. After you answer, it shows the engine's ranking of the plays, the equity you gave up and your streak (consecutive days answered correctly).
Deleting an account
Deletion is self-service from the account panel. It is sent over the authenticated WebSocket, so it needs a key that proved itself on this connection, plus an explicit confirmation string. It runs in a single database transaction (deleteAccount() in src/server/db.js).
Erased:
- email address
- username
- the stored private key
- any outstanding sign-in token
- the whole mail subscription
Kept:
- the public key and the rating row attached to it: rating, deviation, games, wins and points. The display name is replaced with
retired player. - signed match records, which are immutable and stay published.
The public key is kept for correctness, not as a loophole. It is written into signed match records that other players also played and verify offline. Removing it would break third-party verification of games that are not yours alone to erase. What remains is a bare public key with a win/loss count, and nothing on the server links it to a person.
Deletion also retires the key in your browser
After a successful deletion, the client clears the account and the stored private key from localStorage, then reloads. The browser mints a new key and starts over as a new guest. The old rating stays attached to the old public key, and nobody holds a way to play on it again.