Changelog

Notable changes to the Bulwark SDKs and platform, newest first. SDK packages follow Semantic Versioning.


@bulwarkauth/nestjs 0.3.0 — 2026-09-28

BREAKING: default token validation now verifies signatures. BulwarkService.validateToken() no longer trusts unsigned claims by default — when useIntrospection is unset/false (the default), it used to do a LOCAL DECODE with NO signature check (only exp was verified), so any caller could forge a token with an arbitrary sub/roles and have it accepted by the default BulwarkAuthGuard. Every access token is now verified offline against Bulwark's published JWKS (EdDSA/Ed25519 only — none, HMAC, RSA, and ECDSA are all rejected before any key lookup), plus exp/nbf/iss/tenant-binding (and aud when configured).

Bulwark's JWKS is a single instance-wide key set, not scoped per tenant, so validateToken now also requires the token's tid/tenant_id claim to be in an allowlist — defaulting to [tenant] (correct for isolated-mode apps); shared-isolation-mode apps must set allowedTenantIds explicitly to their parent workspace id, or every token is rejected. A JWKS outage throws the new BulwarkVerificationUnavailableError (mapped to 503 by BulwarkAuthGuard) rather than looking like every token happening to be invalid.

Migration: most apps need no code change — just confirm your tenant id (or, for shared-isolation apps, set allowedTenantIds to the parent workspace id) and, if you can't finish that immediately, set dangerouslySkipSignatureVerification: true as a temporary bridge (never a permanent setting). See the @bulwarkauth/[email protected] on npm for the package README and version history (the GitLab source repo is private).


@bulwarkauth/nextjs 0.5.0 — 2026-09-28

BREAKING (security): every access token is now verified against Bulwark's JWKS instead of being locally decoded unverified. createBulwarkMiddleware's page gate, withBulwarkAuth, and the Server Component helpers (getSession, getUser, auth, requireSession) all used to accept a JWT via an unverified local decode — any forged token with a future exp and an arbitrary sub/roles/tenant_id was accepted. All four now verify the EdDSA/Ed25519 signature offline via a Web-Crypto-based verifier (works in both the Next.js Edge Runtime and Node ≥20).

Also in this release, across several rounds of review:

  • bulwarkApiUrl (or its env-var fallback) is now REQUIRED, with NO hosted-default fallback — an unconfigured self-hosted deployment used to have its users' refresh/access tokens silently sent to Bulwark's SaaS origin instead of its own server.
  • A tenant allowlist is now REQUIRED (one way or another) unless explicitly opted out, since Bulwark's JWKS is instance-wide, not tenant-scoped.
  • The path matcher (protectedPaths/publicPaths/loginPath) went through several rounds of fixes; this is the FINAL behavior. First, "/" used to mean "match every route" in BOTH protectedPaths and publicPaths — since publicPaths is checked first, publicPaths: ["/", ...] (this SDK's own documented example, and a real consumer's live config) exposed the entire site unauthenticated. publicPaths now matches EXACTLY (not by subtree) while protectedPaths matches its whole subtree, and a "/x/*"/"/x/**" wildcard behaves differently in each: in protectedPaths it matches /x and its descendants (fail-closed); in publicPaths it matches ONLY strict descendants of /x, never /x itself — a config that wants /x public too must list it explicitly. loginPath is now always implicitly public. A route is matched against BOTH the raw (still-encoded) pathname and a per-segment-decoded one — never the decoded one alone — closing an encoded-segment escape (/%61cme/billing bypassing a /acme/billing protectedPaths pattern) without letting an encoded ALIAS of a public path (/lo%67in, which Next.js routes to a completely different dynamic page than the static /login) be wrongly treated as public. A malformed %-escape fails closed (treated as protected). Every pattern must now be a literal, plain-ASCII path — no %, non-ASCII characters or spaces, ./.. segments, backslashes, parentheses, or a leading : in a segment, and * only as a trailing /*//** wildcard suffix — so a pattern that looks like glob, regex, or Next.js's own matcher syntax (e.g. /dashboard/:path*, /dashboard(.*)) throws at construction instead of being silently accepted as a dead, never-matching literal string. See the Next.js SDK reference for the complete, current matching rules.
  • The refresh fetch, the JWKS fetch, and currentUser()'s /me fetch now all refuse to follow a redirect (redirect: "manual") — a 3xx used to be silently followed, which could retarget signature verification or a refresh-token POST to an attacker-controlled origin.
  • Middleware config (including path lists, roles, and env-derived fallbacks) is validated and snapshotted entirely at construction, from a defensive copy immune to Symbol.species/Proxy tricks on the arrays passed in — a misconfigured (or adversarially-shaped) deployment now fails at module-evaluation time instead of on the first real request, or matching against values that were never actually validated.

Migration: see the @bulwarkauth/[email protected] on npm for the package README (the GitLab source repo is private) — most deployments only need to add BULWARK_TENANT_ID and NEXT_PUBLIC_BULWARK_API_URL/BULWARK_API_URL as environment variables, review any publicPaths entries that assumed "/" meant "everything" or that a plain pattern covered a subtree, and confirm next is ^14.2.25 or >=15.2.3 (CVE-2025-29927).


@bulwarkauth/nextjs 0.4.1 — 2026-06-05

Fixed — installable in downstream pnpm monorepos. 0.4.0 was published in a way that left pnpm's workspace:* protocol in the package's peerDependencies, which could fail to resolve on a clean --frozen-lockfile install under pnpm's isolated linker (surfacing as Module not found: '@bulwarkauth/nextjs' in a consumer build). Republished with real version ranges. No behavior change from 0.4.0 — if 0.4.0 installed fine for you, 0.4.1 is identical; if it didn't, upgrade to ^0.4.1.


@bulwarkauth/nextjs 0.4.0 — 2026-06-05

Fixed — sessions no longer expire at the access-token TTL.

The Next.js middleware previously redirected to the login page as soon as the short-lived access token (~15 min) lapsed, ignoring the longer-lived refresh token. On hard navigations, SSR, and idle-then-return this capped every session at the access-token lifetime even though the refresh/session TTL is 7 days.

createBulwarkMiddleware now silently refreshes the session: when the bulwark_token cookie is missing or expired it exchanges the bulwark_refresh cookie at POST {bulwarkApiUrl}/api/v1/auth/refresh, writes the rotated token pair back onto the response, and lets the request continue. It only redirects when no valid session can be recovered (and clears stale cookies when it does).

  • Requires bulwarkApiUrl in the middleware config — see the Next.js SDK reference.
  • The middleware is now async (Promise<NextResponse>) — backward-compatible with Next.js, but await it in any custom wrapper.

To adopt: upgrade to @bulwarkauth/nextjs@^0.4.0 and ensure your middleware.ts passes bulwarkApiUrl.