/* oxlint-disable executor/no-try-catch-or-throw, executor/no-json-parse -- boundary: one-shot config migration drives a raw SQL client (JSON text columns, transaction + rollback) */ // --------------------------------------------------------------------------- // One-off auth-config migration planner — the driver-agnostic core every app // wires its own SQL around. Each protocol plugin exports a pure // `migrateAuthConfig(config) → config | null` (null = leave the row // untouched); this plans which integration rows to rewrite. Idempotent by // construction: re-running over migrated rows plans zero updates. // --------------------------------------------------------------------------- export type AuthConfigTransform = (config: unknown) => unknown | null; export interface AuthConfigMigrationRow { /** The integration row's primary key (`row_id`). */ readonly rowId: string; readonly pluginId: string; readonly config: unknown; } export interface AuthConfigMigrationUpdate { readonly rowId: string; readonly config: unknown; } /** Plan the config rewrites for a set of integration rows. `transforms` maps * plugin ids (`mcp` / `graphql` / `openapi`) to their migration functions; * rows of other plugins, and rows whose transform returns null, are left * untouched. */ export const planAuthConfigMigration = ( rows: readonly AuthConfigMigrationRow[], transforms: Record, ): readonly AuthConfigMigrationUpdate[] => { const updates: AuthConfigMigrationUpdate[] = []; for (const row of rows) { const transform = transforms[row.pluginId]; if (!transform) continue; const migrated = transform(row.config); if (migrated !== null) updates.push({ rowId: row.rowId, config: migrated }); } return updates; }; // --------------------------------------------------------------------------- // SQLite runner — shared by the libSQL-backed apps (local boot, selfhost // boot). Structural client interface so this package stays dependency-free; // `@libsql/client` satisfies it. Postgres apps (cloud) wire their own SQL // around `planAuthConfigMigration`. // --------------------------------------------------------------------------- export interface SqliteAuthConfigClient { execute( stmt: string | { readonly sql: string; readonly args: readonly unknown[] }, ): Promise<{ readonly rows: readonly Record[] }>; } /** Rewrite pre-canonical integration `config` blobs in a SQLite database. * Idempotent (canonical rows plan no updates); returns the number of rows * rewritten. The `integration` table may not exist yet on a fresh database — * that counts as nothing to migrate. */ export const runSqliteAuthConfigMigration = async ( client: SqliteAuthConfigClient, transforms: Record, ): Promise => { const exists = await client.execute( "SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'integration'", ); if (exists.rows.length === 0) return 0; const result = await client.execute("SELECT row_id, plugin_id, config FROM integration"); const rows: AuthConfigMigrationRow[] = []; for (const row of result.rows) { if (typeof row.row_id !== "string" || typeof row.plugin_id !== "string") continue; if (typeof row.config !== "string") continue; let config: unknown; try { config = JSON.parse(row.config); } catch { continue; } rows.push({ rowId: row.row_id, pluginId: row.plugin_id, config }); } const updates = planAuthConfigMigration(rows, transforms); if (updates.length === 0) return 0; await client.execute("BEGIN"); try { for (const update of updates) { await client.execute({ sql: "UPDATE integration SET config = ? WHERE row_id = ?", args: [JSON.stringify(update.config), update.rowId], }); } await client.execute("COMMIT"); } catch (cause) { await client.execute("ROLLBACK"); throw cause; } return updates.length; };