fix: align legacy general access call boundaries

This commit is contained in:
2026-07-31 07:33:26 +00:00
parent 821da19731
commit 98a3cf514f
34 changed files with 909 additions and 177 deletions
+9 -4
View File
@@ -4,7 +4,13 @@ import { z } from 'zod';
import { asRecord } from '@sammo-ts/common';
import { getDexLevel } from '@sammo-ts/logic';
import { authedProcedure, readOnlyAuthedProcedure, router } from '../../trpc.js';
import {
accessAuthedInputProcedure,
accessReadOnlyAuthedInputProcedure,
authedProcedure,
readOnlyAuthedProcedure,
router,
} from '../../trpc.js';
import { buildBattleSimEnvironment, buildBattleSimJobPayload } from '../../battleSim/environment.js';
import { zBattleSimJobId, zBattleSimRequest } from '../../battleSim/schema.js';
import {
@@ -56,7 +62,7 @@ const resolveDexValue = (meta: Record<string, unknown>, key: string): number =>
};
export const battleRouter = router({
simulate: readOnlyAuthedProcedure.input(zBattleSimRequest).mutation(async ({ ctx, input }) => {
simulate: accessReadOnlyAuthedInputProcedure(zBattleSimRequest).mutation(async ({ ctx, input }) => {
const worldState = await ctx.db.worldState.findFirst();
if (!worldState) {
throw new TRPCError({
@@ -169,8 +175,7 @@ export const battleRouter = router({
generalsByNation,
};
}),
getGeneralDetail: authedProcedure
.input(
getGeneralDetail: accessAuthedInputProcedure(
z.object({
generalId: z.number().int().positive(),
})
+2 -3
View File
@@ -2,7 +2,7 @@ import { TRPCError } from '@trpc/server';
import { GamePrisma } from '@sammo-ts/infra';
import { z } from 'zod';
import { authedProcedure, router } from '../../trpc.js';
import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
import { appendInheritanceLog, readInheritancePoint, setInheritancePoint } from '../../services/inheritance.js';
import { getMyGeneral } from '../shared/general.js';
@@ -30,8 +30,7 @@ const loadWorldDate = async (db: Parameters<typeof getMyGeneral>[0]['db']) => {
};
export const bettingRouter = router({
getList: authedProcedure
.input(z.object({ req: z.literal('bettingNation').optional() }).optional())
getList: accessAuthedInputProcedure(z.object({ req: z.literal('bettingNation').optional() }).optional())
.query(async ({ ctx, input }) => {
requireUserId(ctx.auth);
await getMyGeneral(ctx);
+4 -6
View File
@@ -5,7 +5,7 @@ import { promises as fs } from 'fs';
import { randomUUID } from 'crypto';
import sharp, { type WebpOptions } from 'sharp';
import { authedProcedure, router } from '../../trpc.js';
import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
import { getMyGeneral } from '../shared/general.js';
import { resolveSecretPermission } from '../shared/secretPermission.js';
@@ -107,7 +107,7 @@ export const boardRouter = router({
canSecret: permission >= 2,
};
}),
getArticles: authedProcedure.input(z.object({ isSecret: z.boolean() })).query(async ({ ctx, input }) => {
getArticles: accessAuthedInputProcedure(z.object({ isSecret: z.boolean() })).query(async ({ ctx, input }) => {
const { general, permission } = await getBoardActor(ctx);
assertBoardAccess(permission, input.isSecret);
@@ -159,8 +159,7 @@ export const boardRouter = router({
})),
}));
}),
writeArticle: authedProcedure
.input(
writeArticle: accessAuthedInputProcedure(
z.object({
isSecret: z.boolean(),
title: z.string().trim().max(250),
@@ -189,8 +188,7 @@ export const boardRouter = router({
return { id: post.id };
}),
writeComment: authedProcedure
.input(
writeComment: accessAuthedInputProcedure(
z.object({
postId: z.number().int().positive(),
content: z.string().trim().max(2000),
+6 -10
View File
@@ -4,7 +4,7 @@ import { z } from 'zod';
import { asRecord } from '@sammo-ts/common';
import type { GamePrisma } from '@sammo-ts/infra';
import { authedProcedure, router } from '../../trpc.js';
import { accessAuthedInputProcedure, accessAuthedProcedure, router } from '../../trpc.js';
import { getMyGeneral } from '../shared/general.js';
import { assertNationAccess, resolveNationPermission } from '../nation/shared.js';
@@ -28,7 +28,7 @@ const mapLetterState = (state: string): 'PROPOSED' | 'ACTIVATED' | 'CANCELLED' |
};
export const diplomacyRouter = router({
getLetters: authedProcedure.query(async ({ ctx }) => {
getLetters: accessAuthedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
@@ -98,8 +98,7 @@ export const diplomacyRouter = router({
permission,
};
}),
sendLetter: authedProcedure
.input(
sendLetter: accessAuthedInputProcedure(
z.object({
destNationId: z.number().int().positive(),
prevId: z.number().int().positive().nullable().optional(),
@@ -217,8 +216,7 @@ export const diplomacyRouter = router({
return { id: created.id };
}),
respondLetter: authedProcedure
.input(
respondLetter: accessAuthedInputProcedure(
z.object({
letterId: z.number().int().positive(),
agree: z.boolean(),
@@ -288,8 +286,7 @@ export const diplomacyRouter = router({
return { ok: true };
}),
rollbackLetter: authedProcedure
.input(z.object({ letterId: z.number().int().positive() }))
rollbackLetter: accessAuthedInputProcedure(z.object({ letterId: z.number().int().positive() }))
.mutation(async ({ ctx, input }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
@@ -324,8 +321,7 @@ export const diplomacyRouter = router({
return { ok: true };
}),
destroyLetter: authedProcedure
.input(z.object({ letterId: z.number().int().positive() }))
destroyLetter: accessAuthedInputProcedure(z.object({ letterId: z.number().int().positive() }))
.mutation(async ({ ctx, input }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
+14 -10
View File
@@ -5,7 +5,14 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
import { asRecord } from '@sammo-ts/common';
import type { GameApiContext } from '../../context.js';
import { authedProcedure, engineAuthedProcedure, router } from '../../trpc.js';
import {
accessAuthedProcedure,
accessAuthedInputProcedure,
accessEngineAuthedProcedure,
accessEngineAuthedInputProcedure,
authedProcedure,
router,
} from '../../trpc.js';
import { ConflictingTurnDaemonCommandError } from '../../daemon/databaseTransport.js';
import { resolveAccessWindows } from '../../services/generalAccess.js';
import { getMyGeneral } from '../shared/general.js';
@@ -283,7 +290,7 @@ export const generalRouter = router({
penalties,
};
}),
ensureDieOnPrestartStatus: engineAuthedProcedure.mutation(async ({ ctx }) => {
ensureDieOnPrestartStatus: accessEngineAuthedProcedure.mutation(async ({ ctx }) => {
const userId = ctx.auth?.user.id;
if (!userId) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
@@ -319,14 +326,11 @@ export const generalRouter = router({
availableAt: result.availableAt ?? null,
};
}),
dieOnPrestart: engineAuthedProcedure
.input(zImmediateActionInput)
dieOnPrestart: accessEngineAuthedInputProcedure(zImmediateActionInput)
.mutation(({ ctx, input }) => requestImmediateAction(ctx, input, 'dieOnPrestart')),
buildNationCandidate: engineAuthedProcedure
.input(zImmediateActionInput)
buildNationCandidate: accessEngineAuthedInputProcedure(zImmediateActionInput)
.mutation(({ ctx, input }) => requestImmediateAction(ctx, input, 'buildNationCandidate')),
instantRetreat: engineAuthedProcedure
.input(zImmediateActionInput)
instantRetreat: accessEngineAuthedInputProcedure(zImmediateActionInput)
.mutation(({ ctx, input }) => requestImmediateAction(ctx, input, 'instantRetreat')),
vacation: authedProcedure.mutation(async ({ ctx }) => {
const general = await getMyGeneral(ctx);
@@ -342,7 +346,7 @@ export const generalRouter = router({
}
return { ok: true };
}),
setMySetting: authedProcedure.input(zGeneralSettings).mutation(async ({ ctx, input }) => {
setMySetting: accessAuthedInputProcedure(zGeneralSettings).mutation(async ({ ctx, input }) => {
const general = await getMyGeneral(ctx);
const result = await ctx.turnDaemon.requestCommand({
type: 'setMySetting',
@@ -459,7 +463,7 @@ export const generalRouter = router({
history: trimRecentRecords(history, input.lastWorldHistoryId),
};
}),
getFrontStatus: authedProcedure.query(async ({ ctx }) => {
getFrontStatus: accessAuthedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
const worldState = await ctx.db.worldState.findFirst({
orderBy: { id: 'asc' },
+2 -3
View File
@@ -4,7 +4,7 @@ import { asRecord } from '@sammo-ts/common';
import type { UserSanctions } from '@sammo-ts/common/auth/gameToken';
import { isMessageAccessBlocked } from '@sammo-ts/common/auth/sanctions';
import { authedProcedure, router } from '../../trpc.js';
import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
import {
MESSAGE_MAILBOX_NATIONAL_BASE,
MESSAGE_MAILBOX_PUBLIC,
@@ -388,8 +388,7 @@ export const messagesRouter = router({
...messageBuckets,
};
}),
send: authedProcedure
.input(
send: accessAuthedInputProcedure(
z.object({
generalId: z.number().int().positive(),
mailbox: z.number().int(),
@@ -2,11 +2,11 @@ import { TRPCError } from '@trpc/server';
import { LogCategory } from '@sammo-ts/infra';
import { authedProcedure } from '../../../trpc.js';
import { accessAuthedProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js';
import { assertNationAccess, formatDateTime, resolveNationPermission } from '../shared.js';
export const getBattleCenter = authedProcedure.query(async ({ ctx }) => {
export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
@@ -1,12 +1,12 @@
import { TRPCError } from '@trpc/server';
import { authedProcedure } from '../../../trpc.js';
import { accessAuthedProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js';
import { resolveSecretPermission } from '../../shared/secretPermission.js';
import { MAX_NATION_TURNS, getNationTurnSnapshot } from '../../../turns/reservedTurns.js';
import { assertNationAccess } from '../shared.js';
export const getChiefCenter = authedProcedure.query(async ({ ctx }) => {
export const getChiefCenter = accessAuthedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
@@ -1,6 +1,6 @@
import { TRPCError } from '@trpc/server';
import { authedProcedure } from '../../../trpc.js';
import { accessAuthedProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js';
import {
assertNationAccess,
@@ -18,7 +18,7 @@ const experienceLevel = (experience: number): number =>
const dedicationLevel = (dedication: number): number =>
Math.max(0, Math.min(10, Math.ceil(Math.sqrt(dedication) / 10)));
export const getGeneralList = authedProcedure.query(async ({ ctx }) => {
export const getGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
const general = await getMyGeneral(ctx);
assertNationAccess(general);
@@ -2,11 +2,11 @@ import { TRPCError } from '@trpc/server';
import { asRecord } from '@sammo-ts/common';
import { authedProcedure } from '../../../trpc.js';
import { accessAuthedProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js';
import { assertNationAccess, checkSecretMaxPermission, mapGeneralList, resolveChiefStatMin } from '../shared.js';
export const getPersonnelInfo = authedProcedure.query(async ({ ctx }) => {
export const getPersonnelInfo = accessAuthedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
@@ -2,7 +2,7 @@ import { TRPCError } from '@trpc/server';
import { asRecord } from '@sammo-ts/common';
import { authedProcedure } from '../../../trpc.js';
import { accessAuthedProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js';
import { assertNationAccess, resolveNationPermission } from '../shared.js';
@@ -25,7 +25,7 @@ const leadershipBonus = (officerLevel: number, nationLevel: number): number =>
const defenceTrainText = (value: number): string =>
value === 999 ? '×' : value >= 90 ? '☆' : value >= 80 ? '◎' : value >= 60 ? '○' : '△';
export const getSecretGeneralList = authedProcedure.query(async ({ ctx }) => {
export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
const nation = await ctx.db.nation.findUnique({
@@ -3,7 +3,7 @@ import { TRPCError } from '@trpc/server';
import { asRecord } from '@sammo-ts/common';
import { getGoldIncome, getOutcome, getRiceIncome, getWallIncome, getWarGoldIncome, type NationIncomeContext } from '@sammo-ts/logic';
import { authedProcedure } from '../../../trpc.js';
import { accessAuthedProcedure } from '../../../trpc.js';
import { getMyGeneral } from '../../shared/general.js';
import {
assertNationAccess,
@@ -29,7 +29,7 @@ import {
type NationStratRow,
} from '../shared.js';
export const getStratFinan = authedProcedure.query(async ({ ctx }) => {
export const getStratFinan = accessAuthedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
+4 -4
View File
@@ -4,7 +4,7 @@ import { z } from 'zod';
import { asRecord, isRecord } from '@sammo-ts/common';
import { findCrewTypeById, getTechCost } from '@sammo-ts/logic/world/unitSet.js';
import { authedProcedure, router } from '../../trpc.js';
import { accessAuthedInputProcedure, authedProcedure, router } from '../../trpc.js';
import { loadUnitSetDefinitionByName } from '../../battleSim/unitSetLoader.js';
import type { GameApiContext } from '../../context.js';
import { getMyGeneral } from '../shared/general.js';
@@ -537,7 +537,7 @@ export const npcRouter = router({
permissionLevel,
};
}),
setNationPolicy: authedProcedure.input(z.record(z.string(), z.unknown())).mutation(async ({ ctx, input }) => {
setNationPolicy: accessAuthedInputProcedure(z.record(z.string(), z.unknown())).mutation(async ({ ctx, input }) => {
const general = await getMyGeneral(ctx);
if (general.nationId <= 0) {
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'Nation membership required.' });
@@ -700,7 +700,7 @@ export const npcRouter = router({
return { ok: true };
}),
setNationPriority: authedProcedure.input(z.array(z.string())).mutation(async ({ ctx, input }) => {
setNationPriority: accessAuthedInputProcedure(z.array(z.string())).mutation(async ({ ctx, input }) => {
const general = await getMyGeneral(ctx);
if (general.nationId <= 0) {
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'Nation membership required.' });
@@ -753,7 +753,7 @@ export const npcRouter = router({
return { ok: true };
}),
setGeneralPriority: authedProcedure.input(z.array(z.string())).mutation(async ({ ctx, input }) => {
setGeneralPriority: accessAuthedInputProcedure(z.array(z.string())).mutation(async ({ ctx, input }) => {
const general = await getMyGeneral(ctx);
if (general.nationId <= 0) {
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'Nation membership required.' });
+2 -3
View File
@@ -8,7 +8,7 @@ import { zWorldStateConfig, zWorldStateMeta } from '../../context.js';
import { loadMapLayout } from '../../maps/mapLayout.js';
import { loadPublicMap } from '../../maps/worldMap.js';
import { accessPages, recordGeneralAccess } from '../../services/generalAccess.js';
import { procedure, router, sessionActivityProcedure } from '../../trpc.js';
import { accessInputProcedure, procedure, router, sessionActivityProcedure } from '../../trpc.js';
import { loadTraitNames } from '../nation/shared.js';
type WorldTrendSnapshot = {
@@ -477,8 +477,7 @@ export const publicRouter = router({
intelligence: general.intel,
}));
}),
getNpcList: procedure
.input(
getNpcList: accessInputProcedure(
z
.object({
sort: z.number().int().min(1).max(8).catch(1).optional(),
+3 -5
View File
@@ -6,7 +6,7 @@ import type { ItemModule } from '@sammo-ts/logic/items/types.js';
import { buildLegacyDefaultUniqueItemPool } from '@sammo-ts/logic/rewards/legacyUniqueItemPool.js';
import { resolveUniqueConfig } from '@sammo-ts/logic/rewards/uniqueLottery.js';
import { authedProcedure, procedure, router } from '../../trpc.js';
import { accessAuthedInputProcedure, accessInputProcedure, procedure, router } from '../../trpc.js';
const DEFAULT_BG_COLOR = '#330000';
const DEFAULT_FG_COLOR = '#ffffff';
@@ -81,8 +81,7 @@ const loadUniqueItems = () => {
};
export const rankingRouter = router({
getBestGeneral: authedProcedure
.input(
getBestGeneral: accessAuthedInputProcedure(
z
.object({
view: z.enum(['user', 'npc']).optional(),
@@ -369,8 +368,7 @@ export const rankingRouter = router({
}
return Array.from(seasonMap.values());
}),
getHallOfFame: procedure
.input(
getHallOfFame: accessInputProcedure(
z.object({
season: z.number().int(),
scenario: z.number().int().optional(),
+2 -2
View File
@@ -7,7 +7,7 @@ import type { TournamentState } from '../../tournament/types.js';
import { TournamentStore } from '../../tournament/store.js';
import { buildTournamentKeys } from '../../tournament/keys.js';
import { authedProcedure, router } from '../../trpc.js';
import { accessAuthedProcedure, authedProcedure, router } from '../../trpc.js';
import { getMyGeneral } from '../shared/general.js';
const hasAdminRole = (roles: string[], profileName: string): boolean => {
@@ -127,7 +127,7 @@ export const tournamentRouter = router({
return store.getState();
}),
getAdminStatus: adminProcedure.query(async () => ({ ok: true })),
getSnapshot: authedProcedure.query(async ({ ctx }) => {
getSnapshot: accessAuthedProcedure.query(async ({ ctx }) => {
await getMyGeneral(ctx);
const store = new TournamentStore(ctx.redis, buildTournamentKeys(ctx.profile.name));
const [state, participants, matches, bets] = await Promise.all([
+2 -2
View File
@@ -4,7 +4,7 @@ import { z } from 'zod';
import type { TurnDaemonCommandResult } from '@sammo-ts/common';
import { isValidTroopNameWidth, normalizeTroopName, resolveTroopSecretPermission } from '@sammo-ts/logic';
import { authedProcedure, router } from '../../trpc.js';
import { accessAuthedProcedure, authedProcedure, router } from '../../trpc.js';
import { getMyGeneral } from '../shared/general.js';
const troopNameSchema = z
@@ -33,7 +33,7 @@ const assertCommandResult = <T extends 'troopCreate' | 'troopJoin' | 'troopExit'
};
export const troopRouter = router({
getList: authedProcedure.query(async ({ ctx }) => {
getList: accessAuthedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
if (me.nationId <= 0) {
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: '국가에 소속되어 있지 않습니다.' });
+2 -3
View File
@@ -1,7 +1,7 @@
import { asRecord } from '@sammo-ts/common';
import { z } from 'zod';
import { authedProcedure } from '../../trpc.js';
import { accessAuthedInputProcedure, authedProcedure } from '../../trpc.js';
import { loadTraitNames } from '../nation/shared.js';
import { getMyGeneral } from '../shared/general.js';
import { resolveSecretPermission } from '../shared/secretPermission.js';
@@ -204,8 +204,7 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
});
});
export const getGeneralDirectory = authedProcedure
.input(z.object({ sort: zDirectorySort }).optional())
export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: zDirectorySort }).optional())
.query(async ({ ctx, input }) => {
await getMyGeneral(ctx);
const sort = input?.sort ?? 9;
+2 -3
View File
@@ -2,8 +2,7 @@ import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import { type WorldStateRow, zWorldStateConfig, zWorldStateMeta } from '../../context.js';
import { procedure, router } from '../../trpc.js';
import { authedProcedure } from '../../trpc.js';
import { accessAuthedProcedure, authedProcedure, procedure, router } from '../../trpc.js';
import { asRecord, isRecord } from '@sammo-ts/common';
import { loadWorldMap } from '../../maps/worldMap.js';
import { loadMapLayout } from '../../maps/mapLayout.js';
@@ -71,7 +70,7 @@ const toWorldStateSnapshot = (row: WorldStateRow) => ({
export const worldRouter = router({
getNationDirectory,
getGeneralDirectory,
getGlobalInfo: authedProcedure.query(async ({ ctx }) => {
getGlobalInfo: accessAuthedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
const [nations, cities, diplomacy, map] = await Promise.all([
ctx.db.nation.findMany({ where: { level: { gt: 0 } } }),
+21
View File
@@ -7,6 +7,10 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
import type { GameApiContext } from '../../context.js';
import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js';
import {
generalAccessEndpointWeights,
recordGeneralAccessWeight,
} from '../../services/generalAccess.js';
import { authedProcedure, router } from '../../trpc.js';
import { getMyGeneral } from '../shared/general.js';
@@ -24,6 +28,13 @@ const zServerId = z.string().trim().min(1).max(64);
const computeHash = (payload: unknown): string => createHash('sha256').update(JSON.stringify(payload)).digest('hex');
const recordHistoryAccess = async (ctx: GameApiContext): Promise<void> => {
if (ctx.generalAccessTracking !== true) {
return;
}
await recordGeneralAccessWeight(ctx, generalAccessEndpointWeights['yearbook.getHistory']);
};
const parseTextArray = (value: unknown): string[] =>
Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];
@@ -257,6 +268,10 @@ export const yearbookRouter = router({
}
const targetProfileName = input.serverID ?? ctx.profile.name;
const isCurrentProfile = targetProfileName === ctx.profile.name;
const shouldRecordAfterHashCheck = isCurrentProfile && Boolean(input.hash);
if (isCurrentProfile && !shouldRecordAfterHashCheck) {
await recordHistoryAccess(ctx);
}
const isCurrent =
isCurrentProfile && worldState.currentYear === input.year && worldState.currentMonth === input.month;
@@ -280,6 +295,9 @@ export const yearbookRouter = router({
if (input.hash && input.hash === hash) {
return { notModified: true, hash };
}
if (shouldRecordAfterHashCheck) {
await recordHistoryAccess(ctx);
}
return { notModified: false, hash, data };
}
@@ -317,6 +335,9 @@ export const yearbookRouter = router({
if (input.hash && input.hash === hash) {
return { notModified: true, hash };
}
if (shouldRecordAfterHashCheck) {
await recordHistoryAccess(ctx);
}
return { notModified: false, hash, data };
}),
});