Skip to content

config.authStrategy doesn't typecheck: protect-ffi's AuthStrategy type is narrower than its runtime (auth 0.41 Result envelope) #602

Description

@coderdan

Corrected. The original version of this issue claimed the Node runtime fails and that the bug was in @cipherstash/stack. Both were wrong — see the correction at the bottom. The runtime is fine; this is a types-only bug, and it lives in protect-ffi.

Summary

Assigning an @cipherstash/auth 0.41 strategy (OidcFederationStrategy, AccessKeyStrategy) to config.authStrategy does not typecheck, on either entry:

error TS2322: Type 'OidcFederationStrategy' is not assignable to type 'AuthStrategy'.
  The types returned by 'getToken()' are incompatible between these types.
    Type 'Promise<Result<TokenResult, AuthFailure>>' is not assignable to type 'Promise<{ token: string; }>'.
      Property 'token' is missing in type 'Failure<AuthFailure>' but required in type '{ token: string; }'.

(Reproduced under packages/stack/tsconfig.json, which sets customConditions: ["node"].)

It works at runtime. Only the type is wrong.

Root cause — protect-ffi's exported type is narrower than its runtime

@cipherstash/auth 0.41 (index.d.ts:134):

getToken(): Promise<Result<TokenResult, AuthFailure>>;

@cipherstash/protect-ffi 0.28 (src/index.cts:461):

export type AuthStrategy = {
  getToken: () => Promise<{ token: string }>
}

But protect-ffi 0.28's runtime accepts both shapes, on Node and WASM alike. From its CHANGELOG under [0.28.0]:

Support for @cipherstash/auth 0.41's @byteslice/result Result-shaped getToken(){ data: { token, … } } on success, { failure: { type, error, … } } on error — on both the Node (Neon) and WASM auth paths. The bare { token } shape … is still accepted, so this is backward compatible.

The Neon implementation is at crates/protect-ffi/src/lib.rs:537-575: it reads failure, unwraps data when present, and otherwise falls back to a top-level token.

The TypeScript declaration simply wasn't widened with it. protect-ffi's own integration tests pin @cipherstash/auth ^0.39.0 (integration-tests/package.json:13) — a pre-Result version with a different 3-arg create() — so oidc-federation.test.ts's const asStrategy: AuthStrategy = strategy still compiles there and nothing exercises the 0.41 shape against the type.

Impact

@cipherstash/stack re-exports the auth strategies and types ClientConfig.authStrategy as protect-ffi's AuthStrategy. Any user following the documented identity-aware encryption path gets a type error, even though the code runs correctly.

Interim workaround: authStrategy: strategy.data as unknown as AuthStrategy.

Fix

Upstream in protect-ffi — widen the exported type to the contract the runtime already implements, e.g.:

type TokenResult = { token: string }
type AuthResult =
  | { data: TokenResult; failure?: never }
  | { failure: { type: string; error: Error }; data?: never }

export type AuthStrategy = {
  getToken: () => Promise<TokenResult | AuthResult>
}

Also worth fixing there: the WASM doc comment (crates/protect-ffi/src/wasm.rs:259) still says getToken(): Promise<{ token: string, ... }>, which flows into dist/wasm/protect_ffi.d.ts.

Once released, @cipherstash/stack just needs the version bump — no adapter, no code change — and the caveat in skills/stash-encryption/SKILL.md comes out.

Why nothing caught it

  1. packages/stack/__tests__/lock-context.test.ts — the only test that builds a real OidcFederationStrategy — early-returned when USER_JWT was absent, reporting four passing assertions that never executed. Fixed in docs(stack): refresh the stash-encryption skill; fix the identity-aware auth docs and a vacuously-passing test #598 (skipIf).
  2. packages/stack/__tests__/init-strategy.test.ts hand-rolls a { token } stub, so it stands in for the FFI's declared contract rather than for a real auth strategy. It cannot see the mismatch.
  3. Vitest doesn't typecheck, and pnpm --filter @cipherstash/stack build is already red on an unrelated pre-existing dts error (Type 'EncryptionError' does not satisfy the constraint 'FailureOption'), so tsc output isn't being watched.

Correction to the original report

The first version of this issue asserted that the Node runtime fails with missing 'token' field, and that the bug was Stack's for forwarding the strategy unadapted.

That was inferred from strings on the 0.28 NAPI binary, where missing 'token' field and 'token' field is not a string are still present. That inference was unsound: those strings persist for the legacy bare-{token} path. Diffing 0.27 against 0.28 for strings unique to the new code path gives the opposite answer:

string 0.27 0.28
reading 'failure' field threw absent present
reading 'data' field threw absent present

protect-ffi is open source; the Rust was one clone away and says plainly that both shapes are handled. Reading it would have been faster and correct.

Context

Found while refreshing the stash-encryption skill (#598), which documents config.authStrategy as the current identity-aware path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions