Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
13 changes: 13 additions & 0 deletions .changeset/skills-identity-docs-refresh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'stash': patch
'@cipherstash/stack': patch
---

Docs: stop teaching the deprecated `LockContext.identify()` as the primary
identity-aware-encryption path (#591). The `stash-encryption` and `stash-supabase`
skills and the `@cipherstash/stack` README now lead with the current pattern —
authenticate the client with `OidcFederationStrategy`, then bind the claim per
operation with `.withLockContext({ identityClaim })` — and demote
`LockContext.identify()` to a clearly-marked deprecated note (per-operation CTS
tokens were removed in protect-ffi 0.25). Skills ship in the `stash` tarball, so
this keeps the bundled guidance correct for the 1.0 surface.
85 changes: 61 additions & 24 deletions packages/stack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ The all-in-one TypeScript SDK for the CipherStash data security stack.
- [Schema Definition](#schema-definition)
- [Encryption and Decryption](#encryption-and-decryption)
- [Searchable Encryption](#searchable-encryption)
- [Identity-Aware Encryption](#identity-aware-encryption)
- [Authentication](#authentication)
- [Identity-Aware Encryption](#identity-aware-encryption-lock-contexts)
- [CLI Reference](#cli-reference)
- [Configuration](#configuration)
- [Error Handling](#error-handling)
Expand Down Expand Up @@ -445,37 +446,74 @@ Notes:
- **The concrete type defines the legal operators.** `TextEq` supports `eq` / `ne` / `inArray` / `notInArray`; `*Ord` types add `gt` / `gte` / `lt` / `lte` / `between` / `notBetween` and `asc` / `desc`; `*Match` and `TextSearch` add `contains`; a bare `Text` / `Integer` / `Bigint` column is storage-only. Using an unsupported operator throws `EncryptionOperatorError`.
- Combine conditions with `ops.and` / `ops.or`, and do NULL checks with `ops.isNull` / `ops.isNotNull` (the where-clause operators are `async` and must be `await`ed; `ops.asc` / `ops.desc` are synchronous).

## Identity-Aware Encryption
## Authentication

Lock encryption to a specific user by requiring a valid JWT for decryption.
The client authenticates to ZeroKMS through `config.authStrategy`. Leave it
unset for the default **auto** strategy — credentials from the `CS_*`
environment variables, falling back to the local dev profile created by
`npx stash auth login`. Two explicit strategies cover the other cases:
Comment thread
coderdan marked this conversation as resolved.
Outdated

- **`AccessKeyStrategy`** — service-to-service / CI. Authenticates a *service*
with a CipherStash access key.
- **`OidcFederationStrategy`** — authenticates the client **as the end user**
by federating a third-party OIDC JWT (Clerk, Supabase, Auth0, Okta, ...)
into a CipherStash service token:

```typescript
import { LockContext } from "@cipherstash/stack/identity"
import { Encryption, OidcFederationStrategy } from "@cipherstash/stack"

// The callback is re-invoked on every (re-)federation and must return the
// CURRENT third-party OIDC JWT.
const strategy = OidcFederationStrategy.create(
process.env.CS_WORKSPACE_CRN!,
() => getUserJwt(),
)
if (strategy.failure) throw new Error(strategy.failure.error.message)

// 1. Create a lock context (defaults to the "sub" claim)
const lc = new LockContext()
const client = await Encryption({
schemas: [users],
config: { authStrategy: strategy.data },
Comment thread
coderabbitai[bot] marked this conversation as resolved.
})
```

// 2. Identify the user with their JWT
const identifyResult = await lc.identify(userJwt)
Authentication stands on its own — an OIDC-authenticated client encrypts and
decrypts normally. Binding *data* to the authenticated user is a separate,
optional step: the lock context, below.

if (identifyResult.failure) {
throw new Error(identifyResult.failure.message)
}
## Identity-Aware Encryption (Lock Contexts)

const lockContext = identifyResult.data
Bind a data key to a claim from the end user's JWT, so only that user can
decrypt. Chain `.withLockContext({ identityClaim })` on any operation:

```typescript
// Requires a client authenticated with OidcFederationStrategy (above) — the
// claim's value resolves from the federated JWT.
const IDENTITY = { identityClaim: ["sub"] }

// 3. Encrypt with lock context
const encrypted = await client
.encrypt("sensitive data", { column: users.email, table: users })
.withLockContext(lockContext)
.withLockContext(IDENTITY)

// 4. Decrypt with the same lock context
const decrypted = await client
.decrypt(encrypted.data)
.withLockContext(lockContext)
.withLockContext(IDENTITY)
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Lock contexts work with all operations: `encrypt`, `decrypt`, `encryptModel`, `decryptModel`, `bulkEncryptModels`, `bulkDecryptModels`, `bulkEncrypt`, `bulkDecrypt`.
Lock contexts **require** an `OidcFederationStrategy`-authenticated client
(the auto and access-key strategies authenticate no end user, so there is no
JWT to resolve claims from); plain authentication never requires a lock
context.

`identityClaim` is an array of JWT claim *names* (`["sub"]`), not values, and the
same claim must be supplied to encrypt and decrypt. Lock contexts work with all
operations: `encrypt`, `decrypt`, `encryptModel`, `decryptModel`,
`bulkEncryptModels`, `bulkDecryptModels`, `bulkEncrypt`, `bulkDecrypt`,
`encryptQuery`. `.withLockContext()` also accepts a `LockContext` instance.

> **Deprecated: `LockContext.identify()`.** Per-operation CTS tokens were removed
> in `protect-ffi` 0.25; the token `identify()` fetches is no longer used by
> encryption. Authenticate with `OidcFederationStrategy` and pass the claim
> directly, as above.

## CLI Reference

Expand Down Expand Up @@ -645,14 +683,13 @@ function Encryption(config: EncryptionClientConfig): Promise<EncryptionClient>

All operations are thenable (awaitable) and support `.withLockContext(lockContext)` for identity-aware encryption.

### `LockContext`
### `LockContext` (legacy)

```typescript
import { LockContext } from "@cipherstash/stack/identity"

const lc = new LockContext(options?)
const result = await lc.identify(jwtToken)
```
Identity-aware encryption is done with `OidcFederationStrategy` +
`.withLockContext({ identityClaim })` (see [Identity-Aware Encryption](#identity-aware-encryption-lock-contexts)).
`LockContext` / `identify()` remain for backwards compatibility only — the
per-operation CTS token `identify()` fetches was removed in `protect-ffi` 0.25
and is no longer used by encryption.

### Schema Builders

Expand Down
81 changes: 59 additions & 22 deletions skills/stash-encryption/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,46 +416,83 @@ const results = await client.encryptQuery(terms)

All values in the array must be non-null.

## Identity-Aware Encryption (Lock Contexts)
## Authentication

Lock encryption to a specific user by requiring a valid JWT for decryption.
The client authenticates to ZeroKMS through `config.authStrategy`. Leave it unset for the default **auto** strategy — credentials from the `CS_*` environment variables, falling back to the local dev profile created by `npx stash auth login`. Two explicit strategies cover the other cases:

```typescript
import { LockContext } from "@cipherstash/stack/identity"
- **`AccessKeyStrategy`** — service-to-service / CI. Authenticates a *service* with a CipherStash access key.
- **`OidcFederationStrategy`** — authenticates the client **as the end user** by federating a third-party OIDC JWT (Clerk, Supabase, Auth0, Okta, ...) into a CipherStash service token:

// 1. Create a lock context (defaults to the "sub" claim)
const lc = new LockContext()
// Or with custom claims: new LockContext({ context: { identityClaim: ["sub", "org_id"] } })
// Or with a pre-fetched CTS token: new LockContext({ ctsToken: { accessToken: "...", expiry: 123456 } })
```typescript
import { Encryption, OidcFederationStrategy } from "@cipherstash/stack"

// 2. Identify the user with their JWT
const identifyResult = await lc.identify(userJwt)
if (identifyResult.failure) {
throw new Error(identifyResult.failure.message)
// `getJwt` is re-invoked on every (re-)federation and must return the
// *current* third-party OIDC JWT.
const strategy = OidcFederationStrategy.create(
process.env.CS_WORKSPACE_CRN!,
() => getUserJwt(),
)
if (strategy.failure) {
throw new Error(`[auth] ${strategy.failure.type}: ${strategy.failure.error.message}`)
}
const lockContext = identifyResult.data

// 3. Encrypt with lock context
const client = await Encryption({
schemas: [users],
config: { authStrategy: strategy.data },
})
```

`OidcFederationStrategy.create()` returns a `Result` — **unwrap it**. Passing the envelope straight to `authStrategy` gives the FFI an object with no `getToken()` at all.

> **Known type error (runtime is fine).** The example above works at runtime, but `authStrategy: strategy.data` does not currently typecheck. `@cipherstash/auth` 0.41 strategies declare `getToken(): Promise<Result<TokenResult, AuthFailure>>`, while `@cipherstash/protect-ffi`'s exported `AuthStrategy` type still says `getToken(): Promise<{ token: string }>`. protect-ffi accepts **both** shapes at runtime (0.28+), on the Node and WASM paths alike — only its TypeScript declaration was left behind. Until it's widened, add `as unknown as AuthStrategy` or `// @ts-expect-error`. Tracked in [issue #602](https://github.com/cipherstash/stack/issues/602).

Authentication stands on its own — an OIDC-authenticated client encrypts and decrypts normally. Binding *data* to the authenticated user is a separate, optional step: the lock context, below.

## Identity-Aware Encryption (Lock Contexts)

Bind a data key to a claim from the end user's JWT, so only that user can decrypt. Chain `.withLockContext({ identityClaim })` on any operation:

```typescript
// Requires a client authenticated with OidcFederationStrategy (see
// "Authentication" above) — the claim's value resolves from the federated JWT.
const IDENTITY = { identityClaim: ["sub"] }

const encrypted = await client
.encrypt("sensitive data", { column: users.email, table: users })
.withLockContext(lockContext)
.withLockContext(IDENTITY)
if (encrypted.failure) {
throw new Error(`[encryption] ${encrypted.failure.type}: ${encrypted.failure.message}`)
}

// 4. Decrypt with the same lock context
// Decrypt with the SAME claim. Anything else cannot reproduce the key.
const decrypted = await client
.decrypt(encrypted.data)
.withLockContext(lockContext)
.withLockContext(IDENTITY)
if (decrypted.failure) {
throw new Error(`[encryption] ${decrypted.failure.type}: ${decrypted.failure.message}`)
}
```

Lock contexts work with ALL operations: `encrypt`, `decrypt`, `encryptModel`, `decryptModel`, `bulkEncrypt`, `bulkDecrypt`, `bulkEncryptModels`, `bulkDecryptModels`, `encryptQuery`.
Lock contexts **require** an `OidcFederationStrategy`-authenticated client: the claim's value resolves from the JWT the strategy federated. The auto and access-key strategies authenticate no end user, so there is no JWT to resolve claims from — `AccessKeyStrategy` in particular authenticates a *service* and cannot be used with a lock context. Plain authentication never requires a lock context.

### CTS Token Service
Every operation returns a `Result`. Narrow on `.failure` before touching `.data`: the `Failure` branch has no `data` property, so skipping the check is a type error, not merely a runtime risk.

The lock context exchanges the JWT for a CTS (CipherStash Token Service) token. Set the endpoint:
`identityClaim` is an array of JWT claim *names*, not values: `["sub"]` (the default) or `["sub", "org_id"]`. ZeroKMS resolves each claim's value from the JWT the strategy federated. **The same claim must be supplied to encrypt and decrypt** — it is baked into the data key's tag, so decrypting without it fails with `Failed to retrieve key`.

```bash
CS_CTS_ENDPOINT=https://ap-southeast-2.aws.auth.viturhosted.net
Lock contexts work with every operation: `encrypt`, `decrypt`, `encryptModel`, `decryptModel`, `bulkEncrypt`, `bulkDecrypt`, `bulkEncryptModels`, `bulkDecryptModels`, `encryptQuery`.

### Deprecated: `LockContext.identify()`

Older code fetched a per-operation CTS token:

```typescript
const lc = new LockContext()
const identified = await lc.identify(userJwt) // deprecated
await client.encrypt(...).withLockContext(identified.data)
```
Comment thread
coderdan marked this conversation as resolved.

**Per-operation CTS tokens were removed in `protect-ffi` 0.25.** `LockContext`, `identify()` and `getLockContext()` still exist for backwards compatibility, but the token `identify()` fetches is no longer used by encryption — and `CS_CTS_ENDPOINT` is only read on that dead path. Authenticate with `OidcFederationStrategy` instead and pass the claim directly. `.withLockContext()` accepts either a `LockContext` instance or a plain `{ identityClaim }`.

## Multi-Tenant Encryption (Keysets)

Isolate encryption keys per tenant:
Expand Down
52 changes: 44 additions & 8 deletions skills/stash-supabase/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,25 +289,61 @@ Operator family support is currently being developed in collaboration with the S

`.order()` on non-encrypted columns works normally.

## Identity-Aware Encryption
## Authentication

Chain `.withLockContext()` to tie encryption to a specific user's JWT:
The encryption client authenticates to ZeroKMS through `config.authStrategy`.
Unset, it uses the default **auto** strategy (`CS_*` environment variables,
falling back to the `npx stash auth login` dev profile) — fine for
service-level encryption. To authenticate **as the end user**, federate their
third-party OIDC JWT (Clerk, Supabase, Auth0, ...) with
`OidcFederationStrategy`:

```typescript
import { LockContext } from "@cipherstash/stack/identity"
import { Encryption, OidcFederationStrategy } from "@cipherstash/stack"

const lc = new LockContext()
const identified = await lc.identify(userJwt)
if (identified.failure) throw new Error(identified.failure.message)
const lockContext = identified.data
const strategy = OidcFederationStrategy.create(
process.env.CS_WORKSPACE_CRN!,
() => getUserJwt(), // re-invoked on every (re-)federation
)
if (strategy.failure) throw new Error(strategy.failure.error.message)

const encryptionClient = await Encryption({
schemas: [users],
config: { authStrategy: strategy.data },
})
const eSupabase = encryptedSupabase({ supabaseClient, encryptionClient })
```

Authentication stands on its own — an OIDC-authenticated client runs every
query normally. Binding *data* to the authenticated user is the optional next
step: the lock context.

## Identity-Aware Encryption (Lock Contexts)

Bind the data key to a claim from the end user's JWT by chaining
`.withLockContext({ identityClaim })` on a query. This **requires** an
`OidcFederationStrategy`-authenticated client (above) — the claim's value
resolves from the federated JWT; auto/access-key auth has no user JWT to
resolve claims from. Plain authentication never requires a lock context.

```typescript
const { data, error } = await eSupabase
.from("users", users)
.insert({ email: "alice@example.com", name: "Alice" })
.withLockContext(lockContext)
.withLockContext({ identityClaim: ["sub"] })
.select("id")
```

`identityClaim` is an array of JWT claim *names* (`["sub"]`), not values; the same
claim must be used to encrypt and decrypt. `.withLockContext()` also accepts a
`LockContext` instance.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderdan marked this conversation as resolved.

> **Deprecated: `LockContext.identify()`.** Older code did
> `new LockContext().identify(userJwt)` to fetch a per-operation CTS token. Those
> tokens were removed in `protect-ffi` 0.25 and the fetched token is no longer
> used by encryption. Authenticate with `OidcFederationStrategy` and pass the
> claim directly, as above.

## Audit Logging

Chain `.audit()` to attach metadata for ZeroKMS audit logging:
Expand Down
Loading