Skip to content

feat: added consent notice and intent mapping logic#1586

Merged
tipusinghaw merged 7 commits intomainfrom
feat/consent-notice
Mar 18, 2026
Merged

feat: added consent notice and intent mapping logic#1586
tipusinghaw merged 7 commits intomainfrom
feat/consent-notice

Conversation

@tipusinghaw
Copy link
Copy Markdown
Contributor

@tipusinghaw tipusinghaw commented Mar 18, 2026

Summary by CodeRabbit

  • New Features

    • Intent Notice Management: create, read, update, delete intent notices with URL validation.
    • Ecosystem-Scoped Retrieval: searchable, paginated listing of notices by ecosystem with optional intent filter.
    • Consent Notice Integration: consent notice URLs are fetched and attached to verification presentations when available.
  • Chores

    • Database migration: new intent notices table and relations added to persist notices.

Signed-off-by: Tipu_Singh <tipu.singh@ayanworks.com>
Signed-off-by: Tipu_Singh <tipu.singh@ayanworks.com>
Signed-off-by: Tipu_Singh <tipu.singh@ayanworks.com>
Signed-off-by: Tipu_Singh <tipu.singh@ayanworks.com>
Signed-off-by: Tipu_Singh <tipu.singh@ayanworks.com>
Signed-off-by: Tipu_Singh <tipu.singh@ayanworks.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

Adds intent-notice management across API Gateway, Ecosystem, and OID4VC services: new DTOs/interfaces, controllers, service and repository methods, URL/consent helpers, Prisma schema + migrations, and NATS-driven propagation. Also adds optional ecosystemId to existing intent template retrieval paths.

Changes

Cohort / File(s) Summary
API Gateway — Ecosystem & Intent
apps/api-gateway/src/ecosystem/ecosystem.service.ts, apps/api-gateway/src/ecosystem/intent/intent.controller.ts
Added intent-notice CRUD methods in EcosystemService and new HTTP endpoints in IntentController (create, read, paginated ecosystem-scoped read, update, delete). Note: duplicate set of intent-notice handlers present in controller.
API Gateway — DTOs
apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts
New CreateIntentNoticeDto and UpdateIntentNoticeDto with validation and Swagger metadata.
Ecosystem Service & Controller
apps/ecosystem/src/ecosystem.service.ts, apps/ecosystem/src/ecosystem.controller.ts, apps/ecosystem/src/ecosystem.helper.ts
Added validateEcosystemLead and full Intent Notice CRUD flows, URL validation helper (validateNoticeUrl), ecosystemId support for getIntentTemplateByIntentAndOrg, and permission checks.
Ecosystem Repository
apps/ecosystem/repositories/ecosystem.repository.ts
New DB methods: create/get (single & paginated)/update/delete intent notices, existence and permission checks, plus getIntentTemplateByIntentAndOrg extended to accept ecosystemId.
OID4VC Verification — Integration & Helpers
apps/oid4vc-verification/src/oid4vc-verification.controller.ts, apps/oid4vc-verification/src/oid4vc-verification.service.ts, apps/oid4vc-verification/src/oid4vc-verification.helper.ts, apps/oid4vc-verification/interfaces/intent-notice.interfaces.ts
createIntentBasedVerificationPresentation now accepts ecosystemId and propagates it; service optionally fetches consentNoticeUrl using new fetchConsentNotice helper; new intent-notice interfaces added.
API Gateway — OID4VC Controller
apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts, apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts
Controller and service signatures updated to include ecosystemId; ecosystemId passed through to NATS payloads.
Database — Prisma & Migrations
libs/prisma-service/prisma/schema.prisma, .../migrations/20260312082536_added_intent_notice_table/migration.sql, .../migrations/20260312084927_add_org_id_to_intent_notices/migration.sql
New intent_notices model with relations to intents and organisation; initial table migration and follow-up migration adding nullable orgId with FK to organisation.
Common Messages
libs/common/src/response-messages/index.ts
Added ResponseMessages.intentNotice (success & error strings).

Sequence Diagram

sequenceDiagram
    participant Client as API Client
    participant Gateway as API Gateway<br/>(IntentController)
    participant EcosystemSvc as EcosystemService
    participant Repo as EcosystemRepository
    participant DB as Database
    participant Oid4vcSvc as Oid4vcVerificationService
    participant Helper as Helpers (validateNoticeUrl / fetchConsentNotice)

    Client->>Gateway: POST /intent/notice (CreateIntentNoticeDto)
    Gateway->>EcosystemSvc: createIntentNotice(dto, user)
    EcosystemSvc->>EcosystemSvc: validateEcosystemLead(userId, ecosystemId?)
    EcosystemSvc->>Helper: validateNoticeUrl(noticeUrl)
    Helper-->>EcosystemSvc: validated
    EcosystemSvc->>Repo: intentNoticeExists(intentId, orgId?)
    Repo->>DB: SELECT ...
    DB-->>Repo: exists?
    Repo-->>EcosystemSvc: boolean
    EcosystemSvc->>Repo: createIntentNotice(...)
    Repo->>DB: INSERT intent_notices
    DB-->>Repo: created row
    Repo-->>EcosystemSvc: notice object
    EcosystemSvc-->>Gateway: created notice
    Gateway-->>Client: 201 Created + notice

    Note over Oid4vcSvc, Repo: When creating verification presentation
    Client->>Gateway: POST /oid4vc/intent/presentation (includes ecosystemId)
    Gateway->>Oid4vcSvc: createIntentBasedVerificationPresentation(..., ecosystemId)
    Oid4vcSvc->>Repo: getIntentNoticeByIntentId(intentId, orgId?)
    Repo->>DB: SELECT noticeUrl
    DB-->>Repo: noticeUrl?
    Repo-->>Oid4vcSvc: noticeUrl
    Oid4vcSvc->>Helper: fetchConsentNotice(noticeUrl, transactionId)
    Helper-->>Oid4vcSvc: consentNoticeUrl
    Oid4vcSvc-->>Gateway: session + consentNoticeUrl
    Gateway-->>Client: 201 Created + session
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • shitrerohit
  • RinkalBhojani
  • pranalidhanavade

Poem

🐰 I hopped through code to add a notice tree,
URLs checked and linked to intent with glee,
Migrations dug tunnels and relations took root,
Services and controllers now sing en route,
Consent fetched, notices bloom — a rabbit's small coup! 🌷

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: added consent notice and intent mapping logic' accurately describes the main changes in the PR, which introduce a comprehensive consent notice management system with intent mapping across multiple services.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/consent-notice
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

You can customize the high-level summary generated by CodeRabbit.

Configure the reviews.high_level_summary_instructions setting to provide custom instructions for generating the high-level summary.

@tipusinghaw tipusinghaw changed the title Feat/consent notice https://github.com/credebl/platform/pull/1584 Mar 18, 2026
@tipusinghaw tipusinghaw changed the title https://github.com/credebl/platform/pull/1584 feat: added consent notice and intent mapping logic Mar 18, 2026
@tipusinghaw
Copy link
Copy Markdown
Contributor Author

review @coderabbitai

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 18, 2026

@tipusinghaw Sure! I'll start reviewing the PR right away.

✅ Actions performed

Review triggered.

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.

@tipusinghaw tipusinghaw requested a review from shitrerohit March 18, 2026 11:59
@tipusinghaw tipusinghaw self-assigned this Mar 18, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/api-gateway/src/ecosystem/intent/intent.controller.ts`:
- Around line 636-679: The getIntentNotices controller currently allows unscoped
reads because id and intentId are optional and no ecosystem context is enforced;
fix by requiring and validating caller ecosystem scope and passing it to the
service: extract the caller's ecosystemId from request/user context (or require
an ecosystemId query/path param), validate it (UUID and that the user has
access) and then call ecosystemService.getIntentNotices(id, intentId,
ecosystemId) or reject the request if the caller is out-of-scope; update the
EcosystemRolesGuard or add an explicit check in getIntentNotices (and update the
service signature) so notices are only returned for the scoped ecosystem, or
deprecate this route in favor of a scoped endpoint like GET
/intent/ecosystem/:ecosystemId/notice.

In `@apps/ecosystem/repositories/ecosystem.repository.ts`:
- Around line 1701-1711: The createIntentNotice(intentId, noticeUrl, userId,
orgId) is race-prone when combined with intentNoticeExists; change
createIntentNotice to perform an atomic operation (e.g., use
prisma.intent_notices.upsert or a create with unique constraint handling) so
duplicates cannot be inserted under concurrent requests, and add a DB-level
unique constraint on (intentId, orgId) in the Prisma schema followed by a
migration; also update any callers or intentNoticeExists logic to rely on the DB
uniqueness rather than a separate existence check.
- Around line 814-823: The intent-template lookup in
getIntentTemplateByIntentAndOrg currently treats ecosystemId as optional which
can return wrong templates across ecosystems; update the
prisma.intent_templates.findFirst call to always include ecosystemId in the
intent filter (remove the conditional spread ...(ecosystemId && { ecosystemId })
and use intent: { is: { name: intentName, ecosystemId } }), and add a guard at
the start of getIntentTemplateByIntentAndOrg to throw or validate when
ecosystemId is missing since intents are always created with a non-null
ecosystemId.

In `@apps/ecosystem/src/ecosystem.helper.ts`:
- Around line 12-15: The fetch call is not wired to the AbortController and the
timeout can leak if fetch throws; update the code that creates
controller/timeout and calls fetch(noticeUrl) so you pass controller.signal into
fetch (fetch(noticeUrl, { signal: controller.signal })) and wrap the await
fetch(...) in a try/finally so clearTimeout(timeout) (and optionally
controller.abort() on errors) always runs; reference the AbortController
instance named controller, the timeout variable, and the fetch(noticeUrl) call
when making the change.

In `@apps/ecosystem/src/ecosystem.service.ts`:
- Around line 1044-1051: The org membership check currently calls
getEcosystemOrg and does not validate the membership status; change it to use
findEcosystemOrg with the same active-status validation used elsewhere (replace
getEcosystemOrg(intent['ecosystemId'], orgId) with
findEcosystemOrg(intent['ecosystemId'], orgId) and verify the returned object's
status is EcosystemOrgStatus.ACTIVE, throwing the same RpcException
(HttpStatus.FORBIDDEN / 'The provided orgId is not a member or lead of this
ecosystem.') if the record is missing or not ACTIVE so behavior matches the
template assignment path.
- Around line 1054-1062: Add partitioned unique constraints for intent_notices
and handle P2002 in the repository: create a new migration that adds two unique
indexes like intent_templates (one WHERE "orgId" IS NOT NULL on
("orgId","intentId"), and one WHERE "orgId" IS NULL on ("intentId")) to prevent
duplicates, then update ecosystemRepository.createIntentNotice to catch Prisma
P2002/unique constraint errors and rethrow an RpcException with
HttpStatus.CONFLICT (mirror the createEcosystemInvitation error-mapping
pattern); keep the existing intentNoticeExists check if desired but rely on DB
constraint + repo-level P2002 handling to guarantee uniqueness.

In `@apps/oid4vc-verification/src/oid4vc-verification.helper.ts`:
- Around line 6-8: Build the consent URL using a proper URL/URLSearchParams API
instead of string concatenation: create a new URL(noticeUrl) (or new URL(base)
then set pathname) and append transactionId with
url.searchParams.set('transactionId', transactionId) to avoid double '?' when
noticeUrl already has query params; then perform the outbound call using fetch
guarded by an AbortController with a timeout (create AbortController, pass
signal to fetch, and clear the timeout on completion) so the request cannot hang
indefinitely; update the references to consentNoticeUrl and the fetch call in
the helper where they are used.

In `@apps/oid4vc-verification/src/oid4vc-verification.service.ts`:
- Around line 410-427: The code may throw inside the consent enrichment block
after the verification session is already created (createdSession,
fetchConsentNotice), causing the API to fail and clients to retry; wrap the
consent-fetching logic so failures are caught and do not propagate: when
intentId and intentNotice?.noticeUrl are present call fetchConsentNotice inside
a try/catch (around the await fetchConsentNotice call), log the error (using the
same logger or processLogger) including intentId and noticeUrl, and skip setting
createdSession.consentNoticeUrl on error; keep the existing
this.natsClient.sendNatsMessage and intentNotice lookup unchanged but ensure any
exception from fetchConsentNotice does not bubble up to the caller.

In `@libs/prisma-service/prisma/schema.prisma`:
- Around line 743-757: The intent_notices model allows duplicates because
UNIQUE(intentId, orgId) won't prevent multiple NULL orgId rows; add two partial
unique indexes to enforce DB-level uniqueness by creating a unique index on
(intentId, orgId) WHERE orgId IS NOT NULL and a unique index on intentId WHERE
orgId IS NULL (use descriptive index names like intent_notices_unique_with_org
and intent_notices_unique_no_org), or if you prefer to rely on the existing
application-level check (intentNoticeExists), explicitly document that choice
and remove the current indexes to avoid misleading constraints.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d8c1efb6-1373-45c6-9e81-2277ba03bc93

📥 Commits

Reviewing files that changed from the base of the PR and between 8e2a5df and 8a2fd10.

📒 Files selected for processing (17)
  • apps/api-gateway/src/ecosystem/ecosystem.service.ts
  • apps/api-gateway/src/ecosystem/intent/intent.controller.ts
  • apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts
  • apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts
  • apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts
  • apps/ecosystem/repositories/ecosystem.repository.ts
  • apps/ecosystem/src/ecosystem.controller.ts
  • apps/ecosystem/src/ecosystem.helper.ts
  • apps/ecosystem/src/ecosystem.service.ts
  • apps/oid4vc-verification/interfaces/intent-notice.interfaces.ts
  • apps/oid4vc-verification/src/oid4vc-verification.controller.ts
  • apps/oid4vc-verification/src/oid4vc-verification.helper.ts
  • apps/oid4vc-verification/src/oid4vc-verification.service.ts
  • libs/common/src/response-messages/index.ts
  • libs/prisma-service/prisma/migrations/20260312082536_added_intent_notice_table/migration.sql
  • libs/prisma-service/prisma/migrations/20260312084927_add_org_id_to_intent_notices/migration.sql
  • libs/prisma-service/prisma/schema.prisma

Comment on lines +636 to +679
@Get('/notice')
@ApiBearerAuth()
@Roles(OrgRoles.ECOSYSTEM_LEAD, OrgRoles.ECOSYSTEM_MEMBER)
@UseGuards(AuthGuard('jwt'), EcosystemRolesGuard)
@ApiOperation({
summary: 'Get intent notices',
description: 'Retrieves intent notices. Filter by notice id or intentId (both optional).'
})
@ApiQuery({ name: 'id', required: false, type: String, description: 'Filter by notice PK UUID (optional)' })
@ApiQuery({ name: 'intentId', required: false, type: String, description: 'Filter by intent UUID (optional)' })
@ApiResponse({ status: HttpStatus.OK, description: 'Intent notices fetched successfully', type: ApiResponseDto })
async getIntentNotices(
@Res() res: Response,
@Query(
'id',
new ParseUUIDPipe({
version: '4',
optional: true,
exceptionFactory: (): Error => {
throw new BadRequestException('Invalid notice ID');
}
})
)
id?: string,
@Query(
'intentId',
new ParseUUIDPipe({
version: '4',
optional: true,
exceptionFactory: (): Error => {
throw new BadRequestException('Invalid intent ID');
}
})
)
intentId?: string
): Promise<Response> {
const result = await this.ecosystemService.getIntentNotices(id, intentId);
const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
message: ResponseMessages.intentNotice.success.fetchAll,
data: result
};
return res.status(HttpStatus.OK).json(finalResponse);
}
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

GET /intent/notice allows unscoped cross-ecosystem reads.

At Line 647-Line 672, both filters are optional and no ecosystem/user scope is sent downstream. A user with ecosystem role access can retrieve notices outside their intended scope.

🔧 Suggested direction
 async getIntentNotices(
@@
-  ): Promise<Response> {
+  ): Promise<Response> {
+    if (!id && !intentId) {
+      throw new BadRequestException('Either id or intentId is required');
+    }

Additionally, prefer a scoped contract (ecosystemId + caller context) for this read path, or deprecate this route in favor of GET /intent/ecosystem/:ecosystemId/notice.

📝 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
@Get('/notice')
@ApiBearerAuth()
@Roles(OrgRoles.ECOSYSTEM_LEAD, OrgRoles.ECOSYSTEM_MEMBER)
@UseGuards(AuthGuard('jwt'), EcosystemRolesGuard)
@ApiOperation({
summary: 'Get intent notices',
description: 'Retrieves intent notices. Filter by notice id or intentId (both optional).'
})
@ApiQuery({ name: 'id', required: false, type: String, description: 'Filter by notice PK UUID (optional)' })
@ApiQuery({ name: 'intentId', required: false, type: String, description: 'Filter by intent UUID (optional)' })
@ApiResponse({ status: HttpStatus.OK, description: 'Intent notices fetched successfully', type: ApiResponseDto })
async getIntentNotices(
@Res() res: Response,
@Query(
'id',
new ParseUUIDPipe({
version: '4',
optional: true,
exceptionFactory: (): Error => {
throw new BadRequestException('Invalid notice ID');
}
})
)
id?: string,
@Query(
'intentId',
new ParseUUIDPipe({
version: '4',
optional: true,
exceptionFactory: (): Error => {
throw new BadRequestException('Invalid intent ID');
}
})
)
intentId?: string
): Promise<Response> {
const result = await this.ecosystemService.getIntentNotices(id, intentId);
const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
message: ResponseMessages.intentNotice.success.fetchAll,
data: result
};
return res.status(HttpStatus.OK).json(finalResponse);
}
`@Get`('/notice')
`@ApiBearerAuth`()
`@Roles`(OrgRoles.ECOSYSTEM_LEAD, OrgRoles.ECOSYSTEM_MEMBER)
`@UseGuards`(AuthGuard('jwt'), EcosystemRolesGuard)
`@ApiOperation`({
summary: 'Get intent notices',
description: 'Retrieves intent notices. Filter by notice id or intentId (both optional).'
})
`@ApiQuery`({ name: 'id', required: false, type: String, description: 'Filter by notice PK UUID (optional)' })
`@ApiQuery`({ name: 'intentId', required: false, type: String, description: 'Filter by intent UUID (optional)' })
`@ApiResponse`({ status: HttpStatus.OK, description: 'Intent notices fetched successfully', type: ApiResponseDto })
async getIntentNotices(
`@Res`() res: Response,
`@Query`(
'id',
new ParseUUIDPipe({
version: '4',
optional: true,
exceptionFactory: (): Error => {
throw new BadRequestException('Invalid notice ID');
}
})
)
id?: string,
`@Query`(
'intentId',
new ParseUUIDPipe({
version: '4',
optional: true,
exceptionFactory: (): Error => {
throw new BadRequestException('Invalid intent ID');
}
})
)
intentId?: string
): Promise<Response> {
if (!id && !intentId) {
throw new BadRequestException('Either id or intentId is required');
}
const result = await this.ecosystemService.getIntentNotices(id, intentId);
const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
message: ResponseMessages.intentNotice.success.fetchAll,
data: result
};
return res.status(HttpStatus.OK).json(finalResponse);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api-gateway/src/ecosystem/intent/intent.controller.ts` around lines 636
- 679, The getIntentNotices controller currently allows unscoped reads because
id and intentId are optional and no ecosystem context is enforced; fix by
requiring and validating caller ecosystem scope and passing it to the service:
extract the caller's ecosystemId from request/user context (or require an
ecosystemId query/path param), validate it (UUID and that the user has access)
and then call ecosystemService.getIntentNotices(id, intentId, ecosystemId) or
reject the request if the caller is out-of-scope; update the EcosystemRolesGuard
or add an explicit check in getIntentNotices (and update the service signature)
so notices are only returned for the scoped ecosystem, or deprecate this route
in favor of a scoped endpoint like GET /intent/ecosystem/:ecosystemId/notice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The ecosystem filter is applied in DB query

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check getIntentNotices implementation in the ecosystem service and repository
rg -n "getIntentNotices" --type ts -A 20 -B 2

Length of output: 29429


@tipusinghaw, that's correct — when intentId is supplied, the repository does derive the intent's ecosystemId and scopes the DB query accordingly (Lines 1726–1735 in ecosystem.repository.ts). Thanks for the clarification.

That said, two edge cases remain unscoped:

  1. Only id supplied — the where clause becomes { id } with no ecosystem filter, so a caller can fetch any notice by its PK regardless of ecosystem membership.
  2. Neither id nor intentId supplied — the where clause is {}, which returns all intent_notices across every ecosystem.

A minimal guard at the controller level would close both gaps:

async getIntentNotices(
  `@Res`() res: Response,
  id?: string,
  intentId?: string
): Promise<Response> {
  if (!id && !intentId) {
    throw new BadRequestException('Either id or intentId is required');
  }
  const result = await this.ecosystemService.getIntentNotices(id, intentId);
  ...
}

And for the id-only path, passing the caller's verified ecosystemId (from the JWT / EcosystemRolesGuard context) down to the repository so it can add intent: { is: { ecosystemId } } to the where clause would fully close the cross-ecosystem read.

Would you like to address these two residual cases, or is the GET /intent/notice route intentionally an admin/internal endpoint where cross-ecosystem reads are acceptable?


🧠 Learnings used
Learnt from: tipusinghaw
Repo: credebl/platform PR: 1584
File: apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts:10-13
Timestamp: 2026-03-18T12:02:15.511Z
Learning: In the credebl/platform repository, `noticeUrl` URL validation for intent notices is intentionally handled at the service layer via the `validateNoticeUrl` helper in `apps/ecosystem/src/ecosystem.helper.ts`, which performs a live HTTP reachability check, rather than purely at the DTO layer with `IsUrl()`. Do not flag the absence of `IsUrl()` on `noticeUrl` in `CreateIntentNoticeDto` as a missing validation.

Learnt from: pranalidhanavade
Repo: credebl/platform PR: 1547
File: libs/prisma-service/prisma/migrations/20260119130717_intent_table_creation/migration.sql:4-11
Timestamp: 2026-01-22T12:39:42.299Z
Learning: In the intents table (libs/prisma-service/prisma/schema.prisma), ecosystemId is NOT NULL because intent records are never created without an ecosystemId in the application workflow. This differs from ecosystem_invitations where ecosystemId is nullable to support deferred ecosystem creation.

Learnt from: sujitaw
Repo: credebl/platform PR: 1545
File: libs/prisma-service/prisma/schema.prisma:793-793
Timestamp: 2026-01-19T10:03:19.860Z
Learning: In the ecosystem_invitations model, deletions involving invitedOrg are handled at the application level with conditions matching both ecosystemId and orgId, rather than relying on database-level cascade behavior.

Learnt from: sujitaw
Repo: credebl/platform PR: 1545
File: libs/prisma-service/prisma/schema.prisma:792-792
Timestamp: 2026-01-19T09:58:13.669Z
Learning: In the ecosystem_invitations model (libs/prisma-service/prisma/schema.prisma), ecosystemId is intentionally nullable to support a workflow where users create their ecosystem first and the ecosystemId is added later. Application-level validation prevents duplicate records with the same email when both ecosystemId and orgId are null, addressing the PostgreSQL NULL uniqueness behavior.

Comment on lines +814 to 823
async getIntentTemplateByIntentAndOrg(
intentName: string,
verifierOrgId: string,
ecosystemId?: string
): Promise<intent_templates | null> {
try {
const template = await this.prisma.intent_templates.findFirst({
where: {
intent: { is: { name: intentName } },
intent: { is: { name: intentName, ...(ecosystemId && { ecosystemId }) } },
OR: [{ orgId: verifierOrgId }, { orgId: null }]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Require ecosystem scoping for intent-template lookup.

At Line 822, ecosystemId is optional in the intent filter. If the same intent name exists in multiple ecosystems, this can return the wrong template.

🔧 Proposed fix
-  async getIntentTemplateByIntentAndOrg(
+  async getIntentTemplateByIntentAndOrg(
     intentName: string,
     verifierOrgId: string,
-    ecosystemId?: string
+    ecosystemId: string
   ): Promise<intent_templates | null> {
@@
-          intent: { is: { name: intentName, ...(ecosystemId && { ecosystemId }) } },
+          intent: { is: { name: intentName, ecosystemId } },

Based on learnings: In the intents table (libs/prisma-service/prisma/schema.prisma), ecosystemId is NOT NULL because intent records are never created without an ecosystemId in the application workflow.

📝 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 getIntentTemplateByIntentAndOrg(
intentName: string,
verifierOrgId: string,
ecosystemId?: string
): Promise<intent_templates | null> {
try {
const template = await this.prisma.intent_templates.findFirst({
where: {
intent: { is: { name: intentName } },
intent: { is: { name: intentName, ...(ecosystemId && { ecosystemId }) } },
OR: [{ orgId: verifierOrgId }, { orgId: null }]
async getIntentTemplateByIntentAndOrg(
intentName: string,
verifierOrgId: string,
ecosystemId: string
): Promise<intent_templates | null> {
try {
const template = await this.prisma.intent_templates.findFirst({
where: {
intent: { is: { name: intentName, ecosystemId } },
OR: [{ orgId: verifierOrgId }, { orgId: null }]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ecosystem/repositories/ecosystem.repository.ts` around lines 814 - 823,
The intent-template lookup in getIntentTemplateByIntentAndOrg currently treats
ecosystemId as optional which can return wrong templates across ecosystems;
update the prisma.intent_templates.findFirst call to always include ecosystemId
in the intent filter (remove the conditional spread ...(ecosystemId && {
ecosystemId }) and use intent: { is: { name: intentName, ecosystemId } }), and
add a guard at the start of getIntentTemplateByIntentAndOrg to throw or validate
when ecosystemId is missing since intents are always created with a non-null
ecosystemId.

Comment on lines +1701 to +1711
async createIntentNotice(intentId: string, noticeUrl: string, userId: string, orgId?: string): Promise<object> {
try {
return await this.prisma.intent_notices.create({
data: {
intent: { connect: { id: intentId } },
...(orgId && { organisation: { connect: { id: orgId } } }),
noticeUrl,
createdBy: userId,
lastChangedBy: userId
}
});
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Mar 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Intent notice creation is race-prone without DB uniqueness.

intentNoticeExists (Line 1802) + createIntentNotice (Line 1703) is non-atomic. Concurrent requests can create duplicates for the same (intentId, orgId).

🔧 Proposed hardening
 async createIntentNotice(intentId: string, noticeUrl: string, userId: string, orgId?: string): Promise<object> {
   try {
     return await this.prisma.intent_notices.create({
@@
     });
   } catch (error) {
+    if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
+      throw new BadRequestException('Intent notice already exists for this intent and organization');
+    }
     this.logger.error(`createIntentNotice error: ${error}`);
     throw error;
   }
 }

Also add a DB-level unique constraint on (intentId, orgId) in Prisma schema + migration to make this truly safe.

Also applies to: 1800-1805

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

In `@apps/ecosystem/repositories/ecosystem.repository.ts` around lines 1701 -
1711, The createIntentNotice(intentId, noticeUrl, userId, orgId) is race-prone
when combined with intentNoticeExists; change createIntentNotice to perform an
atomic operation (e.g., use prisma.intent_notices.upsert or a create with unique
constraint handling) so duplicates cannot be inserted under concurrent requests,
and add a DB-level unique constraint on (intentId, orgId) in the Prisma schema
followed by a migration; also update any callers or intentNoticeExists logic to
rely on the DB uniqueness rather than a separate existence check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

const isAlreadyExists = await this.ecosystemRepository.intentNoticeExists(intentId, orgId ?? null); validation exist in service file

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@tipusinghaw, thanks for pointing that out — the service-level guard does cover the sequential case correctly. However, the concern is specifically about concurrent requests:

  1. Request A calls intentNoticeExists(intentId, orgId) → returns false (no record yet).
  2. Request B calls intentNoticeExists(intentId, orgId) → also returns false (Request A hasn't committed yet).
  3. Request A calls createIntentNotice(...) → succeeds.
  4. Request B calls createIntentNotice(...) → also succeeds → duplicate row.

This is the classic TOCTOU (Time-of-Check / Time-of-Use) race. The service-layer check can't prevent it because the check and the insert are two separate, non-atomic DB round-trips.

The recommended hardening is to add a DB-level unique constraint on (intentId, orgId) so that the database itself rejects duplicates, and then catch the P2002 error in createIntentNotice:

async createIntentNotice(intentId: string, noticeUrl: string, userId: string, orgId?: string): Promise<object> {
  try {
    return await this.prisma.intent_notices.create({
      data: {
        intent: { connect: { id: intentId } },
        ...(orgId && { organisation: { connect: { id: orgId } } }),
        noticeUrl,
        createdBy: userId,
        lastChangedBy: userId
      }
    });
  } catch (error) {
    if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
      throw new RpcException({
        status: HttpStatus.CONFLICT,
        message: 'Intent notice already exists for this intent and organization'
      });
    }
    this.logger.error(`createIntentNotice error: ${error}`);
    throw error;
  }
}

And in the Prisma schema/migration:

ALTER TABLE intent_notices ADD CONSTRAINT intent_notices_intentId_orgId_key UNIQUE ("intentId", "orgId");

This makes the uniqueness guarantee atomic and reliable under any level of concurrency. Would you like to address this, or should I open a follow-up issue?


🧠 Learnings used
Learnt from: tipusinghaw
Repo: credebl/platform PR: 1584
File: apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts:10-13
Timestamp: 2026-03-18T12:02:15.511Z
Learning: In the credebl/platform repository, `noticeUrl` URL validation for intent notices is intentionally handled at the service layer via the `validateNoticeUrl` helper in `apps/ecosystem/src/ecosystem.helper.ts`, which performs a live HTTP reachability check, rather than purely at the DTO layer with `IsUrl()`. Do not flag the absence of `IsUrl()` on `noticeUrl` in `CreateIntentNoticeDto` as a missing validation.

Learnt from: pranalidhanavade
Repo: credebl/platform PR: 1547
File: libs/prisma-service/prisma/migrations/20260119130717_intent_table_creation/migration.sql:4-11
Timestamp: 2026-01-22T12:39:42.299Z
Learning: In the intents table (libs/prisma-service/prisma/schema.prisma), ecosystemId is NOT NULL because intent records are never created without an ecosystemId in the application workflow. This differs from ecosystem_invitations where ecosystemId is nullable to support deferred ecosystem creation.

Learnt from: sujitaw
Repo: credebl/platform PR: 1545
File: libs/prisma-service/prisma/schema.prisma:792-792
Timestamp: 2026-01-19T09:58:13.669Z
Learning: In the ecosystem_invitations model (libs/prisma-service/prisma/schema.prisma), ecosystemId is intentionally nullable to support a workflow where users create their ecosystem first and the ecosystemId is added later. Application-level validation prevents duplicate records with the same email when both ecosystemId and orgId are null, addressing the PostgreSQL NULL uniqueness behavior.

Comment thread apps/ecosystem/src/ecosystem.helper.ts Outdated
Comment thread apps/ecosystem/src/ecosystem.helper.ts
Comment on lines +1044 to +1051
if (orgId) {
const orgEcosystemMembership = await this.ecosystemRepository.getEcosystemOrg(intent['ecosystemId'], orgId);
if (!orgEcosystemMembership) {
throw new RpcException({
statusCode: HttpStatus.FORBIDDEN,
message: 'The provided orgId is not a member or lead of this ecosystem.'
});
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository helper definitions:"
rg -n -A10 -B3 'async\s+(findEcosystemOrg|getEcosystemOrg)\s*\(' apps/ecosystem/repositories/ecosystem.repository.ts

echo
echo "Service call sites:"
rg -n -A2 -B2 '\b(findEcosystemOrg|getEcosystemOrg)\s*\(' apps/ecosystem/src/ecosystem.service.ts

Repository: credebl/platform

Length of output: 1629


Use findEcosystemOrg with status validation to match the existing org membership check pattern.

Line 1045 uses getEcosystemOrg without checking status !== EcosystemOrgStatus.ACTIVE, but the identical org membership validation for templates (line 630) uses findEcosystemOrg with the active status check. This inconsistency means notice creation accepts inactive org members while template assignment rejects them.

Suggested fix
-        const orgEcosystemMembership = await this.ecosystemRepository.getEcosystemOrg(intent['ecosystemId'], orgId);
-        if (!orgEcosystemMembership) {
+        const orgEcosystemMembership = await this.ecosystemRepository.findEcosystemOrg(intent['ecosystemId'], orgId);
+        if (!orgEcosystemMembership || orgEcosystemMembership.status !== EcosystemOrgStatus.ACTIVE) {
           throw new RpcException({
             statusCode: HttpStatus.FORBIDDEN,
             message: 'The provided orgId is not a member or lead of this ecosystem.'
           });
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ecosystem/src/ecosystem.service.ts` around lines 1044 - 1051, The org
membership check currently calls getEcosystemOrg and does not validate the
membership status; change it to use findEcosystemOrg with the same active-status
validation used elsewhere (replace getEcosystemOrg(intent['ecosystemId'], orgId)
with findEcosystemOrg(intent['ecosystemId'], orgId) and verify the returned
object's status is EcosystemOrgStatus.ACTIVE, throwing the same RpcException
(HttpStatus.FORBIDDEN / 'The provided orgId is not a member or lead of this
ecosystem.') if the record is missing or not ACTIVE so behavior matches the
template assignment path.

Comment on lines +1054 to +1062
const isAlreadyExists = await this.ecosystemRepository.intentNoticeExists(intentId, orgId ?? null);
if (isAlreadyExists) {
const slotLabel = orgId ? `orgId ${orgId}` : 'no orgId';
throw new RpcException({
statusCode: HttpStatus.CONFLICT,
message: `An intent notice with ${slotLabel} already exists for this intent.`
});
}
return await this.ecosystemRepository.createIntentNotice(intentId, noticeUrl, userId, orgId);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Intent notice schema / migrations:"
rg -n -C3 'intent_notice|intent_notices|@@unique|UNIQUE|intentId|orgId' libs/prisma-service/prisma

echo
echo "Conflict handling for createIntentNotice:"
rg -n -C3 'createIntentNotice|intentNoticeExists|P2002|ConflictException|HttpStatus\.CONFLICT' \
  apps/ecosystem/src/ecosystem.service.ts \
  apps/ecosystem/repositories/ecosystem.repository.ts

Repository: credebl/platform

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for createIntentNotice method in repository
echo "=== Repository method createIntentNotice ==="
rg -n -A10 'createIntentNotice' apps/ecosystem/repositories/ecosystem.repository.ts

echo
echo "=== Error/exception handling for P2002 (unique constraint) ==="
rg -n 'P2002|ConflictException|UniqueConstraint|UNIQUE.*VIOLATION' apps/ecosystem/repositories/ecosystem.repository.ts

echo
echo "=== RPC exception handling in ecosystem service ==="
rg -n -B2 -A5 'catch.*Error\|Prisma.*Error' apps/ecosystem/src/ecosystem.service.ts | head -40

Repository: credebl/platform

Length of output: 1104


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check context around line 86 P2002 handling
echo "=== P2002 handling context around line 86 ==="
sed -n '75,100p' apps/ecosystem/repositories/ecosystem.repository.ts

echo
echo "=== Verify intent_notices table has NO unique constraint ==="
rg -A20 'model intent_notices' libs/prisma-service/prisma/schema.prisma

echo
echo "=== Compare to intent_templates which has proper partitioned unique indexes ==="
rg -A25 'model intent_templates' libs/prisma-service/prisma/schema.prisma

Repository: credebl/platform

Length of output: 3172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check all migrations for intent_notices to confirm no unique constraint
echo "=== All migrations mentioning intent_notices ==="
rg -l 'intent_notices' libs/prisma-service/prisma/migrations/

echo
echo "=== Full content of intent_notices migration files ==="
for file in $(rg -l 'intent_notices' libs/prisma-service/prisma/migrations/); do
  echo "=== File: $file ==="
  cat "$file"
  echo
done

Repository: credebl/platform

Length of output: 1594


Add partitioned unique constraints to prevent duplicate intent notices.

The code uses a read-then-create pattern (intentNoticeExists followed by createIntentNotice) without database-level uniqueness enforcement. Two concurrent requests can both pass the check and insert duplicates, since there is no unique constraint on (intentId, orgId).

The intent_notices table has only simple indexes on intentId and orgId, but lacks any composite uniqueness constraint. Meanwhile, the codebase already demonstrates the correct pattern in the intent_templates migration (20251223061418), which uses partitioned unique indexes to properly handle nullable orgId:

CREATE UNIQUE INDEX intent_templates_unique_org
ON intent_templates ("orgId", "intentId", "templateId")
WHERE "orgId" IS NOT NULL;

CREATE UNIQUE INDEX intent_templates_unique_no_org
ON intent_templates ("intentId", "templateId")
WHERE "orgId" IS NULL;

Apply the same approach to intent_notices with a migration, and add P2002 error handling in the repository's createIntentNotice method to map database constraint violations to HTTP 409 Conflict (similar to the pattern in createEcosystemInvitation).

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

In `@apps/ecosystem/src/ecosystem.service.ts` around lines 1054 - 1062, Add
partitioned unique constraints for intent_notices and handle P2002 in the
repository: create a new migration that adds two unique indexes like
intent_templates (one WHERE "orgId" IS NOT NULL on ("orgId","intentId"), and one
WHERE "orgId" IS NULL on ("intentId")) to prevent duplicates, then update
ecosystemRepository.createIntentNotice to catch Prisma P2002/unique constraint
errors and rethrow an RpcException with HttpStatus.CONFLICT (mirror the
createEcosystemInvitation error-mapping pattern); keep the existing
intentNoticeExists check if desired but rely on DB constraint + repo-level P2002
handling to guarantee uniqueness.

Comment on lines +6 to +8
const consentNoticeUrl = `${noticeUrl}?transactionId=${transactionId}`;

const response = await fetch(consentNoticeUrl);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current helper implementation and runtime baseline for native fetch/AbortSignal usage.
rg -n --type=ts -C2 'fetchConsentNotice|fetch\(|AbortSignal\.timeout|transactionId' apps/oid4vc-verification/src/oid4vc-verification.helper.ts
rg -n --type=json -C2 '"engines"|"\bnode\b"' package.json

Repository: credebl/platform

Length of output: 857


🏁 Script executed:

#!/bin/bash
# Verify the context and usage of fetchConsentNotice
rg -n 'fetchConsentNotice' apps/oid4vc-verification/src/ --type ts -B2 -A2

# Check if there's try-catch or error handling around the fetch
rg -n 'fetchConsentNotice\|try\|catch' apps/oid4vc-verification/src/oid4vc-verification.service.ts --type ts -B3 -A3

# Verify if noticeUrl is validated before being passed to this helper
rg -n 'noticeUrl' apps/oid4vc-verification/src/oid4vc-verification.service.ts --type ts -B2 -A2

# Check for any existing fetch timeout patterns elsewhere in the codebase
rg -n 'AbortSignal|signal.*timeout|fetch.*signal' apps/ --type ts | head -20

Repository: credebl/platform

Length of output: 1909


🏁 Script executed:

#!/bin/bash
# Check the full context of the fetchConsentNotice call and error handling
sed -n '410,430p' apps/oid4vc-verification/src/oid4vc-verification.service.ts

# Check if there's try-catch wrapping the entire session creation
sed -n '400,440p' apps/oid4vc-verification/src/oid4vc-verification.service.ts | cat -n

# Verify the complete fetchConsentNotice implementation including error handling
cat apps/oid4vc-verification/src/oid4vc-verification.helper.ts

Repository: credebl/platform

Length of output: 3358


Build the consent URL safely and guard the outbound call with a timeout.

Line 6 manually concatenates ?transactionId=...; this produces malformed URLs when noticeUrl already contains query parameters (e.g., http://example.com/notice?foo=bar becomes http://example.com/notice?foo=bar?transactionId=xyz). Also, Line 8 has no timeout, so a slow or unresponsive endpoint can block this request indefinitely.

🔧 Proposed fix
-  const consentNoticeUrl = `${noticeUrl}?transactionId=${transactionId}`;
-
-  const response = await fetch(consentNoticeUrl);
+  const consentNoticeEndpoint = new URL(noticeUrl);
+  consentNoticeEndpoint.searchParams.set('transactionId', transactionId);
+
+  const response = await fetch(consentNoticeEndpoint.toString(), {
+    signal: AbortSignal.timeout(5000)
+  });
📝 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
const consentNoticeUrl = `${noticeUrl}?transactionId=${transactionId}`;
const response = await fetch(consentNoticeUrl);
const consentNoticeEndpoint = new URL(noticeUrl);
consentNoticeEndpoint.searchParams.set('transactionId', transactionId);
const response = await fetch(consentNoticeEndpoint.toString(), {
signal: AbortSignal.timeout(5000)
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/oid4vc-verification/src/oid4vc-verification.helper.ts` around lines 6 -
8, Build the consent URL using a proper URL/URLSearchParams API instead of
string concatenation: create a new URL(noticeUrl) (or new URL(base) then set
pathname) and append transactionId with url.searchParams.set('transactionId',
transactionId) to avoid double '?' when noticeUrl already has query params; then
perform the outbound call using fetch guarded by an AbortController with a
timeout (create AbortController, pass signal to fetch, and clear the timeout on
completion) so the request cannot hang indefinitely; update the references to
consentNoticeUrl and the fetch call in the helper where they are used.

Comment thread apps/oid4vc-verification/src/oid4vc-verification.service.ts
Comment on lines +743 to +757
model intent_notices {
id String @id @default(uuid()) @db.Uuid
intentId String @db.Uuid
orgId String? @db.Uuid
noticeUrl String
createDateTime DateTime @default(now()) @db.Timestamptz(6)
createdBy String @db.Uuid
lastChangedDateTime DateTime @default(now()) @db.Timestamptz(6)
lastChangedBy String
intent intents @relation(fields: [intentId], references: [id])
organisation organisation? @relation(fields: [orgId], references: [id])

@@index([intentId])
@@index([orgId])
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether runtime retrieval assumes single-row semantics.
rg -n --type=ts -C3 'get-intent-notice-by-intent-id|intentNotice|noticeUrl' apps/oid4vc-verification apps/ecosystem apps/api-gateway
rg -n --type=sql -C3 'intent_notices|UNIQUE|unique' libs/prisma-service/prisma/migrations

Repository: credebl/platform

Length of output: 50372


🏁 Script executed:

# Check the intent_templates pattern for handling nullable columns with uniqueness
rg -n "intent_templates_unique" libs/prisma-service/prisma/migrations --type=sql -A5 -B5

# Examine schema pattern for intent_notices
rg -n "intent_notices" libs/prisma-service/prisma/schema.prisma -B5 -A15

# Check if there are any database constraints already in the migration SQL
rg -n "intent_notices.*UNIQUE\|CONSTRAINT.*intent_notices" libs/prisma-service/prisma/migrations --type=sql

Repository: credebl/platform

Length of output: 4027


Refine uniqueness constraint for nullable orgId column.

The model allows duplicate notices per (intentId, orgId) pair, but the verification flow expects deterministic single-row lookup. Application-level validation prevents duplicates at insertion, but database-level enforcement should account for PostgreSQL's NULL behavior: a simple UNIQUE(intentId, orgId) allows multiple NULL orgId values.

Follow the pattern used in intent_templates (migration 20251223061418) with partial unique indexes:

♻️ Suggested SQL migration
CREATE UNIQUE INDEX "intent_notices_unique_with_org" 
ON "intent_notices"("intentId", "orgId") 
WHERE "orgId" IS NOT NULL;

CREATE UNIQUE INDEX "intent_notices_unique_no_org" 
ON "intent_notices"("intentId") 
WHERE "orgId" IS NULL;

Alternatively, if application-level validation via intentNoticeExists() suffices, document this intentional choice and remove the indexes.

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

In `@libs/prisma-service/prisma/schema.prisma` around lines 743 - 757, The
intent_notices model allows duplicates because UNIQUE(intentId, orgId) won't
prevent multiple NULL orgId rows; add two partial unique indexes to enforce
DB-level uniqueness by creating a unique index on (intentId, orgId) WHERE orgId
IS NOT NULL and a unique index on intentId WHERE orgId IS NULL (use descriptive
index names like intent_notices_unique_with_org and
intent_notices_unique_no_org), or if you prefer to rely on the existing
application-level check (intentNoticeExists), explicitly document that choice
and remove the current indexes to avoid misleading constraints.

Signed-off-by: Tipu_Singh <tipu.singh@ayanworks.com>
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
apps/ecosystem/src/ecosystem.helper.ts (2)

11-27: ⚠️ Potential issue | 🔴 Critical

Add request timeout/abort wiring for fetch.

Line 12 uses fetch(noticeUrl) without AbortController.signal, so long-hanging requests are not bounded. Add a timeout + finally cleanup.

⏱️ Suggested fix
 export async function validateNoticeUrl(noticeUrl: string): Promise<void> {
   if (!noticeUrl || !noticeUrl.trim()) {
     throw new RpcException({
       statusCode: HttpStatus.BAD_REQUEST,
       message: 'noticeUrl must not be empty.'
     });
   }
+  const controller = new AbortController();
+  const timeout = setTimeout(() => controller.abort(), 5000);
   try {
-    const response = await fetch(noticeUrl);
+    const response = await fetch(noticeUrl, { signal: controller.signal });
     if (!response.ok) {
       throw new RpcException({
         statusCode: HttpStatus.BAD_REQUEST,
         message: `noticeUrl is not reachable (HTTP ${response.status}).`
       });
     }
   } catch (err) {
     if (err instanceof RpcException) {
       throw err;
     }
     throw new RpcException({
       statusCode: HttpStatus.BAD_REQUEST,
       message: `noticeUrl could not be resolved: ${err?.message ?? 'unreachable'}`
     });
+  } finally {
+    clearTimeout(timeout);
   }
 }
#!/bin/bash
# Verify fetch timeout/abort wiring in validateNoticeUrl.
rg -nP 'AbortController|setTimeout\\(|clearTimeout\\(|fetch\\(noticeUrl\\s*,\\s*\\{\\s*signal\\s*:' apps/ecosystem/src/ecosystem.helper.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ecosystem/src/ecosystem.helper.ts` around lines 11 - 27, The
fetch(noticeUrl) call lacks AbortController wiring and a timeout, so requests
can hang; update the validateNoticeUrl logic to create an AbortController, start
a timer (e.g. configurable timeout) that calls controller.abort(), pass {
signal: controller.signal } into fetch(noticeUrl, { signal }), and clear the
timer in a finally block to avoid leaks; preserve existing error handling that
throws RpcException with HttpStatus.BAD_REQUEST but include a clear message for
aborted/timeouts (inspect err.name or err.message) and ensure the timer is
cleared regardless of outcome.

12-12: ⚠️ Potential issue | 🔴 Critical

Block SSRF targets before outbound fetch.

Line 12 fetches a user-controlled URL directly. This can be used to probe internal/private hosts unless protocol/host/IP policy is enforced first.

🔒 Suggested hardening
 export async function validateNoticeUrl(noticeUrl: string): Promise<void> {
   if (!noticeUrl || !noticeUrl.trim()) {
     throw new RpcException({
       statusCode: HttpStatus.BAD_REQUEST,
       message: 'noticeUrl must not be empty.'
     });
   }
+  const parsedUrl = new URL(noticeUrl);
+  if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
+    throw new RpcException({
+      statusCode: HttpStatus.BAD_REQUEST,
+      message: 'noticeUrl must use http/https.'
+    });
+  }
+  // Enforce allowlist/private-network blocking policy before fetch.
+  // At minimum block localhost, loopback, link-local, RFC1918, and metadata hosts.
   try {
     const response = await fetch(noticeUrl);
#!/bin/bash
# Verify whether URL policy checks exist before fetch in validateNoticeUrl.
rg -nP 'validateNoticeUrl|new URL\\(|protocol|hostname|localhost|127\\.0\\.0\\.1|::1|169\\.254\\.169\\.254|10\\.|192\\.168|172\\.(1[6-9]|2[0-9]|3[0-1])|fetch\\(noticeUrl' apps/ecosystem/src/ecosystem.helper.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ecosystem/src/ecosystem.helper.ts` at line 12, The fetch call uses a
user-controlled noticeUrl and must be protected against SSRF: in the
validateNoticeUrl (or wherever noticeUrl is accepted) enforce allowed protocols
(only http/https), parse the URL with new URL(noticeUrl), resolve the hostname
to an IP and block private/internal ranges (localhost, 127.0.0.0/8, ::1,
169.254.169.254, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, link-local, etc.),
and reject hostnames that resolve to those addresses or to non-routable names
before calling fetch(noticeUrl); additionally consider allowing an explicit
allowlist of trusted hosts and failing fast with a clear error when validation
fails.
apps/oid4vc-verification/src/oid4vc-verification.service.ts (1)

420-429: ⚠️ Potential issue | 🟠 Major

Guard the session ID before consent enrichment.

Line 423 still dereferences createdSession.verificationSession.id before the promise .catch(...) can run. If that nested field is missing, this method will still throw after the verification session has already been created, so a client retry can create duplicates.

🛡️ Suggested hardening
-          if (intentNotice?.noticeUrl) {
-            createdSession.consentNoticeUrl = await fetchConsentNotice(
-              intentNotice.noticeUrl,
-              createdSession.verificationSession.id
-            ).catch((err) => {
+          const transactionId = createdSession?.verificationSession?.id;
+          if (intentNotice?.noticeUrl && transactionId) {
+            createdSession.consentNoticeUrl = await fetchConsentNotice(
+              intentNotice.noticeUrl,
+              transactionId
+            ).catch((err) => {
               this.logger.warn(
                 `[createIntentBasedVerificationPresentation] consent notice enrichment failed: ${err?.message}`
               );
               return null;
             });
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/oid4vc-verification/src/oid4vc-verification.service.ts` around lines 420
- 429, The code dereferences createdSession.verificationSession.id inline when
calling fetchConsentNotice which can throw before the .catch runs; to fix, first
read and validate the session id into a local variable (e.g., const sessionId =
createdSession?.verificationSession?.id) and only call fetchConsentNotice if
sessionId is defined, otherwise skip enrichment (set consentNoticeUrl to null).
Update the call site that uses fetchConsentNotice (inside
createIntentBasedVerificationPresentation) to use the guarded sessionId and keep
the existing .catch to log warnings so enrichment failures remain non-fatal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/ecosystem/src/ecosystem.helper.ts`:
- Around line 11-27: The fetch(noticeUrl) call lacks AbortController wiring and
a timeout, so requests can hang; update the validateNoticeUrl logic to create an
AbortController, start a timer (e.g. configurable timeout) that calls
controller.abort(), pass { signal: controller.signal } into fetch(noticeUrl, {
signal }), and clear the timer in a finally block to avoid leaks; preserve
existing error handling that throws RpcException with HttpStatus.BAD_REQUEST but
include a clear message for aborted/timeouts (inspect err.name or err.message)
and ensure the timer is cleared regardless of outcome.
- Line 12: The fetch call uses a user-controlled noticeUrl and must be protected
against SSRF: in the validateNoticeUrl (or wherever noticeUrl is accepted)
enforce allowed protocols (only http/https), parse the URL with new
URL(noticeUrl), resolve the hostname to an IP and block private/internal ranges
(localhost, 127.0.0.0/8, ::1, 169.254.169.254, 10.0.0.0/8, 172.16.0.0/12,
192.168.0.0/16, link-local, etc.), and reject hostnames that resolve to those
addresses or to non-routable names before calling fetch(noticeUrl); additionally
consider allowing an explicit allowlist of trusted hosts and failing fast with a
clear error when validation fails.

In `@apps/oid4vc-verification/src/oid4vc-verification.service.ts`:
- Around line 420-429: The code dereferences
createdSession.verificationSession.id inline when calling fetchConsentNotice
which can throw before the .catch runs; to fix, first read and validate the
session id into a local variable (e.g., const sessionId =
createdSession?.verificationSession?.id) and only call fetchConsentNotice if
sessionId is defined, otherwise skip enrichment (set consentNoticeUrl to null).
Update the call site that uses fetchConsentNotice (inside
createIntentBasedVerificationPresentation) to use the guarded sessionId and keep
the existing .catch to log warnings so enrichment failures remain non-fatal.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73512e1f-6123-40d9-8782-d1d7c9272ea0

📥 Commits

Reviewing files that changed from the base of the PR and between 8a2fd10 and 5903dec.

📒 Files selected for processing (2)
  • apps/ecosystem/src/ecosystem.helper.ts
  • apps/oid4vc-verification/src/oid4vc-verification.service.ts

@tipusinghaw tipusinghaw merged commit 7be8107 into main Mar 18, 2026
8 checks passed
@tipusinghaw tipusinghaw deleted the feat/consent-notice branch March 18, 2026 13:17
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.

2 participants