From 1cd3a15a5d98187ebbbc83afe6fdb91a8074afe6 Mon Sep 17 00:00:00 2001 From: kris Date: Mon, 30 Mar 2026 01:33:06 +0800 Subject: [PATCH] fix: tighten dispatch retry validation --- src/lib/boss-data.ts | 149 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 118 insertions(+), 31 deletions(-) diff --git a/src/lib/boss-data.ts b/src/lib/boss-data.ts index 1b0e4e5..3ac3a83 100644 --- a/src/lib/boss-data.ts +++ b/src/lib/boss-data.ts @@ -1538,6 +1538,16 @@ function trimToDefined(value?: string) { return trimmed ? trimmed : undefined; } +function normalizeStringSet(values: string[]) { + return dedupeStrings(values.map((value) => value.trim()).filter(Boolean)).sort((a, b) => a.localeCompare(b)); +} + +function sameStringSet(a: string[] | undefined, b: string[] | undefined) { + const left = normalizeStringSet(a ?? []); + const right = normalizeStringSet(b ?? []); + return left.length === right.length && left.every((value, index) => value === right[index]); +} + function normalizedDispatchPlanTarget( raw: Partial, fallback?: DispatchPlanTarget, @@ -1576,6 +1586,86 @@ function normalizedDispatchPlanTarget( }; } +function activeSessionForAccount(state: BossState, account: string) { + return ( + state.authSessions.find( + (session) => + session.account === account && + !session.revokedAt && + new Date(session.expiresAt).getTime() > Date.now(), + ) ?? null + ); +} + +function requireDispatchActorSession(state: BossState, account: string) { + const normalizedAccount = account.trim(); + if (!normalizedAccount) { + throw new Error("DISPATCH_ACTOR_ACCOUNT_REQUIRED"); + } + const authAccount = state.authAccounts.find((item) => item.account === normalizedAccount); + if (!authAccount) { + throw new Error("DISPATCH_ACTOR_ACCOUNT_NOT_FOUND"); + } + const session = activeSessionForAccount(state, normalizedAccount); + if (!session) { + throw new Error("DISPATCH_ACTOR_SESSION_REQUIRED"); + } + return { authAccount, session }; +} + +function validateDispatchTargetAgainstState( + state: BossState, + target: DispatchPlanTarget, +): DispatchPlanTarget { + const project = state.projects.find((item) => item.id === target.projectId); + if (!project) { + throw new Error("DISPATCH_TARGET_PROJECT_NOT_FOUND"); + } + + const device = state.devices.find((item) => item.id === target.deviceId); + if (!device || device.source !== "production") { + throw new Error("DISPATCH_TARGET_DEVICE_INVALID"); + } + + if (!project.deviceIds.includes(device.id)) { + throw new Error("DISPATCH_TARGET_DEVICE_PROJECT_MISMATCH"); + } + + const matchingGroupMember = project.groupMembers.find( + (member) => member.deviceId === device.id && member.threadId === target.threadId, + ); + if (project.isGroup) { + if (!matchingGroupMember) { + throw new Error("DISPATCH_TARGET_THREAD_MISMATCH"); + } + if (matchingGroupMember.threadDisplayName !== target.threadDisplayName) { + throw new Error("DISPATCH_TARGET_THREAD_DISPLAY_NAME_MISMATCH"); + } + if (matchingGroupMember.folderName !== target.folderName) { + throw new Error("DISPATCH_TARGET_FOLDER_NAME_MISMATCH"); + } + } else { + if (project.threadMeta.threadId !== target.threadId) { + throw new Error("DISPATCH_TARGET_THREAD_MISMATCH"); + } + if (project.threadMeta.threadDisplayName !== target.threadDisplayName) { + throw new Error("DISPATCH_TARGET_THREAD_DISPLAY_NAME_MISMATCH"); + } + if (project.threadMeta.folderName !== target.folderName) { + throw new Error("DISPATCH_TARGET_FOLDER_NAME_MISMATCH"); + } + } + + if (target.codexThreadRef && project.threadMeta.codexThreadRef && target.codexThreadRef !== project.threadMeta.codexThreadRef) { + throw new Error("DISPATCH_TARGET_CODEX_THREAD_MISMATCH"); + } + if (target.codexFolderRef && project.threadMeta.codexFolderRef && target.codexFolderRef !== project.threadMeta.codexFolderRef) { + throw new Error("DISPATCH_TARGET_CODEX_FOLDER_MISMATCH"); + } + + return target; +} + function normalizeDispatchPlan(raw: Partial, fallback?: DispatchPlan): DispatchPlan { const fallbackTargets = fallback?.targets ?? []; const targets = ensureArray(raw.targets as Partial[] | undefined, fallbackTargets) @@ -1598,17 +1688,12 @@ function normalizeDispatchPlan(raw: Partial, fallback?: DispatchPl createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(), confirmedAt: raw.confirmedAt ?? fallback?.confirmedAt, confirmedBy: raw.confirmedBy ?? fallback?.confirmedBy, - confirmedTargetProjectIds: - ensureArray( - raw.confirmedTargetProjectIds, - fallback?.confirmedTargetProjectIds ?? [], - ).map((projectId) => projectId.trim()).filter(Boolean).length > 0 - ? dedupeStrings( - ensureArray(raw.confirmedTargetProjectIds, fallback?.confirmedTargetProjectIds ?? []) - .map((projectId) => projectId.trim()) - .filter(Boolean), - ) - : undefined, + confirmedTargetProjectIds: (() => { + const values = normalizeStringSet( + ensureArray(raw.confirmedTargetProjectIds, fallback?.confirmedTargetProjectIds ?? []), + ); + return values.length > 0 ? values : undefined; + })(), }; } @@ -3707,6 +3792,7 @@ export async function createDispatchPlan(input: { normalizedDispatchPlanTarget(target, undefined, { allowInvalid: false }), ) as DispatchPlanTarget[]; if (targets.length === 0) throw new Error("DISPATCH_PLAN_TARGETS_REQUIRED"); + const validatedTargets = targets.map((target) => validateDispatchTargetAgainstState(state, target)); const plan: DispatchPlan = { planId: randomToken("dispatch-plan"), @@ -3714,7 +3800,7 @@ export async function createDispatchPlan(input: { requestMessageId, requestedBy, status: "pending_user_confirmation", - targets, + targets: validatedTargets, summary: input.summary?.trim() ?? "", createdAt: nowIso(), }; @@ -3742,25 +3828,19 @@ export async function confirmDispatchPlan(input: { if (plan.status === "rejected") throw new Error("DISPATCH_PLAN_REJECTED"); const confirmedBy = input.confirmedBy.trim(); if (!confirmedBy) throw new Error("DISPATCH_PLAN_CONFIRMED_BY_REQUIRED"); - const approvedTargetProjectIds = dedupeStrings( - input.approvedTargetProjectIds.map((projectId) => projectId.trim()), - ); + requireDispatchActorSession(state, confirmedBy); + const approvedTargetProjectIds = normalizeStringSet(input.approvedTargetProjectIds); if (approvedTargetProjectIds.length === 0) { throw new Error("DISPATCH_PLAN_APPROVED_TARGETS_REQUIRED"); } - const canonicalTargetProjectIds = dedupeStrings(plan.targets.map((target) => target.projectId)); - if ( - approvedTargetProjectIds.some((projectId) => !canonicalTargetProjectIds.includes(projectId)) - ) { + const canonicalTargetProjectIds = normalizeStringSet(plan.targets.map((target) => target.projectId)); + if (approvedTargetProjectIds.some((projectId) => !canonicalTargetProjectIds.includes(projectId))) { throw new Error("DISPATCH_PLAN_APPROVED_TARGETS_INVALID"); } if (plan.confirmedBy && plan.confirmedBy !== confirmedBy) { throw new Error("DISPATCH_PLAN_CONFIRMED_BY_MISMATCH"); } - if ( - plan.confirmedTargetProjectIds?.length && - plan.confirmedTargetProjectIds.join("\u0000") !== approvedTargetProjectIds.join("\u0000") - ) { + if (plan.confirmedTargetProjectIds?.length && !sameStringSet(plan.confirmedTargetProjectIds, approvedTargetProjectIds)) { throw new Error("DISPATCH_PLAN_APPROVED_TARGETS_MISMATCH"); } @@ -3778,7 +3858,6 @@ export async function confirmDispatchPlan(input: { export async function createDispatchExecutionsFromPlan(input: { planId: string; - approvedTargetProjectIds?: string[]; confirmedBy: string; }) { return mutateState((state) => { @@ -3787,6 +3866,7 @@ export async function createDispatchExecutionsFromPlan(input: { if (plan.status === "rejected") throw new Error("DISPATCH_PLAN_REJECTED"); const confirmedBy = input.confirmedBy.trim(); if (!confirmedBy) throw new Error("DISPATCH_PLAN_CONFIRMED_BY_REQUIRED"); + requireDispatchActorSession(state, confirmedBy); if (!plan.confirmedAt || !plan.confirmedBy || !plan.confirmedTargetProjectIds?.length) { throw new Error("DISPATCH_PLAN_NOT_CONFIRMED"); } @@ -3794,13 +3874,11 @@ export async function createDispatchExecutionsFromPlan(input: { throw new Error("DISPATCH_PLAN_CONFIRMED_BY_MISMATCH"); } - const canonicalTargetProjectIds = plan.confirmedTargetProjectIds; + const canonicalTargetProjectIds = normalizeStringSet(plan.confirmedTargetProjectIds); const existingExecutions = state.dispatchExecutions.filter((item) => item.planId === plan.planId); if (existingExecutions.length > 0) { - const existingTargetIds = dedupeStrings( - existingExecutions.map((execution) => execution.targetProjectId), - ); - if (existingTargetIds.join("\u0000") !== canonicalTargetProjectIds.join("\u0000")) { + const existingTargetIds = normalizeStringSet(existingExecutions.map((execution) => execution.targetProjectId)); + if (!sameStringSet(existingTargetIds, canonicalTargetProjectIds)) { throw new Error("DISPATCH_EXECUTION_SET_MISMATCH"); } if (plan.status !== "dispatched") { @@ -3837,19 +3915,28 @@ export async function createDispatchExecutionsFromPlan(input: { export async function completeDispatchExecution(payload: { executionId: string; - deviceId: string; + completedByDeviceId: string; + completedByDeviceToken: string; status: "completed" | "failed"; resultMessageId?: string; }) { return mutateState((state) => { const execution = state.dispatchExecutions.find((item) => item.executionId === payload.executionId); if (!execution) throw new Error("DISPATCH_EXECUTION_NOT_FOUND"); - const deviceId = payload.deviceId.trim(); + const deviceId = payload.completedByDeviceId.trim(); if (!deviceId) throw new Error("DISPATCH_EXECUTION_DEVICE_REQUIRED"); if (execution.deviceId !== deviceId) { throw new Error("DISPATCH_EXECUTION_DEVICE_MISMATCH"); } + const device = state.devices.find((item) => item.id === deviceId); + if (!device || device.source !== "production") { + throw new Error("DISPATCH_EXECUTION_DEVICE_INVALID"); + } + if (!device.token || device.token !== payload.completedByDeviceToken.trim()) { + throw new Error("DISPATCH_EXECUTION_DEVICE_TOKEN_INVALID"); + } + const nextResultMessageId = payload.resultMessageId?.trim() || undefined; if (execution.status === "completed" || execution.status === "failed") { const sameStatus = execution.status === payload.status;