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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
59 changes: 36 additions & 23 deletions packages/stack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,35 +447,49 @@ Notes:

## Identity-Aware Encryption

Lock encryption to a specific user by requiring a valid JWT for decryption.
Bind a data key to a claim from the end user's JWT, so only that user can
decrypt. Do it in two parts: **authenticate the client as the user** with
`OidcFederationStrategy`, then **name the claim** on each operation with
`.withLockContext({ identityClaim })`.

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

// 1. Create a lock context (defaults to the "sub" claim)
const lc = new LockContext()

// 2. Identify the user with their JWT
const identifyResult = await lc.identify(userJwt)
// 1. Authenticate the client as the end user. The callback returns the current
// third-party OIDC JWT (Clerk, Supabase, Auth0, Okta, ...).
const strategy = OidcFederationStrategy.create(
process.env.CS_WORKSPACE_CRN!,
() => getUserJwt(),
)
if (strategy.failure) throw new Error(strategy.failure.error.message)

if (identifyResult.failure) {
throw new Error(identifyResult.failure.message)
}
const client = await Encryption({
schemas: [users],
config: { authStrategy: strategy.data },
})

const lockContext = identifyResult.data
// 2. Bind the data key to the user's `sub` claim on encrypt AND decrypt.
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)
```

Lock contexts work with all operations: `encrypt`, `decrypt`, `encryptModel`, `decryptModel`, `bulkEncryptModels`, `bulkDecryptModels`, `bulkEncrypt`, `bulkDecrypt`.
`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 +659,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)).
`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
72 changes: 50 additions & 22 deletions skills/stash-encryption/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,46 +416,74 @@ const results = await client.encryptQuery(terms)

All values in the array must be non-null.

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

Lock encryption to a specific user by requiring a valid JWT for decryption.
Bind a data key to a claim from the end user's JWT, so only that user can decrypt.

You do this in two parts: **authenticate the client as the user** with `OidcFederationStrategy`, then **name the claim** on each operation with `.withLockContext({ identityClaim })`.

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

// 1. Authenticate the client as the end user. `getJwt` is re-invoked on every
// (re-)federation and must return the *current* third-party OIDC JWT
// (Clerk, Supabase, Auth0, Okta, ...).
const strategy = OidcFederationStrategy.create(
process.env.CS_WORKSPACE_CRN!,
() => getUserJwt(),
)
if (strategy.failure) {
throw new Error(`[auth] ${strategy.failure.type}: ${strategy.failure.error.message}`)
}

// 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 } })
const client = await Encryption({
schemas: [users],
config: { authStrategy: strategy.data },
})

// 2. Identify the user with their JWT
const identifyResult = await lc.identify(userJwt)
if (identifyResult.failure) {
throw new Error(identifyResult.failure.message)
}
const lockContext = identifyResult.data
// 2. Bind the data key to the user's `sub` claim.
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)
if (encrypted.failure) {
throw new Error(`[encryption] ${encrypted.failure.type}: ${encrypted.failure.message}`)
}

// 4. Decrypt with the same lock context
// 3. 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`.
> **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 0.28 accepts **both** shapes at runtime, 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).

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

The lock context exchanges the JWT for a CTS (CipherStash Token Service) token. Set the endpoint:
Every operation returns a `Result` too. 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.

```bash
CS_CTS_ENDPOINT=https://ap-southeast-2.aws.auth.viturhosted.net
`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`.

Lock contexts work with every operation: `encrypt`, `decrypt`, `encryptModel`, `decryptModel`, `bulkEncrypt`, `bulkDecrypt`, `bulkEncryptModels`, `bulkDecryptModels`, `encryptQuery`.

`AccessKeyStrategy` is the service-to-service / CI path. It authenticates a *service*, not a user, so it cannot be used with a lock context.

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

**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
36 changes: 29 additions & 7 deletions skills/stash-supabase/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,23 +291,45 @@ Operator family support is currently being developed in collaboration with the S

## Identity-Aware Encryption

Chain `.withLockContext()` to tie encryption to a specific user's JWT:
Bind encryption to a specific user in two parts: **authenticate the client as the
user** with `OidcFederationStrategy` when you build it, then **name the claim** on
each query with `.withLockContext({ identityClaim })`.

```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
// 1. Authenticate the client as the end user (getUserJwt returns the current
// third-party OIDC JWT — Clerk, Supabase, Auth0, ...).
const strategy = OidcFederationStrategy.create(
process.env.CS_WORKSPACE_CRN!,
() => getUserJwt(),
)
if (strategy.failure) throw new Error(strategy.failure.error.message)

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

// 2. Bind the data key to the user's `sub` claim on each query.
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.

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