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,scripts): close the review nits, and the same defect where it repeats
Remediation for the two review rounds on #789, plus what verifying them turned
up in the same code.
**The linter fix the review asked for twice.** `livePackages` moving from
`readdirSync` to `git ls-files` was the highest-value change in the PR and had
no test that pinned it. It still doesn't fail under the revert anyone would
actually write: keeping the git call and unioning the filesystem back in passes
every existing test while fully restoring the false negative. The new fixture
builds the discriminating case — a package deleted from git whose gitignored
`dist/` and `node_modules/` shells survive on disk — and is the only test that
fails under both a naive `readdirSync` revert and that hybrid. The
`git status` guard is scoped to the probe, not to `packages/`, so an unrelated
uncommitted file doesn't turn it into an assertion about nothing.
**A missing target no longer passes in silence.** A linter whose entire job is
catching dead paths in configuration skipped dead paths in its own: rename a
target and it dropped out of coverage forever, green. Its sibling already
exits 2 for a stale allowlist entry. Both now do, and both report a target
outside the repo by name instead of as a `../../../../..` chain climbing out
of the root — the review's cosmetic nit, which turned out to sit in both files.
**Every step that names an integration-specific API now branches.** Step 5 was
fixed for this; steps 1, 2 and the read-path step were not. A Prisma Next
project was sent at `types.*` / `encryptedTable` — the client `stash schema
build` explicitly refuses to scaffold for it — and a plain-Postgres project was
pointed three times at "the integration skill" it never gets installed.
`encryptQuery` is shown taking the schema objects rather than an
object-shorthand that read as three required strings. `queryOperatorGuidance`
is a switch with a neutral default rather than an if-chain ending in Drizzle's
answer: tsup transpiles without type-checking and this package has no
typecheck script, so nothing would have caught a fifth integration inheriting
it.
One test asserted nothing: it scoped to a `#### Encryption cutover` heading
that does not exist, and `substring(-1)` returns the whole document.
return'Declare it with the `types.*` column factories from `@cipherstash/stack-drizzle` on the existing `pgTable`'
102
+
case'supabase':
103
+
return'Declare the column with the `eql_v3_encrypted` domain in the migration SQL (`encryptedSupabase` derives its encryption config by introspecting those domains)'
104
+
case'prisma-next':
105
+
return'Declare the field with the `cipherstash.*` constructors in `prisma/schema.prisma` (`cipherstash.TextSearch()`, `cipherstash.DoubleOrd()`, …), with the `cipherstash` extension pack wired up per `@cipherstash/prisma-next/control`'
106
+
default:
107
+
return'Declare the table with `encryptedTable` and the `types.*` domain factories from `@cipherstash/stack/v3`, then pass it to `Encryption({ schemas })`'
108
+
}
109
+
}
110
+
71
111
/**
72
112
* How this integration filters on an encrypted column. Named per-integration
73
113
* rather than generically because the APIs are not interchangeable and only one
74
114
* of them is importable from any given project: `createEncryptionOperators` is
75
115
* exported by `@cipherstash/stack-drizzle` alone, so naming it for a Supabase
76
116
* or Prisma project sends the agent after a package that is not installed.
77
117
*
78
-
* `postgresql` is the fallback integration and is installed with no integration
79
-
* skill (see `SKILL_MAP` in `install-skills.ts`), so it is pointed at
80
-
* `stash-encryption` instead of "the integration skill".
118
+
* A `switch` with a neutral `default`, not an if-chain ending in the Drizzle
119
+
* string: `packages/cli` is built by tsup, which transpiles without
120
+
* type-checking, and the package has no `typecheck` script — so nothing would
121
+
* catch a fifth `Integration` variant silently inheriting Drizzle's answer.
122
+
* `skillsFor()` in `install-skills.ts` degrades the same way, for the same
return'query paths filter through the `encryptedSupabase` wrapper (`es.from("users").select(...).eq("email", value)`) — it encrypts filter operands for encrypted columns automatically; see the integration skill'
126
+
switch(integration){
127
+
case'supabase':
128
+
return'query paths filter through the `encryptedSupabase` wrapper (`es.from("users").select(...).eq("email", value)`) — it encrypts filter operands for encrypted columns automatically; see the integration skill'
129
+
case'prisma-next':
130
+
return'query paths use the `eql*` operators on the column inside `.where()` (`u.email.eqlEq(value)`, `eqlMatch`, `eqlGt`, …) — see the integration skill'
131
+
case'drizzle':
132
+
return'query paths use the right operator (`ops.eq`, from `createEncryptionOperators(client)`) — see the integration skill'
133
+
default:
134
+
// `table` and `column` are the schema OBJECTS, not their names as
135
+
// strings, and `queryType` is inferred from the column's configured
136
+
// indexes unless it is passed to override the inference.
137
+
return'query paths encrypt the search term first — `client.encryptQuery(value, { table: usersSchema, column: usersSchema.email })`, passing the schema objects themselves rather than their names — and compare that against the encrypted column; see the `stash-encryption` skill (a plain Postgres project gets no integration-specific skill)'
85
138
}
86
-
if(integration==='prisma-next'){
87
-
return'query paths use the `eql*` operators on the column inside `.where()` (`u.email.eqlEq(value)`, `eqlMatch`, `eqlGt`, …) — see the integration skill'
88
-
}
89
-
if(integration==='postgresql'){
90
-
return'query paths encrypt the search term first with `client.encryptQuery(value, { table, column, queryType })` and compare against the encrypted column — see the `stash-encryption` skill (a plain Postgres project gets no integration skill)'
139
+
}
140
+
141
+
/**
142
+
* How this integration turns ciphertext back into values on the read path.
143
+
*
144
+
* Named per-integration for the third time in this file, and for the third
145
+
* time because the answer is not portable: `decryptModel` is the typed
146
+
* client's, transparent decryption is the Supabase wrapper's.
return'selects through the `encryptedSupabase` wrapper decrypt transparently'
152
+
case'prisma-next':
153
+
return'the encrypted fields decrypt through the Prisma Next client'
154
+
default:
155
+
return'call `decryptModel(row, usersSchema)` — or `bulkDecryptModels` for a set — before returning the value to callers'
91
156
}
92
-
return'query paths use the right operator (`ops.eq`, from `createEncryptionOperators(client)`) — see the integration skill'
93
157
}
94
158
95
159
functionbullet(line: string): string{
@@ -298,10 +362,10 @@ export function renderImplementPrompt(ctx: SetupPromptContext): string {
298
362
'',
299
363
'### Add a new encrypted column',
300
364
'',
301
-
'Use when the column **does not yet exist** in the database (no plaintext predecessor to preserve). This is normal Drizzle / Supabase work plus the encryption client patterns from the integration skill.',
365
+
`Use when the column **does not yet exist** in the database (no plaintext predecessor to preserve). This is normal schema work in the project's own ORM or migration tooling, plus the encryption client patterns from ${integrationSkillRef(ctx.integration)}.`,
302
366
'',
303
367
"1. **If this is the first encrypted column in the project, configure the bundler exclusion first.** `@cipherstash/stack` cannot be bundled (it wraps a native FFI module). Next.js: add `serverExternalPackages: ['@cipherstash/stack', '@cipherstash/protect-ffi']` to `next.config.*`. Webpack: `externals`. esbuild: `external`. Vite SSR: `ssr.external`. Without this, the encryption client crashes at runtime with `Cannot find module '@cipherstash/protect-ffi-*'`. See the `stash-encryption` skill's Installation section for the full snippets.",
304
-
"2. Edit the user's real schema file (`src/db/schema.ts` or wherever they keep it) to declare the new encrypted column. Use the patterns in the integration skill — the `types.*` domain factories from `@cipherstash/stack-drizzle` for Drizzle, and the `types.*` factories from `@cipherstash/stack/eql/v3` (via `encryptedTable`, passed as `schemas`) for Supabase. Encrypted columns must be **nullable `jsonb`** at creation time. Never `.notNull()`.",
368
+
`2. Edit the user's real schema file (\`src/db/schema.ts\` or wherever they keep it) to declare the new encrypted column. ${schemaAuthoringGuidance(ctx.integration)}— the patterns are in ${integrationSkillRef(ctx.integration)}. Encrypted columns must be **nullable \`jsonb\`** at creation time (the \`eql_v3_*\` domains are over \`jsonb\`). Never \`.notNull()\`.`,
305
369
`3. Generate the schema migration${migration ? ` — \`${migration.generate}\` (${migration.tool})` : " using the project's existing migration tooling"}.`,
306
370
`4. Show the user the generated SQL before applying${migration ? ` — \`${migration.apply}\`` : ''}.`,
307
371
`5. Wire the column through the application code: insert paths encrypt before write, select paths decrypt after read, ${queryOperatorGuidance(ctx.integration)}.`,
@@ -328,7 +392,7 @@ export function renderImplementPrompt(ctx: SetupPromptContext): string {
328
392
'',
329
393
`3. **Backfill.** Run \`${cli} encrypt backfill --table <T> --column <c>\`. The CLI prompts the user (or accepts \`--confirm-dual-writes-deployed\` non-interactively) to confirm dual-writes are live, then chunks through the existing rows. Resumable; checkpoints to \`cs_migrations\` after every chunk. SIGINT-safe.`,
330
394
`4. **Switch reads to the encrypted column.** The step depends on the EQL version (\`${cli} encrypt backfill\` prints it; \`${cli} encrypt status\` shows it). **EQL v3 (the default):** there is no rename — update the schema and queries to read/write the encrypted column by its own name, and wire decryption through the encryption client. **EQL v2 (legacy data only):** update the schema file to declare the encrypted column under its final name (drop the twin suffix), then \`${cli} encrypt cutover --table <T> --column <c>\` runs the rename in one transaction (\`<col>\` → \`<col>_plaintext\`, twin → \`<col>\`). Do **not** declare a v2 column with a \`types.*\` domain — those are EQL v3 only. The adapters no longer author v2 (\`@cipherstash/stack-drizzle\` removed \`encryptedType\`), so a v2 column is a read path: declare it with the deprecated \`@cipherstash/stack/schema\` builders and decrypt through \`@cipherstash/stack\`.`,
331
-
'5. **Wire the read path through the encryption client.** The read column now holds ciphertext. Read code paths must decrypt before returning the value to callers — `decryptModel(row, table)` for Drizzle, the `encryptedSupabase` wrapper for Supabase, or the equivalent `decrypt`/`bulkDecryptModels` calls. Without this step, your read paths return raw encrypted payloads to end users. The integration skill has the exact API.',
395
+
`5. **Wire the read path through the encryption client.** The read column now holds ciphertext — ${readPathGuidance(ctx.integration)}. Without this step, your read paths return raw encrypted payloads to end users. See ${integrationSkillRef(ctx.integration)} for the exact API.`,
332
396
'6. **Remove the dual-write code.** The plaintext column (still `<col>` on v3; renamed `<col>_plaintext` on v2) is no longer authoritative. Delete the dual-write logic from the persistence layer.',
333
397
`7. **Drop.** Run \`${cli} encrypt drop --table <T> --column <c>\`. Generates a migration that removes the now-unused plaintext column (on v3 it first verifies no rows are still plaintext-only). Apply with the project's normal migration tooling.`,
0 commit comments