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) ≜ ∃ r ∈ claimed : r.domain = d
HasPendingClaim(d) ≜
∃ r ∈ claims : r.domain = d
ClaimOf(d) ≜
CHOOSE r ∈ claims : r.domain = d
IsBound(d) ≜
∃ r ∈ claims : r.domain = d ∧ r.identity ≠ 0
IdentityOfClaim(d) ≜
(CHOOSE r ∈ claims : r.domain = d).identity
HasCfId(d) ≜
∃ r ∈ claims : r.domain = d ∧ r.hasCfId

--- Type invariant ---

TypeOK ≜
∧ claims ⊆ ClaimRec
∧ claimed ⊆ [domain: Domains, owner: Identities]
∧ DOMAIN dns = Domains
∧ ∀ d ∈ Domains : dns[d] ∈ BOOLEAN

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

∧ ∀ r1, r2 ∈ claims : r1.domain = r2.domain ⇒ r1 = r2

At most one owner per domain

∧ ∀ r1, r2 ∈ claimed : r1.domain = r2.domain ⇒ r1 = r2
Init ≜
∧ claims = {}
∧ claimed = {}
∧ dns = [d ∈ Domains ↦ FALSE]

--- 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 ∪
{[domain ↦ d, identity ↦ 0, hasCfId ↦ FALSE]}
∧ UNCHANGED ⟨claimed, 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 old ≜ ClaimOf(d)
IN claims′ = (claims \ {old}) ∪
{[domain ↦ d, identity ↦ id, hasCfId ↦ old.hasCfId]}
∧ UNCHANGED ⟨claimed, 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]
∧ UNCHANGED ⟨claims, claimed⟩

External event: DNS owner removes or changes CNAME away.

ClearDNS(d) ≜
∧ dns[d]
∧ dns′ = [dns EXCEPT ![d] = FALSE]
∧ UNCHANGED ⟨claims, 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 old ≜ ClaimOf(d)
IN claims′ = (claims \ {old}) ∪
{[domain ↦ d, identity ↦ old.identity, hasCfId ↦ TRUE]}
∧ UNCHANGED ⟨claimed, 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 old ≜ ClaimOf(d)
IN claims′ = (claims \ {old}) ∪
{[domain ↦ d, identity ↦ old.identity, hasCfId ↦ TRUE]}
∧ UNCHANGED ⟨claimed, 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 id ≜ IdentityOfClaim(d)
IN claimed′ = claimed ∪ {[domain ↦ d, owner ↦ id]}

Remove the pending claim for this domain

∧ claims′ = claims \ {r ∈ claims : r.domain = d}
∧ UNCHANGED ⟨dns⟩

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)
∧ ∃ r ∈ claimed : r.domain = d ∧ r.owner = IdentityOfClaim(d)
∧ claims′ = claims \ {r ∈ claims : r.domain = d}
∧ UNCHANGED ⟨claimed, 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)}
∧ UNCHANGED ⟨claimed, 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 ≜
∨ ∃ d ∈ Domains : EnterDomain(d)
∨ ∃ id ∈ Identities, d ∈ Domains : BindIdentity(id, d)
∨ ∃ d ∈ Domains : SetDNS(d)
∨ ∃ d ∈ Domains : ClearDNS(d)
∨ ∃ d ∈ Domains : DomainConnectSuccess(d)
∨ ∃ d ∈ Domains : ActivateDNS(d)
∨ ∃ d ∈ Domains : FinalizeClaim(d)
∨ ∃ d ∈ Domains : FinalizeReclaim(d)
∨ ∃ d ∈ Domains : ExpireClaim(d)
Spec ≜ Init ∧ □[Next]vars

--- Invariants ---

A claimed domain has no lingering pending claims

NoPendingAndClaimed ≜
∀ d ∈ Domains : IsClaimed(d) ⇒
¬∃ r ∈ claims : r.domain = d

At most one owner per domain

OneDomainOneOwner ≜
∀ r1, r2 ∈ claimed : r1.domain = r2.domain ⇒ r1 = r2

At most one pending claim per domain

OneDomainOneClaim ≜
∀ r1, r2 ∈ claims : r1.domain = r2.domain ⇒ r1 = r2

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

ClaimedByIdentity ≜
∀ r ∈ claimed : r.owner ∈ Identities

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

CfIdImpliesBound ≜
∀ r ∈ claims : r.hasCfId ⇒ r.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…