Repository navigation
fix(stack-supabase): read tables that have no encrypted columns - #1544
cipherstash-agent wants to merge 3 commits into
Conversation
The adapter hands Encryption({ schemas }) only the tables that carry an
encrypted column, but its query pipeline still sent every table's rows to
decryptModel / bulkDecryptModels with that table. Both engines refuse a table
they were not initialised with, so every select() on a plaintext-only table,
and every returning mutation on one, failed with "decryptModel received a
table this client was not initialized with". On the WASM entry, writes failed
too, because its encryptModel checks the table first.
The rule for "has encrypted columns" now lives in one function,
tableHasEncryptedColumns, which both decides what Encryption receives and,
through ColumnMap.hasEncryptedColumns, makes the encrypt and decrypt steps pass
a plaintext table's rows straight through. The two cannot drift apart.
The unit suites missed this because their encryption double ignores the table
argument. The new suite uses a double that enforces registration the way the
real engines do, and a live integration suite covers both construction modes.
Fixes CIP-5151.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Jmn7npvR2b5FZLdXNNmCN
…ertext
The passthrough for tables with no encrypted columns returned rows exactly as
PostgREST sent them. A row can still carry an EQL payload: from an embedded
relation to an encrypted table, or from an eql_v3 column that a declared-mode
table declared with {} leaves out. Those reads failed before the passthrough;
after it they returned ciphertext typed as data, which is the hazard
assertTableIsModelled exists to prevent.
The passthrough now walks the rows with isEncryptedPayload, the same predicate
model decryption uses to find ciphertext, and fails with an error that names
the field and never the value. adapter-kit exports the predicate so there is
one spelling of "encrypted value".
The unit double also labels encrypt refusals EncryptionError, matching the
engines. The stash-supabase skill documents plaintext tables and the embedded
relation limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Jmn7npvR2b5FZLdXNNmCN
🦋 Changeset detectedLatest commit: 3334cb7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueTip 🎯 Customize your risk assessments
✅ Automatic approvals, with your rules
|
…ot skip it
The integration harness's no-skips reporter fails the run on any skipped test,
and the plaintext-table suite skipped select('*') in declared mode. Declared
mode refuses select('*') because it has no column list to expand, so the test
now asserts that refusal there and the read everywhere else.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Jmn7npvR2b5FZLdXNNmCN
Requested by CipherStash · project thread
Summary
Every read of a Supabase table with no encrypted columns failed. The adapter hands
Encryption({ schemas })only the tables that carry an encrypted column, but then asked the client to decrypt every table's rows with that table, and both engines refuse a table they were not initialised with. Soes.from('audit_log').select('*'), and any insert, update or upsert followed by.select(), returneddecryptModel received a table this client was not initialized with(or threw, withthrowOnError). On thewasm-inlineentry, writes to such a table failed the same way. Fixes CIP-5151.Changes
column-map.ts:tableHasEncryptedColumns(table)is now the one rule for "plaintext table".create.tsuses it to choose whatEncryptionreceives, andColumnMap.hasEncryptedColumnsuses it in the query pipeline, so the two cannot drift apart.query-mutation.tsandquery-results.ts: for a plaintext table, the encrypt and decrypt steps pass rows through without calling the encryption client. Filters already skipped the client, because terms are collected only for encrypted columns.query-results.ts: before returning a plaintext table's rows, the adapter checks them for an encrypted value and refuses the read if it finds one, with an error that names the field and never the value. A row can still hold ciphertext from an embedded relation to an encrypted table (select('id, users(email)')), or from aneql_v3_*column that a declared-mode table declared with{}leaves out. Those reads used to fail. Without this check they would return ciphertext typed as data.@cipherstash/stack/adapter-kitexportsisEncryptedPayload, the check model decryption already uses, so the adapter uses the same definition of an encrypted value.create.tscomment that said the builder callsdecryptModel(row)with no table.stash-supabaseskill now covers plaintext tables and the embedded-relation limit. There are changesets for@cipherstash/stack-supabase,@cipherstash/stackandstash.Verification
__tests__/supabase-plaintext-table.test.ts. The shared unit double ignores the table argument, which is why the existing suites missed this bug. The new suite uses a double that enforces registration the way the real engines do. It covers introspection and declared modes, single and bulk reads,throwOnError, returning inserts, and an encrypted table on the same instance. All ten read and returning-mutation tests failed onmainwith the production error and pass now. The four ciphertext-guard tests failed before the guard was added.integration/plaintext-table.integration.test.tsruns the same cases against real ZeroKMS, Postgres and PostgREST in both modes. It needs credentials, so I have not run it locally and it runs in the integration job.pnpm --filter @cipherstash/stack-supabase test: 587 passed, 7 skipped.pnpm run code:check: no errors.tsupfor stack-supabase fails locally onCannot find module '@cipherstash/stack/wasm-inline'(the WASM build is missing). It fails the same way onmain, so this branch did not cause it.Review notes
A medium code review ran on this change, and the follow-up commit fixes its two correctness findings (the ciphertext guard) and one test-double nit. Three findings were left as they are:
hasEncryptedColumnsduplicatesencryptedColumnNames.length > 0. It reads the same predicate ascreate.tson purpose. That keeps it tied to whatEncryptionactually received.wasm-inlineintegration coverage. This suite has no harness for that entry. The bypass is in code both entries share, and the unit double reproduces the WASM refusal.Possible follow-up: decrypt embedded encrypted relations instead of refusing them. That needs a table-less decrypt on the WASM client, which it does not have today.
Generated by Claude Code