Context
0353 decision 4 says one directory routes sign-in to the right region: keyed hashes of an email or phone mapped to the regions that hold an account, owned by identity, served by an edge Worker over globally replicated KV and written by each region’s identity service through events. It named no package. The writer and the reader must hash the same way, and neither@stackra/identity nor @stackra/infra-cloudflare may depend on the other: identity is provider-neutral, and the
Cloudflare package carries Pulumi.
Decision
- Identity owns the format.
@stackra/identity/contractsexportsloginDirectoryPrefix(identifier, key), the only code that spells a directory key: the identifier is normalised (an email trimmed, NFKC and lower-cased; a phone stripped to E.164), hashed with HMAC-SHA-256 under the directory key over<kind>:<normal form>, and returned asdir:<key id>:<kind>:<hex>:. It runs on Web Crypto, so the same code runs in a region and in the Worker. An identifier that does not normalise has no row. - One row per identifier and region. A region writes
dir:<key id>:<kind>:<hex>:<region>(value1). Each region writes and deletes only keys ending in its own region key, so no two regions write the same key, no write reads first, and a delete is a plain delete. The Worker lists the prefix to learn every region. - Identity writes.
LoginDirectoryWriterService(@stackra/identity) records and forgets a region’s rows over theLoginDirectoryStoreport, configured byLoginDirectoryConfig(region,keyId, and the hash key from theIDENTITY_LOGIN_DIRECTORY_KEYsecret). The host’s listeners on its own identity events call it through the outbox: an account created or an identifier verified isrecord; a changed email isforgetof the old andrecordof the new; a deleted account isforgetof all. Both calls are idempotent, so a redelivered event is harmless. Stackra declares no directory event; the event names are the host’s. - Cloudflare carries it.
@stackra/infra-cloudflareholds the Cloudflare side:loginDirectoryHandler(the Worker’s fetch handler:POST {kind, value}answers{regions}in the deployment’s order,no-store, the identifier never echoed or logged),loginDirectoryKvRequest(the KV bulk write and bulk delete a region’s store driver sends with its own KV-scoped token) andloginDirectoryWorker(the Pulumi KV namespace and routes). The host wires the two: its Worker passesloginDirectoryPrefixwith the Worker’s key into the handler. - A Worker of its own. The directory is a separate Worker and KV namespace from the OS edge Worker of ADR 0437, on
its own route (for example
login.<product-domain>/directory, same origin as the login page, so no CORS). Its namespace has no jurisdiction: it is replicated everywhere so sign-in survives the loss of any region, which is safe because it holds hashes and region keys only. Rate limits and bot checks on the route are the zone edge’s. - Configuration, not code. The region list, the key id and the routes are deployment config; the hash key is a secret held by the Worker and by each region, never in KV, a log or a config value. A key rotation writes rows under a new key id beside the old ones, the Worker switches its key id, and the old rows are then deleted.
- Hostnames and handles. A hostname’s region is the edge KV
host:row of ADR 0437; the directory does not copy it. Tenant handles reserved across regions (OS-30) are tenancy’s and are not part of this decision.
Consequences
- The directory holds no readable personal data: a leaked namespace yields hashes that cannot be reversed without the key.
- A person with accounts in two regions gets both regions back and picks one (ADR 0353 decision 4, OS-24).
- The directory answers whether an identifier has an account somewhere; the route’s rate limits are the defence against enumeration, as they are for the login page itself.
- A region’s rows lag its identity events by the outbox delay plus KV propagation (about a minute).
- The OS adds a Workers member for the directory and binds a
LoginDirectoryStoredriver that sendsloginDirectoryKvRequest.
Enforcement
- planned —
loginDirectoryPrefixtests pin the key shape and an HMAC known answer, and that no clear identifier is in a key (@stackra/identity). - planned —
loginDirectoryKvRequestrefuses any key that is not adir:key (@stackra/infra-cloudflare). - planned — only the regions’ KV-scoped tokens can write the directory namespace; the Worker binding reads only.
Supersedes and sources
Supersedes: none. Sources: Linear STA-752 and STA-1023 (batch stackra-57); 0353 decision 4; Figentra OS ADR 0002 decision 1; ADR 0437;notes/handover/stackra-package-needs.md (login directory edge
Worker row).