|
| 1 | +--- |
| 2 | +'@cipherstash/stack-supabase': patch |
| 3 | +'stash': patch |
| 4 | +'@cipherstash/wizard': patch |
| 5 | +--- |
| 6 | + |
| 7 | +Stop telling customers the Supabase wrapper cannot run in a Worker. |
| 8 | + |
| 9 | +`@cipherstash/stack-supabase` has shipped two entry points since #912. The |
| 10 | +package root introspects your database and runs on Node; the `wasm-inline` |
| 11 | +entry carries the WASM engine, takes declared `schemas` instead of |
| 12 | +introspecting, and runs on Deno, Supabase Edge Functions and Cloudflare |
| 13 | +Workers. Introspection was the only thing that needed a Postgres socket, and |
| 14 | +that entry does not do it. |
| 15 | + |
| 16 | +Two shipping documents were never updated and still described the state before |
| 17 | +that change: |
| 18 | + |
| 19 | +- `packages/stack-supabase/README.md` said "the factory cannot run in an edge |
| 20 | + Worker or the browser" and did not mention the `wasm-inline` entry anywhere |
| 21 | + in the file. This is the npm package page. |
| 22 | +- `skills/stash-supabase/SKILL.md` said the same thing in its setup section. |
| 23 | + The skill ships inside the `stash` tarball, and `stash init` copies it into |
| 24 | + the customer's own repository, where their coding agent reads it as |
| 25 | + instruction. The one correct mention of the edge entry was in a callout near |
| 26 | + the top that the setup steps never pointed at, so a reader following the |
| 27 | + setup never learned the second entry existed. |
| 28 | + |
| 29 | +The population this misled hardest is the one that needs the edge entry most: |
| 30 | +server code on Lovable, v0, Bolt and Replit runs on an edge runtime, which is |
| 31 | +exactly the case `wasm-inline` was built for and exactly the case these |
| 32 | +documents called impossible. |
| 33 | + |
| 34 | +Both files now describe both entries. The README gains an "Edge runtimes" |
| 35 | +section with the call shape; the skill gains a fifth setup step with the same, |
| 36 | +and the introspection paragraph now scopes its restriction to the native entry |
| 37 | +and points there. Both name the four ways the edge entry differs: `schemas` is |
| 38 | +required, `config` is required, `databaseUrl` is refused, and |
| 39 | +`.withLockContext()` / `.audit()` throw rather than silently dropping an |
| 40 | +identity claim (#797). |
| 41 | + |
| 42 | +The **browser** half of the old sentence was correct and is kept, with the |
| 43 | +reason now given: the WASM client requires a workspace `clientKey` on every |
| 44 | +authentication path, so a browser build would ship the key with it (#804). |
| 45 | + |
| 46 | +Four claims that were wrong in the same neighbourhood are corrected while we |
| 47 | +are here, three of them pre-dating this change: |
| 48 | + |
| 49 | +- **`skills/stash-managed-platforms/SKILL.md` shipped a snippet that does not |
| 50 | + compile.** It authored the `schemas` object from `@cipherstash/stack/wasm-inline` |
| 51 | + and handed it to `encryptedSupabase` from `@cipherstash/stack-supabase/wasm-inline`. |
| 52 | + The adapter types `schemas` from `@cipherstash/stack/eql/v3`, and the two |
| 53 | + entries ship independent declarations of the column classes whose private |
| 54 | + `columnName` field TypeScript compares nominally — so `tsc` rejects it while |
| 55 | + the code runs perfectly, which is why nobody noticed. The schema import now |
| 56 | + comes from `eql/v3`, and a new guard |
| 57 | + (`scripts/__tests__/skills-supabase-edge-schema-entry.test.mjs`) fails if any |
| 58 | + shipped document pairs the two again. |
| 59 | +- **`skills/stash-edge/SKILL.md` is what produced that snippet.** Its "Schema |
| 60 | + Modules Do Not Cross Entries" section told edge projects to author schemas |
| 61 | + from `@cipherstash/stack/wasm-inline` with no carve-out. The rule is really |
| 62 | + "author against the entry whose *client type* consumes the schema": a raw |
| 63 | + `Encryption` client from `wasm-inline` wants `wasm-inline` tables, but the |
| 64 | + Supabase adapter wants `eql/v3` tables on both of its entries, WASM engine or |
| 65 | + not. The section now says so, and the "The Supabase adapter has its own edge |
| 66 | + entry" note points at it. |
| 67 | +- **`skills/stash-supabase/SKILL.md` described the wrong failure mode for a |
| 68 | + missing declaration.** Omitting `schemas` on the edge entry cannot produce a |
| 69 | + no-column client — it is non-optional on the type and throws at construction — |
| 70 | + and an undeclared *table* throws rather than passing through unencrypted. The |
| 71 | + hazard is an undeclared **column** on a declared table, which is treated as |
| 72 | + plaintext; the bullet now says that, along with the one thing that limits it |
| 73 | + (a plaintext write to an `eql_v3_*` column fails the domain CHECK, though a |
| 74 | + NULL still passes) and the fact that the native entry's warning about |
| 75 | + unverified declarations is gated on the introspector and so never fires |
| 76 | + there. Reads get no equivalent backstop, and the bullet now says so: the |
| 77 | + `select('*')` refusal looks like one, but a query awaited with no |
| 78 | + `.select()` at all takes the raw-`*` branch in `query-builder.ts` and |
| 79 | + returns every column undecrypted. |
| 80 | +- **"Undeclared tables behave exactly as with no `schemas` at all" was false on |
| 81 | + the native entry too.** Introspection is gated on a resolved database URL, |
| 82 | + not on the absence of `schemas`, and an ambient `DATABASE_URL` is |
| 83 | + deliberately ignored once tables are declared. The statement holds only when |
| 84 | + `databaseUrl` is passed *alongside* `schemas`, which is what it now says. |
| 85 | + |
| 86 | +`config` is corrected everywhere that called all four `CS_*` values mandatory: |
| 87 | +the README, `skills/stash-supabase/SKILL.md`, and — in `skills/stash-edge/SKILL.md` |
| 88 | +— its frontmatter description, its Credentials section, its troubleshooting |
| 89 | +advice, and the native-vs-WASM comparison table. That skill already contradicted |
| 90 | +itself, since its own `config.authStrategy` example passes two values, not four. |
| 91 | +The same sentence in the `EncryptedSupabaseWasmOptions` doc comment |
| 92 | +(`packages/stack-supabase/src/wasm-inline.ts`) is fixed too, comment-only. Only |
| 93 | +`clientId` and `clientKey` are always required. Beyond them the config is a union — the |
| 94 | +access-key path adds `workspaceCrn` + `accessKey`, and the strategy path takes |
| 95 | +a pre-built `config.authStrategy` and makes `workspaceCrn` optional, because a |
| 96 | +built strategy already carries the CRN. `OidcFederationStrategy` is re-exported |
| 97 | +from `@cipherstash/stack/wasm-inline`, so authenticating as the end user works |
| 98 | +on the edge; what does not work is binding data to that user, which stays |
| 99 | +called out in its own `.withLockContext()` bullet. |
| 100 | + |
| 101 | +Tests now anchor the corrected claims against the code rather than against |
| 102 | +prose. `packages/stack-supabase/__tests__/supabase-wasm-config.test-d.ts` asserts |
| 103 | +at the type level that the edge `config` accepts both the access-key arm and a |
| 104 | +strategy-only arm without `workspaceCrn`, and rejects `clientId` + `clientKey` |
| 105 | +alone. `supabase-declared-mode.test.ts` gains a case pinning the real hazard: an |
| 106 | +undeclared column on a declared table reaches PostgREST as plaintext on insert, |
| 107 | +update and filter, and is absent from the decrypt call. |
| 108 | + |
| 109 | +Review found four more, one of them a change to a published type: |
| 110 | + |
| 111 | +- **`databaseUrl` was only refused for callers who wrote the options inline.** |
| 112 | + `EncryptedSupabaseWasmOptions` left the field out, and omission is policed by |
| 113 | + excess-property checking, which fires on fresh object literals alone. An |
| 114 | + options object assembled as a `const` and passed by variable — which is what |
| 115 | + a Node-to-edge port actually holds — type-checked clean and reached the |
| 116 | + construction-time throw instead, from documents saying the type checker |
| 117 | + enforced it. The field is now declared `databaseUrl?: never`, mirroring |
| 118 | + `WasmClientConfig.eqlVersion?: never` in `@cipherstash/stack`, which exists |
| 119 | + for the identical reason one package along. The runtime throw stays as the |
| 120 | + backstop for plain JS. New type tests cover the inline and by-variable |
| 121 | + shapes on both call forms, plus a positive control that the same options |
| 122 | + object still compiles once `databaseUrl` is dropped. |
| 123 | +- **The `select('*')` correction had landed in only one of its two shipped |
| 124 | + copies.** `skills/stash-supabase/SKILL.md` carried it; |
| 125 | + `skills/stash-managed-platforms/SKILL.md`, edited in the same change, still |
| 126 | + framed declared mode as giving things up "loudly rather than silently" over a |
| 127 | + bullet naming the refusal — precisely the inference the correction exists to |
| 128 | + kill. That skill is read as instruction by an agent on Lovable, v0, Bolt or |
| 129 | + Replit, and the failure it mispromised is silent: raw EQL payloads returned |
| 130 | + as `data`, no error. The wording is carried across, and a new guard |
| 131 | + (`scripts/__tests__/skills-select-star-not-a-read-backstop.test.mjs`) fails |
| 132 | + if any shipped document states the refusal without the caveat in the same |
| 133 | + section. Two copies of one fact drift the moment one of them is edited, and |
| 134 | + no reviewer diff shows the copy nobody touched. |
| 135 | +- **"Everything after construction is the same wrapper" was false in the first |
| 136 | + way a reader hits it.** Both `packages/stack-supabase/README.md` and |
| 137 | + `skills/stash-supabase/SKILL.md` said `from()`, the filters and the response |
| 138 | + shape are identical across the two entries — the README saying so twenty-five |
| 139 | + lines under a paragraph telling the same reader `select('*')` just works. The |
| 140 | + edge entry is always in declared mode, where `select('*')` is refused and |
| 141 | + `from()` on an undeclared table throws. Both sentences now name the two |
| 142 | + exceptions, so the quick-start snippet the section tells you to port no |
| 143 | + longer arrives with a promise it breaks. |
| 144 | +- **The ambient-`DATABASE_URL` warning cannot fire on the edge entry.** |
| 145 | + `skills/stash-managed-platforms/SKILL.md` said an ambient `DATABASE_URL` is |
| 146 | + ignored when `schemas` are passed, "with a warning that the declaration is |
| 147 | + unverified" — in a section whose subject is `wasm-inline`. Both the ambient |
| 148 | + read and the warning are gated on the introspector, which that build does not |
| 149 | + have, so nothing there ever tells you a declaration is incomplete. Now scoped |
| 150 | + to Node, with the edge case stated. The sentence immediately above it had the |
| 151 | + same fault and is fixed with it: the ⚠️ callout on undeclared columns offered |
| 152 | + "pass `databaseUrl` so introspection fills the gaps" as the remedy, inside a |
| 153 | + section about the entry that refuses `databaseUrl` — and it is the remedy a |
| 154 | + Lovable or Replit agent, which has only the edge entry, would have reached |
| 155 | + for. It now says introspection is unavailable there and what to do instead. |
| 156 | + |
| 157 | +`@cipherstash/wizard` is bumped alongside `stash` because `skills/` ships in |
| 158 | +both tarballs — `packages/wizard/tsup.config.ts` copies it into `dist/skills` |
| 159 | +and `package.json` lists that under `files`. Without the bump the published |
| 160 | +wizard keeps shipping the old text until some unrelated change moves its |
| 161 | +version. |
| 162 | + |
| 163 | +One more wording correction, of the kind #952 fixed in this package's `.d.ts`: |
| 164 | +`packages/stack-supabase/README.md` and `skills/stash-supabase/SKILL.md` both |
| 165 | +derived "runs on Node only" from introspection — "introspection needs a direct |
| 166 | +Postgres connection, **so** … this entry runs on Node only". Introspection is |
| 167 | +not the cause. The entry binds the native engine, so it is Node-only whether or |
| 168 | +not you declare `schemas`; a reader who took the stated cause at face value |
| 169 | +would conclude that declaring tables makes the root entry edge-capable, which is |
| 170 | +the exact wrong turn the `wasm-inline` entry exists to prevent. Both sentences |
| 171 | +now attribute the restriction to the engine and say that declaring `schemas` |
| 172 | +does not move it. Both files are enrolled in #952's |
| 173 | +`scripts/__tests__/supabase-runtime-claims.test.mjs`, which is what its own |
| 174 | +comment said to do once this branch stopped rewriting the same lines — the |
| 175 | +README and this skill are the two copies that reach a customer, and the guard |
| 176 | +now fails if either grows the claim back. `skills/stash-managed-platforms/SKILL.md` |
| 177 | +stays out, with the reason written down: its causal claims are correct, but |
| 178 | +rewording the unqualified "Worker" in its frontmatter `description` changes |
| 179 | +what the skill matches on, which is not a rider on a documentation fix. |
| 180 | + |
| 181 | +No runtime behaviour changes. The one non-documentation change is the |
| 182 | +`databaseUrl?: never` field on `EncryptedSupabaseWasmOptions`, which is |
| 183 | +type-level: it rejects at compile time a call that already threw at |
| 184 | +construction. |
0 commit comments