Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions packages/rust-auth/src/nextjs/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
* @layer nextjs-server
*/

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// The explicit `.js` extension keeps `next/server` resolvable when the built package is
// externalized and loaded by Node's native ESM resolver (see the note in `proxy.ts`): `next`
// ships no `exports` map, so Node ESM cannot resolve the extensionless subpath.
import { NextResponse } from "next/server.js";
import type { NextRequest } from "next/server.js";

import {
AUTH_ACCESS_COOKIE_NAME,
Expand Down
26 changes: 24 additions & 2 deletions packages/rust-auth/src/nextjs/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// browser bundle. The `server-only` import makes a Client-Component import a build error.
import "server-only";

import { decode_jwt, verify_jwt_hs256 } from "../../wasm/bymax_auth_wasm.js";
import type {
DashboardJwtPayload,
MfaTempPayload,
Expand All @@ -13,9 +12,30 @@ import type {
* @fileoverview Edge JWT helpers backed by the WASM verifier. `verifyJwtToken` runs the exact
* Rust HS256 codec the backend signs with (authoritative); `decodeJwtToken` and the
* decode-only fallback never check a signature and must never gate an authorization decision.
*
* The WASM glue self-initializes on import (its top-level `__wbindgen_start`), so it is loaded
* lazily via a memoized dynamic `import()` on first use rather than at module load: importing
* this module — or the `/nextjs` barrel — must have NO WASM side effect, so a Next build's
* page-data collection (which cannot instantiate the edge WASM) can evaluate the barrel.
* @layer nextjs-server
*/

/** The edge codec surface this module consumes from the bundled `bymax-auth-wasm` glue. */
type EdgeWasm = typeof import("../../wasm/bymax_auth_wasm.js");

/** The memoized in-flight (then resolved) WASM import; `undefined` until first use. */
let edgeWasm: Promise<EdgeWasm> | undefined;

/**
* Load the edge WASM codec lazily and at most once. The dynamic `import()` defers the glue's
* self-initialization to first use and caches the module namespace, so repeated calls share a
* single wasm-init instance and importing this module stays side-effect-free.
*/
function loadEdgeWasm(): Promise<EdgeWasm> {
edgeWasm ??= import("../../wasm/bymax_auth_wasm.js");
return edgeWasm;
}

/** The three claim shapes the backend issues, discriminated by their `type` field. */
export type AuthJwtPayload = DashboardJwtPayload | PlatformJwtPayload | MfaTempPayload;

Expand Down Expand Up @@ -55,8 +75,9 @@ interface DecodedHeaderPayload {
* @param token - The compact JWS to decode.
* @returns `{ isValid: true, header, payload }` when decodable, else `{ isValid: false }`.
*/
export function decodeJwtToken(token: string): DecodedToken {
export async function decodeJwtToken(token: string): Promise<DecodedToken> {
try {
const { decode_jwt } = await loadEdgeWasm();
const raw = decode_jwt(token);
Comment thread
msalvatti marked this conversation as resolved.
if (raw === undefined) return { isValid: false };
const { header, payload } = JSON.parse(raw) as DecodedHeaderPayload;
Expand All @@ -82,6 +103,7 @@ export async function verifyJwtToken(
secret?: string | null,
): Promise<DecodedToken> {
try {
const { decode_jwt, verify_jwt_hs256 } = await loadEdgeWasm();
if (typeof secret === "string" && secret.length > 0) {
const raw = verify_jwt_hs256(token, secret);
if (raw === undefined) return { isValid: false };
Expand Down
9 changes: 7 additions & 2 deletions packages/rust-auth/src/nextjs/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
* @layer nextjs-server
*/

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
// Imported with the explicit `.js` extension: when this package is installed by a Next 16
// consumer and externalized (`serverExternalPackages`), the built `dist/nextjs` is loaded by
// Node's native ESM resolver, which cannot resolve the bare subpath `next/server` because
// `next` ships no `exports` map (extensionless subpaths require a fully-specified path). The
// `.js` specifier resolves identically under bundlers and Vitest.
import { NextResponse } from "next/server.js";
import type { NextRequest } from "next/server.js";

import {
AUTH_ACCESS_COOKIE_NAME,
Expand Down
10 changes: 5 additions & 5 deletions packages/rust-auth/tests/nextjs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,16 @@ describe("verifyJwtToken — decode-only fallback is non-authoritative", () => {
});

describe("decodeJwtToken", () => {
it("returns the header and payload without verifying the signature", () => {
const decoded = decodeJwtToken(dashboardToken());
it("returns the header and payload without verifying the signature", async () => {
const decoded = await decodeJwtToken(dashboardToken());
expect(decoded.isValid).toBe(true);
expect(decoded.header?.alg).toBe("HS256");
expect(getUserId(decoded)).toBe("u_1");
});

it("returns { isValid: false } for a malformed token and never throws", () => {
expect(decodeJwtToken("not-a-token").isValid).toBe(false);
expect(getTenantId(decodeJwtToken("not-a-token"))).toBeUndefined();
it("returns { isValid: false } for a malformed token and never throws", async () => {
expect((await decodeJwtToken("not-a-token")).isValid).toBe(false);
expect(getTenantId(await decodeJwtToken("not-a-token"))).toBeUndefined();
});
Comment thread
msalvatti marked this conversation as resolved.
});

Expand Down
1 change: 1 addition & 0 deletions packages/rust-auth/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default defineConfig({
"react-dom",
"next",
"next/server",
"next/server.js",
"server-only",
/^\.\.\/wasm\//,
/bymax_auth_wasm/,
Expand Down
Loading