feat!: per-flow PKCE verifier cookies + pure URL helpers (0.5.0)#27
feat!: per-flow PKCE verifier cookies + pure URL helpers (0.5.0)#27
Conversation
Design for porting the authkit-nextjs PR #403 fix (per-flow PKCE cookie names) to authkit-session and its downstream adapters (authkit-sveltekit, authkit-tanstack-start). Additive to the core's public API except for a deliberate breaking change to clearPendingVerifier, which is only called in-tree.
Fix stale line numbers in the evidence section. Resolve contradiction between CreateAuthorizationResult.cookieName and the clearPendingVerifier state-based signature. Add explicit adapter rule for missing-state cleanup (skip, let TTL handle orphans). Replace TanStack's static fallback delete headers with a state-derived variant. Call out SvelteKit test fixture updates and add static-fallback test case for TanStack.
Correct release versions against actual package.json state (session is already 0.4.0, sveltekit 0.2.0, tanstack 0.6.0). Acknowledge the clearPendingVerifier break as consumer-facing (it's in public README and MIGRATION docs) and add README/MIGRATION updates to the scope.
20-task plan across three phases (authkit-session 0.5.0 → authkit-sveltekit 0.3.0 → authkit-tanstack-react-start 0.7.0). Each task TDD-structured with exact code blocks, file paths, and commit messages.
Introduces PKCE_COOKIE_PREFIX and getPKCECookieNameForState(state), backed by an inline FNV-1a 32-bit hash. Zero new dependencies.
Adds cookieName to GeneratedAuthorizationUrl. Byte-length guard now measures the actual (longer) on-wire name. Still well under the 3800-byte cap (worst case 26 chars vs 17).
Addresses code-quality feedback on d248daa: oxlint flagged the unused import as an error. Also hoists the dynamic import in the new test to a static top-level import for consistency.
Callers that destructure the result now see the PKCE verifier cookie name that was written. Type-only change; runtime wiring lands next.
createAuthorization, createSignIn, createSignUp now write under the name returned by generateAuthorizationUrl. Concurrent flows no longer clobber each other's cookies.
handleCallback now reads and clears the flow-specific cookie identified by the URL state parameter. Removes the mirrorToLegacyName test helper that bridged Task 4's partial migration. Concurrent flows are fully isolated end-to-end: callback for flow A leaves flow B's cookie untouched.
getAuthorizationUrl, getSignInUrl, getSignUpUrl — same URL that createAuthorization/createSignIn/createSignUp produce, but no cookie write. Adapters with loop-prone paths (middleware hooks that fire on every request) can use these for non-document requests to avoid HTTP 431 cookie bloat.
BREAKING CHANGE: clearPendingVerifier now takes
{ state: string; redirectUri?: string } (state required). The old
state-less form had no meaning in the per-flow cookie world — there
is no single cookie name to clear without knowing which flow.
Callers on callback paths have URL state in hand. Bailouts with no
state should skip the call entirely; the 10-minute PKCE TTL handles
orphans.
Exposes PKCE_COOKIE_PREFIX and getPKCECookieNameForState for custom adapters. PKCE_COOKIE_NAME remains as back-compat alias.
Update README.md and MIGRATION.md to reflect the 0.5.0 signature change on clearPendingVerifier and introduce the new pure URL- generation methods.
Per-flow PKCE verifier cookies. See MIGRATION.md#050--per-flow-pkce-cookies and docs/superpowers/specs/2026-04-22-per-flow-pkce-cookies-design.md.
`createAuthService` returns a hand-rolled proxy cast to `AuthService`, which silently omitted `getAuthorizationUrl` / `getSignInUrl` / `getSignUpUrl`. Consumers saw them in autocomplete but hit `TypeError: not a function` at runtime. Add the forwarders and cover the factory surface with typeof checks + an end-to-end test.
Greptile SummaryThis PR replaces the single static The implementation is consistent end-to-end: Confidence Score: 5/5Safe to merge — no P0/P1 findings; all changed paths are covered by 215 passing tests including concurrent-flow isolation, tamper, missing-cookie, scheme-agnostic delete, and redirectUri-override scenarios. All remaining findings are at most P2. The hash derivation is correct (verified by known-answer vectors), the cookie read/write/clear paths are consistent, null-guarded absent-state paths are intentional, and the breaking changes are thoroughly documented. The prior concern about pure URL helpers is resolved by their removal. No files require special attention. Important Files Changed
Reviews (5): Last reviewed commit: "chore: drop PKCE_COOKIE_NAME and GetAuth..." | Re-trigger Greptile |
Remove `getAuthorizationUrl` / `getSignInUrl` / `getSignUpUrl`. The
pure variants were added on speculation — no concrete consumer exists,
and Greptile flagged a real footgun: a caller who uses them for a
document-request redirect gets a broken callback with no surfaced
`sealedState` to recover with. The "wasted cookie write on non-document
requests" motivation is micro-optimization that adopters can solve via
middleware hygiene (call `createSignIn` only on the actual sign-in
route) rather than a parallel API surface. Per-flow cookie naming and
the `clearPendingVerifier({ state })` breakage remain — those are the
load-bearing fixes in 0.5.0.
Both are dead weight after the 0.5.0 changes: - `PKCE_COOKIE_NAME` pointed at `'wos-auth-verifier'`, which no cookie on the wire has anymore. Nothing in `src/` uses it. Verified neither authkit-tanstack-start nor authkit-sveltekit imports it. - `GetAuthorizationUrlResult` was a one-field interface that only existed as a shared base for the pure URL helpers we reverted. Inlined into `CreateAuthorizationResult`. Per-flow cookie name is still derivable via `PKCE_COOKIE_PREFIX` + `getPKCECookieNameForState(state)`.
Summary
wos-auth-verifier-<fnv1a>, with suffix derived from the sealed state. Concurrent sign-ins from multiple tabs no longer clobber each other.clearPendingVerifier(response, { state })—stateis now required to identify the per-flow cookie. Guard the call onstatepresence; skip it when the callback URL is malformed (10-min TTL cleans up orphans).CreateAuthorizationResultgains a requiredcookieName: stringfield.PKCE_COOKIE_PREFIXandgetPKCECookieNameForStateare exported from the package root for cookie introspection.Why the hash is safe
FNV-1a 32-bit is a namespacing mechanism, not a security primitive. Integrity of the sealed blob comes from AEAD in
SessionEncryption, andverifyCallbackStatestill does a constant-time byte comparison of the full state. Collision probability is ~n²/2³² (vanishingly small even with 50 concurrent flows); a collision fails closed via state mismatch.Dropped before merge
An earlier draft added
getAuthorizationUrl/getSignInUrl/getSignUpUrl— pure variants that returned{ url, cookieName }without writing a cookie. Removed after review: no concrete consumer, and using them for a document-request redirect silently produces a broken callback with no surfacedsealedStateto recover with. The motivating "non-document requests in middleware" case is better solved by callingcreateSignInonly on the actual sign-in route. Can be reintroduced later with a better-signalled API if a real caller materializes.Test plan
vitest run— 215/215 passingtsc --noEmitcleancookieName.spec.tswith known-answer FNV-1a vectors (empty string,a,foobar)createSignIn/createSignUpclearPendingVerifiercall site before release