diff --git a/IA.md b/IA.md index 622bae1..3244e94 100644 --- a/IA.md +++ b/IA.md @@ -309,7 +309,7 @@ Two notes: - [ ] ⛔ `/integrations/supabase/edge-functions` — pending Deno/FFI answer - [ ] ⛔ `/integrations/supabase/realtime` — pending product verification - [x] `/integrations/drizzle` -- [x] `/integrations/prisma-next` — EQL v2 today; revisit when v3 support lands +- [x] `/integrations/prisma` — Prisma ORM 8 RC, EQL v3, Prisma Postgres, and Prisma Compute - [ ] `/integrations/aws/rds-aurora` — Proxy path - [ ] `/integrations/aws/dynamodb` - [ ] `/integrations/clerk` diff --git a/content/docs/get-started/choose-your-stack.mdx b/content/docs/get-started/choose-your-stack.mdx index 8ec8264..c333a60 100644 --- a/content/docs/get-started/choose-your-stack.mdx +++ b/content/docs/get-started/choose-your-stack.mdx @@ -29,6 +29,7 @@ EQL installs into any Postgres you can connect to. It needs no extension, no sup | Platform | Works | Notes | |---|---|---| | Supabase | Yes | [Supabase integration](/integrations/supabase). Expose the `eql_v3` schema in the dashboard. | +| Prisma Postgres | Yes | [Prisma Postgres integration](/integrations/prisma/prisma-postgres). EQL is installed through the Prisma ORM 8 migration graph. | | Neon, RDS, Aurora, Cloud SQL | Yes | `stash eql install` detects a non-superuser role and adapts. | | Self-hosted Postgres | Yes | Full operator-class support, so the ORE ordering mechanism is available. | | Postgres behind PgBouncer | Yes | Transaction pooling is fine. Proxy has its own pooling. | @@ -45,7 +46,7 @@ Managed platforms usually block custom operator class creation. That's why the d |---|---|---| | Drizzle | [Drizzle integration](/stack/cipherstash/encryption/drizzle) | An `encryptedType` column and typed query operators. Your Drizzle code keeps its shape. | | Supabase JS | [Supabase integration](/integrations/supabase) | `encryptedSupabase` wraps the client. `.eq()`, `.gt()`, `.order()` keep working. | -| Prisma | [Prisma integration](/stack/cipherstash/encryption/prisma-next) | Encrypt in the data layer around Prisma calls. | +| Prisma ORM 8 RC | [Prisma integration](/integrations/prisma) | Encrypted fields and query operators are generated from `schema.prisma`. | | Raw SQL | [Quickstart](/get-started/quickstart) | Encrypt the value, insert it, cast the query operand. | | An ORM we don't list | The SDK | Encrypt before write, decrypt after read. Every ORM supports that. | diff --git a/content/docs/get-started/quickstart.mdx b/content/docs/get-started/quickstart.mdx index 330514d..38571ec 100644 --- a/content/docs/get-started/quickstart.mdx +++ b/content/docs/get-started/quickstart.mdx @@ -38,7 +38,7 @@ This opens a browser for device-based authentication, so local development needs 4. Scaffolds an encryption module at `src/encryption/index.ts`. 5. Writes `.cipherstash/context.json` with what it detected about your project. -See the [CLI reference](/reference/cli/init) for the flags, including `--supabase`, `--drizzle`, and `--prisma-next`. +Integration-specific initialization flags include `--supabase`, `--drizzle`, and `--prisma`. EQL v3 is the default, so `init` installs it directly against the database. Confirm what you ended up with: diff --git a/content/docs/guides/migration/encrypt-existing-data.mdx b/content/docs/guides/migration/encrypt-existing-data.mdx index a613f6f..7cc9975 100644 --- a/content/docs/guides/migration/encrypt-existing-data.mdx +++ b/content/docs/guides/migration/encrypt-existing-data.mdx @@ -10,7 +10,7 @@ Encrypting a populated column is a staged application and database migration. Ci The safe pattern is to add an encrypted mirror column, dual-write new changes, backfill historical rows, and then move reads to the mirror. The plaintext column remains authoritative until the final cleanup, making every earlier stage reversible. -This guide uses `encryptedSupabase` for its examples. The same sequence applies when the application uses [Drizzle](/integrations/drizzle), [Prisma Next](/integrations/prisma-next), or the [Stack SDK](/reference/stack): change the adapter-specific code, not the rollout order. +This guide uses `encryptedSupabase` for its examples. The same sequence applies when the application uses [Drizzle](/integrations/drizzle), [Prisma ORM 8](/integrations/prisma), or the [Stack SDK](/reference/stack): change the adapter-specific code, not the rollout order. ## Migration sequence diff --git a/content/docs/integrations/drizzle.mdx b/content/docs/integrations/drizzle.mdx index 57b6698..9d963c4 100644 --- a/content/docs/integrations/drizzle.mdx +++ b/content/docs/integrations/drizzle.mdx @@ -7,7 +7,7 @@ audience: [developer] integration: category: orm setup: code-only - pairsWith: [supabase, prisma-next] + pairsWith: [supabase, prisma] verifiedAgainst: stack: "1.0.0-rc.0" eql: "3.0.0" diff --git a/content/docs/integrations/index.mdx b/content/docs/integrations/index.mdx index b2ee232..050b058 100644 --- a/content/docs/integrations/index.mdx +++ b/content/docs/integrations/index.mdx @@ -16,8 +16,8 @@ Where the encrypted data lives. EQL installs into any Postgres you can connect t Install EQL with the `stash` CLI, then query encrypted columns through PostgREST. Row Level Security, Supabase Auth, and Edge Functions all apply unchanged. - - Prisma's hosted Postgres. EQL is installed by the Prisma Next migration system, in the same sweep that creates your tables. + + Prisma's managed Postgres, with EQL installed through the Prisma ORM 8 migration graph. @@ -29,8 +29,8 @@ How your application queries it. `encryptedSupabase` wraps your existing client so `.eq()`, `.in()`, `.gt()`, and `.order()` keep working on encrypted columns. - - Encrypted columns declared in `schema.prisma`. Targets EQL v2. Not classic Prisma. + + Encrypted EQL v3 columns declared in `schema.prisma`, with typed encrypted query operators. Concrete encrypted column types and capability-checked operators. Targets EQL v3. diff --git a/content/docs/integrations/meta.json b/content/docs/integrations/meta.json index eb155bd..bfb94b1 100644 --- a/content/docs/integrations/meta.json +++ b/content/docs/integrations/meta.json @@ -1,5 +1,5 @@ { "title": "Integrations", "icon": "Blocks", - "pages": ["!index", "supabase", "..."] + "pages": ["!index", "supabase", "prisma", "..."] } diff --git a/content/docs/integrations/prisma-next.mdx b/content/docs/integrations/prisma-next.mdx deleted file mode 100644 index a5333fa..0000000 --- a/content/docs/integrations/prisma-next.mdx +++ /dev/null @@ -1,146 +0,0 @@ ---- -title: Prisma Next -description: "Encrypted columns with Prisma Next: declare them in schema.prisma, and the migration system installs EQL for you." -type: tutorial -components: [encryption, eql] -audience: [developer] -integration: - category: orm - setup: code-only - pairsWith: [supabase, drizzle] -verifiedAgainst: - prismaNext: "0.3.2" - eql: "2" ---- - -`@cipherstash/prisma-next` adds searchable, field-level encryption to [Prisma Next](https://www.npmjs.com/package/prisma-next). You declare encrypted columns in `schema.prisma`, and the framework's migration system installs the EQL bundle in the same sweep that creates your tables. There is no separate "install EQL" step. - - -This integration targets **Prisma Next**, not the classic `@prisma/client` ORM. If you are on classic Prisma, there is no extension: use the [Stack SDK](/reference/stack) directly, encrypting before a write and decrypting after a read. Every ORM supports that. - -It also targets **EQL v2**, while the rest of these docs target [EQL v3](/reference/eql). Encrypted columns are `eql_v2_encrypted`, and free-text search is `cipherstashIlike` rather than v3's token containment. - - -## Install - -```bash cta cta-type="install" example-id="install-prisma-next" -npm install @cipherstash/stack @cipherstash/prisma-next -``` - -## Declare encrypted columns - -Six column types cover the encryptable surface: string, double, bigint, date, boolean, and JSON. - -```prisma filename="prisma/schema.prisma" -model User { - id String @id - email cipherstash.EncryptedString() - salary cipherstash.EncryptedDouble() - birthday cipherstash.EncryptedDate() - preferences cipherstash.EncryptedJson() -} -``` - -Register the extension pack so the framework knows how to plan migrations and encode values: - -```typescript filename="prisma-next.config.ts" -import cipherstash from "@cipherstash/prisma-next/control" - -export default defineConfig({ - // family, target, adapter, contract - extensionPacks: [cipherstash], -}) -``` - -## Wire up the client - -`cipherstashFromStack` builds the encryption client from your emitted contract, so there is no second schema to maintain alongside `schema.prisma`: - -```typescript filename="src/db.ts" -import "dotenv/config" -import { cipherstashFromStack } from "@cipherstash/prisma-next/stack" -import postgres from "@prisma-next/postgres/runtime" -import type { Contract } from "./prisma/contract.d" -import contractJson from "./prisma/contract.json" with { type: "json" } - -const cipherstash = await cipherstashFromStack({ contractJson }) - -export const db = postgres({ - contractJson, - extensions: cipherstash.extensions, - middleware: cipherstash.middleware, -}) -``` - -## Migrate - -`migration apply` installs the EQL bundle alongside your schema: - -```bash -npx stash auth login # once per developer -npx prisma-next contract emit -npx prisma-next migration plan --name initial -npx prisma-next migration apply # installs EQL and creates your tables -``` - -This is the one place Prisma Next has an advantage over the other integrations: EQL lands inside your migration history, so a rebuilt database has it. - -## Read and write - -Values are wrapped in an envelope class on the way in, and decrypted explicitly on the way out. Plaintext is redacted from every implicit serialisation path (`toJSON`, `toString`, `util.inspect`), so a stray `console.log` cannot leak it. - -```typescript filename="src/queries.ts" -import { EncryptedString, decryptAll } from "@cipherstash/prisma-next/runtime" -import { db } from "./db" - -await db.orm.User.create({ - id: "user-0", - email: EncryptedString.from("alice@example.com"), -}) - -const rows = await db.orm.User - .where((u) => u.email.cipherstashIlike("%@example.com")) - .all() - -await decryptAll(rows) -console.log(await rows[0]?.email.decrypt()) -``` - -`decryptAll` decrypts a whole result set in one round trip. A single field decrypts with `await row.email.decrypt()`. - -## Query operators - -Each operator is a method on the encrypted field, and the search config the column was declared with decides which ones it can answer: - -| Operator | Answers | -| --- | --- | -| `cipherstashEq`, `cipherstashNe` | Equality | -| `cipherstashInArray`, `cipherstashNotInArray` | Set membership | -| `cipherstashGt`, `cipherstashGte`, `cipherstashLt`, `cipherstashLte` | Ranges | -| `cipherstashBetween`, `cipherstashNotBetween` | Bounded ranges | -| `cipherstashIlike`, `cipherstashNotIlike` | Free-text match | -| `cipherstashAsc`, `cipherstashDesc` | Ordering | -| `cipherstashJsonbGet`, `cipherstashJsonbPathExists`, `cipherstashJsonbPathQueryFirst` | Encrypted JSON path access | - -Writes are coalesced: one SDK round trip per `(table, column)` group per query, rather than one per value. - -## Authenticating - -Local development uses your `stash` profile after `npx stash auth login`. In production, supply the `CS_*` environment variables instead. See [going to production](/stack/deploy/going-to-production). - -## Where to next - - - - The EQL v3 ORM integration, with concrete encrypted column types. - - - Prisma Next over a Supabase Postgres connection. - - - Encrypt and decrypt directly, for classic Prisma or any other ORM. - - - What each capability reveals, and how to choose. - - diff --git a/content/docs/integrations/prisma/deployment.mdx b/content/docs/integrations/prisma/deployment.mdx new file mode 100644 index 0000000..8bc63cc --- /dev/null +++ b/content/docs/integrations/prisma/deployment.mdx @@ -0,0 +1,65 @@ +--- +title: Deployment +description: "Deploy CipherStash schema changes, backfills, indexes, and credentials safely with Prisma ORM 8." +type: guide +components: [encryption, eql] +audience: [developer, cto] +integration: + category: orm + setup: code-only +verifiedAgainst: + prismaNext: "0.16.0" + eql: "3.0.4" + stack: "1.0.0" +--- + +Prisma owns the complete database migration graph for this integration, including the EQL bundle, encrypted columns, and their functional indexes. Application releases still need to be staged carefully when a plaintext column already contains data. + +For the reasoning, rollback rules, and adapter-independent sequence, read [Encrypt existing data](/guides/migration/encrypt-existing-data) and the [deployment guide](/guides/deployment). This page maps that process to Prisma ORM 8. + +## New tables and empty columns + +For a new table or an empty column: + +```bash +npx prisma-next contract emit +npx prisma-next migration plan --name add-encrypted-fields +npx prisma-next migrate +``` + +The CipherStash extension pack contributes EQL to the same graph. Do not run `stash eql install`. + +## Populated columns + +Move a populated plaintext column to encryption across separate application releases: + +| Release | Prisma and application change | Gate | +| --- | --- | --- | +| 1. Add and dual-write | Add a nullable encrypted twin; write plaintext and ciphertext on every path; keep plaintext reads | Confirm dual-writes are live | +| Backfill | Run `stash encrypt backfill` against the target database with the deployed app's keyset | Confirm zero uncovered rows | +| Index | Add EQL functional indexes as a Prisma raw-SQL migration operation and run `ANALYZE` | Verify plans with `EXPLAIN` | +| 2. Cut over reads | Query and decrypt the encrypted twin; keep dual-writes | Soak under real traffic | +| 3. Stop dual-writing | Write only the encrypted field; retain the plaintext column | Confirm every instance is updated | +| 4. Remove plaintext | Apply a guarded destructive migration, then remove the old field from the contract | Irreversible | + +Do not combine these boundaries. In particular, a backfill is safe only after every production write path dual-writes, and the plaintext column must remain until encrypted reads have soaked successfully. + +## Index migrations + +Prisma schemas cannot express functional indexes. Keep their DDL as `rawSql` operations from `@prisma-next/postgres/migration` in the Prisma migration history; never apply them ad hoc to production. + +Use the recipes in [EQL indexes](/reference/eql/indexes). Build indexes after the bulk backfill and before encrypted reads go live, then run `ANALYZE` and verify the actual query plan. + +## Credentials + + + +Create separate credentials for each deployed environment. A backfill must use credentials bound to the same keyset as the deployed application; otherwise decryption or encrypted search can fail after a seemingly successful write. + +The encryption client is commonly constructed during module import, so make the four `CS_*` variables available at build time as well as runtime. + +## Runtime packaging + +The Prisma adapter uses the native Stack client. Keep `@cipherstash/stack`, `@cipherstash/protect-ffi`, and `@cipherstash/auth` external to server bundles, and deploy to a runtime that can load native packages. This adapter is not supported on edge runtimes. + +For Prisma's hosted deployment behavior, including additive-only push-to-deploy and preview databases, continue with [Prisma Compute](/integrations/prisma/prisma-compute). diff --git a/content/docs/integrations/prisma/index.mdx b/content/docs/integrations/prisma/index.mdx new file mode 100644 index 0000000..d8fe996 --- /dev/null +++ b/content/docs/integrations/prisma/index.mdx @@ -0,0 +1,59 @@ +--- +title: Overview +description: "Searchable field-level encryption for Prisma ORM 8: declare encrypted columns in schema.prisma and query them with the Prisma ORM." +type: concept +components: [encryption, eql] +audience: [developer] +integration: + category: orm + setup: code-only + pairsWith: [supabase, drizzle] +verifiedAgainst: + prismaNext: "0.16.0" + eql: "3.0.4" + stack: "1.0.0" +--- + +CipherStash adds searchable, field-level encryption to Prisma ORM 8. Encrypted columns are declared in `schema.prisma`, EQL is installed through Prisma's migration graph, and the generated query API encrypts operands and values before they reach Postgres. + + +This integration targets the **Prisma ORM 8 release candidate**, formerly known as **Prisma Next**. The prerelease CLI, package names, and configuration file still use `prisma-next` or `@prisma-next/*`; the examples preserve those names exactly. CipherStash setup uses `stash init --prisma`. + +It does not extend the classic `@prisma/client` API. For earlier Prisma releases, use the [Stack SDK](/reference/stack) around your existing reads and writes. + + +## Choose your path + +| What you want to do | Start here | +| --- | --- | +| Encrypt and query your first Prisma field | [Prisma ORM Quickstart](/integrations/prisma/quickstart) | +| Choose column types or look up query operators | [Schema and queries](/integrations/prisma/schema-and-queries) | +| Encrypt data already in production | [Prisma deployment guide](/integrations/prisma/deployment) | +| Use Prisma's managed database | [Prisma Postgres](/integrations/prisma/prisma-postgres) | +| Deploy the application with Prisma | [Prisma Compute](/integrations/prisma/prisma-compute) | +| Design and verify encrypted indexes | [EQL indexes](/reference/eql/indexes) | +| Look up the Prisma 8 prerelease itself | [Prisma ORM 8 documentation](https://www.prisma.io/docs/orm/next) | + +## How it works + +The [`@cipherstash/stack-prisma`](https://www.npmjs.com/package/@cipherstash/stack-prisma) extension has three integration points: + +1. Domain-named constructors such as `cipherstash.TextSearch()` declare encrypted EQL v3 columns in `schema.prisma`. +2. The CipherStash extension pack adds EQL and those columns to Prisma's contract and migration graph. +3. `cipherstashFromStack` connects the generated Prisma runtime to the Stack encryption client. + +The column type fixes which encrypted queries are possible. For example, `TextEq()` supports equality, while `TextSearch()` also supports ranges, ordering, and token matching. Choosing the narrowest useful type minimizes the information disclosed by searchable encryption. [Schema and queries](/integrations/prisma/schema-and-queries) contains the complete type and operator tables. + +## EQL and migration ownership + +This integration targets EQL v3 only. Prisma owns the EQL installation: `npx prisma-next migrate` installs the EQL bundle alongside your application schema. Do not run `stash eql install` in a Prisma ORM 8 project. + +Encrypted functional indexes also belong in the Prisma migration graph. `schema.prisma` cannot express expression indexes, so add their SQL as a Prisma raw-SQL migration operation. The canonical recipes and `EXPLAIN` checks live in [EQL indexes](/reference/eql/indexes). + +## Runtime and authentication + +Local development uses the CipherStash developer profile created by `stash init`. CI and deployed applications use credentials created with the CLI or Dashboard. [Credentials](/integrations/prisma/deployment#credentials) lists all three options and the four deployment environment variables. + +The Prisma adapter currently uses the native Stack client, so deploy it to a server runtime that can load native packages; it is not an edge-runtime adapter. + +For a new project, continue with the [Quickstart](/integrations/prisma/quickstart). diff --git a/content/docs/integrations/prisma/meta.json b/content/docs/integrations/prisma/meta.json new file mode 100644 index 0000000..67e8db1 --- /dev/null +++ b/content/docs/integrations/prisma/meta.json @@ -0,0 +1,12 @@ +{ + "title": "Prisma ORM", + "icon": "Prisma", + "pages": [ + "index", + "quickstart", + "schema-and-queries", + "deployment", + "prisma-postgres", + "prisma-compute" + ] +} diff --git a/content/docs/integrations/prisma/prisma-compute.mdx b/content/docs/integrations/prisma/prisma-compute.mdx new file mode 100644 index 0000000..b21848f --- /dev/null +++ b/content/docs/integrations/prisma/prisma-compute.mdx @@ -0,0 +1,88 @@ +--- +title: Prisma Compute +description: "Deploy a CipherStash-enabled Prisma ORM 8 application on Prisma Compute." +type: guide +components: [encryption, eql, platform] +audience: [developer, cto] +integration: + category: platform + setup: code-only +verifiedAgainst: + prismaNext: "0.16.0" + eql: "3.0.4" + stack: "1.0.0" +--- + +[Prisma Compute](https://www.prisma.io/docs/compute) deploys TypeScript applications alongside Prisma Postgres. CipherStash has been exercised with Prisma Compute's July 2026 public beta; verify fast-moving CLI and platform details against the current Prisma documentation. + +## Before the first deploy + + + +The local developer profile is not available on Prisma Compute. Create deployment credentials with `stash env` or the Dashboard, then add all four variables to both the preview and production roles. + +These values are needed during the build as well as at runtime because a database module may construct `cipherstashFromStack` when it is imported. Prisma Compute resolves environment variables when it creates a deployment, so redeploy after changing them. + +Use separate credentials for preview and production, bound to the keyset intended for each environment. + +{/* Screenshot candidate: Prisma Compute environment-variable screen showing the + four CS_* key names, preview/production scopes, and hidden values. */} + +## Keep native packages external + +The Prisma adapter uses CipherStash's native client. For Next.js, exclude its packages from the server bundle: + +```typescript title="next.config.ts" +import type { NextConfig } from "next" + +const nextConfig: NextConfig = { + serverExternalPackages: [ + "@cipherstash/stack", + "@cipherstash/protect-ffi", + "@cipherstash/auth", + ], +} + +export default nextConfig +``` + +Do not select an edge runtime for routes that use this Prisma adapter. + +## Understand push-to-deploy + +A Prisma Compute deployment runs contract emission and `prisma-next db init` against the target branch database. + +- Additive changes, including the EQL bundle and new encrypted columns, are reconciled during deployment. +- Destructive changes such as dropping the old plaintext column or setting `NOT NULL` are rejected by the additive-only deployment policy. +- Preview branches receive fresh databases, so a green preview does not prove that a destructive production update will succeed. + +Use separate pull requests for each stage in the [Prisma deployment guide](/integrations/prisma/deployment). + +## Apply destructive migrations deliberately + +After the release that stops writing the plaintext column is live, apply the reviewed destructive migration directly to the production database before merging the contract change: + +```bash +npx @prisma/cli database list +npx @prisma/cli database show --json +npx @prisma/cli database connection create + +npx prisma-next db update --db "" + +npx @prisma/cli database connection remove +``` + +Confirm the production database by name and with `database show`; do not rely on list order or branch metadata. A migration applied to the wrong preview database can succeed while the production deployment continues to fail. + +The destructive migration must include an apply-time coverage guard so the plaintext column is not dropped while any row lacks ciphertext. + +## Operational notes + +- Keep authorization checks in the page, server action, or route handler rather than relying only on Next.js middleware. +- Runtime output and build output use different Prisma commands: `app logs` and `build logs `. +- A first request after scale-to-zero can cold-start; retry before treating it as an application failure. + +{/* Screenshot candidate: Prisma Compute branch page showing an application, + its linked Prisma Postgres database, and separate production/preview state. */} + +Prisma Compute is in public beta. Reconfirm platform limitations before using it for a mission-critical workload. diff --git a/content/docs/integrations/prisma/prisma-postgres.mdx b/content/docs/integrations/prisma/prisma-postgres.mdx new file mode 100644 index 0000000..9dd0d37 --- /dev/null +++ b/content/docs/integrations/prisma/prisma-postgres.mdx @@ -0,0 +1,68 @@ +--- +title: Prisma Postgres +description: "Connect the CipherStash Prisma ORM 8 integration to Prisma's managed Postgres service." +type: guide +components: [encryption, eql, platform] +audience: [developer, cto] +integration: + category: platform + setup: code-only +verifiedAgainst: + prismaNext: "0.16.0" + eql: "3.0.4" + stack: "1.0.0" +--- + +[Prisma Postgres](https://www.prisma.io/docs/postgres) is Prisma's managed Postgres service. CipherStash uses it like any other Postgres database: encryption happens in the application, and EQL plus the encrypted domains live in the database. + +## Choose the connection URL + +Prisma Postgres provides pooled and direct connections: + +| Use | Connection | +| --- | --- | +| Application runtime | Pooled URL on `pooled.db.prisma.io` | +| Migrations, administrative work, and one-off backfills | Direct URL on `db.prisma.io` | + +Both connections require TLS. Copy the URLs from the Prisma Console and keep them in the relevant environment's secret store. + +```dotenv title=".env" +DATABASE_URL=postgresql://...@pooled.db.prisma.io:5432/postgres?sslmode=require +DIRECT_URL=postgresql://...@db.prisma.io:5432/postgres?sslmode=require +``` + +Use the direct URL when applying the Prisma migration graph or running a long-lived backfill. Use the pooled URL when connecting the deployed application. + +{/* Screenshot candidate: Prisma Console connection dialog with pooled and direct + connection choices visible, project names retained, and credentials redacted. */} + +## Initialize the database + +Set the migration process to the direct connection, then run the normal Prisma flow: + +```bash +DATABASE_URL="$DIRECT_URL" npx prisma-next contract emit +DATABASE_URL="$DIRECT_URL" npx prisma-next migration plan --name initial +DATABASE_URL="$DIRECT_URL" npx prisma-next migrate +``` + +This installs EQL through Prisma's migration graph. Do not install EQL separately with the `stash` CLI. + +For a populated column, use the direct connection for the out-of-band backfill and confirm that it targets the intended production or preview database. Follow the [Prisma deployment guide](/integrations/prisma/deployment) before changing live data. + +## What appears in Prisma tools + +Prisma Studio and the Prisma Console see EQL ciphertext payloads, not plaintext. That is expected: encryption keys remain in the application, outside Prisma Postgres. Read and update encrypted values through the CipherStash-enabled Prisma runtime. + +{/* Screenshot candidate: Prisma Studio showing one encrypted field as an EQL + ciphertext payload beside a non-sensitive plaintext field. */} + +## Branch databases + +Prisma platform branches can have isolated databases. Treat each branch as a separate CipherStash environment: + +- Confirm the database identity before migrations or backfills. +- Use separate `CS_*` credentials for preview and production. +- Keep every writer and one-off job on the keyset intended for that environment. + +If the application is also hosted by Prisma, [Prisma Compute](/integrations/prisma/prisma-compute) explains its build, environment-variable, and migration behavior. diff --git a/content/docs/integrations/prisma/quickstart.mdx b/content/docs/integrations/prisma/quickstart.mdx new file mode 100644 index 0000000..6f23700 --- /dev/null +++ b/content/docs/integrations/prisma/quickstart.mdx @@ -0,0 +1,182 @@ +--- +title: Quickstart +description: "Declare, migrate, write, and query your first encrypted field with CipherStash and Prisma ORM 8." +type: tutorial +components: [encryption, eql] +audience: [developer] +integration: + category: orm + setup: code-only + pairsWith: [supabase, drizzle] +verifiedAgainst: + prismaNext: "0.16.0" + eql: "3.0.4" + stack: "1.0.0" +--- + +This tutorial adds an encrypted, searchable email field to an existing Prisma ORM 8 release candidate project. + + +Prisma ORM 8 was formerly called Prisma Next. Its prerelease command remains `prisma-next`, while CipherStash initialization uses `--prisma`. + + +It is designed for a new table or empty column. To encrypt a populated column safely, use the [Prisma deployment guide](/integrations/prisma/deployment). + +## Before you start + +You need: + +- A Prisma ORM 8 release candidate project with `prisma-next.config.ts` +- A Postgres database and `DATABASE_URL` +- A CipherStash account; initialization signs you in + + + + +### Initialize CipherStash + +From the Prisma project, run: + +```bash cta cta-type="install" example-id="prisma-quickstart-init" +npx stash init --prisma +``` + +`stash init` opens the CipherStash device login when necessary, creates the local developer profile, and installs compatible pinned versions of `@cipherstash/stack`, `@cipherstash/stack-prisma`, and the `stash` CLI. + +It intentionally does not install EQL directly. The Prisma migration graph owns that step. + + +Agents can run the same setup. If no developer profile exists, the agent should start `npx stash auth login --json --region --prisma`, present the returned verification URL, and wait for you to complete device login. It can then run `npx stash init --prisma --region `. + +Initialization records project context and can install the CipherStash Prisma, authentication, indexing, deployment, and CLI skills for supported coding agents. + + + + + +### Declare an encrypted field + +Add the CipherStash type to your schema: + +```prisma title="prisma/schema.prisma" +model User { + id String @id + email cipherstash.TextSearch() +} +``` + +`TextSearch()` supports encrypted equality, range, ordering, and token matching. If you only need equality, prefer `TextEq()`. See [Schema and queries](/integrations/prisma/schema-and-queries) for every column type. + + + + +### Register the extension pack + +Add CipherStash to your existing Prisma configuration: + +```typescript title="prisma-next.config.ts" +import cipherstash from "@cipherstash/stack-prisma/control" +import { defineConfig } from "@prisma-next/cli/config-types" +import { prismaContract } from "@prisma-next/sql-contract-psl/provider" +import postgresPack from "@prisma-next/target-postgres/pack" +import { postgresCreateNamespace } from "@prisma-next/target-postgres/types" + +export default defineConfig({ + // Keep your existing family, target, driver, adapter, and database settings. + extensionPacks: [cipherstash], + contract: prismaContract("./prisma/schema.prisma", { + output: "src/prisma/contract.json", + target: postgresPack, + createNamespace: postgresCreateNamespace, + }), + migrations: { dir: "migrations" }, +}) +``` + +If you already use extension packs, include `cipherstash` in the existing array. `createNamespace` is required by Prisma's 0.15 and later prereleases. + + + + +### Emit and migrate + +Generate the contract, plan the migration, and apply it: + +```bash +npx prisma-next contract emit +npx prisma-next migration plan --name add-encrypted-user +npx prisma-next migrate +``` + +The final command installs EQL v3 and applies your application migration in one graph. Do not run `stash eql install` for this integration. + + + + +### Connect the encrypted runtime + +Create the database module from the emitted contract: + +```typescript title="src/db.ts" +import "dotenv/config" +import { cipherstashFromStack } from "@cipherstash/stack-prisma/v3" +import postgres from "@prisma-next/postgres/runtime" +import type { Contract } from "./prisma/contract.d" +import contractJson from "./prisma/contract.json" with { type: "json" } + +const cipherstash = await cipherstashFromStack({ contractJson }) + +export const db = postgres({ + contractJson, + extensions: cipherstash.extensions, + middleware: cipherstash.middleware, +}) +``` + +`cipherstashFromStack` uses the local developer profile automatically. For CI or a deployed application, see [Credentials](/integrations/prisma/deployment#credentials) for the CLI and Dashboard options. + + + + +### Write and query encrypted data + +Wrap plaintext values at the write boundary, then use the generated `eql*` operators: + +```typescript +import { + decryptAll, + EncryptedString, +} from "@cipherstash/stack-prisma/runtime" +import { db } from "./db" + +const runtime = await db.connect({ url: process.env.DATABASE_URL! }) + +try { + await db.orm.public.User.create({ + id: "user-0", + email: EncryptedString.from("alice@example.com"), + }) + + const rows = await db.orm.public.User + .where((user) => user.email.eqlEq("alice@example.com")) + .all() + + await decryptAll(rows) + console.log(await rows[0]?.email.decrypt()) + // alice@example.com +} finally { + await runtime.close() +} +``` + +The write value and query operand are encrypted before reaching Postgres. `decryptAll` batches result decryption; `.decrypt()` returns the plaintext value. + + + + +## Next steps + +- Choose other encrypted field types and operators in [Schema and queries](/integrations/prisma/schema-and-queries). +- Add functional indexes using [EQL indexes](/reference/eql/indexes); keep their raw SQL operations in the Prisma migration graph. +- Follow the [Prisma deployment guide](/integrations/prisma/deployment) before changing a populated production column. +- Continue with [Prisma Postgres](/integrations/prisma/prisma-postgres) or [Prisma Compute](/integrations/prisma/prisma-compute) if you use Prisma's hosted platform. diff --git a/content/docs/integrations/prisma/schema-and-queries.mdx b/content/docs/integrations/prisma/schema-and-queries.mdx new file mode 100644 index 0000000..de26d2f --- /dev/null +++ b/content/docs/integrations/prisma/schema-and-queries.mdx @@ -0,0 +1,130 @@ +--- +title: Schema and queries +description: "CipherStash column constructors, runtime envelopes, and encrypted query operators for Prisma ORM 8." +type: reference +components: [encryption, eql] +audience: [developer] +integration: + category: orm + setup: code-only +verifiedAgainst: + prismaNext: "0.16.0" + eql: "3.0.4" + stack: "1.0.0" +--- + +The CipherStash Prisma extension separates the encrypted **column type** from the runtime **value envelope**: + +- In `schema.prisma`, a domain-named constructor such as `TextEq()` fixes the Postgres plaintext family and searchable capabilities. +- In application code, a primitive-named envelope such as `EncryptedString` wraps a plaintext value for encryption. + +## Column constructors + +Choose the plaintext type first, then the narrowest query capability your application needs. + +| Plaintext | Storage only | Equality | Order and range | Free-text only | All text queries | +| --- | --- | --- | --- | --- | --- | +| `string` | `Text()` | `TextEq()` | `TextOrd()` | `TextMatch()` | `TextSearch()` | +| `number` (`int4`) | `Integer()` | `IntegerEq()` | `IntegerOrd()` | — | — | +| `number` (`int2`) | `Smallint()` | `SmallintEq()` | `SmallintOrd()` | — | — | +| `bigint` (`int8`) | `BigInt()` | `BigIntEq()` | `BigIntOrd()` | — | — | +| `number` (`numeric`) | `Numeric()` | `NumericEq()` | `NumericOrd()` | — | — | +| `number` (`float4`) | `Real()` | `RealEq()` | `RealOrd()` | — | — | +| `number` (`float8`) | `Double()` | `DoubleEq()` | `DoubleOrd()` | — | — | +| `Date` (`date`) | `Date()` | `DateEq()` | `DateOrd()` | — | — | +| `Date` (`timestamp`) | `Timestamp()` | `TimestampEq()` | `TimestampOrd()` | — | — | +| `boolean` | `Boolean()` | — | — | — | — | +| JSON document | `Json()` supports containment and JSONPath queries | — | — | — | — | + +Every `*Ord()` constructor includes equality. `TextMatch()` provides token matching without equality; `TextSearch()` combines equality, range, ordering, and token matching. Bare constructors such as `Text()` are storage-only. + +Each constructor maps directly to an EQL v3 Postgres domain. For example, `cipherstash.DoubleOrd()` creates an `eql_v3_double_ord` column. The type cannot be broadened at query time. + +## Runtime envelopes + +| Schema family | Write envelope | +| --- | --- | +| `Text*` | `EncryptedString.from(value)` | +| `Integer*`, `Smallint*`, `Numeric*`, `Real*`, `Double*` | `EncryptedNumber.from(value)` | +| `BigInt*` | `EncryptedBigInt.from(value)` | +| `Date*`, `Timestamp*` | `EncryptedDate.from(value)` | +| `Boolean` | `EncryptedBoolean.from(value)` | +| `Json` | `EncryptedJson.from(value)` | + +Import envelopes and `decryptAll` from `@cipherstash/stack-prisma/runtime`. + +```typescript +import { + decryptAll, + EncryptedDate, + EncryptedNumber, + EncryptedString, +} from "@cipherstash/stack-prisma/runtime" + +await db.orm.public.User.create({ + id: "user-0", + email: EncryptedString.from("alice@example.com"), + salary: EncryptedNumber.from(100_000), + birthday: EncryptedDate.from(new Date("1990-01-01")), +}) + +const rows = await db.orm.public.User.all() +await decryptAll(rows) +``` + +`decryptAll` groups a result set by table and column to avoid one encryption-service round trip per value. Call `.decrypt()` on an envelope after the batch completes. + +## Query operators + +Operators are generated only where the declared domain supports them. + +| Operator | Meaning | +| --- | --- | +| `eqlEq(value)`, `eqlNeq(value)` | Equality and inequality | +| `eqlIn(values)`, `eqlNotIn(values)` | Set membership | +| `eqlMatch(term)` | Free-text token match | +| `eqlGt/Gte/Lt/Lte(value)` | Range comparison | +| `eqlBetween(low, high)`, `eqlNotBetween(low, high)` | Bounded range | +| `eqlAsc(column)`, `eqlDesc(column)` | Encrypted ordering | +| `eqlJsonContains(value)` | JSON containment | +| `eqlJsonPathEq/Neq(path, value)` | Equality at a JSONPath | +| `eqlJsonPathGt/Gte/Lt/Lte(path, value)` | Range comparison at a JSONPath | +| `eqlJsonPathAsc/Desc(column, path)` | Ordering by a JSONPath leaf | + +```typescript +import { + eqlAsc, + eqlJsonPathDesc, +} from "@cipherstash/stack-prisma/runtime" + +const matches = await db.orm.public.User + .where((user) => user.email.eqlMatch("example.com")) + .all() + +const earners = await db.orm.public.User + .where((user) => user.salary.eqlBetween(100_000, 150_000)) + .orderBy((user) => eqlAsc(user.salary)) + .all() + +const preferences = await db.orm.public.User + .where((user) => user.profile.eqlJsonContains({ theme: "dark" })) + .orderBy((user) => eqlJsonPathDesc(user.profile, "$.score")) + .all() +``` + +Free-text search is token matching, not SQL `ILIKE`. Pass a term such as `example.com`, not a wildcard pattern such as `%example.com%`. + +## Indexes + +Encrypted predicates require functional Postgres indexes at meaningful scale. `schema.prisma` cannot express expression indexes, so put the index DDL in a Prisma raw-SQL migration operation and apply it with `prisma-next migrate`. + +The domain-to-index matrix, SQL recipes, rollout timing, `ANALYZE`, and `EXPLAIN` verification are maintained in [EQL indexes](/reference/eql/indexes). + +## Subpath exports + +| Import | Purpose | +| --- | --- | +| `@cipherstash/stack-prisma/control` | Prisma extension pack | +| `@cipherstash/stack-prisma/v3` | `cipherstashFromStack` and the EQL v3 adapter | +| `@cipherstash/stack-prisma/runtime` | Envelopes, `decryptAll`, and query helpers | +| `@cipherstash/stack-prisma/column-types` | TypeScript-authored contract factories | diff --git a/content/docs/integrations/supabase/index.mdx b/content/docs/integrations/supabase/index.mdx index 854ae70..f9efeec 100644 --- a/content/docs/integrations/supabase/index.mdx +++ b/content/docs/integrations/supabase/index.mdx @@ -7,7 +7,7 @@ audience: [developer, ciso] integration: category: platform setup: code-only - pairsWith: [drizzle, prisma-next, clerk] + pairsWith: [drizzle, prisma, clerk] verifiedAgainst: eql: "3.0.4" stack: "1.0.0" @@ -26,7 +26,7 @@ CipherStash has two parts: The EQL type assigned to a column declares what can be queried. For example, an equality column supports `.eq()` and `.in()`, while an ordered column also supports ranges and `.order()`. Choose only the capabilities you need, because searchable encryption has defined leakage characteristics. See [EQL core concepts](/reference/eql/core-concepts) for the complete model. -With EQL 3.0.4, the Supabase wrapper supports encrypted equality, range, and ordering queries. Encrypted free-text and JSON queries require an adapter such as Drizzle or Prisma Next because PostgREST cannot express the typed operands those operations require. The [wrapper reference](/reference/stack/supabase#free-text-and-json-need-a-different-adapter) documents the limitation. +With EQL 3.0.4, the Supabase wrapper supports encrypted equality, range, and ordering queries. Encrypted free-text and JSON queries require an adapter such as Drizzle or Prisma ORM 8 because PostgREST cannot express the typed operands those operations require. The [wrapper reference](/reference/stack/supabase#free-text-and-json-need-a-different-adapter) documents the limitation. ## Choose your path @@ -37,13 +37,13 @@ With EQL 3.0.4, the Supabase wrapper supports encrypted equality, range, and ord | Look up the complete wrapper API | [`encryptedSupabase` reference](/reference/stack/supabase) | | Bind decryption to the signed-in user | [Supabase Auth](/integrations/supabase/auth) | | Encrypt inside Supabase Edge Functions | [Edge Functions](/integrations/supabase/edge-functions) | -| Use an ORM against Supabase Postgres | [Drizzle](/integrations/drizzle) or [Prisma Next](/integrations/prisma-next) | +| Use an ORM against Supabase Postgres | [Drizzle](/integrations/drizzle) or [Prisma ORM 8](/integrations/prisma) | | Encrypt columns that already contain data | [Encrypt existing data](/guides/migration/encrypt-existing-data) | | Plan a production rollout or rollback | [Deployment guide](/guides/deployment) | ## Supabase, Drizzle, and Prisma -Supabase is Postgres, so you are not limited to PostgREST or `supabase-js`. CipherStash's [Drizzle](/integrations/drizzle) and [Prisma Next](/integrations/prisma-next) integrations can read and write the same EQL columns through a direct database connection. They produce the same ciphertext, so an application can use more than one adapter without re-encrypting its data. +Supabase is Postgres, so you are not limited to PostgREST or `supabase-js`. CipherStash's [Drizzle](/integrations/drizzle) and [Prisma ORM 8](/integrations/prisma) integrations can read and write the same EQL columns through a direct database connection. They produce the same ciphertext, so an application can use more than one adapter without re-encrypting its data. Use `encryptedSupabase` when you want Supabase's query builder and automatic propagation of the caller's JWT. Use an ORM when it better fits your application or when you need encrypted free-text or JSON queries. A privileged direct database connection may bypass Row Level Security, so preserve tenant or user scoping in the connection role or ORM query. diff --git a/content/docs/integrations/supabase/quickstart.mdx b/content/docs/integrations/supabase/quickstart.mdx index c3a3fd4..f7dd3d4 100644 --- a/content/docs/integrations/supabase/quickstart.mdx +++ b/content/docs/integrations/supabase/quickstart.mdx @@ -7,7 +7,7 @@ audience: [developer] integration: category: platform setup: code-only - pairsWith: [drizzle, prisma-next, clerk] + pairsWith: [drizzle, prisma, clerk] verifiedAgainst: eql: "3.0.4" stack: "1.0.0" @@ -162,4 +162,4 @@ The Table Editor cannot decrypt or meaningfully filter the encrypted value becau - Use the complete [`encryptedSupabase` API reference](/reference/stack/supabase) when you need method-level detail. - Add functional indexes when the table reaches meaningful row counts by following [EQL indexes](/reference/eql/indexes). - For a populated table, follow the safe dual-write and backfill process in [Encrypt existing data](/guides/migration/encrypt-existing-data). -- To use a direct Postgres ORM connection, continue with [Drizzle](/integrations/drizzle) or [Prisma Next](/integrations/prisma-next). +- To use a direct Postgres ORM connection, continue with [Drizzle](/integrations/drizzle) or [Prisma ORM 8](/integrations/prisma). diff --git a/content/docs/integrations/supabase/supabase-js.mdx b/content/docs/integrations/supabase/supabase-js.mdx index a1d1ebd..246919d 100644 --- a/content/docs/integrations/supabase/supabase-js.mdx +++ b/content/docs/integrations/supabase/supabase-js.mdx @@ -192,7 +192,7 @@ Storage-only, equality-only, and match-only encrypted columns cannot be ordered. **The current EQL 3.0.4 release cannot run encrypted free-text or encrypted JSON queries through supabase-js.** `.matches()`, encrypted `.contains()`, `.selectorEq()`, and `.selectorNe()` fail before a request is sent. Their SQL operators require an `eql_v3.query_*` cast that PostgREST cannot express. This PostgREST boundary was introduced in EQL 3.0.2 and remains in 3.0.4, which is why the runtime error describes it as `3.0.2+`. -Inserts, selects, decryption, equality, ranges, and ordering are unaffected. Use [Drizzle](/integrations/drizzle), [Prisma Next](/integrations/prisma-next), or a carefully scoped SQL/RPC function for free-text and encrypted JSON queries. +Inserts, selects, decryption, equality, ranges, and ordering are unaffected. Use [Drizzle](/integrations/drizzle), [Prisma ORM 8](/integrations/prisma), or a carefully scoped SQL/RPC function for free-text and encrypted JSON queries. Scalar encrypted filters currently use a full encrypted storage envelope as the PostgREST operand because PostgREST cannot cast a term-only query envelope. PostgREST sends filters in GET query strings, so configure application and proxy logs not to retain query strings. The operand is encrypted, but it can contain ciphertext and every index term for the queried value. diff --git a/content/docs/reference/stack/supabase.mdx b/content/docs/reference/stack/supabase.mdx index 8e4dbe7..c05e347 100644 --- a/content/docs/reference/stack/supabase.mdx +++ b/content/docs/reference/stack/supabase.mdx @@ -185,7 +185,7 @@ await supabase ``` [supabase v3]: matches() on encrypted column "email" is unavailable with EQL 3.0.2+: the SQL operator requires an eql_v3.query_* cast that PostgREST cannot -express. Use the Drizzle or Prisma Next adapter, or a scoped SQL/RPC function. +express. Use the Drizzle or Prisma ORM 8 adapter, or a scoped SQL/RPC function. ``` The error says `3.0.2+` because EQL 3.0.2 introduced this query-domain requirement. EQL 3.0.4 retains it. @@ -194,7 +194,7 @@ The limit is PostgREST's, not the encryption's. Those operators need a typed `eq This applies to `.matches()`, `.contains()` on a `json` column, `.selectorEq()` / `.selectorNe()`, and the same terms reached through `.not()`, `.or()`, or a raw `.filter()`. **Equality, ordering, and range are unaffected** — `.eq()`, `.neq()`, `.in()`, `.gt()` / `.gte()` / `.lt()` / `.lte()`, and `.order()` all work, as do inserts, updates, and decryption on select. -To search encrypted text with EQL 3.0.4, use the [Drizzle](/integrations/drizzle) or [Prisma Next](/integrations/prisma-next) adapter for that query, or put it behind a scoped SQL or RPC function. Everything else on this page still applies. +To search encrypted text with EQL 3.0.4, use the [Drizzle](/integrations/drizzle) or [Prisma ORM 8](/integrations/prisma) adapter for that query, or put it behind a scoped SQL or RPC function. Everything else on this page still applies. On EQL 3.0.1 and earlier, `.matches()` tests whether the stored value's encrypted token set contains the search string's — case-insensitive, and false positives are possible while false negatives are not: diff --git a/next.config.mjs b/next.config.mjs index 990a7b3..efdadca 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -33,6 +33,11 @@ const config = { destination: "/get-started/quickstart", permanent: false, }, + { + source: "/integrations/prisma-next", + destination: "/integrations/prisma", + permanent: true, + }, ...(enableV2Redirects ? v2Redirects : []), // === 4-section consolidation: product sections under /cipherstash/ === { diff --git a/scripts/validate-content-api.ts b/scripts/validate-content-api.ts index 4563c6b..b260ead 100644 --- a/scripts/validate-content-api.ts +++ b/scripts/validate-content-api.ts @@ -68,13 +68,6 @@ const IDENTITY_SCOPE = ["content/docs/", "content/stack/"]; /** The legacy tree documents EQL v2 throughout; only the v2 IA must be v3-only. */ const V3_SCOPE = ["content/docs/"]; -/** - * `@cipherstash/prisma-next` really does target EQL v2 — its migrations create - * `eql_v2_encrypted` columns and call `eql_v2.add_search_config`. Its page must - * name those to be correct. Delete this when prisma-next moves to v3. - */ -const PRISMA_NEXT_IS_V2 = ["content/docs/integrations/prisma-next.mdx"]; - const RULES: Rule[] = [ { id: "lockcontext-identify", @@ -106,14 +99,12 @@ const RULES: Rule[] = [ message: "The `eql_v2_encrypted` column type was removed in EQL 3.0.0.", fix: "Type the column with an EQL v3 domain variant, e.g. `public.eql_v3_text_eq`. See /reference/eql/core-concepts.", scope: V3_SCOPE, - exempt: PRISMA_NEXT_IS_V2, }, { id: "eql-v2-schema-functions", pattern: /\beql_v2\.\w+\s*\(/, message: "The `eql_v2` schema was removed in EQL 3.0.0.", fix: "Use the `eql_v3` equivalents, or the encrypted operators directly with a typed operand.", - exempt: PRISMA_NEXT_IS_V2, scope: V3_SCOPE, }, { diff --git a/source.config.ts b/source.config.ts index 1239b51..38e98e6 100644 --- a/source.config.ts +++ b/source.config.ts @@ -3,6 +3,7 @@ import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins"; import { metaSchema, pageSchema } from "fumadocs-core/source/schema"; import { defineConfig, defineDocs } from "fumadocs-mdx/config"; import { z } from "zod"; +import { cipherstashDark, cipherstashLight } from "./src/lib/shiki-themes"; // You can customise Zod schemas for frontmatter and `meta.json` here // see https://fumadocs.dev/docs/mdx/collections @@ -77,145 +78,6 @@ export const v2docs = defineDocs({ }); // ── Syntax highlighting: the CipherStash code theme ────────────────────────── -// Two custom Shiki themes (light + dark) built from the site palette instead of -// Fumadocs' default GitHub themes: the lime-green brand accent for keywords, -// warm neutrals for text and comments, and teal / gold / amber supporting hues -// tuned to the near-black and off-white grounds the docs already use. Shiki runs -// in dual-theme mode (see `defaultColor: false` in rehypeCodeDefaultOptions), so -// these switch with the site's light/dark toggle via CSS variables. -interface CodePalette { - bg: string; - fg: string; - com: string; - kw: string; - fn: string; - typ: string; - str: string; - num: string; - pun: string; -} - -function cipherstashTheme( - name: string, - type: "light" | "dark", - c: CodePalette, -) { - return { - name, - type, - colors: { - "editor.background": c.bg, - "editor.foreground": c.fg, - }, - settings: [ - { settings: { background: c.bg, foreground: c.fg } }, - { - scope: ["comment", "punctuation.definition.comment", "string.comment"], - settings: { foreground: c.com, fontStyle: "italic" }, - }, - { - scope: [ - "keyword", - "keyword.control", - "keyword.other", - "keyword.operator.new", - "keyword.operator.expression", - "keyword.operator.logical", - "storage", - "storage.type", - "storage.modifier", - "variable.language", - "entity.name.tag", - "punctuation.definition.tag", - ], - settings: { foreground: c.kw }, - }, - { - scope: [ - "entity.name.function", - "support.function", - "meta.function-call", - "meta.function-call.generic", - "variable.function", - ], - settings: { foreground: c.fn }, - }, - { - scope: [ - "entity.name.type", - "entity.name.class", - "support.type", - "support.class", - "entity.other.inherited-class", - "entity.name.namespace", - "support.type.property-name", - "meta.object-literal.key", - "entity.other.attribute-name", - ], - settings: { foreground: c.typ }, - }, - { - scope: [ - "string", - "string.quoted", - "string.template", - "string.regexp", - "punctuation.definition.string", - ], - settings: { foreground: c.str }, - }, - { - scope: [ - "constant.numeric", - "constant.language", - "constant.character", - "constant.other", - "support.constant", - ], - settings: { foreground: c.num }, - }, - { - scope: [ - "punctuation", - "keyword.operator", - "meta.brace", - "punctuation.separator", - "punctuation.terminator", - ], - settings: { foreground: c.pun }, - }, - { - scope: ["variable", "variable.other", "variable.parameter"], - settings: { foreground: c.fg }, - }, - ], - }; -} - -const cipherstashDark = cipherstashTheme("cipherstash-dark", "dark", { - bg: "#0b0b0a", - fg: "#eae8dd", - com: "#8f8f8f", - kw: "#d77595", - fn: "#d2a8ff", - typ: "#7fd0c4", - str: "#c8f031", - num: "#f4dd63", - pun: "#9a9486", -}); - -const cipherstashLight = cipherstashTheme("cipherstash-light", "light", { - bg: "#faf9f4", - fg: "#2b2822", - com: "#7a7a7a", - kw: "#a63057", - fn: "#8250df", - typ: "#1c8577", - str: "#567d0d", - num: "#977c11", - pun: "#6b6559", -}); - // Parse the leftover code-fence meta string (what remains after Fumadocs // extracts `title`, `tab`, and line-number directives) for the analytics // attributes documented for authors: `example-id`, `cta`, and `cta-type`. diff --git a/src/components/cipherstash-credentials.tsx b/src/components/cipherstash-credentials.tsx new file mode 100644 index 0000000..5fa5c4b --- /dev/null +++ b/src/components/cipherstash-credentials.tsx @@ -0,0 +1,122 @@ +import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock"; +import { ExternalLink, Laptop, LayoutDashboard, Terminal } from "lucide-react"; +import { cipherstashDark, cipherstashLight } from "@/lib/shiki-themes"; + +const ENVIRONMENT_VARIABLES = `CS_WORKSPACE_CRN=crn:.: +CS_CLIENT_ID= +CS_CLIENT_KEY= +CS_CLIENT_ACCESS_KEY=CSAK...`; + +const options = [ + { + title: "Developer profile", + useFor: "Local development", + icon: Laptop, + content: ( + <> + Run npx stash auth login. The native Stack client finds the + resulting developer profile automatically, so you do not need + environment variables on your development machine. + + ), + }, + { + title: "stash env", + useFor: "CI and deployed environments", + icon: Terminal, + content: ( + <> + Run npx stash env --name <app-env> while logged in. + It creates a client and prints the four environment variables below. The + access key is shown only once. + + ), + }, + { + title: "Dashboard", + useFor: "CI and deployed environments", + icon: LayoutDashboard, + content: ( + <> + Open your{" "} + + CipherStash workspace + + {" "} + and create deployment credentials there. Add the resulting values to + your platform's secret store. + + ), + }, +] as const; + +/** + * The canonical credential-source chooser for integration and deployment + * guides. Keep the discovery order and environment-variable names aligned + * with the Stack authentication skill. + */ +export function CipherStashCredentials() { + return ( +
+
+

+ Choose a credential source +

+

+ Use the developer profile locally. For CI or a deployed application, + create a separate credential set with the CLI or Dashboard. +

+
+ +
    + {options.map(({ title, useFor, icon: Icon, content }, index) => ( +
  1. +
    + + +
    +

    + {index + 1}. {title} +

    +

    + {useFor} +

    +
    +
    +

    + {content} +

    +
  2. + ))} +
+ +
+

+ Deployment environment variables +

+

+ Both deployment methods produce the same values. Treat{" "} + CS_CLIENT_KEY and CS_CLIENT_ACCESS_KEY as + secrets. +

+ +
+
+ ); +} diff --git a/src/components/icons/prisma.tsx b/src/components/icons/prisma.tsx new file mode 100644 index 0000000..f5e7f67 --- /dev/null +++ b/src/components/icons/prisma.tsx @@ -0,0 +1,19 @@ +export function PrismaIcon(props: React.SVGProps) { + return ( + + + + ); +} diff --git a/src/lib/shiki-themes.ts b/src/lib/shiki-themes.ts new file mode 100644 index 0000000..bb60543 --- /dev/null +++ b/src/lib/shiki-themes.ts @@ -0,0 +1,132 @@ +interface CodePalette { + bg: string; + fg: string; + com: string; + kw: string; + fn: string; + typ: string; + str: string; + num: string; + pun: string; +} + +function cipherstashTheme( + name: string, + type: "light" | "dark", + c: CodePalette, +) { + return { + name, + type, + colors: { + "editor.background": c.bg, + "editor.foreground": c.fg, + }, + settings: [ + { settings: { background: c.bg, foreground: c.fg } }, + { + scope: ["comment", "punctuation.definition.comment", "string.comment"], + settings: { foreground: c.com, fontStyle: "italic" }, + }, + { + scope: [ + "keyword", + "keyword.control", + "keyword.other", + "keyword.operator.new", + "keyword.operator.expression", + "keyword.operator.logical", + "storage", + "storage.type", + "storage.modifier", + "variable.language", + "entity.name.tag", + "punctuation.definition.tag", + ], + settings: { foreground: c.kw }, + }, + { + scope: [ + "entity.name.function", + "support.function", + "meta.function-call", + "meta.function-call.generic", + "variable.function", + ], + settings: { foreground: c.fn }, + }, + { + scope: [ + "entity.name.type", + "entity.name.class", + "support.type", + "support.class", + "entity.other.inherited-class", + "entity.name.namespace", + "support.type.property-name", + "meta.object-literal.key", + "entity.other.attribute-name", + ], + settings: { foreground: c.typ }, + }, + { + scope: [ + "string", + "string.quoted", + "string.template", + "string.regexp", + "punctuation.definition.string", + ], + settings: { foreground: c.str }, + }, + { + scope: [ + "constant.numeric", + "constant.language", + "constant.character", + "constant.other", + "support.constant", + ], + settings: { foreground: c.num }, + }, + { + scope: [ + "punctuation", + "keyword.operator", + "meta.brace", + "punctuation.separator", + "punctuation.terminator", + ], + settings: { foreground: c.pun }, + }, + { + scope: ["variable", "variable.other", "variable.parameter"], + settings: { foreground: c.fg }, + }, + ], + }; +} + +export const cipherstashDark = cipherstashTheme("cipherstash-dark", "dark", { + bg: "#0b0b0a", + fg: "#eae8dd", + com: "#8f8f8f", + kw: "#d77595", + fn: "#d2a8ff", + typ: "#7fd0c4", + str: "#c8f031", + num: "#f4dd63", + pun: "#9a9486", +}); + +export const cipherstashLight = cipherstashTheme("cipherstash-light", "light", { + bg: "#faf9f4", + fg: "#2b2822", + com: "#7a7a7a", + kw: "#a63057", + fn: "#8250df", + typ: "#1c8577", + str: "#567d0d", + num: "#977c11", + pun: "#6b6559", +}); diff --git a/src/lib/source.ts b/src/lib/source.ts index b3b58ca..6c2080d 100644 --- a/src/lib/source.ts +++ b/src/lib/source.ts @@ -3,9 +3,11 @@ import type * as PageTree from "fumadocs-core/page-tree"; import { type InferPageType, loader } from "fumadocs-core/source"; import { icons } from "lucide-react"; import { createElement } from "react"; +import { PrismaIcon } from "@/components/icons/prisma"; import { SupabaseIcon } from "@/components/icons/supabase"; const customIcons: Record React.ReactElement> = { + Prisma: () => createElement(PrismaIcon, { width: 16, height: 16 }), Supabase: () => createElement(SupabaseIcon, { width: 16, height: 16 }), }; diff --git a/src/mdx-components.tsx b/src/mdx-components.tsx index 66ac189..122f68c 100644 --- a/src/mdx-components.tsx +++ b/src/mdx-components.tsx @@ -4,6 +4,7 @@ import defaultMdxComponents from "fumadocs-ui/mdx"; import type { MDXComponents } from "mdx/types"; import type { ComponentProps } from "react"; import { BadExample } from "@/components/bad-example"; +import { CipherStashCredentials } from "@/components/cipherstash-credentials"; import { TrackedCodeBlock } from "@/components/code-block"; import { EqlFn } from "@/components/eql-fn"; import { EqlVersion } from "@/components/eql-version"; @@ -48,6 +49,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents { Steps, Step, BadExample, + CipherStashCredentials, EqlVersion, EqlFn, ZeroKmsRegions, diff --git a/v2-redirects.mjs b/v2-redirects.mjs index e983333..5c93da1 100644 --- a/v2-redirects.mjs +++ b/v2-redirects.mjs @@ -58,7 +58,7 @@ export const v2Redirects = [ }, { source: "/stack/cipherstash/encryption/prisma-next", - destination: "/integrations/prisma-next", + destination: "/integrations/prisma", permanent: false, }, {