@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 CHANGED
@@ -2,13 +2,30 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
- Proposed and upcoming changes may be found on [63Klabs/cache-data Issues](https://github.com/63Klabs/cache-data/issues).
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.7 (unreleased)
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
- - Next release
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.6",
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'",
@@ -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
- * @returns {string|null} The ETag value, or null if not present
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
- * console.log(`ETag: ${etag}`);
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
- * @returns {string|null} The Last-Modified header value in HTTP date format, or null if not present
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
- * console.log(`Last modified: ${lastModified}`);
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
- return ( headers !== null && key in headers) ? headers[key] : null
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
- if ( !("if-none-match" in connection.headers) && cache.getETag() !== null) {
3051
- connection.headers['if-none-match'] = cache.getETag();
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
- if (!("if-modified-since" in connection.headers) && cache.getLastModified() !== null) {
3054
- connection.headers['if-modified-since'] = cache.getLastModified();
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
  };