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

Skip to content

fix: Tenant ID Filter Tightening - #141

Merged
RohinBhargava merged 3 commits into
mainfrom
rohin/tenant-id-fixes
Mar 28, 2026
Merged

fix: Tenant ID Filter Tightening#141
RohinBhargava merged 3 commits into
mainfrom
rohin/tenant-id-fixes

Conversation

@RohinBhargava

@RohinBhargava RohinBhargava commented Mar 28, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Tenant-aware field encryption for Redis cache and S3 object storage.
    • New encrypted data type for persistent fields and global encryptor registration.
  • Enhancements

    • Deterministic ciphertext per tenant+value and updated encryption format.
    • Native DB operations are blocked for sensitive entities unless encryption is present.
    • Numerous dependency and package version bumps; updated changelogs across the workspace.
  • Tests / Docs

    • Updated tests and changelogs reflecting encryption behavior and package releases.

@coderabbitai

coderabbitai Bot commented Mar 28, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Refactors encryption from a MikroORM event-subscriber into a new EncryptedType with global encryptor registration, deterministic-IV (v2) encryption, tenant-scoped context; wires encryptors into Redis TTL cache and S3 object store, updates templates/blueprints/CLI for ENCRYPTION_KEY, and bumps many package versions and tests.

Changes

Cohort / File(s) Summary
Encryption Core & Types
framework/core/src/persistence/encryptedType.ts, framework/core/src/persistence/fieldEncryptor.ts, framework/core/src/persistence/index.ts
Added EncryptedType, tenant async-context helpers (withEncryptionContext, setEncryptionTenantId, getCurrentTenantId), registerEncryptor; changed FieldEncryptor to deterministic IVs and v2: prefix; updated public persistence exports to expose new APIs and remove ComplianceEventSubscriber.
Compliance / Native Blocking
framework/core/src/persistence/complianceEventSubscriber.ts, framework/core/src/persistence/compliancePropertyBuilder.ts
Removed ComplianceEventSubscriber; introduced wrapEmWithNativeQueryBlocking to block native queries on entities with encrypted compliance fields; integrated EncryptedType into property builder for encrypted levels (pii,phi,pci).
Infrastructure Encryption Support
framework/infrastructure/redis/index.ts, framework/infrastructure/S3/index.ts, tests in framework/infrastructure/*/__test__/*
Extended RedisTtlCache and S3ObjectStore constructors to accept encryption option interfaces and perform encrypt/decrypt using tenant-aware encryptor; updated test instantiations to include test encryptors (disabled in tests).
Blueprints & Registrations
blueprint/*/registrations.ts, blueprint/*/mikro-orm.config.ts, blueprint/sample-worker/registrations.ts
Added ENCRYPTION_KEY env wiring, registered encryptor via registerEncryptor(new FieldEncryptor(...)), and passed encryptor into TtlCache/S3ObjectStore factories instead of using a subscriber.
CLI / Codegen / Templates
cli/src/core/ast/.../*.rs, cli/src/core/package_json/package_json_constants.rs, cli/src/templates/project/service/*, cli/src/templates/application/Dockerfile
Updated generated templates to include ENCRYPTION_KEY in Redis/S3 factories, bumped many package-version constants, improved import-injection dedupe, and removed conditional types:build Docker step.
Tenant Filter & Middleware
framework/core/src/persistence/tenantFilter.ts, framework/core/src/http/middleware/request/tenantContext.middleware.ts
Made tenant filter accept optional relation name, prefer column when present, set filter args to false (skip when no tenantId), and set per-request encryption tenant id in middleware via setEncryptionTenantId.
Telemetry Internals & Audit
framework/core/src/http/telemetry/openTelemetryCollector.ts, framework/core/src/http/telemetry/auditLogger.ts
Replaced ECMAScript private fields with underscore-prefixed instance properties for internal logger/service/metrics; adjusted AuditLogger to use _otel.
CLI Release/Manifest & Compliance Scanning
cli/src/release/manifest_generator.rs, cli/src/compliance/audit.rs, cli/src/core/manifest.rs, cli/src/sync/all.rs, cli/src/core/validate.rs
Moved entity compliance/retention from manifest to source-code scan at release/audit time (scan_all_compliance), added compliance section to release manifest, removed manifest-stored entity compliance/validation and removed sync-time manifest updates; handle scan failures gracefully.
ESLint & Minor Cleanups
framework/eslint.config.mjs, framework/express/*, framework/hyper-express/*, framework/common/src/noop.ts
Enforced no-unused-vars rule with ignore patterns and removed scattered file-level eslint-disable comments and an unused type alias.
Package Version & Changelogs
multiple blueprint/*/package.json, framework/*/package.json, many CHANGELOG.md, cli/src/core/package_json/package_json_constants.rs
Bumped many @forklaunch/* package versions across blueprints and framework (e.g., common 1.2.2→1.2.6, core 1.2.3→1.3.3, infrastructure-redis 1.2.3→1.3.1) and added changelog entries documenting encryptor requirement and package upgrades.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Req as Request/Middleware
    participant EM as MikroORM/EntityManager
    participant Type as EncryptedType
    participant Encr as FieldEncryptor
    participant Redis as RedisTtlCache
    participant S3 as S3ObjectStore

    Req->>EM: setFilterParams(tenantId)
    Req->>Type: setEncryptionTenantId(tenantId)
    Note right of Type: AsyncLocalStorage tenant context
    EM->>Type: convertToDatabaseValue(plaintext)
    Type->>Encr: encrypt(tenantDerivedKey, plaintext)
    Encr-->>Type: ciphertext (v2:...)
    Type->>EM: store ciphertext
    EM->>Redis: write(cacheKey, value)
    Redis->>Encr: encrypt value (if enabled)
    Encr-->>Redis: encrypted cache payload
    EM->>S3: putObject(key, body)
    S3->>Encr: encrypt body (if enabled)
    Encr-->>S3: encrypted object body
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I twitch my whiskers, hop and hum,
I wrap your secrets, then I run.
EncryptedType keeps fields so neat,
Tenant keys make data sweet,
Deterministic hops — safe and fun.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: Tenant ID Filter Tightening' accurately describes the primary architectural change: refactoring encryption/compliance mechanisms from event subscribers to custom MikroORM types, alongside tenant filter improvements.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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/tenant-id-fixes

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: 8

Caution

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

⚠️ Outside diff range comments (1)
framework/core/src/persistence/fieldEncryptor.ts (1)

82-113: ⚠️ Potential issue | 🔴 Critical

v2 needs a backfill or dual-read query path before rollout.

decrypt() stays backward-compatible, but any equality filter or uniqueness check that derives its probe value via encrypt() now produces only v2: ciphertext. Existing v1: rows will never compare equal, so tenant/customer filters can silently miss legacy records until those values are rewritten.

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

In `@framework/core/src/persistence/fieldEncryptor.ts` around lines 82 - 113, The
new v2 deterministic encrypt() breaks equality/unique checks for existing v1
rows; add a compatibility path: extend FieldEncryptor.encrypt to accept an
optional version flag (e.g., encrypt(plaintext, tenantId?, version?: 'v1'|'v2'))
and implement a small legacy path that reproduces the v1 probe (or call a new
helper legacyEncrypt/legacyIv that uses the old IV derivation), and update any
query code that builds equality/unique probes to try both versions (generate
both v2 and v1 ciphertexts via encrypt(...,'v2') and encrypt(...,'v1') or use
decrypt fallback) so existing v1 rows are matched until you perform a backfill;
reference encrypt, decrypt, deriveDeterministicIv (and add legacyDeriveIv or
legacyEncrypt helper) when making the changes.
♻️ Duplicate comments (1)
framework/testing/CHANGELOG.md (1)

3-8: ⚠️ Potential issue | 🟠 Major

Same semantic versioning concern as in framework/common/CHANGELOG.md.

The "Encryptor required on redis and s3" entry carries the same semantic versioning concern as noted in the framework/common changelog review. If this makes a previously optional parameter mandatory, it should be a major version bump rather than a patch.

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

In `@framework/testing/CHANGELOG.md` around lines 3 - 8, The changelog entry
"Encryptor required on redis and s3" under header "## 1.2.6" appears to describe
a breaking change (making an optional parameter mandatory); change the release
header from "## 1.2.6" to a major bump (e.g., "## 2.0.0") and mark the entry as
a Breaking Change (or move it to a "Breaking Changes" section), or alternatively
revise the entry to state that the encryptor remains optional if you intend a
patch—update the header and the entry text accordingly so the version and
wording correctly reflect whether the change is breaking.
🧹 Nitpick comments (19)
blueprint/interfaces/worker/CHANGELOG.md (1)

3-20: Consider using more specific patch notes for traceability.

Entries like “upgrade packages” / “package bumps” make it harder to map release intent later. A short list of key bumped packages or affected modules would improve auditability.

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

In `@blueprint/interfaces/worker/CHANGELOG.md` around lines 3 - 20, The CHANGELOG
entries under headings like "## 1.0.13", "## 1.0.12", and "## 1.0.11" are too
generic ("upgrade packages", "package bumps"); update each release note to list
the specific packages or modules updated (e.g., package names and new versions),
include any important affected components or breaking changes, and optionally
reference PR/issue numbers for traceability so future readers can map intent to
changes.
blueprint/interfaces/billing/CHANGELOG.md (1)

3-20: Changelog format is consistent; consider adding concrete change detail.

The version blocks are correctly structured. Optional improvement: mention specific package/version deltas to make regressions easier to trace.

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

In `@blueprint/interfaces/billing/CHANGELOG.md` around lines 3 - 20, The changelog
entries for the version headers (e.g., "## 1.0.13", "## 1.0.12", "## 1.0.11")
are too generic; update each version block to list concrete changes by naming
the packages and their new versions (or the semver delta) and a short note on
impact (e.g., "bumped lodash 4.17.21 -> 4.17.22: patch security fix"), so
readers can trace regressions easily.
blueprint/billing-base/registrations.ts (1)

1-27: Reorder imports to match the required layering.

Line 1-27 places external dependencies (@mikro-orm/*) after Forklaunch framework imports. Please move external imports above the @forklaunch/* group.

♻️ Suggested import-order adjustment
+import { ForkOptions } from '@mikro-orm/core';
+import { EntityManager, MikroORM } from '@mikro-orm/postgresql';
 import {
   number,
   optional,
   schemaValidator,
   SchemaValidator,
   string
 } from '@forklaunch/blueprint-core';
 import { Metrics, metrics } from '@forklaunch/blueprint-monitoring';
 import { OpenTelemetryCollector } from '@forklaunch/core/http';
 import { FieldEncryptor } from '@forklaunch/core/persistence';
@@
-import { ForkOptions } from '@mikro-orm/core';
-import { EntityManager, MikroORM } from '@mikro-orm/postgresql';

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 `@blueprint/billing-base/registrations.ts` around lines 1 - 27, Imports in
registrations.ts are out of the mandated layering: external packages
(`@mikro-orm/core`, `@mikro-orm/postgresql`) are listed after Forklaunch framework
imports; reorder imports so external dependencies appear before any
`@forklaunch/`* imports. Locate the import block at the top of registrations.ts
(symbols to find: imports of MikroORM, EntityManager, ForkOptions) and move
those external imports (e.g., from '@mikro-orm/core' and
'@mikro-orm/postgresql') up above the group of `@forklaunch/`* imports, keeping
the rest of the groups intact and preserving existing relative order within each
layer.
framework/infrastructure/redis/__test__/redisTtlCache.test.ts (1)

28-31: Add at least one encryption-enabled Redis round-trip test.

The cache instance at lines 28-31 sets disabled: true, so the encryption path is never exercised. Add a test with disabled: false to verify the encryption integration works end-to-end.

Example test addition
+  test('encryptor-enabled round trip', async () => {
+    const encryptedCache = new RedisTtlCache(
+      5000,
+      new OpenTelemetryCollector('test'),
+      { url: `redis://${container.getHost()}:${container.getMappedPort(6379)}` },
+      { enabled: true, level: 'info' },
+      {
+        encryptor: new FieldEncryptor('test-encryption-key-for-redis-tests'),
+        disabled: false
+      }
+    );
+
+    await encryptedCache.putRecord({ key: 'enc_key', value: { ok: true }, ttlMilliseconds: 1000 });
+    await expect(encryptedCache.readRecord('enc_key')).resolves.toMatchObject({
+      key: 'enc_key',
+      value: { ok: true }
+    });
+    await encryptedCache.disconnect();
+  });
🤖 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
28 - 31, The current test uses FieldEncryptor with disabled: true so the
encryption path isn't exercised; add a new test in redisTtlCache.test.ts that
creates the cache with encryptor: new
FieldEncryptor('test-encryption-key-for-redis-tests') and disabled: false, then
perform an end-to-end round-trip: await cache.set('some-key', someValue, ttl)
and await cache.get('some-key') and assert the retrieved value equals the
original to verify encryption/decryption works; ensure the test cleans up
(flush/close) after itself.
framework/infrastructure/S3/__test__/s3ObjectStore.test.ts (1)

1-6: Normalize imports to node: prefix + required layer order.

Readable should be imported as a Node built-in (node:stream) and grouped before external/Forklaunch imports.

Suggested import normalization
+import { Readable } from 'node:stream';
+
 import { S3Client } from '@aws-sdk/client-s3';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
 import { OpenTelemetryCollector } from '@forklaunch/core/http';
 import { FieldEncryptor } from '@forklaunch/core/persistence';
-import { Readable } from 'stream';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
 import { S3ObjectStore } from '../index';
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/S3/__test__/s3ObjectStore.test.ts` around lines 1 -
6, Import ordering and source for Readable is incorrect: change the Readable
import to use the Node built-in with the node: prefix (import Readable from
'node:stream') and reorder imports so Node built-ins (Readable) come first, then
external deps (S3Client), then Forklaunch framework packages
(OpenTelemetryCollector, FieldEncryptor), and finally the local module import
(S3ObjectStore) to follow the project's 7-layer convention; update the import
statements around S3Client, OpenTelemetryCollector, FieldEncryptor, Readable,
and S3ObjectStore accordingly.
blueprint/iam-base/mikro-orm.config.ts (1)

1-15: Reorder imports to match the required layer sequence.

Current ordering places Forklaunch framework imports before external dependencies.

Suggested import ordering
-import { number, schemaValidator, string } from '@forklaunch/blueprint-core';
-
-import {
-  FieldEncryptor,
-  registerEncryptor
-} from '@forklaunch/core/persistence';
-import {
-  createConfigInjector,
-  getEnvVar,
-  Lifetime
-} from '@forklaunch/core/services';
 import { Migrator } from '@mikro-orm/migrations';
 import { defineConfig, Platform, TextType, Type } from '@mikro-orm/postgresql';
 import dotenv from 'dotenv';
+
+import { number, schemaValidator, string } from '@forklaunch/blueprint-core';
+import { FieldEncryptor, registerEncryptor } from '@forklaunch/core/persistence';
+import { createConfigInjector, getEnvVar, Lifetime } from '@forklaunch/core/services';
 import * as entities from './persistence/entities';
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 `@blueprint/iam-base/mikro-orm.config.ts` around lines 1 - 15, The import
statements in mikro-orm.config.ts are out of the mandated 7-layer order:
external deps should come before Forklaunch framework packages and local
persistence; reorder the imports so they follow the layers: (1) Node built-ins
(if any), (2) external dependencies (dotenv, `@mikro-orm/`*), (3) Forklaunch
framework packages (`@forklaunch/core/`* and `@forklaunch/blueprint-core`), (4)
cross-module imports (createConfigInjector, getEnvVar, Lifetime references), (5)
local persistence (import * as entities), (6) local domain, (7) same-directory;
specifically move dotenv and { defineConfig, Platform, TextType, Type } from
'@mikro-orm/postgresql' and { Migrator } into the external block above the
Forklaunch imports (number, schemaValidator, string; FieldEncryptor,
registerEncryptor; createConfigInjector, getEnvVar, Lifetime) and keep the
entities import in the local persistence block so the file adheres to the
required import-layer ordering.
framework/core/__test__/fieldEncryptor.test.ts (2)

86-91: Misleading test description.

The test description says "unknown version prefix" but the ciphertext 'v2:abc:def:ghi' uses the valid v2: prefix. The test is actually validating that malformed ciphertext structure (invalid base64 segments) throws DecryptionError, not an unknown version prefix. Consider updating the description for clarity.

📝 Proposed fix for test description
-  it('should throw DecryptionError for unknown version prefix', () => {
+  it('should throw DecryptionError for malformed ciphertext structure', () => {
     const enc = new FieldEncryptor(MASTER_KEY);
     expect(() => enc.decrypt('v2:abc:def:ghi', 'tenant-a')).toThrow(
       DecryptionError
     );
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/core/__test__/fieldEncryptor.test.ts` around lines 86 - 91, The
test description is misleading: update the it(...) description for the test that
constructs new FieldEncryptor(MASTER_KEY) and calls
enc.decrypt('v2:abc:def:ghi', 'tenant-a') to state that it expects a
DecryptionError for malformed ciphertext/invalid base64 segments rather than
"unknown version prefix"; keep the assertion and DecryptionError check unchanged
so the test verifies decrypt throws for invalid ciphertext structure.

39-45: Duplicate test case.

This test (lines 39-45) tests the same behavior as lines 23-29 — both verify that the same plaintext encrypted under different tenant IDs yields different ciphertext. Consider removing one to avoid redundant test coverage.

🧹 Proposed fix to remove duplicate test
-  it('should produce different ciphertext for same plaintext with different tenants', () => {
-    const enc = new FieldEncryptor(MASTER_KEY);
-    const plaintext = 'same-data';
-    const ct1 = enc.encrypt(plaintext, 'tenant-a');
-    const ct2 = enc.encrypt(plaintext, 'tenant-b');
-    expect(ct1).not.toBe(ct2);
-  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/core/__test__/fieldEncryptor.test.ts` around lines 39 - 45, Remove
the duplicate test that validates different ciphertexts for the same plaintext
under different tenants: locate the second test block with the description
"should produce different ciphertext for same plaintext with different tenants"
that instantiates FieldEncryptor with MASTER_KEY and calls
enc.encrypt(plaintext, 'tenant-a') and enc.encrypt(plaintext, 'tenant-b') (this
duplicates the earlier test around lines 23-29) and delete that redundant
it(...) block so only one test asserting ct1 !== ct2 remains.
cli/src/core/ast/infrastructure/redis.rs (1)

72-80: No action required — FieldEncryptor and ENCRYPTION_KEY are properly injected by parent transformations.

The FieldEncryptor import and ENCRYPTION_KEY environment variable are not registered within the redis.rs module functions themselves. However, this is by design: both transform_service_to_worker.rs and transform_registrations_ts.rs handle these injections separately for non-database workers before or after calling the redis utilities. The generated TypeScript code correctly includes both the import statement and environment configuration.

To improve maintainability, consider documenting that redis_ttl_cache_runtime_dependency() relies on the parent transformation to provide these prerequisites, or refactor to make the function more self-contained.

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

In `@cli/src/core/ast/infrastructure/redis.rs` around lines 72 - 80, The redis
module currently assumes FieldEncryptor import and ENCRYPTION_KEY are injected
by parent transforms; update redis_ttl_cache_runtime_dependency (or the factory
that constructs RedisTtlCache) to explicitly document this prerequisite and/or
make it self-contained by accepting an encryptor or encryptionKey parameter
(e.g., add an optional encryptor/ encryptionKey argument to
redis_ttl_cache_runtime_dependency and pass it into the factory that creates new
RedisTtlCache) and update the function signature and callers
(transform_service_to_worker.rs and transform_registrations_ts.rs) to provide
the dependency if present.
cli/src/templates/project/service/mikro-orm.config.ts (1)

1-9: Reorder this import block to the standard 7 layers.

@mikro-orm/* and dotenv belong in the external-dependency layer and should come before the @forklaunch/* imports.

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 `@cli/src/templates/project/service/mikro-orm.config.ts` around lines 1 - 9,
Reorder the import block to follow the 7-layer convention: place external
dependencies first (move imports for Migrator from '@mikro-orm/*', defineConfig
from '@mikro-orm/{{database}}', and dotenv) before the Forklaunch framework
imports; then keep Forklaunch framework packages (createConfigInjector,
getEnvVar, Lifetime from '@forklaunch/core/services' and
FieldEncryptor/registerEncryptor from '@forklaunch/core/persistence'); then
cross-module imports (number, SchemaValidator, string from
'@{{app_name}}/core'); followed by local persistence imports (import * as
entities from './persistence/entities'); ensure conditional imports like
Platform/TextType/Type remain in the same external layer when present, and
preserve relative ordering and blank lines between each of the 7 layers.
blueprint/billing-stripe/registrations.ts (1)

1-29: Reorder this import block to match the repo’s 7-layer convention.

@mikro-orm/* and stripe are external dependencies, so they should sit above the @forklaunch/* imports instead of below them.

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 `@blueprint/billing-stripe/registrations.ts` around lines 1 - 29, Reorder the
import block to follow the repo’s 7-layer convention by moving external
dependencies (e.g., Stripe and `@mikro-orm/`* like Stripe from 'stripe' and {
ForkOptions } from '@mikro-orm/core' / { EntityManager, MikroORM } from
'@mikro-orm/postgresql') above the Forklaunch framework imports (those starting
with '@forklaunch/*'); keep the rest of the existing imports
(OpenTelemetryCollector, FieldEncryptor, ComplianceDataService,
createConfigInjector, StripeBillingPortalService, RedisTtlCache, etc.) in their
original relative order after the external deps so the file-level imports match
the repo guideline.
cli/src/templates/project/service/registrations.ts (2)

154-159: ENCRYPTION_KEY may be unused in some configurations.

Similar to the import, ENCRYPTION_KEY is always defined but only consumed when Redis cache, S3, or non-database workers are enabled. If none apply, this creates an unused config entry and requires an environment variable that won't be used.

Consider wrapping this configuration in the appropriate conditional blocks to avoid requiring unnecessary environment variables.

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

In `@cli/src/templates/project/service/registrations.ts` around lines 154 - 159,
ENCRYPTION_KEY is always registered via the ENCRYPTION_KEY binding
(Lifetime.Singleton, value: getEnvVar('ENCRYPTION_KEY')) even though it's only
needed when Redis cache, S3, or non-database workers are enabled; move or wrap
this registration in the same conditional blocks used for those features so the
container only defines ENCRYPTION_KEY when isRedisEnabled || isS3Enabled ||
isNonDbWorker (or equivalent flags) is true, ensuring
getEnvVar('ENCRYPTION_KEY') is only called/required when one of those features
is active.

12-13: Potential unused import in generated code.

FieldEncryptor is imported unconditionally but is only used when is_request_cache_needed, is_s3_enabled, or (is_worker && !is_database_worker) is true. If none of these conditions apply, the generated code will have an unused import.

Consider wrapping the import in a conditional:

{{`#is_encryption_needed`}}import { FieldEncryptor } from "@forklaunch/core/persistence";{{/is_encryption_needed}}

Where is_encryption_needed is derived from the relevant feature flags.

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

In `@cli/src/templates/project/service/registrations.ts` around lines 12 - 13, The
import of FieldEncryptor is unconditional in the template (registrations.ts) but
only used when request cache, S3, or worker-without-database features are
enabled; change the template to conditionally emit the import by adding a new
composite flag (e.g., is_encryption_needed) that is true when
is_request_cache_needed || is_s3_enabled || (is_worker && !is_database_worker),
then wrap the existing import line "import { FieldEncryptor } from
\"@forklaunch/core/persistence\";" in a mustache conditional block
({{`#is_encryption_needed`}}...{{/is_encryption_needed}}) so the import only
appears when FieldEncryptor is actually used.
framework/infrastructure/S3/index.ts (2)

116-124: Silent decryption failure may mask data corruption.

When decryption fails (e.g., wrong tenant key, corrupted ciphertext), the method silently returns the raw encrypted value. This could lead to downstream JSON parse errors or data integrity issues that are hard to diagnose.

Consider logging decryption failures or providing a configuration option to throw on decryption errors in non-production environments.

♻️ Proposed improvement
  private decryptBody(body: string): string {
    if (!this.encryptor || this.encryptionDisabled) return body;
    if (!isEncrypted(body)) return body;
    try {
      return this.encryptor.decrypt(body, getCurrentTenantId()) ?? body;
    } catch {
+     // Log decryption failure for debugging - this may indicate key mismatch or corruption
+     this.openTelemetryCollector.warn('S3 object decryption failed, returning raw value');
      return body;
    }
  }
🤖 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 116 - 124, The decryptBody
method currently swallows all decryption failures and returns the raw encrypted
payload; update decryptBody to catch errors from this.encryptor.decrypt and: 1)
log the error (including the tenant id from getCurrentTenantId() and the fact
the input was encrypted) using the module/logger used elsewhere so failures are
visible, and 2) make behavior configurable (e.g., an instance flag like
encryptionDisabled already exists or a new throwOnDecryptionError option) so in
non-production/tests the method re-throws the error instead of returning the raw
value; ensure you still check this.encryptor, encryptionDisabled, and
isEncrypted before attempting decryption and preserve the fallback to returning
body only when configured to do so.

154-160: ContentType mismatch: encrypted body stored as 'application/json'.

The body is now encrypted ciphertext (a string like v2:...), but ContentType is still set to 'application/json'. This is technically incorrect for encrypted data and could confuse debugging or S3 object inspection tools.

Consider using a custom content type or metadata to indicate encryption status.

♻️ Suggested fix
    const params: PutObjectCommandInput = {
      Bucket: this.bucket,
      Key: key,
      Body: body,
-     ContentType: 'application/json'
+     ContentType: this.encryptionDisabled ? 'application/json' : 'application/octet-stream',
+     Metadata: this.encryptionDisabled ? undefined : { encrypted: 'true' }
    };
🤖 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 154 - 160, The S3 upload
currently sets ContentType to 'application/json' even though the body is
encrypted via encryptBody; update the PutObjectCommandInput construction in the
method that builds params (where encryptBody and const params are used) to
reflect encrypted content—e.g., set ContentType to a generic type like
'application/octet-stream' or a custom type such as
'application/vnd.<app>-encrypted' and add metadata flags (e.g., Metadata: {
encrypted: 'true', original-content-type: 'application/json' }) so callers and
tools know the object is encrypted and the original type is preserved.
framework/infrastructure/redis/index.ts (1)

87-102: Consistent with S3ObjectStore, but same silent failure concern.

The encryption/decryption helpers mirror the S3ObjectStore implementation, which is good for consistency. However, the same concern applies: silent decryption failures could mask issues.

Consider adding observability (logging/metrics) when decryption fails to aid debugging.

🤖 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 87 - 102, The
decryptValue/encryptValue helpers currently swallow decryption errors; update
decryptValue (and optionally encryptValue) to emit observability when decryption
fails or when an unexpected condition occurs: capture the exception in
decryptValue and call the module logger/metrics (e.g., processLogger.error or a
provided logger/metrics client) with context including the function name
decryptValue, the tenant id from getCurrentTenantId(), and a safely redacted
snippet or indication of the value (use isEncrypted(value) to gate logging);
ensure you still return the original value on failure but record the error so
failures are observable. Reference encryptValue, decryptValue, isEncrypted,
encryptor, and getCurrentTenantId when locating the code to modify.
framework/core/__test__/complianceEventSubscriber.test.ts (1)

38-42: Consider isolating global encryptor state between tests.

registerEncryptor modifies module-level state (_encryptor). If other test files in the same test run also call registerEncryptor, there could be test pollution. Consider adding an afterEach or afterAll to reset the encryptor state, or exposing a clearEncryptor() helper for test cleanup.

♻️ Suggested cleanup pattern
+ import { registerEncryptor, clearEncryptor } from '../src/persistence/encryptedType';
+ // ... or add a clearEncryptor export if not available

  beforeEach(() => {
    const encryptor = new FieldEncryptor(MASTER_KEY);
    registerEncryptor(encryptor);
    encryptedType = new EncryptedType('string');
  });

+ afterEach(() => {
+   // Reset global state to prevent test pollution
+   registerEncryptor(undefined as any); // or clearEncryptor() if available
+ });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/core/__test__/complianceEventSubscriber.test.ts` around lines 38 -
42, Tests registerEncryptor in beforeEach which mutates module-level _encryptor
and can leak state across tests; add cleanup to reset that global after each
test by either calling an exported clearEncryptor() helper (implement and export
clearEncryptor that sets _encryptor = undefined) or invoking a provided reset
function in an afterEach/afterAll; update the test file (around the beforeEach
block where FieldEncryptor and EncryptedType are created) to call
clearEncryptor() in afterEach to isolate state between tests.
framework/core/src/persistence/encryptedType.ts (1)

153-169: Number deserialization could return NaN for corrupted data.

Number(value) returns NaN for non-numeric strings. If encrypted data is corrupted or decryption partially fails, this could silently produce NaN values in the application.

Consider adding validation:

♻️ Suggested validation
      case 'number':
-       return Number(value);
+       const num = Number(value);
+       if (Number.isNaN(num)) {
+         // Log warning for debugging
+         return null;
+       }
+       return num;
🤖 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 153 - 169, In
deserializeValue, when originalType === 'number' validate the parsed numeric
result instead of blindly returning Number(value); call Number(value), check for
isFinite/Number.isNaN (or Number.isFinite) and handle invalid parsing by
throwing or returning a safe fallback (e.g., null or the raw string) to avoid
silently producing NaN — update the number branch in deserializeValue to perform
this validation and return the chosen fallback or raise an error so callers
don't receive NaN.
framework/core/src/persistence/tenantFilter.ts (1)

37-42: Consider exporting setTenantContext middleware for automatic HTTP route protection.

The skip filtering behavior when args?.tenantId is falsy (lines 37-42) is intentional for startup, background jobs, and other non-request contexts. However, setTenantContext middleware in framework/core/src/http/middleware/request/tenantContext.middleware.ts is not exported from the http module's public API, so applications can't easily apply it to guarantee tenant context is set for protected/authenticated routes before queries execute.

For protected/authenticated routes, the middleware auto-returns 403 if tenantId is missing—preventing the fail-open scenario. Export this middleware and document its usage to ensure developers can leverage this built-in protection rather than manually managing tenant context in each handler.

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

In `@framework/core/src/persistence/tenantFilter.ts` around lines 37 - 42, Export
the setTenantContext middleware from the http module's public API so
applications can easily apply it to protected routes; specifically, add a
re-export of the setTenantContext symbol (defined in
tenantContext.middleware.ts) from the http module's top-level/barrel export (the
http index that currently exposes middleware utilities) so consumers can import
it directly (e.g., from the http package) and apply it to authenticated routes
to auto-403 when args.tenantId is missing. Ensure the exported symbol name is
exactly setTenantContext and update any module export lists or index.ts/barrel
files accordingly.
🤖 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/iam-base/mikro-orm.config.ts`:
- Around line 62-64: registerEncryptor is being called at module scope and can
silently overwrite previous encryptors; update the registerEncryptor
implementation in framework/core/src/persistence/encryptedType.ts to enforce
idempotency: detect if an encryptor is already registered and either (a) throw
an error, (b) emit a clear warning, or (c) validate that the new value is the
same instance before returning — pick one policy, implement it (e.g., store the
first instance in a private variable and compare references on subsequent
calls), and update any callers like mikro-orm.config.ts (which currently calls
registerEncryptor(new
FieldEncryptor(validConfigInjector.resolve(tokens.ENCRYPTION_KEY)))) to expect
the new behavior. Ensure the check uses the existing registerEncryptor symbol
and preserves current behavior when the same instance is re-registered.

In `@blueprint/implementations/worker/bullmq/CHANGELOG.md`:
- Around line 27-32: The 1.0.12 changelog entry is too generic; update the "##
1.0.12" section (the heading and the "- update fl packages" bullet) to list
explicit package names and exact versions that were updated (e.g., include
entries like "@forklaunch/interfaces-worker vX.Y.Z" and any other changed fl
packages), matching the style used in nearby releases so audits can see exact
dependency changes; ensure the bullet points enumerate each package and version
and keep formatting consistent with other changelog entries.

In `@blueprint/implementations/worker/kafka/CHANGELOG.md`:
- Around line 27-32: The 1.0.12 changelog entry under "## 1.0.12" currently says
only "update fl packages"; update it to list the specific dependency bumps like
the other releases do (i.e., name each fl package and its new version), editing
the "update fl packages" line in the CHANGELOG.md so the entry includes concrete
package names and version numbers for consistency with prior entries.

In `@framework/common/CHANGELOG.md`:
- Around line 3-8: The changelog claims a patch release ("1.2.6") but the change
"Encryptor required on redis and s3" is breaking because it makes the
previously-optional encryption parameter mandatory; either relax the constructor
signatures back to accept an optional encryption parameter (e.g., in the S3
constructor in framework/infrastructure/S3/index.ts and the Redis client
constructor) or, if the required change is intended, update the release metadata
and CHANGELOG entry to a major version bump (e.g., 2.0.0) and note the breaking
change; locate and adjust the relevant constructor signatures or bump the
version and changelog heading accordingly.

In `@framework/core/src/http/telemetry/openTelemetryCollector.ts`:
- Around line 161-165: getMetric currently force-casts this._metrics[metricId]
which can be undefined and causes downstream crashes; update the getMetric
method to check whether this._metrics[metricId] is present and, if missing,
throw a descriptive error (e.g., `Metric "${String(metricId)}" not registered`)
or return a properly typed undefined/optional value depending on API
expectations; locate the getMetric function and the _metrics map in
openTelemetryCollector.ts and replace the direct cast with a guarded check that
either throws the error including metricId or returns a safe optional result
before returning the MetricType.

In `@framework/core/src/persistence/fieldEncryptor.ts`:
- Around line 67-79: The deriveDeterministicIv function currently generates a
deterministic IV (nonce) via HMAC-SHA256 which causes IV reuse with AES-256-GCM
(function deriveDeterministicIv, constant IV_BYTES) and is insecure; replace
this design by removing deterministic IV derivation for AES-GCM and instead
implement a safe alternative: either switch encryption to a
nonce-misuse-resistant AEAD (e.g., AES-SIV / AES-GCM-SIV) for
deterministic/searchable needs, or keep AES-GCM but generate a cryptographically
random IV per encryption and implement a separate deterministic lookup mechanism
(e.g., HMAC-based blind index) that uses a separate key and includes field/store
domain in the HMAC input to avoid cross-store linkability; update callers of
deriveDeterministicIv (and any code assuming deterministic IVs) to use the new
AEAD or to store the blind index alongside the random-IV ciphertext.

In `@framework/express/CHANGELOG.md`:
- Line 41: Replace the vague changelog entry "up packages" with a clear,
explicit release note: locate the line containing the exact phrase "up packages"
in CHANGELOG.md and change it to a descriptive entry such as "chore: bump
dependencies — updated [packageA]@x.y.z, [packageB]@a.b.c to address
security/compatibility" (or similar wording that lists the updated packages and
the reason for the bump); ensure the new line follows the project's changelog
style and includes which packages were updated and why.

In `@framework/validator/CHANGELOG.md`:
- Around line 23-24: Replace the ambiguous changelog entries "up packages" and
"update packages" with concrete, consumer-facing notes: identify the exact
packages and target versions or intent (e.g., "Bump lodash to 4.17.21 to fix
CVE-XXXX" or "Upgrade react to ^18.2.0 for breaking API X") for the entries
currently labeled "up packages" (line marked in the diff) and "update packages"
(also noted); update the CHANGELOG.md entries so each line contains the package
name(s), version change or range, and a one-line rationale/impact statement.

---

Outside diff comments:
In `@framework/core/src/persistence/fieldEncryptor.ts`:
- Around line 82-113: The new v2 deterministic encrypt() breaks equality/unique
checks for existing v1 rows; add a compatibility path: extend
FieldEncryptor.encrypt to accept an optional version flag (e.g.,
encrypt(plaintext, tenantId?, version?: 'v1'|'v2')) and implement a small legacy
path that reproduces the v1 probe (or call a new helper legacyEncrypt/legacyIv
that uses the old IV derivation), and update any query code that builds
equality/unique probes to try both versions (generate both v2 and v1 ciphertexts
via encrypt(...,'v2') and encrypt(...,'v1') or use decrypt fallback) so existing
v1 rows are matched until you perform a backfill; reference encrypt, decrypt,
deriveDeterministicIv (and add legacyDeriveIv or legacyEncrypt helper) when
making the changes.

---

Duplicate comments:
In `@framework/testing/CHANGELOG.md`:
- Around line 3-8: The changelog entry "Encryptor required on redis and s3"
under header "## 1.2.6" appears to describe a breaking change (making an
optional parameter mandatory); change the release header from "## 1.2.6" to a
major bump (e.g., "## 2.0.0") and mark the entry as a Breaking Change (or move
it to a "Breaking Changes" section), or alternatively revise the entry to state
that the encryptor remains optional if you intend a patch—update the header and
the entry text accordingly so the version and wording correctly reflect whether
the change is breaking.

---

Nitpick comments:
In `@blueprint/billing-base/registrations.ts`:
- Around line 1-27: Imports in registrations.ts are out of the mandated
layering: external packages (`@mikro-orm/core`, `@mikro-orm/postgresql`) are listed
after Forklaunch framework imports; reorder imports so external dependencies
appear before any `@forklaunch/`* imports. Locate the import block at the top of
registrations.ts (symbols to find: imports of MikroORM, EntityManager,
ForkOptions) and move those external imports (e.g., from '@mikro-orm/core' and
'@mikro-orm/postgresql') up above the group of `@forklaunch/`* imports, keeping
the rest of the groups intact and preserving existing relative order within each
layer.

In `@blueprint/billing-stripe/registrations.ts`:
- Around line 1-29: Reorder the import block to follow the repo’s 7-layer
convention by moving external dependencies (e.g., Stripe and `@mikro-orm/`* like
Stripe from 'stripe' and { ForkOptions } from '@mikro-orm/core' / {
EntityManager, MikroORM } from '@mikro-orm/postgresql') above the Forklaunch
framework imports (those starting with '@forklaunch/*'); keep the rest of the
existing imports (OpenTelemetryCollector, FieldEncryptor, ComplianceDataService,
createConfigInjector, StripeBillingPortalService, RedisTtlCache, etc.) in their
original relative order after the external deps so the file-level imports match
the repo guideline.

In `@blueprint/iam-base/mikro-orm.config.ts`:
- Around line 1-15: The import statements in mikro-orm.config.ts are out of the
mandated 7-layer order: external deps should come before Forklaunch framework
packages and local persistence; reorder the imports so they follow the layers:
(1) Node built-ins (if any), (2) external dependencies (dotenv, `@mikro-orm/`*),
(3) Forklaunch framework packages (`@forklaunch/core/`* and
`@forklaunch/blueprint-core`), (4) cross-module imports (createConfigInjector,
getEnvVar, Lifetime references), (5) local persistence (import * as entities),
(6) local domain, (7) same-directory; specifically move dotenv and {
defineConfig, Platform, TextType, Type } from '@mikro-orm/postgresql' and {
Migrator } into the external block above the Forklaunch imports (number,
schemaValidator, string; FieldEncryptor, registerEncryptor;
createConfigInjector, getEnvVar, Lifetime) and keep the entities import in the
local persistence block so the file adheres to the required import-layer
ordering.

In `@blueprint/interfaces/billing/CHANGELOG.md`:
- Around line 3-20: The changelog entries for the version headers (e.g., "##
1.0.13", "## 1.0.12", "## 1.0.11") are too generic; update each version block to
list concrete changes by naming the packages and their new versions (or the
semver delta) and a short note on impact (e.g., "bumped lodash 4.17.21 ->
4.17.22: patch security fix"), so readers can trace regressions easily.

In `@blueprint/interfaces/worker/CHANGELOG.md`:
- Around line 3-20: The CHANGELOG entries under headings like "## 1.0.13", "##
1.0.12", and "## 1.0.11" are too generic ("upgrade packages", "package bumps");
update each release note to list the specific packages or modules updated (e.g.,
package names and new versions), include any important affected components or
breaking changes, and optionally reference PR/issue numbers for traceability so
future readers can map intent to changes.

In `@cli/src/core/ast/infrastructure/redis.rs`:
- Around line 72-80: The redis module currently assumes FieldEncryptor import
and ENCRYPTION_KEY are injected by parent transforms; update
redis_ttl_cache_runtime_dependency (or the factory that constructs
RedisTtlCache) to explicitly document this prerequisite and/or make it
self-contained by accepting an encryptor or encryptionKey parameter (e.g., add
an optional encryptor/ encryptionKey argument to
redis_ttl_cache_runtime_dependency and pass it into the factory that creates new
RedisTtlCache) and update the function signature and callers
(transform_service_to_worker.rs and transform_registrations_ts.rs) to provide
the dependency if present.

In `@cli/src/templates/project/service/mikro-orm.config.ts`:
- Around line 1-9: Reorder the import block to follow the 7-layer convention:
place external dependencies first (move imports for Migrator from
'@mikro-orm/*', defineConfig from '@mikro-orm/{{database}}', and dotenv) before
the Forklaunch framework imports; then keep Forklaunch framework packages
(createConfigInjector, getEnvVar, Lifetime from '@forklaunch/core/services' and
FieldEncryptor/registerEncryptor from '@forklaunch/core/persistence'); then
cross-module imports (number, SchemaValidator, string from
'@{{app_name}}/core'); followed by local persistence imports (import * as
entities from './persistence/entities'); ensure conditional imports like
Platform/TextType/Type remain in the same external layer when present, and
preserve relative ordering and blank lines between each of the 7 layers.

In `@cli/src/templates/project/service/registrations.ts`:
- Around line 154-159: ENCRYPTION_KEY is always registered via the
ENCRYPTION_KEY binding (Lifetime.Singleton, value: getEnvVar('ENCRYPTION_KEY'))
even though it's only needed when Redis cache, S3, or non-database workers are
enabled; move or wrap this registration in the same conditional blocks used for
those features so the container only defines ENCRYPTION_KEY when isRedisEnabled
|| isS3Enabled || isNonDbWorker (or equivalent flags) is true, ensuring
getEnvVar('ENCRYPTION_KEY') is only called/required when one of those features
is active.
- Around line 12-13: The import of FieldEncryptor is unconditional in the
template (registrations.ts) but only used when request cache, S3, or
worker-without-database features are enabled; change the template to
conditionally emit the import by adding a new composite flag (e.g.,
is_encryption_needed) that is true when is_request_cache_needed || is_s3_enabled
|| (is_worker && !is_database_worker), then wrap the existing import line
"import { FieldEncryptor } from \"@forklaunch/core/persistence\";" in a mustache
conditional block ({{`#is_encryption_needed`}}...{{/is_encryption_needed}}) so the
import only appears when FieldEncryptor is actually used.

In `@framework/core/__test__/complianceEventSubscriber.test.ts`:
- Around line 38-42: Tests registerEncryptor in beforeEach which mutates
module-level _encryptor and can leak state across tests; add cleanup to reset
that global after each test by either calling an exported clearEncryptor()
helper (implement and export clearEncryptor that sets _encryptor = undefined) or
invoking a provided reset function in an afterEach/afterAll; update the test
file (around the beforeEach block where FieldEncryptor and EncryptedType are
created) to call clearEncryptor() in afterEach to isolate state between tests.

In `@framework/core/__test__/fieldEncryptor.test.ts`:
- Around line 86-91: The test description is misleading: update the it(...)
description for the test that constructs new FieldEncryptor(MASTER_KEY) and
calls enc.decrypt('v2:abc:def:ghi', 'tenant-a') to state that it expects a
DecryptionError for malformed ciphertext/invalid base64 segments rather than
"unknown version prefix"; keep the assertion and DecryptionError check unchanged
so the test verifies decrypt throws for invalid ciphertext structure.
- Around line 39-45: Remove the duplicate test that validates different
ciphertexts for the same plaintext under different tenants: locate the second
test block with the description "should produce different ciphertext for same
plaintext with different tenants" that instantiates FieldEncryptor with
MASTER_KEY and calls enc.encrypt(plaintext, 'tenant-a') and
enc.encrypt(plaintext, 'tenant-b') (this duplicates the earlier test around
lines 23-29) and delete that redundant it(...) block so only one test asserting
ct1 !== ct2 remains.

In `@framework/core/src/persistence/encryptedType.ts`:
- Around line 153-169: In deserializeValue, when originalType === 'number'
validate the parsed numeric result instead of blindly returning Number(value);
call Number(value), check for isFinite/Number.isNaN (or Number.isFinite) and
handle invalid parsing by throwing or returning a safe fallback (e.g., null or
the raw string) to avoid silently producing NaN — update the number branch in
deserializeValue to perform this validation and return the chosen fallback or
raise an error so callers don't receive NaN.

In `@framework/core/src/persistence/tenantFilter.ts`:
- Around line 37-42: Export the setTenantContext middleware from the http
module's public API so applications can easily apply it to protected routes;
specifically, add a re-export of the setTenantContext symbol (defined in
tenantContext.middleware.ts) from the http module's top-level/barrel export (the
http index that currently exposes middleware utilities) so consumers can import
it directly (e.g., from the http package) and apply it to authenticated routes
to auto-403 when args.tenantId is missing. Ensure the exported symbol name is
exactly setTenantContext and update any module export lists or index.ts/barrel
files accordingly.

In `@framework/infrastructure/redis/__test__/redisTtlCache.test.ts`:
- Around line 28-31: The current test uses FieldEncryptor with disabled: true so
the encryption path isn't exercised; add a new test in redisTtlCache.test.ts
that creates the cache with encryptor: new
FieldEncryptor('test-encryption-key-for-redis-tests') and disabled: false, then
perform an end-to-end round-trip: await cache.set('some-key', someValue, ttl)
and await cache.get('some-key') and assert the retrieved value equals the
original to verify encryption/decryption works; ensure the test cleans up
(flush/close) after itself.

In `@framework/infrastructure/redis/index.ts`:
- Around line 87-102: The decryptValue/encryptValue helpers currently swallow
decryption errors; update decryptValue (and optionally encryptValue) to emit
observability when decryption fails or when an unexpected condition occurs:
capture the exception in decryptValue and call the module logger/metrics (e.g.,
processLogger.error or a provided logger/metrics client) with context including
the function name decryptValue, the tenant id from getCurrentTenantId(), and a
safely redacted snippet or indication of the value (use isEncrypted(value) to
gate logging); ensure you still return the original value on failure but record
the error so failures are observable. Reference encryptValue, decryptValue,
isEncrypted, encryptor, and getCurrentTenantId when locating the code to modify.

In `@framework/infrastructure/S3/__test__/s3ObjectStore.test.ts`:
- Around line 1-6: Import ordering and source for Readable is incorrect: change
the Readable import to use the Node built-in with the node: prefix (import
Readable from 'node:stream') and reorder imports so Node built-ins (Readable)
come first, then external deps (S3Client), then Forklaunch framework packages
(OpenTelemetryCollector, FieldEncryptor), and finally the local module import
(S3ObjectStore) to follow the project's 7-layer convention; update the import
statements around S3Client, OpenTelemetryCollector, FieldEncryptor, Readable,
and S3ObjectStore accordingly.

In `@framework/infrastructure/S3/index.ts`:
- Around line 116-124: The decryptBody method currently swallows all decryption
failures and returns the raw encrypted payload; update decryptBody to catch
errors from this.encryptor.decrypt and: 1) log the error (including the tenant
id from getCurrentTenantId() and the fact the input was encrypted) using the
module/logger used elsewhere so failures are visible, and 2) make behavior
configurable (e.g., an instance flag like encryptionDisabled already exists or a
new throwOnDecryptionError option) so in non-production/tests the method
re-throws the error instead of returning the raw value; ensure you still check
this.encryptor, encryptionDisabled, and isEncrypted before attempting decryption
and preserve the fallback to returning body only when configured to do so.
- Around line 154-160: The S3 upload currently sets ContentType to
'application/json' even though the body is encrypted via encryptBody; update the
PutObjectCommandInput construction in the method that builds params (where
encryptBody and const params are used) to reflect encrypted content—e.g., set
ContentType to a generic type like 'application/octet-stream' or a custom type
such as 'application/vnd.<app>-encrypted' and add metadata flags (e.g.,
Metadata: { encrypted: 'true', original-content-type: 'application/json' }) so
callers and tools know the object is encrypted and the original type is
preserved.
🪄 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: b24aea71-b5b5-4e7e-b5fe-0b48bf6b2573

📥 Commits

Reviewing files that changed from the base of the PR and between 9c3caac and 1c34b1b.

⛔ 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 (93)
  • .gitignore
  • blueprint/billing-base/mikro-orm.config.ts
  • blueprint/billing-base/package.json
  • blueprint/billing-base/registrations.ts
  • blueprint/billing-stripe/mikro-orm.config.ts
  • blueprint/billing-stripe/package.json
  • blueprint/billing-stripe/registrations.ts
  • blueprint/core/package.json
  • blueprint/iam-base/mikro-orm.config.ts
  • blueprint/iam-base/package.json
  • blueprint/iam-better-auth/mikro-orm.config.ts
  • 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/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
  • blueprint/sample-worker/registrations.ts
  • cli/src/core/ast/infrastructure/redis.rs
  • cli/src/core/ast/infrastructure/s3.rs
  • cli/src/core/package_json/package_json_constants.rs
  • cli/src/templates/application/Dockerfile
  • cli/src/templates/project/service/.env.local
  • cli/src/templates/project/service/mikro-orm.config.ts
  • cli/src/templates/project/service/registrations.ts
  • framework/bunrun/CHANGELOG.md
  • framework/bunrun/package.json
  • framework/common/CHANGELOG.md
  • framework/common/package.json
  • framework/common/src/noop.ts
  • framework/core/CHANGELOG.md
  • framework/core/__test__/complianceEventSubscriber.test.ts
  • framework/core/__test__/fieldEncryptor.test.ts
  • framework/core/__test__/tenantFilter.test.ts
  • framework/core/package.json
  • framework/core/src/http/middleware/request/tenantContext.middleware.ts
  • framework/core/src/http/telemetry/auditLogger.ts
  • framework/core/src/http/telemetry/openTelemetryCollector.ts
  • framework/core/src/persistence/complianceEventSubscriber.ts
  • framework/core/src/persistence/compliancePropertyBuilder.ts
  • framework/core/src/persistence/encryptedType.ts
  • framework/core/src/persistence/fieldEncryptor.ts
  • framework/core/src/persistence/index.ts
  • framework/core/src/persistence/tenantFilter.ts
  • framework/e2e-tests/package.json
  • framework/eslint.config.mjs
  • framework/express/CHANGELOG.md
  • framework/express/__test__/typebox.forklaunch.express.test.ts
  • framework/express/__test__/zod.forklaunch.express.test.ts
  • framework/express/index.ts
  • framework/express/package.json
  • framework/express/src/expressApplication.ts
  • framework/express/src/expressRouter.ts
  • framework/hyper-express/CHANGELOG.md
  • framework/hyper-express/__test__/typebox.forklaunch.hyperExpress.test.ts
  • framework/hyper-express/__test__/zod.forklaunch.hyperExpress.test.ts
  • framework/hyper-express/package.json
  • framework/infrastructure/S3/CHANGELOG.md
  • framework/infrastructure/S3/__test__/s3ObjectStore.test.ts
  • framework/infrastructure/S3/index.ts
  • framework/infrastructure/S3/package.json
  • framework/infrastructure/redis/CHANGELOG.md
  • framework/infrastructure/redis/__test__/redisTtlCache.test.ts
  • framework/infrastructure/redis/index.ts
  • framework/infrastructure/redis/package.json
  • framework/internal/CHANGELOG.md
  • framework/internal/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 with no reviewable changes (8)
  • framework/hyper-express/test/typebox.forklaunch.hyperExpress.test.ts
  • framework/express/test/zod.forklaunch.express.test.ts
  • framework/express/index.ts
  • framework/express/test/typebox.forklaunch.express.test.ts
  • framework/hyper-express/test/zod.forklaunch.hyperExpress.test.ts
  • cli/src/templates/application/Dockerfile
  • framework/express/src/expressApplication.ts
  • framework/common/src/noop.ts

Comment on lines +62 to +64
registerEncryptor(
new FieldEncryptor(validConfigInjector.resolve(tokens.ENCRYPTION_KEY))
);

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Locate registerEncryptor implementation and semantics:"
rg -n --type=ts -C4 'registerEncryptor\s*\(|function registerEncryptor|let .*encryptor|global|singleton' framework blueprint cli

echo "2) Find all call sites:"
rg -n --type=ts 'registerEncryptor\s*\(' blueprint framework cli

echo "3) Inspect call-site patterns around config files:"
rg -n --type=ts -C3 'registerEncryptor\s*\(|mikro-orm\.config' blueprint framework cli

Repository: forklaunch/forklaunch

Length of output: 46722


Add idempotency guard to registerEncryptor to prevent silent overwrites.

The function at framework/core/src/persistence/encryptedType.ts:27–29 currently allows duplicate registration without guards or warnings. Since mikro-orm.config.ts executes at module scope and is imported multiple times in tests and multi-config scenarios, repeated calls can silently overwrite with different encryptors, breaking test isolation.

Implement one of:

  • Guard against duplicate registration (throw or warn if already set)
  • Validate that the same encryptor instance is registered twice
  • Document and enforce single-call semantics in code
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blueprint/iam-base/mikro-orm.config.ts` around lines 62 - 64,
registerEncryptor is being called at module scope and can silently overwrite
previous encryptors; update the registerEncryptor implementation in
framework/core/src/persistence/encryptedType.ts to enforce idempotency: detect
if an encryptor is already registered and either (a) throw an error, (b) emit a
clear warning, or (c) validate that the new value is the same instance before
returning — pick one policy, implement it (e.g., store the first instance in a
private variable and compare references on subsequent calls), and update any
callers like mikro-orm.config.ts (which currently calls registerEncryptor(new
FieldEncryptor(validConfigInjector.resolve(tokens.ENCRYPTION_KEY)))) to expect
the new behavior. Ensure the check uses the existing registerEncryptor symbol
and preserves current behavior when the same instance is re-registered.

Comment on lines +27 to +32
## 1.0.12

### Patch Changes

- update fl packages

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.

⚠️ Potential issue | 🟡 Minor

Complete the 1.0.12 entry with explicit dependency update info.

Line 31 is generic, while nearby releases include exact @forklaunch/interfaces-worker versions; this makes 1.0.12 harder to audit.

Suggested changelog patch
 ## 1.0.12

 ### Patch Changes

 - update fl packages
+- Updated dependencies
+  - `@forklaunch/interfaces-worker`@1.0.10
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## 1.0.12
### Patch Changes
- update fl packages
## 1.0.12
### Patch Changes
- update fl packages
- Updated dependencies
- `@forklaunch/interfaces-worker`@1.0.10
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blueprint/implementations/worker/bullmq/CHANGELOG.md` around lines 27 - 32,
The 1.0.12 changelog entry is too generic; update the "## 1.0.12" section (the
heading and the "- update fl packages" bullet) to list explicit package names
and exact versions that were updated (e.g., include entries like
"@forklaunch/interfaces-worker vX.Y.Z" and any other changed fl packages),
matching the style used in nearby releases so audits can see exact dependency
changes; ensure the bullet points enumerate each package and version and keep
formatting consistent with other changelog entries.

Comment on lines +27 to +32
## 1.0.12

### Patch Changes

- update fl packages

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.

⚠️ Potential issue | 🟡 Minor

Add dependency detail to the 1.0.12 release note for consistency.

Line 31 lists “update fl packages,” but unlike Lines 8-9, 16-17, and 24-25, it omits the concrete dependency bump entry.

Suggested changelog patch
 ## 1.0.12

 ### Patch Changes

 - update fl packages
+- Updated dependencies
+  - `@forklaunch/interfaces-worker`@1.0.10
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@blueprint/implementations/worker/kafka/CHANGELOG.md` around lines 27 - 32,
The 1.0.12 changelog entry under "## 1.0.12" currently says only "update fl
packages"; update it to list the specific dependency bumps like the other
releases do (i.e., name each fl package and its new version), editing the
"update fl packages" line in the CHANGELOG.md so the entry includes concrete
package names and version numbers for consistency with prior entries.

Comment on lines +3 to +8
## 1.2.6

### Patch Changes

- Encryptor required on redis and s3

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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for Redis and S3 initialization patterns to understand if encryptor was previously optional

rg -n -C5 'new.*Redis|createRedis|RedisCache|RedisStore' --type=ts
rg -n -C5 'new.*S3|createS3|S3.*Store|S3.*Client' --type=ts

Repository: forklaunch/forklaunch

Length of output: 27915


🏁 Script executed:

# Check the full RedisTtlCache constructor signature
rg -n "constructor\(" framework/infrastructure/redis/index.ts -A 10

# Check the full S3ObjectStore constructor signature  
rg -n "constructor\(" framework/infrastructure/S3/index.ts -A 10

# Search for any TypeScript optional parameter markers in encryption-related code
rg -n "encryption\?" framework/infrastructure/redis/index.ts
rg -n "encryption\?" framework/infrastructure/S3/index.ts

Repository: forklaunch/forklaunch

Length of output: 1065


🏁 Script executed:

# Get full RedisCacheEncryptionOptions interface definition
rg -n "interface RedisCacheEncryptionOptions" framework/infrastructure/redis/index.ts -A 10

# Get full S3EncryptionOptions interface definition  
rg -n "interface S3EncryptionOptions" framework/infrastructure/S3/index.ts -A 10

# Search for any usages that create encryption configs without encryptor
rg -n "encryption\s*:\s*\{" framework/ --type=ts | head -30

Repository: forklaunch/forklaunch

Length of output: 870


🏁 Script executed:

# Look at full CHANGELOG for context
head -50 framework/common/CHANGELOG.md

# Search for previous entries mentioning encryptor
rg -n "encryptor" framework/common/CHANGELOG.md

# Search for any code that might instantiate these classes with partial/undefined encryption
rg -n "new RedisTtlCache|new S3ObjectStore" --type=ts -B 5 -A 5 | head -80

Repository: forklaunch/forklaunch

Length of output: 6635


This is a breaking change and violates semantic versioning for a patch release.

The changelog entry "Encryptor required on redis and s3" indicates that the encryption parameter—which was previously optional—is now mandatory. Evidence: the S3 documentation example (line 68 of framework/infrastructure/S3/index.ts) shows instantiation without the encryption parameter, which would fail under the current required constructor signature.

Making a previously-optional parameter required is a breaking change that incompatible with existing code. Per semantic versioning, this warrants a major version bump (e.g., 2.0.0), not a patch (1.2.6).

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

In `@framework/common/CHANGELOG.md` around lines 3 - 8, The changelog claims a
patch release ("1.2.6") but the change "Encryptor required on redis and s3" is
breaking because it makes the previously-optional encryption parameter
mandatory; either relax the constructor signatures back to accept an optional
encryption parameter (e.g., in the S3 constructor in
framework/infrastructure/S3/index.ts and the Redis client constructor) or, if
the required change is intended, update the release metadata and CHANGELOG entry
to a major version bump (e.g., 2.0.0) and note the breaking change; locate and
adjust the relevant constructor signatures or bump the version and changelog
heading accordingly.

Comment on lines 161 to 165
getMetric<T extends keyof AppliedMetricsDefinition>(
metricId: T
): MetricType<AppliedMetricsDefinition[T]> {
return this.#metrics[metricId] as MetricType<AppliedMetricsDefinition[T]>;
return this._metrics[metricId] as MetricType<AppliedMetricsDefinition[T]>;
}

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.

⚠️ Potential issue | 🟠 Major

Guard missing metric lookups instead of returning a casted undefined.

Line 164 force-casts map access, but _metrics[metricId] can be undefined at runtime (e.g., missing metricDefinitions or unregistered ID), causing downstream crashes when callers invoke metric methods.

Proposed fix
   getMetric<T extends keyof AppliedMetricsDefinition>(
     metricId: T
   ): MetricType<AppliedMetricsDefinition[T]> {
-    return this._metrics[metricId] as MetricType<AppliedMetricsDefinition[T]>;
+    const metric = this._metrics[metricId];
+    if (!metric) {
+      throw new Error(`Metric "${String(metricId)}" is not registered`);
+    }
+    return metric as MetricType<AppliedMetricsDefinition[T]>;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getMetric<T extends keyof AppliedMetricsDefinition>(
metricId: T
): MetricType<AppliedMetricsDefinition[T]> {
return this.#metrics[metricId] as MetricType<AppliedMetricsDefinition[T]>;
return this._metrics[metricId] as MetricType<AppliedMetricsDefinition[T]>;
}
getMetric<T extends keyof AppliedMetricsDefinition>(
metricId: T
): MetricType<AppliedMetricsDefinition[T]> {
const metric = this._metrics[metricId];
if (!metric) {
throw new Error(`Metric "${String(metricId)}" is not registered`);
}
return metric as MetricType<AppliedMetricsDefinition[T]>;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@framework/core/src/http/telemetry/openTelemetryCollector.ts` around lines 161
- 165, getMetric currently force-casts this._metrics[metricId] which can be
undefined and causes downstream crashes; update the getMetric method to check
whether this._metrics[metricId] is present and, if missing, throw a descriptive
error (e.g., `Metric "${String(metricId)}" not registered`) or return a properly
typed undefined/optional value depending on API expectations; locate the
getMetric function and the _metrics map in openTelemetryCollector.ts and replace
the direct cast with a guarded check that either throws the error including
metricId or returns a safe optional result before returning the MetricType.

Comment on lines +67 to +79
* Derive a deterministic IV from the key and plaintext using HMAC-SHA256,
* truncated to IV_BYTES. Same plaintext + same key → same IV → same
* ciphertext. This enables WHERE clause matching on encrypted columns
* while maintaining AES-256-GCM authenticated encryption.
*
* @returns Format: `v1:{base64(iv)}:{base64(authTag)}:{base64(ciphertext)}`
* Meets SOC 2, HIPAA, PCI DSS, GDPR requirements for encryption at rest.
*/
private deriveDeterministicIv(key: Buffer, plaintext: string): Buffer {
return crypto
.createHmac('sha256', key)
.update(plaintext)
.digest()
.subarray(0, IV_BYTES);

@coderabbitai coderabbitai Bot Mar 28, 2026

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Find authoritative guidance from NIST or equivalent cryptography references on using AES-GCM with deterministic IVs / nonce reuse, and on recommended patterns for searchable encrypted fields such as blind indexes, AES-SIV, or AES-GCM-SIV.

💡 Result:

NIST SP 800-38D strictly requires unique IVs (nonces) for AES-GCM: reusing an IV with the same key catastrophically compromises security, allowing key recovery and forgeries (sources 6,7; high confidence). Deterministic IVs are permitted only via the specific 96-bit construction (32-bit fixed + 64-bit counter), but managing state to avoid reuse across distributed systems is challenging (sources 1,2,3; high confidence). NIST discourages nonce reuse and notes ongoing issues in practice (sources 2,8,10; high confidence). For nonce-misuse resistance, NIST has not standardized AES-SIV or AES-GCM-SIV in an SP 800-38 series document. AES-SIV is defined in RFC 5297 (nonce-based or deterministic AEAD) (source 12; high confidence), and AES-GCM-SIV in RFC 8452 (faster GCM variant with SIV construction) (source 11; high confidence). NIST SP 800-38D revision discussions reference these for nonce-misuse scenarios but no approval yet (sources 2,8,14; medium confidence). They are CFRG/IETF consensus but not NIST-approved modes. For searchable encrypted fields (e.g., blind indexes), NIST lacks direct guidance on AES-SIV/AES-GCM-SIV usage. Blind indexes typically use deterministic AEAD like HMAC-SHA256/2 for fixed-length hashes or AES-SIV for confidentiality (non-NIST). NIST SP 800-38G specifies FF1/FF3 for format-preserving encryption (e.g., encrypting SSNs while preserving format for indexing) (sources 26,27,28; high confidence). SP 800-38F provides deterministic key wrapping (KW/KWP). Research on searchable symmetric encryption (SSE) exists but is not NIST-recommended (sources 31-35; low confidence for guidance).

Citations:


🏁 Script executed:

# First, let's check the file structure and examine the fieldEncryptor.ts file
find . -name "fieldEncryptor.ts" -type f

Repository: forklaunch/forklaunch

Length of output: 115


🏁 Script executed:

# Get a broader view of the persistence layer
fd -t f "Encryptor" --max-one-result

Repository: forklaunch/forklaunch

Length of output: 298


🏁 Script executed:

# Look for the full implementation of the fieldEncryptor
cat -n framework/core/src/persistence/fieldEncryptor.ts

Repository: forklaunch/forklaunch

Length of output: 5977


🏁 Script executed:

# Search for how this encryptor is wired into different stores
rg "fieldEncryptor|FieldEncryptor" -t ts --max-count 30

Repository: forklaunch/forklaunch

Length of output: 6102


🏁 Script executed:

cat -n framework/core/__test__/fieldEncryptor.test.ts | head -100

Repository: forklaunch/forklaunch

Length of output: 4499


🏁 Script executed:

# Check if there are any comments about compliance requirements or security trade-offs
rg "deterministic|linkable|blind.*index|AES-SIV" framework/core --type ts -B 2 -A 2

Repository: forklaunch/forklaunch

Length of output: 2468


Deterministic IV derivation with AES-GCM violates NIST SP 800-38D and creates security vulnerabilities.

NIST SP 800-38D strictly prohibits reusing an IV (nonce) with the same key in AES-GCM, as this catastrophically compromises security and allows key recovery and forgeries. The current design derives a deterministic IV from plaintext via HMAC-SHA256, meaning identical values always produce identical ciphertexts within the same tenant context. This linkability extends across all storage backends (database, Redis, S3) enabled by this encryptor, allowing an attacker to correlate encrypted fields and infer plaintext patterns.

The code comment claiming compliance with "SOC 2, HIPAA, PCI DSS, GDPR requirements" is not supported by this design.

For searchable encryption, use NIST-recommended alternatives: AES-SIV (RFC 5297) or AES-GCM-SIV (RFC 8452) for nonce-misuse resistance, blind indexes with deterministic HMAC-based hashing for lookups only, or format-preserving encryption (NIST SP 800-38G) for specific field types. At minimum, incorporate field/store domain into the IV derivation to prevent cross-backend linkability.

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

In `@framework/core/src/persistence/fieldEncryptor.ts` around lines 67 - 79, The
deriveDeterministicIv function currently generates a deterministic IV (nonce)
via HMAC-SHA256 which causes IV reuse with AES-256-GCM (function
deriveDeterministicIv, constant IV_BYTES) and is insecure; replace this design
by removing deterministic IV derivation for AES-GCM and instead implement a safe
alternative: either switch encryption to a nonce-misuse-resistant AEAD (e.g.,
AES-SIV / AES-GCM-SIV) for deterministic/searchable needs, or keep AES-GCM but
generate a cryptographically random IV per encryption and implement a separate
deterministic lookup mechanism (e.g., HMAC-based blind index) that uses a
separate key and includes field/store domain in the HMAC input to avoid
cross-store linkability; update callers of deriveDeterministicIv (and any code
assuming deterministic IVs) to use the new AEAD or to store the blind index
alongside the random-IV ciphertext.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a valid cryptographic concern but it's an intentional design tradeoff documented in the code comments (lines 66-72, 82-87). Deterministic encryption is required for WHERE clause matching on encrypted columns — this is the entire point
of the feature. The code explicitly states this enables WHERE clause matching and UNIQUE constraints. Switching to random IVs would break the core use case. AES-SIV/AES-GCM-SIV aren't available in Node.js crypto module. The HMAC-based IV
derivation is a well-known pattern (called SIV-like construction) that's used in production systems like Google's Tink library. The deterministic IV is derived per (tenant-key, plaintext) pair, so different tenants or different values get
different IVs. No fix needed — this is a documented, intentional design decision.

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.

🧩 Analysis chain

@RohinBhargava, thank you for the detailed explanation. You make valid points — I'll acknowledge this as an intentional, documented design decision.

To confirm your claim about Node.js built-in crypto support: the node:crypto module's authenticated encryption modes currently include GCM, CCM, OCB, and chacha20-poly1305 — AES-SIV and AES-GCM-SIV are indeed absent from the standard library, though third-party npm packages do exist (e.g., micro-aes-gcm has an optional RFC 8452 SIV implementation).

Your reasoning about the HMAC-derived IV construction is sound for this use case:

  • The IV is scoped per (tenant-derived-key, plaintext) pair, so different tenants or different values always produce different IVs.
  • The linkability (same plaintext + same tenant = same ciphertext) is the explicitly desired property for WHERE clause matching and UNIQUE constraints.
  • The doc comments clearly document this tradeoff.

I'll withdraw the blocking concern and record this as a deliberate architectural choice.


🌐 Web query:

AES-SIV AES-GCM-SIV Node.js crypto module support availability

💡 Result:

Status in Node.js node:crypto (built-in)

  • AES-SIV (RFC 5297): Not practically available via crypto.createCipheriv() today. Node’s classic cipher path looks up ciphers by name using OpenSSL’s EVP_get_cipherbyname() (not provider-based fetching). (sources.debian.org)
    OpenSSL documents that EVP_get_cipherbyname() returns NULL for ciphers such as AES-128-SIV (i.e., AES-SIV isn’t reachable that way in OpenSSL 3.x). (docs.openssl.org)

  • AES-GCM-SIV: Same situation, plus it’s newer in OpenSSL. OpenSSL exposes AES-*-GCM-SIV cipher names via the EVP interface, but GCM-SIV ciphers were only added in OpenSSL 3.2. (docs.openssl.org)
    And like AES-SIV, it falls under the SIV family that OpenSSL notes isn’t accessible via EVP_get_cipherbyname() in OpenSSL 3.x. (docs.openssl.org)

What this means

  • Even if your system OpenSSL supports these ciphers, current Node.js crypto.createCipheriv() generally won’t be able to instantiate AES-SIV / AES-GCM-SIV because it uses EVP_get_cipherbyname() rather than EVP_CIPHER_fetch()/provider fetching. (sources.debian.org)

Workarounds

  • Use a third-party JS/WASM/native implementation for AES-SIV or AES-GCM-SIV, or bind directly to OpenSSL in native code using provider-based fetching (EVP_CIPHER_fetch). (docs.openssl.org)

Citations:


@RohinBhargava, thank you for the detailed explanation. The web search confirms your point — Node.js crypto.createCipheriv() uses EVP_get_cipherbyname() under the hood, and OpenSSL documents that this returns NULL for ciphers such as AES-SIV, making it practically unavailable in current Node.js. AES-GCM-SIV ciphers were only added in OpenSSL 3.2 and share the same accessibility limitation, so using AES-SIV or AES-GCM-SIV would require a third-party JS/WASM/native implementation.

Your reasoning on the HMAC-derived IV construction is sound for this use case — the IV is scoped per (tenant-derived-key, plaintext) pair, the linkability tradeoff is explicitly documented in the code comments, and the deterministic output is the desired property for WHERE clause matching and UNIQUE constraints. I'm withdrawing the concern.


✏️ Learnings added
Learnt from: RohinBhargava
Repo: forklaunch/forklaunch PR: 141
File: framework/core/src/persistence/fieldEncryptor.ts:67-79
Timestamp: 2026-03-28T06:53:33.745Z
Learning: In `framework/core/src/persistence/fieldEncryptor.ts`, the `deriveDeterministicIv` method intentionally uses an HMAC-SHA256-derived deterministic IV with AES-256-GCM (a SIV-like construction). This is a documented, deliberate design decision required to support WHERE clause matching and UNIQUE constraints on encrypted database columns. The IV is scoped per (tenant-derived-key, plaintext) pair, so different tenants or different plaintexts produce different IVs/ciphertexts. AES-SIV and AES-GCM-SIV are not available in Node.js built-in crypto, and the deterministic encryption tradeoff (same tenant + same plaintext = same ciphertext) is the desired behavior for searchable encrypted fields. Do not flag this as a security issue in future reviews.

Learnt from: RohinBhargava
Repo: forklaunch/forklaunch PR: 141
File: framework/core/src/persistence/fieldEncryptor.ts:67-79
Timestamp: 2026-03-28T06:53:33.745Z
Learning: In `framework/core/src/persistence/fieldEncryptor.ts`, the `deriveDeterministicIv` method intentionally uses an HMAC-SHA256-derived deterministic IV with AES-256-GCM (a SIV-like construction). This is a documented, deliberate design decision required to support WHERE clause matching and UNIQUE constraints on encrypted database columns. The IV is scoped per (tenant-derived-key, plaintext) pair, so different tenants or different plaintexts produce different IVs/ciphertexts. AES-SIV and AES-GCM-SIV are not accessible via Node.js built-in `crypto.createCipheriv()` (which uses `EVP_get_cipherbyname()` — returning NULL for SIV ciphers in OpenSSL 3.x), making the current HMAC-IV approach the practical choice without introducing third-party WASM/native dependencies. The deterministic encryption tradeoff (same tenant + same plaintext = same ciphertext) is the intentional, desired behavior for searchable encrypted fields. Do not flag this as a security issue in future reviews.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


### Patch Changes

- up packages

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.

⚠️ Potential issue | 🟡 Minor

Use explicit release note phrasing.

Line 41 (up packages) is unclear in isolation. Please use a more specific description so downstream users can quickly assess impact.

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

In `@framework/express/CHANGELOG.md` at line 41, Replace the vague changelog entry
"up packages" with a clear, explicit release note: locate the line containing
the exact phrase "up packages" in CHANGELOG.md and change it to a descriptive
entry such as "chore: bump dependencies — updated [packageA]@x.y.z,
[packageB]@a.b.c to address security/compatibility" (or similar wording that
lists the updated packages and the reason for the bump); ensure the new line
follows the project's changelog style and includes which packages were updated
and why.

Comment on lines +23 to +24
- up packages
- Updated dependencies

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.

⚠️ Potential issue | 🟡 Minor

Clarify vague changelog notes for release consumers.

Line 23 (up packages) and Line 31 (update packages) are too ambiguous. Consider replacing with concrete package/version intent to make upgrade impact clear.

Also applies to: 31-32

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

In `@framework/validator/CHANGELOG.md` around lines 23 - 24, Replace the ambiguous
changelog entries "up packages" and "update packages" with concrete,
consumer-facing notes: identify the exact packages and target versions or intent
(e.g., "Bump lodash to 4.17.21 to fix CVE-XXXX" or "Upgrade react to ^18.2.0 for
breaking API X") for the entries currently labeled "up packages" (line marked in
the diff) and "update packages" (also noted); update the CHANGELOG.md entries so
each line contains the package name(s), version change or range, and a one-line
rationale/impact statement.

@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)
cli/src/core/ast/injections/inject_into_import_statement.rs (1)

32-32: Simplify string comparison by removing unnecessary Cow wrapping.

Both sides of the comparison are already &str, so the Cow::Borrowed wrapping adds noise without benefit.

♻️ Suggested simplification
-        if Cow::Borrowed(import.source.value.as_str()) != Cow::Borrowed(import_source_identifier) {
+        if import.source.value.as_str() != import_source_identifier {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/core/ast/injections/inject_into_import_statement.rs` at line 32, The
comparison in inject_into_import_statement.rs uses unnecessary Cow::Borrowed
wrappers: replace the expression comparing
Cow::Borrowed(import.source.value.as_str()) !=
Cow::Borrowed(import_source_identifier) with a direct &str comparison between
import.source.value.as_str() and import_source_identifier (e.g.,
import.source.value.as_str() != import_source_identifier) so both sides are
plain &str values and the Cow wrapping is removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cli/src/core/ast/injections/inject_into_import_statement.rs`:
- Line 32: The comparison in inject_into_import_statement.rs uses unnecessary
Cow::Borrowed wrappers: replace the expression comparing
Cow::Borrowed(import.source.value.as_str()) !=
Cow::Borrowed(import_source_identifier) with a direct &str comparison between
import.source.value.as_str() and import_source_identifier (e.g.,
import.source.value.as_str() != import_source_identifier) so both sides are
plain &str values and the Cow wrapping is removed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6ea2e13f-d0ac-4711-8ea2-2f24445d2d17

📥 Commits

Reviewing files that changed from the base of the PR and between 1c34b1b and ef8239f.

📒 Files selected for processing (1)
  • cli/src/core/ast/injections/inject_into_import_statement.rs

@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)
cli/src/release/manifest_generator.rs (1)

768-808: Consider logging a warning when compliance scanning fails during release.

The use of .ok().map() silently converts scan errors to None, resulting in compliance being omitted from the release manifest without any indication of failure. In contrast, cli/src/compliance/audit.rs (lines 98-105) logs a [WARN] message when scanning fails.

For a release workflow, silent omission of compliance data could mask issues that matter for compliance audits. Consider aligning the error handling with audit.rs:

♻️ Suggested change to add warning logging
-    // Scan compliance data from source code (never stored in manifest)
-    let compliance_data = scan_all_compliance(&modules_root).ok().map(
-        |(field_classifications, retention_policies)| {
+    // Scan compliance data from source code (never stored in manifest)
+    let compliance_data = match scan_all_compliance(&modules_root) {
+        Ok((field_classifications, retention_policies)) => {
             let compliance_config = manifest.compliance.as_ref();
             let mut entity_names: std::collections::BTreeSet<String> =
                 std::collections::BTreeSet::new();
             entity_names.extend(field_classifications.keys().cloned());
             entity_names.extend(retention_policies.keys().cloned());

             let entities: Vec<ReleaseEntityCompliance> = entity_names
                 .into_iter()
                 .map(|name| {
                     let fields = field_classifications
                         .get(&name)
                         .cloned()
                         .unwrap_or_default();
                     let retention = retention_policies.get(&name).map(|r| {
                         ReleaseRetentionConfig {
                             duration: r.duration.clone(),
                             action: r.action.clone(),
                         }
                     });
                     ReleaseEntityCompliance {
                         name,
                         fields,
                         retention,
                     }
                 })
                 .collect();

-            ReleaseComplianceData {
+            Some(ReleaseComplianceData {
                 entities,
                 data_residency: compliance_config
                     .map(|c| c.data_residency.clone())
                     .unwrap_or_default(),
                 secrets: compliance_config
                     .map(|c| c.secrets.clone())
                     .unwrap_or_default(),
-            }
-        },
-    );
+            })
+        }
+        Err(e) => {
+            eprintln!("[WARN] Failed to scan entity compliance metadata: {}", e);
+            None
+        }
+    };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/src/release/manifest_generator.rs` around lines 768 - 808, The current
chain uses scan_all_compliance(&modules_root).ok().map(...) which silently
discards errors and omits compliance_data; change this to capture the Result
from scan_all_compliance and, on Err(e), emit a warning (e.g., tracing::warn! or
the crate's logger) that includes the error before proceeding so failures are
visible; keep the existing mapping logic for Ok((field_classifications,
retention_policies)) to construct ReleaseComplianceData and assign to
compliance_data, but ensure the Err branch logs the error and returns None (or
appropriate fallback) instead of swallowing it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cli/src/release/manifest_generator.rs`:
- Around line 768-808: The current chain uses
scan_all_compliance(&modules_root).ok().map(...) which silently discards errors
and omits compliance_data; change this to capture the Result from
scan_all_compliance and, on Err(e), emit a warning (e.g., tracing::warn! or the
crate's logger) that includes the error before proceeding so failures are
visible; keep the existing mapping logic for Ok((field_classifications,
retention_policies)) to construct ReleaseComplianceData and assign to
compliance_data, but ensure the Err branch logs the error and returns None (or
appropriate fallback) instead of swallowing it.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9d7c01cc-42d0-4e97-8acf-bb7bbda5af09

📥 Commits

Reviewing files that changed from the base of the PR and between ef8239f and d57a1cd.

📒 Files selected for processing (5)
  • cli/src/compliance/audit.rs
  • cli/src/core/manifest.rs
  • cli/src/core/validate.rs
  • cli/src/release/manifest_generator.rs
  • cli/src/sync/all.rs
💤 Files with no reviewable changes (1)
  • cli/src/core/validate.rs

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