fix(stack,stack-supabase): edge-safe shared seam — process-free adapter-kit + structural v3 columns - #799
Conversation
🦋 Changeset detectedLatest commit: 5761f6b 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 Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Addresses review on #799. - ColumnMap now throws at construction when a builder in an AnyV3Table's columnBuilders fails the structural v3 probe, instead of silently omitting it. An omitted column dropped out of v3Columns and its filter operands went to PostgREST as plaintext — the exact leak this work closes, from the other direction. Regression tests prove construction throws and no PostgREST request is issued. - Harden the logger env guard against a process defined without env.
coderdan
left a comment
There was a problem hiding this comment.
Some test improvement suggestions but otherwise looks good.
freshtonic
left a comment
There was a problem hiding this comment.
Reviewed both fixes in the shared adapter seam, verifying each claim against source and running the gates myself.
Approving. Two well-scoped, well-tested defect fixes.
Verified
- Fix 1 guards the module-scope
process.envread in the core logger so@cipherstash/stack/adapter-kit(re-exported to Supabase, Drizzle, Prisma Next) imports cleanly on process-free runtimes. The edge-safety gate asserts against the real emitteddist/adapter-kit.js, not a proxy, andturbo.jsonwirestest→buildso it can't silently skip in CI. - Fix 2 replaces a cross-bundle-fragile
instanceof EncryptedV3Columngate with a structural four-method probe and fails closed on unrecognised builders — closing a silent plaintext-leak path. The design is sound:columnBuildersis typed to hold only v3 columns, and the completeness test iterating alltypes.*domains confirms every real v3 column passes the probe, so the throw can't misfire on valid tables. code:check→ 0 errors;@cipherstash/stackbuilds; fullstack-supabasesuite +test:typespass. ESM/CJS export maps intact (./adapter-kitkeeps bothimport/requirewith types;EncryptedV3Columnis still exported — only the import incolumn-map.tswas removed). Both changesets present and correctly scoped as patches.
Non-blocking nits
__tests__/helpers/process-free-realm.mjsrejects all bare specifiers — stricter than production resolution; works only because the current externals arenoExternal/bundled. Documented as intentional, but a future legitimate external would fail this gate and need a harness update rather than signalling a real regression.- PR description says the suite goes 466 → 470; the actual run reports 471. Trivial doc drift.
- No skill change needed here (public surface unchanged) — but the
stash-supabase"factory can't run in a Worker" guidance is worth a re-check in the later portable-entry plans that actually make the factory edge-capable.
…v3] prefix Both assertions on the unrecognised-builder path matched `/\[supabase v3\]/`, which 32 messages across this package satisfy — two of them thrown by `ColumnMap` itself. Neither test could tell which error it caught. Demonstrated rather than assumed: making `assertNoPropertyDbNameCollision` throw unconditionally, so the fail-closed probe is bypassed entirely, left both tests GREEN. (Deleting the probe outright does turn them red — construction then succeeds and nothing throws. It is the bypass, not the deletion, the loose matcher was blind to.) `supabase-v3-wire.test.ts` is the more serious of the two: it guards the harm — no query string emitted — not just the mechanism. Both now pin the identity clause and both interpolations. Stops short of the advisory tail, which carries six unescaped `/` and would make the matcher a syntax error rather than a stricter test. Re-running the same bypass mutant now fails both. Also corrects a misattributed citation the assertion's comment repeats: the v2 `build()`/`getName()` at `schema/index.ts:257,264` are `EncryptedField`'s. `EncryptedColumn` opens at :275, so its own are at :442,449.
…t a time `isV3ColumnLike` had no test that could distinguish a four-probe gate from a two-probe one. Every builder reaching it either satisfied all four probes (the 40 catalog domains, the wasm-authored double) or missed two at once (the v2 stub) — so no fixture was ever one member away from passing. Measured with a mutation harness: deleting any single conjunct, weakening any single `typeof` to `true`, or dropping the object/null guard entirely left the whole 471-test suite green. Nine surviving mutants of those enumerated. (The `'x' in builder` half of each conjunct is an equivalent mutant — for any non-exotic object the `typeof` half subsumes it — so those four are correctly left alive.) The new cases are each one member apart from a conforming builder, so each fails if and only if its own probe goes. All nine die, and precisely: deleting a conjunct fails exactly its two tests, weakening a `typeof` exactly its one, and the two halves of the object/null guard are split so a mutant that drops one half names which half it dropped (1 test vs 4). Also covered: prototype-borne members (the real `Encrypted*Column` classes carry all four on the prototype, so `in` must not become `Object.hasOwn`), and a real `encryptedColumn().equality()` v2 builder rather than a hand-rolled stub — the case the four-probe design exists for. Neither kills a mutant the others miss; both are kept because they turn a 335-test avalanche and an opaque `true !== false` into one precise, self-describing failure. `isV3ColumnLike` is exported to allow this. `column-map.ts` is not re-exported from `src/index.ts` and the package publishes only `.`, so the published surface is unchanged: the built `dist/index.d.ts` and `.d.cts` contain no mention of it, and both bundles' runtime exports remain exactly `encryptedSupabase, encryptedSupabaseV3`.
…fect The process-free realm harness rejects every bare specifier, which is stricter than the resolution any real runtime performs. It passes today only because everything the adapter-kit graph reaches is bundled via tsup `noExternal` — load-bearing, not incidental: the chunk adapter-kit imports carries inlined `evlog`, so without that entry it would emit a bare import. `@cipherstash/auth` and `@cipherstash/protect-ffi` stay external and are simply unreachable from that graph; if either became reachable, the gate would fail with a message reading like a self-containment defect. The header and the throw now say what the constraint rests on and what to do about it. No behaviour change — verified by pointing the harness at `dist/index.js`, whose bare `@cipherstash/auth` import produces the reworded error. `turbo.json`'s override also replaced the root `dependsOn` rather than extending it, resolving `test` to `["build"]` and dropping the inherited `["^build"]`. This is defence-in-depth rather than a live bug: ordering survives transitively via `build`'s own `^build`, as `packages/prisma-next` demonstrates with the identical override and real workspace deps. Made explicit anyway, matching how the root spells `test:e2e`; prisma-next is left alone deliberately, since the same reasoning says it is not broken either. Lastly, `logger-edge-safety.test.ts` described a `bundling-isolation.test.ts` that spawns this harness against the WASM entry as though it existed. It does not yet — reworded to future tense, and the turbo/bare-invocation skip asymmetry noted where a reader will hit it.
A v2 `EncryptedTable` is structurally identical to a v3 one — same `tableName`,
same `columnBuilders` — and only `buildColumnKeyMap()` tells them apart. So
nothing that inspected shape caught the swap, and TypeScript only helps callers
who are actually type-checking.
Two paths, two raw TypeErrors, both naming an internal method rather than the
version mismatch behind it:
- `encryptedSupabase({ schemas })` sailed past the record-key check and died in
`verifyDeclaredSchemas` as `builder.getEqlType is not a function`. This is the
reachable one — the realistic mistake is migrating from v2 and passing the old
`schemas` through.
- Constructing the query builder directly died one layer down in `ColumnMap`, on
the constructor's very first statement, as `table.buildColumnKeyMap is not a
function`. Not reachable from the factory today (`mergeDeclaredTables` rebuilds
a fresh v3 table), so this half is defence-in-depth at the seam — symmetric
with the column-level fail-closed guard already there, which cannot cover it
because it runs after the constructor has already crashed.
Both now name the table and state the fix. Each guard is load-bearing: removing
either puts its original TypeError back.
The check routes through `hasBuildColumnKeyMap` rather than a second
hand-written spelling, per that function's own doctrine — "a second hand-written
spelling of the check is how a v2 envelope eventually gets built for a v3 table
once the marker drifts". It is re-exported from `@cipherstash/stack/adapter-kit`,
the first-party adapter seam, and deliberately NOT promoted to `./types`:
deciding which wire version a table targets is adapter plumbing, not end-user
API. Verified edge-safe — `dist/adapter-kit.js` still evaluates in a
process-free realm with no bare specifiers (`OK 14 exports`).
`skills/stash-supabase` gains the error under "Legacy: EQL v2", where a
migrating reader will hit it.
The precondition for retrying #798 stage 4, not a consequence of it. Stage 4 was reverted because the WASM entry threw `ReferenceError: process is not defined` on import — it had begun importing `@/utils/logger`, whose `initStackLogger()` read `process.env` at module scope. The useful part is not the defect but what missed it: 1073 unit tests, the type tests and the bundle-isolation test were all green while the entry was unusable in every runtime it exists for. Nothing could have caught it. `wasm-inline-bundle-isolation.test.ts` reads import SPECIFIERS, so a global is invisible to it. The Deno e2e runs under Deno, which provides `process`. The unit suites run under Node, likewise. So this points the harness #799 built for `dist/adapter-kit.js` at `dist/wasm-inline.js` — which that test's own docblock anticipated ("the portable-entry plan will point the same harness at the WASM entry"). It matters more here than there: the shared operation layer widens this entry's transitive graph, and anything that graph reaches ships to the edge. The harness gains an allowed-externals argument. `adapter-kit` has no bare specifiers; the WASM entry has two, the `/wasm-inline` subpaths of protect-ffi and auth, which Deno and Workers resolve through an import map (and `e2e/wasm/deno.json` maps exactly those). They are STUBBED rather than loaded: the question is whether the graph evaluates without `process`, and loading the real binding adds a megabyte of unrelated behaviour and its own globals for no extra signal. Stub exports are `undefined` on purpose — module-scope code that calls into an external at import time is itself a portability defect and should fail loudly. Anything not named is still rejected, so a new external on this entry fails here too, which is the same native-leak class the isolation test guards. Verified by breaking it: injecting an unguarded module-scope `process.env` read into `wasm-inline.ts` and rebuilding makes this fail with the ReferenceError above. Passing today at 56 exports.
Addresses review on #799. - ColumnMap now throws at construction when a builder in an AnyV3Table's columnBuilders fails the structural v3 probe, instead of silently omitting it. An omitted column dropped out of v3Columns and its filter operands went to PostgREST as plaintext — the exact leak this work closes, from the other direction. Regression tests prove construction throws and no PostgREST request is issued. - Harden the logger env guard against a process defined without env.
Addresses review on #799. - ColumnMap now throws at construction when a builder in an AnyV3Table's columnBuilders fails the structural v3 probe, instead of silently omitting it. An omitted column dropped out of v3Columns and its filter operands went to PostgREST as plaintext — the exact leak this work closes, from the other direction. Regression tests prove construction throws and no PostgREST request is issued. - Harden the logger env guard against a process defined without env.
Blocking fix. `readDatabaseUrlFromEnv()` was consulted on the edge entry too, where `introspector` is null by construction, and the refusal keyed on the RESOLVED url — so an ambient `DATABASE_URL` made a declared-mode client throw "drop `databaseUrl`" at a caller who passed none. Unactionable (nothing in their code fixes it) and ordinary rather than exotic: every runtime exposing `process.env` reaches it, and `DATABASE_URL` is the variable a Supabase project already has. - The ambient read is gated on `introspector`; the refusal keys on `options.databaseUrl`, so it can only fire for an option someone wrote. - The no-schemas error branches per build: telling an edge caller to set `DATABASE_URL` sent them to a variable that build cannot read. - Four regression tests that SET the variable. Both suites previously pinned the ENVIRONMENT to keep declared mode working — the Deno test asserted `DATABASE_URL` was unset — which routed around the coupling instead of testing it. That assertion now sets the variable and expects construction to succeed anyway. Verified the new tests fail against 83179d5. Also from the same review: - `packages/stack-supabase/turbo.json` makes `test` depend on this package's own `build`. Without it `turbo run test` never built the package, so the emitted-bundle edge-safety gate reported 4 skipped and a regression putting the native engine back in the edge graph would have shipped green. Same fix and same reason as packages/stack/turbo.json (#799). - Reverted the `pg` peer floor to `>=8`. The `>=8.16.3` bump was for pg-cloudflare, which this PR does not wire up (#808), and the edge entry carries no `pg` at all — so it narrowed a range for every consumer and bought nothing reachable today. - Dropped the dead `introspection && introspector` conjunct. Claude-Session: https://claude.ai/code/session_01AwM5Cm5ddasXozb6stxPR1
…try (#708) (#912) * supabase: construct without a Postgres connection, and add an edge entry (#708) `encryptedSupabase` derived every column's encryption config by introspecting the database, so it always needed a Postgres connection. That made it unconstructible anywhere a TCP socket to Postgres is unavailable, and cost a second, more privileged credential even on Node -- the caller already had an authenticated Supabase client. The rule now: declare your schemas and it runs anywhere; omit them and we discover them for you, which needs a connection and is therefore Node-only. Two independent mechanisms had to go, and fixing either alone leaves the wrapper broken: - Introspection. Passing `schemas` skips it entirely. The gate is the database URL, NOT the absence of `schemas`, so every existing caller keeps introspection and keeps its drift check; "pass `databaseUrl` as well" is how you declare types and still verify them. - The native engine. `src/index.ts` statically imported `@cipherstash/stack`, which loads protect-ffi and `@cipherstash/auth` (both Node-API) on import whether or not you encrypt. The new `./wasm-inline` entry binds the WASM `Encryption` instead. The introspector is injected for the same reason: a dynamic `import('pg')` is still a specifier a bundler resolves at build time, so the edge entry must not reach the module at all. What declared mode gives up, it gives up loudly: `select('*')` and bare `select()` are refused, `from()` on an undeclared table throws naming the declaration, and `queryDomainsRequired` is forced rather than detected. Also fixed: a read awaited with no `.select()` sent a raw `*` to PostgREST, bypassing the refusal `select('*')` has always performed -- so encrypted columns came back with no `::jsonb` casts. Evidence rather than assertion: `e2e/wasm/supabase-declared.test.ts` runs the emitted entry under real Deno with `ffi: false` and no database, and `wasm-entry-edge-safety.test.ts` scans the emitted bundle for the specifiers that must not be there -- verified to fail when the entry is pointed back at the native engine. Claude-Session: https://claude.ai/code/session_01AwM5Cm5ddasXozb6stxPR1 * Address the two review passes on #708 Claude Code /code-review (10 findings) and GPT-5.6 Sol (2 P1s). The two converged on the same thing from different directions, and it was the important one: the edge entry CONSTRUCTED but could not run a query. - The WASM client is now adapted, not cast (both P1s, finding 5). The engines differ in ways that are silent at construction: `decryptModel` requires the table on WASM and derives it from payloads on native (both call sites now pass it, which native ignores); WASM operations are plain Results with no `withLockContext`/`audit`, so both are attached and throw a sentence naming the gap; `bulkEncrypt` is not forwarded, selecting the supported per-term fallback over a mismatched signature. The edge entry's options now require `schemas` and a `WasmClientConfig`, so omitting credentials is a type error rather than a TypeError from inside the engine. - Reverted the await-without-`.select()` change (finding 1). Routing it through `expandStarOrThrow` skipped the cast/alias step, broke declared renames with a 42703, and pinned the column list at construction. The raw `*` is restored and the pre-existing wart documented. - Ambient `DATABASE_URL` no longer overrules a declaration (finding 2), and says so when it is ignored rather than changing mode in silence (finding 9). - Documented the one declared-mode tradeoff that is NOT loud: an encrypted column missing from the declaration is treated as plaintext (finding 3). - The Deno suite now encrypts a filter operand and decrypts a row through WASM, which is what its header always claimed (finding 6). - CLI: `readOreState` gates on the installed version instead of reporting healthy skewed installs as damage (4); `CASTS_SQL` OR-matches EQL endpoints (7); the bundle parser throws on statement forms no handler models rather than failing open (8); the `--database-url` message moves to messages.ts (10). Claude-Session: https://claude.ai/code/session_01AwM5Cm5ddasXozb6stxPR1 * Close finding 9 properly, and fix a suppression that did nothing Two gaps found while checking whether the review comments could be resolved. - Finding 9 was not actually addressed. The warning fired only when an ambient DATABASE_URL existed and was ignored — but the finding's scenario is that variable going MISSING, where it stayed silent. It now fires whenever the native entry enters declared mode implicitly, which covers both callers: the one who declared deliberately and the one who just lost their env var and used to get a boot throw. Gated on the introspector, so the edge entry — where declared mode is the only mode — stays quiet. - The `biome-ignore` in wasm-client-adapter sat on the closing line of the cast while the diagnostic anchors to the start of the expression, so it suppressed nothing and was itself reported as an unused suppression. Moved. Type-erasure warnings in this package are now 11, against 16 on main. Claude-Session: https://claude.ai/code/session_01AwM5Cm5ddasXozb6stxPR1 * Address James's review: never resolve an ambient URL a build cannot use Blocking fix. `readDatabaseUrlFromEnv()` was consulted on the edge entry too, where `introspector` is null by construction, and the refusal keyed on the RESOLVED url — so an ambient `DATABASE_URL` made a declared-mode client throw "drop `databaseUrl`" at a caller who passed none. Unactionable (nothing in their code fixes it) and ordinary rather than exotic: every runtime exposing `process.env` reaches it, and `DATABASE_URL` is the variable a Supabase project already has. - The ambient read is gated on `introspector`; the refusal keys on `options.databaseUrl`, so it can only fire for an option someone wrote. - The no-schemas error branches per build: telling an edge caller to set `DATABASE_URL` sent them to a variable that build cannot read. - Four regression tests that SET the variable. Both suites previously pinned the ENVIRONMENT to keep declared mode working — the Deno test asserted `DATABASE_URL` was unset — which routed around the coupling instead of testing it. That assertion now sets the variable and expects construction to succeed anyway. Verified the new tests fail against 83179d5. Also from the same review: - `packages/stack-supabase/turbo.json` makes `test` depend on this package's own `build`. Without it `turbo run test` never built the package, so the emitted-bundle edge-safety gate reported 4 skipped and a regression putting the native engine back in the edge graph would have shipped green. Same fix and same reason as packages/stack/turbo.json (#799). - Reverted the `pg` peer floor to `>=8`. The `>=8.16.3` bump was for pg-cloudflare, which this PR does not wire up (#808), and the edge entry carries no `pg` at all — so it narrowed a range for every consumer and bought nothing reachable today. - Dropped the dead `introspection && introspector` conjunct. Claude-Session: https://claude.ai/code/session_01AwM5Cm5ddasXozb6stxPR1
Plan 1 of the portable
encryptedSupabaseseries (docs/superpowers/plans/2026-07-24-edge-safe-shared-seam.md). Two independent defect fixes in the shared seam every first-party adapter's Worker / Edge build imports. Both prerequisites for the rest of the series; neither needs any of it.Base:
remove-v2(the EQL v3 integration branch this line of work targets), notmain— the INDEX notes plan 1 defers its CI gate to plan 4 and the four plans ship as one PR, so this should not land standalone againstmain.Fix 1 —
@cipherstash/stack/adapter-kitimportable without aprocessglobalThe shared core logger read
process.env.STASH_STACK_LOGunguarded while initialising at module scope, so importing adapter-kit — which re-exports that logger — threwReferenceError: process is not definedbefore any user code ran, on any runtime with noprocess(Workers, Deno isolates, Supabase Edge Functions). The read is now guarded.Not Supabase-only:
@cipherstash/stack-supabase,@cipherstash/stack-drizzleand@cipherstash/prisma-nextall value-import adapter-kit, so all three are fixed here.No behaviour change on Node — the logger is guarded, not deferred:
STASH_STACK_LOG, its values, itserrordefault, and the point at which it is configured are unchanged.New regression gate:
packages/stack/__tests__/helpers/process-free-realm.mjsevaluates an emitted ESM graph in anode:vmrealm with noprocess;logger-edge-safety.test.tsspawns it againstdist/adapter-kit.js(skips whendist/is absent).packages/stack/turbo.jsonmakestestdepend on the package's ownbuildso the gate can't silently skip in CI.Fix 2 — recognise EQL v3 columns structurally, not by class identity
ColumnMapgated onbuilder instanceof EncryptedV3Column. tsup emits that class twice (dist/adapter-kit.jsanddist/wasm-inline.jsare separate esbuild runs), so a table authored withencryptedTable/typesfrom@cipherstash/stack/wasm-inline— what the edge examples use — failed theinstanceoffor every column.v3Columnscame out empty and the adapter treated encrypted columns as plaintext: filter operands reached PostgREST in the clear, while::jsonbcasts and result decryption kept working (different path), so the failure was silent. Tables authored from@cipherstash/stack/eql/v3were never affected — same class copy the adapter imports.The check is now a structural four-method probe (
isV3ColumnLike), mirroringhasBuildColumnKeyMap— the repo's canonical answer to the same two-copies-of-a-class hazard. Four probes, not two, so a v2 column builder is still rejected.Tests
@cipherstash/stack-supabase: 466 → 489. The two fixes above added 5 (structural-recognition, v2-rejection, wire-level plaintext-leak, fail-closed-at-construction, and a deterministic full-types.*-catalog completeness guard); review feedback added 18 more — see below.@cipherstash/stack-supabase test:types: 44, no type errors.dist/adapter-kit.js.Changesets
@cipherstash/stack: patch (logger guard) + minor (newadapter-kitexport)@cipherstash/stack-supabase: patch × 2stash: patch (skill update)Review feedback addressed
Three commits on top of the original two fixes. No changeset: test-only, plus one
exporton a module-private function that does not reach the published surface, plus repo tooling.Assertions pinned to the fail-closed message (
02b1734d). Both.toThrow(/\[supabase v3\]/)matchers were prefix-only, and 32 messages in this package satisfy that prefix — two thrown byColumnMapitself. Measured rather than assumed: withassertNoPropertyDbNameCollisionthrowing unconditionally so the fail-closed probe is bypassed, both tests stayed green. They now pin the identity clause and both interpolations, and the same bypass mutant fails both. Extended tosupabase-v3-wire.test.tsas well as the flagged site — it is the one guarding the harm (no query string emitted), not just the mechanism.isV3ColumnLikeunit-tested one member at a time (d34a16b6, new__tests__/column-map-predicate.test.ts, +16 tests). No existing test could distinguish a four-probe gate from a two-probe one: every builder reaching the predicate either satisfied all four probes or missed two at once, so nothing was ever one member away from passing. A mutation harness confirmed nine surviving mutants — deleting any single conjunct, weakening any singletypeoftotrue, or dropping the object/null guard all left the 471-test suite green. All nine now die, and precisely: a conjunct deletion fails exactly its two tests, atypeofweakening exactly its one, and the guard's two halves are split so a mutant naming one half fails 1 test vs 4.The predicate is exported to allow this. Verified not to widen the published surface:
dist/index.d.tsand.d.ctscontain no mention of it, and both bundles' runtime exports remain exactlyencryptedSupabase, encryptedSupabaseV3.Edge-safety harness reworded (
f503d8d0). A new bare specifier now reads as "allowlist it here" rather than "process-global regression" — the gate passes only because everything the adapter-kit graph reaches is bundled vianoExternal, which is load-bearing (the chunk it imports carries inlinedevlog). Also madeturbo.json'sdependsOnextend rather than replace the root's, and corrected a comment describing abundling-isolation.test.tsthat does not exist yet.No skill change in those three commits — none touches a public API, the CLI surface, or a user-facing workflow (Fix 3 below does, and updates
stash-supabaseaccordingly). Thestash-supabase"factory cannot run in a Worker" guidance remains accurate throughout:introspect.tsstill opens a directpgconnection, so it stays a re-check for the later portable-entry plan.Fix 3 — diagnose an EQL v2 table instead of crashing
Found while adding the predicate tests above, fixed on request.
A v2
EncryptedTableis structurally identical to a v3 one — sametableName, samecolumnBuilders— and onlybuildColumnKeyMap()tells them apart, so nothing inspecting shape caught the swap. Two paths each died with a rawTypeErrornaming an internal method rather than the version mismatch:encryptedSupabase({ schemas })with v2 tables — the realistic mistake when migrating — sailed past the record-key check and died inverifyDeclaredSchemasasbuilder.getEqlType is not a function.ColumnMap, on the constructor's first statement, astable.buildColumnKeyMap is not a function. Not reachable from the factory (mergeDeclaredTablesrebuilds a fresh v3 table), but symmetric with the column-level fail-closed guard already there — which cannot cover it, since it runs after the constructor has already crashed.Both now name the table and state the fix, and each guard is load-bearing: removing either puts its original
TypeErrorback.The check routes through
hasBuildColumnKeyMaprather than a second hand-written spelling, per that function's own doctrine. It is re-exported from@cipherstash/stack/adapter-kit— the first-party adapter seam this PR is about — and deliberately not promoted to./types: choosing a wire version is adapter plumbing, not end-user API. Verified edge-safe:dist/adapter-kit.jsstill evaluates in a process-free realm with no bare specifiers (OK 14 exports).skills/stash-supabasegains the error under "Legacy: EQL v2", where a migrating reader will hit it.Notes for review
A prior
/code-reviewpass flagged a "duplicateconst v2" (verified false positive — one block-scoped declaration,tscclean) and a probabilisticfc.constantFromsample in the catalog test (applied — swapped to a deterministic loop overObject.keys(types)for guaranteed full-catalog coverage, dropping the lonefast-checkimport).