Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/auth-strategy-jsdoc-unwrap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@cipherstash/stack': patch
---

Fix the `Encryption()` auth-strategy doc examples to unwrap `create()`'s Result.

Since `@cipherstash/auth` 0.41, `AccessKeyStrategy.create()` and
`OidcFederationStrategy.create()` return a `Result<Strategy, AuthFailure>`,
but the JSDoc examples on the native entry still passed the return value
straight into `config.authStrategy` — demonstrating exactly the mistake that
fails opaquely at the first `getToken()` call. The examples now check
`.failure` and pass `.data`, matching the WASM entry's docs and the
integration tests.

Also corrects the lock-context wording in the `WasmEncryptionClient` doc
comment: a lock context decides who can *retrieve a value's data key* (the
claim from the encrypting caller's service token is bound to the key), not
"which key the value is encrypted under" — key material is identical either
way; the binding gates retrieval.
13 changes: 13 additions & 0 deletions .changeset/eql-migration-prisma-not-needed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'stash': patch
---

`stash eql migration --prisma`: say "not needed", not "not yet".

The command's registry copy, error message, and the `stash-cli` skill all
described a Prisma Next emitter as a coming follow-up. Prisma Next doesn't
need one — its extension pack installs the EQL bundle through prisma-next's
own migration framework (the `migrations/cipherstash/` contract space). The
`--prisma` flag now exists purely to route people there: the error explains
the mechanism and points at `prisma-next migration plan` / `prisma-next
migrate`.
15 changes: 15 additions & 0 deletions .changeset/keyset-default-doc-precision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@cipherstash/stack': patch
---

Correct the `config.keyset` docs on what omitting the option resolves to.

The `Encryption()` and `ClientConfig.keyset` doc comments said omitting
`config.keyset` uses "the workspace's default keyset". The actual resolution
is the **client's** default keyset — the keyset the ZeroKMS client behind
your credentials was created against. The two coincide when the client was
created without naming a keyset — as with the profile credentials in a dev
environment — but a client created against a named keyset defaults to that
keyset, so two processes that both omit `config.keyset` share a keyspace
only if their clients default to the same keyset. The docs now say exactly
that.
33 changes: 33 additions & 0 deletions .changeset/prisma-next-skill-accuracy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
'stash': patch
---

Correct the `stash-prisma-next` skill against the current adapter, and fix a
stale constructor name in `stash init --prisma-next`'s next steps.

The skill was verified line-by-line against `@cipherstash/prisma-next` on
main (constructors, domains, operators, `rawSql` shape, EQL function names,
CLI commands — all confirmed current). Two real errors fixed:

- The config example imported `defineConfig` from `'prisma-next'` — no such
package exists; it comes from `@prisma-next/cli/config-types`.
- The bundling section suggested `@cipherstash/stack/wasm-inline` for edge
runtimes — the Prisma Next adapter is native-only (`cipherstashFromStack`
constructs the native stack client; there is no WASM variant), so the
advice was a dead end. It now says so.

The column-type section now carries the **complete 31-constructor catalog**
(plaintext TS type × capability tier) instead of a six-row sample presented
as the whole surface (#756): every family (`Text*`, `Integer*`, `Smallint*`,
`BigInt*`, `Numeric*`, `Real*`, `Double*`, `Date*`, `Timestamp*`, `Boolean`,
`Json`) with its plaintext type — the column that distinguishes
`IntegerOrd` (JS `number`) from `BigIntOrd` (JS `bigint`) and would have
prevented the integer-cents trap the issue reports. Also states that `*Ord`
includes equality, `TextMatch` is free-text only, and the `*OrdOre` variants
are deliberately unexposed.

Also documented the `column-types` subpath (camelCase factories for
TS-authored contracts), and fixed `stash init --prisma-next`'s next-steps
message, which still told users to declare columns with the old
`cipherstash.Encrypted*()` constructor names (current: `cipherstash.TextSearch()`,
`cipherstash.DateOrd()`, …).
35 changes: 35 additions & 0 deletions .changeset/skills-keyset-auth-wording-pass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
'stash': patch
---

Correct the keyset/credential model in five shipped skills to match the new
canonical sources.

`stash-edge`, `stash-cli`, `stash-postgres`, and `stash-supabase` all carried
a "credential-identity rule": EQL index terms deriving from the ZeroKMS
client key, so rows written under one credential would "decrypt correctly
but never match a query — silently". That model is wrong. Index terms come
from a per-**keyset** key, so every client **bound** to the same keyset
derives the same terms — credential strings never matter. The keyset can
still miss silently, though: encrypt and query use the client's bound
keyset while decrypt follows each payload's keyset subject to grants, so a
reader granted the writer's keyset but bound to a different one decrypts
fine while its searches return zero rows. The old *credential* diagnostic
could never fire; the *keyset-binding* check replaces it, alongside the
other real causes of zero-row queries (operand casts, predicate forms,
missing indexes).

All five sites now state the keyset model and defer to `stash-zerokms`
(keysets/grants) and `stash-auth` (credentials/lock context) as canonical.
`stash-deployment`'s backfill-keyset guidance gets the same pass: bound
keyset (not a mere grant) is what routes the backfill's writes, the failure
table and troubleshooting rows distinguish the no-grant case (decrypt fails)
from the granted-but-differently-bound case (decrypt works, search silently
misses), and `stash-cli`'s backfill precondition now names the credential
*resolution* order — `CS_*` variables when present, else the local
`~/.cipherstash` profile via native auto auth.
`stash-encryption` also drops its claim that identity-bound encryption on
the edge is "configured via `config.authStrategy`" (an auth strategy decides
who the client is; a lock context gates retrieval of a value's data key —
the edge entry simply lacks lock context, #797), and its auth, lock-context,
and keysets sections now point at the canonical skills.
45 changes: 45 additions & 0 deletions .changeset/stash-auth-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
'stash': minor
'@cipherstash/wizard': patch
---

Add a `stash-auth` agent skill and install it for every integration (#794).

Authentication had no canonical home: the guidance was scattered across
skills that each mention one slice of it, and the gap had already produced a
wrong explanation in shipped material (conflating `config.authStrategy` with
lock context). The new skill is the single source of truth; other skills
should point at it rather than restate it.

What it documents:

- The service-token model: every request to a CipherStash service carries a
short-lived JWT minted by CTS; access keys and IdP JWTs are exchanged at
CTS, never sent to ZeroKMS directly. The token carries the workspace, the
role-derived scopes, and the regional ZeroKMS endpoint in its `services`
claim — which is why endpoints are never hand-configured and `CS_*_HOST`
stays debug-only.
- The three separable concerns (client credentials, end-user identity,
key binding) and the canonical statement that an auth strategy decides who
the client is while a lock context decides who can retrieve a value's data
key — the claim from the encrypting caller's service token is bound to the
key, and retrieval requires presenting the same claim. Orthogonal, and
only combined deliberately.
- The `@cipherstash/auth` strategies (`AutoStrategy`, `AccessKeyStrategy`,
`OidcFederationStrategy`, `DeviceSessionStrategy`), including the Result
trap: `create()` returns `Result<Strategy, AuthFailure>` and
`config.authStrategy` takes the unwrapped `.data`, plus the `AuthFailure`
codes worth recognising (`NOT_AUTHENTICATED`, `WORKSPACE_MISMATCH`, …).
- Credential discovery vs explicit config (native env/profile vs the WASM
entry's explicit four values), the mutual-exclusion rule on the WASM entry,
the four `CS_*` variables and `stash env`, and client lifetime with
user-scoped strategies (one client per request — a shared client binds
whoever arrived first).
- Lock context usage and the deprecations around it
(`LockContext.identify()` / `getLockContext()`, `config.strategy`), the
explicit rule that agents never read `~/.cipherstash`, and a note that
Proxy authentication is a different path (dedicated skill to come).

`stash-zerokms` gains a companion section: ZeroKMS accepts only CTS-minted
service tokens, runs in multiple regions, and its endpoint is determined by
CTS — with the bulk deferred to `stash-auth`.
44 changes: 44 additions & 0 deletions .changeset/stash-zerokms-skill.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
'stash': minor
'@cipherstash/wizard': patch
---

Add a `stash-zerokms` agent skill and install it for every integration.

The keyset/client access model had no canonical home: several skills described
credentials and keysets in passing, and some of that wording contradicts how
ZeroKMS actually works. The new skill is the single source of truth for the
model, and other skills should point at it rather than restate it.

What it documents:

- The four-level key hierarchy (root key → per-keyset authority key →
per-client client key → per-value data key) and why revoking one client
blocks all of its future key operations immediately, without
re-encryption (not retroactive — already-held plaintext is beyond recall,
but per-value keys bound the blast radius to what was already accessed).
- The scoping rule and its asymmetry: encrypt and query always use the
client's **bound** keyset, while decrypt follows each payload's keyset
subject to grants. An unreachable bound keyset (no grant, revoked,
disabled) fails loudly at the ZeroKMS round trip, as does decrypting a
payload under an ungranted keyset. The one **silent** case is a reader
granted the writer's keyset but bound to a different one: decrypt works,
while its query terms derive under its own keyset and encrypted search
returns zero rows. The same-keyset rule therefore binds writers and query
readers; decrypt-only readers need just a grant.
- Clients and grants: creation binds a client to one keyset (the workspace
default unless named), `grant`/`revoke` manage further access per
(client, keyset) pair, and two different credentials interoperate fully as
long as both reach the encrypting keyset — "identical credentials
everywhere" was never the requirement.
- The workspace default keyset (`default`, reserved name, cannot be disabled
or renamed) and multi-tenant isolation via `config.keyset` with one
`Encryption()` client per tenant.
- The ZeroKMS API surface for automation (`/create-keyset`, `/grant-keyset`,
`/revoke-keyset`, `/list-clients`, …) with required token scopes, the exact
failure surfaces (404 no-grant, 403 disabled-keyset, 403 missing-scopes,
per-value lock-context denials), and a diagnostic runbook that separates the
client-level keyset gate from the value-level lock-context gate.

`stash-zerokms` joins the set every integration installs, alongside
`stash-encryption`, `stash-indexing`, `stash-deployment` and `stash-cli`.
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ If these variables are missing, tests that require live encryption will fail or
- `e2e/*`: Cross-package end-to-end tests (package managers, supply chain, Prisma example README)
- `examples/*`: Working apps (basic, prisma, supabase-worker)
- `docs/plans/*`: Internal design plans. User-facing documentation lives at https://cipherstash.com/docs (not in this repo).
- `skills/*`: Agent skills (`stash-cli`, `stash-encryption`, `stash-indexing`, `stash-deployment`, `stash-postgres`, `stash-edge`, `stash-drizzle`, `stash-dynamodb`, `stash-supabase`, `stash-prisma-next`, `stash-supply-chain-security`)
- `skills/*`: Agent skills (`stash-cli`, `stash-encryption`, `stash-indexing`, `stash-deployment`, `stash-zerokms`, `stash-auth`, `stash-postgres`, `stash-edge`, `stash-drizzle`, `stash-dynamodb`, `stash-supabase`, `stash-prisma-next`, `stash-supply-chain-security`)

## Agent Skills — these ship to customers

Expand Down Expand Up @@ -115,6 +115,8 @@ nothing type-checks them, and the damage lands in a customer's repo, not ours.
| The deploy sequencing / deploy-gate story, `stash env`, or platform-specific deployment guidance | `skills/stash-deployment` |
| The `@cipherstash/eql` pin, `eql install`/`eql migration` behaviour, or index-related SQL guidance | `skills/stash-indexing` |
| The EQL operator/domain surface (`eql_v3.query_*` casts, predicate forms) | `skills/stash-postgres` |
| The keyset/client model (`config.keyset`, grants, the ZeroKMS access story) | `skills/stash-zerokms` — the canonical source; other skills should point here rather than restate it |
| Auth strategies (`config.authStrategy`), the `CS_*` variables, lock context, `stash env` / `auth login` behaviour | `skills/stash-auth` — the canonical source; other skills should point here rather than restate it |
| `packages/stack/src/wasm-inline.ts`, the WASM entry's exports, or `stash env` | `skills/stash-edge` |
| pnpm config, CI workflows, dependency policy | `skills/stash-supply-chain-security` |
| The durable agent rules themselves | `packages/cli/src/commands/init/doctrine/AGENTS-doctrine.md` |
Expand Down
12 changes: 6 additions & 6 deletions examples/prisma/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@
// Operator visibility per column follows from the domain's query
// capabilities (see the package README's operator surface table):
//
// - email (EncryptedTextSearch → eql_v3_text_search)
// - email (TextSearch → eql_v3_text_search)
// equality + order/range + free-text search:
// eqlEq / Neq / In / NotIn /
// Gt / Gte / Lt / Lte / Between / NotBetween /
// Match, eqlAsc / Desc.
// - salary (EncryptedDoubleOrd → eql_v3_double_ord)
// - salary (DoubleOrd → eql_v3_double_ord)
// equality + order/range.
// - accountId (EncryptedBigIntOrd → eql_v3_bigint_ord)
// - accountId (BigIntOrd → eql_v3_bigint_ord)
// equality + order/range.
// - birthday (EncryptedDateOrd → eql_v3_date_ord)
// - birthday (DateOrd → eql_v3_date_ord)
// equality + order/range.
// - emailVerified (EncryptedBoolean → eql_v3_boolean)
// - emailVerified (Boolean → eql_v3_boolean)
// STORAGE-ONLY: no search operators at all — every
// eql* predicate throws
// EncryptionOperatorError on this column.
// - preferences (EncryptedJson → eql_v3_json_search)
// - preferences (Json → eql_v3_json_search)
// searchable JSON: eqlJsonContains
// (encrypted jsonb `@>` containment).
//
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ export const registry: CommandGroup[] = [
{
name: 'eql migration',
summary:
'Generate an EQL v3 install migration for your ORM (Drizzle now; Prisma Next soon)',
'Generate an EQL v3 install migration for your ORM (Drizzle; Prisma Next installs EQL through its own migrations)',
examples: [
'eql migration --drizzle',
'eql migration --drizzle --supabase',
Expand All @@ -338,7 +338,7 @@ export const registry: CommandGroup[] = [
{
name: '--prisma',
description:
'Emit a Prisma Next migration — not available yet; the emitter is a follow-up tracked in cipherstash/stack#690.',
'Not needed: Prisma Next installs EQL through its own migration framework — run `prisma-next migrate` instead.',
},
{
name: '--supabase',
Expand Down
6 changes: 1 addition & 5 deletions packages/cli/src/commands/eql/__tests__/migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,7 @@ describe('eqlMigrationCommand — target selection', () => {
{ drizzle: true, prisma: true },
() => messages.eql.migrationOneTarget,
],
[
'--prisma',
{ prisma: true },
() => messages.eql.migrationPrismaUnavailable,
],
['--prisma', { prisma: true }, () => messages.eql.migrationPrismaNotNeeded],
// `--supabase` is a modifier, not a target.
[
'--supabase alone',
Expand Down
15 changes: 9 additions & 6 deletions packages/cli/src/commands/eql/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@ function cleanupMigrationFile(filePath: string | undefined): void {
export interface EqlMigrationOptions {
/** Emit a Drizzle custom migration. */
drizzle?: boolean
/** Emit a Prisma Next migration (not yet available — see issue #690). */
/**
* Rejected with a pointer — Prisma Next installs EQL through its own
* migration framework, so there is nothing for this command to emit.
*/
prisma?: boolean
/** Append the Supabase role grants (`eql_v3` + `eql_v3_internal`). */
supabase?: boolean
Expand Down Expand Up @@ -161,11 +164,11 @@ export async function eqlMigrationCommand(
}

if (options.prisma) {
// The Prisma Next emitter is a follow-up (tracked in #690): it will write
// the install migration in the framework `Migration` shape and let
// prisma-next drop its baked install baseline. Until it lands, fail loudly
// with a pointer rather than emit a broken/empty file.
p.log.error(messages.eql.migrationPrismaUnavailable)
// Prisma Next does not need an emitted install migration: its extension
// pack contributes the `migrations/cipherstash/` contract space, which
// installs the EQL bundle through prisma-next's own migration framework.
// The flag exists only to route people who try it to that mechanism.
p.log.error(messages.eql.migrationPrismaNotNeeded)
throw new CliExit(1)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ The CipherStash setup skills carry the API details. Use them when you need speci
- `.codex/skills/<skill>/SKILL.md` (Codex)
- Under `## Skill references` at the bottom of this file (editor agents, or when a skills directory could not be written — if the section exists, it is the current copy)

Skills relevant to this project depend on the integration. Common ones: `stash-encryption` (encryption API), `stash-indexing` (indexes on encrypted columns), `stash-deployment` (taking a rollout to production), `stash-cli` (`stash` commands), and one of `stash-drizzle` / `stash-supabase` / `stash-prisma-next` / `stash-dynamodb` for the chosen ORM.
Skills relevant to this project depend on the integration. Common ones: `stash-encryption` (encryption API), `stash-indexing` (indexes on encrypted columns), `stash-deployment` (taking a rollout to production), `stash-zerokms` (keysets, clients, and key management), `stash-auth` (credentials, auth strategies, lock context), `stash-cli` (`stash` commands), and one of `stash-drizzle` / `stash-supabase` / `stash-prisma-next` / `stash-dynamodb` for the chosen ORM.
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ describe('skillsFor', () => {
'stash-prisma-next',
'stash-indexing',
'stash-deployment',
'stash-zerokms',
'stash-auth',
'stash-cli',
])
})
Expand All @@ -140,6 +142,8 @@ describe('skillsFor', () => {
'stash-encryption',
'stash-indexing',
'stash-deployment',
'stash-zerokms',
'stash-auth',
'stash-cli',
])
})
Expand All @@ -164,6 +168,8 @@ describe('installSkills', () => {
'stash-drizzle',
'stash-indexing',
'stash-deployment',
'stash-zerokms',
'stash-auth',
'stash-cli',
])
expect(failed).toEqual([])
Expand Down Expand Up @@ -207,6 +213,8 @@ describe('installSkills', () => {
'stash-drizzle',
'stash-indexing',
'stash-deployment',
'stash-zerokms',
'stash-auth',
'stash-cli',
],
})
Expand All @@ -233,6 +241,8 @@ describe('installSkills', () => {
'stash-encryption',
'stash-indexing',
'stash-deployment',
'stash-zerokms',
'stash-auth',
'stash-cli',
])
expect(failed).toEqual(['stash-drizzle'])
Expand All @@ -254,6 +264,8 @@ describe('installSkills', () => {
'stash-drizzle',
'stash-indexing',
'stash-deployment',
'stash-zerokms',
'stash-auth',
'stash-cli',
])
})
Expand Down
Loading
Loading