docs: EQL v3 as the sole documented approach#658
Conversation
…flakes The Supabase and Drizzle EQL v3 integration jobs bring up docker-compose stacks that bind fixed host ports (55430 / 55432 / 55433) on shared Blacksmith runners, with no concurrency guard. Two jobs binding the same port on one host fail at `docker compose up` with "address already in use" — a transient flake unrelated to the code under test (e.g. the Supabase job on #586). Add a job-level concurrency group keyed by the compose variant (`integration-live-db-<db>`, NOT the ref, since two PRs contend for a host port as much as two pushes to one PR). The Drizzle `supabase` leg shares the Supabase workflow's key, so those two queue instead of colliding on 55430/55433; the Drizzle `postgres` leg (55432) keeps its own key and still runs in parallel. `cancel-in-progress: false` queues — a live-DB job sharing one ZeroKMS workspace should finish, not be cut off. Also add a pre-`up` `docker compose down -v --remove-orphans || true` to clear any container leaked onto a reused runner by a hard-killed prior run that skipped its teardown.
Add anonymous, opt-out usage analytics to the stash CLI, plus a `stash telemetry [status|enable|disable]` command to manage it. - Coarse events only (command, version, OS/arch, success, duration); a property allowlist at the emitter boundary makes it impossible for plaintext, schema, table/column names, connection strings, or argument values to leave the machine. - Opt-out by default, Homebrew-style: nothing is sent until the one-time first-run notice has been shown once. Four opt-out gates in precedence order: DO_NOT_TRACK, STASH_TELEMETRY_DISABLED, CI auto-detect, and the persisted `stash telemetry disable` flag in ~/.cipherstash/telemetry.json. - Events go through a first-party Cloudflare proxy (infra/telemetry-proxy), so a future US->EU PostHog move is a proxy-target change with no CLI re-release. posthog-node, events-only (no person profiles), lazy client, flush bounded by a timeout so it never blocks or slows the CLI. - Ships dormant: no events are sent until a real PostHog key is embedded at release (STASH_POSTHOG_KEY overrides for testing). Updates the stash-cli skill and adds a changeset. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…n-guard ci(integration): serialize live-DB jobs to stop host-port collisions
Review findings: - shutdownTelemetry: clear the flush-timeout timer so a fast flush no longer leaves a dangling setTimeout that hangs the process for the full timeout; also set fetchRetryCount:0 so an unreachable endpoint fails fast instead of retrying with backoff (which kept internal timers alive and hung the CLI for seconds). Enabled commands now exit in ~140ms even when the endpoint is down. - First-run notice: persist noticeShownAt ONLY when the notice was actually displayed (stderr is a TTY), so a non-interactive first run no longer consumes the freebie and starts sending without the user ever seeing the disclosure. - Broaden DO_NOT_TRACK / STASH_TELEMETRY_DISABLED to opt out on any non-empty value except 0/false (matches the DO_NOT_TRACK convention). - Reuse the shared isCiEnv() from config/tty.ts instead of a second, divergent CI detector; widen isCiEnv() with provider markers (GITHUB_ACTIONS, GITLAB_CI, Azure TF_BUILD, Jenkins, TeamCity, …) so telemetry never fires in CI even when a provider doesn't set CI=true. Export CI_ENV_VARS and make the region / database-url tests hermetic against those ambient vars. - Drop the dead resolveStatus() re-resolve after writing the notice. Copilot: - Sanitize the combined event+base props so the allowlist is the single enforced boundary (a future baseProps field can't bypass it). - Make the first-run opt-out hint runner-aware (npx stash …) so it's actionable before stash is on PATH. - Restore process.env.HOME in the state test so it can't leak across test files. - Worker returns 500 when POSTHOG_HOST is unset instead of forwarding to an invalid host. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
feat(stack,cli): EQL v3 typed schema, typed client, Supabase/Drizzle integrations on eql-3.0.0 + protect-ffi 0.29
Widening isCiEnv() with provider markers (GITHUB_ACTIONS, GITLAB_CI, …) made the pty e2e harness leak this repo's own CI env into the spawned CLI: the harness defaults CI=true and interactive tests override with `CI: ''`, but that no longer fully de-CIs the environment once GITHUB_ACTIONS=true is also present, so `auth login` skipped the region prompt and the test timed out. Strip every CI signal (CI_ENV_VARS) from the child env before applying the harness's CI default and per-test overrides, so CI detection is fully controlled by the harness — matching how the unit tests were made hermetic. Verified the full e2e suite passes with GITHUB_ACTIONS=true CI=true exported. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Enter changesets pre mode with the `rc` tag and add a major changeset for the Stack 1.0 family: @cipherstash/stack, @cipherstash/stack-drizzle, @cipherstash/stack-supabase, and stash. The changesets action will produce a Version Packages PR at 1.0.0-rc.0 for these; merging that PR publishes under the `rc` npm dist-tag (latest stays on the current stable). Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
chore: enter pre-release mode (rc) → Stack family 1.0.0-rc.0
Version Packages (rc)
Add ops.selector(col, '$.path') to the v3 Drizzle integration (#623): comparison methods (eq/ne/gt/gte/lt/lte) bound to a JSONPath into a types.Json column, emitting `eql_v3.<op>(eql_v3."->"(col,'<sel>'), eql_v3."->"('<needle>'::eql_v3_json,'<sel>'))`. Its unique power over `contains` is ORDERING at a path (col->'$.age' > 21). - Selector hash from encryptQuery(path, searchableJson) (string needle → ste_vec_selector). - Comparison entry from a STORAGE encrypt of {path: value} — its ciphertext `c` is required by the eql_v3_jsonb_entry domain (a query term can't carry it). - One new dialect fn (selectorEntry); reuses equality/comparison for the compare. - v1: dot-notation object paths; array/wildcard rejected with a clear error. Core needs no change. Supabase is a follow-up (#650) — blocked by PostgREST's inability to cast a filter value. Refs #623 Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…aint
Rework the selector path to be entirely ciphertext-free (the storage-payload RHS
was wrong — it leaked ciphertext into the WHERE clause). Both operands are query
terms; the comparison reduces to the encrypted term on each side:
eql_v3.<term>(eql_v3.jsonb_path_query_first(col,'<sel>')) <cmp> eql_v3.<term>(<operand>)
where <term> is eq_term (=/<>) or ord_term (</<=/>/>=) — both collapse to
bytea (hmac_256 / ope_cllw) and compare natively.
- selector hash: encryptQuery(path, searchableJson) (string → ste_vec_selector).
- RHS: a scalar encryptQuery(value, { queryType: equality|orderAndRange }) term
cast to eql_v3.query_<T>_<eq|ord>, T inferred from the leaf value's JS type.
- Drops the client `encrypt` requirement and the doc reconstruction entirely.
Assumes protect-ffi keys the scalar query term with the column's key (verified by
the live tests). The bundle lacks a (jsonb_entry, query_<T>) operator, so the
direct-function form is used; adding it upstream is an EQL follow-up.
Refs #623
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
The ciphertext-free approach was non-functional: encryptQuery can't mint a
scalar/ordering query term against a ste_vec (JSON) column, so the value call
threw "Index type not configured" (caught by CI's live integration job).
Verified via FFI probes: the stored ste_vec entry at the JSONPath selector
carries c + op (ordering) / hm (equality), and encryptQuery('$.path') returns
the selector that matches it. So the RHS is a STORAGE encryption of {path: value}
whose entry is extracted with jsonb_path_query_first and compared entry-vs-entry
(eq_term/ord_term read only hm/op). Ciphertext-free ordering needs a protect-ffi
change (cipherstash/protectjs-ffi#137); until then the value's ciphertext is in
the WHERE clause — documented inline.
Also from the code review: use Promise.all for the two independent encrypts;
throw (not silently mis-bind) if the selector term isn't a bare string; fix path
normalization (leading $/.) and reject the empty/root path; drop the fragile
JS-type→query-domain inference entirely.
Refs #623 · protect-ffi gap cipherstash/protectjs-ffi#137 · EQL operator #404
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Update stash-encryption + stash-drizzle skills for the new ops.selector(col,
'$.path').{eq,ne,gt,gte,lt,lte} surface — they previously said JSONPath selector
queries were 'not yet implemented'. Clarify that direct eq/gt/asc on a Json
column still throw; selector-with-constraint is the path-scoped form, with
ordering-at-a-path its unique power over containment. Skills ship in the stash
tarball, so this carries a stash patch changeset.
Refs #623
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
- reconstructSelectorDocument: use null-prototype objects so a `__proto__` path
segment becomes an own key instead of invoking the prototype setter (which
would mis-serialize the needle → no matches).
- Fix stale selectorCompare JSDoc: it still described the reverted ciphertext-free
approach ('No ciphertext, no storage encryption') and referenced the renamed
selectorConstraint. Now documents the storage-needle interim consistently.
- Disclose the interim ciphertext-in-WHERE behavior in both stash-drizzle and
stash-encryption skills (each must be self-sufficient).
Skipped: CodeRabbit's DROP-TABLE-IF-EXISTS suggestion — the sibling
json-contains integration test uses the same DROP+CREATE pattern and the RUN
discriminator isolates rows; hardening should be repo-wide, not one file.
Refs #623
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Must-fixes:
- test:types was broken (operators.test-d.ts): adding the required `encrypt`
member to OperandEncryptionClient made the `{ encryptQuery }`-only doubles
unassignable. Add `encrypt` to both doubles — and to the `erased` double too,
so it's rejected ONLY for the #622 encryptQuery-erasure reason (restoring the
guard's teeth), not for a missing member.
- Re-add the scalar-leaf-type guard dropped in the storage rewrite: reject
non-scalar leaf values (object/array → use contains()) and ordering a boolean,
up front as EncryptionOperatorError instead of a deferred opaque DB error.
- Define + test + document `ne` absent-path semantics: a row lacking the path is
now INCLUDED in `.ne(x)` (OR jsonb_path_query_first(...) IS NULL); eq/ordering
keep SQL's exclude-on-missing. Integration test seeds a row with no `$.age`.
CI gap (root cause the type break slipped through): the adapter packages'
`test:types` was never wired into CI after the #627 split — only @cipherstash/stack
and test-kit ran. Add stack-drizzle + stack-supabase type-test steps to tests.yml
(both green today).
Toby: path-validation errors now surface as EncryptionOperatorError with column
context; add unit tests for the pure helpers (parseSelectorSegments,
reconstructSelectorDocument) + the selector guards.
Also from freshtonic: harden path parsing — reject `..`/malformed/empty-segment
paths (no longer silently query a different path) and prototype-pollution keys
(__proto__/prototype/constructor); surface the interim ciphertext-in-WHERE caveat
in the skill capability-table row.
Follow-ups (noted, not in this PR): share core's path helpers via adapter-kit;
memoize the value-independent selector hash per (column, path).
Refs #623 · protect-ffi gap cipherstash/protectjs-ffi#137
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Symmetric with STASH_POSTHOG_KEY: the host now defaults to the Cloudflare proxy (telemetry.cipherstash.com) but can be overridden for testing against a real PostHog ingestion endpoint before the proxy is deployed, or for self-hosting. Verified the CLI POSTs the event batch to the configured host. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Add a coarse, fixed-enum `caller` property to command telemetry so we can gauge how much stash usage is agent-driven — a lot of it is our own skills being run by a customer's coding agent. resolveCaller() recognises Claude Code (CLAUDECODE / CLAUDE_CODE_ENTRYPOINT), Cursor (CURSOR_TRACE_ID), and Codex (CODEX_SANDBOX) by the env var each sets, falls back to `editor` for a VS Code-family terminal (TERM_PROGRAM=vscode), then to `interactive`/`non-interactive` by stdin TTY. Only the fixed label is emitted, never the raw env value — some carry session/trace ids — so the anonymity contract holds. `caller` is added to the emitter allowlist. Updates the stash-cli skill disclosure and the changeset field list to match. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…repo The Cloudflare Worker that terminates telemetry.cipherstash.com is deployed platform infrastructure, not part of the published stash package, so it belongs with the rest of our infra in cipherstash-suite (infra/telemetry-proxy) rather than shipping in this repo. No CLI behaviour changes: the CLI still targets https://telemetry.cipherstash.com and stays dormant until a key is embedded. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Wire up the injection the placeholder always promised. tsup's esbuild `define` replaces the `__STASH_POSTHOG_KEY__` identifier with the key from the STASH_POSTHOG_KEY env var; release.yml feeds it from a public repo *variable* (vars.STASH_POSTHOG_KEY), so ONLY the official release build embeds a key. Dev, fork, and CI builds bake in an empty string and ship telemetry-dormant. Applied to both bundles; turbo.json lists the var on `build` so the cache key tracks it. Also fixes a latent bug the injection would have exposed: resolveStatus compared the resolved key against EMBEDDED_KEY, which once injected holds the real key — so an embedded build with no env override would have read as `unconfigured`. It now compares against a dedicated PLACEHOLDER_KEY sentinel (the un-rewritten string literal), so an injected build is correctly enabled. New tests cover the placeholder-passthrough (dormant) and injected-key (enabled) paths. The go-live switch is setting the repo variable; until then every release stays dormant. Verified both ways: dormant build reports "not active in this build", injected build reports "Telemetry is enabled" with no runtime env override. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
Ten findings from the high-effort review, all fixed:
1. subcommand value leak (privacy): trackCommand emitted argv[1] verbatim, so
`stash wizard "<prompt>"` sent the natural-language prompt (table/column
names) to PostHog. New classify-command.ts coerces command/subcommand to the
registry's known verbs before emit; anything else becomes `<other>`.
2. non-TTY never activated: the first-run notice only persisted noticeShownAt on
a TTY, so agents/piped callers stayed firstRun forever and never sent — the
exact population the `caller` dimension measures. Now the disclosure is shown
(to stderr) and persisted on every first run, TTY or not.
3. process.exit bypassed the tracking finally: commands exiting via process.exit
emitted nothing and never flushed. run() now intercepts process.exit with an
ExitSignal sentinel that unwinds through the finally (track + flush) before
the real exit, honouring the exit code. Degrades gracefully if a command
catch swallows it.
4. empty-string key enabled telemetry: STASH_POSTHOG_KEY='' slipped past the
nullish fallback; projectKey() now trims and treats empty as unset.
5. first-run notice could show twice: maybeShowFirstRunNotice didn't reassign the
state cache, so a same-run setTelemetryDisabled clobbered noticeShownAt. Fixed.
6. isCiEnv widening leaked into prompt gating: reverted the shared isCiEnv to its
narrow (CI-only) behaviour used by region/database-url; telemetry uses a new
isCiEnvBroad. Reverted the now-unnecessary test neutralizations.
7. eager cost on every invocation: posthog-node is now a dynamic import inside
getClient (loaded only when an event is actually sent), and the state file
read is lazy — both off the --version/--help fast paths.
8. shutdown could linger: pass requestTimeout to PostHog so a black-holed
endpoint can't keep the socket alive past the flush window.
9. dormant tests depended on ambient STASH_POSTHOG_KEY: clearGateEnv now stubs it.
10. lifecycle path was untested: new telemetry-lifecycle.test.ts covers the
emitter/flush contract (no-op when disabled/firstRun, swallows throws, safe
double-shutdown, non-TTY notice persistence, bounded flush on a hung endpoint)
and classify-command.test.ts covers the value allowlist.
Verified end-to-end against a local capture: a process.exit command is now
tracked with its exit code preserved, and a wizard prompt containing a
table.column is scrubbed to `<other>` (0 leakage). 453 unit tests pass.
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
… harden telemetry per review round 2 The round-1 fix intercepted process.exit globally with a thrown signal. The second review pass proved that unsafe: @clack/core calls process.exit(0) from a keypress handler (Ctrl+C during any spinner became an uncaughtException crash), and five broad catches (init's install-eql, plan, status, impl, the jiti config loader) swallowed the signal — init continued past an explicit cancel, impl skipped the cutover deploy-gate, and commands that recovered by design exited with a stale latched code. New design: no global interception. First-party sites verified to unwind cleanly to run() throw `CliExit(code)` (new cli/exit.ts) — main.ts's own unknown-command/subcommand defaults, requireValue, requireStack, the telemetry command, and the outermost CancelledError handlers of init/plan/impl (cancels are now tracked). Deep process.exit sites revert to pre-branch behaviour: immediate, uninterceptable, untracked — documented as a known measurement gap in the telemetry module doc and packages/cli/AGENTS.md. Also fixed from the review: - shutdownTelemetry: the capture enqueue (containing the dynamic posthog-node import) now sits INSIDE the Promise.race, so the 1500ms bound covers everything that can stall; the flush promise carries its own rejection handler so a timeout win can't leave an unhandledRejection. - First-run notice persists BEFORE printing: an unwritable HOME no longer prints the notice on every run forever — no persist, no print, no telemetry. - pty e2e harness sets STASH_TELEMETRY_DISABLED=1 so an ambient STASH_POSTHOG_KEY can't make tests send real events against the developer's real ~/.cipherstash. - errorType is now a closed vocabulary (classifyErrorType): user-defined error class names from jiti-loaded config code collapse to `<other>`. - Wizard analytics aligned with the CLI's privacy contract: honors DO_NOT_TRACK / STASH_TELEMETRY_DISABLED / CI, random per-session id instead of sha256(username@hostname), geoip off, and error events carry fixed labels / class names instead of raw messages. - Docs de-staled: packages/cli/AGENTS.md telemetry section rewritten (it said the CLI had no telemetry), CI_ENV_VARS/resolveStatus/pty.ts comments now describe the isCiEnv (narrow, prompts) vs isCiEnvBroad (telemetry) split, and the changeset names the value-coercion boundary. - Tests: narrow-vs-broad isCiEnv contract pinned; classifyErrorType covered; notice write-failure covered; stderr.isTTY save/restore no longer creates a read-only own property; smoke e2e covers `telemetry status` and the bogus-subcommand CliExit path. Verified: 460 unit + 57 pty e2e tests pass; local capture shows unknown command and telemetry bogus-sub tracked with correct exit codes, wizard free-text still scrubbed to `<other>` (0 leakage), and a read-only-HOME first run prints nothing. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
CodeRabbit flagged that the SKILL.md disclosure lists event fields but never mentions the distinct_id. Add it — with accurate wording: it is a locally generated random UUID stored in ~/.cipherstash/telemetry.json, used only to de-duplicate events in aggregate, and NOT a salted/derived machine id (the review's suggested phrasing described a mechanism we deliberately don't use). Mirrored in the changeset so the release notes carry the same disclosure. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
feat(cli): anonymous opt-out usage analytics + `stash telemetry`
…electors) Closes #650. The v3 Supabase adapter gains encrypted-JSON querying on types.Json columns: - contains(col, subDocument) — exact encrypted ste_vec containment. A live PostgREST probe established the wire form: PostgREST casts the `cs.` filter operand to the column's own domain, so a storage-encrypted needle reaches eql_v3."@>"(eql_v3_json, eql_v3_json) → ste_vec_contains. The operand is a full storage envelope, the same INTERIM shape (and GET-query-string caveat) as every other v3 filter operand. - selectorEq/selectorNe(col, path, value) — JSONPath equality at a dot-notation path, compiled to containment of the reconstructed needle ({user:{role:'admin'}}); ne compiles to not.cs and INCLUDES rows lacking the path, mirroring the Drizzle selector's documented semantics (verified live). Paths/leaves share the Drizzle adapter's validation. - Selector ORDERING is not expressible over PostgREST: arrow paths get to_jsonb-wrapped (domain stripped) and bare-column comparison is blocked by design. Filed cipherstash/encrypt-query-language#407 proposing a needle-comparison overload; until then the methods don't exist and the skill points at Drizzle's ops.selector() for ordering. Core: QueryTypesForColumn gains the searchableJson arm (types.Json no longer resolves to never, so the typed key sets stop erasing JSON columns), and the JSONPath selector-path helpers move from stack-drizzle to @cipherstash/stack/adapter-kit (drizzle re-exports them unchanged) so both adapters share one validation surface. Adapter plumbing: V3SearchableJsonKeys key set + typed contains overload + selector methods on both builder interfaces; the capability resolver re-types the cs-collected term to searchableJson on JSON columns (free-text and JSON capabilities are mutually exclusive by construction); matches() steers to contains()/selectorEq() on JSON columns; not(col,'contains',…) is allowed on JSON (honest negated exact containment) and stays rejected on text. Tests: 17 unit (operand routing, guards, path validation), 6 type-level (key-set narrowing, operand shapes), 11 live integration with real crypto against real PostgREST as anon (containment, selector eq/ne incl. absent-path inclusion, raw cs, capability rejections). Full suites: stack 816, drizzle 368 unit + 16 live JSON, supabase 468 unit + 677 integration — all green. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…uerying Ten findings from the high-effort review of #653, all fixed: 1. Guard bypass on non-method spellings: the freeTextSearch→searchableJson re-typing now enforces the operand boundary AT THE RESOLVER — raw .filter(col,'cs',string), .or() string/structured conditions, and not(col,'contains',scalar) get the actionable sub-document steer instead of silently storage-encrypting a JSON scalar; not(col,'matches',…) on a JSON column gets the same steer matches() gives. 2. Type-surface regression: scalar predicates (eq/gt/in/…) are compile-excluded again for types.Json columns (V3FilterableKeys now excludes columns with no scalar capability); dedicated filter('cs')/not('contains') overloads keep the raw spellings typed. Pinned with @ts-expect-error tests. 3. Empty/non-plain needles: contains('payload', {}) and [] are rejected ("matches every row" — Drizzle parity), and Date/Map/RegExp instances get the sub-document steer instead of serializing to scalars or {}. 4. Array-leaf semantics: the live probe showed finder-claimed ANY-element matching is WRONG with real protect-ffi — a scalar needle does NOT match an array at the path (elements are encoded under their own selectors). Docs corrected to the observed truth and pinned by live fixtures (scalar needle misses the array row; the full array through contains() hits it). 5. SQL-NULL documents: selectorNe now compiles to a structured OR (payload.is.null, payload.not.cs.<needle>) so NULL-document rows are INCLUDED, matching Drizzle's ne for both absence cases — verified live. 6. Integration test: removed the phantom two-arg from(TABLE, schema) (v3 from() takes one argument) and the identity casts; tsc over integration/ is clean. 7. Exposure honesty: the skill now states the JSON-needle ciphertext scales per-node (Drizzle containment ships none) and cites the new tracking issue #654; empty-needle rejection documented. 8. Changeset gains the required 'stash': patch for the skills change. 9. stash-encryption skill now names the Supabase route beside Drizzle's for both containment and selectors (with the ordering caveat). 10. Live decrypt round-trip added (select payload → toEqual original doc), plus live not-contains, and the unit not-contains asserts the encrypted envelope. Also from below-the-fold: the INTENTIONAL FORK JSDoc re-attached to EncryptionOperatorError (the re-export had split them); parseSelectorSegments rejects whitespace at segment boundaries (was silently addressing 'user '); unsupportedLeafReason handles null explicitly (was steering null to contains()); the as-unknown-as double-cast replaced with a plain widening; dead V3ContainsValue removed; core's matrix test-d pins QueryTypesForColumn<Json> = 'searchableJson'; stale "three scalar kinds" comment fixed. Suites: supabase 476 unit + 31 type + 681 live integration (15 in the JSON suite incl. NULL-doc and array-leaf fixtures), stack 62 type tests, drizzle 368 — all green. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…rface - SelectorLeafValue narrowed to the JSON scalar kinds (string|number|boolean): a stored JsonDocument cannot contain Date/bigint, so such a needle could never match a stored leaf. The runtime guard rejects them too, with a serialization steer (pass date.toISOString() / the stored form) — stricter than the shared unsupportedLeafReason, whose Date/bigint arms serve the Drizzle surface (its operand is unknown-typed). Pinned by @ts-expect-error type tests and unit tests. - Integration Row type declares payload: JsonDocument | null — the suite seeds a SQL-NULL document row, so the type must not promise non-null. - Removed the unreachable 'an array' branch from the containment operand error message (Array.isArray is false on that path by construction). Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
The bullet still said order() is rejected on every encrypted v3 column — the supabase-v3-order-by-ope-term change made that false: OPE-backed ordering columns sort via order=col->op (ORE and term-less columns stay rejected). Caught while auditing ordering coverage; the skill ships to customers, so the stale claim was steering users away from a capability they have. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
feat(stack-supabase): EQL v3 encrypted-JSON querying (containment + selector predicates)
Rewrites stash-encryption, stash-drizzle, and stash-supabase skills and the stack README so the EQL v3 typed surface (EncryptionV3, types.* concrete domains, stack-drizzle/v3, encryptedSupabaseV3) is the only approach taught. EQL v2 shrinks to one short Legacy section per document (the API remains for existing deployments; docs-site link), with two explicit carve-outs: - DynamoDB still requires the v2 schema surface — noted, tracked in #657. - The encrypt rollout lifecycle (stash encrypt backfill/cutover, @cipherstash/migrate) currently targets v2 columns — sections kept (launch-critical) under a #648 callout that scopes their eql_v2_* names. Also fixes the legacy packages/drizzle README still recommending the removed @cipherstash/stack/drizzle subpath (now names @cipherstash/stack-drizzle), the README's stale sorting-not-supported claim (superseded by ord_term / OPE-backed order()), the stale ops.contains free-text guidance (matches), and Node >= 18 → >= 22 in Requirements. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
…oach # Conflicts: # skills/stash-drizzle/SKILL.md # skills/stash-encryption/SKILL.md # skills/stash-supabase/SKILL.md
|
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 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 |
🦋 Changeset detectedLatest commit: 3fdd740 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 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 |
Fixes from the #658 review (10 verified findings + below-the-fold): Correctness: - Quick Start + mid-doc examples now narrow the Result union before reading .data (failure branches throw) — they previously failed strict TS (TS2339). - Supabase EQL install: docs now show `--supabase` (grants) and stop implying it is obsolete; without it encrypted queries hit permission-denied. - Removed the false "Supabase cannot ORDER BY encrypted columns" claim from stash-encryption (it contradicted stash-supabase, the README, and the shipped OPE-backed order()). - #648 rollout dead-end fixed: the Supabase skill now EMBEDS the interim v2-encrypted-twin recipe (schema + encryptedSupabase + dual-write) that backfill/cutover need today, instead of pointing at a deleted section. Reference rot & stale claims: - Removed the #602 "known type error" callout (fixed by protect-ffi 0.29; #602 closed) from stash-encryption; corrected the stale #447 citation in all three skills; fixed the "EQL v3 Typed Schema" dead cross-reference and the generate-eql-migration misattribution in stash-drizzle. - stash-cli: --eql-version default 2→3; corrected the Supabase ORDER-BY and read-path (encryptedSupabaseV3) claims. - AGENTS.md Key Concepts now teaches the v3 typed surface (rule 7). Scoping corrections: like/ilike (matches, with the untyped-only shim noted), empty-needle rejection (adapter-level, not core encryptQuery), SQL-NULL ne inclusion (both adapters), Supabase selector leaf types (JSON scalars only), null-operand handling (forwarded, not rejected), per-column (not per-query) ZeroKMS batching, #137-vs-#654 exposure attribution, drizzle operator opts (async encrypting ops + selector methods only). README polish: init flag table (--region etc.) + removed the redundant post-init eql install step; wasm-inline in Requirements; returnType values note restored; markdown `--`→`---` rules. Added the missing @cipherstash/drizzle changeset for its README fix. The doc claims about the Supabase short-needle guard, withLockContext({identityClaim}), and the StackError discriminated union are made true by the companion code PR (short-needle guard, LockContextInput widening, EncryptionErrorTypes as const) — both are RC-gated to ship together. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
a432545
into
docs/skills-identity-and-corrections-refresh
Stacked on #642 (base is its branch — retarget to
mainafter #642 merges; this branch also hasorigin/mainmerged in, so the #651/#653-era skill content is reconciled, verified marker-by-marker).What
EQL v3 becomes the only documented approach across the shipped docs surface:
skills/stash-encryptionskills/stash-drizzleencryptedTypebody, v3 section/v3surface throughout (625 lines)skills/stash-supabaseencryptedSupabaseV3throughout, incl. main's JSON-querying + ordering contentpackages/stack/README.mdEncryptionV3+types.*primary, TOC/anchors verifiedEach document keeps ONE short Legacy: EQL v2 section (the API remains for existing deployments; docs-site link) — no v2 examples or API tables elsewhere.
Deliberate carve-outs (as decided)
stash-dynamodbskill untouched).stash encrypt backfill/cutover,@cipherstash/migrate) — kept in full (launch-critical) under an explicit callout that the tooling currently targets v2 columns, tracked in Make @cipherstash/migrate (andstash encrypt *) compatible with EQL v3 #648.Also
packages/drizzleREADME recommending the removed@cipherstash/stack/drizzlesubpath → now points at@cipherstash/stack-drizzle.ord_termon Drizzle, OPE-backedorder()on Supabase, ORE-only limitation); staleops.containsfree-text guidance →matches; Node >= 18 → >= 22.stash manifest --json; all main-side v3 content (selectors, JSON querying, absent-path/array-leaf semantics, EQL v3 Supabase: JSON containment/selector operands carry full storage ciphertext in GET query strings #654/docs(plans): encryption-migrations follow-ups working doc #407/#137 references) verified present post-merge.stashpatch +@cipherstash/stackpatch (skills ship in the tarball; README ships with the package).https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w