Skip to content

Auth & access

Registration, login, access codes and the waitlist.

Every schema below is exported from @novaterra/contracts and validated on both sides of the wire. Source: packages/contracts/src/auth.ts.

RegistrationProof

The answer to a door’s proof-of-work challenge (Council 038 6-A). challenge is the opaque, server-signed string GET /api/auth/door handed out; nonce is what the browser found such that sha256(challenge + ':' + nonce) starts with the challenge’s number of zero bits. Bounded on both sides for the same reason LinkToken is: the server issues short strings and anything wildly longer is a probe.

FieldTypeNotes
challengestring (min 16, max 256)
noncestring (min 1, max 32)

RegisterRequest

FieldTypeNotes
emailstring (email)
passwordstring (min 8)
handlestring (min 2, max 32, regex)
displayNamestring (min 1, max 80)
accessCodestring (min 4)optional
proofRegistrationProofoptional

DoorState

What the door says about itself, to anybody, before they have an account (GET /api/auth/door). mode is the honest answer to “can I come in without an invitation”: open on a Hall whose owner has not pulled the kill-switch, invite everywhere else. Every limit a stranger meets is published here as a NUMBER read from the constant that enforces it, so the sign-up screen and the deploy pre-flight describe the gate the server actually runs rather than one somebody remembered. challenge is null when the door is shut: there is nothing to prove your way past.

FieldTypeNotes
mode'open' | 'invite'
reasonstring
verificationRequiredboolean
challenge{ token: string; bits: number (safeint, ≥ 0, ≤ 32); expiresAt: ISODate } | null
limits{ perIpPerDay: number (safeint); perDomainPerHour: number (safeint); perProviderDomainPerHour: number (safeint) }

WaitlistNotifyResult

Owner only: the one-shot “the door is open” letter to everybody on the waitlist.

FieldTypeNotes
sentnumber (safeint)
failednumber (safeint)
alreadyToldnumber (safeint)
totalnumber (safeint)

LoginRequest

FieldTypeNotes
emailstring (email)
passwordstring

AuthResponse

FieldTypeNotes
beingBeing
isOwnerboolean

AccessCode

FieldTypeNotes
codestring
createdById
usedById | null
notestringoptional · default ''
createdAtISODate
usedAtISODate | null

WaitlistRequest

FieldTypeNotes
emailstring (email)
namestring (max 80)optional
whystring (max 500)optional

EmailStatus

What the signed-in person’s own address looks like from here. emailConfigured is deliberately part of the response rather than something the UI guesses: a node with no RESEND_API_KEY cannot send anything, and a button offering to “send a link” that silently goes nowhere is worse than no button. It is a capability flag, not a secret — it says whether this node has a mail provider, not who it is or what its key is. ⭐ email IS NULLABLE, AND THE NULL IS A FACT RATHER THAN AN ABSENCE OF ONE. A credentials row is written by exactly one thing — POST /api/auth/register — so every being that arrived any other way has none: the twelve seeded citizens on a fresh node, and anybody created outside registration. GET /api/auth/email used to answer them 404 Account, which is a sentence about the ACCOUNT (“there is no such thing here”) standing in for a sentence about the ADDRESS (“nobody has set one”). Those are different facts and must stay different, the same way an ABSENT capability grade is not a grade of 0 and an empty event list from prose is not an empty calendar. It also made /verify a 404 for every being that is not the registered owner, which tests/contracts/citizenship.test.ts is the standing objection to: a being is a being. null therefore means “no address is set on this account”, and it is the only value that means it. verified: false beside a real address means “set, not confirmed” and must not be read as this case — a UI that branches on verified alone offers to send a link to nowhere.

FieldTypeNotes
emailstring (email) | null
verifiedboolean
verifiedAtISODate | null
pendingboolean
emailConfiguredboolean
requiredbooleanoptional

VerifyEmailRequest

FieldTypeNotes
tokenstring (min 16, max 256)

PasswordResetRequest

“Send me a way back in.” Answered identically whether or not the address is known here.

FieldTypeNotes
emailstring (email)

SetPasswordRequest

The other half: the token from the link, and the password to put in its place.

FieldTypeNotes
tokenstring (min 16, max 256)
passwordstring (min 8)

InviteEmailRequest

Owner only: mint a code and send it to an address, rather than reading it off a screen.

FieldTypeNotes
emailstring (email)
notestring (max 200)optional · default ''

InviteEmail

One invitation, as the owner’s own screen shows it back. codeHint is a MASK (NOVA-••••-7F3K), not the code. An access code is a single-use bearer credential: whoever reads it can claim a place in this world, and this list is the one screen that will hold dozens of them at once. Settings once printed 27 live codes in plaintext and that was the finding; a list whose job is “who did I invite, and did they arrive” does not need to carry the credential to do it. The owner who needs the literal code still has GET /api/auth/codes.

FieldTypeNotes
idId
emailstring (email)
codeHintstring
notestringoptional · default ''
sentById
deliveredboolean
reasonstringoptional · default ''
usedboolean
createdAtISODate