Skip to content

promote: IAM registration and replay hardening - #48

Merged
BeforeLights merged 45 commits into
mainfrom
promote/iam-registration-20260804
Aug 4, 2026
Merged

promote: IAM registration and replay hardening#48
BeforeLights merged 45 commits into
mainfrom
promote/iam-registration-20260804

Conversation

@BeforeLights

@BeforeLights BeforeLights commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Promotion

Promotes the reviewed IAM security completion and the follow-up IAM hardening batch from dev to main.

Included

  • reviewed identity, audit, entitlement, and service-account security foundations
  • registration anti-enumeration and bounded admission
  • encrypted service-account create replay and idempotency conflict handling
  • invitation state, rollback, contract, and evidence corrections

Verification

@coderabbitai full review

Summary by CodeRabbit

  • New Features
    • Registration now returns a privacy-preserving 202 Accepted response with admission controls.
    • Service-account creation supports safe retries and protected secret storage.
    • Audit attestations support idempotent replays, conflict detection, and scoped seal lookup.
    • Session responses always include MFA re-enrollment status.
  • Bug Fixes
    • Invitation, recovery, entitlement, and audit flows now provide clearer failure handling and stronger validation.
    • Recovery delivery failures revoke newly created challenges.
  • Documentation
    • Updated operational records and API contract details for the revised behavior.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@BeforeLights, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c672a42f-3f1b-44e1-9c11-8364a227ec8b

📥 Commits

Reviewing files that changed from the base of the PR and between 67d1982 and 63af721.

📒 Files selected for processing (51)
  • docs/operations/secret-rotation.md
  • packages/domain/src/invitation/v1.ts
  • services/api/openapi/v1.json
  • services/api/prisma/migrations/20260804020000_iam_service_account_replay_bounds/migration.sql
  • services/api/prisma/migrations/20260804030000_iam_recovery_compensation_failures/migration.sql
  • services/api/prisma/migrations/20260804040000_iam_invitation_delivery_failures/migration.sql
  • services/api/prisma/schema/iam.prisma
  • services/api/src/features/bua/api/entitlement-lease.dto.ts
  • services/api/src/features/iam/adapter/iam-registration-crypto.adapter.ts
  • services/api/src/features/iam/adapter/in-memory-iam-invitation-repository.adapter.ts
  • services/api/src/features/iam/adapter/in-memory-recovery-repository.adapter.ts
  • services/api/src/features/iam/adapter/in-memory-service-account-repository.adapter.ts
  • services/api/src/features/iam/adapter/prisma-credential-lookup.adapter.ts
  • services/api/src/features/iam/adapter/prisma-iam-invitation-repository.adapter.ts
  • services/api/src/features/iam/adapter/prisma-recovery-repository.adapter.ts
  • services/api/src/features/iam/adapter/prisma-service-account-repository.adapter.ts
  • services/api/src/features/iam/adapter/prisma-session-lifecycle.adapter.ts
  • services/api/src/features/iam/adapter/service-account-secret-envelope.adapter.ts
  • services/api/src/features/iam/api/authentication.controller.ts
  • services/api/src/features/iam/api/registration.controller.ts
  • services/api/src/features/iam/application/authentication.port.ts
  • services/api/src/features/iam/application/authentication.service.ts
  • services/api/src/features/iam/application/invitation-repository.port.ts
  • services/api/src/features/iam/application/invitation.service.ts
  • services/api/src/features/iam/application/recovery-repository.port.ts
  • services/api/src/features/iam/application/recovery.service.ts
  • services/api/src/features/iam/application/registration-repository.port.ts
  • services/api/src/features/iam/application/service-account-repository.port.ts
  • services/api/src/features/iam/application/service-account.service.ts
  • services/api/src/features/iam/application/tenant-context.ts
  • services/api/src/features/iam/iam.module.ts
  • services/api/src/platform/http/session-tenant-context.adapter.ts
  • services/api/test/features/iam/auth-composition.test.ts
  • services/api/test/features/iam/authentication.test.ts
  • services/api/test/features/iam/invitation-service.test.ts
  • services/api/test/features/iam/prisma-credential-lookup.test.ts
  • services/api/test/features/iam/prisma-service-account-repository.test.ts
  • services/api/test/features/iam/prisma-session-lifecycle.test.ts
  • services/api/test/features/iam/recovery.service.test.ts
  • services/api/test/features/iam/registration-composition.test.ts
  • services/api/test/features/iam/registration-controller.test.ts
  • services/api/test/features/iam/registration-crypto.test.ts
  • services/api/test/features/iam/registration-http.test.ts
  • services/api/test/features/iam/service-account-composition.test.ts
  • services/api/test/features/iam/service-account-repository.test.ts
  • services/api/test/features/iam/service-account-secret-envelope.adapter.test.ts
  • services/api/test/features/iam/service-account.service.test.ts
  • services/api/test/features/iam/session-lifecycle.test.ts
  • services/api/test/http-contract.test.ts
  • services/api/test/platform/http/session-tenant-context.test.ts
  • services/api/test/prisma-foundation.test.mjs
📝 Walkthrough

Walkthrough

This PR hardens IAM registration, recovery, invitations, and service-account creation. It adds audit replay handling, authoritative entitlement verification, encrypted secret envelopes, database uniqueness constraints, generic registration responses, required MFA response fields, and stricter API schemas.

Changes

Audit attestation and seal access

Layer / File(s) Summary
Audit repository contracts
services/api/src/features/aud/application/*, services/api/src/features/aud/api/*
Audit persistence now returns replay or conflict results and supports scoped seal lookup.
Audit persistence and tests
services/api/src/features/aud/adapter/*, services/api/test/features/aud/*
In-memory and Prisma adapters implement replay detection, race handling, visibility checks, and tenant-isolation coverage.

Entitlement and invitation domain behavior

Layer / File(s) Summary
Entitlement lease validation
packages/domain/src/entitlements/v1.ts, services/api/src/features/bua/*
Lease signing fails closed, equality uses explicit fields, and verification uses the service clock.
Invitation token states
packages/domain/src/invitation/v1.ts, packages/domain/test/invitation-v1.test.mjs
Redeemed and other inactive invitation tokens now return different error codes.

IAM flows

Layer / File(s) Summary
Registration admission and response flow
services/api/src/features/iam/application/registration-*, services/api/src/features/iam/api/registration-*, services/api/src/features/iam/iam.module.ts, services/api/test/features/iam/registration-*, docs/operations/iam-registration-2026-08-03.md
Registration performs separate normalized IP and email admission checks, fails closed on admission errors, and returns 202 { accepted: true }.
Invitation and recovery delivery
services/api/src/features/iam/application/{invitation,recovery}.service.ts, services/api/src/features/iam/adapter/*, services/api/test/features/iam/{invitation,recovery}-*, docs/operations/iam-010-invitation-token-2026-08-03.md
Persistence occurs before delivery. Delivery failures return unavailable results and trigger recovery challenge compensation where applicable.
Service-account idempotent creation
services/api/src/features/iam/application/service-account-*, services/api/src/features/iam/adapter/{in-memory,prisma}-service-account-repository.adapter.ts, services/api/prisma/*, services/api/test/features/iam/service-account-*
Creation validates permissions, hashes requests, stores scoped replay metadata, and preserves replay results across account updates.
Service-account secret envelopes
services/api/src/features/iam/adapter/service-account-secret-envelope.adapter.ts, services/api/src/features/iam/iam.module.ts, services/api/test/features/iam/service-account-secret-envelope.adapter.test.ts
Secrets are sealed with AES-256-GCM and reopened only when authentication and validation succeed.
Session response fields
services/api/src/features/iam/api/{auth-session,current-session}.dto.ts, services/api/src/features/iam/api/authentication.controller.ts, services/api/test/http-contract.test.ts
Authentication and current-session responses always include mfaReenrollmentRequired.

API contracts and persistence constraints

Layer / File(s) Summary
API schema constraints
services/api/openapi/v1.json, services/api/src/features/aud/api/*, services/api/src/features/iae/api/*
Schemas now require integer numeric fields and strict millisecond UTC timestamps.
Database migrations and validation
services/api/prisma/migrations/*, services/api/prisma/schema/iam.prisma, services/api/test/prisma-foundation.test.mjs
The database enforces active invitation uniqueness and service-account creation idempotency metadata constraints.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RegistrationController
  participant AdmissionAdapters
  participant RegistrationService
  participant IAMRepository
  Client->>RegistrationController: submit registration
  RegistrationController->>AdmissionAdapters: check IP and email digests
  AdmissionAdapters-->>RegistrationController: allow or reject
  RegistrationController->>RegistrationService: register accepted request
  RegistrationService->>IAMRepository: persist registration
  IAMRepository-->>RegistrationService: registration result
  RegistrationService-->>RegistrationController: accepted result
  RegistrationController-->>Client: 202 Accepted
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies IAM registration and replay hardening, which are significant changes in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch promote/iam-registration-20260804

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

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

⚠️ Outside diff range comments (1)
services/api/src/features/iam/application/invitation.service.ts (1)

174-239: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Add durable recovery for delivery failures.

After saveInvitation commits, a delivery exception returns DELIVERY_UNAVAILABLE. A retry then finds the active token and returns CONFLICT. The raw token exists only in pendingDelivery, while docs/operations/iam-010-invitation-token-2026-08-03.md states that only digests persist. The later resend described on Lines 230-231 cannot use the same bearer.

Persist an encrypted delivery outbox payload and retry the same token, or add a safe replacement and revocation flow. This prevents a transient delivery failure from permanently blocking the invited membership.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/src/features/iam/application/invitation.service.ts` around lines
174 - 239, Update the invitation flow around saveInvitation and delivery.deliver
to persist a durable, encrypted outbox payload containing the raw bearer token
before the transaction commits, then remove or mark it delivered after
successful delivery and retry it on subsequent attempts. Ensure the existing
active token can be recovered for resend without persisting plaintext token
data, and preserve token reuse and revocation semantics so delivery failures do
not return CONFLICT permanently.
🧹 Nitpick comments (9)
services/api/test/http-contract.test.ts (1)

336-336: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Cover the true MFA re-enrollment path.

These assertions cover only the omitted-value fallback to false. Ensure the contract tests also pass mfaReenrollmentRequired: true through sign-in and current-session responses, then assert true. This detects a regression that drops a required re-enrollment state.

Also applies to: 678-678

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/test/http-contract.test.ts` at line 336, Extend the contract
tests around the sign-in and current-session response assertions to provide
mfaReenrollmentRequired: true and verify the responses preserve true, while
retaining the existing omitted-value fallback assertion for false.
services/api/test/prisma-foundation.test.mjs (1)

573-593: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert the complete migration contract.

The current checks validate only selected names and columns. They do not prove that the invitation index uses membership_id with WHERE "status" = 'ACTIVE'.

The service-account checks also omit create_idempotency_key, create_request_hash, and service_accounts_create_idempotency_key. They do not verify the required index columns or partial predicates. Add assertions for these exact SQL clauses from both migration files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/test/prisma-foundation.test.mjs` around lines 573 - 593, Expand
the migration assertions in the test covering active invitations and
service-account idempotency to validate the complete SQL contract, not just
object names. Assert the invitation index includes membership_id and the ACTIVE
status predicate, and add checks for create_idempotency_key,
create_request_hash, service_accounts_create_idempotency_key, each required
index column list, and their partial predicates using the existing
migration-content assertion pattern.
services/api/src/features/iam/application/service-account.service.ts (1)

208-231: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Canonicalize permissions before hashing.

createRequestHash normalizes name and secretExpiresAt, but it hashes input.permissions in the order received. A retry that sends the same permissions in a different order produces a different hash. replayCreate then returns CONFLICT for a semantically identical retry. Sort the permission list in the hash input to keep the hash order-independent.

♻️ Proposed change
-        JSON.stringify({
-          name,
-          workspaceId: workspaceId ?? null,
-          permissions: input.permissions,
-          secretExpiresAt: expiry ?? null,
-        }),
+        JSON.stringify({
+          name,
+          workspaceId: workspaceId ?? null,
+          permissions: [...input.permissions].sort(),
+          secretExpiresAt: expiry ?? null,
+        }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/src/features/iam/application/service-account.service.ts` around
lines 208 - 231, Update createRequestHash to canonicalize permissions before
hashing by sorting the validated permission list in the JSON payload. Preserve
the existing validation and ensure semantically identical permission sets
produce the same order-independent hash for replayCreate.
services/api/test/features/iam/service-account-composition.test.ts (1)

34-46: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that composition wires the secret envelope.

create now returns UNAVAILABLE when secretEnvelope is absent. This test only asserts that the provider resolves to a ServiceAccountService instance, so it passes even when the envelope is unwired and every create fails. Add an assertion that exercises the wired envelope, for example a create call against a stub database, or an assertion on the envelope provider registered by IamModule.register.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/test/features/iam/service-account-composition.test.ts` around
lines 34 - 46, Extend the IAM composition test around IamModule.register and the
SERVICE_ACCOUNT_SERVICE provider to verify the secret envelope is wired, not
only that ServiceAccountService is instantiated. Exercise the service’s create
path with a stub database and assert it does not return UNAVAILABLE, or assert
the envelope provider registered by IamModule.register is present and correctly
configured.
services/api/prisma/migrations/20260804010000_iam_service_account_create_idempotency/migration.sql (1)

25-34: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

service_accounts_create_idempotency_workspace_key is redundant.

This index uses the same column list as service_accounts_create_idempotency_key on lines 9-15, and its predicate only narrows the rows covered. Any duplicate pair rejected by this partial index is already rejected by the full index, because none of the four columns is NULL under the predicate. The workspace-scope race is therefore already closed by the full index.

The organization-scope partial index on lines 19-23 remains necessary, because the full index treats NULL workspace_id values as distinct.

Dropping the redundant index removes one index maintenance cost per insert and update.

♻️ Proposed removal of the redundant index
-CREATE UNIQUE INDEX "service_accounts_create_idempotency_workspace_key"
-ON "iam"."service_accounts"(
-    "organization_id",
-    "workspace_id",
-    "created_by_actor_id",
-    "create_idempotency_key"
-)
-WHERE "workspace_id" IS NOT NULL
-  AND "created_by_actor_id" IS NOT NULL
-  AND "create_idempotency_key" IS NOT NULL;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/api/prisma/migrations/20260804010000_iam_service_account_create_idempotency/migration.sql`
around lines 25 - 34, Remove the redundant CREATE UNIQUE INDEX statement named
service_accounts_create_idempotency_workspace_key from the migration, while
retaining service_accounts_create_idempotency_key and the organization-scope
partial index unchanged.
services/api/src/features/iam/adapter/service-account-secret-envelope.adapter.ts (2)

64-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the durability effect of the process-local fallback key.

randomServiceAccountSecretEnvelopeAdapter generates a new key for each process. Every stored create_secret_envelope value becomes undecryptable after a restart, and a second instance cannot open envelopes sealed by the first. A retry with the original Idempotency-Key then returns UNAVAILABLE instead of the original secret, because open fails closed.

The behavior is safe. The operational consequence is that any deployment with more than one instance, or with restarts, must configure a durable key. Add that constraint to the doc comment or to the operations documentation so the fallback is not selected by accident.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/api/src/features/iam/adapter/service-account-secret-envelope.adapter.ts`
around lines 64 - 67, Update the doc comment for
randomServiceAccountSecretEnvelopeAdapter to explicitly state that its
process-local random key cannot decrypt stored envelopes after restarts or
across instances, and that multi-instance or restart-prone deployments must
configure a durable key.

40-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reject envelopes that carry extra framing segments.

envelope.split('.') returns every segment, and the destructuring pattern drops any segment after the fourth. An envelope such as v1.<iv>.<tag>.<ciphertext>.<extra> therefore opens successfully, because AES-GCM authenticates only the three decoded fields. The trailing segment is unauthenticated and silently ignored.

The current test at services/api/test/features/iam/service-account-secret-envelope.adapter.test.ts line 37 passes only because the IV length check rejects that specific input, not because the framing is strict.

Check the segment count so the framing stays exact.

♻️ Proposed strict framing check
   public open(envelope: string): string | undefined {
     if (typeof envelope !== 'string') return undefined;
-    const [version, ivEncoded, tagEncoded, ciphertextEncoded] = envelope.split('.');
-    if (version !== 'v1' || !ivEncoded || !tagEncoded || !ciphertextEncoded) return undefined;
+    const parts = envelope.split('.');
+    if (parts.length !== 4) return undefined;
+    const [version, ivEncoded, tagEncoded, ciphertextEncoded] = parts;
+    if (version !== 'v1' || !ivEncoded || !tagEncoded || !ciphertextEncoded) return undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@services/api/src/features/iam/adapter/service-account-secret-envelope.adapter.ts`
around lines 40 - 43, Update the open method in the service-account secret
envelope adapter to reject envelopes whose split result contains anything other
than exactly four segments before decoding or decrypting. Preserve the existing
validation for the v1 version and required encoded fields, while ensuring
trailing framing segments cannot be ignored.
services/api/test/features/iam/service-account.service.test.ts (1)

71-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the module-level digestSecret helper.

Line 76 defines a local digest arrow that repeats digestSecret from line 26 of this file. Call the existing helper instead.

♻️ Proposed deduplication
   const iam = new InMemoryIamRepositoryAdapter();
   iam.seed([membership()]);
-  const digest = (secret: string) => createHash('sha256').update(secret, 'utf8').digest('hex');
   const secrets = [
-    { secret: 'dbsa_first', digest: digest('dbsa_first') },
-    { secret: 'dbsa_second', digest: digest('dbsa_second') },
+    { secret: 'dbsa_first', digest: digestSecret('dbsa_first') },
+    { secret: 'dbsa_second', digest: digestSecret('dbsa_second') },
   ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/test/features/iam/service-account.service.test.ts` around lines
71 - 92, In the service helper, remove the local digest arrow and use the
existing module-level digestSecret helper when constructing the secrets array.
Keep the generated secret values and resulting digests unchanged.
services/api/test/features/iam/prisma-service-account-repository.test.ts (1)

213-213: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

This assertion cannot fail.

The test never supplies a raw secret. secretEnvelope is the literal 'v1.encrypted-envelope', and no value passed into saveServiceAccount contains 'dbsa'. The assertion therefore passes regardless of what the adapter writes, so it does not prove that raw secrets stay out of the row.

Assert the property directly instead: confirm that the persisted row carries the envelope value and no plaintext marker that was supplied separately.

♻️ Proposed assertion that can fail
   await repository.saveServiceAccount(organizationContext, value, {
     actorId: organizationContext.actorId,
     idempotencyKey: 'create-key',
     requestHash: 'b'.repeat(64),
-    secretEnvelope: 'v1.encrypted-envelope',
+    secretEnvelope: `v1.${Buffer.from('dbsa_raw_secret').toString('base64url')}`,
   });
-  assert.equal(JSON.stringify(rows[0]).includes('dbsa'), false);
+  // The row stores only the opaque envelope, never the raw secret text.
+  assert.equal(JSON.stringify(rows[0]).includes('dbsa_raw_secret'), false);
+  assert.equal(rows[0]?.createSecretEnvelope, envelope);

Adjust the secretEnvelope and envelope bindings to a single constant so both assertions reference the same value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/test/features/iam/prisma-service-account-repository.test.ts` at
line 213, Update the service-account persistence test around saveServiceAccount
to use a distinct plaintext secret marker alongside a shared
secretEnvelope/envelope constant. Assert directly that the persisted row
contains the envelope value and does not contain the separately supplied
plaintext marker, replacing the ineffective JSON.stringify(...).includes('dbsa')
check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@services/api/src/features/bua/api/entitlement-lease.dto.ts`:
- Around line 13-24: Update the OpenAPI schemas for the snapshotRevision and
securityEpoch query parameters to use type integer and maximum 9007199254740991,
matching the validation decorators in the corresponding DTO fields and
preventing fractional client values.

In
`@services/api/src/features/iam/adapter/prisma-service-account-repository.adapter.ts`:
- Around line 161-193: The persistedReplay flow must replay the original
create-time account rather than the current row after rotate or revoke. Update
the persistence model and lifecycle writes around persistedReplay to store and
preserve a create-time account snapshot with the replay envelope, or invalidate
replay metadata when lifecycle changes make it stale; ensure replayCreate
returns UNAVAILABLE for invalidated metadata and never returns the revoked
current account. Add a Prisma regression test covering rotate and revoke
behavior.

In `@services/api/src/features/iam/api/authentication.controller.ts`:
- Line 71: Make mfaReenrollmentRequired required in both
AuthenticatedPrincipalV1 and IamTenantContextV1, then validate authentication
inputs before the mappings used by me and signIn so omitted values are rejected
rather than defaulted to false. Remove the nullish fallback in the
mfaReenrollmentRequired mapping and preserve explicit boolean values.

In `@services/api/src/features/iam/api/registration.controller.ts`:
- Around line 26-29: Update admissionDigest to use HMAC-SHA-256 with a validated
registration-specific secret instead of unkeyed SHA-256, while preserving the
existing databreeze:iam:registration:{kind}:v1 namespace. Load and validate the
HMAC key through the existing configuration mechanism, keep it out of Redis
values, and support key rotation by accepting the configured current/previous
key set during admission checks.

In `@services/api/src/features/iam/application/recovery.service.ts`:
- Around line 171-173: Update the compensating-revocation handling in the
recovery flow around delivery and saveChallenge so a saveChallenge failure is
not swallowed while the challenge remains ACTIVE. Persist a retryable revocation
task or fail closed until revocation succeeds, and add a test covering delivery
failure followed by compensating-revocation failure; ensure complete() cannot
use the raw token in that state.

In `@services/api/src/features/iam/application/service-account.service.ts`:
- Around line 315-326: Bound one-time secret retention across idempotent replay
and rotation. Add and persist createIdempotencyExpiresAt when creating the
service account, reject replay in findServiceAccountByIdempotency once that
timestamp has expired, and clear createSecretEnvelope when the window expires.
Update replaceServiceAccount to clear the envelope during rotation as well.
- Around line 274-275: Update IamModule.register and the production startup
configuration to require and provide a durable serviceAccountSecretEnvelopeKey
instead of generating a random process-local key. Ensure the same configured key
is reused across restarts and replicas so service-account secret storage remains
readable; preserve the existing UNAVAILABLE behavior only when the durable key
is unavailable.

In `@services/api/src/features/iam/iam.module.ts`:
- Around line 489-504: Update the service-account initialization around
serviceAccountSecretEnvelope and serviceAccountService so durable
serviceAccountDatabase configurations require an explicit
serviceAccountSecretEnvelope or serviceAccountSecretEnvelopeKey, rejecting
missing stable secrets instead of using
randomServiceAccountSecretEnvelopeAdapter(). Retain the random adapter only for
in-memory repositories, and add a restart replay test using shared durable
storage to verify persisted envelopes remain readable.
- Around line 404-423: Update the registration admission setup in
IamModule.register so missing
registrationIpAdmission/registrationIpAdmissionCounter or
registrationEmailAdmission/registrationEmailAdmissionCounter rejects
configuration instead of creating an implicit InMemoryRecoveryAdmissionAdapter.
Retain RedisRecoveryAdmissionAdapter for configured durable counters and allow
InMemoryRecoveryAdmissionAdapter only when explicitly supplied via the admission
provider options.

---

Outside diff comments:
In `@services/api/src/features/iam/application/invitation.service.ts`:
- Around line 174-239: Update the invitation flow around saveInvitation and
delivery.deliver to persist a durable, encrypted outbox payload containing the
raw bearer token before the transaction commits, then remove or mark it
delivered after successful delivery and retry it on subsequent attempts. Ensure
the existing active token can be recovered for resend without persisting
plaintext token data, and preserve token reuse and revocation semantics so
delivery failures do not return CONFLICT permanently.

---

Nitpick comments:
In
`@services/api/prisma/migrations/20260804010000_iam_service_account_create_idempotency/migration.sql`:
- Around line 25-34: Remove the redundant CREATE UNIQUE INDEX statement named
service_accounts_create_idempotency_workspace_key from the migration, while
retaining service_accounts_create_idempotency_key and the organization-scope
partial index unchanged.

In
`@services/api/src/features/iam/adapter/service-account-secret-envelope.adapter.ts`:
- Around line 64-67: Update the doc comment for
randomServiceAccountSecretEnvelopeAdapter to explicitly state that its
process-local random key cannot decrypt stored envelopes after restarts or
across instances, and that multi-instance or restart-prone deployments must
configure a durable key.
- Around line 40-43: Update the open method in the service-account secret
envelope adapter to reject envelopes whose split result contains anything other
than exactly four segments before decoding or decrypting. Preserve the existing
validation for the v1 version and required encoded fields, while ensuring
trailing framing segments cannot be ignored.

In `@services/api/src/features/iam/application/service-account.service.ts`:
- Around line 208-231: Update createRequestHash to canonicalize permissions
before hashing by sorting the validated permission list in the JSON payload.
Preserve the existing validation and ensure semantically identical permission
sets produce the same order-independent hash for replayCreate.

In `@services/api/test/features/iam/prisma-service-account-repository.test.ts`:
- Line 213: Update the service-account persistence test around
saveServiceAccount to use a distinct plaintext secret marker alongside a shared
secretEnvelope/envelope constant. Assert directly that the persisted row
contains the envelope value and does not contain the separately supplied
plaintext marker, replacing the ineffective JSON.stringify(...).includes('dbsa')
check.

In `@services/api/test/features/iam/service-account-composition.test.ts`:
- Around line 34-46: Extend the IAM composition test around IamModule.register
and the SERVICE_ACCOUNT_SERVICE provider to verify the secret envelope is wired,
not only that ServiceAccountService is instantiated. Exercise the service’s
create path with a stub database and assert it does not return UNAVAILABLE, or
assert the envelope provider registered by IamModule.register is present and
correctly configured.

In `@services/api/test/features/iam/service-account.service.test.ts`:
- Around line 71-92: In the service helper, remove the local digest arrow and
use the existing module-level digestSecret helper when constructing the secrets
array. Keep the generated secret values and resulting digests unchanged.

In `@services/api/test/http-contract.test.ts`:
- Line 336: Extend the contract tests around the sign-in and current-session
response assertions to provide mfaReenrollmentRequired: true and verify the
responses preserve true, while retaining the existing omitted-value fallback
assertion for false.

In `@services/api/test/prisma-foundation.test.mjs`:
- Around line 573-593: Expand the migration assertions in the test covering
active invitations and service-account idempotency to validate the complete SQL
contract, not just object names. Assert the invitation index includes
membership_id and the ACTIVE status predicate, and add checks for
create_idempotency_key, create_request_hash,
service_accounts_create_idempotency_key, each required index column list, and
their partial predicates using the existing migration-content assertion pattern.
🪄 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 Plus

Run ID: 5281b260-7016-41d1-aac3-123da13b8514

📥 Commits

Reviewing files that changed from the base of the PR and between fe58177 and 2a49f79.

📒 Files selected for processing (75)
  • docs/operations/iam-010-invitation-token-2026-08-03.md
  • docs/operations/iam-registration-2026-08-03.md
  • packages/domain/src/entitlements/v1.ts
  • packages/domain/src/invitation/v1.ts
  • packages/domain/test/entitlement-lease-issuance-v1.test.mjs
  • packages/domain/test/invitation-v1.test.mjs
  • services/api/openapi/v1.json
  • services/api/prisma/migrations/20260804000000_iam_invitation_active_membership_unique/migration.sql
  • services/api/prisma/migrations/20260804010000_iam_service_account_create_idempotency/migration.sql
  • services/api/prisma/schema/iam.prisma
  • services/api/src/features/aud/adapter/in-memory-audit-attestation-repository.adapter.ts
  • services/api/src/features/aud/adapter/in-memory-audit-repository.adapter.ts
  • services/api/src/features/aud/adapter/prisma-audit-attestation-repository.adapter.ts
  • services/api/src/features/aud/adapter/prisma-audit-repository.adapter.ts
  • services/api/src/features/aud/api/audit-attestation.dto.ts
  • services/api/src/features/aud/application/audit-attestation-repository.port.ts
  • services/api/src/features/aud/application/audit-attestation.service.ts
  • services/api/src/features/aud/application/audit-repository.port.ts
  • services/api/src/features/bua/adapter/in-memory-entitlement-lease-repository.adapter.ts
  • services/api/src/features/bua/adapter/prisma-entitlement-lease-repository.adapter.ts
  • services/api/src/features/bua/api/entitlement-lease.dto.ts
  • services/api/src/features/bua/api/entitlement.controller.ts
  • services/api/src/features/bua/application/entitlement-equality.ts
  • services/api/src/features/bua/application/entitlement-lease.service.ts
  • services/api/src/features/iae/api/artifact-admission.dto.ts
  • services/api/src/features/iae/api/artifact-retention.dto.ts
  • services/api/src/features/iae/api/inbox-item.dto.ts
  • services/api/src/features/iam/adapter/iam-invitation-crypto.adapter.ts
  • services/api/src/features/iam/adapter/iam-recovery-crypto.adapter.ts
  • services/api/src/features/iam/adapter/in-memory-service-account-repository.adapter.ts
  • services/api/src/features/iam/adapter/prisma-mfa-repository.adapter.ts
  • services/api/src/features/iam/adapter/prisma-recovery-repository.adapter.ts
  • services/api/src/features/iam/adapter/prisma-service-account-repository.adapter.ts
  • services/api/src/features/iam/adapter/service-account-secret-envelope.adapter.ts
  • services/api/src/features/iam/api/auth-session.dto.ts
  • services/api/src/features/iam/api/authentication.controller.ts
  • services/api/src/features/iam/api/current-session.dto.ts
  • services/api/src/features/iam/api/registration.controller.ts
  • services/api/src/features/iam/api/registration.dto.ts
  • services/api/src/features/iam/api/service-account.controller.ts
  • services/api/src/features/iam/application/invitation.service.ts
  • services/api/src/features/iam/application/mfa-repository.port.ts
  • services/api/src/features/iam/application/recovery.service.ts
  • services/api/src/features/iam/application/registration-repository.port.ts
  • services/api/src/features/iam/application/registration.service.ts
  • services/api/src/features/iam/application/service-account-repository.port.ts
  • services/api/src/features/iam/application/service-account.service.ts
  • services/api/src/features/iam/iam.module.ts
  • services/api/src/features/sa/api/spreadsheet-audit.dto.ts
  • services/api/test/features/aud/audit-attestation-repository.test.ts
  • services/api/test/features/aud/audit-attestation.service.test.ts
  • services/api/test/features/aud/prisma-audit-attestation-repository.test.ts
  • services/api/test/features/aud/prisma-audit-repository.test.ts
  • services/api/test/features/bua/entitlement-equality.test.ts
  • services/api/test/features/bua/entitlement-lease.service.test.ts
  • services/api/test/features/bua/entitlement.controller.test.ts
  • services/api/test/features/iam/iam-invitation-crypto.adapter.test.ts
  • services/api/test/features/iam/invitation-service.test.ts
  • services/api/test/features/iam/prisma-recovery-repository.test.ts
  • services/api/test/features/iam/prisma-service-account-repository.test.ts
  • services/api/test/features/iam/recovery-composition.test.ts
  • services/api/test/features/iam/recovery-crypto.test.ts
  • services/api/test/features/iam/recovery-http.test.ts
  • services/api/test/features/iam/recovery.service.test.ts
  • services/api/test/features/iam/registration-composition.test.ts
  • services/api/test/features/iam/registration-controller.test.ts
  • services/api/test/features/iam/registration-http.test.ts
  • services/api/test/features/iam/registration.service.test.ts
  • services/api/test/features/iam/service-account-composition.test.ts
  • services/api/test/features/iam/service-account-repository.test.ts
  • services/api/test/features/iam/service-account-secret-envelope.adapter.test.ts
  • services/api/test/features/iam/service-account.controller.test.ts
  • services/api/test/features/iam/service-account.service.test.ts
  • services/api/test/http-contract.test.ts
  • services/api/test/prisma-foundation.test.mjs
💤 Files with no reviewable changes (1)
  • services/api/src/features/bua/api/entitlement.controller.ts

Comment thread services/api/src/features/bua/api/entitlement-lease.dto.ts
Comment thread services/api/src/features/iam/api/authentication.controller.ts Outdated
Comment thread services/api/src/features/iam/api/registration.controller.ts Outdated
Comment on lines +171 to +173
} catch {
// The challenge remains unusable only if the compensating revocation also fails.
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline services/api/src/features/iam/application/recovery.service.ts --match RecoveryService --view expanded
rg -n -C 12 'delivery\.deliver|findChallengeByTokenDigest|saveChallenge|IAM_RECOVERY_REVOKE_INVALID' \
  services/api/src/features/iam/application/recovery.service.ts
rg -n -C 10 'delivery failures|saveChallenge|REVOKED|ACTIVE' \
  services/api/test/features/iam/recovery.service.test.ts

Repository: DatabreezeService/databreeze-platform

Length of output: 10319


Improper Credential Lifecycle (CWE-664)

Reachability: External · Exploitability: Moderate

Reachability path
● Entry
  services/api/src/features/iam/adapter/iam-invitation-crypto.adapter.ts:7
  validKey
│
▼
● Hop
  services/api/src/features/iam/adapter/iam-recovery-crypto.adapter.ts
│
▼
● Sink
  services/api/src/features/iam/application/recovery.service.ts

Do not ignore compensating-revocation failures.

If delivery.deliver throws after partial delivery and the compensating saveChallenge fails, the challenge remains ACTIVE. complete() can then use the raw token to reset the password. Persist a retryable revocation task or fail closed until revocation succeeds. Add a test for this failure path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/api/src/features/iam/application/recovery.service.ts` around lines
171 - 173, Update the compensating-revocation handling in the recovery flow
around delivery and saveChallenge so a saveChallenge failure is not swallowed
while the challenge remains ACTIVE. Persist a retryable revocation task or fail
closed until revocation succeeds, and add a test covering delivery failure
followed by compensating-revocation failure; ensure complete() cannot use the
raw token in that state.

Comment thread services/api/src/features/iam/iam.module.ts
Comment thread services/api/src/features/iam/iam.module.ts
@BeforeLights
BeforeLights merged commit a74c6d0 into main Aug 4, 2026
9 of 10 checks passed
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