Gratos_Signup

by 623f9b12 · generation 1 · every generation passed the checker when published.

DNS-verified domain-claiming flow: anonymous pending claims bind to an identity, advance by external DNS verification or a provider callback, and finalize into an at-most-one-owner registry; claims may expire and DNS may flap at any time. Checks one owner per domain, one pending claim per domain, no lingering claims after ownership, and that advancement always required a bound identity.

Raw .tla Raw .cfg

Gratos_Signup.tla

MODULE Gratos_Signup
EXTENDS Naturals, TLC, FiniteSets
CONSTANTS
Identities, Set of possible identities (human, bot, service, etc.)
Domains Set of possible domain names
VARIABLES
claims, Set of [domain, identity, hasCfId] — pending_claims rows in D1.

identity = 0 means unbound (anonymous claim). hasCfId: TRUE when cf_hostname_id has been written (= "activating").

claimed, Set of [domain, owner] — rows in domains table, at most one per domain.
dns Function Domains -> BOOLEAN

Models whether CNAME for authgravity.{d} points to cname.authgravity.net. The implementation checks against a global target, not per-session.

ClaimRec ≜ [domain: Domains, identity: Identities ∪ {0}, hasCfId: BOOLEAN]
vars ≜ ⟨claims, claimed, dns

--- Helpers ---

IsClaimed(d) ≜ ∃ rclaimed : r.domain = d
HasPendingClaim(d) ≜
rclaims : r.domain = d
ClaimOf(d) ≜
CHOOSE rclaims : r.domain = d
IsBound(d) ≜
rclaims : r.domain = dr.identity ≠ 0
IdentityOfClaim(d) ≜
(CHOOSE rclaims : r.domain = d).identity
HasCfId(d) ≜
rclaims : r.domain = dr.hasCfId

--- Type invariant ---

TypeOK
claimsClaimRec
claimed ⊆ [domain: Domains, owner: Identities]
DOMAIN dns = Domains
∧ ∀ dDomains : dns[d] ∈ BOOLEAN

At most one pending claim per domain (implementation enforces this)

∧ ∀ r1, r2claims : r1.domain = r2.domainr1 = r2

At most one owner per domain

∧ ∀ r1, r2claimed : r1.domain = r2.domainr1 = r2
Init
claims = {}
claimed = {}
dns = [dDomainsFALSE]

--- Actions ---

POST /claims — create pending claim (anonymous, no auth required). One pending claim per domain. Domain must not be claimed. In the implementation, if a pending claim already exists, 409 is returned (unless the caller is the same identity, which we model separately).

EnterDomain(d) ≜
∧ ¬IsClaimed(d)
∧ ¬HasPendingClaim(d)
claims = claims
{[domaind, identity ↦ 0, hasCfIdFALSE]}
UNCHANGEDclaimed, dns

POST /claims when domain is already claimed by same identity — "reclaim" path. Returns the existing domain record. No state change needed in the model since the domain is already claimed. We model it as a no-op that is enabled (proves the path exists without changing state). Reclaim(id, d) == /\ \E r \in claimed : r.domain = d /\ r.owner = id /\ UNCHANGED vars (Commented out: no-op actions don't help TLC and just expand the state space.)

Identity binds to an anonymous claim via POST /claims/:id/activate. In the implementation, the first activate call with auth binds identity_id.

BindIdentity(id, d) ≜
HasPendingClaim(d)
∧ (ClaimOf(d)).identity = 0
LET oldClaimOf(d)
IN claims = (claims \ {old}) ∪
{[domaind, identityid, hasCfIdold.hasCfId]}
UNCHANGEDclaimed, dns

External event: DNS owner sets CNAME for authgravity.{d} to point at cname.authgravity.net (the global target).

SetDNS(d) ≜
∧ ¬dns[d]
dns = [dns EXCEPT ![d] = TRUE]
UNCHANGEDclaims, claimed

External event: DNS owner removes or changes CNAME away.

ClearDNS(d) ≜
dns[d]
dns = [dns EXCEPT ![d] = FALSE]
UNCHANGEDclaims, claimed

Domain Connect success callback: sets cf_hostname_id directly, bypassing DNS check. This models the /domain-connect/callback redirect followed by an activate call with skip_dns=true. Requires auth (identity must be bound) since activate is auth-gated.

DomainConnectSuccess(d) ≜
HasPendingClaim(d)
IsBound(d)
∧ ¬HasCfId(d)
LET oldClaimOf(d)
IN claims = (claims \ {old}) ∪
{[domaind, identityold.identity, hasCfIdTRUE]}
UNCHANGEDclaimed, dns

POST /claims/:id/activate — advance claim via DNS verification path. If cf_hostname_id is NOT set and skip_dns is not true, DNS must be verified. Creates CF custom hostname and sets hasCfId = TRUE. Requires auth (identity bound).

ActivateDNS(d) ≜
HasPendingClaim(d)
IsBound(d)
∧ ¬HasCfId(d)

DNS must point to the global target

dns[d]
LET oldClaimOf(d)
IN claims = (claims \ {old}) ∪
{[domaind, identityold.identity, hasCfIdTRUE]}
UNCHANGEDclaimed, dns

POST /claims/:id/activate when cf_hostname_id is already set. DNS check is skipped (line 148 of index.ts: !claim.cf_hostname_id guard). CF hostname status becomes active, CNAME re-verified, domain claimed. Models the final transition from "activating" to "claimed". Can be triggered by user polling or by the scheduled handler.

FinalizeClaim(d) ≜
HasPendingClaim(d)
IsBound(d)
HasCfId(d)
∧ ¬IsClaimed(d)

Re-verify CNAME still points correctly (line 232-234 of index.ts)

dns[d]
LET idIdentityOfClaim(d)
IN claimed = claimed ∪ {[domaind, ownerid]}

Remove the pending claim for this domain

claims = claims \ {rclaims : r.domain = d}
UNCHANGEDdns

Same-owner reclaim during advanceClaim (lines 220-228 of index.ts): If we reach finalization but domain is already claimed by same identity, just clean up the pending claim.

FinalizeReclaim(d) ≜
HasPendingClaim(d)
IsBound(d)
HasCfId(d)
IsClaimed(d)
∧ ∃ rclaimed : r.domain = dr.owner = IdentityOfClaim(d)
claims = claims \ {rclaims : r.domain = d}
UNCHANGEDclaimed, dns

Claim expires (4-hour TTL) or is cancelled via DELETE /claims/:id. If the claim had a cf_hostname_id, the CF hostname is deleted too. Also models the scheduled handler expiring stale claims.

ExpireClaim(d) ≜
HasPendingClaim(d)
claims = claims \ {ClaimOf(d)}
UNCHANGEDclaimed, dns

Race condition: activate called but claim was already advanced by scheduled handler. The activate endpoint (lines 487-498) returns the most recent domain for that identity. This is a read-only operation in the implementation and doesn't change state, so we don't need a separate action for it.

Next
∨ ∃ dDomains : EnterDomain(d)
∨ ∃ idIdentities, dDomains : BindIdentity(id, d)
∨ ∃ dDomains : SetDNS(d)
∨ ∃ dDomains : ClearDNS(d)
∨ ∃ dDomains : DomainConnectSuccess(d)
∨ ∃ dDomains : ActivateDNS(d)
∨ ∃ dDomains : FinalizeClaim(d)
∨ ∃ dDomains : FinalizeReclaim(d)
∨ ∃ dDomains : ExpireClaim(d)
SpecInit ∧ □[Next]vars

--- Invariants ---

A claimed domain has no lingering pending claims

NoPendingAndClaimed
dDomains : IsClaimed(d) ⇒
¬∃ rclaims : r.domain = d

At most one owner per domain

OneDomainOneOwner
r1, r2claimed : r1.domain = r2.domainr1 = r2

At most one pending claim per domain

OneDomainOneClaim
r1, r2claims : r1.domain = r2.domainr1 = r2

A domain can only be claimed by a bound identity (not anonymous)

ClaimedByIdentity
rclaimed : r.ownerIdentities

If a claim has hasCfId=TRUE, it must be bound to an identity. (Only ActivateDNS or DomainConnectSuccess can set hasCfId, both require IsBound.)

CfIdImpliesBound
rclaims : r.hasCfIdr.identity ≠ 0

Gratos_Signup.cfg

SPECIFICATION Spec

All claims can expire, leaving a valid terminal state where no action is enabled. This is the system quiescing, not a deadlock.

CHECK_DEADLOCK FALSE
CONSTANTS
Identities = {id1, id2}
Domains = {d1, d2}
INVARIANTS
TypeOK
NoPendingAndClaimed
OneDomainOneOwner
OneDomainOneClaim
ClaimedByIdentity
CfIdImpliesBound

Generations

genchangesdistinct statesdepthpublishedraw
1 (latest) First publication to the hub; spec previously validated with the Java TLC toolchain. 256 13 2026-07-20 16:40:08 UTC .tla .cfg

Defend this spec

Ask an AI role-playing the spec's author to defend the design, dissertation-style. This site holds no AI keys: you grant a small revocable budget from your own tokenpony.dev balance (or any TPX provider you choose) and your browser talks to the model directly.

Loading…