From ca64dff7facf369e0ecf30bb35b112d4a782f1da Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Tue, 20 Oct 2026 22:34:50 -0400 Subject: [PATCH 01/16] Add promise support to every public method Each method returns a Promise when no callback is passed and behaves exactly as before when one is (same arguments, return value, error classes, and synchronous validation callbacks). Promises are marked handled so callers that used to fire and forget don't start crashing on unhandled rejections. Also fixes, without changing the callback API: - callbacks ran twice when security couldn't be started ('error' then 'close') - getPassword threw an uncaught TypeError on output it couldn't parse - err was an implicit global in createKeychain/deleteKeychain/setDefaultKeychain - hex password parsing now reads the password line, not the first 0x anywhere - child_process.spawn is looked up at call time so tests can stub it --- keychain.js | 91 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 85 insertions(+), 6 deletions(-) diff --git a/keychain.js b/keychain.js index 5885acd..ac204d0 100644 --- a/keychain.js +++ b/keychain.js @@ -8,9 +8,15 @@ * 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); +} + // Polyfill Buffer.from for Node < 4 that didn't have a #from method if (!Buffer.from) { Buffer.from = function (data, encoding, len) { @@ -105,15 +111,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(); @@ -254,6 +266,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(); @@ -298,6 +311,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(); @@ -337,6 +351,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(); @@ -389,6 +404,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 */ From 3062ceb047572888f7aec705c4e62dff5c3d95d3 Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Tue, 20 Oct 2026 22:34:50 -0400 Subject: [PATCH 02/16] Add mocked promise and robustness tests (run on any OS) --- test/helpers/fake-spawn.js | 47 +++++++++++++ test/promise.test.js | 132 +++++++++++++++++++++++++++++++++++++ test/robustness.test.js | 53 +++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 test/helpers/fake-spawn.js create mode 100644 test/promise.test.js create mode 100644 test/robustness.test.js diff --git a/test/helpers/fake-spawn.js b/test/helpers/fake-spawn.js new file mode 100644 index 0000000..84e5448 --- /dev/null +++ b/test/helpers/fake-spawn.js @@ -0,0 +1,47 @@ +// 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). +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(); + child.stdout = new EventEmitter(); + child.stderr = new EventEmitter(); + var script = behavior || { code: 0 }; + setImmediate(function () { + 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; +}; diff --git a/test/promise.test.js b/test/promise.test.js new file mode 100644 index 0000000..9ee0534 --- /dev/null +++ b/test/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 c.args[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/robustness.test.js b/test/robustness.test.js new file mode 100644 index 0000000..0b7095b --- /dev/null +++ b/test/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(); + }); + }); + }); +}); From 5d01b6e3b22fb653f815eeb042661046b5c952fc Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Tue, 20 Oct 2026 22:37:22 -0400 Subject: [PATCH 03/16] CI: test on Node 18/20/22 (macos-latest is arm64 and has no Node 14 build) --- .github/workflows/test.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 6c842b7..54d8ef3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,11 +10,12 @@ jobs: build: runs-on: macos-latest strategy: + fail-fast: false matrix: - node: [ 14, 16, 18 ] + node: [ 18, 20, 22 ] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - run: npm install From f02994cb96abffa9a4fdf32829cf10bfd78eddf4 Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Tue, 20 Oct 2026 22:38:32 -0400 Subject: [PATCH 04/16] Keep passwords off the security command line; guard keychain names setPassword and createKeychain now send their command to `security -i` on stdin instead of argv, so the password isn't visible in ps. Arguments are double-quoted with backslash escapes, matching security's own split_line parser; lines it couldn't read back intact (newline, NUL, over 4094 bytes) fall back to argv as before. createKeychain, deleteKeychain and setDefaultKeychain put "--" before the keychain name so a name starting with "-" isn't parsed as an option. --- keychain.js | 41 ++++++++++++++++-- test/helpers/fake-spawn.js | 60 ++++++++++++++++++++++++++ test/macos-security.test.js | 50 ++++++++++++++++++++++ test/promise.test.js | 2 +- test/security.test.js | 85 +++++++++++++++++++++++++++++++++++++ 5 files changed, 233 insertions(+), 5 deletions(-) create mode 100644 test/macos-security.test.js create mode 100644 test/security.test.js diff --git a/keychain.js b/keychain.js index ac204d0..8098cb8 100644 --- a/keychain.js +++ b/keychain.js @@ -17,6 +17,39 @@ function spawn(command, args) { 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) { @@ -172,7 +205,7 @@ 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 security = spawnWithSecret(this.executablePath, [ 'add-'+opts.type+'-password', '-a', opts.account, '-s', opts.service, '-w', opts.password ]); var self = this; security.on('error', function(err) { @@ -280,7 +313,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); @@ -319,7 +352,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); @@ -359,7 +392,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); diff --git a/test/helpers/fake-spawn.js b/test/helpers/fake-spawn.js index 84e5448..9376953 100644 --- a/test/helpers/fake-spawn.js +++ b/test/helpers/fake-spawn.js @@ -21,8 +21,15 @@ exports.install = function () { 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 script = behavior || { code: 0 }; setImmediate(function () { if (script.error) { @@ -45,3 +52,56 @@ exports.restore = function () { 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/macos-security.test.js b/test/macos-security.test.js new file mode 100644 index 0000000..80f3d32 --- /dev/null +++ b/test/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/promise.test.js b/test/promise.test.js index 9ee0534..b2fb443 100644 --- a/test/promise.test.js +++ b/test/promise.test.js @@ -105,7 +105,7 @@ describe('promise API (mocked security)', function () { }; return keychain.setPassword({ account: 'a', service: 's', password: 'new' }).then(function (pw) { pw.should.equal('new'); - fake.calls.map(function (c) { return c.args[0]; }) + fake.calls.map(function (c) { return fake.commands(c)[0][0]; }) .should.deep.equal(['add-generic-password', 'delete-generic-password', 'add-generic-password']); }); }); diff --git a/test/security.test.js b/test/security.test.js new file mode 100644 index 0000000..13f8f24 --- /dev/null +++ b/test/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]); + }); + }); + }); + }); +}); From 86eb7d131b40ea4fc5ca396dea7a7c1d2db0e683 Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Tue, 20 Oct 2026 22:43:52 -0400 Subject: [PATCH 05/16] Dev deps: mocha 12 (npm audit clean); CI on Node 20/22/24 plus an old-Node smoke job mocha 10/11 pull in vulnerable serialize-javascript and diff (dev only; the package still has no runtime dependencies). mocha 12 needs Node 20.19+, so the macOS job moves to 20/22/24 and a plain-Node smoke test with security mocked checks the library on Node 10-18. --- .github/workflows/test.yaml | 17 +- package-lock.json | 1606 +++++++---------------------------- package.json | 2 +- test/smoke/old-node.js | 81 ++ 4 files changed, 386 insertions(+), 1320 deletions(-) create mode 100644 test/smoke/old-node.js diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 54d8ef3..7bc584c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - node: [ 18, 20, 22 ] + node: [ 20, 22, 24 ] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -20,3 +20,18 @@ jobs: 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-latest + strategy: + fail-fast: false + matrix: + node: [ 10, 12, 14, 16, 18 ] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - run: node test/smoke/old-node.js diff --git a/package-lock.json b/package-lock.json index 9a5a3e0..61af09f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,60 +10,24 @@ "license": "MIT", "devDependencies": { "chai": "^4.3.7", - "mocha": "^10.2.0" - } - }, - "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==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "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==", - "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==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?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==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" + "mocha": "^12.0.3" } }, "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 +39,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==", + "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": { - "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==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=8" + "node": "20 || >=22" } }, "node_modules/browser-stdout": { @@ -116,18 +65,6 @@ "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==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/chai": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", @@ -146,34 +83,6 @@ "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==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "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==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -184,74 +93,27 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "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" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 20.19.0" }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "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/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, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "funding": { + "url": "https://paulmillr.com/funding/" } }, - "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,24 +124,6 @@ } } }, - "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", @@ -293,170 +137,52 @@ } }, "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==", - "dev": true - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "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", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "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, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "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==", - "dev": true, - "bin": { - "flat": "cli.js" - } - }, - "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==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/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, - "engines": { - "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==", + "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, "engines": { "node": "*" } }, "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "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" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "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==", - "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_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==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -466,89 +192,16 @@ "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==", - "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==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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/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, "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==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" + "node": ">=12" }, - "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_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==", - "dev": true, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-unicode-supported": { @@ -564,476 +217,206 @@ } }, "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==", - "dev": true, - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/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, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "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==", - "dev": true, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "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==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.0" - } - }, - "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==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "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" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": ">= 14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mochajs" - } - }, - "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==", - "dev": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "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_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/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, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/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, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/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, - "engines": { - "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==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.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==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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==", + "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/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "url": "https://github.com/sponsors/puzrin" }, { - "type": "consulting", - "url": "https://feross.org/support" + "type": "github", + "url": "https://github.com/sponsors/nodeca" } - ] - }, - "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==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "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==", - "dev": true, + ], "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "argparse": "^3.0.3" }, - "engines": { - "node": ">=8" + "bin": { + "js-yaml": "bin/js-yaml.mjs" } }, - "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==", + "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==", "dev": true, "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" + "get-func-name": "^2.0.0" } }, - "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==", + "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": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "20 || >=22" } - }, - "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==", + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "brace-expansion": "^5.0.8" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "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/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, "engines": { - "node": ">=8.0" + "node": ">=16 || 14 >=14.17" } }, - "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==", - "dev": true, + "node_modules/mocha": { + "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.js" + }, "engines": { - "node": ">=4" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/workerpool": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "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/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==", + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" }, "engines": { - "node": ">=10" + "node": "18 || 20 || >=22" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "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==", + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "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/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "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": ">=10" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "node_modules/serialize-javascript": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", + "integrity": "sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==", "dev": true, - "dependencies": { - "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" - }, "engines": { - "node": ">=10" + "node": ">=20.0.0" } }, - "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==", + "node_modules/strip-json-comments": { + "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": ">=10" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "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/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, "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" + "has-flag": "^4.0.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/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==", + "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==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } + }, + "node_modules/workerpool": { + "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 } }, "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==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.3.tgz", + "integrity": "sha512-Sn98ftV3KfsO8xfLPHG6oT8y+b0yvbrLgZjgPDXsharNY15tTAjqIsIju4S32nl6/iEUNNE4v+yL2Zhm8ooP3w==", "dev": true }, "assertion-error": { @@ -1043,33 +426,18 @@ "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==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "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==", + "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, "requires": { - "fill-range": "^7.0.1" + "balanced-match": "^4.0.2" } }, "browser-stdout": { @@ -1078,12 +446,6 @@ "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", @@ -1099,27 +461,6 @@ "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", @@ -1127,76 +468,23 @@ "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==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "dev": true, "requires": { - "color-name": "~1.1.4" + "readdirp": "^5.0.0" } }, - "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==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "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 - } + "ms": "^2.1.3" } }, - "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", @@ -1207,121 +495,32 @@ } }, "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==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "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==", + "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 }, "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==", + "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 }, "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==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "requires": { - "is-glob": "^4.0.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" } }, "has-flag": { @@ -1330,68 +529,10 @@ "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==", + "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 }, "is-unicode-supported": { @@ -1401,31 +542,12 @@ "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==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz", + "integrity": "sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==", "dev": true, "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "argparse": "^3.0.3" } }, "loupe": { @@ -1437,42 +559,49 @@ "get-func-name": "^2.0.0" } }, + "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 + }, "minimatch": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, "requires": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.8" } }, + "minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true + }, "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" + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.3.tgz", + "integrity": "sha512-beammieDdiWdkjANWeWPpkxUROYGi5FN88fPOkLiticz7HTg3Y+Pho3EYE6SUdcuWnZ3h2WD8EXCA55JF4o6wg==", + "dev": true, + "requires": { + "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" } }, "ms": { @@ -1481,132 +610,44 @@ "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==", + "path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "requires": { - "wrappy": "1" + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" } }, - "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==", + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "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==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "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" - } + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", + "integrity": "sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==", + "dev": true }, "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 }, "supports-color": { @@ -1618,15 +659,6 @@ "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", @@ -1634,71 +666,9 @@ "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==", + "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 } } diff --git a/package.json b/package.json index e3ae874..0d0ef28 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "keychain.js", "devDependencies": { "chai": "^4.3.7", - "mocha": "^10.2.0" + "mocha": "^12.0.3" }, "scripts": { "test": "mocha" 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); From 615c516c0c44ca95bb6fa13afcb25a3b76a284f6 Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Tue, 20 Oct 2026 22:43:52 -0400 Subject: [PATCH 06/16] README: promises, errors, security notes; version 1.6.0 --- README.md | 62 +++++++++++++++++++++++++++++++++++++++++------ package-lock.json | 4 +-- package.json | 2 +- 3 files changed, 57 insertions(+), 11 deletions(-) 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/package-lock.json b/package-lock.json index 61af09f..70b18bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "keychain", - "version": "1.5.0", + "version": "1.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "keychain", - "version": "1.5.0", + "version": "1.6.0", "license": "MIT", "devDependencies": { "chai": "^4.3.7", diff --git a/package.json b/package.json index 0d0ef28..0408780 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keychain", - "version": "1.5.0", + "version": "1.6.0", "description": "Basic access to the Mac OS X Keychain", "main": "keychain.js", "devDependencies": { From 246e88d4c416206ef017400e371881b64e58de2b Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Tue, 20 Oct 2026 22:49:01 -0400 Subject: [PATCH 07/16] setPassword: retry an exit-45 add once instead of looping forever If the item already exists, delete it and add again once; if the add still returns 45, call back with ServiceFailureError (exitCode 45). The fake spawn helper can now answer per call, so the retry is tested on any OS. --- keychain.js | 58 ++++++++++++++++++++++---------------- test/helpers/fake-spawn.js | 7 +++-- test/retry.test.js | 52 ++++++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 27 deletions(-) create mode 100644 test/retry.test.js diff --git a/keychain.js b/keychain.js index 8098cb8..cc68047 100644 --- a/keychain.js +++ b/keychain.js @@ -205,38 +205,46 @@ KeychainAccess.prototype.setPassword = function(opts, fn) { return; } - var security = spawnWithSecret(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); - return; - } + security.on('error', function(err) { + err = new KeychainAccess.errors.ServiceFailureError(null, err.message); + fn(err, null); + return; + }); - self.setPassword(opts, fn); + 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; + }); + } 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); }; /** diff --git a/test/helpers/fake-spawn.js b/test/helpers/fake-spawn.js index 9376953..18f2f0a 100644 --- a/test/helpers/fake-spawn.js +++ b/test/helpers/fake-spawn.js @@ -10,7 +10,8 @@ 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). +// 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; }; @@ -30,8 +31,10 @@ exports.install = function () { end: function (data) { if (data) call.stdin += data; call.stdinEnded = true; }, on: function () { return child.stdin; } }; - var script = behavior || { code: 0 }; + 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'; diff --git a/test/retry.test.js b/test/retry.test.js new file mode 100644 index 0000000..db6c794 --- /dev/null +++ b/test/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(); + }); + }); +}); From 4e16dd93fc55d2368d631ac643889a0b5d93a0cd Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Tue, 20 Oct 2026 23:07:34 -0400 Subject: [PATCH 08/16] Package: ship only keychain.js (files whitelist); ignore coverage/ output --- .gitignore | 4 +++- package.json | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) 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/package.json b/package.json index 0408780..7a25e5c 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,9 @@ "version": "1.6.0", "description": "Basic access to the Mac OS X Keychain", "main": "keychain.js", + "files": [ + "keychain.js" + ], "devDependencies": { "chai": "^4.3.7", "mocha": "^12.0.3" From 6010a0b99ad790314553b9b7a605811ad8e6be8f Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Tue, 20 Oct 2026 23:14:17 -0400 Subject: [PATCH 09/16] Tests: error-path compat suite (callback + promise) and c8 coverage gate test/error-paths.test.js pins 1.5.0 behavior on every error path through both APIs: validation errors (incl. no opts), non-darwin platforms, security failing to start (callback called once), non-zero exits with exitCode, exit 45 whose delete fails, getPassword parsing, no-Promise runtimes, and the error classes' shape. NK_CALLBACK_ONLY=1 runs just the callback half, which passes against 1.5.0 except the unparseable-output case 1.5.0 crashed on. npm run coverage = c8 over the mocked suite; .c8rc.json fails it below 95% lines / 90% branches / 90% functions. Now 98.8 / 93.1 / 94.1 (only the Node <4.5 Buffer.from polyfill is unreached). New ubuntu CI job runs it plus npm audit. --- .c8rc.json | 9 + .github/workflows/test.yaml | 12 + package-lock.json | 968 ++++++++++++++++++++++++++++++++++++ package.json | 4 +- test/error-paths.test.js | 213 ++++++++ 5 files changed, 1205 insertions(+), 1 deletion(-) create mode 100644 .c8rc.json create mode 100644 test/error-paths.test.js diff --git a/.c8rc.json b/.c8rc.json new file mode 100644 index 0000000..39a70b5 --- /dev/null +++ b/.c8rc.json @@ -0,0 +1,9 @@ +{ + "include": ["keychain.js"], + "reporter": ["text", "lcov"], + "check-coverage": true, + "lines": 95, + "branches": 90, + "functions": 90, + "statements": 95 +} diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 7bc584c..c2cff27 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -35,3 +35,15 @@ jobs: with: node-version: ${{ matrix.node }} - run: node test/smoke/old-node.js + + # Mocked suite under c8 (no macOS needed): fails below 95% lines / 90% branches. + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm install + - run: npm run coverage + - run: npm audit --audit-level=low diff --git a/package-lock.json b/package-lock.json index 70b18bb..afacfba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,84 @@ "version": "1.6.0", "license": "MIT", "devDependencies": { + "c8": "^12.0.0", "chai": "^4.3.7", "mocha": "^12.0.3" } }, + "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": ">=18" + } + }, + "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/@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": { + "@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": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.3.tgz", @@ -65,6 +139,39 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "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": "^20.19.0 || ^22.12.0 || >=23" + }, + "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", @@ -107,6 +214,57 @@ "url": "https://paulmillr.com/funding/" } }, + "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": ">=20" + } + }, + "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, + "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/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/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": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -145,6 +303,37 @@ "node": ">=0.3.1" } }, + "node_modules/emoji-regex": { + "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.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/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, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-up-simple": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", @@ -157,6 +346,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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, + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/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, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -192,6 +418,12 @@ "node": ">=8" } }, + "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-path-inside": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", @@ -216,6 +448,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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/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": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/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==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/js-yaml": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz", @@ -238,6 +524,21 @@ "js-yaml": "bin/js-yaml.mjs" } }, + "node_modules/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, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -256,6 +557,21 @@ "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": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -316,6 +632,54 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/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, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/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, + "engines": { + "node": ">=8" + } + }, + "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": ">=8" + } + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -360,6 +724,18 @@ "url": "https://paulmillr.com/funding/" } }, + "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, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/serialize-javascript": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", @@ -369,6 +745,70 @@ "node": ">=20.0.0" } }, + "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": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "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": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "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": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-json-comments": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", @@ -396,6 +836,20 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "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": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^13.0.6", + "minimatch": "^10.2.2" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", @@ -405,14 +859,185 @@ "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": "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": "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": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "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", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "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": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "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": ">=12" + } + }, + "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, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } }, "dependencies": { + "@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 + }, + "@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 + }, + "@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 + }, + "@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 + }, + "@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, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@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 + }, + "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 + }, + "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 + }, "argparse": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.3.tgz", @@ -446,6 +1071,25 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, + "c8": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/c8/-/c8-12.0.0.tgz", + "integrity": "sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg==", + "dev": true, + "requires": { + "@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" + } + }, "chai": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz", @@ -476,6 +1120,47 @@ "readdirp": "^5.0.0" } }, + "cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "requires": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "dependencies": { + "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, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + } + } + }, + "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 + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -500,12 +1185,56 @@ "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "dev": true }, + "emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true + }, + "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" + } + }, "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 }, + "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, + "requires": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + } + }, + "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-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 + }, "get-func-name": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", @@ -529,6 +1258,12 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "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 + }, "is-path-inside": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", @@ -541,6 +1276,50 @@ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "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 + }, + "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, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.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" + } + } + } + }, + "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, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, "js-yaml": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz", @@ -550,6 +1329,15 @@ "argparse": "^3.0.3" } }, + "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" + } + }, "loupe": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.6.tgz", @@ -565,6 +1353,15 @@ "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true }, + "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, + "requires": { + "semver": "^7.5.3" + } + }, "minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -610,6 +1407,36 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "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-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 + }, "path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -638,12 +1465,58 @@ "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "dev": true }, + "semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true + }, "serialize-javascript": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", "integrity": "sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==", "dev": true }, + "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, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "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 + }, + "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 + }, + "string-width": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", + "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", + "dev": true, + "requires": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + } + }, + "strip-ansi": { + "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, + "requires": { + "ansi-regex": "^6.2.2" + } + }, "strip-json-comments": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", @@ -659,17 +1532,112 @@ "has-flag": "^4.0.0" } }, + "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, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^13.0.6", + "minimatch": "^10.2.2" + } + }, "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 }, + "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, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, "workerpool": { "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 + }, + "wrap-ansi": { + "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, + "requires": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "dependencies": { + "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, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + } + } + }, + "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": "18.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", + "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", + "dev": true, + "requires": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^8.2.1", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "dependencies": { + "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 + } + } + }, + "yargs-parser": { + "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 + }, + "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 7a25e5c..47c76a1 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,13 @@ "keychain.js" ], "devDependencies": { + "c8": "^12.0.0", "chai": "^4.3.7", "mocha": "^12.0.3" }, "scripts": { - "test": "mocha" + "test": "mocha", + "coverage": "c8 mocha --ignore test/keychain.test.js" }, "repository": { "type": "git", diff --git a/test/error-paths.test.js b/test/error-paths.test.js new file mode 100644 index 0000000..eb60a59 --- /dev/null +++ b/test/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!'); + }); + }); + }); +}); From c75e20526bb77133d980439ae9724cd97a8a29e9 Mon Sep 17 00:00:00 2001 From: "Blake (peak)" Date: Tue, 20 Oct 2026 22:35:14 -0400 Subject: [PATCH 10/16] test: mock security CLI, add c8 coverage, pin 1.5.0 callback behavior MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test/helpers/mock-security.js: fake child_process.spawn installed before keychain.js loads (it captures spawn at require time); scripts stdout/stderr/ exit code or spawn error; records exact cmd+argv - test/unit/keychain.unit.test.js: every branch of all six methods, mocked - test/unit/compat.test.js: backwards-compat pins against 1.5.0 — error classes (type/code/message/prototype chain), callback arity per outcome, sync delivery of validation errors, undefined return with callback, exact security argv per method, exit-45 retry sequence, password parsing - existing real-keychain suite moved to test/integration, skipped off darwin - c8 with thresholds (lines/stmts 95, branches 90, funcs 90); npm run coverage - CI: unit+coverage on ubuntu, integration on macos; actions v4, npm ci --- .c8rc.json | 4 +- .github/workflows/test.yaml | 31 +- .mocharc.json | 4 + package-lock.json | 740 ++---------------- package.json | 4 +- test/helpers/mock-security.js | 119 +++ .../keychain.integration.test.js} | 15 +- test/unit/compat.test.js | 351 +++++++++ test/unit/keychain.unit.test.js | 440 +++++++++++ 9 files changed, 992 insertions(+), 716 deletions(-) create mode 100644 .mocharc.json create mode 100644 test/helpers/mock-security.js rename test/{keychain.test.js => integration/keychain.integration.test.js} (97%) create mode 100644 test/unit/compat.test.js create mode 100644 test/unit/keychain.unit.test.js diff --git a/.c8rc.json b/.c8rc.json index 39a70b5..98a1ba8 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -1,9 +1,11 @@ { + "all": true, "include": ["keychain.js"], "reporter": ["text", "lcov"], + "reports-dir": "coverage", "check-coverage": true, "lines": 95, - "branches": 90, "functions": 90, + "branches": 90, "statements": 95 } diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c2cff27..5a9272c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -7,6 +7,25 @@ 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-latest + strategy: + fail-fast: false + matrix: + node: [ 20, 22, 24 ] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + 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: @@ -35,15 +54,3 @@ jobs: with: node-version: ${{ matrix.node }} - run: node test/smoke/old-node.js - - # Mocked suite under c8 (no macOS needed): fails below 95% lines / 90% branches. - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 22 - - run: npm install - - run: npm run coverage - - run: npm audit --audit-level=low diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..ce75a2d --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,4 @@ +{ + "spec": ["test/**/*.test.js"], + "timeout": 5000 +} diff --git a/package-lock.json b/package-lock.json index afacfba..495d576 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "keychain", "version": "1.6.0", - "lockfileVersion": 2, + "lockfileVersion": 3, "requires": true, "packages": { "": { @@ -173,28 +173,31 @@ } }, "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/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "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": { + "get-func-name": "^2.0.2" + }, "engines": { "node": "*" } @@ -283,9 +286,9 @@ } }, "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" @@ -477,18 +480,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/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==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -540,12 +531,12 @@ } }, "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==", + "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.0" + "get-func-name": "^2.0.1" } }, "node_modules/lru-cache": { @@ -626,6 +617,21 @@ "node": "^20.19.0 || >=22.12.0" } }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -822,18 +828,15 @@ } }, "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/test-exclude": { @@ -851,9 +854,9 @@ } }, "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" @@ -984,660 +987,5 @@ "url": "https://github.com/sponsors/sindresorhus" } } - }, - "dependencies": { - "@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 - }, - "@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 - }, - "@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 - }, - "@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 - }, - "@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, - "requires": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "@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 - }, - "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 - }, - "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 - }, - "argparse": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.3.tgz", - "integrity": "sha512-Sn98ftV3KfsO8xfLPHG6oT8y+b0yvbrLgZjgPDXsharNY15tTAjqIsIju4S32nl6/iEUNNE4v+yL2Zhm8ooP3w==", - "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": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true - }, - "brace-expansion": { - "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, - "requires": { - "balanced-match": "^4.0.2" - } - }, - "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 - }, - "c8": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/c8/-/c8-12.0.0.tgz", - "integrity": "sha512-4zpJvrd1nKWutnnKC2pXkFmb6iM1l+ffN//o1CzlTNwW7GSOs9a1xrLqkC48nU8oEkjmPZLPiwMsIaOvoF4Pqg==", - "dev": true, - "requires": { - "@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" - } - }, - "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" - } - }, - "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": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "dev": true, - "requires": { - "readdirp": "^5.0.0" - } - }, - "cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, - "requires": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "dependencies": { - "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, - "requires": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - } - } - } - }, - "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 - }, - "cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "requires": { - "ms": "^2.1.3" - } - }, - "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": "9.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", - "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", - "dev": true - }, - "emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true - }, - "escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true - }, - "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" - } - }, - "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 - }, - "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, - "requires": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - } - }, - "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-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 - }, - "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 - }, - "glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "requires": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - } - }, - "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 - }, - "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 - }, - "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 - }, - "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 - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "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 - }, - "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, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.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" - } - } - } - }, - "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, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - } - }, - "js-yaml": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-5.5.0.tgz", - "integrity": "sha512-paNr4LFIFPjeeSmwNYdrb8FafLLG/Cr2Af3GjbsHJ5CxIjH4r0c2emlmRdAtpPAieP1gRVzk6buBbVP5gBMGAg==", - "dev": true, - "requires": { - "argparse": "^3.0.3" - } - }, - "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" - } - }, - "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" - } - }, - "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 - }, - "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, - "requires": { - "semver": "^7.5.3" - } - }, - "minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "requires": { - "brace-expansion": "^5.0.8" - } - }, - "minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true - }, - "mocha": { - "version": "12.0.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-12.0.3.tgz", - "integrity": "sha512-beammieDdiWdkjANWeWPpkxUROYGi5FN88fPOkLiticz7HTg3Y+Pho3EYE6SUdcuWnZ3h2WD8EXCA55JF4o6wg==", - "dev": true, - "requires": { - "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" - } - }, - "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 - }, - "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-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 - }, - "path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "requires": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - } - }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true - }, - "picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "readdirp": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", - "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", - "dev": true - }, - "semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true - }, - "serialize-javascript": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", - "integrity": "sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==", - "dev": true - }, - "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, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "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 - }, - "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 - }, - "string-width": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.3.0.tgz", - "integrity": "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ==", - "dev": true, - "requires": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - } - }, - "strip-ansi": { - "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, - "requires": { - "ansi-regex": "^6.2.2" - } - }, - "strip-json-comments": { - "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 - }, - "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" - } - }, - "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, - "requires": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^13.0.6", - "minimatch": "^10.2.2" - } - }, - "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 - }, - "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, - "requires": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - } - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "workerpool": { - "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 - }, - "wrap-ansi": { - "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, - "requires": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "dependencies": { - "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, - "requires": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - } - } - } - }, - "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": "18.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.2.0.tgz", - "integrity": "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==", - "dev": true, - "requires": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^8.2.1", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "dependencies": { - "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 - } - } - }, - "yargs-parser": { - "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 - }, - "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 47c76a1..c532518 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ }, "scripts": { "test": "mocha", - "coverage": "c8 mocha --ignore test/keychain.test.js" + "test:unit": "mocha test/unit", + "test:integration": "mocha test/integration", + "coverage": "c8 mocha" }, "repository": { "type": "git", diff --git a/test/helpers/mock-security.js b/test/helpers/mock-security.js new file mode 100644 index 0000000..e83868c --- /dev/null +++ b/test/helpers/mock-security.js @@ -0,0 +1,119 @@ +/*! + * 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 () {}; + calls.push({ cmd: cmd, args: (args || []).slice() }); + + 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]; + }, + + /** 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/unit/compat.test.js b/test/unit/compat.test.js new file mode 100644 index 0000000..9d307e6 --- /dev/null +++ b/test/unit/compat.test.js @@ -0,0 +1,351 @@ +/*! + * 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); }); + }); + }); + + describe('arguments passed to `security` (exact argv)', 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']] + ]; + + 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.deep.equal([{ cmd: '/usr/bin/security', args: argv }]); + 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'); + expect(mock.calls[0].args).to.include('s;rm -rf /'); + expect(mock.calls[0].args).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 c.args; })).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/keychain.unit.test.js b/test/unit/keychain.unit.test.js new file mode 100644 index 0000000..47571bd --- /dev/null +++ b/test/unit/keychain.unit.test.js @@ -0,0 +1,440 @@ +/*! + * 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.deep.equal([{ + cmd: '/usr/bin/security', + args: ['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('spawns security add-generic-password with -a/-s/-w 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.deep.equal([{ + cmd: '/usr/bin/security', + args: ['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.calls[0].args[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 c.args[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.deep.equal([{ + cmd: '/usr/bin/security', + args: ['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('spawns security create-keychain -p 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.deep.equal([{ + cmd: '/usr/bin/security', + args: ['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.deep.equal([{ + cmd: '/usr/bin/security', + args: ['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.deep.equal([{ + cmd: '/usr/bin/security', + args: ['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 argv entries, 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(['add-generic-password', '-a', evil, '-s', evil, '-w', evil]); + done(); + }); + }); + }); +}); From 1e68ac210ce265999961e9079842515ac0e0d06b Mon Sep 17 00:00:00 2001 From: "Blake (Fortysix)" Date: Tue, 20 Oct 2026 22:44:02 -0400 Subject: [PATCH 11/16] test: adapt mock and pinned-command tests to security -i transport and -- guards; run full suite on macOS CI --- test/helpers/mock-security.js | 20 +++++++++- test/unit/compat.test.js | 34 +++++++++++++---- test/unit/keychain.unit.test.js | 68 +++++++++++++++++---------------- 3 files changed, 81 insertions(+), 41 deletions(-) diff --git a/test/helpers/mock-security.js b/test/helpers/mock-security.js index e83868c..9473da3 100644 --- a/test/helpers/mock-security.js +++ b/test/helpers/mock-security.js @@ -40,7 +40,16 @@ function fakeSpawn(cmd, args) { child.stderr = new EventEmitter(); child.pid = 4242; child.kill = function () {}; - calls.push({ cmd: cmd, args: (args || []).slice() }); + 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 }; @@ -78,6 +87,15 @@ module.exports = { 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); diff --git a/test/unit/compat.test.js b/test/unit/compat.test.js index 9d307e6..335d55a 100644 --- a/test/unit/compat.test.js +++ b/test/unit/compat.test.js @@ -262,7 +262,13 @@ describe('Backwards compatibility (pinned at 1.5.0)', function () { }); }); - describe('arguments passed to `security` (exact argv)', function () { + // 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']], @@ -270,17 +276,27 @@ describe('Backwards compatibility (pinned at 1.5.0)', function () { ['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']] + ['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.deep.equal([{ cmd: '/usr/bin/security', args: argv }]); + 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(); }); }); @@ -291,8 +307,10 @@ describe('Backwards compatibility (pinned at 1.5.0)', function () { 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'); - expect(mock.calls[0].args).to.include('s;rm -rf /'); - expect(mock.calls[0].args).to.include('$(id)'); + 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(); }); }); @@ -304,7 +322,7 @@ describe('Backwards compatibility (pinned at 1.5.0)', function () { 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 c.args; })).to.deep.equal([ + 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'] diff --git a/test/unit/keychain.unit.test.js b/test/unit/keychain.unit.test.js index 47571bd..2775f5c 100644 --- a/test/unit/keychain.unit.test.js +++ b/test/unit/keychain.unit.test.js @@ -63,10 +63,9 @@ describe('KeychainAccess (unit, mocked security)', function () { keychain.getPassword({ account: 'me', service: 'svc' }, function (err, pass) { expect(err).to.equal(null); expect(pass).to.equal('hunter2'); - expect(mock.calls).to.deep.equal([{ - cmd: '/usr/bin/security', - args: ['find-generic-password', '-a', 'me', '-s', 'svc', '-g'] - }]); + 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(); }); }); @@ -178,15 +177,18 @@ describe('KeychainAccess (unit, mocked security)', function () { }); describe('.setPassword(opts, fn)', function () { - it('spawns security add-generic-password with -a/-s/-w and returns the password', function (done) { + 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.deep.equal([{ - cmd: '/usr/bin/security', - args: ['add-generic-password', '-a', 'me', '-s', 'svc', '-w', '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(); }); }); @@ -195,7 +197,7 @@ describe('KeychainAccess (unit, mocked security)', function () { mock.respond({ code: 0 }); keychain.setPassword({ account: 'me', service: 'svc', password: 'pw', type: 'internet' }, function (err) { expect(err).to.equal(null); - expect(mock.calls[0].args[0]).to.equal('add-internet-password'); + expect(mock.commands(mock.calls[0])[0][0]).to.equal('add-internet-password'); done(); }); }); @@ -207,7 +209,7 @@ describe('KeychainAccess (unit, mocked security)', function () { 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 c.args[0]; })).to.deep.equal([ + 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(); @@ -260,10 +262,9 @@ describe('KeychainAccess (unit, mocked security)', function () { mock.respond({ code: 0 }); keychain.deletePassword({ account: 'me', service: 'svc' }, function (err) { expect(err).to.equal(null); - expect(mock.calls).to.deep.equal([{ - cmd: '/usr/bin/security', - args: ['delete-generic-password', '-a', 'me', '-s', 'svc'] - }]); + 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(); }); }); @@ -303,15 +304,17 @@ describe('KeychainAccess (unit, mocked security)', function () { }); describe('.createKeychain(opts, fn)', function () { - it('spawns security create-keychain -p and returns the name', function (done) { + 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.deep.equal([{ - cmd: '/usr/bin/security', - args: ['create-keychain', '-p', 'pw', '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(); }); }); @@ -346,15 +349,14 @@ describe('KeychainAccess (unit, mocked security)', function () { }); describe('.deleteKeychain(opts, fn)', function () { - it('spawns security delete-keychain and returns the name', function (done) { + 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.deep.equal([{ - cmd: '/usr/bin/security', - args: ['delete-keychain', '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(); }); }); @@ -387,15 +389,14 @@ describe('KeychainAccess (unit, mocked security)', function () { }); describe('.setDefaultKeychain(opts, fn)', function () { - it('spawns security default-keychain -s and returns the name', function (done) { + 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.deep.equal([{ - cmd: '/usr/bin/security', - args: ['default-keychain', '-s', '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(); }); }); @@ -428,11 +429,14 @@ describe('KeychainAccess (unit, mocked security)', function () { }); describe('argument safety', function () { - it('passes hostile account/service/password values as discrete argv entries, never through a shell', function (done) { + 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(['add-generic-password', '-a', evil, '-s', evil, '-w', evil]); + 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(); }); }); From 7b89239ce2df09bf3c8d4960223c8d29400acd47 Mon Sep 17 00:00:00 2001 From: "Blake (peak)" Date: Tue, 20 Oct 2026 22:55:27 -0400 Subject: [PATCH 12/16] test: pin the argv fallback when a secret cannot travel over security -i (newline, NUL, >4094 bytes, boundary) --- test/unit/compat.test.js | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/unit/compat.test.js b/test/unit/compat.test.js index 335d55a..e8c5f74 100644 --- a/test/unit/compat.test.js +++ b/test/unit/compat.test.js @@ -302,6 +302,76 @@ describe('Backwards compatibility (pinned at 1.5.0)', function () { }); }); + // `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 () { From ea9e18f2cad152d2f4dddd0c62e4bf046ac74465 Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Tue, 20 Oct 2026 23:25:58 -0400 Subject: [PATCH 13/16] Tests: put mocked suites in test/unit and real-keychain suites in test/integration; scope test:unit/test:integration mocha 12 appends CLI paths to the spec in .mocharc.json instead of replacing it, so test:unit and test:integration both ran all 279 tests. The spec now lives in the npm scripts and .mocharc.json only sets the timeout. --- .mocharc.json | 1 - package.json | 8 ++++---- test/{ => integration}/macos-security.test.js | 2 +- test/{ => unit}/error-paths.test.js | 4 ++-- test/{ => unit}/promise.test.js | 4 ++-- test/{ => unit}/retry.test.js | 4 ++-- test/{ => unit}/robustness.test.js | 4 ++-- test/{ => unit}/security.test.js | 4 ++-- 8 files changed, 15 insertions(+), 16 deletions(-) rename test/{ => integration}/macos-security.test.js (98%) rename test/{ => unit}/error-paths.test.js (99%) rename test/{ => unit}/promise.test.js (98%) rename test/{ => unit}/retry.test.js (95%) rename test/{ => unit}/robustness.test.js (95%) rename test/{ => unit}/security.test.js (97%) diff --git a/.mocharc.json b/.mocharc.json index ce75a2d..0c3ee50 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -1,4 +1,3 @@ { - "spec": ["test/**/*.test.js"], "timeout": 5000 } diff --git a/package.json b/package.json index c532518..99931bb 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,10 @@ "mocha": "^12.0.3" }, "scripts": { - "test": "mocha", - "test:unit": "mocha test/unit", - "test:integration": "mocha test/integration", - "coverage": "c8 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/macos-security.test.js b/test/integration/macos-security.test.js similarity index 98% rename from test/macos-security.test.js rename to test/integration/macos-security.test.js index 80f3d32..a894f45 100644 --- a/test/macos-security.test.js +++ b/test/integration/macos-security.test.js @@ -1,7 +1,7 @@ // 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 keychain = require('../../'); var chai = require('chai'); chai.should(); diff --git a/test/error-paths.test.js b/test/unit/error-paths.test.js similarity index 99% rename from test/error-paths.test.js rename to test/unit/error-paths.test.js index eb60a59..4f54530 100644 --- a/test/error-paths.test.js +++ b/test/unit/error-paths.test.js @@ -1,8 +1,8 @@ // 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'); +var fake = require('../helpers/fake-spawn'); fake.install(); -var keychain = require('../'); +var keychain = require('../../'); fake.restore(); var errors = keychain.constructor.errors; var chai = require('chai'); diff --git a/test/promise.test.js b/test/unit/promise.test.js similarity index 98% rename from test/promise.test.js rename to test/unit/promise.test.js index b2fb443..eac626b 100644 --- a/test/promise.test.js +++ b/test/unit/promise.test.js @@ -1,8 +1,8 @@ // 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'); +var fake = require('../helpers/fake-spawn'); fake.install(); -var keychain = require('../'); +var keychain = require('../../'); fake.restore(); var chai = require('chai'); chai.should(); diff --git a/test/retry.test.js b/test/unit/retry.test.js similarity index 95% rename from test/retry.test.js rename to test/unit/retry.test.js index db6c794..84a92ca 100644 --- a/test/retry.test.js +++ b/test/unit/retry.test.js @@ -1,8 +1,8 @@ // 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'); +var fake = require('../helpers/fake-spawn'); fake.install(); -var keychain = require('../'); +var keychain = require('../../'); fake.restore(); var chai = require('chai'); chai.should(); diff --git a/test/robustness.test.js b/test/unit/robustness.test.js similarity index 95% rename from test/robustness.test.js rename to test/unit/robustness.test.js index 0b7095b..c34c2e9 100644 --- a/test/robustness.test.js +++ b/test/unit/robustness.test.js @@ -1,7 +1,7 @@ // Regression tests for bugs in 1.5.0. Mocked, so they run on any OS. -var fake = require('./helpers/fake-spawn'); +var fake = require('../helpers/fake-spawn'); fake.install(); -var keychain = require('../'); +var keychain = require('../../'); fake.restore(); var chai = require('chai'); chai.should(); diff --git a/test/security.test.js b/test/unit/security.test.js similarity index 97% rename from test/security.test.js rename to test/unit/security.test.js index 13f8f24..9a9e5d0 100644 --- a/test/security.test.js +++ b/test/unit/security.test.js @@ -1,8 +1,8 @@ // 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'); +var fake = require('../helpers/fake-spawn'); fake.install(); -var keychain = require('../'); +var keychain = require('../../'); fake.restore(); var chai = require('chai'); chai.should(); From abf0aadb0c6b92b1aca0e49ac7b5d5f0460c6cd2 Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Wed, 21 Oct 2026 13:00:28 -0400 Subject: [PATCH 14/16] ci: bump actions/checkout and actions/setup-node to v7 (Node 24); pin ubuntu jobs to ubuntu-24.04 Clears the 'Node.js 20 is deprecated' annotation on every job and the ubuntu-latest -> Ubuntu 26 migration notice (actions/runner-images#14748). macOS jobs stay on macos-latest. --- .github/workflows/test.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5a9272c..26b43f3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,14 +10,14 @@ jobs: # Mocked unit + compat suite: runs everywhere, enforces c8 coverage thresholds # (95% lines / 90% branches / 90% functions) and npm audit. unit: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: node: [ 20, 22, 24 ] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version: ${{ matrix.node }} - run: npm install @@ -33,8 +33,8 @@ jobs: matrix: node: [ 20, 22, 24 ] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version: ${{ matrix.node }} - run: npm install @@ -43,14 +43,14 @@ jobs: # 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-latest + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: node: [ 10, 12, 14, 16, 18 ] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 with: node-version: ${{ matrix.node }} - run: node test/smoke/old-node.js From 7f402a6a886d241f48732e0b8d3937952d180e73 Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Sun, 25 Oct 2026 09:05:26 -0400 Subject: [PATCH 15/16] deps(dev): bump serialize-javascript 7.1.2 -> 7.1.4 in lockfile GHSA-m5mm-p8h7-m29v (low, published Oct 22) affects serialize-javascript <7.1.3, pulled in by mocha. It made `npm audit --audit-level=low` (CI unit job, Node 22) fail. Lockfile only; mocha's range ^7.1.1 already allows 7.1.4. Dev dependency, not in the published tarball. --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 495d576..724c167 100644 --- a/package-lock.json +++ b/package-lock.json @@ -743,9 +743,9 @@ } }, "node_modules/serialize-javascript": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.2.tgz", - "integrity": "sha512-GL2BWwVa6JydKO6l/ljVgjAZF4QJ3S7dWDWi53s5GZT8PJzD2fNxi67HANQGKAqPrwEpL5ba7gUBGi0Ls/sEoQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.4.tgz", + "integrity": "sha512-FqlEuoTq/zRmA4Lg6aOqyH3lCHbUtCB0bmHCeFniX2GBdDqMJM3FwdvEhlE5Pim1dlt2FfBdVPmUNNiGhhKqpw==", "dev": true, "engines": { "node": ">=20.0.0" From 2b9c466bea8c3dec718923e4864eea3842c05860 Mon Sep 17 00:00:00 2001 From: adk-colvin Date: Sat, 31 Oct 2026 05:11:24 -0400 Subject: [PATCH 16/16] deps(dev): override glob to ^14.0.3 and minimatch to ^11.0.3 GHSA-3g3m-2fjr-6pw4 (glob <14.0.2) and GHSA-g3hv-75wg-m249 (minimatch <11.0.1), both moderate and published Oct 30, made `npm audit --audit-level=low` (CI unit job, Node 22) fail. mocha 12.0.3 and test-exclude 8.0.0 (via c8) pin glob ^13 / minimatch ^10, so there is no lockfile-only fix; npm overrides move them to the patched majors. Both still ship CommonJS and need Node 18+ (CI installs on 20/22/24 only; the old-node smoke job never runs npm install). Dev dependencies only, not in the published tarball. --- package-lock.json | 31 +++++++++++++++++++------------ package.json | 4 ++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 724c167..e813caa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,12 @@ "node": ">=18" } }, + "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", @@ -396,14 +402,14 @@ } }, "node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-14.0.3.tgz", + "integrity": "sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==", "dev": true, "dependencies": { - "minimatch": "^10.2.2", + "minimatch": "^11.0.2", "minipass": "^7.1.3", - "path-scurry": "^2.0.2" + "path-scurry": "^3.0.0" }, "engines": { "node": "18 || 20 || >=22" @@ -564,12 +570,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.3.tgz", + "integrity": "sha512-iny/40PEGkNJQ5/Pv6z3nm6FbRAby32GeZRjCUJk2WnNDbgPSTcdI+DDvLqsgBdD4cD5sMtMheqPPnehCCa8Nw==", "dev": true, "dependencies": { - "brace-expansion": "^5.0.8" + "@isaacs/nonrecursive-glob-match": "^1.0.3", + "brace-expansion": "^5.0.12" }, "engines": { "node": "18 || 20 || >=22" @@ -687,9 +694,9 @@ } }, "node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "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", diff --git a/package.json b/package.json index 99931bb..f976883 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "chai": "^4.3.7", "mocha": "^12.0.3" }, + "overrides": { + "glob": "^14.0.3", + "minimatch": "^11.0.3" + }, "scripts": { "test": "mocha \"test/{unit,integration}/**/*.test.js\"", "test:unit": "mocha \"test/unit/**/*.test.js\"",