diff --git a/.changeset/express-request-types.md b/.changeset/express-request-types.md new file mode 100644 index 0000000..d8b0bff --- /dev/null +++ b/.changeset/express-request-types.md @@ -0,0 +1,5 @@ +--- +"@seamless-auth/express": patch +--- + +Ship the `Express.Request` augmentation. `req.user` (set by `requireAuth`) and `req.cookiePayload` (set by `createEnsureCookiesMiddleware`) now typecheck in a consumer that imports the package, without declaring the augmentation itself. It previously lived in an ambient `.d.ts` that tsc never copied into `dist`. diff --git a/.changeset/fastify-request-types.md b/.changeset/fastify-request-types.md new file mode 100644 index 0000000..b79fcb0 --- /dev/null +++ b/.changeset/fastify-request-types.md @@ -0,0 +1,5 @@ +--- +"@seamless-auth/fastify": patch +--- + +Load the `FastifyRequest` augmentation (`request.user`, `request.cookiePayload`) from the package entry through an import that resolves under `moduleResolution: "NodeNext"`. Consumers on NodeNext previously lost it because the entry reached it only through an extensionless relative import. diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts index aec4c36..4909086 100644 --- a/packages/express/src/index.ts +++ b/packages/express/src/index.ts @@ -1,3 +1,6 @@ +// Loads the request type augmentation for anyone importing the package. The +// .js extension keeps it resolvable under NodeNext as well as bundler. +import "./types/express.js"; import { createSeamlessAuthServer, SeamlessAuthServerOptions, diff --git a/packages/express/src/types/express.d.ts b/packages/express/src/types/express.d.ts deleted file mode 100644 index 84de5fa..0000000 --- a/packages/express/src/types/express.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -declare namespace Express { - interface Request { - user?: SeamlessAuthUser; - } -} diff --git a/packages/express/src/types/express.ts b/packages/express/src/types/express.ts new file mode 100644 index 0000000..01fe392 --- /dev/null +++ b/packages/express/src/types/express.ts @@ -0,0 +1,16 @@ +import type { SeamlessAuthUser } from "@seamless-auth/core"; + +// A module rather than an ambient .d.ts: tsc does not copy .d.ts sources into +// dist, so an ambient file never reached consumers. +declare global { + namespace Express { + interface Request { + /** Set by `requireAuth` once the access cookie or bearer token verifies. */ + user?: SeamlessAuthUser; + /** Set by `createEnsureCookiesMiddleware` once a session cookie verifies. */ + cookiePayload?: Record; + } + } +} + +export {}; diff --git a/packages/express/tests/consumerTypes.test.js b/packages/express/tests/consumerTypes.test.js new file mode 100644 index 0000000..fb1802f --- /dev/null +++ b/packages/express/tests/consumerTypes.test.js @@ -0,0 +1,46 @@ +// Type-checks a consumer against the built dist. Request augmentation that +// lives only in source (an ambient .d.ts tsc never emits) fails here. +import { execFileSync } from "node:child_process"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); +const tsc = require.resolve("typescript/bin/tsc"); +const fixture = fileURLToPath(new URL("./types/consumer.ts", import.meta.url)); + +const RESOLUTIONS = [ + { module: "ESNext", moduleResolution: "bundler" }, + { module: "NodeNext", moduleResolution: "NodeNext" }, +]; + +describe("@seamless-auth/express consumer types", () => { + it.each(RESOLUTIONS)( + "types req.user and req.cookiePayload under moduleResolution $moduleResolution", + ({ module, moduleResolution }) => { + let output = ""; + try { + execFileSync( + process.execPath, + [ + tsc, + "--noEmit", + "--strict", + "--skipLibCheck", + "--target", + "ES2022", + "--module", + module, + "--moduleResolution", + moduleResolution, + fixture, + ], + { encoding: "utf8", stdio: "pipe" }, + ); + } catch (err) { + output = `${err.stdout ?? ""}${err.stderr ?? ""}`; + } + expect(output).toBe(""); + }, + 60000, + ); +}); diff --git a/packages/express/tests/types/consumer.ts b/packages/express/tests/types/consumer.ts new file mode 100644 index 0000000..66355bf --- /dev/null +++ b/packages/express/tests/types/consumer.ts @@ -0,0 +1,18 @@ +// Resolves "@seamless-auth/express" by name through package.json `exports`, so +// it sees the built dist exactly as an installed consumer does. +import express from "express"; +import { requireAuth, requireRole } from "@seamless-auth/express"; + +const app = express(); + +app.get( + "/me", + requireAuth({ cookieSecret: "x".repeat(64) }), + requireRole("admin"), + (req, res) => { + const id: string | undefined = req.user?.id; + const roles: string[] | undefined = req.user?.roles; + const payload: Record | undefined = req.cookiePayload; + res.json({ id, roles, payload }); + }, +); diff --git a/packages/express/tsconfig.json b/packages/express/tsconfig.json index 7b4c8a6..1e68fb9 100644 --- a/packages/express/tsconfig.json +++ b/packages/express/tsconfig.json @@ -7,7 +7,6 @@ "esModuleInterop": true, "strict": true, "skipLibCheck": true, - "typeRoots": ["src/types/express.d.types"], "declaration": true, "emitDeclarationOnly": false, "declarationMap": true diff --git a/packages/fastify/src/index.ts b/packages/fastify/src/index.ts index a6f15c9..e05496a 100644 --- a/packages/fastify/src/index.ts +++ b/packages/fastify/src/index.ts @@ -1,3 +1,6 @@ +// Loads the request type augmentation for anyone importing the package. The +// .js extension keeps it resolvable under NodeNext as well as bundler. +import "./types/fastify.js"; import { seamlessAuth } from "./plugin"; export { seamlessAuth }; diff --git a/packages/fastify/src/options.ts b/packages/fastify/src/options.ts index 9c110d9..f376ba9 100644 --- a/packages/fastify/src/options.ts +++ b/packages/fastify/src/options.ts @@ -1,7 +1,6 @@ import type { CookieSameSite, SeamlessAuthMessagingOptions, - SeamlessAuthUser, } from "@seamless-auth/core"; import type { ClientIpResolver } from "./internal/buildForwardedClientIp"; @@ -54,12 +53,3 @@ export type ResolvedOptions = SeamlessAuthServerOptions & { refreshCookieName: string; preAuthCookieName: string; }; - -declare module "fastify" { - interface FastifyRequest { - /** Set by the plugin's cookie hook once a session cookie has been verified. */ - cookiePayload?: Record; - /** Set by `requireAuth`. */ - user?: SeamlessAuthUser; - } -} diff --git a/packages/fastify/src/types/fastify.ts b/packages/fastify/src/types/fastify.ts new file mode 100644 index 0000000..fde86c9 --- /dev/null +++ b/packages/fastify/src/types/fastify.ts @@ -0,0 +1,12 @@ +import type { SeamlessAuthUser } from "@seamless-auth/core"; + +declare module "fastify" { + interface FastifyRequest { + /** Set by the plugin's cookie hook once a session cookie has been verified. */ + cookiePayload?: Record; + /** Set by `requireAuth`. */ + user?: SeamlessAuthUser; + } +} + +export {}; diff --git a/packages/fastify/tests/consumerTypes.test.js b/packages/fastify/tests/consumerTypes.test.js new file mode 100644 index 0000000..37bf74b --- /dev/null +++ b/packages/fastify/tests/consumerTypes.test.js @@ -0,0 +1,46 @@ +// Type-checks a consumer against the built dist. Request augmentation that +// lives only in source (an ambient .d.ts tsc never emits) fails here. +import { execFileSync } from "node:child_process"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(import.meta.url); +const tsc = require.resolve("typescript/bin/tsc"); +const fixture = fileURLToPath(new URL("./types/consumer.ts", import.meta.url)); + +const RESOLUTIONS = [ + { module: "ESNext", moduleResolution: "bundler" }, + { module: "NodeNext", moduleResolution: "NodeNext" }, +]; + +describe("@seamless-auth/fastify consumer types", () => { + it.each(RESOLUTIONS)( + "types req.user and req.cookiePayload under moduleResolution $moduleResolution", + ({ module, moduleResolution }) => { + let output = ""; + try { + execFileSync( + process.execPath, + [ + tsc, + "--noEmit", + "--strict", + "--skipLibCheck", + "--target", + "ES2022", + "--module", + module, + "--moduleResolution", + moduleResolution, + fixture, + ], + { encoding: "utf8", stdio: "pipe" }, + ); + } catch (err) { + output = `${err.stdout ?? ""}${err.stderr ?? ""}`; + } + expect(output).toBe(""); + }, + 60000, + ); +}); diff --git a/packages/fastify/tests/types/consumer.ts b/packages/fastify/tests/types/consumer.ts new file mode 100644 index 0000000..1264795 --- /dev/null +++ b/packages/fastify/tests/types/consumer.ts @@ -0,0 +1,22 @@ +// Resolves "@seamless-auth/fastify" by name through package.json `exports`, so +// it sees the built dist exactly as an installed consumer does. +import Fastify from "fastify"; +import { requireAuth, requireRole } from "@seamless-auth/fastify"; + +const app = Fastify(); + +app.get( + "/me", + { + preHandler: [ + requireAuth({ cookieSecret: "x".repeat(64) }), + requireRole("admin"), + ], + }, + async (req) => { + const id: string | undefined = req.user?.id; + const roles: string[] | undefined = req.user?.roles; + const payload: Record | undefined = req.cookiePayload; + return { id, roles, payload }; + }, +);