-
Notifications
You must be signed in to change notification settings - Fork 3
feat: add stash forge package #314
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@cipherstash/stack": minor | ||
| --- | ||
|
|
||
| Exposed a public method on the Encryption client to expose the build Encryption schema. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the exact API name in the changeset note. Line 5 doesn’t match the actual public surface added in this PR. The note should reference 🤖 Prompt for AI Agents |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@cipherstash/stack-forge": minor | ||
| --- | ||
|
|
||
| Initial release of the `stash-forge` CLI utility. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,6 @@ | ||||||||||||||||||||||||||||
| import { defineConfig } from '@cipherstash/stack-forge' | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export default defineConfig({ | ||||||||||||||||||||||||||||
| databaseUrl: process.env.DATABASE_URL!, | ||||||||||||||||||||||||||||
| client: './encrypt.ts', | ||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fail fast when Line 4 can pass Suggested patch import { defineConfig } from '@cipherstash/stack-forge'
+const databaseUrl = process.env.DATABASE_URL
+if (!databaseUrl) {
+ throw new Error('DATABASE_URL is required')
+}
+
export default defineConfig({
- databaseUrl: process.env.DATABASE_URL!,
+ databaseUrl,
})As per coding guidelines, "Example apps should be small, idiomatic, and runnable as-is with documented env vars; never leak secrets in code or logs". 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,289 @@ | ||
| # @cipherstash/stack-forge | ||
|
|
||
| Dev-time CLI and library for managing [CipherStash EQL](https://github.com/cipherstash/encrypt-query-language) (Encrypted Query Language) in your PostgreSQL database. | ||
|
|
||
| [](https://www.npmjs.com/package/@cipherstash/stack-forge) | ||
| [](https://github.com/cipherstash/protectjs/blob/main/LICENSE.md) | ||
| [](https://www.typescriptlang.org/) | ||
|
|
||
| --- | ||
|
|
||
| ## 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. | ||
|
|
||
| Think of it like Prisma or Drizzle Kit — a companion CLI that sets up the database while the main SDK handles runtime operations. | ||
|
|
||
| ## Install | ||
|
|
||
| ```bash | ||
| npm install -D @cipherstash/stack-forge | ||
| ``` | ||
|
|
||
| Or with your preferred package manager: | ||
|
|
||
| ```bash | ||
| pnpm add -D @cipherstash/stack-forge | ||
| yarn add -D @cipherstash/stack-forge | ||
| 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. | ||
|
|
||
| ### 1. Create a config file | ||
|
|
||
| Create `stash.config.ts` in your project root: | ||
|
|
||
| ```typescript | ||
| import { defineConfig } from '@cipherstash/stack-forge' | ||
|
|
||
| export default defineConfig({ | ||
| databaseUrl: process.env.DATABASE_URL!, | ||
| }) | ||
| ``` | ||
|
|
||
| ### 2. Add a `.env` file | ||
|
|
||
| ```env | ||
| DATABASE_URL=postgresql://user:password@localhost:5432/mydb | ||
| ``` | ||
|
|
||
| ### 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. | ||
|
|
||
| --- | ||
|
|
||
| ## Configuration | ||
|
|
||
| The `stash.config.ts` file is the single source of truth for stack-forge. It uses the `defineConfig` helper for type safety. | ||
|
|
||
| ```typescript | ||
| import { defineConfig } from '@cipherstash/stack-forge' | ||
|
|
||
| export default defineConfig({ | ||
| // Required: PostgreSQL connection string | ||
| 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 | ||
| 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 | | ||
|
|
||
| The CLI automatically loads `.env` files before evaluating the config, so `process.env` references work out of the box. | ||
|
|
||
| The config file is resolved by walking up from the current working directory, similar to how `tsconfig.json` resolution works. | ||
|
|
||
| --- | ||
|
|
||
| ## CLI Reference | ||
|
|
||
| ``` | ||
| stash-forge <command> [options] | ||
| ``` | ||
|
|
||
| ### `install` | ||
|
|
||
| Install the CipherStash EQL extensions into your database. | ||
|
|
||
| ```bash | ||
| npx stash-forge install [options] | ||
| ``` | ||
|
|
||
| | Option | Description | | ||
| |--------|-------------| | ||
| | `--dry-run` | Show what would happen without making changes | | ||
| | `--force` | Reinstall even if EQL is already installed | | ||
| | `--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 | | ||
| | `--name <value>` | Migration name when using `--drizzle` (default: `install-eql`) | | ||
| | `--out <value>` | Drizzle output directory when using `--drizzle` (default: `drizzle`) | | ||
|
|
||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| **Standard install:** | ||
|
|
||
| ```bash | ||
| npx stash-forge install | ||
| ``` | ||
|
|
||
| **Supabase install:** | ||
|
|
||
| ```bash | ||
| npx stash-forge install --supabase | ||
| ``` | ||
|
|
||
| The `--supabase` flag: | ||
| - Downloads 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` | ||
|
|
||
| **Preview changes first:** | ||
|
|
||
| ```bash | ||
| npx stash-forge install --dry-run | ||
| ``` | ||
|
|
||
| #### `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. | ||
|
|
||
| ```bash | ||
| npx stash-forge install --drizzle | ||
| 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). | ||
| 3. Writes the EQL SQL into the generated migration file. | ||
|
|
||
| With a custom migration name or output directory: | ||
|
|
||
| ```bash | ||
| npx stash-forge install --drizzle --name setup-eql --out ./migrations | ||
| 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`). | ||
|
|
||
| ### `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`). | ||
|
|
||
| ```bash | ||
| npx stash-forge push [options] | ||
| ``` | ||
|
|
||
| | Option | Description | | ||
| |--------|-------------| | ||
| | `--dry-run` | Load and validate the schema, then print it as JSON. No database changes. | | ||
|
|
||
| **Push schema to the database:** | ||
|
|
||
| ```bash | ||
| npx stash-forge push | ||
| ``` | ||
|
|
||
| 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. | ||
|
|
||
| **Preview your encryption schema without writing to the database:** | ||
|
|
||
| ```bash | ||
| npx stash-forge push --dry-run | ||
| ``` | ||
|
|
||
| ### Permission Pre-checks (install) | ||
|
|
||
| Before installing, `stash-forge` verifies that the connected database role has the required permissions: | ||
|
|
||
| - `CREATE` on the database (for `CREATE SCHEMA` and `CREATE EXTENSION`) | ||
| - `CREATE` on the `public` schema (for `CREATE TYPE public.eql_v2_encrypted`) | ||
| - `SUPERUSER` or extension owner (for `CREATE EXTENSION pgcrypto`, if not already installed) | ||
|
|
||
| If permissions are insufficient, the CLI exits with a clear message listing what's missing. | ||
|
|
||
| ### 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 | | ||
|
|
||
| --- | ||
|
|
||
| ## Programmatic API | ||
|
|
||
| You can also use stack-forge as a library: | ||
|
|
||
| ```typescript | ||
| import { EQLInstaller, loadStashConfig } from '@cipherstash/stack-forge' | ||
|
|
||
| // Load config from stash.config.ts | ||
| const config = await loadStashConfig() | ||
|
|
||
| // Create an installer | ||
| const installer = new EQLInstaller({ | ||
| databaseUrl: config.databaseUrl, | ||
| }) | ||
|
|
||
| // Check permissions before installing | ||
| const permissions = await installer.checkPermissions() | ||
| if (!permissions.ok) { | ||
| console.error('Missing permissions:', permissions.missing) | ||
| process.exit(1) | ||
| } | ||
|
|
||
| // Check if already installed | ||
| if (await installer.isInstalled()) { | ||
| console.log('EQL is already installed') | ||
| } else { | ||
| await installer.install() | ||
| } | ||
| ``` | ||
|
|
||
| ### `EQLInstaller` | ||
|
|
||
| | Method | Returns | Description | | ||
| |--------|---------|-------------| | ||
| | `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 | ||
|
|
||
| ```typescript | ||
| await installer.install({ | ||
| excludeOperatorFamily: true, // Skip CREATE OPERATOR FAMILY | ||
| supabase: true, // Supabase mode (implies excludeOperatorFamily + grants roles) | ||
| }) | ||
| ``` | ||
|
|
||
| ### `defineConfig` | ||
|
|
||
| Type-safe identity function for `stash.config.ts`: | ||
|
|
||
| ```typescript | ||
| import { defineConfig } from '@cipherstash/stack-forge' | ||
|
|
||
| export default defineConfig({ | ||
| databaseUrl: process.env.DATABASE_URL!, | ||
| }) | ||
| ``` | ||
|
|
||
| ### `loadStashConfig` | ||
|
|
||
| Finds and loads the nearest `stash.config.ts`, validates it with Zod, applies defaults (e.g. `client`), and returns the typed config: | ||
|
|
||
| ```typescript | ||
| import { loadStashConfig } from '@cipherstash/stack-forge' | ||
|
|
||
| const config = await loadStashConfig() | ||
| // config.databaseUrl — guaranteed to be a non-empty string | ||
| // config.client — path to encryption client (default: './src/encryption/index.ts') | ||
| ``` | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# First, check the actual .changeset/config.json file cat .changeset/config.jsonRepository: cipherstash/stack
Length of output: 448
🏁 Script executed:
Repository: cipherstash/stack
Length of output: 590
🌐 Web query:
Changesets onlyUpdatePeerDependentsWhenOutOfRange option behavior documentation💡 Result:
onlyUpdatePeerDependentsWhenOutOfRangeis an experimental Changesets config flag (boolean, defaultfalse) that you set under___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCHin.changeset/config.json. When set totrue, Changesets will only bump packages that peer-depend on another package when the updated peer’s new version would no longer satisfy the dependent’speerDependenciesversion range (“leaving the range”). In other words: if the peer’s new version is still within the existing peer range, the peer-dependent won’t be bumped just because the peer changed. [1][2]Example:
{ "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { "onlyUpdatePeerDependentsWhenOutOfRange": true } }Sources: Changesets “Experimental Options” docs [1] and the release note entry that introduced the flag [2].
[1] (changesets-docs.vercel.app)
[2] (newreleases.io)
Citations:
Global peer-dependent bump behavior changed; this can silently skip coordinated releases.
Lines 11–13 enable
onlyUpdatePeerDependentsWhenOutOfRange: true, which suppresses automatic bumping of peer dependents when internal peers update but remain within the dependent's version range. With existing peer ranges likeworkspace:*(packages/protect-dynamodb) and>=...(packages/drizzle), peer dependents will not be bumped unless the peer version goes out of range—causing silent version coordination drift. This is particularly problematic forworkspace:*dependencies, where updates rarely exceed range bounds.Consider removing this flag or setting it to an empty object:
Suggested config change
📝 Committable suggestion
🤖 Prompt for AI Agents