{"id":22486698,"url":"https://github.com/mark-bradshaw/mrhorse","last_synced_at":"2025-08-02T19:32:47.892Z","repository":{"id":24264413,"uuid":"27658416","full_name":"mark-bradshaw/mrhorse","owner":"mark-bradshaw","description":"Policies for Hapi routes","archived":false,"fork":false,"pushed_at":"2024-06-16T09:57:19.000Z","size":563,"stargazers_count":111,"open_issues_count":1,"forks_count":12,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-11-30T19:58:30.737Z","etag":null,"topics":["hapi","hapijs"],"latest_commit_sha":null,"homepage":null,"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/mark-bradshaw.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}},"created_at":"2014-12-07T03:48:18.000Z","updated_at":"2024-07-02T04:09:36.000Z","dependencies_parsed_at":"2024-06-18T17:06:31.782Z","dependency_job_id":null,"html_url":"https://github.com/mark-bradshaw/mrhorse","commit_stats":{"total_commits":228,"total_committers":16,"mean_commits":14.25,"dds":0.6447368421052632,"last_synced_commit":"6819e7a66320bac24cd5cef005e400ffcd479030"},"previous_names":[],"tags_count":32,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mark-bradshaw%2Fmrhorse","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mark-bradshaw%2Fmrhorse/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mark-bradshaw%2Fmrhorse/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mark-bradshaw%2Fmrhorse/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mark-bradshaw","download_url":"https://codeload.github.com/mark-bradshaw/mrhorse/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228500180,"owners_count":17930003,"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":["hapi","hapijs"],"created_at":"2024-12-06T17:15:11.608Z","updated_at":"2024-12-06T17:16:05.772Z","avatar_url":"https://github.com/mark-bradshaw.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"## MrHorse\n\nManage your [**hapi**](https://github.com/hapijs/hapi) routes with modular policies.\n\nLead Maintainer: [Mark Bradshaw](https://github.com/mark-bradshaw), [contributors](CONTRIBUTORS.md)\n\n[![Build Status](https://travis-ci.org/mark-bradshaw/mrhorse.svg?branch=master)](https://travis-ci.org/mark-bradshaw/mrhorse) [![Coverage Status](https://img.shields.io/coveralls/mark-bradshaw/mrhorse.svg)](https://coveralls.io/r/mark-bradshaw/mrhorse) [![NPM Downloads](https://img.shields.io/npm/dm/mrhorse.svg?style%253Dflat-square)](https://www.npmjs.com/package/mrhorse)\n\n### What is it?\n\nWouldn't it be nice to easily configure your routes for authentication by adding an `isLoggedIn` tag?  Or before replying to a request checking to see if `userHasAccessToWidget`?  Maybe you'd like to do some a/b testing, and want to change some requests to a different handler with `splitAB`.  Or you'd like to add some special analytics tracking to some of your api requests, after your controller has already responded, with `trackThisAtAWS`.  You create the policies and MrHorse applies them as directed, when directed.\n\nMrHorse allows you to do all of these and more in a way that centralizes repeated code, and very visibly demonstrates what routes are doing.  You don't have to guess any more whether a route is performing an action.\n\nIt looks like this:\n\n```javascript\nserver.route({\n    method: 'GET',\n    path: '/loggedin',\n    handler: async function() {},\n    options: {\n        plugins: {\n            policies: ['isLoggedIn', 'addTracking', 'logThis']\n        }\n    }\n});\n\nserver.route({\n    method: 'GET',\n    path: '/admin',\n    handler: async function() {},\n    options: {\n        plugins: {\n            policies: [\n                ['isLoggedIn', 'isAnAdmin'], // Do these two in parallel\n                'onlyInUS' // Then do this last\n            ]\n        }\n    }\n});\n```\n\n### Why use this?\n\nOften your route handlers end up doing a lot of repeated work to collect data, check for user rights, tack on special data, and otherwise prepare to do the work of replying to a request.  It'd be very nice to put the code that keeps getting repeated in a single location, and just apply it to routes declaratively. Often you end up repeating the same small bit of code across a lot of handlers to check for rights, or generate some tracking code, update a cookie, etc.  It's hard to see where these actions are happening across your site, code gets repeated, and updating that code to correct a bug can be tricky.\n\n[MrHorse](https://github.com/mark-bradshaw/mrhorse) let's you take those repeated bits of code and centralize them into  \"policies\", which are just single purpose javascript functions with the signature `async function(request, h)`.  Policies are a good fit whenever you find yourself repeating code in your handlers.  Policies can be used for authentication, authorization, reply modification and shaping, logging, or just about anything else you can imagine.  Policies can be applied at any point in the [Hapi request life cycle](http://hapijs.com/api#request-lifecycle), before authentication, before the request is processed, or even after a response has been created.  Once you've created a policy, you just apply it to whatever routes need it and let MrHorse take care of the rest.\n\nUsing policies you can easily mix and match your business logic into your routes in a declarative manner.  This makes it much easier to see what is being done on each route, and allows you to centralize your authentication, authorization, or logging in one place to DRY out your code.  If a policy decides that there's a problem with the current request it can immediately reply back with a 403 forbidden error, or the error of your choice.  You always have the option of doing a custom reply as well, and MrHorse will see that and step out of the way.\n\n### Why use MrHorse instead of Hapi route prerequisites\n\nHapi provides a somewhat similar mechanism for doing things before a route handler is executed, called route prerequisites.  MrHorse seems to be overlapping this functionality, so why not just use prerequisites?\n\n1. MrHorse puts more focus on whether to continue on to the next policy, allowing you to more easily short circuit a request and skip other policies or the route handler.  This makes authentication and authorization tasks more straightforward.  Since you can stop processing with any policy, it allows you to fail quickly and early, and avoid later processing.\n1. MrHorse gives you the option of running policies at any point in the [Hapi request life cycle](http://hapijs.com/api#request-lifecycle), including **after** a request handler has run.  This allows you to easily modify responses, add additional data, or do logging tasks and still run your normal handler.  With prerequisites, you can take over a response, but your route handler won't get run.  It gives you no ability to do additional processing post handler.\n1. MrHorse helps you to keep your policy type code in a central location, and loads it up for you.  Prerequisites don't provide any help with this.\n1. MrHorse can allow policies to run at even more places in the [Hapi request life cycle](http://hapijs.com/api#request-lifecycle) than just right before the handler.  This is a flexibility that prerequisites probably will never have.\n\n### Examples\n\nLook in the `example` folder to see MrHorse in action.  `node example/index.js`.\n\n### Install\n\nTo install mrhorse:\n\n```\nnpm install mrhorse --save\n```\n\nRequires Node \u003e= 14, following the baseline requirements of hapi v21.\n\n### Updating\n\n#### From 2.x\n\nVersion 3.x contains breaking changes from 2.x. In particular, the Node callback model has been abandoned in favor of `async / await`.  This is a change in the entire Hapi ecosystem, so we are following their decision.  This also means that you must be running at least Node 8.\n\nThe following functions are now `async` and do not accept a callback parameter any longer:\n\n* `server.plugins.mrhorse.loadPolicies`\n\n* Policies are now defined as `async` functions. If the function does not throw, it will be considered successful (and should return `h.continue`). In other words, policy definition should change from:\n\n```javascript\nfunction myPolicy(request, reply, next) {\n  if (isAdmin(request) === true ) {\n    return next(null, true);\n  }\n\n  return next(Boom.forbidden('Sorry')); // failure\n}\n```\n\nto:\n\n```javascript\nasync function myPolicy(request, h) {\n  if (isAdmin(request) === true) {\n    return h.continue; // success\n  }\n\n  throw Boom.forbidden('Sorry'); // failure\n}\n```\n\n### Setup\n\n*Mrhorse* looks for policy files in a directory you create.  I recommend calling the directory `policies`, but you can choose any name you want.  You can have this directory sit anywhere in your Hapi project structure.  If you are using plugins for different site functionality, each plugin can have its own, separate policies directory.\n\nOnce you have created your policies directory you must tell MrHorse where it is.  You do this in two ways.  You can either pass the directory location in to the mrhorse plugin when you register it, like this:\n\n```javascript\nawait server.register({\n    plugin: require('mrhorse'),\n    options: {\n        policyDirectory: `${__dirname}/policies`\n    }\n});\n```\n\nOr you can provide a directory location using the `loadPolicies` function, like this:\n\n```javascript\nserver.plugins.mrhorse.loadPolicies(server, {\n        policyDirectory: `${__dirname}/policies`\n    });\n```\n\nBoth strategies are fine, and can be complementary.  If your Hapi project uses plugins to separate up functionality it is perfectly acceptable for each plugin to have its own `policies` folder.  Just use the `loadPolicies` function in each plugin.  See the example folder for additional detail.\n\nYou can use MrHorse in as many places as you want.  It's ok to have multiple policies folders in different locations, and tell MrHorse to look in each one.  The only requirement is that each policy file name **must** be globally unique, since policies can be used on any route in any location.\n\nNormally MrHorse would throw an error when it encounters a duplicate policy, and that's to keep you from accidentally duplicating a policy name, but there are situations that might make sense to ignore the duplicates.  For instance, you might be using a development tool like `wallaby` that will reload your server as you change code, and inadvertently cause MrHorse to reinitialize.  This would cause the process to throw an error and likely abort the server.  In that case you can add `ignoreDuplicates: true` to your MrHorse options and duplicate policies will be silently ignored.\n\nBy default policies are applied at the `onPreHandler` event in the [Hapi request life cycle](http://hapijs.com/api#request-lifecycle) if no other event is specified in the policy.  Each policy can control which event to apply at.  You can also change the default event to whatever you want.  You would do this by passing in `defaultApplyPoint` in the options object when registering the plugin, like this:\n\n```javascript\nawait server.register({\n        plugin: require('mrhorse'),\n        options: {\n            policyDirectory: `${__dirname}/policies`\n            defaultApplyPoint: 'onPreHandler' /* optional.  Defaults to onPreHandler */,\n        }\n    });\n```\n\n#### Policies\n\nNow create a policy file inside the `policies` folder.  This is just a simple javascript file that exports one `async` javascript function.  The name of the file should be the name you want to use for your policy.  MrHorse uses the file name, **not** the function name, to identify the policy so make sure you name the file appropriately.  If this policy file is named `isAdmin.js`, then the policy would be identified as `isAdmin`.\n\n```javascript\nconst isAdmin = async function(request, h) {\n   const role = _do_something_to_check_user_role(request);\n   if (role \u0026\u0026 role === 'admin') {\n       return h.continue; // All is well with this request.  Proceed to the next policy or the route handler.\n   }\n\n   throw Boom.forbidden( 'Noo!' ); // This policy is not satisfied.  Return a 403 forbidden.\n};\n\n// This is optional.  It will default to 'onPreHandler' unless you use a different defaultApplyPoint.\nisAdmin.applyPoint = 'onPreHandler';\n\nmodule.exports = isAdmin;\n```\n\nOn success, the policy function **must** return `h.continue`. In case of a failure, the policy function must throw an error.\n\nNon-Boom errors are wrapped into a `Boom.forbidden` object automatically. The `error.message` field will be returned as part of the 403 error.\n\nBy default all policies are assumed to be pre-handlers unless you specify otherwise.  You can, however, choose to run a policy at any point in the [Hapi request life cycle](http://hapijs.com/api#request-lifecycle) by specifying one of the event names that Hapi provides.  If you would like additional information about events that are called in the Hapi request life cycle, please refer to the [Hapi documentation](http://hapijs.com/api#request-lifecycle).\n\nThe events in the life cycle are:\n\n1. 'onRequest'\n2. 'onPreAuth'\n3. 'onPostAuth'\n4. 'onPreHandler'\n5. 'onPostHandler'\n6. 'onPreResponse'\n\nPost handlers can alter the response created by the response handler before it gets sent.  This is useful if you want to add additional data to the response before it goes out on the wire.  The response can be found in `request.response.source`, **only** after the request handler has run.  Before that time there is no response object.\n\n#### Loading many policies from a file\n\nA single file can contain multiple policies, if it exports them in the exports object.\n\n```javascript\nmodule.exports = {\n    myPolicy1 : async function(request, h) { ... },\n    myPolicy2 : async function(request, h) { ... },\n    ...\n};\n```\n\n#### Adding named policies programmatically\n\n```javascript\nserver.plugins.addPolicy('myPolicy1', async function(request, h) { ... });\n```\n\n#### Check if policy exists\n\n```javascript\nserver.plugins.hasPolicy('myPolicy'); // true | false\n```\n\n#### Apply to routes\n\nNow that you've created your policy, apply it to whatever routes you want.\n\n```javascript\nconst routes = [\n   {\n       method: 'your_method',\n       path: '/your/path/here',\n       handler: your_route_handler,\n       options: {\n           plugins: {\n               policies: ['isAdmin']\n           }\n       }\n   }\n];\n```\n\n##### Specifying policies dynamically as functions\n\nIn the `config.plugins.policies` array you can also include raw policy functions.\n\n```javascript\nconst isAdminPolicy = async function isAdmin (request, h) {\n\n    if (hasAdminAccess(request)) {\n        return h.continue;\n    }\n\n    throw Boom.forbidden();\n};\n\nisAdminPolicy.applyPoint = 'onPreHandler';\n\nconst routes = [\n   {\n       method: 'your_method',\n       path: '/your/path/here',\n       handler: your_route_handler,\n       options: {\n           plugins: {\n               policies: [ isAdminPolicy ]\n           }\n       }\n   }\n];\n```\n\nThis can be used with currying to great effect.  For instance, a `hasRole` function can be used with policies with a variety of different named roles without creating separate functions for each type of role.\n\n```javascript\nconst hasRole = function(roleName) {\n\n    const hasSpecificRole = async function hasSpecificRole (request, h) {\n\n        if (hasRole(request, roleName)) {\n            return h.continue;\n        }\n\n        throw Boom.forbidden();\n    };\n\n    hasSpecificRole.applyPoint = 'onPreHandler';\n\n    return hasSpecificRole;\n};\n\nconst routes = [\n   {\n       method: 'your_method',\n       path: '/your/path/here',\n       handler: your_route_handler,\n       options: {\n           plugins: {\n               policies: [ hasRole('user') ]\n           }\n       }\n   }\n];\n```\n\n##### Running ONLY dynamic policies\n\nIf you want to only assign policies dynamically by passing functions to the `policies` config option, this presents a small problem for MrHorse.  In order to not impact the efficiency of Hapi we only run our policy handlers on life cycle hooks when necessary, but due to the way dynamic policies are loaded, we can't determine which hooks are going to be needed ahead of time.\n\nIf you want to only use dynamic policies, then you'll need to give MrHorse a bit of a clue, by manually telling it which life cycle hooks to watch for.  Yes, this *DOES* include the `onPreHandler` hook.\n\nTo provide the needed clue add the `watchApplyPoints` option to your plugin options, with an array of the apply points you will be using.\n\n```javascript\nawait server.register({\n        plugin: require('mrhorse'),\n        options: {\n            watchApplyPoints: ['onPreHandler', 'onPostHandler']\n        }\n    });\n```\n\n##### Running policies in parallel\n\nIf you'd like to run policies in parallel, you can specify a list of loaded policies' names as an array or as individual arguments to `MrHorse.parallel`.  When policies are run in parallel, expect all policies to complete.  If any of the policies throw an error, the error response from the left-most policy that was rejected will be returned to the browser.\n\n```javascript\nconst routes = [\n   {\n       method: 'your_method',\n       path: '/your/path/here',\n       handler: your_route_handler,\n       options: {\n           plugins: {\n               policies: [\n                    'isFarmer',\n                    ['eatsFruit', 'eatsVegetables']\n                ]\n           }\n       }\n   }\n];\n```\n\nor equivalently,\n\n```javascript\nconst routes = [\n   {\n       method: 'your_method',\n       path: '/your/path/here',\n       handler: your_route_handler,\n       options: {\n           plugins: {\n               policies: [\n                    'isFarmer',\n                    MrHorse.parallel('eatsFruit', 'eatsVegetables')\n                ]\n           }\n       }\n   }\n];\n```\n\n`MrHorse.parallel` optionally accepts a custom error handler as its final argument.  This may be used to aggregate errors from multiple policies into a single custom error or message.  The signature of this function is `handler(ranPolicies, results)`.\n\nIf custom error handler is used, the custom error handler **must throw** in the cases it detects any reason to reject the policy. If the error handler function returns without throwing an error, the parallel policy will be considered satisfied.\n\n* `ranPolicies` is an array of the names of the policies that were run, with original listed order maintained.\n* `results` is an object whose keys are the names of the individual listed policies that ran, and whose values are objects of the format,\n  * `err:` the error thrown by the policy\n  * `status:` a field indicating whether the policy passed (`'ok'`) or not (`'error'`)\n\n##### Conditional Policies\n\nNormally all policies must be satisfied.\n\nMrHorse exposes `MrHorse.orPolicy()` function to provide an easy way to define a set of policies of which **at least one** must be satisfied.\nThe tests are run in parallel. Error messages from unsatisfied policies are ignored, as long as at least one listed policy is satisfied.\n\nIf all policies are unsatisfied, the request is rejected with the error message of the left-most policy.\n\n```javascript\nconst MrHorse = require('mrhorse');\n\nconst routes = [\n   {\n       method: 'your_method',\n       path: '/your/path/here',\n       handler: your_route_handler,\n       options: {\n           plugins: {\n               policies: [\n                    'isAnimal', // must be satisfied\n                    MrHorse.orPolicy('isMammal', 'isReptile', 'isInsect'), // at least ONE must be satisfied\n                    ['isBird', 'isBluejay'] // ALL must be satisfied\n                ]\n           }\n       }\n   }\n];\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmark-bradshaw%2Fmrhorse","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmark-bradshaw%2Fmrhorse","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmark-bradshaw%2Fmrhorse/lists"}