Skip to content

edi json, access token, edi pipeline, edi envelope, cdc fix, outbound path testing - #16

Merged
pramodnarayana merged 5 commits into
developmentfrom
feature/edi-json
Jul 11, 2026
Merged

edi json, access token, edi pipeline, edi envelope, cdc fix, outbound path testing#16
pramodnarayana merged 5 commits into
developmentfrom
feature/edi-json

Conversation

@pramodnarayana

@pramodnarayana pramodnarayana commented Jul 11, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Added Developers → API Access page to create, view, revoke, and delete developer API tokens.
    • Added developer API-key authentication for tenant-scoped usage.
    • Expanded inbound/outbound route creation with trading-partner + ISA/GS mapping and outbound default standard/version.
    • Added outbound EDI JSON submission to translate and deliver messages with X12/EDIFACT envelope generation.
    • Added AS2 certificate rotation (generate/upload).
  • Bug Fixes

    • Improved inbound AS2 routing by matching ISA sender/receiver to the correct inbound route.
    • Hardened outbound HTTP delivery against unsafe destination addresses.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 25 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0b889055-a22d-4aaa-baae-739f909cad90

📥 Commits

Reviewing files that changed from the base of the PR and between 81b2e05 and 4c0dfd5.

📒 Files selected for processing (3)
  • libs/database/tests/test_database_repository.py
  • libs/transformer/src/transformer/domain/envelope/edifact.py
  • services/api/src/api/services/as2_receive_service.py

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key: "version"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

This PR adds API token management, outbound EDI JSON submission, route configuration fields, tenant-aware storage and replication, CDC queue routing, envelope construction, a unified worker entrypoint, and supporting frontend, infrastructure, migration, and test updates.

Changes

Platform infrastructure and domain foundations

Layer / File(s) Summary
Development, identity, and CDC infrastructure
.agents/AGENTS.md, Makefile, docker-compose.yml, docker/*, infra/zitadel/*, libs/domain/*, libs/identity/*
Development commands, Debezium proxying, SQS queues, local identity provisioning, domain packaging, and tenant session resolution are updated.
Database models, migrations, and contracts
libs/database/*, libs/config/*, services/api/src/api/domain/*, services/api/src/api/ports/*
API token, EDI JSON, route, timestamp, tenant-default, storage, and replicated configuration models and contracts are added or reshaped.

Frontend and API workflows

Layer / File(s) Summary
API token access and route configuration UI
frontend/web/src/components/ui/*, frontend/web/src/features/developers/*, frontend/web/src/features/routes/*, frontend/web/src/routes/*
Token creation, listing, revocation, deletion, credential display, route forms, route metadata, and tenant navigation are added.
API services and HTTP workflows
services/api/src/api/*
API-key authentication, token lifecycle endpoints, outbound EDI JSON submission, route and partner services, certificate rotation, CDC relay routing, and service wiring are introduced.

Processing workers

Layer / File(s) Summary
Translation, envelopes, storage, and delivery
libs/pipeline/*, libs/transformer/*
Payload storage is configurable, JSON-to-EDI translation supports X12 and EDIFACT envelopes, metadata extraction is added, and delivery reads persisted payloads.
Provisioning and worker orchestration
services/worker/*
Typed worker ports, database replication, SQS processing, Vault access, provisioning error handling, and the unified worker entrypoint are added.

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

Possibly related PRs

Poem

A rabbit hops through queues of code,
With tokens tucked in carrot mode.
Routes bloom, envelopes fly,
Workers hum beneath the sky. 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is related to the PR, but it reads like a keyword list rather than a concise summary of the main change. Replace it with a short sentence that captures the primary change, e.g. "Add EDI JSON pipeline, API tokens, and route/outbox updates."
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 feature/edi-json

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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: 52

Caution

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

⚠️ Outside diff range comments (4)
services/api/tests/test_cdc_relay.py (1)

98-113: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test names and docstrings contradict the new behavior.

test_cdc_relay_rejects_unknown_table (line 98) and test_cdc_relay_rejects_missing_trace_id (line 142) both assert HTTP 200 with {"status": "ok"}, yet their names say "rejects" and their docstrings say "fails explicitly" and "must be rejected." The CDC relay now silently accepts and ignores these events instead of rejecting them. Update the test names and docstrings to reflect the actual behavior (e.g., ignores_unknown_table, skips_missing_trace_id).

✏️ Suggested rename and docstring updates
-def test_cdc_relay_rejects_unknown_table(memory_queue: InMemoryQueueAdapter) -> None:
-    """Tests the CDC relay fails explicitly on unknown table sources to prevent silent drops."""
+def test_cdc_relay_ignores_unknown_table(memory_queue: InMemoryQueueAdapter) -> None:
+    """Tests the CDC relay silently ignores events from unknown tables to avoid blocking the pipeline."""
-def test_cdc_relay_rejects_missing_trace_id(memory_queue: InMemoryQueueAdapter) -> None:
-    """Payloads without trace_id must be rejected to prevent poison messages in SQS."""
+def test_cdc_relay_skips_missing_trace_id(memory_queue: InMemoryQueueAdapter) -> None:
+    """Payloads without trace_id are acknowledged but not enqueued to prevent poison messages in SQS."""

Also applies to: 142-156

🤖 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/tests/test_cdc_relay.py` around lines 98 - 113, Rename
test_cdc_relay_rejects_unknown_table and test_cdc_relay_rejects_missing_trace_id
to reflect that these events are accepted but ignored, such as
ignores_unknown_table and skips_missing_trace_id. Update both docstrings to
describe the silent ignore/skip behavior while preserving their existing HTTP
200, response, and queue assertions.
services/api/src/api/routers/webhooks/webhook.py (1)

36-38: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

SSRF check is vulnerable to DNS rebinding (TOCTOU).

The hostname is resolved and validated at webhook creation time, but the URL (not the IP) is persisted. When the webhook is delivered later, the hostname is resolved again — potentially to a different IP. An attacker controlling DNS can return a public IP during validation and a private/internal IP during delivery, bypassing the SSRF protection entirely.

Additionally, socket.gethostbyname returns only one IPv4 address; if the hostname resolves to multiple IPs, only one is validated.

Consider one or more of the following mitigations:

  1. Pin the resolved IP — store the validated IP alongside the URL and use it for the outbound connection (set Host header to the original hostname).
  2. Re-validate at delivery time — repeat the SSRF check in the delivery worker before making the HTTP request.
  3. Use a custom DNS resolver with caching/pinning to ensure the delivery-time resolution matches the validation-time resolution.
🛡️ Proposed re-validation at delivery time
 ip = await anyio.to_thread.run_sync(socket.gethostbyname, hostname)
 ip_obj = ipaddress.ip_address(ip)
-if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_unspecified:
+if (
+    ip_obj.is_private
+    or ip_obj.is_loopback
+    or ip_obj.is_unspecified
+    or ip_obj.is_link_local
+    or ip_obj.is_multicast
+    or ip_obj.is_reserved
+):
     raise HTTPException(status_code=400, detail="Webhook URL must be a public address")

Also add is_link_local explicitly as defense-in-depth for cloud metadata endpoint (169.254.169.254) protection, since is_private coverage of this range varies across Python versions.

🤖 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/api/routers/webhooks/webhook.py` around lines 36 - 38,
Mitigate DNS rebinding by repeating SSRF validation immediately before the
outbound request in the webhook delivery worker, and validate every address
returned by DNS rather than only one IPv4 result. Update the shared validation
logic around socket.gethostbyname and delivery code to reject private, loopback,
unspecified, and explicitly is_link_local addresses, including IPv6 results,
before connecting.
libs/pipeline/tests/test_delivery_service_as2.py (1)

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

Remove dead storage and edi_s3_uri variables from AS2 delivery tests.

After the refactoring, make_service no longer accepts a storage adapter and _seed_as2_route takes edi_data directly. However, several tests still create InMemoryStorageAdapter() instances and edi_s3_uri variables that are never used. This dead code appears in test_deliver_as2_plain_no_crypto, test_deliver_as2_http_failure_sets_failed_status, test_deliver_as2_null_adapter_is_caught_and_marked_failed, test_deliver_as2_idempotent_claim, and test_deliver_as2_missing_local_partner_sets_failed.

♻️ Example cleanup for test_deliver_as2_plain_no_crypto
 async def test_deliver_as2_plain_no_crypto() -> None:
     """
     When no crypto material is configured, the raw EDI payload is transmitted
     as-is and the AS2 HTTP headers are correctly set.
     """
     # ── Arrange ────────────────────────────────────────────────────────────────
-    storage = InMemoryStorageAdapter()
     repo = InMemoryRepositoryAdapter()
     as2_adapter = FakeAS2DeliveryAdapter()

     trace_id = "trace-as2-plain"
-    edi_s3_uri = f"s3://bucket/edi/{trace_id}/raw.edi"
     raw_edi = (
         b"ISA*00*          *00*          *ZZ*SENDER         "
         b"*ZZ*RECEIVER       *210101*1200*^*00501*000000001*0*P*>~"
     )
-    storage.store[edi_s3_uri] = raw_edi
     _seed_as2_route(repo, trace_id, raw_edi.decode("utf-8"))
🤖 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 `@libs/pipeline/tests/test_delivery_service_as2.py` around lines 93 - 113,
Remove the unused InMemoryStorageAdapter instances and edi_s3_uri variables from
test_deliver_as2_plain_no_crypto,
test_deliver_as2_http_failure_sets_failed_status,
test_deliver_as2_null_adapter_is_caught_and_marked_failed,
test_deliver_as2_idempotent_claim, and
test_deliver_as2_missing_local_partner_sets_failed; keep each test’s raw EDI
data passed directly through _seed_as2_route and preserve the existing delivery
assertions.
services/api/src/api/routers/trading_partners/platform/as2_partners.py (1)

72-85: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use p.id consistently and add a null check on the fetched partner.

The create endpoint uses entity.partner_id for the id field while the list and update endpoints use p.id / updated_partner.id from the fetched record. Additionally, unlike the update endpoint (line 135–136), there's no null check on p after the fetch — an AttributeError would occur if get_as2_partner returns None.

🔧 Proposed fix for consistency and safety
             await uow.commit()
             p = await uow.control_plane.get_as2_partner(tenant_id=0, partner_id=entity.partner_id)
+
+            if not p:
+                raise HTTPException(status_code=404, detail="Partner not found after creation")

             return AS2TradingPartnerResponse(
-                id=str(entity.partner_id),
+                id=str(p.id),
                 name=p.name,
                 as2_id=p.as2_id,
                 is_local=p.is_local,
                 url=p.url,
                 active=p.active,
             )
🤖 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/api/routers/trading_partners/platform/as2_partners.py`
around lines 72 - 85, In the create endpoint, update the fetched partner
handling after get_as2_partner to check whether p is None and raise the same
appropriate not-found error used by the update endpoint; then populate
AS2TradingPartnerResponse.id from p.id instead of entity.partner_id, keeping
identifiers consistent with the list and update endpoints.
🤖 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 `@docker/localstack/init-aws.sh`:
- Around line 17-23: Add dead-letter queues and redrive policies for
TranslateQueue, DeliverQueue, and ProvisioningQueue in init-aws.sh, matching the
existing EdiTransformerQueue pattern and using maxReceiveCount=3 so
poison-message handling can be tested locally.

In `@frontend/web/src/components/ui/alert.tsx`:
- Around line 35-57: Update AlertTitle and AlertDescription to use ref types
matching their rendered elements: HTMLHeadingElement for the h5 and
HTMLDivElement for the div. Since the project uses React 19, remove
React.forwardRef and accept ref as a regular prop while preserving the existing
className and prop handling.

In `@frontend/web/src/components/ui/dropdown-menu.tsx`:
- Around line 19-37: Update DropdownMenuSubTrigger and the other dropdown-menu
wrappers to accept ref directly in their component props instead of using
React.forwardRef, matching the React 19 pattern while preserving existing ref
forwarding behavior, props, styling, and exported component APIs.

In `@frontend/web/src/features/developers/api/apiTokenHooks.ts`:
- Around line 43-49: Add an enabled condition to useApiTokensQuery so useQuery
only runs when a valid access token is available, rather than invoking
repo.getApiTokens with the repository’s empty-token fallback. Use the same
authentication state that supplies useRepository, and ensure the query becomes
enabled and fetches once the token is restored.

In `@frontend/web/src/features/developers/components/ApiTokensTable.tsx`:
- Around line 91-97: Update the last_used_at cell renderer in ApiTokensTable to
wrap parseISO and formatDistanceToNow in the same try-catch pattern used by the
created_at column, returning the existing fallback for invalid dates instead of
allowing parsing errors to crash the table.

In `@frontend/web/src/features/developers/components/TokenCredentialsModal.tsx`:
- Around line 25-34: Handle rejected clipboard writes in copyToClipboard by
wrapping navigator.clipboard.writeText in try/catch, and only update
copiedSecret or copiedId state and timers after a successful write. Add
appropriate user feedback for failures, such as displaying an error state or
notification, without allowing an unhandled rejection.

In `@frontend/web/src/features/routes/components/CreateRouteModal.tsx`:
- Around line 14-26: Update the Dialog in CreateRouteModal to use the default
modal behavior by removing modal={false}, preserving focus trapping and dialog
semantics. Keep onPointerDownOutside preventing dismissal for the non-closable
form, and remove the contradictory onFocusOutside prevention unless a specific
accessibility requirement requires it.

In `@infra/zitadel/machinekey.json`:
- Line 1: Remove the committed RSA private key from machinekey.json, immediately
rotate/revoke the Zitadel service-account credential, and purge the file and
secret from all Git history using git filter-repo or BFG before force-pushing.
Add machinekey.json or its containing directory to .gitignore, then update the
runtime configuration to read the replacement credential from an injected
environment variable or secret manager rather than repository files.

In
`@libs/database/src/database/migrations/global/versions/94b943b2920d_global_initial_schema.py`:
- Around line 147-150: Remove the redundant uniqueness definition for
api_tokens.client_id: update the api_tokens table declaration and migration
operations so only either UniqueConstraint("client_id") or the unique index
ix_api_tokens_client_id remains, preferably retaining the unique index and
deleting the table-level constraint.

In `@libs/database/src/database/models/common.py`:
- Around line 40-46: Update the timestamp definitions in the common model mixin
and OutboxMixin to use UTC-aware values: replace datetime.utcnow with
datetime.now(timezone.utc) and configure each corresponding DateTime column with
timezone=True, including OutboxMixin.created_at.

In `@libs/database/src/database/models/control_plane.py`:
- Around line 98-105: Remove the redundant unique index declaration from the
model’s __table_args__, keeping client_id’s existing unique=True constraint;
ensure no separate Index is created for client_id.

In `@libs/database/src/database/models/data_plane.py`:
- Line 165: Remove the inline created_at and updated_at column declarations from
EdiMessage, allowing TimestampMixin to provide them without duplication.

In `@libs/database/src/database/models/replicated_mixins.py`:
- Around line 61-62: Add a matching database-side server_default of false to
every active column with a Python default across AS2PartnerMixin,
AS2PartnershipMixin, SFTPPartnerMixin, WebhookMixin, InboundRouteMixin, and
OutboundRouteMixin. Preserve the existing Python default and Boolean type,
following the established pattern used by OutboundRouteMixin.default_standard
and default_version.

In `@libs/database/src/database/repository.py`:
- Around line 67-77: Update the repository method get_inbound_route to accept a
tenant_id parameter and add InboundRoute.tenant_id == tenant_id to its query
filters alongside the ISA IDs and active condition; update all callers to
provide the appropriate tenant ID.

In `@libs/identity/docker/docker-compose.yml`:
- Line 45: Remove the committed Zitadel machine credential file
infra/zitadel/machinekey.json from version control, remove any related volume
mounts such as the machinekey mapping in the Docker Compose configuration, and
rotate/revoke the exposed credential by generating a replacement.

In `@libs/pipeline/src/pipeline/adapters/transformer.py`:
- Line 38: Remove the unused import datetime and delete the discarded
datetime.datetime.utcnow() call in the affected transformation logic, leaving
the surrounding behavior unchanged.

In `@libs/pipeline/src/pipeline/core/translate.py`:
- Line 108: Remove the unused json.dumps(json_dict).encode("utf-8") statement
and the redundant edi_msg.get("tenant_id") call from the translation logic;
retain the existing tenant_id usage near line 132 and ensure behavior remains
unchanged.

In `@libs/transformer/src/transformer/domain/envelope/edifact.py`:
- Line 71: Replace the naive UTC timestamp created with
datetime.datetime.utcnow() in the envelope timestamp logic with
datetime.datetime.now(datetime.timezone.utc), ensuring the resulting datetime is
timezone-aware and compatible with aware datetime comparisons.

In `@libs/transformer/src/transformer/domain/envelope/x12.py`:
- Around line 110-116: Replace the random generation of ISA13 and GS06 in the
envelope-generation logic with collision-safe monotonic control numbers,
preferably sourced from a database-backed sequence or distributed counter scoped
to the trading partner relationship; preserve the required formatting and ensure
each value is unique across concurrent messages.
- Line 110: Replace the deprecated datetime.datetime.utcnow() call in the
envelope timestamp logic with datetime.datetime.now(datetime.timezone.utc),
ensuring the resulting timestamp is timezone-aware and remains in UTC.

In `@services/api/src/api/adapters/repository.py`:
- Around line 815-843: Update get_tenant_id_by_credentials to restrict the
ApiToken query to tokens whose expires_at is either null or later than the
current UTC time, alongside the existing client_id and active filters, so
expired tokens return None before secret verification.
- Around line 96-112: Update rotate_as2_certificates to raise ValueError
immediately when get_as2_partner returns None, before applying certificate
changes or calling session.flush(); preserve the existing rotation and flush
behavior for a found partner, matching create_as2_partnership’s missing-partner
handling.

In `@services/api/src/api/auth/api_key.py`:
- Around line 87-92: Address the multi-process invalidation gap in
invalidate_token_cache: replace the process-local _token_cache approach with a
shared Redis-backed cache and TTL, add pub/sub invalidation, or remove caching
and rely on the O(1) database lookup. Ensure revoking a token immediately
prevents cached authentication across all workers.
- Around line 62-63: Fix the authentication bypass in the token-cache logic:
update the cache used by the authentication function to store both the tenant_id
and the client_secret hash, compute the provided secret’s hash before checking
the cache, and on cache hits compare hashes using constant-time verification
before returning the tenant_id. Apply the same validation to the cache
write/read paths around the referenced authentication method.

In `@services/api/src/api/cdc_relay.py`:
- Around line 64-72: Handle non-mapping elements in the raw_events loop before
constructing DebeziumUnwrappedEvent, or catch the resulting TypeError alongside
ValidationError. Log the invalid payload and continue so malformed items are
skipped without failing the batch; update the exception handling around the
validated_events append.
- Around line 86-89: Handle malformed string payloads in the event payload
parsing logic by catching JSONDecodeError around json.loads within the CDC relay
handler, logging or recording the invalid payload as appropriate, and
skipping/acknowledging the event instead of allowing the exception to propagate
and trigger retries. Use the surrounding handler’s existing skip/error flow and
symbols to preserve consistent behavior.

In `@services/api/src/api/core/services/route_service.py`:
- Around line 93-199: Extract the duplicated destination type/name selection
from both loops in list_routes into a private helper method, such as
_resolve_destination, accepting a route and the fetched AS2, SFTP, and webhook
name mappings. Have it return the destination type and name, handling inbound
webhook destinations while preserving outbound behavior, then call it from both
loops.

In `@services/api/src/api/domain/models.py`:
- Around line 205-220: Replace CreateApiTokenCmd.expires_at’s Any | None
annotation with datetime | None and import datetime as needed. Propagate the
explicit datetime | None type through the related API token port and adapter
method signatures, replacing object | None or Any | None while preserving
existing behavior.

In `@services/api/src/api/routers/developers/api_tokens.py`:
- Around line 39-42: Replace the placeholder tenant_name logic and remove the
TODO comments by fetching the actual tenant name from the tenant repository
using tenant_id, then pass that value to the token service’s prefix/slug
generation logic. Handle a missing tenant consistently with existing repository
conventions before generating the token.
- Around line 88-101: The hard-delete endpoint currently permits any
authenticated tenant user. Update delete_api_token to add
require_platform_admin() as a dependency, while retaining tenant resolution and
repository injection, so only platform administrators can invoke the
irreversible deletion.
- Around line 44-55: Replace the `"just now"` placeholder in the
`ApiTokenCreatedResponse` returned by the token-creation handler with
`token.created_at` when available; otherwise use the current UTC timestamp in
the format expected by `ApiTokenCreatedResponse.created_at`, ensuring the
response remains type-safe and parseable.

In `@services/api/src/api/routers/routes.py`:
- Around line 119-127: Move UNSET, UpdateInboundRouteCmd, and
UpdateOutboundRouteCmd from the inbound and outbound route handler bodies to the
module-level imports, alongside the existing Create* command imports; remove the
now-redundant inline imports.
- Line 47: Update the docstrings for the route creation methods near the
referenced symbols to remove “directly in the Tenant Data Plane” and describe
routes as being managed in the global/control-plane schema, consistent with the
uow.control_plane implementation and migration.

In `@services/api/src/api/routers/trading_partners/as2.py`:
- Around line 156-159: Update the certificate rotation endpoint’s exception
handling around the shown block: handle ValueError separately with HTTP 400 and
a safe client-facing detail, log unexpected exceptions with context, and return
HTTP 500 without exposing str(e). Make vault.delete_secret cleanup best-effort
by catching and logging cleanup failures so it cannot mask the original
exception; preserve exception chaining where applicable.

In `@services/api/src/api/services/as2_receive_service.py`:
- Around line 292-314: Update _extract_isa_headers to decode pure_edi_bytes
using a one-to-one encoding such as latin-1 instead of ASCII with
errors="ignore", preserving fixed byte-to-character offsets for
element_separator, isa_segment, and sender/receiver extraction.
- Around line 327-336: The inbound-route lookup in the AS2 receive flow is not
sufficiently scoped and may resolve the wrong tenant. Update get_inbound_route
and its call from the AS2 receive service to include the tenant/partnership and
transaction_type keys required by the tenant-scoped unique index, then derive
true_tenant_id only from the fully scoped route result.

In `@services/api/src/api/services/outbound_service.py`:
- Around line 57-80: List payloads currently skip transaction_type inference,
causing extraction to receive an empty type. Update the transaction_type
inference logic in the outbound service to inspect payload[0] when payload is a
non-empty list, applying the same transaction_type, heading, and ST fallback
checks before calling extractor.extract; preserve existing behavior for
dictionary payloads.

In `@services/api/tests/test_api_repository.py`:
- Around line 177-179: Strengthen the tenant-isolation assertion in the relevant
test by removing the standalone `assert "1" in compiled_clean` and the
unverified `tenant_id=` alternative. Assert that the compiled SQL explicitly
binds the tenant value in the predicate, such as `tenant_id=1` or
`tenant_idIN(1,0)`, while preserving whitespace normalization.

In `@services/worker/src/worker/adapters/db_outbox.py`:
- Around line 59-61: Replace the manual global_gen.__anext__() call in the
finally block with await global_gen.aclose(), retaining appropriate suppression
if needed, so the async generator is closed through its standard cleanup
mechanism and reliably releases the session.

In `@services/worker/src/worker/adapters/db_replication.py`:
- Around line 256-289: Add webhook_id to the on_conflict_do_update set_
dictionary in the TenantInboundRoute upsert within the inbound route replication
logic, matching the value already supplied by insert_ir_stmt.values() so
existing tenant records receive updated webhook references.
- Around line 370-406: Replace the global_model.__name__ check in _sync_deletes
with an explicit include_shared: bool parameter, using it to decide whether
tenant_id 0 or NULL records are included. Update every _sync_deletes call site
to pass the appropriate value: true for AS2Partner and AS2Partnership
synchronization, false for other models.
- Around line 33-58: Move acquisition of global_gen and tenant_gen and their
__anext__() calls inside an outer cleanup scope in
replicate_tenant_configuration, using contextlib.aclosing() so each async
generator is closed even when startup or connection acquisition fails. Preserve
tenant rollback/commit behavior, and explicitly finalize the global session
(rollback or equivalent cleanup for its read-only work) before closing both
generators; retain the existing provisioning error handling and replication
flow.

In `@services/worker/src/worker/adapters/db_tenant.py`:
- Line 13: Implement cache invalidation for the cache used by resolve_shard: add
a TTL or explicit cache-clear mechanism, ensure expired entries are refreshed,
and expose invalidation for shard reconfiguration. Update resolve_shard and
related cache access so migrated tenants or changed DSNs cannot remain cached
indefinitely.

In `@services/worker/src/worker/adapters/sqs_outbox.py`:
- Around line 84-98: Both exception branches in the SQS event context manager
currently swallow failures after queue handling. In the exception handler around
yielding the event, retain the existing permanent/transient logging and delete
behavior, then re-raise the original exception so process_next_event() can
detect the failed event.

In `@services/worker/src/worker/adapters/vault.py`:
- Around line 26-36: Update VaultAdapter.get_secret and its nested _fetch
function to fail clearly when the response contains no value or the first value
is not a string, instead of returning an empty string. Raise an appropriate
descriptive error that identifies the invalid or missing Vault secret.

In `@services/worker/src/worker/core/service.py`:
- Around line 35-50: Track whether any tenant replication fails in the global
broadcast branch of process_next_event, while continuing to attempt all tenants;
after the loop, raise TransientProvisioningError if a failure occurred so the
event is retried instead of being marked processed. Preserve the per-tenant
logger.exception diagnostics and avoid swallowing the failure.

In `@services/worker/src/worker/data/main.py`:
- Around line 137-144: Replace the print calls in process_translation with the
module logger: use logger.info for the translation start and success messages,
and logger.exception in the exception handler to preserve the traceback and
contextual trace_id.
- Around line 173-176: Apply the existing SSRF protection used by
HttpxDeliveryAdapter to HttpxAS2DeliveryAdapter by passing validate_target_url
when constructing as2_adapter, and ensure the adapter invokes it before posting
to remote_partner.url; alternatively enforce the same validation during AS2
partner creation.

In `@services/worker/src/worker/main.py`:
- Around line 14-17: Update the concurrent task orchestration in the worker
entrypoint to isolate failures: await data_main and provision_main with
asyncio.gather(..., return_exceptions=True), inspect each result, and log an
error identifying the corresponding task and exception before exiting or
handling the failure. Keep one task’s exception from cancelling its sibling.

In `@services/worker/src/worker/provision/main.py`:
- Around line 17-28: In run_worker, remove or replace the stale SQS
WaitTimeSeconds comment with wording that reflects database outbox polling, and
change the idle asyncio.sleep(0.1) delay to asyncio.sleep(1.0) to reduce
unnecessary database polling while retaining responsive processing.

In `@services/worker/tests/test_provision_worker.py`:
- Around line 9-73: Add tests alongside test_process_next_event_no_event,
test_process_next_event_tenant_specific, and test_process_next_event_global for
the requested edge cases: configure replicate_tenant_configuration to raise
PermanentProvisioningError and assert the message is deleted with the expected
behavior; configure a transient exception and assert the message remains queued;
and provide an event payload without tenant_id, verifying process_next_event
handles it gracefully. Use mocks to assert the relevant SqsOutboxAdapter message
deletion/retention interactions.

In `@TECHNICAL_DEBT.md`:
- Around line 59-60: Add blank lines after the `### No Frontend Test Runner
(Vitest)` heading and the other affected `###` heading around lines 66-67,
placing an empty line before their following content to satisfy MD022.

---

Outside diff comments:
In `@libs/pipeline/tests/test_delivery_service_as2.py`:
- Around line 93-113: Remove the unused InMemoryStorageAdapter instances and
edi_s3_uri variables from test_deliver_as2_plain_no_crypto,
test_deliver_as2_http_failure_sets_failed_status,
test_deliver_as2_null_adapter_is_caught_and_marked_failed,
test_deliver_as2_idempotent_claim, and
test_deliver_as2_missing_local_partner_sets_failed; keep each test’s raw EDI
data passed directly through _seed_as2_route and preserve the existing delivery
assertions.

In `@services/api/src/api/routers/trading_partners/platform/as2_partners.py`:
- Around line 72-85: In the create endpoint, update the fetched partner handling
after get_as2_partner to check whether p is None and raise the same appropriate
not-found error used by the update endpoint; then populate
AS2TradingPartnerResponse.id from p.id instead of entity.partner_id, keeping
identifiers consistent with the list and update endpoints.

In `@services/api/src/api/routers/webhooks/webhook.py`:
- Around line 36-38: Mitigate DNS rebinding by repeating SSRF validation
immediately before the outbound request in the webhook delivery worker, and
validate every address returned by DNS rather than only one IPv4 result. Update
the shared validation logic around socket.gethostbyname and delivery code to
reject private, loopback, unspecified, and explicitly is_link_local addresses,
including IPv6 results, before connecting.

In `@services/api/tests/test_cdc_relay.py`:
- Around line 98-113: Rename test_cdc_relay_rejects_unknown_table and
test_cdc_relay_rejects_missing_trace_id to reflect that these events are
accepted but ignored, such as ignores_unknown_table and skips_missing_trace_id.
Update both docstrings to describe the silent ignore/skip behavior while
preserving their existing HTTP 200, response, and queue assertions.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7ed0791a-af5c-40c8-9484-cdb72a20959d

📥 Commits

Reviewing files that changed from the base of the PR and between c13c86b and 4c50552.

⛔ Files ignored due to path filters (3)
  • frontend/web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • infra/zitadel/terraform.tfstate is excluded by !**/*.tfstate
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (133)
  • .agents/AGENTS.md
  • Makefile
  • TECHNICAL_DEBT.md
  • docker-compose.yml
  • docker/debezium/application-global.properties
  • docker/localstack/init-aws.sh
  • frontend/web/package.json
  • frontend/web/src/components/ui/alert.tsx
  • frontend/web/src/components/ui/dropdown-menu.tsx
  • frontend/web/src/components/ui/tabs.tsx
  • frontend/web/src/features/developers/api/IApiTokenRepository.ts
  • frontend/web/src/features/developers/api/apiTokenHooks.ts
  • frontend/web/src/features/developers/api/apiTokensApi.ts
  • frontend/web/src/features/developers/components/ApiTokensTable.tsx
  • frontend/web/src/features/developers/components/CreateApiTokenModal.tsx
  • frontend/web/src/features/developers/components/TokenCredentialsModal.tsx
  • frontend/web/src/features/developers/types.ts
  • frontend/web/src/features/partners/components/PartnershipDetails.tsx
  • frontend/web/src/features/partners/components/PartnershipsTable.tsx
  • frontend/web/src/features/partners/types.ts
  • frontend/web/src/features/routes/components/CreateRouteModal.tsx
  • frontend/web/src/features/routes/components/InboundRouteForm.tsx
  • frontend/web/src/features/routes/components/OutboundRouteForm.tsx
  • frontend/web/src/features/routes/components/RouteDetails.tsx
  • frontend/web/src/features/routes/components/RoutesTable.tsx
  • frontend/web/src/features/routes/types.ts
  • frontend/web/src/routeTree.gen.ts
  • frontend/web/src/routes/tenant.tsx
  • frontend/web/src/routes/tenant/developers.tsx
  • infra/zitadel/.terraform.lock.hcl
  • infra/zitadel/machinekey.json
  • infra/zitadel/main.tf
  • libs/config/src/config/settings.py
  • libs/database/src/database/migrations/global/versions/94b943b2920d_global_initial_schema.py
  • libs/database/src/database/migrations/tenant/versions/0841cfb2afb2_tenant_initial_schema.py
  • libs/database/src/database/models/common.py
  • libs/database/src/database/models/control_plane.py
  • libs/database/src/database/models/data_plane.py
  • libs/database/src/database/models/replicated_mixins.py
  • libs/database/src/database/repository.py
  • libs/domain/README.md
  • libs/domain/pyproject.toml
  • libs/domain/src/domain/__init__.py
  • libs/domain/src/domain/events.py
  • libs/identity/docker/docker-compose.yml
  • libs/identity/src/identity/dependencies.py
  • libs/identity/tests/test_dependencies.py
  • libs/pipeline/pyproject.toml
  • libs/pipeline/src/pipeline/adapters/repository.py
  • libs/pipeline/src/pipeline/adapters/transformer.py
  • libs/pipeline/src/pipeline/core/deliver.py
  • libs/pipeline/src/pipeline/core/metadata_extractor.py
  • libs/pipeline/src/pipeline/core/translate.py
  • libs/pipeline/src/pipeline/ports/repository.py
  • libs/pipeline/src/pipeline/ports/transformer.py
  • libs/pipeline/tests/fakes.py
  • libs/pipeline/tests/test_delivery_service.py
  • libs/pipeline/tests/test_delivery_service_as2.py
  • libs/pipeline/tests/test_pipeline_repository.py
  • libs/pipeline/tests/test_transformer.py
  • libs/pipeline/tests/test_translation_service.py
  • libs/transformer/src/transformer/domain/ast_utils.py
  • libs/transformer/src/transformer/domain/envelope/__init__.py
  • libs/transformer/src/transformer/domain/envelope/base.py
  • libs/transformer/src/transformer/domain/envelope/edifact.py
  • libs/transformer/src/transformer/domain/envelope/x12.py
  • libs/transformer/src/transformer/domain/envelope_factory.py
  • libs/transformer/tests/domain_tests/__init__.py
  • libs/transformer/tests/domain_tests/test_domain_models.py
  • pyproject.toml
  • scripts/purge_sqs.py
  • services/api/pyproject.toml
  • services/api/src/api/adapters/http/dtos.py
  • services/api/src/api/adapters/repository.py
  • services/api/src/api/adapters/vault.py
  • services/api/src/api/auth/__init__.py
  • services/api/src/api/auth/api_key.py
  • services/api/src/api/cdc_relay.py
  • services/api/src/api/core/authorization.py
  • services/api/src/api/core/provisioning.py
  • services/api/src/api/core/services/__init__.py
  • services/api/src/api/core/services/api_token_service.py
  • services/api/src/api/core/services/as2_partner_service.py
  • services/api/src/api/core/services/as2_partnership_service.py
  • services/api/src/api/core/services/route_service.py
  • services/api/src/api/core/services/sftp_partner_service.py
  • services/api/src/api/core/services/webhook_service.py
  • services/api/src/api/dependencies.py
  • services/api/src/api/domain/models.py
  • services/api/src/api/main.py
  • services/api/src/api/ports/repository.py
  • services/api/src/api/routers/developers/__init__.py
  • services/api/src/api/routers/developers/api_tokens.py
  • services/api/src/api/routers/edi_json.py
  • services/api/src/api/routers/routes.py
  • services/api/src/api/routers/trading_partners/as2.py
  • services/api/src/api/routers/trading_partners/platform/as2_partners.py
  • services/api/src/api/routers/trading_partners/platform/as2_partnerships.py
  • services/api/src/api/routers/trading_partners/sftp.py
  • services/api/src/api/routers/webhooks/webhook.py
  • services/api/src/api/services/as2_receive_service.py
  • services/api/src/api/services/outbound_service.py
  • services/api/tests/api_fakes.py
  • services/api/tests/test_api_repository.py
  • services/api/tests/test_api_token_service.py
  • services/api/tests/test_as2_partner_service.py
  • services/api/tests/test_as2_receive_service.py
  • services/api/tests/test_cdc_relay.py
  • services/api/tests/test_outbound_service.py
  • services/api/tests/test_provisioning_core.py
  • services/api/tests/test_routers_api_tokens.py
  • services/api/tests/test_routers_partners.py
  • services/api/tests/test_routers_routes.py
  • services/worker/pyproject.toml
  • services/worker/src/worker/adapters/__init__.py
  • services/worker/src/worker/adapters/db_outbox.py
  • services/worker/src/worker/adapters/db_replication.py
  • services/worker/src/worker/adapters/db_tenant.py
  • services/worker/src/worker/adapters/sqs_outbox.py
  • services/worker/src/worker/adapters/vault.py
  • services/worker/src/worker/core/__init__.py
  • services/worker/src/worker/core/errors.py
  • services/worker/src/worker/core/service.py
  • services/worker/src/worker/data/main.py
  • services/worker/src/worker/main.py
  • services/worker/src/worker/ports/__init__.py
  • services/worker/src/worker/ports/outbox.py
  • services/worker/src/worker/ports/replication.py
  • services/worker/src/worker/ports/tenant.py
  • services/worker/src/worker/provision/main.py
  • services/worker/tests/test_data_worker.py
  • services/worker/tests/test_provision_worker.py
  • services/worker/tests/test_utils.py
💤 Files with no reviewable changes (2)
  • services/worker/tests/test_utils.py
  • services/api/src/api/core/provisioning.py

Comment thread docker/localstack/init-aws.sh Outdated
Comment thread frontend/web/src/components/ui/alert.tsx Outdated
Comment thread frontend/web/src/features/developers/api/apiTokenHooks.ts
Comment thread infra/zitadel/machinekey.json Outdated
Comment on lines 173 to +176
http_adapter = HttpxDeliveryAdapter(validator=validate_target_url)
sftp_adapter = ParamikoSftpDeliveryAdapter()
vault_adapter = WorkerVaultAdapter()
as2_adapter = HttpxAS2DeliveryAdapter()

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP 'class HttpxAS2DeliveryAdapter' --type=py -A25

Repository: pramodnarayana/soopaedi

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the AS2 adapter and related validation wiring.
rg -n --hidden --glob '!**/.git/**' -e 'HttpxAS2DeliveryAdapter|validate_target_url|AS2' services/worker/src

# Show file list candidates if the adapter lives elsewhere.
git ls-files | rg 'AS2|as2|delivery|worker/data/main\.py'

Repository: pramodnarayana/soopaedi

Length of output: 5544


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where the AS2 delivery adapter is defined and how it handles target URLs.
fd -a '.*\.py$' services/worker/src | xargs rg -n --no-heading -e 'class .*AS2|def .*AS2|validate_target_url|target_url|urlparse|SSRF|allowlist|validator' || true

Repository: pramodnarayana/soopaedi

Length of output: 1400


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Broadly locate AS2-related implementation files and inspect the worker wiring.
files=$(git ls-files | rg 'as2|AS2|worker/data/main\.py')
printf '%s\n' "$files"

# If relevant files exist, show concise outlines / snippets.
for f in $files; do
  if [ -f "$f" ]; then
    echo "### $f"
    wc -l "$f"
    sed -n '1,260p' "$f" | rg -n 'HttpxAS2DeliveryAdapter|validate_target_url|target_url|validator|AS2|as2'
  fi
done

Repository: pramodnarayana/soopaedi

Length of output: 6376


🌐 Web query:

"HttpxAS2DeliveryAdapter validate_target_url SSRF"

💡 Result:

The term HttpxAS2DeliveryAdapter does not refer to a widely recognized standard library or public vulnerability. It is likely a custom implementation or a specific component within a private or niche codebase, as evidenced by its naming convention often found in internal delivery or webhook processing modules [1][2][3]. In the context of SSRF (Server-Side Request Forgery) protection for HTTP clients like httpx, the validation pattern validate_target_url is a common, though historically precarious, approach to security [4][1][2]. Key technical considerations regarding validate_target_url and SSRF include: 1. The TOCTOU (Time-of-Check to Time-of-Use) Vulnerability: Many implementations of validate_target_url perform URL validation (parsing the scheme, hostname, and checking against a blocklist) as a separate, initial step [4]. Because the actual network request is made later by the HTTP client, an attacker can exploit DNS rebinding to bypass these checks [5][4]. An attacker-controlled domain may resolve to a public IP during the validation check and subsequently to a private or internal IP (e.g., 127.0.0.1, 169.254.169.254) when the client actually connects [4]. 2. Robust Mitigation Strategies: To prevent SSRF effectively, relying solely on a validate_target_url function is generally insufficient [4]. Industry-standard solutions involve: - Using a custom, SSRF-safe httpx transport [6][7][4]. - Resolving the hostname once and performing the validation on the resolved IP addresses [8][4]. - Pinning the connection to the validated IP to ensure the request is only made to the intended, safe destination, thereby closing the DNS rebinding window [6][4]. 3. General HTTPX Security: While httpx is a robust library, it is not inherently immune to SSRF if used without proper transport-level controls [4]. Past security advisories for httpx (such as CVE-2021-41945) have focused on URL parsing and input validation issues, but the primary defense against SSRF in applications using httpx must be implemented by the developer at the transport or network layer [8][4][9][10]. If you are auditing a system containing a HttpxAS2DeliveryAdapter, you should verify whether the validate_target_url function merely parses the URL string or if it enforces secure, atomic DNS resolution and IP validation before the connection is established [4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  libs/pipeline/src/pipeline/adapters/as2.py \
  libs/pipeline/src/pipeline/ports/as2.py \
  libs/pipeline/tests/test_delivery_service_as2.py
do
  echo "### $f"
  wc -l "$f"
  sed -n '1,260p' "$f"
  echo
done

Repository: pramodnarayana/soopaedi

Length of output: 12185


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Trace how the AS2 remote URL reaches the adapter.
for f in \
  libs/pipeline/src/pipeline/core/deliver.py \
  libs/pipeline/src/pipeline/core/as2_orchestrator.py \
  services/worker/src/worker/data/main.py
do
  echo "### $f"
  wc -l "$f"
  sed -n '1,260p' "$f"
  echo
done

Repository: pramodnarayana/soopaedi

Length of output: 25954


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search the API layer for AS2 remote_url validation or sanitization.
rg -n --hidden --glob '!**/.git/**' -e 'remote_url|validate_target_url|urlparse|http[s]?://|AS2 partner|AS2Partnership|AS2Partner' services/api/src/api services/api/tests

Repository: pramodnarayana/soopaedi

Length of output: 17463


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in \
  services/api/src/api/adapters/http/dtos.py \
  services/api/src/api/domain/models.py \
  services/api/src/api/core/services/as2_partner_service.py \
  services/api/src/api/routers/trading_partners/platform/as2_partners.py \
  services/api/src/api/routers/trading_partners/platform/as2_partnerships.py
do
  echo "### $f"
  wc -l "$f"
  sed -n '1,260p' "$f"
  echo
done

Repository: pramodnarayana/soopaedi

Length of output: 39018


Add SSRF validation to AS2 delivery. HttpxAS2DeliveryAdapter posts to remote_partner.url directly, and the AS2 partner schema only uses HttpUrl—that still allows loopback/private hosts. Apply the same SSRF guard here or reject unsafe AS2 endpoints at partner creation time.

🤖 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/worker/src/worker/data/main.py` around lines 173 - 176, Apply the
existing SSRF protection used by HttpxDeliveryAdapter to HttpxAS2DeliveryAdapter
by passing validate_target_url when constructing as2_adapter, and ensure the
adapter invokes it before posting to remote_partner.url; alternatively enforce
the same validation during AS2 partner creation.

Comment on lines +14 to +17
data_task = asyncio.create_task(data_main())
provision_task = asyncio.create_task(provision_main())

await asyncio.gather(data_task, provision_task)

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No error isolation between concurrent worker tasks.

asyncio.gather without return_exceptions=True cancels the sibling task on the first exception. A transient crash in data_main will kill provision_main (and vice versa), causing unnecessary downtime for the unaffected component. There is also no logging to identify which task failed before the process exits.

🔧 Suggested fix: isolate failures and log before exiting
 async def main() -> None:
     logger.info("Starting unified Enterprise EDI Worker (Data + Provisioning)...")

-    # Run both the Provisioning and Data worker tasks concurrently
-    data_task = asyncio.create_task(data_main())
-    provision_task = asyncio.create_task(provision_main())
-
-    await asyncio.gather(data_task, provision_task)
+    results = await asyncio.gather(
+        asyncio.create_task(data_main()),
+        asyncio.create_task(provision_main()),
+        return_exceptions=True,
+    )
+
+    for name, result in zip(("data", "provision"), results):
+        if isinstance(result, BaseException):
+            logger.error("Worker '%s' exited with exception", name, exc_info=result)
+            raise result
🤖 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/worker/src/worker/main.py` around lines 14 - 17, Update the
concurrent task orchestration in the worker entrypoint to isolate failures:
await data_main and provision_main with asyncio.gather(...,
return_exceptions=True), inspect each result, and log an error identifying the
corresponding task and exception before exiting or handling the failure. Keep
one task’s exception from cancelling its sibling.

Comment on lines +17 to 28
async def run_worker(service: ProvisioningWorkerService) -> None:
logger.info("Started polling Database for PROVISION events")
while True:
global_gen = db_router.get_global_session()
global_session = await global_gen.__anext__()
try:
# Find a pending provision event
stmt = (
select(GlobalOutbox)
.where(
GlobalOutbox.status == "PENDING",
GlobalOutbox.event_type.in_(
[
"AS2_PARTNER_CREATED",
"AS2_PARTNERSHIP_CREATED",
"AS2_PARTNER_UPDATED",
"AS2_PARTNERSHIP_UPDATED",
"AS2_PARTNER_DELETED",
"AS2_PARTNERSHIP_DELETED",
"SFTP_PARTNER_CREATED",
"SFTP_PARTNER_UPDATED",
"SFTP_PARTNER_DELETED",
"WEBHOOK_CREATED",
"WEBHOOK_UPDATED",
"WEBHOOK_DELETED",
"INBOUND_ROUTE_CREATED",
"INBOUND_ROUTE_UPDATED",
"INBOUND_ROUTE_DELETED",
"OUTBOUND_ROUTE_CREATED",
"OUTBOUND_ROUTE_UPDATED",
"OUTBOUND_ROUTE_DELETED",
]
),
)
.limit(1)
.with_for_update(skip_locked=True)
)

result = await global_session.execute(stmt)
outbox_event = result.scalar_one_or_none()

if outbox_event:
payload = outbox_event.payload
tenant_id = payload.get("tenant_id")

if tenant_id is None:
logger.error(f"Missing tenant_id in provision event: {outbox_event.id}")
outbox_event.status = "FAILED"
await global_session.commit()
continue

logger.info(f"Processing provision event for tenant_id={tenant_id}")

shard_name, shard_dsn = await resolver.resolve(tenant_id)
tenant_gen = db_router.get_tenant_session(tenant_id, shard_name, shard_dsn)
tenant_session = await tenant_gen.__anext__()
try:
await replicate_tenant_config(tenant_id, global_session, tenant_session)

# Mark outbox event as processed
outbox_event.status = "PROCESSED"
await global_session.commit()
except (ValueError, KeyError) as e:
# Permanent data errors: bad payload or missing key — mark FAILED
await tenant_session.rollback()
logger.error(f"Permanent provisioning failure for tenant {tenant_id}: {e}")
outbox_event.status = "FAILED"
await global_session.commit()
except Exception as e:
# Transient errors (network, DB): leave PENDING for retry
await tenant_session.rollback()
logger.exception(
f"Transient error provisioning tenant {tenant_id}, will retry: {e}"
)
# Do NOT change status — let the poller pick it up again
finally:
with contextlib.suppress(StopAsyncIteration):
await tenant_gen.__anext__()

processed_event = await service.process_next_event()
# The SQS receive_message already blocks for up to 5 seconds (WaitTimeSeconds=5)
# We don't need to sleep here if we didn't process an event, but we can do a tiny yield
if not processed_event:
await asyncio.sleep(0.1)
except Exception as e:
logger.exception(f"Error polling global outbox: {e}")
logger.exception(f"Error in provisioning loop: {e}")
await asyncio.sleep(5)

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove or update stale SQS comment and consider a longer idle poll interval.

The comment on lines 22-23 references SQS WaitTimeSeconds=5, but this worker now polls a database outbox via ProvisioningWorkerService, not SQS. The comment is misleading and should be removed or updated.

Additionally, asyncio.sleep(0.1) when idle results in ~10 database polls per second. For a provisioning worker that processes configuration changes (low frequency), a 1-second interval would significantly reduce idle database load without meaningfully increasing provisioning latency.

♻️ Proposed fix
 async def run_worker(service: ProvisioningWorkerService) -> None:
     logger.info("Started polling Database for PROVISION events")
     while True:
         try:
             processed_event = await service.process_next_event()
-            # The SQS receive_message already blocks for up to 5 seconds (WaitTimeSeconds=5)
-            # We don't need to sleep here if we didn't process an event, but we can do a tiny yield
             if not processed_event:
-                await asyncio.sleep(0.1)
+                await asyncio.sleep(1)
         except Exception as e:
             logger.exception(f"Error in provisioning loop: {e}")
             await asyncio.sleep(5)
📝 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
async def run_worker(service: ProvisioningWorkerService) -> None:
logger.info("Started polling Database for PROVISION events")
while True:
global_gen = db_router.get_global_session()
global_session = await global_gen.__anext__()
try:
# Find a pending provision event
stmt = (
select(GlobalOutbox)
.where(
GlobalOutbox.status == "PENDING",
GlobalOutbox.event_type.in_(
[
"AS2_PARTNER_CREATED",
"AS2_PARTNERSHIP_CREATED",
"AS2_PARTNER_UPDATED",
"AS2_PARTNERSHIP_UPDATED",
"AS2_PARTNER_DELETED",
"AS2_PARTNERSHIP_DELETED",
"SFTP_PARTNER_CREATED",
"SFTP_PARTNER_UPDATED",
"SFTP_PARTNER_DELETED",
"WEBHOOK_CREATED",
"WEBHOOK_UPDATED",
"WEBHOOK_DELETED",
"INBOUND_ROUTE_CREATED",
"INBOUND_ROUTE_UPDATED",
"INBOUND_ROUTE_DELETED",
"OUTBOUND_ROUTE_CREATED",
"OUTBOUND_ROUTE_UPDATED",
"OUTBOUND_ROUTE_DELETED",
]
),
)
.limit(1)
.with_for_update(skip_locked=True)
)
result = await global_session.execute(stmt)
outbox_event = result.scalar_one_or_none()
if outbox_event:
payload = outbox_event.payload
tenant_id = payload.get("tenant_id")
if tenant_id is None:
logger.error(f"Missing tenant_id in provision event: {outbox_event.id}")
outbox_event.status = "FAILED"
await global_session.commit()
continue
logger.info(f"Processing provision event for tenant_id={tenant_id}")
shard_name, shard_dsn = await resolver.resolve(tenant_id)
tenant_gen = db_router.get_tenant_session(tenant_id, shard_name, shard_dsn)
tenant_session = await tenant_gen.__anext__()
try:
await replicate_tenant_config(tenant_id, global_session, tenant_session)
# Mark outbox event as processed
outbox_event.status = "PROCESSED"
await global_session.commit()
except (ValueError, KeyError) as e:
# Permanent data errors: bad payload or missing key — mark FAILED
await tenant_session.rollback()
logger.error(f"Permanent provisioning failure for tenant {tenant_id}: {e}")
outbox_event.status = "FAILED"
await global_session.commit()
except Exception as e:
# Transient errors (network, DB): leave PENDING for retry
await tenant_session.rollback()
logger.exception(
f"Transient error provisioning tenant {tenant_id}, will retry: {e}"
)
# Do NOT change status — let the poller pick it up again
finally:
with contextlib.suppress(StopAsyncIteration):
await tenant_gen.__anext__()
processed_event = await service.process_next_event()
# The SQS receive_message already blocks for up to 5 seconds (WaitTimeSeconds=5)
# We don't need to sleep here if we didn't process an event, but we can do a tiny yield
if not processed_event:
await asyncio.sleep(0.1)
except Exception as e:
logger.exception(f"Error polling global outbox: {e}")
logger.exception(f"Error in provisioning loop: {e}")
await asyncio.sleep(5)
async def run_worker(service: ProvisioningWorkerService) -> None:
logger.info("Started polling Database for PROVISION events")
while True:
try:
processed_event = await service.process_next_event()
if not processed_event:
await asyncio.sleep(1)
except Exception as e:
logger.exception(f"Error in provisioning loop: {e}")
await asyncio.sleep(5)
🤖 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/worker/src/worker/provision/main.py` around lines 17 - 28, In
run_worker, remove or replace the stale SQS WaitTimeSeconds comment with wording
that reflects database outbox polling, and change the idle asyncio.sleep(0.1)
delay to asyncio.sleep(1.0) to reduce unnecessary database polling while
retaining responsive processing.

Comment on lines +9 to +73
async def test_process_next_event_no_event() -> None:
mock_tenant = AsyncMock()
mock_outbox = MagicMock()
mock_replication = AsyncMock()

# Setup the async context manager to yield None
from contextlib import asynccontextmanager

@asynccontextmanager
async def fake_cm():
yield None

mock_outbox.process_next_event.return_value = fake_cm()

svc = ProvisioningWorkerService(mock_tenant, mock_outbox, mock_replication)
result = await svc.process_next_event()
assert result is False
mock_replication.replicate_tenant_configuration.assert_not_called()


async def test_process_next_event_tenant_specific() -> None:
mock_tenant = AsyncMock()
mock_outbox = MagicMock()
mock_replication = AsyncMock()

mock_event = MagicMock()
mock_event.payload = {"tenant_id": 99}

from contextlib import asynccontextmanager

@asynccontextmanager
async def fake_cm():
yield mock_event

mock_outbox.process_next_event.return_value = fake_cm()

svc = ProvisioningWorkerService(mock_tenant, mock_outbox, mock_replication)
result = await svc.process_next_event()
assert result is True
mock_replication.replicate_tenant_configuration.assert_awaited_once_with(99)


async def test_process_next_event_global() -> None:
mock_tenant = AsyncMock()
mock_outbox = MagicMock()
mock_replication = AsyncMock()

mock_event = MagicMock()
mock_event.payload = {"tenant_id": 0}

from contextlib import asynccontextmanager

@asynccontextmanager
async def fake_cm():
yield mock_event

mock_outbox.process_next_event.return_value = fake_cm()

mock_tenant.get_all_tenant_ids.return_value = [1, 2, 3]

svc = ProvisioningWorkerService(mock_tenant, mock_outbox, mock_replication)
result = await svc.process_next_event()

assert result is True
assert mock_replication.replicate_tenant_configuration.call_count == 3

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for error and edge-case scenarios.

The three tests cover happy paths only. Missing coverage:

  • replicate_tenant_configuration raises PermanentProvisioningError — verify message is deleted and behavior is correct.
  • replicate_tenant_configuration raises a transient exception — verify message is left on queue.
  • payload missing tenant_id key — verify graceful handling.

These paths are critical given the exception-handling logic in SqsOutboxAdapter.

🤖 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/worker/tests/test_provision_worker.py` around lines 9 - 73, Add
tests alongside test_process_next_event_no_event,
test_process_next_event_tenant_specific, and test_process_next_event_global for
the requested edge cases: configure replicate_tenant_configuration to raise
PermanentProvisioningError and assert the message is deleted with the expected
behavior; configure a transient exception and assert the message remains queued;
and provide an event payload without tenant_id, verifying process_next_event
handles it gracefully. Use mocks to assert the relevant SqsOutboxAdapter message
deletion/retention interactions.

Comment thread TECHNICAL_DEBT.md

@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.

Review continued from previous batch...

Comment thread frontend/web/src/components/ui/dropdown-menu.tsx Outdated
Comment thread frontend/web/src/features/developers/components/ApiTokensTable.tsx
Comment thread frontend/web/src/features/routes/components/CreateRouteModal.tsx Outdated
Comment thread libs/database/src/database/models/control_plane.py Outdated
Comment thread libs/database/src/database/models/data_plane.py
Comment on lines +39 to +42
# Get the tenant name from somewhere? For now, we can pass a dummy or fetch it.
# We should ideally fetch the tenant name to use in the prefix.
# For now, using a generic placeholder as the slug generator handles it.
tenant_name = f"tenant{tenant_id}"

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 | 🔵 Trivial

Address the placeholder tenant_name and TODO comments.

The tenant_name = f"tenant{tenant_id}" placeholder produces generic token prefixes (e.g., tenant1-...) instead of using the actual tenant name. The inline comments acknowledge this needs fixing. Would you like me to generate a solution that fetches the tenant name from the tenant repository and passes it to the token service?

🤖 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/api/routers/developers/api_tokens.py` around lines 39 - 42,
Replace the placeholder tenant_name logic and remove the TODO comments by
fetching the actual tenant name from the tenant repository using tenant_id, then
pass that value to the token service’s prefix/slug generation logic. Handle a
missing tenant consistently with existing repository conventions before
generating the token.

Comment on lines +44 to +55
cmd = CreateApiTokenCmd(name=request.name, expires_at=request.expires_at)

token = await service.create_token(tenant_id=tenant_id, tenant_name=tenant_name, cmd=cmd)

return ApiTokenCreatedResponse(
id=token.id,
name=token.name,
client_id=token.client_id,
client_secret=token.client_secret,
active=token.active,
created_at="just now", # This is usually handled by the DB, but since we return the entity immediately, we need a value. Let's just return the current ISO string if needed, or rely on the UI to just know it's new. Actually, we should probably fetch the record or just return a fresh datetime.
)

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Replace created_at="just now" placeholder with a real timestamp.

If ApiTokenCreatedResponse.created_at is typed as datetime, FastAPI will raise a ResponseValidationError at runtime when serializing "just now". If it's str, the frontend receives an unparseable value. Either way, this breaks the API contract.

🐛 Proposed fix
+from datetime import UTC, datetime
+
 ...
     return ApiTokenCreatedResponse(
         id=token.id,
         name=token.name,
         client_id=token.client_id,
         client_secret=token.client_secret,
         active=token.active,
-        created_at="just now",
+        created_at=token.created_at if hasattr(token, "created_at") and token.created_at else datetime.now(UTC).isoformat(),
     )

If the token entity has created_at populated after DB insert, use token.created_at directly. Otherwise, datetime.now(UTC).isoformat() is a safe fallback.

📝 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
cmd = CreateApiTokenCmd(name=request.name, expires_at=request.expires_at)
token = await service.create_token(tenant_id=tenant_id, tenant_name=tenant_name, cmd=cmd)
return ApiTokenCreatedResponse(
id=token.id,
name=token.name,
client_id=token.client_id,
client_secret=token.client_secret,
active=token.active,
created_at="just now", # This is usually handled by the DB, but since we return the entity immediately, we need a value. Let's just return the current ISO string if needed, or rely on the UI to just know it's new. Actually, we should probably fetch the record or just return a fresh datetime.
)
from datetime import UTC, datetime
cmd = CreateApiTokenCmd(name=request.name, expires_at=request.expires_at)
token = await service.create_token(tenant_id=tenant_id, tenant_name=tenant_name, cmd=cmd)
return ApiTokenCreatedResponse(
id=token.id,
name=token.name,
client_id=token.client_id,
client_secret=token.client_secret,
active=token.active,
created_at=token.created_at if hasattr(token, "created_at") and token.created_at else datetime.now(UTC).isoformat(),
)
🤖 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/api/routers/developers/api_tokens.py` around lines 44 - 55,
Replace the `"just now"` placeholder in the `ApiTokenCreatedResponse` returned
by the token-creation handler with `token.created_at` when available; otherwise
use the current UTC timestamp in the format expected by
`ApiTokenCreatedResponse.created_at`, ensuring the response remains type-safe
and parseable.

Comment on lines +88 to +101
@router.delete(
"/{token_id}/hard",
status_code=status.HTTP_204_NO_CONTENT,
)
async def delete_api_token(
token_id: UUID,
tenant_id: int = Depends(get_current_tenant_id),
repo: ApiTokenRepositoryPort = Depends(get_api_token_repo),
) -> None:
"""Permanently delete an API token."""
service = ApiTokenService(repo)
success = await service.delete_token(tenant_id, token_id)
if not success:
raise HTTPException(status_code=404, detail="Token not found")

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how other destructive endpoints handle authorization
rg -n 'AuthorizationService\|require_admin\|require_role' --type py services/api/src/api/routers/ | head -20

Repository: pramodnarayana/soopaedi

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== api_tokens.py outline ==\n'
ast-grep outline services/api/src/api/routers/developers/api_tokens.py --view expanded || true

printf '\n== api_tokens.py relevant lines ==\n'
sed -n '1,180p' services/api/src/api/routers/developers/api_tokens.py | cat -n

printf '\n== dependencies.py auth-related symbols ==\n'
ast-grep outline services/api/src/api/dependencies.py --view expanded || true

printf '\n== search for authz helpers in API routers ==\n'
rg -n 'require_platform_admin|require_admin|require_role|AuthorizationService|Depends\\(' services/api/src/api/routers services/api/src/api/dependencies.py || true

Repository: pramodnarayana/soopaedi

Length of output: 5656


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== dependencies.py excerpt ==\n'
sed -n '96,145p' services/api/src/api/dependencies.py | cat -n

printf '\n== api_token_service excerpt ==\n'
ast-grep outline services/api/src/api/core/services/api_token_service.py --view expanded || true
sed -n '1,220p' services/api/src/api/core/services/api_token_service.py | cat -n

printf '\n== authz usage around token routes ==\n'
rg -n 'require_platform_admin|get_authorization_service|AuthorizationService|api token|delete_token\\(|revoke_token\\(' services/api/src | head -200

Repository: pramodnarayana/soopaedi

Length of output: 7919


Gate hard delete with platform-admin auth

/{token_id}/hard only depends on get_current_tenant_id, so any authenticated tenant user can reach the irreversible delete path. require_platform_admin() already exists in services/api/src/api/dependencies.py; use it here if hard delete should be limited to elevated users.

🤖 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/api/routers/developers/api_tokens.py` around lines 88 - 101,
The hard-delete endpoint currently permits any authenticated tenant user. Update
delete_api_token to add require_platform_admin() as a dependency, while
retaining tenant resolution and repository injection, so only platform
administrators can invoke the irreversible deletion.

Comment thread services/api/src/api/services/outbound_service.py Outdated
Comment on lines +26 to +36
async def get_secret(self, vault_ref: str) -> str:
# HVAC is synchronous, but we wrap in asyncio.to_thread for the port
def _fetch() -> str:
resp = self.client.secrets.kv.v2.read_secret_version(
path=vault_ref, mount_point=self.mount_point
)
data = resp.get("data", {}).get("data", {})
val = next(iter(data.values()), None)
return val if isinstance(val, str) else ""

return await asyncio.to_thread(_fetch)

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and nearby callers/usages.
git ls-files services/worker/src/worker/adapters/vault.py
ast-grep outline services/worker/src/worker/adapters/vault.py --view expanded

printf '\n## Usages of get_secret\n'
rg -n "get_secret\(" services/worker -S

printf '\n## Read target file with line numbers\n'
cat -n services/worker/src/worker/adapters/vault.py

Repository: pramodnarayana/soopaedi

Length of output: 2028


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file's surrounding context in smaller chunks if needed.
wc -l services/worker/src/worker/adapters/vault.py
sed -n '1,220p' services/worker/src/worker/adapters/vault.py

Repository: pramodnarayana/soopaedi

Length of output: 1405


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the implementation of the Vault client interface and secret consumers.
rg -n "mount_point|Vault|secrets.kv.v2.read_secret_version|vault_ref|secret" services/worker/src -S

Repository: pramodnarayana/soopaedi

Length of output: 2779


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the worker entrypoints and any secret-consuming code paths.
sed -n '150,230p' services/worker/src/worker/data/main.py
printf '\n## Secret field definitions / consumers\n'
rg -n "credentials_vault_ref|auth_header_vault_ref|public_cert_vault_ref|private_key_vault_ref|prev_public_cert_vault_ref|prev_private_key_vault_ref|get_secret\(" services/worker/src -S

Repository: pramodnarayana/soopaedi

Length of output: 4883


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect schema/models for vault reference fields to infer expected secret shapes.
rg -n "credentials_vault_ref|auth_header_vault_ref|public_cert_vault_ref|private_key_vault_ref|prev_public_cert_vault_ref|prev_private_key_vault_ref" services/worker/src -S

Repository: pramodnarayana/soopaedi

Length of output: 2110


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find actual call sites and the expected shapes of secrets retrieved from Vault.
rg -n "\.get_secret\(|vault\." services -S
printf '\n## DeliveryService and related secret usage\n'
rg -n "class DeliveryService|get_secret|credentials|auth_header|private_key|public_cert" services/worker/src -S

Repository: pramodnarayana/soopaedi

Length of output: 5484


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the implementation and any helper that reads secret values.
ast-grep outline services/worker/src/worker -r --view expanded 2>/dev/null || true

Repository: pramodnarayana/soopaedi

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate DeliveryService and secret-related classes.
fd -a "delivery_service.py|service.py|*.py" services/worker/src | sed -n '1,120p'

Repository: pramodnarayana/soopaedi

Length of output: 507


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the DeliveryService definition and inspect nearby secret handling.
service_file=$(rg -l "class DeliveryService" services/worker/src || true)
printf 'SERVICE_FILE=%s\n' "${service_file:-}"
if [ -n "${service_file:-}" ]; then
  wc -l "$service_file"
  sed -n '1,260p' "$service_file"
fi

Repository: pramodnarayana/soopaedi

Length of output: 175


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search the whole repo for get_secret usage and vault adapter injection.
rg -n "get_secret\(|WorkerVaultAdapter|vault_adapter|vault=" . -S

Repository: pramodnarayana/soopaedi

Length of output: 1780


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect references to vault-backed credential fields and their consumers.
rg -n "credentials_vault_ref|auth_header_vault_ref|public_cert_vault_ref|private_key_vault_ref|prev_public_cert_vault_ref|prev_private_key_vault_ref|auth_header|private_key|public_cert" services -S

Repository: pramodnarayana/soopaedi

Length of output: 27118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the domain/service code that consumes Vault-backed refs and any empty-string handling.
rg -n "vault_ref|get_secret|WorkerVaultAdapter|empty string|not a string|isinstance\(.*str\)" . -S

Repository: pramodnarayana/soopaedi

Length of output: 26924


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for tests or docs around WorkerVaultAdapter secret semantics.
rg -n "WorkerVaultAdapter|get_secret\(|Vault" tests services/worker -S

Repository: pramodnarayana/soopaedi

Length of output: 569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the DeliveryService implementation.
service_file=$(rg -l "class DeliveryService" services/worker/src . -S || true)
printf 'SERVICE_FILE=%s\n' "${service_file:-}"
if [ -n "${service_file:-}" ]; then
  wc -l "$service_file"
  sed -n '1,260p' "$service_file"
fi

Repository: pramodnarayana/soopaedi

Length of output: 11542


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if any tests encode the expected behavior for Vault secrets.
rg -n "secret.*string|missing or not a string|VAULT_TOKEN|required in non-dev|read_secret_version" tests services -S

Repository: pramodnarayana/soopaedi

Length of output: 1108


Fail fast on missing or non-string Vault values. Returning "" here hides a Vault misconfiguration and turns it into a hard-to-debug auth failure; raise an error or log a warning instead.

🤖 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/worker/src/worker/adapters/vault.py` around lines 26 - 36, Update
VaultAdapter.get_secret and its nested _fetch function to fail clearly when the
response contains no value or the first value is not a string, instead of
returning an empty string. Raise an appropriate descriptive error that
identifies the invalid or missing Vault secret.

@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: 4

Caution

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

⚠️ Outside diff range comments (5)
services/api/src/api/adapters/repository.py (1)

117-124: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Scope tenant-scoped writes to tenant-owned partners only. get_as2_partner is shared by update_as2_identity and rotate_as2_certificates, so the tenant_id=0 fallback lets a tenant mutate global AS2 partners. Keep the shared read path, but use a write-specific lookup or admin-only guard for these mutations.

🤖 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/api/adapters/repository.py` around lines 117 - 124, Keep
get_as2_partner’s shared read behavior, but prevent update_as2_identity and
rotate_as2_certificates from using its tenant_id=0 fallback for writes. Add a
write-specific partner lookup restricted to the requesting tenant, or enforce an
equivalent admin-only guard, and route both mutation methods through it while
preserving global-partner access for permitted reads.
services/api/src/api/services/outbound_service.py (1)

116-127: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

None transaction_type passed to envelope builder produces invalid EDI.

When transaction_type can't be determined (lines 57-74), it remains None and is placed into route_config at line 119. The envelope builders call route_config.get("transaction_type", "UNKNOWN"), which returns None (not "UNKNOWN") because the key exists with value None. This results in None being used as ST01 in X12 and UNH02.01 in EDIFACT, producing invalid EDI segments. The metadata extractor on line 83 correctly uses transaction_type or "", but the route_config does not apply the same fallback.

🔧 Proposed fix
                 route_config = {
                     "default_standard": route.default_standard,
                     "default_version": route.default_version,
-                    "transaction_type": transaction_type,
+                    "transaction_type": transaction_type or "UNKNOWN",
                     "isa_sender_qualifier": route.isa_sender_qualifier,
🤖 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/api/services/outbound_service.py` around lines 116 - 127,
The route_config construction must prevent a missing transaction type from
reaching the envelope builders as None. Update the transaction_type value in the
route_config assembled by the outbound service to use the same empty-string
fallback already applied by the metadata extractor, while preserving determined
transaction types.
libs/transformer/src/transformer/domain/envelope/edifact.py (1)

76-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use monotonic counter for unb05 to match X12 builder and avoid collisions.

random.randint(1, 999999999) for the EDIFACT interchange control reference (UNB05) has the same birthday-paradox collision risk that was fixed in the X12 builder (lines 114-116 of x12.py). Two interchanges within the same trading partner relationship sharing a UNB05 can cause rejection by the receiving partner. The X12 builder was updated to use int(now.timestamp() * 1000) % 1000000000 in this PR, but the EDIFACT builder was not — apply the same approach for consistency.

🔧 Proposed fix
-        unb05 = str(random.randint(1, 999999999))
+        monotonic_counter = int(now.timestamp() * 1000) % 1000000000
+        unb05 = str(monotonic_counter)
🤖 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 `@libs/transformer/src/transformer/domain/envelope/edifact.py` at line 76,
Replace the random UNB05 generation in the EDIFACT envelope builder with the
same monotonic timestamp-based control reference used by the X12 builder, using
the existing current-time value and millisecond timestamp modulo 1,000,000,000.
Keep the result within the existing nine-digit range and align the
implementation with the X12 control-reference logic.
services/api/src/api/services/as2_receive_service.py (1)

366-387: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add explicit rollback() on error path for tenant session.

If create_edi_message or create_outbox_event raises, the finally block only calls async_gen_tenant.aclose(). Without an explicit rollback(), the session's uncommitted transaction state depends entirely on the generator's cleanup behavior. A failed commit() also leaves the session in an error state that requires rollback before disposal.

🔒️ Proposed fix
         try:
             dp_repo = SqlAlchemyDataPlaneRepository(tenant_session)
             msg_id = await dp_repo.create_edi_message(tenant_id=true_tenant_id, payload=edi_record)

             outbox_payload = {
                 "edi_message_id": str(msg_id),
                 "sender_id": as2_msg.as2_from,
                 "receiver_id": as2_msg.as2_to,
                 "status": "RECEIVED",
             }
             await dp_repo.create_outbox_event(
                 tenant_id=true_tenant_id,
                 event_type="edi_message.received",
                 payload=outbox_payload,
             )

             await tenant_session.commit()
             return str(msg_id)
+        except Exception:
+            await tenant_session.rollback()
+            raise
         finally:
             await async_gen_tenant.aclose()
🤖 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/api/services/as2_receive_service.py` around lines 366 - 387,
Update the tenant-session transaction handling around
SqlAlchemyDataPlaneRepository operations so any exception from
create_edi_message, create_outbox_event, or commit explicitly rolls back
tenant_session before closing async_gen_tenant; preserve the existing
commit-and-return path on success.
services/api/src/api/core/services/route_service.py (1)

116-130: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Parallelize partner name lookups with asyncio.gather.

The three await calls for AS2, SFTP, and webhook name resolution are independent. Sequential execution adds unnecessary latency to the list_routes endpoint.

♻️ Proposed refactor
+        import asyncio
+
+        results_tasks = await asyncio.gather(
+            self.global_repo.get_as2_partners_by_ids(tenant_id, list(as2_ids))
+            if as2_ids
+            else _empty_dict(),
+            self.global_repo.get_sftp_partners_by_ids(tenant_id, list(sftp_ids))
+            if sftp_ids
+            else _empty_dict(),
+            self.global_repo.get_webhooks_by_ids(tenant_id, list(webhook_ids))
+            if webhook_ids
+            else _empty_dict(),
+        )
+        as2_names, sftp_names, webhook_names = results_tasks

Or more readably with helper coroutines:

+        import asyncio
+
+        async def _or_empty(coro, ids):
+            return await coro if ids else {}
+
+        as2_names, sftp_names, webhook_names = await asyncio.gather(
+            _or_empty(self.global_repo.get_as2_partners_by_ids(tenant_id, list(as2_ids)), as2_ids),
+            _or_empty(self.global_repo.get_sftp_partners_by_ids(tenant_id, list(sftp_ids)), sftp_ids),
+            _or_empty(self.global_repo.get_webhooks_by_ids(tenant_id, list(webhook_ids)), webhook_ids),
+        )
🤖 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/api/core/services/route_service.py` around lines 116 - 130,
Update the partner name lookups in list_routes to execute the independent AS2,
SFTP, and webhook repository calls concurrently via asyncio.gather, while
preserving the existing empty-ID fallback dictionaries and assigning each result
to its corresponding name mapping.
♻️ Duplicate comments (1)
services/api/src/api/cdc_relay.py (1)

64-72: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Non-mapping batch element still throws uncaught TypeError.

DebeziumUnwrappedEvent(**raw_event) raises TypeError (not ValidationError) when raw_event is not a dict (e.g., a string or number in a JSON array). The except ValidationError on line 67 won't catch it, so it escapes uncaught → 500 → Debezium re-delivers the same batch indefinitely (poison loop). This was flagged in a previous review and remains unfixed.

🛠️ Proposed fix
     for raw_event in raw_events:
+        if not isinstance(raw_event, dict):
+            logger.error(f"[CDC Relay] Skipping non-object event: {raw_event!r}")
+            continue
         try:
             validated_events.append(DebeziumUnwrappedEvent(**raw_event))
         except ValidationError as e:
🤖 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/api/cdc_relay.py` around lines 64 - 72, Update the
validation handling in the raw_events loop around DebeziumUnwrappedEvent so
non-mapping elements cannot raise an uncaught TypeError. Catch and log the
invalid element alongside ValidationError, then continue processing the
remaining batch without propagating the exception.
🤖 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 `@libs/database/src/database/repository.py`:
- Around line 67-81: The get_inbound_route method can raise MultipleResultsFound
and may select an unintended cross-tenant route because its query lacks
transaction_type and deterministic selection. Update the InboundRoute lookup to
include the appropriate transaction_type constraint when available, and make the
result deterministic with ordering and a single-row limit while preserving the
existing tenant filtering behavior.

In `@libs/identity/docker/docker-compose.yml`:
- Line 45: Update the volume declaration for the zitadel service so /machinekey
uses a dedicated named volume instead of zitadel_data, and add that new volume
to the compose file’s top-level volumes section while leaving the Postgres data
mount unchanged.

In `@services/api/src/api/adapters/repository.py`:
- Around line 848-851: Throttle the last-used timestamp update in the
authentication flow around the ApiToken update so successful requests do not
write on every authentication. Only update last_used_at when the stored value is
older than the configured interval, preserving the existing tenant_id return
behavior and avoiding unnecessary row updates.

In `@services/worker/src/worker/core/service.py`:
- Around line 47-59: Update the global broadcast result handling around the
errors collection to distinguish PermanentProvisioningError from transient
exceptions. Log permanent failures and continue without adding them to the
retryable errors, while retaining only transient failures in errors so
TransientProvisioningError is raised only when retryable failures remain.

---

Outside diff comments:
In `@libs/transformer/src/transformer/domain/envelope/edifact.py`:
- Line 76: Replace the random UNB05 generation in the EDIFACT envelope builder
with the same monotonic timestamp-based control reference used by the X12
builder, using the existing current-time value and millisecond timestamp modulo
1,000,000,000. Keep the result within the existing nine-digit range and align
the implementation with the X12 control-reference logic.

In `@services/api/src/api/adapters/repository.py`:
- Around line 117-124: Keep get_as2_partner’s shared read behavior, but prevent
update_as2_identity and rotate_as2_certificates from using its tenant_id=0
fallback for writes. Add a write-specific partner lookup restricted to the
requesting tenant, or enforce an equivalent admin-only guard, and route both
mutation methods through it while preserving global-partner access for permitted
reads.

In `@services/api/src/api/core/services/route_service.py`:
- Around line 116-130: Update the partner name lookups in list_routes to execute
the independent AS2, SFTP, and webhook repository calls concurrently via
asyncio.gather, while preserving the existing empty-ID fallback dictionaries and
assigning each result to its corresponding name mapping.

In `@services/api/src/api/services/as2_receive_service.py`:
- Around line 366-387: Update the tenant-session transaction handling around
SqlAlchemyDataPlaneRepository operations so any exception from
create_edi_message, create_outbox_event, or commit explicitly rolls back
tenant_session before closing async_gen_tenant; preserve the existing
commit-and-return path on success.

In `@services/api/src/api/services/outbound_service.py`:
- Around line 116-127: The route_config construction must prevent a missing
transaction type from reaching the envelope builders as None. Update the
transaction_type value in the route_config assembled by the outbound service to
use the same empty-string fallback already applied by the metadata extractor,
while preserving determined transaction types.

---

Duplicate comments:
In `@services/api/src/api/cdc_relay.py`:
- Around line 64-72: Update the validation handling in the raw_events loop
around DebeziumUnwrappedEvent so non-mapping elements cannot raise an uncaught
TypeError. Catch and log the invalid element alongside ValidationError, then
continue processing the remaining batch without propagating the exception.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dd6b9a26-594d-4abe-be12-e90eaa7cf547

📥 Commits

Reviewing files that changed from the base of the PR and between 4c50552 and d4be8f1.

📒 Files selected for processing (39)
  • TECHNICAL_DEBT.md
  • docker/localstack/init-aws.sh
  • frontend/web/src/components/ui/alert.tsx
  • frontend/web/src/components/ui/dropdown-menu.tsx
  • frontend/web/src/features/developers/api/apiTokenHooks.ts
  • frontend/web/src/features/developers/components/ApiTokensTable.tsx
  • frontend/web/src/features/developers/components/TokenCredentialsModal.tsx
  • frontend/web/src/features/routes/components/CreateRouteModal.tsx
  • libs/database/src/database/migrations/global/versions/94b943b2920d_global_initial_schema.py
  • libs/database/src/database/models/common.py
  • libs/database/src/database/models/control_plane.py
  • libs/database/src/database/models/data_plane.py
  • libs/database/src/database/models/replicated_mixins.py
  • libs/database/src/database/repository.py
  • libs/identity/docker/docker-compose.yml
  • libs/pipeline/src/pipeline/adapters/http.py
  • libs/pipeline/src/pipeline/adapters/transformer.py
  • libs/pipeline/src/pipeline/core/translate.py
  • libs/pipeline/tests/test_http.py
  • libs/transformer/src/transformer/domain/envelope/edifact.py
  • libs/transformer/src/transformer/domain/envelope/x12.py
  • services/api/src/api/adapters/http/dtos.py
  • services/api/src/api/adapters/repository.py
  • services/api/src/api/auth/api_key.py
  • services/api/src/api/cdc_relay.py
  • services/api/src/api/core/services/route_service.py
  • services/api/src/api/domain/models.py
  • services/api/src/api/ports/repository.py
  • services/api/src/api/routers/routes.py
  • services/api/src/api/routers/trading_partners/as2.py
  • services/api/src/api/services/as2_receive_service.py
  • services/api/src/api/services/outbound_service.py
  • services/api/tests/test_cdc_relay.py
  • services/worker/src/worker/adapters/db_replication.py
  • services/worker/src/worker/adapters/sqs_outbox.py
  • services/worker/src/worker/adapters/vault.py
  • services/worker/src/worker/core/service.py
  • services/worker/src/worker/data/main.py
  • services/worker/tests/test_provision_worker.py
💤 Files with no reviewable changes (5)
  • libs/pipeline/src/pipeline/adapters/transformer.py
  • libs/pipeline/src/pipeline/core/translate.py
  • libs/database/src/database/migrations/global/versions/94b943b2920d_global_initial_schema.py
  • libs/database/src/database/models/data_plane.py
  • libs/database/src/database/models/control_plane.py

Comment thread libs/database/src/database/repository.py
Comment thread libs/identity/docker/docker-compose.yml Outdated
Comment thread services/api/src/api/adapters/repository.py Outdated
Comment thread services/worker/src/worker/core/service.py
@pramodnarayana

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 5

Caution

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

⚠️ Outside diff range comments (4)
services/api/src/api/adapters/repository.py (2)

117-124: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Keep control-plane and data-plane AS2Partner tenant semantics separate. The production lookup and its test both apply data-plane assumptions to the control-plane model.

  • services/api/src/api/adapters/repository.py#L117-L124: match shared control-plane partners using tenant_id IS NULL, not tenant zero.
  • services/api/tests/test_api_repository.py#L189-L204: construct the control-plane model and assert the generated query contains the tenant predicate.
🤖 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/api/adapters/repository.py` around lines 117 - 124, The
get_as2_partner lookup in services/api/src/api/adapters/repository.py lines
117-124 must treat shared control-plane AS2Partner records as tenant_id IS NULL
rather than tenant_id 0; update its tenant predicate accordingly. In
services/api/tests/test_api_repository.py lines 189-204, construct the
control-plane model and assert the generated query includes the NULL tenant
predicate.

843-866: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use naive UTC here or migrate ApiToken timestamps. api_tokens.last_used_at and api_tokens.expires_at are still naive DateTime, so datetime.now(UTC) can trip timezone-mismatch errors in the lookup/update path once last_used_at is set. The smallest schema-compatible fix is datetime.now(UTC).replace(tzinfo=None).

🤖 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/api/adapters/repository.py` around lines 843 - 866, Update
the timestamp initialization in the ApiToken lookup/update flow to use a naive
UTC datetime via datetime.now(UTC).replace(tzinfo=None), keeping comparisons
against expires_at and last_used_at schema-compatible. Preserve the existing
expiration filtering and hourly last_used_at update behavior.
services/api/src/api/services/outbound_service.py (1)

56-76: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not continue with an empty transaction type.

When payload inference fails, Line 119 emits an empty transaction identifier while the persisted record still receives None. Fall back to route.transaction_type, then reject the request if no type is available.

Proposed fix
             if not transaction_type:
                 # Try ST segment directly (for raw transaction payloads)
                 st = first_payload.get("ST", {})
                 if st:
                     transaction_type = st.get("ST01")
 
+            transaction_type = transaction_type or route.transaction_type
             if not transaction_type:
-                logger.warning(
-                    "Could not determine transaction_type from payload, metadata extraction might fail."
-                )
+                raise ValueError("Could not determine transaction_type")
...
-                    "transaction_type": transaction_type or "",
+                    "transaction_type": transaction_type,

Also applies to: 116-120

🤖 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/api/services/outbound_service.py` around lines 56 - 76,
Update the transaction type resolution in the outbound service after payload
inference to fall back to route.transaction_type when no type was found. Before
emitting the transaction identifier or persisting the record, reject the request
if transaction_type remains unavailable, ensuring neither path proceeds with an
empty value.
services/api/src/api/cdc_relay.py (1)

51-72: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Quarantine rejected CDC records before acknowledging them.

Every validation failure is skipped and the endpoint still returns success, permanently dropping the corresponding outbox event. Persist the raw event to a DLQ/quarantine store before continuing; if quarantine fails, return an error so Debezium retries.

Also applies to: 80-94, 110-116

🤖 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/api/cdc_relay.py` around lines 51 - 72, Update the CDC relay
validation flow around DebeziumUnwrappedEvent so every rejected raw_event is
persisted to the DLQ/quarantine store before continuing. If quarantine succeeds,
retain the current skip behavior; if it fails, return an error response instead
of acknowledging the batch so Debezium retries. Apply the same
quarantine-before-acknowledgment handling to the related failure paths at the
indicated locations.
🤖 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 `@libs/database/tests/test_database_repository.py`:
- Around line 62-86: Extend the tests for
InboundRouteRepository.get_inbound_route with a no-match case where
scalar_one_or_none returns None and assert the method returns None. Also add
coverage for tenant_id=None and transaction_type=None to verify optional filters
are handled correctly, reusing the existing mock session setup.

In `@libs/identity/docker/docker-compose.yml`:
- Around line 45-54: Update the Zitadel service environment configuration in the
start-from-init setup to set ZITADEL_FIRSTINSTANCE_MACHINEKEYPATH to the mounted
/machinekey location, ensuring generated machine keys persist in the
zitadel_machinekey volume. Also set
ZITADEL_FIRSTINSTANCE_ORG_MACHINE_MACHINEKEY_TYPE to JSON when the Terraform
machine account requires JSON output.

In `@libs/transformer/src/transformer/domain/envelope/edifact.py`:
- Line 75: Replace the timestamp-and-modulo generation in the UNB05 construction
with a sender-scoped sequence or another collision-resistant identifier that
remains unique for concurrent requests and does not repeat on a short cycle.
Preserve the required UNB05 string format and update the surrounding
envelope-building logic only as needed to obtain the identifier.

In `@services/api/src/api/adapters/repository.py`:
- Around line 516-528: Update get_inbound_route to require and propagate a
tenant_id for inbound route lookups, preventing tenant-less queries from
matching another tenant’s route. If tenant-less lookup must remain supported,
first enforce a globally unique discovery key before delegating to
InboundRouteRepository.get_inbound_route.

In `@services/api/tests/test_api_repository.py`:
- Around line 189-204: Update the test around
SqlAlchemyControlPlaneRepository.get_as2_partner_for_write to return
database.models.control_plane.AS2Partner and verify the executed statement
targets the control-plane model. Inspect mock_session.execute’s statement to
assert predicates for both the requested partner ID and tenant_id, rather than
only asserting the result is non-null.

---

Outside diff comments:
In `@services/api/src/api/adapters/repository.py`:
- Around line 117-124: The get_as2_partner lookup in
services/api/src/api/adapters/repository.py lines 117-124 must treat shared
control-plane AS2Partner records as tenant_id IS NULL rather than tenant_id 0;
update its tenant predicate accordingly. In
services/api/tests/test_api_repository.py lines 189-204, construct the
control-plane model and assert the generated query includes the NULL tenant
predicate.
- Around line 843-866: Update the timestamp initialization in the ApiToken
lookup/update flow to use a naive UTC datetime via
datetime.now(UTC).replace(tzinfo=None), keeping comparisons against expires_at
and last_used_at schema-compatible. Preserve the existing expiration filtering
and hourly last_used_at update behavior.

In `@services/api/src/api/cdc_relay.py`:
- Around line 51-72: Update the CDC relay validation flow around
DebeziumUnwrappedEvent so every rejected raw_event is persisted to the
DLQ/quarantine store before continuing. If quarantine succeeds, retain the
current skip behavior; if it fails, return an error response instead of
acknowledging the batch so Debezium retries. Apply the same
quarantine-before-acknowledgment handling to the related failure paths at the
indicated locations.

In `@services/api/src/api/services/outbound_service.py`:
- Around line 56-76: Update the transaction type resolution in the outbound
service after payload inference to fall back to route.transaction_type when no
type was found. Before emitting the transaction identifier or persisting the
record, reject the request if transaction_type remains unavailable, ensuring
neither path proceeds with an empty value.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ccf81d28-ac1e-437d-aa9b-25c58cb0b38d

📥 Commits

Reviewing files that changed from the base of the PR and between d4be8f1 and 10860ef.

📒 Files selected for processing (10)
  • libs/database/src/database/repository.py
  • libs/database/tests/test_database_repository.py
  • libs/identity/docker/docker-compose.yml
  • libs/transformer/src/transformer/domain/envelope/edifact.py
  • services/api/src/api/adapters/repository.py
  • services/api/src/api/cdc_relay.py
  • services/api/src/api/ports/repository.py
  • services/api/src/api/services/outbound_service.py
  • services/api/tests/test_api_repository.py
  • services/worker/src/worker/core/service.py

Comment thread libs/database/tests/test_database_repository.py
Comment thread libs/identity/docker/docker-compose.yml
Comment thread libs/transformer/src/transformer/domain/envelope/edifact.py Outdated
Comment thread services/api/src/api/adapters/repository.py
Comment thread services/api/tests/test_api_repository.py

@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: 1

Caution

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

⚠️ Outside diff range comments (3)
libs/transformer/src/transformer/domain/envelope/edifact.py (1)

66-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move import uuid to module level.

The import uuid statement inside build() executes on every call. While Python caches module imports, it's still a code smell and should be at the top of the file with the other imports.

♻️ Proposed fix

Move the import to the top of the file (after the existing imports shown in lines 1-7):

+import uuid
+
 class EdifactEnvelopeBuilder(BaseEnvelopeBuilder):

And remove the inline import:

         # Generation values
-        import uuid
-
         unb05 = str(uuid.uuid4().int % 1000000000).zfill(9)
🤖 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 `@libs/transformer/src/transformer/domain/envelope/edifact.py` around lines 66
- 90, Move the uuid import to the module-level import section of the EDIFACT
envelope module, and remove the inline import from the build classmethod while
preserving its existing UUID generation behavior.
services/api/src/api/services/as2_receive_service.py (1)

371-381: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Propagate trace_id into the inbound outbox payload
edi_message.received is routed through the TRANSLATE path and dropped when trace_id is missing, so inbound AS2 messages never reach translation. Add edi_record["trace_id"] to this payload, matching the outbound flow.

🤖 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/api/services/as2_receive_service.py` around lines 371 - 381,
Update the inbound outbox payload in the AS2 receive flow to include the
existing edi_record["trace_id"] value, matching the outbound event payload.
Preserve the current edi_message.received event fields and routing behavior
while ensuring trace_id is propagated for translation.
services/api/src/api/ports/repository.py (1)

80-86: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the no-filters test with the non-null tenant contract
get_inbound_route now requires tenant_id: int, and the InboundRoute model stores tenant_id as non-nullable. Update test_inbound_route_repository_get_inbound_route_no_filters to pass a real tenant id, or the test will keep masking the contract mismatch.

🤖 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/api/ports/repository.py` around lines 80 - 86, Update
test_inbound_route_repository_get_inbound_route_no_filters to pass a valid
integer tenant_id when calling get_inbound_route, matching the required non-null
tenant contract and InboundRoute model.
🤖 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 `@libs/database/tests/test_database_repository.py`:
- Around line 104-127: Update
test_inbound_route_repository_get_inbound_route_no_filters to pass a valid
tenant_id such as 1 instead of None, matching
InboundRouteRepository.get_inbound_route’s required tenant_id filter. Rename the
test to indicate that only transaction_type is unfiltered, while preserving the
existing assertions and setup.

---

Outside diff comments:
In `@libs/transformer/src/transformer/domain/envelope/edifact.py`:
- Around line 66-90: Move the uuid import to the module-level import section of
the EDIFACT envelope module, and remove the inline import from the build
classmethod while preserving its existing UUID generation behavior.

In `@services/api/src/api/ports/repository.py`:
- Around line 80-86: Update
test_inbound_route_repository_get_inbound_route_no_filters to pass a valid
integer tenant_id when calling get_inbound_route, matching the required non-null
tenant contract and InboundRoute model.

In `@services/api/src/api/services/as2_receive_service.py`:
- Around line 371-381: Update the inbound outbox payload in the AS2 receive flow
to include the existing edi_record["trace_id"] value, matching the outbound
event payload. Preserve the current edi_message.received event fields and
routing behavior while ensuring trace_id is propagated for translation.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ce566b13-559a-4410-adcd-a772b83c8125

📥 Commits

Reviewing files that changed from the base of the PR and between 10860ef and 81b2e05.

📒 Files selected for processing (11)
  • libs/database/src/database/repository.py
  • libs/database/tests/test_database_repository.py
  • libs/domain/src/domain/events.py
  • libs/identity/docker/docker-compose.yml
  • libs/transformer/src/transformer/domain/envelope/edifact.py
  • services/api/src/api/adapters/repository.py
  • services/api/src/api/cdc_relay.py
  • services/api/src/api/ports/repository.py
  • services/api/src/api/services/as2_receive_service.py
  • services/api/src/api/services/outbound_service.py
  • services/api/tests/test_api_repository.py

Comment thread libs/database/tests/test_database_repository.py Outdated
@pramodnarayana
pramodnarayana merged commit 6dfe75e into development Jul 11, 2026
2 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