Prima Get an API key

Module kit / Identity and auth

Desk domain sign-in

identity-auth/desk-domain-sign-in

Kind backend
Domain identity-auth
Maturity prod-flag-off
Reusable extract
Stack fastapi-0.118, sqlalchemy-sync, postgres, dnspython-2.8, smtp
Depends on _shared/fastapi-project-layout, _shared/auth-dependency-pattern, _shared/workspace-scoping, _shared/error-envelope, _shared/alembic-migrations, _shared/scheduler-and-background-work, _shared/test-layout
Frontends identity-auth/ui/auth-screens
Reference implementation
  • alchemy_labs/backend/api/routers/desk_login_router.py
  • alchemy_labs/backend/services/service_desk/signin.py
  • alchemy_labs/backend/services/service_desk/access.py
  • alchemy_labs/backend/models/user_login_email_model.py
  • alchemy_labs/backend/api/routers/auth_router.py
  • alchemy_labs/backend/api/dependencies.py
  • alchemy_labs/backend/services/auth_service.py
  • alchemy_labs/backend/services/scheduled_jobs.py
  • alchemy_labs/backend/migrations/versions/20260905_0001_desk_domain_login.py
  • alchemy_labs/backend/tests/test_desk_domain_signin.py
  • alchemy_labs/backend/tests/test_desk_domain_verification.py
  • alchemy_labs/backend/tests/test_desk_acting_email.py
Measured on alchemy_labs d1a-token-source fd8558f 2026-09-12
Also measured on
  • alchemycloud main b3d34da 2026-09-12
Version 1

Purpose

Anyone with a mailbox at a domain a workspace has proven (one TXT record) can ask for a magic link and land on the account the workspace's rule names, either the practice's one shared account or their own auto-created one, with no user administration by anyone; and everything that session then does carries the address that asked, so a shared account never hides who ordered what. The invariant that makes it trustworthy is that a declaration grants nothing until the zone's owner has published the challenge, that the link is always mailed to the address that asked and never to the account's own address, that a sign-in request never creates an account, that a revoked address gets no new link however many rules would re-mint it (its session rows are deactivated at once; its still-valid access token is refused only with the denylist on, Gotchas), and that a desk-only account cannot reach any Labs route outside /api/desk/ and /api/auth/.

When to use

Include it when a product must let a whole organisation in through its mail domain without provisioning people one by one, and when a shared login must still say who acted. Trigger phrases: "everyone at this domain", "one account for the practice", "who actually ordered this", "add my colleague's other address", "revoke the departed employee", "the DNS record", "verify the domain", "the session says acting email", "desk-only account".

Do not use it for the magic link itself (identity-auth/magic-link-login, B5, whose create_magic_link and verify_magic_link this recipe calls), for the session claims aem and dsk (_shared/auth-dependency-pattern, S5, which mints and re-derives them), for the workspace configuration document that declares a rule (service-desk and ADR-0035: desk.login_domains is a section of workspace_configs.config), for the desk's roles and agent gating (B138, DeskAgent and PlatformAdmin), or for the ticket surface that renders acting_email (B132 onward). Prima surfaces are out: resolve_login_email is entered only when the requested surface is labs (auth_router.py 1039 to 1041).

Stack assumptions

Pinned at fd8558f (2026-09-12): fastapi==0.118.2, dnspython==2.8.0 (requirements.txt 28; imported lazily on the verify path, access.py 475), SQLAlchemy sync, Postgres (15-alpine in docker-compose.prod.yml 9; 16 on labs-staging per alchemycloud README at b3d34da line 28, and its scripts/sync-prod-to-staging.sh 77 to 79 pulls a postgres:18-alpine client because prod answers as 18; the tests below ran on the :5434 server, postgres:14 in docker-compose.test.yml 6, which reports 14.19) with every timestamp in this module timestamptz (user_login_email_model.py 31 to 33 and _ts, 54 to 58), the desk's mail transport (notifications.send_requester_mail, notifications.py 58 to 80: the desk's own intake mailbox when one is configured, inbox_config.send_via_mailbox, the same connection the IMAP poller reads; otherwise the platform default transport, email_service._send_email; never the workspace SMTP relay, as the function's own comment says). That transport carries the alias confirmation only (_send_alias_confirmation, desk_login_router.py 479 to 507); the magic link itself rides B5's email_service.send_magic_link_email_task with the template_override argument (auth_router.py 1109 to 1117). Foundations assumed: S4, S5 (create_session with acting_email, _enforce_desk_only_containment), S6 (WorkspaceID on the agent routes), S8 (one additive revision), S12 (the daily re-check under the leader lock), S9.

Environment names and their fail behaviour (core/config.py 982 to 988):

NameDefaultBehaviour
DESK_DOMAIN_LOGIN_ENABLEDfalseoff: resolve_login_email returns None (signin.py 110 to 111) and the daily re-check returns without checking (access.py 769 to 770); the agent routes still work, so a workspace can declare and prove a domain before the flag is turned on. Set nowhere in alchemycloud at b3d34da (grep DESK_DOMAIN_LOGIN_ENABLED scripts/distribute-env.sh env/.env.example prints nothing), which is why the maturity is prod-flag-off
DESK_SIGNIN_LINKS_PER_DOMAIN_PER_HOUR60the per-domain budget for addresses without a binding; 0 disables it
DESK_SIGNIN_INTENT_TTL_MINUTES15an individual intent's lifetime; matches the magic link's 15 minutes (auth_service.py 1141)
DESK_ALIAS_CONFIRM_TTL_HOURS72the agent-added alias confirmation's lifetime
DESK_DOMAIN_DNS_TIMEOUT_SECONDS5per query; the resolution lifetime is twice it (access.py 480 to 483)
DESK_DOMAIN_UNVERIFY_AFTER_FAILURES14consecutive daily failures before the proof lapses; agents warned at 3
DESK_DOMAIN_REMOVAL_GRACE_DAYS30how long a disabled verification row keeps its token
FRONTEND_URLhttp://localhost:3000 (core/config.py 43)the base of the alias confirmation link (desk_login_router.py 65 to 67)

Data model

The migration desk_domain_login_001 (20260905_0001_desk_domain_login.py, revises service_desk_core_001, every column add guarded so a re-run is a no-op, 32 to 50):

-- one row per address that may sign a person in, beyond users.email. models/user_login_email_model.py 65 to 123
user_login_emails (
  id uuid PRIMARY KEY,
  user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  email varchar(320) NOT NULL,                       -- lowercased, plus tag stripped
  source varchar(16) NOT NULL CHECK (source IN ('explicit','domain_rule')),   -- ck_user_login_emails_source
  workspace_id uuid NULL REFERENCES workspaces(id) ON DELETE SET NULL,         -- the rule's or the granting workspace; every write path sets it
  domain varchar(253) NULL,                          -- the rule's domain for domain_rule rows; the provenance re-check reads it
  verified_at timestamptz NULL,                      -- inert while NULL
  revoked_at timestamptz NULL,                       -- set, never deleted: the deny marker
  revoked_by_user_id uuid NULL REFERENCES users(id) ON DELETE SET NULL,
  created_by_user_id uuid NULL REFERENCES users(id) ON DELETE SET NULL,
  confirm_token_hash varchar(64) NULL,               -- sha256 hex of an explicit alias's confirmation token
  confirm_expires_at timestamptz NULL,
  last_login_at timestamptz NULL,
  created_at timestamptz NOT NULL DEFAULT now(),
  CONSTRAINT uq_user_login_emails_email UNIQUE (email)                          -- one address, one account
);
CREATE INDEX ix_user_login_emails_user ON user_login_emails(user_id);
CREATE INDEX ix_user_login_emails_ws ON user_login_emails(workspace_id);
CREATE UNIQUE INDEX uq_user_login_emails_confirm_token ON user_login_emails(confirm_token_hash) WHERE confirm_token_hash IS NOT NULL;

-- the proof that a workspace controls a domain. 126 to 182
desk_domain_verifications (
  id uuid PRIMARY KEY,
  workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
  domain varchar(253) NOT NULL,                      -- lowercased, no @, no trailing dot
  verification_token varchar(64) NOT NULL,           -- secrets.token_urlsafe(24); a challenge, returned to the agent in full
  verified_at timestamptz NULL, verified_by_user_id uuid NULL REFERENCES users(id) ON DELETE SET NULL,
  verification_method varchar(16) NULL,              -- dns | manual
  last_checked_at timestamptz NULL, check_failures integer NOT NULL DEFAULT 0,
  disabled_at timestamptz NULL,                      -- soft removal from the document; token kept for the grace window
  created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(),
  CONSTRAINT uq_desk_domain_verifications_ws_domain UNIQUE (workspace_id, domain)
);
CREATE UNIQUE INDEX uq_desk_domain_verifications_verified ON desk_domain_verifications(domain)
  WHERE verified_at IS NOT NULL AND disabled_at IS NULL;                        -- at most one live proof per domain, platform-wide
CREATE INDEX ix_desk_domain_verifications_domain ON desk_domain_verifications(domain);

-- individual mode: a mailbox proof that exists before the account does. 185 to 209
desk_signin_intents (
  id uuid PRIMARY KEY, email varchar(320) NOT NULL,
  workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
  domain varchar(253) NOT NULL, role varchar(20) NOT NULL,                     -- desk_requester | viewer
  token_hash varchar(64) NOT NULL, next_path varchar(512) NULL,
  expires_at timestamptz NOT NULL, consumed_at timestamptz NULL, created_at timestamptz NOT NULL DEFAULT now(),
  CONSTRAINT uq_desk_signin_intents_token_hash UNIQUE (token_hash)
);
CREATE INDEX ix_desk_signin_intents_email ON desk_signin_intents(email);
CREATE INDEX ix_desk_signin_intents_domain_created ON desk_signin_intents(domain, created_at);   -- the per-domain budget reads this

-- additive columns on existing tables (migration 55 to 81)
ALTER TABLE magic_links ADD COLUMN login_email varchar(320) NULL, ADD COLUMN login_workspace_id uuid NULL REFERENCES workspaces(id), ADD COLUMN next_path varchar(512) NULL;
ALTER TABLE sessions ADD COLUMN acting_email varchar(320) NULL;
ALTER TABLE desk_tickets ADD COLUMN acting_email varchar(320) NULL;      -- backfilled from requester_email (231 to 232)
ALTER TABLE desk_messages ADD COLUMN acting_email varchar(320) NULL;
ALTER TABLE desk_status_events ADD COLUMN acting_email varchar(320) NULL;

The declaration lives elsewhere: workspace_configs.config -> 'desk' -> 'login_domains' is a JSONB array of {domain, mode: shared_account | individual, account_email?, role?} (config_schema.py 29 to 33; LOGIN_DOMAINS_SCHEMA defined at 195 to 203 and wired into the desk section at 290; ADR-0035), read by _declarations with a containment probe (access.py 134 to 152). config_service._write mirrors every declaration into desk_domain_verifications after the flush (config_service.py 167 to 169; sync_domain_verifications, access.py 507 to 565: a new domain gets a row and a token, a removed one gets disabled_at, a re-add inside the grace clears it and keeps the token).

What is not stored: the raw confirmation token and the raw intent token (SHA-256 only; magic_links.token itself is stored raw by B5, which this module inherits); a user row for an individual request before the click; a deleted binding (rows are revoked); the DNS answer (only check_failures and last_checked_at); the address in any log line (email_fingerprint, signin.py 53 to 55, twelve hex characters of its SHA-256).

API surface

Agent routes, included into the desk router (desk_login_router.py 599) and therefore under /api/desk on LABS_SURFACES (app_fastapi.py 824), each with DeskAgent (VerifiedUser plus WorkspaceID plus the agent desk role, service_desk_router.py 80 to 90, 110) unless noted:

RouteAuth depTenancyCodesNotes
GET /api/desk/config/domainsDeskAgentX-Workspace-ID200 list of {domain, mode, account_email, role, declared, verified_at, verification_method, was_verified, disabled_at, dns_name, dns_value, last_checked_at, check_failures}runs _sync first (self-heal for documents that predate the mirror); rows without a rule are listed as declared: false (136 to 188)
POST /api/desk/config/domains/{domain}/verifyDeskAgentheader200 {verified_at, method: "dns"}; 422 {"error": "domain_public"}; 404 {"error": "domain_not_declared"}; 409 {"error": "domain_verified_elsewhere"}; 409 {"error": "txt_record_not_found", expected_name, expected_value, found: [...]}; 409 {"error": "shared_account_unusable"}; 422 {"error": "shared_account_missing_email"}the resolver's answer is the gate (250 to 273); idempotent on an already-verified row
POST /api/desk/config/domains/{domain}/verify-manualPlatformAdmin and DeskAgentheader200 {verified_at, method: "manual", verified_by_user_id}; 403 {"error": "admin_required"}; the _prepare refusalsus, never the workspace's agent (276 to 296)
DELETE /api/desk/config/domains/{domain}/verificationDeskAgentheader200 {verified_at: null, revoked_bindings, killed_sessions}; 404 {"error": "not_found"}body {revoke_sessions: true} by default; the declaration stays (299 to 326)
GET /api/desk/config/login-emailsDeskAgentheader200 list of {email, source, domain, verified_at, revoked_at, last_login_at, created_by, user: {id, email, name}}scoped by user_login_emails.workspace_id (334 to 368)
POST /api/desk/config/login-emailsDeskAgentheader201 {email, user_id, verified_at: null, confirm_sent_to}; 403 {"error": "target_not_member"}, {"error": "target_is_admin"}; 409 {"error": "domain_owned_elsewhere"}, {"error": "email_taken"}body {email, user_id}; the target must already be an active member of this workspace; mails a confirmation to the address, mints no session (376 to 476)
POST /api/desk/login-emails/confirmnonenone200 {email, workspace, account}; 400 {"error": "invalid_or_expired"}body {token}; sets verified_at, clears the token, mints nothing (515 to 552); no rate limit of its own (Gotchas)
DELETE /api/desk/config/login-emails/{email}DeskAgentheader200 {revoked_at, killed_sessions}; 404 {"error": "not_found"} (also for another workspace's binding); 409 {"error": "is_account_email"}, {"error": "target_is_agent"}revokes, deactivates that address's session rows only and denylists their tokens (560 to 592; the access token outlives this while the denylist is off, Gotchas)

The sign-in routes are B5's, with this module's branches: POST /api/auth/request-magic-link (auth_router.py 1007 to 1120; auth_rate_limit and the per-mailbox throttle, B18) answers 202 whatever the resolution, and POST /api/auth/verify-magic-link (1127 onward) returns the session pair plus acting_email, next and desk_only (the body from 1243 to 1258; MagicLinkCallback.tsx 68 reads desk_only to choose the landing route), or 503 "Access control is temporarily unavailable." when the containment question cannot be answered (1211 to 1216), or 403 "This account is not permitted on this surface." from create_session.

Error shape: the agent routes raise HTTPException with a dict detail ({"error": "<code>"}), rendered by S7 (app_fastapi.py 772 to 787): the dict's error string becomes the envelope's error and the whole dict travels in details (schemas/error_schemas.py 112 to 136); the auth routes use string details.

Application logic

Resolution, at request-magic-link on labs (signin.resolve_login_email, 107 to 171), in order:

  1. Flag off: None, and the pre-slice-2 path runs unchanged. Normalise the address (normalise_login_email, 58 to 72: strip, lowercase, drop the plus tag), take its domain.
  2. The deny marker first: a row with revoked_at for this address is None and a desk.signin_denied reason=revoked log line (118 to 123).
  3. effective_rule_for_domain (access.py 197 to 227): the live proof row for the domain (verified_at IS NOT NULL AND disabled_at IS NULL, at most one by the partial index), then that workspace's declaration; a proof without a declaration grants nothing. user_for_email (252 to 301): users.email first, then a live binding whose provenance is re-checked (a domain_rule row whose domain is no longer proven, or now proven by a workspace whose shared account is someone else, is inert).
  4. An owner exists: kind="user", and if a rule exists and the account is not api_only, heal_membership adds the rule's role (never overwriting, never re-activating an inactive row, 327 to 350). The person's own account always wins over the rule.
  5. No owner and no rule: None. No owner and mode="shared_account": the account named by account_email must exist, be active and be a member, else None with a desk.shared_account_missing error (fail closed; verification provisioned it, a login request never will); heal_shared_account re-grants labs and the membership; kind="shared_account". No owner and mode="individual": kind="individual" with user_id=None.
  6. domain_link_budget_ok (179 to 227): exempt for kind="user" and for any address with a live binding; else count the last hour's magic_links rows with a login_email at this domain (a naive-datetime table, compared with utcnow()) plus desk_signin_intents rows (aware, now(timezone.utc)), and refuse at the limit with a domain_link_budget notification to the agents. A refusal turns the resolution back into None, so the answer is still 202 and the ordinary path mints nothing for an unknown address (auth_router.py 1042 to 1048).
  7. Mint: individual writes an intent (create_intent, 267 to 292: secrets.token_urlsafe(32), SHA-256 stored, TTL, next_path) and mails the raw token as the magic link; user and shared_account call create_magic_link_for_user(user_id, surface=LABS, login_email=addr, next_path, login_workspace_id) (auth_service.py 1088 onward: login_email stored only when it differs from users.email). The mail goes to request_data.email, the address that asked (auth_router.py 1109 to 1117); the template is magic_link__desk for a shared account and for a desk-only person, the ordinary one for an ordinary account that happens to be at a proven domain (1082 to 1087; email_service.py 464 to 480 tries the override first).

Redemption, at verify-magic-link (auth_router.py 1148 to 1262):

  1. auth_service.verify_magic_link(token) first; on None, desk_signin.redeem_intent(db, token) (signin.py 307 to 397): find by hash, refuse consumed or expired; if a user with that email now exists (created between request and click), refuse an inactive or api_only one and an inactive membership, else heal the membership; else create the user (password_hash=None, verified, 2FA off, role="user", token_quota=0, 343 to 360), grant_surfaces({labs}), heal the membership with the intent's role; upsert the user_login_emails row as domain_rule with verified_at and last_login_at; mark the intent consumed; return a Redemption(user_id, surface=LABS, login_email, login_workspace_id, next_path).
  2. The user must exist and be active; the verification gate (REQUIRE_EMAIL_VERIFICATION) applies as for any magic link.
  3. record_login (405 to 467) when login_email differs from users.email: refuse an address that is another account's users.email, or bound to another account, or revoked (log and return; the login proceeds without a binding); else insert or update the row with verified_at, last_login_at and the workspace carried from the link row. Wrapped so a ledger failure never costs the login (1192 to 1202).
  4. is_desk_only_user (access.py 367 to 391): at least one active membership, every one in DESK_ONLY_ROLES = ("desk_requester",), and the user owns no workspace; any query error raises DeskAccessUnavailable and the route answers 503 rather than guessing. A desk-only account skips _ensure_workspace (1211 to 1218): it must never own a workspace, because ownership is checked instead of membership in three places and an owner is inside every fence.
  5. create_session(..., surface=LABS, acting_email=<the address, when it is not the account's own>) mints the pair with aem and dsk (S5); the response carries acting_email, next and desk_only.

Verification, at the agent routes:

  1. _prepare (191 to 212): normalise; a public mail domain is 422 (PUBLIC_MAIL_DOMAINS, access.py 43 to 55, 23 entries); _sync; the rule must be declared and its row live; a domain proven by another workspace is 409 before anything is written (the partial index would otherwise surface as a 500 with a constraint name, 115 to 122).
  2. check_domain_txt (access.py 458 to 499): resolve _alchemy-desk.<domain> TXT with the configured timeout; every resolver exception is (False, []); each record's strings are joined (a value split at 255 bytes is one value, 442 to 455); match is case-insensitive on the alchemy-desk-verify prefix and case-sensitive on the token. A miss answers 409 with expected_name, expected_value and everything actually found at that name (262 to 272).
  3. _finish (215 to 247): for shared_account, ensure_shared_account runs before the proof is written (598 to 669: create the account with no password or reuse it, refuse an inactive or api_only one, grant_surfaces({labs}), refuse an inactive membership, heal the membership); then verified_at = verified_at or now, verification_method, verified_by_user_id, check_failures = 0, commit. Verifying twice keeps the first date and re-runs the healing.
  4. DELETE .../verification calls revoke_domain_access(kill_sessions=True) (677 to 745): clear verified_at and verification_method (keep verified_by_user_id, which is what was_verified reads), revoke every domain_rule binding of that workspace and domain, deactivate those users' session rows for those addresses only and denylist their tokens (revoke_sessions_for, signin.py 470 to 499, always scoped by user_id; what a deactivated row stops is in Gotchas), keep memberships. The document is untouched.
  5. Daily (scheduled_jobs.run_daily_jobs 1766, _run_desk_domain_recheck 1792 to 1813, recheck_domain_verifications 748 to 811): with the flag on, re-resolve every live DNS proof (manual ones are skipped); success resets check_failures; a failure increments it, warns the agents at exactly 3, and at 14 calls revoke_domain_access(kill_sessions=False): bindings go inert, sessions survive, because the record proves the declaration, not the person at the keyboard. Then purge_disabled_verifications hard-deletes rows disabled longer than the grace.

Aliases, at the login-emails routes:

  1. POST (376 to 476): the target must be an active member of this workspace, not a platform admin; the address's domain must not be proven by another workspace; the address must not be another account's users.email or a live binding of another account; then a row (or the re-armed revoked row) with source="explicit", verified_at=None, a 32-byte token hashed into confirm_token_hash and a 72-hour expiry; commit before the mail (458 to 464); the German confirmation names the workspace, the agent and the account it binds to (479 to 507). confirm (515 to 552) flips verified_at on the matching hash and clears the token. DELETE (560 to 592): 404 for a row of another workspace, 409 for the account's own address or an agent's, else revoked_at, revoked_by_user_id, and that address's session rows deactivated.

Invariants

  • A declaration proves nothing: effective_rule_for_domain starts from the proof row, and test_a_declared_but_unproven_domain_signs_nobody_in (test_desk_domain_signin.py 146) and test_declaring_creates_the_row_and_the_token_but_proves_nothing (test_desk_domain_verification.py 217) pin it.
  • At most one live proof per domain, platform-wide, by the partial unique index; _verified_elsewhere answers 409 before the index would answer 500.
  • The mail goes to the address that asked, never to account_email (auth_router.py 1109 to 1117; test_request_magic_link_mints_for_the_account_and_mails_the_asker, 720).
  • A sign-in request creates no user row (test_no_user_row_is_created_by_resolving, 278; test_a_request_writes_an_intent_and_no_user, 385); an individual account exists only after the click.
  • The deny marker is read before the rule branch (signin.py 118 to 123); a revoked address is refused under a live rule (test_a_revoked_address_is_refused_even_under_a_live_rule, 210).
  • Rows are revoked, never deleted; grep -n "db.delete" services/service_desk/signin.py api/routers/desk_login_router.py prints nothing (the only delete in the module is purge_disabled_verifications, on proof rows past the grace).
  • An existing account always wins over a rule (test_an_existing_account_always_wins_and_gains_the_membership, 165); record_login refuses to bind an address that is another account's users.email (450).
  • An inactive membership is a removal and stays one: heal_membership never re-activates (181, 432).
  • A shared account and an individual account have no password (ensure_shared_account 627 to 646; redeem_intent 350 to 352), so login() can never authenticate a shared mailbox and bypass the per-address trail.
  • An agent nominates an address, never an account: the confirmation token sets verified_at and mints nothing (515 to 522; test_an_alias_mails_a_confirmation_and_mints_no_session, 588).
  • The containment question is answered or the login is refused: DeskAccessUnavailable is 503 at redemption (1211 to 1216) and in the dependency (S5), never False (test_is_desk_only_user_refuses_rather_than_guessing, 528; test_redemption_refuses_when_containment_cannot_be_answered, 841).
  • A desk-only account never owns a workspace: _ensure_workspace is skipped for it at redemption (1218) and at login (603 to 604).
  • The per-domain budget can never lock out someone who already signed in (test_the_cap_never_locks_out_someone_who_already_signed_in, 315).
  • Session revocation is scoped by user_id first and addresses second (revoke_sessions_for), so an address list from an endpoint cannot reach another account (test_revocation_can_never_reach_another_account, 506). What it does is set sessions.is_active = False and denylist both jtis (493 to 497); refresh refuses the row at once (auth_service.py 809 to 821), the access token only when the denylist answers (Gotchas).
  • Every timestamp in the three tables is aware; the one naive comparison is against magic_links.created_at and uses utcnow() deliberately (signin.py 202 to 217).

Frontend

identity-auth/ui/auth-screens                  interim: the magic-link request screen (the address is typed here; the
                                               desk branch is invisible to it, the answer is 202 either way) and
                                               MagicLinkCallback, which posts verify-magic-link and honours `next`
service-desk/ui/desk-client-portal             /desk/login-email/confirm ConfirmLoginEmail (pages/desk/ConfirmLoginEmail.tsx,
                                               App.tsx 255): posts the token, shows the workspace and account it bound;
                                               the ticket views render acting_email beside the requester
service-desk/ui/desk-settings-and-notifications /admin/desk/login AdminDeskLogin (pages/admin/desk/AdminDeskLogin.tsx, App.tsx
                                               381): the domains table with the proof states removed | declared | failing |
                                               proven from lib/deskLogin.ts (unit-tested in lib/deskLogin.test.ts), the
                                               dns_name and dns_value to copy, verify and un-verify, the login-email
                                               aliases list with add and revoke; serviceDeskService.ts 1038 to 1105

Minimum admin and debug surface: GET /api/desk/config/domains for one workspace (every state, the token, check_failures, last_checked_at); GET /api/desk/config/login-emails; the log lines desk.domain_verified, desk.domain_verification_deleted, desk.login_email_added, desk.login_email_confirmed, desk.login_email_revoked, desk.signin_denied, desk.binding_inert, desk.shared_account_missing, desk.domain_link_budget_exhausted, desk.domain_unverified, desk.sessions_revoked, each with a workspace id or a user id, and a fingerprint wherever an address is involved (desk.domain_unverified carries workspace and domain, desk.sessions_revoked a user id and a count), never an address; SCHEDULER: desk domain re-check {stats} once a day when anything was checked; dig TXT _alchemy-desk.<domain> from the host, which is what the verifier sees.

Security & tenancy

Auth dependency: DeskAgent on the agent routes (a VerifiedUser with the agent role in the named workspace), PlatformAdmin (is_admin) additionally on manual verification, none on confirm and on the two auth routes. Workspace derivation: X-Workspace-ID through WorkspaceID (S6) on every agent route; the listing and revoke routes filter on user_login_emails.workspace_id and answer 404, not 403, for another tenant's row (567 to 570). Fail direction: closed everywhere a decision is made (an unresolvable domain is not proven; a missing shared account mints nothing; an unanswerable containment question is 503; a ledger failure at record_login is logged and the login proceeds without the binding, which is the one open seam, chosen so that the audit trail can never cost a login, 1192 to 1202).

Credentials at rest: the confirmation and intent tokens as SHA-256 (confirm_token_hash, token_hash); the DNS challenge token in clear, because it is a challenge that proves control of a zone only to whoever can already write that zone (_domain_payload 164 to 167); magic_links.token in clear, inherited from B5. Rate limits: auth_rate_limit and the per-mailbox throttle on request-magic-link (B18), the per-domain hourly budget here (which counts links, not requests, and exempts known addresses), nothing on confirm (Gotchas), nothing on the agent routes beyond the global tier.

Attacker-controlled inputs: the requested address (normalised; its domain looked up; never logged in clear; its plus tag stripped so one mailbox cannot mint unlimited distinct identities against the per-domain cap, signin.py 61 to 65); next (a relative path by validator, schemas/auth_schemas.py 150 to 156, stored on the link row and returned at redemption; test_an_absolute_next_is_refused, 785); the domain path parameter on the agent routes (normalised, refused when public, matched against the workspace's own declarations); the TXT record's contents (compared, then returned to the agent in the 409 as found, which is the zone's own data); the confirmation and intent tokens (hashed before lookup, single use, expiring); the user_id an agent nominates (must be an active member of the agent's own workspace, never an admin, 383 to 394: desk.agents is readable through the workspace config, so an unchecked id would let any agent target any account). The X-Alchemy-Surface and Host decide the surface (B5), and the desk branch runs only for labs.

Tenancy of the binding: a domain_rule row belongs to the workspace whose rule minted it and carries that domain; the provenance re-check (user_for_email 283 to 300) makes it inert when that domain is no longer proven or is now proven by a workspace whose shared account is a different user, so a domain that changes hands does not carry its old bindings to the new owner. A desk-only account is fenced to /api/desk/ and /api/auth/ plus GET /api/workspaces and one bare workspace object (desk_only_path_allowed, access.py 405 to 423); the fence is on the user, not the workspace, because POST /api/meta/disconnect names a workspace in its body and reaches neither workspace fence (the docstring at 406 to 417). GET /api/workspaces/{id}/settings is outside the fence on purpose: it returns the whole settings JSON (identity-auth/credential-encryption-at-rest, B19).

Tests

Backend (alchemy_labs/backend/tests/, run 2026-09-12 at fd8558f against the :5434 Postgres): python -m pytest tests/test_desk_domain_signin.py tests/test_desk_domain_verification.py tests/test_desk_acting_email.py -q --no-cov -o addopts="" -p no:randomly prints 1 failed, 121 passed in 75.76s. The -p no:randomly is load-bearing: the three files together are not order-independent (Gotchas), and without it the run is under pytest-randomly like every other suite in this codebase, so the same command can surface a different failure count on a different seed.

  • test_desk_domain_signin.py (998 lines, 45 test functions): the flag (test_the_flag_is_off_by_default, 135), the three resolution shapes (142 to 278), the budget (308 to 353), intents (385 to 432), record_login (450, 462), revocation scope (492, 506), containment (522, 528), the alias routes (553 to 696), the two auth routes end to end (720 to 841), the claims on the token and the session row and the per-address session cap (914 to 983: six people on one account keep six sessions, one person still cannot hoard devices).
  • test_desk_domain_verification.py (628 lines, 34 test functions): the TXT check against a fake resolver (zone fixture, 63 to 81, monkeypatches dns.resolver.Resolver; 167 to 208, including the split-record join and the resolver-failure case), the mirror (217 to 261), the verify routes (281 to 435), the daily re-check (495 to 579: nothing lost at a few failures, warned at three, lost at the fourteenth, sessions spared, manual proofs skipped, nothing while the flag is off), deletion (591 to 625).
  • test_desk_acting_email.py (614 lines, 27 test functions; the three files collect 122 cases with parametrisation): aem and dsk end to end with OptionalUser (S5 Tests); test_requester_owns_is_decided_by_the_acting_address (276 to 305) is the red one (Gotchas).
  • Known-answer vectors: normalise_login_email(" Fritz+Desk@Practice.EXAMPLE ") is fritz@practice.example (adapted from the docstring's example at signin.py 59, which uses a client domain, with a fictional one; test_a_plus_tag_is_the_same_person, 262); dns_challenge("Practice.Example.", "tok") is ("_alchemy-desk.practice.example", "alchemy-desk-verify=tok"); a TXT answer of alchemy-desk-verify=tok matches, ALCHEMY-DESK-VERIFY=tok matches (prefix case-folded), alchemy-desk-verify=TOK does not (test_the_token_is_compared_case_sensitively, 172; test_the_prefix_is_not, 178); email_fingerprint is the first twelve hex characters of sha256(lower(strip(email))), so email_fingerprint("a@example.com") is 08168cd80dfd (computed 2026-09-12 from the definition at 53 to 55).
  • Hermetic throughout: DNS is the fake resolver, mail is the mail_log fixture, the flag is monkeypatched (login_on, 58 to 61). Playwright: none at fd8558f for the confirm page or the admin login page; the deskLogin.test.ts vitest (proof-state model) is F59's.

Deploy & ops

Config keys per app heredoc (S13): DESK_DOMAIN_LOGIN_ENABLED=true when a workspace is ready to sign people in by domain (absent at b3d34da: the code ships, the switch is off); the six tuning values only when a default is wrong; FRONTEND_URL for the confirmation link. Migrations order: desk_domain_login_001 after service_desk_core_001 and before any code from c591c90 (2026-09-05, the router's first commit, which already reads the three tables); it is chained ahead of the mail half so the login half can be reverted alone (its docstring 9 to 13); downgrade drops the three tables and the columns (236 to 248). Scheduler jobs: _run_desk_domain_recheck inside run_daily_jobs under the S12 leader lock; it commits its own stats and rolls back on error. Argus checks: dig +short TXT _alchemy-desk.<domain> for every proven domain returning the published value (the daily job checks the same thing from inside; an external check catches a resolver difference between the host and the world); the desk.domain_unverified log line as an alert. Rollback: DESK_DOMAIN_LOGIN_ENABLED=false stops resolution and re-checks at once without touching a row; bindings and proofs stay and mean nothing until the flag returns; a code rollback past c923b8a needs the migration's downgrade only if the columns must go, and the acting_email columns are additive and nullable, so they can stay.

Gotchas

  • 2026-09-12 (measured at fd8558f, D8 gotcha, red test): tests/test_desk_acting_email.py::test_requester_owns_is_decided_by_the_acting_address (276 to 305) constructs DeskSettings(...) without client_brief, a field dd38691 (2026-09-06) added after the test was written on 2026-09-05; it fails with TypeError: DeskSettings.__init__() missing 1 required positional argument: 'client_brief'. The other 121 pass. The fix is client_brief=None in the constructor call, or a fixture that builds DeskSettings from config_service.resolved_desk.
  • 2026-09-12 (measured at fd8558f): POST /api/desk/login-emails/confirm is unauthenticated and carries no auth_rate_limit (515 to 516), unlike every other token-redeeming route (B18). The token is 32 bytes of token_urlsafe looked up by hash, so guessing is not the risk; the risk is a flood that costs a database read per attempt inside the global 1000 per minute. Add dependencies=[Depends(auth_rate_limit)].
  • 2026-09-12 (measured at fd8558f, D8 gotcha): revocation does not end a live access token. revoke_sessions_for sets sessions.is_active = False and calls auth_service._denylist_token for both tokens (signin.py 493 to 497), but get_current_user reads the JWT and the users row and never the sessions table (api/dependencies.py 186 to 241), and decode_token asks the denylist only through is_jti_revoked, which answers False whenever JWT_DENYLIST_ENABLED is off or Redis is unreachable (auth_service.py 57 to 70, 146 to 163, 516 to 520). core/config.py declares no such field, so the flag is the environment variable, default false; alchemycloud at b3d34da sets it nowhere (git grep JWT_DENYLIST b3d34da prints nothing; B13's Gotchas record the same), and the harness runs with REDIS_ENABLED=false (tooling/verify/labs-local.sh 103). So a revoked address keeps its access token for up to JWT_ACCESS_TOKEN_EXPIRE_MINUTES (60, core/config.py 117) and loses only refresh (refresh_session filters Session.is_active == True, auth_service.py 809 to 821, and the route turns the ValueError into 401). The test pins only fritz.is_active is False (test_desk_domain_signin.py 676 to 693), never a 401. The fix the reference needs: JWT_DENYLIST_ENABLED=true with Redis reachable on every app that mints Labs sessions, at which point the access lifetime also drops to 15 minutes (S5); a new build reads sessions.is_active in its dependency instead, so revocation needs no second store.
  • 2026-09-12 (measured at fd8558f): the per-domain budget counts magic_links rows with a login_email at the domain (202 to 213), and create_magic_link_for_user stores login_email only when it differs from users.email (auth_service.py docstring, "stored ONLY when it differs"). A shared account whose own account_email asks for a link is therefore not counted, and a kind="user" resolution is exempt anyway; the budget bounds strangers, which is what the docstring says it does.
  • 2026-09-12 (measured at fd8558f): _notify_budget and _warn_dns emit through DeskNotification.emit inside the caller's transaction (signin.py 230 to 259) and swallow every exception; the notification's commit is the caller's. At request-magic-link the ordinary refusal path sets resolution = None and commits at auth_router.py 1050, so the notification is committed with it; it would be lost only if something raised between the emit and that commit, which reaches the rollback at 1053, and nothing on that path raises today. Harmless for the requester (202 either way); a new build that adds work between the budget check and the commit should keep the notification's fate in mind.
  • 2026-09-12 (measured at fd8558f): the daily re-check runs check_domain_txt serially for every proven domain with a lifetime of twice the timeout each (10 s at defaults), inside the one run_daily_jobs session; a hundred proven domains with a dead resolver hold the daily job for a quarter of an hour. Bounded today by the number of desk workspaces; a new build resolves in a thread pool with a total deadline.
  • 2026-09-12 (measured at fd8558f): the migration backfills desk_tickets.acting_email from requester_email (231 to 232), so tickets older than 2026-09-05 read the requester as the actor, which is right for tickets a person filed as themselves and a guess for tickets filed by the shared account before the column existed. The docstring says so; nothing else can be known.
  • 2026-09-12 (measured at fd8558f): PUBLIC_MAIL_DOMAINS is a 23-entry literal (access.py 43 to 55), so a freemail provider not on it can be declared, and a workspace that happens to control a TXT record there (it cannot, for any real provider) would sign in every stranger with an address there. The list is the fence; extend it before a new market.
  • 2026-09-12 (measured at fd8558f): sessions.ip_address at redemption is request.client.host (auth_router.py 1228), the forgeable value (B18 Gotchas); the acting_email on the same row is trustworthy, the IP is not.
  • 2026-09-12 (measured at fd8558f, public-safety): the schema examples for the desk login-domain fields (services/service_desk/config_schema.py lines 170, 183 and 201, the field-level domain and account_email examples and the whole-entry example that repeats them) carry a real client domain and a real client mailbox as their examples values, not a fictional one, at all three sites, not the one this recipe first measured. All three are reachable through the same JSON schema endpoint (GET /api/config/schema, mounted at app_fastapi.py 826, VerifiedUser only) and returned to every Labs session, not only desk agents. Replace all three with practice.example / desk@practice.example, matching the fictional domain this recipe uses throughout; the real values are not repeated here (AUTHORING.md, no client names in a recipe).
  • 2026-09-12 (measured at fd8558f, D8 gotcha): the three desk test files are not order-independent. test_desk_domain_signin.py::test_refresh_rederives_the_claim_from_the_session_row uses a committed_account fixture that opens core.database.get_db_context() directly and inserts a User row without depending on the session-scoped schema-building fixture (conftest.py, the same pattern B4's Gotchas records for test_surface_scoped_access.py); run alone, or first in a shuffled order, it fails at setup with psycopg2.errors.UndefinedTable: relation "users" does not exist (measured: pytest tests/test_desk_domain_signin.py -k test_refresh_rederives_the_claim_from_the_session_row errors on its own; the full three-file run without -p no:randomly printed 1 failed, 120 passed, 1 error against the same 1 failed, 121 passed the pinned order gives). Fix: have committed_account request the db fixture (or any fixture that forces schema creation) instead of opening its own context, the same fix B4 names for its own flaky test.
  • 2026-09-05 (c591c90, the router's first commit, whose docstring 18 to 24 already records it): an earlier draft let an agent name a user_id and mailed that account a session-minting link; because desk.agents is a list of user ids readable through the workspace config, every target was public. The confirmation token that sets verified_at and mints nothing is the replacement. A new build never lets an authenticated caller cause a session to be minted for an account they name.
  • 2026-09-05 (e895e7d, found while building this module): the shared per-IP counter was 429ing a client's own login because their site's analytics beacon shared the office IP; /api/auth left the volumetric tier and the per-mailbox throttle arrived (B18). Any product that puts a tracking beacon and a login behind one IP needs the same split.

Reference implementation

alchemy_labs, branch d1a-token-source at fd8558f (2026-09-12; backend identical to feat/service-desk b1089b6; the module landed across 034e23b (the three models and the migration), fbd26b1 (the declaration schema), c591c90 (the router and signin.py), cfd833b (the DNS proof and the re-check), c923b8a (containment in access.py and test_desk_acting_email.py) and 44955d1 (was_verified, desk_only, the frontend), all on 2026-09-05). Paths relative to backend/:

  • api/routers/desk_login_router.py (599 lines): _confirm_url (65 to 67), _sync (90 to 101), _verified_elsewhere (115 to 133), _domain_payload (136 to 172), list_domains (175 to 188), _prepare (191 to 212), _finish (215 to 247), verify_domain (250 to 273), verify_domain_manually (276 to 296), delete_domain_verification (299 to 326), list_login_emails (334 to 368), add_login_email (376 to 476), _send_alias_confirmation (479 to 507), confirm_login_email (515 to 552), revoke_login_email (560 to 592), the include (599).
  • services/service_desk/signin.py (499 lines): email_fingerprint (53 to 55), normalise_login_email (58 to 72), Resolution (79 to 85), resolve_login_email (107 to 171), domain_link_budget_ok (179 to 227), _notify_budget (230 to 259), create_intent (267 to 292), _unique_username (295 to 304), redeem_intent (307 to 397), record_login (405 to 467), revoke_sessions_for (470 to 499).
  • services/service_desk/access.py (839 lines): PUBLIC_MAIL_DOMAINS (43 to 55), DESK_ONLY_ROLES (58), DeskAccessUnavailable (75 to 85), DomainRule (88 to 109), _declarations (134 to 152), _verification (155 to 165), declared_rule_for_domain (168 to 194), effective_rule_for_domain (197 to 227), rules_for_workspace (230 to 245), user_for_email (252 to 301), is_workspace_member (304 to 314), heal_membership (327 to 350), heal_shared_account (353 to 364), is_desk_only_user (367 to 391), desk_only_path_allowed (405 to 423), dns_challenge (437 to 439), _txt_strings (442 to 455), check_domain_txt (458 to 499), sync_domain_verifications (507 to 565), purge_disabled_verifications (567 to 590), ensure_shared_account (598 to 669), revoke_domain_access (677 to 745), recheck_domain_verifications (748 to 811), _warn_dns (814 to 839).
  • models/user_login_email_model.py (209 lines): the three tables.
  • api/routers/auth_router.py: the desk branch of request_magic_link (1032 to 1120), verify_magic_link (1127 to 1262: intent redemption 1152 to 1159, record_login 1190 to 1202, containment 1204 to 1218, the session 1222 to 1236), the same containment in login (603 to 604).
  • api/dependencies.py: _enforce_desk_only_containment (160 to 184), ActingEmail (782 to 794) (S5).
  • services/auth_service.py: Redemption (308 to 325), create_magic_link_for_user (1088 to 1146), create_session with acting_email (683 to 769).
  • services/service_desk/config_service.py 167 to 169 (the mirror call), services/service_desk/config_schema.py 29 to 33, LOGIN_DOMAINS_SCHEMA 195 to 203 (definition) and 290 (wired into the desk section), services/service_desk/access.py as above.
  • services/scheduled_jobs.py: run_daily_jobs (1753 to 1770), _run_desk_domain_recheck (1792 to 1813).
  • migrations/versions/20260905_0001_desk_domain_login.py (248 lines).
  • core/config.py 982 to 988.
  • Tests as listed under Tests.
  • Frontend (frontend/): src/services/serviceDeskService.ts 1038 to 1105, src/pages/admin/desk/AdminDeskLogin.tsx, src/pages/desk/ConfirmLoginEmail.tsx, src/lib/deskLogin.ts and deskLogin.test.ts, src/pages/MagicLinkCallback.tsx 48, src/App.tsx 255 and 381.
  • Wiki (philosophers_stone 6322fa3): platform/service-desk.md line 105 (the founder's model, 2026-09-04) and 126 (the slice).

Deviations of the reference from this recipe, all dated 2026-09-12: the flag off on the estate; the red DeskSettings test; no rate limit on confirm; serial DNS re-checks; the backfilled acting_email on old tickets; the three schema examples naming a client; one test not order-independent; the forgeable session IP; the access token outliving a revocation while the denylist is off.

Acceptance checks

Run from alchemy_labs/backend at fd8558f with the :5434 Postgres up, or from a new service built to this recipe. Expected observations are exact.

  1. python -m pytest tests/test_desk_domain_signin.py tests/test_desk_domain_verification.py tests/test_desk_acting_email.py -q --no-cov -o addopts="" -p no:randomly prints 1 failed, 121 passed at fd8558f and 122 passed once the DeskSettings call carries client_brief.
  2. python -c "from core.config import settings; print(settings.DESK_DOMAIN_LOGIN_ENABLED)" prints False with no environment set; in alchemycloud at b3d34da, grep -c DESK_DOMAIN_LOGIN_ENABLED scripts/distribute-env.sh prints 0.
  3. python -c " from services.service_desk.signin import normalise_login_email as n, email_fingerprint as f from services.service_desk.access import dns_challenge as d, is_public_mail_domain as p print(n(' Fritz+Desk@Practice.EXAMPLE '), d('Practice.Example.', 'tok'), p('gmail.com'), p('practice.example'), f('a@example.com'))" prints fritz@practice.example ('_alchemy-desk.practice.example', 'alchemy-desk-verify=tok') True False 08168cd80dfd.
  4. grep -n "db.delete" services/service_desk/signin.py api/routers/desk_login_router.py | wc -l prints 0; grep -c "db.delete(row)" services/service_desk/access.py prints 1 (the purge past the grace window, and nothing else).
  5. On a database at alembic head (the harness's scratch database labs_render from tooling/verify/labs-local.sh up, which runs alembic upgrade head, or alembic upgrade head against a scratch database of your own; not test_db after a pytest run, because tests/conftest.py 102 to 108 builds the schema with Base.metadata.create_all and drops every table at session teardown, so no alembic_version row and no migrated schema survive it): psql postgresql://test_user:test_password@localhost:5434/labs_render -c "\d desk_domain_verifications" | grep -c "uq_desk_domain_verifications_verified" prints 1 and the index line reads "uq_desk_domain_verifications_verified" UNIQUE, btree (domain) WHERE verified_at IS NOT NULL AND disabled_at IS NULL. Keep -c, not -Atc: tuples-only mode prints the column rows and suppresses the Indexes footer, so the same grep prints 0 on a fully migrated database (measured 2026-09-12).
  6. Setup for 6 to 11, with the harness up and DESK_DOMAIN_LOGIN_ENABLED=true exported before tooling/verify/labs-local.sh up (the script starts the backend through env with its own fixed list, labs-local.sh 100 to 107 and 160, and env inherits the caller's exports, so the variable reaches it). The harness seeds one owner, render-owner@example.com, with no desk role and no admin bit; keep it that way (AUTHORING.md). $W is that owner's workspace: psql postgresql://test_user:test_password@localhost:5434/labs_render -Atc "SELECT id FROM workspaces WHERE owner_id = (SELECT id FROM users WHERE email = 'render-owner@example.com')"; $O is the owner's id from the inner select. $P: curl -s -X POST -H "Content-Type: application/json" -d '{"email":"platform-admin@example.com","password":"<a fresh one>","first_name":"Platform","last_name":"Admin"}' http://127.0.0.1:8020/api/auth/register returns a session (REQUIRE_EMAIL_VERIFICATION=false); then psql ... -c "UPDATE users SET is_admin = true, admin_appointed_by = id, admin_appointed_at = now() WHERE email = 'platform-admin@example.com'" (the shape from identity-auth/admin-user-management-and-impersonation, Deploy & ops; is_admin is read from the row on every request, so the session just minted is now an admin's, and an admin is an agent in every workspace, roles.py 64 to 65); then psql ... -c "INSERT INTO workspace_users (id, workspace_id, user_id, role, is_active, joined_at, updated_at) VALUES (gen_random_uuid(), '$W', (SELECT id FROM users WHERE email = 'platform-admin@example.com'), 'member', true, now(), now())", because the workspace middleware admits X-Workspace-ID only for an owner or an active workspace_users row and has no admin bypass (middleware/workspace_middleware.py 286 to 360; the tests never see this, desk_support.py 119 to 125 overrides the workspace dependency). The harness's /login page auto-signs-in the first admin from now on (dev-login, AUTHORING.md); tooling/verify/labs-local.sh reset undoes all of this. The declaration and the agent list, written by $P because the owner is not yet an agent and ConfigAgent refuses anyone else (service_desk_router.py 1012 to 1020): curl -s -i -H "Authorization: Bearer $P" http://127.0.0.1:8020/api/workspaces/$W/config | grep -i etag prints ETag: "1" (the document is created at v1 on first read, config_service.py 173 to 187), then curl -s -X PATCH -H "Authorization: Bearer $P" -H "If-Match: \"1\"" -H "Content-Type: application/json" -d '{"patch": {"agents": ["'$O'"], "login_domains": [{"domain": "practice.example", "mode": "shared_account", "account_email": "desk@practice.example"}]}}' http://127.0.0.1:8020/api/workspaces/$W/config/desk -w "\n%{http_code}" prints 200 (the owner counts as an active member for desk.agents, roles.py 105 to 121; the shared account need not exist yet, config_validate.py 101 to 107). $A: POST /api/auth/login with {"email": "render-owner@example.com", "password": "<tooling/verify/.labs-local/owner.pass>"} returns the pair; the owner is now an agent (roles.py 72 to 73). With $A, $W and $P in hand: curl -s -H "Authorization: Bearer $A" -H "X-Workspace-ID: $W" http://127.0.0.1:8020/api/desk/config/domains | python -c "import json,sys; d=json.load(sys.stdin)[0]; print(d['declared'], d['verified_at'], d['dns_name'], d['dns_value'][:20])" prints True None _alchemy-desk.practice.example alchemy-desk-verify=; then curl -s -X POST -H "Authorization: Bearer $A" -H "X-Workspace-ID: $W" http://127.0.0.1:8020/api/desk/config/domains/practice.example/verify -w "\n%{http_code}" prints a body with "txt_record_not_found", "expected_name", "expected_value" and "found":[] and the code 409 (the harness has no such zone).
  7. Same setup, with the platform admin's session in $P: curl -s -X POST -H "Authorization: Bearer $P" -H "X-Workspace-ID: $W" http://127.0.0.1:8020/api/desk/config/domains/practice.example/verify-manual | python -c "import json,sys; d=json.load(sys.stdin); print(d['method'], bool(d['verified_at']))" prints manual True; psql ... -Atc "SELECT password_hash IS NULL, is_verified FROM users WHERE email = 'desk@practice.example'" prints t|t (the shared account was provisioned at verification with no password); the same call with $A instead of $P is 403 with "admin_required".
  8. curl -s -X POST -H "Content-Type: application/json" -d '{"email":"someone@practice.example"}' http://127.0.0.1:8020/api/auth/request-magic-link -w "%{http_code}" prints 202; the harness log carries a [DEV] Magic link (labs) for someone@practice.example: line whose token, posted to POST /api/auth/verify-magic-link, returns a body with "acting_email":"someone@practice.example" and "user":{"email":"desk@practice.example", ...}; decoding the access token's payload (S5 check 10) shows aem equal to the acting address and dsk equal to 1; psql ... -Atc "SELECT source, verified_at IS NOT NULL, workspace_id = '$W' FROM user_login_emails WHERE email = 'someone@practice.example'" prints domain_rule|t|t.
  9. With that session in $S: curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $S" http://127.0.0.1:8020/api/forms/templates prints 403 and the body's error is desk_only_role with details.error equal to it (the S7 envelope of a dict detail); curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $S" http://127.0.0.1:8020/api/desk/tickets prints 200; psql ... -Atc "SELECT count(*) FROM workspaces WHERE owner_id = (SELECT id FROM users WHERE email = 'desk@practice.example')" prints 0.
  10. curl -s -X DELETE -H "Authorization: Bearer $A" -H "X-Workspace-ID: $W" http://127.0.0.1:8020/api/desk/config/login-emails/someone@practice.example | python -c "import json,sys; d=json.load(sys.stdin); print(bool(d['revoked_at']), d['killed_sessions'])" prints True 1; psql ... -Atc "SELECT is_active FROM sessions WHERE acting_email = 'someone@practice.example'" prints f; curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" -d '{"refresh_token": "'$R'"}' http://127.0.0.1:8020/api/auth/refresh (with $R the refresh token minted in check 8) prints 401; curl -s -o /dev/null -w "%{http_code}" -H "Authorization: Bearer $S" http://127.0.0.1:8020/api/desk/tickets still prints 200 on the harness and on the estate at b3d34da, and prints 401 only where JWT_DENYLIST_ENABLED=true and Redis are set (Gotchas); a second request-magic-link for the same address is 202 and the log carries no [DEV] Magic link line for it, only desk.signin_denied reason=revoked.
  11. curl -s -X DELETE -H "Authorization: Bearer $A" -H "X-Workspace-ID: $W" -H "Content-Type: application/json" -d '{"revoke_sessions": true}' http://127.0.0.1:8020/api/desk/config/domains/practice.example/verification | python -c "import json,sys; d=json.load(sys.stdin); print(d['verified_at'], d['revoked_bindings'] >= 0)" prints None True; GET /api/desk/config/domains now shows "verified_at": null, "was_verified": true, "declared": true for the domain, and a fresh request-magic-link for another@practice.example is 202 with no link minted.
  12. grep -n "acting_email" migrations/versions/20260905_0001_desk_domain_login.py | wc -l prints 10 (four adds, the two-line backfill, four drops); python -m alembic heads from backend/ prints one head.

Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.