diff --git a/crates/libs/db/migrator/src/bootstrap_roles.rs b/crates/libs/db/migrator/src/bootstrap_roles.rs index b781384f66..03efde0af7 100644 --- a/crates/libs/db/migrator/src/bootstrap_roles.rs +++ b/crates/libs/db/migrator/src/bootstrap_roles.rs @@ -110,6 +110,7 @@ const RO_COLUMN_DENYLIST: &[(&str, &[&str])] = &[ ("sessions", &["token_hash"]), ("api_keys", &["key_hash"]), ("data_plane_sessions", &["token_hash"]), + ("data_plane_session_tokens", &["token_hash"]), ("sandbox_federated_service_grants", &["grant_blob"]), ("template_system_publication_grants", &["id"]), ("template_tenant_publication_grants", &["id"]), diff --git a/db/postgres/20261021182104_data_plane_session_tokens.down.sql b/db/postgres/20261021182104_data_plane_session_tokens.down.sql new file mode 100644 index 0000000000..4a8a4c208e --- /dev/null +++ b/db/postgres/20261021182104_data_plane_session_tokens.down.sql @@ -0,0 +1,8 @@ +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '60s'; + +-- Reverting drops every token a replay minted beyond the session's latest, +-- which `data_plane_sessions.token_hash` still holds. Roll the API and +-- internal-api releases back with this migration: a release that writes the +-- table fails its session creates and replays once the table is gone. +DROP TABLE IF EXISTS data_plane_session_tokens; diff --git a/db/postgres/20261021182104_data_plane_session_tokens.up.sql b/db/postgres/20261021182104_data_plane_session_tokens.up.sql new file mode 100644 index 0000000000..cbf3d81297 --- /dev/null +++ b/db/postgres/20261021182104_data_plane_session_tokens.up.sql @@ -0,0 +1,23 @@ +SET LOCAL lock_timeout = '5s'; +SET LOCAL statement_timeout = '60s'; + +-- Live attach tokens of a data-plane session, one row per token. +-- +-- Replaying a session's create with the same idempotency key mints another +-- token for the same session. Each token stays valid until its own +-- `expires_at`, so a replay no longer invalidates the tokens earlier replays +-- returned. `data_plane_sessions.token_hash` and `token_expires_at` keep +-- naming the most recent token while servers that read only those columns are +-- still deployed; validators consult this table first and fall back to them. +CREATE TABLE IF NOT EXISTS data_plane_session_tokens ( + -- SHA-256 hex digest of the bearer token; the raw token is never stored. + token_hash text PRIMARY KEY, + session_id text NOT NULL REFERENCES data_plane_sessions (session_id) ON DELETE CASCADE, + expires_at timestamptz NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); + +-- Replays prune and cap a session's tokens by expiry, and the session +-- foreign key's cascade finds its rows through the leading column. +CREATE INDEX IF NOT EXISTS idx_data_plane_session_tokens_session_id_expires_at + ON data_plane_session_tokens (session_id, expires_at);