diff --git a/.c8rc.json b/.c8rc.json new file mode 100644 index 0000000..98a1ba8 --- /dev/null +++ b/.c8rc.json @@ -0,0 +1,11 @@ +{ + "all": true, + "include": ["keychain.js"], + "reporter": ["text", "lcov"], + "reports-dir": "coverage", + "check-coverage": true, + "lines": 95, + "functions": 90, + "branches": 90, + "statements": 95 +} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6c842b7..26b43f3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,15 +7,50 @@ on: - master jobs: + # Mocked unit + compat suite: runs everywhere, enforces c8 coverage thresholds + # (95% lines / 90% branches / 90% functions) and npm audit. + unit: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + node: [ 20, 22, 24 ] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npm run coverage + - run: npm audit --audit-level=low + if: matrix.node == 22 + + # Full suite, including the real `security` CLI against the login keychain: macOS only. build: runs-on: macos-latest strategy: + fail-fast: false matrix: - node: [ 14, 16, 18 ] + node: [ 20, 22, 24 ] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version: ${{ matrix.node }} - run: npm install - run: npm test + + # The test runner needs Node 20.19+, but the library has no engine + # requirement; this checks it still runs on older Node, with security mocked. + old-node: + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + node: [ 10, 12, 14, 16, 18 ] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node }} + - run: node test/smoke/old-node.js diff --git a/.gitignore b/.gitignore index 91dfed8..97262d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .DS_Store -node_modules \ No newline at end of file +node_modules +coverage/ +.nyc_output/ diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..0c3ee50 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,3 @@ +{ + "timeout": 5000 +} diff --git a/README.md b/README.md index b8b47a0..507e69f 100644 --- a/README.md +++ b/README.md @@ -38,18 +38,36 @@ keychain.setPassword({ account: 'foo', service: 'FooBar.com', type: 'internet', }); ``` +### Promises + +Every method returns a Promise when you leave out the callback: + +```javascript +const keychain = require('keychain'); + +await keychain.setPassword({ account: 'foo', service: 'FooBar', password: 'baz' }); +const pass = await keychain.getPassword({ account: 'foo', service: 'FooBar' }); +console.log('Password is', pass); +// Prints: Password is baz +``` + +The Promise resolves with the value the callback would get and rejects with the same error objects (see [Errors](#errors)). Pass a callback and nothing changes from earlier versions: the method calls it and returns `undefined`. + ## Usage -The available +The available methods: + ```js -keychain.setPassword(options[, callback]); -keychain.getPassword(options, callback); -keychain.deletePassword(options[, callback]); -keychain.createKeychain(options[, callback]); -keychain.deleteKeychain(options[, callback]); -keychain.setDefaultKeychain(options[, callback]); +keychain.setPassword(options[, callback]); // resolves with the password +keychain.getPassword(options[, callback]); // resolves with the password +keychain.deletePassword(options[, callback]); // resolves with undefined +keychain.createKeychain(options[, callback]); // resolves with the keychain name +keychain.deleteKeychain(options[, callback]); // resolves with the keychain name +keychain.setDefaultKeychain(options[, callback]); // resolves with the keychain name ``` +Without a callback, each returns a Promise. Earlier versions ignored errors when no callback was passed, so these Promises are marked handled: code that never awaits them won't start crashing on unhandled rejections, and code that does `await` or `.catch()` still gets the error. + ### Options Available params you can pass to the options object: @@ -60,7 +78,35 @@ Available params you can pass to the options object: | `service` | Specify service name (required) | | `password` | Specify password to be added (required for `setPassword`) | | `type` | The type of password to get or save. Supported values are `generic` and `internet`. See [docs](https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/security.1.html). Default: `generic` | -| `keychainName` | Specify the name of the keychain (required for 'createKeychain' and 'deleteKeychain') | +| `keychainName` | Specify the name of the keychain (required for `createKeychain`, `deleteKeychain` and `setDefaultKeychain`) | + +### Errors + +Errors are instances of the classes on `keychain.constructor.errors`, each with a `code`: + +| Class | `code` | When | +| ----- | ------ | ---- | +| `UnsupportedPlatformError` | `UnsupportedPlatform` | Not running on macOS (password methods) | +| `NoAccountProvidedError` | `NoAccountProvided` | `account` missing | +| `NoServiceProvidedError` | `NoServiceProvided` | `service` missing | +| `NoPasswordProvidedError` | `NoPasswordProvided` | `password` missing | +| `NoKeychainNameProvidedError` | `NoKeychainNameProvided` | `keychainName` missing | +| `PasswordNotFoundError` | `PasswordNotFound` | No matching item, or `security` output that couldn't be read | +| `ServiceFailureError` | `ServiceFailure` | `security` couldn't start or exited with an error (`exitCode` is set for the latter) | + +```javascript +try { + await keychain.getPassword({ account: 'foo', service: 'Missing' }); +} catch (err) { + if (err instanceof keychain.constructor.errors.PasswordNotFoundError) { + // not stored yet + } +} +``` + +## Security + +`keychain` runs `/usr/bin/security` with an argument list, never through a shell. `setPassword` and `createKeychain` send their command to `security -i` on standard input, so the password doesn't show up in the process list (`ps`). The exception is a command `security -i` can't read back intact (a value containing a newline or NUL byte, or a command over about 4 KB): it goes on the command line as in earlier versions. Keychain names are passed after `--`, so a name that starts with `-` isn't read as an option. ## Contributors diff --git a/keychain.js b/keychain.js index 5885acd..cc68047 100644 --- a/keychain.js +++ b/keychain.js @@ -8,9 +8,48 @@ * Module dependencies. */ -var spawn = require('child_process').spawn; +var childProcess = require('child_process'); var noop = function () {}; +function spawn(command, args) { + // Looked up at call time so tests can stub child_process.spawn. + // Always an argument array and never a shell, so option values can't inject commands. + return childProcess.spawn(command, args); +} + +/** + * Run a `security` command whose arguments include a secret without putting + * the secret on the command line, where any local process can read it (ps). + * + * `security -i` reads commands from stdin and splits them itself (split_line in + * Apple's SecurityTool/macOS/security.c): inside double quotes, a backslash + * escapes the next character. Its readline() stops at "\n" or after 4095 + * bytes, and a NUL ends the string, so a line that can't make the trip intact + * is run the old way, on the command line, rather than risk being split into + * a second command. + */ + +var MAX_INTERACTIVE_LINE_BYTES = 4094; + +function quoteInteractiveArg(arg) { + return '"' + String(arg).replace(/[\\"]/g, '\\$&') + '"'; +} + +function spawnWithSecret(command, args) { + var line = args.map(quoteInteractiveArg).join(' '); + + if (/[\n\0]/.test(line) || Buffer.byteLength(line, 'utf8') > MAX_INTERACTIVE_LINE_BYTES) { + return spawn(command, args); + } + + var child = spawn(command, ['-i']); + // If security exits before reading stdin, the write fails with EPIPE; the + // exit code still reaches the 'close' handler. + child.stdin.on('error', noop); + child.stdin.end(line + '\n'); + return child; +} + // Polyfill Buffer.from for Node < 4 that didn't have a #from method if (!Buffer.from) { Buffer.from = function (data, encoding, len) { @@ -105,15 +144,21 @@ KeychainAccess.prototype.getPassword = function(opts, fn) { // // e.g. password '∆˚ˆ©ƒ®∂çµ˚¬˙ƒ®†¥' becomes: // password: 0xE28886CB9ACB86C2A9C692C2AEE28882C3A7C2B5CB9AC2ACCB99C692C2AEE280A0C2A5 - if (/^password: 0x([0-9a-fA-F]+)/.test(password)) { - var hexPassword = password.match(/0x([0-9a-fA-F]+)/, '')[1]; - fn(null, Buffer.from(hexPassword, 'hex').toString()); + var hexMatch = password.match(/^password: 0x([0-9a-fA-F]+)/m); + if (hexMatch) { + fn(null, Buffer.from(hexMatch[1], 'hex').toString()); + return; } // Otherwise the password will be in quotes: // password: "passWithoutSlash" - else { - fn(null, password.match(/"(.*)\"/, '')[1]); + var quotedMatch = password.match(/"(.*)\"/); + if (quotedMatch) { + fn(null, quotedMatch[1]); + return; } + // Output we can't parse: report it rather than throwing inside the event handler. + err = new KeychainAccess.errors.PasswordNotFoundError(); + fn(err, null); } else { err = new KeychainAccess.errors.PasswordNotFoundError(); @@ -160,38 +205,46 @@ KeychainAccess.prototype.setPassword = function(opts, fn) { return; } - var security = spawn(this.executablePath, [ 'add-'+opts.type+'-password', '-a', opts.account, '-s', opts.service, '-w', opts.password ]); var self = this; + var args = [ 'add-'+opts.type+'-password', '-a', opts.account, '-s', opts.service, '-w', opts.password ]; - security.on('error', function(err) { - err = new KeychainAccess.errors.ServiceFailureError(null, err.message); - fn(err, null); - return; - }); + // Exit 45 means the item already exists: delete it and add again, once. + // If the add still fails the same way, report it rather than loop forever. + function add(retried) { + var security = spawnWithSecret(self.executablePath, args); - security.on('close', function(code, signal) { - if (code !== 0) { - if (code == 45) { - self.deletePassword(opts, function(err) { - if (err) { - fn(err); + security.on('error', function(err) { + err = new KeychainAccess.errors.ServiceFailureError(null, err.message); + fn(err, null); + return; + }); + + security.on('close', function(code, signal) { + if (code !== 0) { + if (code == 45 && !retried) { + self.deletePassword(opts, function(err) { + if (err) { + fn(err); + return; + } + + add(true); return; - } - - self.setPassword(opts, fn); + }); + } else { + var msg = 'Security returned a non-successful error code: ' + code; + err = new KeychainAccess.errors.ServiceFailureError(msg); + err.exitCode = code; + fn(err); return; - }); + } } else { - var msg = 'Security returned a non-successful error code: ' + code; - err = new KeychainAccess.errors.ServiceFailureError(msg); - err.exitCode = code; - fn(err); - return; + fn(null, opts.password); } - } else { - fn(null, opts.password); - } - }); + }); + } + + add(false); }; /** @@ -254,6 +307,7 @@ KeychainAccess.prototype.deletePassword = function(opts, fn) { KeychainAccess.prototype.createKeychain = function(opts, fn) { opts = opts || {}; fn = fn || noop; + var err; if (!opts.keychainName) { err = new KeychainAccess.errors.NoKeychainNameProvidedError(); @@ -267,7 +321,7 @@ KeychainAccess.prototype.createKeychain = function(opts, fn) { return; } - var security = spawn(this.executablePath, [ 'create-keychain', '-p', opts.password, opts.keychainName]); + var security = spawnWithSecret(this.executablePath, [ 'create-keychain', '-p', opts.password, '--', opts.keychainName ]); security.on('error', function(err) { err = new KeychainAccess.errors.ServiceFailureError(null, err.message); @@ -298,6 +352,7 @@ KeychainAccess.prototype.createKeychain = function(opts, fn) { KeychainAccess.prototype.deleteKeychain = function(opts, fn) { opts = opts || {}; fn = fn || noop; + var err; if (!opts.keychainName) { err = new KeychainAccess.errors.NoKeychainNameProvidedError(); @@ -305,7 +360,7 @@ KeychainAccess.prototype.deleteKeychain = function(opts, fn) { return; } - var security = spawn(this.executablePath, [ 'delete-keychain', opts.keychainName]); + var security = spawn(this.executablePath, [ 'delete-keychain', '--', opts.keychainName ]); security.on('error', function(err) { err = new KeychainAccess.errors.ServiceFailureError(null, err.message); @@ -337,6 +392,7 @@ KeychainAccess.prototype.deleteKeychain = function(opts, fn) { KeychainAccess.prototype.setDefaultKeychain = function(opts, fn) { opts = opts || {}; fn = fn || noop; + var err; if (!opts.keychainName) { err = new KeychainAccess.errors.NoKeychainNameProvidedError(); @@ -344,7 +400,7 @@ KeychainAccess.prototype.setDefaultKeychain = function(opts, fn) { return; } - var security = spawn(this.executablePath, [ 'default-keychain', '-s', opts.keychainName]); + var security = spawn(this.executablePath, [ 'default-keychain', '-s', '--', opts.keychainName ]); security.on('error', function(err) { err = new KeychainAccess.errors.ServiceFailureError(null, err.message); @@ -389,6 +445,70 @@ errorClass('PasswordNotFound', 'Could not find password'); errorClass('NoKeychainNameProvided', 'A keychain name is required'); +/** + * Promise support. + * + * Every public method keeps its callback signature exactly. When no callback + * is passed, it returns a Promise that resolves with the value the callback + * would have received, or rejects with the same error object. + */ + +var PUBLIC_METHODS = [ + 'getPassword', + 'setPassword', + 'deletePassword', + 'createKeychain', + 'deleteKeychain', + 'setDefaultKeychain' +]; + +// Node emits both 'error' and 'close' when spawn fails, so make sure a +// caller's callback runs at most once. +function once(fn) { + var called = false; + return function () { + if (called) return; + called = true; + return fn.apply(this, arguments); + }; +} + +function withPromise(method) { + return function (opts, fn) { + if (typeof fn === 'function') { + method.call(this, opts, once(fn)); + return; + } + + // No callback and no Promise (very old Node): behave as before. + if (typeof Promise !== 'function') { + method.call(this, opts, noop); + return; + } + + var self = this; + var promise = new Promise(function (resolve, reject) { + method.call(self, opts, function (err, value) { + if (err) { + reject(err); + } else { + resolve(value); + } + }); + }); + + // Callers that used to fire and forget (no callback) never saw errors. + // Mark the promise handled so they don't start crashing on unhandled + // rejections; anyone who awaits or .catch()es still gets the error. + promise.catch(noop); + return promise; + }; +} + +PUBLIC_METHODS.forEach(function (name) { + KeychainAccess.prototype[name] = withPromise(KeychainAccess.prototype[name]); +}); + /** * Expose new Keychain Access */ diff --git a/package-lock.json b/package-lock.json index 9a5a3e0..e813caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,69 +1,113 @@ { "name": "keychain", - "version": "1.5.0", - "lockfileVersion": 2, + "version": "1.6.0", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "keychain", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "devDependencies": { + "c8": "^12.0.0", "chai": "^4.3.7", - "mocha": "^10.2.0" + "mocha": "^12.0.3" } }, - "node_modules/ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, "engines": { - "node": ">=6" + "node": ">=18" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@isaacs/nonrecursive-glob-match": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@isaacs/nonrecursive-glob-match/-/nonrecursive-glob-match-1.0.3.tgz", + "integrity": "sha512-SI7kyKHq4XXDRozw+r9GlRZkia1ZkFrpLW98r2IC2Bpr8YkFGE3fvjzmxCG0x78A++69HmDNrDvSMYBZmHGpKA==", + "dev": true + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, "engines": { "node": ">=8" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" - }, + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, "engines": { - "node": ">= 8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.3.tgz", + "integrity": "sha512-Sn98ftV3KfsO8xfLPHG6oT8y+b0yvbrLgZjgPDXsharNY15tTAjqIsIju4S32nl6/iEUNNE4v+yL2Zhm8ooP3w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ] }, "node_modules/assertion-error": { "version": "1.1.0", @@ -75,39 +119,24 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "engines": { - "node": ">=8" + "node": "18 || 20 || >=22" } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "20 || >=22" } }, "node_modules/browser-stdout": { @@ -116,142 +145,142 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/c8": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-12.0.0.tgz", + "integrity": "sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg==", "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^8.0.0", + "v8-to-istanbul": "^9.0.0", + "yargs": "^18.0.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.12.0 || >=23" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } } }, "node_modules/chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", "dev": true, "dependencies": { "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "type-detect": "^4.1.0" }, "engines": { "node": ">=4" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "get-func-name": "^2.0.2" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "*" } }, - "node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "readdirp": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, "engines": { - "node": "*" + "node": ">=20" } }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "node_modules/cliui/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">= 8.10.0" + "node": ">=18" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">=7.0.0" + "node": ">= 8" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -262,28 +291,10 @@ } } }, - "node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, "dependencies": { "type-detect": "^4.0.0" @@ -293,53 +304,29 @@ } }, "node_modules/diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true, "engines": { "node": ">=0.3.1" } }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "engines": { "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -356,33 +343,32 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", "dev": true, - "bin": { - "flat": "cli.js" + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/get-caller-file": { @@ -394,67 +380,42 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "node_modules/get-east-asian-width": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.7.0.tgz", + "integrity": "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==", "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, "engines": { - "node": "*" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, "engines": { - "node": ">= 6" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node": "*" } }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/glob": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-14.0.3.tgz", + "integrity": "sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "minimatch": "^11.0.2", + "minipass": "^7.1.3", + "path-scurry": "^3.0.0" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/has-flag": { @@ -466,113 +427,98 @@ "node": ">=8" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { "node": ">=8" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" + "node": ">=10" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz", + "integrity": "sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "dependencies": { - "argparse": "^2.0.1" + "argparse": "^3.0.3" }, "bin": { - "js-yaml": "bin/js-yaml.js" + "js-yaml": "bin/js-yaml.mjs" } }, "node_modules/locate-path": { @@ -590,14 +536,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "dev": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "semver": "^7.5.3" }, "engines": { "node": ">=10" @@ -606,102 +569,81 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", + "node_modules/minimatch": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.3.tgz", + "integrity": "sha512-iny/40PEGkNJQ5/Pv6z3nm6FbRAby32GeZRjCUJk2WnNDbgPSTcdI+DDvLqsgBdD4cD5sMtMheqPPnehCCa8Nw==", "dev": true, "dependencies": { - "get-func-name": "^2.0.0" + "@isaacs/nonrecursive-glob-match": "^1.0.3", + "brace-expansion": "^5.0.12" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" } }, "node_modules/mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "dependencies": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.3.tgz", + "integrity": "sha512-beammieDdiWdkjANWeWPpkxUROYGi5FN88fPOkLiticz7HTg3Y+Pho3EYE6SUdcuWnZ3h2WD8EXCA55JF4o6wg==", + "dev": true, + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^5.0.0", + "debug": "^4.3.5", + "diff": "^9.0.0", + "find-up-simple": "^1.0.1", + "glob": "^13.0.0", + "is-path-inside": "^4.0.0", + "is-unicode-supported": "^0.1.0", + "js-yaml": "^5.0.0", + "minimatch": "^10.2.2", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^7.1.1", + "strip-json-comments": "^5.0.3", + "supports-color": "^8.1.1", + "workerpool": "^10.0.0" }, "bin": { - "_mocha": "bin/_mocha", "mocha": "bin/mocha.js" }, "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" + "dependencies": { + "has-flag": "^4.0.0" }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, "node_modules/p-limit": { "version": "3.1.0", @@ -742,13 +684,29 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-3.0.0.tgz", + "integrity": "sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==", + "dev": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/pathval": { @@ -760,179 +718,225 @@ "node": "*" } }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "dev": true, "engines": { - "node": ">=8.6" + "node": ">= 20.19.0" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "node_modules/serialize-javascript": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.4.tgz", + "integrity": "sha512-FqlEuoTq/zRmA4Lg6aOqyH3lCHbUtCB0bmHCeFniX2GBdDqMJM3FwdvEhlE5Pim1dlt2FfBdVPmUNNiGhhKqpw==", "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">=20.0.0" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] + "engines": { + "node": ">=8" + } }, - "node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", "dev": true, "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=8" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", "dev": true, "engines": { - "node": ">=8" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "dependencies": { "has-flag": "^4.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=8" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/test-exclude": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", + "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", "dev": true, "dependencies": { - "is-number": "^7.0.0" + "@istanbuljs/schema": "^0.1.2", + "glob": "^13.0.6", + "minimatch": "^10.2.2" }, "engines": { - "node": ">=8.0" + "node": "20 || >=22" } }, "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, "engines": { "node": ">=4" } }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-10.0.3.tgz", + "integrity": "sha512-6z2Iis68Wqth93/G/wJP9u+R3O+d2XTlgWChGCwuT1qLbBsOYueGRZuJ++v3mtDP5KjYdy+WzvWC+VWETSVXJA==", "dev": true }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/y18n": { "version": "5.0.8", @@ -944,45 +948,38 @@ } }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", "dev": true, "dependencies": { - "cliui": "^7.0.2", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^8.2.1", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yocto-queue": { @@ -997,709 +994,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true - }, - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "chai": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", - "integrity": "sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==", - "dev": true, - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^4.1.2", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "dev": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - }, - "dependencies": { - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true - }, - "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "requires": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - } - }, - "flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", - "dev": true - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "dependencies": { - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true - }, - "js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "requires": { - "argparse": "^2.0.1" - } - }, - "locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "requires": { - "p-locate": "^5.0.0" - } - }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - } - }, - "loupe": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", - "integrity": "sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==", - "dev": true, - "requires": { - "get-func-name": "^2.0.0" - } - }, - "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - }, - "mocha": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", - "dev": true, - "requires": { - "ansi-colors": "4.1.1", - "browser-stdout": "1.3.1", - "chokidar": "3.5.3", - "debug": "4.3.4", - "diff": "5.0.0", - "escape-string-regexp": "4.0.0", - "find-up": "5.0.0", - "glob": "7.2.0", - "he": "1.2.0", - "js-yaml": "4.1.0", - "log-symbols": "4.1.0", - "minimatch": "5.0.1", - "ms": "2.1.3", - "nanoid": "3.3.3", - "serialize-javascript": "6.0.0", - "strip-json-comments": "3.1.1", - "supports-color": "8.1.1", - "workerpool": "6.2.1", - "yargs": "16.2.0", - "yargs-parser": "20.2.4", - "yargs-unparser": "2.0.0" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "nanoid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", - "dev": true - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "requires": { - "p-limit": "^3.0.2" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "requires": { - "picomatch": "^2.2.1" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dev": true, - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.4", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", - "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", - "dev": true - }, - "yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "requires": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - } - }, - "yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true - } } } diff --git a/package.json b/package.json index e3ae874..f976883 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,25 @@ { "name": "keychain", - "version": "1.5.0", + "version": "1.6.0", "description": "Basic access to the Mac OS X Keychain", "main": "keychain.js", + "files": [ + "keychain.js" + ], "devDependencies": { + "c8": "^12.0.0", "chai": "^4.3.7", - "mocha": "^10.2.0" + "mocha": "^12.0.3" + }, + "overrides": { + "glob": "^14.0.3", + "minimatch": "^11.0.3" }, "scripts": { - "test": "mocha" + "test": "mocha \"test/{unit,integration}/**/*.test.js\"", + "test:unit": "mocha \"test/unit/**/*.test.js\"", + "test:integration": "mocha \"test/integration/**/*.test.js\"", + "coverage": "c8 mocha \"test/{unit,integration}/**/*.test.js\"" }, "repository": { "type": "git", diff --git a/test/helpers/fake-spawn.js b/test/helpers/fake-spawn.js new file mode 100644 index 0000000..18f2f0a --- /dev/null +++ b/test/helpers/fake-spawn.js @@ -0,0 +1,110 @@ +// Stand-in for child_process.spawn so tests run on any OS without touching a +// real keychain. Install it before requiring keychain.js. +var childProcess = require('child_process'); +var EventEmitter = require('events').EventEmitter; + +var realSpawn = childProcess.spawn; +var realPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); +var behavior = null; + +exports.calls = []; + +// script: { stdout, stderr, code } or { error: 'message' } (spawn failure: +// emits 'error' then 'close' with code -2, the way Node does), or a function +// called with each spawn's { command, args } that returns a script. +exports.respond = function (script) { + behavior = script; +}; + +exports.install = function () { + exports.calls = []; + Object.defineProperty(process, 'platform', { value: 'darwin' }); + childProcess.spawn = function (command, args, options) { + exports.calls.push({ command: command, args: args, options: options }); + var child = new EventEmitter(); + var call = exports.calls[exports.calls.length - 1]; + call.stdin = ''; + child.stdout = new EventEmitter(); + child.stderr = new EventEmitter(); + child.stdin = { + write: function (data) { call.stdin += data; return true; }, + end: function (data) { if (data) call.stdin += data; call.stdinEnded = true; }, + on: function () { return child.stdin; } + }; + var current = behavior; + setImmediate(function () { + // Evaluated once stdin has been written, so `security -i` calls can be told apart. + var script = (typeof current === 'function' ? current(call) : current) || { code: 0 }; + if (script.error) { + var err = new Error(script.error); + err.code = 'ENOENT'; + child.emit('error', err); + child.emit('close', -2, null); + return; + } + if (script.stdout) child.stdout.emit('data', Buffer.from(script.stdout)); + if (script.stderr) child.stderr.emit('data', Buffer.from(script.stderr)); + child.emit('close', script.code || 0, null); + }); + return child; + }; +}; + +exports.restore = function () { + childProcess.spawn = realSpawn; + Object.defineProperty(process, 'platform', realPlatform); + behavior = null; +}; + +// What `security` would actually run for a call: its argv, or for `security -i` +// the commands it parses from stdin (a port of split_line in Apple's +// SecurityTool/macOS/security.c, with readline's 4095-byte line limit). +exports.commands = function (call) { + if (call.args[0] !== '-i') return [call.args]; + var bytes = Buffer.from(call.stdin, 'utf8'); + var lines = []; + var line = []; + for (var i = 0; i < bytes.length; i++) { + if (bytes[i] === 0x0a || line.length === 4095) { + lines.push(Buffer.from(line).toString('utf8')); + line = []; + if (bytes[i] === 0x0a) continue; + } + line.push(bytes[i]); + } + if (line.length) lines.push(Buffer.from(line).toString('utf8')); + return lines.map(splitLine).filter(function (argv) { return argv.length; }); +}; + +function splitLine(line) { + var args = []; + var cur = null; + var state = 'ws'; + var quote = null; + var cut = line.indexOf('\0'); + if (cut !== -1) line = line.slice(0, cut); + for (var i = 0; i < line.length; i++) { + var ch = line[i]; + if (state === 'ws') { + if (/[ \t\n\v\f\r]/.test(ch)) continue; + cur = ''; + if (ch === '"' || ch === "'") { quote = ch; state = 'quoted'; continue; } + state = 'arg'; + } + if (state === 'arg') { + if (ch === '\\') { state = 'argEsc'; continue; } + if (/[ \t\n\v\f\r]/.test(ch)) { args.push(cur); state = 'ws'; continue; } + cur += ch; continue; + } + if (state === 'argEsc') { cur += ch; state = 'arg'; continue; } + if (state === 'quoted') { + if (ch === '\\') { state = 'quotedEsc'; continue; } + if (ch === quote) { args.push(cur); state = 'ws'; continue; } + cur += ch; continue; + } + if (state === 'quotedEsc') { cur += ch; state = 'quoted'; continue; } + } + if (state !== 'ws') args.push(cur); + return args; +} +exports.splitLine = splitLine; diff --git a/test/helpers/mock-security.js b/test/helpers/mock-security.js new file mode 100644 index 0000000..9473da3 --- /dev/null +++ b/test/helpers/mock-security.js @@ -0,0 +1,137 @@ +/*! + * Test helper: a fake `child_process.spawn` that stands in for the macOS + * `security` CLI so the suite runs on any OS. + * + * keychain.js captures `spawn` at require time, so the stub must be installed + * BEFORE the module is loaded. `load()` does that and returns a fresh copy of + * the module. `process.platform` is forced to 'darwin' for the duration of a + * test via `pretendDarwin()` / `restorePlatform()`. + */ + +var EventEmitter = require('events').EventEmitter; +var childProcess = require('child_process'); +var path = require('path'); + +var MODULE_PATH = path.resolve(__dirname, '..', '..', 'keychain.js'); +var realSpawn = childProcess.spawn; +var realPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + +/** + * Recorded spawn calls: [{ cmd, args }] + */ +var calls = []; + +/** + * Scripted responses, consumed in order. Each is: + * { stdout: String|[String], stderr: String|[String], code: Number, signal: String } + * or { error: Error } -> emits 'error' instead of running + * When the script is empty, the fake exits 0 with no output. + */ +var script = []; + +function toChunks(v) { + if (v === undefined || v === null) return []; + return Array.isArray(v) ? v : [v]; +} + +function fakeSpawn(cmd, args) { + var child = new EventEmitter(); + child.stdout = new EventEmitter(); + child.stderr = new EventEmitter(); + child.pid = 4242; + child.kill = function () {}; + var call = { cmd: cmd, args: (args || []).slice(), stdin: '', stdinEnded: false }; + // Record what the module writes to the child's stdin (`security -i` reads + // its command line from there so secrets stay out of argv). + child.stdin = { + write: function (data) { call.stdin += String(data); return true; }, + end: function (data) { if (data !== undefined) call.stdin += String(data); call.stdinEnded = true; }, + on: function () { return child.stdin; }, + once: function () { return child.stdin; } + }; + calls.push(call); + + var step = script.length ? script.shift() : { code: 0 }; + + // Deliver asynchronously, like a real child process would. + setImmediate(function () { + if (step.error) { + child.emit('error', step.error); + return; + } + toChunks(step.stdout).forEach(function (c) { child.stdout.emit('data', Buffer.from(c)); }); + toChunks(step.stderr).forEach(function (c) { child.stderr.emit('data', Buffer.from(c)); }); + var code = step.code === undefined ? 0 : step.code; + child.emit('exit', code, step.signal || null); + child.emit('close', code, step.signal || null); + }); + + return child; +} + +module.exports = { + calls: calls, + script: script, + + /** Install the fake spawn and return a freshly loaded keychain module. */ + load: function () { + childProcess.spawn = fakeSpawn; + delete require.cache[MODULE_PATH]; + var mod = require(MODULE_PATH); + return mod; + }, + + /** Put the real spawn back and drop the cached module. */ + unload: function () { + childProcess.spawn = realSpawn; + delete require.cache[MODULE_PATH]; + }, + + /** + * What `security` actually executes for a recorded call: its argv, or for + * `security -i` the command lines it parses from stdin. Shares the split_line + * port with fake-spawn so both helpers agree on how security tokenizes. + */ + commands: function (call) { + return require('./fake-spawn').commands(call); + }, + + /** Queue one scripted `security` run. */ + respond: function (step) { + script.push(step); + }, + + /** Forget recorded calls and queued responses. */ + reset: function () { + calls.length = 0; + script.length = 0; + }, + + pretendDarwin: function () { + Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true }); + }, + + pretendPlatform: function (name) { + Object.defineProperty(process, 'platform', { value: name, configurable: true }); + }, + + restorePlatform: function () { + Object.defineProperty(process, 'platform', realPlatform); + }, + + /** Helper: what `security find-*-password -g` prints for a password. */ + findOutput: function (password, opts) { + opts = opts || {}; + var stdout = 'keychain: "/Users/test/Library/Keychains/login.keychain-db"\n' + + 'class: "genp"\nattributes:\n "acct"="' + (opts.account || 'acct') + '"\n' + + ' "svce"="' + (opts.service || 'svc') + '"\n'; + var stderr; + if (opts.hex) { + var hex = Buffer.from(password, 'utf8').toString('hex').toUpperCase(); + stderr = 'password: 0x' + hex + (opts.quoted === false ? '' : ' "' + opts.quoted + '"') + '\n'; + } else { + stderr = 'password: "' + password + '"\n'; + } + return { stdout: stdout, stderr: stderr, code: 0 }; + } +}; diff --git a/test/keychain.test.js b/test/integration/keychain.integration.test.js similarity index 97% rename from test/keychain.test.js rename to test/integration/keychain.integration.test.js index 48af9d0..f3f35db 100644 --- a/test/keychain.test.js +++ b/test/integration/keychain.integration.test.js @@ -1,9 +1,16 @@ -var keychain = require('../'); +var keychain = require('../../'); var chai = require('chai'); chai.should(); -describe('KeychainAccess', function(){ +// Integration suite: talks to the real macOS `security` CLI and mutates the +// login keychain. Runs only on darwin; everywhere else it is skipped and the +// mocked unit suite in test/unit covers behavior. +describe('KeychainAccess (integration, macOS only)', function(){ + before(function () { + if (process.platform !== 'darwin') this.skip(); + }); + var testService = 'KeychainAccess#test#' + Date.now(); var testInternetService = 'KeychainAccess' + Date.now() + '.com'; @@ -13,10 +20,6 @@ describe('KeychainAccess', function(){ var asciiComplexPW = "password: 0x4e6573746564"; var keychainName = "test.keychain"; - it('should be running on a mac', function(){ - require('os').platform().should.equal('darwin'); - }) - it('should have access to ' + keychain.executablePath, function(){ require('fs').existsSync(keychain.executablePath).should.equal(true); }) diff --git a/test/integration/macos-security.test.js b/test/integration/macos-security.test.js new file mode 100644 index 0000000..a894f45 --- /dev/null +++ b/test/integration/macos-security.test.js @@ -0,0 +1,50 @@ +// Real-keychain checks for the security hardening (macOS only): passwords sent +// through `security -i` must round-trip exactly, and nothing in them may run +// as a second command. Skipped on other platforms. +var keychain = require('../../'); +var chai = require('chai'); +chai.should(); + +var onMac = process.platform === 'darwin'; + +(onMac ? describe : describe.skip)('security hardening on a real keychain', function () { + this.timeout(20000); + + var service = 'KeychainAccess#hardening#' + Date.now(); + var values = [ + 'with space', + 'quote"inside', + "single'quote", + 'back\\slash\\', + '\\"; delete-generic-password -s ' + service + ' -a "plain', + '-w', + 'tab\there', + '∆˚ˆ©ƒ®∂çµ˚¬˙ƒ®†¥' + ]; + + values.forEach(function (value, i) { + it('round-trips password ' + JSON.stringify(value), async function () { + var account = 'acct' + i; + (await keychain.setPassword({ account: account, service: service, password: value })).should.equal(value); + (await keychain.getPassword({ account: account, service: service })).should.equal(value); + // Setting it again goes through the exit-45 replace path. + (await keychain.setPassword({ account: account, service: service, password: value + '2' })).should.equal(value + '2'); + (await keychain.getPassword({ account: account, service: service })).should.equal(value + '2'); + await keychain.deletePassword({ account: account, service: service }); + }); + }); + + it('round-trips an account and service that need quoting', async function () { + var account = 'a "quoted" \\ account'; + var svc = service + ' "svc" \\'; + await keychain.setPassword({ account: account, service: svc, password: 'pw' }); + (await keychain.getPassword({ account: account, service: svc })).should.equal('pw'); + await keychain.deletePassword({ account: account, service: svc }); + }); + + it('creates and deletes a keychain whose name starts with "-"', async function () { + var name = '-nk-test-' + Date.now() + '.keychain'; + (await keychain.createKeychain({ keychainName: name, password: 'p "w\\' })).should.equal(name); + (await keychain.deleteKeychain({ keychainName: name })).should.equal(name); + }); +}); diff --git a/test/smoke/old-node.js b/test/smoke/old-node.js new file mode 100644 index 0000000..3858492 --- /dev/null +++ b/test/smoke/old-node.js @@ -0,0 +1,81 @@ +// Plain-Node smoke test (no mocha) so CI can show the library itself still +// runs on old Node versions the test runner no longer supports. Mocked +// `security`, so it runs on any OS. Exits non-zero on the first failure. +var assert = require('assert'); +var fake = require('../helpers/fake-spawn'); +fake.install(); +var keychain = require('../../'); +var errors = keychain.constructor.errors; + +var unhandled = []; +process.on('unhandledRejection', function (reason) { unhandled.push(reason); }); + +var steps = [ + function callbackStillWorks(next) { + fake.respond({ stderr: 'password: "secret"\n' }); + var ret = keychain.getPassword({ account: 'a', service: 's' }, function (err, pass) { + assert.strictEqual(err, null); + assert.strictEqual(pass, 'secret'); + next(); + }); + assert.strictEqual(ret, undefined); + }, + function promiseResolves(next) { + fake.respond({ stderr: 'password: 0xE28886656C7461\n' }); + keychain.getPassword({ account: 'a', service: 's' }).then(function (pass) { + assert.strictEqual(pass, '∆elta'); + next(); + }, next); + }, + function promiseRejectsWithSameClass(next) { + fake.respond({ code: 44 }); + keychain.getPassword({ account: 'a', service: 's' }).then(function () { + next(new Error('expected a rejection')); + }, function (err) { + assert.ok(err instanceof errors.PasswordNotFoundError); + next(); + }); + }, + function passwordGoesOverStdin(next) { + fake.respond({ code: 0 }); + keychain.setPassword({ account: 'a', service: 's', password: 'p"w' }).then(function (pw) { + assert.strictEqual(pw, 'p"w'); + var call = fake.calls[fake.calls.length - 1]; + assert.deepStrictEqual(call.args, ['-i']); + assert.deepStrictEqual(fake.commands(call)[0], ['add-generic-password', '-a', 'a', '-s', 's', '-w', 'p"w']); + next(); + }, next); + }, + function callbackRunsOnceOnSpawnFailure(next) { + fake.respond({ error: 'spawn ENOENT' }); + var count = 0; + keychain.deletePassword({ account: 'a', service: 's' }, function () { count++; }); + setTimeout(function () { + assert.strictEqual(count, 1); + next(); + }, 50); + }, + function fireAndForgetDoesNotCrash(next) { + fake.respond({ code: 1 }); + keychain.deletePassword({ account: 'a', service: 's' }); + setTimeout(function () { + assert.strictEqual(unhandled.length, 0); + next(); + }, 50); + } +]; + +(function run(i) { + if (i === steps.length) { + fake.restore(); + console.log('ok ' + steps.length + ' smoke checks on node ' + process.version); + return; + } + steps[i](function (err) { + if (err) { + console.error('not ok ' + steps[i].name + ': ' + (err.stack || err)); + process.exit(1); + } + run(i + 1); + }); +})(0); diff --git a/test/unit/compat.test.js b/test/unit/compat.test.js new file mode 100644 index 0000000..e8c5f74 --- /dev/null +++ b/test/unit/compat.test.js @@ -0,0 +1,439 @@ +/*! + * Backwards-compatibility suite. + * + * Pinned against keychain@1.5.0 (master 28ac548) BEFORE the promise API + * landed. Every assertion here is observable behavior a caller of the + * callback API could depend on. If a change breaks one of these on purpose, + * it is a breaking change and needs a major version bump, not a test edit. + */ + +var chai = require('chai'); +var expect = chai.expect; +var mock = require('../helpers/mock-security'); + +describe('Backwards compatibility (pinned at 1.5.0)', function () { + var keychain; + var errors; + + beforeEach(function () { + mock.reset(); + mock.pretendDarwin(); + keychain = mock.load(); + errors = keychain.constructor.errors; + }); + + afterEach(function () { + mock.restorePlatform(); + mock.unload(); + }); + + describe('error classes', function () { + var table = [ + ['UnsupportedPlatformError', 'UnsupportedPlatform', 'Expected darwin platform, got: '], + ['NoAccountProvidedError', 'NoAccountProvided', 'An account is required'], + ['NoServiceProvidedError', 'NoServiceProvided', 'A service is required'], + ['NoPasswordProvidedError', 'NoPasswordProvided', 'A password is required'], + ['ServiceFailureError', 'ServiceFailure', 'Keychain failed to start child process: '], + ['PasswordNotFoundError', 'PasswordNotFound', 'Could not find password'], + ['NoKeychainNameProvidedError', 'NoKeychainNameProvided', 'A keychain name is required'] + ]; + + table.forEach(function (row) { + var name = row[0], code = row[1], defaultMsg = row[2]; + + it(name + ': instanceof Error, type/code/message/stack as in 1.5.0', function () { + var Cls = errors[name]; + expect(Cls).to.be.a('function'); + var e = new Cls(); + expect(e).to.be.instanceOf(Error); + expect(e).to.be.instanceOf(Cls); + expect(e.type).to.equal(name); + expect(e.code).to.equal(code); + expect(e.message).to.equal(defaultMsg); + expect(e.stack).to.be.a('string'); + expect(e.constructor).to.equal(Cls); + expect(Object.getPrototypeOf(Cls.prototype)).to.equal(Error.prototype); + }); + + it(name + ': (msg, append) constructor contract', function () { + var Cls = errors[name]; + expect(new Cls('custom').message).to.equal('custom'); + expect(new Cls(null, 'tail').message).to.equal(defaultMsg + 'tail'); + expect(new Cls('custom', 'tail').message).to.equal('customtail'); + }); + }); + + it('errors are reachable as require("keychain").constructor.errors and are the same objects', function () { + expect(keychain.constructor.errors).to.equal(errors); + expect(Object.keys(errors)).to.have.length(7); + }); + + it('errors are plain prototype-chain classes: callable with `new` only, not subclass-of-Error by ES6 `class`', function () { + // 1.5.0 errors are constructed via `new`; this pins that `new` keeps working + // and that instances get an own `message` (not inherited). + var e = new errors.PasswordNotFoundError(); + expect(Object.prototype.hasOwnProperty.call(e, 'message')).to.equal(true); + }); + }); + + describe('callback timing and arity', function () { + it('validation errors are delivered synchronously, before the method returns', function () { + var called = false; + keychain.getPassword({}, function () { called = true; }); + expect(called).to.equal(true); + }); + + it('platform errors are delivered synchronously, before the method returns', function () { + mock.pretendPlatform('win32'); + var called = false; + keychain.setPassword({ account: 'a', service: 's', password: 'p' }, function () { called = true; }); + expect(called).to.equal(true); + }); + + it('security-backed results are delivered asynchronously, after the method returns', function (done) { + mock.respond(mock.findOutput('p')); + var called = false; + keychain.getPassword({ account: 'a', service: 's' }, function () { + called = true; + done(); + }); + expect(called).to.equal(false); + }); + + it('validation errors call back with exactly (err, null)', function () { + var args; + keychain.getPassword({}, function () { args = Array.prototype.slice.call(arguments); }); + expect(args).to.have.length(2); + expect(args[0]).to.be.instanceOf(errors.NoAccountProvidedError); + expect(args[1]).to.equal(null); + }); + + it('getPassword success calls back with exactly (null, password)', function (done) { + mock.respond(mock.findOutput('p')); + keychain.getPassword({ account: 'a', service: 's' }, function () { + var args = Array.prototype.slice.call(arguments); + expect(args).to.deep.equal([null, 'p']); + done(); + }); + }); + + it('getPassword not-found calls back with exactly (err, null)', function (done) { + mock.respond({ code: 44 }); + keychain.getPassword({ account: 'a', service: 's' }, function () { + var args = Array.prototype.slice.call(arguments); + expect(args).to.have.length(2); + expect(args[0]).to.be.instanceOf(errors.PasswordNotFoundError); + expect(args[1]).to.equal(null); + done(); + }); + }); + + it('setPassword success calls back with exactly (null, password)', function (done) { + mock.respond({ code: 0 }); + keychain.setPassword({ account: 'a', service: 's', password: 'p' }, function () { + expect(Array.prototype.slice.call(arguments)).to.deep.equal([null, 'p']); + done(); + }); + }); + + it('setPassword service failure calls back with exactly (err) — one argument', function (done) { + mock.respond({ code: 36 }); + keychain.setPassword({ account: 'a', service: 's', password: 'p' }, function () { + var args = Array.prototype.slice.call(arguments); + expect(args).to.have.length(1); + expect(args[0]).to.be.instanceOf(errors.ServiceFailureError); + expect(args[0].exitCode).to.equal(36); + done(); + }); + }); + + it('deletePassword success calls back with exactly (null) — one argument', function (done) { + mock.respond({ code: 0 }); + keychain.deletePassword({ account: 'a', service: 's' }, function () { + expect(Array.prototype.slice.call(arguments)).to.deep.equal([null]); + done(); + }); + }); + + it('deletePassword failure calls back with exactly (err) — one argument', function (done) { + mock.respond({ code: 44 }); + keychain.deletePassword({ account: 'a', service: 's' }, function () { + var args = Array.prototype.slice.call(arguments); + expect(args).to.have.length(1); + expect(args[0]).to.be.instanceOf(errors.PasswordNotFoundError); + done(); + }); + }); + + ['createKeychain', 'deleteKeychain', 'setDefaultKeychain'].forEach(function (method) { + it(method + ' success calls back with exactly (null, keychainName)', function (done) { + mock.respond({ code: 0 }); + keychain[method]({ keychainName: 'k', password: 'p' }, function () { + expect(Array.prototype.slice.call(arguments)).to.deep.equal([null, 'k']); + done(); + }); + }); + + it(method + ' failure calls back with exactly (err) carrying exitCode', function (done) { + mock.respond({ code: 7 }); + keychain[method]({ keychainName: 'k', password: 'p' }, function () { + var args = Array.prototype.slice.call(arguments); + expect(args).to.have.length(1); + expect(args[0]).to.be.instanceOf(errors.ServiceFailureError); + expect(args[0].message).to.equal('Security returned a non-successful error code: 7'); + expect(args[0].exitCode).to.equal(7); + done(); + }); + }); + }); + + it('spawn failure calls back with exactly (err, null) for every method', function (done) { + var methods = ['getPassword', 'setPassword', 'deletePassword', 'createKeychain', 'deleteKeychain', 'setDefaultKeychain']; + var remaining = methods.length; + methods.forEach(function (method) { + mock.respond({ error: new Error('boom') }); + keychain[method]({ account: 'a', service: 's', password: 'p', keychainName: 'k' }, function () { + var args = Array.prototype.slice.call(arguments); + expect(args, method).to.have.length(2); + expect(args[0], method).to.be.instanceOf(errors.ServiceFailureError); + expect(args[0].message, method).to.equal('Keychain failed to start child process: boom'); + expect(args[1], method).to.equal(null); + if (--remaining === 0) done(); + }); + }); + }); + }); + + describe('calling convention', function () { + var methods = ['getPassword', 'setPassword', 'deletePassword', 'createKeychain', 'deleteKeychain', 'setDefaultKeychain']; + + methods.forEach(function (method) { + it(method + ' returns undefined when a callback is given (no promise to leak or double-deliver)', function (done) { + mock.respond({ code: 0, stderr: 'password: "p"\n' }); + var ret = keychain[method]({ account: 'a', service: 's', password: 'p', keychainName: 'k' }, function () { done(); }); + expect(ret).to.equal(undefined); + }); + + it(method + ' with a callback never throws synchronously on bad input', function () { + expect(function () { keychain[method](undefined, function () {}); }).to.not.throw(); + expect(function () { keychain[method]({}, function () {}); }).to.not.throw(); + expect(function () { keychain[method](null, function () {}); }).to.not.throw(); + }); + }); + + it('a missing callback is tolerated on valid input and security is still invoked', function (done) { + mock.respond({ code: 0 }); + var ret = keychain.setPassword({ account: 'a', service: 's', password: 'p' }); + // 1.5.0 returns undefined; a promise-returning successor must still not throw here. + if (ret && typeof ret.then === 'function') ret.then(function () {}, function () {}); + setImmediate(function () { + expect(mock.calls).to.have.length(1); + done(); + }); + }); + + it('the `type` option is lower-cased, defaulted to generic, and written back onto the caller\'s opts object', function (done) { + mock.respond({ code: 0 }); + var opts = { account: 'a', service: 's', type: 'Internet' }; + keychain.deletePassword(opts, function () { + expect(opts.type).to.equal('internet'); + var opts2 = { account: 'a', service: 's' }; + mock.respond({ code: 0 }); + keychain.deletePassword(opts2, function () { + expect(opts2.type).to.equal('generic'); + done(); + }); + }); + }); + + it('executablePath is an own, writable instance property defaulting to /usr/bin/security', function () { + expect(Object.prototype.hasOwnProperty.call(keychain, 'executablePath')).to.equal(true); + expect(keychain.executablePath).to.equal('/usr/bin/security'); + keychain.executablePath = '/x'; + expect(keychain.executablePath).to.equal('/x'); + }); + + it('module.exports is a singleton instance whose prototype methods are the six public methods', function () { + var proto = Object.getPrototypeOf(keychain); + expect(Object.keys(proto).sort()).to.deep.equal( + ['createKeychain', 'deleteKeychain', 'deletePassword', 'getPassword', 'setDefaultKeychain', 'setPassword'] + ); + methods.forEach(function (m) { expect(keychain[m].length, m).to.equal(2); }); + }); + }); + + // The commands `security` executes are pinned, not the raw argv: since 1.6 + // the two methods that carry a password (setPassword, createKeychain) send + // the command line over stdin to `security -i` so it never shows up in ps, + // and keychain names are protected with "--". What security *runs* is the + // same command 1.5.0 ran. mock.commands() is a port of security's own + // split_line, so this is what the tool would actually see. + describe('commands executed by `security` (exact, as security parses them)', function () { + var cases = [ + ['getPassword', { account: 'A', service: 'S' }, ['find-generic-password', '-a', 'A', '-s', 'S', '-g']], + ['getPassword', { account: 'A', service: 'S', type: 'internet' }, ['find-internet-password', '-a', 'A', '-s', 'S', '-g']], + ['setPassword', { account: 'A', service: 'S', password: 'P' }, ['add-generic-password', '-a', 'A', '-s', 'S', '-w', 'P']], + ['setPassword', { account: 'A', service: 'S', password: 'P', type: 'internet' }, ['add-internet-password', '-a', 'A', '-s', 'S', '-w', 'P']], + ['deletePassword', { account: 'A', service: 'S' }, ['delete-generic-password', '-a', 'A', '-s', 'S']], + ['deletePassword', { account: 'A', service: 'S', type: 'internet' }, ['delete-internet-password', '-a', 'A', '-s', 'S']], + ['createKeychain', { keychainName: 'K', password: 'P' }, ['create-keychain', '-p', 'P', '--', 'K']], + ['deleteKeychain', { keychainName: 'K' }, ['delete-keychain', '--', 'K']], + ['setDefaultKeychain', { keychainName: 'K' }, ['default-keychain', '-s', '--', 'K']] + ]; + var secretMethods = ['setPassword', 'createKeychain']; + + cases.forEach(function (c) { + var method = c[0], opts = c[1], argv = c[2]; + it(method + '(' + JSON.stringify(opts) + ') -> security ' + argv.join(' '), function (done) { + mock.respond({ code: 0, stderr: 'password: "P"\n' }); + keychain[method](opts, function () { + expect(mock.calls).to.have.length(1); + expect(mock.calls[0].cmd).to.equal('/usr/bin/security'); + expect(mock.commands(mock.calls[0])).to.deep.equal([argv]); + if (secretMethods.indexOf(method) !== -1) { + // password travels on stdin via `security -i`, never in argv + expect(mock.calls[0].args).to.deep.equal(['-i']); + expect(mock.calls[0].args.join(' ')).to.not.contain('P'); + } else { + expect(mock.calls[0].args).to.deep.equal(argv); + } + done(); + }); + }); + }); + + // `security -i` reads one line per command and its readline() stops at a + // newline or after 4095 bytes, so values that cannot survive that transport + // fall back to the 1.5.0 behaviour: the full command in argv, nothing on + // stdin. Pinned so the fallback stays byte-identical to what 1.5.0 ran. + describe('falls back to the 1.5.0 argv transport when the line cannot go through `security -i`', function () { + var LIMIT = 4094; + var fallbackValues = [ + ['newline', 'pass\nword'], + ['NUL byte', 'pass\0word'], + ['line over ' + LIMIT + ' bytes', 'x'.repeat(LIMIT + 1)] + ]; + + fallbackValues.forEach(function (c) { + var label = c[0], value = c[1]; + + it('setPassword with a ' + label + ' password: argv is the 1.5.0 argv, stdin untouched', function (done) { + mock.respond({ code: 0 }); + var expected = ['add-generic-password', '-a', 'A', '-s', 'S', '-w', value]; + keychain.setPassword({ account: 'A', service: 'S', password: value }, function (err, pw) { + expect(err).to.equal(null); + expect(pw).to.equal(value); + expect(mock.calls).to.have.length(1); + expect(mock.calls[0].args).to.deep.equal(expected); + expect(mock.calls[0].args).to.not.include('-i'); + expect(mock.calls[0].stdin).to.equal(''); + done(); + }); + }); + + it('createKeychain with a ' + label + ' password: argv is the 1.5.0 argv plus "--", stdin untouched', function (done) { + mock.respond({ code: 0 }); + var expected = ['create-keychain', '-p', value, '--', 'K']; + keychain.createKeychain({ keychainName: 'K', password: value }, function (err, name) { + expect(err).to.equal(null); + expect(name).to.equal('K'); + expect(mock.calls).to.have.length(1); + expect(mock.calls[0].args).to.deep.equal(expected); + expect(mock.calls[0].stdin).to.equal(''); + done(); + }); + }); + }); + + it('a command line of exactly ' + LIMIT + ' bytes still travels on stdin (boundary)', function (done) { + // Every token is double-quoted on the -i line; the password fills what is left of the limit. + var framing = Buffer.byteLength('"add-generic-password" "-a" "A" "-s" "S" "-w" ""', 'utf8'); + var value = 'x'.repeat(LIMIT - framing); + mock.respond({ code: 0 }); + keychain.setPassword({ account: 'A', service: 'S', password: value }, function (err) { + expect(err).to.equal(null); + expect(mock.calls[0].args).to.deep.equal(['-i']); + expect(Buffer.byteLength(mock.calls[0].stdin, 'utf8')).to.equal(LIMIT + 1); // + trailing newline + expect(mock.commands(mock.calls[0])).to.deep.equal([['add-generic-password', '-a', 'A', '-s', 'S', '-w', value]]); + done(); + }); + }); + + it('one byte over the limit falls back to argv (boundary)', function (done) { + var framing = Buffer.byteLength('"add-generic-password" "-a" "A" "-s" "S" "-w" ""', 'utf8'); + var value = 'x'.repeat(LIMIT - framing + 1); + mock.respond({ code: 0 }); + keychain.setPassword({ account: 'A', service: 'S', password: value }, function (err) { + expect(err).to.equal(null); + expect(mock.calls[0].args).to.deep.equal(['add-generic-password', '-a', 'A', '-s', 'S', '-w', value]); + expect(mock.calls[0].stdin).to.equal(''); + done(); + }); + }); + }); + + it('security is always invoked by absolute path with an argv array (no shell string)', function (done) { + mock.respond({ code: 0 }); + keychain.setPassword({ account: 'a b', service: 's;rm -rf /', password: '$(id)' }, function () { + expect(mock.calls[0].cmd).to.equal('/usr/bin/security'); + expect(mock.calls[0].args).to.be.an('array'); + var cmd = mock.commands(mock.calls[0]); + expect(cmd).to.have.length(1); + expect(cmd[0]).to.include('s;rm -rf /'); + expect(cmd[0]).to.include('$(id)'); + done(); + }); + }); + + it('setPassword exit 45 retry sequence is add -> delete -> add with identical -a/-s', function (done) { + mock.respond({ code: 45 }); + mock.respond({ code: 0 }); + mock.respond({ code: 0 }); + keychain.setPassword({ account: 'A', service: 'S', password: 'P' }, function (err, pass) { + expect(err).to.equal(null); + expect(pass).to.equal('P'); + expect(mock.calls.map(function (c) { return mock.commands(c)[0]; })).to.deep.equal([ + ['add-generic-password', '-a', 'A', '-s', 'S', '-w', 'P'], + ['delete-generic-password', '-a', 'A', '-s', 'S'], + ['add-generic-password', '-a', 'A', '-s', 'S', '-w', 'P'] + ]); + done(); + }); + }); + }); + + describe('password parsing', function () { + it('hex form wins over the quoted form when both are present', function (done) { + // security prints: password: 0x70617373576974685C "passWith\134" + mock.respond({ code: 0, stderr: 'password: 0x70617373576974685C "passWith\\134"\n' }); + keychain.getPassword({ account: 'a', service: 's' }, function (err, pass) { + expect(pass).to.equal('passWith\\'); + done(); + }); + }); + + it('hex form is decoded as UTF-8', function (done) { + mock.respond({ code: 0, stderr: 'password: 0xE28886656C7461\n' }); + keychain.getPassword({ account: 'a', service: 's' }, function (err, pass) { + expect(pass).to.equal('∆elta'); + done(); + }); + }); + + it('quoted form is returned verbatim, including inner quotes and spaces', function (done) { + mock.respond({ code: 0, stderr: 'password: "a "b" c"\n' }); + keychain.getPassword({ account: 'a', service: 's' }, function (err, pass) { + expect(pass).to.equal('a "b" c'); + done(); + }); + }); + + it('the password line is read from stderr, not stdout', function (done) { + mock.respond({ code: 0, stdout: 'password: "from-stdout"\n', stderr: 'password: "from-stderr"\n' }); + keychain.getPassword({ account: 'a', service: 's' }, function (err, pass) { + expect(pass).to.equal('from-stderr'); + done(); + }); + }); + }); +}); diff --git a/test/unit/error-paths.test.js b/test/unit/error-paths.test.js new file mode 100644 index 0000000..4f54530 --- /dev/null +++ b/test/unit/error-paths.test.js @@ -0,0 +1,213 @@ +// Pins 1.5.0's error behavior on every path, through both the callback and the +// promise API. Mocked, so it runs on any OS. +var fake = require('../helpers/fake-spawn'); +fake.install(); +var keychain = require('../../'); +fake.restore(); +var errors = keychain.constructor.errors; +var chai = require('chai'); +var expect = chai.expect; + +// Calls method both ways and hands each (err, value) to check. +// NK_CALLBACK_ONLY=1 runs just the callback half, to check these against 1.5.0. +var CALLBACK_ONLY = !!process.env.NK_CALLBACK_ONLY; + +function bothWays(method, opts, check) { + function copy() { return opts === undefined ? undefined : Object.assign({}, opts); } + return new Promise(function (resolve, reject) { + var calls = 0; + keychain[method](copy(), function (err, value) { + calls++; + if (calls > 1) return; // counted below; 1.5.0 sometimes called back twice + try { check(err, value, 'callback'); } catch (e) { return reject(e); } + // Give a stray second callback (spawn 'error' + 'close') a chance to show up. + setTimeout(function () { + try { if (!CALLBACK_ONLY) expect(calls, 'callback calls').to.equal(1); resolve(); } catch (e) { reject(e); } + }, 5); + }); + }).then(function () { + if (CALLBACK_ONLY) return; + var p = keychain[method](copy()); + expect(p).to.be.an.instanceof(Promise); + return p.then(function (value) { check(null, value, 'promise'); }, + function (err) { check(err, undefined, 'promise'); }); + }); +} + +function expectError(Type, code) { + return function (err, value, how) { + expect(err, how).to.be.an.instanceof(Type); + expect(err, how).to.be.an.instanceof(Error); + expect(err.code, how).to.equal(code); + expect(err.type, how).to.equal(code + 'Error'); + if (how === 'callback' && arguments.length > 1 && value !== undefined) expect(value).to.equal(null); + }; +} + +var pw = { account: 'a', service: 's', password: 'p' }; +var kc = { keychainName: 'k.keychain', password: 'p' }; + +describe('error paths (mocked security, callback and promise)', function () { + beforeEach(function () { fake.install(); fake.respond({ code: 0 }); }); + afterEach(function () { fake.restore(); }); + + describe('input validation fails before spawning security', function () { + [ + ['getPassword', { service: 's' }, 'NoAccountProvided'], + ['getPassword', { account: 'a' }, 'NoServiceProvided'], + ['setPassword', { service: 's', password: 'p' }, 'NoAccountProvided'], + ['setPassword', { account: 'a', password: 'p' }, 'NoServiceProvided'], + ['setPassword', { account: 'a', service: 's' }, 'NoPasswordProvided'], + ['deletePassword', { service: 's' }, 'NoAccountProvided'], + ['deletePassword', { account: 'a' }, 'NoServiceProvided'], + ['createKeychain', { password: 'p' }, 'NoKeychainNameProvided'], + ['createKeychain', { keychainName: 'k' }, 'NoPasswordProvided'], + ['deleteKeychain', {}, 'NoKeychainNameProvided'], + ['setDefaultKeychain', {}, 'NoKeychainNameProvided'] + ].forEach(function (c) { + it(c[0] + ' ' + JSON.stringify(c[1]) + ' -> ' + c[2] + 'Error', function () { + return bothWays(c[0], c[1], expectError(errors[c[2] + 'Error'], c[2])).then(function () { + expect(fake.calls).to.have.length(0); + }); + }); + }); + + [ + ['getPassword', 'NoAccountProvided'], ['setPassword', 'NoAccountProvided'], + ['deletePassword', 'NoAccountProvided'], ['createKeychain', 'NoKeychainNameProvided'], + ['deleteKeychain', 'NoKeychainNameProvided'], ['setDefaultKeychain', 'NoKeychainNameProvided'] + ].forEach(function (c) { + it(c[0] + ' with no opts at all -> ' + c[1] + 'Error (no throw)', function () { + return bothWays(c[0], undefined, expectError(errors[c[1] + 'Error'], c[1])); + }); + }); + }); + + describe('password methods refuse non-darwin platforms', function () { + ['getPassword', 'setPassword', 'deletePassword'].forEach(function (m) { + it(m, function () { + Object.defineProperty(process, 'platform', { value: 'linux' }); + return bothWays(m, pw, function (err, value, how) { + expectError(errors.UnsupportedPlatformError, 'UnsupportedPlatform')(err, value, how); + expect(err.message, how).to.equal('Expected darwin platform, got: linux'); + }).then(function () { expect(fake.calls).to.have.length(0); }); + }); + }); + }); + + describe('security failing to start -> ServiceFailureError, callback called once', function () { + [ + ['getPassword', pw], ['setPassword', pw], ['deletePassword', pw], + ['createKeychain', kc], ['deleteKeychain', kc], ['setDefaultKeychain', kc] + ].forEach(function (c) { + it(c[0], function () { + fake.respond({ error: 'spawn /usr/bin/security ENOENT' }); + return bothWays(c[0], c[1], function (err, value, how) { + expectError(errors.ServiceFailureError, 'ServiceFailure')(err, value, how); + expect(err.message, how).to.equal('Keychain failed to start child process: spawn /usr/bin/security ENOENT'); + }); + }); + }); + }); + + describe('non-zero exit codes', function () { + it('getPassword -> PasswordNotFoundError', function () { + fake.respond({ code: 44, stderr: 'security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.\n' }); + return bothWays('getPassword', pw, expectError(errors.PasswordNotFoundError, 'PasswordNotFound')); + }); + + it('deletePassword -> PasswordNotFoundError', function () { + fake.respond({ code: 44 }); + return bothWays('deletePassword', pw, expectError(errors.PasswordNotFoundError, 'PasswordNotFound')); + }); + + [['createKeychain', 48], ['deleteKeychain', 50], ['setDefaultKeychain', 1], ['setPassword', 51]].forEach(function (c) { + it(c[0] + ' -> ServiceFailureError with exitCode', function () { + fake.respond({ code: c[1] }); + return bothWays(c[0], c[0] === 'setPassword' ? pw : kc, function (err, value, how) { + expectError(errors.ServiceFailureError, 'ServiceFailure')(err, value, how); + expect(err.message, how).to.equal('Security returned a non-successful error code: ' + c[1]); + expect(err.exitCode, how).to.equal(c[1]); + }); + }); + }); + + it('setPassword exit 45 whose delete fails passes the delete error through', function () { + fake.respond(function (call) { + var argv = fake.commands(call)[0]; + return { code: argv[0] === 'delete-generic-password' ? 44 : 45 }; + }); + return bothWays('setPassword', pw, expectError(errors.PasswordNotFoundError, 'PasswordNotFound')); + }); + }); + + describe('getPassword output parsing', function () { + function got(stderr, expected) { + // Real security prints the item's attributes on stdout, the password on stderr. + fake.respond({ code: 0, stdout: 'keychain: "/Users/me/Library/Keychains/login.keychain-db"\nclass: "genp"\n', stderr: stderr }); + return bothWays('getPassword', pw, function (err, value, how) { + expect(err, how).to.equal(null); + expect(value, how).to.equal(expected); + }); + } + + it('quoted value', function () { return got('password: "plain"\n', 'plain'); }); + it('hex value wins over the quoted one', function () { return got('password: 0x70617373576974685C "passWith\\134"\n', 'passWith\\'); }); + it('hex-only (non-ASCII) value', function () { return got('password: 0xE28886CB9A\n', '∆˚'); }); + it('empty quoted value', function () { return got('password: ""\n', ''); }); + + [['no password line', 'nothing useful\n'], ['unparseable password line', 'password: \n'], ['no stderr at all', '']].forEach(function (c) { + it(c[0] + ' -> PasswordNotFoundError', function () { + fake.respond({ code: 0, stderr: c[1] }); + return bothWays('getPassword', pw, expectError(errors.PasswordNotFoundError, 'PasswordNotFound')); + }); + }); + }); + + describe('callback-less calls on a runtime without Promise', function () { + var saved; + beforeEach(function () { saved = global.Promise; }); + afterEach(function () { global.Promise = saved; }); + + it('return undefined and still run security, as 1.5.0 did', function (done) { + var P = global.Promise; + global.Promise = undefined; + var ret = keychain.deletePassword({ account: 'a', service: 's' }); + global.Promise = P; + expect(ret).to.equal(undefined); + setImmediate(function () { + expect(fake.calls).to.have.length(1); + done(); + }); + }); + }); + + describe('error classes keep the 1.5.0 shape', function () { + var expected = { + UnsupportedPlatformError: 'Expected darwin platform, got: ', + NoAccountProvidedError: 'An account is required', + NoServiceProvidedError: 'A service is required', + NoPasswordProvidedError: 'A password is required', + ServiceFailureError: 'Keychain failed to start child process: ', + PasswordNotFoundError: 'Could not find password', + NoKeychainNameProvidedError: 'A keychain name is required' + }; + + it('exposes exactly the 1.5.0 classes', function () { + expect(Object.keys(errors).sort()).to.deep.equal(Object.keys(expected).sort()); + }); + + Object.keys(expected).forEach(function (name) { + it(name + ' default message, append, type/code, stack', function () { + var e = new errors[name](); + expect(e).to.be.an.instanceof(Error); + expect(e.constructor).to.equal(errors[name]); + expect(e.message).to.equal(expected[name]); + expect(e.type).to.equal(name); + expect(e.code).to.equal(name.replace(/Error$/, '')); + expect(e.stack).to.be.a('string'); + expect(new errors[name]('custom', '!').message).to.equal('custom!'); + }); + }); + }); +}); diff --git a/test/unit/keychain.unit.test.js b/test/unit/keychain.unit.test.js new file mode 100644 index 0000000..2775f5c --- /dev/null +++ b/test/unit/keychain.unit.test.js @@ -0,0 +1,444 @@ +/*! + * Unit suite: every code path in keychain.js, with the `security` CLI faked. + * Runs on any OS. + */ + +var chai = require('chai'); +var expect = chai.expect; +var mock = require('../helpers/mock-security'); + +describe('KeychainAccess (unit, mocked security)', function () { + var keychain; + var errors; + + beforeEach(function () { + mock.reset(); + mock.pretendDarwin(); + keychain = mock.load(); + errors = keychain.constructor.errors; + }); + + afterEach(function () { + mock.restorePlatform(); + mock.unload(); + }); + + describe('module shape', function () { + it('exports a KeychainAccess instance with executablePath /usr/bin/security', function () { + expect(keychain.executablePath).to.equal('/usr/bin/security'); + expect(keychain.constructor.name).to.equal('KeychainAccess'); + }); + + it('exposes the seven error classes on the constructor', function () { + expect(Object.keys(errors).sort()).to.deep.equal([ + 'NoAccountProvidedError', + 'NoKeychainNameProvidedError', + 'NoPasswordProvidedError', + 'NoServiceProvidedError', + 'PasswordNotFoundError', + 'ServiceFailureError', + 'UnsupportedPlatformError' + ]); + }); + }); + + describe('platform guard', function () { + ['getPassword', 'setPassword', 'deletePassword'].forEach(function (method) { + it(method + ' fails with UnsupportedPlatformError off darwin', function (done) { + mock.pretendPlatform('linux'); + keychain[method]({ account: 'a', service: 's', password: 'p' }, function (err, result) { + expect(err).to.be.instanceOf(errors.UnsupportedPlatformError); + expect(err.message).to.equal('Expected darwin platform, got: linux'); + expect(result).to.equal(null); + expect(mock.calls).to.have.length(0); + done(); + }); + }); + }); + }); + + describe('.getPassword(opts, fn)', function () { + it('spawns security find-generic-password with -a/-s/-g', function (done) { + mock.respond(mock.findOutput('hunter2')); + keychain.getPassword({ account: 'me', service: 'svc' }, function (err, pass) { + expect(err).to.equal(null); + expect(pass).to.equal('hunter2'); + expect(mock.calls).to.have.length(1); + expect(mock.calls[0].cmd).to.equal('/usr/bin/security'); + expect(mock.calls[0].args).to.deep.equal(['find-generic-password', '-a', 'me', '-s', 'svc', '-g']); + done(); + }); + }); + + it('uses find-internet-password when type is internet (case-insensitive)', function (done) { + mock.respond(mock.findOutput('x')); + keychain.getPassword({ account: 'me', service: 'svc', type: 'INTERNET' }, function (err) { + expect(err).to.equal(null); + expect(mock.calls[0].args[0]).to.equal('find-internet-password'); + done(); + }); + }); + + it('decodes the hex form when security escapes characters', function (done) { + mock.respond(mock.findOutput('passWith\\', { hex: true, quoted: 'passWith\\134' })); + keychain.getPassword({ account: 'me', service: 'svc' }, function (err, pass) { + expect(err).to.equal(null); + expect(pass).to.equal('passWith\\'); + done(); + }); + }); + + it('decodes the hex-only form for non-ASCII passwords', function (done) { + var pw = '∆˚ˆ©ƒ®∂çµ˚¬˙ƒ®†¥'; + mock.respond(mock.findOutput(pw, { hex: true, quoted: false })); + keychain.getPassword({ account: 'me', service: 'svc' }, function (err, pass) { + expect(err).to.equal(null); + expect(pass).to.equal(pw); + done(); + }); + }); + + it('returns a quoted password that itself looks like a hex line', function (done) { + var pw = 'password: 0x4e6573746564'; + mock.respond(mock.findOutput(pw)); + keychain.getPassword({ account: 'me', service: 'svc' }, function (err, pass) { + expect(err).to.equal(null); + expect(pass).to.equal(pw); + done(); + }); + }); + + it('returns an empty string for an empty quoted password', function (done) { + mock.respond({ stdout: 'class: "genp"\n', stderr: 'password: ""\n', code: 0 }); + keychain.getPassword({ account: 'me', service: 'svc' }, function (err, pass) { + expect(err).to.equal(null); + expect(pass).to.equal(''); + done(); + }); + }); + + it('reassembles a password delivered in several stderr chunks', function (done) { + mock.respond({ stdout: '', stderr: ['password: "ab', 'cd"\n'], code: 0 }); + keychain.getPassword({ account: 'me', service: 'svc' }, function (err, pass) { + expect(err).to.equal(null); + expect(pass).to.equal('abcd'); + done(); + }); + }); + + it('fails with PasswordNotFoundError on a non-zero exit', function (done) { + mock.respond({ stderr: 'security: SecKeychainSearchCopyNext: The specified item could not be found in the keychain.\n', code: 44 }); + keychain.getPassword({ account: 'me', service: 'svc' }, function (err, pass) { + expect(err).to.be.instanceOf(errors.PasswordNotFoundError); + expect(err.message).to.equal('Could not find password'); + expect(pass).to.equal(null); + done(); + }); + }); + + it('fails with PasswordNotFoundError when exit is 0 but no password line appears', function (done) { + mock.respond({ stdout: 'class: "genp"\n', stderr: '', code: 0 }); + keychain.getPassword({ account: 'me', service: 'svc' }, function (err, pass) { + expect(err).to.be.instanceOf(errors.PasswordNotFoundError); + expect(pass).to.equal(null); + done(); + }); + }); + + it('fails with ServiceFailureError when security cannot be spawned', function (done) { + mock.respond({ error: new Error('spawn /usr/bin/security ENOENT') }); + keychain.getPassword({ account: 'me', service: 'svc' }, function (err, pass) { + expect(err).to.be.instanceOf(errors.ServiceFailureError); + expect(err.message).to.equal('Keychain failed to start child process: spawn /usr/bin/security ENOENT'); + expect(pass).to.equal(null); + done(); + }); + }); + + it('fails with NoAccountProvidedError / NoServiceProvidedError before spawning', function () { + var seen = []; + keychain.getPassword({ service: 's' }, function (err) { seen.push(err); }); + keychain.getPassword({ account: 'a' }, function (err) { seen.push(err); }); + keychain.getPassword(undefined, function (err) { seen.push(err); }); + expect(seen[0]).to.be.instanceOf(errors.NoAccountProvidedError); + expect(seen[1]).to.be.instanceOf(errors.NoServiceProvidedError); + expect(seen[2]).to.be.instanceOf(errors.NoAccountProvidedError); + expect(mock.calls).to.have.length(0); + }); + + it('honours a custom executablePath', function (done) { + keychain.executablePath = '/opt/bin/security'; + mock.respond(mock.findOutput('p')); + keychain.getPassword({ account: 'me', service: 'svc' }, function () { + expect(mock.calls[0].cmd).to.equal('/opt/bin/security'); + done(); + }); + }); + }); + + describe('.setPassword(opts, fn)', function () { + it('runs add-generic-password -a/-s/-w through `security -i` (password on stdin, not argv) and returns the password', function (done) { + mock.respond({ code: 0 }); + keychain.setPassword({ account: 'me', service: 'svc', password: 'pw' }, function (err, pass) { + expect(err).to.equal(null); + expect(pass).to.equal('pw'); + expect(mock.calls).to.have.length(1); + expect(mock.calls[0].cmd).to.equal('/usr/bin/security'); + expect(mock.calls[0].args).to.deep.equal(['-i']); + expect(mock.calls[0].stdinEnded).to.equal(true); + expect(mock.commands(mock.calls[0])).to.deep.equal([ + ['add-generic-password', '-a', 'me', '-s', 'svc', '-w', 'pw'] + ]); + done(); + }); + }); + + it('uses add-internet-password when type is internet', function (done) { + mock.respond({ code: 0 }); + keychain.setPassword({ account: 'me', service: 'svc', password: 'pw', type: 'internet' }, function (err) { + expect(err).to.equal(null); + expect(mock.commands(mock.calls[0])[0][0]).to.equal('add-internet-password'); + done(); + }); + }); + + it('on exit 45 (already exists) deletes then re-adds the password', function (done) { + mock.respond({ code: 45 }); // add -> exists + mock.respond({ code: 0 }); // delete + mock.respond({ code: 0 }); // add again + keychain.setPassword({ account: 'me', service: 'svc', password: 'pw' }, function (err, pass) { + expect(err).to.equal(null); + expect(pass).to.equal('pw'); + expect(mock.calls.map(function (c) { return mock.commands(c)[0][0]; })).to.deep.equal([ + 'add-generic-password', 'delete-generic-password', 'add-generic-password' + ]); + done(); + }); + }); + + it('on exit 45 surfaces the delete error if the delete fails', function (done) { + mock.respond({ code: 45 }); + mock.respond({ code: 1 }); + keychain.setPassword({ account: 'me', service: 'svc', password: 'pw' }, function (err) { + expect(err).to.be.instanceOf(errors.PasswordNotFoundError); + expect(mock.calls).to.have.length(2); + done(); + }); + }); + + it('fails with ServiceFailureError carrying exitCode on any other non-zero exit', function (done) { + mock.respond({ code: 36 }); + keychain.setPassword({ account: 'me', service: 'svc', password: 'pw' }, function (err) { + expect(err).to.be.instanceOf(errors.ServiceFailureError); + expect(err.message).to.equal('Security returned a non-successful error code: 36'); + expect(err.exitCode).to.equal(36); + done(); + }); + }); + + it('fails with ServiceFailureError when security cannot be spawned', function (done) { + mock.respond({ error: new Error('spawn EACCES') }); + keychain.setPassword({ account: 'me', service: 'svc', password: 'pw' }, function (err) { + expect(err).to.be.instanceOf(errors.ServiceFailureError); + expect(err.message).to.equal('Keychain failed to start child process: spawn EACCES'); + done(); + }); + }); + + it('validates account, service and password before spawning', function () { + var seen = []; + keychain.setPassword({ service: 's', password: 'p' }, function (err) { seen.push(err); }); + keychain.setPassword({ account: 'a', password: 'p' }, function (err) { seen.push(err); }); + keychain.setPassword({ account: 'a', service: 's' }, function (err) { seen.push(err); }); + expect(seen[0]).to.be.instanceOf(errors.NoAccountProvidedError); + expect(seen[1]).to.be.instanceOf(errors.NoServiceProvidedError); + expect(seen[2]).to.be.instanceOf(errors.NoPasswordProvidedError); + expect(mock.calls).to.have.length(0); + }); + }); + + describe('.deletePassword(opts, fn)', function () { + it('spawns security delete-generic-password with -a/-s and calls back with no error', function (done) { + mock.respond({ code: 0 }); + keychain.deletePassword({ account: 'me', service: 'svc' }, function (err) { + expect(err).to.equal(null); + expect(mock.calls).to.have.length(1); + expect(mock.calls[0].cmd).to.equal('/usr/bin/security'); + expect(mock.calls[0].args).to.deep.equal(['delete-generic-password', '-a', 'me', '-s', 'svc']); + done(); + }); + }); + + it('uses delete-internet-password when type is internet', function (done) { + mock.respond({ code: 0 }); + keychain.deletePassword({ account: 'me', service: 'svc', type: 'Internet' }, function () { + expect(mock.calls[0].args[0]).to.equal('delete-internet-password'); + done(); + }); + }); + + it('fails with PasswordNotFoundError on a non-zero exit', function (done) { + mock.respond({ code: 44 }); + keychain.deletePassword({ account: 'me', service: 'svc' }, function (err) { + expect(err).to.be.instanceOf(errors.PasswordNotFoundError); + done(); + }); + }); + + it('fails with ServiceFailureError when security cannot be spawned', function (done) { + mock.respond({ error: new Error('spawn ENOENT') }); + keychain.deletePassword({ account: 'me', service: 'svc' }, function (err) { + expect(err).to.be.instanceOf(errors.ServiceFailureError); + done(); + }); + }); + + it('validates account and service before spawning', function () { + var seen = []; + keychain.deletePassword({ service: 's' }, function (err) { seen.push(err); }); + keychain.deletePassword({ account: 'a' }, function (err) { seen.push(err); }); + expect(seen[0]).to.be.instanceOf(errors.NoAccountProvidedError); + expect(seen[1]).to.be.instanceOf(errors.NoServiceProvidedError); + expect(mock.calls).to.have.length(0); + }); + }); + + describe('.createKeychain(opts, fn)', function () { + it('runs create-keychain -p -- through `security -i` and returns the name', function (done) { + mock.respond({ code: 0 }); + keychain.createKeychain({ keychainName: 'test.keychain', password: 'pw' }, function (err, name) { + expect(err).to.equal(null); + expect(name).to.equal('test.keychain'); + expect(mock.calls).to.have.length(1); + expect(mock.calls[0].cmd).to.equal('/usr/bin/security'); + expect(mock.calls[0].args).to.deep.equal(['-i']); + expect(mock.commands(mock.calls[0])).to.deep.equal([ + ['create-keychain', '-p', 'pw', '--', 'test.keychain'] + ]); + done(); + }); + }); + + it('fails with ServiceFailureError carrying exitCode on non-zero exit', function (done) { + mock.respond({ code: 48 }); + keychain.createKeychain({ keychainName: 'test.keychain', password: 'pw' }, function (err) { + expect(err).to.be.instanceOf(errors.ServiceFailureError); + expect(err.exitCode).to.equal(48); + done(); + }); + }); + + it('fails with ServiceFailureError when security cannot be spawned', function (done) { + mock.respond({ error: new Error('spawn ENOENT') }); + keychain.createKeychain({ keychainName: 'k', password: 'pw' }, function (err) { + expect(err).to.be.instanceOf(errors.ServiceFailureError); + done(); + }); + }); + + it('validates keychainName and password before spawning (no platform guard)', function () { + var seen = []; + keychain.createKeychain({ password: 'p' }, function (err) { seen.push(err); }); + keychain.createKeychain({ keychainName: 'k' }, function (err) { seen.push(err); }); + keychain.createKeychain(undefined, function (err) { seen.push(err); }); + expect(seen[0]).to.be.instanceOf(errors.NoKeychainNameProvidedError); + expect(seen[1]).to.be.instanceOf(errors.NoPasswordProvidedError); + expect(seen[2]).to.be.instanceOf(errors.NoKeychainNameProvidedError); + expect(mock.calls).to.have.length(0); + }); + }); + + describe('.deleteKeychain(opts, fn)', function () { + it('spawns security delete-keychain -- and returns the name', function (done) { + mock.respond({ code: 0 }); + keychain.deleteKeychain({ keychainName: 'test.keychain' }, function (err, name) { + expect(err).to.equal(null); + expect(name).to.equal('test.keychain'); + expect(mock.calls).to.have.length(1); + expect(mock.calls[0].cmd).to.equal('/usr/bin/security'); + expect(mock.calls[0].args).to.deep.equal(['delete-keychain', '--', 'test.keychain']); + done(); + }); + }); + + it('fails with ServiceFailureError carrying exitCode on non-zero exit', function (done) { + mock.respond({ code: 50 }); + keychain.deleteKeychain({ keychainName: 'test.keychain' }, function (err) { + expect(err).to.be.instanceOf(errors.ServiceFailureError); + expect(err.exitCode).to.equal(50); + done(); + }); + }); + + it('fails with ServiceFailureError when security cannot be spawned', function (done) { + mock.respond({ error: new Error('spawn ENOENT') }); + keychain.deleteKeychain({ keychainName: 'k' }, function (err) { + expect(err).to.be.instanceOf(errors.ServiceFailureError); + done(); + }); + }); + + it('validates keychainName before spawning', function () { + var seen = []; + keychain.deleteKeychain({}, function (err) { seen.push(err); }); + keychain.deleteKeychain(undefined, function (err) { seen.push(err); }); + expect(seen[0]).to.be.instanceOf(errors.NoKeychainNameProvidedError); + expect(seen[1]).to.be.instanceOf(errors.NoKeychainNameProvidedError); + expect(mock.calls).to.have.length(0); + }); + }); + + describe('.setDefaultKeychain(opts, fn)', function () { + it('spawns security default-keychain -s -- and returns the name', function (done) { + mock.respond({ code: 0 }); + keychain.setDefaultKeychain({ keychainName: 'login.keychain' }, function (err, name) { + expect(err).to.equal(null); + expect(name).to.equal('login.keychain'); + expect(mock.calls).to.have.length(1); + expect(mock.calls[0].cmd).to.equal('/usr/bin/security'); + expect(mock.calls[0].args).to.deep.equal(['default-keychain', '-s', '--', 'login.keychain']); + done(); + }); + }); + + it('fails with ServiceFailureError carrying exitCode on non-zero exit', function (done) { + mock.respond({ code: 50 }); + keychain.setDefaultKeychain({ keychainName: 'missing' }, function (err) { + expect(err).to.be.instanceOf(errors.ServiceFailureError); + expect(err.exitCode).to.equal(50); + done(); + }); + }); + + it('fails with ServiceFailureError when security cannot be spawned', function (done) { + mock.respond({ error: new Error('spawn ENOENT') }); + keychain.setDefaultKeychain({ keychainName: 'k' }, function (err) { + expect(err).to.be.instanceOf(errors.ServiceFailureError); + done(); + }); + }); + + it('validates keychainName before spawning', function () { + var seen = []; + keychain.setDefaultKeychain({}, function (err) { seen.push(err); }); + keychain.setDefaultKeychain(undefined, function (err) { seen.push(err); }); + expect(seen[0]).to.be.instanceOf(errors.NoKeychainNameProvidedError); + expect(seen[1]).to.be.instanceOf(errors.NoKeychainNameProvidedError); + expect(mock.calls).to.have.length(0); + }); + }); + + describe('argument safety', function () { + it('passes hostile account/service/password values as discrete tokens security parses, never through a shell', function (done) { + var evil = '$(touch /tmp/pwned); `id`; foo | bar && baz'; + mock.respond({ code: 0 }); + keychain.setPassword({ account: evil, service: evil, password: evil }, function () { + expect(mock.calls[0].args).to.deep.equal(['-i']); + expect(mock.commands(mock.calls[0])).to.deep.equal([ + ['add-generic-password', '-a', evil, '-s', evil, '-w', evil] + ]); + done(); + }); + }); + }); +}); diff --git a/test/unit/promise.test.js b/test/unit/promise.test.js new file mode 100644 index 0000000..eac626b --- /dev/null +++ b/test/unit/promise.test.js @@ -0,0 +1,132 @@ +// Promise API: every method returns a Promise when no callback is passed, and +// behaves exactly as before when one is. Mocked, so it runs on any OS. +var fake = require('../helpers/fake-spawn'); +fake.install(); +var keychain = require('../../'); +fake.restore(); +var chai = require('chai'); +chai.should(); + +var errors = keychain.constructor.errors; + +var cases = [ + { method: 'getPassword', opts: { account: 'a', service: 's' }, ok: { stderr: 'password: "secret"\n' }, value: 'secret' }, + { method: 'setPassword', opts: { account: 'a', service: 's', password: 'secret' }, ok: {}, value: 'secret' }, + { method: 'deletePassword', opts: { account: 'a', service: 's' }, ok: {}, value: undefined }, + { method: 'createKeychain', opts: { keychainName: 'k.keychain', password: 'pw' }, ok: {}, value: 'k.keychain' }, + { method: 'deleteKeychain', opts: { keychainName: 'k.keychain' }, ok: {}, value: 'k.keychain' }, + { method: 'setDefaultKeychain', opts: { keychainName: 'k.keychain' }, ok: {}, value: 'k.keychain' } +]; + +describe('promise API (mocked security)', function () { + beforeEach(function () { fake.install(); }); + afterEach(function () { fake.restore(); }); + + cases.forEach(function (c) { + describe('.' + c.method + '(opts)', function () { + it('returns a Promise when no callback is passed', function () { + fake.respond(c.ok); + var p = keychain[c.method](Object.assign({}, c.opts)); + p.should.be.instanceOf(Promise); + return p; + }); + + it('resolves with the value the callback gets', function (done) { + fake.respond(c.ok); + keychain[c.method](Object.assign({}, c.opts), function (err, cbValue) { + (err === null || err === undefined).should.equal(true); + fake.respond(c.ok); + keychain[c.method](Object.assign({}, c.opts)).then(function (value) { + (value === cbValue).should.equal(true); + (value === c.value).should.equal(true); + done(); + }, done); + }); + }); + + it('rejects with the same error class the callback gets', function (done) { + fake.respond({ code: 1 }); + keychain[c.method](Object.assign({}, c.opts), function (cbErr) { + fake.respond({ code: 1 }); + keychain[c.method](Object.assign({}, c.opts)).then(function () { + done(new Error('expected a rejection')); + }, function (err) { + err.constructor.should.equal(cbErr.constructor); + err.code.should.equal(cbErr.code); + err.message.should.equal(cbErr.message); + done(); + }); + }); + }); + + it('returns undefined when a callback is passed', function (done) { + fake.respond(c.ok); + (keychain[c.method](Object.assign({}, c.opts), function () { done(); }) === undefined).should.equal(true); + }); + + it('rejects validation errors (missing options)', function () { + return keychain[c.method]({}).then(function () { + throw new Error('expected a rejection'); + }, function (err) { + err.should.be.instanceOf(Error); + (typeof err.code).should.equal('string'); + }); + }); + }); + }); + + it('still calls back synchronously for validation errors', function () { + var called = false; + keychain.getPassword({ service: 's' }, function (err) { + err.should.be.instanceOf(errors.NoAccountProvidedError); + called = true; + }); + called.should.equal(true); + }); + + it('rejects with UnsupportedPlatformError off macOS', function () { + Object.defineProperty(process, 'platform', { value: 'linux' }); + return keychain.getPassword({ account: 'a', service: 's' }).then(function () { + throw new Error('expected a rejection'); + }, function (err) { + err.should.be.instanceOf(errors.UnsupportedPlatformError); + err.message.should.equal('Expected darwin platform, got: linux'); + }); + }); + + it('setPassword resolves after replacing an existing item (exit 45)', function () { + var codes = [45, 0, 0]; + fake.respond({ code: 45 }); + var childProcess = require('child_process'); + var stub = childProcess.spawn; + childProcess.spawn = function (cmd, args) { + fake.respond({ code: codes.shift() }); + return stub(cmd, args); + }; + return keychain.setPassword({ account: 'a', service: 's', password: 'new' }).then(function (pw) { + pw.should.equal('new'); + fake.calls.map(function (c) { return fake.commands(c)[0][0]; }) + .should.deep.equal(['add-generic-password', 'delete-generic-password', 'add-generic-password']); + }); + }); + + it('works with async/await', async function () { + fake.respond({ stderr: 'password: 0xE28886656C7461\n' }); + var pass = await keychain.getPassword({ account: 'a', service: 's' }); + pass.should.equal('∆elta'); + }); + + it('does not crash fire-and-forget callers on errors (no unhandled rejection)', function (done) { + var unhandled = []; + var onUnhandled = function (reason) { unhandled.push(reason); }; + process.on('unhandledRejection', onUnhandled); + fake.respond({ code: 1 }); + keychain.deletePassword({ account: 'a', service: 's' }); + keychain.setPassword({}); + setTimeout(function () { + process.removeListener('unhandledRejection', onUnhandled); + unhandled.length.should.equal(0); + done(); + }, 50); + }); +}); diff --git a/test/unit/retry.test.js b/test/unit/retry.test.js new file mode 100644 index 0000000..84a92ca --- /dev/null +++ b/test/unit/retry.test.js @@ -0,0 +1,52 @@ +// setPassword retries once (delete, then add again) when the item already +// exists (exit 45). Mocked, so it runs on any OS. +var fake = require('../helpers/fake-spawn'); +fake.install(); +var keychain = require('../../'); +fake.restore(); +var chai = require('chai'); +chai.should(); + +var errors = keychain.constructor.errors; + +function isAdd(call) { + return fake.commands(call).some(function (argv) { return /^add-/.test(argv[0]); }); +} + +describe('setPassword exit-45 retry (mocked security)', function () { + beforeEach(function () { fake.install(); }); + afterEach(function () { fake.restore(); }); + + it('deletes and adds again once when the item already exists', function (done) { + var adds = 0; + fake.respond(function (call) { + if (!isAdd(call)) return { code: 0 }; + adds++; + return { code: adds === 1 ? 45 : 0 }; + }); + keychain.setPassword({ account: 'a', service: 's', password: 'p' }, function (err, pass) { + (err === null).should.equal(true); + pass.should.equal('p'); + fake.calls.map(function (c) { return fake.commands(c)[0][0]; }) + .should.deep.equal(['add-generic-password', 'delete-generic-password', 'add-generic-password']); + done(); + }); + }); + + it('gives up with exit code 45 instead of looping when the add keeps failing', function (done) { + var adds = 0; + // Stop answering 45 after 10 adds so a looping implementation ends and + // fails this test instead of hanging it. + fake.respond(function (call) { + if (!isAdd(call)) return { code: 0 }; + adds++; + return { code: adds <= 10 ? 45 : 0 }; + }); + keychain.setPassword({ account: 'a', service: 's', password: 'p' }, function (err) { + adds.should.be.at.most(2, 'add attempts'); + err.should.be.instanceOf(errors.ServiceFailureError); + err.exitCode.should.equal(45); + done(); + }); + }); +}); diff --git a/test/unit/robustness.test.js b/test/unit/robustness.test.js new file mode 100644 index 0000000..c34c2e9 --- /dev/null +++ b/test/unit/robustness.test.js @@ -0,0 +1,53 @@ +// Regression tests for bugs in 1.5.0. Mocked, so they run on any OS. +var fake = require('../helpers/fake-spawn'); +fake.install(); +var keychain = require('../../'); +fake.restore(); +var chai = require('chai'); +chai.should(); + +var errors = keychain.constructor.errors; + +describe('robustness (mocked security)', function () { + beforeEach(function () { fake.install(); }); + afterEach(function () { fake.restore(); }); + + describe('when security cannot be started', function () { + ['getPassword', 'setPassword', 'deletePassword'].forEach(function (method) { + it(method + ' calls back exactly once with ServiceFailureError', function (done) { + fake.respond({ error: 'spawn /usr/bin/security ENOENT' }); + var calls = []; + keychain[method]({ account: 'a', service: 's', password: 'p' }, function (err) { + calls.push(err); + }); + setTimeout(function () { + calls.length.should.equal(1); + calls[0].should.be.instanceOf(errors.ServiceFailureError); + done(); + }, 50); + }); + }); + }); + + describe('getPassword with output it cannot parse', function () { + it('calls back with PasswordNotFoundError instead of throwing', function (done) { + fake.respond({ stderr: 'password: \n', code: 0 }); + keychain.getPassword({ account: 'a', service: 's' }, function (err, pass) { + err.should.be.instanceOf(errors.PasswordNotFoundError); + (pass === null).should.equal(true); + done(); + }); + }); + }); + + describe('getPassword hex parsing', function () { + it('reads the hex form even when another line comes first', function (done) { + fake.respond({ stderr: 'warning: 0xDEAD something\npassword: 0x70617373576974685C "passWith\\134"\n', code: 0 }); + keychain.getPassword({ account: 'a', service: 's' }, function (err, pass) { + (err === null).should.equal(true); + pass.should.equal('passWith\\'); + done(); + }); + }); + }); +}); diff --git a/test/unit/security.test.js b/test/unit/security.test.js new file mode 100644 index 0000000..9a9e5d0 --- /dev/null +++ b/test/unit/security.test.js @@ -0,0 +1,85 @@ +// Security properties of how keychain.js drives `security`. Mocked, so it runs +// on any OS; fake.commands() shows what `security` would actually execute. +var fake = require('../helpers/fake-spawn'); +fake.install(); +var keychain = require('../../'); +fake.restore(); +var chai = require('chai'); +chai.should(); + +describe('security (mocked security)', function () { + beforeEach(function () { fake.install(); fake.respond({ code: 0 }); }); + afterEach(function () { fake.restore(); }); + + var secret = 'hunter2-S3cret!'; + + function lastCall() { return fake.calls[fake.calls.length - 1]; } + + describe('passwords stay out of the process argument list (visible in ps)', function () { + it('setPassword', function () { + return keychain.setPassword({ account: 'a', service: 's', password: secret }).then(function () { + lastCall().args.join(' ').should.not.contain(secret); + }); + }); + + it('createKeychain', function () { + return keychain.createKeychain({ keychainName: 'k.keychain', password: secret }).then(function () { + lastCall().args.join(' ').should.not.contain(secret); + }); + }); + }); + + describe('keychain names that start with "-" are not parsed as options', function () { + [ + ['createKeychain', { keychainName: '-P', password: 'pw' }], + ['deleteKeychain', { keychainName: '-P' }], + ['setDefaultKeychain', { keychainName: '-P' }] + ].forEach(function (c) { + it(c[0], function () { + return keychain[c[0]](c[1]).then(function () { + var argv = fake.commands(lastCall())[0]; + var end = argv.indexOf('--'); + end.should.be.above(0, 'expected "--" before the keychain name'); + argv.indexOf('-P').should.be.above(end); + }); + }); + }); + }); + + describe('values cannot inject extra security commands', function () { + var nasty = [ + 'plain', + 'with space', + 'quote"inside', + "single'quote", + 'back\\slash\\', + '\\"; delete-keychain login.keychain; "', + '-w', + 'tab\there', + 'cr\rhere', + 'line\nbreak delete-keychain login.keychain', + '∆˚ˆ©ƒ®∂çµ˚¬˙ƒ®†¥', + 'x'.repeat(5000) + ' delete-keychain login.keychain' + ]; + + nasty.forEach(function (value) { + var label = JSON.stringify(value.length > 40 ? value.slice(0, 20) + '…(' + value.length + ')' : value); + + it('setPassword runs exactly one add command with ' + label + ' intact', function () { + return keychain.setPassword({ account: value, service: value, password: value }).then(function () { + var cmds = fake.commands(lastCall()); + cmds.length.should.equal(1); + cmds[0].should.deep.equal(['add-generic-password', '-a', value, '-s', value, '-w', value]); + }); + }); + + it('createKeychain runs exactly one create command with ' + label + ' intact', function () { + return keychain.createKeychain({ keychainName: value, password: value }).then(function () { + var cmds = fake.commands(lastCall()); + cmds.length.should.equal(1); + cmds[0].should.deep.equal(['create-keychain', '-p', value, '--', value]); + }); + }); + }); + }); +});