Skip to content

feat(stack): protect-ffi 0.26.0 + auth 0.39 OidcFederationStrategy (stacked on #496)#497

Open
coderdan wants to merge 6 commits into
mainfrom
feat/stack-protect-ffi-025
Open

feat(stack): protect-ffi 0.26.0 + auth 0.39 OidcFederationStrategy (stacked on #496)#497
coderdan wants to merge 6 commits into
mainfrom
feat/stack-protect-ffi-025

Conversation

@coderdan

@coderdan coderdan commented May 29, 2026

Copy link
Copy Markdown
Contributor

Stacked on top of #496 (feat/stack-wasm-inline) — review/merge that first; this PR's base is the #496 branch, not main.

Supersedes the earlier 0.25.0 work: bumps to @cipherstash/protect-ffi@0.26.0 and @cipherstash/auth@0.39.0, and uses the new OidcFederationStrategy to replace the lock-context token ceremony with a simpler, strategy-based approach for identity-bound encryption.

1. Version bumps

  • @cipherstash/protect-ffi 0.25.00.26.0. The public TypeScript API is identical to 0.25.0 (verified by diffing the published lib/index.d.cts); 0.26.0 is internal fixes only (per-isolate Neon Channel cleanup, try_catch around the JS getToken).
  • @cipherstash/auth catalog (and the six platform entries) 0.38.00.39.0, which adds OidcFederationStrategy.
  • e2e/wasm/deno.json pins + pnpm-lock.yaml regenerated.

2. Strategy-based, identity-bound encryption (replaces the ceremony)

protect-ffi 0.25 removed the per-operation serviceToken, which left the old LockContext ceremony half-broken: identify() fetched a CTS token the operations no longer sent, so the request authenticated as the service while identityClaim asked ZeroKMS to bind to a user it couldn't verify.

OidcFederationStrategy and identityClaim compose: the strategy federates the end user's OIDC JWT into a CTS token at the client level (so requests authenticate as the user), and identityClaim still selects which claim ZeroKMS bakes into the data-key tag.

import { Encryption, OidcFederationStrategy } from "@cipherstash/stack"

const client = await Encryption({
  schemas: [users],
  config: { strategy: OidcFederationStrategy.create(region, workspaceId, () => getUserJwt()) },
})

await client
  .encrypt("alice@example.com", { column: users.email, table: users })
  .withLockContext({ identityClaim: ["sub"] })
  • .withLockContext() now accepts a plain { identityClaim } (or a LockContext) and resolves the claim synchronously — no CTS token, no identify() call.
  • LockContext.identify() / getLockContext() are deprecated (kept for back-compat); the client strategy handles token acquisition.
  • This is a non-breaking minor: omit config.strategy and existing credential/env behaviour is unchanged; existing .withLockContext(lockContext) call sites still compile.

3. Strategy re-exports

  • OidcFederationStrategy, AccessKeyStrategy, AutoStrategy, DeviceSessionStrategy re-exported from @cipherstash/stack; OidcFederationStrategy / AccessKeyStrategy from @cipherstash/stack/wasm-inline (+ the wasm config.strategy type broadened to accept either). Integrators no longer need a separate @cipherstash/auth install.
  • @cipherstash/stack now declares the @cipherstash/auth-<platform> packages as optionalDependencies@cipherstash/auth ships them as optional peer deps (not auto-installed), so this is required for the re-exported Node strategies to resolve their native binding for consumers.

Also updated

  • lock-context-wiring.test.ts — asserts both a LockContext and a plain { identityClaim } forward identityClaim, and serviceToken is still never sent.
  • init-strategy.test.ts — adds an OidcFederationStrategy-shaped forwarding case.
  • lock-context.test.ts — live (USER_JWT-gated) round-trip rewritten to use OidcFederationStrategy + .withLockContext({ identityClaim }), confirming per-user binding.
  • JSDoc (Encryption(), LockContext, ClientConfig.strategy), AGENTS.md, README.md, changeset.

⚠️ Worth confirming in a live CTS round-trip that per-user binding behaves as intended under 0.26 — the offline tests guard the wiring; the live test guards the server-side semantics when USER_JWT is supplied.

Summary by CodeRabbit

Release Notes

  • New Features

    • Strategy-based authentication now available via OidcFederationStrategy for identity-bound encryption.
    • Simplified lock context using direct identity claims—no longer requires token ceremony.
  • Documentation

    • Updated authentication and encryption guides for new strategy approach.
    • Environment configuration now uses CS_WORKSPACE_CRN (replaces CS_REGION).
  • Deprecations

    • LockContext.identify() and getLockContext() deprecated (kept for backward compatibility).
  • Chores

    • Updated dependencies: protect-ffi 0.26.0, auth 0.39.0.

@coderdan coderdan requested a review from a team as a code owner May 29, 2026 08:45
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Upgrades @cipherstash/protect-ffi to 0.26.0 and replaces the CTS-token/LockContext.identify() ceremony with a synchronous LockContextInput/resolveLockContext pattern across all operation classes. Adds AuthStrategy/config.strategy forwarding to newClient, re-exports auth strategy classes from @cipherstash/stack, and replaces CS_REGION/region with CS_WORKSPACE_CRN/workspaceCrn throughout.

Changes

OIDC Strategy & LockContextInput Migration

Layer / File(s) Summary
LockContextInput type, resolveLockContext helper, and LockContext deprecations
packages/stack/src/identity/index.ts
Adds exported LockContextInput = LockContext | Context union type and resolveLockContext() helper that synchronously normalizes either input to Context; adds identityContext getter; deprecates identify() and getLockContext(); relaxes getLockContext() to no longer require a CTS token.
AuthStrategy type, ClientConfig.strategy field, and public re-exports
packages/stack/src/types.ts, packages/stack/src/types-public.ts, packages/stack/src/index.ts
Imports AuthStrategy from @cipherstash/protect-ffi; adds strategy?: AuthStrategy to ClientConfig; re-exports AuthStrategy through the public types entrypoint; re-exports AccessKeyStrategy, AutoStrategy, DeviceSessionStrategy, OidcFederationStrategy as values with InstanceType type aliases from @cipherstash/auth, using a default-import workaround for ESM/CJS compatibility.
EncryptionClient.init: strategy forwarding to newClient
packages/stack/src/encryption/index.ts, packages/stack/package.json
Adds strategy?: AuthStrategy to EncryptionClient.init config and passes it into newClient(); adds TSDoc example using OidcFederationStrategy; bumps @cipherstash/protect-ffi to 0.26.0 and adds optional platform-specific @cipherstash/auth-* native bindings.
All operation classes: LockContext → LockContextInput + serviceToken removal
packages/stack/src/encryption/operations/encrypt.ts, decrypt.ts, encrypt-model.ts, decrypt-model.ts, bulk-encrypt.ts, bulk-decrypt.ts, bulk-encrypt-models.ts, bulk-decrypt-models.ts, encrypt-query.ts, batch-encrypt-query.ts
Updates all 10 operation classes to accept LockContextInput on withLockContext(), replace async getLockContext()/failure-check/serviceToken paths with synchronous resolveLockContext(), and remove serviceToken from all FFI call option objects.
model-helpers: Context param replacing GetLockContextResponse
packages/stack/src/encryption/helpers/model-helpers.ts
Updates decryptModelFieldsWithLockContext, encryptModelFieldsWithLockContext, bulkDecryptModelsWithLockContext, and bulkEncryptModelsWithLockContext to accept Context directly; removes serviceToken: lockContext.ctsToken from all four bulk FFI call sites.
wasm-inline: workspaceCrn, WasmAuthStrategy, and OidcFederationStrategy re-export
packages/stack/src/wasm-inline.ts
Requires workspaceCrn in WasmClientConfig (removes region); exports WasmAuthStrategy = AccessKeyStrategy | OidcFederationStrategy; re-exports both from @cipherstash/auth/wasm-inline; updates resolveStrategy to use workspaceCrn; changes wasmNewClient to new single-options-object call convention.
Test fixtures and lock-context wiring tests
packages/stack/__tests__/fixtures/index.ts, packages/stack/__tests__/init-strategy.test.ts, packages/stack/__tests__/lock-context-wiring.test.ts, packages/stack/__tests__/encrypt-query*.test.ts
Rewrites createMockLockContext to return { identityClaim } directly; removes null/failing mock helpers; adds init-strategy.test.ts verifying config.strategy forwarding; adds lock-context-wiring.test.ts asserting identityClaim forwarded and serviceToken never present across all operation types.
E2E tests, CI, examples, and docs updated for workspaceCrn + OidcFederationStrategy
packages/stack/__tests__/lock-context.test.ts, e2e/wasm/*, .github/workflows/tests.yml, examples/supabase-worker/*, pnpm-workspace.yaml, README.md, AGENTS.md, .changeset/...
Updates integration tests to use OidcFederationStrategy + .withLockContext({identityClaim}); bumps WASM e2e deps; adds CS_WORKSPACE_CRN to CI env and secret checks; replaces CS_REGION with CS_WORKSPACE_CRN in the Supabase worker example; bumps auth platform bindings to 0.39.0.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant EncryptionClient
  participant OidcFederationStrategy
  participant EncryptOperation
  participant resolveLockContext
  participant protectFFI as protect-ffi

  rect rgba(70, 130, 180, 0.5)
    note over App,EncryptionClient: Initialization
    App->>OidcFederationStrategy: OidcFederationStrategy.create(workspaceCrn, jwt)
    OidcFederationStrategy-->>App: strategy
    App->>EncryptionClient: Encryption({ workspaceCrn, clientKey, strategy })
    EncryptionClient->>protectFFI: newClient({ strategy, clientOpts: { workspaceCrn, clientKey } })
    protectFFI-->>EncryptionClient: client
  end

  rect rgba(60, 179, 113, 0.5)
    note over App,protectFFI: Identity-bound encrypt
    App->>EncryptionClient: encrypt(plaintext).withLockContext({ identityClaim: ["sub"] })
    EncryptionClient-->>App: EncryptOperationWithLockContext
    App->>EncryptOperation: execute()
    EncryptOperation->>resolveLockContext: resolveLockContext({ identityClaim })
    resolveLockContext-->>EncryptOperation: Context
    EncryptOperation->>protectFFI: ffiEncrypt(payload, { lockContext: context })
    note right of protectFFI: strategy.getToken() called per request — no serviceToken
    protectFFI-->>EncryptOperation: ciphertext
    EncryptOperation-->>App: Result
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • cipherstash/stack#326: Modifies EncryptionClient/newClient initialization in packages/stack/src/encryption/index.ts to change how workspace credentials are passed to protect-ffi, the same file and call site changed in this PR to add strategy forwarding.
  • cipherstash/stack#496: Introduces the WASM-inline client and config structure in packages/stack/src/wasm-inline.ts that this PR further modifies for workspaceCrn, WasmAuthStrategy, and the new wasmNewClient calling convention.
  • cipherstash/stack#493: Overlaps with this PR on the same packages/stack/src/encryption/operations/* classes, touching execute() logic in encrypt/decrypt/bulk/query variants.

Suggested reviewers

  • tobyhede

Poem

🐇 Hippity-hop, the token's gone away,
No CTS ceremony needed today!
resolveLockContext — one quick call,
identityClaim binds the key for all.
OidcFederationStrategy leads the dance,
workspaceCrn gives region its chance! 🔑

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: upgrading to protect-ffi 0.26.0 and auth 0.39.0 with OidcFederationStrategy support for identity-bound encryption.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stack-protect-ffi-025

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@changeset-bot

changeset-bot Bot commented May 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a23fe7e

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@cipherstash/stack Minor
@cipherstash/bench Patch
@cipherstash/prisma-next Patch
@cipherstash/basic-example Patch
@cipherstash/prisma-next-example Patch
@cipherstash/e2e Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderdan coderdan changed the title feat(stack): protect-ffi 0.25.0 + auth strategy option (stacked on #496) feat(stack): protect-ffi 0.26.0 + auth 0.39 OidcFederationStrategy (stacked on #496) Jun 8, 2026
@coderdan coderdan requested a review from Copilot June 8, 2026 09:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR upgrades Stack’s Protect/Auth dependencies and updates the identity-bound encryption flow to match protect-ffi ≥0.25’s removal of per-operation serviceToken, moving authentication to a client-level config.strategy (notably via OidcFederationStrategy) while keeping lock context as a pure { identityClaim } value.

Changes:

  • Bump @cipherstash/protect-ffi to 0.26.0 and @cipherstash/auth to 0.39.0, updating workspace config to use workspaceCrn consistently (including /wasm-inline).
  • Replace the lock-context “token ceremony” with a synchronous lock-context resolution (.withLockContext({ identityClaim }) or LockContext), and stop forwarding serviceToken in all operations.
  • Re-export auth strategies from @cipherstash/stack (and selected ones from /wasm-inline), add wiring tests to ensure identityClaim is forwarded and serviceToken is never sent.

Reviewed changes

Copilot reviewed 34 out of 35 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
README.md Updates identity-aware encryption docs to the strategy-based approach.
pnpm-workspace.yaml Bumps @cipherstash/auth* catalog versions to 0.39.0.
pnpm-lock.yaml Regenerated lockfile reflecting new auth/protect versions and optional deps.
packages/stack/src/wasm-inline.ts Switches WASM config to workspaceCrn, supports OIDC strategy, updates protect-ffi wasm newClient call shape, and re-exports strategies.
packages/stack/src/types.ts Re-exports protect-ffi AuthStrategy type and adds ClientConfig.strategy.
packages/stack/src/types-public.ts Exposes AuthStrategy in public type surface.
packages/stack/src/index.ts Re-exports auth strategies from @cipherstash/auth in an ESM-compatible way.
packages/stack/src/identity/index.ts Adds LockContextInput + resolveLockContext, deprecates old token ceremony methods, makes CTS token optional in response type.
packages/stack/src/encryption/operations/encrypt.ts Uses synchronous lock-context resolution; removes serviceToken forwarding.
packages/stack/src/encryption/operations/encrypt-query.ts Uses synchronous lock-context resolution; removes serviceToken forwarding.
packages/stack/src/encryption/operations/encrypt-model.ts Uses synchronous lock-context resolution and passes Context through model helpers.
packages/stack/src/encryption/operations/decrypt.ts Uses synchronous lock-context resolution; removes serviceToken forwarding.
packages/stack/src/encryption/operations/decrypt-model.ts Uses synchronous lock-context resolution and passes Context through model helpers.
packages/stack/src/encryption/operations/bulk-encrypt.ts Uses synchronous lock-context resolution; removes serviceToken forwarding.
packages/stack/src/encryption/operations/bulk-encrypt-models.ts Uses synchronous lock-context resolution and passes Context through model helpers.
packages/stack/src/encryption/operations/bulk-decrypt.ts Uses synchronous lock-context resolution; removes serviceToken forwarding.
packages/stack/src/encryption/operations/bulk-decrypt-models.ts Uses synchronous lock-context resolution and passes Context through model helpers.
packages/stack/src/encryption/operations/batch-encrypt-query.ts Uses synchronous lock-context resolution; removes serviceToken forwarding.
packages/stack/src/encryption/index.ts Plumbs optional config.strategy into protect-ffi newClient and documents identity-bound usage.
packages/stack/src/encryption/helpers/model-helpers.ts Removes CTS token plumbing from model bulk encrypt/decrypt helpers.
packages/stack/package.json Bumps protect-ffi and adds optionalDependencies for auth platform binaries.
packages/stack/tests/lock-context.test.ts Rewrites live identity-bound tests to use OidcFederationStrategy + { identityClaim }.
packages/stack/tests/lock-context-wiring.test.ts Adds offline mocks ensuring identityClaim is forwarded and serviceToken is never sent.
packages/stack/tests/init-strategy.test.ts Adds tests verifying config.strategy is forwarded to protect-ffi newClient.
packages/stack/tests/fixtures/index.ts Simplifies lock-context fixtures to a plain { identityClaim } input.
packages/stack/tests/encrypt-query.test.ts Updates lock-context tests to use plain { identityClaim } input and removes CTS-token-failure cases.
packages/stack/tests/encrypt-query-searchable-json.test.ts Updates lock-context tests to use plain { identityClaim } input and removes CTS-token-failure cases.
examples/supabase-worker/supabase/functions/cipherstash-roundtrip/index.ts Updates example config to use CS_WORKSPACE_CRN (no CS_REGION).
examples/supabase-worker/README.md Updates env setup instructions to include CS_WORKSPACE_CRN.
examples/supabase-worker/.env.example Updates example env file to use CS_WORKSPACE_CRN and remove CS_REGION.
e2e/wasm/roundtrip.test.ts Updates WASM e2e to require/use CS_WORKSPACE_CRN and drop explicit region.
e2e/wasm/deno.json Pins protect/auth wasm-inline imports to 0.26.0 / 0.39.0.
AGENTS.md Updates guidance to the strategy-based identity-aware encryption flow.
.github/workflows/tests.yml Exposes CS_WORKSPACE_CRN to wasm e2e job and asserts it’s present.
.changeset/stack-protect-ffi-0-26-oidc-strategy.md Adds release notes for the dependency bumps and new identity-bound strategy flow.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)

packages/stack/tests/lock-context.test.ts:145

  • This test tries to assert that decrypting without the lock context fails, but decryptModel() returns a Result (it doesn’t throw). As written, the try/catch never runs and the test will pass without asserting anything. Assert on the returned Result’s failure instead.
    try {
      await protectClient.decryptModel(encryptedModel.data)
    } catch (error) {
      const e = error as Error
      expect(e.message.startsWith('Failed to retrieve key')).toEqual(true)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

* "ap-southeast-2.aws", workspaceId, () => getClerkSessionToken(req),
* { store: cookieStore({ request: req, responseHeaders }) },
* )
* const client = await Encryption({ schemas, config: { strategy, clientId, clientKey } })
Base automatically changed from feat/stack-wasm-inline to main June 19, 2026 13:22
@auxesis auxesis requested review from auxesis and freshtonic and removed request for a team June 23, 2026 23:55
coderdan added 6 commits June 24, 2026 11:39
protect-ffi 0.25.0 is a breaking release for both entries:

WASM (@cipherstash/stack/wasm-inline):
- newClient(strategy, opts) -> newClient(opts) with strategy nested.
- Config takes a workspaceCrn instead of region; the AccessKeyStrategy
  region is derived from the CRN (crn:<region>:<workspace-id>). CS_REGION
  is no longer consulted; set CS_WORKSPACE_CRN.

Node:
- serviceToken removed from the encrypt/decrypt/query option types (and
  the CtsToken export). The per-operation CTS token is no longer
  forwarded; lock contexts still travel as lockContext.identityClaim.
  Public LockContext/identify() API is unchanged.

Adds offline lock-context wiring tests (mock protect-ffi) asserting every
operation forwards identityClaim and never sends serviceToken, plus
extractRegionFromCrn unit tests. Updates the Deno e2e test, Supabase
example, and wasm-e2e CI job to CS_WORKSPACE_CRN.
protect-ffi 0.25 lets newClient take an AuthStrategy (any
{ getToken(): Promise<{ token }> } object). Expose it on the Node
Encryption client via config.strategy: when supplied, getToken() is
invoked on every ZeroKMS request, taking precedence over the
credentials-derived default (clientKey is still used for encryption).
Omitting it preserves existing credentials/env behaviour.

Kept on init (rather than a separate initWithStrategy) so a future
keyProvider option can land in the same config. AuthStrategy is
re-exported from @cipherstash/stack for consumers to type their own.
…lace lock-context ceremony

Supersedes the 0.25.0 bump with protect-ffi 0.26.0 (API-identical; internal
fixes only) and @cipherstash/auth 0.39.0, and uses the new
OidcFederationStrategy to replace the lock-context token ceremony with a
strategy-based approach for identity-bound encryption.

- bump @cipherstash/protect-ffi 0.25.0 -> 0.26.0; @cipherstash/auth catalog
  (and platform entries) 0.38.0 -> 0.39.0; e2e/wasm/deno.json pins; lockfile
- .withLockContext() now accepts a plain { identityClaim } (or a LockContext)
  and resolves the claim synchronously — no CTS token, no identify() call
- deprecate LockContext.identify() / getLockContext(); the client strategy
  (OidcFederationStrategy) now handles user token acquisition
- re-export OidcFederationStrategy/AccessKeyStrategy/AutoStrategy/
  DeviceSessionStrategy from @cipherstash/stack, and the strategies from
  @cipherstash/stack/wasm-inline
- broaden the wasm-inline config strategy type to accept OidcFederationStrategy
- declare @cipherstash/auth platform optionalDependencies (auth ships them as
  optional peer deps, not auto-installed) so the re-exported Node strategies
  resolve their native binding for consumers
- update wiring/init/live tests, JSDoc, AGENTS.md, README, changeset
…Dependencies

The optionalDependencies block added to packages/stack/package.json was not
reflected in pnpm-lock.yaml, breaking `pnpm install --frozen-lockfile` in CI.
@cipherstash/auth 0.39 changed AccessKeyStrategy.create(region, accessKey)
to AccessKeyStrategy.create(workspaceCrn, accessKey) — it derives the region
from the CRN itself. The wasm-inline resolveStrategy still passed a derived
region, so the Deno WASM e2e failed with 'Invalid CRN: <region>'. Pass the
CRN directly and drop the now-obsolete extractRegionFromCrn helper + tests.
(OidcFederationStrategy.create still takes region + workspaceId.)
…ext tests

- index.ts: @cipherstash/auth's Node entry is CJS with `module.exports =
  { ...native }`; the spread defeats cjs-module-lexer so a static
  `export { AccessKeyStrategy } from` throws 'Named export not found' under
  real Node ESM (the E2E cli failure). Default-import the module (which is
  module.exports at runtime, all names present) and re-export each binding
  explicitly, with instance types for the strategy classes.
- encrypt-query / encrypt-query-searchable-json tests + fixtures: the ops no
  longer call getLockContext(); .withLockContext() takes a plain
  { identityClaim }. createMockLockContext() now returns that shape; dropped
  the getLockContext spy assertions and the obsolete failure / null-context
  cases (resolveLockContext is synchronous and cannot fail).
@coderdan coderdan force-pushed the feat/stack-protect-ffi-025 branch from 1efdd03 to a23fe7e Compare June 24, 2026 01:57
@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/cipherstash/stack/issues/comments/4572736076","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/cipherstash/stack/pull/497?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: defaults\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro\n> \n> **Run ID**: `f4cfb06e-dd68-43d7-ae11-abd864deb732`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between df9c8e33c00b87e7b19886eb79aeed9805e21134 and a23fe7e3defa17d4e20d1d55a7cf6fe6656c2ba2.\n> \n> </details>\n> \n> <details>\n> <summary>⛔ Files ignored due to path filters (1)</summary>\n> \n> * `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (34)</summary>\n> \n> * `.changeset/stack-protect-ffi-0-26-oidc-strategy.md`\n> * `.github/workflows/tests.yml`\n> * `AGENTS.md`\n> * `README.md`\n> * `e2e/wasm/deno.json`\n> * `e2e/wasm/roundtrip.test.ts`\n> * `examples/supabase-worker/.env.example`\n> * `examples/supabase-worker/README.md`\n> * `examples/supabase-worker/supabase/functions/cipherstash-roundtrip/index.ts`\n> * `packages/stack/__tests__/encrypt-query-searchable-json.test.ts`\n> * `packages/stack/__tests__/encrypt-query.test.ts`\n> * `packages/stack/__tests__/fixtures/index.ts`\n> * `packages/stack/__tests__/init-strategy.test.ts`\n> * `packages/stack/__tests__/lock-context-wiring.test.ts`\n> * `packages/stack/__tests__/lock-context.test.ts`\n> * `packages/stack/package.json`\n> * `packages/stack/src/encryption/helpers/model-helpers.ts`\n> * `packages/stack/src/encryption/index.ts`\n> * `packages/stack/src/encryption/operations/batch-encrypt-query.ts`\n> * `packages/stack/src/encryption/operations/bulk-decrypt-models.ts`\n> * `packages/stack/src/encryption/operations/bulk-decrypt.ts`\n> * `packages/stack/src/encryption/operations/bulk-encrypt-models.ts`\n> * `packages/stack/src/encryption/operations/bulk-encrypt.ts`\n> * `packages/stack/src/encryption/operations/decrypt-model.ts`\n> * `packages/stack/src/encryption/operations/decrypt.ts`\n> * `packages/stack/src/encryption/operations/encrypt-model.ts`\n> * `packages/stack/src/encryption/operations/encrypt-query.ts`\n> * `packages/stack/src/encryption/operations/encrypt.ts`\n> * `packages/stack/src/identity/index.ts`\n> * `packages/stack/src/index.ts`\n> * `packages/stack/src/types-public.ts`\n> * `packages/stack/src/types.ts`\n> * `packages/stack/src/wasm-inline.ts`\n> * `pnpm-workspace.yaml`\n> \n> </details>\n> \n> ```ascii\n>  ____________________________________________________________________________\n> < I've got bills to pay, so I'm gonna find, find, find those bugs every day. >\n>  ----------------------------------------------------------------------------\n>   \\\n>    \\   (\\__/)\n>        (•ㅅ•)\n>        /   づ\n> ```\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n<details>\n<summary>📝 Generate docstrings</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> Create stacked PR\n- [ ] <!-- {\"checkboxId\": \"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98\"} --> Commit on current branch\n\n</details>\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `feat/stack-protect-ffi-025`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=cipherstash/stack&utm_content=497)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/stack/src/encryption/operations/batch-encrypt-query.ts`:
- Around line 207-209: The resolveLockContext call is positioned outside the
withResult wrapper, which allows resolution errors to escape instead of being
caught and returned as a failure Result. Move the
resolveLockContext(this.lockContext) call from before the withResult statement
into the callback function that is passed to withResult, so that any errors
during lock context resolution are properly wrapped and returned as { failure }
according to the Result contract required by the encryption operations
guidelines.

In `@packages/stack/src/encryption/operations/encrypt-query.ts`:
- Around line 151-153: Move the resolveLockContext(this.lockContext) call inside
the withResult callback to ensure backward-compatible LockContext resolution
errors are properly caught and converted to the Result contract shape { failure
}. The context resolution should happen as the first step within the async
callback passed to withResult, matching the pattern used in other migrated
encryption operations in this codebase. This ensures all potential failures are
wrapped in the Result contract rather than rejecting before withResult can
handle them.

In `@packages/stack/src/identity/index.ts`:
- Around line 66-67: The OIDC example using OidcFederationStrategy.create() at
lines 66–67 uses the outdated signature with separate region and workspaceId
parameters. Update the example to use the current signature where workspaceCrn
is passed as the first parameter instead of region and workspaceId, while
keeping the callback function () => getJwt() as the second parameter to match
the `@cipherstash/auth` v0.39.0+ API.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f4cfb06e-dd68-43d7-ae11-abd864deb732

📥 Commits

Reviewing files that changed from the base of the PR and between df9c8e3 and a23fe7e.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (34)
  • .changeset/stack-protect-ffi-0-26-oidc-strategy.md
  • .github/workflows/tests.yml
  • AGENTS.md
  • README.md
  • e2e/wasm/deno.json
  • e2e/wasm/roundtrip.test.ts
  • examples/supabase-worker/.env.example
  • examples/supabase-worker/README.md
  • examples/supabase-worker/supabase/functions/cipherstash-roundtrip/index.ts
  • packages/stack/__tests__/encrypt-query-searchable-json.test.ts
  • packages/stack/__tests__/encrypt-query.test.ts
  • packages/stack/__tests__/fixtures/index.ts
  • packages/stack/__tests__/init-strategy.test.ts
  • packages/stack/__tests__/lock-context-wiring.test.ts
  • packages/stack/__tests__/lock-context.test.ts
  • packages/stack/package.json
  • packages/stack/src/encryption/helpers/model-helpers.ts
  • packages/stack/src/encryption/index.ts
  • packages/stack/src/encryption/operations/batch-encrypt-query.ts
  • packages/stack/src/encryption/operations/bulk-decrypt-models.ts
  • packages/stack/src/encryption/operations/bulk-decrypt.ts
  • packages/stack/src/encryption/operations/bulk-encrypt-models.ts
  • packages/stack/src/encryption/operations/bulk-encrypt.ts
  • packages/stack/src/encryption/operations/decrypt-model.ts
  • packages/stack/src/encryption/operations/decrypt.ts
  • packages/stack/src/encryption/operations/encrypt-model.ts
  • packages/stack/src/encryption/operations/encrypt-query.ts
  • packages/stack/src/encryption/operations/encrypt.ts
  • packages/stack/src/identity/index.ts
  • packages/stack/src/index.ts
  • packages/stack/src/types-public.ts
  • packages/stack/src/types.ts
  • packages/stack/src/wasm-inline.ts
  • pnpm-workspace.yaml

Comment on lines +207 to 209
const context = resolveLockContext(this.lockContext)

const result = await withResult(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep lock-context resolution inside the Result wrapper.

This locked batch path resolves before withResult, so a resolution error can escape instead of returning { failure }. Move it into the callback before building the payloads. As per coding guidelines, **/src/**/*.{ts,tsx} must preserve the Result contract shape { data } or { failure } returned by encryption operations.

Proposed fix
-    const context = resolveLockContext(this.lockContext)
-
     const result = await withResult(
       async () => {
         if (!this.client) throw noClientError()
+        const context = resolveLockContext(this.lockContext)

         const { metadata } = this.getAuditData()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const context = resolveLockContext(this.lockContext)
const result = await withResult(
const result = await withResult(
async () => {
if (!this.client) throw noClientError()
const context = resolveLockContext(this.lockContext)
const { metadata } = this.getAuditData()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack/src/encryption/operations/batch-encrypt-query.ts` around lines
207 - 209, The resolveLockContext call is positioned outside the withResult
wrapper, which allows resolution errors to escape instead of being caught and
returned as a failure Result. Move the resolveLockContext(this.lockContext) call
from before the withResult statement into the callback function that is passed
to withResult, so that any errors during lock context resolution are properly
wrapped and returned as { failure } according to the Result contract required by
the encryption operations guidelines.

Source: Coding guidelines

Comment on lines +151 to 153
const context = resolveLockContext(this.lockContext)

const result = await withResult(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep lock-context resolution inside the Result wrapper.

If resolving a backward-compatible LockContext fails, this currently rejects before withResult can convert it to { failure }. Move the resolution into the callback, matching the other migrated operations. As per coding guidelines, **/src/**/*.{ts,tsx} must preserve the Result contract shape { data } or { failure } returned by encryption operations.

Proposed fix
-    const context = resolveLockContext(this.lockContext)
-
     const result = await withResult(
       async () => {
         if (!this.client) throw noClientError()
+        const context = resolveLockContext(this.lockContext)

         const { metadata } = this.getAuditData()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const context = resolveLockContext(this.lockContext)
const result = await withResult(
const result = await withResult(
async () => {
if (!this.client) throw noClientError()
const context = resolveLockContext(this.lockContext)
const { metadata } = this.getAuditData()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack/src/encryption/operations/encrypt-query.ts` around lines 151 -
153, Move the resolveLockContext(this.lockContext) call inside the withResult
callback to ensure backward-compatible LockContext resolution errors are
properly caught and converted to the Result contract shape { failure }. The
context resolution should happen as the first step within the async callback
passed to withResult, matching the pattern used in other migrated encryption
operations in this codebase. This ensures all potential failures are wrapped in
the Result contract rather than rejecting before withResult can handle them.

Source: Coding guidelines

Comment on lines +66 to +67
* strategy: OidcFederationStrategy.create(region, workspaceId, () => getJwt()),
* },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

For @cipherstash/authv0.39.0, what is the exactOidcFederationStrategy.create(...)signature, and does it useworkspaceCrninstead ofregion/workspaceId?

💡 Result:

For @cipherstash/auth v0.39.0, the OidcFederationStrategy.create signature is: OidcFederationStrategy.create(workspaceCrn, getJwt) In this version, the strategy uses workspaceCrn instead of separate region and workspaceId parameters [1]. The getJwt parameter is an asynchronous function that returns the OIDC identity token (JWT) [1]. This change reflects a broader transition in CipherStash's SDKs to use the Workspace CRN (CipherStash Resource Name) as the primary identifier for workspace configuration, replacing the older combination of region and workspace ID [1].

Citations:


🏁 Script executed:

cat -n packages/stack/src/identity/index.ts | sed -n '60,75p'

Repository: cipherstash/stack

Length of output: 808


Update the OIDC example to use the current OidcFederationStrategy.create() signature.

The example at lines 66–67 uses the outdated OidcFederationStrategy.create(region, workspaceId, () => getJwt()) signature. In @cipherstash/auth v0.39.0+, this method accepts workspaceCrn as the first parameter instead of separate region and workspaceId arguments.

✏️ Suggested doc fix
-    strategy: OidcFederationStrategy.create(region, workspaceId, () => getJwt()),
+    strategy: OidcFederationStrategy.create(workspaceCrn, () => getJwt()),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
* strategy: OidcFederationStrategy.create(region, workspaceId, () => getJwt()),
* },
* strategy: OidcFederationStrategy.create(workspaceCrn, () => getJwt()),
* },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/stack/src/identity/index.ts` around lines 66 - 67, The OIDC example
using OidcFederationStrategy.create() at lines 66–67 uses the outdated signature
with separate region and workspaceId parameters. Update the example to use the
current signature where workspaceCrn is passed as the first parameter instead of
region and workspaceId, while keeping the callback function () => getJwt() as
the second parameter to match the `@cipherstash/auth` v0.39.0+ API.

@auxesis auxesis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test-coverage review — PR #497 (protect-ffi 0.26 / auth 0.39, strategy-based lock context)

Verdict: Strong test coverage for the core change. The new lock-context-wiring.test.ts exhaustively asserts that every operation forwards identityClaim and never re-introduces serviceToken, and init-strategy.test.ts covers config.strategy forwarding well. Two gaps are clearly worth closing: the PR's headline auth-strategy re-exports have no test guarding the deliberate ESM workaround, and the new central resolveLockContext branch has no direct unit test that distinguishes a constructed claim from the default.

No crypto/security concerns to flag — the change removes the per-operation CTS token entirely and moves auth onto the client strategy, which is a design decision reviewed elsewhere.

Additional coverage gaps not posted inline

  • packages/stack/src/identity/index.ts:187getLockContext()'s contract changed: the guard that threw when no CTS token was set has been removed, and ctsToken may now be undefined. No test asserts the new non-throwing behaviour. It's a deprecated method (low priority), but a cheap regression test would lock it down: const r = await new LockContext().getLockContext(); expect(r.failure).toBeUndefined(); expect(r.data.ctsToken).toBeUndefined() (with CS_WORKSPACE_CRN set).
  • packages/stack/src/wasm-inline.ts:374 — the WASM Encryption accessKey + workspaceCrn path now builds AccessKeyStrategy.create(cfg.workspaceCrn, …) (region derived from the CRN, replacing the old region field). This is only covered by the gated Deno e2e (e2e/wasm/roundtrip.test.ts), which skips without real CS_* secrets. There's no offline unit test — mirroring wasm-inline-normalize.test.ts — asserting the CRN reaches AccessKeyStrategy.create (or that strategy + accessKey together still throw).
  • packages/stack/__tests__/lock-context-wiring.test.ts — the plain { identityClaim } input (as opposed to a LockContext instance) is only asserted for encrypt/decrypt; the model/bulk/query operations are exercised only with a LockContext. Low risk since they all funnel through resolveLockContext, but the alternate-input axis is lopsided across the 11 operations.

// instance type — so this works under real Node ESM, not just the bundler.
import auth from '@cipherstash/auth'

export const AccessKeyStrategy = auth.AccessKeyStrategy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gap: The four newly re-exported auth strategies (AccessKeyStrategy, AutoStrategy, DeviceSessionStrategy, OidcFederationStrategy) — the PR's headline "no separate @cipherstash/auth install" feature — have no test asserting they resolve to defined values. The explicit default-import + per-name re-export is a deliberate ESM workaround (the comment above notes a naive export { X } from '@cipherstash/auth' resolves to undefined/throws under real Node ESM). If it ever regresses, the names silently become undefined and OidcFederationStrategy.create(...) blows up only at call time, with nothing catching it at build/test.

import auth from '@cipherstash/auth'
import { describe, expect, it } from 'vitest'
import * as stack from '@/index'

describe('@cipherstash/stack auth strategy re-exports', () => {
  it.each([
    'AccessKeyStrategy',
    'AutoStrategy',
    'DeviceSessionStrategy',
    'OidcFederationStrategy',
  ] as const)('re-exports %s as the real auth binding', (name) => {
    // biome-ignore lint/suspicious/noExplicitAny: dynamic key lookup
    const exported = (stack as any)[name]
    expect(exported, `${name} re-export is undefined`).toBeDefined()
    expect(typeof exported).toBe('function')
    // biome-ignore lint/suspicious/noExplicitAny: dynamic key lookup
    expect(exported).toBe((auth as any)[name])
  })
})

Expected: passes against the current explicit re-export; fails on toBeDefined() if it's swapped back to a plain export { … } from '@cipherstash/auth' that goes undefined under Node ESM. (init-strategy.test.ts already imports @/index, so the native @cipherstash/auth binding loads fine in this harness.)

* Resolve a {@link LockContextInput} to the {@link Context} (identity claim)
* that protect-ffi expects. Synchronous — no token round-trip.
*/
export function resolveLockContext(input: LockContextInput): Context {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gap: resolveLockContext is the new synchronous branch that replaced the old await getLockContext() flow in every operation, but it has no direct unit test. Both branches are only exercised indirectly via new LockContext(), whose context is the default { identityClaim: ['sub'] } — so a regression where the LockContext branch returns a hardcoded ['sub'] (instead of the claim actually constructed) would pass every existing test, including lock-context-wiring.test.ts.

import { beforeAll, describe, expect, it } from 'vitest'
import { LockContext, resolveLockContext } from '@/identity'

describe('resolveLockContext', () => {
  beforeAll(() => {
    // LockContext's constructor resolves the workspace id from the env.
    process.env.CS_WORKSPACE_CRN = 'crn:ap-southeast-2.aws:test-workspace'
  })

  it('returns a plain Context input unchanged', () => {
    const ctx = { identityClaim: ['email'] }
    expect(resolveLockContext(ctx)).toBe(ctx)
  })

  it('extracts the constructed claim from a LockContext (not the default)', () => {
    const lc = new LockContext({ context: { identityClaim: ['email'] } })
    expect(resolveLockContext(lc)).toEqual({ identityClaim: ['email'] })
  })
})

Expected: both pass today; the second fails if identityContext/resolveLockContext ever returns the default ['sub'] rather than the claim the LockContext was constructed with.

@auxesis auxesis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, thanks @coderdan.

I have left some comments about test coverage to be addressed before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants