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
5 changes: 5 additions & 0 deletions .changeset/express-request-types.md
Original file line number Diff line number Diff line change
@@ -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`.
5 changes: 5 additions & 0 deletions .changeset/fastify-request-types.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions packages/express/src/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
5 changes: 0 additions & 5 deletions packages/express/src/types/express.d.ts

This file was deleted.

16 changes: 16 additions & 0 deletions packages/express/src/types/express.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>;
}
}
}

export {};
46 changes: 46 additions & 0 deletions packages/express/tests/consumerTypes.test.js
Original file line number Diff line number Diff line change
@@ -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,
);
});
18 changes: 18 additions & 0 deletions packages/express/tests/types/consumer.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> | undefined = req.cookiePayload;
res.json({ id, roles, payload });
},
);
1 change: 0 additions & 1 deletion packages/express/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"typeRoots": ["src/types/express.d.types"],
"declaration": true,
"emitDeclarationOnly": false,
"declarationMap": true
Expand Down
3 changes: 3 additions & 0 deletions packages/fastify/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 };
Expand Down
10 changes: 0 additions & 10 deletions packages/fastify/src/options.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
CookieSameSite,
SeamlessAuthMessagingOptions,
SeamlessAuthUser,
} from "@seamless-auth/core";

import type { ClientIpResolver } from "./internal/buildForwardedClientIp";
Expand Down Expand Up @@ -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<string, any>;
/** Set by `requireAuth`. */
user?: SeamlessAuthUser;
}
}
12 changes: 12 additions & 0 deletions packages/fastify/src/types/fastify.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>;
/** Set by `requireAuth`. */
user?: SeamlessAuthUser;
}
}

export {};
46 changes: 46 additions & 0 deletions packages/fastify/tests/consumerTypes.test.js
Original file line number Diff line number Diff line change
@@ -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,
);
});
22 changes: 22 additions & 0 deletions packages/fastify/tests/types/consumer.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> | undefined = req.cookiePayload;
return { id, roles, payload };
},
);
Loading