Skip to content

fix(stack,bench): wasm-inline DynamoDB v2 reads, and a bench seed that never encrypted (#772 review findings 10, 12) - #788

Merged
tobyhede merged 4 commits into
remove-v2from
fix/stack-runtime-correctness
Jul 27, 2026
Merged

fix(stack,bench): wasm-inline DynamoDB v2 reads, and a bench seed that never encrypted (#772 review findings 10, 12)#788
tobyhede merged 4 commits into
remove-v2from
fix/stack-runtime-correctness

Conversation

@tobyhede

Copy link
Copy Markdown
Contributor

Review remediation for #772 — findings 10 and 12.

Finding 10 — the wasm-inline client on the DynamoDB v2 read path

decrypt-model.ts calls client.decryptModel(item) with no table for a v2 table, deliberately: a v2 table means nothing to a v3 client's reconstructor map, and the native clients derive it from the payloads. WasmEncryptionClient cannot — its decrypt requires the table and resolves date fields from a per-table map, so the omitted argument reached requireTable(undefined) and threw TypeError: Cannot read properties of undefined (reading 'tableName') from deep inside the client.

That client ships in this package, is the documented entry for Deno / Workers / Supabase Edge Functions, and satisfies DynamoDBEncryptionClient structurally — so it was accepted with no cast and only failed at the first read.

Now rejected where the message can name the combination, keyed on a declared capability (requiresTableForDecrypt) rather than sniffing arity or constructor name. EQL v3 tables are always passed the table, so they still work — pinned by a test, so this cannot become a blanket rejection of the wasm entry.

Three comments claimed audit metadata is forwarded "regardless of client shape" and that "every client this package ships carries .audit() on decrypt". Neither is true of the wasm client. Corrected, along with a debug message that told the user to build a client with Encryption({ schemas }) — which is exactly what the wasm entry's own factory is.

Finding 12 — the bench seed never encrypted anything

extractEncryptionSchema keys the column map by the Drizzle table's JS property (encText), not the DB column name (enc_text). The v2→v3 port left the seed keyed by DB name, so no field matched: bulkEncryptModels returned every row untouched with no failure, and the remap at seed.ts:66 was moving plaintext.

Not a data-safety bug — verified against a real Postgres with the pinned EQL 3.0.2 bundle: all three domains reject the insert with 23514. It is a bench that cannot run for anyone with credentials.

Nothing caught it because nothing could: tsc --noEmit passes (the row type was hand-written and agreed with itself), harness.test.ts asserts a row count and never that a column is encrypted, and CI runs only the db-only filter, which never seeds.

So the new check is deliberately cheap — __unit__/seed-keys.test.ts compares the row's keys against buildColumnKeyMap()'s under a second vitest config with no globalSetup, so it needs neither a database nor credentials and cannot be skipped for want of either.

Not in this PR

Cut-list items 3 (nested dotted date columns never Date-reconstructed — pre-existing on main) and 8 (dead v2-only PgComposite re-exports on adapter-kit and the top-level entry). Both are real; 8 removes public surface and wants its own review.

Verification

pnpm --filter @cipherstash/stack test → 140 passed, 51 skipped; the 2 failing files are the pre-existing credential-gated live suites (identical count on the base). test:types 99 passed. pnpm --filter @cipherstash/bench build (tsc) clean, test:unit 3 passed. Biome: 0 errors.

tobyhede added 2 commits July 25, 2026 14:37
…base

extractEncryptionSchema keys the encrypted-table column map by the Drizzle
table's JS PROPERTY (encText), not the DB column name (enc_text) — so
buildColumnKeyMap() is {encText: 'enc_text', ...} and resolveEncryptColumnMap
matches models on the property names. The v2 -> v3 port left the seed keyed by
DB name, so no field matched: bulkEncryptModels returned every row untouched,
with no failure, and the insert then carried PLAINTEXT into eql_v3_* columns.
The remap at seed.ts:66 was moving plaintext, not ciphertext, and its comment
had become false.

Not a data-safety bug — verified against a real Postgres with the pinned EQL
3.0.2 bundle, all three domains reject the insert with 23514. It is a bench
that cannot run at all for anyone with credentials.

Nothing caught it because nothing could. `tsc --noEmit` passes: BenchPlaintextRow
was hand-written and agreed with itself. harness.test.ts asserts a row count and
never that a column is encrypted. And CI runs only `test:local db-only`, which
never seeds — every suite that touches the seed needs credentials.

So the check that fails on this is deliberately cheap: __unit__/seed-keys.test.ts
compares the row's keys against buildColumnKeyMap()'s, under a second vitest
config with no globalSetup, so it needs neither a database nor credentials and
cannot be skipped for want of either. BenchPlaintextRow is now the property
space, and the identity remap is gone.
The adapter's v2 read path calls decryptModel(item) with NO table, on purpose:
a v2 table means nothing to a v3 client's reconstructor map, and the native
clients derive the table from the payloads. WasmEncryptionClient cannot — its
decrypt requires the table and resolves date fields from a per-table map, so
the omitted argument reached requireTable(undefined) and threw
`TypeError: Cannot read properties of undefined (reading 'tableName')` from
deep inside the client.

That client ships in this package, is the documented entry for Deno, Workers
and Supabase Edge Functions, and satisfies DynamoDBEncryptionClient
structurally — so encryptedDynamoDB accepted it with no cast and the pairing
only failed at the first read, with a message pointing nowhere near the cause.

Reject it where the message can name the combination. The signal is a declared
capability on the client (`requiresTableForDecrypt`) rather than sniffing arity
or constructor name. v3 tables are always passed the table, so they still work.

Also corrects three comments this package carried claiming audit metadata is
forwarded "regardless of client shape" and that "every client this package
ships carries .audit() on decrypt". The wasm-inline client's decrypt is a plain
async method; its audit metadata is dropped, and the debug message that fires
told the user to build a client with Encryption({ schemas }) — which is exactly
what the wasm entry's own factory is.
@tobyhede
tobyhede requested a review from a team as a code owner July 25, 2026 04:40
@changeset-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 687173a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 11 packages
Name Type
stash Patch
@cipherstash/stack Patch
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/bench Patch
@cipherstash/prisma-next Patch
@cipherstash/stack-drizzle Patch
@cipherstash/stack-supabase Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-next-example Patch
@cipherstash/wizard Patch

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

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1523f119-1964-4ea0-9a86-3f8d4101086f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stack-runtime-correctness

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve.

Both fixes are correct and contract-preserving. Finding 10 rejects the wasm-inline + EQL-v2 pairing at the call site keyed on the declared requiresTableForDecrypt capability rather than sniffing arity/constructor, and the native/typed path is provably unaffected (they never set that flag — the still-green forwarding tests confirm it). Finding 12 is a real "moving plaintext" bench bug; the fix is right and is now pinned by a credential-free structural test (derived from the table's own buildColumnKeyMap()) wired into tests-bench.yml, which closes the "too expensive to run, so it rotted" gap that caused it. Result/payload shapes, ESM/CJS exports, and the v3 read path are untouched. Changeset present and correctly scoped (bench is private, needs none).

Non-blocking:

  • dynamodb/index.ts guard message is read-flavored ("would fail at the first read") but also fires on encryptModel/bulkEncryptModels. Outcome is correct (wasm-inline is v3-only), but consider softening to "cannot handle legacy EQL v2 tables" so it reads correctly on encrypt calls too.
  • skills/stash-dynamodb/SKILL.md states a blanket "decrypt still reads existing v2 items" — a one-line caveat that the wasm-inline edge client is now the exception would preempt a customer hitting the new throw.

@freshtonic freshtonic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the authoritative gh pr diff at head bcf247aa, with local test runs and a check of the PR's own (green) CI.

Approving. A tight, well-reasoned pair of correctness fixes with strong tests.

Verified

  • Finding 10: the wasm-inline client is refused on the DynamoDB EQL v2 read path via a declared requiresTableForDecrypt capability (not arity/name sniffing), keyed only on WasmEncryptionClient. Confirmed the native FFI client has no such flag, so the native decrypt path is not regressed, and v3 tables still forward the table (pinned by test).
  • Finding 12: the bench seed is re-keyed by JS property and a remap that was silently moving plaintext is dropped, guarded by a new credential-free unit test (vitest.unit.config.ts, no globalSetup) wired into CI — which directly closes the "too expensive to run, so it rotted" gap that let this ship.
  • Payload-shape contract untouched; changeset present and correctly scoped (@cipherstash/stack patch; bench is private). Local tests pass (bench seed 3, v2-table-forwarding 4, stack build clean); PR CI fully green (Node 22/24, Bun, Deno WASM E2E, Drizzle v3, E2E).

Non-blocking findings

  • skills/stash-dynamodb/SKILL.md (minor): documents EQL v2 decrypt as supported but never mentions that the wasm-inline client now hard-rejects v2 tables. Since wasm-inline is the documented entry for Deno/Workers/Supabase Edge — runtimes commonly paired with DynamoDB — a customer reading legacy v2 items there hits the new error with no forewarning. AGENTS.md's skills-drift policy asks for the skill update in the same PR. Please add a one-line caveat here (the runtime error itself is explicit, so this is non-blocking rather than a blocker).
  • packages/stack/src/dynamodb/index.ts:44-61 (minor): assertClientTableVersionMatch also runs for encrypt ops, so a wasm client + v2 table on encryptModel throws the "cannot read legacy EQL v2 items" message, which is phrased for the read path. Not a regression (v2 encrypt is out of contract), just slightly off-target wording.
  • packages/bench/package.json (nit): reformatted space→tab across the whole file, adding whitespace-only churn. Biome-conformant, just incidental diff noise.

Highlight: the declared-capability design over constructor/arity sniffing is robust and self-documenting, and the v3-accepted test guards against the fix degrading into a blanket rejection of the wasm entry.

tobyhede added 2 commits July 27, 2026 13:27
…te (#788 review)

Addresses the three review findings on #788, plus a real defect found while
verifying the second one.

The wasm+v2 guard message is now operation-neutral. `assertClientTableVersionMatch`
runs on all four operations, so a plain-JS caller reaching `encryptModel` with a
v2 table got "cannot read legacy EQL v2 items ... would fail at the first read" —
naming an operation that never ran. The pairing is wrong in both directions
anyway (`Encryption()` on that entry rejects a v2 schema), so the message now
says so instead of describing a read.

Verifying that finding surfaced the larger one: `encryptModel` /
`bulkEncryptModels` chained `.audit()` onto the client's result unconditionally.
The native clients return a thenable operation carrying it; the wasm-inline
client's encrypt returns a bare `Promise<WasmResult>` and has no `.audit()`
anywhere. So EVERY EQL v3 write through this adapter on that entry failed with
`client.encryptModel(...).audit is not a function` — surfaced as a
`DYNAMODB_ENCRYPTION_ERROR`, indistinguishable from a genuine encryption fault.
This PR's own changeset claimed the opposite ("EQL v3 tables are unaffected ...
the wasm-inline client keeps working there"); it was true of decrypt only.

`resolveEncryptResult` mirrors the existing `resolveDecryptResult`: tolerate the
bare promise, drop audit metadata observably rather than crashing, and fail
closed on a malformed result so an unencrypted item can never pass through as a
success. The changeset and the `types.ts` docblock are corrected to match.

Regression tests are credential-free. The chainable half matters most — the
native clients' encrypt audit trail had no coverage outside a live-ZeroKMS
suite, so nothing would have caught breaking it.

skills/stash-dynamodb documented v2 decrypt as unconditionally supported and
never mentioned that wasm-inline refuses v2 tables, on the documented entry for
the runtimes most often paired with DynamoDB. It also claimed audit metadata
forwards "regardless of client shape" — the exact phrase this PR removed from
the source as untrue. Both corrected, plus the wasm-inline row in
skills/stash-encryption, which never said the entry is v3-only.

packages/bench/package.json is back to 2-space indent, keeping only the
substantive `test:unit` addition: 53 changed lines down to 1. The repo's Biome
config is `indentStyle: "space"` and the file was spaces on main, so the churn
was neither required nor conformant — Biome ignores `**/package.json` entirely,
verified by direct invocation.
… disproved (#788 review)

Follow-up to the #788 review of this branch. The largest item is that the fix
landed at runtime but the TYPE still declared the invariant it disproves.

`CallableEncryptionClient.encryptModel` / `bulkEncryptModels` returned
`ChainableEncryptOperation`, declaring an `.audit()` that the wasm-inline entry
does not have — precisely the assumption that let the write path chain it
unconditionally and fail every EQL v3 write there. The decrypt members already
returned `unknown` with a docblock explaining why; encrypt never got the same
treatment. Both are now `unknown`, `ChainableEncryptOperation` is deleted (it had
no other reference), and the docblock covers all four members.

This is not cosmetic: re-introducing `client.encryptModel(...).audit(...)` now
fails to compile with TS2571, verified by reverting the call site under tsc. The
regression is statically unreachable rather than merely fixed. The type is
internal — absent from every emitted .d.ts — so there is no API change and no
changeset.

The test I had described as load-bearing was the weakest one. It stubbed
`audit()` returning a promise; the native contract is `audit(): this` on a
thenable whose `then()` calls `execute()`, so metadata is read back off the
operation at execution time rather than passed forward. That stub passes even if
`audit()` stops recording entirely. It now subclasses the real
`EncryptionOperation`, mirroring what the decrypt half of the file already did,
and asserts the metadata reaches `execute()` and that the operation runs exactly
once. A native-failure case covers the other arm.

Also strengthened, all verified by mutation (each kills exactly 2 tests):
- the audit-drop test asserted only that the result succeeded — it now asserts
  the drop is logged and names the entry to switch to, plus a mirror proving a
  chainable client does NOT trip the drop path
- both malformed-result loops gained `null` and `[]`. `null` makes the
  `resolved === null` clause load-bearing (without it `'data' in null` throws);
  `[]` is `typeof 'object'` so it must be rejected on the key checks

`DecryptFailure` is renamed `ResultFailure` — it was already the encrypt
helper's failure type, structurally identical, wrong name.

bench: the `BenchPlaintextRow` docblock claimed it was "derived from the table so
the two cannot drift again". It is hand-written. Deriving it was investigated and
is actively worse: `InferInsertModel` describes the ENCRYPTED column and degrades
to optional `any`, and `extractEncryptionSchema` returns the widened `AnyV3Table`
whose index-signature column map admits both `encTxt: 'x'` and `encText: 12345`,
which the literal rejects. The comment now says so and names the unit test as the
real guard.

The credential-free bench step moved ahead of `docker compose up`: leaving it
last meant a database failure would skip the one check whose entire rationale is
that it cannot be skipped for want of a database. Its `server.deps.inline` for
test-kit was inert — nothing on that path imports it — and is gone; verified
still passing with DATABASE_URL and all four CS_* vars unset.
@tobyhede tobyhede mentioned this pull request Jul 27, 2026
7 tasks
@tobyhede
tobyhede merged commit c3f4292 into remove-v2 Jul 27, 2026
10 checks passed
@tobyhede
tobyhede deleted the fix/stack-runtime-correctness branch July 27, 2026 04:31
tobyhede added a commit that referenced this pull request Jul 28, 2026
…te (#788 review)

Addresses the three review findings on #788, plus a real defect found while
verifying the second one.

The wasm+v2 guard message is now operation-neutral. `assertClientTableVersionMatch`
runs on all four operations, so a plain-JS caller reaching `encryptModel` with a
v2 table got "cannot read legacy EQL v2 items ... would fail at the first read" —
naming an operation that never ran. The pairing is wrong in both directions
anyway (`Encryption()` on that entry rejects a v2 schema), so the message now
says so instead of describing a read.

Verifying that finding surfaced the larger one: `encryptModel` /
`bulkEncryptModels` chained `.audit()` onto the client's result unconditionally.
The native clients return a thenable operation carrying it; the wasm-inline
client's encrypt returns a bare `Promise<WasmResult>` and has no `.audit()`
anywhere. So EVERY EQL v3 write through this adapter on that entry failed with
`client.encryptModel(...).audit is not a function` — surfaced as a
`DYNAMODB_ENCRYPTION_ERROR`, indistinguishable from a genuine encryption fault.
This PR's own changeset claimed the opposite ("EQL v3 tables are unaffected ...
the wasm-inline client keeps working there"); it was true of decrypt only.

`resolveEncryptResult` mirrors the existing `resolveDecryptResult`: tolerate the
bare promise, drop audit metadata observably rather than crashing, and fail
closed on a malformed result so an unencrypted item can never pass through as a
success. The changeset and the `types.ts` docblock are corrected to match.

Regression tests are credential-free. The chainable half matters most — the
native clients' encrypt audit trail had no coverage outside a live-ZeroKMS
suite, so nothing would have caught breaking it.

skills/stash-dynamodb documented v2 decrypt as unconditionally supported and
never mentioned that wasm-inline refuses v2 tables, on the documented entry for
the runtimes most often paired with DynamoDB. It also claimed audit metadata
forwards "regardless of client shape" — the exact phrase this PR removed from
the source as untrue. Both corrected, plus the wasm-inline row in
skills/stash-encryption, which never said the entry is v3-only.

packages/bench/package.json is back to 2-space indent, keeping only the
substantive `test:unit` addition: 53 changed lines down to 1. The repo's Biome
config is `indentStyle: "space"` and the file was spaces on main, so the churn
was neither required nor conformant — Biome ignores `**/package.json` entirely,
verified by direct invocation.
tobyhede added a commit that referenced this pull request Jul 28, 2026
… disproved (#788 review)

Follow-up to the #788 review of this branch. The largest item is that the fix
landed at runtime but the TYPE still declared the invariant it disproves.

`CallableEncryptionClient.encryptModel` / `bulkEncryptModels` returned
`ChainableEncryptOperation`, declaring an `.audit()` that the wasm-inline entry
does not have — precisely the assumption that let the write path chain it
unconditionally and fail every EQL v3 write there. The decrypt members already
returned `unknown` with a docblock explaining why; encrypt never got the same
treatment. Both are now `unknown`, `ChainableEncryptOperation` is deleted (it had
no other reference), and the docblock covers all four members.

This is not cosmetic: re-introducing `client.encryptModel(...).audit(...)` now
fails to compile with TS2571, verified by reverting the call site under tsc. The
regression is statically unreachable rather than merely fixed. The type is
internal — absent from every emitted .d.ts — so there is no API change and no
changeset.

The test I had described as load-bearing was the weakest one. It stubbed
`audit()` returning a promise; the native contract is `audit(): this` on a
thenable whose `then()` calls `execute()`, so metadata is read back off the
operation at execution time rather than passed forward. That stub passes even if
`audit()` stops recording entirely. It now subclasses the real
`EncryptionOperation`, mirroring what the decrypt half of the file already did,
and asserts the metadata reaches `execute()` and that the operation runs exactly
once. A native-failure case covers the other arm.

Also strengthened, all verified by mutation (each kills exactly 2 tests):
- the audit-drop test asserted only that the result succeeded — it now asserts
  the drop is logged and names the entry to switch to, plus a mirror proving a
  chainable client does NOT trip the drop path
- both malformed-result loops gained `null` and `[]`. `null` makes the
  `resolved === null` clause load-bearing (without it `'data' in null` throws);
  `[]` is `typeof 'object'` so it must be rejected on the key checks

`DecryptFailure` is renamed `ResultFailure` — it was already the encrypt
helper's failure type, structurally identical, wrong name.

bench: the `BenchPlaintextRow` docblock claimed it was "derived from the table so
the two cannot drift again". It is hand-written. Deriving it was investigated and
is actively worse: `InferInsertModel` describes the ENCRYPTED column and degrades
to optional `any`, and `extractEncryptionSchema` returns the widened `AnyV3Table`
whose index-signature column map admits both `encTxt: 'x'` and `encText: 12345`,
which the literal rejects. The comment now says so and names the unit test as the
real guard.

The credential-free bench step moved ahead of `docker compose up`: leaving it
last meant a database failure would skip the one check whose entire rationale is
that it cannot be skipped for want of a database. Its `server.deps.inline` for
test-kit was inert — nothing on that path imports it — and is gone; verified
still passing with DATABASE_URL and all four CS_* vars unset.
tobyhede added a commit that referenced this pull request Jul 28, 2026
…te (#788 review)

Addresses the three review findings on #788, plus a real defect found while
verifying the second one.

The wasm+v2 guard message is now operation-neutral. `assertClientTableVersionMatch`
runs on all four operations, so a plain-JS caller reaching `encryptModel` with a
v2 table got "cannot read legacy EQL v2 items ... would fail at the first read" —
naming an operation that never ran. The pairing is wrong in both directions
anyway (`Encryption()` on that entry rejects a v2 schema), so the message now
says so instead of describing a read.

Verifying that finding surfaced the larger one: `encryptModel` /
`bulkEncryptModels` chained `.audit()` onto the client's result unconditionally.
The native clients return a thenable operation carrying it; the wasm-inline
client's encrypt returns a bare `Promise<WasmResult>` and has no `.audit()`
anywhere. So EVERY EQL v3 write through this adapter on that entry failed with
`client.encryptModel(...).audit is not a function` — surfaced as a
`DYNAMODB_ENCRYPTION_ERROR`, indistinguishable from a genuine encryption fault.
This PR's own changeset claimed the opposite ("EQL v3 tables are unaffected ...
the wasm-inline client keeps working there"); it was true of decrypt only.

`resolveEncryptResult` mirrors the existing `resolveDecryptResult`: tolerate the
bare promise, drop audit metadata observably rather than crashing, and fail
closed on a malformed result so an unencrypted item can never pass through as a
success. The changeset and the `types.ts` docblock are corrected to match.

Regression tests are credential-free. The chainable half matters most — the
native clients' encrypt audit trail had no coverage outside a live-ZeroKMS
suite, so nothing would have caught breaking it.

skills/stash-dynamodb documented v2 decrypt as unconditionally supported and
never mentioned that wasm-inline refuses v2 tables, on the documented entry for
the runtimes most often paired with DynamoDB. It also claimed audit metadata
forwards "regardless of client shape" — the exact phrase this PR removed from
the source as untrue. Both corrected, plus the wasm-inline row in
skills/stash-encryption, which never said the entry is v3-only.

packages/bench/package.json is back to 2-space indent, keeping only the
substantive `test:unit` addition: 53 changed lines down to 1. The repo's Biome
config is `indentStyle: "space"` and the file was spaces on main, so the churn
was neither required nor conformant — Biome ignores `**/package.json` entirely,
verified by direct invocation.
tobyhede added a commit that referenced this pull request Jul 28, 2026
… disproved (#788 review)

Follow-up to the #788 review of this branch. The largest item is that the fix
landed at runtime but the TYPE still declared the invariant it disproves.

`CallableEncryptionClient.encryptModel` / `bulkEncryptModels` returned
`ChainableEncryptOperation`, declaring an `.audit()` that the wasm-inline entry
does not have — precisely the assumption that let the write path chain it
unconditionally and fail every EQL v3 write there. The decrypt members already
returned `unknown` with a docblock explaining why; encrypt never got the same
treatment. Both are now `unknown`, `ChainableEncryptOperation` is deleted (it had
no other reference), and the docblock covers all four members.

This is not cosmetic: re-introducing `client.encryptModel(...).audit(...)` now
fails to compile with TS2571, verified by reverting the call site under tsc. The
regression is statically unreachable rather than merely fixed. The type is
internal — absent from every emitted .d.ts — so there is no API change and no
changeset.

The test I had described as load-bearing was the weakest one. It stubbed
`audit()` returning a promise; the native contract is `audit(): this` on a
thenable whose `then()` calls `execute()`, so metadata is read back off the
operation at execution time rather than passed forward. That stub passes even if
`audit()` stops recording entirely. It now subclasses the real
`EncryptionOperation`, mirroring what the decrypt half of the file already did,
and asserts the metadata reaches `execute()` and that the operation runs exactly
once. A native-failure case covers the other arm.

Also strengthened, all verified by mutation (each kills exactly 2 tests):
- the audit-drop test asserted only that the result succeeded — it now asserts
  the drop is logged and names the entry to switch to, plus a mirror proving a
  chainable client does NOT trip the drop path
- both malformed-result loops gained `null` and `[]`. `null` makes the
  `resolved === null` clause load-bearing (without it `'data' in null` throws);
  `[]` is `typeof 'object'` so it must be rejected on the key checks

`DecryptFailure` is renamed `ResultFailure` — it was already the encrypt
helper's failure type, structurally identical, wrong name.

bench: the `BenchPlaintextRow` docblock claimed it was "derived from the table so
the two cannot drift again". It is hand-written. Deriving it was investigated and
is actively worse: `InferInsertModel` describes the ENCRYPTED column and degrades
to optional `any`, and `extractEncryptionSchema` returns the widened `AnyV3Table`
whose index-signature column map admits both `encTxt: 'x'` and `encText: 12345`,
which the literal rejects. The comment now says so and names the unit test as the
real guard.

The credential-free bench step moved ahead of `docker compose up`: leaving it
last meant a database failure would skip the one check whose entire rationale is
that it cannot be skipped for want of a database. Its `server.deps.inline` for
test-kit was inert — nothing on that path imports it — and is gone; verified
still passing with DATABASE_URL and all four CS_* vars unset.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants