From e68b52ef5a83552696ff438444c582c716257dd3 Mon Sep 17 00:00:00 2001 From: Nicole Rappe Date: Wed, 29 Oct 2025 19:33:28 -0600 Subject: [PATCH] ENGINE: Adjusted Persistent Assets --- Borealis.ps1 | 39 +++++- Data/Engine/Unit_Tests/conftest.py | 15 +++ Data/Engine/Unit_Tests/test_enrollment_api.py | 120 +++++++++++++++++- Data/Engine/auth/jwt_service.py | 4 +- Data/Engine/config.py | 4 +- Data/Engine/database.py | 53 ++++++++ Data/Engine/services/API/devices/approval.py | 48 ++++++- Data/Engine/services/API/enrollment/routes.py | 26 ++++ Data/Server/Modules/admin/routes.py | 48 ++++++- Data/Server/Modules/db_migrations.py | 87 +++++++++++++ Data/Server/Modules/enrollment/routes.py | 26 ++++ Data/Server/Modules/jobs/prune.py | 38 +++++- 12 files changed, 496 insertions(+), 12 deletions(-) diff --git a/Borealis.ps1 b/Borealis.ps1 index 6e84eb12..b4df5d6f 100644 --- a/Borealis.ps1 +++ b/Borealis.ps1 @@ -1432,15 +1432,46 @@ switch ($choice) { New-Item -Path $engineDataRoot -ItemType Directory -Force | Out-Null } - if (Test-Path (Join-Path $scriptDir $engineDataDestination)) { - Remove-Item (Join-Path $scriptDir $engineDataDestination) -Recurse -Force -ErrorAction SilentlyContinue + $engineDataAbsolute = Join-Path $scriptDir $engineDataDestination + + $runtimeAssemblies = Join-Path $scriptDir 'Engine\Assemblies' + $sourceAssemblies = Join-Path $engineSourceAbsolute 'Assemblies' + + $runtimeDatabase = Join-Path $scriptDir 'Engine\database.db' + + $runtimeAuthTokens = Join-Path $scriptDir 'Engine\Auth_Tokens' + + if (Test-Path $engineDataAbsolute) { + Remove-Item $engineDataAbsolute -Recurse -Force -ErrorAction SilentlyContinue } - New-Item -Path (Join-Path $scriptDir $engineDataDestination) -ItemType Directory -Force | Out-Null + New-Item -Path $engineDataAbsolute -ItemType Directory -Force | Out-Null if (-not (Test-Path $engineSourceAbsolute)) { throw "Engine source directory '$engineSourceAbsolute' not found." } - Copy-Item (Join-Path $engineSourceAbsolute '*') (Join-Path $scriptDir $engineDataDestination) -Recurse -Force + Get-ChildItem -Path $engineSourceAbsolute -Force | ForEach-Object { + if ($_.Name -ieq 'Assemblies') { + return + } + Copy-Item -Path $_.FullName -Destination $engineDataAbsolute -Recurse -Force + } + + if (-not (Test-Path $runtimeAssemblies) -and (Test-Path $sourceAssemblies)) { + Copy-Item -Path $sourceAssemblies -Destination $runtimeAssemblies -Recurse -Force + } elseif (-not (Test-Path $runtimeAssemblies)) { + New-Item -Path $runtimeAssemblies -ItemType Directory -Force | Out-Null + } + + if (-not (Test-Path $runtimeAuthTokens)) { + New-Item -Path $runtimeAuthTokens -ItemType Directory -Force | Out-Null + } + + if (-not (Test-Path $runtimeDatabase)) { + $runtimeDatabaseDir = Split-Path -Path $runtimeDatabase -Parent + if (-not (Test-Path $runtimeDatabaseDir)) { + New-Item -Path $runtimeDatabaseDir -ItemType Directory -Force | Out-Null + } + } . (Join-Path $venvFolder 'Scripts\Activate') } diff --git a/Data/Engine/Unit_Tests/conftest.py b/Data/Engine/Unit_Tests/conftest.py index cee3a2f5..2e0e0939 100644 --- a/Data/Engine/Unit_Tests/conftest.py +++ b/Data/Engine/Unit_Tests/conftest.py @@ -69,6 +69,21 @@ CREATE TABLE IF NOT EXISTS enrollment_install_codes ( use_count INTEGER, last_used_at TEXT ); +CREATE TABLE IF NOT EXISTS enrollment_install_codes_persistent ( + id TEXT PRIMARY KEY, + code TEXT UNIQUE, + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL, + created_by_user_id TEXT, + used_at TEXT, + used_by_guid TEXT, + max_uses INTEGER NOT NULL DEFAULT 1, + last_known_use_count INTEGER NOT NULL DEFAULT 0, + last_used_at TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + archived_at TEXT, + consumed_at TEXT +); CREATE TABLE IF NOT EXISTS device_approvals ( id TEXT PRIMARY KEY, approval_reference TEXT UNIQUE, diff --git a/Data/Engine/Unit_Tests/test_enrollment_api.py b/Data/Engine/Unit_Tests/test_enrollment_api.py index b59fa7c2..8b8371f0 100644 --- a/Data/Engine/Unit_Tests/test_enrollment_api.py +++ b/Data/Engine/Unit_Tests/test_enrollment_api.py @@ -18,6 +18,7 @@ from cryptography.hazmat.primitives.asymmetric import ed25519 from flask.testing import FlaskClient from Data.Engine.crypto import keys as crypto_keys +from Data.Engine.database import initialise_engine_database from .conftest import EngineTestHarness @@ -32,7 +33,9 @@ def _iso(dt: datetime) -> str: def _seed_install_code(db_path: os.PathLike[str], code: str) -> str: record_id = str(uuid.uuid4()) - expires_at = _iso(_now() + timedelta(days=1)) + baseline = _now() + issued_at = _iso(baseline) + expires_at = _iso(baseline + timedelta(days=1)) with sqlite3.connect(str(db_path)) as conn: conn.execute( """ @@ -42,6 +45,40 @@ def _seed_install_code(db_path: os.PathLike[str], code: str) -> str: """, (record_id, code, expires_at, None, None, 1, 0, None), ) + conn.execute( + """ + INSERT INTO enrollment_install_codes_persistent ( + id, + code, + created_at, + expires_at, + created_by_user_id, + used_at, + used_by_guid, + max_uses, + last_known_use_count, + last_used_at, + is_active, + archived_at, + consumed_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + record_id, + code, + issued_at, + expires_at, + "test-suite", + None, + None, + 1, + 0, + None, + 1, + None, + None, + ), + ) conn.commit() return record_id @@ -193,6 +230,15 @@ def test_enrollment_poll_finalizes_when_approved(engine_harness: EngineTestHarne (final_guid,), ) key_count = cur.fetchone()[0] + cur.execute( + """ + SELECT is_active, last_known_use_count, used_by_guid, consumed_at + FROM enrollment_install_codes_persistent + WHERE id = ? + """, + (install_code_id,), + ) + persistent_row = cur.fetchone() assert approval_row is not None approval_guid, approval_status = approval_row @@ -211,3 +257,75 @@ def test_enrollment_poll_finalizes_when_approved(engine_harness: EngineTestHarne assert use_count == 1 assert used_by_guid == final_guid assert key_count == 1 + assert persistent_row is not None + is_active, last_known_use_count, persistent_guid, consumed_at = persistent_row + assert is_active == 0 + assert last_known_use_count == 1 + assert persistent_guid == final_guid + assert consumed_at is not None + + +def test_persistent_enrollment_codes_restore_active_table(engine_harness: EngineTestHarness) -> None: + harness = engine_harness + code_id = str(uuid.uuid4()) + baseline = _now() + issued_iso = _iso(baseline) + expires_iso = _iso(baseline + timedelta(hours=4)) + + with sqlite3.connect(str(harness.db_path)) as conn: + conn.execute("DELETE FROM enrollment_install_codes") + conn.execute( + """ + INSERT OR REPLACE INTO enrollment_install_codes_persistent ( + id, + code, + created_at, + expires_at, + created_by_user_id, + used_at, + used_by_guid, + max_uses, + last_known_use_count, + last_used_at, + is_active, + archived_at, + consumed_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + code_id, + "RESTORE-CODE-001", + issued_iso, + expires_iso, + "restorer", + None, + None, + 3, + 0, + None, + 1, + None, + None, + ), + ) + conn.commit() + + initialise_engine_database(str(harness.db_path)) + + with sqlite3.connect(str(harness.db_path)) as conn: + cur = conn.execute( + """ + SELECT code, expires_at, max_uses, use_count + FROM enrollment_install_codes + WHERE id = ? + """, + (code_id,), + ) + row = cur.fetchone() + + assert row is not None + restored_code, restored_expires, restored_max_uses, restored_use_count = row + assert restored_code == "RESTORE-CODE-001" + assert restored_expires == expires_iso + assert restored_max_uses == 3 + assert restored_use_count == 0 diff --git a/Data/Engine/auth/jwt_service.py b/Data/Engine/auth/jwt_service.py index eae36cc0..8e96e596 100644 --- a/Data/Engine/auth/jwt_service.py +++ b/Data/Engine/auth/jwt_service.py @@ -1,6 +1,6 @@ # ====================================================== # Data\Engine\auth\jwt_service.py -# Description: Engine-native JWT access-token helpers with signing key storage under Engine/Data/Auth_Tokens. +# Description: Engine-native JWT access-token helpers with signing key storage under Engine/Auth_Tokens. # # API Endpoints (if applicable): None # ====================================================== @@ -53,7 +53,7 @@ def _token_root() -> Path: if env: env.mkdir(parents=True, exist_ok=True) return env - root = _engine_runtime_root() / "Data" / "Auth_Tokens" + root = _engine_runtime_root() / "Auth_Tokens" root.mkdir(parents=True, exist_ok=True) return root diff --git a/Data/Engine/config.py b/Data/Engine/config.py index 4b3456d0..ea121ca7 100644 --- a/Data/Engine/config.py +++ b/Data/Engine/config.py @@ -24,7 +24,7 @@ environment variables prefixed with ``BOREALIS_``, and finally built-in defaults that mirror the legacy server runtime. Key environment variables are ``BOREALIS_DATABASE_PATH`` path to the SQLite database file. Defaults to -the Engine runtime copy under ``/Engine/Data/Engine/database.db``. +``/Engine/database.db`` so data persists across Engine redeploys. ``BOREALIS_CORS_ORIGINS`` comma separated list of allowed origins for CORS. ``BOREALIS_SECRET`` Flask session secret key. ``BOREALIS_COOKIE_*`` Session cookie policies (``SAMESITE``, ``SECURE``, @@ -72,7 +72,7 @@ def _discover_project_root() -> Path: PROJECT_ROOT = _discover_project_root() -DEFAULT_DATABASE_PATH = PROJECT_ROOT / "Engine" / "Data" / "Engine" / "database.db" +DEFAULT_DATABASE_PATH = PROJECT_ROOT / "Engine" / "database.db" LOG_ROOT = PROJECT_ROOT / "Engine" / "Logs" LOG_FILE_PATH = LOG_ROOT / "engine.log" ERROR_LOG_FILE_PATH = LOG_ROOT / "error.log" diff --git a/Data/Engine/database.py b/Data/Engine/database.py index 61021d5d..0bbecffb 100644 --- a/Data/Engine/database.py +++ b/Data/Engine/database.py @@ -45,6 +45,7 @@ def initialise_engine_database(database_path: str, *, logger: Optional[logging.L conn = sqlite3.connect(str(path)) try: _apply_legacy_migrations(conn, logger=logger) + _restore_persisted_enrollment_codes(conn, logger=logger) _ensure_activity_history(conn, logger=logger) _ensure_device_list_views(conn, logger=logger) _ensure_sites(conn, logger=logger) @@ -79,6 +80,58 @@ def _apply_legacy_migrations(conn: sqlite3.Connection, *, logger: Optional[loggi raise +def _restore_persisted_enrollment_codes(conn: sqlite3.Connection, *, logger: Optional[logging.Logger]) -> None: + cur = conn.cursor() + try: + cur.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='enrollment_install_codes_persistent'" + ) + if cur.fetchone() is None: + return + cur.execute( + """ + INSERT INTO enrollment_install_codes ( + id, + code, + expires_at, + created_by_user_id, + used_at, + used_by_guid, + max_uses, + use_count, + last_used_at + ) + SELECT + p.id, + p.code, + p.expires_at, + p.created_by_user_id, + p.used_at, + p.used_by_guid, + p.max_uses, + p.last_known_use_count, + p.last_used_at + FROM enrollment_install_codes_persistent AS p + WHERE p.is_active = 1 + ON CONFLICT(id) DO UPDATE + SET code = excluded.code, + expires_at = excluded.expires_at, + created_by_user_id = excluded.created_by_user_id, + used_at = excluded.used_at, + used_by_guid = excluded.used_by_guid, + max_uses = excluded.max_uses, + use_count = excluded.use_count, + last_used_at = excluded.last_used_at + """ + ) + conn.commit() + except Exception as exc: + if logger: + logger.error("Failed to restore enrollment codes from persistence: %s", exc, exc_info=True) + finally: + cur.close() + + def _ensure_activity_history(conn: sqlite3.Connection, *, logger: Optional[logging.Logger]) -> None: cur = conn.cursor() try: diff --git a/Data/Engine/services/API/devices/approval.py b/Data/Engine/services/API/devices/approval.py index 8a61899a..afe1d8cf 100644 --- a/Data/Engine/services/API/devices/approval.py +++ b/Data/Engine/services/API/devices/approval.py @@ -244,7 +244,8 @@ class AdminDeviceService: cur = conn.cursor() created_by = self._lookup_user_id(cur, username) or username or "system" code_value = _generate_install_code() - expires_at = _now() + timedelta(hours=ttl_hours) + issued_at = _now() + expires_at = issued_at + timedelta(hours=ttl_hours) record_id = str(uuid.uuid4()) cur.execute( """ @@ -255,6 +256,40 @@ class AdminDeviceService: """, (record_id, code_value, _iso(expires_at), created_by, max_uses), ) + cur.execute( + """ + INSERT INTO enrollment_install_codes_persistent ( + id, + code, + created_at, + expires_at, + created_by_user_id, + used_at, + used_by_guid, + max_uses, + last_known_use_count, + last_used_at, + is_active, + archived_at, + consumed_at + ) + VALUES (?, ?, ?, ?, ?, NULL, NULL, ?, 0, NULL, 1, NULL, NULL) + ON CONFLICT(id) DO UPDATE + SET code = excluded.code, + created_at = excluded.created_at, + expires_at = excluded.expires_at, + created_by_user_id = excluded.created_by_user_id, + max_uses = excluded.max_uses, + last_known_use_count = 0, + used_at = NULL, + used_by_guid = NULL, + last_used_at = NULL, + is_active = 1, + archived_at = NULL, + consumed_at = NULL + """, + (record_id, code_value, _iso(issued_at), _iso(expires_at), created_by, max_uses), + ) conn.commit() finally: conn.close() @@ -284,6 +319,17 @@ class AdminDeviceService: (code_id,), ) deleted = cur.rowcount + if deleted: + archive_ts = _iso(_now()) + cur.execute( + """ + UPDATE enrollment_install_codes_persistent + SET is_active = 0, + archived_at = COALESCE(archived_at, ?) + WHERE id = ? + """, + (archive_ts, code_id), + ) conn.commit() finally: conn.close() diff --git a/Data/Engine/services/API/enrollment/routes.py b/Data/Engine/services/API/enrollment/routes.py index b5b57a75..abca4020 100644 --- a/Data/Engine/services/API/enrollment/routes.py +++ b/Data/Engine/services/API/enrollment/routes.py @@ -681,6 +681,32 @@ def register( enrollment_code_id, ), ) + cur.execute( + """ + UPDATE enrollment_install_codes_persistent + SET last_known_use_count = ?, + used_by_guid = ?, + last_used_at = ?, + used_at = CASE WHEN ? THEN ? ELSE used_at END, + is_active = CASE WHEN ? THEN 0 ELSE is_active END, + consumed_at = CASE WHEN ? THEN COALESCE(consumed_at, ?) ELSE consumed_at END, + archived_at = CASE WHEN ? THEN COALESCE(archived_at, ?) ELSE archived_at END + WHERE id = ? + """, + ( + new_count, + effective_guid, + now_iso, + 1 if consumed else 0, + now_iso, + 1 if consumed else 0, + 1 if consumed else 0, + now_iso, + 1 if consumed else 0, + now_iso, + enrollment_code_id, + ), + ) # Update approval record with final state cur.execute( diff --git a/Data/Server/Modules/admin/routes.py b/Data/Server/Modules/admin/routes.py index 25d149e8..c20a925a 100644 --- a/Data/Server/Modules/admin/routes.py +++ b/Data/Server/Modules/admin/routes.py @@ -195,7 +195,8 @@ def register( cur = conn.cursor() created_by = _lookup_user_id(cur, username) or username or "system" code_value = _generate_install_code() - expires_at = _now() + timedelta(hours=ttl_hours) + issued_at = _now() + expires_at = issued_at + timedelta(hours=ttl_hours) record_id = str(uuid.uuid4()) cur.execute( """ @@ -206,6 +207,40 @@ def register( """, (record_id, code_value, _iso(expires_at), created_by, max_uses), ) + cur.execute( + """ + INSERT INTO enrollment_install_codes_persistent ( + id, + code, + created_at, + expires_at, + created_by_user_id, + used_at, + used_by_guid, + max_uses, + last_known_use_count, + last_used_at, + is_active, + archived_at, + consumed_at + ) + VALUES (?, ?, ?, ?, ?, NULL, NULL, ?, 0, NULL, 1, NULL, NULL) + ON CONFLICT(id) DO UPDATE + SET code = excluded.code, + created_at = excluded.created_at, + expires_at = excluded.expires_at, + created_by_user_id = excluded.created_by_user_id, + max_uses = excluded.max_uses, + last_known_use_count = 0, + used_at = NULL, + used_by_guid = NULL, + last_used_at = NULL, + is_active = 1, + archived_at = NULL, + consumed_at = NULL + """, + (record_id, code_value, _iso(issued_at), _iso(expires_at), created_by, max_uses), + ) conn.commit() finally: conn.close() @@ -235,6 +270,17 @@ def register( (code_id,), ) deleted = cur.rowcount + if deleted: + archive_ts = _iso(_now()) + cur.execute( + """ + UPDATE enrollment_install_codes_persistent + SET is_active = 0, + archived_at = COALESCE(archived_at, ?) + WHERE id = ? + """, + (archive_ts, code_id), + ) conn.commit() finally: conn.close() diff --git a/Data/Server/Modules/db_migrations.py b/Data/Server/Modules/db_migrations.py index b6d1aa1c..1e99275e 100644 --- a/Data/Server/Modules/db_migrations.py +++ b/Data/Server/Modules/db_migrations.py @@ -27,6 +27,7 @@ def apply_all(conn: sqlite3.Connection) -> None: _ensure_device_aux_tables(conn) _ensure_refresh_token_table(conn) _ensure_install_code_table(conn) + _ensure_install_code_persistence_table(conn) _ensure_device_approval_table(conn) conn.commit() @@ -190,6 +191,92 @@ def _ensure_install_code_table(conn: sqlite3.Connection) -> None: ) +def _ensure_install_code_persistence_table(conn: sqlite3.Connection) -> None: + cur = conn.cursor() + cur.execute( + """ + CREATE TABLE IF NOT EXISTS enrollment_install_codes_persistent ( + id TEXT PRIMARY KEY, + code TEXT NOT NULL UNIQUE, + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL, + created_by_user_id TEXT, + used_at TEXT, + used_by_guid TEXT, + max_uses INTEGER NOT NULL DEFAULT 1, + last_known_use_count INTEGER NOT NULL DEFAULT 0, + last_used_at TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + archived_at TEXT, + consumed_at TEXT + ) + """ + ) + cur.execute( + """ + CREATE INDEX IF NOT EXISTS idx_eicp_active + ON enrollment_install_codes_persistent(is_active, expires_at) + """ + ) + cur.execute( + """ + CREATE UNIQUE INDEX IF NOT EXISTS uq_eicp_code + ON enrollment_install_codes_persistent(code) + """ + ) + + columns = {row[1] for row in _table_info(cur, "enrollment_install_codes_persistent")} + if "last_known_use_count" not in columns: + cur.execute( + """ + ALTER TABLE enrollment_install_codes_persistent + ADD COLUMN last_known_use_count INTEGER NOT NULL DEFAULT 0 + """ + ) + if "archived_at" not in columns: + cur.execute( + """ + ALTER TABLE enrollment_install_codes_persistent + ADD COLUMN archived_at TEXT + """ + ) + if "consumed_at" not in columns: + cur.execute( + """ + ALTER TABLE enrollment_install_codes_persistent + ADD COLUMN consumed_at TEXT + """ + ) + if "is_active" not in columns: + cur.execute( + """ + ALTER TABLE enrollment_install_codes_persistent + ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1 + """ + ) + if "used_at" not in columns: + cur.execute( + """ + ALTER TABLE enrollment_install_codes_persistent + ADD COLUMN used_at TEXT + """ + ) + if "used_by_guid" not in columns: + cur.execute( + """ + ALTER TABLE enrollment_install_codes_persistent + ADD COLUMN used_by_guid TEXT + """ + ) + if "last_used_at" not in columns: + cur.execute( + """ + ALTER TABLE enrollment_install_codes_persistent + ADD COLUMN last_used_at TEXT + """ + ) + + def _ensure_device_approval_table(conn: sqlite3.Connection) -> None: cur = conn.cursor() cur.execute( diff --git a/Data/Server/Modules/enrollment/routes.py b/Data/Server/Modules/enrollment/routes.py index 0cd6455c..948bd33b 100644 --- a/Data/Server/Modules/enrollment/routes.py +++ b/Data/Server/Modules/enrollment/routes.py @@ -671,6 +671,32 @@ def register( enrollment_code_id, ), ) + cur.execute( + """ + UPDATE enrollment_install_codes_persistent + SET last_known_use_count = ?, + used_by_guid = ?, + last_used_at = ?, + used_at = CASE WHEN ? THEN ? ELSE used_at END, + is_active = CASE WHEN ? THEN 0 ELSE is_active END, + consumed_at = CASE WHEN ? THEN COALESCE(consumed_at, ?) ELSE consumed_at END, + archived_at = CASE WHEN ? THEN COALESCE(archived_at, ?) ELSE archived_at END + WHERE id = ? + """, + ( + new_count, + effective_guid, + now_iso, + 1 if consumed else 0, + now_iso, + 1 if consumed else 0, + 1 if consumed else 0, + now_iso, + 1 if consumed else 0, + now_iso, + enrollment_code_id, + ), + ) # Update approval record with final state cur.execute( diff --git a/Data/Server/Modules/jobs/prune.py b/Data/Server/Modules/jobs/prune.py index bc9f18e1..f86b7245 100644 --- a/Data/Server/Modules/jobs/prune.py +++ b/Data/Server/Modules/jobs/prune.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime, timedelta, timezone -from typing import Callable, Optional +from typing import Callable, List, Optional import eventlet from flask_socketio import SocketIO @@ -31,6 +31,27 @@ def _run_once(db_conn_factory: Callable[[], any], log: Callable[[str, str, Optio conn = db_conn_factory() try: cur = conn.cursor() + persistent_table_exists = False + try: + cur.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='enrollment_install_codes_persistent'" + ) + persistent_table_exists = cur.fetchone() is not None + except Exception: + persistent_table_exists = False + + expired_ids: List[str] = [] + if persistent_table_exists: + cur.execute( + """ + SELECT id + FROM enrollment_install_codes + WHERE use_count = 0 + AND expires_at < ? + """, + (now_iso,), + ) + expired_ids = [str(row[0]) for row in cur.fetchall() if row and row[0]] cur.execute( """ DELETE FROM enrollment_install_codes @@ -40,6 +61,21 @@ def _run_once(db_conn_factory: Callable[[], any], log: Callable[[str, str, Optio (now_iso,), ) codes_pruned = cur.rowcount or 0 + if expired_ids: + placeholders = ",".join("?" for _ in expired_ids) + try: + cur.execute( + f""" + UPDATE enrollment_install_codes_persistent + SET is_active = 0, + archived_at = COALESCE(archived_at, ?) + WHERE id IN ({placeholders}) + """, + (now_iso, *expired_ids), + ) + except Exception: + # Best-effort archival; continue if the persistence table is absent. + pass cur.execute( """