├── .editorconfig ├── .eslintignore ├── .github ├── FUNDING.yml └── workflows │ └── CI.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── codecov.yml ├── lib ├── index.d.ts ├── index.js └── index.test.js ├── package-lock.json └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: vvo 4 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - next 8 | pull_request: 9 | branches: 10 | - main 11 | - next 12 | jobs: 13 | ci: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | - name: Read Node.js version to install from `.nvmrc` 20 | run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)" 21 | id: nvm 22 | - name: Install required Node.js version 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: "${{ steps.nvm.outputs.NVMRC }}" 26 | - uses: actions/cache@v2 27 | with: 28 | path: ~/.npm 29 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-node- 32 | - run: npm install -g npm@7.24.0 33 | - run: npm ci 34 | - run: npm test 35 | - name: Code coverage 36 | uses: codecov/codecov-action@v1 37 | - name: Release 38 | if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/next') 39 | run: npx semantic-release 40 | env: 41 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 42 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules/ 3 | /coverage/ 4 | 5 | # misc 6 | .DS_Store 7 | /dist/ 8 | 9 | # debug 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.10.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.* 2 | !*.css 3 | !*.js 4 | !*.json 5 | !*.jsx 6 | !*.less 7 | !*.md 8 | !*.mdx 9 | !*.ts 10 | !*.tsx 11 | !*.yml 12 | /coverage/ 13 | /dist/ 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "Orta.vscode-jest"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "eslint.packageManager": "npm", 4 | "eslint.alwaysShowStatus": true, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "[markdown]": { 7 | "editor.formatOnSave": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) CodeAgain SASU https://codeagain.com/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iron-store [![GitHub license](https://img.shields.io/github/license/vvo/iron-store?style=flat)](https://github.com/vvo/iron-store/blob/main/LICENSE) [![Tests](https://github.com/vvo/iron-store/workflows/CI/badge.svg)](https://github.com/vvo/iron-store/actions) [![codecov](https://codecov.io/gh/vvo/iron-store/branch/main/graph/badge.svg)](https://codecov.io/gh/vvo/iron-store) ![npm](https://img.shields.io/npm/v/iron-store) 2 | 3 | _🧿 in-memory, signed and encrypted JavaScript store_ 4 | 5 | **UPDATE FROM MAINTAINER**: You can now use https://github.com/vvo/iron-session/ directly and its methods `sealData` `unsealData` which will provide the same functionnality than iron-store. 6 | 7 | --- 8 | 9 | This is a low-level module that you can use to implement signed and encrypted sessions using cookies for example, like [`next-iron-session`](https://github.com/vvo/next-iron-session) does. 10 | 11 | Signature and encryption is based on [@hapi/iron](https://hapi.dev/family/iron/). 12 | 13 | Use https://1password.com/password-generator/ to generate strong passwords. 14 | 15 | You can use multiple passwords (password rotation). 16 | 17 | ```bash 18 | npm add iron-store 19 | ``` 20 | 21 | ## Examples 22 | 23 | **Creating a store with sealed data (_encrypt_)**: 24 | 25 | ```js 26 | import ironStore from "iron-store"; 27 | 28 | const store = await ironStore({ 29 | password: "generated_complex_password_at_least_32_characters_long", 30 | }); 31 | store.set("user", { id: 80, admin: true }); 32 | const seal = await store.seal(); 33 | // 34 | ``` 35 | 36 | **Creating a store from previously sealed data (_decrypt_)**: 37 | 38 | ```js 39 | import ironStore from "iron-store"; 40 | 41 | const store = await ironStore({ 42 | password: "generated_complex_password_at_least_32_characters_long", 43 | sealed: "seal_obtained_from_previous_store.seal()_call", 44 | }); 45 | const user = store.get("user"); 46 | console.log(user); 47 | // { id:80, admin:true } 48 | ``` 49 | 50 | **Creating a store using multiple passwords (_password rotation_)**: 51 | 52 | You can implement password rotation by providing an array of passwords and ids. The id can be a string (letters, numbers and \_) or just a number. 53 | 54 | The first password in the array is always the one used to `seal` data. All the other passwords are used to decrypt data. 55 | 56 | Note: The `id` is mandatory and part of the seal, so that we can know in advance which password to use when decrypting. You need to use unique ids. You cannot reuse a id for a different password. 57 | 58 | Note: If you started to use the `string` form of password, you can always move to an `array` of password objects. The `string` form of your password is internally handled as `{ id: 1, password }`. 59 | 60 | ```js 61 | import ironStore from "iron-store"; 62 | 63 | const store = await ironStore({ 64 | password: [ 65 | { 66 | id: 2, 67 | password: "generated_complex_password_at_least_32_characters_long", 68 | }, 69 | { 70 | id: 1, 71 | password: "generated_complex_password_at_least_32_characters_long", 72 | }, 73 | ], 74 | }); 75 | store.set("user", { id: 80, admin: true }); 76 | const seal = await store.seal(); 77 | ``` 78 | 79 | ## API 80 | 81 | ### ironStore({ [sealed], password, ttl = 0 }) 82 | 83 | ### store.set(name, value) 84 | 85 | ### store.get([name]) 86 | 87 | ### store.setFlash(name, value) 88 | 89 | ### store.unset(name) 90 | 91 | ### store.seal() 92 | 93 | ### store.clear() 94 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: 2 | require_changes: true # only show codecoverage when different 3 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export type StoreOptions = { 2 | sealed?: string; 3 | password: string | { id: number; password: string }[]; 4 | ttl?: number; 5 | }; 6 | 7 | export type Store = { 8 | set: (name: string, value: T) => T; 9 | setFlash: (name: string, value: T) => T; 10 | unset: (name: string) => void; 11 | get: (name?: string) => Record | any; 12 | clear: () => void; 13 | seal: () => Promise; 14 | }; 15 | 16 | export default function ironStore(storeOptions: StoreOptions): Promise; 17 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import Iron from "@hapi/iron"; 2 | import clone from "clone"; 3 | 4 | export default async function ironStore({ sealed, password, ttl = 0 }) { 5 | if (typeof password !== "string" && !Array.isArray(password)) { 6 | throw new Error( 7 | "iron-store: bad `password` format, expected string or array of objects", 8 | ); 9 | } 10 | 11 | const store = 12 | sealed !== undefined 13 | ? await Iron.unseal( 14 | sealed, 15 | normalizePasswordForUnseal(sealed, password), 16 | { 17 | ...Iron.defaults, 18 | ttl, 19 | }, 20 | ) 21 | : { persistent: {}, flash: {} }; 22 | 23 | return { 24 | set(name, value) { 25 | return (store.persistent[name] = clone(value)); 26 | }, 27 | setFlash(name, value) { 28 | return (store.flash[name] = clone(value)); 29 | }, 30 | unset(name) { 31 | delete store.persistent[name]; 32 | delete store.flash[name]; 33 | }, 34 | get(name) { 35 | if (name === undefined) { 36 | const flash = store.flash; 37 | store.flash = {}; 38 | return clone({ 39 | ...flash, 40 | ...store.persistent, 41 | }); 42 | } 43 | 44 | if (store.flash[name] !== undefined) { 45 | const value = store.flash[name]; 46 | delete store.flash[name]; 47 | return value; // no need to clone, as we already removed the reference from the flash store 48 | } else { 49 | return clone(store.persistent[name]); 50 | } 51 | }, 52 | clear() { 53 | store.persistent = {}; 54 | store.flash = {}; 55 | }, 56 | seal() { 57 | const passwordForSeal = Array.isArray(password) 58 | ? { 59 | id: password[0].id, 60 | secret: password[0].password, 61 | } 62 | : { 63 | id: 1, 64 | secret: password, 65 | }; 66 | return Iron.seal(store, passwordForSeal, { ...Iron.defaults, ttl }); 67 | }, 68 | }; 69 | } 70 | 71 | function normalizePasswordForUnseal(sealed, password) { 72 | if (typeof password === "string") { 73 | // sealed data comes from a previous version of iron-store (<= 1.2.0) 74 | if (sealed.startsWith(`${Iron.macPrefix}**`)) { 75 | return password; 76 | } 77 | 78 | return { 1: password }; 79 | } 80 | 81 | return password.reduce((acc, currentPassword) => { 82 | return { 83 | [currentPassword.id]: currentPassword.password, 84 | ...acc, 85 | }; 86 | }, {}); 87 | } 88 | -------------------------------------------------------------------------------- /lib/index.test.js: -------------------------------------------------------------------------------- 1 | import ironStore from "./index.js"; 2 | 3 | const password = "Gbm49ATjnqnkCCCdhV4uDBhbfnPqsCW0"; 4 | 5 | test("it creates a store", async () => { 6 | await expect(ironStore({ password })).resolves.toMatchInlineSnapshot(` 7 | Object { 8 | "clear": [Function], 9 | "get": [Function], 10 | "seal": [Function], 11 | "set": [Function], 12 | "setFlash": [Function], 13 | "unset": [Function], 14 | } 15 | `); 16 | }); 17 | 18 | test("set(name, value)", async () => { 19 | const store = await ironStore({ password }); 20 | expect(store.set("user", { id: 1 })).toMatchInlineSnapshot(` 21 | Object { 22 | "id": 1, 23 | } 24 | `); 25 | }); 26 | 27 | test("get(name)", async () => { 28 | const store = await ironStore({ password }); 29 | store.set("user", { id: 2 }); 30 | expect(store.get("user")).toMatchInlineSnapshot(` 31 | Object { 32 | "id": 2, 33 | } 34 | `); 35 | }); 36 | 37 | test("unset(name, value)", async () => { 38 | const store = await ironStore({ password }); 39 | store.set("state", { id: 1 }); 40 | expect(store.get("state")).toMatchInlineSnapshot(` 41 | Object { 42 | "id": 1, 43 | } 44 | `); 45 | expect(store.unset("state")).toMatchInlineSnapshot(`undefined`); 46 | expect(store.get("state")).toMatchInlineSnapshot(`undefined`); 47 | }); 48 | 49 | test("get()", async () => { 50 | const store = await ironStore({ password }); 51 | store.set("user", { id: 2 }); 52 | expect(store.get()).toMatchInlineSnapshot(` 53 | Object { 54 | "user": Object { 55 | "id": 2, 56 | }, 57 | } 58 | `); 59 | }); 60 | 61 | test("setFlash(name, value)", async () => { 62 | const store = await ironStore({ password }); 63 | store.setFlash("state", "4lk21j4k2l1j4"); 64 | expect(store.get("state")).toMatchInlineSnapshot(`"4lk21j4k2l1j4"`); 65 | expect(store.get("state")).toMatchInlineSnapshot(`undefined`); 66 | }); 67 | 68 | test("it seals data", async () => { 69 | const store = await ironStore({ password }); 70 | store.set("user", { id: 3 }); 71 | const seal = await store.seal(); 72 | expect(typeof seal).toBe("string"); 73 | expect(seal.length).toBe(271); // we can't test the actual value are there's a random crypto/timestamp in it 74 | }); 75 | 76 | test("it reads sealed data", async () => { 77 | // seal obtained from previous test using console.log, 78 | const sealed = 79 | "Fe26.2*1*1753454b45c36dc0f6600729a7be3c4460a227591631085f6a3c89765c08861a*K_i9AvjRlY79Vo999BMnJQ*jlfcl2VRtUNxAN2Wj-jrj3c26EppmxC4aQQzIuDLJhBWIdO8l1nGgF37l2iLqU7G**4063522d0f819c84e8ce439924a58358ef6883a0c7f7d03394a4e899161cbcb9*cEQvS7FdHFx74uyGEntKtuA6Vq2wY9PJ746qosLjams"; 80 | const store = await ironStore({ password, sealed }); 81 | expect(store.get("user")).toMatchInlineSnapshot(` 82 | Object { 83 | "id": 3, 84 | } 85 | `); 86 | }); 87 | 88 | test("Data is cloned on set", async () => { 89 | const store = await ironStore({ password }); 90 | const user = { id: 1200, admin: true }; 91 | store.set("user", user); 92 | expect(store.get()).toMatchInlineSnapshot(` 93 | Object { 94 | "user": Object { 95 | "admin": true, 96 | "id": 1200, 97 | }, 98 | } 99 | `); 100 | user.id = 2200; 101 | expect(store.get()).toMatchInlineSnapshot(` 102 | Object { 103 | "user": Object { 104 | "admin": true, 105 | "id": 1200, 106 | }, 107 | } 108 | `); 109 | }); 110 | 111 | test("Data is cloned on get", async () => { 112 | const store = await ironStore({ password }); 113 | const user = { id: 1700, admin: true }; 114 | store.set("user", user); 115 | const sessionUser = store.get("user"); 116 | sessionUser.id = 3400; 117 | expect(store.get()).toMatchInlineSnapshot(` 118 | Object { 119 | "user": Object { 120 | "admin": true, 121 | "id": 1700, 122 | }, 123 | } 124 | `); 125 | }); 126 | 127 | test("store.clear()", async () => { 128 | const store = await ironStore({ password }); 129 | const user = { id: 2000, admin: true }; 130 | store.set("user", user); 131 | expect(store.get()).toMatchInlineSnapshot(` 132 | Object { 133 | "user": Object { 134 | "admin": true, 135 | "id": 2000, 136 | }, 137 | } 138 | `); 139 | store.clear(); 140 | expect(store.get()).toMatchInlineSnapshot(`Object {}`); 141 | }); 142 | 143 | test("it allows for multiple passwords (password rotation)", async () => { 144 | const firstPassword = [ 145 | { 146 | id: 1, 147 | password: "MJsZcjVkJKDoH8f35dAJNVWMbR8Z0cBr", 148 | }, 149 | ]; 150 | const secondPassword = [ 151 | { 152 | id: 2, 153 | password: "xgzqACsoFxwgg95DTkVi1wT0U0zfZu39", 154 | }, 155 | { 156 | id: 1, 157 | password: "MJsZcjVkJKDoH8f35dAJNVWMbR8Z0cBr", 158 | }, 159 | ]; 160 | 161 | const firstStore = await ironStore({ password: firstPassword }); 162 | firstStore.set("user", { id: 200 }); 163 | const firstSeal = await firstStore.seal(); 164 | 165 | const secondStore = await ironStore({ 166 | sealed: firstSeal, 167 | password: secondPassword, 168 | }); 169 | expect(secondStore.get("user")).toMatchInlineSnapshot(` 170 | Object { 171 | "id": 200, 172 | } 173 | `); 174 | }); 175 | 176 | test("it allows for multiple passwords, even when first password was a regular string", async () => { 177 | const firstPassword = "QK317mtQky71D5MEd5BDPXNEEAPwAmnQ"; 178 | const secondPassword = [ 179 | { 180 | id: 2, 181 | password: "Eza3mXLurdtg91yUbAcCKbWK8nqwGhhW", 182 | }, 183 | { 184 | id: 1, 185 | password: "QK317mtQky71D5MEd5BDPXNEEAPwAmnQ", 186 | }, 187 | ]; 188 | 189 | const firstStore = await ironStore({ password: firstPassword }); 190 | firstStore.set("user", { id: 220 }); 191 | const firstSeal = await firstStore.seal(); 192 | 193 | const secondStore = await ironStore({ 194 | sealed: firstSeal, 195 | password: secondPassword, 196 | }); 197 | expect(secondStore.get("user")).toMatchInlineSnapshot(` 198 | Object { 199 | "id": 220, 200 | } 201 | `); 202 | }); 203 | 204 | test("it always uses first password from list for seal", async () => { 205 | const firstPassword = [ 206 | { 207 | id: 2, 208 | password: "kimJoUsybVCw2hKZ3RN5j5FGjo33KiDt", 209 | }, 210 | { 211 | id: 1, 212 | password: "voNwDrdCnmMNLNQBPUcE9mQcgBCfjYZu", 213 | }, 214 | ]; 215 | const secondPassword = [ 216 | { 217 | id: 2, 218 | password: "kimJoUsybVCw2hKZ3RN5j5FGjo33KiDt", 219 | }, 220 | ]; 221 | 222 | const firstStore = await ironStore({ password: firstPassword }); 223 | firstStore.set("user", { id: 240 }); 224 | const firstSeal = await firstStore.seal(); 225 | 226 | const secondStore = await ironStore({ 227 | sealed: firstSeal, 228 | password: secondPassword, 229 | }); 230 | expect(secondStore.get("user")).toMatchInlineSnapshot(` 231 | Object { 232 | "id": 240, 233 | } 234 | `); 235 | }); 236 | 237 | test("it reads sealed data not containing password ids", async () => { 238 | // seal obtained from a previous test using console.log, 239 | // this seal does not contains a password id since it was created before 240 | // password ids were automatically added by the library (<= 1.2.0) 241 | // but it still works and is tested. Allowing for non breaking update 242 | const sealed = 243 | "Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw"; 244 | const store = await ironStore({ password, sealed }); 245 | expect(store.get("user")).toMatchInlineSnapshot(` 246 | Object { 247 | "id": 3, 248 | } 249 | `); 250 | }); 251 | 252 | test("it throws when trying to decrypt single passwords seals from iron-store <= 1.2.0 using multi passwords", async () => { 253 | // This test the situation where we're trying to move from single password seals (<= 1.2.0) to 254 | // multi passwords. There's no good way I can think of to do this well so we'll just throw instead 255 | // Session libraries should re-create a session when this happens 256 | // This is an edge case that won't exist for new users of the library or people that never move from old 257 | // version single password to new version multi passwords 258 | const sealed = 259 | "Fe26.2**4e769b9b7b921621ed5658cfc0d7d8e267dc8ee93663c2803c257b31111394e3*jRXOJHmt_BDG9nNTXcVRXQ*UHpK9GYp7SXTiEsxTzTUq_tQD_-ZUp7PguEXy-bRFuBE4fW74-9wm9UtlWO2rlwB**d504d6d197d183efec0ae6d3c2378c43048c8752d6c3c591c92289ed01142b3c*3NG2fCo8A53CXPU8rEAMnDB7X9UkwzTaHieumPBqyTw"; 260 | 261 | await expect(async function () { 262 | await ironStore({ password: [{ id: 1, password }], sealed }); 263 | }).rejects.toThrowErrorMatchingInlineSnapshot(`"Cannot find password: "`); 264 | }); 265 | 266 | test("it throws on password bad format", async () => { 267 | await expect(async function () { 268 | await ironStore({ password: 12321 }); 269 | }).rejects.toThrowErrorMatchingInlineSnapshot( 270 | `"iron-store: bad \`password\` format, expected string or array of objects"`, 271 | ); 272 | }); 273 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iron-store", 3 | "version": "0.0.0-development", 4 | "private": false, 5 | "description": "in-memory, signed and encrypted JavaScript store", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/vvo/iron-store.git" 9 | }, 10 | "license": "MIT", 11 | "author": "Vincent Voyer ", 12 | "main": "dist/", 13 | "types": "lib/index.d.ts", 14 | "files": [ 15 | "dist/", 16 | "lib/", 17 | "LICENSE", 18 | "README.md" 19 | ], 20 | "scripts": { 21 | "build": "babel lib/ -d dist/ --source-maps --ignore '**/*.test.js' --delete-dir-on-start", 22 | "format": "prettier --write '**/*.*' && eslint . --fix", 23 | "lint": "prettier --check '**/*.*' && eslint .", 24 | "prepublishOnly": "npm run build", 25 | "semantic-release": "semantic-release", 26 | "test": "jest --coverage && npm run lint" 27 | }, 28 | "babel": { 29 | "presets": [ 30 | [ 31 | "@babel/preset-env", 32 | { 33 | "targets": { 34 | "node": "12" 35 | } 36 | } 37 | ] 38 | ] 39 | }, 40 | "prettier": { 41 | "trailingComma": "all" 42 | }, 43 | "eslintConfig": { 44 | "env": { 45 | "es6": true, 46 | "jest": true, 47 | "node": true 48 | }, 49 | "parser": "babel-eslint", 50 | "parserOptions": { 51 | "ecmaVersion": 2019, 52 | "sourceType": "module" 53 | }, 54 | "plugins": [ 55 | "jest" 56 | ], 57 | "extends": [ 58 | "eslint:recommended", 59 | "plugin:jest/recommended", 60 | "plugin:import/recommended" 61 | ], 62 | "rules": { 63 | "arrow-body-style": [ 64 | "error", 65 | "always" 66 | ], 67 | "curly": "error", 68 | "import/order": [ 69 | "error", 70 | { 71 | "newlines-between": "always" 72 | } 73 | ] 74 | } 75 | }, 76 | "jest": {}, 77 | "dependencies": { 78 | "@hapi/iron": "^6.0.0", 79 | "clone": "^2.1.2" 80 | }, 81 | "devDependencies": { 82 | "@babel/cli": "7.15.7", 83 | "@babel/core": "7.15.8", 84 | "@babel/preset-env": "7.15.8", 85 | "@types/jest": "27.0.3", 86 | "babel-eslint": "10.1.0", 87 | "babel-jest": "27.2.5", 88 | "eslint": "7.32.0", 89 | "eslint-plugin-import": "2.25.2", 90 | "eslint-plugin-jest": "24.5.2", 91 | "jest": "27.2.5", 92 | "jest-date-mock": "1.0.8", 93 | "prettier": "2.4.1", 94 | "prettier-plugin-packagejson": "2.2.13", 95 | "semantic-release": "18.0.1", 96 | "semantic-release-cli": "5.4.4" 97 | }, 98 | "release": { 99 | "branches": [ 100 | "main", 101 | "next" 102 | ] 103 | }, 104 | "renovate": { 105 | "extends": [ 106 | "config:js-lib", 107 | ":automergePatch", 108 | ":automergeBranch", 109 | ":automergePatch", 110 | ":automergeBranch", 111 | ":automergeLinters", 112 | ":automergeTesters", 113 | ":automergeTypes" 114 | ], 115 | "timezone": "Europe/Paris", 116 | "schedule": [ 117 | "before 3am on Monday" 118 | ], 119 | "groupName": "all" 120 | } 121 | } 122 | --------------------------------------------------------------------------------