Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/api-gateway/src/ecosystem/ecosystem.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { CreateIntentTemplateDto, UpdateIntentTemplateDto } from '../utilities/d
import { GetAllIntentTemplatesDto } from '../utilities/dtos/get-all-intent-templates.dto';
import { IIntentTemplateList } from '@credebl/common/interfaces/intents-template.interface';
import { IPaginationSortingDto, PaginatedResponse } from 'libs/common/src/interfaces/interface';
import { CreateIntentNoticeDto, UpdateIntentNoticeDto } from '../oid4vc-verification/dtos/create-intent-notice.dto';

@Injectable()
export class EcosystemService {
Expand Down Expand Up @@ -251,4 +252,42 @@ export class EcosystemService {
async getCreateEcosystemInvitationStatus(email: string, status: Invitation): Promise<boolean> {
return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-ecosystem-created-status', { email, status });
}

async createIntentNotice(createIntentNoticeDto: CreateIntentNoticeDto, userDetails: user): Promise<object> {
const payload = { createIntentNoticeDto, userDetails };
return this.natsClient.sendNatsMessage(this.serviceProxy, 'create-intent-notice', payload);
Comment thread
tipusinghdev marked this conversation as resolved.
}

async getIntentNotices(id?: string, intentId?: string): Promise<object[]> {
return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-intent-notices', { id, intentId });
}

async getIntentNoticesByEcosystemId(
ecosystemId: string,
pageNumber: number,
pageSize: number,
search: string,
intentId?: string
): Promise<object> {
return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-intent-notices-by-ecosystem', {
ecosystemId,
pageNumber,
pageSize,
search,
intentId
});
}

async updateIntentNotice(
id: string,
updateIntentNoticeDto: UpdateIntentNoticeDto,
userDetails: user
): Promise<object> {
const payload = { id, updateIntentNoticeDto, userDetails };
return this.natsClient.sendNatsMessage(this.serviceProxy, 'update-intent-notice', payload);
}

async deleteIntentNotice(id: string): Promise<object> {
return this.natsClient.sendNatsMessage(this.serviceProxy, 'delete-intent-notice', { id });
}
}
179 changes: 179 additions & 0 deletions apps/api-gateway/src/ecosystem/intent/intent.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { PaginationDto } from '@credebl/common/dtos/pagination.dto';
import { TrimStringParamPipe } from '@credebl/common/cast.helper';
import { EcosystemService } from '../ecosystem.service';
import { ForbiddenErrorDto } from '../../dtos/forbidden-error.dto';
import { CreateIntentNoticeDto, UpdateIntentNoticeDto } from '../../oid4vc-verification/dtos/create-intent-notice.dto';

@UseFilters(CustomExceptionFilter)
@Controller('intent')
Expand Down Expand Up @@ -611,4 +612,182 @@ export class IntentController {
};
return res.status(HttpStatus.OK).json(finalResponse);
}

@Post('/notice')
@ApiBearerAuth()
@Roles(OrgRoles.ECOSYSTEM_LEAD)
@UseGuards(AuthGuard('jwt'), EcosystemRolesGuard)
@ApiOperation({ summary: 'Create intent notice', description: 'Stores a notice URL associated with an intent.' })
@ApiResponse({ status: HttpStatus.CREATED, description: 'Intent notice created successfully', type: ApiResponseDto })
async createIntentNotice(
@Body() createIntentNoticeDto: CreateIntentNoticeDto,
@User() user: PrismaUser,
@Res() res: Response
): Promise<Response> {
const result = await this.ecosystemService.createIntentNotice(createIntentNoticeDto, user);
const finalResponse: IResponse = {
statusCode: HttpStatus.CREATED,
message: ResponseMessages.intentNotice.success.create,
data: result
};
return res.status(HttpStatus.CREATED).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> {
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);
}
Comment thread
tipusinghdev marked this conversation as resolved.

@Get('/ecosystem/:ecosystemId/notice')
@ApiBearerAuth()
@Roles(OrgRoles.ECOSYSTEM_LEAD, OrgRoles.ECOSYSTEM_MEMBER)
@UseGuards(AuthGuard('jwt'), EcosystemRolesGuard)
@ApiOperation({
summary: 'Get intent notices by ecosystem',
description: 'Retrieves all intent notices for an ecosystem with pagination, search, and optional intent filter.'
})
@ApiQuery({ name: 'pageNumber', required: false, type: Number, description: 'Page number (default: 1)' })
@ApiQuery({ name: 'pageSize', required: false, type: Number, description: 'Page size (default: 10, max: 100)' })
@ApiQuery({ name: 'search', required: false, type: String, description: 'Search by notice URL' })
@ApiQuery({ name: 'intentId', required: false, type: String, description: 'Filter by intent UUID' })
@ApiResponse({ status: HttpStatus.OK, description: 'Intent notices fetched successfully', type: ApiResponseDto })
async getIntentNoticesByEcosystemId(
@Param(
'ecosystemId',
TrimStringParamPipe,
new ParseUUIDPipe({
exceptionFactory: (): Error => {
throw new BadRequestException(ResponseMessages.ecosystem.error.invalidFormatOfEcosystemId);
}
})
)
ecosystemId: string,
@Query() pageDto: PaginationDto,
@Query(
'intentId',
new ParseUUIDPipe({
version: '4',
optional: true,
exceptionFactory: (): Error => {
throw new BadRequestException('Invalid intent ID');
}
})
)
intentId: string,
@Res() res: Response
): Promise<Response> {
const result = await this.ecosystemService.getIntentNoticesByEcosystemId(
ecosystemId,
pageDto.pageNumber,
pageDto.pageSize,
pageDto.search,
intentId
);
return res.status(HttpStatus.OK).json({
statusCode: HttpStatus.OK,
message: ResponseMessages.intentNotice.success.fetchAll,
data: result
});
}

@Put('/notice/:id')
@ApiBearerAuth()
@Roles(OrgRoles.ECOSYSTEM_LEAD)
@UseGuards(AuthGuard('jwt'), EcosystemRolesGuard)
@ApiOperation({
summary: 'Update intent notice',
description: 'Updates the notice URL for a given notice ID.'
})
@ApiResponse({ status: HttpStatus.OK, description: 'Intent notice updated successfully', type: ApiResponseDto })
async updateIntentNotice(
@Param(
'id',
TrimStringParamPipe,
new ParseUUIDPipe({
exceptionFactory: (): Error => {
throw new BadRequestException('Invalid notice ID');
}
})
)
id: string,
@Body() updateIntentNoticeDto: UpdateIntentNoticeDto,
@User() user: PrismaUser,
@Res() res: Response
): Promise<Response> {
const result = await this.ecosystemService.updateIntentNotice(id, updateIntentNoticeDto, user);
const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
message: ResponseMessages.intentNotice.success.update,
data: result
};
return res.status(HttpStatus.OK).json(finalResponse);
}

@Delete('/notice/:id')
@ApiBearerAuth()
@Roles(OrgRoles.ECOSYSTEM_LEAD)
@UseGuards(AuthGuard('jwt'), EcosystemRolesGuard)
@ApiOperation({ summary: 'Delete intent notice', description: 'Deletes an intent notice by its ID.' })
@ApiResponse({ status: HttpStatus.OK, description: 'Intent notice deleted successfully', type: ApiResponseDto })
async deleteIntentNotice(
@Param(
'id',
TrimStringParamPipe,
new ParseUUIDPipe({
exceptionFactory: (): Error => {
throw new BadRequestException('Invalid notice ID');
}
})
)
id: string,
@Res() res: Response
): Promise<Response> {
const result = await this.ecosystemService.deleteIntentNotice(id);
const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
message: ResponseMessages.intentNotice.success.delete,
data: result
};
return res.status(HttpStatus.OK).json(finalResponse);
}
Comment thread
tipusinghdev marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsDefined, IsOptional, IsString, IsUrl, IsUUID } from 'class-validator';

export class CreateIntentNoticeDto {
@ApiProperty({ description: 'Intent ID to associate the notice with', example: 'uuid-of-intent' })
@IsDefined()
@IsUUID()
intentId: string;

@ApiProperty({ description: 'URL of the notice', example: 'https://example.com/notice' })
@IsDefined()
@IsString()
noticeUrl: string;
Comment on lines +10 to +13

@coderabbitai coderabbitai Bot Mar 17, 2026 •

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 validation decorators and where CreateIntentNoticeDto is used.
rg -n -C2 'class CreateIntentNoticeDto|class UpdateIntentNoticeDto|noticeUrl|IsString|IsUrl' apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts
rg -n -C2 'CreateIntentNoticeDto|noticeUrl' apps/api-gateway/src/ecosystem apps/api-gateway/src/oid4vc-verification

Repository: credebl/platform

Length of output: 4690


Enforce URL validation consistently for noticeUrl

Line 10-13 allows any string on create, while line 22-25 requires a valid URL on update. This inconsistency permits invalid URLs to be persisted at creation time, failing validation only during updates.

🔧 Proposed fix
-import { IsDefined, IsOptional, IsString, IsUrl, IsUUID } from 'class-validator';
+import { IsDefined, IsOptional, IsUrl, IsUUID } from 'class-validator';
@@
   `@ApiProperty`({ description: 'URL of the notice', example: 'https://example.com/notice' })
   `@IsDefined`()
-  `@IsString`()
+  `@IsUrl`()
   noticeUrl: string;

Also applies to: 22-25

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

In `@apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts`
around lines 10 - 13, The create DTO's noticeUrl field currently allows any
string (noticeUrl with `@IsDefined` and `@IsString`) while the update DTO enforces a
valid URL; add URL validation to the create DTO by applying the same `@IsUrl`
decorator (in
apps/api-gateway/src/oid4vc-verification/dtos/create-intent-notice.dto.ts for
the noticeUrl property) so both CreateIntentNoticeDto and UpdateIntentNoticeDto
consistently enforce a valid URL value.

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.

URL validation is implemented 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.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the validateNoticeUrl helper implementation
rg -n -A 30 'validateNoticeUrl' apps/ecosystem/src/ecosystem.helper.ts

# Also check how it's called in the service
rg -n -B2 -A5 'validateNoticeUrl' apps/ecosystem/src/ecosystem.service.ts

Length of output: 1865


@tipusinghaw, understood — the service-level validateNoticeUrl in apps/ecosystem/src/ecosystem.helper.ts does perform more thorough validation (live HTTP reachability check), which is stronger than a simple format check via @IsUrl().

However, there is a bug in that helper worth fixing: the AbortController signal is never passed to fetch, so the 5-second timeout is completely ineffective — the fetch will hang indefinitely regardless.

- const response = await fetch(noticeUrl);
+ const response = await fetch(noticeUrl, { signal: controller.signal });

Regarding the original comment, while DTO-level @IsUrl() on CreateIntentNoticeDto would still be beneficial for early rejection (before hitting the service) and accurate Swagger documentation, I acknowledge that the service-layer validation does exist. The decision is yours.


✏️ Learnings added
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.

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


@ApiPropertyOptional({ description: 'Organization ID (optional)', example: 'uuid-of-org' })
@IsOptional()
@IsUUID()
orgId?: string;
}

export class UpdateIntentNoticeDto {
@ApiPropertyOptional({ description: 'URL of the notice', example: 'https://example.com/notice' })
@IsOptional()
@IsUrl()
noticeUrl?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ export class Oid4vcVerificationController {
description: 'Verification presentation created successfully.',
type: ApiResponseDto
})
@ApiQuery({ name: 'ecosystemId', required: true })
@ApiBearerAuth()
@Roles(OrgRoles.OWNER)
@UseGuards(AuthGuard('jwt'), OrgRolesGuard)
Expand All @@ -341,19 +342,29 @@ export class Oid4vcVerificationController {
})
)
verifierId: string,
@Query(
'ecosystemId',
new ParseUUIDPipe({
exceptionFactory: (): Error => {
throw new BadRequestException('Invalid ecosystem ID');
}
})
)
ecosystemId: string,
@User() user: user,
@Body() createIntentDto: CreateIntentBasedVerificationDto,
@Res() res: Response
): Promise<Response> {
this.logger.debug(
`[createIntentBasedVerificationPresentation] Called with orgId=${orgId}, verifierId=${verifierId}, intent=${createIntentDto?.intent}, user=${user.id}`
`[createIntentBasedVerificationPresentation] Called with orgId=${orgId}, verifierId=${verifierId}, intent=${createIntentDto?.intent}, ecosystemId=${ecosystemId}, user=${user.id}`
);

const presentation = await this.oid4vcVerificationService.createIntentBasedVerificationPresentation(
orgId,
verifierId,
createIntentDto,
user
user,
ecosystemId
Comment on lines +345 to +367

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 -u

echo "== createIntentBasedVerificationPresentation implementations =="
rg -n -C8 '\bcreateIntentBasedVerificationPresentation\s*\(' \
  apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts \
  apps/oid4vc-verification/src/oid4vc-verification.service.ts || true

echo
echo "== org/ecosystem scoping around downstream lookups =="
rg -n -C6 '\becosystemId\b|\borgId\b|findUnique|findFirst|where:' \
  apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts \
  apps/oid4vc-verification/src/oid4vc-verification.service.ts \
  apps/ecosystem/src/ecosystem.service.ts \
  apps/ecosystem/repositories/ecosystem.repository.ts || true

Repository: credebl/platform

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -u

# Read the api-gateway service implementation
echo "=== API-Gateway Oid4vcVerificationService.createIntentBasedVerificationPresentation ==="
sed -n '24,80p' apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts

echo
echo "=== Check for org/ecosystem validation in the service before intent operations ==="
rg -n -B5 -A10 'oid4vpProxy\.|ecosystemId|orgId' \
  apps/api-gateway/src/oid4vc-verification/oid4vc-verification.service.ts \
  | head -80

Repository: credebl/platform

Length of output: 5978


🏁 Script executed:

#!/bin/bash
set -u

# Read the core oid4vc-verification service around the createIntentBasedVerificationPresentation method
echo "=== Core Service Implementation (lines 309-400) ==="
wc -l apps/oid4vc-verification/src/oid4vc-verification.service.ts

sed -n '309,400p' apps/oid4vc-verification/src/oid4vc-verification.service.ts

Repository: credebl/platform

Length of output: 3898


🏁 Script executed:

#!/bin/bash
set -u

echo "=== Search for 'get-intent-template-by-intent-and-org' handler ==="
rg -n 'get-intent-template-by-intent-and-org' --type ts

echo
echo "=== Search for intent template fetching logic with orgId scoping ==="
rg -n -B5 -A15 'intentName|intent.*template.*orgId' \
  apps/ecosystem/src/ecosystem.service.ts \
  apps/oid4vc-verification/src/oid4vc-verification.service.ts \
  2>/dev/null | head -100

Repository: credebl/platform

Length of output: 5909


🏁 Script executed:

#!/bin/bash
set -u

echo "=== Repository implementation of getIntentTemplateByIntentAndOrg ==="
rg -n -A30 'async getIntentTemplateByIntentAndOrg' \
  apps/ecosystem/repositories/ecosystem.repository.ts

Repository: credebl/platform

Length of output: 1279


🏁 Script executed:

#!/bin/bash
set -u

echo "=== Ecosystem controller handler for get-intent-template-by-intent-and-org ==="
sed -n '237,255p' apps/ecosystem/src/ecosystem.controller.ts

Repository: credebl/platform

Length of output: 698


Verify ecosystemId is scoped to the caller's org membership.

The downstream template lookup (apps/ecosystem/repositories/ecosystem.repository.ts, line 820-824) does not validate that the supplied ecosystemId corresponds to an ecosystem the caller's orgId belongs to. The repository query constrains by intent.ecosystemId and template orgId, but lacks an ecosystem_orgs membership check. An organization owner can supply an arbitrary ecosystemId from a different ecosystem and retrieve templates if their org ID matches the template's org ID or if the template is shared (orgId: null).

Add a check confirming verifierOrgId exists in ecosystem_orgs for the given ecosystemId before the template lookup, or constrain the repository query to validate the org-ecosystem relationship.

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

In `@apps/api-gateway/src/oid4vc-verification/oid4vc-verification.controller.ts`
around lines 345 - 367, The controller currently passes an ecosystemId into
createIntentBasedVerificationPresentation without ensuring the caller's org is a
member of that ecosystem; before calling
oid4vcVerificationService.createIntentBasedVerificationPresentation (or inside
that service) add a membership validation that verifies the caller's
orgId/verifierOrgId exists in the ecosystem_orgs for the provided ecosystemId.
Either (a) perform an explicit check using the EcosystemRepository to query
ecosystem_orgs for a row matching ecosystemId + verifierOrgId and throw
BadRequest/Forbidden if missing, or (b) modify the template lookup in
EcosystemRepository (the template query that filters by intent.ecosystemId and
template.orgId) to also join/validate ecosystem_orgs contains the verifierOrgId
for that ecosystemId so templates from other ecosystems cannot be returned.

);

this.logger.debug(`[createIntentBasedVerificationPresentation] Presentation created successfully`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,20 @@ export class Oid4vcVerificationService {
orgId: string,
verifierId: string,
createIntentDto: CreateIntentBasedVerificationDto,
userDetails: user
userDetails: user,
ecosystemId: string
): Promise<object> {
const { intent, responseMode, requestSigner, expectedOrigins } = createIntentDto;
const payload = { orgId, verifierId, intent, responseMode, requestSigner, expectedOrigins, userDetails };
const payload = {
orgId,
verifierId,
intent,
responseMode,
requestSigner,
expectedOrigins,
userDetails,
ecosystemId
};
this.logger.debug(
`[createIntentBasedVerificationPresentation] Called with orgId=${orgId}, verifierId=${verifierId}, intent=${intent}, user=${userDetails?.id}`
);
Expand Down
Loading
Loading