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 BOTHprotectedPathsandpublicPaths— sincepublicPathsis checked first,publicPaths: ["/", ...](this SDK's own documented example, and a real consumer's live config) exposed the entire site unauthenticated.publicPathsnow matches EXACTLY (not by subtree) whileprotectedPathsmatches its whole subtree, and a"/x/*"/"/x/**"wildcard behaves differently in each: inprotectedPathsit matches/xand its descendants (fail-closed); inpublicPathsit matches ONLY strict descendants of/x, never/xitself — a config that wants/xpublic too must list it explicitly.loginPathis 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/billingbypassing a/acme/billingprotectedPaths 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/mefetch 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
bulwarkApiUrlin the middleware config — see the Next.js SDK reference. - The middleware is now async (
Promise<NextResponse>) — backward-compatible with Next.js, butawaitit in any custom wrapper.
To adopt: upgrade to @bulwarkauth/nextjs@^0.4.0 and ensure your middleware.ts
passes bulwarkApiUrl.