Releases: cipherstash/stack
Release list
stash@1.1.1
Patch Changes
-
44e2921: Fix
stash eql migration --drizzle, which aborted for every project with adrizzle.config.ts(#924).- Stop passing
--outtodrizzle-kit generate. drizzle-kit reads its config file or its command-line options, never both: any of--schema/--out/--dialectswitches 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. Yourdrizzle.config.tsnow decides the output directory and stash follows the path drizzle-kit reports, warning when it differs from a--outyou passed.--outremains the fallback directory to search. - Pass the resolved
DATABASE_URLinto the drizzle-kit child process. Adrizzle.config.tsthat readsprocess.env.DATABASE_URL(and often throws when it is missing) previously saw nothing, because the project's usualdotenv -e .env.local -- drizzle-kit …wrapper never runs when stash invokes drizzle-kit directly. stash already loads.env/.env.localat 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_URLgets a follow-up naming that instead.
- Stop passing
-
67b137a:
stash initinstalls the agent skills again, and does it first.Since 1.0.0-rc.4 the only callers of the skills installer were the
planand
implhandoff steps, whichstash initnever reaches — sostash@1.1.0
installed nostash-*skills for anyone, in any mode. The most common flow, a
coding agent runningnpx stash init --supabaseinside a project, completed
with a green summary, a plausible-looking.cipherstash/context.json, and zero
guidance: the skills sat unread innode_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 theclaudebinary 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. Unlikeplan --target/impl --targetit
selects the destination only —initstill 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
silentinstalledSkills: []. --targetis validated properly oninit,planandimpl. A
trailing--targetwith 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-mdinstalls no skill directories of its own and used to overwrite
installedSkillswith an empty list, dropping skills that were on disk.
Deliveries are merged across hops now.
- New optional
-
ec0c5a7:
stash-managed-platformsskill: 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 branch20260819-01-lovable):- Command-time ceilings. Replaying the ~2.6 MB EQL bundle with
psql -fsends one statement
per round trip and dies partway under Lovable's 600 s ceiling, leaving a half-installed schema.
The skill now says to preferstash 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.tomlminimumReleaseAgeand Deno's
--minimum-dependency-ageboth 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 runstash envthemselves, or copy from the
agent-written 0600 file and delete it), never through chat or logs. - @cipherstash/migrate@1.0.0
- Command-time ceilings. Replaying the ~2.6 MB EQL bundle with
@cipherstash/wizard@1.1.1
Version Packages (#928)
@cipherstash/stack@1.1.1
Version Packages (#928)
@cipherstash/stack-supabase@1.1.1
Patch Changes
- @cipherstash/stack@1.1.1
@cipherstash/stack-prisma@1.1.1
Patch Changes
- @cipherstash/stack@1.1.1
@cipherstash/stack-drizzle@1.1.1
Patch Changes
- @cipherstash/stack@1.1.1
stash@1.1.0
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
sslmodeandsslrootcertfrom the connection string — andPGSSLMODE/PGSSLROOTCERTfrom the environment when the URL carries no TLS parameters (URL wins; unlike raw node-postgres,PGSSLROOTCERTis actually consumed):verify-full(andrequire/verify-ca/prefer, kept as full verification — node-postgres's current behaviour) verifies the server certificate;no-verifyis honoured with a one-line stderr warning;disableturns TLS off. Client-certificate setups (sslcert/sslkey) pass through untouched. - CA resolution:
sslrootcert=<path>(libpq semantics — sole trust anchor;sslrootcert=systemselects the system store) →PGSSLROOTCERT→ for*.supabase.co/*.supabase.comhosts a bundled Supabase root CA (appended to the system roots) → the system store.sslmode=verify-fullagainst 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=…, thensslmode=no-verifyas a last resort with the consequence spelled out), and explicitly warn againstNODE_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=requireURLs: the CLI decides the TLS config itself and hands pg a URL with the TLS params stripped (fixes the upstream-advisory passthrough).
- Every CLI database connection honours
-
a2b0b45: EQL installs no longer abort on managed platforms whose database role is not
postgres, and a newstash eql preflightcommand reports role capability before anything is attempted.stash eql install(andeql 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 ofpostgres(e.g. Lovable'ssandbox_exec), the three owner-scopedALTER DEFAULT PRIVILEGES FOR ROLE postgresstatements are skipped and the install completes without them — they are optional (they only cover EQL objectspostgresmight 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 plainGRANTstill runs. Previously that single refused statement rolled back the entire install (~194 functions).- Re-running
stash eql installon 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 --supabasewraps the owner-scoped statements in apg_has_roleguard, 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(--jsonfor agents): reportscurrent_user, superuser, membership ofpostgres(guarded for databases with nopostgresrole),CREATEon the database and onpublic(guarded for databases without apublicschema),pgcryptopresence and placement (a pgcrypto outsideextensions/publicaborts the bundle, even for superusers), and the EQL v3 schemas' presence and drop-ownership (a reinstall begins withDROP SCHEMA ... CASCADE) — each blocked row naming the statement it blocks. Exits 1 on blocking gaps; membership ofpostgresnever blocks.--jsonstdout 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 ofeql 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()(richPreflightResult) supersedescheckPermissions(), which remains as a deprecated adapter with itsPermissionCheckResultshape unchanged — no breaking change for existingstash@1.xconsumers.install()now returnsInstallResultwith the skipped SQL, if any, andapplySupabaseGrants()re-applies the grants alone. The exactSUPABASE_PERMISSIONS_SQL_V3block 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 verifycompares 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.--jsonemits the structured report for agents.stash eql installnow 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 toDATABASE_URL— previouslyeql install --database-url --forcecould drop and reinstall the EQL schemas on a database the command never named. -
a2b0b45: Add a
lovablehandoff target tostash planandstash 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 atAGENTS.mdand.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_oredomain with a loud-failure CHECK in its place. That is a supported configuration — but nothing said so where the choice betweentypes.*Ordandtypes.*OrdOreis actually made, so the trade was discovered at query time.stash eql preflightnow probes whether the role can create an operator class and reports it as a non-blockingORE operator classrow (creatable/not creatable/unknown;canCreateOperatorClassin--json). It is probed, not inferred fromsuperuser:CREATE OPERATOR CLASSis superuser-gated in stock PostgreSQL, but AWS RDS and Aurora let their admin role create one while cloud-hosted Supabase does not, sorolsuperis 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 reportsunknownrather than guessing.stash eql installnames 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 statusreports 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_opedomains; the bundle creates those, but@cipherstash/stackships notypes.*OrdOpefactory, so it named a column type no schema author could declare. Every command now saystypes.*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, andeql 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
*Ordrather than*OrdOre.
-
a2b0b45: Rewrite
db validateaseql validate, for the EQL v3 domain-type vocabulary.Fixes a false finding on the most ordinary v3 columns. The old rule set
checked forore/unique/match/ste_vecindexes and never learned
aboutope. EQL v3's default ordering domains emitope, so
types.IntegerOrd('age')andtypes.TimestampOrd('created_at')were both
reported as "Column is encrypted but has no indexes ...
@cipherstash/wizard@1.1.0
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 initpins 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/stack—clientKeyis hex-only. A decoder fallback that
also accepted standard padded base64 is gone, and such a key is now rejected
at client construction withinvalid clientKey: expected a hex-encoded key.
Hex is the only encoding ever documented, and the only onestash envor 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 whicherror.codevalues DynamoDB operations
report.stash—stash eql validatelost--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 validatein thestashchangelog.
@cipherstash/stack-prismaalso 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
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 makescast_as: 'number'with an
opeindex ambiguous acrosseql_v3_integer_ord,smallint_ord,real_ord,
double_ordandnumeric_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 withcolumn.getQueryCapabilities(),
and its DB name withcolumn.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 validateis 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 initpins 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/stack—clientKeyis hex-only. A decoder fallback that
also accepted standard padded base64 is gone, and such a key is now rejected
at client construction withinvalid clientKey: expected a hex-encoded key.
Hex is the only encoding ever documented, and the only onestash envor 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 whicherror.codevalues DynamoDB operations
report.stash—stash eql validatelost--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 validatein thestashchangelog.
@cipherstash/stack-prismaalso 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/diagnosticssubpath, 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 ownMODULE_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
exportsmap, and reading an export never reaches the@neon-rs/loadproxy. Importing@cipherstash/stackitself 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 ownassertNativeBindingAvailable— 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, anundefinedunder CJS).Available as both
importandrequire.
Patch Changes
-
a2b0b45: Adopt protect-ffi 0.31.0.
clientKeyis 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 whatstash envemits 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-profilecrate uses for~/.cipherstash/secretkey.jsonon 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 withinvalid 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.jsonis 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.
handleErroraccepted any string-valuedcodeon a caught error and passed it
through as aProtectErrorCode, 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 onerror.codefor 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-normalisescast_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
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 initpins 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/stack—clientKeyis hex-only. A decoder fallback that
also accepted standard padded base64 is gone, and such a key is now rejected
at client construction withinvalid clientKey: expected a hex-encoded key.
Hex is the only encoding ever documented, and the only onestash envor 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 whicherror.codevalues DynamoDB operations
report.stash—stash eql validatelost--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 validatein thestashchangelog.
@cipherstash/stack-prismaalso moves to Prisma Next 0.17 in this release,
which requires migration steps from its consumers — see its own changelog
entry. -
a2b0b45:
encryptedSupabasecan 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
databaseUrlas well.- Passing
schemaswith no database URL skips introspection entirely. No connection, nopg, nodatabaseUrl. - New
@cipherstash/stack-supabase/wasm-inlineentry. Identical wrapper, WASM engine. The package root statically imports the native engine (@cipherstash/protect-ffiand@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_URLis now read through a guard. On a runtime with noprocessglobal a bareprocess.env.Xis aReferenceError, notundefined, 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 — fromoptions.databaseUrlorDATABASE_URL— introspection still runs, and aschemas-passing caller still gets the drift check that verifies their declaration against the real column domains. "PassdatabaseUrlas well" is how you keep verification while declaring types.What declared mode gives up, it gives up loudly rather than silently:
select('*')and bareselect()are refused.allColumnscomes only from introspection, and an unexpanded*reaches PostgREST without the::jsonbcasts 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
23514CHECK violation on the first write instead of at construction. queryDomainsRequiredis 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
databaseUrlto thewasm-inlineentry 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_v3domain in the database but absent fromschemasis treated as an ordinary plaintext column — aselectnaming 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 passdatabaseUrlso introspection fills the gaps.An ambient
DATABASE_URLno 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 aDATABASE_URLthat happens to be set in the environment cannot break a declared-mode client. On the native entry, passingschemaswithout an explicitdatabaseUrlignores the variable and warns that the declaration is unverified. The refusal of adatabaseUrlon 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/bulkDecryptModelsrequire 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 bareTypeError; andbulkEncryptis 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:
schemasand aWasmClientConfigconfigare both mandatory (there is no~/.cipherstashto discover credentials from), anddatabaseUrlis 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 aTypeErrorfrom inside the engine. - Passing