From 7fd6bdac8297948ba757d281b8c9de06f20fb938 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 17 May 2026 10:23:26 +1000 Subject: [PATCH 1/2] chore: scaffold prek pre-commit hooks for local quality gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds .pre-commit-config.yaml driving prek (Rust reimplementation of pre-commit, drop-in compatible). Hooks run locally before each commit to catch issues that previously only surfaced in CI. Pre-commit stage: - ESLint --fix on staged .ts in packages/cachekit/src - Prettier --write on staged ts/js/json/md/yaml/yml - cargo fmt --check and cargo clippy -D warnings on Rust changes - actionlint on .github/workflows (with cachekit-lean/cachekit self-hosted labels whitelisted via .github/actionlint.yaml) - pre-commit-hooks: trailing-whitespace, end-of-file-fixer, check-yaml, check-json, check-toml, check-added-large-files (1MB cap), check-merge-conflict, check-case-conflict - detect-secrets against .secrets.baseline (16 known test fixtures baselined: ck_test_* keys, deterministic hex test vectors) Pre-push stage: - pnpm type-check (full repo, too slow for pre-commit) Side-effect fixes uncovered by the new hooks (these were latent issues the lack of pre-commit was hiding): - cargo fmt normalisations in packages/cachekit-core-ts/src/lib.rs - impl Default for ByteStorage (clippy::new_without_default) - trailing newline added to .nvmrc and .gitignore - prettier reflow across READMEs, CHANGELOGs, intents.ts, test files, tsconfig.json, vitest.workspace.ts, etc. Install (one-time per clone): prek install --install-hooks Existing local pre-commit hooks (e.g. personal caliber refresh) are preserved automatically — prek migrates them to .legacy and chains them. --- .github/actionlint.yaml | 4 + .github/workflows/ci.yml | 2 +- .gitignore | 2 +- .nvmrc | 2 +- .pre-commit-config.yaml | 98 +++++++ .secrets.baseline | 262 ++++++++++++++++++ README.md | 6 +- SECURITY.md | 2 +- packages/cachekit-core-ts/CHANGELOG.md | 7 +- packages/cachekit-core-ts/src/lib.rs | 43 ++- packages/cachekit/CHANGELOG.md | 11 +- packages/cachekit/README.md | 20 +- packages/cachekit/src/index.ts | 8 +- packages/cachekit/src/intents.test.ts | 28 +- packages/cachekit/src/intents.ts | 18 +- ...encryption-real-crypto.integration.test.ts | 2 +- .../redis-backend.integration.test.ts | 2 +- .../test/protocol/aad-format.protocol.test.ts | 24 +- .../cross-sdk-interop.protocol.test.ts | 12 +- pnpm-workspace.yaml | 2 +- release-please-config.json | 24 +- tsconfig.json | 5 +- vitest.workspace.ts | 4 +- 23 files changed, 491 insertions(+), 97 deletions(-) create mode 100644 .github/actionlint.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 .secrets.baseline diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..cc39a61 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,4 @@ +self-hosted-runner: + labels: + - cachekit-lean + - cachekit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4561585..1e1e934 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,7 +60,7 @@ jobs: run: pnpm test test-integration: - runs-on: cachekit # needs DinD for Redis service container + runs-on: cachekit # needs DinD for Redis service container services: redis: image: redis:7-alpine diff --git a/.gitignore b/.gitignore index d983314..2e41621 100644 --- a/.gitignore +++ b/.gitignore @@ -54,4 +54,4 @@ AGENTS.md .caliber/ CALIBER_LEARNINGS.md .cursor/ -.cursorrules \ No newline at end of file +.cursorrules diff --git a/.nvmrc b/.nvmrc index 2edeafb..209e3ef 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 \ No newline at end of file +20 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..3ae0ed1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,98 @@ +# Pre-commit hooks for cachekit-ts +# +# Install (one time): prek install --install-hooks +# Run on all files: prek run --all-files +# Run on staged only: prek run +# Update hook versions: prek autoupdate +# +# Uses prek (https://github.com/j178/prek), a Rust reimplementation of +# pre-commit that consumes this same .pre-commit-config.yaml format. Falling +# back to Python `pre-commit` works identically. + +default_install_hook_types: [pre-commit, pre-push] + +repos: + # ─── TypeScript / JavaScript ───────────────────────────────────────────── + - repo: local + hooks: + - id: eslint + name: ESLint (auto-fix) + entry: pnpm exec eslint --fix --max-warnings 0 + language: system + files: ^packages/cachekit/src/.*\.(ts|tsx)$ + pass_filenames: true + + - id: prettier + name: Prettier (auto-format) + entry: pnpm exec prettier --write --ignore-unknown + language: system + files: \.(ts|tsx|js|mjs|cjs|json|md|yaml|yml)$ + exclude: | + (?x)^( + pnpm-lock\.yaml| + packages/[^/]+/dist/.*| + packages/cachekit-core-ts/index\.(js|d\.ts)| + \.release-please-manifest\.json + )$ + pass_filenames: true + + - id: type-check + name: TypeScript type-check (pre-push) + entry: pnpm type-check + language: system + files: \.(ts|tsx)$ + pass_filenames: false + stages: [pre-push] + + # ─── Rust (packages/cachekit-core-ts) ──────────────────────────────────── + - repo: local + hooks: + - id: cargo-fmt + name: Cargo format + entry: bash -c 'cd packages/cachekit-core-ts && cargo fmt --all -- --check' + language: system + files: ^packages/cachekit-core-ts/.*\.rs$ + pass_filenames: false + + - id: cargo-clippy + name: Cargo clippy + entry: bash -c 'cd packages/cachekit-core-ts && cargo clippy --all-targets -- -D warnings' + language: system + files: ^packages/cachekit-core-ts/.*\.rs$ + pass_filenames: false + + # ─── GitHub Actions workflow linting ───────────────────────────────────── + - repo: https://github.com/rhysd/actionlint + rev: 914e7df21a07ef503a81201c76d2b11c789d3fca # v1.7.12 # pragma: allowlist secret + hooks: + - id: actionlint + + # ─── General file hygiene ──────────────────────────────────────────────── + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 # pragma: allowlist secret + hooks: + - id: trailing-whitespace + exclude: \.md$ # markdown uses trailing whitespace for line breaks + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + exclude: ^(\.vscode/|tsconfig.*\.json$) # tsconfig allows comments + - id: check-toml + - id: check-added-large-files + args: [--maxkb=1000] + - id: check-merge-conflict + - id: check-case-conflict + + # ─── Secret scanning ───────────────────────────────────────────────────── + - repo: https://github.com/Yelp/detect-secrets + rev: 01886c8a910c64595c47f186ca1ffc0b77fa5458 # v1.5.0 # pragma: allowlist secret + hooks: + - id: detect-secrets + args: [--baseline, .secrets.baseline] + exclude: | + (?x)^( + pnpm-lock\.yaml| + packages/[^/]+/dist/.*| + packages/cachekit-core-ts/index\.(js|d\.ts)| + .*\.snap$ + )$ diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 0000000..5996611 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,262 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "GitLabTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "IPPublicDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "OpenAIDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "PypiTokenDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TelegramBotTokenDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + }, + { + "path": "detect_secrets.filters.regex.should_exclude_file", + "pattern": [ + "pnpm-lock\\.yaml|node_modules/|packages/.+/dist/|packages/cachekit-core-ts/index\\.(js|d\\.ts)|\\.snap$" + ] + } + ], + "results": { + "packages/cachekit/src/backends/cachekitio-lockable.test.ts": [ + { + "type": "Secret Keyword", + "filename": "packages/cachekit/src/backends/cachekitio-lockable.test.ts", + "hashed_secret": "e0fdd3a410527dc3165e4b0454a4f751532fe2d6", + "is_verified": false, + "line_number": 21 + } + ], + "packages/cachekit/src/backends/cachekitio-ttl.test.ts": [ + { + "type": "Secret Keyword", + "filename": "packages/cachekit/src/backends/cachekitio-ttl.test.ts", + "hashed_secret": "d5cddcdc91bee2fd64ae234ac73fdd0591c0e3ca", + "is_verified": false, + "line_number": 20 + } + ], + "packages/cachekit/src/backends/cachekitio.test.ts": [ + { + "type": "Secret Keyword", + "filename": "packages/cachekit/src/backends/cachekitio.test.ts", + "hashed_secret": "9242986caf890b01f4265ee595e88ee087af4b90", + "is_verified": false, + "line_number": 19 + }, + { + "type": "Secret Keyword", + "filename": "packages/cachekit/src/backends/cachekitio.test.ts", + "hashed_secret": "a62f2225bf70bfaccbc7f1ef2a397836717377de", + "is_verified": false, + "line_number": 45 + }, + { + "type": "Secret Keyword", + "filename": "packages/cachekit/src/backends/cachekitio.test.ts", + "hashed_secret": "310ca4c3a208b51df7a1d454bab0730b474e0d99", + "is_verified": false, + "line_number": 358 + } + ], + "packages/cachekit/src/intents.test.ts": [ + { + "type": "Secret Keyword", + "filename": "packages/cachekit/src/intents.test.ts", + "hashed_secret": "42c48ae0d1c6bc8d47b3b25fdcf2eb1156cd0c6a", + "is_verified": false, + "line_number": 206 + }, + { + "type": "Secret Keyword", + "filename": "packages/cachekit/src/intents.test.ts", + "hashed_secret": "18060b49185cba9a51b0d10290136007c3c8ab00", + "is_verified": false, + "line_number": 242 + } + ], + "packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts": [ + { + "type": "Hex High Entropy String", + "filename": "packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts", + "hashed_secret": "43f8a5e00c7a0964cdf90022b9eb2cab0b0f96ca", + "is_verified": false, + "line_number": 39 + }, + { + "type": "Hex High Entropy String", + "filename": "packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts", + "hashed_secret": "78957630e10fc59808977ffa94afcab92592eb41", + "is_verified": false, + "line_number": 46 + }, + { + "type": "Hex High Entropy String", + "filename": "packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts", + "hashed_secret": "769703a72cded41716f0bb63f41ba5594f1f9459", + "is_verified": false, + "line_number": 47 + }, + { + "type": "Hex High Entropy String", + "filename": "packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts", + "hashed_secret": "549cfabd06bf6e6628429cb26091a7fb31658135", + "is_verified": false, + "line_number": 54 + }, + { + "type": "Hex High Entropy String", + "filename": "packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts", + "hashed_secret": "a3c0c29cee36901e4791679ae22d7259900561a4", + "is_verified": false, + "line_number": 55 + }, + { + "type": "Hex High Entropy String", + "filename": "packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts", + "hashed_secret": "4b579b62045b402109810e56d7e35dbb5047df7a", + "is_verified": false, + "line_number": 339 + }, + { + "type": "Hex High Entropy String", + "filename": "packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts", + "hashed_secret": "638d131f1034c3a24b1b8b6829b7c765c85f568b", + "is_verified": false, + "line_number": 340 + } + ], + "packages/cachekit/test/protocol/wire-format.protocol.test.ts": [ + { + "type": "Hex High Entropy String", + "filename": "packages/cachekit/test/protocol/wire-format.protocol.test.ts", + "hashed_secret": "94a0e90cfabf50717f7f969ff1b8d5335f2b0ed2", + "is_verified": false, + "line_number": 135 + }, + { + "type": "Hex High Entropy String", + "filename": "packages/cachekit/test/protocol/wire-format.protocol.test.ts", + "hashed_secret": "433223517d64226aefd32d9b2e6bebaa8599bddd", + "is_verified": false, + "line_number": 137 + } + ] + }, + "generated_at": "2026-05-17T00:20:23Z" +} diff --git a/README.md b/README.md index 3f03081..690ec71 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ TypeScript SDK for CacheKit - Production-ready Redis caching with L1 in-memory, ## Packages -| Package | Description | -|---------|-------------| -| [@cachekit-io/cachekit](./packages/cachekit) | Main SDK | +| Package | Description | +| ------------------------------------------------------------ | ----------------------- | +| [@cachekit-io/cachekit](./packages/cachekit) | Main SDK | | [@cachekit-io/cachekit-core-ts](./packages/cachekit-core-ts) | Native bindings (N-API) | ## Development diff --git a/SECURITY.md b/SECURITY.md index 38a45b5..241a74e 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,7 +11,7 @@ Instead, use [GitHub's private vulnerability reporting](https://github.com/cache ## Supported Versions | Version | Supported | -|---------|-----------| +| ------- | --------- | | 0.x | Yes | ## Scope diff --git a/packages/cachekit-core-ts/CHANGELOG.md b/packages/cachekit-core-ts/CHANGELOG.md index 7c1743a..175f5b6 100644 --- a/packages/cachekit-core-ts/CHANGELOG.md +++ b/packages/cachekit-core-ts/CHANGELOG.md @@ -2,9 +2,8 @@ ## [0.1.1](https://github.com/cachekit-io/cachekit-ts/compare/cachekit-core-ts-v0.1.0...cachekit-core-ts-v0.1.1) (2026-04-26) - ### Features -* CachekitIO backend full parity (session, metrics, SSRF, locking, TTL) ([d408364](https://github.com/cachekit-io/cachekit-ts/commit/d408364a424a24f191632cc297519d1f951fb069)) -* initial commit ([048585c](https://github.com/cachekit-io/cachekit-ts/commit/048585cb5e8934567a518b220337a4d10b48f83d)) -* wire ByteStorage into cache pipeline for protocol-compliant wire format ([#27](https://github.com/cachekit-io/cachekit-ts/issues/27)) ([d246294](https://github.com/cachekit-io/cachekit-ts/commit/d246294471967a49c4161a9f05f0232e84bf6c54)) +- CachekitIO backend full parity (session, metrics, SSRF, locking, TTL) ([d408364](https://github.com/cachekit-io/cachekit-ts/commit/d408364a424a24f191632cc297519d1f951fb069)) +- initial commit ([048585c](https://github.com/cachekit-io/cachekit-ts/commit/048585cb5e8934567a518b220337a4d10b48f83d)) +- wire ByteStorage into cache pipeline for protocol-compliant wire format ([#27](https://github.com/cachekit-io/cachekit-ts/issues/27)) ([d246294](https://github.com/cachekit-io/cachekit-ts/commit/d246294471967a49c4161a9f05f0232e84bf6c54)) diff --git a/packages/cachekit-core-ts/src/lib.rs b/packages/cachekit-core-ts/src/lib.rs index b5a5655..e94586c 100644 --- a/packages/cachekit-core-ts/src/lib.rs +++ b/packages/cachekit-core-ts/src/lib.rs @@ -3,12 +3,11 @@ use napi::bindgen_prelude::*; use napi_derive::napi; -use cachekit_core::ByteStorage as CoreByteStorage; -use cachekit_core::encryption::{ZeroKnowledgeEncryptor, derive_domain_key}; use cachekit_core::encryption::key_derivation::{ - TenantKeys as CoreTenantKeys, - derive_tenant_keys as core_derive_tenant_keys, + derive_tenant_keys as core_derive_tenant_keys, TenantKeys as CoreTenantKeys, }; +use cachekit_core::encryption::{derive_domain_key, ZeroKnowledgeEncryptor}; +use cachekit_core::ByteStorage as CoreByteStorage; // Security limits to prevent DoS const MAX_PLAINTEXT_SIZE: usize = 100 * 1024 * 1024; // 100 MB @@ -20,7 +19,10 @@ fn validate_encryption_input(plaintext_len: usize, aad_len: usize) -> Result<()> if plaintext_len > MAX_PLAINTEXT_SIZE { return Err(Error::new( Status::InvalidArg, - format!("Plaintext exceeds maximum size of {} bytes", MAX_PLAINTEXT_SIZE), + format!( + "Plaintext exceeds maximum size of {} bytes", + MAX_PLAINTEXT_SIZE + ), )); } if aad_len > MAX_AAD_SIZE { @@ -37,7 +39,10 @@ fn validate_decryption_input(ciphertext_len: usize, aad_len: usize) -> Result<() if ciphertext_len > MAX_CIPHERTEXT_SIZE { return Err(Error::new( Status::InvalidArg, - format!("Ciphertext exceeds maximum size of {} bytes", MAX_CIPHERTEXT_SIZE), + format!( + "Ciphertext exceeds maximum size of {} bytes", + MAX_CIPHERTEXT_SIZE + ), )); } if aad_len > MAX_AAD_SIZE { @@ -63,6 +68,12 @@ pub struct ByteStorage { inner: CoreByteStorage, } +impl Default for ByteStorage { + fn default() -> Self { + Self::new() + } +} + #[napi] impl ByteStorage { /// Create a new ByteStorage instance. @@ -189,7 +200,9 @@ pub fn derive_key( )); } - let key_arr: [u8; 32] = master_key.as_ref().try_into() + let key_arr: [u8; 32] = master_key + .as_ref() + .try_into() .map_err(|_| Error::new(Status::InvalidArg, "Master key must be 32 bytes"))?; derive_domain_key(&key_arr, &domain, tenant_salt.as_bytes()) @@ -270,15 +283,15 @@ pub fn derive_tenant_keys(master_key: Uint8Array, tenant_id: String) -> Result Result { validate_encryption_input(plaintext.len(), aad.len())?; - tenant_keys.encryptor + tenant_keys + .encryptor .encrypt_aes_gcm(&plaintext, &tenant_keys.inner.encryption_key, &aad) .map(|ciphertext| ciphertext.into()) .map_err(|e| Error::new(Status::GenericFailure, e.to_string())) @@ -336,7 +350,8 @@ pub fn decrypt_with_tenant_keys( ) -> Result { validate_decryption_input(ciphertext.len(), aad.len())?; - tenant_keys.encryptor + tenant_keys + .encryptor .decrypt_aes_gcm(&ciphertext, &tenant_keys.inner.encryption_key, &aad) .map(|plaintext| plaintext.into()) .map_err(|e| Error::new(Status::GenericFailure, e.to_string())) diff --git a/packages/cachekit/CHANGELOG.md b/packages/cachekit/CHANGELOG.md index bf79d03..b32bab6 100644 --- a/packages/cachekit/CHANGELOG.md +++ b/packages/cachekit/CHANGELOG.md @@ -2,11 +2,10 @@ ## [0.1.1](https://github.com/cachekit-io/cachekit-ts/compare/cachekit-v0.1.0...cachekit-v0.1.1) (2026-04-26) - ### Features -* CachekitIO backend full parity — session, metrics, SSRF, errors, locking, TTL ([985cf09](https://github.com/cachekit-io/cachekit-ts/commit/985cf09bf1fd5cd12975bd0e504997b9eb9b8fd2)) -* CachekitIO backend full parity (session, metrics, SSRF, locking, TTL) ([d408364](https://github.com/cachekit-io/cachekit-ts/commit/d408364a424a24f191632cc297519d1f951fb069)) -* initial commit ([048585c](https://github.com/cachekit-io/cachekit-ts/commit/048585cb5e8934567a518b220337a4d10b48f83d)) -* intent-based cache API (createCache.io, .minimal, .production, .secure) ([#42](https://github.com/cachekit-io/cachekit-ts/issues/42)) ([c551bfb](https://github.com/cachekit-io/cachekit-ts/commit/c551bfb75bf644a06a9c34eaa338c4980358a74a)) -* wire ByteStorage into cache pipeline for protocol-compliant wire format ([#27](https://github.com/cachekit-io/cachekit-ts/issues/27)) ([d246294](https://github.com/cachekit-io/cachekit-ts/commit/d246294471967a49c4161a9f05f0232e84bf6c54)) +- CachekitIO backend full parity — session, metrics, SSRF, errors, locking, TTL ([985cf09](https://github.com/cachekit-io/cachekit-ts/commit/985cf09bf1fd5cd12975bd0e504997b9eb9b8fd2)) +- CachekitIO backend full parity (session, metrics, SSRF, locking, TTL) ([d408364](https://github.com/cachekit-io/cachekit-ts/commit/d408364a424a24f191632cc297519d1f951fb069)) +- initial commit ([048585c](https://github.com/cachekit-io/cachekit-ts/commit/048585cb5e8934567a518b220337a4d10b48f83d)) +- intent-based cache API (createCache.io, .minimal, .production, .secure) ([#42](https://github.com/cachekit-io/cachekit-ts/issues/42)) ([c551bfb](https://github.com/cachekit-io/cachekit-ts/commit/c551bfb75bf644a06a9c34eaa338c4980358a74a)) +- wire ByteStorage into cache pipeline for protocol-compliant wire format ([#27](https://github.com/cachekit-io/cachekit-ts/issues/27)) ([d246294](https://github.com/cachekit-io/cachekit-ts/commit/d246294471967a49c4161a9f05f0232e84bf6c54)) diff --git a/packages/cachekit/README.md b/packages/cachekit/README.md index 4fa7fd4..e354805 100644 --- a/packages/cachekit/README.md +++ b/packages/cachekit/README.md @@ -80,12 +80,12 @@ const managed = createCache.io({ Each intent pre-configures the full stack with sensible defaults: -| Intent | Backend | Circuit Breaker | Retry | L1 SWR | Encryption | Default TTL | -|--------|---------|----------------|-------|--------|------------|-------------| -| `minimal` | Redis | Off | Off | Off | No | 300s | -| `production` | Redis | On (threshold: 5) | On | On | No | 600s | -| `secure` | Redis | On (threshold: 5) | On | On | AES-256-GCM | 600s | -| `io` | cachekit.io | On (threshold: 5) | On | On | Optional | 3600s | +| Intent | Backend | Circuit Breaker | Retry | L1 SWR | Encryption | Default TTL | +| ------------ | ----------- | ----------------- | ----- | ------ | ----------- | ----------- | +| `minimal` | Redis | Off | Off | Off | No | 300s | +| `production` | Redis | On (threshold: 5) | On | On | No | 600s | +| `secure` | Redis | On (threshold: 5) | On | On | AES-256-GCM | 600s | +| `io` | cachekit.io | On (threshold: 5) | On | On | Optional | 3600s | All defaults are overridable — pass `reliability`, `l1`, or `metrics` to customize. @@ -158,10 +158,10 @@ Delete a key. Returns `true` if existed. Wrap an async function with caching. ```typescript -const cachedFn = cache.wrap( - async (id: number) => fetchData(id), - { namespace: 'api:getData', ttl: 300 } -); +const cachedFn = cache.wrap(async (id: number) => fetchData(id), { + namespace: 'api:getData', + ttl: 300, +}); ``` ### cache.invalidate(level, options?) diff --git a/packages/cachekit/src/index.ts b/packages/cachekit/src/index.ts index 7860d69..3cfad50 100644 --- a/packages/cachekit/src/index.ts +++ b/packages/cachekit/src/index.ts @@ -1,6 +1,12 @@ // ============ Main API ============ export { createCache } from './intents.js'; -export type { CreateCacheFn, MinimalOptions, ProductionOptions, SecureOptions, IOOptions } from './intents.js'; +export type { + CreateCacheFn, + MinimalOptions, + ProductionOptions, + SecureOptions, + IOOptions, +} from './intents.js'; export { redis } from './backends/redis.js'; export { cachekitio, diff --git a/packages/cachekit/src/intents.test.ts b/packages/cachekit/src/intents.test.ts index 8273ff2..4d326da 100644 --- a/packages/cachekit/src/intents.test.ts +++ b/packages/cachekit/src/intents.test.ts @@ -206,9 +206,11 @@ describe('Intent-based Cache API', () => { createCache.io({ apiKey: 'ck_live_test123' }); expect(capturedOptions).not.toBeNull(); - expect(capturedOptions!.backend).toEqual(expect.objectContaining({ - apiKey: 'ck_live_test123', - })); + expect(capturedOptions!.backend).toEqual( + expect.objectContaining({ + apiKey: 'ck_live_test123', + }) + ); expect(capturedOptions!.defaultTtl).toBe(3600); }); @@ -219,11 +221,13 @@ describe('Intent-based Cache API', () => { timeout: 10000, }); - expect(capturedOptions!.backend).toEqual(expect.objectContaining({ - apiKey: 'ck_live_test123', - apiUrl: 'https://custom.endpoint.io', - timeout: 10000, - })); + expect(capturedOptions!.backend).toEqual( + expect.objectContaining({ + apiKey: 'ck_live_test123', + apiUrl: 'https://custom.endpoint.io', + timeout: 10000, + }) + ); }); it('enables production-grade reliability', () => { @@ -239,9 +243,11 @@ describe('Intent-based Cache API', () => { createCache.io({}); - expect(capturedOptions!.backend).toEqual(expect.objectContaining({ - apiKey: 'ck_live_from_env', - })); + expect(capturedOptions!.backend).toEqual( + expect.objectContaining({ + apiKey: 'ck_live_from_env', + }) + ); }); it('throws ConfigurationError without apiKey', () => { diff --git a/packages/cachekit/src/intents.ts b/packages/cachekit/src/intents.ts index 45d0c99..82359f2 100644 --- a/packages/cachekit/src/intents.ts +++ b/packages/cachekit/src/intents.ts @@ -20,7 +20,13 @@ * ``` */ -import type { CacheOptions, SecureCache, ReliabilityConfig, EncryptionConfig, InvalidationConfig } from './types/cache.js'; +import type { + CacheOptions, + SecureCache, + ReliabilityConfig, + EncryptionConfig, + InvalidationConfig, +} from './types/cache.js'; import type { L1Config } from './l1/types.js'; import type { SerializerConfig } from './serialization/serializer.js'; import { createCache as _createCache } from './cache.js'; @@ -221,7 +227,7 @@ function createSecure(options: SecureOptions): SecureCache { if (!masterKey) { throw new ConfigurationError( 'createCache.secure() requires a master key. ' + - 'Provide masterKey in options or set CACHEKIT_MASTER_KEY environment variable.', + 'Provide masterKey in options or set CACHEKIT_MASTER_KEY environment variable.' ); } @@ -256,7 +262,7 @@ function createIO(options: IOOptions): SecureCache { if (!apiKey) { throw new ConfigurationError( 'createCache.io() requires an API key. ' + - 'Provide apiKey in options or set CACHEKIT_API_KEY environment variable.', + 'Provide apiKey in options or set CACHEKIT_API_KEY environment variable.' ); } @@ -302,16 +308,14 @@ export const createCache = Object.assign(_createCache, { function mergeReliability( defaults: ReliabilityConfig, - overrides?: Partial, + overrides?: Partial ): ReliabilityConfig { if (!overrides) return defaults; return { circuitBreaker: overrides.circuitBreaker ? { ...defaults.circuitBreaker, ...overrides.circuitBreaker } : defaults.circuitBreaker, - retry: overrides.retry - ? { ...defaults.retry, ...overrides.retry } - : defaults.retry, + retry: overrides.retry ? { ...defaults.retry, ...overrides.retry } : defaults.retry, degradation: overrides.degradation ?? defaults.degradation, }; } diff --git a/packages/cachekit/test/integration/encryption-real-crypto.integration.test.ts b/packages/cachekit/test/integration/encryption-real-crypto.integration.test.ts index b60c714..f492e07 100644 --- a/packages/cachekit/test/integration/encryption-real-crypto.integration.test.ts +++ b/packages/cachekit/test/integration/encryption-real-crypto.integration.test.ts @@ -202,7 +202,7 @@ describe('Real Crypto Integration (No Mocks)', () => { describe('Ciphertext structure (AES-GCM format)', () => { it('produces ciphertext with [nonce(12)][tag(16)][encrypted_data] format', () => { - const plaintext = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]); + const plaintext = new Uint8Array([0xde, 0xad, 0xbe, 0xef]); const aad = buildAAD(TEST_TENANT_ID, 'structure-test'); const ciphertext = encryptWithTenantKeys(plaintext, aad, tenantKeys); diff --git a/packages/cachekit/test/integration/redis-backend.integration.test.ts b/packages/cachekit/test/integration/redis-backend.integration.test.ts index 2780e9a..061ea43 100644 --- a/packages/cachekit/test/integration/redis-backend.integration.test.ts +++ b/packages/cachekit/test/integration/redis-backend.integration.test.ts @@ -34,7 +34,7 @@ describe.skipIf(!dockerAvailable)('RedisBackend Integration (Testcontainers)', ( client = new Redis(redisUrl); backend = redis({ url: redisUrl, keyPrefix: testPrefix }); - }, 60000); // 60s timeout for container startup + }, 60000); // 60s timeout for container startup afterAll(async () => { // Cleanup diff --git a/packages/cachekit/test/protocol/aad-format.protocol.test.ts b/packages/cachekit/test/protocol/aad-format.protocol.test.ts index 390eaa2..0db245b 100644 --- a/packages/cachekit/test/protocol/aad-format.protocol.test.ts +++ b/packages/cachekit/test/protocol/aad-format.protocol.test.ts @@ -28,7 +28,7 @@ function buildAAD( encoder.encode(tenantId), encoder.encode(cacheKey), encoder.encode(format), - encoder.encode(compressed ? 'True' : 'False'), // Python str(bool) format + encoder.encode(compressed ? 'True' : 'False'), // Python str(bool) format ]; // Calculate total length: version byte + (4-byte length + data) for each component @@ -43,7 +43,7 @@ function buildAAD( // Each component: 4-byte big-endian length + data for (const component of components) { - view.setUint32(offset, component.length, false); // false = big-endian + view.setUint32(offset, component.length, false); // false = big-endian offset += 4; aad.set(component, offset); offset += component.length; @@ -70,7 +70,7 @@ function parseAAD(aad: Uint8Array): { const components: string[] = []; while (offset < aad.length) { - const length = view.getUint32(offset, false); // big-endian + const length = view.getUint32(offset, false); // big-endian offset += 4; const component = decoder.decode(aad.slice(offset, offset + length)); components.push(component); @@ -101,15 +101,15 @@ describe('AAD v0x03 Protocol Compatibility', () => { expect(parsed.tenantId).toBe('tenant-123'); expect(parsed.cacheKey).toBe('cache:users:42'); expect(parsed.format).toBe('msgpack'); - expect(parsed.compressed).toBe('False'); // Python str(False) format + expect(parsed.compressed).toBe('False'); // Python str(False) format }); it('uses Python str(bool) format for compressed', () => { const aadFalse = buildAAD('t', 'k', 'msgpack', false); const aadTrue = buildAAD('t', 'k', 'msgpack', true); - expect(parseAAD(aadFalse).compressed).toBe('False'); // Not 'false' - expect(parseAAD(aadTrue).compressed).toBe('True'); // Not 'true' + expect(parseAAD(aadFalse).compressed).toBe('False'); // Not 'false' + expect(parseAAD(aadTrue).compressed).toBe('True'); // Not 'true' }); it('uses 4-byte big-endian length prefixes', () => { @@ -117,10 +117,10 @@ describe('AAD v0x03 Protocol Compatibility', () => { const view = new DataView(aad.buffer); // After version byte (offset 0), first length at offset 1 - expect(view.getUint32(1, false)).toBe(2); // 'AB' = 2 bytes + expect(view.getUint32(1, false)).toBe(2); // 'AB' = 2 bytes // After 'AB' (offset 1 + 4 + 2 = 7), second length - expect(view.getUint32(7, false)).toBe(2); // 'CD' = 2 bytes + expect(view.getUint32(7, false)).toBe(2); // 'CD' = 2 bytes }); /** @@ -147,13 +147,11 @@ describe('AAD v0x03 Protocol Compatibility', () => { const aad = buildAAD('test', 'mykey', 'msgpack', false); // Expected bytes from Python (spaces removed for comparison) - const expectedHex = '03000000047465737400000005' + - '6d796b657900000007' + - '6d73677061636b00000005' + - '46616c7365'; + const expectedHex = + '03000000047465737400000005' + '6d796b657900000007' + '6d73677061636b00000005' + '46616c7365'; const actualHex = Array.from(aad) - .map(b => b.toString(16).padStart(2, '0')) + .map((b) => b.toString(16).padStart(2, '0')) .join(''); expect(actualHex).toBe(expectedHex); diff --git a/packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts b/packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts index a96e5bf..e0c2b4f 100644 --- a/packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts +++ b/packages/cachekit/test/protocol/cross-sdk-interop.protocol.test.ts @@ -78,7 +78,12 @@ function bytesToHex(bytes: Uint8Array): string { * * Format: [version_byte(0x03)][len1(4)][tenant_id][len2(4)][cache_key][len3(4)][format][len4(4)][compressed] */ -function buildAAD(tenantId: string, cacheKey: string, format = 'msgpack', compressed = false): Uint8Array { +function buildAAD( + tenantId: string, + cacheKey: string, + format = 'msgpack', + compressed = false +): Uint8Array { const encoder = new TextEncoder(); const components = [ @@ -211,7 +216,10 @@ describe('Cross-SDK Interoperability (Python <-> TypeScript)', () => { const aad = buildAAD(PYTHON_FIXTURES.tenantId, 'nonce:test'); // Fresh keys for deterministic nonce testing - const freshKeys = deriveTenantKeys(hexToBytes(PYTHON_FIXTURES.masterKeyHex), 'nonce-test-tenant'); + const freshKeys = deriveTenantKeys( + hexToBytes(PYTHON_FIXTURES.masterKeyHex), + 'nonce-test-tenant' + ); const ct1 = encryptWithTenantKeys(plaintext, aad, freshKeys); const ct2 = encryptWithTenantKeys(plaintext, aad, freshKeys); diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dee51e9..18ec407 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,2 @@ packages: - - "packages/*" + - 'packages/*' diff --git a/release-please-config.json b/release-please-config.json index f15e64f..7811d12 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,18 +4,18 @@ "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": true, "changelog-sections": [ - {"type": "feat", "section": "Features"}, - {"type": "fix", "section": "Bug Fixes"}, - {"type": "perf", "section": "Performance Improvements"}, - {"type": "security", "section": "Security"}, - {"type": "revert", "section": "Reverts"}, - {"type": "docs", "section": "Documentation", "hidden": true}, - {"type": "chore", "section": "Miscellaneous", "hidden": true}, - {"type": "refactor", "section": "Code Refactoring", "hidden": true}, - {"type": "style", "section": "Styles", "hidden": true}, - {"type": "test", "section": "Tests", "hidden": true}, - {"type": "ci", "section": "CI/CD", "hidden": true}, - {"type": "build", "section": "Build System", "hidden": true} + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "security", "section": "Security" }, + { "type": "revert", "section": "Reverts" }, + { "type": "docs", "section": "Documentation", "hidden": true }, + { "type": "chore", "section": "Miscellaneous", "hidden": true }, + { "type": "refactor", "section": "Code Refactoring", "hidden": true }, + { "type": "style", "section": "Styles", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true }, + { "type": "ci", "section": "CI/CD", "hidden": true }, + { "type": "build", "section": "Build System", "hidden": true } ], "packages": { "packages/cachekit": { diff --git a/tsconfig.json b/tsconfig.json index d56d59e..634b123 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,8 +18,5 @@ }, "compileOnSave": false, "include": [], - "references": [ - { "path": "./packages/cachekit" }, - { "path": "./packages/cachekit-core-ts" } - ] + "references": [{ "path": "./packages/cachekit" }, { "path": "./packages/cachekit-core-ts" }] } diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 6ff949c..92851d1 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -1,5 +1,3 @@ import { defineWorkspace } from 'vitest/config'; -export default defineWorkspace([ - 'packages/cachekit', -]); +export default defineWorkspace(['packages/cachekit']); From ec4965a25b4d205e8f441231179a7f8e043e7a93 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 17 May 2026 10:47:21 +1000 Subject: [PATCH 2/2] chore: bump dev Node to 22 in .nvmrc and root engines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Node 20 (Iron LTS) reached end-of-life on 2026-04-30. CI already runs Node 22 (and matrix [22, 24]); both publishable packages declare engines.node ">=22.0.0". The .nvmrc and root engines field were the last places lagging at 20 — fixed for consistency so fresh clones via nvm use land on a supported runtime that actually satisfies the per-package engines constraint. Addresses CodeRabbit review on PR #47. --- .nvmrc | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.nvmrc b/.nvmrc index 209e3ef..2bd5a0a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -20 +22 diff --git a/package.json b/package.json index 70b7d8d..015ac70 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "description": "CacheKit TypeScript SDK monorepo", "license": "MIT", "engines": { - "node": ">=20.0.0", + "node": ">=22.0.0", "pnpm": ">=8.0.0" }, "packageManager": "pnpm@8.15.0",