Repository navigation
Host key rotation writes a JWK thumbprint into UUID primary keys with generateId: false #42
Description
Summary
With PostgreSQL UUID primary keys and advanced.database.generateId: false, using the public SDK's explicit UUID issuer bypasses the host lookup failure in #37. However, host key rotation still fails: the server replaces the host's primary key with the new JWK thumbprint.
Related: #37 and #41. This report focuses on the remaining rotation failure, not a duplicate request to reorder the host lookup.
Environment
@better-auth/agent-auth@0.6.2@auth/agent@0.6.2better-auth@1.7.2,@better-auth/drizzle-adapter@1.7.2drizzle-orm@0.45.2,pg@8.23.0, PostgreSQL 18, Node 24- Drizzle
agentHost.id: PostgreSQLuuid, primary key, defaultgen_random_uuid();kid: text. Referencing host IDs are UUIDs too. advanced: { database: { generateId: false } }- Delegated mode, device authorization, dynamic host registration, Ed25519, zero default host capabilities.
Reproduction
Against an auth instance using the above UUID schema:
- Generate the host and agent keypairs with
generateKeypair(). - Sign registration with
signHostJWT({ hostKeypair, agentPublicKey, issuer: crypto.randomUUID(), audience }). Do not change either JWK'skid. - POST to
/agent/register. This succeeds and returns a different, database-generated UUIDhost_id. - Retain that returned
host_id; use it asissuerfor subsequent host JWTs. Device approval and host-authenticated status polling succeed. - Approve the delegated agent through the native user-session approval endpoint.
- Generate a replacement host keypair. Sign with the current key and the returned UUID
host_id:
const proof = await signHostJWT({
hostKeypair: currentKeypair,
issuer: registration.host_id,
audience,
});
const replacement = await generateKeypair();
// POST /host/rotate-key, Authorization: Bearer <proof>
// JSON body: { public_key: replacement.publicKey }Actual
The rotation handler assigns newHostId = newThumbprint and updates the host with id: newHostId. PostgreSQL rejects the fingerprint as a UUID with 22P02 / string_to_uuid; the HTTP endpoint returns 500. This is a write failure, independent of the lookup ordering. generateId: false does not help because this path explicitly supplies a new ID.
Our disposable-Postgres integration test reproduces the native HTTP 500 and the direct API error cause. The previous host key and UUID remain unchanged after this failed statement and still authenticate. No package patches or synthetic host records were used to establish the host.
Expected / design question
Can host key rotation preserve a database-generated host ID and update the public key / kid independently? If thumbprint-based primary keys are a protocol requirement, please document that typed UUID IDs are unsupported even with generateId: false, including the migration implications for existing host references.
The explicit issuer option is useful, but the high-level client still signs registration, polling and lifecycle requests using the default thumbprint, so an application-side workaround would need more than a one-time registration override. Ideally the supported client/server flow works with database-generated UUIDs without duplicating the high-level client's lifecycle implementation.
Verified scope
The UUID-issuer experiment passed registration, pending/approved polling, restoring keys and server IDs into a fresh auth instance, repeated registration under the same host, wrong-key and cross-host denials, agent/host revocation, and expired-agent reactivation with zero restored grants. Rotation remains blocked. These are local protocol/database tests, not a claim of complete production or distributed-replay acceptance.
Activity