Skip to content

feat(cli): add the stash-postgres and stash-edge skills — raw-SQL predicates and the WASM entry#777

Open
coderdan wants to merge 4 commits into
remove-v2from
feat/stash-sql-edge-skills
Open

feat(cli): add the stash-postgres and stash-edge skills — raw-SQL predicates and the WASM entry#777
coderdan wants to merge 4 commits into
remove-v2from
feat/stash-sql-edge-skills

Conversation

@coderdan

@coderdan coderdan commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Closes #754.

Targets remove-v2 (#772), not main — so nothing here references the EQL v2 surface. Rebased onto a16302fc; see Alignment with the v2 removal for what that changed.

Problem

There is a stash-drizzle, a stash-supabase, and a stash-dynamodb. Nothing covered the integrations that don't use an ORM — hand-written SQL over pg / postgres-js, and the WASM entry on Deno / Supabase Edge Functions / Workers.

Grepping the skills stash init installs for postgres-js|::jsonb::eql|sql.json|query_text_search|query_numeric_ord|eql_v3.matches( returns one hit across all of them, in an unrelated code comment. An integration on that path had to recover the binding surface from dist/*.d.ts JSDoc, the Postgres catalog, and experiment.

Two skills, not one

Per the scope question in the issue: the raw-SQL predicate cookbook serves a supported plain-Node path (hono-pg — no Deno, no WASM, same binding problem), so scoping it under an edge-named skill would leave that surface uncovered. Same reasoning that split stash-indexing out rather than folding it into stash-encryption.

skills/stash-postgres/SKILL.md (422 lines) — the column-domain × query-domain operator matrix, storage-vs-query payload shapes, per-driver parameter binding, and recipes for equality / free-text / range / ORDER BY / JSON containment / JSON field selectors, plus the information_schema drift check.

skills/stash-edge/SKILL.md (398 lines) — import specifier per runtime, the four mandatory CS_* variables and minting them with stash env, how the WASM client surface differs from the native one, and why a schema module can't be shared across entries.

Both are single self-sufficient files, required for the Codex/editor-agent inline path.

Named stash-postgres, not stash-sql

The original name overclaimed. Nothing in the skill generalises to another SQL dialect: an encrypted column is a Postgres domain over jsonb, the driver rules cover pg and postgres-js, and the drift check reads information_schema.

stash-postgres also lines up with the CLI's postgresql integration key — the no-ORM path whose SKILL_MAP entry installs it — so the mapping now reads as intent rather than coincidence.

Renamed before release, so no shipped artifact carries the old name: there is no CHANGELOG entry for it, only the changeset in this PR, and no customer .claude/skills/ directory to leave stranded. The changeset is amended in place rather than adding a second one. git mv on both the skill and the changeset, so history follows through.

Scoped against Proxy, and cited to EQL upstream

Two things a hand-written-SQL reader was otherwise left to infer.

CipherStash Proxy. The skill assumed a direct connection with client-side encryption without ever saying so — and that is precisely the fork stash init puts to every user (--proxy / --no-proxy, stored as usesProxy in .cipherstash/context.json). The audience most likely to be on the Proxy side is exactly this one, since Proxy's pitch is "write normal SQL". For those readers every rule in the skill is wrong — no encryptQuery, no eql_v3.query_* cast, no payload to bind — and nothing said so. Now a scope callout up front, plus the lifecycle note: Proxy's config path (stash db pusheql_v2_configuration) is the EQL v2 one, while this skill is v3, where config lives in the column's domain and db push refuses outright.

EQL provenance. The operator matrix read as though the client library defined it. It does not — the domains, operators, CHECKs, and extractors all come from the bundle developed at cipherstash/encrypt-query-language and shipped as @cipherstash/eql. A reader hitting an operator the matrix omits had nowhere authoritative to check and no idea where to file it. Adds a Where this surface is defined section with the version check (SELECT eql_v3.version(), verified against the pinned 3.0.2 bundle), ties the operator does not exist troubleshooting entry to it, and adds upstream links for both EQL and Proxy to the Reference list.

Filed cipherstash/encrypt-query-language#422 for the reciprocal EQL-side skill, since a good deal of this skill is a hand-transcription of that repo's surface with nothing checking it against the bundle.

Two claims were corrected during this pass rather than shipped: stash status does not surface usesProxy (only init writes it, plan/impl read it), and the literal node_modules/@cipherstash/eql/... path does not resolve under pnpm.

Verified against a live install, not transcribed

I had a Supabase stack with EQL v3 3.0.2 running, so the predicate matrix is extracted from pg_operator rather than inferred, and the binding rules were tested. Three claims came out differently than the issue recorded:

  • The binding rule is driver-specific, not universal. On postgres-js a bare object fails to INSERT (invalid input syntax for type json) and JSON.stringify(...)::jsonb fails the domain CHECK; only sql.json() works in both positions. On pg, all three forms work. "Bind as an object" is postgres-js guidance, so the skill splits it per driver.
  • A bare-jsonb operand does not fall through to native jsonb semantics. EQL defines jsonb overloads that coerce to the storage domain — eql_v3.eq_term(b::public.eql_v3_text_search) — which requires the ciphertext key c. A ciphertext-free query term without the domain cast therefore raises a CHECK violation rather than silently matching nothing. Corrected in three places where I'd first written the issue's version.
  • The schema type incompatibility reproduces in both directions, not just one. The column classes carry private fields, so TypeScript compares them nominally: Types have separate declarations of a private property 'columnName'. That rules out "author against wasm-inline and share" as a fix — the guidance is to author against exactly one entry. Re-verified on this branch after feat(stack)!: EQL v3 audit-on-decrypt + collapse EncryptionV3 into Encryption (EQL v2 removal PR 3) #768 collapsed the client factories.

The double-encoding failure also gets a one-line diagnosis it didn't have: SELECT jsonb_typeof($1::jsonb) returns 'string', and every EQL domain CHECK opens with jsonb_typeof(VALUE) = 'object' — which is why the error names neither JSON nor encoding.

Two bugs found along the way

  • packages/stack/src/wasm-inline.ts — the module JSDoc passed OidcFederationStrategy.create(...)'s Result straight to config.authStrategy without unwrapping. That is the same JSDoc the issue names as the reverse-engineering source, so it was actively teaching the mistake. Fixed there and written correctly in the skill.
  • The npm:@cipherstash/stack@^1.0.0/wasm-inline form I first wrote would not resolve — semver ranges exclude prereleases, and the published version is 1.0.0-rc.4. The skill pins exactly and explains why.

Alignment with the v2 removal

Rebased from main onto remove-v2 so the new skills describe only the post-removal surface. What that changed:

  • EncryptionV3 is now a deprecated alias of Encryption (feat(stack)!: EQL v3 audit-on-decrypt + collapse EncryptionV3 into Encryption (EQL v2 removal PR 3) #768), so stash-edge's "use Encryption, not EncryptionV3" contrast was stale. Replaced with the distinction that actually survives: both entries name the factory Encryption, so the import path is the only thing separating them — which is a better warning anyway, since the two take different config and different bulk shapes.
  • Schema-authoring references point at @cipherstash/stack/v3, matching stash-encryption on this branch.
  • Conflict in stash-encryption's subpath table resolved in favour of remove-v2's rows (@cipherstash/stack-drizzle at root, encryptedSupabase, the v3-only DynamoDB note), keeping only my wasm-inline addition.
  • Audited every added line: no eql_v2, EncryptionV3, encryptedColumn, encryptedSupabaseV3, or stack-drizzle/v3 reference is introduced.

Folded into existing skills

  • stash-cli — the credential-identity rule under env and under encrypt backfill, where it actually bites (backfill from a laptop, query from an Edge Function → every row decrypts, no search ever matches). Backfill is version-aware, not v2-only, so this applies to the v3 columns these skills are about.
  • stash-supabase — same rule, plus pointers to both new skills.
  • stash-encryption — the entry table said the WASM entry has "the v3 authoring surface", which reads as though a schema can be shared. Corrected, with an [!IMPORTANT] explaining the nominal-typing mechanism and why as any is the wrong fix.
  • stash-drizzle, stash-prisma-next — cross-links for the raw-SQL escape hatch (db.execute(sql\…`)` bypasses the operators those integrations emit).
  • stash-indexing — its "Raw SQL / plain PostgreSQL" bullet now points somewhere.

Wiring

SKILL_MAP (CLI + wizard) installs both for postgresql and supabase — Supabase because Edge Functions are the flagship WASM-entry use and its migrations/RPC are hand-written SQL. Drizzle and Prisma Next get cross-links instead, since they emit correctly-typed operands themselves. SKILL_PURPOSES entries added (covered by the existing "purpose line for every installable skill" test), plus new SKILL_MAP assertions.

Verification

On the rebased base:

  • pnpm run code:check — 0 errors, 196 warnings, matching the remove-v2 baseline in feat!: remove EQL v2 repo-wide (umbrella, #707) #772.
  • pnpm --filter stash test — 841 passed; pnpm --filter @cipherstash/wizard test — 265 passed.
  • Both skills confirmed present in packages/cli/dist/skills/ after build.
  • Every command and flag referenced resolves against stash manifest --json.
  • Post-rename: pnpm --filter stash test — 841 passed; biome check on the touched paths — 0 errors; no stash-sql reference remains anywhere outside dist/.

Not done

The schema non-interchange is documented here rather than fixed, per the issue's scoping ("worth stating explicitly either way").

Correction to an earlier version of this section: it suggested dropping private on columnName / definition to restore structural compatibility across the two .d.ts bundles. That is the wrong fix, and the code says so directly (packages/stack/src/eql/v3/columns.ts:426-433):

the private definition field carries the literal eqlType/castAs/capabilities, so two otherwise-empty subclasses (e.g. EncryptedBooleanColumn and EncryptedDateColumn, both storage-only) are NOT mutually assignable. This nominality is what keeps plaintext inference precise.

Those privates are load-bearing. Removing them would make storage-only domains mutually assignable and degrade the very inference the v3 surface exists to provide — trading a papercut for a real regression.

It is also not an isolated papercut. #780's Not in this PR section defers a second symptom of the same root cause: encryptQuery in the published .d.ts collapses its plaintext parameter to never under private-field type erasure. Reproduced against a fresh dist/ build:

TS2345: Argument of type '"alice@example.com"' is not assignable to parameter of type 'never'.

Both symptoms share a cause — the v3 column classes' type identity does not survive .d.ts emission intact — and both are invisible to CI for the same reason: the type tests import source, not dist/. Whatever the fix turns out to be, a dist/-consuming type test is the thing that would have caught either one. Worth scoping the two together rather than separately.

@coderdan
coderdan requested a review from a team as a code owner July 24, 2026 07:24
@changeset-bot

changeset-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d191c25

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

This PR includes changesets to release 11 packages
Name Type
stash Minor
@cipherstash/wizard Minor
@cipherstash/stack Minor
@cipherstash/basic-example Patch
@cipherstash/e2e Patch
@cipherstash/bench Patch
@cipherstash/prisma-next Minor
@cipherstash/stack-drizzle Minor
@cipherstash/stack-supabase Minor
@cipherstash/test-kit Patch
@cipherstash/prisma-next-example 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 24, 2026

Copy link
Copy Markdown

Review Change Stack

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: c32d5d0c-bd16-4d6a-9bcd-4f8842b9ecc5

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
📝 Walkthrough

Walkthrough

Adds stash-sql and stash-edge skill documentation, expands CLI and wizard installation mappings, documents credential and schema constraints, updates related skill references, and corrects the WASM authentication example.

Changes

SQL and edge integration guidance

Layer / File(s) Summary
Raw SQL predicate and binding guidance
skills/stash-sql/SKILL.md, skills/stash-drizzle/SKILL.md, skills/stash-prisma-next/SKILL.md, skills/stash-indexing/SKILL.md, skills/stash-encryption/SKILL.md
Documents EQL v3 domains, operators, driver-specific JSON binding, SQL recipes, decryption, indexing, and troubleshooting.
WASM edge runtime and credential contract
skills/stash-edge/SKILL.md, skills/stash-cli/SKILL.md, skills/stash-supabase/SKILL.md, packages/stack/src/wasm-inline.ts
Documents WASM runtime setup, explicit credentials, credential identity, client differences, schema isolation, edge querying, and Result unwrapping.
Skill installation and repository guidance
packages/cli/src/commands/init/lib/install-skills.ts, packages/wizard/src/lib/install-skills.ts, packages/cli/src/commands/init/lib/setup-prompt.ts, packages/cli/src/commands/init/lib/__tests__/install-skills.test.ts, .changeset/stash-sql-edge-skills.md, AGENTS.md
Adds the skills to integration mappings, prompt descriptions, regression tests, release metadata, and repository cross-references.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

  • cipherstash/stack issue 601 — Adds the raw-Postgres guidance and driver-specific binding rules covered here.
  • cipherstash/stack issue 664 — Covers the new stash-sql skill’s EQL v3 casting and encryptQuery guidance.
  • cipherstash/stack issue 756 — Overlaps the raw-SQL documentation updates in stash-prisma-next.
  • cipherstash/stack issue 764 — Covers the new edge skill, credential identity, and indexing guidance.
  • cipherstash/stack issue 757 — Addresses credential mismatches that cause silent query failures.

Possibly related PRs

Suggested reviewers: calvinbrewer

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is mostly aligned, but it names "stash-postgres" instead of the new "stash-sql" skill, which is misleading. Rename the skill to stash-sql so the title accurately reflects the new raw-SQL and WASM skill additions.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements the requested stash-edge and raw-SQL guidance, credential rules, and related skill mappings described in #754.
Out of Scope Changes check ✅ Passed The remaining doc, test, and JSDoc updates all support the new skills and cross-links, with no clear unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stash-sql-edge-skills

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.changeset/stash-sql-edge-skills.md:
- Around line 48-51: Update the change description to include repository
examples covering the revised stash init/impl handoffs, wizard skill
installation, and raw-SQL/WASM guidance, and explicitly note that the separately
maintained documentation site requires corresponding updates. Preserve the
existing integration-specific guidance and cross-links.

In `@skills/stash-drizzle/SKILL.md`:
- Around line 420-421: Update the raw SQL example in the surrounding
documentation to remove the nested backticks from the inline Markdown code span.
Use a safe placeholder such as db.execute(sqlTemplate), or convert the example
to a fenced TypeScript block while preserving the guidance about operand casts
and driver-specific parameter binding.

In `@skills/stash-edge/SKILL.md`:
- Around line 291-294: Update the fenced code block containing the
EncryptedTextSearchColumn error in SKILL.md to specify the text language tag,
such as ```text, while leaving the documented error content unchanged.

In `@skills/stash-sql/SKILL.md`:
- Around line 68-73: Update the fenced code blocks containing the eql_v3
mappings in SKILL.md, including the additionally referenced block, by adding the
text language identifier to each opening fence while preserving their contents.
- Around line 182-188: Update every recipe using encryptQuery, including the
snippets around term and their corresponding examples, to guard the Result
before dereferencing .data: check .failure and handle or return the failure,
then execute the SQL only on success. Apply the same pattern consistently to all
referenced recipes, or explicitly state that success/error handling is omitted
for brevity.
- Line 3: Update the description metadata in SKILL.md to include the missing <=
and > operators alongside the existing operator list, keeping it consistent with
the predicate matrix and preserving all other description content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 17c1183c-c043-47b6-878b-9830d357e011

📥 Commits

Reviewing files that changed from the base of the PR and between f6b82c6 and e6498d4.

📒 Files selected for processing (15)
  • .changeset/stash-sql-edge-skills.md
  • AGENTS.md
  • packages/cli/src/commands/init/lib/__tests__/install-skills.test.ts
  • packages/cli/src/commands/init/lib/install-skills.ts
  • packages/cli/src/commands/init/lib/setup-prompt.ts
  • packages/stack/src/wasm-inline.ts
  • packages/wizard/src/lib/install-skills.ts
  • skills/stash-cli/SKILL.md
  • skills/stash-drizzle/SKILL.md
  • skills/stash-edge/SKILL.md
  • skills/stash-encryption/SKILL.md
  • skills/stash-indexing/SKILL.md
  • skills/stash-prisma-next/SKILL.md
  • skills/stash-sql/SKILL.md
  • skills/stash-supabase/SKILL.md

Comment on lines +48 to +51
`stash init` / `stash impl` handoffs and the `@cipherstash/wizard` skills
prompt install both skills for the `postgresql` and `supabase` integrations.
Drizzle and Prisma Next get cross-links from their own skills instead, since
those integrations emit correctly-typed operands themselves.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Add example coverage and a documentation-site follow-up.

These changes alter the public stash init / wizard handoff workflow and introduce raw-SQL/WASM usage guidance. Add or update repository examples and explicitly track corresponding updates for the separately maintained documentation site.

As per coding guidelines, “When public APIs or user-facing workflows change, update repository examples and flag that the separately maintained documentation site needs corresponding updates.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/stash-sql-edge-skills.md around lines 48 - 51, Update the change
description to include repository examples covering the revised stash init/impl
handoffs, wizard skill installation, and raw-SQL/WASM guidance, and explicitly
note that the separately maintained documentation site requires corresponding
updates. Preserve the existing integration-specific guidance and cross-links.

Source: Coding guidelines

Comment thread skills/stash-drizzle/SKILL.md Outdated
Comment on lines +420 to +421
> **Dropping to raw SQL?** `db.execute(sql\`…\`)` bypasses the operators this integration emits, so you own the operand casts yourself — an encrypted predicate needs its needle cast to the column's `eql_v3.query_*` domain, and the driver's parameter-binding rules differ between `pg` and `postgres-js`. The `stash-sql` skill is the reference for both.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid nested backticks inside this inline code span.

Line 420 embeds a template literal inside an inline Markdown code span, triggering MD038 and risking incorrect rendering. Use a fenced TypeScript example or a placeholder such as db.execute(sqlTemplate).

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 420-420: Spaces inside code span elements

(MD038, no-space-in-code)

🪛 SkillSpector (2.3.11)

[warning] 27: [RP1] null: npx commands without a version suffix (e.g. @1.0.0) create a rug-pull risk if the upstream server is compromised and publishes a malicious update.

Remediation: Pin the version: npx @scope/server@1.2.3

(MCP Rug Pull (RP1))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/stash-drizzle/SKILL.md` around lines 420 - 421, Update the raw SQL
example in the surrounding documentation to remove the nested backticks from the
inline Markdown code span. Use a safe placeholder such as
db.execute(sqlTemplate), or convert the example to a fenced TypeScript block
while preserving the guidance about operand casts and driver-specific parameter
binding.

Source: Linters/SAST tools

Comment on lines +291 to +294
```
Type 'EncryptedTextSearchColumn' is not assignable to type 'AnyEncryptedV3Column'.
Types have separate declarations of a private property 'columnName'.
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language tag to this fenced block.

Line 291 should use ```text (or another appropriate language) so the skill passes MD040 and renders the TypeScript error example consistently.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 291-291: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/stash-edge/SKILL.md` around lines 291 - 294, Update the fenced code
block containing the EncryptedTextSearchColumn error in SKILL.md to specify the
text language tag, such as ```text, while leaving the documented error content
unchanged.

Source: Linters/SAST tools

Comment thread skills/stash-sql/SKILL.md Outdated
@@ -0,0 +1,365 @@
---
name: stash-sql
description: Query EQL v3 encrypted columns from hand-written SQL over `pg` (node-postgres) or `postgres` (postgres-js) — no ORM. Covers the column-domain-to-query-domain operator matrix (which of `=`, `<>`, `<`, `>=`, `@@`, `@>` each encrypted domain accepts), minting search needles with `encryptQuery`, the per-driver parameter-binding rules for encrypted payloads, and the double-encoding failure that trips the domain CHECK with a message naming neither JSON nor encoding. Use when writing INSERT/SELECT against an encrypted column without an ORM, when a predicate returns zero rows or raises "operator does not exist", or when a domain CHECK constraint rejects an encrypted value on write.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep the operator list consistent with the predicate matrix.

The description omits <= and > even though the matrix documents both. Include every operator covered by the skill so users do not incorrectly conclude those predicates are unsupported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/stash-sql/SKILL.md` at line 3, Update the description metadata in
SKILL.md to include the missing <= and > operators alongside the existing
operator list, keeping it consistent with the predicate matrix and preserving
all other description content.

Comment on lines +68 to +73
```
public.eql_v3_text_eq → eql_v3.query_text_eq
public.eql_v3_text_search → eql_v3.query_text_search
public.eql_v3_bigint_ord → eql_v3.query_bigint_ord
public.eql_v3_timestamp_ord → eql_v3.query_timestamp_ord
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to fenced code blocks.

The bare fences trigger Markdownlint MD040. Mark these blocks as text (or another appropriate language) so the published skill passes documentation checks.

Also applies to: 159-161

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 68-68: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/stash-sql/SKILL.md` around lines 68 - 73, Update the fenced code
blocks containing the eql_v3 mappings in SKILL.md, including the additionally
referenced block, by adding the text language identifier to each opening fence
while preserving their contents.

Source: Linters/SAST tools

Comment on lines +182 to +188
```ts
const term = await client.encryptQuery(email, {
table: users, column: users.email, queryType: 'equality',
})
await sql`SELECT * FROM users
WHERE email = ${sql.json(term.data)}::jsonb::eql_v3.query_text_eq`
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle encryptQuery failures in every recipe.

These snippets dereference .data without checking .failure, unlike the earlier examples. Add the same Result guard to each recipe, or explicitly state that success handling is omitted for brevity.

Also applies to: 195-203, 212-219, 229-235, 247-251, 257-260

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@skills/stash-sql/SKILL.md` around lines 182 - 188, Update every recipe using
encryptQuery, including the snippets around term and their corresponding
examples, to guard the Result before dereferencing .data: check .failure and
handle or return the failure, then execute the SQL only on success. Apply the
same pattern consistently to all referenced recipes, or explicitly state that
success/error handling is omitted for brevity.

…es and the WASM entry

Closes #754.

No shipped skill covered the integrations that don't use an ORM: hand-written
SQL over `pg` / `postgres-js`, and the WASM entry on Deno / Supabase Edge
Functions / Workers. Grepping the skills `stash init` installs for
`postgres-js|::jsonb::eql|sql.json|query_text_search` returned one hit, in an
unrelated code comment — so an integration on that path had to recover the
binding surface from `dist/*.d.ts`, the Postgres catalog, and experiment.

Two skills rather than one, per the scope question in the issue: the raw-SQL
predicate cookbook serves a supported plain-Node path (`hono-pg`) with no edge
or WASM involvement, so scoping it under an edge-named skill would leave that
surface uncovered.

skills/stash-sql — the predicate matrix (which of `=`, `<>`, `<`, `>=`, `@@`,
`@>` each column domain accepts, against which `eql_v3.query_*` operand),
storage-vs-query payload shapes, per-driver parameter binding, and recipes for
equality / free-text / range / ORDER BY / JSON containment / field selectors.

skills/stash-edge — import specifier per runtime, the four mandatory `CS_*`
variables and minting them with `stash env`, how the WASM client surface
differs from the native typed client, and why a schema module can't be shared
across the two entries.

Both carry the credential-identity rule, also folded into stash-cli (under
`env` and `encrypt backfill`, where it bites) and stash-supabase: EQL index
terms derive from the ZeroKMS client key, so rows written under one credential
and queried under another decrypt correctly and never match a query.

Three claims were corrected against a live EQL v3 3.0.2 install rather than
carried over from the issue:

- The binding rule is driver-specific. On postgres-js a bare object fails to
  INSERT and `JSON.stringify(...)::jsonb` fails the domain CHECK; only
  `sql.json()` works in both positions. On `pg` all three forms work.
- A bare-`jsonb` operand does not fall through to native jsonb semantics — EQL
  defines `jsonb` overloads that coerce to the *storage* domain, which requires
  the ciphertext key `c`, so a query term without the domain cast raises a
  CHECK violation rather than silently matching nothing.
- The schema type incompatibility reproduces in both directions, not one.

Also fixes the wasm-inline module JSDoc, which passed
`OidcFederationStrategy.create(...)`'s Result straight to
`config.authStrategy` without unwrapping — the same JSDoc the raw-SQL surface
was being reverse-engineered from.

SKILL_MAP (CLI + wizard) installs both for `postgresql` and `supabase`;
Drizzle and Prisma Next get cross-links from their own skills instead.

@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.

Checked every load-bearing technical claim in the two new skills against source rather than trusting the prose:

  • The WASM OidcFederationStrategy.create(...) Result-unwrap guidance (strategy.data / strategy.failure.error.message) matches resolveStrategy in wasm-inline.ts and its regression test.
  • The stash-edge client-surface and queryType lists match WasmEncryptionClient exactly; the encryptedTable/types re-export from the WASM entry is real.
  • Wiring is complete: both packages/cli/tsup.config.ts and packages/wizard/tsup.config.ts copy skills/ wholesale, both SKILL_MAPs and setup-prompt.ts SKILL_PURPOSES are updated, regression tests added, AGENTS.md layout updated. Changeset present and correctly scoped; no Linear IDs; both SKILL.md files are self-sufficient (safe for readBundledSkill inlining).

The six CodeRabbit comments are all valid but cosmetic (markdownlint MD038/MD040 fence tags, a nested-backtick span in stash-drizzle:420, and adding <=/> to the stash-sql frontmatter). None affect CI (we gate on Biome, not markdownlint) or correctness. Its "add repository examples" ask overreaches for a docs-only change.

Worth folding in before the next release (optional): sweep the MD040 language tags; add a one-line "error handling omitted for brevity" note to the mid-document stash-sql recipes that deref .data without a .failure guard; and note that the WASM decryptModel/bulkDecryptModels require an explicit table arg (unlike native). The postgres-js driver-binding matrix and the "needles ≥3 chars" claim are the highest-risk-if-wrong items — worth a live Postgres+EQL sanity check if not already done.

coderdan added 2 commits July 25, 2026 18:19
The skill is Postgres-specific throughout — encrypted columns are Postgres
domains over jsonb, the driver rules cover pg and postgres-js, and the drift
check reads information_schema. Nothing in it generalises to another SQL
dialect, so the name overclaimed.

Renames the directory and frontmatter name, updates the SKILL_MAP entries in
both the CLI and wizard installers, the setup-prompt purpose line, the
cross-links in the five sibling skills, and the AGENTS.md skill list and
change→skill map.

The skill has not shipped yet (no CHANGELOG entry — only the pending
changeset), so no installed .claude/skills/ directory carries the old name
and the existing changeset is amended in place rather than adding a second
one. The new name also matches the CLI's `postgresql` integration key, which
is the no-ORM path that installs it.
The skill covered client-side encryption over a direct connection without
ever saying so. That is precisely the fork `stash init` asks about and stores
as `usesProxy`: a CipherStash Proxy user writes plaintext SQL and Proxy
encrypts on the wire, so every rule in the skill — mint a term with
encryptQuery, cast to eql_v3.query_*, bind with sql.json — is wrong for them,
and nothing said which world they were in. Adds a scope callout up front, and
notes that Proxy's config lifecycle (stash db push into eql_v2_configuration)
is the EQL v2 one, while this skill is v3 where there is nothing to push.

The operator matrix also read as if the client library defined it. It does
not: the domains, operators, CHECKs, and extractors all come from the EQL
bundle developed at cipherstash/encrypt-query-language and shipped as
@cipherstash/eql. A reader hitting an operator the matrix does not list had
nowhere authoritative to check and no idea where to file it. Adds a
provenance section with the version check (SELECT eql_v3.version()), ties the
"operator does not exist" troubleshooting entry to it, and adds upstream
links for EQL and Proxy to the Reference list.

Also fixes the frontmatter description, which now states the direct-connection
assumption so skill selection reflects it.
@coderdan coderdan changed the title feat(cli): add the stash-sql and stash-edge skills — raw-SQL predicates and the WASM entry feat(cli): add the stash-postgres and stash-edge skills — raw-SQL predicates and the WASM entry Jul 25, 2026
…y callout

The callout told readers to check `usesProxy` in `.cipherstash/context.json`
to learn which path they were on. That flag and field are being removed (the
CLI never used the value for anything beyond gating a wizard `stash db push`
step), so the pointer would have shipped stale. The Proxy scoping itself
stands — it is the substance of the callout.
@coderdan
coderdan force-pushed the feat/stash-sql-edge-skills branch from ab620a4 to d191c25 Compare July 25, 2026 09:09
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.

Add a stash-edge skill: WASM entry, credentials, and the raw-SQL predicate surface

2 participants