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 +256 -318
- package/index.js +2 -2
- package/libs/labs.js +393 -46
- package/package.json +4 -3
- package/utility/report.js +26 -0
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
|
-
**
|
|
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
|
|
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 (
|
|
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 (
|
|
113
|
-
└─ ❌ Isolated from
|
|
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 –
|
|
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
|
|
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,
|
|
147
|
+
│ │ └─ Example: userId, orderData, status │ │
|
|
171
148
|
│ └──────────────────────────────────────────────────────┘ │
|
|
172
149
|
│ │
|
|
173
|
-
│ LEVEL 2:
|
|
150
|
+
│ LEVEL 2: ENCRYPTED SECRETS (AES-256-GCM) │
|
|
174
151
|
│ ┌──────────────────────────────────────────────────────┐ │
|
|
175
|
-
│ │
|
|
176
|
-
│ │
|
|
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:
|
|
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
|
-
│ │
|
|
185
|
-
│ │ └─ Example: timestamps, temp calculations │ │
|
|
172
|
+
│ │ └─ Example: timestamps, temp calculations │ │
|
|
186
173
|
│ └──────────────────────────────────────────────────────┘ │
|
|
187
174
|
│ │
|
|
188
175
|
└─────────────────────────────────────────────────────────────┘
|
|
189
176
|
```
|
|
190
177
|
|
|
191
|
-
|
|
178
|
+
---
|
|
192
179
|
|
|
193
|
-
|
|
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
|
-
|
|
201
|
-
kit.setStore("userId", userId);
|
|
202
|
-
kit.setStore("userEmail", email);
|
|
182
|
+
Sensitive data should never live in plain STORE.
|
|
203
183
|
|
|
204
|
-
|
|
205
|
-
|
|
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("
|
|
191
|
+
kit.done("Tokens stored securely");
|
|
208
192
|
});
|
|
209
|
-
});
|
|
210
193
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
213
|
+
---
|
|
226
214
|
|
|
227
|
-
|
|
215
|
+
## 🗂️ NEW in v1.2.0: Namespaced Store
|
|
228
216
|
|
|
229
|
-
|
|
217
|
+
Organize data into logical groups.
|
|
230
218
|
|
|
231
|
-
#### 1. Clear specific key (inside Lab)
|
|
232
219
|
```typescript
|
|
233
|
-
await newLabs("
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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("
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
await kit.test("
|
|
262
|
-
const
|
|
263
|
-
kit.
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
328
|
-
kit.
|
|
329
|
-
kit.
|
|
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.
|
|
314
|
+
kit.log(`Recorded ${last5.length} operations`);
|
|
332
315
|
});
|
|
333
316
|
});
|
|
317
|
+
```
|
|
334
318
|
|
|
335
|
-
|
|
336
|
-
clearStore("chargeId");
|
|
319
|
+
---
|
|
337
320
|
|
|
338
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
}
|
|
350
|
-
});
|
|
323
|
+
```typescript
|
|
324
|
+
// Clear specific key
|
|
325
|
+
kit.clearStore("authToken");
|
|
326
|
+
kit.clearSecret("accessToken");
|
|
351
327
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
});
|
|
356
|
-
});
|
|
328
|
+
// Clear everything
|
|
329
|
+
kit.clearStore();
|
|
330
|
+
kit.clearSecret();
|
|
357
331
|
```
|
|
358
332
|
|
|
359
|
-
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## ⏰ TTL Support (Time-To-Live)
|
|
360
336
|
|
|
361
|
-
```
|
|
362
|
-
|
|
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
|
|
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
|
|
361
|
+
// Persistent STORE
|
|
384
362
|
setStore(key: string, value: any): void
|
|
385
363
|
getStore(key: string): any
|
|
386
|
-
clearStore(key?: string): void
|
|
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
|
-
##
|
|
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("
|
|
408
|
-
kit.
|
|
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("
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
kit.
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
423
|
+
await kit.test("Security Cleanup", async () => {
|
|
424
|
+
kit.clearSecret("token"); // remove sensitive data
|
|
425
|
+
kit.done("Secrets cleared");
|
|
426
|
+
});
|
|
430
427
|
|
|
431
|
-
|
|
432
|
-
|
|
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
|
-
##
|
|
437
|
+
## 📊 Storage Lifecycle (per file)
|
|
438
438
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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. **
|
|
459
|
+
1. **Secrets always go into the Vault**
|
|
454
460
|
```typescript
|
|
455
|
-
kit.
|
|
456
|
-
kit.setStore("
|
|
461
|
+
kit.setSecret("token", token); // ✅
|
|
462
|
+
kit.setStore("token", token); // ❌ avoid
|
|
457
463
|
```
|
|
458
464
|
|
|
459
|
-
2. **
|
|
465
|
+
2. **Use namespaces for related data**
|
|
460
466
|
```typescript
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
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.
|
|
474
|
+
kit.clearSecret("accessToken");
|
|
470
475
|
kit.clearStore("creditCard");
|
|
471
476
|
```
|
|
472
477
|
|
|
473
|
-
4. **
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
**
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
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/
|
|
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
|
|
1
|
+
import { newLabs } from "./libs/labs.js";
|
|
2
2
|
import { testing } from "./libs/testing.js";
|
|
3
|
-
export { newLabs, testing
|
|
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/
|
|
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
|
+
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"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
|
-
|
|
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
|
+
}
|