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
12 changes: 12 additions & 0 deletions .changeset/stack-protect-ffi-0-25-wasm-inline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@cipherstash/stack": minor
---

Bump `@cipherstash/protect-ffi` to `0.25.0` and align the WASM-inline path with its API.

protect-ffi `0.25.0` is a breaking release for both entries:

- **WASM (`@cipherstash/stack/wasm-inline`)**: `newClient` now takes a single options object with the auth strategy nested under `strategy` (was a separate first argument). The WASM `Encryption()` config now takes a **`workspaceCrn`** instead of a `region` — the CRN is the single source of truth for workspace identity, and the `AccessKeyStrategy` region is derived from it (`crn:<region>:<workspace-id>`). `CS_REGION` is no longer consulted; set `CS_WORKSPACE_CRN`. This matches protect-ffi `0.25`, which dropped `CS_REGION` in favour of `CS_WORKSPACE_CRN`.
- **Node**: `serviceToken` was removed from the encrypt / decrypt / query option types (and the `CtsToken` export). The per-operation CTS token is no longer forwarded — auth flows through the client's strategy / credentials, while lock contexts continue to travel as `lockContext.identityClaim`. The public `LockContext` / `identify()` API is unchanged.

Also adds an optional **`config.strategy`** to `Encryption()` (Node): pass an `AuthStrategy` — any `{ getToken(): Promise<{ token }> }`-shaped object, e.g. `AccessKeyStrategy` from `@cipherstash/auth` — and its `getToken()` is invoked on every ZeroKMS request, taking precedence over the credentials-derived default (the `clientKey` is still used for encryption). Omitting it preserves the existing credentials / env behaviour. `AuthStrategy` is re-exported from `@cipherstash/stack`.
11 changes: 6 additions & 5 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ jobs:
run: pnpm exec turbo run test:e2e --filter @cipherstash/e2e

# Verifies @cipherstash/stack/wasm-inline works under Deno — i.e. the
# WASM build of protect-ffi 0.24+ and auth 0.37+ can round-trip an
# WASM build of protect-ffi 0.25+ and auth 0.38+ can round-trip an
# encryption against ZeroKMS / CTS in a runtime with no native
# bindings available. The deno.json deliberately omits --allow-ffi so
# a silent fallback to the NAPI module is impossible.
Expand All @@ -168,10 +168,11 @@ jobs:
permissions:
contents: read

# CS_WORKSPACE_CRN deliberately not exposed here: the WASM client
# doesn't read it. A separate ticket tracks adding parity with the
# Node entry, at which point the CRN should be re-added.
# CS_WORKSPACE_CRN is the single source of truth for workspace
# identity and region — the stack /wasm-inline config requires it and
# derives the AccessKeyStrategy region from it.
env:
CS_WORKSPACE_CRN: ${{ secrets.CS_WORKSPACE_CRN }}
CS_CLIENT_ID: ${{ secrets.CS_CLIENT_ID }}
CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }}
CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }}
Expand Down Expand Up @@ -222,7 +223,7 @@ jobs:
# Fail loudly instead.
- name: Assert CS_* secrets are present
run: |
for v in CS_CLIENT_ID CS_CLIENT_KEY CS_CLIENT_ACCESS_KEY; do
for v in CS_WORKSPACE_CRN CS_CLIENT_ID CS_CLIENT_KEY CS_CLIENT_ACCESS_KEY; do
if [ -z "${!v}" ]; then
echo "::error::Required secret $v is not set on this runner — the WASM smoke test would silently skip."
exit 1
Expand Down
2 changes: 1 addition & 1 deletion e2e/wasm/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
},
"imports": {
"@cipherstash/stack/wasm-inline": "../../packages/stack/dist/wasm-inline.js",
"@cipherstash/protect-ffi/wasm-inline": "npm:@cipherstash/protect-ffi@0.24.0/wasm-inline",
"@cipherstash/protect-ffi/wasm-inline": "npm:@cipherstash/protect-ffi@0.25.0/wasm-inline",
"@cipherstash/auth/wasm-inline": "npm:@cipherstash/auth@0.38.0/wasm-inline"
}
}
15 changes: 8 additions & 7 deletions e2e/wasm/roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import {
isEncrypted,
} from '@cipherstash/stack/wasm-inline'

// `CS_WORKSPACE_CRN` is intentionally not in this list — the WASM
// client doesn't read it (workspace identity comes from the access-key
// token). A separate ticket tracks adding parity with the Node entry,
// at which point CRN should be added back here.
// `CS_WORKSPACE_CRN` is the single source of truth for workspace
// identity and region — the stack `/wasm-inline` config requires it and
// derives the `AccessKeyStrategy` region from it. `CS_REGION` is not
// consulted.
const REQUIRED_ENV = [
'CS_WORKSPACE_CRN',
'CS_CLIENT_ACCESS_KEY',
'CS_CLIENT_ID',
'CS_CLIENT_KEY',
Expand Down Expand Up @@ -69,9 +70,9 @@ Deno.test({
const client = await Encryption({
schemas: [users],
config: {
// Default region in the stack is ap-southeast-2.aws; the WASM
// entry needs an explicit region for AccessKeyStrategy.
region: 'ap-southeast-2.aws',
// CRN is the single source of truth — the region the
// AccessKeyStrategy needs is derived from it.
workspaceCrn: env!.CS_WORKSPACE_CRN,
accessKey: env!.CS_CLIENT_ACCESS_KEY,
clientId: env!.CS_CLIENT_ID,
clientKey: env!.CS_CLIENT_KEY,
Expand Down
8 changes: 4 additions & 4 deletions examples/supabase-worker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
CS_CLIENT_ACCESS_KEY=
CS_CLIENT_ID=
CS_CLIENT_KEY=
CS_REGION=ap-southeast-2.aws

# `CS_WORKSPACE_CRN` is intentionally omitted: the WASM client derives
# workspace identity from the access-key token, not from the CRN. This
# is a known parity gap with the Node entry — tracked separately.
# Workspace CRN, e.g. crn:ap-southeast-2.aws:your-workspace-id. The
# single source of truth for workspace identity and region — the
# AccessKeyStrategy region is derived from it. CS_REGION is not used.
CS_WORKSPACE_CRN=
2 changes: 1 addition & 1 deletion examples/supabase-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pnpm install

```sh
cp .env.example .env.local
# fill in CS_CLIENT_ID, CS_CLIENT_KEY, CS_CLIENT_ACCESS_KEY (and optionally CS_REGION)
# fill in CS_WORKSPACE_CRN, CS_CLIENT_ID, CS_CLIENT_KEY, CS_CLIENT_ACCESS_KEY

supabase functions serve --env-file .env.local cipherstash-roundtrip
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ Deno.serve(async (_req: Request) => {
const accessKey = Deno.env.get('CS_CLIENT_ACCESS_KEY')
const clientId = Deno.env.get('CS_CLIENT_ID')
const clientKey = Deno.env.get('CS_CLIENT_KEY')
const region = Deno.env.get('CS_REGION') ?? 'ap-southeast-2.aws'
const workspaceCrn = Deno.env.get('CS_WORKSPACE_CRN')

const missing = Object.entries({
CS_WORKSPACE_CRN: workspaceCrn,
CS_CLIENT_ACCESS_KEY: accessKey,
CS_CLIENT_ID: clientId,
CS_CLIENT_KEY: clientKey,
Expand All @@ -52,7 +53,7 @@ Deno.serve(async (_req: Request) => {
const client = await Encryption({
schemas: [users],
config: {
region,
workspaceCrn: workspaceCrn!,
accessKey: accessKey!,
clientId: clientId!,
clientKey: clientKey!,
Expand Down
74 changes: 74 additions & 0 deletions packages/stack/__tests__/init-strategy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* Tests for the optional `config.strategy` auth strategy.
*
* protect-ffi 0.25 lets `newClient` take an `AuthStrategy` (any
* `{ getToken(): Promise<{ token }> }` object). `Encryption` exposes it
* via `config.strategy`; when provided it must reach `newClient` as
* `opts.strategy`, and when omitted the option must be absent so the
* default credentials-derived strategy is used.
*/
import { Encryption } from '@/index'
import { encryptedColumn, encryptedTable } from '@/schema'
import type { AuthStrategy } from '@/types'
import { beforeEach, describe, expect, it, vi } from 'vitest'

vi.mock('@cipherstash/protect-ffi', () => ({
newClient: vi.fn(async () => ({ __mock: 'client' })),
}))

import * as ffi from '@cipherstash/protect-ffi'

const users = encryptedTable('users', {
email: encryptedColumn('email'),
})

beforeEach(() => {
vi.clearAllMocks()
})

describe('Encryption config.strategy', () => {
it('forwards a supplied strategy to newClient', async () => {
const strategy: AuthStrategy = {
getToken: vi.fn(async () => ({ token: 'service-token' })),
}

await Encryption({ schemas: [users], config: { strategy } })

// biome-ignore lint/suspicious/noExplicitAny: reading recorded mock args
const opts = vi.mocked(ffi.newClient).mock.calls.at(-1)![0] as any
expect(opts.strategy).toBe(strategy)
})

it('passes the strategy alongside the credential clientOpts', async () => {
const strategy: AuthStrategy = {
getToken: vi.fn(async () => ({ token: 'service-token' })),
}

await Encryption({
schemas: [users],
config: {
strategy,
workspaceCrn: 'crn:ap-southeast-2.aws:test-workspace',
clientId: 'client-id',
clientKey: 'client-key',
},
})

// biome-ignore lint/suspicious/noExplicitAny: reading recorded mock args
const opts = vi.mocked(ffi.newClient).mock.calls.at(-1)![0] as any
expect(opts.strategy).toBe(strategy)
// clientKey is still required even when a strategy is supplied.
expect(opts.clientOpts.clientKey).toBe('client-key')
expect(opts.clientOpts.workspaceCrn).toBe(
'crn:ap-southeast-2.aws:test-workspace',
)
})

it('leaves strategy undefined when none is supplied', async () => {
await Encryption({ schemas: [users] })

// biome-ignore lint/suspicious/noExplicitAny: reading recorded mock args
const opts = vi.mocked(ffi.newClient).mock.calls.at(-1)![0] as any
expect(opts.strategy).toBeUndefined()
})
})
Loading
Loading