"""git_sources.token: private-repo credential column (phase 121) Revision ID: 0021 Revises: 0020 Create Date: 2026-09-24 Phase 121 (private git sources: a token that never reaches the UI or the API — task 01, storage only): * ``git_sources.token`` — TEXT NULLABLE, no server default: the private repo credential (LOCKED A2) the owner types into the masked Sources-page field. NULL = public repo (or a legacy row whose credential is still embedded in ``url`` — those rows keep their stored value, which is what authenticates the clone, and are sanitized on OUTPUT only, task 02). Stored plaintext BY NECESSITY: the repo must remain cloneable, so the raw credential must be recoverable at sync time; the Postgres DB is the trusted store and is never served to the UI. The column is injected into the clone URL ONLY at clone time (task 02's ``clone_url_for``) and is NEVER returned by any API shape (the output models gain no token field — the omission is a documented contract). One additive, fully reversible migration (A13); no other schema change. Normalization of embedded-token URLs on write and output sanitization are code (tasks 02/03) — this revision only carries the column. """ from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "0021" down_revision = "0020" branch_labels = None depends_on = None def upgrade() -> None: op.add_column("git_sources", sa.Column("token", sa.Text(), nullable=True)) def downgrade() -> None: # The token column is the only 0021 artefact — dropping it leaves # 0020's schema byte-identical (A13, fully reversible). op.drop_column("git_sources", "token")