Repository navigation
config.authStrategy doesn't typecheck: protect-ffi's AuthStrategy type is narrower than its runtime (auth 0.41 Result envelope) #602
Description
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 inprotect-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/auth0.41's@byteslice/resultResult-shapedgetToken()—{ 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
packages/stack/__tests__/lock-context.test.ts— the only test that builds a realOidcFederationStrategy— early-returned whenUSER_JWTwas 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).packages/stack/__tests__/init-strategy.test.tshand-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.- Vitest doesn't typecheck, and
pnpm --filter @cipherstash/stack buildis already red on an unrelated pre-existing dts error (Type 'EncryptionError' does not satisfy the constraint 'FailureOption'), sotscoutput 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.
Activity
docs(stack): link the Node authStrategy caveat to issue #602
[-]config.authStrategy is broken on the Node entry: @cipherstash/auth 0.41 strategies don't satisfy protect-ffi's AuthStrategy[/-][+]config.authStrategy doesn't typecheck: protect-ffi's AuthStrategy type is narrower than its runtime (auth 0.41 Result envelope)[/+]coderdan commentedon Jul 9, 2026
Upstream fix opened: cipherstash/protectjs-ffi#131 — widens the exported
AuthStrategytype toPromise<TokenResult | TokenResultEnvelope>, corrects the WASMnewClientdoc comment, and adds type-level coverage. No behaviour change; the runtime has accepted both shapes since 0.28.0.Once released,
@cipherstash/stackneeds only a version bump — no adapter — and the workaround note inskills/stash-encryption/SKILL.mdcomes out.docs: split Authentication from Identity-Aware Encryption (lock conte…
coderdan commentedon Jul 15, 2026
Fixed by the
@cipherstash/protect-ffi0.29 bump: its exportedAuthStrategytype is now{ getToken: () => Promise<TokenResult | TokenResultEnvelope> }(lib/index.d.ts:348-350), which accepts the auth-0.41 Result-envelope shapeOidcFederationStrategy/AccessKeyStrategyreturn.config.authStrategy: strategy.datatypechecks with no cast — verified by compiling the documented example against the workspace. The 'Known type error' callouts in the skills/README are being removed in the v3 docs PR (#658). Closed-#602-session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94wdocs: address code-review findings on the v3-sole-approach restructure