tlc-claude-code 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/components/AuditPane.d.ts +30 -0
- package/dashboard/dist/components/AuditPane.js +127 -0
- package/dashboard/dist/components/AuditPane.test.d.ts +1 -0
- package/dashboard/dist/components/AuditPane.test.js +339 -0
- package/dashboard/dist/components/CompliancePane.d.ts +39 -0
- package/dashboard/dist/components/CompliancePane.js +96 -0
- package/dashboard/dist/components/CompliancePane.test.d.ts +1 -0
- package/dashboard/dist/components/CompliancePane.test.js +183 -0
- package/dashboard/dist/components/SSOPane.d.ts +36 -0
- package/dashboard/dist/components/SSOPane.js +71 -0
- package/dashboard/dist/components/SSOPane.test.d.ts +1 -0
- package/dashboard/dist/components/SSOPane.test.js +155 -0
- package/dashboard/dist/components/WorkspaceDocsPane.js +0 -16
- package/dashboard/dist/components/WorkspacePane.d.ts +1 -1
- package/dashboard/dist/components/ZeroRetentionPane.d.ts +44 -0
- package/dashboard/dist/components/ZeroRetentionPane.js +83 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.d.ts +1 -0
- package/dashboard/dist/components/ZeroRetentionPane.test.js +160 -0
- package/package.json +1 -1
- package/server/lib/access-control-doc.js +541 -0
- package/server/lib/access-control-doc.test.js +672 -0
- package/server/lib/adr-generator.js +423 -0
- package/server/lib/adr-generator.test.js +586 -0
- package/server/lib/agent-progress-monitor.js +223 -0
- package/server/lib/agent-progress-monitor.test.js +202 -0
- package/server/lib/audit-attribution.js +191 -0
- package/server/lib/audit-attribution.test.js +359 -0
- package/server/lib/audit-classifier.js +202 -0
- package/server/lib/audit-classifier.test.js +209 -0
- package/server/lib/audit-command.js +275 -0
- package/server/lib/audit-command.test.js +325 -0
- package/server/lib/audit-exporter.js +380 -0
- package/server/lib/audit-exporter.test.js +464 -0
- package/server/lib/audit-logger.js +236 -0
- package/server/lib/audit-logger.test.js +364 -0
- package/server/lib/audit-query.js +257 -0
- package/server/lib/audit-query.test.js +352 -0
- package/server/lib/audit-storage.js +269 -0
- package/server/lib/audit-storage.test.js +272 -0
- package/server/lib/bulk-repo-init.js +342 -0
- package/server/lib/bulk-repo-init.test.js +388 -0
- package/server/lib/compliance-checklist.js +866 -0
- package/server/lib/compliance-checklist.test.js +476 -0
- package/server/lib/compliance-command.js +616 -0
- package/server/lib/compliance-command.test.js +551 -0
- package/server/lib/compliance-reporter.js +692 -0
- package/server/lib/compliance-reporter.test.js +707 -0
- package/server/lib/data-flow-doc.js +665 -0
- package/server/lib/data-flow-doc.test.js +659 -0
- package/server/lib/ephemeral-storage.js +249 -0
- package/server/lib/ephemeral-storage.test.js +254 -0
- package/server/lib/evidence-collector.js +627 -0
- package/server/lib/evidence-collector.test.js +901 -0
- package/server/lib/flow-diagram-generator.js +474 -0
- package/server/lib/flow-diagram-generator.test.js +446 -0
- package/server/lib/idp-manager.js +626 -0
- package/server/lib/idp-manager.test.js +587 -0
- package/server/lib/memory-exclusion.js +326 -0
- package/server/lib/memory-exclusion.test.js +241 -0
- package/server/lib/mfa-handler.js +452 -0
- package/server/lib/mfa-handler.test.js +490 -0
- package/server/lib/oauth-flow.js +375 -0
- package/server/lib/oauth-flow.test.js +487 -0
- package/server/lib/oauth-registry.js +190 -0
- package/server/lib/oauth-registry.test.js +306 -0
- package/server/lib/readme-generator.js +490 -0
- package/server/lib/readme-generator.test.js +493 -0
- package/server/lib/repo-dependency-tracker.js +261 -0
- package/server/lib/repo-dependency-tracker.test.js +350 -0
- package/server/lib/retention-policy.js +281 -0
- package/server/lib/retention-policy.test.js +486 -0
- package/server/lib/role-mapper.js +236 -0
- package/server/lib/role-mapper.test.js +395 -0
- package/server/lib/saml-provider.js +765 -0
- package/server/lib/saml-provider.test.js +643 -0
- package/server/lib/security-policy-generator.js +682 -0
- package/server/lib/security-policy-generator.test.js +544 -0
- package/server/lib/sensitive-detector.js +112 -0
- package/server/lib/sensitive-detector.test.js +209 -0
- package/server/lib/service-interaction-diagram.js +700 -0
- package/server/lib/service-interaction-diagram.test.js +638 -0
- package/server/lib/service-summary.js +553 -0
- package/server/lib/service-summary.test.js +619 -0
- package/server/lib/session-purge.js +460 -0
- package/server/lib/session-purge.test.js +312 -0
- package/server/lib/sso-command.js +544 -0
- package/server/lib/sso-command.test.js +552 -0
- package/server/lib/sso-session.js +492 -0
- package/server/lib/sso-session.test.js +670 -0
- package/server/lib/workspace-command.js +249 -0
- package/server/lib/workspace-command.test.js +264 -0
- package/server/lib/workspace-config.js +270 -0
- package/server/lib/workspace-config.test.js +312 -0
- package/server/lib/workspace-docs-command.js +547 -0
- package/server/lib/workspace-docs-command.test.js +692 -0
- package/server/lib/workspace-memory.js +451 -0
- package/server/lib/workspace-memory.test.js +403 -0
- package/server/lib/workspace-scanner.js +452 -0
- package/server/lib/workspace-scanner.test.js +677 -0
- package/server/lib/workspace-test-runner.js +315 -0
- package/server/lib/workspace-test-runner.test.js +294 -0
- package/server/lib/zero-retention-command.js +439 -0
- package/server/lib/zero-retention-command.test.js +448 -0
- package/server/lib/zero-retention.js +322 -0
- package/server/lib/zero-retention.test.js +258 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ephemeral Storage - In-memory storage that never touches disk
|
|
3
|
+
*
|
|
4
|
+
* Provides a key-value store that:
|
|
5
|
+
* - Stores data in memory only (no disk persistence)
|
|
6
|
+
* - Auto-expires based on TTL
|
|
7
|
+
* - Provides same API as persistent storage (get, set, delete, clear)
|
|
8
|
+
* - Clears all data on process exit
|
|
9
|
+
* - Supports optional encryption in memory
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Simple encryption utilities for in-memory data
|
|
16
|
+
*/
|
|
17
|
+
class MemoryEncryption {
|
|
18
|
+
constructor() {
|
|
19
|
+
// Generate a random key for this session (not persisted)
|
|
20
|
+
this.key = crypto.randomBytes(32);
|
|
21
|
+
this.algorithm = 'aes-256-gcm';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
encrypt(data) {
|
|
25
|
+
const iv = crypto.randomBytes(16);
|
|
26
|
+
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
|
|
27
|
+
|
|
28
|
+
const serialized = typeof data === 'string' ? data : JSON.stringify(data);
|
|
29
|
+
let encrypted = cipher.update(serialized, 'utf8', 'hex');
|
|
30
|
+
encrypted += cipher.final('hex');
|
|
31
|
+
|
|
32
|
+
const authTag = cipher.getAuthTag();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
iv: iv.toString('hex'),
|
|
36
|
+
data: encrypted,
|
|
37
|
+
tag: authTag.toString('hex'),
|
|
38
|
+
isObject: typeof data !== 'string'
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
decrypt(encrypted) {
|
|
43
|
+
const iv = Buffer.from(encrypted.iv, 'hex');
|
|
44
|
+
const authTag = Buffer.from(encrypted.tag, 'hex');
|
|
45
|
+
|
|
46
|
+
const decipher = crypto.createDecipheriv(this.algorithm, this.key, iv);
|
|
47
|
+
decipher.setAuthTag(authTag);
|
|
48
|
+
|
|
49
|
+
let decrypted = decipher.update(encrypted.data, 'hex', 'utf8');
|
|
50
|
+
decrypted += decipher.final('utf8');
|
|
51
|
+
|
|
52
|
+
return encrypted.isObject ? JSON.parse(decrypted) : decrypted;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Storage entry with optional TTL
|
|
58
|
+
*/
|
|
59
|
+
class StorageEntry {
|
|
60
|
+
constructor(value, options = {}) {
|
|
61
|
+
this.value = value;
|
|
62
|
+
this.createdAt = Date.now();
|
|
63
|
+
this.expiresAt = options.ttl ? this.createdAt + options.ttl : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
isExpired() {
|
|
67
|
+
if (!this.expiresAt) return false;
|
|
68
|
+
return Date.now() > this.expiresAt;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* EphemeralStorage - In-memory key-value store
|
|
74
|
+
*/
|
|
75
|
+
class EphemeralStorage {
|
|
76
|
+
/**
|
|
77
|
+
* Create an ephemeral storage instance
|
|
78
|
+
* @param {Object} options - Configuration options
|
|
79
|
+
* @param {boolean} options.encrypt - Enable encryption for stored values
|
|
80
|
+
* @param {boolean} options.registerExitHandler - Register cleanup on process exit
|
|
81
|
+
* @param {string} options.basePath - Ignored, for API compatibility only
|
|
82
|
+
*/
|
|
83
|
+
constructor(options = {}) {
|
|
84
|
+
this.store = new Map();
|
|
85
|
+
this.encryption = options.encrypt ? new MemoryEncryption() : null;
|
|
86
|
+
this.options = options;
|
|
87
|
+
|
|
88
|
+
if (options.registerExitHandler) {
|
|
89
|
+
this._registerExitHandlers();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Store a value in memory
|
|
95
|
+
* @param {string} key - The key to store under
|
|
96
|
+
* @param {*} value - The value to store
|
|
97
|
+
* @param {Object} options - Storage options
|
|
98
|
+
* @param {number} options.ttl - Time to live in milliseconds
|
|
99
|
+
*/
|
|
100
|
+
set(key, value, options = {}) {
|
|
101
|
+
const storedValue = this.encryption
|
|
102
|
+
? this.encryption.encrypt(value)
|
|
103
|
+
: value;
|
|
104
|
+
|
|
105
|
+
const entry = new StorageEntry(storedValue, options);
|
|
106
|
+
this.store.set(key, entry);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Retrieve a value from memory
|
|
111
|
+
* @param {string} key - The key to retrieve
|
|
112
|
+
* @returns {*} The stored value, or null if not found/expired
|
|
113
|
+
*/
|
|
114
|
+
get(key) {
|
|
115
|
+
const entry = this.store.get(key);
|
|
116
|
+
|
|
117
|
+
if (!entry) return null;
|
|
118
|
+
|
|
119
|
+
if (entry.isExpired()) {
|
|
120
|
+
this.store.delete(key);
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.encryption) {
|
|
125
|
+
return this.encryption.decrypt(entry.value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return entry.value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get raw stored value (for testing encryption)
|
|
133
|
+
* @param {string} key - The key to retrieve
|
|
134
|
+
* @returns {*} The raw stored value
|
|
135
|
+
*/
|
|
136
|
+
getRaw(key) {
|
|
137
|
+
const entry = this.store.get(key);
|
|
138
|
+
if (!entry || entry.isExpired()) return null;
|
|
139
|
+
return entry.value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if a key exists and is not expired
|
|
144
|
+
* @param {string} key - The key to check
|
|
145
|
+
* @returns {boolean} True if key exists and is valid
|
|
146
|
+
*/
|
|
147
|
+
has(key) {
|
|
148
|
+
const entry = this.store.get(key);
|
|
149
|
+
if (!entry) return false;
|
|
150
|
+
if (entry.isExpired()) {
|
|
151
|
+
this.store.delete(key);
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Delete a specific key
|
|
159
|
+
* @param {string} key - The key to delete
|
|
160
|
+
*/
|
|
161
|
+
delete(key) {
|
|
162
|
+
this.store.delete(key);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Clear all stored data
|
|
167
|
+
*/
|
|
168
|
+
clear() {
|
|
169
|
+
this.store.clear();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get all keys
|
|
174
|
+
* @returns {string[]} Array of all keys (excluding expired)
|
|
175
|
+
*/
|
|
176
|
+
keys() {
|
|
177
|
+
const validKeys = [];
|
|
178
|
+
for (const [key, entry] of this.store) {
|
|
179
|
+
if (!entry.isExpired()) {
|
|
180
|
+
validKeys.push(key);
|
|
181
|
+
} else {
|
|
182
|
+
this.store.delete(key);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return validKeys;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get memory usage statistics
|
|
190
|
+
* @returns {Object} Stats including keyCount, approximateBytes, expiringKeys
|
|
191
|
+
*/
|
|
192
|
+
getStats() {
|
|
193
|
+
let approximateBytes = 0;
|
|
194
|
+
let expiringKeys = 0;
|
|
195
|
+
let validKeys = 0;
|
|
196
|
+
|
|
197
|
+
for (const [key, entry] of this.store) {
|
|
198
|
+
if (entry.isExpired()) {
|
|
199
|
+
this.store.delete(key);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
validKeys++;
|
|
204
|
+
|
|
205
|
+
// Approximate size calculation
|
|
206
|
+
approximateBytes += key.length * 2; // UTF-16 string
|
|
207
|
+
const value = entry.value;
|
|
208
|
+
if (typeof value === 'string') {
|
|
209
|
+
approximateBytes += value.length * 2;
|
|
210
|
+
} else if (typeof value === 'object') {
|
|
211
|
+
approximateBytes += JSON.stringify(value).length * 2;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (entry.expiresAt) {
|
|
215
|
+
expiringKeys++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
keyCount: validKeys,
|
|
221
|
+
approximateBytes,
|
|
222
|
+
expiringKeys
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Handle process exit - clear all data
|
|
228
|
+
*/
|
|
229
|
+
handleExit() {
|
|
230
|
+
this.clear();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Register exit handlers for process cleanup
|
|
235
|
+
* @private
|
|
236
|
+
*/
|
|
237
|
+
_registerExitHandlers() {
|
|
238
|
+
const exitHandler = () => this.handleExit();
|
|
239
|
+
|
|
240
|
+
process.on('exit', exitHandler);
|
|
241
|
+
process.on('SIGINT', exitHandler);
|
|
242
|
+
process.on('SIGTERM', exitHandler);
|
|
243
|
+
process.on('uncaughtException', exitHandler);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
module.exports = {
|
|
248
|
+
EphemeralStorage
|
|
249
|
+
};
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import { EphemeralStorage } from './ephemeral-storage.js';
|
|
6
|
+
|
|
7
|
+
describe('EphemeralStorage', () => {
|
|
8
|
+
let storage;
|
|
9
|
+
let testDir;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tlc-ephemeral-test-'));
|
|
13
|
+
storage = new EphemeralStorage();
|
|
14
|
+
vi.useFakeTimers();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
storage.clear();
|
|
19
|
+
vi.useRealTimers();
|
|
20
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('set', () => {
|
|
24
|
+
it('stores value in memory', () => {
|
|
25
|
+
storage.set('key1', 'value1');
|
|
26
|
+
|
|
27
|
+
expect(storage.get('key1')).toBe('value1');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('stores objects in memory', () => {
|
|
31
|
+
const data = { name: 'test', count: 42 };
|
|
32
|
+
storage.set('obj', data);
|
|
33
|
+
|
|
34
|
+
expect(storage.get('obj')).toEqual(data);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('get', () => {
|
|
39
|
+
it('retrieves stored value', () => {
|
|
40
|
+
storage.set('myKey', 'myValue');
|
|
41
|
+
|
|
42
|
+
const result = storage.get('myKey');
|
|
43
|
+
|
|
44
|
+
expect(result).toBe('myValue');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns null for missing key', () => {
|
|
48
|
+
const result = storage.get('nonexistent');
|
|
49
|
+
|
|
50
|
+
expect(result).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('TTL expiration', () => {
|
|
55
|
+
it('set with TTL expires after time', () => {
|
|
56
|
+
storage.set('expiring', 'value', { ttl: 1000 }); // 1 second TTL
|
|
57
|
+
|
|
58
|
+
// Before expiry
|
|
59
|
+
expect(storage.get('expiring')).toBe('value');
|
|
60
|
+
|
|
61
|
+
// After expiry
|
|
62
|
+
vi.advanceTimersByTime(1001);
|
|
63
|
+
expect(storage.get('expiring')).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('TTL does not affect other keys', () => {
|
|
67
|
+
storage.set('short', 'expires', { ttl: 500 });
|
|
68
|
+
storage.set('long', 'stays', { ttl: 5000 });
|
|
69
|
+
|
|
70
|
+
vi.advanceTimersByTime(600);
|
|
71
|
+
|
|
72
|
+
expect(storage.get('short')).toBeNull();
|
|
73
|
+
expect(storage.get('long')).toBe('stays');
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('clear', () => {
|
|
78
|
+
it('removes all data', () => {
|
|
79
|
+
storage.set('a', 1);
|
|
80
|
+
storage.set('b', 2);
|
|
81
|
+
storage.set('c', 3);
|
|
82
|
+
|
|
83
|
+
storage.clear();
|
|
84
|
+
|
|
85
|
+
expect(storage.get('a')).toBeNull();
|
|
86
|
+
expect(storage.get('b')).toBeNull();
|
|
87
|
+
expect(storage.get('c')).toBeNull();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('delete', () => {
|
|
92
|
+
it('removes specific key', () => {
|
|
93
|
+
storage.set('keep', 'value1');
|
|
94
|
+
storage.set('remove', 'value2');
|
|
95
|
+
|
|
96
|
+
storage.delete('remove');
|
|
97
|
+
|
|
98
|
+
expect(storage.get('keep')).toBe('value1');
|
|
99
|
+
expect(storage.get('remove')).toBeNull();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('does nothing for non-existent key', () => {
|
|
103
|
+
storage.set('exists', 'value');
|
|
104
|
+
|
|
105
|
+
// Should not throw
|
|
106
|
+
storage.delete('nonexistent');
|
|
107
|
+
|
|
108
|
+
expect(storage.get('exists')).toBe('value');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('data not persisted to disk', () => {
|
|
113
|
+
it('does not write to disk', () => {
|
|
114
|
+
const filePath = path.join(testDir, 'should-not-exist.json');
|
|
115
|
+
const diskStorage = new EphemeralStorage({ basePath: testDir });
|
|
116
|
+
|
|
117
|
+
diskStorage.set('key', 'value');
|
|
118
|
+
diskStorage.set('data', { complex: true, nested: { value: 123 } });
|
|
119
|
+
|
|
120
|
+
// Verify no files were created
|
|
121
|
+
const files = fs.readdirSync(testDir);
|
|
122
|
+
expect(files.length).toBe(0);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('onExit handler', () => {
|
|
127
|
+
it('clears all data on exit', () => {
|
|
128
|
+
storage.set('session', 'data');
|
|
129
|
+
storage.set('user', { id: 1 });
|
|
130
|
+
|
|
131
|
+
// Simulate process exit
|
|
132
|
+
storage.handleExit();
|
|
133
|
+
|
|
134
|
+
expect(storage.get('session')).toBeNull();
|
|
135
|
+
expect(storage.get('user')).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('registerExitHandler registers cleanup on process exit', () => {
|
|
139
|
+
const processSpy = vi.spyOn(process, 'on');
|
|
140
|
+
|
|
141
|
+
const newStorage = new EphemeralStorage({ registerExitHandler: true });
|
|
142
|
+
|
|
143
|
+
// Should have registered at least one exit handler
|
|
144
|
+
expect(processSpy).toHaveBeenCalledWith('exit', expect.any(Function));
|
|
145
|
+
|
|
146
|
+
processSpy.mockRestore();
|
|
147
|
+
newStorage.clear();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('encrypt option', () => {
|
|
152
|
+
it('encrypts values in memory', () => {
|
|
153
|
+
const encryptedStorage = new EphemeralStorage({ encrypt: true });
|
|
154
|
+
const sensitiveData = 'my-secret-token';
|
|
155
|
+
|
|
156
|
+
encryptedStorage.set('secret', sensitiveData);
|
|
157
|
+
|
|
158
|
+
// Get should return decrypted value
|
|
159
|
+
expect(encryptedStorage.get('secret')).toBe(sensitiveData);
|
|
160
|
+
|
|
161
|
+
// Internal storage should be encrypted (not plain text)
|
|
162
|
+
const rawValue = encryptedStorage.getRaw('secret');
|
|
163
|
+
expect(rawValue).not.toBe(sensitiveData);
|
|
164
|
+
// Encrypted value is stored as an object with iv, data, tag properties
|
|
165
|
+
expect(typeof rawValue).toBe('object');
|
|
166
|
+
expect(rawValue).toHaveProperty('iv');
|
|
167
|
+
expect(rawValue).toHaveProperty('data');
|
|
168
|
+
expect(rawValue).toHaveProperty('tag');
|
|
169
|
+
|
|
170
|
+
encryptedStorage.clear();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('encrypts object values', () => {
|
|
174
|
+
const encryptedStorage = new EphemeralStorage({ encrypt: true });
|
|
175
|
+
const data = { apiKey: 'secret123', user: 'admin' };
|
|
176
|
+
|
|
177
|
+
encryptedStorage.set('config', data);
|
|
178
|
+
|
|
179
|
+
expect(encryptedStorage.get('config')).toEqual(data);
|
|
180
|
+
|
|
181
|
+
const rawValue = encryptedStorage.getRaw('config');
|
|
182
|
+
expect(rawValue).not.toEqual(data);
|
|
183
|
+
|
|
184
|
+
encryptedStorage.clear();
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
describe('getStats', () => {
|
|
189
|
+
it('returns memory usage info', () => {
|
|
190
|
+
storage.set('a', 'short');
|
|
191
|
+
storage.set('b', 'a'.repeat(1000));
|
|
192
|
+
storage.set('c', { nested: { deep: 'value' } });
|
|
193
|
+
|
|
194
|
+
const stats = storage.getStats();
|
|
195
|
+
|
|
196
|
+
expect(stats).toHaveProperty('keyCount', 3);
|
|
197
|
+
expect(stats).toHaveProperty('approximateBytes');
|
|
198
|
+
expect(typeof stats.approximateBytes).toBe('number');
|
|
199
|
+
expect(stats.approximateBytes).toBeGreaterThan(0);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('returns zero stats for empty storage', () => {
|
|
203
|
+
const stats = storage.getStats();
|
|
204
|
+
|
|
205
|
+
expect(stats.keyCount).toBe(0);
|
|
206
|
+
expect(stats.approximateBytes).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('includes expired key tracking', () => {
|
|
210
|
+
storage.set('temp', 'value', { ttl: 100 });
|
|
211
|
+
|
|
212
|
+
const statsBefore = storage.getStats();
|
|
213
|
+
expect(statsBefore.expiringKeys).toBe(1);
|
|
214
|
+
|
|
215
|
+
vi.advanceTimersByTime(101);
|
|
216
|
+
|
|
217
|
+
// Trigger cleanup by accessing
|
|
218
|
+
storage.get('temp');
|
|
219
|
+
|
|
220
|
+
const statsAfter = storage.getStats();
|
|
221
|
+
expect(statsAfter.expiringKeys).toBe(0);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe('API compatibility', () => {
|
|
226
|
+
it('provides same API as persistent storage (get, set, delete, clear)', () => {
|
|
227
|
+
// All methods should exist
|
|
228
|
+
expect(typeof storage.get).toBe('function');
|
|
229
|
+
expect(typeof storage.set).toBe('function');
|
|
230
|
+
expect(typeof storage.delete).toBe('function');
|
|
231
|
+
expect(typeof storage.clear).toBe('function');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('has method returns boolean for key existence', () => {
|
|
235
|
+
storage.set('exists', 'value');
|
|
236
|
+
|
|
237
|
+
expect(storage.has('exists')).toBe(true);
|
|
238
|
+
expect(storage.has('missing')).toBe(false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('keys method returns all keys', () => {
|
|
242
|
+
storage.set('a', 1);
|
|
243
|
+
storage.set('b', 2);
|
|
244
|
+
storage.set('c', 3);
|
|
245
|
+
|
|
246
|
+
const keys = storage.keys();
|
|
247
|
+
|
|
248
|
+
expect(keys).toContain('a');
|
|
249
|
+
expect(keys).toContain('b');
|
|
250
|
+
expect(keys).toContain('c');
|
|
251
|
+
expect(keys.length).toBe(3);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|