From 66ad3e943164ba682882e5a53b3b4d1a2efb4a19 Mon Sep 17 00:00:00 2001 From: fuomag9 <1580624+fuomag9@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:41:12 +0100 Subject: [PATCH] fix: enforce unique provider+subject constraint and harden sync route - Change providerSubjectIdx from index to uniqueIndex in schema.ts to prevent multiple users sharing the same (provider, subject) pair, which caused non-deterministic sign-in resolution via findFirst. - Add migration 0008_unique_provider_subject.sql: DROP the existing non-unique index and CREATE UNIQUE INDEX in its place. - Validate INSTANCE_SYNC_MAX_BYTES env var in sync route: fall back to 10 MB default when the value is non-numeric (e.g. 'off') or non-positive, preventing NaN comparisons that silently disabled the size limit. - Return a generic error message to callers on applySyncPayload / applyCaddyConfig failure instead of leaking the raw error string; the original message is still stored internally via setSlaveLastSync. Co-Authored-By: Claude Sonnet 4.6 --- app/api/instances/sync/route.ts | 10 +++++++--- drizzle/0008_unique_provider_subject.sql | 2 ++ drizzle/meta/_journal.json | 7 +++++++ src/lib/db/schema.ts | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 drizzle/0008_unique_provider_subject.sql diff --git a/app/api/instances/sync/route.ts b/app/api/instances/sync/route.ts index a2e62313..7861d59b 100644 --- a/app/api/instances/sync/route.ts +++ b/app/api/instances/sync/route.ts @@ -3,7 +3,11 @@ import { timingSafeEqual } from "crypto"; import { applyCaddyConfig } from "@/src/lib/caddy"; import { applySyncPayload, getInstanceMode, getSlaveMasterToken, setSlaveLastSync, SyncPayload } from "@/src/lib/instance-sync"; -const MAX_SYNC_BODY_BYTES = Number(process.env.INSTANCE_SYNC_MAX_BYTES ?? 10 * 1024 * 1024); +const DEFAULT_MAX_SYNC_BODY_BYTES = 10 * 1024 * 1024; // 10 MB +const _parsedMaxBytes = Number(process.env.INSTANCE_SYNC_MAX_BYTES); +const MAX_SYNC_BODY_BYTES = Number.isFinite(_parsedMaxBytes) && _parsedMaxBytes > 0 + ? _parsedMaxBytes + : DEFAULT_MAX_SYNC_BODY_BYTES; const SYNC_RATE_MAX = Number(process.env.INSTANCE_SYNC_RATE_MAX ?? 60); const SYNC_RATE_WINDOW_MS = Number(process.env.INSTANCE_SYNC_RATE_WINDOW_MS ?? 60_000); const SYNC_RATE_LIMITS = new Map(); @@ -235,7 +239,7 @@ export async function POST(request: NextRequest) { return NextResponse.json({ ok: true }); } catch (error) { const message = error instanceof Error ? error.message : "Failed to apply sync payload"; - await setSlaveLastSync({ ok: false, error: message }); - return NextResponse.json({ error: message }, { status: 500 }); + await setSlaveLastSync({ ok: false, error: message }); // still store internally + return NextResponse.json({ error: "Failed to apply sync payload" }, { status: 500 }); } } diff --git a/drizzle/0008_unique_provider_subject.sql b/drizzle/0008_unique_provider_subject.sql new file mode 100644 index 00000000..9130645f --- /dev/null +++ b/drizzle/0008_unique_provider_subject.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS `users_provider_subject_idx`; +CREATE UNIQUE INDEX `users_provider_subject_idx` ON `users`(`provider`,`subject`); diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index caa68170..059f3376 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1740441600000, "tag": "0007_linking_tokens", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1740960000000, + "tag": "0008_unique_provider_subject", + "breakpoints": true } ] } diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index e9400a0e..5a98f78b 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -17,7 +17,7 @@ export const users = sqliteTable( }, (table) => ({ emailUnique: uniqueIndex("users_email_unique").on(table.email), - providerSubjectIdx: index("users_provider_subject_idx").on(table.provider, table.subject) + providerSubjectIdx: uniqueIndex("users_provider_subject_idx").on(table.provider, table.subject) }) );