{"id":14975629,"url":"https://github.com/deliciousinsights/mongoose-pii","last_synced_at":"2025-10-27T14:30:35.503Z","repository":{"id":46940710,"uuid":"150718277","full_name":"deliciousinsights/mongoose-pii","owner":"deliciousinsights","description":"A Mongoose plugin that lets you transparently cipher stored PII and use securely-hashed passwords","archived":false,"fork":false,"pushed_at":"2023-01-04T21:38:13.000Z","size":1746,"stargazers_count":44,"open_issues_count":14,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-10-30T08:16:29.271Z","etag":null,"topics":["bcrypt","mongodb","mongoose","mongoose-plugin","password","passwords","pii","pii-ciphering","security"],"latest_commit_sha":null,"homepage":"https://deliciousinsights.github.io/mongoose-pii/","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/deliciousinsights.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-09-28T09:36:27.000Z","updated_at":"2024-02-26T00:20:18.000Z","dependencies_parsed_at":"2023-02-02T20:45:34.701Z","dependency_job_id":null,"html_url":"https://github.com/deliciousinsights/mongoose-pii","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deliciousinsights%2Fmongoose-pii","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deliciousinsights%2Fmongoose-pii/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deliciousinsights%2Fmongoose-pii/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/deliciousinsights%2Fmongoose-pii/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/deliciousinsights","download_url":"https://codeload.github.com/deliciousinsights/mongoose-pii/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238508525,"owners_count":19484149,"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":["bcrypt","mongodb","mongoose","mongoose-plugin","password","passwords","pii","pii-ciphering","security"],"created_at":"2024-09-24T13:52:18.580Z","updated_at":"2025-10-27T14:30:35.112Z","avatar_url":"https://github.com/deliciousinsights.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Mongoose PII Plugin\n\n[![npm version](https://badge.fury.io/js/mongoose-pii.svg)](https://npmjs.com/package/mongoose-pii)\n[![MIT license](https://img.shields.io/github/license/deliciousinsights/mongoose-pii.svg)](https://en.wikipedia.org/wiki/MIT_License)\n[![Travis build](https://img.shields.io/travis/deliciousinsights/mongoose-pii.svg)](https://travis-ci.org/deliciousinsights/mongoose-pii)\n[![CodeCov Code Coverage score](https://img.shields.io/codecov/c/github/deliciousinsights/mongoose-pii.svg)](https://codecov.io/gh/deliciousinsights/mongoose-pii)\n\u003c!-- [![CodeClimate Code Coverage score](https://img.shields.io/codeclimate/coverage/deliciousinsights/mongoose-pii.svg)](https://codeclimate.com/github/deliciousinsights/mongoose-pii) --\u003e\n\n![Dependencies freshness](https://img.shields.io/david/deliciousinsights/mongoose-pii.svg)\n[![Greenkeeper badge](https://badges.greenkeeper.io/deliciousinsights/mongoose-pii.svg)](https://greenkeeper.io/)\n\n[![CodeClimate maintainability score](https://img.shields.io/codeclimate/maintainability/deliciousinsights/mongoose-pii.svg)](https://codeclimate.com/github/deliciousinsights/mongoose-pii)\n[![Coding style is StandardJS-based](https://img.shields.io/badge/style-standard-brightgreen.svg)](https://standardjs.com/)\n[![Code of Conduct is Contributor Covenant](https://img.shields.io/badge/code%20of%20conduct-contributor%20covenant-brightgreen.svg)](http://contributor-covenant.org/version/1/4/)\n\n## TL;DR\n\nStore your data like your MongoDB database is getting stolen tomorrow, without sacrificing Mongoose comfort.\n\n## The slightly longer intro\n\nBest practices for data storage dictate that:\n\n1. **Passwords should be securely hashed**; the typical state of the art right now being BCrypt with a securely-random IV and 10+ rounds (e.g. 2\u003csup\u003e10\u003c/sup\u003e+ iterations) in production.\n2. **PII should be securely ciphered**; typically we'd use AES256.\n\nThese help avoid access to cleartext passwords and compromission of PII (_Personally Identifiable Information_, such as e-mails, names, Social Security Numbers, Driver’s License information, Passport numbers…) by database theft or unauthorized direct access.\n\nThis is all good, but we want to retain the comfort of authenticating, in our code, with cleartext password values that were typed in a form or sent in the API call; we also want to be able to query based on PII fields using cleartext values, or to update them with cleartext values.\n\nIn short, we want secure storage without having to worry about it.\n\nThis plugin does exactly that.\n\n## In this document\n\n1. [Installing](#installing)\n2. [API](#api)\n3. [Caveats](#caveats)\n4. [Contributing](#contributing)\n5. [License and copyright](#license-and-copyright)\n\n## Installing\n\nIf you’re using npm:\n\n```bash\nnpm install mongoose-pii\n# or npm install --save mongoose-pii if you're running npm \u003c 5.x\n```\n\nWith yarn:\n\n```bash\nyarn add mongoose-pii\n```\n\n## Quick start\n\n### First, prep your schemas\n\nFor every schema that has PII, passwords, or both:\n\n1. open the file that define your schema\n2. Require the plugin\n3. Register it as a schema plugin, providing relevant field lists and, for ciphering PII, the ciphering key.\n\nHere’s what it could look like:\n\n```js\n// 2. Require the plugin\nconst { markFieldsAsPII } = require('mongoose-pii')\n\nconst userSchema = new Schema({\n  address: String,\n  email: { type: String, required: true, index: true },\n  firstName: String,\n  lastName: String,\n  password: { type: String, required: true },\n  role: String,\n})\n\n// 3. Register the plugin\nuserSchema.plugin(markFieldsAsPII, {\n  fields: ['address', 'email', 'firstName', 'lastName'],\n  key: process.env.MONGOOSE_PII_KEY,\n  passwordFields: 'password',\n})\n\nconst User = mongoose.model('User', userSchema)\n```\n\nThat’s it! Now…\n\n- **Your PII fields will be automatically ciphered at save and deciphered at load** (so in-memory, they’re cleartext), and you can use cleartext values for queries and updates on them.\n- **Your PII fields will be automatically ciphered in query arguments** for finders (e.g. `findOne()`) and updaters (e.g. `updateMany()`, `findOneAndUpdate()`).\n- **Your password fields will be automatically hashed** in a secure manner at save. This is a one-way hash, so you’ll never have access to the cleartext again, which is as it should be. To authenticate, use the plugin-provided `authenticate()` static method:\n\n```js\nconst user = await User.authenticate({\n  email: 'foo@bar.com',\n  password: 'secret',\n})\n```\n\nIn its default mode, this resolves to either `null`, or the first matching `User` document.\n\n### Second, convert your existing data\n\nYou’re likely to have a ton of existing, unprotected data in your collections already.  However, the moment you register the plugin with your Mongoose schemas, loading data starts to break down because it expected hashed passwords for authentication and ciphered PII in the database!\n\nIt would be way too detrimental to loading performance to check for the ciphered state of data in the raw loaded document (not to mention heuristics are not universal there), so instead, we provide a helper API for you to convert your existing collections once you registered the plugin with the proper options.\n\nSee the `convertDataForModel()` API below for details.\n\n### Check out our examples!\n\nFind more usage examples in the [`examples`](https://github.com/deliciousinsights/mongoose-pii/tree/master/examples) directory.\n\n----\n\n## API\n\nClick on the API names (or press Return when they have focus) to toggle API documentation for them.\n\n\u003cdetails\u003e\n  \u003csummary\u003emarkFieldsAsPII\u003c/summary\u003e\n\n### `markFieldsAsPII` (the plugin itself)\n\nThis is the core plugin, that you register with any schema you need it for through Mongoose’s `schema.plugin()` API.\n\nIf you want PII ciphering, you’ll need to pass the `fields` and `key` options. If you want password hashing, you’ll need to pass the `passwordsFields` option. You can mix both, naturally.\n\nPassing no option is an invalid use and will trigger the appropriate exception.\n\n**Options**\n\n| Name             | Default | Description                                                                                                                                                                                                                                                                                                                                                                                                                                           |\n| ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `fields`         | `[]`    | A list of PII fields to be ciphered. Can be provided either as an array of field names, or as a `String` listing fields separated by commas and/or whitespace, depending on your personal style and convenience.                                                                                                                                                                                                                                      |\n| `key`            | none    | **Required for PII ciphering**. This is either a `String` or a `Buffer` that contains the ciphering key. The value should **never be stored in code**, especially it should **not be versioned**, and is expected to come from an environment variable.  Because we’re using AES-256 for ciphering, **the key needs to be 32-byte long**, hence a 32-character `String` (regardless of its contents, hex or otherwise), or a `Buffer` with 32 bytes. |\n| `passwordFields` | `[]`    | **Required for password hashing**. A list of password fields to hash; 99% of the time we expect this to just be `'password'` or some such (that is, single-field).  The format is identical to `fields`.                                                                                                                                                                                                                                              |\n\n**Example calls**\n\nThese all assume a required plugin and a Mongoose Schema stored as `schema`, something like:\n\n```js\nconst { markFieldsAsPII } = require('mongoose-pii')\nconst { Schema } = require('mongoose')\n\nconst schema = new Schema({\n  // …\n})\n```\n\nBased on this, let’s start with PII only:\n\n```js\nschema.plugin(markFieldsAsPII, {\n  fields: ['address', 'city', 'email', 'ssn', 'lastName'],\n  key: process.env.MONGOOSE_PII_KEY,\n})\n```\n\nPassword hashing only, using the `String` form for field lists:\n\n```js\nschema.plugin(markFieldsAsPII, { passwordFields: 'password' })\n```\n\nMixed use, using only `String` forms:\n\n```js\nschema.plugin(markFieldsAsPII, {\n  fields: 'address city email ssn lastName',\n  key: process.env.MONGOOSE_PII_KEY,\n  passwordFields: 'password',\n})\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003eauthenticate(query[, options])\u003c/summary\u003e\n\n### `authenticate(query[, options])`\n\n\u003e **Important note about password hashing:** When you use password hashing, authenticating cannot be done at the MongoDB query level, because password hashes are intentionally unstable: hashing the same clear-text password multiple times will yield different values every time.\n\u003e\n\u003e Unlike PII ciphering, that we made intentionally stable, allowing for query-based filtering, we thus need to grab all documents matching the parts of `query` that do not relate to password fields, then check each matching document for password fields match using secure (fixed-time) Bcrypt-aware comparison methods.\n\nIn order to make the API as unobtrusive as possible, we require a single query field; the plugin will distinguish between parts of the query that match your `passwordFields` settings, and the remainder, that will be used as a regular query (possibly ciphered for PII fields it may contain).\n\n**Beware: this method is asynchronous** and returns a Promise.  You can use a `.then()` chain or, better yet, make the call site an `async` function if it isn’t yet, and use a simple `await` on the call.  Asynchrony is a given considering this does a database fetch, anyway, but password checking is asynchronous too, FWIW.\n\n**Options**\n\n|Name|Default|Description|\n|-|-|-|\n|`single`|`true`|Whether to return a single Document (or `null` if none is found), or *all matching documents* (with an empty Array if none is found). Defaults to single-document mode, which is expected to be the vast majority of use cases, and makes for convenient truthiness of the result value.|\n\n**Example call**\n\nSay `User` is a Mongoose model built based on a schema with a `password` hashed field:\n\n```js\nasync function logIn(req, res) {\n  try {\n    const { email, password } = req.body\n    const user = await User.authenticate({ email, password })\n    if (!user) {\n      req.flash('warning', 'No user matches these credentials')\n      res.render('sessions/new')\n      return\n    }\n\n    req.logIn(user)\n    req.flash('success', `Welcome back, ${user.firstName}!`)\n    res.redirect(paths.userDashboard)\n  } catch (err) {\n    req.flash('error', `Authentication failed: ${err.message}`)\n    res.redirect(paths.logIn)\n  }\n}\n```\n\u003c/details\u003e\n\n### Helper functions\n\nThese functions are used internally by the plugin but we thought you’d like to have them around. They’re accessible as named exports from the module, just like the plugin.\n\n\u003cdetails\u003e\n  \u003csummary\u003echeckPassword(clearText, hashed)\u003c/summary\u003e\n\n### `checkPassword(clearText, hashed)`\n\nAsynchronously checks that a given cleartext matches the provided hash.  This is asynchronous because depending on the amount of rounds used for the hash, computing a matching hash from cleartext could take nontrivial time and should therefore be nonblocking.\n\nThis returns a Promise that resolves to a Boolean, indicating whether there is a match or not.\n\n**Example call**\n\n```js\nif (await checkPassword('secret', user.password)) {\n  req.flash('warning', 'Your password is a disgrace to privacy')\n}\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003ecipher(key, clearText[, options])\u003c/summary\u003e\n\n### `cipher(key, clearText[, options])`\n\nCiphers a clear-text value using the AES-256-CBC algorithm, with the provided key.  By default, ciphering will derive its IV (*Initialization Vector*) from the cleartext, ensuring stable ciphers, thereby opening the way for query-level ciphered field filtering.\n\nBoth `key` and `clearText` can be either a `String` or `Buffer`.\n\nThis returns the ciphered value as a Base64-encoded `String`, that includes the IV used. Base64 was preferred over hex-encoding as it is 33% more compact, resulting in less data storage requirements.\n\nBecause we expect only short values to be ciphered (PII data are usually small bits of discrete information, such as address lines, names, e-mails or identification numbers), and because AES-256 remains a pretty fast algorithm, this function remains synchronous.\n\n**Options**\n\n|Name|Default|Description|\n|-|-|-|\n|`deriveIV`|`true`|Whether to produce stable ciphers for a given clear-text value (by deriving the IV off it in a secure way), or to use random IVs, which are slightly more secure but prevent querying ciphered fields. Defaults to stable ciphers.|\n\n**Example call**\n\n```js\nconst key = 'I say: kickass keys rule supreme'\ncipher(key, 'I wish all APIs were this nice')\n// =\u003e 'urWDOjnc6EeMv3ASdrerGAn9YIZw3gjO7lve2EzBQ7Qz7uq4b8UsEBRsOCUPfHitA='\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003edecipher(key, cipherText)\u003c/summary\u003e\n\n### `decipher(key, cipherText)`\n\nDeciphers a ciphered value using the AES-256-CBC algorithm, with the provided\nkey.  The ciphered text is assumed to have been ciphered with the sister `cipher()` function, hence to contain the IV.\n\nBoth `key` and `cipherText` can be either a `String` or `Buffer`.\n\nThis returns the clear-text value, unless the ciphered text is invalid, which results in an exception being thrown.\n\nBecause we expect only short values to be ciphered (PII data are usually small bits of discrete information, such as address lines, names, e-mails or identification numbers), and because AES-256 remains a pretty fast algorithm, this function remains synchronous.\n\n**Example call**\n\n```js\nconst key = 'I say: kickass keys rule supreme'\ndecipher(key, 'urWDOjnc6EeMv3ASdrerGAn9YIZw3gjO7lve2EzBQ7Qz7uq4b8UsEBRsOCUPfHitA=')\n// =\u003e 'I wish all APIs were this nice'\ndecipher(key, 'ZdZK5sk5P6BGfQJX9qqvFgBUFhR/OXZtv27LaPeCk7kuGrglgq2BS+jSZU1H34GJs=')\n// =\u003e 'I wish all APIs were this nice' -- this used a non-derived IV\n```\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003ehashPassword(clearText[, options])\u003c/summary\u003e\n\n### `hashPassword(clearText[, options])`\n\nHashes a clear-text password using Bcrypt, with an amount of rounds depending on the current environment (production or otherwise).\n\n\u003e Note: Bcrypt has a rather low (72 bytes) limit on the size of the input it can hash, so this function transparently handles longer inputs for you by turning them into their SHA512 hashes (to preserve entropy as best it can) and using the resulting value as input internally.\n\nDepending on the `sync` option, synchronously returns the hashed value, or returns a Promise resolving to it, to accomodate all use-cases.\n\n**Options**\n\n|Name|Default|Description|\n|-|-|-|\n|`rounds`|2 or 10|How many Bcrypt rounds (powers of 2 for iteration, so 10 rounds is actually 2\u003csup\u003e10\u003c/sup\u003e iterations) to use for hashing.  We use recommended defaults for production (10) or test/development (2).  Still, you can customize it by passing the option.|\n|`sync`|`false`|Whether to synchronously or asynchronously do the hashing. Synchronous returns the hash, asynchronous returns a Promise resolving to the hash. Defaults to asynchronous.|\n\n**Example calls**\n\nAsynchronously, here in the context of caller code that remains old-school Node callback-based:\n\n```js\nasync function demo(newPass, cb) {\n  try {\n    cb(null, await hashPassword(newPass))\n  } catch (err) {\n    cb(err)\n  }\n}\n```\n\nSynchronously, in the same context as above, but blocking instead of nonblocking:\n\n```js\nfunction demo(newPass, cb) {\n  try {\n    cb(null, hashPassword(newPass, { sync: true }))\n  } catch(err) {\n    cb(err)\n  }\n}\n```\n\u003c/details\u003e\n\n### Data migration utility\n\n\u003cdetails\u003e\n  \u003csummary\u003econvertDataForModel(Model[, emitter])\u003c/summary\u003e\n\n### `convertDataForModel(Model[, emitter])`\n\nIn order to facilitate the initial migration of your collections’ raw data, we provide a helper API for you to convert your existing collections once you registered the plugin with the proper options.\n\nHere’s how to go about it, for a given schema:\n\n1. Register the plugin, with all relevant options, on the schema\n2. Write a small script that will establish the underlying connection (if your code doesn't do that automatically on model loading, for instance).\n\nThis returns a promise, so if you’re into `async` / `await` (and you should!), go right ahead.\n\nAs this is likely to be run just once in the terminal, it outputs by default, on `process.stderr`, a simple progress bar (that tops at 100 chars wide but can be narrower if your terminal mandates it).\n\nIf you prefer to control the output, you can pass your own event emitter, as shown in the second example below.\n\n**Example uses**\n\nInteractively in the terminal, with a dynamic progress bar:\n\n```js\nconst YourModel = require('./path-to-your-model')\nconst { convertDataForModel } = require('mongoose-pii/convert')\n\nconvertDataForModel(YourModel)\n  .then((convertedCount) =\u003e console.log(`Converted ${convertedCount} documents`))\n  .catch((error) =\u003e console.error('Failed during the conversion:', error))\n```\n\nUsing our own custom event emitter for reporting:\n\n```js\nconst EventEmitter = require('events')\nconst YourModel = require('./path-to-your-model')\nconst { convertDataForModel } = require('mongoose-pii/convert')\n\nconst emitter = new EventEmitter()\n// This is fired for every successfully-converted Document (1-n)\nemitter.on('docs', (convertedCount) =\u003e { /* … */ })\n// This is fired every time the (rounded-down) process completion percentage changes (1-100)\nemitter.on('progress', (updatedPercentage) =\u003e { /* … */ })\n\nconvertDataForModel(YourModel, emitter)\n  .then((convertedCount) =\u003e console.log(`Converted ${convertedCount} documents`))\n  .catch((error) =\u003e console.error('Failed during the conversion:', error))\n```\n\n\u003c/details\u003e\n\n----\n\n## Caveats\n\nThere are a few things to keep in mind when using this plugin.\n\n### You need to cipher `deleteMany()` queries yourself\n\nMongoose does not yet provide a `deleteMany` hook, which means we’re not auto-ciphering queries used with `deleteMany()`.  If you’re using queries on ciphered fields with it, you currently need to cipher values yourself, using your ciphering key and the helper `cipher()` function we provide (see above), staying in `deriveIV` mode.\n\n### We have to mutate many objects you pass Mongoose methods\n\nMongoose’s plugin API does not let us return updated objects for documents, queries or update descriptors: all we have to work with are the “original” objects, and we have to mutate these.\n\nThis means you should be super-careful to not inadvertently reuse an object you pass Mongoose that contains ciphered or hashed-password fields, as such objects will likely be mangled by the plugin; you’d end up double-ciphering stuff, possibly yell at double-deciphering attempts, too.\n\n### Rotating keys isn’t easy right now\n\nSecurity best practices would mandate that you rotate ciphering keys as time passes, to further reduce the risk of compromission.  However, using a new key would:\n\n- invalidate deciphering of existing PII in the database\n- incorrectly cipher fields in queries, causing empty results or mismatches\n\nThe usual workaround for this is to work with a *keyring*: a small array of keys, most-recent first, where we use the most-recent for writes and all keys for queries.  This is fine for cookie signature scenarios, but it seems to us that this could quickly aggravate queries on the database, and presents challenges in query descriptor mutations or composition to turn otherwise single-value matches into working OR clauses.  If anyone can whip up a good benchmark on this, perf-wise, and a working PR with tests, we’d love it!\n\n### Be wary of your maximum field sizes\n\nIn MongoDB, maximum field sizes aren’t very useful…  Still, sometimes you put some maximum length in there, usually based on well-known data formats, such as SSN’s, driver’s license numbers, phone numbers, etc.  Many such fields are PII indeed.\n\nDo remember that ciphering these fields results in 22 characters of IV prefix plus at least 33% more characters than the original data, due to ciphering and Base64 encoding.  Adjust your maximum lengths accordingly, if any.\n\n### Avoid case transforms\n\nWe store all our ciphered and hashed data in Base64 format, which is case-sensitive.  It's nice to normalize such data as e-mail addresses to lowercase, but this will break deciphering in a very big way, as this essentially corrupts the ciphertext.  Make sure you don't have such transforms set on your ciphered or hashed fields.\n\n### We need Node 8.6+, unless you Babelize us\n\nWe use modern ECMAScript, including REST/Spread properties (“Object spread”).  Although it became an official part of the language in ES2018, it’s been available in Node since v8.6.0. Node 8 is currently (October 2018) the Maintenance LTS version, with Node 10 being the Active LTS, and Node 11 out already.  Our `package.json` contains an `engines.node` field requiring `\u003e= 8.6`, in order for npm to display a warning should you install it on a lower version.\n\nStill, if you absolutely must use a version below it (which means you’re on a version that was, or is imminently going to be, End-Of-Lifed: not a wise choice), you can configure Babel to transpile our source, too.\n\nWe’re soon going to dual-publish (using both our native and transpiled source in the module’s package), but still, you should keep your Node runtimes up-to-date, at least with the latest LTS. There’s a lot to gain with this approach.\n\n----\n\n## Contributing\n\nYou want to help?  That’s awesome!  Check out the details of our [contribution process](./CONTRIBUTING.md) (it’s fairly standard).\n\nThis project is run under the [Contributor Covenant](./CODE_OF_CONDUCT.md): make sure you read its dispositions and agree with it before you start contributing.\n\n## License and copyright\n\nThis library is © 2018 Delicious Insights and is MIT licensed. See [LICENSE.md](./LICENSE.md) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeliciousinsights%2Fmongoose-pii","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdeliciousinsights%2Fmongoose-pii","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdeliciousinsights%2Fmongoose-pii/lists"}