Skip to content

docs: resolve protect-ffi's real types so the Stack reference stops saying any - #77

Merged
coderdan merged 1 commit into
v2from
docs/fix-typedoc-conditions
Jul 28, 2026
Merged

docs: resolve protect-ffi's real types so the Stack reference stops saying any#77
coderdan merged 1 commit into
v2from
docs/fix-typedoc-conditions

Conversation

@coderdan

Copy link
Copy Markdown
Contributor

Found while debugging why #76 fails CI on main. The build failure turned out to be the visible half of a bug that is also silently degrading the published Stack reference on v2.

The bug

@cipherstash/protect-ffi's exports["."] is conditional, and the fallback branch has no types:

{ "node": { "import":  { "types": "./lib/index.d.mts", "default": "./lib/index.mjs" },
            "require": { "types": "./lib/index.d.cts", "default": "./lib/index.cjs" } },
  "default": "./dist/wasm/protect_ffi.js" }

moduleResolution: "bundler" does not imply the node condition — you need customConditions: ["node"]. packages/stack/tsconfig.json sets exactly that, which is why the stack team never hits this. But typedoc.tsconfig.json extends stack's root tsconfig, which doesn't. So TypeScript misses the node branch, falls to default, finds no types, and resolves the sibling dist/wasm/protect_ffi.d.ts — raw wasm-bindgen output, where none of the hand-written types exist.

Hence the giveaway in the error text: "has no exported member EncryptedV3Query. Did you mean encryptQuery?"

Minimal repro against protect-ffi 0.30.0 — the version stack 1.0.0-rc.4 pins exactly, so this is not resolution drift:

tsconfig result
moduleResolution: bundler 3 errors, incl. ProtectError, EncryptedV3Query
+ customConditions: ["node"] clean, exit 0

Why this matters beyond the build

On v2 the errors were hidden by skipErrorChecking: true, whose comment said the references were "unresolved even though the source is correct" and that "TypeDoc still emits accurate signatures from the source AST".

That last part isn't true. An unresolved import doesn't just warn — TypeDoc emits any. So the workaround converted a loud build failure into a silently wrong reference. Generating with and without the fix back-to-back against the same stack tag changes 23 pages, including 13 anys:

page before after
EncryptionError.code any ProtectErrorCode
EncryptQueryOperation.execute() Promise<Result<any, …>> Promise<Result<EncryptedQueryResult, …>>
encryptQuery plaintext param any Plaintext | null | undefined
AuthStrategy description the module's blurb ("Public type re-exports for…") the type's own docs

So this is a docs-accuracy fix first and a build fix second.

Changes

  1. customConditions: ["node"] on the generated typedoc tsconfig — the actual fix.
  2. skipErrorChecking back to false. With the condition fixed the whole surface typechecks with 0 errors (down from 36), so the next resolution break fails the build instead of quietly publishing any.

Verified

bun run generate-docs → exit 0, Found 0 errors and 7 warnings, no error TS lines. biome check clean. The 23-page diff was produced by two back-to-back runs against the same stack tag, so it is attributable to this change and not to upstream movement.

Follow-ups, not in this PR

  • This also unblocks main. ci: open a PR when a new EQL patch release lands #76's red CI is pre-existing: pristine origin/main fails the same way (I checked it out clean and reproduced), because it never got skipErrorChecking: true and has no fix for the underlying cause. PR Docs V2: new information architecture (CIP-3307) #37 (v2 → main) will hit it the moment it leaves draft. main's lockfile is stale too — bun install --frozen-lockfile fails there.
  • The docs follow the latest @cipherstash/stack@ tag, unpinned (tagFilter in scripts/generate-docs.ts). That is precisely the hazard we pin EQL against: a stack release can redden the docs build with no commit here. Worth pinning the same way, now that docs: add an EQL pin resolver so patch bumps stop relying on memory #75 gives us the machinery.
  • protect-ffi 0.31.0 will break stack when it takes the bump, independently of this. Its exports map is byte-identical, so upgrading does not fix the above — and it drops ProtectError and QueryOpName from lib/index.d.cts. packages/stack/src/dynamodb/helpers.ts imports ProtectError as a value, and encryption/helpers/infer-index-type.ts imports QueryOpName. Looks like an unintended gap in the re-export barrel rather than a deliberate removal.
  • The real upstream fix would be for protect-ffi to give its default condition a types entry, so no consumer needs customConditions to see the package's own types.

https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8

…aying `any`

The generated Stack API reference documents 13 types as `any` and gives
AuthStrategy the wrong description entirely. Both come from one missing
compiler option.

@cipherstash/protect-ffi's `exports` map routes the `node` condition to
lib/index.d.cts (the real type surface) and everything else to
`default: ./dist/wasm/protect_ffi.js`, which ships no `types`. TypeScript
therefore falls through to the sibling dist/wasm/protect_ffi.d.ts — raw
wasm-bindgen output — where none of the hand-written types exist.

`moduleResolution: "bundler"` does not imply the `node` condition;
packages/stack/tsconfig.json adds `customConditions: ["node"]`, which is why
the stack team never sees this. But typedoc.tsconfig.json extends stack's ROOT
tsconfig, which does not set it. Set it on the generated config.

Confirmed with a minimal repro against protect-ffi 0.30.0 (the version stack
1.0.0-rc.4 pins exactly): `moduleResolution: bundler` alone yields
"has no exported member 'ProtectError'. Did you mean 'encryptQuery'?"; adding
customConditions typechecks clean.

Also turns error checking back on. It was disabled to tolerate this, on the
theory that the references were "unresolved even though the source is correct"
and TypeDoc would still emit accurate signatures. It does not — an unresolved
import becomes `any` in the output, so the workaround converted a loud failure
into a silently wrong reference. With the condition fixed the surface
typechecks with 0 errors, so the next resolution break fails the build instead.

Generating with and without the fix, back to back against the same stack tag,
changes 23 pages. Representative:

  EncryptionError.code            any -> ProtectErrorCode
  EncryptQueryOperation.execute() Promise<Result<any, ...>>
                                    -> Promise<Result<EncryptedQueryResult, ...>>
  encryptQuery plaintext param    any -> Plaintext | null | undefined
  AuthStrategy description        the module's blurb -> the type's own docs

Claude-Session: https://claude.ai/code/session_01NkuQNMvw9BpB4BWWeKtfV8
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
public-docs Ready Ready Preview, Comment Jul 28, 2026 6:31am

Request Review

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.

1 participant