Skip to content

docs(stack): refresh the stash-encryption skill; fix the identity-aware auth docs and a vacuously-passing test#598

Open
coderdan wants to merge 5 commits into
mainfrom
docs/stash-encryption-skill-refresh
Open

docs(stack): refresh the stash-encryption skill; fix the identity-aware auth docs and a vacuously-passing test#598
coderdan wants to merge 5 commits into
mainfrom
docs/stash-encryption-skill-refresh

Conversation

@coderdan

@coderdan coderdan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Why this skill

skills/stash-encryption/SKILL.md is the highest-blast-radius skill in the repo:

  • It's the only one stash init installs for all three integrations (SKILL_MAP — drizzle, supabase, postgresql).
  • stash-drizzle and stash-supabase both call it "the canonical reference for the lifecycle." The CLI's setup-prompt calls it "the source of truth for taking encryption to production."
  • It ships inside the stash tarball and gets inlined into users' AGENTS.md.

It was last substantively updated 2026-05-19, before the auth-strategy rename. packages/stack/src moved as recently as 2026-07-08.

Two of these are product bugs, not doc bugs

1. create() returns a Result that nobody unwraps

As of @cipherstash/auth 0.41, OidcFederationStrategy.create() and AccessKeyStrategy.create() return Result<Strategy, AuthFailure>. The envelope has no getToken():

$ node -e "console.log(Object.keys(OidcFederationStrategy.create(crn, fn)), typeof …getToken)"
[ 'failure' ]  undefined

Encryption() forwards config.authStrategy verbatim to the FFI (encryption/index.ts:166strategy: config.authStrategy), which then calls getToken() on it. Yet the JSDoc on Encryption() (:761, :779) and the live lock-context test (:41) all passed create(...) straight through.

TypeScript does reject this under the node condition:

error TS2322: Type 'Result<OidcFederationStrategy, AuthFailure>' is not assignable to type 'AuthStrategy'.

It survived because vitest doesn't typecheck, and the only test that exercises it never runs — see below. The /wasm-inline path already unwraps correctly (wasm-inline.ts:474-482); the Node path did not.

Fixed in the JSDoc, the test, the skill, and AGENTS.md (which also still named the deprecated config.strategy).

2. The lock-context live tests passed without running

Each test early-returned when USER_JWT was absent:

const userJwt = process.env.USER_JWT
if (!userJwt) { console.log('Skipping…'); return }   // ← reports as PASSED

So CI has been reporting four green assertions that never executed. That is exactly how the unwrap bug hid. They now skipIf out:

before:  Tests  4 passed          ← vacuous
after:   Tests  4 skipped         ← honest
with creds:  4 tests | 4 failed → "Failed to construct OidcFederationStrategy (INVALID_CRN)"

The last line is a fake CRN — the point is the unwrap path is now actually exercised.

Skill fixes

Identity-aware encryption, rewritten. The chapter taught new LockContext() + await lc.identify(userJwt) + a CS_CTS_ENDPOINT env var. Per-operation CTS tokens were removed in protect-ffi 0.25 (identity/index.ts:119), and CS_CTS_ENDPOINT is only read on that dead path. The current path is OidcFederationStrategy on config.authStrategy plus .withLockContext({ identityClaim }). The replacement snippet was extracted from the skill and typechecked verbatim under --strict --moduleResolution nodenext before shipping. LockContext is documented as the legacy path, not deleted.

Two copy-pasteable SQL bugs.

  • CREATE EXTENSION IF NOT EXISTS eql_v2; — there is no such extension. The install SQL does CREATE SCHEMA eql_v2 + CREATE TYPE public.eql_v2_encrypted. Anyone pasting this gets extension "eql_v2" is not available. The skill never mentioned stash eql install at all.
  • The fallback DDL declared email jsonb NOT NULL — which invariant 1 of the doctrine shipped alongside it explicitly forbids: "Never mark them NOT NULL at creation … a NOT NULL constraint will break inserts."

Post-cutover reads are not automatic. Same bug as #594. The skill said <col> decrypts "transparently" after stash encrypt cutover, then contradicted itself one table row later ("Read paths must decrypt … Without this step, reads return raw eql_v2_encrypted payloads to end users"). Transparent decryption is Proxy-only. Notably stash-drizzle and stash-supabase already stated this correctly — the canonical reference was the outlier.

Lifecycle corrections. Closed #447 → open #585. Documents stash db activate: an additive db push is promoted by db activate, not by cutover (push.ts:110-145), which the skill got wrong. Scopes db push/db activate as EQL v2 + CipherStash Proxy only.

API corrections. Adds the missing timestamp data type (the CastAs union has 8 members, the skill listed 7) and the SDK→EQL cast_as map; documents authStrategy and eqlVersion config; adds the wasm-inline and v3 / eql/v3 subpath exports; fixes the stale EncryptedFromSchema<T,S> generic → EncryptedFromBuildableTable<T,Table>; and calls out the two distinct failure shapes — encryption ops give a flat failure.message, auth strategies give failure.error.message.

Verification

  • The skill's identity snippet was extracted from the markdown and typechecked verbatim — clean under --strict --moduleResolution nodenext.
  • Both v3 subpath exports (./v3, ./eql/v3) were runtime-imported to confirm they resolve — after ./secrets (chore: remove the secrets skill and the leftovers from the secrets removal #595) I don't trust an exports entry without checking.
  • lock-context.test.ts now reports 4 skipped without credentials, and executes the unwrap path with them.
  • init-strategy (16) + lock-context-wiring (13) tests pass. Biome clean on the changed TS.
  • grep guards: no Linear IDs, no #447, no CREATE EXTENSION, no jsonb NOT NULL, no "transparently"; identify() appears only inside the Deprecated subsection.

Scope note

The skill grew 817 → 888 lines. Every added line is a correction or a documented-but-missing surface (cast_as map, authStrategy/eqlVersion, wasm-inline, EQL v3, the two failure shapes). I did not trim elsewhere to compensate; happy to if you'd rather hold the line on size.

EQL v3 is documented as available-behind-a-flag (main ships --eql-version 2), structured so #586/#588 landing needs additive edits only.

Summary by CodeRabbit

  • New Features

    • Added clearer guidance for identity-aware encryption setup, including updated auth strategy usage and rollout/cutover flow.
    • Expanded support notes for additional export paths and data type mappings.
  • Bug Fixes

    • Fixed live test handling so missing credentials are skipped instead of appearing to pass.
    • Corrected SQL and schema guidance for encryption-related database setup.
  • Documentation

    • Updated examples and references to match the latest API behavior.
    • Clarified error messages, Proxy-only behavior after cutover, and SDK wiring requirements.

…re auth docs

`skills/stash-encryption/SKILL.md` ships inside the `stash` tarball and
`stash init` installs it for *every* integration (Drizzle, Supabase, plain
PostgreSQL), so its errors reach every user. Both `stash-drizzle` and
`stash-supabase` name it "the canonical reference for the lifecycle", and the
CLI's setup-prompt calls it "the source of truth for taking encryption to
production". It was last substantively updated before the auth-strategy rename.

Two of the findings are bugs in the product, not just the docs.

**`create()` returns a `Result` that nobody unwraps.** As of `@cipherstash/auth`
0.41, `OidcFederationStrategy.create()` and `AccessKeyStrategy.create()` return
`Result<Strategy, AuthFailure>`. The envelope has no `getToken()`, and
`Encryption()` forwards `config.authStrategy` verbatim to the FFI, which calls
`getToken()` on it. The JSDoc examples on `Encryption()` and the live
`lock-context` test all passed `create(...)` straight through. TypeScript rejects
that under the `node` condition -- it went unnoticed because vitest doesn't
typecheck and the test never runs. Fixed in the JSDoc, the test, the skill, and
`AGENTS.md` (which also still named the deprecated `config.strategy`).

**The `lock-context` live tests passed without running.** Each early-returned
when `USER_JWT` was absent, so CI reported four green assertions that never
executed. That is how the unwrap bug survived. They now `skipIf` out: absent
credentials read as *skipped*, and with credentials present they execute and
fail loudly.

Skill changes:

- Identity-aware encryption rewritten. It taught `new LockContext()` +
  `lc.identify(jwt)` + `CS_CTS_ENDPOINT`; per-operation CTS tokens were removed
  in `protect-ffi` 0.25. Now `OidcFederationStrategy` on `config.authStrategy`
  plus `.withLockContext({ identityClaim })`. The new snippet typechecks under
  `--strict --moduleResolution nodenext`.
- Two copy-pasteable SQL bugs. `CREATE EXTENSION IF NOT EXISTS eql_v2` -- no
  such extension; `eql_v2` is a schema installed by `stash eql install` (which
  the skill never mentioned). And `email jsonb NOT NULL`, which the shipped
  agent doctrine forbids because it breaks inserts during a rollout.
- Post-cutover reads are not automatic. The skill said `<col>` decrypts
  "transparently", then contradicted itself one table row later. Proxy-only;
  SDK users must wire reads through the encryption client. (stash-drizzle and
  stash-supabase already had this right.)
- Closed #447 repointed at open #585. Documents `stash db activate` -- an
  additive `db push` is promoted by `db activate`, not by cutover. Scopes
  `db push`/`db activate` as EQL v2 + Proxy only.
- Adds the missing `timestamp` data type and the SDK->EQL `cast_as` map;
  `authStrategy` and `eqlVersion` config; the `wasm-inline` and `v3` subpath
  exports; and the two distinct failure shapes (`EncryptionError.message` vs
  `AuthFailure.error.message`).
@coderdan coderdan requested a review from a team as a code owner July 9, 2026 07:37
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 26ff5fd

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

This PR includes changesets to release 7 packages
Name Type
@cipherstash/stack Patch
stash Patch
@cipherstash/bench Patch
@cipherstash/prisma-next Patch
@cipherstash/basic-example Patch
@cipherstash/prisma-next-example Patch
@cipherstash/e2e 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 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@coderdan, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7099171e-eb4b-4b22-8b2f-2bfbfa6848c2

📥 Commits

Reviewing files that changed from the base of the PR and between a371dd9 and 26ff5fd.

📒 Files selected for processing (3)
  • packages/stack/__tests__/init-strategy.test.ts
  • packages/stack/__tests__/lock-context.test.ts
  • skills/stash-encryption/SKILL.md
📝 Walkthrough

Walkthrough

This PR refreshes identity-aware encryption documentation and examples to require unwrapping the Result returned by OidcFederationStrategy.create()/AccessKeyStrategy.create() before assigning to config.authStrategy, updates lock-context live tests to skip via describe.skipIf when USER_JWT is absent, and rewrites SKILL.md sections covering config fields, subpath exports, data types, error shapes, and EQL v2/v3/Proxy rollout guidance, alongside a release Changeset and updated API reference generics.

Changes

Stash-Encryption Skill and Docs Refresh

Layer / File(s) Summary
Lock-context live test gating and strategy unwrapping
packages/stack/__tests__/lock-context.test.ts
Introduces a LIVE flag from USER_JWT, wraps the test suite with describe.skipIf(!LIVE), removes per-test skip checks, and updates userClient to construct OidcFederationStrategy via create() and unwrap/throw on its Result.
Encryption library doc examples for Result unwrapping
packages/stack/src/encryption/index.ts, AGENTS.md
Updates JSDoc examples for AccessKeyStrategy/OidcFederationStrategy and AGENTS.md guidance to unwrap create()'s Result (strategy.data) before use, throwing on strategy.failure.
SKILL.md refresh: config, identity-aware encryption, error shapes
skills/stash-encryption/SKILL.md
Rewrites description, config field matrix (authStrategy, eqlVersion, keyset), subpath exports (wasm-inline, v3), dataType()cast_as mapping, identity-aware encryption walkthrough using OidcFederationStrategy, and clarifies EncryptionError vs AuthFailure error shapes.
SKILL.md EQL/Proxy rollout and API reference updates
skills/stash-encryption/SKILL.md, .changeset/stash-encryption-skill-refresh.md
Replaces CREATE EXTENSION eql_v2 guidance with stash eql install, expands EQL v2 vs v3 behavior, updates Proxy-only stash db push/activate rollout/cutover notes and invariants, updates encryptModel/bulkEncryptModels generics, and adds the release Changeset.

Estimated code review effort: 2 (Simple) | ~12 minutes

Possibly related issues

Possibly related PRs

  • cipherstash/stack#497: Builds on the same auth-strategy/lock-context flow changes updated here.
  • cipherstash/stack#562: Introduces the config.strategyconfig.authStrategy migration that this PR's docs/tests build on.
  • cipherstash/stack#568: Introduces the @cipherstash/auth 0.41 Result<AuthFailure> migration that this PR's Result-unwrapping updates depend on.

Suggested reviewers: tobyhede

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main doc refresh and the test fix without adding unrelated details.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/stash-encryption-skill-refresh

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.

Copilot AI 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.

Pull request overview

This PR refreshes the shipped stash-encryption skill (a bundled, user-installed artifact) to reflect current Stack auth/identity-aware encryption behavior, and fixes a related live test so it no longer “passes” without actually exercising the code path.

Changes:

  • Update skills/stash-encryption/SKILL.md to document authStrategy (formerly strategy), correct identity-aware encryption guidance, fix SQL examples, and clarify Proxy-only rollout steps.
  • Fix Encryption() JSDoc examples to unwrap OidcFederationStrategy.create() / AccessKeyStrategy.create() Results before passing to config.authStrategy.
  • Update the lock-context live tests to skipIf when credentials are missing and to unwrap the strategy Result before initializing Encryption().

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
skills/stash-encryption/SKILL.md Refreshes the published stash-encryption skill: auth strategy rename, identity-aware encryption, SQL fixes, lifecycle clarifications, and new surface/docs.
packages/stack/src/encryption/index.ts Fixes public JSDoc examples to unwrap auth strategy create() Results before wiring into Encryption().
packages/stack/tests/lock-context.test.ts Makes live lock-context tests skip honestly when creds are missing; updates strategy wiring to unwrap create() Result.
AGENTS.md Updates repo agent guidance to match config.authStrategy and Result-unwrapping requirement.
.changeset/stash-encryption-skill-refresh.md Adds a changeset for the shipped skill + doc/test fixes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/stack/__tests__/lock-context.test.ts Outdated
Comment thread packages/stack/__tests__/lock-context.test.ts Outdated
Comment thread skills/stash-encryption/SKILL.md
coderdan added 2 commits July 9, 2026 18:18
…t narrowing

All three review comments were valid. The first found the same class of bug this
PR is about, in a test this PR already touched.

**The negative test asserted nothing.** `decryptModel()` resolves to a `Result`;
it does not throw. The "should encrypt with context and be unable to decrypt
without it" case wrapped it in try/catch, so the catch never ran and the `expect`
inside never executed. That is the security-critical assertion in the file --
"the claim is required to decrypt" -- and it has been silently green. It now
asserts on `decrypted.failure`.

**The skip gate was narrower than the docstring.** `LIVE` checked only
`USER_JWT` and `CS_WORKSPACE_CRN`, while the suite also needs `CS_CLIENT_ID` and
`CS_CLIENT_KEY`. With partial credentials the suite ran and died at init instead
of skipping. Verified across all three states: none -> skipped, partial ->
skipped, full -> executes.

**The skill's identity snippet read `.data` without narrowing `.failure`.** That
is a type error, not just a style issue: `@byteslice/result`'s `Failure` branch
has no `data` property. Added the failure checks and a note on the contract.

Also answers a question raised on the PR: the bundler exclusion is still required
for the default entry (it loads the NAPI bindings). Shipping WASM added a
separate `@cipherstash/stack/wasm-inline` entry that needs no exclusion, at the
cost of a smaller client surface. Documented in Installation.
`config.authStrategy` cannot accept an `@cipherstash/auth` 0.41 strategy on the
Node entry. Their `getToken()` resolves a `Result<TokenResult, AuthFailure>`
envelope; protect-ffi's Node `AuthStrategy` declares
`getToken(): Promise<{ token: string }>`, and the NAPI binary reads `.token`
straight off the resolved value -- `strings` on the 0.28 binding still contains
`missing 'token' field` and `'token' field is not a string`, byte-identical to
0.27. protect-ffi's 0.28 Result-unwrap landed only in its WASM `newClient`
(`wasm-inline.ts:215`), so the Node path never got an adapter.

The bug is Stack's: protect-ffi's Node type matches its native behaviour, and
auth 0.41's Result API is its own contract. Stack is what promises the two
compose -- it re-exports auth's strategies, types `ClientConfig.authStrategy` as
protect-ffi's `AuthStrategy`, and forwards it verbatim to `newClient`. The fix
is to adapt at that boundary. Not attempted here; this PR is a docs refresh.

- Add a known-issue callout to the identity chapter. The snippet stays as the
  intended API, with the caveat that it does not typecheck or run on the Node
  entry today, and that `@cipherstash/stack/wasm-inline` is unaffected.
- Correct `init-strategy.test.ts`'s doc comment, which claimed the FFI shape is
  "the shape every `@cipherstash/auth` strategy satisfies". That stopped being
  true at auth 0.41, and the claim is part of why nothing caught this: the test
  hand-rolls a `{ token }` stub, so it stands in for the FFI's contract rather
  than for a real auth strategy.
coderdan added 2 commits July 9, 2026 18:41
…ype is stale

I got this wrong in 19b3ddc and said the Node runtime fails. It does not.

I inferred a runtime break from `strings` on the 0.28 NAPI binary, because
`missing 'token' field` and `'token' field is not a string` were still present.
That was an unsound test: those strings persist for the legacy bare-`{token}`
path. Diffing 0.27 against 0.28 for strings *unique* to the new code path shows
the opposite --

  reading 'failure' field threw    0.27: absent   0.28: present
  reading 'data' field threw       0.27: absent   0.28: present

-- and protect-ffi's own source (`crates/protect-ffi/src/lib.rs:537-575`) and
CHANGELOG confirm it: 0.28 added Result-envelope support "on both the Node
(Neon) and WASM auth paths", keeping bare `{ token }` for `<= 0.40` strategies.
The Rust was public the whole time; I reasoned from a binary instead of reading
it.

So the real defect is narrower and lives upstream: protect-ffi's exported
`AuthStrategy` type (`src/index.cts:461`) still declares
`getToken: () => Promise<{ token: string }>`, which its runtime outgrew. Its own
integration tests pin `@cipherstash/auth ^0.39.0`, so nothing there exercises
the 0.41 shape against the type.

- Reword the skill caveat: identity-aware encryption *works* on Node; only the
  assignment is a type error, with a cast as the interim workaround.
- Correct the `init-strategy.test.ts` note to match.
@coderdan

coderdan commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

Things should be addressed with this skill:

  1. Before the environment section, add a section for local development where the user (or agent) can sign in using npx stack auth login - this will store creds and a local key in their profile (no env vars required) - this relies on the AutoStrategy for auth which is the default when no strategy or config is provided (by far the easiest!)
  2. Update to be focused on EQL v3
  3. List the ORM specific skills
  4. Split the migration stuff into its own skill (and reference it here)
  5. Reference the CLI skill

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.

Make agents less insistent on running stash db push when not using Proxy

2 participants