Skip to content

Commit 3414761

Browse files
committed
chore: usability improvements
1 parent 391f57f commit 3414761

File tree

31 files changed

+18870
-168
lines changed

31 files changed

+18870
-168
lines changed

.changeset/sharp-phones-own.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cipherstash/stack": minor
3+
---
4+
5+
Fixed Supabase or wrapper to escape EQL payloads correctly.

.changeset/wet-zoos-follow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@cipherstash/stack-forge": minor
3+
---
4+
5+
Add additional CLI tools for validate, status, init. Fixed push command to work with CipherStash Proxy.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { encryptedTable, encryptedColumn } from '@cipherstash/stack/schema'
2+
import { Encryption } from '@cipherstash/stack'
3+
4+
export const helloTable = encryptedTable('hello', {
5+
world: encryptedColumn('world').equality().orderAndRange(),
6+
name: encryptedColumn('name').equality().freeTextSearch(),
7+
age: encryptedColumn('age').dataType('number').equality().orderAndRange(),
8+
})
9+
10+
export const encryptionClient = await Encryption({
11+
schemas: [helloTable],
12+
})

examples/basic/stash.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ import { defineConfig } from '@cipherstash/stack-forge'
22

33
export default defineConfig({
44
databaseUrl: process.env.DATABASE_URL!,
5-
client: './encrypt.ts',
5+
client: './src/encryption/index.ts',
66
})

packages/stack-forge/README.md

Lines changed: 194 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Dev-time CLI and library for managing [CipherStash EQL](https://github.com/ciphe
1010

1111
## Why stack-forge?
1212

13-
`@cipherstash/stack` is the runtime encryption SDK — it should stay lean and free of heavy dependencies like `pg`. `@cipherstash/stack-forge` is a **devDependency** that handles database tooling: installing EQL extensions, checking permissions, and managing schema lifecycle.
13+
`@cipherstash/stack` is the runtime encryption SDK — it should stay lean and free of heavy dependencies like `pg`. `@cipherstash/stack-forge` is a **devDependency** that handles database tooling: installing EQL extensions, checking permissions, validating schemas, and managing schema lifecycle.
1414

1515
Think of it like Prisma or Drizzle Kit — a companion CLI that sets up the database while the main SDK handles runtime operations.
1616

@@ -30,9 +30,32 @@ bun add -D @cipherstash/stack-forge
3030

3131
## Quick Start
3232

33-
You can install EQL in two ways: **direct install** (connects to the DB and runs the SQL) or **Drizzle migration** (generates a migration file; you run `drizzle-kit migrate` yourself). The steps below use the direct install path.
33+
The fastest way to get started is with the interactive `init` command:
3434

35-
### 1. Create a config file
35+
```bash
36+
npx stash-forge init
37+
```
38+
39+
This will:
40+
1. Check if `@cipherstash/stack` is installed and offer to install it
41+
2. Ask for your database URL
42+
3. Ask which integration you're using (Drizzle, Supabase, or plain PostgreSQL)
43+
4. Let you build an encryption schema interactively or use a placeholder
44+
5. Generate `stash.config.ts` and your encryption client file
45+
46+
Then install EQL in your database:
47+
48+
```bash
49+
npx stash-forge install
50+
```
51+
52+
That's it. EQL is now installed and your encryption schema is ready.
53+
54+
### Manual setup
55+
56+
If you prefer to set things up manually:
57+
58+
#### 1. Create a config file
3659

3760
Create `stash.config.ts` in your project root:
3861

@@ -44,22 +67,18 @@ export default defineConfig({
4467
})
4568
```
4669

47-
### 2. Add a `.env` file
70+
#### 2. Add a `.env` file
4871

4972
```env
5073
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
5174
```
5275

53-
### 3. Install EQL
76+
#### 3. Install EQL
5477

5578
```bash
5679
npx stash-forge install
5780
```
5881

59-
That's it. EQL is now installed in your database.
60-
61-
If your encryption client lives elsewhere, set `client` in `stash.config.ts` (e.g. `client: './lib/encryption.ts'`). That path is used by `stash-forge push`.
62-
6382
**Using Drizzle?** To install EQL via your migration pipeline instead, run `npx stash-forge install --drizzle`, then `npx drizzle-kit migrate`. See [install --drizzle](#install---drizzle) below.
6483

6584
---
@@ -76,21 +95,15 @@ export default defineConfig({
7695
databaseUrl: process.env.DATABASE_URL!,
7796

7897
// Optional: path to your encryption client (default: './src/encryption/index.ts')
79-
// Used by `stash-forge push` to load the encryption schema
98+
// Used by `stash-forge push` and `stash-forge validate` to load the encryption schema
8099
client: './src/encryption/index.ts',
81-
82-
// Optional: CipherStash workspace and credentials (for future schema sync)
83-
workspaceId: process.env.CS_WORKSPACE_ID,
84-
clientAccessKey: process.env.CS_CLIENT_ACCESS_KEY,
85100
})
86101
```
87102

88103
| Option | Required | Description |
89104
|--------|----------|-------------|
90105
| `databaseUrl` | Yes | PostgreSQL connection string |
91-
| `client` | No | Path to encryption client file (default: `'./src/encryption/index.ts'`). Used by `push` to load the encryption schema. |
92-
| `workspaceId` | No | CipherStash workspace ID |
93-
| `clientAccessKey` | No | CipherStash client access key |
106+
| `client` | No | Path to encryption client file (default: `'./src/encryption/index.ts'`). Used by `push` and `validate` to load the encryption schema. |
94107

95108
The CLI automatically loads `.env` files before evaluating the config, so `process.env` references work out of the box.
96109

@@ -104,9 +117,29 @@ The config file is resolved by walking up from the current working directory, si
104117
stash-forge <command> [options]
105118
```
106119

120+
### `init`
121+
122+
Initialize CipherStash Forge in your project with an interactive wizard.
123+
124+
```bash
125+
npx stash-forge init
126+
```
127+
128+
The wizard will:
129+
- Check if `@cipherstash/stack` is installed and prompt to install it (detects your package manager automatically)
130+
- Ask for your database URL (pre-fills from `DATABASE_URL` env var)
131+
- Ask which integration you're using (Drizzle ORM, Supabase, or plain PostgreSQL)
132+
- Ask where to create the encryption client file
133+
- If the client file already exists, ask whether to keep it or overwrite
134+
- Let you choose between building a schema interactively or using a placeholder:
135+
- **Build a schema:** asks for table name, column names, data types, and search operations for each column
136+
- **Placeholder:** generates an example `users` table with `email` and `name` columns
137+
- Generate `stash.config.ts` and the encryption client file
138+
- Print next steps with links to the [CipherStash dashboard](https://dashboard.cipherstash.com/sign-in) for credentials
139+
107140
### `install`
108141

109-
Install the CipherStash EQL extensions into your database.
142+
Install the CipherStash EQL extensions into your database. Uses bundled SQL by default for offline, deterministic installs.
110143

111144
```bash
112145
npx stash-forge install [options]
@@ -119,6 +152,7 @@ npx stash-forge install [options]
119152
| `--supabase` | Use Supabase-compatible install (excludes operator families + grants Supabase roles) |
120153
| `--exclude-operator-family` | Skip operator family creation (for non-superuser database roles) |
121154
| `--drizzle` | Generate a Drizzle migration instead of direct install |
155+
| `--latest` | Fetch the latest EQL from GitHub instead of using the bundled version |
122156
| `--name <value>` | Migration name when using `--drizzle` (default: `install-eql`) |
123157
| `--out <value>` | Drizzle output directory when using `--drizzle` (default: `drizzle`) |
124158

@@ -135,15 +169,23 @@ npx stash-forge install --supabase
135169
```
136170

137171
The `--supabase` flag:
138-
- Downloads the Supabase-specific SQL variant (no `CREATE OPERATOR FAMILY`)
172+
- Uses the Supabase-specific SQL variant (no `CREATE OPERATOR FAMILY`)
139173
- Grants `USAGE`, table, routine, and sequence permissions on the `eql_v2` schema to `anon`, `authenticated`, and `service_role`
140174

175+
> **Note:** Without operator families, `ORDER BY` on encrypted columns is not currently supported — regardless of the client or ORM used. Sort application-side after decrypting the results as a workaround. Operator family support for Supabase is being developed with the Supabase and CipherStash teams. This limitation also applies when using `--exclude-operator-family` on any database.
176+
141177
**Preview changes first:**
142178

143179
```bash
144180
npx stash-forge install --dry-run
145181
```
146182

183+
**Fetch the latest EQL from GitHub instead of using the bundled version:**
184+
185+
```bash
186+
npx stash-forge install --latest
187+
```
188+
147189
#### `install --drizzle`
148190

149191
If you use [Drizzle ORM](https://orm.drizzle.team/) and want EQL installation as part of your migration history, use the `--drizzle` flag. It creates a Drizzle migration file containing the EQL install SQL, then you run your normal Drizzle migrations to apply it.
@@ -156,7 +198,7 @@ npx drizzle-kit migrate
156198
**How it works:**
157199

158200
1. Runs `drizzle-kit generate --custom --name=<name>` to create an empty migration.
159-
2. Downloads the EQL install script from the [EQL GitHub releases](https://github.com/cipherstash/encrypt-query-language/releases/latest).
201+
2. Loads the bundled EQL install SQL (or downloads from GitHub with `--latest`).
160202
3. Writes the EQL SQL into the generated migration file.
161203

162204
With a custom migration name or output directory:
@@ -168,9 +210,66 @@ npx drizzle-kit migrate
168210

169211
You need `drizzle-kit` installed in your project (`npm install -D drizzle-kit`). The `--out` directory must match your Drizzle config (e.g. `drizzle.config.ts`).
170212

213+
### `upgrade`
214+
215+
Upgrade an existing EQL installation to the version bundled with the package (or the latest from GitHub).
216+
217+
```bash
218+
npx stash-forge upgrade [options]
219+
```
220+
221+
| Option | Description |
222+
|--------|-------------|
223+
| `--dry-run` | Show what would happen without making changes |
224+
| `--supabase` | Use Supabase-compatible upgrade |
225+
| `--exclude-operator-family` | Skip operator family creation |
226+
| `--latest` | Fetch the latest EQL from GitHub instead of using the bundled version |
227+
228+
The EQL install SQL is idempotent and safe to re-run. The upgrade command checks the current version, re-runs the install SQL, then reports the new version.
229+
230+
```bash
231+
npx stash-forge upgrade
232+
```
233+
234+
If EQL is not installed, the command suggests running `stash-forge install` instead.
235+
236+
### `validate`
237+
238+
Validate your encryption schema for common misconfigurations.
239+
240+
```bash
241+
npx stash-forge validate [options]
242+
```
243+
244+
| Option | Description |
245+
|--------|-------------|
246+
| `--supabase` | Check for Supabase-specific issues (e.g. ORDER BY without operator families) |
247+
| `--exclude-operator-family` | Check for issues when operator families are excluded |
248+
249+
**Validation rules:**
250+
251+
| Rule | Severity | Description |
252+
|------|----------|-------------|
253+
| `freeTextSearch` on non-string column | Warning | Free-text search only works with string data |
254+
| `orderAndRange` without operator families | Warning | ORDER BY won't work without operator families |
255+
| No indexes on encrypted column | Info | Column is encrypted but not searchable |
256+
| `searchableJson` without `json` data type | Error | searchableJson requires `dataType("json")` |
257+
258+
```bash
259+
# Basic validation
260+
npx stash-forge validate
261+
262+
# Validate with Supabase context
263+
npx stash-forge validate --supabase
264+
```
265+
266+
Validation is also automatically run before `push` — issues are logged as warnings but don't block the push.
267+
268+
The command exits with code 1 if there are errors (not for warnings or info).
269+
171270
### `push`
172271

173-
Load your encryption schema from the file specified by `client` in `stash.config.ts` and apply it to the database (or preview with `--dry-run`).
272+
Push your encryption schema to the database. **This is only required when using CipherStash Proxy.** If you're using the SDK directly (Drizzle, Supabase, or plain PostgreSQL), this step is not needed — the schema lives in your application code.
174273

175274
```bash
176275
npx stash-forge push [options]
@@ -180,20 +279,50 @@ npx stash-forge push [options]
180279
|--------|-------------|
181280
| `--dry-run` | Load and validate the schema, then print it as JSON. No database changes. |
182281

183-
**Push schema to the database:**
282+
When pushing, stash-forge:
283+
1. Loads the encryption client from the path in `stash.config.ts`
284+
2. Runs schema validation (warns but doesn't block)
285+
3. Transforms SDK data types to EQL-compatible `cast_as` values (see table below)
286+
4. Connects to Postgres and marks existing `eql_v2_configuration` rows as `inactive`
287+
5. Inserts the new config as an `active` row
288+
289+
**SDK to EQL type mapping:**
290+
291+
The SDK uses developer-friendly type names (e.g. `'string'`, `'number'`), but EQL expects PostgreSQL-aligned types. The `push` command automatically maps these before writing to the database:
292+
293+
| SDK type (`dataType()`) | EQL `cast_as` |
294+
|-------------------------|---------------|
295+
| `string` | `text` |
296+
| `text` | `text` |
297+
| `number` | `double` |
298+
| `bigint` | `big_int` |
299+
| `boolean` | `boolean` |
300+
| `date` | `date` |
301+
| `json` | `jsonb` |
302+
303+
### `status`
304+
305+
Show the current state of EQL in your database.
184306

185307
```bash
186-
npx stash-forge push
308+
npx stash-forge status
187309
```
188310

189-
This connects to Postgres, marks any existing rows in `eql_v2_configuration` as `inactive`, and inserts the current encrypt config as a new row with state `active`. Your runtime encryption (e.g. `@cipherstash/stack`) reads the active configuration from this table.
311+
Reports:
312+
- Whether EQL is installed and which version
313+
- Database permission status
314+
- Whether an active encrypt config exists in `eql_v2_configuration` (only relevant for CipherStash Proxy)
315+
316+
### `test-connection`
190317

191-
**Preview your encryption schema without writing to the database:**
318+
Verify that the database URL in your config is valid and the database is reachable.
192319

193320
```bash
194-
npx stash-forge push --dry-run
321+
npx stash-forge test-connection
195322
```
196323

324+
Reports the database name, connected user/role, and PostgreSQL server version. Useful for debugging connection issues before running `install` or `push`.
325+
197326
### Permission Pre-checks (install)
198327

199328
Before installing, `stash-forge` verifies that the connected database role has the required permissions:
@@ -206,13 +335,23 @@ If permissions are insufficient, the CLI exits with a clear message listing what
206335

207336
### Planned Commands
208337

209-
The following commands are defined but not yet implemented:
210-
211338
| Command | Description |
212339
|---------|-------------|
213-
| `init` | Initialize CipherStash Forge in your project |
214340
| `migrate` | Run pending encrypt config migrations |
215-
| `status` | Show EQL installation status |
341+
342+
---
343+
344+
## Bundled EQL SQL
345+
346+
The EQL install SQL is bundled with the package for offline, deterministic installs. Three variants are included:
347+
348+
| File | Used when |
349+
|------|-----------|
350+
| `cipherstash-encrypt.sql` | Default install |
351+
| `cipherstash-encrypt-supabase.sql` | `--supabase` flag |
352+
| `cipherstash-encrypt-no-operator-family.sql` | `--exclude-operator-family` flag |
353+
354+
The bundled SQL version is pinned to the package version. Use `--latest` to fetch the newest version from GitHub instead.
216355

217356
---
218357

@@ -253,17 +392,41 @@ if (await installer.isInstalled()) {
253392
| `checkPermissions()` | `Promise<PermissionCheckResult>` | Check if the database role has required permissions |
254393
| `isInstalled()` | `Promise<boolean>` | Check if the `eql_v2` schema exists |
255394
| `getInstalledVersion()` | `Promise<string \| null>` | Get the installed EQL version (or `null`) |
256-
| `install(options?)` | `Promise<void>` | Download and execute the EQL install SQL in a transaction |
395+
| `install(options?)` | `Promise<void>` | Execute the EQL install SQL in a transaction |
257396

258397
#### Install Options
259398

260399
```typescript
261400
await installer.install({
262401
excludeOperatorFamily: true, // Skip CREATE OPERATOR FAMILY
263402
supabase: true, // Supabase mode (implies excludeOperatorFamily + grants roles)
403+
latest: true, // Fetch latest from GitHub instead of bundled
264404
})
265405
```
266406

407+
### `loadBundledEqlSql`
408+
409+
Load the bundled EQL install SQL as a string (useful for custom install workflows):
410+
411+
```typescript
412+
import { loadBundledEqlSql } from '@cipherstash/stack-forge'
413+
414+
const sql = loadBundledEqlSql() // standard
415+
const sql = loadBundledEqlSql({ supabase: true }) // supabase variant
416+
const sql = loadBundledEqlSql({ excludeOperatorFamily: true }) // no operator family
417+
```
418+
419+
### `downloadEqlSql`
420+
421+
Download the latest EQL install SQL from GitHub:
422+
423+
```typescript
424+
import { downloadEqlSql } from '@cipherstash/stack-forge'
425+
426+
const sql = await downloadEqlSql() // standard
427+
const sql = await downloadEqlSql(true) // no operator family variant
428+
```
429+
267430
### `defineConfig`
268431

269432
Type-safe identity function for `stash.config.ts`:

0 commit comments

Comments
 (0)