Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: tenant filtering on redis and s3 - #150

Merged
RohinBhargava merged 4 commits into
mainfrom
rohin/infra-tenant-filtering
Apr 7, 2026
Merged

feat: tenant filtering on redis and s3#150
RohinBhargava merged 4 commits into
mainfrom
rohin/infra-tenant-filtering

Conversation

@RohinBhargava

@RohinBhargava RohinBhargava commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Per-operation compliance context for tenant-scoped reads/writes across caching and object storage.
  • Refactor

    • Encryption controls moved from global setup to per-operation compliance, enabling finer-grained tenant-aware encryption.
  • Bug Fixes

    • Decryption now errors explicitly when encryptor is missing; cache reads guard against malformed subscription/plan data.
  • Chores

    • Widespread package and changelog version bumps across multiple packages.

@coderabbitai

coderabbitai Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Per-operation tenant-scoped compliance context (ComplianceContext with tenantId) was added across cache and object-store interfaces and implementations; Redis and S3 now conditionally encrypt/decrypt when a compliance context is provided. Blueprint services (auth, billing) were updated to pass tenantId into cache reads/writes and billing plan APIs changed to accept organizationId + planId. EncryptedType now errors if decryptor missing.

Changes

Cohort / File(s) Summary
Compliance types
framework/core/src/cache/types/ttlCacheRecord.types.ts
Added exported ComplianceContext (tenantId: string).
Core cache interface
framework/core/src/cache/interfaces/ttlCache.interface.ts
Added optional compliance?: ComplianceContext to read/write/queue methods (put/read/enqueue/dequeue/peek, batch variants).
Core objectstore interface
framework/core/src/objectstore/interfaces/objectstore.interface.ts
Added optional compliance?: ComplianceContext to put/read/stream-upload methods; doc updates about encryption for body vs streaming ops.
Redis implementation
framework/infrastructure/redis/index.ts, framework/infrastructure/redis/__test__/redisTtlCache.test.ts
Refactored to per-operation compliance-driven encryption; removed global disabled/tenant state; added optional compliance param across methods; tests updated to drop explicit disabled flag.
S3 implementation
framework/infrastructure/S3/index.ts, framework/infrastructure/S3/__test__/s3ObjectStore.test.ts
Removed global encryption-disable option; added optional compliance param to put/read/stream-upload methods; encryption only when compliance passed; test options updated.
Persistence encryption
framework/core/src/persistence/encryptedType.ts
EncryptedType.convertToJSValue() now throws if value is encrypted but no encryptor registered; captures tenantId once and passes to decrypt.
Blueprint: auth cache
blueprint/core/auth/cache.ts
Cache reads/writes for roles/permissions/org-roles now pass { tenantId } into cache.readRecord/cache.putRecord.
Blueprint: billing cache
blueprint/core/billing/cache.ts
getCachedPlan/setCachedPlan signatures now require organizationId + planId; cache keys include org; all cache reads/writes pass { tenantId: organizationId }; subscription read guards added.
Stripe types and DTOs
blueprint/implementations/billing/stripe/domain/types/..., .../schemas/...
Introduced wrapped Stripe DTO interfaces and replaced many usages of direct stripe SDK types with internal typed interfaces; updated schemas/entities to use new types.
Package/meta bumps & changelogs
many package.json and CHANGELOG.md files (e.g., framework/*, blueprint/*, cli/src/...)
Multiple package version bumps and changelog entries across repo (dev/runtime dependency updates, MikroORM, stripe, tooling).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client as rgba(46,117,182,0.5)
  participant AuthService as rgba(67,160,71,0.5)
  participant TtlCache as rgba(255,193,7,0.5)
  participant Redis as rgba(156,39,176,0.5)
  participant Encryptor as rgba(233,30,99,0.5)

  Client->>AuthService: request roles/permissions (tenantId)
  AuthService->>TtlCache: readRecord(key, compliance:{tenantId})
  TtlCache->>Redis: get(key)
  Redis->>Encryptor: decrypt(value, tenantId) (if compliance provided)
  Encryptor-->>Redis: plaintext
  Redis-->>TtlCache: record
  TtlCache-->>AuthService: record (decrypted)
  AuthService-->>Client: roles/permissions response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hop through tenant tunnels, light and fleet,

Per-op keys in paw make each secret neat,
No global switch — each burrow keeps its own,
Compliance stamped, every cipher shown,
I nibble bugs, then dance — the cache is sweet.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: tenant filtering on redis and s3' accurately describes the main changes in the PR, which add optional compliance context (tenant ID) parameters to Redis cache and S3 object store operations for tenant-aware encryption/decryption.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rohin/infra-tenant-filtering

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (6)
framework/infrastructure/S3/index.ts (1)

83-90: Fail explicitly on encrypted reads without matching compliance.

Right now a missing or wrong compliance context falls through to JSON.parse(body) and surfaces as a generic syntax error. Throwing a targeted decryption/context error here would make tenant mismatches much easier to diagnose and matches the explicit failure you added in EncryptedType.

♻️ Proposed refinement
   private decryptBody(body: string, compliance?: ComplianceContext): string {
-    if (!compliance || !this.encryptor) return body;
+    if (!this.encryptor) return body;
+    if (!compliance) {
+      if (isEncrypted(body)) {
+        throw new Error('Encrypted object requires a compliance context');
+      }
+      return body;
+    }
     if (!isEncrypted(body)) return body;
-    try {
-      return this.encryptor.decrypt(body, compliance.tenantId) ?? body;
-    } catch {
-      return body;
-    }
+    const decrypted = this.encryptor.decrypt(body, compliance.tenantId);
+    if (decrypted === null) {
+      throw new Error('Failed to decrypt object for the provided tenant');
+    }
+    return decrypted;
   }

Also applies to: 163-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/infrastructure/S3/index.ts` around lines 83 - 90, The decryptBody
method currently returns the raw body when compliance is missing or decryptor
fails, which causes downstream JSON.parse to surface generic syntax errors;
update decryptBody (and the similar logic around the other occurrence at lines
163-176) to explicitly throw a descriptive DecryptionContextError (or a new
error type) when the payload is encrypted but compliance is absent/mismatched or
when decryptor throws, include tenantId and a short context message referencing
the EncryptedType expectation so callers can distinguish tenant/decryption
issues from JSON syntax errors.
framework/infrastructure/S3/__test__/s3ObjectStore.test.ts (1)

50-59: This setup change still doesn't test the new compliance flow.

All specs here continue to exercise streaming downloads, which intentionally bypass encryption/decryption. Please add putObject/readObject coverage with a ComplianceContext — plus a wrong-tenant or missing-context read — so the per-operation encryption logic is actually validated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/infrastructure/S3/__test__/s3ObjectStore.test.ts` around lines 50 -
59, The tests currently created by makeStore only exercise streaming downloads
which skip encryption; add unit tests that call S3ObjectStore.putObject and
S3ObjectStore.readObject (not the streaming path) using a ComplianceContext so
the FieldEncryptor logic runs: create a ComplianceContext with the correct
tenant id and pass it into putObject, then readObject with the same context to
assert decrypted payload matches original; also add two negative tests — one
readObject with a wrong-tenant ComplianceContext and one readObject with no
ComplianceContext — and assert those reads fail or return unenforced/blocked
results per the compliance policy. Use the existing makeStore helper to
construct the S3ObjectStore (FieldEncryptor('test-encryption-key-for-s3-tests'))
and reuse or stub mockSend to verify encrypted payloads were sent where
appropriate.
framework/infrastructure/redis/__test__/redisTtlCache.test.ts (1)

18-31: Add a tenant-scoped round-trip test.

This suite now always wires an encryptor, but every assertion still calls Redis without a ComplianceContext, so the new per-tenant encryption path is never exercised. Please add at least one { tenantId } write/read and one mismatched-tenant read so regressions show up in CI.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/infrastructure/redis/__test__/redisTtlCache.test.ts` around lines
18 - 31, Add tests in redisTtlCache.test.ts that exercise per-tenant encryption
by performing a tenant-scoped write/read and a mismatched-tenant read: use the
existing RedisTtlCache instance and call its set(...) to store a key with a
ComplianceContext containing { tenantId: 'tenant-A' } then call get(...) with
the same ComplianceContext and assert the original value is returned; then call
get(...) with a different ComplianceContext ({ tenantId: 'tenant-B' } or no
tenant) and assert the value is not readable (null/undefined or decryption
failure as the suite expects). Locate calls to RedisTtlCache.set and
RedisTtlCache.get (and the ComplianceContext type/constructor) in the test file
and add these two assertions so the tenant-scoped encryption path is covered.
framework/infrastructure/redis/index.ts (2)

84-91: Make plaintext writes an explicit opt-in.

This class already requires RedisCacheEncryptionOptions, but encryptValue treats missing compliance as “store plaintext”. That makes undefined ambiguous: it can mean “plaintext by design” or “caller forgot to thread the new tenant context”, and both succeed silently.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/infrastructure/redis/index.ts` around lines 84 - 91, encryptValue
currently treats a missing ComplianceContext as implicit permission to write
plaintext; change this so plaintext writes are explicit by requiring an explicit
opt-in flag or by making ComplianceContext mandatory. Update the encryptValue
signature (currently encryptValue(serialized: string, compliance?:
ComplianceContext)) to either require compliance or add a boolean allowPlaintext
parameter defaulting to false, and make the method throw or return an error when
compliance is undefined and allowPlaintext is not true; ensure you reference the
class-level encryptor and ComplianceContext in the check and update all callers
of encryptValue (and any wrapper methods) to pass the compliance or explicit
allowPlaintext flag.

1-14: Move redis into the external-dependency layer.

The new import block places the external redis package after the @forklaunch/* imports, which breaks the repository's import layering.

♻️ Proposed cleanup
+import { createClient, RedisClientOptions } from 'redis';
 import { safeParse, safeStringify } from '@forklaunch/common';
 import {
   type ComplianceContext,
   TtlCache,
   TtlCacheRecord
 } from '@forklaunch/core/cache';
 import {
   evaluateTelemetryOptions,
   MetricsDefinition,
   OpenTelemetryCollector,
   TelemetryOptions
 } from '@forklaunch/core/http';
 import { type FieldEncryptor } from '@forklaunch/core/persistence';
-import { createClient, RedisClientOptions } from 'redis';
As per coding guidelines, "Organize imports in 7 layers: (1) Node built-ins with node: prefix, (2) External dependencies, (3) Forklaunch framework packages, (4) Cross-module imports, (5) Local persistence, (6) Local domain, (7) Same directory".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/infrastructure/redis/index.ts` around lines 1 - 14, The redis
import (createClient, RedisClientOptions) is placed after the `@forklaunch/`*
imports which breaks the 7-layer import ordering; move the "redis" import into
the external-dependencies layer so it appears before any `@forklaunch/`* imports,
keeping the existing symbols (createClient, RedisClientOptions) intact and
preserving the rest of the import block order.
framework/core/src/cache/interfaces/ttlCache.interface.ts (1)

17-28: Update the in-tree TtlCache doubles with the new compliance argument.

Because this parameter is optional, old object-literal mocks keep compiling even if they never exercise the tenant-scoped path. The doubles in framework/core/__test__/rateLimiter.test.ts:10-40 and framework/core/__test__/rateLimiter.test.ts:57-76 still use the pre-change readRecord/putRecord shape.

Also applies to: 78-89

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/core/src/cache/interfaces/ttlCache.interface.ts` around lines 17 -
28, Update the in-test TtlCache doubles to match the new method signatures that
accept an optional ComplianceContext: update the mocked methods
readRecord<T>(key: string, compliance?: ComplianceContext),
putRecord<T>(cacheRecord: TtlCacheRecord<T>, compliance?: ComplianceContext) and
putBatchRecords<T>(cacheRecords: TtlCacheRecord<T>[], compliance?:
ComplianceContext) so tests in the rate limiter mocks call/implement the extra
optional parameter (leave behavior the same when compliance is undefined).
Locate the doubles by the TtlCache/readRecord/putRecord/putBatchRecords
identifiers used in the tests and add the optional parameter to their function
signatures and any places that construct or call them.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@blueprint/core/billing/cache.ts`:
- Around line 125-149: getCachedPlan and setCachedPlan currently use a key
`${PLAN_CACHE_PREFIX}${planId}` which causes different organizations to share
the same Redis key and corrupt encrypted payloads; update both functions to
include organizationId in the cache key (e.g.
`${PLAN_CACHE_PREFIX}${organizationId}:${planId}`) wherever the key is
constructed so reads and writes use the same namespaced key (or alternatively
make the cache explicitly global and skip encryption), ensuring the key format
is consistently changed in both getCachedPlan and setCachedPlan.

In `@framework/infrastructure/redis/index.ts`:
- Around line 94-101: decryptValue currently returns the raw ciphertext on
decryption failures which lets callers (e.g., blueprint/core/auth/cache.ts)
treat unreadable entries as valid strings; change decryptValue (in
framework/infrastructure/redis/index.ts) so that if isEncrypted(value) is true
but decryption throws or returns null, it does NOT return the ciphertext—either
throw the error or return a sentinel (e.g., undefined/null) and update the
function signature and callers (including safeParse usages in
blueprint/core/auth/cache.ts) to treat that sentinel as a cache miss/unreadable
entry instead of a successful value.
- Around line 313-329: peekQueueRecord and peekQueueRecords are reading from the
head (lRange(0,...)) but the queue uses lPush / rPop so the next item to dequeue
is at the tail; change the lRange calls in peekQueueRecord (function
peekQueueRecord<T>) to read the tail (use lRange(..., -1, -1) and take the
first/only element) and in peekQueueRecords (function peekQueueRecords<T>) to
read the last N items (lRange(queueName, -pageSize, -1)) and then reverse the
returned array before calling parseValue<T> so the results are in dequeue order
(next-to-dequeue first). Ensure you still pass compliance into parseValue and
handle empty results safely.

---

Nitpick comments:
In `@framework/core/src/cache/interfaces/ttlCache.interface.ts`:
- Around line 17-28: Update the in-test TtlCache doubles to match the new method
signatures that accept an optional ComplianceContext: update the mocked methods
readRecord<T>(key: string, compliance?: ComplianceContext),
putRecord<T>(cacheRecord: TtlCacheRecord<T>, compliance?: ComplianceContext) and
putBatchRecords<T>(cacheRecords: TtlCacheRecord<T>[], compliance?:
ComplianceContext) so tests in the rate limiter mocks call/implement the extra
optional parameter (leave behavior the same when compliance is undefined).
Locate the doubles by the TtlCache/readRecord/putRecord/putBatchRecords
identifiers used in the tests and add the optional parameter to their function
signatures and any places that construct or call them.

In `@framework/infrastructure/redis/__test__/redisTtlCache.test.ts`:
- Around line 18-31: Add tests in redisTtlCache.test.ts that exercise per-tenant
encryption by performing a tenant-scoped write/read and a mismatched-tenant
read: use the existing RedisTtlCache instance and call its set(...) to store a
key with a ComplianceContext containing { tenantId: 'tenant-A' } then call
get(...) with the same ComplianceContext and assert the original value is
returned; then call get(...) with a different ComplianceContext ({ tenantId:
'tenant-B' } or no tenant) and assert the value is not readable (null/undefined
or decryption failure as the suite expects). Locate calls to RedisTtlCache.set
and RedisTtlCache.get (and the ComplianceContext type/constructor) in the test
file and add these two assertions so the tenant-scoped encryption path is
covered.

In `@framework/infrastructure/redis/index.ts`:
- Around line 84-91: encryptValue currently treats a missing ComplianceContext
as implicit permission to write plaintext; change this so plaintext writes are
explicit by requiring an explicit opt-in flag or by making ComplianceContext
mandatory. Update the encryptValue signature (currently encryptValue(serialized:
string, compliance?: ComplianceContext)) to either require compliance or add a
boolean allowPlaintext parameter defaulting to false, and make the method throw
or return an error when compliance is undefined and allowPlaintext is not true;
ensure you reference the class-level encryptor and ComplianceContext in the
check and update all callers of encryptValue (and any wrapper methods) to pass
the compliance or explicit allowPlaintext flag.
- Around line 1-14: The redis import (createClient, RedisClientOptions) is
placed after the `@forklaunch/`* imports which breaks the 7-layer import ordering;
move the "redis" import into the external-dependencies layer so it appears
before any `@forklaunch/`* imports, keeping the existing symbols (createClient,
RedisClientOptions) intact and preserving the rest of the import block order.

In `@framework/infrastructure/S3/__test__/s3ObjectStore.test.ts`:
- Around line 50-59: The tests currently created by makeStore only exercise
streaming downloads which skip encryption; add unit tests that call
S3ObjectStore.putObject and S3ObjectStore.readObject (not the streaming path)
using a ComplianceContext so the FieldEncryptor logic runs: create a
ComplianceContext with the correct tenant id and pass it into putObject, then
readObject with the same context to assert decrypted payload matches original;
also add two negative tests — one readObject with a wrong-tenant
ComplianceContext and one readObject with no ComplianceContext — and assert
those reads fail or return unenforced/blocked results per the compliance policy.
Use the existing makeStore helper to construct the S3ObjectStore
(FieldEncryptor('test-encryption-key-for-s3-tests')) and reuse or stub mockSend
to verify encrypted payloads were sent where appropriate.

In `@framework/infrastructure/S3/index.ts`:
- Around line 83-90: The decryptBody method currently returns the raw body when
compliance is missing or decryptor fails, which causes downstream JSON.parse to
surface generic syntax errors; update decryptBody (and the similar logic around
the other occurrence at lines 163-176) to explicitly throw a descriptive
DecryptionContextError (or a new error type) when the payload is encrypted but
compliance is absent/mismatched or when decryptor throws, include tenantId and a
short context message referencing the EncryptedType expectation so callers can
distinguish tenant/decryption issues from JSON syntax errors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7b8da96b-847a-4f36-a805-db7410e91907

📥 Commits

Reviewing files that changed from the base of the PR and between 309714b and db120b5.

📒 Files selected for processing (10)
  • blueprint/core/auth/cache.ts
  • blueprint/core/billing/cache.ts
  • framework/core/src/cache/interfaces/ttlCache.interface.ts
  • framework/core/src/cache/types/ttlCacheRecord.types.ts
  • framework/core/src/objectstore/interfaces/objectstore.interface.ts
  • framework/core/src/persistence/encryptedType.ts
  • framework/infrastructure/S3/__test__/s3ObjectStore.test.ts
  • framework/infrastructure/S3/index.ts
  • framework/infrastructure/redis/__test__/redisTtlCache.test.ts
  • framework/infrastructure/redis/index.ts

Comment thread blueprint/core/billing/cache.ts
Comment thread framework/infrastructure/redis/index.ts
Comment thread framework/infrastructure/redis/index.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
framework/core/src/persistence/encryptedType.ts (1)

209-217: Consider including tenantId in the decryption error for easier debugging.

When decryption fails due to a tenant context mismatch (e.g., data encrypted with tenant A but decrypted in an empty context or tenant B's context), the current error message doesn't hint at this common cause. Including the tenantId value would help diagnose these issues faster, especially given that getCurrentTenantId() returns '' during startup, seeders, and background jobs (per the comment on line 50).

💡 Suggested enhancement
     let decrypted: string | null;
     const tenantId = getCurrentTenantId();
     try {
       decrypted = _encryptor.decrypt(value, tenantId);
     } catch (err) {
       throw new Error(
-        `Failed to decrypt encrypted column value: ${err instanceof Error ? err.message : String(err)}`,
+        `Failed to decrypt encrypted column value (tenantId=${tenantId ? `'${tenantId}'` : '<empty>'}): ${err instanceof Error ? err.message : String(err)}`,
         { cause: err }
       );
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/core/src/persistence/encryptedType.ts` around lines 209 - 217, The
decryption catch block around _encryptor.decrypt(value, tenantId) should include
the tenant context to aid debugging; update the thrown Error message in the
catch to incorporate the tenantId (from getCurrentTenantId()) and relevant
context (e.g., `tenantId: ${tenantId}`) so failures clearly show which tenant
(including empty string) was used when decrypting in encryptedType.ts; keep the
existing error cause (err) intact when constructing the new Error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@framework/core/src/persistence/encryptedType.ts`:
- Around line 209-217: The decryption catch block around
_encryptor.decrypt(value, tenantId) should include the tenant context to aid
debugging; update the thrown Error message in the catch to incorporate the
tenantId (from getCurrentTenantId()) and relevant context (e.g., `tenantId:
${tenantId}`) so failures clearly show which tenant (including empty string) was
used when decrypting in encryptedType.ts; keep the existing error cause (err)
intact when constructing the new Error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51a02067-6c4d-45a4-81c7-58c60fc0a467

📥 Commits

Reviewing files that changed from the base of the PR and between db120b5 and d3199f0.

📒 Files selected for processing (1)
  • framework/core/src/persistence/encryptedType.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
framework/infrastructure/redis/index.ts (1)

227-241: ⚠️ Potential issue | 🔴 Critical

A single bad item can drop an already-popped batch.

rPop removes the whole batch before parsing it. If parseValue() throws for one corrupt or wrong-tenant record, this call rejects after every popped item is already gone, so the unreadable item and its valid siblings are lost.

🐛 Proposed fix
    const values = await multiCommand.exec();
-    return values
-      .map((value) =>
-        this.parseValue<T>(value as unknown as RedisCommandRawReply, compliance)
-      )
-      .filter(Boolean);
+    const records: T[] = [];
+    for (const value of values) {
+      try {
+        const parsed = this.parseValue<T>(
+          value as unknown as RedisCommandRawReply,
+          compliance
+        );
+        if (parsed != null) {
+          records.push(parsed);
+        }
+      } catch (error) {
+        if (this.telemetryOptions.enabled.logging) {
+          this.openTelemetryCollector.error(error);
+        }
+      }
+    }
+    return records;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/infrastructure/redis/index.ts` around lines 227 - 241,
dequeueBatchRecords currently issues multiple rPop calls in a multi and only
parses items after exec, so a parse error can discard an entire popped batch;
change the implementation to read items without removing first (use
client.lRange or client.lRange/LRANGE equivalent on the queueName for the last
pageSize items), parse each item with parseValue, and only when parsing succeeds
remove the corresponding items from Redis (use client.lTrim or a second multi
that runs LTRIM to drop the consumed tail) so no items are removed before
parseValue completes; update dequeueBatchRecords to call parseValue before
executing removal and reference multiCommand/rPop only if you switch to a second
removal multi after successful parsing.
♻️ Duplicate comments (2)
framework/infrastructure/redis/index.ts (2)

94-96: ⚠️ Potential issue | 🟠 Major

Don't let encrypted hits bypass tenant context.

decryptValue() returns before checking isEncrypted(), so a missed compliance propagation turns an encrypted Redis value into a bogus cache hit instead of an unreadable/miss path.

🐛 Proposed fix
  private decryptValue(value: string, compliance?: ComplianceContext): string {
-    if (!compliance || !this.encryptor) return value;
+    if (!this.encryptor) return value;
     if (!isEncrypted(value)) return value;
+    if (!compliance) {
+      throw new Error(
+        'Redis: compliance context is required for encrypted values'
+      );
+    }
     // If a value is encrypted but we cannot decrypt it, treat the entry as
     // unreadable rather than returning the ciphertext. Returning the raw
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/infrastructure/redis/index.ts` around lines 94 - 96, decryptValue
currently early-returns when compliance or this.encryptor is missing before
checking isEncrypted, allowing encrypted blobs to be treated as valid plaintext;
change the logic in decryptValue to check isEncrypted(value) first and, if the
value is encrypted but compliance or this.encryptor is absent, do not return the
encrypted string as a hit—instead surface an unreadable/miss signal (e.g.,
return null/undefined or throw a RecoverableDecryptionError) so callers treat it
as a cache miss; if the value is encrypted and prerequisites exist, proceed to
decrypt normally using this.encryptor and ComplianceContext.

329-337: ⚠️ Potential issue | 🟡 Minor

Tighten the queue peek edge cases.

peekQueueRecord() still returns null as T on an empty list, and peekQueueRecords(..., 0, ...) expands to LRANGE 0 -1, i.e. the entire queue. The generic .filter(Boolean) also drops valid false/0 payloads.

🐛 Proposed fix
  async peekQueueRecord<T>(
    queueName: string,
    compliance?: ComplianceContext
  ): Promise<T> {
     // Queues use lPush + rPop, so the next item to dequeue lives at the
     // tail of the list, not the head. Reading lRange(0, 0) would return the
     // most-recently-pushed item — the opposite of dequeue order.
-    const value = await this.client.lRange(queueName, -1, -1);
-    return this.parseValue<T>(value[0], compliance);
+    const [value] = await this.client.lRange(queueName, -1, -1);
+    if (value === undefined) {
+      throw new Error(`Queue is empty: ${queueName}`);
+    }
+    return this.parseValue<T>(value, compliance);
  }

  async peekQueueRecords<T>(
    queueName: string,
    pageSize: number,
    compliance?: ComplianceContext
  ): Promise<T[]> {
+    if (pageSize <= 0) return [];
     // Tail-relative range: the last `pageSize` items, where the very last
     // item is the next to be dequeued. Redis returns them in list order
     // (oldest-tail-end first), so reverse to put next-to-dequeue first.
     const values = await this.client.lRange(queueName, -pageSize, -1);
     if (values.length === 0) return [];
-    return values
-      .reverse()
-      .map((value) => this.parseValue<T>(value, compliance))
-      .filter(Boolean);
+    return values.reverse().flatMap((value) => {
+      const parsed = this.parseValue<T>(value, compliance);
+      return parsed == null ? [] : [parsed];
+    });
  }

Also applies to: 340-353

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/infrastructure/redis/index.ts` around lines 329 - 337,
peekQueueRecord and peekQueueRecords have edge cases: peekQueueRecord returns
null when lRange returns empty without explicit check, peekQueueRecords expands
a count of 0 into LRANGE 0 -1 and uses .filter(Boolean) which drops valid falsy
payloads. Fixes: in peekQueueRecord (function peekQueueRecord) explicitly check
if the lRange result array is empty and return null (or undefined per API)
before calling parseValue; in peekQueueRecords compute the LRANGE bounds so
count===0 immediately returns an empty array, and when building the parsed list
avoid .filter(Boolean) — use a filter that only removes null/undefined (e.g. x
=> x !== null && x !== undefined) so values like 0 or false are preserved; also
ensure LRANGE uses negative start indices (e.g. -count) or correctly clamps to
list length to avoid the 0 -> -1 whole-list expansion.
🧹 Nitpick comments (3)
blueprint/implementations/billing/stripe/domain/schemas/zod/plan.schema.ts (1)

20-21: Duplicate PlanOmissions type definition.

Similar to SubscriptionOmissions, this duplicates the definition from stripe.dto.types.ts (line 203). Consider exporting and reusing it.

 import {
   StripePlanCreateParams,
   StripePlanUpdateParams,
-  StripeProduct
+  StripeProduct,
+  PlanOmissions
 } from '../../types/stripe.dto.types';

-type PlanOmissions = 'product' | 'interval' | 'currency';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blueprint/implementations/billing/stripe/domain/schemas/zod/plan.schema.ts`
around lines 20 - 21, The PlanOmissions type is duplicated here; remove this
local declaration of PlanOmissions and instead import and reuse the shared type
from stripe.dto.types.ts (where SubscriptionOmissions/PlanOmissions is defined),
updating the exports if necessary so plan.schema.ts references the single
exported type; ensure you replace the local "PlanOmissions" declaration with an
import statement and adjust any uses in plan.schema.ts to reference the imported
symbol.
blueprint/implementations/billing/stripe/domain/schemas/zod/subscription.schema.ts (1)

17-18: Duplicate SubscriptionOmissions type definition.

The SubscriptionOmissions type is identical to the one defined in stripe.dto.types.ts (line 245). Consider exporting and importing it from there to ensure they stay in sync and reduce duplication.

 import {
   StripeSubscription,
   StripeSubscriptionCreateParams,
-  StripeSubscriptionUpdateParams
+  StripeSubscriptionUpdateParams,
+  SubscriptionOmissions
 } from '../../types/stripe.dto.types';

-type SubscriptionOmissions = 'items' | 'customer';

Note: This requires exporting SubscriptionOmissions from stripe.dto.types.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@blueprint/implementations/billing/stripe/domain/schemas/zod/subscription.schema.ts`
around lines 17 - 18, The file defines a duplicate type SubscriptionOmissions
that is already declared in stripe.dto.types.ts; remove the local definition and
import the shared type instead: export SubscriptionOmissions from
stripe.dto.types.ts (add the export there if missing) and replace the local
declaration with an import of SubscriptionOmissions in subscription.schema.ts so
both modules reference the same symbol.
blueprint/billing-stripe/persistence/entities/subscription.entity.ts (1)

1-5: Import ordering could be improved for consistency.

The imports are functional but could follow the 7-layer organization more strictly. Consider grouping @forklaunch/core/persistence with @forklaunch/blueprint-core as framework packages.

 import { sqlBaseProperties } from '@forklaunch/blueprint-core';
+import { defineComplianceEntity, fp } from '@forklaunch/core/persistence';
 import { BillingProviderEnum } from '@forklaunch/implementation-billing-stripe/enum';
 import { StripeSubscription } from '@forklaunch/implementation-billing-stripe/types';
-import { defineComplianceEntity, fp } from '@forklaunch/core/persistence';
 import { PartyEnum } from '../../domain/enum/party.enum';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blueprint/billing-stripe/persistence/entities/subscription.entity.ts` around
lines 1 - 5, Reorder the import statements to follow the 7-layer convention by
grouping framework packages together: place imports from
'@forklaunch/core/persistence' (defineComplianceEntity, fp) and
'@forklaunch/blueprint-core' (sqlBaseProperties) adjacent, then
third-party/implementation imports like
'@forklaunch/implementation-billing-stripe' (BillingProviderEnum,
StripeSubscription), and finally local project enums like PartyEnum; adjust the
current import order so sqlBaseProperties and defineComplianceEntity/fp are next
to each other to improve consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@framework/infrastructure/redis/index.ts`:
- Around line 227-241: dequeueBatchRecords currently issues multiple rPop calls
in a multi and only parses items after exec, so a parse error can discard an
entire popped batch; change the implementation to read items without removing
first (use client.lRange or client.lRange/LRANGE equivalent on the queueName for
the last pageSize items), parse each item with parseValue, and only when parsing
succeeds remove the corresponding items from Redis (use client.lTrim or a second
multi that runs LTRIM to drop the consumed tail) so no items are removed before
parseValue completes; update dequeueBatchRecords to call parseValue before
executing removal and reference multiCommand/rPop only if you switch to a second
removal multi after successful parsing.

---

Duplicate comments:
In `@framework/infrastructure/redis/index.ts`:
- Around line 94-96: decryptValue currently early-returns when compliance or
this.encryptor is missing before checking isEncrypted, allowing encrypted blobs
to be treated as valid plaintext; change the logic in decryptValue to check
isEncrypted(value) first and, if the value is encrypted but compliance or
this.encryptor is absent, do not return the encrypted string as a hit—instead
surface an unreadable/miss signal (e.g., return null/undefined or throw a
RecoverableDecryptionError) so callers treat it as a cache miss; if the value is
encrypted and prerequisites exist, proceed to decrypt normally using
this.encryptor and ComplianceContext.
- Around line 329-337: peekQueueRecord and peekQueueRecords have edge cases:
peekQueueRecord returns null when lRange returns empty without explicit check,
peekQueueRecords expands a count of 0 into LRANGE 0 -1 and uses .filter(Boolean)
which drops valid falsy payloads. Fixes: in peekQueueRecord (function
peekQueueRecord) explicitly check if the lRange result array is empty and return
null (or undefined per API) before calling parseValue; in peekQueueRecords
compute the LRANGE bounds so count===0 immediately returns an empty array, and
when building the parsed list avoid .filter(Boolean) — use a filter that only
removes null/undefined (e.g. x => x !== null && x !== undefined) so values like
0 or false are preserved; also ensure LRANGE uses negative start indices (e.g.
-count) or correctly clamps to list length to avoid the 0 -> -1 whole-list
expansion.

---

Nitpick comments:
In `@blueprint/billing-stripe/persistence/entities/subscription.entity.ts`:
- Around line 1-5: Reorder the import statements to follow the 7-layer
convention by grouping framework packages together: place imports from
'@forklaunch/core/persistence' (defineComplianceEntity, fp) and
'@forklaunch/blueprint-core' (sqlBaseProperties) adjacent, then
third-party/implementation imports like
'@forklaunch/implementation-billing-stripe' (BillingProviderEnum,
StripeSubscription), and finally local project enums like PartyEnum; adjust the
current import order so sqlBaseProperties and defineComplianceEntity/fp are next
to each other to improve consistency.

In `@blueprint/implementations/billing/stripe/domain/schemas/zod/plan.schema.ts`:
- Around line 20-21: The PlanOmissions type is duplicated here; remove this
local declaration of PlanOmissions and instead import and reuse the shared type
from stripe.dto.types.ts (where SubscriptionOmissions/PlanOmissions is defined),
updating the exports if necessary so plan.schema.ts references the single
exported type; ensure you replace the local "PlanOmissions" declaration with an
import statement and adjust any uses in plan.schema.ts to reference the imported
symbol.

In
`@blueprint/implementations/billing/stripe/domain/schemas/zod/subscription.schema.ts`:
- Around line 17-18: The file defines a duplicate type SubscriptionOmissions
that is already declared in stripe.dto.types.ts; remove the local definition and
import the shared type instead: export SubscriptionOmissions from
stripe.dto.types.ts (add the export there if missing) and replace the local
declaration with an import of SubscriptionOmissions in subscription.schema.ts so
both modules reference the same symbol.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1dee890-33dc-43c4-85ae-79fc008006ee

📥 Commits

Reviewing files that changed from the base of the PR and between d3199f0 and dedd292.

⛔ Files ignored due to path filters (2)
  • blueprint/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • framework/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (66)
  • blueprint/billing-base/package.json
  • blueprint/billing-stripe/package.json
  • blueprint/billing-stripe/persistence/entities/billingPortal.entity.ts
  • blueprint/billing-stripe/persistence/entities/checkoutSession.entity.ts
  • blueprint/billing-stripe/persistence/entities/paymentLink.entity.ts
  • blueprint/billing-stripe/persistence/entities/plan.entity.ts
  • blueprint/billing-stripe/persistence/entities/subscription.entity.ts
  • blueprint/core/billing/cache.ts
  • blueprint/core/package.json
  • blueprint/iam-base/package.json
  • blueprint/iam-better-auth/package.json
  • blueprint/implementations/billing/base/CHANGELOG.md
  • blueprint/implementations/billing/base/package.json
  • blueprint/implementations/billing/stripe/CHANGELOG.md
  • blueprint/implementations/billing/stripe/domain/schemas/zod/paymentLink.schema.ts
  • blueprint/implementations/billing/stripe/domain/schemas/zod/plan.schema.ts
  • blueprint/implementations/billing/stripe/domain/schemas/zod/subscription.schema.ts
  • blueprint/implementations/billing/stripe/domain/types/stripe.dto.types.ts
  • blueprint/implementations/billing/stripe/package.json
  • blueprint/implementations/iam/base/CHANGELOG.md
  • blueprint/implementations/iam/base/package.json
  • blueprint/implementations/worker/bullmq/CHANGELOG.md
  • blueprint/implementations/worker/bullmq/package.json
  • blueprint/implementations/worker/database/CHANGELOG.md
  • blueprint/implementations/worker/database/package.json
  • blueprint/implementations/worker/kafka/CHANGELOG.md
  • blueprint/implementations/worker/kafka/package.json
  • blueprint/implementations/worker/redis/CHANGELOG.md
  • blueprint/implementations/worker/redis/package.json
  • blueprint/interfaces/billing/CHANGELOG.md
  • blueprint/interfaces/billing/package.json
  • blueprint/interfaces/iam/CHANGELOG.md
  • blueprint/interfaces/iam/package.json
  • blueprint/interfaces/worker/CHANGELOG.md
  • blueprint/interfaces/worker/package.json
  • blueprint/monitoring/package.json
  • blueprint/package.json
  • blueprint/sample-worker/package.json
  • cli/src/core/package_json/package_json_constants.rs
  • framework/bunrun/CHANGELOG.md
  • framework/bunrun/package.json
  • framework/common/CHANGELOG.md
  • framework/common/package.json
  • framework/core/CHANGELOG.md
  • framework/core/package.json
  • framework/e2e-tests/package.json
  • framework/express/CHANGELOG.md
  • framework/express/package.json
  • framework/hyper-express/CHANGELOG.md
  • framework/hyper-express/package.json
  • framework/infrastructure/S3/CHANGELOG.md
  • framework/infrastructure/S3/package.json
  • framework/infrastructure/redis/CHANGELOG.md
  • framework/infrastructure/redis/index.ts
  • framework/infrastructure/redis/package.json
  • framework/internal/CHANGELOG.md
  • framework/internal/package.json
  • framework/package.json
  • framework/testing/CHANGELOG.md
  • framework/testing/package.json
  • framework/universal-sdk/CHANGELOG.md
  • framework/universal-sdk/package.json
  • framework/validator/CHANGELOG.md
  • framework/validator/package.json
  • framework/ws/CHANGELOG.md
  • framework/ws/package.json
✅ Files skipped from review due to trivial changes (52)
  • blueprint/interfaces/billing/CHANGELOG.md
  • blueprint/implementations/billing/stripe/CHANGELOG.md
  • framework/e2e-tests/package.json
  • framework/bunrun/CHANGELOG.md
  • framework/testing/CHANGELOG.md
  • blueprint/implementations/billing/base/CHANGELOG.md
  • framework/common/CHANGELOG.md
  • blueprint/interfaces/iam/CHANGELOG.md
  • blueprint/monitoring/package.json
  • blueprint/interfaces/worker/CHANGELOG.md
  • blueprint/implementations/worker/redis/CHANGELOG.md
  • framework/universal-sdk/CHANGELOG.md
  • framework/validator/CHANGELOG.md
  • blueprint/implementations/worker/kafka/CHANGELOG.md
  • blueprint/implementations/iam/base/CHANGELOG.md
  • blueprint/implementations/worker/database/CHANGELOG.md
  • blueprint/implementations/worker/bullmq/CHANGELOG.md
  • blueprint/billing-stripe/persistence/entities/plan.entity.ts
  • blueprint/interfaces/iam/package.json
  • framework/ws/CHANGELOG.md
  • framework/internal/CHANGELOG.md
  • framework/ws/package.json
  • blueprint/interfaces/worker/package.json
  • blueprint/billing-stripe/persistence/entities/billingPortal.entity.ts
  • framework/package.json
  • framework/infrastructure/S3/package.json
  • framework/bunrun/package.json
  • framework/testing/package.json
  • framework/infrastructure/redis/package.json
  • blueprint/interfaces/billing/package.json
  • framework/express/CHANGELOG.md
  • framework/hyper-express/package.json
  • framework/common/package.json
  • framework/universal-sdk/package.json
  • framework/hyper-express/CHANGELOG.md
  • framework/validator/package.json
  • blueprint/implementations/worker/kafka/package.json
  • blueprint/implementations/worker/bullmq/package.json
  • blueprint/implementations/worker/database/package.json
  • framework/express/package.json
  • blueprint/package.json
  • blueprint/implementations/billing/base/package.json
  • blueprint/implementations/billing/stripe/package.json
  • blueprint/implementations/worker/redis/package.json
  • framework/core/package.json
  • blueprint/implementations/iam/base/package.json
  • blueprint/sample-worker/package.json
  • blueprint/iam-base/package.json
  • blueprint/iam-better-auth/package.json
  • framework/internal/package.json
  • blueprint/billing-stripe/package.json
  • cli/src/core/package_json/package_json_constants.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • blueprint/core/billing/cache.ts

@RohinBhargava
RohinBhargava merged commit 57c4297 into main Apr 7, 2026
12 checks passed
@RohinBhargava
RohinBhargava deleted the rohin/infra-tenant-filtering branch April 7, 2026 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant