feat: tenant filtering on redis and s3 - #150
Conversation
📝 WalkthroughWalkthroughPer-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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
compliancecontext falls through toJSON.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 inEncryptedType.♻️ 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/readObjectcoverage with aComplianceContext— 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, butencryptValuetreats missingcomplianceas “store plaintext”. That makesundefinedambiguous: 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: Moveredisinto the external-dependency layer.The new import block places the external
redispackage after the@forklaunch/*imports, which breaks the repository's import layering.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".♻️ 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';🤖 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-treeTtlCachedoubles with the newcomplianceargument.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-40andframework/core/__test__/rateLimiter.test.ts:57-76still use the pre-changereadRecord/putRecordshape.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
📒 Files selected for processing (10)
blueprint/core/auth/cache.tsblueprint/core/billing/cache.tsframework/core/src/cache/interfaces/ttlCache.interface.tsframework/core/src/cache/types/ttlCacheRecord.types.tsframework/core/src/objectstore/interfaces/objectstore.interface.tsframework/core/src/persistence/encryptedType.tsframework/infrastructure/S3/__test__/s3ObjectStore.test.tsframework/infrastructure/S3/index.tsframework/infrastructure/redis/__test__/redisTtlCache.test.tsframework/infrastructure/redis/index.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
framework/core/src/persistence/encryptedType.ts (1)
209-217: Consider includingtenantIdin 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
📒 Files selected for processing (1)
framework/core/src/persistence/encryptedType.ts
There was a problem hiding this comment.
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 | 🔴 CriticalA single bad item can drop an already-popped batch.
rPopremoves the whole batch before parsing it. IfparseValue()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 | 🟠 MajorDon't let encrypted hits bypass tenant context.
decryptValue()returns before checkingisEncrypted(), so a missedcompliancepropagation 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 | 🟡 MinorTighten the queue peek edge cases.
peekQueueRecord()still returnsnull as Ton an empty list, andpeekQueueRecords(..., 0, ...)expands toLRANGE 0 -1, i.e. the entire queue. The generic.filter(Boolean)also drops validfalse/0payloads.🐛 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: DuplicatePlanOmissionstype definition.Similar to
SubscriptionOmissions, this duplicates the definition fromstripe.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: DuplicateSubscriptionOmissionstype definition.The
SubscriptionOmissionstype is identical to the one defined instripe.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
SubscriptionOmissionsfromstripe.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/persistencewith@forklaunch/blueprint-coreas 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
⛔ Files ignored due to path filters (2)
blueprint/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlframework/pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (66)
blueprint/billing-base/package.jsonblueprint/billing-stripe/package.jsonblueprint/billing-stripe/persistence/entities/billingPortal.entity.tsblueprint/billing-stripe/persistence/entities/checkoutSession.entity.tsblueprint/billing-stripe/persistence/entities/paymentLink.entity.tsblueprint/billing-stripe/persistence/entities/plan.entity.tsblueprint/billing-stripe/persistence/entities/subscription.entity.tsblueprint/core/billing/cache.tsblueprint/core/package.jsonblueprint/iam-base/package.jsonblueprint/iam-better-auth/package.jsonblueprint/implementations/billing/base/CHANGELOG.mdblueprint/implementations/billing/base/package.jsonblueprint/implementations/billing/stripe/CHANGELOG.mdblueprint/implementations/billing/stripe/domain/schemas/zod/paymentLink.schema.tsblueprint/implementations/billing/stripe/domain/schemas/zod/plan.schema.tsblueprint/implementations/billing/stripe/domain/schemas/zod/subscription.schema.tsblueprint/implementations/billing/stripe/domain/types/stripe.dto.types.tsblueprint/implementations/billing/stripe/package.jsonblueprint/implementations/iam/base/CHANGELOG.mdblueprint/implementations/iam/base/package.jsonblueprint/implementations/worker/bullmq/CHANGELOG.mdblueprint/implementations/worker/bullmq/package.jsonblueprint/implementations/worker/database/CHANGELOG.mdblueprint/implementations/worker/database/package.jsonblueprint/implementations/worker/kafka/CHANGELOG.mdblueprint/implementations/worker/kafka/package.jsonblueprint/implementations/worker/redis/CHANGELOG.mdblueprint/implementations/worker/redis/package.jsonblueprint/interfaces/billing/CHANGELOG.mdblueprint/interfaces/billing/package.jsonblueprint/interfaces/iam/CHANGELOG.mdblueprint/interfaces/iam/package.jsonblueprint/interfaces/worker/CHANGELOG.mdblueprint/interfaces/worker/package.jsonblueprint/monitoring/package.jsonblueprint/package.jsonblueprint/sample-worker/package.jsoncli/src/core/package_json/package_json_constants.rsframework/bunrun/CHANGELOG.mdframework/bunrun/package.jsonframework/common/CHANGELOG.mdframework/common/package.jsonframework/core/CHANGELOG.mdframework/core/package.jsonframework/e2e-tests/package.jsonframework/express/CHANGELOG.mdframework/express/package.jsonframework/hyper-express/CHANGELOG.mdframework/hyper-express/package.jsonframework/infrastructure/S3/CHANGELOG.mdframework/infrastructure/S3/package.jsonframework/infrastructure/redis/CHANGELOG.mdframework/infrastructure/redis/index.tsframework/infrastructure/redis/package.jsonframework/internal/CHANGELOG.mdframework/internal/package.jsonframework/package.jsonframework/testing/CHANGELOG.mdframework/testing/package.jsonframework/universal-sdk/CHANGELOG.mdframework/universal-sdk/package.jsonframework/validator/CHANGELOG.mdframework/validator/package.jsonframework/ws/CHANGELOG.mdframework/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
Summary by CodeRabbit
New Features
Refactor
Bug Fixes
Chores