{"id":27401259,"url":"https://github.com/edjcase/liminal","last_synced_at":"2026-01-21T08:12:09.005Z","repository":{"id":276246525,"uuid":"928688277","full_name":"edjCase/liminal","owner":"edjCase","description":"A collection of http Motoko libraries. Routing, parsing, middleware, etc...","archived":false,"fork":false,"pushed_at":"2025-10-31T20:28:34.000Z","size":3842,"stargazers_count":11,"open_issues_count":10,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-31T22:14:27.396Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Motoko","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/edjCase.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-02-07T03:54:37.000Z","updated_at":"2025-10-31T20:28:38.000Z","dependencies_parsed_at":"2025-04-29T00:28:05.470Z","dependency_job_id":"71ced036-7e25-46a6-a8b7-775b6544aa80","html_url":"https://github.com/edjCase/liminal","commit_stats":null,"previous_names":["edjcase/motoko_http","edjcase/liminal"],"tags_count":0,"template":false,"template_full_name":"edjCase/motoko-library-template","purl":"pkg:github/edjCase/liminal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edjCase%2Fliminal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edjCase%2Fliminal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edjCase%2Fliminal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edjCase%2Fliminal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/edjCase","download_url":"https://codeload.github.com/edjCase/liminal/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/edjCase%2Fliminal/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28629922,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-21T04:47:28.174Z","status":"ssl_error","status_checked_at":"2026-01-21T04:47:22.943Z","response_time":86,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-04-14T03:54:33.581Z","updated_at":"2026-01-21T08:12:08.997Z","avatar_url":"https://github.com/edjCase.png","language":"Motoko","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Liminal\n\n![Logo](logo.svg)\n\n[![MOPS](https://img.shields.io/badge/MOPS-liminal-blue)](https://mops.one/liminal)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/edjcase/motoko_http/blob/main/LICENSE)\n\nA middleware-based HTTP framework for Motoko on the Internet Computer.\n\n## Overview\n\nLiminal is a flexible HTTP framework designed to make building web applications with Motoko simpler and more maintainable. It provides a middleware pipeline architecture, built-in routing capabilities, and a variety of helper modules for common web development tasks.\n\nKey features:\n\n-   🔄 **Middleware**: Compose your application using reusable middleware components\n-   🛣️ **Routing**: Powerful route matching with parameter extraction and group support\n-   🔒 **CORS Support**: Configurable Cross-Origin Resource Sharing\n-   🔐 **CSP Support**: Content Security Policy configuration\n-   📦 **Asset Canister Integration**: Simplified interface with Internet Computer's certified assets\n-   🔑 **JWT Authentication**: Built-in JWT parsing and validation\n-   🚀 **Compression**: Automatic response compression for performance\n-   ⏱️ **Rate Limiting**: Protect your APIs from abuse\n-   🛡️ **Authentication**: Configurable authentication requirements\n-   🔀 **Content Negotiation**: Automatically convert data to JSON, CBOR, XML based on Accept header\n-   📤 **File Uploads**: Parse and process multipart/form-data for handling file uploads (limited to 2MB)\n-   📝 **Logging**: Built-in logging system with configurable levels and custom logger support\n-   🔐 **OAuth Authentication**: Built-in OAuth 2.0 support with PKCE for Google, GitHub, and custom providers\n\n## Package\n\n### MOPS\n\n```bash\nmops add liminal\n```\n\nTo setup MOPS package manager, follow the instructions from the [MOPS Site](https://mops.one)\n\n## Liminal Middleware Pipeline\n\nLiminal uses a **middleware pipeline** pattern where each middleware component processes requests as they flow down the pipeline, and then processes responses as they flow back up in reverse order. This creates a \"sandwich\" effect where the first middleware to see a request is the last to process the response.\n\n### Basic Flow Example\n\n```\n                  Request ──┐     ┌─\u003e Response\n                            │     |\n                            ▼     │\n                        ┌─────────────┐\n        - Decompresses  │ Compression │ - Compresses\n          request       │ Middleware  │   response\n                        └─────────────┘\n                            │     ▲\n                            ▼     │\n                        ┌─────────────┐\n        - Parses JWT    │  JWT        │ - Ignores\n        - Sets identity │  Middleware │   response\n                        └─────────────┘\n                            │     ▲\n                            ▼     │\n                        ┌─────────────┐\n        - Matches url   │ API Router  │ - Returns API\n          to function   │ Middleware  │   response\n                        └─────────────┘\n```\n\n### How It Works\n\n1. **Request Flow (Down)**: The HTTP request starts at the first middleware and flows down through each middleware in the order they were defined in your middleware array.\n\n2. **Response Generation**: Any middleware in the pipeline can choose to generate a response and stop the request flow. When this happens, the response immediately begins flowing back up through only the middleware that have already processed the request, bypassing any remaining middleware further down the pipeline.\n\n3. **Response Flow (Up)**: The response then flows back up through the middleware pipeline in **reverse order**, allowing each middleware to modify or enhance the response.\n\n## Query/Update Upgrade Flow\n\n### How Middleware Handles Query→Update Upgrades\n\nIn the Internet Computer, all HTTP requests start as **Query calls** (fast, read-only). If a middleware needs to modify state or make async calls, it can **upgrade** the request to an **Update call** (slower, can modify state). When this happens, the entire request restarts from the beginning with the same middleware pipeline.\n\n### Upgrade Flow Example\n\n```\n        Query Flow                       Update Flow\n\n\nRequest ──┐             ┌──────► Request ──┐     ┌─► Response\n          │             │                  │     │\n          ▼             │                  ▼     │\n      ┌─────────────┐   │              ┌─────────────┐\n      │ Compression │   │              │ Compression │\n      │ Middleware  │   │              │ Middleware  │\n      └─────────────┘   │              └─────────────┘\n          │             │                  │     ▲\n          ▼             │                  ▼     │\n      ┌─────────────┐   │              ┌─────────────┐\n      │ JWT         │ ──┘              │ JWT         │\n      │ Middleware  │ Upgrade          │ Middleware  │\n      └─────────────┘                  └─────────────┘\n                                           │     ▲\n                                           ▼     │\n      ┌─────────────┐                  ┌─────────────┐\n      │ API Router  │                  │ API Router  │\n      │ Middleware  │                  │ Middleware  │\n      └─────────────┘                  └─────────────┘\n```\n\n### How Upgrades Work\n\n1. **Query Processing**: Request flows down through middleware as Query calls (fast path)\n\n2. **Upgrade Decision**: Any middleware can decide it needs to upgrade (e.g., needs to modify state, make async calls)\n\n3. **Request Restart**: When upgraded, the entire request restarts from the beginning as an Update call and go through each middleware again\n\n## Quick Start\n\nHere's a minimal example to get started:\n\n```motoko\nimport Liminal \"mo:liminal\";\nimport Route \"mo:liminal/Route\";\nimport Router \"mo:liminal/Router\";\nimport RouteContext \"mo:liminal/RouteContext\";\nimport RouterMiddleware \"mo:liminal/Middleware/Router\";\nimport CORSMiddleware \"mo:liminal/Middleware/CORS\";\n\nactor {\n    // Define your routes\n    let routerConfig = {\n        prefix = ?\"/api\";\n        identityRequirement = null;\n        routes = [\n            Router.get(\n                \"/hello/{name}\",\n                #query_(func(context : RouteContext.RouteContext) : Route.HttpResponse {\n                    let name = context.getRouteParam(\"name\");\n                    context.buildResponse(#ok, #text(\"Hello, \" # name # \"!\"));\n                })\n            )\n        ]\n    };\n\n    // Create the HTTP App with middleware\n    let app = Liminal.App({\n        middleware = [\n            // Order matters\n            // First middleware will be called FIRST with the HTTP request\n            // and LAST with handling the HTTP response\n            CORSMiddleware.default(),\n            RouterMiddleware.new(routerConfig),\n        ];\n        errorSerializer = Liminal.defaultJsonErrorSerializer;\n        candidRepresentationNegotiator = Liminal.defaultCandidRepresentationNegotiator;\n        logger = Liminal.buildDebugLogger(#info);\n        urlNormalization = {\n            pathIsCaseSensitive = false;\n            preserveTrailingSlash = false;\n            queryKeysAreCaseSensitive = false;\n            removeEmptyPathSegments = true;\n            resolvePathDotSegments = true;\n            usernameIsCaseSensitive = false;\n        };\n    });\n\n    // Expose standard HTTP interface\n    public query func http_request(request : Liminal.RawQueryHttpRequest) : async Liminal.RawQueryHttpResponse {\n        app.http_request(request)\n    };\n\n    public func http_request_update(request : Liminal.RawUpdateHttpRequest) : async Liminal.RawUpdateHttpResponse {\n        await* app.http_request_update(request)\n    };\n}\n```\n\n## More Complete Example\n\nHere's a more comprehensive example demonstrating multiple middleware components:\n\n```motoko\nimport Liminal \"mo:liminal\";\nimport Route \"mo:liminal/Route\";\nimport Router \"mo:liminal/Router\";\nimport RouteContext \"mo:liminal/RouteContext\";\nimport RouterMiddleware \"mo:liminal/Middleware/Router\";\nimport CORSMiddleware \"mo:liminal/Middleware/CORS\";\nimport JWTMiddleware \"mo:liminal/Middleware/JWT\";\nimport CompressionMiddleware \"mo:liminal/Middleware/Compression\";\nimport CSPMiddleware \"mo:liminal/Middleware/CSP\";\nimport AssetsMiddleware \"mo:liminal/Middleware/Assets\";\nimport SessionMiddleware \"mo:liminal/Middleware/Session\";\nimport HttpAssets \"mo:http-assets\";\n\nactor {\n    // Define your routes\n    let routerConfig = {\n        prefix = ?\"/api\";\n        identityRequirement = null;\n        routes = [\n            Router.get(\n                \"/public\",\n                #query_(func(context : RouteContext.RouteContext) : Route.HttpResponse {\n                    context.buildResponse(#ok, #text(\"Public endpoint\"))\n                })\n            ),\n            Router.groupWithAuthorization(\n                \"/secure\",\n                [\n                    Router.get(\n                        \"/profile\",\n                        #query_(func(context : RouteContext.RouteContext) : Route.HttpResponse {\n                            context.buildResponse(#ok, #text(\"Secure profile endpoint\"))\n                        })\n                    )\n                ],\n                #authenticated\n            )\n        ]\n    };\n\n    ];\n\n    // Initialize asset store\n    let canisterId = Principal.fromActor(self);\n    let assetStableData = HttpAssets.init_stable_store(canisterId, initializer)\n    |\u003e HttpAssets.upgrade_stable_store(_);\n    let assetStore = HttpAssets.Assets(assetStableData, ?setPermissions);\n\n    // Create the HTTP App with middleware\n    let app = Liminal.App({\n        middleware = [\n            // Order matters - middleware are executed in this order for requests\n            // and in reverse order for responses\n            CompressionMiddleware.default(),\n            CORSMiddleware.default(),\n            SessionMiddleware.inMemoryDefault(),\n            JWTMiddleware.new({\n                locations = JWTMiddleware.defaultLocations;\n                validation = {\n                    audience = #skip;\n                    issuer = #skip;\n                    signature = #skip;\n                    notBefore = false;\n                    expiration = false;\n                };\n            }),\n            RouterMiddleware.new(routerConfig),\n            CSPMiddleware.default(),\n            AssetsMiddleware.new({\n                store = assetStore;\n            }),\n        ];\n        errorSerializer = Liminal.defaultJsonErrorSerializer;\n        candidRepresentationNegotiator = Liminal.defaultCandidRepresentationNegotiator;\n        logger = Liminal.buildDebugLogger(#info);\n        urlNormalization = {\n            pathIsCaseSensitive = false;\n            preserveTrailingSlash = false;\n            queryKeysAreCaseSensitive = false;\n            removeEmptyPathSegments = true;\n            resolvePathDotSegments = true;\n            usernameIsCaseSensitive = false;\n        };\n    });\n\n    // Expose standard HTTP interface\n    public query func http_request(request : Liminal.RawQueryHttpRequest) : async Liminal.RawQueryHttpResponse {\n        app.http_request(request)\n    };\n\n    public func http_request_update(request : Liminal.RawUpdateHttpRequest) : async Liminal.RawUpdateHttpResponse {\n        await* app.http_request_update(request)\n    };\n}\n```\n\n## Core Concepts\n\n### Middleware\n\nMiddleware are components that process HTTP requests and responses in a pipeline. Each middleware can:\n\n-   Handle the request and produce a response\n-   Pass the request to the next middleware in the pipeline\n-   Modify the request before passing it on\n-   Modify the response after the next middleware processes it\n\n```motoko\nimport App \"mo:liminal/App\";\nimport HttpContext \"mo:liminal/HttpContext\";\nimport HttpMethod \"mo:liminal/HttpMethod\";\n\n// Example of a simple logging middleware\npublic func createLoggingMiddleware() : App.Middleware {\n    {\n        handleQuery = func(context : HttpContext.HttpContext, next : App.Next) : App.QueryResult {\n            context.log(#info, \"Query: \" # HttpMethod.toText(context.method) # \" \" # context.request.url);\n            let response = next();\n            switch (response) {\n                case (#response(r)) context.log(#info, \"Response: \" # debug_show(r.statusCode));\n                case (#upgrade) context.log(#info, \"Response: Upgrade to update call\");\n            };\n            response\n        };\n        handleUpdate = func(context : HttpContext.HttpContext, next : App.NextAsync) : async* App.HttpResponse {\n            context.log(#info, \"Update: \" # HttpMethod.toText(context.method) # \" \" # context.request.url);\n            let response = await* next();\n            context.log(#info, \"Response: \" # debug_show(response.statusCode));\n            response\n        };\n    }\n}\n```\n\n### Routing\n\nThe routing system supports:\n\n-   Path parameters (`/users/{id}`)\n-   Nested routes with prefixes\n-   HTTP method-specific handlers\n-   Query, update, and async handlers\n-   Authorization controls\n\n#### Route Handlers\n\nLiminal provides three types of route handlers to match different execution requirements:\n\n##### Query Handlers (`#query_`)\n\nFor read-only operations that don't modify state. These execute as fast query calls on the Internet Computer.\n\n```motoko\nRouter.get(\"/users\", #query_(func(context : RouteContext.RouteContext) : HttpResponse {\n    // Read-only logic\n    context.buildResponse(#ok, #text(\"User list\"))\n}))\n```\n\n##### Update Handlers (`#update`)\n\nFor operations that modify state or need async capabilities. Update handlers come in three variants:\n\n**Sync Update (`#sync`)** - Synchronous update handler without system access:\n\n```motoko\nRouter.post(\"/users\", #update(#sync(func(context : RouteContext.RouteContext) : HttpResponse {\n    // Modify state synchronously\n    context.buildResponse(#created, #text(\"User created\"))\n})))\n```\n\n**Sync System Update (`#syncSystem`)** - Synchronous update handler with `\u003csystem\u003e` access:\n\n```motoko\nRouter.post(\"/data\", #update(#syncSystem(func\u003csystem\u003e(context : RouteContext.RouteContext) : HttpResponse {\n    // Modify state with system access\n    context.buildResponse(#ok, #text(\"Data updated\"))\n})))\n```\n\n**Async Update (`#async_`)** - Asynchronous handler for inter-canister calls:\n\n```motoko\nRouter.put(\"/users/{id}\", #update(#async_(func(context : RouteContext.RouteContext) : async* HttpResponse {\n    let result = await* externalCanister.updateUser(userId);\n    context.buildResponse(#ok, #text(\"User updated\"))\n})))\n```\n\n##### Upgradable Query Handlers (`#upgradableQuery`)\n\nFor operations that start as queries but can upgrade to updates when needed. This is useful for optimistic reads that may need to write:\n\n```motoko\nRouter.get(\"/data\", #upgradableQuery({\n    queryHandler = func(context : RouteContext.RouteContext) : { #response : HttpResponse; #upgrade } {\n        // Try to handle as query\n        if (canHandleAsQuery()) {\n            #response(context.buildResponse(#ok, #text(\"Data\")))\n        } else {\n            #upgrade // Upgrade to update call\n        }\n    };\n    updateHandler = #async_(func(context : RouteContext.RouteContext) : async* HttpResponse {\n        // Handle as update after upgrade\n        await* performUpdate();\n        context.buildResponse(#ok, #text(\"Data updated\"))\n    });\n}))\n```\n\n#### Route Configuration Example\n\n```motoko\n// Route configuration example\nlet routerConfig = {\n    prefix = ?\"/api\"; // All routes with have prefix `/api`\n    identityRequirement = null; // Default identity requirement for all routes\n    routes = [\n        // Group adds a prefix to all nested routes of `/users`\n        Router.group(\n            \"/users\",\n            [\n                Router.get(\"/\", #query_(getAllUsers)), // GET + query call -\u003e getAllUsers\n                Router.post(\"/\", #update(#sync(createUser))), // POST + update call -\u003e createUser\n                Router.get(\"/{id}\", #query_(getUserById)), // GET + query call -\u003e getUserById\n                Router.put(\"/{id}\", #update(#async_(updateUser))), // PUT + update call (using async method) -\u003e updateUser\n                Router.delete(\"/{id}\", #update(#sync(deleteUser))) // DELETE + update call -\u003e deleteUser\n            ]\n        )\n    ]\n};\n```\n\n### Route Path Formatting\n\nLiminal provides a flexible and powerful path matching system that supports various path patterns:\n\n#### Static Paths\n\nBasic routes with fixed path segments:\n\n```motoko\nRouter.get(\"/users\", #query_(getAllUsers))\nRouter.get(\"/api/products\", #query_(getProducts))\n```\n\n#### Path Parameters\n\nCapture dynamic values from the URL using curly braces:\n\n```motoko\n// Matches: /users/123, /users/abc\nRouter.get(\"/users/{id}\", #query_(getUserById))\n\n// Multiple parameters\n// Matches: /blog/2023/05/hello-world\nRouter.get(\"/blog/{year}/{month}/{slug}\", #query_(getBlogPost))\n```\n\nAccess parameters in your handler:\n\n```motoko\nfunc getUserById(context : RouteContext.RouteContext) : Route.HttpResponse {\n    let userId : Text = context.getRouteParam(\"id\"); // or getRouteParamOrNull(\"id\")\n    // ...\n}\n```\n\n#### Wildcard Segments\n\n##### Single Wildcard (\\*)\n\nMatches exactly one segment in the path:\n\n```motoko\n// Matches: /files/document.txt, /files/image.jpg\n// Does NOT match: /files/folder/document.txt\nRouter.get(\"/files/*\", #query_(getFile))\n\n// Can appear in the middle of a path\n// Matches: /files/document.txt/versions\nRouter.get(\"/files/*/versions\", #query_(getFileVersions))\n```\n\n##### Multi Wildcard (\\*\\*)\n\nMatches any number of segments (including zero):\n\n```motoko\n// Matches: /api, /api/users, /api/users/123/profile\nRouter.get(\"/api/**\", #query_(handleApiRequest))\n\n// Can appear in the middle of a path\n// Matches: /api/info, /api/users/123/info\nRouter.get(\"/api/**/info\", #query_(getApiInfo))\n```\n\n### HTTP Context\n\nThe `HttpContext` provides access to request details:\n\n-   Path and query parameters\n-   Headers\n-   Request body (with JSON parsing helpers)\n-   HTTP method\n-   Identity (for authentication)\n\n```motoko\npublic func handleRequest(context : RouteContext.RouteContext) : Route.HttpResponse {\n    // Access route parameters\n    let id = context.getRouteParam(\"id\");\n\n    // Access query parameters\n    let filter = context.getQueryParam(\"filter\");\n\n    // Access headers\n    let authorization = context.getHeader(\"Authorization\");\n\n    // Get authenticated identity\n    let identity = context.getIdentity();\n\n    // Parse JSON body\n    let result = context.parseJsonBody\u003cCreateRequest\u003e(deserializeCreateRequest);\n\n    // Return a response\n    let response = context.buildResponse(#ok, #content(#Record([(\"id\", #number(#int(id)))])));\n\n    // Log\n    context.log(#info, \"Created item with id: \" # id)\n}\n```\n\n### Content Negotiation\n\nThe framework includes built-in content negotiation that converts Candid data to various formats based on the client's Accept header:\n\n```motoko\n// Return data using automatic content negotiation\ncontext.buildResponse(#ok, #content(myCandidData))\n```\n\nThe `#content` response kind takes a Candid representation of your data and uses the client's Accept header to determine the appropriate format (JSON, CBOR, Candid, or XML). This works around Motoko's lack of reflection by using Candid as the common intermediate format - Motoko's `to_candid` converts your types to Candid, which is then converted to the requested format.\n\n### File Uploads\n\nLiminal provides built-in support for handling file uploads via multipart/form-data requests. The file upload functionality allows you to easily access uploaded files within your route handlers:\n\n```motoko\nfunc(context : RouteContext.RouteContext) : Route.HttpResponse {\n    // Access all uploaded files\n    let files = context.getUploadedFiles();\n\n    // Process each uploaded file\n    for (file in files.vals()) {\n        // Each file has: fieldName, filename, contentType, size, and content\n        let fieldName = file.fieldName;  // Form field name\n        let filename = file.filename;    // Original filename\n        let contentType = file.contentType;  // MIME type\n        let size = file.size;            // Size in bytes\n        let content = file.content;      // Blob containing file data\n\n        // Process the file as needed...\n    };\n\n    return context.buildResponse(#ok, #text(\"Upload successful\"));\n}\n```\n\nThe `getUploadedFiles()` method automatically parses the multipart/form-data content from the request and returns information about each uploaded file. This makes it straightforward to handle file uploads without needing to manually parse complex multipart boundaries and headers.\n\n## Built-in Middleware\n\n### Router\n\nHandles route matching and dispatching to the appropriate handler.\n\n```motoko\nRouterMiddleware.new(routerConfig)\n```\n\n### CORS\n\nConfigures Cross-Origin Resource Sharing.\n\n```motoko\nCORSMiddleware.default()\n\n// Or with custom options\nCORSMiddleware.new({\n    allowOrigins = [\"https://yourdomain.com\"];\n    allowMethods = [#get, #post, #put, #delete];\n    allowHeaders = [\"Content-Type\", \"Authorization\"];\n    maxAge = ?86400;\n    allowCredentials = true;\n    exposeHeaders = [\"Content-Length\"];\n})\n```\n\n### JWT\n\nHandles JSON Web Token authentication and parsing.\n\n```motoko\nJWTMiddleware.new({\n    locations = [#header(\"Authorization\"), #cookie(\"jwt\"), #queryString(\"token\")];\n    validation = {\n        audience = #skip;\n        issuer = #skip;\n        signature = #skip;\n        notBefore = false;\n        expiration = false;\n    };\n})\n\n// Or use default settings\nJWTMiddleware.new({\n    locations = JWTMiddleware.defaultLocations;\n    validation = {\n        audience = #skip;\n        issuer = #skip;\n        signature = #skip;\n        notBefore = false;\n        expiration = false;\n    };\n})\n```\n\n### Compression\n\nAutomatically compresses HTTP responses for better performance.\n\n```motoko\nCompressionMiddleware.default()\n\n// Or with custom options\nCompressionMiddleware.new({\n    minSize = 1024; // Minimum size in bytes to apply compression\n    mimeTypes = [\n        \"text/\",\n        \"application/javascript\",\n        \"application/json\",\n        \"application/xml\"\n    ];\n    skipCompressionIf = null;\n})\n```\n\n### Rate Limiter\n\nProtects your API from abuse by limiting request rates.\n\n```motoko\nRateLimiterMiddleware.new({\n    limit = 100; // Maximum requests per window\n    windowSeconds = 60; // Time window in seconds\n    includeResponseHeaders = true;\n    limitExceededMessage = ?\"Rate limit exceeded. Try again later.\";\n    keyExtractor = #ip; // Use client IP as the rate limit key\n    skipIf = null;\n})\n```\n\n### Require Authentication\n\nEnforces authentication requirements for specific routes.\n\n```motoko\nRequireAuthMiddleware.new(#authenticated)\n\n// Or with a custom validation function\nRequireAuthMiddleware.new(#custom(func(identity : Identity) : Bool {\n    // Custom validation logic\n    let ?id = identity.getId() else return false;\n    // Check roles, permissions, etc.\n    return true;\n}))\n```\n\n### Session\n\nProvides session management with configurable storage and cookie options.\n\n```motoko\n// Use default in-memory session store\nSessionMiddleware.inMemoryDefault()\n\n// Or with custom configuration\nSessionMiddleware.new({\n    cookieName = \"session\";\n    idleTimeout = 1200; // 20 minutes in seconds\n    cookieOptions = {\n        path = \"/\";\n        secure = true;\n        httpOnly = true;\n        sameSite = ?#lax;\n        maxAge = null;\n    };\n    store = myCustomSessionStore;\n    idGenerator = generateCustomSessionId;\n})\n```\n\nAccess session data in route handlers:\n\n```motoko\nfunc handleRequest(context : RouteContext.RouteContext) : Route.HttpResponse {\n    // Get session (automatically created if needed)\n    let ?session = context.session else {\n        return context.buildResponse(#internalServerError, #error(#message(\"Session unavailable\")));\n    };\n\n    // Store data in session\n    session.set(\"user_id\", \"123\");\n    session.set(\"preferences\", \"dark_mode\");\n\n    // Retrieve data from session\n    let ?userId = session.get(\"user_id\") else {\n        return context.buildResponse(#unauthorized, #error(#message(\"Not logged in\")));\n    };\n\n    // Remove specific key\n    session.remove(\"temp_data\");\n\n    // Clear entire session\n    session.clear();\n\n    context.buildResponse(#ok, #text(\"Session updated\"));\n}\n```\n\n### CSRF\n\nProvides Cross-Site Request Forgery protection with configurable token validation.\n\n```motoko\n// Use with session storage\nCSRFMiddleware.new(CSRFMiddleware.defaultConfig({\n    get = func() : ?Text {\n        // Get token from session or other storage\n        null\n    };\n    set = func(token : Text) {\n        // Store token in session or other storage\n    };\n}))\n\n// Or with custom configuration\nCSRFMiddleware.new({\n    tokenTTL = 1_800_000_000_000; // 30 minutes in nanoseconds\n    tokenStorage = myTokenStorage;\n    headerName = \"X-CSRF-Token\";\n    protectedMethods = [#post, #put, #patch, #delete];\n    exemptPaths = [\"/api/public\"];\n    tokenRotation = #perRequest;\n})\n```\n\nCSRF tokens are automatically generated for GET requests and validated for protected HTTP methods. Include the token in your forms or AJAX requests using the configured header name.\n\n### Assets\n\nServes static files with configurable caching.\n\n```motoko\nAssetsMiddleware.new({\n    prefix = ?\"/static\";\n    store = assetStore;\n    indexAssetPath = ?\"/index.html\";\n    cache = {\n        default = #public_({\n            immutable = false;\n            maxAge = 3600;\n        });\n        rules = [\n            {\n                pattern = \"/*.css\";\n                cache = #public_({\n                    immutable = true;\n                    maxAge = 86400;\n                });\n            }\n        ];\n    };\n})\n```\n\n### CSP (Content Security Policy)\n\nConfigures security policies for your application.\n\n```motoko\nCSPMiddleware.default()\n\n// Or with custom options\nCSPMiddleware.new({\n    defaultSrc = [\"'self'\"];\n    scriptSrc = [\"'self'\", \"'unsafe-inline'\", \"https://trusted-scripts.com\"];\n    connectSrc = [\"'self'\", \"https://api.example.com\"];\n    // Additional CSP directives...\n})\n```\n\n### OAuth (Experimental)\n\nProvides secure OAuth 2.0 authentication with PKCE for popular providers. PKCE (Proof Key for Code Exchange) is used for all OAuth flows, eliminating the need to store client secrets.\n\n```motoko\nimport OAuthMiddleware \"mo:liminal/Middleware/OAuth\";\n\nlet oauthConfig = {\n    providers = [{\n        OAuthMiddleware.GitHub with\n        name = \"GitHub\";\n        clientId = \"your-client-id\";\n        scopes = [\"read:user\", \"user:email\"];\n        // PKCE is mandatory - no client secrets needed\n    }];\n    siteUrl = \"https://your-canister-url.ic0.app\";\n    store = OAuthMiddleware.inMemoryStore();\n    onLogin = func(context, data) {\n        // Handle successful login\n        context.buildRedirectResponse(\"/dashboard\", false);\n    };\n    onLogout = func(context, data) {\n        // Handle logout\n        context.buildRedirectResponse(\"/\", false);\n    };\n};\n\nOAuthMiddleware.new(oauthConfig)\n```\n\nRoutes: `GET /auth/{provider}/login`, `GET /auth/{provider}/callback`, `POST /auth/{provider}/logout`\n\n## Assets Integration\n\nLiminal provides a wrapper around the Internet Computer's asset canister functionality:\n\n```motoko\nimport HttpAssets \"mo:http-assets\";\nimport AssetCanister \"mo:liminal/AssetCanister\";\n\nshared ({ caller = initializer }) persistent actor class Actor() = self {\n    transient let canisterId = Principal.fromActor(self);\n\n    // Initialize asset store (persists across upgrades with persistent actor)\n    let assetStableData = HttpAssets.init_stable_store(canisterId, initializer)\n    |\u003e HttpAssets.upgrade_stable_store(_);\n\n    transient let setPermissions : HttpAssets.SetPermissions = {\n        commit = [initializer];\n        manage_permissions = [initializer];\n        prepare = [initializer];\n    };\n\n    transient let assetStore = HttpAssets.Assets(assetStableData, ?setPermissions);\n    transient let assetCanister = AssetCanister.AssetCanister(assetStore);\n\n    // Use in middleware\n    let app = Liminal.App({\n        middleware = [\n            AssetsMiddleware.new({\n                store = assetStore;\n            }),\n        ];\n        // ... other config\n    });\n\n    // Expose asset canister methods\n    public shared query func get(args : HttpAssets.GetArgs) : async HttpAssets.EncodedAsset {\n        assetCanister.get(args);\n    }\n\n    // Additional asset canister methods...\n}\n```\n\n## Error Handling\n\nCustom error handling can be configured via the app's `errorSerializer`:\n\n```motoko\nimport Json \"mo:json\";\nimport Text \"mo:core/Text\";\nimport Option \"mo:core/Option\";\n\nlet app = Liminal.App({\n    middleware = [ /* ... */ ];\n    errorSerializer = func(error : HttpContext.HttpError) : HttpContext.ErrorSerializerResponse {\n        let body = switch (error.data) {\n            case (#none) #object_([\n                (\"error\", #string(\"Error\")),\n                (\"code\", #number(#int(error.statusCode))),\n            ]);\n            case (#message(message)) #object_([\n                (\"error\", #string(\"Custom Error\")),\n                (\"code\", #number(#int(error.statusCode))),\n                (\"message\", #string(message)),\n            ]);\n            case (#rfc9457(details)) #object_([\n                (\"error\", #string(\"Custom Error\")),\n                (\"code\", #number(#int(error.statusCode))),\n                (\"type\", #string(details.type_)),\n                // Additional fields from RFC 9457...\n            ]);\n        }\n        |\u003e Json.stringify(_, null)\n        |\u003e Text.encodeUtf8(_);\n\n        {\n            body = ?body;\n            headers = [(\"content-type\", \"application/json\")];\n        };\n    };\n    candidRepresentationNegotiator = Liminal.defaultCandidRepresentationNegotiator;\n});\n```\n\nThe `candidRepresentationNegotiator` handles the conversion of Candid values to different representations based on the client's Accept header. The default implementation supports converting to JSON, CBOR, Candid, and XML formats.\n\n## URL Normalization\n\nLiminal provides comprehensive URL normalization to ensure consistent request handling. The `urlNormalization` configuration controls how URLs are processed before routing:\n\n```motoko\nlet app = Liminal.App({\n    middleware = [/* ... */];\n    errorSerializer = Liminal.defaultJsonErrorSerializer;\n    candidRepresentationNegotiator = Liminal.defaultCandidRepresentationNegotiator;\n    logger = Liminal.buildDebugLogger(#info);\n    urlNormalization = {\n        // Path comparison is case-sensitive (/Users != /users)\n        pathIsCaseSensitive = false;\n\n        // Keep trailing slashes (/users/ != /users)\n        preserveTrailingSlash = false;\n\n        // Query parameter keys are case-sensitive (sort != Sort)\n        queryKeysAreCaseSensitive = false;\n\n        // Remove empty path segments (/users//123 -\u003e /users/123)\n        removeEmptyPathSegments = true;\n\n        // Resolve . and .. in paths (/users/../admin -\u003e /admin)\n        resolvePathDotSegments = true;\n\n        // Username in URLs is case-sensitive (user@host != User@host)\n        usernameIsCaseSensitive = false;\n    };\n});\n```\n\nThese settings help ensure your application handles URLs consistently regardless of how clients format them.\n\n## Breaking Changes (v2 → v3)\n\nVersion 3 introduces significant improvements to the routing API for better type safety and consistency. Here's what changed:\n\n### Router API Changes\n\n#### Route Handler Variants (Breaking Change)\n\n**Old (v2):** Method-specific handler functions\n\n```motoko\n// v2 - Multiple specialized methods\nRouter.getQuery(\"/users\", getUsersHandler)\nRouter.getUpdate(\"/users\", getUsersHandler)\nRouter.getAsyncUpdate(\"/users\", getUsersHandler)\nRouter.postQuery(\"/users\", createUserHandler)\nRouter.postUpdate(\"/users\", createUserHandler)\nRouter.postAsyncUpdate(\"/users\", createUserHandler)\n// ... and similar for PUT, PATCH, DELETE\n```\n\n**New (v3):** Unified methods with handler type variants\n\n```motoko\n// v3 - Single method per HTTP verb with explicit handler type\nRouter.get(\"/users\", #query_(getUsersHandler))\nRouter.post(\"/users\", #update(#sync(createUserHandler)))\nRouter.put(\"/users/{id}\", #update(#async_(updateUserHandler)))\n```\n\n#### Removed Methods\n\nThe following methods have been **removed** in v3:\n\n-   `Router.getQuery()` → Use `Router.get()` with `#query_()` handler\n-   `Router.getUpdate()` → Use `Router.get()` with `#update(#sync())` handler\n-   `Router.getAsyncUpdate()` → Use `Router.get()` with `#update(#async_())` handler\n-   `Router.postQuery()` → Use `Router.post()` with `#query_()` handler\n-   `Router.postUpdate()` → Use `Router.post()` with `#update(#sync())` handler\n-   `Router.postAsyncUpdate()` → Use `Router.post()` with `#update(#async_())` handler\n-   `Router.putQuery()` → Use `Router.put()` with `#query_()` handler\n-   `Router.putUpdate()` → Use `Router.put()` with `#update(#sync())` handler\n-   `Router.putAsyncUpdate()` → Use `Router.put()` with `#update(#async_())` handler\n-   `Router.patchQuery()` → Use `Router.patch()` with `#query_()` handler\n-   `Router.patchUpdate()` → Use `Router.patch()` with `#update(#sync())` handler\n-   `Router.patchAsyncUpdate()` → Use `Router.patch()` with `#update(#async_())` handler\n-   `Router.deleteQuery()` → Use `Router.delete()` with `#query_()` handler\n-   `Router.deleteUpdate()` → Use `Router.delete()` with `#update(#sync())` handler\n-   `Router.deleteAsyncUpdate()` → Use `Router.delete()` with `#update(#async_())` handler\n\n### RouteHandler Type Changes\n\n**Old (v2):**\n\n```motoko\npublic type RouteHandler = {\n    #syncQuery : RouteContext -\u003e HttpResponse;\n    #syncUpdate : \u003csystem\u003e(RouteContext) -\u003e HttpResponse;\n    #asyncUpdate : RouteContext -\u003e async* HttpResponse;\n};\n```\n\n**New (v3):**\n\n```motoko\npublic type UpdateHandlerKind = {\n    #sync : (RouteContext) -\u003e HttpResponse;\n    #syncSystem : \u003csystem\u003e(RouteContext) -\u003e HttpResponse;\n    #async_ : (RouteContext) -\u003e async* HttpResponse;\n};\n\npublic type RouteHandler = {\n    #query_ : (RouteContext) -\u003e HttpResponse;\n    #upgradableQuery : {\n        queryHandler : (RouteContext) -\u003e { #response : HttpResponse; #upgrade };\n        updateHandler : UpdateHandlerKind;\n    };\n    #update : UpdateHandlerKind;\n};\n```\n\n### App Configuration\n\nThe `App` constructor now **requires** a `urlNormalization` configuration:\n\n**Old (v2):**\n\n```motoko\nlet app = Liminal.App({\n    middleware = [...];\n    errorSerializer = Liminal.defaultJsonErrorSerializer;\n    candidRepresentationNegotiator = Liminal.defaultCandidRepresentationNegotiator;\n    logger = Liminal.buildDebugLogger(#info);\n});\n```\n\n**New (v3):**\n\n```motoko\nlet app = Liminal.App({\n    middleware = [...];\n    errorSerializer = Liminal.defaultJsonErrorSerializer;\n    candidRepresentationNegotiator = Liminal.defaultCandidRepresentationNegotiator;\n    logger = Liminal.buildDebugLogger(#info);\n    urlNormalization = {\n        pathIsCaseSensitive = false;\n        preserveTrailingSlash = false;\n        queryKeysAreCaseSensitive = false;\n        removeEmptyPathSegments = true;\n        resolvePathDotSegments = true;\n        usernameIsCaseSensitive = false;\n    };\n});\n```\n\n### Benefits of v3 Changes\n\n-   **More Consistent API:** Single method per HTTP verb with variant for handler type\n-   **More Flexible:** New `#upgradableQuery` and `#syncSystem` variants provide more control\n-   **Clearer Intent:** Code shows whether a route is query or update at a glance\n\n## Testing\n\nRun the test suite with:\n\n```bash\nmops test\n```\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedjcase%2Fliminal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fedjcase%2Fliminal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fedjcase%2Fliminal/lists"}