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 9 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 9 commits into
remove-v2from
feat/stash-sql-edge-skills

Conversation

@coderdan

@coderdan coderdan commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Closes #754.

Important

Blocked on #797 — do not merge until .withLockContext() ships on wasm-inline.
skills/stash-edge now documents lock context as available on both entries, which is
true only after #797 lands. The code example assumes the chainable form matching
native; if #797 lands a per-call option instead, that example must be corrected first.
Background in #793.

Targets remove-v2 (#772), not main — so nothing here references the EQL v2 surface. Rebased onto f7b82204 (current remove-v2); 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:

Re-run after rebasing onto current remove-v2 (f7b82204), clean replay of all six commits with no conflicts:

  • pnpm run code:check — exit 0, no errors (191 warnings, all pre-existing).
  • pnpm --filter stash test875 passed; pnpm --filter @cipherstash/wizard test305 passed; pnpm --filter stash test:e2e76 passed (16 files).
  • Both skills confirmed present in packages/cli/dist/skills/ after build, and stash manifest --json builds and resolves.
  • No stash-sql reference remains anywhere outside dist/ and CHANGELOGs.

Earlier counts in this PR's history (841 / 265) were measured against the older base a16302fc; the base has since gained 28 commits.

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: 94ba032

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: f97cf098-61d0-4118-aeeb-5c409c9f4fd3

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 | ✅ 5
✅ Passed checks (5 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.
Title check ✅ Passed The title captures the main change, though it misnames stash-sql as stash-postgres.
✨ 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 thread skills/stash-edge/SKILL.md Outdated
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 thread skills/stash-postgres/SKILL.md Outdated
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.

@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 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
@coderdan
coderdan force-pushed the feat/stash-sql-edge-skills branch from ab620a4 to d191c25 Compare July 25, 2026 09:09
coderdan added 6 commits July 25, 2026 20:54
…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.
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.
…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.
"Assume `users.email` is a `types.TextEq` column" named the schema builder as
though it were the column's type. It isn't: `types.TextEq` is a TypeScript
factory in this repo, while the column is `public.eql_v3_text_eq`, a Postgres
domain over jsonb. The distinction is the one the whole section turns on, so
opening by conflating them was the wrong footing.

Now opens with the database type and states that the domain is the authority —
what the CHECK enforces and what decides the operator set — then lists the
three things that map onto it: the `types.*` schema factory, the `TextEq` /
`TextEqQuery` wire types from `@cipherstash/eql`, and the `eql_v3.query_*`
operand domain.

Records where the TypeScript types come from: generated with the JSON Schemas
from the Rust `eql-bindings` crate, with the SQL bundle built from the same
commit, so the wire shape and the domain CHECK cannot drift. Verified against
@cipherstash/eql 3.0.2 — `TextEq` is `{ v, i, c, hm }` and `TextEqQuery` is
`{ v, i, hm }`, so the generated pair is exactly the storage-vs-query split
this section goes on to explain.
The domain and operator tables read as authoritative. They are not — they are
a snapshot of a versioned surface defined in `encrypt-query-language`, and
nothing in CI catches them drifting when the `@cipherstash/eql` pin moves.

Marks them as a snapshot at the point of use, and adds a ranked list of places
to confirm current types: the EQL skill first (it ships beside the bundle it
documents, so it tracks the installed version), then the generated
`@cipherstash/eql` TypeScript types, then the install SQL's CREATE OPERATOR
statements, then `SELECT eql_v3.version()` against the database. The middle
two need only `node_modules` — the `stash` CLI depends on `@cipherstash/eql`
at an exact pin — so the check costs nothing and needs no connection.

The EQL skill does not exist yet (cipherstash/encrypt-query-language#422), so
it is referenced by role rather than by a name that may not survive review,
and every other rung of the ladder works today without it.
@coderdan
coderdan force-pushed the feat/stash-sql-edge-skills branch from 9a64b7a to b351200 Compare July 25, 2026 10:55
`stash-edge` named `@cipherstash/protect-ffi` five times. Three of those were
configuration for the runtime this skill exists to steer readers away from:
the entry-choice table's Node row explained how to externalise it in Next, and
the Workers section named it again to say the config was irrelevant. A reader
here is not configuring a Node server, so that is the wrong skill to carry it
— the bundling guide already owns it, and is now linked instead.

Two mentions stay, for different reasons:

- The `When to Use` trigger keeps the name. Deploying the default entry to an
  edge runtime fails with `Cannot find module '@cipherstash/protect-ffi'`, and
  that string in a deploy log is how an agent should route itself here.
- The Deno note keeps `--allow-ffi`, which is Deno's own permission flag
  rather than the package — a diagnostic that the wrong entry resolved.

The intro now says "a Node-API native module" without naming the package; the
contrast it draws does not need it.
coderdan added 2 commits July 25, 2026 21:38
DEPENDS ON #797 — do not merge #777 until the wasm-inline
`.withLockContext()` lands. This describes the post-#797 surface.

The skill said "Identity-bound encryption is configured, not chained" and
presented `config.authStrategy` as the replacement for `.withLockContext()`.
That conflates two orthogonal mechanisms. An auth strategy decides who the
client is; a lock context decides which key the value is encrypted under. You
need both, and a strategy alone writes data that is not identity-bound.

What actually changed on the native entry was authentication: per-operation
CTS tokens were removed in protect-ffi 0.25, so `LockContext.identify()` is
deprecated and the strategy handles token acquisition. `.withLockContext()`
never went anywhere. The WASM entry picked up the authentication half and not
the key-binding half, and the skill then described the gap as though the first
subsumed the second.

Rewrites the section as two numbered steps, adds the encrypt-side example, and
states the symmetry rule — the same claim must be supplied on decrypt, and a
mismatch surfaces as a failed decrypt rather than a key error.

The code example assumes the chainable form, matching native. If #797 lands a
per-call option instead, this example must change before #777 merges.
Five of six findings applied; the sixth skipped with a reason.

- `stash-drizzle`: `db.execute(sql\`…\`)` used backslash-escaped backticks
  inside an inline code span. Escapes do not work inside code spans, so the
  backslashes rendered literally. Switched to a double-backtick span, which is
  the actual mechanism for embedding a backtick.
- `stash-edge`, `stash-postgres`: language tags on three unlabelled fences —
  a TypeScript error, the domain-name mapping, and a Postgres CHECK-violation
  message. All `text`.
- `stash-postgres` frontmatter: the operator list omitted `<=` and `>`, which
  the predicate matrix includes. The description is the skill-selection
  surface, so an incomplete list there is worse than verbose.
- `stash-postgres` Query Recipes: the recipes dereference `.data` without
  guarding `.failure`, and agents copy from them. Rather than adding the guard
  to six short recipes, the section now states the omission up front and shows
  the guard once — unguarded, the failure surfaces as a domain CHECK violation
  rather than as the encryption error it is, which is the confusing outcome
  this skill exists to prevent.

Skipped: adding a "the docs site needs a corresponding update" note to the
changeset. Changesets become CHANGELOG entries, which are customer-facing;
an internal follow-up action does not belong there. Tracked separately.

Two other unlabelled fences exist in `stash-encryption` and `stash-cli` but
predate this branch and are untouched by it.
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