refactor: short id branded type (#552)

* refactor: short id branded type

* chore: pr feedbacks
This commit is contained in:
Nico 2026-02-21 11:16:15 +01:00 committed by GitHub
parent 45fd9f9de1
commit a0a813ed09
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 193 additions and 161 deletions

View file

@ -10,6 +10,7 @@ import type {
} from "~/schemas/restic";
import type { BackendStatus, BackendType, volumeConfigSchema } from "~/schemas/volumes";
import type { NotificationType, notificationConfigSchema } from "~/schemas/notifications";
import type { ShortId } from "~/server/utils/branded";
/**
* Users Table
@ -179,7 +180,7 @@ export const volumesTable = sqliteTable(
"volumes_table",
{
id: int().primaryKey({ autoIncrement: true }),
shortId: text("short_id").notNull().unique(),
shortId: text("short_id").$type<ShortId>().notNull().unique(),
name: text().notNull(),
type: text().$type<BackendType>().notNull(),
status: text().$type<BackendStatus>().notNull().default("unmounted"),
@ -210,7 +211,7 @@ export type VolumeInsert = typeof volumesTable.$inferInsert;
*/
export const repositoriesTable = sqliteTable("repositories_table", {
id: text().primaryKey(),
shortId: text("short_id").notNull().unique(),
shortId: text("short_id").$type<ShortId>().notNull().unique(),
name: text().notNull(),
type: text().$type<RepositoryBackend>().notNull(),
config: text("config", { mode: "json" }).$type<typeof repositoryConfigSchema.inferOut>().notNull(),
@ -243,7 +244,7 @@ export type RepositoryInsert = typeof repositoriesTable.$inferInsert;
*/
export const backupSchedulesTable = sqliteTable("backup_schedules_table", {
id: int().primaryKey({ autoIncrement: true }),
shortId: text("short_id").notNull().unique(),
shortId: text("short_id").$type<ShortId>().notNull().unique(),
name: text().notNull(),
volumeId: int("volume_id")
.notNull()

View file

@ -16,7 +16,7 @@ export class VolumeAutoRemountJob extends Job {
if (volume.autoRemount) {
try {
await withContext({ organizationId: volume.organizationId }, async () => {
await volumeService.mountVolume(volume.id);
await volumeService.mountVolume(volume.shortId);
});
} catch (err) {
logger.error(`Failed to auto-remount volume ${volume.name}:`, err);

View file

@ -15,12 +15,16 @@ export class VolumeHealthCheckJob extends Job {
});
for (const volume of volumes) {
await withContext({ organizationId: volume.organizationId }, async () => {
const { status } = await volumeService.checkHealth(volume.id);
if (status === "error" && volume.autoRemount) {
await volumeService.mountVolume(volume.id);
}
});
try {
await withContext({ organizationId: volume.organizationId }, async () => {
const { status } = await volumeService.checkHealth(volume.shortId);
if (status === "error" && volume.autoRemount) {
await volumeService.mountVolume(volume.shortId);
}
});
} catch (error) {
logger.error(`Health check failed for volume ${volume.name}:`, error);
}
}
return { done: true, timestamp: new Date() };

View file

@ -15,7 +15,7 @@ export class RepositoryHealthCheckJob extends Job {
for (const repository of repositories) {
try {
await withContext({ organizationId: repository.organizationId }, async () => {
await repositoriesService.checkHealth(repository.id);
await repositoriesService.checkHealth(repository.shortId);
});
} catch (error) {
logger.error(`Health check failed for repository ${repository.name}:`, error);

View file

@ -44,6 +44,7 @@ import { notificationsService } from "../notifications/notifications.service";
import { requireAuth } from "../auth/auth.middleware";
import { backupsExecutionService } from "./backups.execution";
import { logger } from "~/server/utils/logger";
import { asShortId } from "~/server/utils/branded";
export const backupScheduleController = new Hono()
.use(requireAuth)
@ -53,13 +54,13 @@ export const backupScheduleController = new Hono()
return c.json<ListBackupSchedulesResponseDto>(schedules, 200);
})
.get("/:shortId", getBackupScheduleDto, async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
const schedule = await backupsService.getScheduleByShortId(shortId);
return c.json<GetBackupScheduleDto>(schedule, 200);
})
.get("/volume/:volumeShortId", getBackupScheduleForVolumeDto, async (c) => {
const volumeShortId = c.req.param("volumeShortId");
const volumeShortId = asShortId(c.req.param("volumeShortId"));
const schedule = await backupsService.getScheduleForVolume(volumeShortId);
return c.json<GetBackupScheduleForVolumeResponseDto>(schedule, 200);
@ -71,20 +72,20 @@ export const backupScheduleController = new Hono()
return c.json<CreateBackupScheduleDto>(schedule, 201);
})
.patch("/:shortId", updateBackupScheduleDto, validator("json", updateBackupScheduleBody), async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
const body = c.req.valid("json");
const schedule = await backupsService.updateSchedule(shortId, body);
return c.json<UpdateBackupScheduleDto>(schedule, 200);
})
.delete("/:shortId", deleteBackupScheduleDto, async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
await backupsService.deleteSchedule(shortId);
return c.json<DeleteBackupScheduleDto>({ success: true }, 200);
})
.post("/:shortId/run", runBackupNowDto, async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
const schedule = await backupsService.getScheduleByShortId(shortId);
const result = await backupsExecutionService.validateBackupExecution(schedule.id, true);
@ -103,21 +104,21 @@ export const backupScheduleController = new Hono()
return c.json<RunBackupNowDto>({ success: true }, 200);
})
.post("/:shortId/stop", stopBackupDto, async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
const schedule = await backupsService.getScheduleByShortId(shortId);
await backupsExecutionService.stopBackup(schedule.id);
return c.json<StopBackupDto>({ success: true }, 200);
})
.post("/:shortId/forget", runForgetDto, async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
const schedule = await backupsService.getScheduleByShortId(shortId);
await backupsExecutionService.runForget(schedule.id);
return c.json<RunForgetDto>({ success: true }, 200);
})
.get("/:shortId/notifications", getScheduleNotificationsDto, async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
const schedule = await backupsService.getScheduleByShortId(shortId);
const assignments = await notificationsService.getScheduleNotifications(schedule.id);
@ -128,7 +129,7 @@ export const backupScheduleController = new Hono()
updateScheduleNotificationsDto,
validator("json", updateScheduleNotificationsBody),
async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
const schedule = await backupsService.getScheduleByShortId(shortId);
const body = c.req.valid("json");
const assignments = await notificationsService.updateScheduleNotifications(schedule.id, body.assignments);
@ -137,27 +138,27 @@ export const backupScheduleController = new Hono()
},
)
.get("/:shortId/mirrors", getScheduleMirrorsDto, async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
const mirrors = await backupsService.getMirrors(shortId);
return c.json<GetScheduleMirrorsDto>(mirrors, 200);
})
.put("/:shortId/mirrors", updateScheduleMirrorsDto, validator("json", updateScheduleMirrorsBody), async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
const body = c.req.valid("json");
const mirrors = await backupsService.updateMirrors(shortId, body);
return c.json<UpdateScheduleMirrorsDto>(mirrors, 200);
})
.get("/:shortId/mirrors/compatibility", getMirrorCompatibilityDto, async (c) => {
const shortId = c.req.param("shortId");
const shortId = asShortId(c.req.param("shortId"));
const compatibility = await backupsService.getMirrorCompatibility(shortId);
return c.json<GetMirrorCompatibilityDto>(compatibility, 200);
})
.post("/reorder", reorderBackupSchedulesDto, validator("json", reorderBackupSchedulesBody), async (c) => {
const body = c.req.valid("json");
await backupsService.reorderSchedules(body.scheduleShortIds);
await backupsService.reorderSchedules(body.scheduleShortIds.map(asShortId));
return c.json<ReorderBackupSchedulesDto>({ success: true }, 200);
});

View file

@ -9,6 +9,7 @@ import { checkMirrorCompatibility, getIncompatibleMirrorError } from "~/server/u
import { generateShortId } from "~/server/utils/id";
import { getOrganizationId } from "~/server/core/request-context";
import { calculateNextRun } from "./backup.helpers";
import { asShortId, type ShortId } from "~/server/utils/branded";
const listSchedules = async () => {
const organizationId = getOrganizationId();
@ -38,10 +39,10 @@ const getScheduleById = async (scheduleId: number) => {
return schedule;
};
const getScheduleByShortId = async (shortId: string) => {
const getScheduleByShortId = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
const schedule = await db.query.backupSchedulesTable.findFirst({
where: { AND: [{ shortId }, { organizationId }] },
where: { AND: [{ shortId: { eq: shortId } }, { organizationId }] },
with: { volume: true, repository: true },
});
@ -60,7 +61,10 @@ const getScheduleByIdOrShortId = async (idOrShortId: string | number) => {
const organizationId = getOrganizationId();
const schedule = await db.query.backupSchedulesTable.findFirst({
where: {
AND: [{ OR: [{ id: Number(idOrShortId) }, { shortId: String(idOrShortId) }] }, { organizationId }],
AND: [
{ OR: [{ id: Number(idOrShortId) }, { shortId: { eq: asShortId(String(idOrShortId)) } }] },
{ organizationId },
],
},
with: { volume: true, repository: true },
});
@ -94,7 +98,10 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
const volume = await db.query.volumesTable.findFirst({
where: {
AND: [{ OR: [{ id: Number(data.volumeId) }, { shortId: String(data.volumeId) }] }, { organizationId }],
AND: [
{ OR: [{ id: Number(data.volumeId) }, { shortId: { eq: asShortId(String(data.volumeId)) } }] },
{ organizationId },
],
},
});
@ -104,7 +111,7 @@ const createSchedule = async (data: CreateBackupScheduleBody) => {
const repository = await db.query.repositoriesTable.findFirst({
where: {
AND: [{ OR: [{ id: data.repositoryId }, { shortId: data.repositoryId }] }, { organizationId }],
AND: [{ OR: [{ id: data.repositoryId }, { shortId: { eq: asShortId(data.repositoryId) } }] }, { organizationId }],
},
});
@ -145,7 +152,7 @@ const updateSchedule = async (scheduleIdOrShortId: number | string, data: Update
const schedule = await db.query.backupSchedulesTable.findFirst({
where: {
AND: [
{ OR: [{ id: Number(scheduleIdOrShortId) }, { shortId: String(scheduleIdOrShortId) }] },
{ OR: [{ id: Number(scheduleIdOrShortId) }, { shortId: { eq: asShortId(String(scheduleIdOrShortId)) } }] },
{ organizationId },
],
},
@ -173,7 +180,7 @@ const updateSchedule = async (scheduleIdOrShortId: number | string, data: Update
const repository = await db.query.repositoriesTable.findFirst({
where: {
AND: [{ OR: [{ id: data.repositoryId }, { shortId: data.repositoryId }] }, { organizationId }],
AND: [{ OR: [{ id: data.repositoryId }, { shortId: { eq: asShortId(data.repositoryId) } }] }, { organizationId }],
},
});
@ -202,7 +209,7 @@ const deleteSchedule = async (scheduleIdOrShortId: number | string) => {
const schedule = await db.query.backupSchedulesTable.findFirst({
where: {
AND: [
{ OR: [{ id: Number(scheduleIdOrShortId) }, { shortId: String(scheduleIdOrShortId) }] },
{ OR: [{ id: Number(scheduleIdOrShortId) }, { shortId: { eq: asShortId(String(scheduleIdOrShortId)) } }] },
{ organizationId },
],
},
@ -221,7 +228,10 @@ const getScheduleForVolume = async (volumeIdOrShortId: number | string) => {
const organizationId = getOrganizationId();
const volume = await db.query.volumesTable.findFirst({
where: {
AND: [{ OR: [{ id: Number(volumeIdOrShortId) }, { shortId: String(volumeIdOrShortId) }] }, { organizationId }],
AND: [
{ OR: [{ id: Number(volumeIdOrShortId) }, { shortId: { eq: asShortId(String(volumeIdOrShortId)) } }] },
{ organizationId },
],
},
columns: { id: true },
});
@ -249,7 +259,7 @@ const getMirrors = async (scheduleIdOrShortId: number | string) => {
const schedule = await db.query.backupSchedulesTable.findFirst({
where: {
AND: [
{ OR: [{ id: Number(scheduleIdOrShortId) }, { shortId: String(scheduleIdOrShortId) }] },
{ OR: [{ id: Number(scheduleIdOrShortId) }, { shortId: { eq: asShortId(String(scheduleIdOrShortId)) } }] },
{ organizationId },
],
},
@ -284,7 +294,7 @@ const updateMirrors = async (scheduleIdOrShortId: number | string, data: UpdateS
const schedule = await db.query.backupSchedulesTable.findFirst({
where: {
AND: [
{ OR: [{ id: Number(scheduleIdOrShortId) }, { shortId: String(scheduleIdOrShortId) }] },
{ OR: [{ id: Number(scheduleIdOrShortId) }, { shortId: { eq: asShortId(String(scheduleIdOrShortId)) } }] },
{ organizationId },
],
},
@ -299,7 +309,10 @@ const updateMirrors = async (scheduleIdOrShortId: number | string, data: UpdateS
data.mirrors.map(async (mirror) => {
const repo = await db.query.repositoriesTable.findFirst({
where: {
AND: [{ OR: [{ id: mirror.repositoryId }, { shortId: mirror.repositoryId }] }, { organizationId }],
AND: [
{ OR: [{ id: mirror.repositoryId }, { shortId: { eq: asShortId(mirror.repositoryId) } }] },
{ organizationId },
],
},
});
@ -360,7 +373,7 @@ const getMirrorCompatibility = async (scheduleIdOrShortId: number | string) => {
const schedule = await db.query.backupSchedulesTable.findFirst({
where: {
AND: [
{ OR: [{ id: Number(scheduleIdOrShortId) }, { shortId: String(scheduleIdOrShortId) }] },
{ OR: [{ id: Number(scheduleIdOrShortId) }, { shortId: { eq: asShortId(String(scheduleIdOrShortId)) } }] },
{ organizationId },
],
},
@ -381,7 +394,7 @@ const getMirrorCompatibility = async (scheduleIdOrShortId: number | string) => {
return compatibility;
};
const reorderSchedules = async (scheduleShortIds: string[]) => {
const reorderSchedules = async (scheduleShortIds: ShortId[]) => {
const organizationId = getOrganizationId();
const uniqueIds = new Set(scheduleShortIds);
if (uniqueIds.size !== scheduleShortIds.length) {

View file

@ -20,7 +20,7 @@ const ensureLatestConfigurationSchema = async () => {
for (const volume of volumes) {
await withContext({ organizationId: volume.organizationId }, async () => {
await volumeService.updateVolume(volume.id, volume).catch((err) => {
await volumeService.updateVolume(volume.shortId, volume).catch((err) => {
logger.error(`Failed to update volume ${volume.name}: ${err}`);
});
});
@ -30,7 +30,7 @@ const ensureLatestConfigurationSchema = async () => {
for (const repo of repositories) {
await withContext({ organizationId: repo.organizationId }, async () => {
await repositoriesService.updateRepository(repo.id, {}).catch((err) => {
await repositoriesService.updateRepository(repo.shortId, {}).catch((err) => {
logger.error(`Failed to update repository ${repo.name}: ${err}`);
});
});
@ -77,7 +77,7 @@ export const startup = async () => {
for (const volume of volumes) {
await withContext({ organizationId: volume.organizationId }, async () => {
await volumeService.mountVolume(volume.id).catch((err) => {
await volumeService.mountVolume(volume.shortId).catch((err) => {
logger.error(`Error auto-remounting volume ${volume.name} on startup: ${err.message}`);
});
});

View file

@ -56,6 +56,7 @@ import { requireAuth, requireOrgAdmin } from "../auth/auth.middleware";
import { toMessage } from "~/server/utils/errors";
import { requireDevPanel } from "../auth/dev-panel.middleware";
import { getSnapshotDuration } from "../../utils/snapshots";
import { asShortId } from "~/server/utils/branded";
export const repositoriesController = new Hono()
.use(requireAuth)
@ -86,30 +87,31 @@ export const repositoriesController = new Hono()
return c.json(remotes);
})
.get("/:shortId", getRepositoryDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const res = await repositoriesService.getRepository(shortId);
return c.json<GetRepositoryDto>(res.repository, 200);
})
.get("/:shortId/stats", getRepositoryStatsDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const stats = await repositoriesService.getRepositoryStats(shortId);
return c.json<GetRepositoryStatsDto>(stats, 200);
})
.delete("/:shortId", deleteRepositoryDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
await repositoriesService.deleteRepository(shortId);
return c.json<DeleteRepositoryDto>({ message: "Repository deleted" }, 200);
})
.get("/:shortId/snapshots", listSnapshotsDto, validator("query", listSnapshotsFilters), async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const { backupId } = c.req.valid("query");
const backupShortId = backupId ? asShortId(backupId) : undefined;
const [res, retentionCategories] = await Promise.all([
repositoriesService.listSnapshots(shortId, backupId),
repositoriesService.getRetentionCategories(shortId, backupId),
repositoriesService.listSnapshots(shortId, backupShortId),
repositoriesService.getRetentionCategories(shortId, backupShortId),
]);
const snapshots = res.map((snapshot) => {
@ -132,13 +134,14 @@ export const repositoriesController = new Hono()
return c.json<ListSnapshotsDto>(snapshots, 200);
})
.post("/:shortId/snapshots/refresh", refreshSnapshotsDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const result = await repositoriesService.refreshSnapshots(shortId);
return c.json<RefreshSnapshotsDto>(result, 200);
})
.get("/:shortId/snapshots/:snapshotId", getSnapshotDetailsDto, async (c) => {
const { shortId, snapshotId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const snapshotId = c.req.param("snapshotId");
const snapshot = await repositoriesService.getSnapshotDetails(shortId, snapshotId);
const duration = getSnapshotDuration(snapshot.summary);
@ -162,7 +165,8 @@ export const repositoriesController = new Hono()
listSnapshotFilesDto,
validator("query", listSnapshotFilesQuery),
async (c) => {
const { shortId, snapshotId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const snapshotId = c.req.param("snapshotId");
const { path, ...query } = c.req.valid("query");
const decodedPath = path ? decodeURIComponent(path) : undefined;
@ -178,7 +182,8 @@ export const repositoriesController = new Hono()
},
)
.get("/:shortId/snapshots/:snapshotId/dump", dumpSnapshotDto, validator("query", dumpSnapshotQuery), async (c) => {
const { shortId, snapshotId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const snapshotId = c.req.param("snapshotId");
const { path, kind } = c.req.valid("query");
const dumpStream = await repositoriesService.dumpSnapshot(shortId, snapshotId, path, kind);
@ -202,55 +207,56 @@ export const repositoriesController = new Hono()
});
})
.post("/:shortId/restore", restoreSnapshotDto, validator("json", restoreSnapshotBody), async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const { snapshotId, ...options } = c.req.valid("json");
const result = await repositoriesService.restoreSnapshot(shortId, snapshotId, options);
return c.json<RestoreSnapshotDto>(result, 200);
})
.post("/:shortId/doctor", startDoctorDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const result = await repositoriesService.startDoctor(shortId);
return c.json<StartDoctorDto>(result, 202);
})
.delete("/:shortId/doctor", cancelDoctorDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const result = await repositoriesService.cancelDoctor(shortId);
return c.json<CancelDoctorDto>(result, 200);
})
.post("/:shortId/unlock", unlockRepositoryDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const result = await repositoriesService.unlockRepository(shortId);
return c.json<UnlockRepositoryDto>(result, 200);
})
.delete("/:shortId/snapshots/:snapshotId", deleteSnapshotDto, async (c) => {
const { shortId, snapshotId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const snapshotId = c.req.param("snapshotId");
await repositoriesService.deleteSnapshot(shortId, snapshotId);
return c.json<DeleteSnapshotDto>({ message: "Snapshot deleted" }, 200);
})
.delete("/:shortId/snapshots", deleteSnapshotsDto, validator("json", deleteSnapshotsBody), async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const { snapshotIds } = c.req.valid("json");
await repositoriesService.deleteSnapshots(shortId, snapshotIds);
return c.json<DeleteSnapshotsResponseDto>({ message: "Snapshots deleted" }, 200);
})
.post("/:shortId/snapshots/tag", tagSnapshotsDto, validator("json", tagSnapshotsBody), async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const { snapshotIds, ...tags } = c.req.valid("json");
await repositoriesService.tagSnapshots(shortId, snapshotIds, tags);
return c.json<TagSnapshotsResponseDto>({ message: "Snapshots tagged" }, 200);
})
.patch("/:shortId", updateRepositoryDto, validator("json", updateRepositoryBody), async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const body = c.req.valid("json");
const res = await repositoriesService.updateRepository(shortId, body);
@ -263,7 +269,7 @@ export const repositoriesController = new Hono()
devPanelExecDto,
validator("json", devPanelExecBody),
async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const body = c.req.valid("json");
return streamSSE(c, async (stream) => {

View file

@ -28,14 +28,15 @@ import { REPOSITORY_BASE } from "~/server/core/constants";
import { findCommonAncestor } from "~/utils/common-ancestor";
import { prepareSnapshotDump } from "./helpers/dump";
import { executeDoctor } from "./helpers/doctor";
import type { ShortId } from "~/server/utils/branded";
const runningDoctors = new Map<string, AbortController>();
const findRepository = async (shortId: string) => {
const findRepository = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
return await db.query.repositoriesTable.findFirst({
where: {
AND: [{ shortId }, { organizationId }],
AND: [{ shortId: { eq: shortId } }, { organizationId }],
},
});
};
@ -184,7 +185,7 @@ const createRepository = async (name: string, config: RepositoryConfig, compress
throw new InternalServerError(`Failed to initialize repository: ${errorMessage}`);
};
const getRepository = async (shortId: string) => {
const getRepository = async (shortId: ShortId) => {
const repository = await findRepository(shortId);
if (!repository) {
@ -194,7 +195,7 @@ const getRepository = async (shortId: string) => {
return { repository };
};
const getRepositoryStats = async (shortId: string) => {
const getRepositoryStats = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
const repository = await findRepository(shortId);
@ -218,7 +219,7 @@ const getRepositoryStats = async (shortId: string) => {
}
};
const deleteRepository = async (shortId: string) => {
const deleteRepository = async (shortId: ShortId) => {
const repository = await findRepository(shortId);
if (!repository) {
@ -244,7 +245,7 @@ const deleteRepository = async (shortId: string) => {
*
* @returns List of snapshots
*/
const listSnapshots = async (shortId: string, backupId?: string) => {
const listSnapshots = async (shortId: ShortId, backupId?: ShortId) => {
const organizationId = getOrganizationId();
const repository = await findRepository(shortId);
@ -277,7 +278,7 @@ const listSnapshots = async (shortId: string, backupId?: string) => {
};
const listSnapshotFiles = async (
shortId: string,
shortId: ShortId,
snapshotId: string,
path?: string,
options?: { offset?: number; limit?: number },
@ -342,7 +343,7 @@ const listSnapshotFiles = async (
};
const restoreSnapshot = async (
shortId: string,
shortId: ShortId,
snapshotId: string,
options?: {
include?: string[];
@ -414,7 +415,7 @@ const restoreSnapshot = async (
}
};
const dumpSnapshot = async (shortId: string, snapshotId: string, path?: string, kind?: DumpPathKind) => {
const dumpSnapshot = async (shortId: ShortId, snapshotId: string, path?: string, kind?: DumpPathKind) => {
const organizationId = getOrganizationId();
const repository = await findRepository(shortId);
@ -474,7 +475,7 @@ const dumpSnapshot = async (shortId: string, snapshotId: string, path?: string,
}
};
const getSnapshotDetails = async (shortId: string, snapshotId: string) => {
const getSnapshotDetails = async (shortId: ShortId, snapshotId: string) => {
const organizationId = getOrganizationId();
const repository = await findRepository(shortId);
@ -506,9 +507,9 @@ const getSnapshotDetails = async (shortId: string, snapshotId: string) => {
return snapshot;
};
const checkHealth = async (repositoryId: string) => {
const checkHealth = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
const repository = await findRepository(repositoryId);
const repository = await findRepository(shortId);
if (!repository) {
throw new NotFoundError("Repository not found");
@ -535,7 +536,7 @@ const checkHealth = async (repositoryId: string) => {
}
};
const startDoctor = async (shortId: string) => {
const startDoctor = async (shortId: ShortId) => {
const repository = await findRepository(shortId);
if (!repository) {
@ -574,7 +575,7 @@ const startDoctor = async (shortId: string) => {
return { message: "Doctor operation started", repositoryId: repository.shortId };
};
const cancelDoctor = async (shortId: string) => {
const cancelDoctor = async (shortId: ShortId) => {
const repository = await findRepository(shortId);
if (!repository) {
@ -601,7 +602,7 @@ const cancelDoctor = async (shortId: string) => {
return { message: "Doctor operation cancelled" };
};
const deleteSnapshot = async (shortId: string, snapshotId: string) => {
const deleteSnapshot = async (shortId: ShortId, snapshotId: string) => {
const organizationId = getOrganizationId();
const repository = await findRepository(shortId);
@ -618,7 +619,7 @@ const deleteSnapshot = async (shortId: string, snapshotId: string) => {
}
};
const deleteSnapshots = async (shortId: string, snapshotIds: string[]) => {
const deleteSnapshots = async (shortId: ShortId, snapshotIds: string[]) => {
const organizationId = getOrganizationId();
const repository = await findRepository(shortId);
@ -636,7 +637,7 @@ const deleteSnapshots = async (shortId: string, snapshotIds: string[]) => {
};
const tagSnapshots = async (
shortId: string,
shortId: ShortId,
snapshotIds: string[],
tags: { add?: string[]; remove?: string[]; set?: string[] },
) => {
@ -656,7 +657,7 @@ const tagSnapshots = async (
}
};
const refreshSnapshots = async (shortId: string) => {
const refreshSnapshots = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
const repository = await findRepository(shortId);
@ -681,7 +682,7 @@ const refreshSnapshots = async (shortId: string) => {
}
};
const updateRepository = async (shortId: string, updates: UpdateRepositoryBody) => {
const updateRepository = async (shortId: ShortId, updates: UpdateRepositoryBody) => {
const existing = await findRepository(shortId);
if (!existing) {
@ -752,7 +753,7 @@ const updateRepository = async (shortId: string, updates: UpdateRepositoryBody)
return { repository: updated };
};
const unlockRepository = async (shortId: string) => {
const unlockRepository = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
const repository = await findRepository(shortId);
@ -770,7 +771,7 @@ const unlockRepository = async (shortId: string) => {
};
const execResticCommand = async (
shortId: string,
shortId: ShortId,
command: string,
args: string[] | undefined,
onStdout: (line: string) => void,
@ -800,7 +801,7 @@ const execResticCommand = async (
return { exitCode: result.exitCode };
};
const getRetentionCategories = async (repositoryId: string, scheduleId?: string) => {
const getRetentionCategories = async (repositoryId: ShortId, scheduleId?: ShortId) => {
if (!scheduleId) {
return new Map<string, RetentionCategory[]>();
}

View file

@ -8,16 +8,17 @@ import os from "node:os";
import path from "node:path";
import { createTestSession } from "~/test/helpers/auth";
import { withContext } from "~/server/core/request-context";
import { asShortId } from "~/server/utils/branded";
describe("volumeService", () => {
describe("findVolume", () => {
test("should find volume by numeric id", async () => {
test("should find volume by shortId", async () => {
const { organizationId, user } = await createTestSession();
const [volume] = await db
.insert(volumesTable)
.values({
shortId: randomUUID().slice(0, 8),
shortId: asShortId(randomUUID().slice(0, 8)),
name: `test-vol-${randomUUID().slice(0, 8)}`,
type: "directory",
status: "mounted",
@ -28,19 +29,19 @@ describe("volumeService", () => {
.returning();
await withContext({ organizationId, userId: user.id }, async () => {
const result = await volumeService.getVolume(String(volume.id));
const result = await volumeService.getVolume(volume.shortId);
expect(result.volume.id).toBe(volume.id);
expect(result.volume.shortId).toBe(volume.shortId);
});
});
test("should find volume by shortId", async () => {
test("should find volume by shortId from literal input", async () => {
const { organizationId, user } = await createTestSession();
const [volume] = await db
.insert(volumesTable)
.values({
shortId: "test1234",
shortId: asShortId("test1234"),
name: `test-vol-${randomUUID().slice(0, 8)}`,
type: "directory",
status: "mounted",
@ -63,7 +64,7 @@ describe("volumeService", () => {
const [volume] = await db
.insert(volumesTable)
.values({
shortId: "499780",
shortId: asShortId("499780"),
name: `test-vol-${randomUUID().slice(0, 8)}`,
type: "directory",
status: "mounted",
@ -74,9 +75,9 @@ describe("volumeService", () => {
.returning();
await withContext({ organizationId, userId: user.id }, async () => {
const result = await volumeService.getVolume("499780");
const result = await volumeService.getVolume(asShortId("499780"));
expect(result.volume.id).toBe(volume.id);
expect(result.volume.shortId).toBe("499780");
expect(result.volume.shortId).toBe(asShortId("499780"));
});
});
@ -84,7 +85,7 @@ describe("volumeService", () => {
const { organizationId, user } = await createTestSession();
await withContext({ organizationId, userId: user.id }, async () => {
expect(volumeService.getVolume("nonexistent")).rejects.toThrow("Volume not found");
expect(volumeService.getVolume(asShortId("nonexistent"))).rejects.toThrow("Volume not found");
});
});
});
@ -105,7 +106,7 @@ describe("volumeService security", () => {
const [volume] = await db
.insert(volumesTable)
.values({
shortId: randomUUID().slice(0, 8),
shortId: asShortId(randomUUID().slice(0, 8)),
name: `test-vol-${randomUUID().slice(0, 8)}`,
type: "directory",
status: "mounted",
@ -119,7 +120,7 @@ describe("volumeService security", () => {
await withContext({ organizationId, userId: user.id }, async () => {
const traversalPath = `../${path.basename(secretPath)}`;
expect(volumeService.listFiles(volume.id, traversalPath)).rejects.toThrow("Invalid path");
expect(volumeService.listFiles(volume.shortId, traversalPath)).rejects.toThrow("Invalid path");
});
} finally {
await fs.rm(tempRoot, { recursive: true, force: true });

View file

@ -26,6 +26,7 @@ import {
import { volumeService } from "./volume.service";
import { getVolumePath } from "./helpers";
import { requireAuth } from "../auth/auth.middleware";
import { asShortId } from "~/server/utils/branded";
export const volumeController = new Hono()
.use(requireAuth)
@ -52,13 +53,13 @@ export const volumeController = new Hono()
return c.json(result, 200);
})
.delete("/:shortId", deleteVolumeDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
await volumeService.deleteVolume(shortId);
return c.json({ message: "Volume deleted" }, 200);
})
.get("/:shortId", getVolumeDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const res = await volumeService.getVolume(shortId);
const response = {
@ -76,7 +77,7 @@ export const volumeController = new Hono()
return c.json<GetVolumeDto>(response, 200);
})
.put("/:shortId", updateVolumeDto, validator("json", updateVolumeBody), async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const body = c.req.valid("json");
const res = await volumeService.updateVolume(shortId, body);
@ -88,25 +89,25 @@ export const volumeController = new Hono()
return c.json<UpdateVolumeDto>(response, 200);
})
.post("/:shortId/mount", mountVolumeDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const { error, status } = await volumeService.mountVolume(shortId);
return c.json({ error, status }, error ? 500 : 200);
})
.post("/:shortId/unmount", unmountVolumeDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const { error, status } = await volumeService.unmountVolume(shortId);
return c.json({ error, status }, error ? 500 : 200);
})
.post("/:shortId/health-check", healthCheckDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const { error, status } = await volumeService.checkHealth(shortId);
return c.json({ error, status }, 200);
})
.get("/:shortId/files", validator("query", listFilesQuery), listFilesDto, async (c) => {
const { shortId } = c.req.param();
const shortId = asShortId(c.req.param("shortId"));
const { path, ...query } = c.req.valid("query");
const offset = Math.max(0, Number.parseInt(query.offset ?? "0", 10) || 0);

View file

@ -19,6 +19,7 @@ import { volumeConfigSchema, type BackendConfig } from "~/schemas/volumes";
import { type } from "arktype";
import { getOrganizationId } from "~/server/core/request-context";
import { isNodeJSErrnoException } from "~/server/utils/fs";
import { asShortId, type ShortId } from "~/server/utils/branded";
async function encryptSensitiveFields(config: BackendConfig): Promise<BackendConfig> {
switch (config.backend) {
@ -52,11 +53,11 @@ const listVolumes = async () => {
return volumes;
};
const findVolume = async (identifier: string | number) => {
const findVolume = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
return await db.query.volumesTable.findFirst({
where: {
AND: [{ OR: [{ id: Number(identifier) }, { shortId: String(identifier) }] }, { organizationId: organizationId }],
AND: [{ shortId: { eq: shortId } }, { organizationId: organizationId }],
},
});
};
@ -98,9 +99,9 @@ const createVolume = async (name: string, backendConfig: BackendConfig) => {
return { volume: created, status: 201 };
};
const deleteVolume = async (identifier: string | number) => {
const deleteVolume = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
const volume = await findVolume(identifier);
const volume = await findVolume(shortId);
if (!volume) {
throw new NotFoundError("Volume not found");
@ -113,9 +114,9 @@ const deleteVolume = async (identifier: string | number) => {
.where(and(eq(volumesTable.id, volume.id), eq(volumesTable.organizationId, organizationId)));
};
const mountVolume = async (identifier: string | number) => {
const mountVolume = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
const volume = await findVolume(identifier);
const volume = await findVolume(shortId);
if (!volume) {
throw new NotFoundError("Volume not found");
@ -136,9 +137,9 @@ const mountVolume = async (identifier: string | number) => {
return { error, status };
};
const unmountVolume = async (identifier: string | number) => {
const unmountVolume = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
const volume = await findVolume(identifier);
const volume = await findVolume(shortId);
if (!volume) {
throw new NotFoundError("Volume not found");
@ -159,8 +160,8 @@ const unmountVolume = async (identifier: string | number) => {
return { error, status };
};
const getVolume = async (identifier: string | number) => {
const volume = await findVolume(identifier);
const getVolume = async (shortId: ShortId) => {
const volume = await findVolume(shortId);
if (!volume) {
throw new NotFoundError("Volume not found");
@ -177,9 +178,9 @@ const getVolume = async (identifier: string | number) => {
return { volume, statfs };
};
const updateVolume = async (identifier: string | number, volumeData: UpdateVolumeBody) => {
const updateVolume = async (shortId: ShortId, volumeData: UpdateVolumeBody) => {
const organizationId = getOrganizationId();
const existing = await findVolume(identifier);
const existing = await findVolume(shortId);
if (!existing) {
throw new NotFoundError("Volume not found");
@ -242,7 +243,7 @@ const testConnection = async (backendConfig: BackendConfig) => {
const mockVolume = {
id: 0,
shortId: "test",
shortId: asShortId("test"),
name: "test-connection",
path: tempDir,
config: backendConfig,
@ -270,9 +271,9 @@ const testConnection = async (backendConfig: BackendConfig) => {
};
};
const checkHealth = async (identifier: string | number) => {
const checkHealth = async (shortId: ShortId) => {
const organizationId = getOrganizationId();
const volume = await findVolume(identifier);
const volume = await findVolume(shortId);
if (!volume) {
throw new NotFoundError("Volume not found");
@ -296,13 +297,8 @@ const checkHealth = async (identifier: string | number) => {
const DEFAULT_PAGE_SIZE = 500;
const MAX_PAGE_SIZE = 500;
const listFiles = async (
identifier: string | number,
subPath?: string,
offset: number = 0,
limit: number = DEFAULT_PAGE_SIZE,
) => {
const volume = await findVolume(identifier);
const listFiles = async (shortId: ShortId, subPath?: string, offset: number = 0, limit: number = DEFAULT_PAGE_SIZE) => {
const volume = await findVolume(shortId);
if (!volume) {
throw new NotFoundError("Volume not found");

View file

@ -0,0 +1,9 @@
declare const brand: unique symbol;
export type Branded<T, B> = T & { [brand]: B };
export type ShortId = Branded<string, "ShortId">;
export const asShortId = (value: string): ShortId => value as ShortId;
export const isShortId = (value: string): value is ShortId => /^[A-Za-z0-9_-]+$/.test(value);

View file

@ -1,6 +1,7 @@
import crypto from "node:crypto";
import type { ShortId } from "./branded";
export const generateShortId = (length = 8): string => {
export const generateShortId = (length = 8): ShortId => {
const bytesNeeded = Math.ceil((length * 3) / 4);
return crypto.randomBytes(bytesNeeded).toString("base64url").slice(0, length);
return crypto.randomBytes(bytesNeeded).toString("base64url").slice(0, length) as ShortId;
};

View file

@ -4,6 +4,7 @@ import { backupSchedulesTable, type BackupScheduleInsert } from "~/server/db/sch
import { createTestOrganization, ensureTestOrganization, TEST_ORG_ID } from "./organization";
import { createTestVolume } from "./volume";
import { createTestRepository } from "./repository";
import { generateShortId } from "~/server/utils/id";
export const createTestBackupSchedule = async (overrides: Partial<BackupScheduleInsert> = {}) => {
const organizationId = overrides.organizationId ?? TEST_ORG_ID;
@ -22,7 +23,7 @@ export const createTestBackupSchedule = async (overrides: Partial<BackupSchedule
cronExpression: "0 0 * * *",
repositoryId,
volumeId,
shortId: faker.string.uuid(),
shortId: generateShortId(),
organizationId,
...overrides,
};

View file

@ -2,6 +2,7 @@ import { db } from "~/server/db/db";
import { faker } from "@faker-js/faker";
import { repositoriesTable, type RepositoryInsert } from "~/server/db/schema";
import { ensureTestOrganization, TEST_ORG_ID } from "./organization";
import { generateShortId } from "~/server/utils/id";
export const createTestRepository = async (overrides: Partial<RepositoryInsert> = {}) => {
await ensureTestOrganization();
@ -9,7 +10,7 @@ export const createTestRepository = async (overrides: Partial<RepositoryInsert>
const repository: RepositoryInsert = {
id: faker.string.alphanumeric(6),
name: faker.string.alphanumeric(10),
shortId: faker.string.alphanumeric(6),
shortId: generateShortId(),
config: {
path: `/var/lib/zerobyte/repositories/${faker.string.alphanumeric(8)}`,
backend: "local",

View file

@ -2,6 +2,7 @@ import { db } from "~/server/db/db";
import { faker } from "@faker-js/faker";
import { volumesTable, type VolumeInsert } from "~/server/db/schema";
import { ensureTestOrganization, TEST_ORG_ID } from "./organization";
import { generateShortId } from "~/server/utils/id";
export const createTestVolume = async (overrides: Partial<VolumeInsert> = {}) => {
await ensureTestOrganization();
@ -14,7 +15,7 @@ export const createTestVolume = async (overrides: Partial<VolumeInsert> = {}) =>
},
status: "mounted",
autoRemount: true,
shortId: faker.string.alphanumeric(6),
shortId: generateShortId(),
type: "directory",
organizationId: TEST_ORG_ID,
...overrides,

View file

@ -28,11 +28,9 @@
"@scalar/hono-api-reference": "^0.9.41",
"@tanstack/react-hotkeys": "^0.1.0",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-query-devtools": "^5.91.3",
"@tanstack/react-router": "^1.160.2",
"@tanstack/react-router-devtools": "^1.160.2",
"@tanstack/react-router-ssr-query": "^1.160.2",
"@tanstack/react-start": "^1.160.2",
"@tanstack/react-router": "^1.161.3",
"@tanstack/react-router-ssr-query": "^1.161.3",
"@tanstack/react-start": "^1.161.3",
"arktype": "^2.1.28",
"better-auth": "^1.4.18",
"class-variance-authority": "^0.7.1",
@ -763,49 +761,41 @@
"@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.93.0", "", {}, "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg=="],
"@tanstack/react-hotkeys": ["@tanstack/react-hotkeys@0.1.0", "", { "dependencies": { "@tanstack/hotkeys": "0.1.0", "@tanstack/react-store": "^0.8.0" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-Hteo+hdPKdR2G7uAwTmScu1JUHM/f03lVihxVfR/A3XMyVJmbzi4SJieRk/kLgYKeXFRGIUx/xSI3mHeVc6DbQ=="],
"@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.3", "", { "dependencies": { "@tanstack/query-devtools": "5.93.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.20", "react": "^18 || ^19" } }, "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA=="],
"@tanstack/react-router": ["@tanstack/react-router@1.161.3", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.9.1", "@tanstack/router-core": "1.161.3", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-evYPrkuFt4T6E0WVyBGGq83lWHJjsYy3E5SpPpfPY/uRnEgmgwfr6Xl570msRnWYMj7DIkYg8ZWFFwzqKrSlBw=="],
"@tanstack/react-router": ["@tanstack/react-router@1.160.2", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.160.0", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-EJWAMS4qCfWKNCzzYGy6ZuWTdBATYEEWieaQdmM7zUesyOQ01j7o6aKXdmCp9rWuSKjPHXagWubEnEo+Puhi3w=="],
"@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.161.3", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.161.3" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-0tLk3yrbQlcHBwbgSPT5I67rzq/v3KXZ4pSsQ7pkk4jgNTldE7sV8haHTiMU8yXI78HajgwMEZrfBvx0mrQfIQ=="],
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.160.2", "", { "dependencies": { "@tanstack/router-devtools-core": "1.160.0" }, "peerDependencies": { "@tanstack/react-router": "^1.160.2", "@tanstack/router-core": "^1.160.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-uSdQD77S+LcANCiWcLsrqSxyEqTXdZhBETbciKcYJrcgd8rfkxg6AIewYi7148QPU6gb3VKQbeRlqXmBeEs5dg=="],
"@tanstack/react-start": ["@tanstack/react-start@1.161.3", "", { "dependencies": { "@tanstack/react-router": "1.161.3", "@tanstack/react-start-client": "1.161.3", "@tanstack/react-start-server": "1.161.3", "@tanstack/router-utils": "^1.158.0", "@tanstack/start-client-core": "1.161.3", "@tanstack/start-plugin-core": "1.161.3", "@tanstack/start-server-core": "1.161.3", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-wWx+wuR4U6yHgnNsHLocjMYrsORdOWom6UuBzOsh+FmFJ1RK+NcjyNNt6WjsmWPU5ZUZ31KHKe4ZEt27R+15IA=="],
"@tanstack/react-router-ssr-query": ["@tanstack/react-router-ssr-query@1.160.2", "", { "dependencies": { "@tanstack/router-ssr-query-core": "1.160.0" }, "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/react-query": ">=5.90.0", "@tanstack/react-router": ">=1.127.0", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-/9eDT69AtsFdSY2lSz2BjW2c8SwJMXB/VGW/HxVKGavTPYD6J4xN6Ew7iSwJqfbjB7yRer5Dp0lvKFSQbcJGtw=="],
"@tanstack/react-start-client": ["@tanstack/react-start-client@1.161.3", "", { "dependencies": { "@tanstack/react-router": "1.161.3", "@tanstack/router-core": "1.161.3", "@tanstack/start-client-core": "1.161.3", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-MMyc8WCBkoipJ5rNqgAANOB4gITsfxs/f05pY3K1BUpUAOi91s8YgNO2IBnIOj8sqNoI0Gx2lWgQBCwm4H4+wg=="],
"@tanstack/react-start": ["@tanstack/react-start@1.160.2", "", { "dependencies": { "@tanstack/react-router": "1.160.2", "@tanstack/react-start-client": "1.160.2", "@tanstack/react-start-server": "1.160.2", "@tanstack/router-utils": "^1.158.0", "@tanstack/start-client-core": "1.160.0", "@tanstack/start-plugin-core": "1.160.2", "@tanstack/start-server-core": "1.160.0", "pathe": "^2.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" } }, "sha512-+v+m33wTOlcjp0L66YcjS6YCi9tz9QZBC1ZC4L6o3eV/6S2gZqNZ32Tz6LLfsDuipr7iyyeAiXcFTwtZrfhSdQ=="],
"@tanstack/react-start-client": ["@tanstack/react-start-client@1.160.2", "", { "dependencies": { "@tanstack/react-router": "1.160.2", "@tanstack/router-core": "1.160.0", "@tanstack/start-client-core": "1.160.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-LZRM5hKb5nj0vKhgdydm7S0kXZG7yrv11Pyt0itzUOomXg2JuDM9ckvAw8PDl/O+sp3RJWBqfGj90Cpl5PQOuQ=="],
"@tanstack/react-start-server": ["@tanstack/react-start-server@1.160.2", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-router": "1.160.2", "@tanstack/router-core": "1.160.0", "@tanstack/start-client-core": "1.160.0", "@tanstack/start-server-core": "1.160.0" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-y1PsLVfgD8UWNA3mFIg4mQNJkbjz6nZJTMBne5L/dndhk/SGQGBM/AA2El5n/4tKl82TgEum59bVjvKjudX6RQ=="],
"@tanstack/react-start-server": ["@tanstack/react-start-server@1.161.3", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/react-router": "1.161.3", "@tanstack/router-core": "1.161.3", "@tanstack/start-client-core": "1.161.3", "@tanstack/start-server-core": "1.161.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-5FY3+2LHPPlVkHrrPbhi+TKVl93UCWKC0Ta/hxhXBaYrvBU1uWhIuKgspXT+cWP6XcMPuPrc4qfVWrXHKBMIqg=="],
"@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="],
"@tanstack/router-core": ["@tanstack/router-core@1.160.0", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-vbh6OsE0MG+0c+SKh2uk5yEEZlWsxT96Ub2JaTs7ixOvZp3Wu9PTEIe2BA3cShNZhEsDI0Le4NqgY4XIaHLLvA=="],
"@tanstack/router-core": ["@tanstack/router-core@1.161.3", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/store": "^0.9.1", "cookie-es": "^2.0.0", "seroval": "^1.4.2", "seroval-plugins": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-8EuaGXLUjugQE9Rsb8VrWSy+wImcs/DZ9JORqUJYCmiiWnJzbat8KedQItq/9LCjMJyx4vTLCt8NnZCL+j1Ayg=="],
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.160.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.160.0", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-P/l0GVd0qmDbskg8/UbkOrCxuFz0t69BCxv2j4+8Xfy8AcqnFtoR1LChKgYyxGPy9sWOxktAneFdy1xA3X/Q6A=="],
"@tanstack/router-generator": ["@tanstack/router-generator@1.161.3", "", { "dependencies": { "@tanstack/router-core": "1.161.3", "@tanstack/router-utils": "1.158.0", "@tanstack/virtual-file-routes": "1.154.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-GKOrsOu7u5aoK1+lRu6KUUOmbb42mYF2ezfXf27QMiBjMx/yDHXln8wmdR7ZQ+FdSGz2YVubt2Ns3KuFsDsZJg=="],
"@tanstack/router-generator": ["@tanstack/router-generator@1.160.1", "", { "dependencies": { "@tanstack/router-core": "1.160.0", "@tanstack/router-utils": "1.158.0", "@tanstack/virtual-file-routes": "1.154.7", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-De6TicInwy3/9rQ++RZAyFOvB2oi5UV5T0iiIlxe3jgiOLFxMA4EKKVlT+alDxKnq6udTLam9xqhvGOVZ6a2hw=="],
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.161.3", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.161.3", "@tanstack/router-generator": "1.161.3", "@tanstack/router-utils": "1.158.0", "@tanstack/virtual-file-routes": "1.154.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.161.3", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-3Uy4AxgHNYjmCGf2WYWB8Gy3C6m0YE5DV1SK2p3yUrA/PhCMYRe+xzjyD5pViMUSLUoPHQYGY6bOIM9OOPRI/Q=="],
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.160.2", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.160.0", "@tanstack/router-generator": "1.160.1", "@tanstack/router-utils": "1.158.0", "@tanstack/virtual-file-routes": "1.154.7", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.160.2", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-+uyjLK4hImFyENb08PwDfvnKpwfdjCA29sKrHRk/ff9H23RUm10MW0UMjIrVU+4Dql/xsbzsqrTwIyRS6zna9Q=="],
"@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.160.0", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-RS2L7NchJ2/YadTWJjOA1czGiTlshMLd5CBPUZURKAlkVymGsupOHG9ds63egpyHgKI/zMQOUHgtOY74ishoLA=="],
"@tanstack/router-ssr-query-core": ["@tanstack/router-ssr-query-core@1.161.3", "", { "peerDependencies": { "@tanstack/query-core": ">=5.90.0", "@tanstack/router-core": ">=1.127.0" } }, "sha512-dFEWRKnv4UVWOGY53ypFivHkuM/NKxtGuj7O0UGHaQshpVEH+oXfb/KupHeCuSbDLdREaqQYMBq0L6VfHRw5FA=="],
"@tanstack/router-utils": ["@tanstack/router-utils@1.158.0", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-qZ76eaLKU6Ae9iI/mc5zizBX149DXXZkBVVO3/QRIll79uKLJZHQlMKR++2ba7JsciBWz1pgpIBcCJPE9S0LVg=="],
"@tanstack/start-client-core": ["@tanstack/start-client-core@1.160.0", "", { "dependencies": { "@tanstack/router-core": "1.160.0", "@tanstack/start-fn-stubs": "1.154.7", "@tanstack/start-storage-context": "1.160.0", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-qVzbDT6tTED3+3rPJsyAOskszEjCW0/DkzEMoG4an8Gh3cNnEXqrCDyKWjvqYpj9zrquNTzgBh3GFZzzfgr4ZQ=="],
"@tanstack/start-client-core": ["@tanstack/start-client-core@1.161.3", "", { "dependencies": { "@tanstack/router-core": "1.161.3", "@tanstack/start-fn-stubs": "1.154.7", "@tanstack/start-storage-context": "1.161.3", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-tE9PJCk+64CeIie70f6MZd8LP3A+5LWjjwksEaqxsZMYGN0Re6BWI/oTpZtnvRHhtCUB5ASz6K/eMZt8R9wq5A=="],
"@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.154.7", "", {}, "sha512-D69B78L6pcFN5X5PHaydv7CScQcKLzJeEYqs7jpuyyqGQHSUIZUjS955j+Sir8cHhuDIovCe2LmsYHeZfWf3dQ=="],
"@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.160.2", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.160.0", "@tanstack/router-generator": "1.160.1", "@tanstack/router-plugin": "1.160.2", "@tanstack/router-utils": "1.158.0", "@tanstack/start-client-core": "1.160.0", "@tanstack/start-server-core": "1.160.0", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "srvx": "^0.11.2", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-scFSvcoHHLhiz0RhH4tetBXkjZLgUR+xfAA5aeqWcC0dcMS4Cj5Irb+AxAkvbdGQPc0Afyrs77OmOUi3v4YjcA=="],
"@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.161.3", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@rolldown/pluginutils": "1.0.0-beta.40", "@tanstack/router-core": "1.161.3", "@tanstack/router-generator": "1.161.3", "@tanstack/router-plugin": "1.161.3", "@tanstack/router-utils": "1.158.0", "@tanstack/start-client-core": "1.161.3", "@tanstack/start-server-core": "1.161.3", "cheerio": "^1.0.0", "exsolve": "^1.0.7", "pathe": "^2.0.3", "picomatch": "^4.0.3", "source-map": "^0.7.6", "srvx": "^0.11.7", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^3.24.2" }, "peerDependencies": { "vite": ">=7.0.0" } }, "sha512-HrxDuh5nn1F4LhJyQ1cHwou1VdlOsH3uOK/EEQXYBPpo+NKWGeaw06ff9fwmH6/FkpgWrh1c+4T0V1BS+T/YJA=="],
"@tanstack/start-server-core": ["@tanstack/start-server-core@1.160.0", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/router-core": "1.160.0", "@tanstack/start-client-core": "1.160.0", "@tanstack/start-storage-context": "1.160.0", "h3-v2": "npm:h3@2.0.1-rc.14", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3" } }, "sha512-zr30GxMV5VBAFSPQ9le/VW9Ql8p6ctrv4a94avgR3hEzjnXOxOBpOO29EdzVtrTcbTPO6rxC9B0f3yOwbEcucg=="],
"@tanstack/start-server-core": ["@tanstack/start-server-core@1.161.3", "", { "dependencies": { "@tanstack/history": "1.154.14", "@tanstack/router-core": "1.161.3", "@tanstack/start-client-core": "1.161.3", "@tanstack/start-storage-context": "1.161.3", "h3-v2": "npm:h3@2.0.1-rc.14", "seroval": "^1.4.2", "tiny-invariant": "^1.3.3" } }, "sha512-+/1XUd8Dvf462/OHsJJEhFZhERqd0iI/1JD9SyNPSqP6cFnkhIqIK+yaiD6ziQaGO+pkBqSmuRB2/acJg4NRTg=="],
"@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.160.0", "", { "dependencies": { "@tanstack/router-core": "1.160.0" } }, "sha512-Y4mvNu0/R1poZsBoz9JCXeanwj9BVaiv4XBt33bQk12kLhdNwyTXzZ3cuuNyfiHL+tNK0/b6oQyQZFGMLfB1Hw=="],
"@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.161.3", "", { "dependencies": { "@tanstack/router-core": "1.161.3" } }, "sha512-X89oEykLrrhIn+41Q3jXVYRsg9NirM+7Nr0FLajFRle3FpAYggHq6TS8XPRhrv664uLa5Dz225sxCDwC5OT+sQ=="],
"@tanstack/store": ["@tanstack/store@0.8.0", "", {}, "sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ=="],
@ -1151,8 +1141,6 @@
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
"goober": ["goober@2.1.18", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"h3": ["h3@2.0.1-rc.11", "", { "dependencies": { "rou3": "^0.7.12", "srvx": "^0.10.1" }, "peerDependencies": { "crossws": "^0.4.1" }, "optionalPeers": ["crossws"] }, "sha512-2myzjCqy32c1As9TjZW9fNZXtLqNedjFSrdFy2AjFBQQ3LzrnGoDdFDYfC0tV2e4vcyfJ2Sfo/F6NQhO2Ly/Mw=="],
@ -1781,6 +1769,10 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tanstack/react-router/@tanstack/react-store": ["@tanstack/react-store@0.9.1", "", { "dependencies": { "@tanstack/store": "0.9.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-YzJLnRvy5lIEFTLWBAZmcOjK3+2AepnBv/sr6NZmiqJvq7zTQggyK99Gw8fqYdMdHPQWXjz0epFKJXC+9V2xDA=="],
"@tanstack/router-core/@tanstack/store": ["@tanstack/store@0.9.1", "", {}, "sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg=="],
"@tanstack/router-generator/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@tanstack/router-plugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
@ -1791,7 +1783,7 @@
"@tanstack/start-plugin-core/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.40", "", {}, "sha512-s3GeJKSQOwBlzdUrj4ISjJj5SfSh+aqn0wjOar4Bx95iV1ETI7F6S/5hLcfAxZ9kXDcyrAkxPlqmd1ZITttf+w=="],
"@tanstack/start-plugin-core/srvx": ["srvx@0.11.4", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-m/2p87bqWZ94xpRN06qNBwh0xq/D0dXajnvPDSHFqrTogxuTWYNP1UHz6Cf+oY7D+NPLY35TJAp4ESIKn0WArQ=="],
"@tanstack/start-plugin-core/srvx": ["srvx@0.11.7", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-p9qj9wkv/MqG1VoJpOsqXv1QcaVcYRk7ifsC6i3TEwDXFyugdhJN4J3KzQPZq2IJJ2ZCt7ASOB++85pEK38jRw=="],
"@tanstack/start-plugin-core/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
@ -1869,6 +1861,8 @@
"@tailwindcss/node/lightningcss/lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw=="],
"@tanstack/react-router/@tanstack/react-store/@tanstack/store": ["@tanstack/store@0.9.1", "", {}, "sha512-+qcNkOy0N1qSGsP7omVCW0SDrXtaDcycPqBDE726yryiA5eTDFpjBReaYjghVJwNf1pcPMyzIwTGlYjCSQR0Fg=="],
"@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"@tanstack/router-plugin/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],

View file

@ -49,11 +49,9 @@
"@scalar/hono-api-reference": "^0.9.41",
"@tanstack/react-hotkeys": "^0.1.0",
"@tanstack/react-query": "^5.90.21",
"@tanstack/react-query-devtools": "^5.91.3",
"@tanstack/react-router": "^1.160.2",
"@tanstack/react-router-devtools": "^1.160.2",
"@tanstack/react-router-ssr-query": "^1.160.2",
"@tanstack/react-start": "^1.160.2",
"@tanstack/react-router": "^1.161.3",
"@tanstack/react-router-ssr-query": "^1.161.3",
"@tanstack/react-start": "^1.161.3",
"arktype": "^2.1.28",
"better-auth": "^1.4.18",
"class-variance-authority": "^0.7.1",

View file

@ -13,6 +13,9 @@ export default defineConfig({
router: {
routesDirectory: "routes",
},
importProtection: {
behavior: "error",
},
}),
nitro({
preset: "bun",