Skip to content
Closed
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
15 changes: 15 additions & 0 deletions .changeset/wasm-inline-workspace-crn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@cipherstash/stack": minor
---

**Breaking (`@cipherstash/stack/wasm-inline`):** `WasmClientConfig` now takes a
`workspaceCrn` instead of `region`. The region is derived from the CRN and the
access-key token's workspace is asserted against it (`getToken()` fails with
`code === "WORKSPACE_MISMATCH"` on a mismatch), so the CRN is the single source
of truth for workspace identity — matching the Node entry.

Bumps `@cipherstash/auth` to 0.40.0 to pick up the
`AccessKeyStrategy.create(workspaceCrn, accessKey)` signature.

Migration: replace `config.region` (e.g. `"ap-southeast-2.aws"`) with
`config.workspaceCrn` (e.g. `"crn:ap-southeast-2.aws:<workspace-id>"`).
8 changes: 3 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.24+ and auth 0.40+ 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,13 +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.
env:
CS_CLIENT_ID: ${{ secrets.CS_CLIENT_ID }}
CS_CLIENT_KEY: ${{ secrets.CS_CLIENT_KEY }}
CS_CLIENT_ACCESS_KEY: ${{ secrets.CS_CLIENT_ACCESS_KEY }}
CS_WORKSPACE_CRN: ${{ secrets.CS_WORKSPACE_CRN }}

steps:
- name: Checkout Repo
Expand Down Expand Up @@ -222,7 +220,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_CLIENT_ID CS_CLIENT_KEY CS_CLIENT_ACCESS_KEY CS_WORKSPACE_CRN; 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
15 changes: 7 additions & 8 deletions e2e/wasm/roundtrip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ 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` identifies the workspace and the region is derived
// from it; the access-key token's workspace is asserted against the CRN.
const REQUIRED_ENV = [
'CS_CLIENT_ACCESS_KEY',
'CS_CLIENT_ID',
'CS_CLIENT_KEY',
'CS_WORKSPACE_CRN',
] as const

function envOrSkip(): Record<(typeof REQUIRED_ENV)[number], string> | null {
Expand Down Expand Up @@ -72,10 +71,10 @@ Deno.test({
const client = await Encryption({
schemas: [users],
config: {
// The WASM entry needs an explicit region for AccessKeyStrategy.
// This is the region of the CI test workspace the CS_* secrets
// target — not the documented default (see wasm-inline.ts).
region: 'ap-southeast-2.aws',
// The WASM entry identifies the workspace by CRN; the region is
// derived from it and the access-key token's workspace is asserted
// against it (see wasm-inline.ts).
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=us-east-1.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, format `crn:<region>:<workspace-id>`. The WASM client
# derives the region from this and asserts the access-key token's
# workspace against it.
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_CLIENT_ID, CS_CLIENT_KEY, CS_CLIENT_ACCESS_KEY, CS_WORKSPACE_CRN

supabase functions serve --env-file .env.local cipherstash-roundtrip
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ 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') ?? 'us-east-1.aws'
const workspaceCrn = Deno.env.get('CS_WORKSPACE_CRN')

const missing = Object.entries({
CS_CLIENT_ACCESS_KEY: accessKey,
CS_CLIENT_ID: clientId,
CS_CLIENT_KEY: clientKey,
CS_WORKSPACE_CRN: workspaceCrn,
})
.filter(([, v]) => !v)
.map(([k]) => k)
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
48 changes: 22 additions & 26 deletions packages/stack/src/wasm-inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
* const client = await Encryption({
* schemas: [users],
* config: {
* region: "us-east-1.aws",
* accessKey: Deno.env.get("CS_CLIENT_ACCESS_KEY")!,
* clientId: Deno.env.get("CS_CLIENT_ID")!,
* clientKey: Deno.env.get("CS_CLIENT_KEY")!,
* workspaceCrn: Deno.env.get("CS_WORKSPACE_CRN")!,
* accessKey: Deno.env.get("CS_CLIENT_ACCESS_KEY")!,
* clientId: Deno.env.get("CS_CLIENT_ID")!,
* clientKey: Deno.env.get("CS_CLIENT_KEY")!,
* },
* })
*
Expand All @@ -37,7 +37,7 @@
*
* For runtimes that need a custom token store (e.g. cookies on a
* Supabase Edge Function), build the strategy yourself with
* `AccessKeyStrategy.create(region, accessKey, { store })` from
* `AccessKeyStrategy.create(workspaceCrn, accessKey, { store })` from
* `@cipherstash/auth/wasm-inline` and pass it via `config.strategy`.
*/

Expand Down Expand Up @@ -115,30 +115,25 @@ export type WasmPlaintext =
/**
* Config for {@link Encryption} on the WASM entry point.
*
* Unlike the Node entry, the WASM path needs the region passed
* explicitly today (no default — workspace deployment region is a
* caller concern). For service-to-service / CI use, pass `accessKey`
* plus the workspace `clientId` / `clientKey` and we construct an
* `AccessKeyStrategy` for you. To plug in a custom token store
* (cookies on Supabase Edge, KV on Cloudflare Workers, …) build the
* strategy with `AccessKeyStrategy.create(region, accessKey, { store })`
* and hand it to `config.strategy` instead.
*
* NOTE: `region` will be removed in a future release. The strategy
* will then take a `workspaceCrn` and derive the region from it —
* single source of truth, with the bearer token's workspace asserted
* against the CRN. Plan accordingly; the field is required for now
* because the underlying `@cipherstash/auth/wasm-inline`
* `AccessKeyStrategy.create()` still takes a region argument.
* Identify the workspace with a `workspaceCrn`
* (`crn:<region>:<workspace-id>`). The region is derived from the CRN —
* there's no separate `region` field — and the bearer token's workspace
* is asserted against the CRN (`getToken()` fails with
* `code === "WORKSPACE_MISMATCH"` on a mismatch). For service-to-service
* / CI use, pass `accessKey` plus the workspace `clientId` / `clientKey`
* and we construct an `AccessKeyStrategy` for you. To plug in a custom
* token store (cookies on Supabase Edge, KV on Cloudflare Workers, …)
* build the strategy with
* `AccessKeyStrategy.create(workspaceCrn, accessKey, { store })` and hand
* it to `config.strategy` instead.
*/
export type WasmClientConfig = {
/**
* CipherStash region, e.g. `"us-east-1.aws"`. Required for now.
* @deprecated will be replaced by `workspaceCrn` once
* `@cipherstash/auth` switches `AccessKeyStrategy.create()` to derive
* region from a CRN.
* Workspace CRN, format `crn:<region>:<workspace-id>` (e.g.
* `"crn:ap-southeast-2.aws:ZVATKW3VHMFG27DY"`). The region is parsed
* from the CRN; the workspace ID is asserted against the bearer token.
*/
region: string
workspaceCrn: string
/** Workspace client identifier — required by the WASM client. */
clientId: string
/** Workspace client key — required by the WASM client. */
Expand Down Expand Up @@ -331,7 +326,8 @@ function resolveStrategy(cfg: WasmClientConfig): AccessKeyStrategy {
)
}
if (cfg.strategy) return cfg.strategy
if (cfg.accessKey) return AccessKeyStrategy.create(cfg.region, cfg.accessKey)
if (cfg.accessKey)
return AccessKeyStrategy.create(cfg.workspaceCrn, cfg.accessKey)
throw new Error(
'[encryption]: WASM entry requires either `config.accessKey` or `config.strategy`.',
)
Expand Down
Loading
Loading