diff --git a/keeper/__tests__/data/keys.json b/keeper/__tests__/data/keys.json new file mode 100644 index 00000000..f7a905be --- /dev/null +++ b/keeper/__tests__/data/keys.json @@ -0,0 +1,16 @@ +{ + "testkey": { + "keyId": "testkey", + "publicPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAtYxJV8CpS8HLjo3vFwFrc5hXTHWtKTKESzDaSI6Nt5U=\n-----END PUBLIC KEY-----\n", + "algorithm": "ed25519", + "active": true, + "createdAt": "2026-06-01T13:25:56.063Z" + }, + "k1": { + "keyId": "k1", + "publicPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEACu2Y/F98xCZDTyFwaFUX/ERg2u87ELNkZqVtcX7XgLE=\n-----END PUBLIC KEY-----\n", + "algorithm": "ed25519", + "active": true, + "createdAt": "2026-06-01T13:25:56.069Z" + } +} \ No newline at end of file diff --git a/keeper/__tests__/security.test.js b/keeper/__tests__/security.test.js new file mode 100644 index 00000000..c6bdf8a8 --- /dev/null +++ b/keeper/__tests__/security.test.js @@ -0,0 +1,36 @@ +const { buildSecurityStack } = require('../src/keeperSecurity'); + +describe('Keeper security stack', () => { + let stack; + + beforeEach(() => { + stack = buildSecurityStack({ storageDir: __dirname + '/data', auditFile: __dirname + '/data/audit.log' }); + }); + + test('creates keys in HSM and lists them', async () => { + const k = await stack.keyManager.createKey({ keyId: 'testkey' }); + expect(k.keyId).toBe('testkey'); + const list = await stack.keyManager.listKeys(); + expect(list.find(l => l.keyId === 'testkey')).toBeTruthy(); + }); + + test('permissions grant allows signing and revocation blocks it', async () => { + await stack.keyManager.createKey({ keyId: 'k1' }); + const grant = stack.permissions.createGrant({ subject: 'alice', resource: 'k1', actions: ['sign'], scope: 'tx' }); + const { signature } = await stack.signing.sign({ requester: 'alice', keyId: 'k1', payload: 'hello', purpose: 'tx' }); + expect(signature).toBeTruthy(); + + // revoke and assert unauthorized + stack.permissions.revokeGrant(grant.grantId); + await expect(stack.signing.sign({ requester: 'alice', keyId: 'k1', payload: 'hello', purpose: 'tx' })).rejects.toThrow(/Unauthorized/); + }); + + test('audit log records events', () => { + const tail = stack.audit.tail(10); + expect(Array.isArray(tail)).toBeTruthy(); + // create a new grant and ensure audit recorded + const g = stack.permissions.createGrant({ subject: 'bob', resource: 'k1' }); + const tail2 = stack.audit.tail(5); + expect(tail2.some(e => e.eventType === 'permission.grant.create' && e.details.grantId === g.grantId)).toBeTruthy(); + }); +}); diff --git a/keeper/coverage/coverage-summary.json b/keeper/coverage/coverage-summary.json index 3d6e50ae..754a46d9 100644 --- a/keeper/coverage/coverage-summary.json +++ b/keeper/coverage/coverage-summary.json @@ -1,8 +1,8 @@ -{"total": {"lines":{"total":706,"covered":22,"skipped":0,"pct":3.11},"statements":{"total":725,"covered":22,"skipped":0,"pct":3.03},"functions":{"total":112,"covered":9,"skipped":0,"pct":8.03},"branches":{"total":530,"covered":7,"skipped":0,"pct":1.32},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"}} -,"/Users/aliphatic/Desktop/SoroTask/keeper/src/concurrency.js": {"lines":{"total":50,"covered":0,"skipped":0,"pct":0},"functions":{"total":8,"covered":0,"skipped":0,"pct":0},"statements":{"total":51,"covered":0,"skipped":0,"pct":0},"branches":{"total":26,"covered":0,"skipped":0,"pct":0}} -,"/Users/aliphatic/Desktop/SoroTask/keeper/src/logger.js": {"lines":{"total":44,"covered":22,"skipped":0,"pct":50},"functions":{"total":27,"covered":9,"skipped":0,"pct":33.33},"statements":{"total":44,"covered":22,"skipped":0,"pct":50},"branches":{"total":26,"covered":7,"skipped":0,"pct":26.92}} -,"/Users/aliphatic/Desktop/SoroTask/keeper/src/poller.js": {"lines":{"total":258,"covered":0,"skipped":0,"pct":0},"functions":{"total":22,"covered":0,"skipped":0,"pct":0},"statements":{"total":260,"covered":0,"skipped":0,"pct":0},"branches":{"total":195,"covered":0,"skipped":0,"pct":0}} -,"/Users/aliphatic/Desktop/SoroTask/keeper/src/queue.js": {"lines":{"total":147,"covered":0,"skipped":0,"pct":0},"functions":{"total":19,"covered":0,"skipped":0,"pct":0},"statements":{"total":150,"covered":0,"skipped":0,"pct":0},"branches":{"total":108,"covered":0,"skipped":0,"pct":0}} -,"/Users/aliphatic/Desktop/SoroTask/keeper/src/registry.js": {"lines":{"total":136,"covered":0,"skipped":0,"pct":0},"functions":{"total":23,"covered":0,"skipped":0,"pct":0},"statements":{"total":145,"covered":0,"skipped":0,"pct":0},"branches":{"total":97,"covered":0,"skipped":0,"pct":0}} -,"/Users/aliphatic/Desktop/SoroTask/keeper/src/retry.js": {"lines":{"total":71,"covered":0,"skipped":0,"pct":0},"functions":{"total":13,"covered":0,"skipped":0,"pct":0},"statements":{"total":75,"covered":0,"skipped":0,"pct":0},"branches":{"total":78,"covered":0,"skipped":0,"pct":0}} +{"total": {"lines":{"total":706,"covered":274,"skipped":0,"pct":38.81},"statements":{"total":725,"covered":279,"skipped":0,"pct":38.48},"functions":{"total":112,"covered":49,"skipped":0,"pct":43.75},"branches":{"total":530,"covered":183,"skipped":0,"pct":34.52},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"}} +,"/home/ezekiel001/SoroTask/keeper/src/concurrency.js": {"lines":{"total":50,"covered":47,"skipped":0,"pct":94},"functions":{"total":8,"covered":6,"skipped":0,"pct":75},"statements":{"total":51,"covered":47,"skipped":0,"pct":92.15},"branches":{"total":26,"covered":23,"skipped":0,"pct":88.46}} +,"/home/ezekiel001/SoroTask/keeper/src/logger.js": {"lines":{"total":44,"covered":33,"skipped":0,"pct":75},"functions":{"total":27,"covered":18,"skipped":0,"pct":66.66},"statements":{"total":44,"covered":33,"skipped":0,"pct":75},"branches":{"total":26,"covered":15,"skipped":0,"pct":57.69}} +,"/home/ezekiel001/SoroTask/keeper/src/poller.js": {"lines":{"total":258,"covered":0,"skipped":0,"pct":0},"functions":{"total":22,"covered":0,"skipped":0,"pct":0},"statements":{"total":260,"covered":0,"skipped":0,"pct":0},"branches":{"total":195,"covered":0,"skipped":0,"pct":0}} +,"/home/ezekiel001/SoroTask/keeper/src/queue.js": {"lines":{"total":147,"covered":124,"skipped":0,"pct":84.35},"functions":{"total":19,"covered":12,"skipped":0,"pct":63.15},"statements":{"total":150,"covered":125,"skipped":0,"pct":83.33},"branches":{"total":108,"covered":74,"skipped":0,"pct":68.51}} +,"/home/ezekiel001/SoroTask/keeper/src/registry.js": {"lines":{"total":136,"covered":0,"skipped":0,"pct":0},"functions":{"total":23,"covered":0,"skipped":0,"pct":0},"statements":{"total":145,"covered":0,"skipped":0,"pct":0},"branches":{"total":97,"covered":0,"skipped":0,"pct":0}} +,"/home/ezekiel001/SoroTask/keeper/src/retry.js": {"lines":{"total":71,"covered":70,"skipped":0,"pct":98.59},"functions":{"total":13,"covered":13,"skipped":0,"pct":100},"statements":{"total":75,"covered":74,"skipped":0,"pct":98.66},"branches":{"total":78,"covered":71,"skipped":0,"pct":91.02}} } diff --git a/keeper/coverage/lcov-report/concurrency.js.html b/keeper/coverage/lcov-report/concurrency.js.html index 430402e4..35536fad 100644 --- a/keeper/coverage/lcov-report/concurrency.js.html +++ b/keeper/coverage/lcov-report/concurrency.js.html @@ -23,30 +23,30 @@

All files concurrency.js

- 0% + 92.15% Statements - 0/51 + 47/51
- 0% + 88.46% Branches - 0/26 + 23/26
- 0% + 75% Functions - 0/8 + 6/8
- 0% + 94% Lines - 0/50 + 47/50
@@ -61,7 +61,7 @@

All files concurrency.js

-
+
1 2 @@ -192,37 +192,37 @@

All files concurrency.js

      -  -  -  -  -  +58x +58x +58x +58x +58x   -  -  +58x +58x   -  -  -  +58x +140x +47x       -  -  +93x +10x       -  -  +83x +77x   -  -  +77x +12x     -  -  -  -  +77x +25x +3x +3x       @@ -232,61 +232,61 @@

All files concurrency.js

      -  -  +25x +3x       -  -  -  -  +25x +25x +25x +25x       -  -  -  +58x +3x +3x         -  -  +58x +58x   -  -  +58x +52x     -  +58x       -  -  +54x +54x       -  -  -  -  +58x +61x +61x +61x     -  -  -  -  +58x +3x +2x +2x       -  +58x           -  +58x       @@ -296,7 +296,7 @@

All files concurrency.js

      -  +2x  
/**
  * Creates a rate limiter that controls both concurrency (active tasks)
  * and throughput (requests per second).
@@ -308,38 +308,38 @@ 

All files concurrency.js

* @param {string} options.name - Name for logging/metrics identification * @returns {Function} Limiter function that takes a task function */ -function createRateLimiter(options = {}) { - const { concurrency = Infinity, rps = Infinity, logger, name = 'default' } = options; - let activeCount = 0; - const queue = []; - const requestTimestamps = []; - let isThrottled = false; +function createRateLimiter(options = {}) { + const { concurrency = Infinity, rps = Infinity, logger, name = 'default' } = options; + let activeCount = 0; + const queue = []; + const requestTimestamps = []; + let isThrottled = false;   - const clearedError = new Error('Queue cleared'); - clearedError.name = 'QueueClearedError'; + const clearedError = new Error('Queue cleared'); + clearedError.name = 'QueueClearedError';   - const next = () => { - if (queue.length === 0) { - return; + const next = () => { + if (queue.length === 0) { + return; }   // Check concurrency limit - if (activeCount >= concurrency) { - return; + if (activeCount >= concurrency) { + return; }   // Check RPS limit - if (rps !== Infinity) { - const now = Date.now(); + if (rps !== Infinity) { + const now = Date.now(); // Remove timestamps older than 1 second - while (requestTimestamps.length > 0 && requestTimestamps[0] <= now - 1000) { - requestTimestamps.shift(); + while (requestTimestamps.length > 0 && requestTimestamps[0] <= now - 1000) { + requestTimestamps.shift(); }   - if (requestTimestamps.length >= rps) { - if (!isThrottled) { - isThrottled = true; - if (logger) { + if (requestTimestamps.length >= rps) { + if (!isThrottled) { + isThrottled = true; + Iif (logger) { logger.warn('Backpressure active: RPS limit reached', { name, rps, @@ -349,61 +349,61 @@

All files concurrency.js

}   // Call onThrottle callback if provided - if (options.onThrottle) { - options.onThrottle({ name, rps, queueDepth: queue.length }); + if (options.onThrottle) { + options.onThrottle({ name, rps, queueDepth: queue.length }); }   // Schedule next attempt based on when the oldest timestamp will expire - const oldestTimestamp = requestTimestamps[0]; - const delay = Math.max(0, 1000 - (now - oldestTimestamp) + 1); // +1ms buffer - setTimeout(next, delay); - return; + const oldestTimestamp = requestTimestamps[0]; + const delay = Math.max(0, 1000 - (now - oldestTimestamp) + 1); // +1ms buffer + setTimeout(next, delay); + return; } }   - if (isThrottled) { - isThrottled = false; - if (logger) { + if (isThrottled) { + isThrottled = false; + Iif (logger) { logger.info('Backpressure released', { name }); } }   - const task = queue.shift(); - activeCount++; + const task = queue.shift(); + activeCount++;   - if (rps !== Infinity) { - requestTimestamps.push(Date.now()); + if (rps !== Infinity) { + requestTimestamps.push(Date.now()); }   - Promise.resolve() + Promise.resolve() .then(task.fn) .then(task.resolve, task.reject) - .finally(() => { - activeCount--; - next(); + .finally(() => { + activeCount--; + next(); }); };   - const limit = (fn) => - new Promise((resolve, reject) => { - queue.push({ fn, resolve, reject }); - next(); + const limit = (fn) => + new Promise((resolve, reject) => { + queue.push({ fn, resolve, reject }); + next(); });   - limit.clearQueue = () => { - while (queue.length > 0) { - const task = queue.shift(); - task.reject(clearedError); + limit.clearQueue = () => { + while (queue.length > 0) { + const task = queue.shift(); + task.reject(clearedError); } };   - limit.getStats = () => ({ + limit.getStats = () => ({ activeCount, queueDepth: queue.length, isThrottled, });   - return limit; + return limit; }   /** @@ -413,7 +413,7 @@

All files concurrency.js

return createRateLimiter({ concurrency }); }   -module.exports = { createRateLimiter, createConcurrencyLimit }; +module.exports = { createRateLimiter, createConcurrencyLimit };  
@@ -421,7 +421,7 @@

All files concurrency.js