fenneckit 1.0.4 → 1.2.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/README.md +55 -576
- package/docs/api-reference.md +72 -0
- package/docs/audit-log.md +47 -0
- package/docs/best-practices.md +23 -0
- package/docs/getting-started.md +82 -0
- package/docs/http-kit.md +71 -0
- package/docs/storage.md +90 -0
- package/index.d.ts +5 -0
- package/index.d.ts.map +1 -0
- package/index.js +2 -2
- package/libs/fenneckit.d.ts +2 -0
- package/libs/fenneckit.d.ts.map +1 -0
- package/libs/labs.d.ts +70 -0
- package/libs/labs.d.ts.map +1 -0
- package/libs/labs.js +397 -46
- package/libs/testing.d.ts +9 -0
- package/libs/testing.d.ts.map +1 -0
- package/package.json +14 -3
- package/types.d.ts +39 -0
- package/types.d.ts.map +1 -0
- package/types.js +1 -0
- package/utility/report.d.ts +2 -0
- package/utility/report.d.ts.map +1 -0
- package/utility/report.js +26 -0
package/libs/labs.js
CHANGED
|
@@ -1,38 +1,310 @@
|
|
|
1
1
|
/*==================================
|
|
2
2
|
* Copyright 2026 lasith ruwantha amrwansha
|
|
3
|
-
* Written by 2026/09/
|
|
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 {
|
|
7
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync, } from "crypto";
|
|
8
|
+
import report from "../utility/report.js";
|
|
9
9
|
import path from "path";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
10
|
+
/*===========================
|
|
11
|
+
All calass
|
|
12
|
+
=============================*/
|
|
13
|
+
class SecretVault {
|
|
14
|
+
vault = new Map();
|
|
15
|
+
masterKey;
|
|
16
|
+
cipher = "aes-256-gcm";
|
|
17
|
+
keyLength = 32;
|
|
18
|
+
ivLength = 16;
|
|
19
|
+
constructor(password = "fenneckit-default") {
|
|
20
|
+
this.masterKey = scryptSync(password, "salt", this.keyLength);
|
|
21
|
+
}
|
|
22
|
+
encrypt(data) {
|
|
23
|
+
const iv = randomBytes(this.ivLength);
|
|
24
|
+
const cipher = createCipheriv(this.cipher, this.masterKey, iv);
|
|
25
|
+
let encrypted = cipher.update(data, "utf8");
|
|
26
|
+
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
|
27
|
+
const tag = cipher.getAuthTag();
|
|
28
|
+
return { encrypted, iv, tag };
|
|
29
|
+
}
|
|
30
|
+
decrypt(data, iv, tag) {
|
|
31
|
+
const decipher = createDecipheriv(this.cipher, this.masterKey, iv);
|
|
32
|
+
decipher.setAuthTag(tag);
|
|
33
|
+
let decrypted = decipher.update(data);
|
|
34
|
+
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
35
|
+
return decrypted.toString("utf8");
|
|
36
|
+
}
|
|
37
|
+
set(key, value) {
|
|
38
|
+
const { encrypted, iv, tag } = this.encrypt(value);
|
|
39
|
+
const stored = Buffer.concat([iv, tag, encrypted]);
|
|
40
|
+
this.vault.set(key, stored);
|
|
41
|
+
}
|
|
42
|
+
get(key) {
|
|
43
|
+
const stored = this.vault.get(key);
|
|
44
|
+
if (!stored)
|
|
45
|
+
return undefined;
|
|
46
|
+
try {
|
|
47
|
+
const iv = stored.slice(0, this.ivLength);
|
|
48
|
+
const tag = stored.slice(this.ivLength, this.ivLength + 16);
|
|
49
|
+
const encrypted = stored.slice(this.ivLength + 16);
|
|
50
|
+
return this.decrypt(encrypted, iv, tag);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
console.error(`Failed to decrypt secret: ${key}`);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
delete(key) {
|
|
58
|
+
this.vault.delete(key);
|
|
59
|
+
}
|
|
60
|
+
clear() {
|
|
61
|
+
this.vault.clear();
|
|
62
|
+
}
|
|
63
|
+
keys() {
|
|
64
|
+
return Array.from(this.vault.keys());
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
class NamespacedStore {
|
|
68
|
+
data = new Map();
|
|
69
|
+
name;
|
|
70
|
+
constructor(name) {
|
|
71
|
+
this.name = name;
|
|
72
|
+
}
|
|
73
|
+
set(key, value) {
|
|
74
|
+
this.data.set(key, value);
|
|
75
|
+
}
|
|
76
|
+
get(key) {
|
|
77
|
+
return this.data.get(key);
|
|
78
|
+
}
|
|
79
|
+
delete(key) {
|
|
80
|
+
this.data.delete(key);
|
|
81
|
+
}
|
|
82
|
+
clear(key) {
|
|
83
|
+
if (key) {
|
|
84
|
+
this.data.delete(key);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
this.data.clear();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
keys() {
|
|
91
|
+
return Array.from(this.data.keys());
|
|
92
|
+
}
|
|
93
|
+
entries() {
|
|
94
|
+
return Array.from(this.data.entries());
|
|
95
|
+
}
|
|
96
|
+
has(key) {
|
|
97
|
+
return this.data.has(key);
|
|
98
|
+
}
|
|
99
|
+
toJSON() {
|
|
100
|
+
const obj = {};
|
|
101
|
+
for (const [key, value] of this.data) {
|
|
102
|
+
obj[key] = value;
|
|
103
|
+
}
|
|
104
|
+
return obj;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
class AuditLog {
|
|
108
|
+
entries = [];
|
|
109
|
+
record(entry) {
|
|
110
|
+
this.entries.push(entry);
|
|
111
|
+
}
|
|
112
|
+
getLog() {
|
|
113
|
+
return [...this.entries];
|
|
114
|
+
}
|
|
115
|
+
getLast(n) {
|
|
116
|
+
return this.entries.slice(-n);
|
|
117
|
+
}
|
|
118
|
+
filterByKey(key) {
|
|
119
|
+
return this.entries.filter((e) => e.key === key);
|
|
120
|
+
}
|
|
121
|
+
filterByOperation(op) {
|
|
122
|
+
return this.entries.filter((e) => e.operation === op);
|
|
123
|
+
}
|
|
124
|
+
filterByNamespace(ns) {
|
|
125
|
+
return this.entries.filter((e) => e.namespace === ns);
|
|
126
|
+
}
|
|
127
|
+
export(format) {
|
|
128
|
+
if (format === "json") {
|
|
129
|
+
return JSON.stringify(this.entries, null, 2);
|
|
130
|
+
}
|
|
131
|
+
if (format === "csv") {
|
|
132
|
+
const headers = [
|
|
133
|
+
"timestamp",
|
|
134
|
+
"lab",
|
|
135
|
+
"test",
|
|
136
|
+
"operation",
|
|
137
|
+
"namespace",
|
|
138
|
+
"key",
|
|
139
|
+
"dataType",
|
|
140
|
+
"success",
|
|
141
|
+
].join(",");
|
|
142
|
+
const rows = this.entries.map((e) => [
|
|
143
|
+
e.timestamp,
|
|
144
|
+
e.lab,
|
|
145
|
+
e.test,
|
|
146
|
+
e.operation,
|
|
147
|
+
e.namespace || "",
|
|
148
|
+
e.key,
|
|
149
|
+
e.dataType,
|
|
150
|
+
e.success ? "true" : "false",
|
|
151
|
+
].join(","));
|
|
152
|
+
return [headers, ...rows].join("\n");
|
|
153
|
+
}
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
clear() {
|
|
157
|
+
this.entries = [];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
class HttpKit {
|
|
161
|
+
authHeader = null;
|
|
162
|
+
requestHistory = [];
|
|
163
|
+
baseHeaders = {
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
"User-Agent": "FennecKit/1.2.0",
|
|
166
|
+
};
|
|
167
|
+
setAuth(type, credentials) {
|
|
168
|
+
this.authHeader = { type, value: credentials };
|
|
27
169
|
}
|
|
28
|
-
|
|
29
|
-
|
|
170
|
+
clearAuth() {
|
|
171
|
+
this.authHeader = null;
|
|
172
|
+
}
|
|
173
|
+
getAuthHeader() {
|
|
174
|
+
if (!this.authHeader)
|
|
175
|
+
return {};
|
|
176
|
+
if (this.authHeader.type === "bearer") {
|
|
177
|
+
return { Authorization: `Bearer ${this.authHeader.value}` };
|
|
178
|
+
}
|
|
179
|
+
else if (this.authHeader.type === "api-key") {
|
|
180
|
+
return { "X-API-Key": this.authHeader.value };
|
|
181
|
+
}
|
|
182
|
+
else if (this.authHeader.type === "basic") {
|
|
183
|
+
return { Authorization: `Basic ${this.authHeader.value}` };
|
|
184
|
+
}
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
async makeRequest(method, url, body, options = {}) {
|
|
188
|
+
const startTime = Date.now();
|
|
189
|
+
const requestId = `req_\( {Date.now()}_ \){Math.random().toString(36).substr(2, 9)}`;
|
|
190
|
+
const headers = {
|
|
191
|
+
...this.baseHeaders,
|
|
192
|
+
...this.getAuthHeader(),
|
|
193
|
+
...(options.headers || {}),
|
|
194
|
+
};
|
|
195
|
+
// Use AbortController for timeout (RequestInit does not support `timeout`)
|
|
196
|
+
const controller = new AbortController();
|
|
197
|
+
const timeoutMs = options.timeout ?? 30000;
|
|
198
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
199
|
+
const reqConfig = {
|
|
200
|
+
method,
|
|
201
|
+
headers,
|
|
202
|
+
signal: controller.signal,
|
|
203
|
+
redirect: options.followRedirects !== false ? "follow" : "manual",
|
|
204
|
+
};
|
|
205
|
+
if (body) {
|
|
206
|
+
reqConfig.body = typeof body === "string" ? body : JSON.stringify(body);
|
|
207
|
+
}
|
|
208
|
+
let lastError = null;
|
|
209
|
+
const maxRetries = options.retry?.max || 1;
|
|
210
|
+
const retryDelay = options.retry?.delay || 1000;
|
|
211
|
+
try {
|
|
212
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
213
|
+
try {
|
|
214
|
+
const response = await fetch(url, reqConfig);
|
|
215
|
+
const duration = Date.now() - startTime;
|
|
216
|
+
let data = null;
|
|
217
|
+
const contentType = response.headers.get("content-type");
|
|
218
|
+
if (contentType?.includes("application/json")) {
|
|
219
|
+
data = await response.json();
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
data = await response.text();
|
|
223
|
+
}
|
|
224
|
+
const httpResponse = {
|
|
225
|
+
status: response.status,
|
|
226
|
+
statusText: response.statusText,
|
|
227
|
+
headers: Object.fromEntries(response.headers.entries()),
|
|
228
|
+
data,
|
|
229
|
+
raw: response,
|
|
230
|
+
};
|
|
231
|
+
this.recordRequest({
|
|
232
|
+
id: requestId,
|
|
233
|
+
timestamp: new Date().toISOString(),
|
|
234
|
+
method,
|
|
235
|
+
url,
|
|
236
|
+
status: response.status,
|
|
237
|
+
duration,
|
|
238
|
+
headers,
|
|
239
|
+
requestBody: body,
|
|
240
|
+
responseBody: data,
|
|
241
|
+
});
|
|
242
|
+
return httpResponse;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
lastError = error;
|
|
246
|
+
if (attempt < maxRetries - 1) {
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
clearTimeout(timeoutId);
|
|
254
|
+
}
|
|
255
|
+
const duration = Date.now() - startTime;
|
|
256
|
+
this.recordRequest({
|
|
257
|
+
id: requestId,
|
|
258
|
+
timestamp: new Date().toISOString(),
|
|
259
|
+
method,
|
|
260
|
+
url,
|
|
261
|
+
status: 0,
|
|
262
|
+
duration,
|
|
263
|
+
headers,
|
|
264
|
+
requestBody: body,
|
|
265
|
+
error: lastError?.message,
|
|
266
|
+
});
|
|
267
|
+
throw lastError || new Error(`HTTP request failed: ${method} ${url}`);
|
|
268
|
+
}
|
|
269
|
+
recordRequest(req) {
|
|
270
|
+
this.requestHistory.push(req);
|
|
271
|
+
}
|
|
272
|
+
async get(url, options) {
|
|
273
|
+
return this.makeRequest("GET", url, undefined, options);
|
|
274
|
+
}
|
|
275
|
+
async post(url, body, options) {
|
|
276
|
+
return this.makeRequest("POST", url, body, options);
|
|
277
|
+
}
|
|
278
|
+
async put(url, body, options) {
|
|
279
|
+
return this.makeRequest("PUT", url, body, options);
|
|
280
|
+
}
|
|
281
|
+
async patch(url, body, options) {
|
|
282
|
+
return this.makeRequest("PATCH", url, body, options);
|
|
283
|
+
}
|
|
284
|
+
async delete(url, options) {
|
|
285
|
+
return this.makeRequest("DELETE", url, undefined, options);
|
|
286
|
+
}
|
|
287
|
+
getRequestHistory() {
|
|
288
|
+
return [...this.requestHistory];
|
|
289
|
+
}
|
|
290
|
+
clearHistory() {
|
|
291
|
+
this.requestHistory = [];
|
|
292
|
+
}
|
|
293
|
+
getLastRequest() {
|
|
294
|
+
return this.requestHistory[this.requestHistory.length - 1];
|
|
30
295
|
}
|
|
31
296
|
}
|
|
297
|
+
// ====================== Lab Context ======================
|
|
32
298
|
export async function newLabs(name, fn, retCount = 0) {
|
|
33
299
|
let logs = `## ${name} 🌏\n\n\`\`\`\n`;
|
|
34
300
|
let currentTest = "";
|
|
301
|
+
const Store = new Map();
|
|
302
|
+
const secretVault = new SecretVault();
|
|
303
|
+
const namespaces = new Map();
|
|
35
304
|
const temp = new Map();
|
|
305
|
+
const http = new HttpKit();
|
|
306
|
+
const audit = new AuditLog();
|
|
307
|
+
const ttlMap = new Map();
|
|
36
308
|
const Context = {
|
|
37
309
|
out: () => {
|
|
38
310
|
throw new Error("__Lab_Out__");
|
|
@@ -66,26 +338,91 @@ export async function newLabs(name, fn, retCount = 0) {
|
|
|
66
338
|
logs += `[🟡] ${currentTest}: ${msg}\n`;
|
|
67
339
|
console.log(` ⚠️ [Warning] ${currentTest} -> ${msg}`);
|
|
68
340
|
},
|
|
341
|
+
// Store
|
|
69
342
|
setStore: (key, value) => {
|
|
70
343
|
logs += `[⚙️] [STORE] set "${key}"\n`;
|
|
71
344
|
Store.set(key, value);
|
|
345
|
+
audit.record({
|
|
346
|
+
timestamp: new Date().toISOString(),
|
|
347
|
+
lab: name,
|
|
348
|
+
test: currentTest,
|
|
349
|
+
operation: "set",
|
|
350
|
+
key,
|
|
351
|
+
dataType: typeof value,
|
|
352
|
+
success: true,
|
|
353
|
+
});
|
|
72
354
|
},
|
|
73
355
|
getStore: (key) => {
|
|
74
356
|
logs += `[⚙️] [STORE] get "${key}"\n`;
|
|
357
|
+
audit.record({
|
|
358
|
+
timestamp: new Date().toISOString(),
|
|
359
|
+
lab: name,
|
|
360
|
+
test: currentTest,
|
|
361
|
+
operation: "get",
|
|
362
|
+
key,
|
|
363
|
+
dataType: "unknown",
|
|
364
|
+
success: true,
|
|
365
|
+
});
|
|
75
366
|
return Store.get(key);
|
|
76
367
|
},
|
|
77
368
|
clearStore: (key) => {
|
|
78
369
|
if (key) {
|
|
79
370
|
logs += `[⚙️] [STORE] cleared key "${key}"\n`;
|
|
80
|
-
console.log(` 🧹 [Store] Cleared: "${key}"`);
|
|
81
371
|
Store.delete(key);
|
|
82
372
|
}
|
|
83
373
|
else {
|
|
84
374
|
logs += `[⚙️] [STORE] cleared all data\n`;
|
|
85
|
-
console.log(` 🧹 [Store] Cleared all Store data`);
|
|
86
375
|
Store.clear();
|
|
87
376
|
}
|
|
88
377
|
},
|
|
378
|
+
// Secrets
|
|
379
|
+
setSecret: (key, value) => {
|
|
380
|
+
logs += `[🔐] [SECRET] set "${key}"\n`;
|
|
381
|
+
secretVault.set(key, value);
|
|
382
|
+
audit.record({
|
|
383
|
+
timestamp: new Date().toISOString(),
|
|
384
|
+
lab: name,
|
|
385
|
+
test: currentTest,
|
|
386
|
+
operation: "setSecret",
|
|
387
|
+
key,
|
|
388
|
+
dataType: "secret",
|
|
389
|
+
success: true,
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
getSecret: (key) => {
|
|
393
|
+
logs += `[🔐] [SECRET] get "${key}"\n`;
|
|
394
|
+
audit.record({
|
|
395
|
+
timestamp: new Date().toISOString(),
|
|
396
|
+
lab: name,
|
|
397
|
+
test: currentTest,
|
|
398
|
+
operation: "getSecret",
|
|
399
|
+
key,
|
|
400
|
+
dataType: "secret",
|
|
401
|
+
success: true,
|
|
402
|
+
});
|
|
403
|
+
return secretVault.get(key);
|
|
404
|
+
},
|
|
405
|
+
clearSecret: (key) => {
|
|
406
|
+
if (key) {
|
|
407
|
+
logs += `[🔐] [SECRET] cleared key "${key}"\n`;
|
|
408
|
+
secretVault.delete(key);
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
logs += `[🔐] [SECRET] cleared all data\n`;
|
|
412
|
+
secretVault.clear();
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
// Namespaces
|
|
416
|
+
namespace: (ns) => {
|
|
417
|
+
if (!namespaces.has(ns)) {
|
|
418
|
+
namespaces.set(ns, new NamespacedStore(ns));
|
|
419
|
+
}
|
|
420
|
+
return namespaces.get(ns);
|
|
421
|
+
},
|
|
422
|
+
getNamespace: (ns) => {
|
|
423
|
+
return namespaces.get(ns);
|
|
424
|
+
},
|
|
425
|
+
// Temp
|
|
89
426
|
setTemp: (key, value) => {
|
|
90
427
|
logs += `[⏱️] [TEMP] set "${key}"\n`;
|
|
91
428
|
temp.set(key, value);
|
|
@@ -94,6 +431,34 @@ export async function newLabs(name, fn, retCount = 0) {
|
|
|
94
431
|
logs += `[⏱️] [TEMP] get "${key}"\n`;
|
|
95
432
|
return temp.get(key);
|
|
96
433
|
},
|
|
434
|
+
http,
|
|
435
|
+
audit,
|
|
436
|
+
// TTL
|
|
437
|
+
setStoreWithTTL: (key, value, ttlMs) => {
|
|
438
|
+
Context.setStore(key, value);
|
|
439
|
+
const existingTimer = ttlMap.get(key);
|
|
440
|
+
if (existingTimer)
|
|
441
|
+
clearTimeout(existingTimer);
|
|
442
|
+
const timer = setTimeout(() => {
|
|
443
|
+
Store.delete(key);
|
|
444
|
+
ttlMap.delete(key);
|
|
445
|
+
logs += `[⏰] [TTL] expired key "${key}"\n`;
|
|
446
|
+
}, ttlMs);
|
|
447
|
+
ttlMap.set(key, timer);
|
|
448
|
+
},
|
|
449
|
+
setSecretWithTTL: (key, value, ttlMs) => {
|
|
450
|
+
Context.setSecret(key, value);
|
|
451
|
+
const existingTimer = ttlMap.get(`secret:${key}`);
|
|
452
|
+
if (existingTimer)
|
|
453
|
+
clearTimeout(existingTimer);
|
|
454
|
+
const timer = setTimeout(() => {
|
|
455
|
+
secretVault.delete(key);
|
|
456
|
+
ttlMap.delete(`secret:${key}`);
|
|
457
|
+
logs += `[⏰] [TTL] expired secret "${key}"\n`;
|
|
458
|
+
}, ttlMs);
|
|
459
|
+
ttlMap.set(`secret:${key}`, timer);
|
|
460
|
+
},
|
|
461
|
+
// Testing
|
|
97
462
|
test: async (testName, testFn) => {
|
|
98
463
|
currentTest = testName;
|
|
99
464
|
try {
|
|
@@ -103,7 +468,7 @@ export async function newLabs(name, fn, retCount = 0) {
|
|
|
103
468
|
console.log(` 🐛 [Bug Exception] in "${testName}":`, e.message || e);
|
|
104
469
|
throw e;
|
|
105
470
|
}
|
|
106
|
-
}
|
|
471
|
+
},
|
|
107
472
|
};
|
|
108
473
|
console.log(`\n🔬 [Lab Initiated]: ${name}`);
|
|
109
474
|
try {
|
|
@@ -113,36 +478,22 @@ export async function newLabs(name, fn, retCount = 0) {
|
|
|
113
478
|
if (err.message === "__Lab_Out__") {
|
|
114
479
|
logs += "```\n";
|
|
115
480
|
temp.clear();
|
|
116
|
-
|
|
481
|
+
ttlMap.forEach((timer) => clearTimeout(timer));
|
|
482
|
+
// Write report here
|
|
483
|
+
return;
|
|
117
484
|
}
|
|
118
485
|
else if (err.message === "__Restart__") {
|
|
486
|
+
// Fixed: was newLabsV12
|
|
119
487
|
return newLabs(name, fn, retCount + 1);
|
|
120
488
|
}
|
|
121
489
|
console.log(`❌ [Lab Failed]: ${name}\n`);
|
|
122
|
-
await writeRepot(logs + "```\n");
|
|
123
490
|
temp.clear();
|
|
491
|
+
ttlMap.forEach((timer) => clearTimeout(timer));
|
|
124
492
|
return;
|
|
125
493
|
}
|
|
126
494
|
logs += "```\n";
|
|
495
|
+
await report(logs);
|
|
127
496
|
console.log(`✅ [Lab Success]: ${name} completed successfully.\n`);
|
|
128
497
|
temp.clear();
|
|
129
|
-
|
|
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);
|
|
498
|
+
ttlMap.forEach((timer) => clearTimeout(timer));
|
|
144
499
|
}
|
|
145
|
-
process.on('exit', () => {
|
|
146
|
-
console.log("[⛔] All Labs stopped");
|
|
147
|
-
Store.clear();
|
|
148
|
-
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface TestContext {
|
|
2
|
+
succes: (msg: string) => void;
|
|
3
|
+
err: (msg: string) => never;
|
|
4
|
+
log: (msg: string) => void;
|
|
5
|
+
worrn: (msg: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function testing<T>(fn: (t: TestContext) => T | Promise<T>): Promise<T | void>;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=testing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/libs/testing.ts"],"names":[],"mappings":"AAiCA,UAAU,WAAW;IACnB,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,KAAK,CAAC;IAC5B,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAKD,wBAAsB,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,WAAW,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAc1F"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fenneckit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
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",
|
|
@@ -29,16 +29,24 @@
|
|
|
29
29
|
"author": "lasith ruwantha amrwansha",
|
|
30
30
|
"type": "module",
|
|
31
31
|
"main": "index.js",
|
|
32
|
+
"types":"./dist/index.d.ts",
|
|
33
|
+
"exports":{
|
|
34
|
+
".":{
|
|
35
|
+
"types":"./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
32
39
|
"scripts": {
|
|
33
40
|
"eslint": "eslint .",
|
|
34
41
|
"build": "tsc",
|
|
35
42
|
"test": "cd test && node ../dist/libs/fenneckit.js"
|
|
36
43
|
},
|
|
37
|
-
"bin":{
|
|
38
|
-
|
|
44
|
+
"bin": {
|
|
45
|
+
"fenneckit": "./bin/fenneckit"
|
|
39
46
|
},
|
|
40
47
|
"dependencies": {
|
|
41
48
|
"chalk": "^6.0.0",
|
|
49
|
+
"crypto": "^1.0.1",
|
|
42
50
|
"fs-extra": "^11.4.0",
|
|
43
51
|
"path": "^0.12.7"
|
|
44
52
|
},
|
|
@@ -51,5 +59,8 @@
|
|
|
51
59
|
"tsx": "^4.23.13",
|
|
52
60
|
"typescript": "^6.0.3",
|
|
53
61
|
"typescript-eslint": "^8.70.0"
|
|
62
|
+
},
|
|
63
|
+
"engines":{
|
|
64
|
+
"node":">=20"
|
|
54
65
|
}
|
|
55
66
|
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface HttpResponse<T = any> {
|
|
2
|
+
status: number;
|
|
3
|
+
statusText: string;
|
|
4
|
+
headers: Record<string, string>;
|
|
5
|
+
data: T;
|
|
6
|
+
raw: Response;
|
|
7
|
+
}
|
|
8
|
+
export interface RequestOptions {
|
|
9
|
+
headers?: Record<string, string>;
|
|
10
|
+
timeout?: number;
|
|
11
|
+
retry?: {
|
|
12
|
+
max: number;
|
|
13
|
+
delay: number;
|
|
14
|
+
};
|
|
15
|
+
followRedirects?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface TracedRequest {
|
|
18
|
+
id: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
method: string;
|
|
21
|
+
url: string;
|
|
22
|
+
status: number;
|
|
23
|
+
duration: number;
|
|
24
|
+
headers: Record<string, string>;
|
|
25
|
+
requestBody?: any;
|
|
26
|
+
responseBody?: any;
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
export interface AuditEntry {
|
|
30
|
+
timestamp: string;
|
|
31
|
+
lab: string;
|
|
32
|
+
test: string;
|
|
33
|
+
operation: "set" | "get" | "delete" | "clear" | "setSecret" | "getSecret";
|
|
34
|
+
namespace?: string;
|
|
35
|
+
key: string;
|
|
36
|
+
dataType: string;
|
|
37
|
+
success: boolean;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=types.d.ts.map
|
package/types.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,GAAG;IACnC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,CAAC,CAAC;IACR,GAAG,EAAE,QAAQ,CAAC;CACf;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACvC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,WAAW,CAAC,EAAE,GAAG,CAAC;IAClB,YAAY,CAAC,EAAE,GAAG,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,KAAK,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,WAAW,GAAG,WAAW,CAAC;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB"}
|
package/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../src/utility/report.ts"],"names":[],"mappings":"AAoBA,wBAA8B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAShE"}
|
|
@@ -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
|
+
}
|