From 1d766f6d3ad8fc1c1f91bbc24d95ef4c2baa704b Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 4 Oct 2026 16:08:27 +1100 Subject: [PATCH 1/4] ci(context7): scope the index, add agent rules, refresh on release (LAB-8004) context7.json excludes tests, fuzz, benches and internal tooling, and gives coding agents rules: master key from the environment or a secret manager, exact package names, and the CacheKit Cloud naming. The new workflow asks Context7 to re-index when a release is published or on manual dispatch. It needs the CONTEXT7_API_KEY Actions secret. The README and crate-level encryption example no longer use a literal all-zero master key. --- .github/workflows/context7-refresh.yml | 43 ++++++++++++++++++++++++++ README.md | 3 +- context7.json | 13 ++++++++ src/lib.rs | 3 +- 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/context7-refresh.yml create mode 100644 context7.json diff --git a/.github/workflows/context7-refresh.yml b/.github/workflows/context7-refresh.yml new file mode 100644 index 0000000..9d15697 --- /dev/null +++ b/.github/workflows/context7-refresh.yml @@ -0,0 +1,43 @@ +# Context7 serves this repo's docs to coding agents, and re-indexes on its own +# schedule, which can lag a release by weeks. This asks it to re-index when a +# release is published, so agents read the docs for the version users install. +# Scope and agent rules live in context7.json. +# Endpoint: https://context7.com/docs/integrations/github-actions +name: Context7 Refresh + +on: + release: + types: [published] + workflow_dispatch: + +permissions: {} + +# A release run can publish several releases at once (one per package). Queue +# them, so at most one refresh is in flight. +concurrency: + group: context7-refresh + +jobs: + refresh: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Ask Context7 to re-index /cachekit-io/cachekit-core + env: + CONTEXT7_API_KEY: ${{ secrets.CONTEXT7_API_KEY }} + run: | + if [ -z "$CONTEXT7_API_KEY" ]; then + echo "::error::The CONTEXT7_API_KEY secret is not available to this repository." + exit 1 + fi + status=$(curl -sS --max-time 60 -o "$RUNNER_TEMP/context7-response.json" -w '%{http_code}' \ + -X POST https://context7.com/api/v1/refresh \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $CONTEXT7_API_KEY" \ + --data '{"libraryName": "/cachekit-io/cachekit-core"}') + cat "$RUNNER_TEMP/context7-response.json" + echo + case "$status" in + 2??) echo "Context7 accepted the refresh (HTTP $status)." ;; + *) echo "::error::Context7 refresh failed with HTTP $status."; exit 1 ;; + esac diff --git a/README.md b/README.md index 4bab124..a16040d 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ assert_eq!(data.as_slice(), retrieved.as_slice()); use cachekit_core::{ByteStorage, ZeroKnowledgeEncryptor, derive_domain_key}; // Derive tenant-isolated key from master secret -let master_key = [0u8; 32]; // Use secure key in production! +// 32-byte master key from your secret manager. Never hard-code it. +let master_key: [u8; 32] = load_master_key_from_secret_manager()?; let tenant_key = derive_domain_key( &master_key, "cache", // domain separation diff --git a/context7.json b/context7.json new file mode 100644 index 0000000..19476f1 --- /dev/null +++ b/context7.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://context7.com/schema/context7.json", + "folders": [], + "excludeFolders": [".github", "benches", "fuzz", "scripts", "supply-chain", "tests"], + "excludeFiles": ["CHANGELOG.md"], + "rules": [ + "Read the CacheKit master key from the CACHEKIT_MASTER_KEY environment variable or a secret manager. Never hard-code a key in source, not even a placeholder such as [0u8; 32].", + "`CACHEKIT_MASTER_KEY` holds the key as a hex string. Use exactly 32 bytes (64 hex characters), for example from `openssl rand -hex 32`: it is the only length every CacheKit SDK accepts.", + "Install the official packages: `cachekit` on PyPI, `@cachekit-io/cachekit` on npm, and `cachekit-rs` on crates.io (imported as `cachekit`). The crates.io crate named `cachekit` is an unrelated project.", + "The hosted backend is CacheKit Cloud. In code it is named `CachekitIO` (in Python, `CachekitIOBackend`).", + "Applications normally use a CacheKit SDK. cachekit-core is the shared byte-storage and encryption layer underneath them." + ] +} diff --git a/src/lib.rs b/src/lib.rs index 93a3e43..3e743b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,8 @@ //! use cachekit_core::{ZeroKnowledgeEncryptor, derive_domain_key}; //! //! // Derive tenant-isolated key -//! let master_key = [0u8; 32]; // Use secure key in production! +//! // 32-byte master key from your secret manager. Never hard-code it. +//! let master_key: [u8; 32] = load_master_key_from_secret_manager(); //! let tenant_key = derive_domain_key(&master_key, "cache", b"tenant-123").unwrap(); //! //! // Encrypt From 6225d28198b47dd2d6cb5b6e1d19a31bb2c791dc Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 4 Oct 2026 16:28:25 +1100 Subject: [PATCH 2/4] ci(context7): correct agent rules, warn on HTTP 202 (LAB-8004) Name each SDK's CacheKit Cloud identifier, since TypeScript exports no CachekitIO. Treat HTTP 202 (library not finalized) as a warning to re-run, not success. Drop the default empty folders list. --- .github/workflows/context7-refresh.yml | 6 +----- README.md | 3 ++- context7.json | 17 ++++++++++++----- src/lib.rs | 3 ++- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.github/workflows/context7-refresh.yml b/.github/workflows/context7-refresh.yml index 9d15697..6de577d 100644 --- a/.github/workflows/context7-refresh.yml +++ b/.github/workflows/context7-refresh.yml @@ -12,11 +12,6 @@ on: permissions: {} -# A release run can publish several releases at once (one per package). Queue -# them, so at most one refresh is in flight. -concurrency: - group: context7-refresh - jobs: refresh: runs-on: ubuntu-latest @@ -38,6 +33,7 @@ jobs: cat "$RUNNER_TEMP/context7-response.json" echo case "$status" in + 202) echo "::warning::Context7 has not finalized this library yet (HTTP 202). Re-run this workflow later." ;; 2??) echo "Context7 accepted the refresh (HTTP $status)." ;; *) echo "::error::Context7 refresh failed with HTTP $status."; exit 1 ;; esac diff --git a/README.md b/README.md index a16040d..92f5f35 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,8 @@ assert_eq!(data.as_slice(), retrieved.as_slice()); use cachekit_core::{ByteStorage, ZeroKnowledgeEncryptor, derive_domain_key}; // Derive tenant-isolated key from master secret -// 32-byte master key from your secret manager. Never hard-code it. +// From your secret manager or CACHEKIT_MASTER_KEY, hex-decoded to 32 raw bytes. +// Never hard-code it, and never pass the hex string's bytes. let master_key: [u8; 32] = load_master_key_from_secret_manager()?; let tenant_key = derive_domain_key( &master_key, diff --git a/context7.json b/context7.json index 19476f1..a405303 100644 --- a/context7.json +++ b/context7.json @@ -1,13 +1,20 @@ { "$schema": "https://context7.com/schema/context7.json", - "folders": [], - "excludeFolders": [".github", "benches", "fuzz", "scripts", "supply-chain", "tests"], - "excludeFiles": ["CHANGELOG.md"], + "excludeFolders": [ + ".github", + "benches", + "fuzz", + "scripts", + "supply-chain", + "tests" + ], + "excludeFiles": [ + "CHANGELOG.md" + ], "rules": [ "Read the CacheKit master key from the CACHEKIT_MASTER_KEY environment variable or a secret manager. Never hard-code a key in source, not even a placeholder such as [0u8; 32].", - "`CACHEKIT_MASTER_KEY` holds the key as a hex string. Use exactly 32 bytes (64 hex characters), for example from `openssl rand -hex 32`: it is the only length every CacheKit SDK accepts.", + "cachekit-core reads no environment variable. Hex-decode the key (for example from `CACHEKIT_MASTER_KEY` or a secret manager) to exactly 32 raw bytes before `derive_domain_key`. Never pass the hex string's bytes: that derives a key no CacheKit SDK derives, so cross-SDK decryption fails.", "Install the official packages: `cachekit` on PyPI, `@cachekit-io/cachekit` on npm, and `cachekit-rs` on crates.io (imported as `cachekit`). The crates.io crate named `cachekit` is an unrelated project.", - "The hosted backend is CacheKit Cloud. In code it is named `CachekitIO` (in Python, `CachekitIOBackend`).", "Applications normally use a CacheKit SDK. cachekit-core is the shared byte-storage and encryption layer underneath them." ] } diff --git a/src/lib.rs b/src/lib.rs index 3e743b1..bbc48c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,8 @@ //! use cachekit_core::{ZeroKnowledgeEncryptor, derive_domain_key}; //! //! // Derive tenant-isolated key -//! // 32-byte master key from your secret manager. Never hard-code it. +//! // From your secret manager or CACHEKIT_MASTER_KEY, hex-decoded to 32 raw bytes. +//! // Never hard-code it, and never pass the hex string's bytes. //! let master_key: [u8; 32] = load_master_key_from_secret_manager(); //! let tenant_key = derive_domain_key(&master_key, "cache", b"tenant-123").unwrap(); //! From c10b5c1e0c89d2a466d62a170a985a42ae51b2d7 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 4 Oct 2026 16:58:09 +1100 Subject: [PATCH 3/4] docs: zeroize keys in the encryption quick start (LAB-8004) derive_domain_key borrows the master key and returns the derived key as a plain [u8; 32], so neither is cleared when the example drops it. Wrap both in zeroize::Zeroizing, matching the crate's own key handling. ZeroKnowledgeEncryptor::new() returns a Result, so the README and crate-doc examples called encrypt_aes_gcm on a Result and did not compile. Unwrap it in all three examples. --- README.md | 18 ++++++++++-------- src/lib.rs | 12 +++++++----- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 92f5f35..f1138f7 100644 --- a/README.md +++ b/README.md @@ -78,26 +78,28 @@ assert_eq!(data.as_slice(), retrieved.as_slice()); ```rust use cachekit_core::{ByteStorage, ZeroKnowledgeEncryptor, derive_domain_key}; +use zeroize::Zeroizing; // add the zeroize crate to your Cargo.toml // Derive tenant-isolated key from master secret // From your secret manager or CACHEKIT_MASTER_KEY, hex-decoded to 32 raw bytes. // Never hard-code it, and never pass the hex string's bytes. -let master_key: [u8; 32] = load_master_key_from_secret_manager()?; -let tenant_key = derive_domain_key( - &master_key, +// Zeroizing wipes each key from memory when it is dropped. +let master_key = Zeroizing::new(load_master_key_from_secret_manager()?); +let tenant_key = Zeroizing::new(derive_domain_key( + master_key.as_slice(), "cache", // domain separation b"tenant-12345", // tenant isolation -)?; +)?); // Encrypt sensitive data -let encryptor = ZeroKnowledgeEncryptor::new(); +let encryptor = ZeroKnowledgeEncryptor::new()?; let plaintext = b"sensitive user data"; let aad = b"tenant-12345"; // Additional authenticated data -let ciphertext = encryptor.encrypt_aes_gcm(plaintext, &tenant_key, aad)?; +let ciphertext = encryptor.encrypt_aes_gcm(plaintext, tenant_key.as_slice(), aad)?; // Decrypt (fails if AAD doesn't match) -let decrypted = encryptor.decrypt_aes_gcm(&ciphertext, &tenant_key, aad)?; +let decrypted = encryptor.decrypt_aes_gcm(&ciphertext, tenant_key.as_slice(), aad)?; assert_eq!(plaintext.as_slice(), decrypted.as_slice()); ``` @@ -123,7 +125,7 @@ fn cache_sensitive_data( let tenant_key = derive_domain_key(master_key, "cache", tenant_id.as_bytes())?; // Step 3: Encrypt compressed envelope - let encryptor = ZeroKnowledgeEncryptor::new(); + let encryptor = ZeroKnowledgeEncryptor::new()?; let ciphertext = encryptor.encrypt_aes_gcm( &compressed, &tenant_key, diff --git a/src/lib.rs b/src/lib.rs index bbc48c2..4f689b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,19 +40,21 @@ //! //! ```rust,ignore //! use cachekit_core::{ZeroKnowledgeEncryptor, derive_domain_key}; +//! use zeroize::Zeroizing; // add the zeroize crate to your Cargo.toml //! //! // Derive tenant-isolated key //! // From your secret manager or CACHEKIT_MASTER_KEY, hex-decoded to 32 raw bytes. //! // Never hard-code it, and never pass the hex string's bytes. -//! let master_key: [u8; 32] = load_master_key_from_secret_manager(); -//! let tenant_key = derive_domain_key(&master_key, "cache", b"tenant-123").unwrap(); +//! // Zeroizing wipes each key from memory when it is dropped. +//! let master_key = Zeroizing::new(load_master_key_from_secret_manager()); +//! let tenant_key = Zeroizing::new(derive_domain_key(master_key.as_slice(), "cache", b"tenant-123").unwrap()); //! //! // Encrypt -//! let encryptor = ZeroKnowledgeEncryptor::new(); -//! let ciphertext = encryptor.encrypt_aes_gcm(b"secret", &tenant_key, b"tenant-123").unwrap(); +//! let encryptor = ZeroKnowledgeEncryptor::new().unwrap(); +//! let ciphertext = encryptor.encrypt_aes_gcm(b"secret", tenant_key.as_slice(), b"tenant-123").unwrap(); //! //! // Decrypt -//! let plaintext = encryptor.decrypt_aes_gcm(&ciphertext, &tenant_key, b"tenant-123").unwrap(); +//! let plaintext = encryptor.decrypt_aes_gcm(&ciphertext, tenant_key.as_slice(), b"tenant-123").unwrap(); //! ``` //! //! ## Security Properties From 0008518a0d445cc3edc06cecec7fbbe906fb90e3 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Sun, 4 Oct 2026 20:17:27 +1100 Subject: [PATCH 4/4] docs(lib): propagate errors in the encryption quick start (LAB-8004) The crate-level encryption example unwrapped every fallible call, so a short key from a secret manager panicked. It now runs in a Result-returning main and uses ?, matching the README quick start. --- src/lib.rs | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4f689b0..5738f39 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,19 +42,23 @@ //! use cachekit_core::{ZeroKnowledgeEncryptor, derive_domain_key}; //! use zeroize::Zeroizing; // add the zeroize crate to your Cargo.toml //! -//! // Derive tenant-isolated key -//! // From your secret manager or CACHEKIT_MASTER_KEY, hex-decoded to 32 raw bytes. -//! // Never hard-code it, and never pass the hex string's bytes. -//! // Zeroizing wipes each key from memory when it is dropped. -//! let master_key = Zeroizing::new(load_master_key_from_secret_manager()); -//! let tenant_key = Zeroizing::new(derive_domain_key(master_key.as_slice(), "cache", b"tenant-123").unwrap()); -//! -//! // Encrypt -//! let encryptor = ZeroKnowledgeEncryptor::new().unwrap(); -//! let ciphertext = encryptor.encrypt_aes_gcm(b"secret", tenant_key.as_slice(), b"tenant-123").unwrap(); -//! -//! // Decrypt -//! let plaintext = encryptor.decrypt_aes_gcm(&ciphertext, tenant_key.as_slice(), b"tenant-123").unwrap(); +//! fn main() -> Result<(), Box> { +//! // Derive tenant-isolated key +//! // From your secret manager or CACHEKIT_MASTER_KEY, hex-decoded to 32 raw bytes. +//! // Never hard-code it, and never pass the hex string's bytes. +//! // Zeroizing wipes each key from memory when it is dropped. +//! let master_key = Zeroizing::new(load_master_key_from_secret_manager()?); +//! let tenant_key = Zeroizing::new(derive_domain_key(master_key.as_slice(), "cache", b"tenant-123")?); +//! +//! // Encrypt +//! let encryptor = ZeroKnowledgeEncryptor::new()?; +//! let ciphertext = encryptor.encrypt_aes_gcm(b"secret", tenant_key.as_slice(), b"tenant-123")?; +//! +//! // Decrypt +//! let plaintext = encryptor.decrypt_aes_gcm(&ciphertext, tenant_key.as_slice(), b"tenant-123")?; +//! assert_eq!(plaintext, b"secret"); +//! Ok(()) +//! } //! ``` //! //! ## Security Properties