How EndpointTrust is put together

The shape


                        ┌──────────────────────────┐
  Windows endpoints ───▶│  apps/device-api  :8080  │
  (credential provider) │  5 contract ops + 2      │
                        │  pairing ops             │
                        └────────────┬─────────────┘
                                     │
                      ┌──────────────┴──────────────┐
                      │  packages/core              │
                      │  the domain: decisions,      │
                      │  seats, verification,        │
                      │  licensing, signing, SMS,    │
                      │  billing, analytics          │
                      └──────────────┬──────────────┘
                                     │
                      ┌──────────────┴──────────────┐
                      │  packages/db   PostgreSQL 18 │
                      │  schema + committed          │
                      │  migrations                  │
                      └──────────────┬──────────────┘
                                     │
  Administrators ─────▶┌─────────────┴────────────┐
  Stripe webhooks ────▶│  apps/dashboard   :3000  │
                       └──────────────────────────┘

packages/contract holds the wire schemas, shared by both apps, mirroring the endpoint

repository's api/openapi-mvp.json. Where they disagree, **the endpoint repository is

normative and this repository has the defect.**

Why invariants live in the database

The interesting constraints here are not "this field is a string". They are "at most one of

these can exist", under concurrency, while several machines are signing people in at the

same moment. Those are expressed as partial unique indexes, because an index is a guarantee

and a code path is an intention:

IndexWhat it makes true
phone_numbers_one_active_per_user WHERE retired_at is nullExactly one live destination per person. This is what makes "EndpointTrust selects the registered destination" a fact rather than a policy: there is only one to select.
end_users_tenant_account_key on (tenant, lower(domain), lower(sam))One record per account. Windows compares case-insensitively; without lower() this table would hold CORP\alice twice.
end_users_tenant_sid_key WHERE user_sid is not nullA SID identifies exactly one person in a tenant.
devices_tenant_device_keyOne registration per installation GUID. A second activation is a controlled re-enrollment, not a second row — which is what stops one machine quietly consuming two seats.
associations_user_device_keyA seat is one (user, device) pair.
challenges_one_pending_per_attempt WHERE state = 'pending'One live code per attempt.
attempts_device_nonce_keyA nonce cannot be reused on a device.
licenses_one_active_per_tenant WHERE status = 'active'One licence in force.
signing_keys_one_active_per_tenant WHERE retired_at is nullOne signing key in force.
subscriptions_one_live_per_tenant WHERE status is non-terminalOne subscription. A cancelled one may coexist with its replacement.
pairings_one_live_per_deviceOne pairing in flight per machine.
sms_providers_one_default_per_tenant WHERE is_defaultCodes go out through exactly one provider.
audit_events.id = the endpoint's own event_idAn at-least-once uploader becomes exactly-once storage, with no deduplication logic anywhere.

Seat allocation

packages/core/src/seats.ts. The invariant is a COUNT over rows — "at most N allocated

associations for this tenant" — so it uses a SERIALIZABLE transaction with a retry on

40001/40P01, not an advisory lock and not a counter column.

A counter column would need its own locking to stay consistent with the rows. A lock held

across the whole allocation would serialise every tenant's sign-ins behind each other.

Serializable isolation lets PostgreSQL detect the conflict and retry only the loser.

tests/seats.test.ts runs real concurrent allocations against a real server to prove it.

Single-use codes

packages/core/src/verification.ts. The primitive is a conditional UPDATE:


update challenges set state = 'verified' where id = $1 and state = 'pending'

Exactly one concurrent caller gets a row back. That caller verified the code; everyone else

did not. The attempt counter is incremented before the comparison, under a precondition,

so a crash between the two cannot hand out a free guess.

Signing

ES256, one key per tenant. Per tenant rather than one global root so that a compromise

affects one customer and a rotation does not have to be coordinated across every deployment

at once. A tenant's bundle carries only that tenant's public key, which is also what makes

a licence issued for one organisation structurally unusable at another.

Signatures are JOSE form: R‖S, 64 bytes, not ASN.1 DER. In Node that is

dsaEncoding: "ieee-p1363", and packages/core/src/crypto.ts asserts the length because

getting this wrong produces signatures that verify in Node and fail on Windows.

Idempotency

Two patterns, because the operations differ:

returns the recorded result.

reserveIdempotent → act → finaliseIdempotent, with releaseIdempotent on failure. The

reservation happens before acting, because an SMS cannot be un-sent and a credential

cannot be un-minted. A concurrent duplicate is refused before it acts, not after.

Keys are bound to caller, route and body. Reusing a key with a different body is a 409

idempotency_conflict, not a silent replay of the wrong answer.

A retry of verify with the same key returns the original result, including the same

grant, even after the code is consumed. That is one verification, not two — an endpoint

whose response was lost to a dropped connection must not be punished for retrying.

Egress

A tenant may point the http SMS provider at a URL they control, which makes this service

able to be asked to make a request to an address of somebody's choosing.

packages/core/src/sms/guards.ts resolves the host first, checks the resolved address

against loopback, link-local, 169.254.169.254 and private ranges, and then connects to

that address — so a DNS record that changes between the check and the connection does not

help. The check runs when the URL is saved as well as when a message is sent.

What the endpoint cannot observe, and what this service therefore cannot report

The contract fixes two endpoint capabilities to false, and this service checks rather

than trusts them:

authentication exchange.

monitor.

The second one shapes the whole analytics surface. There is no logoff event, no lock event,

and no reliable "signed in right now" signal. A machine that loses power between submitting

a credential and hearing back from Windows reports windows_outcome: "unknown" and stays

that way forever.

So the analytics report attempts and their outcomes, verifications this service performed

itself, machines last seen, and bypasses used — and they do not report who is logged in,

session duration, or logoffs. Nothing in this repository turns an unknown into a success.

See the long comment at the top of packages/core/src/analytics.ts.

What the analytics can and cannot say

/app/analytics reports everything a credential provider observes: sign-ins and unlocks,

split by channel, Windows product, agent version and hour; who authenticated, from which

address, on which machine, and what the machine said afterwards; how many people are

registered, licensed, newly added, active and dormant.

The page ends with a card listing what it does not report and why, because a dashboard

that silently omits a metric invites the reader to assume it is zero:

Not measuredWhy
Sign-outsA credential provider is invoked when a credential is presented, not when a session ends. supports_complete_session_audit is fixed false for this reason.
LocksLocking presents no credential. The unlock afterwards does, and is counted.
Who is signed in nowFollows from the first two: without session ends the list would only ever grow.
BitLocker state, hardware typeDevice posture was deliberately left out of the agent's scope. Reporting it would mean putting posture back in.

Source addresses are recorded on the ATTEMPT, not on the audit event — an audit batch is

uploaded later and possibly from a different network, so its source address is not where

the sign-in happened. X-Forwarded-For is honoured only when TRUST_PROXY is set, because

a header is whatever the client wrote in it; otherwise the socket's peer address is used.

Either way it is the address this service saw, which behind NAT is a site rather than a

workstation, and the page says so.

Time windows carry a five-minute forward margin. occurred_at comes from the

endpoint's clock, not this server's, so a machine running a minute fast would otherwise

vanish from "the last 30 days".

Paging and date ranges

Every grid in the console is paged server-side, with the page number in the query string,

so a page of a table is a URL that can be bookmarked, pasted into a ticket, or reloaded

after a form post. Offset paging rather than a cursor: these are administrative tables of a

few thousand rows at most, bounded per tenant, and a cursor would cost a stable sort key on

every query for a benefit nobody here would notice.

Two details that are easy to get wrong and are tested:

date range would show the reader a different set than they think they are looking at.

(page, src_page, ev_page…), so turning one does not reset the others.

The pager is rendered even for a single page, because "1–8 of 8" tells a reader the table

is complete; a table with no footer does not.

Date ranges are presets plus a from/to form, both plain HTML — the presets are anchors

and the custom range is a GET form, so a chosen range is a URL and the CSV export link

carries it. Every range is validated server-side: a backwards range is swapped rather than

returning nothing, a span over two years is clamped, a malformed date falls back to the

default, and the page says when it corrected the request. A query string is user input, and

an unbounded one is an unindexed full-table scan.

The console's UI

React, rendered on the server with renderToStaticMarkup, and no client bundle at all.

Components and composition where they help; zero hydration, zero JavaScript shipped.

That combination is what lets the content security policy be default-src 'none' with no

script-src and style-src 'self' with no 'unsafe-inline' — the strictest policy a page

can have. It also means every page works with script disabled, and the whole UI is testable

by asking the app for a URL and reading the response.

The consequences are deliberate and worth knowing before changing anything:

stores nothing and lets prefers-color-scheme decide; the other two stamp data-theme

on <html>, which the server renders. The right theme is therefore in the first byte of

HTML and there is no flash of the wrong colours. The toggle is a form POST.

SVG; everything else is a utility class.

contents, which is what makes immutable caching correct. A stale hash redirects to the

current one rather than 404ing a page cached across a deploy.

Tests

205 tests across ten suites, all against a real PostgreSQL 18 server

(embedded-postgres, no Docker and no root), with fileParallelism: false so each suite

owns its database.

SuiteWhat it covers
tests/schema.test.tsThe indexes above, as behaviour
tests/seats.test.tsConcurrent allocation under serializable isolation
tests/verification.test.tsSingle-use codes, counters, grants
tests/pairing.test.tsOne test per bound in pairing-security.md
tests/device-api.test.tsThe device API over HTTP, end to end, including a complete sign-in
tests/dashboard.test.tsThe console over HTTP: sessions, CSRF, roles, escaping, uploads, the bundle
tests/billing.test.tsEntitlement via synthetic Stripe events
tests/releases.test.tsWhat a release offers, refuses, and tells a machine
tests/analytics.test.tsThat the dashboard's counts are the real counts, and that the breakdowns split the way they claim
packages/core/src/crypto.test.tsES256 against OpenSSL

The one thing stubbed anywhere is the socket to the SMS provider — and in

tests/device-api.test.ts that stub is what makes a full sign-in testable, because the

code is not recoverable from the database by design, so a test has to read it off the wire

exactly like a handset would.