chore: test deploy snapshot
This commit is contained in:
@@ -22,6 +22,8 @@ import {
|
||||
upsertTokenSettingsConfig,
|
||||
type TokenSettingRecord,
|
||||
} from '../services/token-setting-config-service.js';
|
||||
import { listTokenSettingActivities } from '../services/token-setting-activity-service.js';
|
||||
import { extractRequestAuditContext } from '../utils/request-audit.js';
|
||||
|
||||
const CHAT_SHARE_PATH_PREFIX = '/chat/share/';
|
||||
|
||||
@@ -388,7 +390,10 @@ export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
const nextTokenSettingsInput = parsed.tokenSettings as Partial<TokenSettingRecord>[];
|
||||
const savedTokenSettings =
|
||||
accessContext.scope === 'full'
|
||||
? await upsertTokenSettingsConfig(nextTokenSettingsInput)
|
||||
? await upsertTokenSettingsConfig(nextTokenSettingsInput, {
|
||||
actorLabel: 'manager',
|
||||
audit: extractRequestAuditContext(request),
|
||||
})
|
||||
: await (async () => {
|
||||
const authorizedSettingId = accessContext.tokenSetting.id;
|
||||
const requestedSetting = nextTokenSettingsInput.find(
|
||||
@@ -403,7 +408,10 @@ export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
const nextTokenSettings = currentTokenSettings.map((item) =>
|
||||
item.id === authorizedSettingId ? { ...requestedSetting, id: authorizedSettingId } : item,
|
||||
);
|
||||
return upsertTokenSettingsConfig(nextTokenSettings);
|
||||
return upsertTokenSettingsConfig(nextTokenSettings, {
|
||||
actorLabel: 'shared-link-manager',
|
||||
audit: extractRequestAuditContext(request),
|
||||
});
|
||||
})();
|
||||
|
||||
return {
|
||||
@@ -417,6 +425,26 @@ export async function registerAppConfigRoutes(app: FastifyInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/token-settings/:settingId/activities', async (request, reply) => {
|
||||
const accessContext = await resolveTokenSettingsAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendTokenSettingsAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
const settingId = z.string().trim().min(1).parse((request.params as { settingId: string }).settingId);
|
||||
if (accessContext.scope === 'shared' && accessContext.tokenSetting.id !== settingId) {
|
||||
return reply.code(403).send({
|
||||
message: '현재 공유 링크에서는 연결된 토큰 설정 이력만 볼 수 있습니다.',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
activities: await listTokenSettingActivities(settingId),
|
||||
};
|
||||
});
|
||||
|
||||
app.put('/api/app-config', async (request, reply) => {
|
||||
const accessContext = await resolveAppConfigAccessContext(request);
|
||||
if (!accessContext) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { hasErrorLogViewAccessToken } from '../services/error-log-service.js';
|
||||
import { getSharedResourceTokenDetailByShareToken } from '../services/shared-resource-token-service.js';
|
||||
import {
|
||||
createBaseballTicketBayAlert,
|
||||
createBaseballTicketBayLog,
|
||||
@@ -39,46 +41,115 @@ function readHeader(request: { headers: Record<string, string | string[] | undef
|
||||
return Array.isArray(raw) ? String(raw[0] ?? '').trim() : String(raw ?? '').trim();
|
||||
}
|
||||
|
||||
function hasBaseballTicketBayGlobalAccess(request: { headers: Record<string, string | string[] | undefined> }) {
|
||||
return hasErrorLogViewAccessToken(request.headers['x-access-token']);
|
||||
}
|
||||
|
||||
type BaseballTicketBayRouteAccessContext =
|
||||
| { scope: 'all' }
|
||||
| { scope: 'client'; clientId: string }
|
||||
| { scope: 'shared-token'; clientId: string; tokenId: string };
|
||||
|
||||
function toOwnerScope(accessContext: Exclude<BaseballTicketBayRouteAccessContext, { scope: 'all' }> | { scope: 'all' }) {
|
||||
if (accessContext.scope === 'all') {
|
||||
return { kind: 'all' } as const;
|
||||
}
|
||||
|
||||
if (accessContext.scope === 'shared-token') {
|
||||
return { kind: 'owner', ownerType: 'shared-token', ownerId: accessContext.tokenId } as const;
|
||||
}
|
||||
|
||||
return { kind: 'owner', ownerType: 'client', ownerId: accessContext.clientId } as const;
|
||||
}
|
||||
|
||||
async function resolveBaseballTicketBayAccessContext(
|
||||
request: { headers: Record<string, string | string[] | undefined> },
|
||||
) : Promise<BaseballTicketBayRouteAccessContext | null> {
|
||||
const clientId = readHeader(request, 'x-client-id');
|
||||
|
||||
if (hasBaseballTicketBayGlobalAccess(request)) {
|
||||
return { scope: 'all' };
|
||||
}
|
||||
|
||||
const accessToken = readHeader(request, 'x-access-token');
|
||||
|
||||
if (accessToken) {
|
||||
const sharedTokenDetail = await getSharedResourceTokenDetailByShareToken(accessToken);
|
||||
|
||||
if (
|
||||
sharedTokenDetail
|
||||
&& sharedTokenDetail.token.enabled !== false
|
||||
&& !sharedTokenDetail.token.revokedAt
|
||||
&& sharedTokenDetail.token.allowedAppIds.some((item) => item.trim().toLowerCase() === 'baseball-ticket-bay')
|
||||
) {
|
||||
if (!clientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
scope: 'shared-token',
|
||||
clientId,
|
||||
tokenId: sharedTokenDetail.token.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!clientId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
scope: 'client',
|
||||
clientId,
|
||||
};
|
||||
}
|
||||
|
||||
export async function registerBaseballTicketBayRoutes(app: FastifyInstance) {
|
||||
app.post('/api/baseball-ticket-bay/search', async (request) => searchBaseballTicketBayListings(request.body ?? {}));
|
||||
|
||||
app.get('/api/baseball-ticket-bay/alerts', async (request, reply) => {
|
||||
const clientId = readHeader(request, 'x-client-id');
|
||||
const accessContext = await resolveBaseballTicketBayAccessContext(request);
|
||||
|
||||
if (!clientId) {
|
||||
return reply.code(400).send({ message: '클라이언트 ID가 없어 알림 목록을 불러올 수 없습니다.' });
|
||||
if (!accessContext) {
|
||||
return reply.code(400).send({ message: '접근 식별값이 없어 알림 목록을 불러올 수 없습니다.' });
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
items: await listBaseballTicketBayAlerts(clientId),
|
||||
includeAllClients: accessContext.scope === 'all',
|
||||
accessScope: accessContext.scope,
|
||||
scopeOwnerId: accessContext.scope === 'shared-token' ? accessContext.tokenId : accessContext.scope === 'client' ? accessContext.clientId : null,
|
||||
items: await listBaseballTicketBayAlerts(toOwnerScope(accessContext)),
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/baseball-ticket-bay/logs', async (request, reply) => {
|
||||
const clientId = readHeader(request, 'x-client-id');
|
||||
const accessContext = await resolveBaseballTicketBayAccessContext(request);
|
||||
|
||||
if (!clientId) {
|
||||
return reply.code(400).send({ message: '클라이언트 ID가 없어 로그를 불러올 수 없습니다.' });
|
||||
if (!accessContext) {
|
||||
return reply.code(400).send({ message: '접근 식별값이 없어 로그를 불러올 수 없습니다.' });
|
||||
}
|
||||
|
||||
const query = z.object({ alertId: z.string().trim().min(1).optional() }).parse(request.query ?? {});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
items: await listBaseballTicketBayLogs(clientId, query.alertId),
|
||||
includeAllClients: accessContext.scope === 'all',
|
||||
accessScope: accessContext.scope,
|
||||
scopeOwnerId: accessContext.scope === 'shared-token' ? accessContext.tokenId : accessContext.scope === 'client' ? accessContext.clientId : null,
|
||||
items: await listBaseballTicketBayLogs(toOwnerScope(accessContext), query.alertId),
|
||||
};
|
||||
});
|
||||
|
||||
app.delete('/api/baseball-ticket-bay/logs/:id', async (request, reply) => {
|
||||
const clientId = readHeader(request, 'x-client-id');
|
||||
const accessContext = await resolveBaseballTicketBayAccessContext(request);
|
||||
|
||||
if (!clientId) {
|
||||
return reply.code(400).send({ message: '클라이언트 ID가 없어 로그를 삭제할 수 없습니다.' });
|
||||
if (!accessContext || accessContext.scope === 'all') {
|
||||
return reply.code(400).send({ message: '현재 접근 범위에서는 로그를 삭제할 수 없습니다.' });
|
||||
}
|
||||
|
||||
const params = z.object({ id: z.string().trim().min(1) }).parse(request.params ?? {});
|
||||
const item = await deleteBaseballTicketBayLog(params.id, clientId);
|
||||
const item = await deleteBaseballTicketBayLog(params.id, toOwnerScope(accessContext));
|
||||
|
||||
if (!item) {
|
||||
return reply.code(404).send({ message: '삭제할 로그를 찾지 못했습니다.' });
|
||||
@@ -91,20 +162,24 @@ export async function registerBaseballTicketBayRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
app.post('/api/baseball-ticket-bay/alerts', async (request, reply) => {
|
||||
const clientId = readHeader(request, 'x-client-id');
|
||||
const accessContext = await resolveBaseballTicketBayAccessContext(request);
|
||||
|
||||
if (!clientId) {
|
||||
return reply.code(400).send({ message: '클라이언트 ID가 없어 알림을 저장할 수 없습니다.' });
|
||||
if (!accessContext || accessContext.scope === 'all') {
|
||||
return reply.code(400).send({ message: '현재 접근 범위에서는 알림을 저장할 수 없습니다.' });
|
||||
}
|
||||
|
||||
const payload = alertPayloadSchema.parse(request.body ?? {});
|
||||
const item = await createBaseballTicketBayAlert(payload, {
|
||||
clientId,
|
||||
clientId: accessContext.clientId,
|
||||
ownerType: accessContext.scope === 'shared-token' ? 'shared-token' : 'client',
|
||||
ownerId: accessContext.scope === 'shared-token' ? accessContext.tokenId : accessContext.clientId,
|
||||
appOrigin: readHeader(request, 'x-app-origin'),
|
||||
appDomain: readHeader(request, 'x-app-domain'),
|
||||
});
|
||||
await createBaseballTicketBayLog({
|
||||
clientId,
|
||||
clientId: accessContext.clientId,
|
||||
ownerType: accessContext.scope === 'shared-token' ? 'shared-token' : 'client',
|
||||
ownerId: accessContext.scope === 'shared-token' ? accessContext.tokenId : accessContext.clientId,
|
||||
alertId: item.id,
|
||||
alertTitle: item.title,
|
||||
action: 'create',
|
||||
@@ -120,21 +195,25 @@ export async function registerBaseballTicketBayRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
app.patch('/api/baseball-ticket-bay/alerts/:id', async (request, reply) => {
|
||||
const clientId = readHeader(request, 'x-client-id');
|
||||
const accessContext = await resolveBaseballTicketBayAccessContext(request);
|
||||
|
||||
if (!clientId) {
|
||||
return reply.code(400).send({ message: '클라이언트 ID가 없어 알림을 수정할 수 없습니다.' });
|
||||
if (!accessContext || accessContext.scope === 'all') {
|
||||
return reply.code(400).send({ message: '현재 접근 범위에서는 알림을 수정할 수 없습니다.' });
|
||||
}
|
||||
|
||||
const params = z.object({ id: z.string().trim().min(1) }).parse(request.params ?? {});
|
||||
const payload = alertPayloadSchema.partial().parse(request.body ?? {});
|
||||
const item = await updateBaseballTicketBayAlert(params.id, payload, {
|
||||
clientId,
|
||||
clientId: accessContext.clientId,
|
||||
ownerType: accessContext.scope === 'shared-token' ? 'shared-token' : 'client',
|
||||
ownerId: accessContext.scope === 'shared-token' ? accessContext.tokenId : accessContext.clientId,
|
||||
appOrigin: readHeader(request, 'x-app-origin'),
|
||||
appDomain: readHeader(request, 'x-app-domain'),
|
||||
});
|
||||
await createBaseballTicketBayLog({
|
||||
clientId,
|
||||
clientId: accessContext.clientId,
|
||||
ownerType: accessContext.scope === 'shared-token' ? 'shared-token' : 'client',
|
||||
ownerId: accessContext.scope === 'shared-token' ? accessContext.tokenId : accessContext.clientId,
|
||||
alertId: item.id,
|
||||
alertTitle: item.title,
|
||||
action: payload.active === false ? 'pause' : payload.active === true ? 'resume' : 'run',
|
||||
@@ -155,21 +234,23 @@ export async function registerBaseballTicketBayRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
app.delete('/api/baseball-ticket-bay/alerts/:id', async (request, reply) => {
|
||||
const clientId = readHeader(request, 'x-client-id');
|
||||
const accessContext = await resolveBaseballTicketBayAccessContext(request);
|
||||
|
||||
if (!clientId) {
|
||||
return reply.code(400).send({ message: '클라이언트 ID가 없어 알림을 삭제할 수 없습니다.' });
|
||||
if (!accessContext || accessContext.scope === 'all') {
|
||||
return reply.code(400).send({ message: '현재 접근 범위에서는 알림을 삭제할 수 없습니다.' });
|
||||
}
|
||||
|
||||
const params = z.object({ id: z.string().trim().min(1) }).parse(request.params ?? {});
|
||||
const item = await deleteBaseballTicketBayAlert(params.id, clientId);
|
||||
const item = await deleteBaseballTicketBayAlert(params.id, toOwnerScope(accessContext));
|
||||
|
||||
if (!item) {
|
||||
return reply.code(404).send({ message: '삭제할 알림을 찾지 못했습니다.' });
|
||||
}
|
||||
|
||||
await createBaseballTicketBayLog({
|
||||
clientId,
|
||||
clientId: accessContext.clientId,
|
||||
ownerType: accessContext.scope === 'shared-token' ? 'shared-token' : 'client',
|
||||
ownerId: accessContext.scope === 'shared-token' ? accessContext.tokenId : accessContext.clientId,
|
||||
alertId: item.id,
|
||||
alertTitle: item.title,
|
||||
action: 'delete',
|
||||
@@ -185,14 +266,22 @@ export async function registerBaseballTicketBayRoutes(app: FastifyInstance) {
|
||||
});
|
||||
|
||||
app.post('/api/baseball-ticket-bay/alerts/:id/run', async (request, reply) => {
|
||||
const clientId = readHeader(request, 'x-client-id');
|
||||
const accessContext = await resolveBaseballTicketBayAccessContext(request);
|
||||
|
||||
if (!clientId) {
|
||||
return reply.code(400).send({ message: '클라이언트 ID가 없어 즉시 실행할 수 없습니다.' });
|
||||
if (!accessContext) {
|
||||
return reply.code(400).send({ message: '접근 식별값이 없어 즉시 실행할 수 없습니다.' });
|
||||
}
|
||||
|
||||
const params = z.object({ id: z.string().trim().min(1) }).parse(request.params ?? {});
|
||||
const result = await runBaseballTicketBayAlert(params.id, { ignoreTimeWindow: true });
|
||||
|
||||
if (accessContext.scope === 'all') {
|
||||
return reply.code(403).send({ message: '전체 보기 범위에서는 즉시 실행할 수 없습니다.' });
|
||||
}
|
||||
|
||||
const result = await runBaseballTicketBayAlert(params.id, {
|
||||
ignoreTimeWindow: true,
|
||||
scope: toOwnerScope(accessContext),
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { resolveStaticContentType, shouldAutoCompleteShareReplyParentVerification } from './chat.js';
|
||||
import { resolvePromptFollowupMode, resolveStaticContentType } from './chat.js';
|
||||
|
||||
test('resolveStaticContentType returns html content type for chat resource html files', () => {
|
||||
assert.equal(resolveStaticContentType('/tmp/sample.html'), 'text/html; charset=utf-8');
|
||||
@@ -12,40 +12,9 @@ test('resolveStaticContentType keeps plain text content type for code resources'
|
||||
assert.equal(resolveStaticContentType('/tmp/sample.diff'), 'text/plain; charset=utf-8');
|
||||
});
|
||||
|
||||
test('shouldAutoCompleteShareReplyParentVerification only completes answered requests that are not already verified', () => {
|
||||
assert.equal(
|
||||
shouldAutoCompleteShareReplyParentVerification({
|
||||
responseMessageId: 101,
|
||||
responseText: '',
|
||||
manualVerificationCompletedAt: null,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
shouldAutoCompleteShareReplyParentVerification({
|
||||
responseMessageId: null,
|
||||
responseText: '답변 본문',
|
||||
manualVerificationCompletedAt: null,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
shouldAutoCompleteShareReplyParentVerification({
|
||||
responseMessageId: null,
|
||||
responseText: '',
|
||||
manualVerificationCompletedAt: null,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
shouldAutoCompleteShareReplyParentVerification({
|
||||
responseMessageId: 102,
|
||||
responseText: '답변 본문',
|
||||
manualVerificationCompletedAt: '2026-05-24T06:00:00.000Z',
|
||||
}),
|
||||
false,
|
||||
);
|
||||
test('resolvePromptFollowupMode defaults to queue and preserves direct mode', () => {
|
||||
assert.equal(resolvePromptFollowupMode(undefined), 'queue');
|
||||
assert.equal(resolvePromptFollowupMode(null), 'queue');
|
||||
assert.equal(resolvePromptFollowupMode('queue'), 'queue');
|
||||
assert.equal(resolvePromptFollowupMode('direct'), 'direct');
|
||||
});
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
ensureChatSessionResourceDirectories,
|
||||
getActiveChatService,
|
||||
getChatRuntimeController,
|
||||
shouldAutoCompleteReplyParentVerification,
|
||||
} from '../services/chat-service.js';
|
||||
import { rollbackChatRuntimeRequest } from '../services/chat-runtime-rollback-service.js';
|
||||
import {
|
||||
@@ -43,10 +42,12 @@ import {
|
||||
listChatConversationDetailPage,
|
||||
listChatConversations,
|
||||
markChatConversationRequestManualCompletion,
|
||||
ChatConversationManualCompletionBlockedError,
|
||||
markChatConversationResponsesRead,
|
||||
persistChatConversationPromptSelection,
|
||||
upsertChatConversationRequest,
|
||||
updateChatConversationContext,
|
||||
hasPendingAttentionVerificationRequest,
|
||||
} from '../services/chat-room-service.js';
|
||||
import { chatRuntimeService } from '../services/chat-runtime-service.js';
|
||||
import { resolveMainProjectRoot } from '../services/main-project-root-service.js';
|
||||
@@ -123,6 +124,10 @@ async function findExistingActivePromptFollowupRequest(
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
export function resolvePromptFollowupMode(mode?: 'queue' | 'direct' | null) {
|
||||
return mode === 'direct' ? 'direct' : 'queue';
|
||||
}
|
||||
|
||||
function encodeBase64Url(value: string) {
|
||||
return Buffer.from(value, 'utf-8').toString('base64url');
|
||||
}
|
||||
@@ -223,19 +228,6 @@ function resolveChatShareTokenSettingSnapshot(tokenPayload: ChatShareTokenPayloa
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldAutoCompleteShareReplyParentVerification(request: {
|
||||
responseMessageId?: number | null;
|
||||
responseText?: string | null;
|
||||
manualVerificationCompletedAt?: string | null;
|
||||
} | null | undefined) {
|
||||
return shouldAutoCompleteReplyParentVerification({
|
||||
requestOrigin: 'composer',
|
||||
responseMessageId: request?.responseMessageId ?? null,
|
||||
responseText: request?.responseText ?? '',
|
||||
manualVerificationCompletedAt: request?.manualVerificationCompletedAt ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
function createManagedChatShareTokenId() {
|
||||
return `chat_share_${randomUUID().replace(/-/g, '').slice(0, 20)}`;
|
||||
}
|
||||
@@ -802,36 +794,40 @@ function hasUnresolvedPromptPart(message: ListedChatConversationMessage) {
|
||||
function hasPendingPromptRequest(
|
||||
request: ListedChatConversationRequest,
|
||||
relatedMessages: ListedChatConversationMessage[],
|
||||
promptSubmittedCount = 0,
|
||||
) {
|
||||
if (request.manualPromptCompletedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return relatedMessages.some(
|
||||
(message) => (message.author === 'codex' || message.author === 'system') && hasUnresolvedPromptPart(message),
|
||||
);
|
||||
}
|
||||
const unresolvedPromptCount = relatedMessages.reduce((count, message) => {
|
||||
if (message.author !== 'codex' && message.author !== 'system') {
|
||||
return count;
|
||||
}
|
||||
|
||||
function hasVerificationTargetMessage(message: ListedChatConversationMessage) {
|
||||
if (message.author !== 'codex' && message.author !== 'system') {
|
||||
const promptParts = (message.parts ?? []).filter(
|
||||
(
|
||||
part: NonNullable<ListedChatConversationMessage['parts']>[number],
|
||||
): part is Extract<NonNullable<ListedChatConversationMessage['parts']>[number], { type: 'prompt' }> => part.type === 'prompt',
|
||||
);
|
||||
|
||||
return count + promptParts.filter(
|
||||
(part: Extract<NonNullable<ListedChatConversationMessage['parts']>[number], { type: 'prompt' }>) => !isPromptPartResolved(part),
|
||||
).length;
|
||||
}, 0);
|
||||
|
||||
if (unresolvedPromptCount === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const text = String(message.text ?? '').trim();
|
||||
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.length > 720) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return /```diff[\s\S]*?```|\[\[preview:|\[\[link-card:|\[\[prompt:/i.test(text);
|
||||
return unresolvedPromptCount > Math.max(0, promptSubmittedCount);
|
||||
}
|
||||
|
||||
function hasVerificationTargetRequest(relatedMessages: ListedChatConversationMessage[]) {
|
||||
return relatedMessages.some((message) => hasVerificationTargetMessage(message));
|
||||
function hasVerificationTargetRequest(
|
||||
request: ListedChatConversationRequest,
|
||||
relatedMessages: ListedChatConversationMessage[],
|
||||
) {
|
||||
return hasPendingAttentionVerificationRequest(request, relatedMessages);
|
||||
}
|
||||
|
||||
function buildChildRequestCountMap(requests: ListedChatConversationRequest[]) {
|
||||
@@ -850,6 +846,30 @@ function buildChildRequestCountMap(requests: ListedChatConversationRequest[]) {
|
||||
return nextMap;
|
||||
}
|
||||
|
||||
function buildPromptFollowupCountMap(requests: ListedChatConversationRequest[]) {
|
||||
const nextMap = new Map<string, number>();
|
||||
|
||||
requests.forEach((request) => {
|
||||
if (request.requestOrigin !== 'prompt') {
|
||||
return;
|
||||
}
|
||||
|
||||
const parentRequestId = request.parentRequestId?.trim() || '';
|
||||
|
||||
if (!parentRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
nextMap.set(parentRequestId, (nextMap.get(parentRequestId) ?? 0) + 1);
|
||||
});
|
||||
|
||||
return nextMap;
|
||||
}
|
||||
|
||||
function isPromptFollowupRoomRequest(request: ListedChatConversationRequest) {
|
||||
return request.requestOrigin === 'prompt';
|
||||
}
|
||||
|
||||
function isRequestInFlight(status: ListedChatConversationRequest['status']) {
|
||||
return status === 'accepted' || status === 'queued' || status === 'started';
|
||||
}
|
||||
@@ -858,20 +878,29 @@ function isPendingCompletionRoomRequest(
|
||||
request: ListedChatConversationRequest,
|
||||
relatedMessages: ListedChatConversationMessage[],
|
||||
childRequestCountByParentId?: Map<string, number>,
|
||||
promptFollowupCountByParentId?: Map<string, number>,
|
||||
) {
|
||||
if (isPromptFollowupRoomRequest(request)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isRequestInFlight(request.status)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasPendingPromptRequest(request, relatedMessages)) {
|
||||
if (hasPendingPromptRequest(request, relatedMessages, promptFollowupCountByParentId?.get(request.requestId.trim()) ?? 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request.manualPromptCompletedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((childRequestCountByParentId?.get(request.requestId.trim()) ?? 0) > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasVerificationTargetRequest(relatedMessages)) {
|
||||
if (!hasVerificationTargetRequest(request, relatedMessages)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -884,6 +913,7 @@ function buildRoomRequestCounts(
|
||||
) {
|
||||
const requestMessagesById = new Map<string, ListedChatConversationMessage[]>();
|
||||
const childRequestCountByParentId = buildChildRequestCountMap(requests);
|
||||
const promptFollowupCountByParentId = buildPromptFollowupCountMap(requests);
|
||||
|
||||
messages.forEach((message) => {
|
||||
const requestId = message.clientRequestId?.trim() || '';
|
||||
@@ -897,12 +927,15 @@ function buildRoomRequestCounts(
|
||||
requestMessagesById.set(requestId, current);
|
||||
});
|
||||
|
||||
const processingCount = requests.filter((request) => isRequestInFlight(request.status)).length;
|
||||
const processingCount = requests.filter(
|
||||
(request) => !isPromptFollowupRoomRequest(request) && isRequestInFlight(request.status),
|
||||
).length;
|
||||
const unansweredCount = requests.filter((request) =>
|
||||
isPendingCompletionRoomRequest(
|
||||
request,
|
||||
requestMessagesById.get(request.requestId.trim()) ?? [],
|
||||
childRequestCountByParentId,
|
||||
promptFollowupCountByParentId,
|
||||
),
|
||||
).length;
|
||||
|
||||
@@ -937,8 +970,10 @@ function buildManagedSharePlaceholderRequest(tokenPayload: ChatShareTokenPayload
|
||||
requestOrigin: 'composer',
|
||||
sharedResourceTokenId: tokenPayload.managedResourceTokenId?.trim() || null,
|
||||
parentRequestId: null,
|
||||
promptContextRef: null,
|
||||
status: 'completed',
|
||||
statusMessage: '공유 채팅방 시작 요청을 복원했습니다.',
|
||||
retryCount: 0,
|
||||
userMessageId: null,
|
||||
userText: '',
|
||||
responseMessageId: null,
|
||||
@@ -1554,6 +1589,9 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
const payload = z.object({
|
||||
accessPin: z.string().regex(/^\d{4}$/u).optional().nullable(),
|
||||
accessPinPromptTtlMinutes: z.number().int().min(0).max(7 * 24 * 60).optional().nullable(),
|
||||
chatTypeId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||
chatTypeLabel: z.string().trim().min(1).max(200).optional().nullable(),
|
||||
notifyOffline: z.boolean().optional().nullable(),
|
||||
}).parse(request.body ?? {});
|
||||
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||
const tokenPayload = resolveChatSharePayloadFromManagedResource(managedContext.managedResource) ?? parseChatShareToken(params.token);
|
||||
@@ -1615,10 +1653,35 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
let updatedConversation = await getChatConversation(tokenPayload.sessionId, getRequestClientId(request));
|
||||
|
||||
if (payload.chatTypeId || payload.notifyOffline != null) {
|
||||
updatedConversation = await updateChatConversationContext(tokenPayload.sessionId, {
|
||||
clientId: getRequestClientId(request),
|
||||
chatTypeId: payload.chatTypeId?.trim() || undefined,
|
||||
lastChatTypeId: payload.chatTypeId?.trim() || undefined,
|
||||
contextLabel: payload.chatTypeLabel?.trim() || undefined,
|
||||
contextDescription: payload.chatTypeId ? null : undefined,
|
||||
notifyOffline: payload.notifyOffline ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
hasAccessPin: saved.token.hasAccessPin,
|
||||
accessPinPromptTtlMinutes: saved.token.accessPinPromptTtlMinutes,
|
||||
conversation: updatedConversation
|
||||
? {
|
||||
sessionId: updatedConversation.sessionId,
|
||||
title: updatedConversation.title,
|
||||
requestBadgeLabel: updatedConversation.requestBadgeLabel ?? null,
|
||||
chatTypeId: updatedConversation.chatTypeId ?? null,
|
||||
lastChatTypeId: updatedConversation.lastChatTypeId ?? null,
|
||||
contextLabel: updatedConversation.contextLabel ?? null,
|
||||
contextDescription: updatedConversation.contextDescription ?? null,
|
||||
notifyOffline: updatedConversation.notifyOffline,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1866,6 +1929,11 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
sessionId: shareSnapshot.conversation?.sessionId ?? tokenPayload.sessionId,
|
||||
title: shareSnapshot.conversation?.title ?? '공유 채팅',
|
||||
requestBadgeLabel: shareSnapshot.conversation?.requestBadgeLabel ?? null,
|
||||
chatTypeId: shareSnapshot.conversation?.chatTypeId ?? null,
|
||||
lastChatTypeId: shareSnapshot.conversation?.lastChatTypeId ?? null,
|
||||
contextLabel: shareSnapshot.conversation?.contextLabel ?? null,
|
||||
contextDescription: shareSnapshot.conversation?.contextDescription ?? null,
|
||||
notifyOffline: shareSnapshot.conversation?.notifyOffline === true,
|
||||
},
|
||||
rootRequestId: shareSnapshot.rootRequestId,
|
||||
targetRequest: shareSnapshot.targetRequest,
|
||||
@@ -1955,6 +2023,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
}).parse(request.params ?? {});
|
||||
const payload = z.object({
|
||||
text: z.string().trim().min(1).max(20000),
|
||||
mode: z.enum(['queue', 'direct']).optional(),
|
||||
parentRequestId: z.string().trim().min(1).max(120).optional().nullable(),
|
||||
}).parse(request.body ?? {});
|
||||
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||
@@ -2012,11 +2081,13 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
const requestedParentRequestId = payload.parentRequestId?.trim() || '';
|
||||
const resolvedParentRequestId = resolveRecoveredShareParentRequestId(
|
||||
shareSnapshot,
|
||||
requestedParentRequestId,
|
||||
[shareSnapshot.targetRequest.requestId],
|
||||
);
|
||||
const resolvedParentRequestId = requestedParentRequestId
|
||||
? resolveRecoveredShareParentRequestId(
|
||||
shareSnapshot,
|
||||
requestedParentRequestId,
|
||||
[shareSnapshot.targetRequest.requestId],
|
||||
)
|
||||
: null;
|
||||
|
||||
if (
|
||||
resolvedParentRequestId
|
||||
@@ -2028,7 +2099,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
const queuedRequestId = await getActiveChatService()?.submitExternalMessage(tokenPayload.sessionId, payload.text, {
|
||||
mode: 'direct',
|
||||
mode: payload.mode === 'direct' ? 'direct' : 'queue',
|
||||
requestOrigin: 'composer',
|
||||
sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null,
|
||||
parentRequestId: resolvedParentRequestId,
|
||||
@@ -2041,22 +2112,6 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
const parentRequest = resolvedParentRequestId
|
||||
? shareSnapshot.requests.find((request) => request.requestId.trim() === resolvedParentRequestId) ?? null
|
||||
: null;
|
||||
|
||||
if (resolvedParentRequestId && shouldAutoCompleteShareReplyParentVerification(parentRequest)) {
|
||||
const updatedParentRequest = await markChatConversationRequestManualCompletion(
|
||||
tokenPayload.sessionId,
|
||||
resolvedParentRequestId,
|
||||
'verification',
|
||||
);
|
||||
|
||||
if (updatedParentRequest) {
|
||||
getActiveChatService()?.broadcastRequestUpdate(tokenPayload.sessionId, updatedParentRequest);
|
||||
}
|
||||
}
|
||||
|
||||
if (managedContext.managedResource) {
|
||||
await recordSharedResourceTokenUsage(managedContext.managedResource.token.id, {
|
||||
actorLabel: 'share-viewer',
|
||||
@@ -2179,6 +2234,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
summaryText: z.string().max(10000).optional().nullable(),
|
||||
attachments: z.array(chatComposerAttachmentSchema).max(20).optional(),
|
||||
followupText: z.string().trim().min(1).max(20000),
|
||||
mode: z.enum(['queue', 'direct']).optional(),
|
||||
contextRef: chatPromptContextRefSchema,
|
||||
}).parse(request.body ?? {});
|
||||
const managedContext = await resolveManagedChatShareContext(params.token);
|
||||
@@ -2261,7 +2317,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
);
|
||||
|
||||
const queuedRequestId = existingPromptRequest?.requestId ?? await getActiveChatService()?.submitExternalMessage(tokenPayload.sessionId, payload.followupText, {
|
||||
mode: 'direct',
|
||||
mode: resolvePromptFollowupMode(payload.mode),
|
||||
requestOrigin: 'prompt',
|
||||
sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null,
|
||||
parentRequestId: normalizedParentRequestId,
|
||||
@@ -2363,11 +2419,23 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
const item = await markChatConversationRequestManualCompletion(
|
||||
tokenPayload.sessionId,
|
||||
normalizedParentRequestId,
|
||||
payload.type,
|
||||
);
|
||||
let item = null;
|
||||
|
||||
try {
|
||||
item = await markChatConversationRequestManualCompletion(
|
||||
tokenPayload.sessionId,
|
||||
normalizedParentRequestId,
|
||||
payload.type,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ChatConversationManualCompletionBlockedError) {
|
||||
return reply.code(409).send({
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
return reply.code(404).send({
|
||||
@@ -2587,6 +2655,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
}
|
||||
|
||||
const queuedRequestId = await getActiveChatService()?.submitExternalMessage(tokenPayload.sessionId, normalizedUserText, {
|
||||
requestId: normalizedParentRequestId,
|
||||
mode: 'direct',
|
||||
requestOrigin: targetRequest.requestOrigin === 'prompt' ? 'prompt' : 'composer',
|
||||
sharedResourceTokenId: managedContext.managedResource?.token.id ?? tokenPayload.managedResourceTokenId ?? null,
|
||||
@@ -2928,11 +2997,23 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
type: z.enum(['prompt', 'verification']),
|
||||
}).parse(request.body ?? {});
|
||||
|
||||
const item = await markChatConversationRequestManualCompletion(
|
||||
params.sessionId,
|
||||
params.requestId,
|
||||
payload.type,
|
||||
);
|
||||
let item = null;
|
||||
|
||||
try {
|
||||
item = await markChatConversationRequestManualCompletion(
|
||||
params.sessionId,
|
||||
params.requestId,
|
||||
payload.type,
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof ChatConversationManualCompletionBlockedError) {
|
||||
return reply.code(409).send({
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
return reply.code(404).send({
|
||||
@@ -3044,7 +3125,7 @@ export async function registerChatRoutes(app: FastifyInstance) {
|
||||
);
|
||||
|
||||
const queuedRequestId = existingPromptRequest?.requestId ?? await getActiveChatService()?.submitExternalMessage(params.sessionId, payload.followupText, {
|
||||
mode: payload.mode === 'direct' ? 'direct' : 'queue',
|
||||
mode: resolvePromptFollowupMode(payload.mode),
|
||||
requestOrigin: 'prompt',
|
||||
parentRequestId: params.requestId,
|
||||
promptContextRef: payload.contextRef ?? null,
|
||||
|
||||
@@ -28,6 +28,19 @@ function buildRuntimeResponse() {
|
||||
export async function registerRuntimeRoutes(app: FastifyInstance) {
|
||||
app.get('/api/runtime', async () => buildRuntimeResponse());
|
||||
|
||||
app.post('/api/runtime/recover-interrupted-chat', async () => {
|
||||
const recovered = await getActiveChatService()?.recoverInterruptedSessions();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
recovered: recovered ?? {
|
||||
sessionCount: 0,
|
||||
restartedCount: 0,
|
||||
requeuedCount: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/runtime/drain', async (request) => {
|
||||
const { draining } = runtimeDrainBodySchema.parse(request.body ?? {});
|
||||
|
||||
|
||||
@@ -2,7 +2,15 @@ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { env } from '../config/env.js';
|
||||
import { getSharedResourceTokenDetailBySharePath } from '../services/shared-resource-token-service.js';
|
||||
import { listServerCommands, restartServerCommand, serverCommandKeys } from '../services/server-command-service.js';
|
||||
import {
|
||||
deployTestServerCommand,
|
||||
deployWorkServerCommand,
|
||||
listServerCommands,
|
||||
readWorkServerDeploymentState,
|
||||
restartServerCommand,
|
||||
serverCommandKeys,
|
||||
} from '../services/server-command-service.js';
|
||||
import { readTestServerDeploymentState } from '../services/test-server-deployment-service.js';
|
||||
import {
|
||||
cancelServerRestartReservation,
|
||||
confirmServerRestartReservation,
|
||||
@@ -43,16 +51,7 @@ function getImmediateRestartBlockInfo(
|
||||
}
|
||||
|
||||
if (key === 'work-server') {
|
||||
const pendingCount = codexPendingCount + automationPendingCount;
|
||||
|
||||
if (pendingCount === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
pendingCount,
|
||||
message: `진행 중인 Codex Live/자동화 작업 ${pendingCount}건이 있어 즉시 재기동할 수 없습니다. 재기동 예약을 사용해 주세요.`,
|
||||
};
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -92,7 +91,7 @@ async function resolveSharedServerCommandAccessContext(request: FastifyRequest)
|
||||
|
||||
return {
|
||||
scope: 'shared' as const,
|
||||
allowedKeys: new Set<string>(['work-server']),
|
||||
allowedKeys: new Set<string>(['work-server', 'test']),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -182,6 +181,12 @@ export async function registerServerCommandRoutes(app: FastifyInstance) {
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '서버 재기동에 실패했습니다.';
|
||||
const statusCode = error && typeof error === 'object' && 'statusCode' in error ? Number((error as { statusCode?: unknown }).statusCode) : null;
|
||||
|
||||
if (statusCode === 409) {
|
||||
reply.status(409);
|
||||
return { ok: false, message };
|
||||
}
|
||||
|
||||
if (key !== 'test' && key !== 'work-server') {
|
||||
throw error;
|
||||
@@ -207,6 +212,99 @@ export async function registerServerCommandRoutes(app: FastifyInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/server-commands/work-server/deployment', async (request, reply) => {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
if (accessContext.scope !== 'full' && !accessContext.allowedKeys.has('work-server')) {
|
||||
reply.status(403);
|
||||
return { ok: false, message: '현재 공유채팅 링크로는 WORK 서버 배포 상태를 확인할 수 없습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: (await readWorkServerDeploymentState()) ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/server-commands/work-server/actions/deploy', async (request, reply) => {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
if (accessContext.scope !== 'full' && !accessContext.allowedKeys.has('work-server')) {
|
||||
reply.status(403);
|
||||
return { ok: false, message: '현재 공유채팅 링크로는 WORK 서버를 배포할 수 없습니다.' };
|
||||
}
|
||||
|
||||
const result = await deployWorkServerCommand();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: result.server,
|
||||
commandOutput: result.commandOutput,
|
||||
restartState: result.restartState,
|
||||
deployment: result.deployment ?? result.server.deployment ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
app.get('/api/server-commands/test/deployment', async (request, reply) => {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
if (accessContext.scope !== 'full' && !accessContext.allowedKeys.has('test')) {
|
||||
reply.status(403);
|
||||
return { ok: false, message: '현재 공유채팅 링크로는 TEST 서버 배포 상태를 확인할 수 없습니다.' };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: (await readTestServerDeploymentState()) ?? null,
|
||||
};
|
||||
});
|
||||
|
||||
app.post('/api/server-commands/test/actions/deploy', async (request, reply) => {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
sendAccessDenied(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
if (accessContext.scope !== 'full' && !accessContext.allowedKeys.has('test')) {
|
||||
reply.status(403);
|
||||
return { ok: false, message: '현재 공유채팅 링크로는 TEST 서버를 배포할 수 없습니다.' };
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await deployTestServerCommand();
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
item: result.server,
|
||||
commandOutput: result.commandOutput,
|
||||
restartState: result.restartState,
|
||||
testDeployment: result.testDeployment ?? null,
|
||||
};
|
||||
} catch (error) {
|
||||
const statusCode = error && typeof error === 'object' && 'statusCode' in error ? Number((error as { statusCode?: unknown }).statusCode) : null;
|
||||
|
||||
if (statusCode === 409) {
|
||||
reply.status(409);
|
||||
return { ok: false, message: error instanceof Error ? error.message : 'TEST 서버 배포가 이미 진행 중입니다.' };
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/server-commands/restart-reservation', async (request, reply) => {
|
||||
const accessContext = await resolveServerCommandAccessContext(request);
|
||||
if (!accessContext) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
sharedResourceTokenSchema,
|
||||
upsertSharedResourceToken,
|
||||
} from '../services/shared-resource-token-service.js';
|
||||
import { extractRequestAuditContext } from '../utils/request-audit.js';
|
||||
|
||||
function getRequestAccessToken(request: { headers: Record<string, string | string[] | undefined> }) {
|
||||
const tokenHeader = request.headers['x-access-token'];
|
||||
@@ -142,7 +143,10 @@ export async function registerSharedResourceTokenRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
const saved = await upsertSharedResourceToken(payload);
|
||||
const saved = await upsertSharedResourceToken(payload, {
|
||||
actorLabel: accessContext.scope === 'full' ? 'manager' : 'shared-link-manager',
|
||||
audit: extractRequestAuditContext(request),
|
||||
});
|
||||
return {
|
||||
ok: true,
|
||||
...saved,
|
||||
@@ -174,7 +178,10 @@ export async function registerSharedResourceTokenRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
const result = await revokeSharedResourceTokens(payload.tokenIds, payload.reason);
|
||||
const result = await revokeSharedResourceTokens(payload.tokenIds, payload.reason, {
|
||||
actorLabel: accessContext.scope === 'full' ? 'manager' : 'shared-link-manager',
|
||||
audit: extractRequestAuditContext(request),
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -201,7 +208,10 @@ export async function registerSharedResourceTokenRoutes(app: FastifyInstance) {
|
||||
reason: z.string().trim().max(500).optional().nullable(),
|
||||
})
|
||||
.parse(request.body ?? {});
|
||||
const saved = await revokeSharedResourceToken(tokenId, payload.reason);
|
||||
const saved = await revokeSharedResourceToken(tokenId, payload.reason, {
|
||||
actorLabel: accessContext.scope === 'full' ? 'manager' : 'shared-link-manager',
|
||||
audit: extractRequestAuditContext(request),
|
||||
});
|
||||
|
||||
if (!saved) {
|
||||
return reply.code(404).send({
|
||||
@@ -229,7 +239,10 @@ export async function registerSharedResourceTokenRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
const saved = await restoreSharedResourceToken(tokenId);
|
||||
const saved = await restoreSharedResourceToken(tokenId, {
|
||||
actorLabel: accessContext.scope === 'full' ? 'manager' : 'shared-link-manager',
|
||||
audit: extractRequestAuditContext(request),
|
||||
});
|
||||
|
||||
if (!saved) {
|
||||
return reply.code(404).send({
|
||||
@@ -265,7 +278,10 @@ export async function registerSharedResourceTokenRoutes(app: FastifyInstance) {
|
||||
usageDelta: z.number().int().min(1).max(1_000_000).optional().nullable(),
|
||||
})
|
||||
.parse(request.body ?? {});
|
||||
const saved = await recordSharedResourceTokenUsage(tokenId, payload);
|
||||
const saved = await recordSharedResourceTokenUsage(tokenId, {
|
||||
...payload,
|
||||
audit: extractRequestAuditContext(request),
|
||||
});
|
||||
|
||||
if (!saved) {
|
||||
return reply.code(404).send({
|
||||
@@ -298,7 +314,10 @@ export async function registerSharedResourceTokenRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
const result = await deleteSharedResourceTokens(payload.tokenIds);
|
||||
const result = await deleteSharedResourceTokens(payload.tokenIds, {
|
||||
actorLabel: accessContext.scope === 'full' ? 'manager' : 'shared-link-manager',
|
||||
audit: extractRequestAuditContext(request),
|
||||
});
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
@@ -320,7 +339,10 @@ export async function registerSharedResourceTokenRoutes(app: FastifyInstance) {
|
||||
});
|
||||
}
|
||||
|
||||
const deleted = await deleteSharedResourceToken(tokenId);
|
||||
const deleted = await deleteSharedResourceToken(tokenId, {
|
||||
actorLabel: accessContext.scope === 'full' ? 'manager' : 'shared-link-manager',
|
||||
audit: extractRequestAuditContext(request),
|
||||
});
|
||||
|
||||
if (!deleted) {
|
||||
return reply.code(404).send({
|
||||
|
||||
Reference in New Issue
Block a user