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
18 changes: 18 additions & 0 deletions .changeset/brave-hounds-monitor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'stash': patch
---

Correct the Dependabot section of the bundled `stash-supply-chain-security`
skill. It described two monitored ecosystems (`npm`, `github-actions`); there
are now three, because the in-tree Rust workspace at `packages/protect-ffi`
brought a `Cargo.lock` that nothing proposed updates for. The skill now names
the `cargo` entry, its non-root `directory`, its monthly cadence, and the
exact-pinned CipherStash crates it ignores.

Two things the section previously got wrong are also fixed. Major bumps do not
"stay un-grouped — one PR each": every entry ignores
`version-update:semver-major`, so Dependabot proposes no major bumps at all and
they are applied by hand. And `ignore` conditions suppress Dependabot *security*
PRs as well as version updates — the skill now says so, and points at
`osv-scanner.yml` (which scans every lockfile in the tree, `Cargo.lock`
included) as the compensating control.
10 changes: 10 additions & 0 deletions .changeset/cli-tls-handling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'stash': minor
---

The CLI now handles database TLS properly, so the discoverable fix for a certificate failure is never `NODE_TLS_REJECT_UNAUTHORIZED=0`.

- Every CLI database connection honours `sslmode` and `sslrootcert` from the connection string — and `PGSSLMODE` / `PGSSLROOTCERT` from the environment when the URL carries no TLS parameters (URL wins; unlike raw node-postgres, `PGSSLROOTCERT` is actually consumed): `verify-full` (and `require`/`verify-ca`/`prefer`, kept as full verification — node-postgres's current behaviour) verifies the server certificate; `no-verify` is honoured with a one-line stderr warning; `disable` turns TLS off. Client-certificate setups (`sslcert`/`sslkey`) pass through untouched.
- CA resolution: `sslrootcert=<path>` (libpq semantics — sole trust anchor; `sslrootcert=system` selects the system store) → `PGSSLROOTCERT` → for `*.supabase.co`/`*.supabase.com` hosts a **bundled Supabase root CA** (appended to the system roots) → the system store. `sslmode=verify-full` against Supabase — direct hosts and the pgBouncer pooler — now verifies out of the box.
- Certificate-verification failures — shaped centrally in the connection factory, so every command surfaces them — name the host and the supported remedies in order (`sslrootcert=…`, then `sslmode=no-verify` as a last resort with the consequence spelled out), and explicitly warn against `NODE_TLS_REJECT_UNAUTHORIZED=0`, which is process-wide and would also disable verification for the connections carrying CipherStash credentials.
- The node-postgres "SSL modes … are treated as aliases for verify-full" SECURITY WARNING no longer appears on every invocation against `sslmode=require` URLs: the CLI decides the TLS config itself and hands pg a URL with the TLS params stripped (fixes the upstream-advisory passthrough).
12 changes: 12 additions & 0 deletions .changeset/eql-preflight-deferred-grants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'stash': minor
---

EQL installs no longer abort on managed platforms whose database role is not `postgres`, and a new `stash eql preflight` command reports role capability before anything is attempted.

- `stash eql install` (and `eql upgrade`) now run the EQL v3 bundle in its own transaction and the Supabase role grants after it commits, so a grants failure can no longer roll back a working install. When the connecting role is not a member of `postgres` (e.g. Lovable's `sandbox_exec`), the three owner-scoped `ALTER DEFAULT PRIVILEGES FOR ROLE postgres` statements are skipped and the install completes without them — they are optional (they only cover EQL objects `postgres` might later create outside stash tooling, and stash re-grants every object on each install/upgrade); the SQL is printed as "Optional SQL — requires postgres" for operators who want it. Every plain `GRANT` still runs. Previously that single refused statement rolled back the entire install (~194 functions).
- Re-running `stash eql install` on an already-installed Supabase database now re-applies the role grants (idempotent) instead of exiting early, so an install whose grants step failed heals on a plain re-run.
- The migration generated by `stash eql migration --supabase` wraps the owner-scoped statements in a `pg_has_role` guard, so it applies cleanly whatever role the project's migration runner uses — a non-member role skips them instead of aborting the whole migration.
- New read-only `stash eql preflight` (`--json` for agents): reports `current_user`, superuser, membership of `postgres` (guarded for databases with no `postgres` role), `CREATE` on the database and on `public` (guarded for databases without a `public` schema), `pgcrypto` presence *and placement* (a pgcrypto outside `extensions`/`public` aborts the bundle, even for superusers), and the EQL v3 schemas' presence and drop-ownership (a reinstall begins with `DROP SCHEMA ... CASCADE`) — each blocked row naming the statement it blocks. Exits 1 on blocking gaps; membership of `postgres` never blocks. `--json` stdout is pure JSON in every outcome: `{ status: 'ok' | 'blocked', ... }`, or the shared `{ status: 'error', code, message }` envelope — including when no DATABASE_URL is configured. The same check runs at the head of `eql install`.
- Install failure messages now state recoverability: a bundle failure says nothing was applied (rolled back); a grants failure says the install itself was kept.
- Library surface: `EQLInstaller.preflight()` (rich `PreflightResult`) supersedes `checkPermissions()`, which remains as a deprecated adapter with its `PermissionCheckResult` shape unchanged — no breaking change for existing `stash@1.x` consumers. `install()` now returns `InstallResult` with the skipped SQL, if any, and `applySupabaseGrants()` re-applies the grants alone. The exact `SUPABASE_PERMISSIONS_SQL_V3` block is unchanged byte-for-byte; new exports expose its immediate (`SUPABASE_IMMEDIATE_GRANTS_SQL_V3`), owner-scoped (`SUPABASE_DEFAULT_PRIVILEGES_SQL_V3`), guarded (`SUPABASE_GUARDED_DEFAULT_PRIVILEGES_SQL_V3`), and migration (`SUPABASE_MIGRATION_GRANTS_SQL_V3`) forms.
21 changes: 21 additions & 0 deletions .changeset/eql-verify-review-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'stash': patch
---

`stash eql status` no longer reports ORE damage on a healthy database running a
different EQL version. The `_ord_ore` domains its poison CHECKs are counted over
come from the bundle this CLI pins, so a fallback install of an older EQL poisons
domains the pinned list only partly sees — which classified as an incoherent
half-install and told the operator to reinstall with `--force`, on the ordinary
"CLI upgraded, database not yet" case. The ORE probe now gates on the installed
version the same way `eql verify` does and reports that the state could not be
compared, pointing at `eql upgrade`.

Two hardening fixes to `stash eql verify` alongside it. Its cast check now
matches an EQL endpoint on either side, so a future bundle cast to or from a
`pg_catalog` type (`jsonb`, `text`) cannot enter the expected surface while being
unreadable as installed — which would have reported "Cast missing" on every
healthy database. And the parser that derives the expected surface from the
pinned bundle now fails loudly on any statement it does not model, instead of
silently omitting the objects it creates: a bundle that outgrows the parser can
no longer make `verify` report a partial install as complete.
5 changes: 5 additions & 0 deletions .changeset/eql-verify-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'stash': minor
---

New `stash eql verify`: assert the installed EQL surface is complete and coherent, independent of any application schema. A partial install — domains present, some of their comparison functions or operators absent — used to report success at install time and fail at query time on a specific predicate (e.g. `weight >= x`); nothing detected it. `eql verify` compares the database against everything the pinned bundle installs (every domain, function overload, operator, cast, and the ORE operator class) via read-only catalog queries, reports damage grouped per domain, and distinguishes expected absence from damage: the ORE operator class being skipped on managed Postgres, with its loud-failure fallback in place, reads as the supported configuration it is rather than a failed install. Exit 0 means exactly one thing — the surface was checked and found complete; damage, EQL absent, and a version mismatch with the pinned bundle (nothing verifiable) all exit 1. `--json` emits the structured report for agents. `stash eql install` now runs the same check automatically before declaring success, on the fresh-install path and the already-installed early exit alike — there, only damage fails the install: a version mismatch warns and continues, so a no-op re-run over an older EQL stays exit 0 for idempotent provisioning scripts. A valueless `--database-url` (booleanised by the parser when the next token is another flag) is now rejected up front on every command instead of silently falling back to `DATABASE_URL` — previously `eql install --database-url --force` could drop and reinstall the EQL schemas on a database the command never named.
34 changes: 34 additions & 0 deletions .changeset/ffi-platform-packages-publishing-repository.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
'@cipherstash/protect-ffi-darwin-x64': patch
'@cipherstash/protect-ffi-darwin-arm64': patch
'@cipherstash/protect-ffi-win32-x64-msvc': patch
'@cipherstash/protect-ffi-linux-x64-gnu': patch
'@cipherstash/protect-ffi-linux-arm64-gnu': patch
'@cipherstash/protect-ffi-linux-x64-musl': patch
---

**This is the first release of these packages published from
`cipherstash/stack`.** Every version up to and including 0.31.0 was published
from `cipherstash/protectjs-ffi`, which is archived once this release is out.

If you verify npm provenance, the attested source repository changes with this
release:

```
0.31.0 github.com/cipherstash/protectjs-ffi .github/workflows/release.yml
0.32.0 github.com/cipherstash/stack .github/workflows/release.yml
```

A verification policy that pins the source repository will reject 0.32.0 until
it is updated. The packages, their contents and their maintainers are otherwise
unchanged: the Rust source moved into the monorepo at
`packages/protect-ffi/crates/protect-ffi`, and each of these packages'
`repository.url` now names `cipherstash/stack`, with `repository.directory`
pointing at its own stub under `packages/protect-ffi/platforms/`.

`CHANGELOG.md` is also added to each package's published files, so this note and
later ones are readable in the package you install rather than only on GitHub.

The binaries themselves differ from 0.31.0 in one user-visible way: the Rust
core's `InvariantViolation` message asks the reader to file an issue, and the
repository it names has moved with the rest.
5 changes: 5 additions & 0 deletions .changeset/lovable-handoff-target.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'stash': minor
---

Add a `lovable` handoff target to `stash plan` and `stash impl` (`--target lovable`, plus a new agent-target picker entry). It writes the same AGENTS.md as the editor-agent handoff — doctrine plus the per-integration skills inlined — but the next-steps guidance is Lovable-specific: commit and push the generated files through Lovable's GitHub sync, then add a Knowledge note in the Lovable project settings pointing the agent at `AGENTS.md` and `.cipherstash/setup-prompt.md`. Without repo-local guidance, Lovable's agent answers CipherStash questions from stale training data (the pre-EQL-v3 "needs a Postgres extension and superuser" story) and talks users out of a supported Supabase setup.
17 changes: 17 additions & 0 deletions .changeset/lucky-cows-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'stash': patch
---

Document in the bundled `stash-auth` skill that `CS_CLIENT_KEY` must be
hex-encoded. Hex is what `stash env` emits and what the skill's variable table
already stated, but the decoder underneath used to fall back to standard padded
base64 — the encoding the Rust `stash-profile` crate uses for
`~/.cipherstash/secretkey.json` on disk — so a key copied out of that file
happened to work despite never being a supported input. That fallback is gone
and such a key is now rejected at client construction, with a message that
deliberately withholds detail — so the skill names the symptom and the fix.

The recovery advice is split by entry point: falling back to the profile store
works on the native entry, but not on `@cipherstash/stack/wasm-inline`, where
`clientId` and `clientKey` are required config and the target runtimes have no
profile store to read. Re-encoding as hex is the fix that works on both.
18 changes: 18 additions & 0 deletions .changeset/olive-moons-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'stash': patch
---

Correct the release-workflow section of the bundled `stash-supply-chain-security`
skill. It described the no-Actions-cache rule as a property of one file — "no
`cache:`, `package-manager-cache: false`, `pnpm/action-setup` with
`cache: false`" — which is no longer the whole rule.

The gate now follows any local composite action or reusable workflow the job
reaches, so the constraint is on the whole call tree rather than the workflow
file. And every published `uses:` must appear in the script's `AUDITED_ACTIONS`
allowlist: the check cannot open a published action to prove it does not cache,
and caching actions are not reliably named — a `setup-<tool>` action that caches
by default has no `cache:` input and nothing in its name to match. The list is
therefore what is permitted, not what is forbidden, and adding a step to
`release.yml` or `tests-supply-chain.yml` means auditing the action and adding
it there in the same PR.
29 changes: 29 additions & 0 deletions .changeset/olive-poems-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@cipherstash/stack': minor
---

Add `EncryptionClient.getSchemas()` — the tables passed to
`Encryption({ schemas })`, returned by reference.

This is the domain-bearing view of your schema. `getEncryptConfig()` returns
what the FFI consumes: each column builds to `{ cast_as, indexes }`, and the
concrete EQL v3 domain name is dropped. That makes `cast_as: 'number'` with an
`ope` index ambiguous across `eql_v3_integer_ord`, `smallint_ord`, `real_ord`,
`double_ord` and `numeric_ord` — so tooling that has to reason about the
*declared* domain (schema linting, drift-checking a live database's
`information_schema.columns.domain_name`) could not recover it from a client
alone.

`getSchemas()` closes that gap. Read a column's domain with
`column.getEqlType()`, its capabilities with `column.getQueryCapabilities()`,
and its DB name with `column.getName()`:

```typescript
for (const table of client.getSchemas()) {
for (const column of Object.values(table.columnBuilders)) {
console.log(table.tableName, column.getName(), column.getEqlType())
}
}
```

`stash eql validate` is the first consumer.
38 changes: 38 additions & 0 deletions .changeset/olive-pugs-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@cipherstash/stack': patch
---

Adopt protect-ffi 0.31.0.

**`clientKey` is hex, and a decoder tolerance that accepted other spellings is
gone.** Hex has always been the documented and only supported encoding for
`config.clientKey` / `CS_CLIENT_KEY` — it is what `stash env` emits and what
the docs and skills have always shown. The decoder underneath happened to fall
back to standard padded base64, which is the encoding the Rust
`stash-profile` crate uses for `~/.cipherstash/secretkey.json` on disk; that
fallback was never part of this package's contract, and nothing in the
JavaScript stack ever produced or accepted a base64 key. It is now rejected at
client construction with `invalid clientKey: expected a hex-encoded key`.

The message deliberately says nothing more, because the underlying decode error
names the offending character and its offset and would put part of a live key
into your logs. So if construction starts failing after this upgrade, the key
you supplied is not hex — re-encode it, or drop the explicit key and let the
native client read it from the profile store.

Reading the key from `~/.cipherstash/secretkey.json` is unaffected — that path
still uses base64, and only an explicitly supplied key is hex-only.

**DynamoDB errors no longer report foreign error codes as encryption codes.**
`handleError` accepted any string-valued `code` on a caught error and passed it
through as a `ProtectErrorCode`, so a Node or AWS SDK failure — `ECONNRESET`,
say — surfaced as though it were an encryption error code. Codes are now checked
against the set the encryption layer actually emits, and anything else becomes
`DYNAMODB_ENCRYPTION_ERROR`. If you branch on `error.code` for DynamoDB
operations, a branch that was matching transport errors will stop.

Also in this release, with no action needed: the WASM entry passes credentials
under the option shape 0.31 expects and no longer pre-normalises `cast_as`
(the native layer does it on both bindings now), and bulk operations no longer
forward their internal correlation id across the FFI boundary, which 0.31
rejects rather than ignores.
14 changes: 14 additions & 0 deletions .changeset/ore-unavailable-at-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'stash': minor
---

Report the ORE-unavailable case once, at install time, instead of leaving it to surface as a failing predicate the first time a column is cast.

The EQL bundle skips the ORE btree operator class when the installing role cannot create one and poisons every `_ord_ore` domain with a loud-failure CHECK in its place. That is a supported configuration — but nothing said so where the choice between `types.*Ord` and `types.*OrdOre` is actually made, so the trade was discovered at query time.

- **`stash eql preflight` now probes whether the role can create an operator class** and reports it as a non-blocking `ORE operator class` row (`creatable` / `not creatable` / `unknown`; `canCreateOperatorClass` in `--json`). It is *probed*, not inferred from `superuser`: `CREATE OPERATOR CLASS` is superuser-gated in stock PostgreSQL, but AWS RDS and Aurora let their admin role create one while cloud-hosted Supabase does not, so `rolsuper` is not evidence either way. The probe attempts the DDL inside a transaction it always rolls back, leaving preflight read-only; a probe that could not ask reports `unknown` rather than guessing.
- **`stash eql install` names the consequence and the remedy** on its own line when the fallback was installed, rather than as a parenthetical on the "verified" line.
- **`stash eql status` reports the ORE state** on a v3 install, so the answer survives past the install output.
- **The remedy now names a type that exists.** The previous wording pointed at the `_ord_ope` domains; the bundle creates those, but `@cipherstash/stack` ships no `types.*OrdOpe` factory, so it named a column type no schema author could declare. Every command now says `types.*Ord` (`public.eql_v3_*_ord`), which is the same CLLW-OPE ordering and has a factory behind it.
- The ORE state machine, the catalogue probe, and this copy now live in one module shared by `eql preflight`, `eql install`, `eql status`, `eql verify`, and `eql validate`, so the five commands cannot drift into disagreeing about the same catalogue fact.
- The scaffolded encryption client's type cheat-sheet now says why ordered columns should be `*Ord` rather than `*OrdOre`.
Loading
Loading