fenneckit 1.0.4 → 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/README.md CHANGED
@@ -6,9 +6,10 @@
6
6
  npm install fenneckit@latest --save-dev
7
7
  npx fenneckit --help
8
8
  ```
9
- **English**: A Complete Storage System for Practical and Seamless Data Analysis
10
9
 
11
- **Zero Config** • Sequential Labs • Inter-Lab Data Sharing (within the same file) • Store Management
10
+ **English**: A Complete Storage + Security + API Testing System for Practical and Seamless Data Analysis
11
+
12
+ **Zero Config** • Sequential Labs • Inter-Lab Data Sharing (within the same file) • Store Management • Encrypted Secrets • Namespaces • Audit Log • HTTP Testing Kit
12
13
 
13
14
  **🦊Example** : soon
14
15
 
@@ -70,8 +71,8 @@ npx fenneckit
70
71
  4. Generates a report (`fenneckit.md`) after execution
71
72
 
72
73
  > **Important limitation**
73
- > Data sharing (`setStore` / `getStore`) only works **inside the same file**.
74
- > Different lab files **cannot** share STORE or TEMP data with each other.
74
+ > Data sharing (`setStore` / `getStore` / secrets / namespaces) only works **inside the same file**.
75
+ > Different lab files **cannot** share STORE, TEMP, Secrets or Namespaces with each other.
75
76
 
76
77
  ---
77
78
 
@@ -80,71 +81,47 @@ npx fenneckit
80
81
  ```
81
82
  📁 FennecKit Execution
82
83
  │
83
- ├─ 📄 file1.labs.js (STORE Instance #1)
84
+ ├─ 📄 file1.labs.js (STORE Instance #1 + SecretVault + Namespaces)
84
85
  │ │
85
86
  │ ├─ 🔬 Lab 1 (User Registration)
86
87
  │ │ ├─ 📝 Test 1: Create User
87
88
  │ │ │ ├─ STORE: {"userId": "123"} ✅ Shared with Lab 2
89
+ │ │ │ ├─ SECRET: encrypted token ✅ Shared with Lab 2
90
+ │ │ │ ├─ NAMESPACE "user": {...} ✅ Shared with Lab 2
88
91
  │ │ │ └─ TEMP: {"token": "abc"} ❌ Only here
89
92
  │ │ │
90
93
  │ │ └─ 📝 Test 2: Send Email
91
- │ │ └─ Can access STORE from Test 1
94
+ │ │ └─ Can access STORE / SECRET / Namespaces from Test 1
92
95
  │ │
93
96
  │ ├─ 🔬 Lab 2 (Authentication)
94
97
  │ │ ├─ 📝 Test 1: Generate Token
95
98
  │ │ │ ├─ STORE: {"userId": "123"} ✅ From Lab 1
99
+ │ │ │ ├─ SECRET / Namespace ✅ From Lab 1
96
100
  │ │ │ └─ TEMP: {"token": "new"} ❌ Only here
97
101
  │ │ │
98
102
  │ │ └─ 📝 Test 2: Verify Token
99
- │ │ └─ Can access STORE from Labs 1 & 2
103
+ │ │ └─ Can access STORE / SECRET / Namespaces from Labs 1 & 2
100
104
  │ │
101
105
  │ └─ 🔬 Lab 3 (Cleanup)
102
- │ └─ File STORE cleared when execution ends
106
+ │ └─ File STORE + Secrets + Namespaces cleared when execution ends
103
107
  │
104
- ├─ 📄 file2.labs.js (STORE Instance #2 - ISOLATED)
105
- │ │
106
- │ ├─ 🔬 Lab 1
107
- │ │ └─ ❌ CANNOT access file1.labs.js STORE
108
- │ │
109
- │ └─ 🔬 Lab 2
110
- │ └─ ❌ CANNOT access file1.labs.js STORE
108
+ ├─ 📄 file2.labs.js (Completely ISOLATED)
109
+ │ └─ ❌ CANNOT access anything from file1.labs.js
111
110
  │
112
- └─ 📄 file3.labs.js (STORE Instance #3 - ISOLATED)
113
- └─ ❌ Isolated from file1.labs.js and file2.labs.js
111
+ └─ 📄 file3.labs.js (Completely ISOLATED)
112
+ └─ ❌ Isolated from all other files
114
113
  ```
115
114
 
116
115
  ### Understanding the Hierarchy
117
116
 
118
- **🔴 Level 1: Different Files = NO Data Sharing**
119
- ```
120
- file1.labs.js ← STORE Instance #1 (isolated)
121
- file2.labs.js ← STORE Instance #2 (isolated)
122
- file3.labs.js ← STORE Instance #3 (isolated)
123
-
124
- ❌ file1's STORE ≠ file2's STORE ≠ file3's STORE
125
- ```
117
+ **🔴 Level 1: Different Files = NO Data Sharing**
118
+ Each file gets its own isolated STORE, SecretVault and Namespaces.
126
119
 
127
- **🟡 Level 2: Same File, Different Labs = STORE Sharing**
128
- ```
129
- file1.labs.js
130
- ├─ Lab 1: setStore("userId", "123")
131
- ├─ Lab 2: getStore("userId") ✅ Can access
132
- └─ Lab 3: getStore("userId") ✅ Can still access
133
- ```
120
+ **🟡 Level 2: Same File, Different Labs = STORE + Secrets + Namespaces Sharing**
121
+ Data set in Lab 1 is available in Lab 2, Lab 3, etc. (until cleared).
134
122
 
135
- **🟢 Level 3: Same Lab, Different Tests = STORE + TEMP Sharing**
136
- ```
137
- Lab 1 (User Registration)
138
- ├─ Test 1:
139
- │ ├─ setStore("userId", "123") ✅ Shared with other tests
140
- │ └─ setTemp("token", "abc") ✅ Shared with other tests in Lab 1
141
- │
142
- ├─ Test 2:
143
- │ ├─ getStore("userId") ✅ Works (from Test 1)
144
- │ └─ getTemp("token") ✅ Works (from Test 1)
145
- │
146
- └─ Lab 1 ends → TEMP cleared, STORE remains
147
- ```
123
+ **🟢 Level 3: Same Lab, Different Tests = STORE + TEMP + Secrets + Namespaces Sharing**
124
+ TEMP is automatically cleared when the Lab ends. Everything else remains.
148
125
 
149
126
  ---
150
127
 
@@ -154,217 +131,218 @@ Lab 1 (User Registration)
154
131
 
155
132
  In Jest and Vitest every test is isolated. You cannot pass data from one test to another.
156
133
 
157
- ### FennecKit Solution – Three Storage Levels
134
+ ### FennecKit Solution – Storage Levels (v1.2.0)
158
135
 
159
136
  ```
160
137
  ┌─────────────────────────────────────────────────────────────┐
161
138
  │ FennecKit Storage System (Per File) │
162
139
  ├─────────────────────────────────────────────────────────────┤
163
140
  │ │
164
- │ LEVEL 1: FILE SCOPE (Entire File) │
141
+ │ LEVEL 1: FILE SCOPE │
165
142
  │ ┌──────────────────────────────────────────────────────┐ │
166
143
  │ │ STORE (Global - Persistent within file) │ │
167
144
  │ │ ├─ Shared across ALL Labs in this file │ │
168
145
  │ │ ├─ Available until file execution ends │ │
169
146
  │ │ ├─ Can be manually cleared with clearStore() │ │
170
- │ │ └─ Example: userId, authToken, orderData │ │
147
+ │ │ └─ Example: userId, orderData, status │ │
171
148
  │ └──────────────────────────────────────────────────────┘ │
172
149
  │ │
173
- │ LEVEL 2: LAB SCOPE (Single Lab) │
150
+ │ LEVEL 2: ENCRYPTED SECRETS (AES-256-GCM) │
174
151
  │ ┌──────────────────────────────────────────────────────┐ │
175
- │ │ STORE (Available to this and following Labs) │ │
176
- │ │ └─ Set in Lab 1, used in Lab 2, Lab 3, etc │ │
152
+ │ │ SECRET VAULT │ │
153
+ │ │ ├─ Encrypted at rest inside the process │ │
154
+ │ │ ├─ Shared across Labs in the same file │ │
155
+ │ │ ├─ setSecret / getSecret / clearSecret │ │
156
+ │ │ └─ Example: auth tokens, API keys, passwords │ │
177
157
  │ └──────────────────────────────────────────────────────┘ │
178
158
  │ │
179
- │ LEVEL 3: LAB-LOCAL SCOPE (Single Lab Only) │
159
+ │ LEVEL 3: NAMESPACES │
160
+ │ ┌──────────────────────────────────────────────────────┐ │
161
+ │ │ kit.namespace("user") / kit.namespace("order") │ │
162
+ │ │ ├─ Isolated key-value stores │ │
163
+ │ │ ├─ Shared across Labs in the same file │ │
164
+ │ │ └─ Perfect for grouping related data │ │
165
+ │ └──────────────────────────────────────────────────────┘ │
166
+ │ │
167
+ │ LEVEL 4: LAB-LOCAL SCOPE │
180
168
  │ ┌──────────────────────────────────────────────────────┐ │
181
169
  │ │ TEMP (Local - Auto-Cleaned) │ │
182
170
  │ │ ├─ Only available inside current Lab │ │
183
171
  │ │ ├─ Automatically cleared when Lab ends │ │
184
- │ │ ├─ Cannot be manually cleared │ │
185
- │ │ └─ Example: timestamps, temp calculations │ │
172
+ │ │ └─ Example: timestamps, temp calculations │ │
186
173
  │ └──────────────────────────────────────────────────────┘ │
187
174
  │ │
188
175
  └─────────────────────────────────────────────────────────────┘
189
176
  ```
190
177
 
191
- ### Storage Demo (same file)
178
+ ---
192
179
 
193
- ```typescript
194
- // ========== LAB 1: User Registration ==========
195
- await newLabs("User Registration", async (kit) => {
196
- await kit.test("Create User", async () => {
197
- const userId = "user_123";
198
- const email = "john@example.com";
180
+ ## 🔐 NEW in v1.2.0: Secret Vault (AES-256-GCM)
199
181
 
200
- // STORE → available to all Labs in this file
201
- kit.setStore("userId", userId);
202
- kit.setStore("userEmail", email);
182
+ Sensitive data should never live in plain STORE.
203
183
 
204
- // TEMP → only for this Lab
205
- kit.setTemp("tempToken", "abc123");
184
+ ```typescript
185
+ await newLabs("Auth Lab", async (kit) => {
186
+ await kit.test("Login", async () => {
187
+ // Encrypted storage
188
+ kit.setSecret("accessToken", "eyJhbGciOiJIUzI1NiIs...");
189
+ kit.setSecret("refreshToken", "rt_abc123");
206
190
 
207
- kit.done("User created");
191
+ kit.done("Tokens stored securely");
208
192
  });
209
- });
210
193
 
211
- // ========== LAB 2: Authentication ==========
212
- await newLabs("Authentication", async (kit) => {
213
- await kit.test("Generate Token", async () => {
214
- const userId = kit.getStore("userId"); // ✅ works (from Lab 1)
215
- const email = kit.getStore("userEmail"); // ✅ works (from Lab 1)
216
- const token = kit.getTemp("tempToken"); // ❌ undefined (cleared after Lab 1)
194
+ await kit.test("Use Token", async () => {
195
+ const token = kit.getSecret("accessToken"); // decrypted on the fly
196
+ kit.http.setAuth("bearer", token!);
197
+ kit.done("Token retrieved");
198
+ });
217
199
 
218
- kit.done(`Token generated for: ${email}`);
200
+ await kit.test("Cleanup", async () => {
201
+ kit.clearSecret("accessToken"); // remove one key
202
+ // kit.clearSecret(); // clear entire vault
203
+ kit.done("Secrets cleared");
219
204
  });
220
205
  });
221
206
  ```
222
207
 
223
- ---
208
+ **TTL Support for Secrets**
209
+ ```typescript
210
+ kit.setSecretWithTTL("tempToken", "abc123", 30_000); // auto-delete after 30s
211
+ ```
224
212
 
225
- ## 🧹 NEW: clearStore() Feature (v1.1.0)
213
+ ---
226
214
 
227
- Manually clear Store data between Labs for better state management and security.
215
+ ## 🗂️ NEW in v1.2.0: Namespaced Store
228
216
 
229
- ### Two Ways to Clear
217
+ Organize data into logical groups.
230
218
 
231
- #### 1. Clear specific key (inside Lab)
232
219
  ```typescript
233
- await newLabs("My Lab", async (kit) => {
234
- await kit.test("Store sensitive data", async () => {
235
- kit.setStore("authToken", "secret123");
236
- kit.done("Token stored");
220
+ await newLabs("E-Commerce Flow", async (kit) => {
221
+ const user = kit.namespace("user");
222
+ const order = kit.namespace("order");
223
+ const cart = kit.namespace("cart");
224
+
225
+ await kit.test("Register", async () => {
226
+ user.set("id", "usr_123");
227
+ user.set("email", "john@example.com");
228
+ kit.done("User namespace populated");
237
229
  });
238
230
 
239
- await kit.test("Cleanup", async () => {
240
- kit.clearStore("authToken"); // Remove only this key
241
- kit.done("Token cleared");
231
+ await kit.test("Create Order", async () => {
232
+ order.set("id", "ord_456");
233
+ order.set("total", 299.99);
234
+ kit.done("Order namespace populated");
235
+ });
236
+
237
+ await kit.test("Read Later", async () => {
238
+ const userId = kit.getNamespace("user")?.get("id"); // "usr_123"
239
+ const total = order.get("total"); // 299.99
240
+ kit.done(`User ${userId} ordered ${total}`);
242
241
  });
243
242
  });
244
243
  ```
245
244
 
246
- #### 2. Clear all data (global)
245
+ ---
246
+
247
+ ## 📡 NEW in v1.2.0: HTTP Kit (API Testing)
248
+
249
+ Built-in HTTP client with auth, retries, history and tracing.
250
+
247
251
  ```typescript
248
- import { newLabs, clearStore } from "fenneckit";
252
+ await newLabs("API Smoke Test", async (kit) => {
253
+ // Set auth once
254
+ kit.http.setAuth("bearer", "your-token");
255
+ // or kit.http.setAuth("basic", "base64creds");
256
+ // or kit.http.setAuth("api-key", "key123");
257
+
258
+ await kit.test("GET Users", async () => {
259
+ const res = await kit.http.get("https://api.example.com/users", {
260
+ timeout: 5000,
261
+ retry: { max: 3, delay: 1000 }
262
+ });
263
+
264
+ if (res.status !== 200) {
265
+ kit.err(`Expected 200, got ${res.status}`);
266
+ }
249
267
 
250
- await newLabs("Lab 1", async (kit) => {
251
- await kit.test("Setup", async () => {
252
- kit.setStore("data1", "value1");
253
- kit.setStore("data2", "value2");
254
- kit.done("Stored");
268
+ kit.setStore("users", res.data);
269
+ kit.done(`Fetched ${res.data.length} users`);
255
270
  });
256
- });
257
271
 
258
- clearStore(); // Clear all STORE data globally
272
+ await kit.test("POST Order", async () => {
273
+ const res = await kit.http.post("https://api.example.com/orders", {
274
+ productId: "prod_1",
275
+ qty: 2
276
+ });
277
+
278
+ kit.done(`Order created: ${res.data.id}`);
279
+ });
259
280
 
260
- await newLabs("Lab 2", async (kit) => {
261
- await kit.test("Fresh Start", async () => {
262
- const data = kit.getStore("data1"); // undefined
263
- kit.done("Fresh state");
281
+ // Inspect request history
282
+ await kit.test("Check History", async () => {
283
+ const last = kit.http.getLastRequest();
284
+ kit.log(`Last request took ${last?.duration}ms`);
264
285
  });
265
286
  });
266
287
  ```
267
288
 
268
- ### When to Use clearStore()
269
-
270
- | Scenario | Method | Why |
271
- |----------|--------|-----|
272
- | Remove sensitive data | `clearStore("token")` | Security |
273
- | Free memory | `clearStore("largeObject")` | Performance |
274
- | Test isolation | `clearStore()` | Prevent data leakage |
275
- | Between phases | `clearStore("tempData")` | Clean state |
289
+ **Available methods**:
290
+ - `kit.http.get / post / put / patch / delete`
291
+ - `kit.http.setAuth(type, credentials)` / `clearAuth()`
292
+ - `kit.http.getRequestHistory()` / `getLastRequest()` / `clearHistory()`
276
293
 
277
294
  ---
278
295
 
279
- ## 📊 Storage Lifecycle (per file)
280
-
281
- ```
282
- FILE EXECUTION START
283
- │
284
- ├─ Lab 1
285
- │ ├─ setStore(...) → kept across all labs
286
- │ ├─ setTemp(...) → kept only for this Lab
287
- │ ├─ clearStore(...) → optionally remove keys
288
- │ └─ Lab ends → TEMP cleared, STORE remains (unless cleared)
289
- │
290
- ├─ Lab 2
291
- │ ├─ getStore(...) → works (if not cleared)
292
- │ ├─ getTemp(...) → undefined (cleared after Lab 1)
293
- │ ├─ clearStore(...) → can clear for next labs
294
- │ └─ Lab ends → TEMP cleared, STORE remains (unless cleared)
295
- │
296
- └─ FILE END → STORE completely cleared
297
- ```
298
-
299
- **Remember**: This lifecycle is **per file**.
300
- Running `npx fenneckit fileA.labs.js` and then `npx fenneckit fileB.labs.js` gives two completely separate STORE instances.
296
+ ## 📋 NEW in v1.2.0: Audit Log
301
297
 
302
- ---
303
-
304
- ## 💡 Real-World Example (Single File)
298
+ Every STORE / SECRET operation is automatically recorded.
305
299
 
306
300
  ```typescript
307
- // order-pipeline.labs.js
308
-
309
- import { newLabs, clearStore } from "fenneckit";
310
-
311
- // LAB 1
312
- await newLabs("Order Validation", async (kit) => {
313
- await kit.test("Check Product Stock", async () => {
314
- kit.setStore("productId", "prod_456");
315
- kit.setStore("stockAvailable", 50);
316
- kit.setTemp("validationTime", Date.now());
317
- kit.done("Product stock verified");
301
+ await newLabs("Audit Demo", async (kit) => {
302
+ await kit.test("Operations", async () => {
303
+ kit.setStore("userId", "123");
304
+ kit.setSecret("token", "secret");
305
+ kit.getStore("userId");
318
306
  });
319
- });
320
-
321
- // LAB 2
322
- await newLabs("Payment Processing", async (kit) => {
323
- await kit.test("Charge Customer Card", async () => {
324
- const productId = kit.getStore("productId"); // ✅ From Lab 1
325
- const chargeId = `charge_${Date.now()}`;
326
307
 
327
- kit.setStore("chargeId", chargeId);
328
- kit.setStore("orderStatus", "paid");
329
- kit.setTemp("transactionId", chargeId);
308
+ await kit.test("Inspect Audit", async () => {
309
+ const last5 = kit.audit.getLast(5);
310
+ const byKey = kit.audit.filterByKey("token");
311
+ const json = kit.audit.export("json");
312
+ const csv = kit.audit.export("csv");
330
313
 
331
- kit.done(`Payment charged: ${chargeId}`);
314
+ kit.log(`Recorded ${last5.length} operations`);
332
315
  });
333
316
  });
317
+ ```
334
318
 
335
- // Clear sensitive payment data before shipping
336
- clearStore("chargeId");
319
+ ---
337
320
 
338
- // LAB 3
339
- await newLabs("Shipping & Notification", async (kit) => {
340
- await kit.test("Create Shipping Label", async () => {
341
- const status = kit.getStore("orderStatus"); // ✅ Available
342
- const chargeId = kit.getStore("chargeId"); // ❌ Cleared
343
- const tempTx = kit.getTemp("transactionId"); // ❌ Undefined
321
+ ## 🧹 clearStore() & clearSecret()
344
322
 
345
- if (status === "paid") {
346
- const tracking = `TRACK_${Date.now()}`;
347
- kit.setStore("trackingNumber", tracking);
348
- kit.done(`Shipping label created: ${tracking}`);
349
- }
350
- });
323
+ ```typescript
324
+ // Clear specific key
325
+ kit.clearStore("authToken");
326
+ kit.clearSecret("accessToken");
351
327
 
352
- await kit.test("Send Notification Email", async () => {
353
- const tracking = kit.getStore("trackingNumber");
354
- kit.done(`Email sent with tracking: ${tracking}`);
355
- });
356
- });
328
+ // Clear everything
329
+ kit.clearStore();
330
+ kit.clearSecret();
357
331
  ```
358
332
 
359
- Run it:
333
+ ---
334
+
335
+ ## ⏰ TTL Support (Time-To-Live)
360
336
 
361
- ```bash
362
- npx fenneckit order-pipeline.labs.js
337
+ ```typescript
338
+ // Auto-expire after 10 seconds
339
+ kit.setStoreWithTTL("sessionId", "sess_abc", 10_000);
340
+ kit.setSecretWithTTL("tempKey", "value", 30_000);
363
341
  ```
364
342
 
365
343
  ---
366
344
 
367
- ## 🛠️ LabContext Methods
345
+ ## 🛠️ LabContext Methods (v1.2.0)
368
346
 
369
347
  ```typescript
370
348
  interface LabContext {
@@ -374,184 +352,136 @@ interface LabContext {
374
352
  err(msg: string): void // stops the Lab
375
353
  flatErr(msg: string): void // continues
376
354
  log(msg: string): void
377
- warning(msg: string): void // warning
355
+ warning(msg: string): void
378
356
 
379
357
  // Flow control
380
358
  out(): void // exit Lab immediately
381
- ret(): void // restart Lab
359
+ ret(): void // restart Lab (max 3 times)
382
360
 
383
- // Persistent (across Labs in same file)
361
+ // Persistent STORE
384
362
  setStore(key: string, value: any): void
385
363
  getStore(key: string): any
386
- clearStore(key?: string): void // NEW: clear specific key or all
364
+ clearStore(key?: string): void
365
+ setStoreWithTTL(key: string, value: any, ttlMs: number): void
366
+
367
+ // Encrypted Secrets (AES-256-GCM)
368
+ setSecret(key: string, value: string): void
369
+ getSecret(key: string): string | undefined
370
+ clearSecret(key?: string): void
371
+ setSecretWithTTL(key: string, value: string, ttlMs: number): void
372
+
373
+ // Namespaces
374
+ namespace(name: string): NamespacedStore
375
+ getNamespace(name: string): NamespacedStore | undefined
387
376
 
388
377
  // Temporary (Lab-local only)
389
378
  setTemp(key: string, value: any): void
390
379
  getTemp(key: string): any
380
+
381
+ // HTTP Kit
382
+ http: HttpKit
383
+
384
+ // Audit Log
385
+ audit: AuditLog
391
386
  }
392
387
  ```
393
388
 
394
389
  ---
395
390
 
396
- ## 🚀 Quick Start
397
-
398
- ### 1. Create a lab file
399
-
400
- ```bash
401
- # example.labs.js
402
- ```
391
+ ## 💡 Real-World Example (v1.2.0)
403
392
 
404
393
  ```typescript
405
394
  import { newLabs } from "fenneckit";
406
395
 
407
- await newLabs("User Registration Workflow", async (kit) => {
408
- kit.log("Starting user registration...");
396
+ await newLabs("E-Commerce API Test", async (kit) => {
397
+ const user = kit.namespace("user");
398
+ const order = kit.namespace("order");
409
399
 
410
- await kit.test("Create User", async () => {
411
- const id = "user_" + Date.now();
412
- kit.setStore("userId", id);
413
- kit.done(`User created: ${id}`);
414
- });
400
+ await kit.test("Register User", async () => {
401
+ const res = await kit.http.post("https://api.shop.com/auth/register", {
402
+ email: "test@example.com",
403
+ password: "secure123"
404
+ });
415
405
 
416
- await kit.test("Send Welcome Email", async () => {
417
- const id = kit.getStore("userId");
418
- kit.done(`Email sent to user: ${id}`);
406
+ user.set("id", res.data.id);
407
+ kit.setSecret("token", res.data.token); // encrypted
408
+ kit.http.setAuth("bearer", res.data.token);
409
+
410
+ kit.done("User registered");
419
411
  });
420
- });
421
- ```
422
412
 
423
- ### 2. Run
413
+ await kit.test("Create Order", async () => {
414
+ const res = await kit.http.post("https://api.shop.com/orders", {
415
+ items: [{ id: "prod_1", qty: 2 }]
416
+ });
424
417
 
425
- ```bash
426
- npx fenneckit example.labs.js
427
- ```
418
+ order.set("id", res.data.orderId);
419
+ order.set("total", res.data.total);
420
+ kit.done(`Order created: ${res.data.orderId}`);
421
+ });
428
422
 
429
- ### 3. Check report
423
+ await kit.test("Security Cleanup", async () => {
424
+ kit.clearSecret("token"); // remove sensitive data
425
+ kit.done("Secrets cleared");
426
+ });
430
427
 
431
- ```bash
432
- cat fenneckit.md
428
+ await kit.test("Audit Check", async () => {
429
+ const log = kit.audit.getLast(10);
430
+ kit.log(`Audit entries: ${log.length}`);
431
+ });
432
+ });
433
433
  ```
434
434
 
435
435
  ---
436
436
 
437
- ## 📋 setStore vs setTemp vs clearStore
437
+ ## 📊 Storage Lifecycle (per file)
438
438
 
439
- | Scenario | Use | Why |
440
- |----------|-----|-----|
441
- | Pass data between Labs | `setStore` | Survives Lab end |
442
- | Performance timing | `setTemp` | Only needed inside one Lab |
443
- | Auth token / DB connection | `setStore` | Needed by multiple Labs |
444
- | Temporary calculation | `setTemp` | Auto-cleaned |
445
- | Remove sensitive data | `clearStore` | Security |
446
- | Reset before next phase | `clearStore` | Fresh state |
447
- | Cross-file sharing | ❌ Impossible | STORE is scoped to one file only |
439
+ ```
440
+ FILE EXECUTION START
441
+ │
442
+ ├─ Lab 1
443
+ │ ├─ setStore / setSecret / namespace.set → kept across Labs
444
+ │ ├─ setTemp → kept only for this Lab
445
+ │ ├─ clearStore / clearSecret → optional cleanup
446
+ │ └─ Lab ends → TEMP cleared, everything else remains
447
+ │
448
+ ├─ Lab 2
449
+ │ ├─ getStore / getSecret / namespace.get → works
450
+ │ └─ Lab ends → TEMP cleared
451
+ │
452
+ └─ FILE END → STORE + Secrets + Namespaces completely cleared
453
+ ```
448
454
 
449
455
  ---
450
456
 
451
- ## 🎯 Best Practices
457
+ ## 🎯 Best Practices (v1.2.0)
452
458
 
453
- 1. **Clear names**
459
+ 1. **Secrets always go into the Vault**
454
460
  ```typescript
455
- kit.setStore("userId", id);
456
- kit.setStore("authToken", token);
461
+ kit.setSecret("token", token); // ✅
462
+ kit.setStore("token", token); // ❌ avoid
457
463
  ```
458
464
 
459
- 2. **Always check before use**
465
+ 2. **Use namespaces for related data**
460
466
  ```typescript
461
- const userId = kit.getStore("userId");
462
- if (!userId) {
463
- kit.err("userId missing from previous Lab!");
464
- }
467
+ const user = kit.namespace("user");
468
+ user.set("id", id);
469
+ user.set("email", email);
465
470
  ```
466
471
 
467
- 3. **Clear sensitive data**
472
+ 3. **Clear sensitive data when no longer needed**
468
473
  ```typescript
469
- kit.clearStore("password");
474
+ kit.clearSecret("accessToken");
470
475
  kit.clearStore("creditCard");
471
476
  ```
472
477
 
473
- 4. **One concern per Lab**
474
- Keep each Lab focused. Use STORE to pass only the necessary data.
475
-
476
- 5. **Do not rely on cross-file data**
477
- If you need data from another file, write it to disk or a database yourself.
478
-
479
- ---
480
-
481
- ## 🔄 Complete Multi-Lab Example (Payment → Invoice)
482
-
483
- ```typescript
484
- import { newLabs, clearStore } from "fenneckit";
485
-
486
- await newLabs("Payment Validation", async (kit) => {
487
- await kit.test("Validate Payment Details", async () => {
488
- kit.setStore("customerId", "cust_123");
489
- kit.setStore("amount", 299.99);
490
- kit.setStore("currency", "USD");
491
- kit.setTemp("validatedAt", Date.now());
492
- kit.done("Payment validated: 299.99 USD");
493
- });
494
- });
495
-
496
- await newLabs("Process Charge", async (kit) => {
497
- await kit.test("Charge Card", async () => {
498
- const amount = kit.getStore("amount");
499
- const chargeId = `charge_${Date.now()}`;
500
- kit.setStore("chargeId", chargeId);
501
- kit.setStore("chargedAt", new Date().toISOString());
502
- kit.done(`Charged: ${chargeId} for ${amount}`);
503
- });
504
- });
505
-
506
- // Clear payment details (security)
507
- clearStore("chargeId");
508
-
509
- await newLabs("Generate Invoice", async (kit) => {
510
- await kit.test("Create Invoice PDF", async () => {
511
- const invoiceId = `inv_${Date.now()}`;
512
- kit.setStore("invoiceId", invoiceId);
513
- kit.done(`Invoice created: ${invoiceId}`);
514
- });
515
-
516
- await kit.test("Send Invoice Email", async () => {
517
- const invoiceId = kit.getStore("invoiceId");
518
- const customerId = kit.getStore("customerId");
519
- kit.done(`Invoice ${invoiceId} emailed to ${customerId}`);
520
- });
521
- });
522
- ```
523
-
524
- Run:
525
-
526
- ```bash
527
- npx fenneckit payment-pipeline.labs.js
528
- ```
529
-
530
- ---
531
-
532
- ## 📞 Troubleshooting
533
-
534
- **Q: Lab 2 cannot see Lab 1 data?**
535
- A: You used `setTemp`. Switch to `setStore`.
536
-
537
- **Q: I cleared data but it's still there?**
538
- A: Make sure you're using `clearStore()` correctly. Check key name.
539
-
540
- **Q: Can STORE survive across different files?**
541
- A: No. Each file execution has its own isolated STORE.
542
- `npx fenneckit a.labs.js` and `npx fenneckit b.labs.js` do not share data.
543
-
544
- **Q: How is TEMP cleaned?**
545
- A: Automatically when the Lab finishes. No manual cleanup needed.
478
+ 4. **Check before use**
479
+ ```typescript
480
+ const token = kit.getSecret("token");
481
+ if (!token) kit.err("Token missing!");
482
+ ```
546
483
 
547
- **Q: How do I run multiple files?**
548
- A:
549
- ```bash
550
- npx fenneckit file1.labs.js
551
- npx fenneckit file2.labs.js
552
- # or let the runner discover all *.labs.* files
553
- npx fenneckit
554
- ```
484
+ 5. **Never rely on cross-file sharing** – write to disk/DB if needed.
555
485
 
556
486
  ---
557
487
 
@@ -563,32 +493,40 @@ npx fenneckit
563
493
  - `⚠️` / `🟡` Warning
564
494
  - `📝` / `⚪` Info
565
495
  - `⚙️` Store operation
496
+ - `🔐` Secret operation
566
497
  - `⏱️` Temp operation
567
498
  - `🧹` Clear operation
499
+ - `⏰` TTL expiration
568
500
 
569
501
  ---
570
502
 
571
503
  ## 💪 Perfect For
572
504
 
573
- - Backend API testing (Express, Fastify, etc.)
505
+ - Backend API testing (Express, Fastify, NestJS, etc.)
506
+ - Security-conscious test flows (tokens, keys)
574
507
  - Database migration validation
575
- - CLI tool workflows
576
508
  - Microservice chaining
577
509
  - Pre-deployment smoke checks
578
- - Development-time sanity tests
579
- - State management testing
510
+ - State management + audit trails
580
511
  - Data pipeline validation
581
512
 
582
513
  ---
583
514
 
584
515
  ## 📝 Version History
585
516
 
586
- ### v1.0.0-beta (Current)
517
+ ### v1.2.0 (Current)
518
+ - 🔐 **Secret Vault** – AES-256-GCM encryption
519
+ - 🗂️ **Namespaced Store**
520
+ - 📋 **Audit Log** (JSON / CSV export)
521
+ - 📡 **HTTP Kit** with auth, retry, history
522
+ - ⏰ **TTL support** for STORE & Secrets
523
+ - Improved LabContext API
524
+
525
+ ### v1.1.0
587
526
  - ✨ Added `clearStore()` for store management
588
527
  - 🌳 Improved data sharing hierarchy documentation
589
- - 🧹 Better state cleanup capabilities
590
528
 
591
- ### v1.0.0 (Initial Release)
529
+ ### v1.0.0
592
530
  - Core Lab functionality
593
531
  - STORE & TEMP storage
594
532
  - Zero config runner
@@ -600,7 +538,7 @@ npx fenneckit
600
538
 
601
539
  **Copyright © 2026 Lasith Ruwantha Amrwansha**
602
540
  Written: 2026/09/17
603
- Updated: 2026/09/20
541
+ Updated: 2026/09/21
604
542
  Author: Ruwantha Amrwansha
605
543
  Library: FennecKit 🦊
606
544
 
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/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.4",
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
+ }