From d6ec85c2d9e23eabf89516b9465233bc35d83cf9 Mon Sep 17 00:00:00 2001 From: man4ish Date: Sun, 4 Oct 2026 10:37:15 -0500 Subject: [PATCH 1/2] fix: widen api_keys.key_prefix from 12 to 20 chars M13 (live/test key prefixes) changed the stored prefix to len("omni_sk_live_"/"omni_sk_test_") + 4 display chars = 17 characters, but never widened this column from its pre-M13 size of 12 (sized for the plain "omni_sk_XXXX" scheme). Impact: every real key creation against a real MySQL database has failed with "Data too long for column 'key_prefix'" since M13 shipped. SQLite enforces no VARCHAR length limit at all, so the existing (SQLite-backed) test suite never caught this -- found while testing a real account's "Create key" flow against a live MySQL-backed stack. Co-Authored-By: Claude Sonnet 5 --- alembic/versions/0029_widen_api_key_prefix.py | 37 +++++++++++++++++++ app/db/models.py | 9 ++++- 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/0029_widen_api_key_prefix.py diff --git a/alembic/versions/0029_widen_api_key_prefix.py b/alembic/versions/0029_widen_api_key_prefix.py new file mode 100644 index 0000000..a103f6d --- /dev/null +++ b/alembic/versions/0029_widen_api_key_prefix.py @@ -0,0 +1,37 @@ +"""Widen api_keys.key_prefix from 12 to 20 characters. + +M13 (live/test key prefixes) changed the stored prefix to +prefix_len("omni_sk_live_"/"omni_sk_test_") + 4 display chars = 17 +characters, but never widened this column from its pre-M13 size of 12 +(sized for the plain "omni_sk_XXXX" scheme). Every real key creation +against a real MySQL database has failed with "Data too long for +column 'key_prefix'" since M13 shipped -- SQLite enforces no VARCHAR +length limit at all, so the existing (SQLite-backed) test suite never +caught this. + +Revision ID: 0029_widen_api_key_prefix +Revises: 0028_auth_audit_integrity +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + +revision: str = "0029_widen_api_key_prefix" +down_revision: Union[str, None] = "0028_auth_audit_integrity" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + with op.batch_alter_table("api_keys") as batch_op: + batch_op.alter_column( + "key_prefix", existing_type=sa.String(length=12), type_=sa.String(length=20), existing_nullable=True, + ) + + +def downgrade() -> None: + with op.batch_alter_table("api_keys") as batch_op: + batch_op.alter_column( + "key_prefix", existing_type=sa.String(length=20), type_=sa.String(length=12), existing_nullable=True, + ) diff --git a/app/db/models.py b/app/db/models.py index 34d71db..7013f28 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -403,7 +403,14 @@ class ApiKey(Base): organization_id = Column(Integer, ForeignKey("organizations.id"), nullable=False) created_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=False) name = Column(String(255), nullable=True) - key_prefix = Column(String(12), nullable=True) + # M13 (live/test key prefixes) widened the stored prefix to + # len("omni_sk_live_"/"omni_sk_test_") + 4 display chars = 17, but + # this column stayed at its pre-M13 width (12, sized for the plain + # "omni_sk_XXXX" scheme) -- every real key creation against a real + # MySQL database has 500'd since M13 shipped (SQLite enforces no + # VARCHAR length at all, so no test caught it). See + # alembic/versions/0029_widen_api_key_prefix.py. + key_prefix = Column(String(20), nullable=True) key_hash = Column(String(64), unique=True, nullable=True) scopes = Column(JSON, nullable=True) status = Column(String(20), default="active") # active | revoked From 01d8b4e48d6d75dc43223f6e9ecb4a3b631ffb71 Mon Sep 17 00:00:00 2001 From: man4ish Date: Sun, 4 Oct 2026 10:55:27 -0500 Subject: [PATCH 2/2] fix: update hardcoded migration-head assertions for 0029 Two tests pinned the full-upgrade alembic_version to the previous head (0028_auth_audit_integrity); update both to the new head this branch adds (0029_widen_api_key_prefix). Co-Authored-By: Claude Sonnet 5 --- tests/test_migrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 622e3b2..fb2e500 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -476,7 +476,7 @@ def test_sqlite_stamp_then_upgrade_matches_real_deployment_procedure(sqlite_db_u with engine.connect() as conn: recorded = conn.execute(text("SELECT version_num FROM alembic_version")).scalar() - assert recorded == "0028_auth_audit_integrity" + assert recorded == "0029_widen_api_key_prefix" def test_sqlite_0019_is_purely_additive_existing_session_rows_survive(sqlite_db_url): @@ -1120,7 +1120,7 @@ def test_mysql_pre_existing_role_rows_survive_0016_as_platform_wide(mysql_db_url with engine.connect() as conn: recorded = conn.execute(text("SELECT version_num FROM alembic_version")).scalar() - assert recorded == "0028_auth_audit_integrity" + assert recorded == "0029_widen_api_key_prefix" def test_mysql_0020_pre_existing_team_membership_row_backfills_member_role(mysql_db_url):