fenneckit 1.0.3 โ†’ 1.2.0

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/bin/fenneckit CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- require('../lib/fenneckit.js')
2
+ import '../libs/fenneckit.js'
package/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { newLabs, getStoreState, clearStore, } from "./libs/labs.js";
1
+ import { newLabs } from "./libs/labs.js";
2
2
  import { testing } from "./libs/testing.js";
3
- export { newLabs, testing, getStoreState, clearStore };
3
+ export { newLabs, testing };
package/libs/fenneckit.js CHANGED
@@ -61,7 +61,7 @@ Usage:
61
61
  return;
62
62
  }
63
63
  if (args.includes("--version") || args.includes("-v")) {
64
- console.log("FennecKit ๐ŸฆŠ v1.1.0-beta");
64
+ console.log("FennecKit ๐ŸฆŠ v1.0.4");
65
65
  return;
66
66
  }
67
67
  const singleFile = args.find(a => !a.startsWith("-"));
package/libs/labs.js CHANGED
@@ -1,38 +1,306 @@
1
1
  /*==================================
2
2
  * Copyright 2026 lasith ruwantha amrwansha
3
- * Written by 2026/09/17
3
+ * Written by 2026/09/20
4
4
  * Author: ruwantha amrwansha
5
5
  * Library: FennecKit ๐ŸฆŠ
6
- * Version: 1.1.0-beta (with clearStore feature)
7
6
  *===================================*/
8
- import { access, writeFile } from "fs/promises";
7
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync, } from "crypto";
8
+ import report from "../utility/report.js";
9
9
  import path from "path";
10
- const d = new Date();
11
- const nowDate = d.toISOString().split('T')[0].replaceAll('-', '/');
12
- const Store = new Map();
13
- const headOfRepotPath = `
14
- # FennecKit ๐ŸฆŠ Report
15
-
16
- **๐Ÿš€ Version:** 1.1.0-beta
17
- **โฐ Date:** ${nowDate}
18
- **โœจ Features:** Zero Config โ€ข Sequential Labs โ€ข Inter-Lab Data Sharing โ€ข Store Management
19
-
20
- ---\n\n
21
- `;
22
- async function writeRepot(data) {
23
- const repotPath = path.join(process.cwd(), "fenneckit.md");
24
- try {
25
- await access(repotPath);
26
- await writeFile(repotPath, data, { flag: 'a' });
10
+ class SecretVault {
11
+ vault = new Map();
12
+ masterKey;
13
+ cipher = "aes-256-gcm";
14
+ keyLength = 32;
15
+ ivLength = 16;
16
+ constructor(password = "fenneckit-default") {
17
+ this.masterKey = scryptSync(password, "salt", this.keyLength);
18
+ }
19
+ encrypt(data) {
20
+ const iv = randomBytes(this.ivLength);
21
+ const cipher = createCipheriv(this.cipher, this.masterKey, iv);
22
+ let encrypted = cipher.update(data, "utf8");
23
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
24
+ const tag = cipher.getAuthTag();
25
+ return { encrypted, iv, tag };
26
+ }
27
+ decrypt(data, iv, tag) {
28
+ const decipher = createDecipheriv(this.cipher, this.masterKey, iv);
29
+ decipher.setAuthTag(tag);
30
+ let decrypted = decipher.update(data);
31
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
32
+ return decrypted.toString("utf8");
33
+ }
34
+ set(key, value) {
35
+ const { encrypted, iv, tag } = this.encrypt(value);
36
+ const stored = Buffer.concat([iv, tag, encrypted]);
37
+ this.vault.set(key, stored);
38
+ }
39
+ get(key) {
40
+ const stored = this.vault.get(key);
41
+ if (!stored)
42
+ return undefined;
43
+ try {
44
+ const iv = stored.slice(0, this.ivLength);
45
+ const tag = stored.slice(this.ivLength, this.ivLength + 16);
46
+ const encrypted = stored.slice(this.ivLength + 16);
47
+ return this.decrypt(encrypted, iv, tag);
48
+ }
49
+ catch {
50
+ console.error(`Failed to decrypt secret: ${key}`);
51
+ return undefined;
52
+ }
53
+ }
54
+ delete(key) {
55
+ this.vault.delete(key);
56
+ }
57
+ clear() {
58
+ this.vault.clear();
59
+ }
60
+ keys() {
61
+ return Array.from(this.vault.keys());
62
+ }
63
+ }
64
+ class NamespacedStore {
65
+ data = new Map();
66
+ name;
67
+ constructor(name) {
68
+ this.name = name;
69
+ }
70
+ set(key, value) {
71
+ this.data.set(key, value);
72
+ }
73
+ get(key) {
74
+ return this.data.get(key);
75
+ }
76
+ delete(key) {
77
+ this.data.delete(key);
78
+ }
79
+ clear(key) {
80
+ if (key) {
81
+ this.data.delete(key);
82
+ }
83
+ else {
84
+ this.data.clear();
85
+ }
86
+ }
87
+ keys() {
88
+ return Array.from(this.data.keys());
89
+ }
90
+ entries() {
91
+ return Array.from(this.data.entries());
92
+ }
93
+ has(key) {
94
+ return this.data.has(key);
95
+ }
96
+ toJSON() {
97
+ const obj = {};
98
+ for (const [key, value] of this.data) {
99
+ obj[key] = value;
100
+ }
101
+ return obj;
102
+ }
103
+ }
104
+ class AuditLog {
105
+ entries = [];
106
+ record(entry) {
107
+ this.entries.push(entry);
108
+ }
109
+ getLog() {
110
+ return [...this.entries];
111
+ }
112
+ getLast(n) {
113
+ return this.entries.slice(-n);
114
+ }
115
+ filterByKey(key) {
116
+ return this.entries.filter((e) => e.key === key);
117
+ }
118
+ filterByOperation(op) {
119
+ return this.entries.filter((e) => e.operation === op);
120
+ }
121
+ filterByNamespace(ns) {
122
+ return this.entries.filter((e) => e.namespace === ns);
123
+ }
124
+ export(format) {
125
+ if (format === "json") {
126
+ return JSON.stringify(this.entries, null, 2);
127
+ }
128
+ if (format === "csv") {
129
+ const headers = [
130
+ "timestamp",
131
+ "lab",
132
+ "test",
133
+ "operation",
134
+ "namespace",
135
+ "key",
136
+ "dataType",
137
+ "success",
138
+ ].join(",");
139
+ const rows = this.entries.map((e) => [
140
+ e.timestamp,
141
+ e.lab,
142
+ e.test,
143
+ e.operation,
144
+ e.namespace || "",
145
+ e.key,
146
+ e.dataType,
147
+ e.success ? "true" : "false",
148
+ ].join(","));
149
+ return [headers, ...rows].join("\n");
150
+ }
151
+ return "";
152
+ }
153
+ clear() {
154
+ this.entries = [];
155
+ }
156
+ }
157
+ class HttpKit {
158
+ authHeader = null;
159
+ requestHistory = [];
160
+ baseHeaders = {
161
+ "Content-Type": "application/json",
162
+ "User-Agent": "FennecKit/1.2.0",
163
+ };
164
+ setAuth(type, credentials) {
165
+ this.authHeader = { type, value: credentials };
27
166
  }
28
- catch {
29
- await writeFile(repotPath, headOfRepotPath + data);
167
+ clearAuth() {
168
+ this.authHeader = null;
169
+ }
170
+ getAuthHeader() {
171
+ if (!this.authHeader)
172
+ return {};
173
+ if (this.authHeader.type === "bearer") {
174
+ return { Authorization: `Bearer ${this.authHeader.value}` };
175
+ }
176
+ else if (this.authHeader.type === "api-key") {
177
+ return { "X-API-Key": this.authHeader.value };
178
+ }
179
+ else if (this.authHeader.type === "basic") {
180
+ return { Authorization: `Basic ${this.authHeader.value}` };
181
+ }
182
+ return {};
183
+ }
184
+ async makeRequest(method, url, body, options = {}) {
185
+ const startTime = Date.now();
186
+ const requestId = `req_\( {Date.now()}_ \){Math.random().toString(36).substr(2, 9)}`;
187
+ const headers = {
188
+ ...this.baseHeaders,
189
+ ...this.getAuthHeader(),
190
+ ...(options.headers || {}),
191
+ };
192
+ // Use AbortController for timeout (RequestInit does not support `timeout`)
193
+ const controller = new AbortController();
194
+ const timeoutMs = options.timeout ?? 30000;
195
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
196
+ const reqConfig = {
197
+ method,
198
+ headers,
199
+ signal: controller.signal,
200
+ redirect: options.followRedirects !== false ? "follow" : "manual",
201
+ };
202
+ if (body) {
203
+ reqConfig.body = typeof body === "string" ? body : JSON.stringify(body);
204
+ }
205
+ let lastError = null;
206
+ const maxRetries = options.retry?.max || 1;
207
+ const retryDelay = options.retry?.delay || 1000;
208
+ try {
209
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
210
+ try {
211
+ const response = await fetch(url, reqConfig);
212
+ const duration = Date.now() - startTime;
213
+ let data = null;
214
+ const contentType = response.headers.get("content-type");
215
+ if (contentType?.includes("application/json")) {
216
+ data = await response.json();
217
+ }
218
+ else {
219
+ data = await response.text();
220
+ }
221
+ const httpResponse = {
222
+ status: response.status,
223
+ statusText: response.statusText,
224
+ headers: Object.fromEntries(response.headers.entries()),
225
+ data,
226
+ raw: response,
227
+ };
228
+ this.recordRequest({
229
+ id: requestId,
230
+ timestamp: new Date().toISOString(),
231
+ method,
232
+ url,
233
+ status: response.status,
234
+ duration,
235
+ headers,
236
+ requestBody: body,
237
+ responseBody: data,
238
+ });
239
+ return httpResponse;
240
+ }
241
+ catch (error) {
242
+ lastError = error;
243
+ if (attempt < maxRetries - 1) {
244
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
245
+ }
246
+ }
247
+ }
248
+ }
249
+ finally {
250
+ clearTimeout(timeoutId);
251
+ }
252
+ const duration = Date.now() - startTime;
253
+ this.recordRequest({
254
+ id: requestId,
255
+ timestamp: new Date().toISOString(),
256
+ method,
257
+ url,
258
+ status: 0,
259
+ duration,
260
+ headers,
261
+ requestBody: body,
262
+ error: lastError?.message,
263
+ });
264
+ throw lastError || new Error(`HTTP request failed: ${method} ${url}`);
265
+ }
266
+ recordRequest(req) {
267
+ this.requestHistory.push(req);
268
+ }
269
+ async get(url, options) {
270
+ return this.makeRequest("GET", url, undefined, options);
271
+ }
272
+ async post(url, body, options) {
273
+ return this.makeRequest("POST", url, body, options);
274
+ }
275
+ async put(url, body, options) {
276
+ return this.makeRequest("PUT", url, body, options);
277
+ }
278
+ async patch(url, body, options) {
279
+ return this.makeRequest("PATCH", url, body, options);
280
+ }
281
+ async delete(url, options) {
282
+ return this.makeRequest("DELETE", url, undefined, options);
283
+ }
284
+ getRequestHistory() {
285
+ return [...this.requestHistory];
286
+ }
287
+ clearHistory() {
288
+ this.requestHistory = [];
289
+ }
290
+ getLastRequest() {
291
+ return this.requestHistory[this.requestHistory.length - 1];
30
292
  }
31
293
  }
32
294
  export async function newLabs(name, fn, retCount = 0) {
33
295
  let logs = `## ${name} ๐ŸŒ\n\n\`\`\`\n`;
34
296
  let currentTest = "";
297
+ const Store = new Map();
298
+ const secretVault = new SecretVault();
299
+ const namespaces = new Map();
35
300
  const temp = new Map();
301
+ const http = new HttpKit();
302
+ const audit = new AuditLog();
303
+ const ttlMap = new Map();
36
304
  const Context = {
37
305
  out: () => {
38
306
  throw new Error("__Lab_Out__");
@@ -66,26 +334,91 @@ export async function newLabs(name, fn, retCount = 0) {
66
334
  logs += `[๐ŸŸก] ${currentTest}: ${msg}\n`;
67
335
  console.log(` โš ๏ธ [Warning] ${currentTest} -> ${msg}`);
68
336
  },
337
+ // Store
69
338
  setStore: (key, value) => {
70
339
  logs += `[โš™๏ธ] [STORE] set "${key}"\n`;
71
340
  Store.set(key, value);
341
+ audit.record({
342
+ timestamp: new Date().toISOString(),
343
+ lab: name,
344
+ test: currentTest,
345
+ operation: "set",
346
+ key,
347
+ dataType: typeof value,
348
+ success: true,
349
+ });
72
350
  },
73
351
  getStore: (key) => {
74
352
  logs += `[โš™๏ธ] [STORE] get "${key}"\n`;
353
+ audit.record({
354
+ timestamp: new Date().toISOString(),
355
+ lab: name,
356
+ test: currentTest,
357
+ operation: "get",
358
+ key,
359
+ dataType: "unknown",
360
+ success: true,
361
+ });
75
362
  return Store.get(key);
76
363
  },
77
364
  clearStore: (key) => {
78
365
  if (key) {
79
366
  logs += `[โš™๏ธ] [STORE] cleared key "${key}"\n`;
80
- console.log(` ๐Ÿงน [Store] Cleared: "${key}"`);
81
367
  Store.delete(key);
82
368
  }
83
369
  else {
84
370
  logs += `[โš™๏ธ] [STORE] cleared all data\n`;
85
- console.log(` ๐Ÿงน [Store] Cleared all Store data`);
86
371
  Store.clear();
87
372
  }
88
373
  },
374
+ // Secrets
375
+ setSecret: (key, value) => {
376
+ logs += `[๐Ÿ”] [SECRET] set "${key}"\n`;
377
+ secretVault.set(key, value);
378
+ audit.record({
379
+ timestamp: new Date().toISOString(),
380
+ lab: name,
381
+ test: currentTest,
382
+ operation: "setSecret",
383
+ key,
384
+ dataType: "secret",
385
+ success: true,
386
+ });
387
+ },
388
+ getSecret: (key) => {
389
+ logs += `[๐Ÿ”] [SECRET] get "${key}"\n`;
390
+ audit.record({
391
+ timestamp: new Date().toISOString(),
392
+ lab: name,
393
+ test: currentTest,
394
+ operation: "getSecret",
395
+ key,
396
+ dataType: "secret",
397
+ success: true,
398
+ });
399
+ return secretVault.get(key);
400
+ },
401
+ clearSecret: (key) => {
402
+ if (key) {
403
+ logs += `[๐Ÿ”] [SECRET] cleared key "${key}"\n`;
404
+ secretVault.delete(key);
405
+ }
406
+ else {
407
+ logs += `[๐Ÿ”] [SECRET] cleared all data\n`;
408
+ secretVault.clear();
409
+ }
410
+ },
411
+ // Namespaces
412
+ namespace: (ns) => {
413
+ if (!namespaces.has(ns)) {
414
+ namespaces.set(ns, new NamespacedStore(ns));
415
+ }
416
+ return namespaces.get(ns);
417
+ },
418
+ getNamespace: (ns) => {
419
+ return namespaces.get(ns);
420
+ },
421
+ // Temp
89
422
  setTemp: (key, value) => {
90
423
  logs += `[โฑ๏ธ] [TEMP] set "${key}"\n`;
91
424
  temp.set(key, value);
@@ -94,6 +427,34 @@ export async function newLabs(name, fn, retCount = 0) {
94
427
  logs += `[โฑ๏ธ] [TEMP] get "${key}"\n`;
95
428
  return temp.get(key);
96
429
  },
430
+ http,
431
+ audit,
432
+ // TTL
433
+ setStoreWithTTL: (key, value, ttlMs) => {
434
+ Context.setStore(key, value);
435
+ const existingTimer = ttlMap.get(key);
436
+ if (existingTimer)
437
+ clearTimeout(existingTimer);
438
+ const timer = setTimeout(() => {
439
+ Store.delete(key);
440
+ ttlMap.delete(key);
441
+ logs += `[โฐ] [TTL] expired key "${key}"\n`;
442
+ }, ttlMs);
443
+ ttlMap.set(key, timer);
444
+ },
445
+ setSecretWithTTL: (key, value, ttlMs) => {
446
+ Context.setSecret(key, value);
447
+ const existingTimer = ttlMap.get(`secret:${key}`);
448
+ if (existingTimer)
449
+ clearTimeout(existingTimer);
450
+ const timer = setTimeout(() => {
451
+ secretVault.delete(key);
452
+ ttlMap.delete(`secret:${key}`);
453
+ logs += `[โฐ] [TTL] expired secret "${key}"\n`;
454
+ }, ttlMs);
455
+ ttlMap.set(`secret:${key}`, timer);
456
+ },
457
+ // Testing
97
458
  test: async (testName, testFn) => {
98
459
  currentTest = testName;
99
460
  try {
@@ -103,7 +464,7 @@ export async function newLabs(name, fn, retCount = 0) {
103
464
  console.log(` ๐Ÿ› [Bug Exception] in "${testName}":`, e.message || e);
104
465
  throw e;
105
466
  }
106
- }
467
+ },
107
468
  };
108
469
  console.log(`\n๐Ÿ”ฌ [Lab Initiated]: ${name}`);
109
470
  try {
@@ -113,36 +474,22 @@ export async function newLabs(name, fn, retCount = 0) {
113
474
  if (err.message === "__Lab_Out__") {
114
475
  logs += "```\n";
115
476
  temp.clear();
116
- return await writeRepot(logs);
477
+ ttlMap.forEach((timer) => clearTimeout(timer));
478
+ // Write report here
479
+ return;
117
480
  }
118
481
  else if (err.message === "__Restart__") {
482
+ // Fixed: was newLabsV12
119
483
  return newLabs(name, fn, retCount + 1);
120
484
  }
121
485
  console.log(`โŒ [Lab Failed]: ${name}\n`);
122
- await writeRepot(logs + "```\n");
123
486
  temp.clear();
487
+ ttlMap.forEach((timer) => clearTimeout(timer));
124
488
  return;
125
489
  }
126
490
  logs += "```\n";
491
+ await report(logs);
127
492
  console.log(`โœ… [Lab Success]: ${name} completed successfully.\n`);
128
493
  temp.clear();
129
- await writeRepot(logs);
130
- }
131
- // Global Store hadels
132
- export function clearStore(key) {
133
- if (key) {
134
- Store.delete(key);
135
- console.log(`๐Ÿงน [Global] Cleared store key: "${key}"`);
136
- }
137
- else {
138
- Store.clear();
139
- console.log(`๐Ÿงน [Global] Cleared all Store data`);
140
- }
141
- }
142
- export function getStoreState() {
143
- return new Map(Store);
494
+ ttlMap.forEach((timer) => clearTimeout(timer));
144
495
  }
145
- process.on('exit', () => {
146
- console.log("[โ›”] All Labs stopped");
147
- Store.clear();
148
- });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fenneckit",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "A tiny, fast & adorable testing kit. Lightweight test framework with zero config โ€“ easy to hold, easy to run.",
5
5
  "keywords": [
6
6
  "testing",
@@ -34,11 +34,12 @@
34
34
  "build": "tsc",
35
35
  "test": "cd test && node ../dist/libs/fenneckit.js"
36
36
  },
37
- "bin":{
38
- "fenneckit": "./bin/fenneckit"
37
+ "bin": {
38
+ "fenneckit": "./bin/fenneckit"
39
39
  },
40
40
  "dependencies": {
41
41
  "chalk": "^6.0.0",
42
+ "crypto": "^1.0.1",
42
43
  "fs-extra": "^11.4.0",
43
44
  "path": "^0.12.7"
44
45
  },
@@ -0,0 +1,26 @@
1
+ import { access, writeFile } from "fs/promises";
2
+ import path from "path";
3
+ const d = new Date();
4
+ const nowDate = d.toISOString().split('T')[0].replaceAll('-', '/');
5
+ const Store = new Map();
6
+ const headOfRepotPath = `
7
+ # FennecKit ๐ŸฆŠ Report
8
+
9
+ **๐Ÿš€ Version:** 1.2.0
10
+
11
+ **โฐ Date:** ${nowDate}
12
+
13
+ **โœจ Features:** Zero Config โ€ข Sequential Labs โ€ข Inter-Lab Data Sharing โ€ข Store Management
14
+
15
+ ---\n\n
16
+ `;
17
+ export default async function report(data) {
18
+ const repotPath = path.join(process.cwd(), "fenneckit.md");
19
+ try {
20
+ await access(repotPath);
21
+ await writeFile(repotPath, data, { flag: 'a' });
22
+ }
23
+ catch {
24
+ await writeFile(repotPath, headOfRepotPath + data);
25
+ }
26
+ }