Skip to content

Releases: cipherstash/stack

stash@1.1.1

Choose a tag to compare

@github-actions github-actions released this 20 Aug 06:56
4940712

Patch Changes

  • 44e2921: Fix stash eql migration --drizzle, which aborted for every project with a drizzle.config.ts (#924).

    • Stop passing --out to drizzle-kit generate. drizzle-kit reads its config file or its command-line options, never both: any of --schema/--out/--dialect switches it into CLI mode, where it then aborts demanding the two we cannot supply (Please provide required params: [x] schema [x] dialect). Verified against drizzle-kit 0.28.5, 0.30.6 and 0.31.4 — this was never version-specific. Your drizzle.config.ts now decides the output directory and stash follows the path drizzle-kit reports, warning when it differs from a --out you passed. --out remains the fallback directory to search.
    • Pass the resolved DATABASE_URL into the drizzle-kit child process. A drizzle.config.ts that reads process.env.DATABASE_URL (and often throws when it is missing) previously saw nothing, because the project's usual dotenv -e .env.local -- drizzle-kit … wrapper never runs when stash invokes drizzle-kit directly. stash already loads .env/.env.local at startup; it now also threads down a URL only the CLI can find, such as a running local Supabase.
    • Report the actual failure. drizzle-kit writes its errors to stdout, not stderr, so the abort printed nothing but "Make sure drizzle-kit is installed and configured" — the one thing that was never wrong. Both streams are now surfaced, and a config that could not read DATABASE_URL gets a follow-up naming that instead.
  • 67b137a: stash init installs the agent skills again, and does it first.

    Since 1.0.0-rc.4 the only callers of the skills installer were the plan and
    impl handoff steps, which stash init never reaches — so stash@1.1.0
    installed no stash-* skills for anyone, in any mode. The most common flow, a
    coding agent running npx stash init --supabase inside a project, completed
    with a green summary, a plausible-looking .cipherstash/context.json, and zero
    guidance: the skills sat unread in node_modules/stash/dist/skills/ unless the
    agent thought to go digging. Fixes #923.

    Init now copies the per-integration skills into .claude/skills/ (Claude Code
    detected via the claude binary or a .claude/ directory) and .codex/skills/
    (Codex), installing to both when both are detected, and records them in
    context.json.

    It runs as init's first step, ahead of authentication. Installing skills
    needs no network, no credentials and no database, while authenticate,
    resolve-database and install-eql each need one and each can exit non-zero —
    so the guidance now survives a run that fails partway, which is when it is
    needed most. One behaviour change falls out of that: a run cancelled at the
    first prompt leaves the skills directory behind where previously it wrote
    nothing.

    Also:

    • New optional stash init --target <claude-code|codex> names the skills
      destination and skips detection. Unlike plan --target / impl --target it
      selects the destination only — init still performs no handoff. Existing
      invocations are unaffected.
    • The summary reports the outcome either way. A run that installs nothing
      now says so, and prints the command that will install them, instead of a
      silent installedSkills: [].
    • --target is validated properly on init, plan and impl. A
      trailing --target with no value, and --target=, were both treated as
      "flag absent" — so the command silently did whatever it does with no flag at
      all, rather than telling you the value was missing. All three commands share
      one validator now.
    • A later handoff no longer erases the record. stash plan --target agents-md installs no skill directories of its own and used to overwrite
      installedSkills with an empty list, dropping skills that were on disk.
      Deliveries are merged across hops now.
  • ec0c5a7: stash-managed-platforms skill: fold in what a live Lovable Cloud integration actually hit.

    Four additions, each from a verified failure in the 2026-08-19 skilltester run
    (cipherstash/skilltester branch 20260819-01-lovable):

    • Command-time ceilings. Replaying the ~2.6 MB EQL bundle with psql -f sends one statement
      per round trip and dies partway under Lovable's 600 s ceiling, leaving a half-installed schema.
      The skill now says to prefer stash eql install / the generated migration, gives the
      chunk-and-apply recipe for when raw SQL is unavoidable, and covers the ownership trap when
      cleaning up a half-install.
    • Data API grants. The EQL install grants nothing to authenticated / anon /
      service_role, so every PostgREST function-form call fails until an explicit
      GRANT USAGE / EXECUTE — now stated with the exact SQL.
    • Install cooldowns. Lovable's bunfig.toml minimumReleaseAge and Deno's
      --minimum-dependency-age both refuse a same-day CipherStash release; the skill names the
      exclude-list workaround and says to disclose it.
    • Lovable secrets. Who sets them depends on where the agent runs: Lovable's in-product agent
      can store project secrets itself, while the external Lovable MCP surface has no secrets tool —
      there the values are handed to the human (they run stash env themselves, or copy from the
      agent-written 0600 file and delete it), never through chat or logs.
    • @cipherstash/migrate@1.0.0

@cipherstash/wizard@1.1.1

Choose a tag to compare

@github-actions github-actions released this 20 Aug 06:56
4940712
Version Packages (#928)

@cipherstash/stack@1.1.1

Choose a tag to compare

@github-actions github-actions released this 20 Aug 06:56
4940712
Version Packages (#928)

@cipherstash/stack-supabase@1.1.1

Choose a tag to compare

@github-actions github-actions released this 20 Aug 06:56
4940712

Patch Changes

  • @cipherstash/stack@1.1.1

@cipherstash/stack-prisma@1.1.1

Choose a tag to compare

@github-actions github-actions released this 20 Aug 06:56
4940712

Patch Changes

  • @cipherstash/stack@1.1.1

@cipherstash/stack-drizzle@1.1.1

Choose a tag to compare

@github-actions github-actions released this 20 Aug 06:56
4940712

Patch Changes

  • @cipherstash/stack@1.1.1

stash@1.1.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 07:25
f03805f

Minor Changes

  • a2b0b45: 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).
  • a2b0b45: 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.
  • a2b0b45: 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.

  • a2b0b45: 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.

  • a2b0b45: 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.
  • a2b0b45: Rewrite db validate as eql validate, for the EQL v3 domain-type vocabulary.

    Fixes a false finding on the most ordinary v3 columns. The old rule set
    checked for ore / unique / match / ste_vec indexes and never learned
    about ope. EQL v3's default ordering domains emit ope, so
    types.IntegerOrd('age') and types.TimestampOrd('created_at') were both
    reported as "Column is encrypted but has no indexes ...

Read more

@cipherstash/wizard@1.1.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 07:25
f03805f

Minor Changes

  • a2b0b45: Reading this release. These packages share one version line with
    @cipherstash/stack-prisma, so all six move together:

    • stash
    • @cipherstash/stack
    • @cipherstash/stack-drizzle
    • @cipherstash/stack-supabase
    • @cipherstash/stack-prisma
    • @cipherstash/wizard

    They are versioned together on purpose. stash init pins the versions of the
    packages it installs and the CLI embeds that map at build time, so a package
    shipping alone would leave the CLI recommending versions that no longer match
    what is published, and warning about a skew it had itself created.

    Two changes in this release can need action from some users. They are named
    here so you do not have to read six changelogs to find them:

    • @cipherstash/stackclientKey is hex-only. A decoder fallback that
      also accepted standard padded base64 is gone, and such a key is now rejected
      at client construction with invalid clientKey: expected a hex-encoded key.
      Hex is the only encoding ever documented, and the only one stash env or any
      part of the JavaScript stack has ever produced — the base64 tolerance was an
      accident of the underlying Rust decoder, which accepts base64 solely to read
      its own profile store. A key pasted out of ~/.cipherstash/secretkey.json
      (which stores base64) stops working; re-encode it, or drop the explicit key
      and let the client read the profile store directly, which is unaffected. The
      full entry is "Adopt protect-ffi 0.31.0" in the @cipherstash/stack
      changelog; it also narrows which error.code values DynamoDB operations
      report.
    • stashstash eql validate lost --exclude-operator-family, and two
      checks that used to exit 1 no longer do. A script passing that flag, or a CI
      gate relying on those exit codes, needs updating. The full entry is under
      eql validate in the stash changelog.

    @cipherstash/stack-prisma also moves to Prisma Next 0.17 in this release,
    which requires migration steps from its consumers — see its own changelog
    entry.

@cipherstash/stack@1.1.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 07:25
f03805f

Minor Changes

  • a2b0b45: 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():

    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.

  • a2b0b45: Reading this release. These packages share one version line with
    @cipherstash/stack-prisma, so all six move together:

    • stash
    • @cipherstash/stack
    • @cipherstash/stack-drizzle
    • @cipherstash/stack-supabase
    • @cipherstash/stack-prisma
    • @cipherstash/wizard

    They are versioned together on purpose. stash init pins the versions of the
    packages it installs and the CLI embeds that map at build time, so a package
    shipping alone would leave the CLI recommending versions that no longer match
    what is published, and warning about a skew it had itself created.

    Two changes in this release can need action from some users. They are named
    here so you do not have to read six changelogs to find them:

    • @cipherstash/stackclientKey is hex-only. A decoder fallback that
      also accepted standard padded base64 is gone, and such a key is now rejected
      at client construction with invalid clientKey: expected a hex-encoded key.
      Hex is the only encoding ever documented, and the only one stash env or any
      part of the JavaScript stack has ever produced — the base64 tolerance was an
      accident of the underlying Rust decoder, which accepts base64 solely to read
      its own profile store. A key pasted out of ~/.cipherstash/secretkey.json
      (which stores base64) stops working; re-encode it, or drop the explicit key
      and let the client read the profile store directly, which is unaffected. The
      full entry is "Adopt protect-ffi 0.31.0" in the @cipherstash/stack
      changelog; it also narrows which error.code values DynamoDB operations
      report.
    • stashstash eql validate lost --exclude-operator-family, and two
      checks that used to exit 1 no longer do. A script passing that flag, or a CI
      gate relying on those exit codes, needs updating. The full entry is under
      eql validate in the stash changelog.

    @cipherstash/stack-prisma also moves to Prisma Next 0.17 in this release,
    which requires migration steps from its consumers — see its own changelog
    entry.

  • a2b0b45: Add a @cipherstash/stack/diagnostics subpath, for tooling that needs to prove the protect-ffi native binding is installed.

    It exports one function, assertNativeBindingAvailable(). Calling it forces the platform binary to load and throws the loader's own MODULE_NOT_FOUND — unwrapped, naming the missing @cipherstash/protect-ffi-<platform>-<arch> package — if it is absent. Importing the subpath does not force anything, so the laziness that makes the native load cost nothing for callers that never encrypt is preserved.

    The subpath exists because there is no way to do this from outside: the package's loader is not in its exports map, and reading an export never reaches the @neon-rs/load proxy. Importing @cipherstash/stack itself is not a substitute either — the root entry re-exports the auth strategies, so evaluating it resolves @cipherstash/auth's binding instead. This entry reaches protect-ffi and nothing else.

    It probes by calling isEncrypted, which has been published since 0.28.0, rather than protect-ffi's own assertNativeBindingAvailable — that export arrived with the lazy native load and is not in any released version, so re-exporting it would build here and fail wherever it shipped (a link-time error under ESM, an undefined under CJS).

    Available as both import and require.

Patch Changes

  • a2b0b45: 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.

  • Updated dependencies [a2b0b45]

  • Updated dependencies [a2b0b45]

  • Updated dependencies [a2b0b45]

    • @cipherstash/protect-ffi@0.32.0

@cipherstash/stack-supabase@1.1.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 07:25
f03805f

Minor Changes

  • a2b0b45: Reading this release. These packages share one version line with
    @cipherstash/stack-prisma, so all six move together:

    • stash
    • @cipherstash/stack
    • @cipherstash/stack-drizzle
    • @cipherstash/stack-supabase
    • @cipherstash/stack-prisma
    • @cipherstash/wizard

    They are versioned together on purpose. stash init pins the versions of the
    packages it installs and the CLI embeds that map at build time, so a package
    shipping alone would leave the CLI recommending versions that no longer match
    what is published, and warning about a skew it had itself created.

    Two changes in this release can need action from some users. They are named
    here so you do not have to read six changelogs to find them:

    • @cipherstash/stackclientKey is hex-only. A decoder fallback that
      also accepted standard padded base64 is gone, and such a key is now rejected
      at client construction with invalid clientKey: expected a hex-encoded key.
      Hex is the only encoding ever documented, and the only one stash env or any
      part of the JavaScript stack has ever produced — the base64 tolerance was an
      accident of the underlying Rust decoder, which accepts base64 solely to read
      its own profile store. A key pasted out of ~/.cipherstash/secretkey.json
      (which stores base64) stops working; re-encode it, or drop the explicit key
      and let the client read the profile store directly, which is unaffected. The
      full entry is "Adopt protect-ffi 0.31.0" in the @cipherstash/stack
      changelog; it also narrows which error.code values DynamoDB operations
      report.
    • stashstash eql validate lost --exclude-operator-family, and two
      checks that used to exit 1 no longer do. A script passing that flag, or a CI
      gate relying on those exit codes, needs updating. The full entry is under
      eql validate in the stash changelog.

    @cipherstash/stack-prisma also moves to Prisma Next 0.17 in this release,
    which requires migration steps from its consumers — see its own changelog
    entry.

  • a2b0b45: encryptedSupabase can now be constructed without a Postgres connection, and there is a new edge entry that runs it off Node.

    The rule: declare your schemas and it runs anywhere; omit them and we discover them for you, which needs a database connection and is therefore Node-only.

    Previously the wrapper always introspected the database to derive each column's encryption config from its Postgres domain. That made it unconstructible anywhere a TCP socket to Postgres is unavailable, and cost a second, more privileged credential even on Node — the caller already had an authenticated Supabase client and had to supply a databaseUrl as well.

    • Passing schemas with no database URL skips introspection entirely. No connection, no pg, no databaseUrl.
    • New @cipherstash/stack-supabase/wasm-inline entry. Identical wrapper, WASM engine. The package root statically imports the native engine (@cipherstash/protect-ffi and @cipherstash/auth, both Node-API), and a static import loads whether or not you encrypt anything — so an edge runtime needs a different entry, not a different code path. ESM-only, matching @cipherstash/stack/wasm-inline. Server-side only; not browser-safe (#804).
    • DATABASE_URL is now read through a guard. On a runtime with no process global a bare process.env.X is a ReferenceError, not undefined, so the unguarded read threw during construction before declared mode could help.

    Existing callers are unaffected. The gate is the database URL, not the presence of schemas: if a URL resolves — from options.databaseUrl or DATABASE_URL — introspection still runs, and a schemas-passing caller still gets the drift check that verifies their declaration against the real column domains. "Pass databaseUrl as well" is how you keep verification while declaring types.

    What declared mode gives up, it gives up loudly rather than silently:

    • select('*') and bare select() are refused. allColumns comes only from introspection, and an unexpanded * reaches PostgREST without the ::jsonb casts encrypted columns need.
    • from() on an undeclared table throws, naming the declaration rather than an introspection pass that never ran.
    • The drift check is absent, so a wrong declared domain surfaces as a 23514 CHECK violation on the first write instead of at construction.
    • queryDomainsRequired is forced rather than detected, since the installed EQL version is read by introspection. This is the fail-loud direction: correct on EQL >= 3.0.2, and on an older install the operand cast fails visibly instead of emitting an operator the database will not engage.
    • Passing databaseUrl to the wasm-inline entry is refused — it carries no Postgres driver, and saying so beats ignoring the option.

    One tradeoff is not loud, and is the declared-mode contract you have to hold yourself: your declaration must cover every encrypted column of a table you query. Nothing introspects, so a column carrying an eql_v3 domain in the database but absent from schemas is treated as an ordinary plaintext column — a select naming it returns the raw EQL payload as data, and a filter on it sends the plaintext operand to PostgREST. The always-introspect path could not do this (undeclared columns were synthesized from their domains). Declare every encrypted column, or pass databaseUrl so introspection fills the gaps.

    An ambient DATABASE_URL no longer overrules a declaration, and is consulted only by a build that could act on it: on the edge entry — which cannot introspect at all — it is never read, so a DATABASE_URL that happens to be set in the environment cannot break a declared-mode client. On the native entry, passing schemas without an explicit databaseUrl ignores the variable and warns that the declaration is unverified. The refusal of a databaseUrl on the edge entry now keys on the option you actually passed, so it can never fire for a value you did not write. Previously a stray variable silently exited declared mode — introspecting a database the caller never named on Node, and on the edge entry throwing "drop databaseUrl" about an option never passed.

    The edge entry adapts the WASM client rather than casting to it. The two engines are not drop-in for each other, and every difference is silent at construction — the entry would have built a client happily while each query through it failed. decryptModel / bulkDecryptModels require the table on WASM and derive it from the payloads on native (both call sites now pass it, which native ignores); WASM operations are plain Results with no .withLockContext() or .audit(), so both are attached and throw a sentence naming the gap rather than a bare TypeError; and bulkEncrypt is deliberately not forwarded, selecting the supported per-term fallback instead of a mismatched signature. Lock context is a real capability gap on the WASM engine (#797) — failing loudly is the only honest option, since silently dropping the claim would write values any keyset holder could decrypt.

    The edge entry's options are also typed for what it actually requires: schemas and a WasmClientConfig config are both mandatory (there is no ~/.cipherstash to discover credentials from), and databaseUrl is absent from the type as well as refused at runtime. Previously the shared factory's erased config type let an edge caller omit credentials entirely and reach a TypeError from inside the engine.

Patch Changes

  • Updated dependencies [a2b0b45]
  • Updated dependencies [a2b0b45]
  • Updated dependencies [a2b0b45]
  • Updated dependencies [a2b0b45]
    • @cipherstash/stack@1.1.0