Commit Graph
8 Commits
Author SHA1 Message Date
Evan JarrettandClaude Opus 5 13edb7184d appview: stop writing last_seen and last_used on every event
Neither is a correctness problem; both are round trips on hot paths for
timestamps nothing reads at that resolution.

UpdateUserLastSeen ran per Jetstream event for cached users, so once per indexed
record. DeviceStore.UpdateLastUsed ran per /auth/token call, so once per docker
push and pull including each layer's re-auth. Cheap against a local file, a
network round trip each against a remote primary, and the second sat on the
authentication path.

Both are now throttled to once per five minutes per subject. The MAU queries and
the admin views work in hours or days, so nothing loses meaning. The throttle
state is per-process and lost on restart, costing at most one extra write per
subject per boot; only the lease holder runs the consumer, so exactly one process
is doing the first of these at a time.

UpdateLastUsed stamps the throttle before writing rather than after, so a slow or
failing write cannot let every concurrent layer upload through to pile on more of
them.

Verified by disabling the throttle: 50 back-to-back calls then rewrite the
timestamp every time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 09:14:45 -05:00
Evan JarrettandClaude Opus 5 08121f3cd0 appview: fix O(n) bcrypt scan making /auth/token take 15s+
ValidateDeviceSecret ran bcrypt.CompareHashAndPassword against every row in
the devices table until one matched — no WHERE clause. At bcrypt cost 10
(~65ms on the single-core production host) and 244 registered devices, a
device near the end of the scan cost ~15.8s of pure CPU per /auth/token,
which is past Docker's client deadline. Measured on production: 15.7-16.0s
steady state with the appview pinned at 100% CPU for the duration, while
anonymous requests on the same box served in 20ms.

The cost grew linearly with every device registered, and the scan ran in
rowid order, so the newest devices — the ones most likely to be in active
use — paid the most. This is the timeout users were reporting.

Devices now carry secret_lookup = hex(sha256(secret)), indexed, and
authentication fetches the single matching row.

SHA-256 is the verifier here, not merely an index. Device secrets are 32
bytes from crypto/rand, so presenting a value that hashes to a stored digest
requires a preimage or a 2^256 search. bcrypt's work factor only helps when
the input space is small enough to enumerate, which does not apply to a
random 256-bit token, and a database leak exposes no more than before.

The plaintext is not recoverable from a bcrypt hash, so existing rows cannot
be backfilled directly. They are migrated lazily on their next successful
authentication, which any push, pull or login triggers, and the legacy scan
is filtered to un-migrated rows so its cost decays as devices migrate. The
backfill runs after the cursor is closed: issuing it inside the rows loop
deadlocks, because the open cursor holds the connection the write needs.

bcrypt now exists solely to carry legacy rows across and can be deleted once
the table is fully migrated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-08 23:01:07 -05:00
Evan Jarrett efef46b15a various linting fixes 2026-01-04 22:02:01 -06:00
Evan Jarrett b0799cd94d unit tests 2025-10-28 17:40:11 -05:00
Evan Jarrett c79d0ac3ab more slogggggggg 2025-10-25 13:38:39 -05:00
Evan Jarrett 751fa1a3f0 post to bluesky when manifests uploaded. linting fixes 2025-10-23 12:24:04 -05:00
Evan Jarrett f5e6e6954f fix credential helper to read insecure-registries. fix device registration flow 2025-10-10 19:55:15 -05:00
Evan Jarrett 08d5fce21f move file store cache to sqlite. implement repository page 2025-10-07 21:51:53 -05:00