|
| 1 | +# Migrate Proxy to Canonical Encryption Config — Implementation Plan |
| 2 | + |
| 3 | +> **For Claude:** REQUIRED SUB-SKILL: Use cipherpowers:executing-plans to implement this plan task-by-task. |
| 4 | +
|
| 5 | +**Goal:** Remove the proxy's local `CastAs` enum and `ColumnEncryptionConfig` parser, replacing them with `CanonicalEncryptionConfig` from the `cipherstash-config` crate. |
| 6 | + |
| 7 | +**Architecture:** The proxy currently has its own JSON config parser in `encrypt_config/config.rs` (~490 lines) that duplicates what `cipherstash-config` provides. We replace it with the canonical parser, keeping only the `EncryptConfig` wrapper and `EncryptConfigManager` which handle proxy-specific concerns (Arc-wrapped config, background reload). |
| 8 | + |
| 9 | +**Tech Stack:** Rust, serde, serde_json, cipherstash-config |
| 10 | + |
| 11 | +**Prerequisite:** The canonical config work in `cipherstash-suite` (CIP-2871) must be completed first — specifically, `CanonicalEncryptionConfig`, `PlaintextType`, `Identifier`, and `into_config_map()` must exist in `cipherstash-config`. |
| 12 | + |
| 13 | +**Design doc:** `~/cipherstash/cipherstash-suite/docs/plans/2026-04-01-canonical-encryption-config-design.md` |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +### Task 1: Add `cipherstash-config` dependency |
| 18 | + |
| 19 | +**Files:** |
| 20 | +- Modify: `Cargo.toml` (workspace root) |
| 21 | +- Modify: `packages/cipherstash-proxy/Cargo.toml` |
| 22 | + |
| 23 | +**Step 1: Add to workspace dependencies** |
| 24 | + |
| 25 | +In the root `Cargo.toml`, add `cipherstash-config` to `[workspace.dependencies]`. Match the version/source used for `cipherstash-client`. |
| 26 | + |
| 27 | +**Step 2: Add to cipherstash-proxy package** |
| 28 | + |
| 29 | +In `packages/cipherstash-proxy/Cargo.toml`, add: |
| 30 | + |
| 31 | +```toml |
| 32 | +cipherstash-config = { workspace = true } |
| 33 | +``` |
| 34 | + |
| 35 | +**Step 3: Verify it compiles** |
| 36 | + |
| 37 | +Run: `cargo check -p cipherstash-proxy` |
| 38 | +Expected: Clean build |
| 39 | + |
| 40 | +**Step 4: Commit** |
| 41 | + |
| 42 | +```bash |
| 43 | +git add Cargo.toml Cargo.lock packages/cipherstash-proxy/Cargo.toml |
| 44 | +git commit --no-gpg-sign -m "chore: add cipherstash-config dependency to cipherstash-proxy" |
| 45 | +``` |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +### Task 2: Replace `ColumnEncryptionConfig` with `CanonicalEncryptionConfig` in the manager |
| 50 | + |
| 51 | +**Files:** |
| 52 | +- Modify: `packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs` |
| 53 | + |
| 54 | +**Step 1: Update imports** |
| 55 | + |
| 56 | +Replace the import of local `ColumnEncryptionConfig`: |
| 57 | + |
| 58 | +```rust |
| 59 | +// Before |
| 60 | +use super::config::ColumnEncryptionConfig; |
| 61 | + |
| 62 | +// After |
| 63 | +use cipherstash_config::CanonicalEncryptionConfig; |
| 64 | +``` |
| 65 | + |
| 66 | +**Step 2: Update `load_encrypt_config` function** |
| 67 | + |
| 68 | +The function currently does (around line 216): |
| 69 | + |
| 70 | +```rust |
| 71 | +let encrypt_config: ColumnEncryptionConfig = serde_json::from_value(json_value)?; |
| 72 | +let encrypt_config = EncryptConfig::new_from_config(encrypt_config.into_config_map()); |
| 73 | +``` |
| 74 | + |
| 75 | +Change to: |
| 76 | + |
| 77 | +```rust |
| 78 | +let encrypt_config: CanonicalEncryptionConfig = serde_json::from_value(json_value) |
| 79 | + .map_err(|e| /* map to existing error type */)?; |
| 80 | +let config_map = encrypt_config.into_config_map() |
| 81 | + .map_err(|e| /* map ConfigError to proxy Error */)?; |
| 82 | +let encrypt_config = EncryptConfig::new_from_config(config_map); |
| 83 | +``` |
| 84 | + |
| 85 | +Note: The canonical `into_config_map()` returns `Result<HashMap<Identifier, ColumnConfig>, ConfigError>` (fallible, with validation) while the proxy's was infallible. You'll need to handle the `Result` — map `ConfigError` to the proxy's error type. |
| 86 | + |
| 87 | +Also note: The canonical `Identifier` is from `cipherstash_config::Identifier`, not `cipherstash_client::eql::Identifier`. Check that `EncryptConfig::new_from_config` and `EncryptConfig::get_column_config` use the same `Identifier` type. If they differ, update `EncryptConfig` to use the canonical `Identifier`. |
| 88 | + |
| 89 | +**Step 3: Run tests** |
| 90 | + |
| 91 | +Run: `cargo test -p cipherstash-proxy --lib -- encrypt_config` |
| 92 | +Expected: All pass |
| 93 | + |
| 94 | +**Step 4: Commit** |
| 95 | + |
| 96 | +```bash |
| 97 | +git add packages/cipherstash-proxy/src/proxy/encrypt_config/manager.rs |
| 98 | +git commit --no-gpg-sign -m "refactor: use CanonicalEncryptionConfig in EncryptConfigManager" |
| 99 | +``` |
| 100 | + |
| 101 | +--- |
| 102 | + |
| 103 | +### Task 3: Remove local config types |
| 104 | + |
| 105 | +**Files:** |
| 106 | +- Modify: `packages/cipherstash-proxy/src/proxy/encrypt_config/config.rs` |
| 107 | + |
| 108 | +**Step 1: Delete local types** |
| 109 | + |
| 110 | +Remove the following from `config.rs`: |
| 111 | +- `ColumnEncryptionConfig` struct |
| 112 | +- `Tables` struct and its `IntoIterator` impl |
| 113 | +- `Table` struct and its `IntoIterator` impl |
| 114 | +- `Column` struct |
| 115 | +- `CastAs` enum |
| 116 | +- `From<CastAs> for ColumnType` impl |
| 117 | +- `OreIndexOpts`, `MatchIndexOpts`, `SteVecIndexOpts`, `UniqueIndexOpts` structs |
| 118 | +- `Indexes` struct |
| 119 | +- `FromStr for ColumnEncryptionConfig` impl |
| 120 | +- `ColumnEncryptionConfig::into_config_map` method |
| 121 | +- `Column::into_column_config` method |
| 122 | +- All default functions (`default_tokenizer`, `default_k`, `default_m`, `default_array_index_mode`) |
| 123 | + |
| 124 | +This should remove ~200 lines of code. What remains in `config.rs` (if anything) depends on whether the proxy has any config types not covered by the canonical module. |
| 125 | + |
| 126 | +**Step 2: Update `mod.rs` if needed** |
| 127 | + |
| 128 | +If `config.rs` is now empty or only has tests, update `packages/cipherstash-proxy/src/proxy/encrypt_config/mod.rs` accordingly. |
| 129 | + |
| 130 | +**Step 3: Run tests** |
| 131 | + |
| 132 | +Run: `cargo test -p cipherstash-proxy --lib -- encrypt_config` |
| 133 | +Expected: All pass |
| 134 | + |
| 135 | +Run: `cargo clippy --no-deps --tests --all-features --all-targets -p cipherstash-proxy -- -D warnings` |
| 136 | +Expected: No warnings |
| 137 | + |
| 138 | +**Step 4: Commit** |
| 139 | + |
| 140 | +```bash |
| 141 | +git add packages/cipherstash-proxy/src/proxy/encrypt_config/ |
| 142 | +git commit --no-gpg-sign -m "refactor: remove local CastAs and ColumnEncryptionConfig, use canonical types" |
| 143 | +``` |
| 144 | + |
| 145 | +--- |
| 146 | + |
| 147 | +### Task 4: Update tests to use canonical types |
| 148 | + |
| 149 | +**Files:** |
| 150 | +- Modify: `packages/cipherstash-proxy/src/proxy/encrypt_config/config.rs` (test module) |
| 151 | + |
| 152 | +**Step 1: Migrate tests** |
| 153 | + |
| 154 | +The existing tests (lines 210-489) test JSON parsing of the local types. Rewrite them to test via `CanonicalEncryptionConfig` and `into_config_map()`. Key tests to preserve: |
| 155 | + |
| 156 | +- `column_with_empty_options_gets_defaults` — empty column defaults to `Text` with no indexes |
| 157 | +- `can_parse_column_with_cast_as` — `"cast_as": "int"` parses correctly |
| 158 | +- `can_parse_ore_index` — ORE index deserializes |
| 159 | +- `can_parse_unique_index_with_token_filter` — unique with downcase filter |
| 160 | +- `can_parse_match_index_with_defaults` — match index gets k=6, m=2048, Standard tokenizer |
| 161 | +- `can_parse_match_index_with_all_opts_set` — custom match options |
| 162 | +- `can_parse_ste_vec_index` — STE vec with prefix and array_index_mode |
| 163 | + |
| 164 | +Each test should: |
| 165 | +1. Build JSON with `serde_json::json!` |
| 166 | +2. Deserialize to `CanonicalEncryptionConfig` |
| 167 | +3. Call `into_config_map()` |
| 168 | +4. Assert on the resulting `ColumnConfig` |
| 169 | + |
| 170 | +Example: |
| 171 | + |
| 172 | +```rust |
| 173 | +#[test] |
| 174 | +fn column_with_empty_options_gets_defaults() { |
| 175 | + let json = json!({ |
| 176 | + "v": 1, |
| 177 | + "tables": { |
| 178 | + "users": { |
| 179 | + "email": {} |
| 180 | + } |
| 181 | + } |
| 182 | + }); |
| 183 | + |
| 184 | + let config: CanonicalEncryptionConfig = serde_json::from_value(json).unwrap(); |
| 185 | + let map = config.into_config_map().unwrap(); |
| 186 | + |
| 187 | + let id = Identifier::new("users", "email"); |
| 188 | + let col = map.get(&id).unwrap(); |
| 189 | + assert_eq!(col.cast_type, ColumnType::Text); |
| 190 | + assert!(col.indexes.is_empty()); |
| 191 | +} |
| 192 | +``` |
| 193 | + |
| 194 | +Add a backwards-compat test: |
| 195 | + |
| 196 | +```rust |
| 197 | +#[test] |
| 198 | +fn it_accepts_old_cast_as_jsonb() { |
| 199 | + let json = json!({ |
| 200 | + "v": 1, |
| 201 | + "tables": { |
| 202 | + "events": { |
| 203 | + "data": { |
| 204 | + "cast_as": "jsonb", |
| 205 | + "indexes": { |
| 206 | + "ste_vec": { "prefix": "test" } |
| 207 | + } |
| 208 | + } |
| 209 | + } |
| 210 | + } |
| 211 | + }); |
| 212 | + |
| 213 | + let config: CanonicalEncryptionConfig = serde_json::from_value(json).unwrap(); |
| 214 | + let map = config.into_config_map().unwrap(); |
| 215 | + let id = Identifier::new("events", "data"); |
| 216 | + let col = map.get(&id).unwrap(); |
| 217 | + assert_eq!(col.cast_type, ColumnType::Json); |
| 218 | +} |
| 219 | +``` |
| 220 | + |
| 221 | +**Step 2: Run tests** |
| 222 | + |
| 223 | +Run: `cargo test -p cipherstash-proxy --lib -- encrypt_config` |
| 224 | +Expected: All pass |
| 225 | + |
| 226 | +**Step 3: Commit** |
| 227 | + |
| 228 | +```bash |
| 229 | +git add packages/cipherstash-proxy/src/proxy/encrypt_config/ |
| 230 | +git commit --no-gpg-sign -m "test: migrate encrypt_config tests to use canonical types" |
| 231 | +``` |
| 232 | + |
| 233 | +--- |
| 234 | + |
| 235 | +### Task 5: Full build and test verification |
| 236 | + |
| 237 | +**Files:** None (verification only) |
| 238 | + |
| 239 | +**Step 1: Workspace clippy** |
| 240 | + |
| 241 | +Run: `cargo clippy --no-deps --tests --all-features --all-targets --workspace -- -D warnings` |
| 242 | +Expected: No warnings |
| 243 | + |
| 244 | +**Step 2: Unit tests** |
| 245 | + |
| 246 | +Run: `cargo test --workspace --all-features --lib` |
| 247 | +Expected: All pass |
| 248 | + |
| 249 | +**Step 3: Integration tests (if environment available)** |
| 250 | + |
| 251 | +Run: `mise run test:local:integration` (requires PostgreSQL running) |
| 252 | +Expected: All pass |
| 253 | + |
| 254 | +**Step 4: If any failures, fix and commit** |
| 255 | + |
| 256 | +```bash |
| 257 | +git add -u |
| 258 | +git commit --no-gpg-sign -m "fix: resolve build issues from canonical config migration" |
| 259 | +``` |
0 commit comments