Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sharp-phones-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cipherstash/stack": minor
---

Fixed Supabase or wrapper to escape EQL payloads correctly.
5 changes: 5 additions & 0 deletions .changeset/wet-zoos-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cipherstash/stack-forge": minor
---

Add additional CLI tools for validate, status, init. Fixed push command to work with CipherStash Proxy.
12 changes: 12 additions & 0 deletions examples/basic/src/encryption/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { encryptedTable, encryptedColumn } from '@cipherstash/stack/schema'
import { Encryption } from '@cipherstash/stack'

export const helloTable = encryptedTable('hello', {
world: encryptedColumn('world').equality().orderAndRange(),
name: encryptedColumn('name').equality().freeTextSearch(),
age: encryptedColumn('age').dataType('number').equality().orderAndRange(),
})

export const encryptionClient = await Encryption({
schemas: [helloTable],
})
2 changes: 1 addition & 1 deletion examples/basic/stash.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ import { defineConfig } from '@cipherstash/stack-forge'

export default defineConfig({
databaseUrl: process.env.DATABASE_URL!,
client: './encrypt.ts',
client: './src/encryption/index.ts',
})
225 changes: 194 additions & 31 deletions packages/stack-forge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Dev-time CLI and library for managing [CipherStash EQL](https://github.com/ciphe

## Why stack-forge?

`@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.
`@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.

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

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

## Quick Start

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.
The fastest way to get started is with the interactive `init` command:

### 1. Create a config file
```bash
npx stash-forge init
```

This will:
1. Check if `@cipherstash/stack` is installed and offer to install it
2. Ask for your database URL
3. Ask which integration you're using (Drizzle, Supabase, or plain PostgreSQL)
4. Let you build an encryption schema interactively or use a placeholder
5. Generate `stash.config.ts` and your encryption client file

Then install EQL in your database:

```bash
npx stash-forge install
```

That's it. EQL is now installed and your encryption schema is ready.

### Manual setup

If you prefer to set things up manually:

#### 1. Create a config file

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

Expand All @@ -44,22 +67,18 @@ export default defineConfig({
})
```

### 2. Add a `.env` file
#### 2. Add a `.env` file

```env
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
```

### 3. Install EQL
#### 3. Install EQL

```bash
npx stash-forge install
```

That's it. EQL is now installed in your database.

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`.

**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.

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

// Optional: path to your encryption client (default: './src/encryption/index.ts')
// Used by `stash-forge push` to load the encryption schema
// Used by `stash-forge push` and `stash-forge validate` to load the encryption schema
client: './src/encryption/index.ts',

// Optional: CipherStash workspace and credentials (for future schema sync)
workspaceId: process.env.CS_WORKSPACE_ID,
clientAccessKey: process.env.CS_CLIENT_ACCESS_KEY,
})
```

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

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

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

### `init`

Initialize CipherStash Forge in your project with an interactive wizard.

```bash
npx stash-forge init
```

The wizard will:
- Check if `@cipherstash/stack` is installed and prompt to install it (detects your package manager automatically)
- Ask for your database URL (pre-fills from `DATABASE_URL` env var)
- Ask which integration you're using (Drizzle ORM, Supabase, or plain PostgreSQL)
- Ask where to create the encryption client file
- If the client file already exists, ask whether to keep it or overwrite
- Let you choose between building a schema interactively or using a placeholder:
- **Build a schema:** asks for table name, column names, data types, and search operations for each column
- **Placeholder:** generates an example `users` table with `email` and `name` columns
- Generate `stash.config.ts` and the encryption client file
- Print next steps with links to the [CipherStash dashboard](https://dashboard.cipherstash.com/sign-in) for credentials

### `install`

Install the CipherStash EQL extensions into your database.
Install the CipherStash EQL extensions into your database. Uses bundled SQL by default for offline, deterministic installs.

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

Expand All @@ -135,15 +169,23 @@ npx stash-forge install --supabase
```

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

> **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.

**Preview changes first:**

```bash
npx stash-forge install --dry-run
```

**Fetch the latest EQL from GitHub instead of using the bundled version:**

```bash
npx stash-forge install --latest
```

#### `install --drizzle`

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.
Expand All @@ -156,7 +198,7 @@ npx drizzle-kit migrate
**How it works:**

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

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

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`).

### `upgrade`

Upgrade an existing EQL installation to the version bundled with the package (or the latest from GitHub).

```bash
npx stash-forge upgrade [options]
```

| Option | Description |
|--------|-------------|
| `--dry-run` | Show what would happen without making changes |
| `--supabase` | Use Supabase-compatible upgrade |
| `--exclude-operator-family` | Skip operator family creation |
| `--latest` | Fetch the latest EQL from GitHub instead of using the bundled version |

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.

```bash
npx stash-forge upgrade
```

If EQL is not installed, the command suggests running `stash-forge install` instead.

### `validate`

Validate your encryption schema for common misconfigurations.

```bash
npx stash-forge validate [options]
```

| Option | Description |
|--------|-------------|
| `--supabase` | Check for Supabase-specific issues (e.g. ORDER BY without operator families) |
| `--exclude-operator-family` | Check for issues when operator families are excluded |

**Validation rules:**

| Rule | Severity | Description |
|------|----------|-------------|
| `freeTextSearch` on non-string column | Warning | Free-text search only works with string data |
| `orderAndRange` without operator families | Warning | ORDER BY won't work without operator families |
| No indexes on encrypted column | Info | Column is encrypted but not searchable |
| `searchableJson` without `json` data type | Error | searchableJson requires `dataType("json")` |

```bash
# Basic validation
npx stash-forge validate

# Validate with Supabase context
npx stash-forge validate --supabase
```

Validation is also automatically run before `push` — issues are logged as warnings but don't block the push.

The command exits with code 1 if there are errors (not for warnings or info).

### `push`

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`).
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.

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

**Push schema to the database:**
When pushing, stash-forge:
1. Loads the encryption client from the path in `stash.config.ts`
2. Runs schema validation (warns but doesn't block)
3. Transforms SDK data types to EQL-compatible `cast_as` values (see table below)
4. Connects to Postgres and marks existing `eql_v2_configuration` rows as `inactive`
5. Inserts the new config as an `active` row

**SDK to EQL type mapping:**

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:

| SDK type (`dataType()`) | EQL `cast_as` |
|-------------------------|---------------|
| `string` | `text` |
| `text` | `text` |
| `number` | `double` |
| `bigint` | `big_int` |
| `boolean` | `boolean` |
| `date` | `date` |
| `json` | `jsonb` |

### `status`

Show the current state of EQL in your database.

```bash
npx stash-forge push
npx stash-forge status
```

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.
Reports:
- Whether EQL is installed and which version
- Database permission status
- Whether an active encrypt config exists in `eql_v2_configuration` (only relevant for CipherStash Proxy)

### `test-connection`

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

```bash
npx stash-forge push --dry-run
npx stash-forge test-connection
```

Reports the database name, connected user/role, and PostgreSQL server version. Useful for debugging connection issues before running `install` or `push`.

### Permission Pre-checks (install)

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

### Planned Commands

The following commands are defined but not yet implemented:

| Command | Description |
|---------|-------------|
| `init` | Initialize CipherStash Forge in your project |
| `migrate` | Run pending encrypt config migrations |
| `status` | Show EQL installation status |

---

## Bundled EQL SQL

The EQL install SQL is bundled with the package for offline, deterministic installs. Three variants are included:

| File | Used when |
|------|-----------|
| `cipherstash-encrypt.sql` | Default install |
| `cipherstash-encrypt-supabase.sql` | `--supabase` flag |
| `cipherstash-encrypt-no-operator-family.sql` | `--exclude-operator-family` flag |

The bundled SQL version is pinned to the package version. Use `--latest` to fetch the newest version from GitHub instead.

---

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

#### Install Options

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

### `loadBundledEqlSql`

Load the bundled EQL install SQL as a string (useful for custom install workflows):

```typescript
import { loadBundledEqlSql } from '@cipherstash/stack-forge'

const sql = loadBundledEqlSql() // standard
const sql = loadBundledEqlSql({ supabase: true }) // supabase variant
const sql = loadBundledEqlSql({ excludeOperatorFamily: true }) // no operator family
```

### `downloadEqlSql`

Download the latest EQL install SQL from GitHub:

```typescript
import { downloadEqlSql } from '@cipherstash/stack-forge'

const sql = await downloadEqlSql() // standard
const sql = await downloadEqlSql(true) // no operator family variant
```

### `defineConfig`

Type-safe identity function for `stash.config.ts`:
Expand Down
Loading
Loading