You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(cli, migrate): address CodeRabbit feedback on encryption-migrations
Aggregates the actionable items from CodeRabbit's review of #357. None
introduce new behaviour; each closes a specific defect.
Security / correctness:
- drizzle-helper: replace `execSync` with `spawnSync` so caller-provided
migration names can't escape into the shell.
- migrate/backfill: drop the value preview from the leak-guard error
message — the path that hits this branch is precisely where the
encryption client passed plaintext through, so the value is sensitive.
- cli/backfill: emit a generic catch-block error instead of bubbling
`error.message` (same plaintext-leak risk via upstream library
exception text).
Schema-qualified table names:
- requireTable: fall back to unqualified-name lookup so
`--table public.users` resolves to a schema whose `tableName === 'users'`.
- status: use lastIndexOf('.') when splitting `${table}.${column}` keys
so schema-qualified table names don't drop the column segment.
- cutover.buildRenameMigrationSql + drop.dropSql: split `schema.table`
into separate quoted identifiers so the generated `ALTER TABLE` is
valid and the `information_schema` probe matches.
EQL state / SQL:
- push, activate, cutover, eql.discardPendingConfig, status: schema-
qualify `eql_v2_configuration` as `public.eql_v2_configuration` so a
custom `search_path` doesn't shadow or hide the table.
- countEncryptedWithActiveConfig: return `bigint` instead of `number` —
the BIGINT row count silently truncates past `Number.MAX_SAFE_INTEGER`
on the exact large tables this is meant to sanity-check.
Process / lifecycle hygiene:
- Every CLI handler that did `process.exit(1)` inside a try/catch with
an async `finally`: replace with an `exitCode` flag and a single
`if (exitCode) process.exit(exitCode)` after the finally, so
`client.end()` actually runs on the error path.
- backfill: move `pool.connect()` inside the same try/finally that
registers the SIGINT handlers, so handlers/pool are cleaned up if
connection acquisition fails.
- cutover: catch proxy `reloadConfig()` failures separately and warn —
reload runs after the cutover transaction commits, so a transient
Proxy connectivity blip shouldn't make the outer catch report the
whole cutover as failed.
Schema validation:
- manifest: replace `castAs: z.string()` with a `z.enum(...)` of the
supported EQL types so a typo (`timestampz`, `strnig`) fails at
manifest read instead of at use-time.
Doc + skill accuracy:
- setup-prompt: drop the incorrect "no read-path code change" claim
from the migrate-existing-column flow. Add an explicit step 6
pointing at `decryptModel` / `encryptedSupabase` — without it the
agent ships read paths returning raw ciphertext to end users.
- stash-drizzle: `createProtectOperators` → `createEncryptionOperators`
(the only correct name; every other reference in the file already
uses it).
- stash-encryption: fix the `runBackfill` example's parameter names
(`table` → `tableName`, `column` → `schemaColumnKey/plaintextColumn/
encryptedColumn`, `client` → `encryptionClient`, plus the missing
`tableSchema` and `pkColumn`).
- design doc: cutover section now reflects the shipped flow
(`migrate_config()` + `activate_config()` inside the transaction
alongside the rename), and the read-path claim matches the skill.
Test infrastructure:
- backfill.integration.test.ts: import `dotenv/config` so PG_TEST_URL
loads from `.env`; add `dotenv` to devDependencies.
Deferred (separate follow-up entry):
- The cutover preflight only verifies one column's phase, but the
underlying EQL function promotes the whole pending config in one
call — already tracked as item 3.8 in the working follow-ups doc.
SELECTeql_v2.activate_config(); -- encrypting → active (prior active → inactive)
142
147
COMMIT;
143
148
144
149
-- If Proxy URL is configured, force refresh
145
150
\c <proxy_url>
146
151
SELECTeql_v2.reload_config();
147
152
```
148
153
149
-
Record `cut_over` event. App's existing `SELECT email FROM users`now returns the encrypted column (decrypted transparently by Proxy or client-side by Stack). No app code change required for reads — this is the big payoff of the rename approach.
154
+
Record `cut_over` event. App's existing `SELECT email FROM users` returns the encrypted column post-cutover; reading code paths must decrypt via the encryption client (e.g. `decryptModel(rows[0], usersTable)` in Drizzle, or the equivalent `encryptedSupabase` wrapper in Supabase) so the call site receives plaintext. No write-path code change beyond removing the dual-write logic — that comes after cutover.
"SELECT EXISTS(SELECT 1 FROM eql_v2_configuration WHERE state = 'pending') AS exists",
79
+
"SELECT EXISTS(SELECT 1 FROM public.eql_v2_configuration WHERE state = 'pending') AS exists",
78
80
)
79
81
if(pending.rows[0]?.exists!==true){
80
82
p.log.error(
81
83
'No pending EQL configuration. Update your schema to point at the encrypted column (drop the `_encrypted` suffix), then run `stash db push` to register the pending change before cutting over.',
82
84
)
83
-
process.exit(1)
85
+
exitCode=1
86
+
return
84
87
}
85
88
86
89
// Full lifecycle in one transaction:
@@ -142,13 +145,26 @@ export async function cutoverCommand(options: CutoverCommandOptions) {
142
145
}
143
146
}
144
147
148
+
// Proxy reload runs *after* the cutover transaction has committed.
149
+
// Any error from here on is post-commit cosmetic — the rename and
150
+
// config promotion are durable. Catch the proxy reload separately so
151
+
// a transient Proxy connectivity blip doesn't make the outer catch
152
+
// exit(1), which would falsely tell automation the cutover failed
@@ -61,10 +62,16 @@ export async function dropCommand(options: DropCommandOptions) {
61
62
p.log.error(
62
63
`Cannot generate drop migration: ${options.table}.${options.column} is in phase '${state?.phase??'—'}'. Must be 'cut-over'.`,
63
64
)
64
-
process.exit(1)
65
+
exitCode=1
66
+
return
65
67
}
66
68
67
-
constdropSql=`-- Generated by stash encrypt drop\n-- Drops the plaintext column now that ${options.table}.${options.column} is encrypted.\n\nALTER TABLE "${options.table}" DROP COLUMN "${options.column}_plaintext";\n`
constdropSql=`-- Generated by stash encrypt drop\n-- Drops the plaintext column now that ${options.table}.${options.column} is encrypted.\n\nALTER TABLE ${qualifiedTable} DROP COLUMN "${options.column}_plaintext";\n`
0 commit comments