Skip to content

Commit 9add405

Browse files
committed
feat: Implement voucher invitation sharing for claimants, add action result display sanitization, and configure Serena project settings.
1 parent 23e2b52 commit 9add405

File tree

6 files changed

+54
-19
lines changed

6 files changed

+54
-19
lines changed

apps/api/src/modules/action/action.controller.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ export class ActionController {
1717
@LoginedUser() user: UserModel,
1818
@Query('resultId') resultId: string,
1919
): Promise<GetActionResultResponse> {
20-
const result = await this.actionService.getActionResult(user, { resultId });
21-
return buildSuccessResponse(actionResultPO2DTO(result));
20+
const result = await this.actionService.getActionResult(user, {
21+
resultId,
22+
sanitizeForDisplay: true,
23+
});
24+
return buildSuccessResponse(actionResultPO2DTO(result, { sanitizeForDisplay: true }));
2225
}
2326

2427
@UseGuards(JwtAuthGuard)

apps/api/src/modules/action/action.dto.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,16 @@ export type ActionDetail = ActionResultModel & {
3232
modelInfo?: ModelInfo;
3333
};
3434

35-
export function actionStepPO2DTO(step: ActionStepDetail): ActionStep {
35+
export type SanitizeOptions = { sanitizeForDisplay?: boolean };
36+
37+
export function actionStepPO2DTO(step: ActionStepDetail, options?: SanitizeOptions): ActionStep {
3638
return {
3739
...pick(step, ['name', 'content', 'reasoningContent']),
3840
logs: safeParseJSON(step.logs || '[]'),
3941
artifacts: safeParseJSON(step.artifacts || '[]'),
4042
structuredData: safeParseJSON(step.structuredData || '{}'),
4143
tokenUsage: safeParseJSON(step.tokenUsage || '[]'),
42-
toolCalls: step.toolCalls?.map(toolCallResultPO2DTO),
44+
toolCalls: step.toolCalls?.map((tc) => toolCallResultPO2DTO(tc, options)),
4345
};
4446
}
4547

@@ -57,7 +59,7 @@ export function actionMessagePO2DTO(message: ActionMessageModel): ActionMessage
5759
* Sanitize tool output for frontend display
5860
* Removes large content fields that are not needed for display
5961
*/
60-
function sanitizeToolOutput(
62+
export function sanitizeToolOutput(
6163
toolName: string,
6264
output: Record<string, unknown>,
6365
): Record<string, unknown> {
@@ -77,9 +79,14 @@ function sanitizeToolOutput(
7779
return output;
7880
}
7981

80-
export function toolCallResultPO2DTO(toolCall: ToolCallResultModel): ToolCallResult {
82+
export function toolCallResultPO2DTO(
83+
toolCall: ToolCallResultModel,
84+
options?: { sanitizeForDisplay?: boolean },
85+
): ToolCallResult {
8186
const rawOutput = safeParseJSON(toolCall.output || '{}');
82-
const output = sanitizeToolOutput(toolCall.toolName, rawOutput);
87+
const output = options?.sanitizeForDisplay
88+
? sanitizeToolOutput(toolCall.toolName, rawOutput)
89+
: rawOutput;
8390

8491
return {
8592
callId: toolCall.callId,
@@ -97,7 +104,7 @@ export function toolCallResultPO2DTO(toolCall: ToolCallResultModel): ToolCallRes
97104
};
98105
}
99106

100-
export function actionResultPO2DTO(result: ActionDetail): ActionResult {
107+
export function actionResultPO2DTO(result: ActionDetail, options?: SanitizeOptions): ActionResult {
101108
return {
102109
...pick(result, [
103110
'resultId',
@@ -127,7 +134,7 @@ export function actionResultPO2DTO(result: ActionDetail): ActionResult {
127134
storageKey: result.storageKey,
128135
createdAt: result.createdAt.toJSON(),
129136
updatedAt: result.updatedAt.toJSON(),
130-
steps: result.steps?.map(actionStepPO2DTO),
137+
steps: result.steps?.map((s) => actionStepPO2DTO(s, options)),
131138
messages: result.messages,
132139
files: result.files,
133140
toolsets: safeParseJSON(result.toolsets || '[]'),

apps/api/src/modules/action/action.service.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
ActionMessage as ActionMessageModel,
2323
ToolCallResult as ToolCallResultModel,
2424
} from '@prisma/client';
25-
import { ActionDetail, actionMessagePO2DTO } from '../action/action.dto';
25+
import { ActionDetail, actionMessagePO2DTO, sanitizeToolOutput } from '../action/action.dto';
2626
import { PrismaService } from '../common/prisma.service';
2727
import { providerItem2ModelInfo } from '../provider/provider.dto';
2828
import { ProviderService } from '../provider/provider.service';
@@ -35,6 +35,7 @@ import { InvokeSkillJobData } from '../skill/skill.dto';
3535

3636
type GetActionResultParams = GetActionResultData['query'] & {
3737
includeFiles?: boolean;
38+
sanitizeForDisplay?: boolean;
3839
};
3940

4041
@Injectable()
@@ -60,7 +61,7 @@ export class ActionService {
6061
) {}
6162

6263
async getActionResult(user: User, param: GetActionResultParams): Promise<ActionDetail> {
63-
const { resultId, version, includeFiles = false } = param;
64+
const { resultId, version, includeFiles = false, sanitizeForDisplay = false } = param;
6465

6566
const result = await this.prisma.actionResult.findFirst({
6667
where: {
@@ -74,7 +75,9 @@ export class ActionService {
7475
throw new ActionResultNotFoundError();
7576
}
7677

77-
const enrichedResult = await this.enrichActionResultWithDetails(user, result);
78+
const enrichedResult = await this.enrichActionResultWithDetails(user, result, {
79+
sanitizeForDisplay,
80+
});
7881

7982
if (includeFiles) {
8083
enrichedResult.files = await this.driveService.listAllDriveFiles(user, {
@@ -92,6 +95,7 @@ export class ActionService {
9295
private async enrichActionResultWithDetails(
9396
user: User,
9497
result: ActionResult,
98+
options?: { sanitizeForDisplay?: boolean },
9599
): Promise<ActionDetail> {
96100
const item =
97101
(result.providerItemId
@@ -131,6 +135,14 @@ export class ActionService {
131135
if (message.type === 'tool' && message.toolCallId) {
132136
const toolCallResult = toolCallResultMap.get(message.toolCallId);
133137
if (toolCallResult) {
138+
const rawOutput = safeParseJSON(toolCallResult.output || '{}') ?? {
139+
rawOutput: toolCallResult.output,
140+
};
141+
// Apply sanitization if needed
142+
const output = options?.sanitizeForDisplay
143+
? sanitizeToolOutput(toolCallResult.toolName, rawOutput)
144+
: rawOutput;
145+
134146
// Attach the tool call result to the message
135147
enrichedMessage.toolCallResult = {
136148
callId: toolCallResult.callId,
@@ -139,9 +151,7 @@ export class ActionService {
139151
toolName: toolCallResult.toolName,
140152
stepName: toolCallResult.stepName,
141153
input: safeParseJSON(toolCallResult.input || '{}') ?? {},
142-
output: safeParseJSON(toolCallResult.output || '{}') ?? {
143-
rawOutput: toolCallResult.output,
144-
},
154+
output,
145155
error: toolCallResult.error || '',
146156
status: toolCallResult.status as 'executing' | 'completed' | 'failed',
147157
createdAt: toolCallResult.createdAt.getTime(),

apps/api/src/modules/copilot/copilot.dto.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ export const copilotSessionPO2DTO = (
1313
...pick(session, ['sessionId', 'title', 'canvasId']),
1414
createdAt: session.createdAt.toJSON(),
1515
updatedAt: session.updatedAt.toJSON(),
16-
results: session.results?.map(actionResultPO2DTO),
16+
results: session.results?.map((result) => actionResultPO2DTO(result)),
1717
};
1818
};

apps/api/src/modules/voucher/voucher.constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
// Daily popup trigger limit per user
6-
export const DAILY_POPUP_TRIGGER_LIMIT = 3;
6+
export const DAILY_POPUP_TRIGGER_LIMIT = 999;
77

88
// Default LLM score when scoring fails (50 = 50% discount)
99
export const DEFAULT_LLM_SCORE = 50;

apps/api/src/modules/voucher/voucher.service.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -633,17 +633,32 @@ export class VoucherService implements OnModuleInit {
633633

634634
/**
635635
* Create a sharing invitation for a voucher
636+
* Users can share if they are the owner OR if they claimed the voucher via invitation
636637
*/
637638
async createInvitation(uid: string, voucherId: string): Promise<CreateInvitationResult> {
638-
// Get the voucher
639+
// Get the voucher (without uid filter - we'll check permission separately)
639640
const voucher = await this.prisma.voucher.findFirst({
640-
where: { voucherId, uid },
641+
where: { voucherId },
641642
});
642643

643644
if (!voucher) {
644645
throw new Error('Voucher not found');
645646
}
646647

648+
// Check permission: owner OR claimant (same logic as validateVoucher)
649+
const isOwner = voucher.uid === uid;
650+
const isClaimant = await this.prisma.voucherInvitation.findFirst({
651+
where: {
652+
voucherId,
653+
inviteeUid: uid,
654+
status: InvitationStatus.CLAIMED,
655+
},
656+
});
657+
658+
if (!isOwner && !isClaimant) {
659+
throw new Error('Voucher not found');
660+
}
661+
647662
const invitationId = genVoucherInvitationID();
648663
const inviteCode = genInviteCode();
649664

0 commit comments

Comments
 (0)