{"id":15068271,"url":"https://github.com/jaredhanson/flowstate","last_synced_at":"2025-07-08T21:33:44.912Z","repository":{"id":46130601,"uuid":"56338105","full_name":"jaredhanson/flowstate","owner":"jaredhanson","description":"Per-request state management middleware.","archived":false,"fork":false,"pushed_at":"2023-10-18T23:23:55.000Z","size":827,"stargazers_count":6,"open_issues_count":7,"forks_count":11,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-06-24T06:42:46.711Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","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/jaredhanson.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":"jaredhanson","patreon":"jaredhanson","ko_fi":"jaredhanson"}},"created_at":"2016-04-15T17:29:02.000Z","updated_at":"2023-09-02T03:08:06.000Z","dependencies_parsed_at":"2024-06-18T16:53:24.987Z","dependency_job_id":"2f1f740a-2b99-4e16-a7f0-9658c8c94a78","html_url":"https://github.com/jaredhanson/flowstate","commit_stats":{"total_commits":1097,"total_committers":3,"mean_commits":365.6666666666667,"dds":0.006381039197812188,"last_synced_commit":"16dfdfe59470d2da10813667132a51e9f77028c9"},"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/jaredhanson/flowstate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredhanson%2Fflowstate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredhanson%2Fflowstate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredhanson%2Fflowstate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredhanson%2Fflowstate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jaredhanson","download_url":"https://codeload.github.com/jaredhanson/flowstate/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jaredhanson%2Fflowstate/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263698750,"owners_count":23497912,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":"2024-09-25T01:33:05.981Z","updated_at":"2025-07-08T21:33:44.895Z","avatar_url":"https://github.com/jaredhanson.png","language":"JavaScript","funding_links":["https://github.com/sponsors/jaredhanson","https://patreon.com/jaredhanson","https://ko-fi.com/jaredhanson"],"categories":[],"sub_categories":[],"readme":"# flowstate\n\nThis middleware manages and propagates per-request state across HTTP requests to\na web application.  This allows for implementing flows which are sequences of\nrequests and responses that, taken together, culminate in a desired outcome.\n\nBy default, this state is kept in the session.  The session itself stores state\nby setting a cookie which applies to _all_ requests to an application.  This\nmiddleware isolates that state so it can be applied to an individual sequence of\nrequests.  To do this, state is propagated in `return_to` and `state` parameters\nacross requests.  This middleware does this automatically whenever possible,\nsuch as when redirecting.  When not possible, such as when rendering a view,\nlocals and helpers are made available to the view so that `return_to` and\n`state` parameters can be added to links and forms.\n\nThis middleware emerged from the state management functionality implemented by\nauthentication-related packages, in particular [passport-oauth2](https://www.passportjs.org/packages/passport-oauth2/) and\n[oauth2orize](https://www.oauth2orize.org/) which implement OAuth 2.0.  With\nthis package, the functionality is made generic so that it can be applied to\nany HTTP endpoint.\n\n\u003cdiv align=\"right\"\u003e\n  \u003csup\u003eDeveloped by \u003ca href=\"#authors\"\u003eJared Hanson\u003c/a\u003e.\u003c/sub\u003e\n\u003c/div\u003e\n\n## Install\n\n```bash\n$ npm install flowstate\n```\n\n## Usage\n\n#### Add Middleware\n\nAdd state middleware to your application or route:\n\n```js\nvar flowstate = require('flowstate');\n\napp.get('/login', flowstate(), function(req, res, next) {\n  // ...\n});\n\n```\n\nThe middleware will attempt to load any state intended for the endpoint, based\nthe `state` parameter in the query or body of the request.  If state is loaded,\nit will be set at `req.state` so that the handler can process it.  The value set\nat `req.state` is referred to as the \"current state\".\n\nIf state is not loaded, an \"uninitialized\" state will be set at `req.state`.  A\nstate is uninitialized when it is new but not modified.  If the request contains\na `return_to` and optional `state` parameter, those will be captured by the\nuninitialized state as the location to return the user to when the current state\nhas been completely processed.\n\nWhen a response is sent, any modifications to the current state will be saved\nif the state is not complete.  If the state is complete, any persisted state\nwill be removed.  Note that an uninitialized state will never be saved since it\nis not modified.  However, the location to return the user to will be preserved\nby propagating the `return_to` and optional `state` parameters on subsequent\nrequests.\n\n#### Render a View\n\n```js\napp.get('/login', flowstate(), function(req, res, next) {\n  var msgs = req.state.messages || [];\n  res.locals.messages = msgs;\n  res.locals.hasMessages = !! msgs.length;\n  res.render('login');\n});\n\n```\n\nWhen a response is sent by rendering a view, if there is state associated with\nthe request, `res.locals.state` will be set to the current state's handle.\nOtherwise the `return_to` and `state` parameters, if any, will be propagated by\nsetting `res.locals.returnTo` and `res.locals.state`.  The view is expected to\ndecorate links with these properties and add them as hidden input to forms, in\norder to propagate state to subsequent requests.\n\nFor example, if the above `/login` endpoint is requested with a `return_to`\nparameter:\n\n```http\nGET /login?return_to=%2Fdashboard  HTTP/1.1\n```\n\nThen `res.locals.returnTo` will be set to `/dashboard`, making it available to\nthe view.\n\nIf the `/login` endpoint is requested with both a `return_to` and `state`\nparameter:\n\n```http\nGET /login?return_to=%2Fauthorize%2Fcontinue\u0026state=xyz  HTTP/1.1\n```\n\nThen `res.locals.returnTo` will be set to `/authorize/continue` and `res.locals.state`\nwill be set to `xyz`, making them available to the view.\n\nIf the `/login` endpoint is requested with:\n\n```http\nGET /login?state=Zwu8y84x  HTTP/1.1\n```\n\nAssuming the state was valid and intended for `/login`, `res.locals.state` will\nbe set to `Zwu8y84x` and made available to the view.  `res.locals.returnTo` will\n_not_ be set.\n\n#### Redirect to a Location\n\n```js\napp.post('/login', flowstate(), authenticate(), function(req, res, next) {\n  if (mfaRequired(req.user)) {\n    return res.redirect('/stepup');\n  }\n  // ...\n}, function(err, req, res, next) {\n  if (err.status !== 401) { return next(err); }\n  req.state.messages = req.state.messages || [];\n  req.state.messages.push('Invalid username or password.');\n  req.state.failureCount = req.state.failureCount ? req.state.failureCount + 1 : 1;\n  req.state.complete(false);\n  res.redirect('/login');\n});\n\n```\n\nWhen a response redirects the browser, if the current state is complete, any\n`return_to` and `state` parameters will be propagated by decorating the target\nURL.  If the current state is not complete, modifications will be saved and the\nredirect will be decorated with the current state's handle.\n\nFor example, if the above `/login` endpoint is requested with a `return_to` and\n`state` parameter:\n\n```http\nPOST /login HTTP/1.1\nHost: www.example.com\nContent-Type: application/x-www-form-urlencoded\n\nusername=alice\u0026password=letmein\u0026return_to=%2Fauthorize%2Fcontinue\u0026state=xyz\n```\n\nThen the user will be redirected to `/stepup?return_to=%2Fauthorize%2Fcontinue\u0026state=xyz`,\nassuming the password is valid and MFA is required.\n\nIf the password is not valid, an uninitialized state is set at `req.state` that\ncaptures the `return_to` and `state` parameters.  It is then saved and the user\nis redirected to `/login?state=Zwu8y84x` (where `'Zwu8y84x'` is the handle of\nthe newly saved state).  The state data stored in the session is as follows:\n\n```json\n{\n  \"state\": {\n    \"Zwu8y84x\": {\n      \"location\": \"https://www.example.com/login\",\n      \"messages\": [ \"Invalid username or password.\" ],\n      \"failureCount\": 1,\n      \"returnTo\": \"/authorize/continue\",\n      \"state\": \"xyz\"\n    }\n  }\n}\n```\n\nThis redirect will cause the browser to request the `GET /login` route above.\nSince the request is made with a `state=Zwu8y84x` query parameter, the route will\nload the state and make the handle (as well as messages) available to the view.\nThe view must add the handle to the login form as a hidden input field named\n`state`.  When submitted, the browser will then make a request with that `state`\nparameter:\n\n```http\nPOST /login HTTP/1.1\nHost: www.example.com\nContent-Type: application/x-www-form-urlencoded\n\nusername=alice\u0026password=letmeinnow\u0026state=Zwu8y84x\n```\n\nThis time, the `POST /login` route will load the state.  If the password is\nvalid and MFA is required, the user will be will be redirected to\n`/stepup?return_to=%2Fauthorize%2Fcontinue\u0026state=xyz`, as before.  This is\nbecause the original `return_to` and `state` parameters were captured by the\nloaded state object, and are propagated by decorating the redirect location.\n\nIf another invalid password is submitted, the cycle of redirecting, rendering\nthe login view, and prompting the user for a password will repeat, with the\n`failureCount` incremented and saved each time.\n\n#### Resume State\n\n```js\napp.post('/login', flowstate(), authenticate(), function(req, res, next) {\n  if (mfaRequired(req.user)) {\n    return res.redirect('/stepup');\n  }\n  res.resumeState(next);\n}, function(req, res, next) {\n  res.redirect('/');\n}, function(err, req, res, next) {\n  // ...\n});\n\n```\n\nWhen a user has completed a given flow, they should be returned to the location\nthey were navigating prior to entering the flow.  This is accomplished by\ncalling `resumeState()`, a function added to the response by this middleware.\n\nIf a current state was loaded, `resumeState()` will return the user to the\ncaptured `return_to` and `state` parameters, if any.  Otherwise, it will return\nthe user to the `return_to` and `state` parameters carried by the request.  If\nneither of these exist, `resumeState()` will call a callback, which will\ntypically be `next` to invoke the next middleware.  This middleware can then\nredirect the user to a default location.\n\nFor example, when `POST /login` is requested with a `state` parameter:\n\n```http\nPOST /login HTTP/1.1\nHost: www.example.com\nContent-Type: application/x-www-form-urlencoded\n\nusername=alice\u0026password=letmeinnow\u0026state=Zwu8y84x\n```\n\nThen the user will be redirected to `/authorize/continue\u0026state=xyz`,\nassuming the password is valid and MFA is not required.\n\nIf the `/login` endpoint is requested with a `return_to` parameter:\n\n```http\nPOST /login HTTP/1.1\nHost: www.example.com\nContent-Type: application/x-www-form-urlencoded\n\nusername=alice\u0026password=letmein\u0026return_to=%2Fdashboard\n```\n\nThen the user will be redirected to `/dashboard`, after logging in.\n\nIf the `/login` endpoint is requested without any state-related parameters:\n\n```http\nPOST /login HTTP/1.1\nHost: www.example.com\nContent-Type: application/x-www-form-urlencoded\n\nusername=alice\u0026password=letmein\n```\n\nThen the user will be redirected to `/` by the next middleware in the stack.\n\n#### Push State\n\n\n## Authors\n\n- [Jared Hanson](https://www.jaredhanson.me/) { [![WWW](https://raw.githubusercontent.com/jaredhanson/jaredhanson/master/images/globe-12x12.svg)](https://www.jaredhanson.me/) [![Facebook](https://raw.githubusercontent.com/jaredhanson/jaredhanson/master/images/facebook-12x12.svg)](https://www.facebook.com/jaredhanson) [![LinkedIn](https://raw.githubusercontent.com/jaredhanson/jaredhanson/master/images/linkedin-12x12.svg)](https://www.linkedin.com/in/jaredhanson) [![Twitter](https://raw.githubusercontent.com/jaredhanson/jaredhanson/master/images/twitter-12x12.svg)](https://twitter.com/jaredhanson) [![GitHub](https://raw.githubusercontent.com/jaredhanson/jaredhanson/master/images/github-12x12.svg)](https://github.com/jaredhanson) }\n\n## License\n\n[The MIT License](http://opensource.org/licenses/MIT)\n\nCopyright (c) 2016-2023 Jared Hanson\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjaredhanson%2Fflowstate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjaredhanson%2Fflowstate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjaredhanson%2Fflowstate/lists"}