remix-utils
Advanced tools
Comparing version
import type { Promisable } from "type-fest"; | ||
type Origin = boolean | string | RegExp | Array<string | RegExp>; | ||
interface CORSOptions { | ||
/** | ||
* Configures the **Access-Control-Allow-Origin** CORS header. | ||
* | ||
* Possible values: | ||
* - true: Enable CORS for any origin (same as "*") | ||
* - false: Don't setup CORS | ||
* - string: Set to a specific origin, if set to "*" it will allow any origin | ||
* - RegExp: Set to a RegExp to match against the origin | ||
* - Array<string | RegExp>: Set to an array of origins to match against the | ||
* string or RegExp | ||
* - Function: Set to a function that will be called with the request origin | ||
* and should return a boolean indicating if the origin is allowed or not. | ||
* @default true | ||
*/ | ||
origin?: Origin | ((origin: string) => Promisable<Origin>); | ||
/** | ||
* Configures the **Access-Control-Allow-Methods** CORS header. | ||
* @default ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"] | ||
*/ | ||
methods?: Array<string>; | ||
/** | ||
* Configures the **Access-Control-Allow-Headers** CORS header. | ||
* @default [] | ||
*/ | ||
allowedHeaders?: string[]; | ||
/** | ||
* Configures the **Access-Control-Expose-Headers** CORS header. | ||
* @default [] | ||
*/ | ||
exposedHeaders?: string[]; | ||
/** | ||
* Configures the **Access-Control-Allow-Credentials** CORS header. | ||
* @default false | ||
*/ | ||
credentials?: boolean; | ||
/** | ||
* Configures the **Access-Control-Max-Age** CORS header. | ||
* @default 0 | ||
*/ | ||
maxAge?: number; | ||
/** | ||
* @private You should not use this class directly, use the `cors` function instead, or the `unstable_createCorsMiddleware` | ||
*/ | ||
export declare class CORS { | ||
private options; | ||
constructor(options: cors.Options); | ||
exec(request: Request, response: Response): Promise<Response>; | ||
private resolveOrigin; | ||
private configureMaxAge; | ||
private configureExposedHeaders; | ||
private configureAllowedHeaders; | ||
private configureCredentials; | ||
private configureMethods; | ||
private configureOrigin; | ||
private isOriginAllowed; | ||
private isString; | ||
private isNumber; | ||
} | ||
@@ -100,3 +75,47 @@ /** | ||
*/ | ||
export declare function cors(request: Request, response: Response, options?: CORSOptions): Promise<Response>; | ||
export {}; | ||
export declare function cors(request: Request, response: Response, options?: cors.Options): Promise<Response>; | ||
export declare namespace cors { | ||
type Origin = boolean | string | RegExp | Array<string | RegExp>; | ||
interface Options { | ||
/** | ||
* Configures the **Access-Control-Allow-Origin** CORS header. | ||
* | ||
* Possible values: | ||
* - true: Enable CORS for any origin (same as "*") | ||
* - false: Don't setup CORS | ||
* - string: Set to a specific origin, if set to "*" it will allow any origin | ||
* - RegExp: Set to a RegExp to match against the origin | ||
* - Array<string | RegExp>: Set to an array of origins to match against the | ||
* string or RegExp | ||
* - Function: Set to a function that will be called with the request origin | ||
* and should return a boolean indicating if the origin is allowed or not. | ||
* @default true | ||
*/ | ||
origin?: Origin | ((origin: string) => Promisable<Origin>); | ||
/** | ||
* Configures the **Access-Control-Allow-Methods** CORS header. | ||
* @default ["GET", "HEAD", "PUT", "PATCH", "POST", "DELETE"] | ||
*/ | ||
methods?: Array<string>; | ||
/** | ||
* Configures the **Access-Control-Allow-Headers** CORS header. | ||
* @default [] | ||
*/ | ||
allowedHeaders?: string[]; | ||
/** | ||
* Configures the **Access-Control-Expose-Headers** CORS header. | ||
* @default [] | ||
*/ | ||
exposedHeaders?: string[]; | ||
/** | ||
* Configures the **Access-Control-Allow-Credentials** CORS header. | ||
* @default false | ||
*/ | ||
credentials?: boolean; | ||
/** | ||
* Configures the **Access-Control-Max-Age** CORS header. | ||
* @default 0 | ||
*/ | ||
maxAge?: number; | ||
} | ||
} |
@@ -7,3 +7,6 @@ const DEFAULT_OPTIONS = { | ||
}; | ||
class CORS { | ||
/** | ||
* @private You should not use this class directly, use the `cors` function instead, or the `unstable_createCorsMiddleware` | ||
*/ | ||
export class CORS { | ||
options; | ||
@@ -10,0 +13,0 @@ constructor(options) { |
@@ -0,1 +1,130 @@ | ||
/** | ||
*> This depends on `@oslojs/crypto`, and `@oslojs/encoding`. | ||
* | ||
* The Basic Auth middleware let's you add a basic authentication to your | ||
* routes, this can be useful to protect routes that need to be private. | ||
* | ||
* > **Warning**: Basic Auth is not secure by itself, it should be used with | ||
* > HTTPS to ensure the username and password are encrypted. Do not use it to | ||
* > protect sensitive data, use a more secure method instead. | ||
* | ||
* ```ts | ||
* import { unstable_createBasicAuthMiddleware } from "remix-utils/middleware/basic-auth"; | ||
* | ||
* export const [basicAuthMiddleware] = unstable_createBasicAuthMiddleware({ | ||
* user: { username: "admin", password: "password" }, | ||
* }); | ||
* ``` | ||
* | ||
* To use it, you need to add it to the `unstable_middleware` array in the | ||
* route where you want to use it. | ||
* | ||
* ```ts | ||
* import { basicAuthMiddleware } from "~/middleware/basic-auth.server"; | ||
* export const unstable_middleware = [basicAuthMiddleware]; | ||
* ``` | ||
* | ||
* Now, when you access the route you will be prompted to enter the username and password. | ||
* | ||
* The `realm` option let's you set the realm for the authentication, this is the name of the protected area. | ||
* | ||
* ```ts | ||
* import { unstable_createBasicAuthMiddleware } from "remix-utils/middleware/basic-auth"; | ||
* | ||
* export const [basicAuthMiddleware] = unstable_createBasicAuthMiddleware({ | ||
* realm: "My Realm", | ||
* user: { username: "admin", password: "password" }, | ||
* }); | ||
* ``` | ||
* | ||
* The `user` option let's you set the username and password to authenticate, | ||
* you can also pass an array of users. | ||
* | ||
* ```ts | ||
* import { unstable_createBasicAuthMiddleware } from "remix-utils/middleware/basic-auth"; | ||
* | ||
* export const [basicAuthMiddleware] = unstable_createBasicAuthMiddleware({ | ||
* user: [ | ||
* { username: "admin", password: "password" }, | ||
* { username: "user", password: "password" }, | ||
* ], | ||
* }); | ||
* ``` | ||
* | ||
* The `verifyUser` option let's you pass a function to verify the user, this | ||
* can be useful to check the user against a database. | ||
* | ||
* ```ts | ||
* import { unstable_createBasicAuthMiddleware } from "remix-utils/middleware/basic-auth"; | ||
* | ||
* export const [basicAuthMiddleware] = unstable_createBasicAuthMiddleware({ | ||
* verifyUser(username, password) { | ||
* let user = await getUser(username); | ||
* if (!user) return false; | ||
* return await verifyPassword(password, user.password); | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* The `verifyUser` function should return `true` if the user is authenticated, | ||
* and `false` otherwise. | ||
* | ||
* In case of an invalid username or password the middleware will return a | ||
* `401` status code with a `WWW-Authenticate` header. | ||
* | ||
* ```http | ||
* HTTP/1.1 401 Unauthorized | ||
* WWW-Authenticate: Basic realm="My Realm" | ||
* | ||
* Unauthorized | ||
* ``` | ||
* | ||
* The `invalidUserMessage` option let's you customize the message sent when | ||
* the user is invalid. | ||
* | ||
* ```ts | ||
* import { unstable_createBasicAuthMiddleware } from "remix-utils/middleware/basic-auth"; | ||
* | ||
* export const [basicAuthMiddleware] = unstable_createBasicAuthMiddleware({ | ||
* invalidUserMessage: "Invalid username or password", | ||
* user: { username: "admin", password: "password" }, | ||
* }); | ||
* ``` | ||
* | ||
* And this will be the response when the user is invalid. | ||
* | ||
* ```http | ||
* HTTP/1.1 401 Unauthorized | ||
* WWW-Authenticate: Basic realm="My Realm" | ||
* | ||
* Invalid username or password | ||
* ``` | ||
* | ||
* You can also customize the `invalidUserMessage` by passing a function which | ||
* will receive the Request and context objects. | ||
* | ||
* ```ts | ||
* import { unstable_createBasicAuthMiddleware } from "remix-utils/middleware/basic-auth"; | ||
* | ||
* export const [basicAuthMiddleware] = unstable_createBasicAuthMiddleware({ | ||
* invalidUserMessage({ request, context }) { | ||
* // do something with request or context here | ||
* return { message: `Invalid username or password for ${username}` }; | ||
* }, | ||
* user: { username: "admin", password: "password" }, | ||
* }); | ||
* ``` | ||
* | ||
* In both cases, with a hard-coded value or a function, the invalid message | ||
* can be a string or an object, if it's an object it will be converted to JSON. | ||
* | ||
* ```http | ||
* HTTP/1.1 401 Unauthorized | ||
* WWW-Authenticate: Basic realm="My Realm" | ||
* | ||
* {"message":"Invalid username or password"} | ||
* ``` | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Basic Auth | ||
*/ | ||
import { sha256 } from "@oslojs/crypto/sha2"; | ||
@@ -2,0 +131,0 @@ import { decodeBase64 } from "@oslojs/encoding"; |
@@ -0,1 +1,64 @@ | ||
/** | ||
* > This depends on `@edgefirst-dev/batcher`. | ||
* | ||
* The batcher middleware let's you get a per request instance of a batcher object that will dedupe and batch multiple calls to the same function. | ||
* | ||
* This is specially useful to avoid making multiple API calls to the same endpoint in a single request, or DB queries. The batcher will call the function only once and return the same result to all calls. | ||
* | ||
* ```ts | ||
* import { unstable_createBatcherMiddleware } from "remix-utils/middleware/batcher"; | ||
* | ||
* export const [batcherMiddleware, getBatcher] = | ||
* unstable_createBatcherMiddleware(); | ||
* ``` | ||
* | ||
* To use it, you need to add it to the `unstable_middleware` array in the route where you want to use it. | ||
* | ||
* ```ts | ||
* import { batcherMiddleware } from "~/middleware/batcher.server"; | ||
* export const unstable_middleware = [batcherMiddleware]; | ||
* ``` | ||
* | ||
* And you can use the `getBatcher` function in your loaders to get the batcher object. | ||
* | ||
* ```ts | ||
* import { getBatcher } from "~/middleware/batcher.server"; | ||
* | ||
* export async function loader({ context }: LoaderFunctionArgs) { | ||
* let batcher = getBatcher(context); | ||
* let result = await batcher.batch("key", async () => { | ||
* return await getData(); | ||
* }); | ||
* // ... | ||
* } | ||
* ``` | ||
* | ||
* If you move your `batcher.batch` call to a separate function, you can use it in different route loaders and actions, and the batcher will still dedupe the calls. | ||
* | ||
* ```ts | ||
* import type { Batcher } from "remix-utils/middleware/batcher"; | ||
* import { getData } from "~/data"; | ||
* | ||
* export function getDataBatched(batcher: Batcher) { | ||
* return batcher.batch("key", async () => { | ||
* return await getData(); | ||
* }); | ||
* } | ||
* ``` | ||
* | ||
* Then you can call it in any route loader who has access to the batcher. | ||
* | ||
* ```ts | ||
* import { getBatcher } from "~/middleware/batcher.server"; | ||
* import { getDataBatched } from "~/data"; | ||
* | ||
* export async function loader({ context }: LoaderFunctionArgs) { | ||
* let batcher = getBatcher(context); | ||
* let result = await getDataBatched(batcher); | ||
* // ... | ||
* } | ||
* ``` | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Batcher | ||
*/ | ||
import { Batcher } from "@edgefirst-dev/batcher"; | ||
@@ -2,0 +65,0 @@ import { unstable_createSingletonMiddleware } from "./singleton.js"; |
@@ -0,1 +1,64 @@ | ||
/** | ||
* > This depends on `@edgefirst-dev/batcher`. | ||
* | ||
* The batcher middleware let's you get a per request instance of a batcher object that will dedupe and batch multiple calls to the same function. | ||
* | ||
* This is specially useful to avoid making multiple API calls to the same endpoint in a single request, or DB queries. The batcher will call the function only once and return the same result to all calls. | ||
* | ||
* ```ts | ||
* import { unstable_createBatcherMiddleware } from "remix-utils/middleware/batcher"; | ||
* | ||
* export const [batcherMiddleware, getBatcher] = | ||
* unstable_createBatcherMiddleware(); | ||
* ``` | ||
* | ||
* To use it, you need to add it to the `unstable_middleware` array in the route where you want to use it. | ||
* | ||
* ```ts | ||
* import { batcherMiddleware } from "~/middleware/batcher.server"; | ||
* export const unstable_middleware = [batcherMiddleware]; | ||
* ``` | ||
* | ||
* And you can use the `getBatcher` function in your loaders to get the batcher object. | ||
* | ||
* ```ts | ||
* import { getBatcher } from "~/middleware/batcher.server"; | ||
* | ||
* export async function loader({ context }: LoaderFunctionArgs) { | ||
* let batcher = getBatcher(context); | ||
* let result = await batcher.batch("key", async () => { | ||
* return await getData(); | ||
* }); | ||
* // ... | ||
* } | ||
* ``` | ||
* | ||
* If you move your `batcher.batch` call to a separate function, you can use it in different route loaders and actions, and the batcher will still dedupe the calls. | ||
* | ||
* ```ts | ||
* import type { Batcher } from "remix-utils/middleware/batcher"; | ||
* import { getData } from "~/data"; | ||
* | ||
* export function getDataBatched(batcher: Batcher) { | ||
* return batcher.batch("key", async () => { | ||
* return await getData(); | ||
* }); | ||
* } | ||
* ``` | ||
* | ||
* Then you can call it in any route loader who has access to the batcher. | ||
* | ||
* ```ts | ||
* import { getBatcher } from "~/middleware/batcher.server"; | ||
* import { getDataBatched } from "~/data"; | ||
* | ||
* export async function loader({ context }: LoaderFunctionArgs) { | ||
* let batcher = getBatcher(context); | ||
* let result = await getDataBatched(batcher); | ||
* // ... | ||
* } | ||
* ``` | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Batcher | ||
*/ | ||
import { Batcher } from "@edgefirst-dev/batcher"; | ||
@@ -2,0 +65,0 @@ import { unstable_createSingletonMiddleware } from "./singleton.js"; |
@@ -0,1 +1,66 @@ | ||
/** | ||
* The Context Storage middleware stores the Router context provider and request in AsyncLocalStorage and gives you functions to access it in your code. | ||
* | ||
* ```ts | ||
* import { unstable_createContextStorageMiddleware } from "remix-utils/middleware/context-storage"; | ||
* | ||
* export const [contextStorageMiddleware, getContext, getRequest] = | ||
* unstable_createContextStorageMiddleware(); | ||
* ``` | ||
* | ||
* To use it, you need to add it to the `unstable_middleware` array in your `app/root.tsx` file. | ||
* | ||
* ```ts | ||
* import { contextStorageMiddleware } from "~/middleware/context-storage.server"; | ||
* | ||
* export const unstable_middleware = [contextStorageMiddleware]; | ||
* ``` | ||
* | ||
* And you can use the `getContext` and `getRequest` functions in your function to get the context and request objects. | ||
* | ||
* ```ts | ||
* import { getContext, getRequest } from "~/middleware/context-storage.server"; | ||
* | ||
* export async function doSomething() { | ||
* let context = getContext(); | ||
* let request = getRequest(); | ||
* // ... | ||
* } | ||
* ``` | ||
* | ||
* Then call `doSomething` in any loader, action, or another middleware, and you will have access to the context and request objects without passing them around. | ||
* | ||
* You can pair this with any other middleware that uses the context to simplify using their returned getters. | ||
* | ||
* ```ts | ||
* import { unstable_createBatcherMiddleware } from "remix-utils/middleware/batcher"; | ||
* import { getContext } from "~/middleware/context-storage.server"; | ||
* | ||
* const [batcherMiddleware, getBatcherFromContext] = | ||
* unstable_createBatcherMiddleware(); | ||
* | ||
* export { bathcherMiddleware }; | ||
* | ||
* export function getBatcher() { | ||
* let context = getContext(); | ||
* return getBatcherFromContext(context); | ||
* } | ||
* ``` | ||
* | ||
* Now instead of calling `getBatcher(context)` you can just call `getBatcher()` and it will return the batcher instance. | ||
* | ||
* ```ts | ||
* import { getBatcher } from "~/middleware/batcher.server"; | ||
* | ||
* export async function loader(_: LoaderFunctionArgs) { | ||
* let batcher = getBatcher(); | ||
* let result = await batcher.batch("key", async () => { | ||
* return await getData(); | ||
* }); | ||
* // ... | ||
* } | ||
* ``` | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Context Storage | ||
*/ | ||
import { AsyncLocalStorage } from "node:async_hooks"; | ||
@@ -2,0 +67,0 @@ const storage = new AsyncLocalStorage(); |
@@ -0,1 +1,97 @@ | ||
/** | ||
* > This depends on `react`, `@oslojs/crypto`, and `@oslojs/encoding`. | ||
* | ||
* The Honeypot middleware allows you to add a honeypot mechanism to your routes, providing a simple yet effective way to protect public forms from spam bots. | ||
* | ||
* To use the Honeypot middleware, first import and configure it: | ||
* | ||
* ```ts | ||
* import { unstable_createHoneypotMiddleware } from "remix-utils/middleware/honeypot"; | ||
* | ||
* export const [honeypotMiddleware, getHoneypotInputProps] = | ||
* unstable_createHoneypotMiddleware({ | ||
* // Randomize the honeypot field name | ||
* randomizeNameFieldName: false, | ||
* // Default honeypot field name | ||
* nameFieldName: "name__confirm", | ||
* // Optional timestamp field for validation | ||
* validFromFieldName: "from__confirm", | ||
* // Unique seed for encryption (recommended for extra security) | ||
* encryptionSeed: undefined, | ||
* | ||
* onSpam(error) { | ||
* // Handle SpamError here and return a Response | ||
* return new Response("Spam detected", { status: 400 }); | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* Add the `honeypotMiddleware` to the `unstable_middleware` array in the route where you want to enable spam protection, use it in your `app/root.tsx` file to apply it globally: | ||
* | ||
* ```ts | ||
* import { honeypotMiddleware } from "~/middleware/honeypot"; | ||
* | ||
* export const unstable_middleware = [honeypotMiddleware]; | ||
* ``` | ||
* | ||
* Use the `getHoneypotInputProps` function in your root loader to retrieve the honeypot input properties: | ||
* | ||
* ```ts | ||
* import { getHoneypotInputProps } from "~/middleware/honeypot"; | ||
* | ||
* export async function loader({ request }: LoaderFunctionArgs) { | ||
* let honeypotInputProps = await getHoneypotInputProps(); | ||
* return json({ honeypotInputProps }); | ||
* } | ||
* ``` | ||
* | ||
* Wrap your application in the `HoneypotProvider` component to make the honeypot input properties available throughout your app: | ||
* | ||
* ```tsx | ||
* import { HoneypotProvider } from "remix-utils/honeypot/react"; | ||
* | ||
* export default function RootComponent() { | ||
* return ( | ||
* <HoneypotProvider {...honeypotInputProps}> | ||
* <Outlet /> | ||
* </HoneypotProvider> | ||
* ); | ||
* } | ||
* ``` | ||
* | ||
* In any public form, include the `HoneypotInputs` component to add the honeypot fields: | ||
* | ||
* ```tsx | ||
* import { HoneypotInputs } from "remix-utils/honeypot/react"; | ||
* | ||
* function PublicForm() { | ||
* return ( | ||
* <Form method="post"> | ||
* <HoneypotInputs label="Please leave this field blank" /> | ||
* <input type="text" name="name" placeholder="Your Name" /> | ||
* <input type="email" name="email" placeholder="Your Email" /> | ||
* <button type="submit">Submit</button> | ||
* </Form> | ||
* ); | ||
* } | ||
* ``` | ||
* | ||
* For requests with a body (e.g., POST, PUT, DELETE) and a content type of `application/x-www-form-urlencoded` or `multipart/form-data`, the middleware validates the honeypot fields. If the request passes the honeypot check, it proceeds to the action handler. If the honeypot check fails, the `onSpam` handler is invoked, allowing you to handle spam requests appropriately. | ||
* | ||
* In your action handlers, you can process the form data as usual, without worrying about spam checks—they are already handled by the middleware: | ||
* | ||
* ```ts | ||
* export async function action({ request }: ActionFunctionArgs) { | ||
* // If this code runs, the honeypot check passed | ||
* let formData = await request.formData(); | ||
* let name = formData.get("name"); | ||
* let email = formData.get("email"); | ||
* // Process the form data | ||
* } | ||
* ``` | ||
* | ||
* The honeypot middleware is designed to be lightweight and effective against basic spam bots. For advanced spam protection, consider combining this with other techniques like CAPTCHA or rate limiting. | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Honeypot | ||
*/ | ||
import type { unstable_MiddlewareFunction } from "react-router"; | ||
@@ -2,0 +98,0 @@ import type { HoneypotConfig, HoneypotInputProps } from "../honeypot.js"; |
@@ -0,1 +1,152 @@ | ||
/** | ||
* > This depends on `@edgefirst-dev/jwt`. | ||
* | ||
* The JWK Auth middleware let's you add a JSON Web Key authentication to your routes, this can be useful to protect routes that need to be private and will be accessed by other services. | ||
* | ||
* > **Warning**: JWK Auth is more secure than Basic Auth, but it should be used with HTTPS to ensure the token is encrypted. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* | ||
* export const [jwkAuthMiddleware, getJWTPayload] = | ||
* unstable_createJWKAuthMiddleware({ | ||
* jwksUri: "https://auth.example.com/.well-known/jwks.json", | ||
* }); | ||
* ``` | ||
* | ||
* The `jwksUri` option let's you set the URL to the JWKS endpoint, this is the URL where the public keys are stored. | ||
* | ||
* To use the middleware, you need to add it to the `unstable_middleware` array in the route where you want to use it. | ||
* | ||
* ```ts | ||
* import { jwkAuthMiddleware } from "~/middleware/jwk-auth"; | ||
* export const unstable_middleware = [jwkAuthMiddleware]; | ||
* ``` | ||
* | ||
* Now, when you access the route it will check the JWT token in the `Authorization` header. | ||
* | ||
* In case of an invalid token the middleware will return a `401` status code with a `WWW-Authenticate` header. | ||
* | ||
* ```http | ||
* HTTP/1.1 401 Unauthorized | ||
* WWW-Authenticate: Bearer realm="Secure Area" | ||
* | ||
* Unauthorized | ||
* ``` | ||
* | ||
* The `realm` option let's you set the realm for the authentication, this is the name of the protected area. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* | ||
* export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({ | ||
* realm: "My Realm", | ||
* jwksUri: "https://auth.example.com/.well-known/jwks.json", | ||
* }); | ||
* ``` | ||
* | ||
* If you want to customize the message sent when the token is invalid you can use the `invalidTokenMessage` option. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* | ||
* export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({ | ||
* invalidTokenMessage: "Invalid token", | ||
* jwksUri: "https://auth.example.com/.well-known/jwks.json", | ||
* }); | ||
* ``` | ||
* | ||
* And this will be the response when the token is invalid. | ||
* | ||
* ```http | ||
* HTTP/1.1 401 Unauthorized | ||
* WWW-Authenticate: Bearer realm="Secure Area" | ||
* | ||
* Invalid token | ||
* ``` | ||
* | ||
* You can also customize the `invalidTokenMessage` by passing a function which will receive the Request and context objects. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* | ||
* export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({ | ||
* invalidTokenMessage({ request, context }) { | ||
* // do something with request or context here | ||
* return { message: `Invalid token` }; | ||
* }, | ||
* jwksUri: "https://auth.example.com/.well-known/jwks.json", | ||
* }); | ||
* ``` | ||
* | ||
* In both cases, with a hard-coded value or a function, the invalid message can be a string or an object, if it's an object it will be converted to JSON. | ||
* | ||
* ```http | ||
* HTTP/1.1 401 Unauthorized | ||
* WWW-Authenticate: Bearer realm="Secure Area" | ||
* | ||
* {"message":"Invalid token"} | ||
* ``` | ||
* | ||
* If you want to get the JWT payload in your loaders, actions, or other middleware you can use the `getJWTPayload` function. | ||
* | ||
* ```ts | ||
* import { getJWTPayload } from "~/middleware/jwk-auth.server"; | ||
* | ||
* export async function loader({ request }: LoaderFunctionArgs) { | ||
* let payload = getJWTPayload(); | ||
* // ... | ||
* } | ||
* ``` | ||
* | ||
* And you can use the payload to get the subject, scope, issuer, audience, or any other information stored in the token. | ||
* | ||
* ## With a Custom Header | ||
* | ||
* If your app receives the JWT in a custom header instead of the `Authorization` header you can tell the middleware to look for the token in that header. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* | ||
* export const [jwkAuthMiddleware, getJWTPayload] = | ||
* unstable_createJWKAuthMiddleware({ header: "X-API-Key" }); | ||
* ``` | ||
* | ||
* Now use the middleware as usual, but now instead of looking for the token in the `Authorization` header it will look for it in the `X-API-Key` header. | ||
* | ||
* ```ts | ||
* import { jwkAuthMiddleware } from "~/middleware/jwk-auth"; | ||
* | ||
* export const unstable_middleware = [jwkAuthMiddleware]; | ||
* ``` | ||
* | ||
* ## With a Cookie | ||
* | ||
* If you save a JWT in a cookie using React Router's Cookie API, you can tell the middleware to look for the token in the cookie instead of the `Authorization` header. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* import { createCookie } from "react-router"; | ||
* | ||
* export const cookie = createCookie("jwt", { | ||
* path: "/", | ||
* sameSite: "lax", | ||
* httpOnly: true, | ||
* secure: process.env.NODE_ENV === "true", | ||
* }); | ||
* | ||
* export const [jwkAuthMiddleware, getJWTPayload] = | ||
* unstable_createJWKAuthMiddleware({ cookie }); | ||
* ``` | ||
* | ||
* Then use the middleware as usual, but now instead of looking for the token in the `Authorization` header it will look for it in the cookie. | ||
* | ||
* ```ts | ||
* import { jwkAuthMiddleware } from "~/middleware/jwk-auth"; | ||
* | ||
* export const unstable_middleware = [jwkAuthMiddleware]; | ||
* ``` | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/JWK Auth | ||
*/ | ||
import { JWK, JWT } from "@edgefirst-dev/jwt"; | ||
@@ -2,0 +153,0 @@ import { type Cookie, type unstable_MiddlewareFunction, unstable_RouterContextProvider } from "react-router"; |
@@ -0,1 +1,152 @@ | ||
/** | ||
* > This depends on `@edgefirst-dev/jwt`. | ||
* | ||
* The JWK Auth middleware let's you add a JSON Web Key authentication to your routes, this can be useful to protect routes that need to be private and will be accessed by other services. | ||
* | ||
* > **Warning**: JWK Auth is more secure than Basic Auth, but it should be used with HTTPS to ensure the token is encrypted. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* | ||
* export const [jwkAuthMiddleware, getJWTPayload] = | ||
* unstable_createJWKAuthMiddleware({ | ||
* jwksUri: "https://auth.example.com/.well-known/jwks.json", | ||
* }); | ||
* ``` | ||
* | ||
* The `jwksUri` option let's you set the URL to the JWKS endpoint, this is the URL where the public keys are stored. | ||
* | ||
* To use the middleware, you need to add it to the `unstable_middleware` array in the route where you want to use it. | ||
* | ||
* ```ts | ||
* import { jwkAuthMiddleware } from "~/middleware/jwk-auth"; | ||
* export const unstable_middleware = [jwkAuthMiddleware]; | ||
* ``` | ||
* | ||
* Now, when you access the route it will check the JWT token in the `Authorization` header. | ||
* | ||
* In case of an invalid token the middleware will return a `401` status code with a `WWW-Authenticate` header. | ||
* | ||
* ```http | ||
* HTTP/1.1 401 Unauthorized | ||
* WWW-Authenticate: Bearer realm="Secure Area" | ||
* | ||
* Unauthorized | ||
* ``` | ||
* | ||
* The `realm` option let's you set the realm for the authentication, this is the name of the protected area. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* | ||
* export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({ | ||
* realm: "My Realm", | ||
* jwksUri: "https://auth.example.com/.well-known/jwks.json", | ||
* }); | ||
* ``` | ||
* | ||
* If you want to customize the message sent when the token is invalid you can use the `invalidTokenMessage` option. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* | ||
* export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({ | ||
* invalidTokenMessage: "Invalid token", | ||
* jwksUri: "https://auth.example.com/.well-known/jwks.json", | ||
* }); | ||
* ``` | ||
* | ||
* And this will be the response when the token is invalid. | ||
* | ||
* ```http | ||
* HTTP/1.1 401 Unauthorized | ||
* WWW-Authenticate: Bearer realm="Secure Area" | ||
* | ||
* Invalid token | ||
* ``` | ||
* | ||
* You can also customize the `invalidTokenMessage` by passing a function which will receive the Request and context objects. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* | ||
* export const [jwkAuthMiddleware] = unstable_createJWKAuthMiddleware({ | ||
* invalidTokenMessage({ request, context }) { | ||
* // do something with request or context here | ||
* return { message: `Invalid token` }; | ||
* }, | ||
* jwksUri: "https://auth.example.com/.well-known/jwks.json", | ||
* }); | ||
* ``` | ||
* | ||
* In both cases, with a hard-coded value or a function, the invalid message can be a string or an object, if it's an object it will be converted to JSON. | ||
* | ||
* ```http | ||
* HTTP/1.1 401 Unauthorized | ||
* WWW-Authenticate: Bearer realm="Secure Area" | ||
* | ||
* {"message":"Invalid token"} | ||
* ``` | ||
* | ||
* If you want to get the JWT payload in your loaders, actions, or other middleware you can use the `getJWTPayload` function. | ||
* | ||
* ```ts | ||
* import { getJWTPayload } from "~/middleware/jwk-auth.server"; | ||
* | ||
* export async function loader({ request }: LoaderFunctionArgs) { | ||
* let payload = getJWTPayload(); | ||
* // ... | ||
* } | ||
* ``` | ||
* | ||
* And you can use the payload to get the subject, scope, issuer, audience, or any other information stored in the token. | ||
* | ||
* ## With a Custom Header | ||
* | ||
* If your app receives the JWT in a custom header instead of the `Authorization` header you can tell the middleware to look for the token in that header. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* | ||
* export const [jwkAuthMiddleware, getJWTPayload] = | ||
* unstable_createJWKAuthMiddleware({ header: "X-API-Key" }); | ||
* ``` | ||
* | ||
* Now use the middleware as usual, but now instead of looking for the token in the `Authorization` header it will look for it in the `X-API-Key` header. | ||
* | ||
* ```ts | ||
* import { jwkAuthMiddleware } from "~/middleware/jwk-auth"; | ||
* | ||
* export const unstable_middleware = [jwkAuthMiddleware]; | ||
* ``` | ||
* | ||
* ## With a Cookie | ||
* | ||
* If you save a JWT in a cookie using React Router's Cookie API, you can tell the middleware to look for the token in the cookie instead of the `Authorization` header. | ||
* | ||
* ```ts | ||
* import { unstable_createJWKAuthMiddleware } from "remix-utils/middleware/jwk-auth"; | ||
* import { createCookie } from "react-router"; | ||
* | ||
* export const cookie = createCookie("jwt", { | ||
* path: "/", | ||
* sameSite: "lax", | ||
* httpOnly: true, | ||
* secure: process.env.NODE_ENV === "true", | ||
* }); | ||
* | ||
* export const [jwkAuthMiddleware, getJWTPayload] = | ||
* unstable_createJWKAuthMiddleware({ cookie }); | ||
* ``` | ||
* | ||
* Then use the middleware as usual, but now instead of looking for the token in the `Authorization` header it will look for it in the cookie. | ||
* | ||
* ```ts | ||
* import { jwkAuthMiddleware } from "~/middleware/jwk-auth"; | ||
* | ||
* export const unstable_middleware = [jwkAuthMiddleware]; | ||
* ``` | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/JWK Auth | ||
*/ | ||
import { JWK, JWT } from "@edgefirst-dev/jwt"; | ||
@@ -2,0 +153,0 @@ import { unstable_RouterContextProvider, unstable_createContext, } from "react-router"; |
@@ -0,1 +1,41 @@ | ||
/** | ||
* The logger middleware let's you log the request and response information to | ||
* the console, this can be useful to debug issues with the request and response. | ||
* | ||
* ```ts | ||
* import { unstable_createLoggerMiddleware } from "remix-utils/middleware/logger"; | ||
* | ||
* export const [loggerMiddleware] = unstable_createLoggerMiddleware(); | ||
* ``` | ||
* | ||
* To use it, you need to add it to the `unstable_middleware` array in your | ||
* `app/root.tsx` file. | ||
* | ||
* ```ts | ||
* import { loggerMiddleware } from "~/middleware/logger.server"; | ||
* export const unstable_middleware = [loggerMiddleware]; | ||
* ``` | ||
* | ||
* Now, every request and response will be logged to the console. | ||
* | ||
* The logger middleware can be customized by passing an options object to the | ||
* `unstable_createLoggerMiddleware` function. | ||
* | ||
* ```ts | ||
* let [loggerMiddleware] = unstable_createLoggerMiddleware({ | ||
* logger: console, | ||
* precision: 2, | ||
* formatMessage(request, response, time) { | ||
* return `${request.method} ${request.url} - ${response.status} - ${time}ms`; | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* The `logger` option let's you pass a custom logger, the `precision` option | ||
* let's you set the number of decimal places to use in the response time, and | ||
* the `formatMessage` option let's you customize the message that will be | ||
* logged. | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Logger | ||
*/ | ||
import type { unstable_MiddlewareFunction } from "react-router"; | ||
@@ -2,0 +42,0 @@ export declare function unstable_createLoggerMiddleware({ logger, precision, formatMessage, }?: unstable_createLoggerMiddleware.Options): unstable_createLoggerMiddleware.ReturnType; |
@@ -0,1 +1,58 @@ | ||
/** | ||
* The Request ID middleware generates a unique ID for each request and stores it in the Router context, this can be useful to log the request and response information and correlate them. | ||
* | ||
* ```ts | ||
* import { unstable_createRequestIDMiddleware } from "remix-utils/middleware/request-id"; | ||
* | ||
* export const [requestIDMiddleware, getRequestID] = | ||
* unstable_createRequestIDMiddleware(); | ||
* ``` | ||
* | ||
* To use it, you need to add it to the `unstable_middleware` array in your `app/root.tsx` file. | ||
* | ||
* ```ts | ||
* import { requestIDMiddleware } from "~/middleware/request-id.server"; | ||
* | ||
* export const unstable_middleware = [requestIDMiddleware]; | ||
* ``` | ||
* | ||
* And you can use the `getRequestID` function in your loaders, actions, and other middleware to get the request ID. | ||
* | ||
* ```ts | ||
* import { getRequestID } from "~/middleware/request-id.server"; | ||
* | ||
* export async function loader({ request }: LoaderFunctionArgs) { | ||
* let requestID = getRequestID(); | ||
* // ... | ||
* } | ||
* ``` | ||
* | ||
* By default the request ID is a UUID, but you can customize it by passing a function to the `unstable_createRequestIDMiddleware` function. | ||
* | ||
* ```ts | ||
* import { unstable_createRequestIDMiddleware } from "remix-utils/middleware/request-id"; | ||
* | ||
* export const [requestIDMiddleware, getRequestID] = | ||
* unstable_createRequestIDMiddleware({ | ||
* generator() { | ||
* return Math.random().toString(36).slice(2); | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* The middleware also gets the request ID from the `X-Request-ID` header if it's present, this can be useful to correlate requests between services. | ||
* | ||
* If you want to use a different header you can pass the header name to the `unstable_createRequestIDMiddleware` function. | ||
* | ||
* ```ts | ||
* import { unstable_createRequestIDMiddleware } from "remix-utils/middleware/request-id"; | ||
* | ||
* export const [requestIDMiddleware, getRequestID] = | ||
* unstable_createRequestIDMiddleware({ | ||
* header: "X-Correlation-ID", | ||
* }); | ||
* ``` | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Request ID | ||
*/ | ||
import type { unstable_MiddlewareFunction } from "react-router"; | ||
@@ -2,0 +59,0 @@ import type { unstable_MiddlewareGetter } from "./utils.js"; |
@@ -0,1 +1,27 @@ | ||
/** | ||
* The secure headers middleware simplifies the setup of security headers. Inspired in part by the version from Hono `secureHeaders` middleware. | ||
* | ||
* ```ts | ||
* import { unstable_createSecureHeadersMiddleware } from "remix-utils/middleware/secure-headers"; | ||
* | ||
* export const [secureHeadersMiddleware] = | ||
* unstable_createSecureHeadersMiddleware(); | ||
* ``` | ||
* | ||
* To use it, you need to add it to the `unstable_middleware` array in your `app/root.tsx` file. | ||
* | ||
* ```ts | ||
* import { secureHeadersMiddleware } from "~/middleware/secure-headers.server"; | ||
* export const unstable_middleware = [secureHeadersMiddleware]; | ||
* ``` | ||
* | ||
* Now, every response will have the security header responses. | ||
* | ||
* The secure headers middleware middleware can be customized by passing an options object to the `unstable_createSecureHeadersMiddleware` function. | ||
* | ||
* The options let's you configure the headers key values. The middleware accepts the same options as the Hono Secure Headers Middleware. | ||
* @author [Floryan Simar](https://github.com/TheYoxy) | ||
* @module Middleware/Secure Headers | ||
* @see {@link https://hono.dev/docs/middleware/builtin/secure-headers | Hono Secure Headers Middleware} | ||
*/ | ||
import type { unstable_MiddlewareFunction } from "react-router"; | ||
@@ -2,0 +28,0 @@ /** |
@@ -0,1 +1,37 @@ | ||
/** | ||
* > This depends on `@edgefirst-dev/server-timing`. | ||
* The server timing middleware let's you add a `Server-Timing` header to the response with the time it took to run the loaders and actions. | ||
* | ||
* ```ts | ||
* import { unstable_createServerTimingMiddleware } from "remix-utils/middleware/server-timing"; | ||
* | ||
* export const [serverTimingMiddleware, getTimingCollector] = | ||
* unstable_createServerTimingMiddleware(); | ||
* ``` | ||
* | ||
* To use it, you need to add it to the `unstable_middleware` array in your `app/root.tsx` file. | ||
* | ||
* ```ts | ||
* import { serverTimingMiddleware } from "~/middleware/server-timing.server"; | ||
* | ||
* export const unstable_middleware = [serverTimingMiddleware]; | ||
* ``` | ||
* | ||
* And you can use the `getTimingCollector` function in your loaders and actions to add timings to the response. | ||
* | ||
* ```ts | ||
* import { getTimingCollector } from "~/middleware/server-timing.server"; | ||
* | ||
* export async function loader({ request }: LoaderFunctionArgs) { | ||
* let collector = getTimingCollector(); | ||
* return await collector.measure("name", "optional description", async () => { | ||
* return await getData(); | ||
* }); | ||
* } | ||
* ``` | ||
* | ||
* The `measure` function will measure the time it took to run the function passed as the last argument and add it to the `Server-Timing` header. | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Server Timing | ||
*/ | ||
import { TimingCollector } from "@edgefirst-dev/server-timing"; | ||
@@ -2,0 +38,0 @@ import type { unstable_MiddlewareFunction } from "react-router"; |
@@ -0,1 +1,37 @@ | ||
/** | ||
* > This depends on `@edgefirst-dev/server-timing`. | ||
* The server timing middleware let's you add a `Server-Timing` header to the response with the time it took to run the loaders and actions. | ||
* | ||
* ```ts | ||
* import { unstable_createServerTimingMiddleware } from "remix-utils/middleware/server-timing"; | ||
* | ||
* export const [serverTimingMiddleware, getTimingCollector] = | ||
* unstable_createServerTimingMiddleware(); | ||
* ``` | ||
* | ||
* To use it, you need to add it to the `unstable_middleware` array in your `app/root.tsx` file. | ||
* | ||
* ```ts | ||
* import { serverTimingMiddleware } from "~/middleware/server-timing.server"; | ||
* | ||
* export const unstable_middleware = [serverTimingMiddleware]; | ||
* ``` | ||
* | ||
* And you can use the `getTimingCollector` function in your loaders and actions to add timings to the response. | ||
* | ||
* ```ts | ||
* import { getTimingCollector } from "~/middleware/server-timing.server"; | ||
* | ||
* export async function loader({ request }: LoaderFunctionArgs) { | ||
* let collector = getTimingCollector(); | ||
* return await collector.measure("name", "optional description", async () => { | ||
* return await getData(); | ||
* }); | ||
* } | ||
* ``` | ||
* | ||
* The `measure` function will measure the time it took to run the function passed as the last argument and add it to the `Server-Timing` header. | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Server Timing | ||
*/ | ||
import { TimingCollector } from "@edgefirst-dev/server-timing"; | ||
@@ -2,0 +38,0 @@ import { unstable_createContext } from "react-router"; |
@@ -0,1 +1,88 @@ | ||
/** | ||
* The session middleware let's you save a session object in the Router context | ||
* so you can access it in any loader and ensure you're always working with the | ||
* same Session instance. | ||
* | ||
* ```ts | ||
* import { unstable_createSessionMiddleware } from "remix-utils/middleware/session"; | ||
* ``` | ||
* | ||
* To use it, you need to create a session storage object and pass it to the | ||
* middleware. | ||
* | ||
* ```ts | ||
* import { createCookieSessionStorage } from "react-router"; | ||
* | ||
* let sessionStorage = createCookieSessionStorage({ | ||
* cookie: createCookie("session", { path: "/", sameSite: "lax" }), | ||
* }); | ||
* | ||
* let [sessionMiddleware, getSession] = | ||
* unstable_createSessionMiddleware(sessionStorage); | ||
* ``` | ||
* | ||
* Then you can use the `sessionMiddleware` in your `app/root.tsx` function. | ||
* | ||
* ```ts | ||
* import { sessionMiddleware } from "~/middleware/session.server"; | ||
* | ||
* export const unstable_middleware = [sessionMiddleware]; | ||
* ``` | ||
* | ||
* And you can use the `getSession` function in your loaders to get the session | ||
* object. | ||
* | ||
* ```ts | ||
* import { getSession } from "~/middleware/session.server"; | ||
* | ||
* export async function loader({ context }: Route.LoaderArgs) { | ||
* let session = await getSession(context); | ||
* let user = await getUser(); | ||
* session.set("user", user); | ||
* return json({ user }); | ||
* } | ||
* ``` | ||
* | ||
* By default the middleware will automaticaly commit the session at the end of | ||
* the request, but you can customize this behavior by passing a second | ||
* argument to the `unstable_createSessionMiddleware` function. | ||
* | ||
* ```ts | ||
* let [sessionMiddleware, getSession] = unstable_createSessionMiddleware( | ||
* sessionStorage, | ||
* shouldCommit | ||
* ); | ||
* ``` | ||
* | ||
* The `shouldCommit` function will be called at the end of the request with | ||
* the previous session data and the session data before the request, if it | ||
* returns `true` the session will be committed, if it returns `false` the | ||
* session will be discarded. | ||
* | ||
* If you want to commit the session only if the session data changed you can | ||
* use a library like `dequal` to compare the session data. | ||
* | ||
* ```ts | ||
* import { dequal } from "dequal"; | ||
* | ||
* let [sessionMiddleware, getSession] = unstable_createSessionMiddleware( | ||
* sessionStorage, | ||
* (previous, next) => !dequal(previous, next) // Only commit if session changed | ||
* ); | ||
* ``` | ||
* | ||
* Or you can use a custom function to compare the session data, maybe only if | ||
* some specific fields changed. | ||
* | ||
* ```ts | ||
* let [sessionMiddleware, getSession] = unstable_createSessionMiddleware( | ||
* sessionStorage, | ||
* (previous, next) => { | ||
* return current.user.id !== previous.user.id; | ||
* } | ||
* ); | ||
* ``` | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Session | ||
*/ | ||
import type { Session, SessionData, SessionStorage, unstable_MiddlewareFunction } from "react-router"; | ||
@@ -2,0 +89,0 @@ import type { unstable_MiddlewareGetter } from "./utils.js"; |
@@ -0,1 +1,82 @@ | ||
/** | ||
* The singleton middleware let's you create a singleton object that will be shared between loaders of a single requests. | ||
* | ||
* This is specially useful to share objects that needs to be created only once per request, like a cache, but not shared between requests. | ||
* | ||
* ```ts | ||
* import { unstable_createSingletonMiddleware } from "remix-utils/middleware/singleton"; | ||
* | ||
* export const [singletonMiddleware, getSingleton] = | ||
* unstable_createSingletonMiddleware({ | ||
* instantiator: () => new MySingletonClass(), | ||
* }); | ||
* ``` | ||
* | ||
* To use it, you need to add it to the `unstable_middleware` array in the route where you want to use it. | ||
* | ||
* ```ts | ||
* import { singletonMiddleware } from "~/middleware/singleton.server"; | ||
* export const unstable_middleware = [singletonMiddleware]; | ||
* ``` | ||
* | ||
* And you can use the `getSingleton` function in your loaders to get the singleton object. | ||
* | ||
* ```ts | ||
* import { getSingleton } from "~/middleware/singleton.server"; | ||
* | ||
* export async function loader({ context }: LoaderFunctionArgs) { | ||
* let singleton = getSingleton(context); | ||
* let result = await singleton.method(); | ||
* // ... | ||
* } | ||
* ``` | ||
* | ||
* The singleton middleware can be created with different classes and arguments, so you can have multiple singletons in the same request. | ||
* | ||
* ```ts | ||
* import { unstable_createSingletonMiddleware } from "remix-utils/middleware/singleton"; | ||
* | ||
* export const [singletonMiddleware, getSingleton] = | ||
* unstable_createSingletonMiddleware({ | ||
* instantiator: () => new MySingletonClass("arg1", "arg2"), | ||
* }); | ||
* | ||
* export const [anotherSingletonMiddleware, getAnotherSingleton] = | ||
* unstable_createSingletonMiddleware({ | ||
* instantiator: () => new AnotherSingletonClass("arg1", "arg2"), | ||
* }); | ||
* ``` | ||
* | ||
* And use it in a route like this. | ||
* | ||
* ```ts | ||
* import { | ||
* singletonMiddleware, | ||
* anotherSingletonMiddleware, | ||
* } from "~/middleware/singleton.server"; | ||
* | ||
* export const unstable_middleware = [ | ||
* singletonMiddleware, | ||
* anotherSingletonMiddleware, | ||
* ]; | ||
* ``` | ||
* | ||
* You can also access the `request` and `context` objects in the `instantiator` function, so you can create the singleton based on the request or context. | ||
* | ||
* ```ts | ||
* import { unstable_createSingletonMiddleware } from "remix-utils/middleware/singleton"; | ||
* import { MySingletonClass } from "~/singleton"; | ||
* | ||
* export const [singletonMiddleware, getSingleton] = | ||
* unstable_createSingletonMiddleware({ | ||
* instantiator: (request, context) => { | ||
* return new MySingletonClass(request, context); | ||
* }, | ||
* }); | ||
* ``` | ||
* | ||
* This can allows you to create a class that depends on the request, maybe to read the URL or body, or depends on the context, maybe to read the session or some other data. | ||
* @author [Sergio Xalambrí](https://sergiodxa.com) | ||
* @module Middleware/Singleton | ||
*/ | ||
import type { unstable_MiddlewareFunction, unstable_RouterContextProvider } from "react-router"; | ||
@@ -2,0 +83,0 @@ import type { unstable_MiddlewareGetter } from "./utils.js"; |
import type { Cookie } from "react-router"; | ||
import { z } from "zod"; | ||
import type { TypedCookie } from "./typed-cookie.js"; | ||
/** | ||
* Rolling cookies allows you to prolong the expiration of a cookie by updating | ||
* the expiration date of every cookie. | ||
* | ||
* The `rollingCookie` function is prepared to be used in `entry.server` | ||
* exported function to update the expiration date of a cookie if no loader set | ||
* it. | ||
* | ||
* For document request you can use it on the `handleRequest` function: | ||
* | ||
* ```ts | ||
* import { rollingCookie } from "remix-utils/rolling-cookie"; | ||
* import { sessionCookie } from "~/session.server"; | ||
* | ||
* export default function handleRequest( | ||
* request: Request, | ||
* responseStatusCode: number, | ||
* responseHeaders: Headers, | ||
* remixContext: EntryContext | ||
* ) { | ||
* await rollingCookie(sessionCookie, request, responseHeaders); | ||
* | ||
* return isbot(request.headers.get("user-agent")) | ||
* ? handleBotRequest( | ||
* request, | ||
* responseStatusCode, | ||
* responseHeaders, | ||
* remixContext | ||
* ) | ||
* : handleBrowserRequest( | ||
* request, | ||
* responseStatusCode, | ||
* responseHeaders, | ||
* remixContext | ||
* ); | ||
* } | ||
* ``` | ||
* | ||
* And for data request you can do it on the `handleDataRequest` function: | ||
* ```ts | ||
* import { rollingCookie } from "remix-utils/rolling-cookie"; | ||
* | ||
* export let handleDataRequest: HandleDataRequestFunction = async ( | ||
* response: Response, | ||
* { request } | ||
* ) => { | ||
* let cookieValue = await sessionCookie.parse( | ||
* responseHeaders.get("set-cookie") | ||
* ); | ||
* if (!cookieValue) { | ||
* cookieValue = await sessionCookie.parse(request.headers.get("cookie")); | ||
* responseHeaders.append( | ||
* "Set-Cookie", | ||
* await sessionCookie.serialize(cookieValue) | ||
* ); | ||
* } | ||
* | ||
* return response; | ||
* }; | ||
* ``` | ||
* | ||
* @param cookie The cookie to keep alive | ||
* @param request The request object | ||
* @param responseHeaders The response headers object | ||
* @returns A promise that resolves when the cookie has been set in the response | ||
* @see https://sergiodxa.com/articles/add-rolling-sessions-to-remix | ||
*/ | ||
export declare function rollingCookie<Schema extends z.ZodTypeAny>(cookie: Cookie | TypedCookie<Schema>, request: Request, responseHeaders: Headers): Promise<void>; |
import { z } from "zod"; | ||
/** | ||
* Rolling cookies allows you to prolong the expiration of a cookie by updating | ||
* the expiration date of every cookie. | ||
* | ||
* The `rollingCookie` function is prepared to be used in `entry.server` | ||
* exported function to update the expiration date of a cookie if no loader set | ||
* it. | ||
* | ||
* For document request you can use it on the `handleRequest` function: | ||
* | ||
* ```ts | ||
* import { rollingCookie } from "remix-utils/rolling-cookie"; | ||
* import { sessionCookie } from "~/session.server"; | ||
* | ||
* export default function handleRequest( | ||
* request: Request, | ||
* responseStatusCode: number, | ||
* responseHeaders: Headers, | ||
* remixContext: EntryContext | ||
* ) { | ||
* await rollingCookie(sessionCookie, request, responseHeaders); | ||
* | ||
* return isbot(request.headers.get("user-agent")) | ||
* ? handleBotRequest( | ||
* request, | ||
* responseStatusCode, | ||
* responseHeaders, | ||
* remixContext | ||
* ) | ||
* : handleBrowserRequest( | ||
* request, | ||
* responseStatusCode, | ||
* responseHeaders, | ||
* remixContext | ||
* ); | ||
* } | ||
* ``` | ||
* | ||
* And for data request you can do it on the `handleDataRequest` function: | ||
* ```ts | ||
* import { rollingCookie } from "remix-utils/rolling-cookie"; | ||
* | ||
* export let handleDataRequest: HandleDataRequestFunction = async ( | ||
* response: Response, | ||
* { request } | ||
* ) => { | ||
* let cookieValue = await sessionCookie.parse( | ||
* responseHeaders.get("set-cookie") | ||
* ); | ||
* if (!cookieValue) { | ||
* cookieValue = await sessionCookie.parse(request.headers.get("cookie")); | ||
* responseHeaders.append( | ||
* "Set-Cookie", | ||
* await sessionCookie.serialize(cookieValue) | ||
* ); | ||
* } | ||
* | ||
* return response; | ||
* }; | ||
* ``` | ||
* | ||
* @param cookie The cookie to keep alive | ||
* @param request The request object | ||
* @param responseHeaders The response headers object | ||
* @returns A promise that resolves when the cookie has been set in the response | ||
* @see https://sergiodxa.com/articles/add-rolling-sessions-to-remix | ||
*/ | ||
export async function rollingCookie(cookie, request, responseHeaders) { | ||
let value = await cookie.parse(responseHeaders.get("Set-Cookie")); | ||
if (value !== null) | ||
const requestCookie = await cookie.parse(request.headers.get("Cookie")); | ||
if (!requestCookie) | ||
return; | ||
value = await cookie.parse(request.headers.get("Cookie")); | ||
if (!value) | ||
const responseCookie = await cookie.parse(responseHeaders.get("Set-Cookie")); | ||
if (responseCookie !== null) | ||
return; | ||
responseHeaders.append("Set-Cookie", await cookie.serialize(value)); | ||
responseHeaders.append("Set-Cookie", await cookie.serialize(requestCookie)); | ||
} | ||
//# sourceMappingURL=rolling-cookie.js.map |
import { z } from "zod"; | ||
declare const FetchDestSchema: z.ZodEnum<["audio", "audioworklet", "document", "embed", "empty", "font", "frame", "iframe", "image", "manifest", "object", "paintworklet", "report", "script", "serviceworker", "sharedworker", "style", "track", "video", "worker", "xslt"]>; | ||
export declare const FetchDestSchema: z.ZodEnum<["audio", "audioworklet", "document", "embed", "empty", "font", "frame", "iframe", "image", "manifest", "object", "paintworklet", "report", "script", "serviceworker", "sharedworker", "style", "track", "video", "worker", "xslt"]>; | ||
export type FetchDest = z.output<typeof FetchDestSchema>; | ||
@@ -22,3 +22,3 @@ /** | ||
export declare function fetchDest(headers: Headers): FetchDest | null; | ||
declare const FetchModeSchema: z.ZodEnum<["cors", "navigate", "no-cors", "same-origin", "websocket"]>; | ||
export declare const FetchModeSchema: z.ZodEnum<["cors", "navigate", "no-cors", "same-origin", "websocket"]>; | ||
export type FetchMode = z.output<typeof FetchModeSchema>; | ||
@@ -30,3 +30,3 @@ /** | ||
export declare function fetchMode(headers: Headers): FetchMode | null; | ||
declare const FetchSiteSchema: z.ZodEnum<["cross-site", "same-origin", "same-site", "none"]>; | ||
export declare const FetchSiteSchema: z.ZodEnum<["cross-site", "same-origin", "same-site", "none"]>; | ||
export type FetchSite = z.output<typeof FetchSiteSchema>; | ||
@@ -38,3 +38,3 @@ /** | ||
export declare function fetchSite(headers: Headers): FetchSite | null; | ||
declare const FetchUserSchema: z.ZodLiteral<"?1">; | ||
export declare const FetchUserSchema: z.ZodLiteral<"?1">; | ||
export type FetchUser = z.output<typeof FetchUserSchema>; | ||
@@ -46,2 +46,1 @@ /** | ||
export declare function isUserInitiated(headers: Headers): boolean; | ||
export {}; |
import { z } from "zod"; | ||
import { getHeaders } from "./get-headers.js"; | ||
const FetchDestSchema = z.enum([ | ||
export const FetchDestSchema = z.enum([ | ||
"audio", | ||
@@ -33,3 +33,3 @@ "audioworklet", | ||
} | ||
const FetchModeSchema = z.enum([ | ||
export const FetchModeSchema = z.enum([ | ||
"cors", | ||
@@ -48,3 +48,3 @@ "navigate", | ||
} | ||
const FetchSiteSchema = z.enum([ | ||
export const FetchSiteSchema = z.enum([ | ||
"cross-site", | ||
@@ -62,3 +62,3 @@ "same-origin", | ||
} | ||
const FetchUserSchema = z.literal("?1"); | ||
export const FetchUserSchema = z.literal("?1"); | ||
export function isUserInitiated(input) { | ||
@@ -65,0 +65,0 @@ let headers = getHeaders(input).get("Set-Fetch-User"); |
{ | ||
"name": "remix-utils", | ||
"version": "8.6.0", | ||
"version": "8.7.0", | ||
"license": "MIT", | ||
@@ -26,2 +26,10 @@ "engines": { | ||
}, | ||
"./middleware/cors": { | ||
"types": "./build/server/middleware/cors.d.ts", | ||
"default": "./build/server/middleware/cors.js" | ||
}, | ||
"./middleware/honeypot": { | ||
"types": "./build/server/middleware/honeypot.d.ts", | ||
"default": "./build/server/middleware/honeypot.js" | ||
}, | ||
"./middleware/jwk-auth": { | ||
@@ -39,2 +47,6 @@ "types": "./build/server/middleware/jwk-auth.d.ts", | ||
}, | ||
"./middleware/rolling-cookie": { | ||
"types": "./build/server/middleware/rolling-cookie.d.ts", | ||
"default": "./build/server/middleware/rolling-cookie.js" | ||
}, | ||
"./middleware/secure-headers": { | ||
@@ -41,0 +53,0 @@ "types": "./build/server/middleware/secure-headers.d.ts", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
417890
15.37%171
3.64%6088
30.9%2948
2.57%