The normaliser's predicate never matched under MySQL's default
case-insensitive collation, and the comment called that the right answer
because nothing on MySQL was invisible. Only the SQL lookups are tolerant
there: the domain lookup is followed by an exact Go compare in
SynthesizeServiceForDomain, and the proxy's host map is keyed by the domain
verbatim, so a mixed-case row still missed. Spell the predicate byte-wise on
MySQL so the fold applies.
Two rows whose identities differ only by case would fold onto one endpoint,
which the unique index refuses with a driver message that names no row.
Both the reshape and the normaliser now stop first and name the hostname
that needs a human, in the same voice as the reshape's existing loud
failure.
The comments cited SNI folding as the reason the columns must be lowercase;
the SNI router folds both sides and would have matched. The readers that
miss are the exact ones, and the comments now name those.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
The legacy bootstrap stored the cluster as the caller spelled it — trimmed,
never folded — and the reshape that shipped in 0.78 copied it into
proxy_address, and subdomain.cluster into domain, verbatim. Everything that
reads those columns compares against canonical lowercase: proxies
canonicalise their address at connect, the gateway-pin check a proxy
registration runs matches proxy_address exactly, cluster-scoped synthesis
finds an account's row by proxy_address, and the proxy folds the SNI host
before matching a mapping's domain. A row that kept capitals is invisible
to all of them — its pin protects nothing and its endpoint never routes.
The reshape now writes LOWER() for both columns, and an idempotent
normaliser lowercases rows a released reshape already copied, registered
right after it. The predicate selects only rows that would change, so a
normalised table costs one pass over one row per account; on MySQL the
default collation compares case-insensitively already and it is a no-op.
Reported by cubic on #7402: the exact proxy_address match a proxy
registration relies on misses a migrated pin with capitals.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
Prevent unvalidated registrations from reserving domain names indefinitely.
Give pending registrations a 48-hour validation window and clean up expired entries at startup and every 60 minutes. Emit CustomDomainValidationExpired for each deletion and preserve registrations referenced by services.
Reject validation after expiry and prevent concurrent validation from recreating deleted registrations. Normalize domain names with the shared parser before registration.
Migrate existing pending registrations to receive a fresh 48-hour validation window.
Store the per-account gateway endpoint as {domain, proxy_address} with a
global unique index on the full hostname; dedicated = (domain ==
proxy_address). Bootstrap becomes an explicit POST carrying exactly one
of proxy_address (server allocates an adjective-noun label beneath it)
or endpoint (claimed verbatim, address-first); provider create loses its
bootstrap side effect. PUT is a full replace with every field required —
the immutable identity fields must be echoed unchanged and a mismatch is
rejected with 422. A guarded DELETE releases the endpoint: refused with
412 while providers exist or a proxy is actively serving the endpoint
hostname (matched case-insensitively); re-creating bootstraps fresh. A
self-addressed pin excludes its address from the account's cluster allow
list, and the live mapping update path now addresses the serving proxy
from the synthesized service. Existing rows are migrated on all three
store engines.
* [management] Ensure SessionStartedAt has a default value
Avoid null values for the new column
* [management] Add PeerStatus with LastSeen in peer_test
* [management] Add migration for PeerStatusSessionStartedAt default value
* [management] Add PeerStatus with LastSeen in migration tests
* Fix store migration on empty string
when fetching empty values from the database to check for migration our parser failed to handle null strings preventing the service from start
this uses sql.NullString to handle that and check for empty string resulted from null data
---------
Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com>
* serialize net.IP as json
* migrate net ip field from blob to json
* run net ip migration
* remove duplicate index
* Refactor
* Add tests
* fix tests
* migrate null blob values
This PR simplifies Store and FileStore
by keeping just the Get and Save account methods.
The AccountManager operates mostly around
a single account, so it makes sense to fetch
the whole account object from the store.
* feature: add User entity to Account
* test: new file store creation test
* test: add FileStore persist-restore tests
* test: add GetOrCreateAccountByUser Accountmanager test
* refactor: rename account manager users file
* refactor: use userId instead of accountId when handling Management HTTP API
* fix: new account creation for every request
* fix: golint
* chore: add account creator to Account Entity to identify who created the account.
* chore: use xid ID generator for account IDs
* fix: test failures
* test: check that CreatedBy is stored when account is stored
* chore: add account copy method
* test: remove test for non existent GetOrCreateAccount func
* chore: add accounts conversion function
* fix: golint
* refactor: simplify admin user creation
* refactor: move migration script to a separate package