@63klabs/cache-data 1.3.6 → 1.3.7
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/CHANGELOG.md +20 -3
- package/CONTRIBUTING.md +18 -12
- package/package.json +5 -1
- package/src/lib/dao-cache.js +137 -11
- package/AI_CONTEXT.md +0 -863
- package/scripts/README.md +0 -175
- package/scripts/audit-documentation.mjs +0 -856
- package/scripts/review-documentation-files.mjs +0 -406
- package/scripts/setup-dev-environment.sh +0 -59
- package/scripts/verify-example-files.mjs +0 -194
package/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **Note:** This project is still in beta. While every effort is made to prevent breaking changes, they may still occur. If after upgrading to a new version you experience any issue, please report the issue and go back to a previous version until a fix is released.
|
|
6
|
+
|
|
7
|
+
To report an issue, or to see proposed and upcoming enhancements, check out [63Klabs/cache-data Issues](https://github.com/63Klabs/cache-data/issues) page on GitHub.
|
|
6
8
|
|
|
7
9
|
Report all vulnerabilities under the [Security menu](https://github.com/63Klabs/cache-data/security/advisories) in the Cache-Data GitHub repository.
|
|
8
10
|
|
|
9
|
-
## v1.3.
|
|
11
|
+
## v1.3.8 (unreleased)
|
|
12
|
+
|
|
13
|
+
- Nothing yet
|
|
14
|
+
|
|
15
|
+
## v1.3.7 (2026-02-06)
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **Cache DAO Undefined Header Bug** [Spec: 1-3-7-cache-dao-fix](.kiro/specs/1-3-7-cache-dao-fix/) - Fixed production bug where undefined values were passed to HTTP headers, causing request failures with "Invalid value 'undefined' for header" errors
|
|
19
|
+
- Cache.getHeader() now normalizes undefined to null for consistent behavior
|
|
20
|
+
- Added defensive validation at header assignment points in CacheableDataAccess.getData()
|
|
21
|
+
- Added Cache._isValidHeaderValue() helper method for header validation
|
|
22
|
+
- **Most likely cause:** The move from over-use of JSON Stringify/parse cycles in favor of cloning in v1.3.6. JSON stringify/parse removed undefined values from objects, covering an underlying issue that is now fixed.
|
|
10
23
|
|
|
11
|
-
|
|
24
|
+
### Added
|
|
25
|
+
- **Jest Testing Framework** [Spec: 1-3-7-cache-dao-fix](.kiro/specs/1-3-7-cache-dao-fix/) - Set up Jest alongside Mocha for better AWS integration testing
|
|
26
|
+
- Configured Jest with ES module support
|
|
27
|
+
- Added npm scripts: `test:jest`, `test:all`, `test:cache:jest`
|
|
28
|
+
- Jest tests use `*.jest.mjs` pattern to avoid conflicts with Mocha tests
|
|
12
29
|
|
|
13
30
|
## v1.3.6 (2025-02-02)
|
|
14
31
|
|
package/CONTRIBUTING.md
CHANGED
|
@@ -12,6 +12,24 @@ Submit feature requests. To keep this project simple and maintainable we accept
|
|
|
12
12
|
|
|
13
13
|
After you have successfully participated in the bug reporting and feature request process, fork the repository and make your changes in a separate branch. Once you're satisfied with your changes, submit a pull request for review. Please only submit small changes (a single feature) at first. Pull requests with major code updates or frequent pull requests will often get ignored. Changes should also have code and testing methods well documented.
|
|
14
14
|
|
|
15
|
+
All code changes MUST start as an Issue (or security report) with a clear description of the problem or enhancement. No changes should be submitted to the repository without an attached, and approved, Issue.
|
|
16
|
+
|
|
17
|
+
Code developed (by AI or Human) outside of Kiro (see below) must NOT be submitted directly to the repository. Instead submit a proof of concept for a new piece of code or method via the Issue tracker as an enhancement. Someone from the team will review, evaluate the usefulness, and then implement using the proper process.
|
|
18
|
+
|
|
19
|
+
## Use of AI
|
|
20
|
+
|
|
21
|
+
This project utilizes the Spec-Driven, AI-Assisted Engineering approach.
|
|
22
|
+
|
|
23
|
+
Spec-Driven, AI-Assisted Engineering (SD-AI) is a software development methodology that prioritizes creating detailed, structured specifications before writing code. It priortizes context, requirements, and architectural constraints to generate accurate, non-hallucinated code. This approach shifts from ad-hoc, prompt-driven "vibe coding" to a structured, human-guided, AI-executed workflow, improving reliability in complex projects.
|
|
24
|
+
|
|
25
|
+
> Contributors are responsible for every line of code--AI-generated or not.
|
|
26
|
+
|
|
27
|
+
Code must be reviewed, understood, and tested by a human before being merged.
|
|
28
|
+
|
|
29
|
+
Kiro is the required AI coding assistant for final integrations, documentation, and testing, as it is in the AWS Ecosystem and this project is deveoped to deploy on the AWS platform. Just like test suites, Kiro ensures the proper tests, documentation, and guardrails are in place. Kiro is as important as commit-hooks and tests as it is a tool that ensures quality checks and should not be bypassed.
|
|
30
|
+
|
|
31
|
+
Ensure [AI Context](./AI_CONTEXT.md) and [Kiro steering documents](.kiro/steering/ai-context-reference.md) are reviewed, understood, and used by both humans and AI.
|
|
32
|
+
|
|
15
33
|
## Development Setup
|
|
16
34
|
|
|
17
35
|
Tests and documentation are critical to this project.
|
|
@@ -138,18 +156,6 @@ All public APIs must have complete JSDoc documentation. See [Documentation Stand
|
|
|
138
156
|
*/
|
|
139
157
|
```
|
|
140
158
|
|
|
141
|
-
## Use of AI
|
|
142
|
-
|
|
143
|
-
This project utilizes the Spec-Driven, AI-Assisted Engineering approach.
|
|
144
|
-
|
|
145
|
-
Spec-Driven, AI-Assisted Engineering (SD-AI) is a software development methodology that prioritizes creating detailed, structured specifications before writing code. It priortizes context, requirements, and architectural constraints to generate accurate, non-hallucinated code. This approach shifts from ad-hoc, prompt-driven "vibe coding" to a structured, human-guided, AI-executed workflow, improving reliability in complex projects.
|
|
146
|
-
|
|
147
|
-
> Contributors are responsible for every line of code--AI-generated or not.
|
|
148
|
-
|
|
149
|
-
Code must be reviewed, understood, and tested by a human before being merged.
|
|
150
|
-
|
|
151
|
-
Kiro is the preferred AI coding assistant as it is in the AWS Ecosystem and this project is deveoped to deploy on the AWS platform. Ensure [AI Context](./AI_CONTEXT.md) and [Kiro steering documents](.kiro/steering/ai-context-reference.md) are reviewed, understood, and used by both humans and AI.
|
|
152
|
-
|
|
153
159
|
## Current Contributors
|
|
154
160
|
|
|
155
161
|
Thank you to the following people who have contributed to this project:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@63klabs/cache-data",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.7",
|
|
4
4
|
"description": "Cache data from an API endpoint or application process using AWS S3 and DynamoDb",
|
|
5
5
|
"author": "Chad Leigh Kluck (https://chadkluck.me)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"chai": "^6.x",
|
|
29
29
|
"chai-http": "^5.x",
|
|
30
30
|
"fast-check": "^4.x",
|
|
31
|
+
"jest": "^30.2.0",
|
|
31
32
|
"mocha": "^11.x",
|
|
32
33
|
"sinon": "^21.x"
|
|
33
34
|
},
|
|
@@ -36,7 +37,10 @@
|
|
|
36
37
|
},
|
|
37
38
|
"scripts": {
|
|
38
39
|
"test": "mocha 'test/**/*-tests.mjs'",
|
|
40
|
+
"test:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
41
|
+
"test:all": "npm test && npm run test:jest",
|
|
39
42
|
"test:cache": "mocha 'test/cache/**/*-tests.mjs'",
|
|
43
|
+
"test:cache:jest": "node --experimental-vm-modules node_modules/jest/bin/jest.js test/cache",
|
|
40
44
|
"test:config": "mocha 'test/config/**/*-tests.mjs'",
|
|
41
45
|
"test:endpoint": "mocha 'test/endpoint/**/*-tests.mjs'",
|
|
42
46
|
"test:logging": "mocha 'test/logging/**/*-tests.mjs'",
|
package/src/lib/dao-cache.js
CHANGED
|
@@ -1683,6 +1683,45 @@ class Cache {
|
|
|
1683
1683
|
// Boolean("false") is true so we need to code for it. As long as it is not "false", trust Boolean()
|
|
1684
1684
|
return (( value !== "false") ? Boolean(value) : false );
|
|
1685
1685
|
};
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Validate that a header value is suitable for HTTP header assignment.
|
|
1689
|
+
* Returns true if the value is a non-empty string (except "undefined") or number, false otherwise.
|
|
1690
|
+
*
|
|
1691
|
+
* This method is used internally to validate header values before assignment
|
|
1692
|
+
* to prevent undefined, null, or invalid types from being used in HTTP headers.
|
|
1693
|
+
* The string "undefined" is explicitly rejected because it causes HTTP errors
|
|
1694
|
+
* when used as a header value.
|
|
1695
|
+
*
|
|
1696
|
+
* @param {*} value The value to validate
|
|
1697
|
+
* @returns {boolean} True if value is valid for HTTP header, false otherwise
|
|
1698
|
+
* @private
|
|
1699
|
+
* @example
|
|
1700
|
+
* Cache._isValidHeaderValue('text'); // true
|
|
1701
|
+
* Cache._isValidHeaderValue(123); // true
|
|
1702
|
+
* Cache._isValidHeaderValue(''); // false
|
|
1703
|
+
* Cache._isValidHeaderValue('undefined'); // false (string "undefined" is invalid)
|
|
1704
|
+
* Cache._isValidHeaderValue(null); // false
|
|
1705
|
+
* Cache._isValidHeaderValue(undefined); // false
|
|
1706
|
+
* Cache._isValidHeaderValue(NaN); // false
|
|
1707
|
+
* Cache._isValidHeaderValue(true); // false
|
|
1708
|
+
* Cache._isValidHeaderValue({}); // false
|
|
1709
|
+
* Cache._isValidHeaderValue([]); // false
|
|
1710
|
+
*/
|
|
1711
|
+
static _isValidHeaderValue(value) {
|
|
1712
|
+
if (value === null || value === undefined) {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
const type = typeof value;
|
|
1716
|
+
if (type === 'string') {
|
|
1717
|
+
// Filter out empty strings and the string "undefined"
|
|
1718
|
+
return value.length > 0 && value !== 'undefined';
|
|
1719
|
+
}
|
|
1720
|
+
if (type === 'number') {
|
|
1721
|
+
return !isNaN(value);
|
|
1722
|
+
}
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1686
1725
|
|
|
1687
1726
|
/**
|
|
1688
1727
|
* Generate an ETag hash for cache validation. Creates a unique hash by combining
|
|
@@ -2252,12 +2291,18 @@ class Cache {
|
|
|
2252
2291
|
* Get the ETag header value from the cached data. The ETag is used for
|
|
2253
2292
|
* cache validation and conditional requests.
|
|
2254
2293
|
*
|
|
2255
|
-
*
|
|
2294
|
+
* Returns null if the ETag header is not present, is null, or is undefined.
|
|
2295
|
+
* This method uses getHeader() internally, which normalizes undefined values
|
|
2296
|
+
* to null for consistent behavior.
|
|
2297
|
+
*
|
|
2298
|
+
* @returns {string|null} The ETag value, or null if not present, null, or undefined
|
|
2256
2299
|
* @example
|
|
2257
2300
|
* const cache = new Cache(connection, cacheProfile);
|
|
2258
2301
|
* await cache.read();
|
|
2259
2302
|
* const etag = cache.getETag();
|
|
2260
|
-
*
|
|
2303
|
+
* if (etag !== null) {
|
|
2304
|
+
* console.log(`ETag: ${etag}`);
|
|
2305
|
+
* }
|
|
2261
2306
|
*/
|
|
2262
2307
|
getETag() {
|
|
2263
2308
|
return this.getHeader("etag");
|
|
@@ -2267,12 +2312,18 @@ class Cache {
|
|
|
2267
2312
|
* Get the Last-Modified header value from the cached data. This timestamp
|
|
2268
2313
|
* indicates when the cached content was last modified at the origin.
|
|
2269
2314
|
*
|
|
2270
|
-
*
|
|
2315
|
+
* Returns null if the Last-Modified header is not present, is null, or is undefined.
|
|
2316
|
+
* This method uses getHeader() internally, which normalizes undefined values
|
|
2317
|
+
* to null for consistent behavior.
|
|
2318
|
+
*
|
|
2319
|
+
* @returns {string|null} The Last-Modified header value in HTTP date format, or null if not present, null, or undefined
|
|
2271
2320
|
* @example
|
|
2272
2321
|
* const cache = new Cache(connection, cacheProfile);
|
|
2273
2322
|
* await cache.read();
|
|
2274
2323
|
* const lastModified = cache.getLastModified();
|
|
2275
|
-
*
|
|
2324
|
+
* if (lastModified !== null) {
|
|
2325
|
+
* console.log(`Last modified: ${lastModified}`);
|
|
2326
|
+
* }
|
|
2276
2327
|
*/
|
|
2277
2328
|
getLastModified() {
|
|
2278
2329
|
return this.getHeader("last-modified");
|
|
@@ -2435,18 +2486,36 @@ class Cache {
|
|
|
2435
2486
|
* Get a specific header value from the cached data by key. Header keys are
|
|
2436
2487
|
* case-insensitive (stored as lowercase).
|
|
2437
2488
|
*
|
|
2489
|
+
* Returns null if the header doesn't exist, has a null value, or has an undefined value.
|
|
2490
|
+
* This normalization ensures consistent behavior for conditional checks and prevents
|
|
2491
|
+
* undefined values from being passed to HTTP headers.
|
|
2492
|
+
*
|
|
2493
|
+
* Note: This method normalizes undefined to null. If a header key exists but has an
|
|
2494
|
+
* undefined value, this method returns null (never undefined). This ensures that
|
|
2495
|
+
* conditional checks like `!== null` work reliably.
|
|
2496
|
+
*
|
|
2438
2497
|
* @param {string} key The header key to retrieve (case-insensitive)
|
|
2439
|
-
* @returns {string|number|null} The header value, or null if the header doesn't exist
|
|
2498
|
+
* @returns {string|number|null} The header value, or null if the header doesn't exist, is null, or is undefined
|
|
2440
2499
|
* @example
|
|
2441
2500
|
* const cache = new Cache(connection, cacheProfile);
|
|
2442
2501
|
* await cache.read();
|
|
2443
2502
|
* const contentType = cache.getHeader('content-type');
|
|
2444
2503
|
* const etag = cache.getHeader('ETag'); // Case-insensitive
|
|
2445
2504
|
* console.log(`Content-Type: ${contentType}`);
|
|
2505
|
+
*
|
|
2506
|
+
* @example
|
|
2507
|
+
* // Undefined values are normalized to null
|
|
2508
|
+
* const undefinedHeader = cache.getHeader('missing-header');
|
|
2509
|
+
* console.log(undefinedHeader === null); // true (never undefined)
|
|
2446
2510
|
*/
|
|
2447
2511
|
getHeader(key) {
|
|
2448
2512
|
let headers = this.getHeaders();
|
|
2449
|
-
|
|
2513
|
+
if (headers === null || !(key in headers)) {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
// Normalize undefined to null for consistent behavior
|
|
2517
|
+
const value = headers[key];
|
|
2518
|
+
return (value === undefined || value === null) ? null : value;
|
|
2450
2519
|
};
|
|
2451
2520
|
|
|
2452
2521
|
/**
|
|
@@ -3047,11 +3116,23 @@ class CacheableDataAccess {
|
|
|
3047
3116
|
|
|
3048
3117
|
// add etag and last modified to connection
|
|
3049
3118
|
if ( !("headers" in connection)) { connection.headers = {}; }
|
|
3050
|
-
|
|
3051
|
-
|
|
3119
|
+
|
|
3120
|
+
// Sanitize existing headers to remove invalid values (including string "undefined")
|
|
3121
|
+
for (const key in connection.headers) {
|
|
3122
|
+
if (!Cache._isValidHeaderValue(connection.headers[key])) {
|
|
3123
|
+
delete connection.headers[key];
|
|
3124
|
+
tools.DebugAndLog.debug(`Removed invalid header value for "${key}": ${connection.headers[key]}`);
|
|
3125
|
+
}
|
|
3052
3126
|
}
|
|
3053
|
-
|
|
3054
|
-
|
|
3127
|
+
|
|
3128
|
+
const etag = cache.getETag();
|
|
3129
|
+
if ( !("if-none-match" in connection.headers) && Cache._isValidHeaderValue(etag)) {
|
|
3130
|
+
connection.headers['if-none-match'] = etag;
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
const lastModified = cache.getLastModified();
|
|
3134
|
+
if (!("if-modified-since" in connection.headers) && Cache._isValidHeaderValue(lastModified)) {
|
|
3135
|
+
connection.headers['if-modified-since'] = lastModified;
|
|
3055
3136
|
}
|
|
3056
3137
|
|
|
3057
3138
|
// request data from original source
|
|
@@ -3095,7 +3176,52 @@ class CacheableDataAccess {
|
|
|
3095
3176
|
};
|
|
3096
3177
|
};
|
|
3097
3178
|
|
|
3179
|
+
/**
|
|
3180
|
+
* TestHarness provides access to internal classes for testing purposes only.
|
|
3181
|
+
* This class should NEVER be used in production code - it exists solely to
|
|
3182
|
+
* enable proper testing of the cache-dao module without exposing internal
|
|
3183
|
+
* implementation details to end users.
|
|
3184
|
+
*
|
|
3185
|
+
* @private
|
|
3186
|
+
* @example
|
|
3187
|
+
* // In tests only - DO NOT use in production
|
|
3188
|
+
* import { TestHarness } from '../../src/lib/dao-cache.js';
|
|
3189
|
+
* const { CacheData } = TestHarness.getInternals();
|
|
3190
|
+
*
|
|
3191
|
+
* // Mock CacheData.read for testing
|
|
3192
|
+
* const originalRead = CacheData.read;
|
|
3193
|
+
* CacheData.read = async () => ({ cache: { body: 'test', headers: {} } });
|
|
3194
|
+
* // ... run tests ...
|
|
3195
|
+
* CacheData.read = originalRead; // Restore
|
|
3196
|
+
*/
|
|
3197
|
+
class TestHarness {
|
|
3198
|
+
/**
|
|
3199
|
+
* Get access to internal classes for testing purposes.
|
|
3200
|
+
* WARNING: This method is for testing only and should never be used in production.
|
|
3201
|
+
*
|
|
3202
|
+
* @returns {{CacheData: typeof CacheData, S3Cache: typeof S3Cache, DynamoDbCache: typeof DynamoDbCache}} Object containing internal classes
|
|
3203
|
+
* @private
|
|
3204
|
+
* @example
|
|
3205
|
+
* // In tests only - DO NOT use in production
|
|
3206
|
+
* const { CacheData, S3Cache, DynamoDbCache } = TestHarness.getInternals();
|
|
3207
|
+
*
|
|
3208
|
+
* // Mock CacheData.read for property-based testing
|
|
3209
|
+
* const originalRead = CacheData.read;
|
|
3210
|
+
* CacheData.read = async () => ({
|
|
3211
|
+
* cache: { body: 'test', headers: { 'content-type': 'application/json' }, expires: 1234567890, statusCode: '200' }
|
|
3212
|
+
* });
|
|
3213
|
+
*/
|
|
3214
|
+
static getInternals() {
|
|
3215
|
+
return {
|
|
3216
|
+
CacheData,
|
|
3217
|
+
S3Cache,
|
|
3218
|
+
DynamoDbCache
|
|
3219
|
+
};
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3098
3223
|
module.exports = {
|
|
3099
3224
|
Cache,
|
|
3100
|
-
CacheableDataAccess
|
|
3225
|
+
CacheableDataAccess,
|
|
3226
|
+
TestHarness
|
|
3101
3227
|
};
|