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 <noreply@anthropic.com>
This commit is contained in:
fuomag9
2026-02-25 18:41:12 +01:00
parent cb3c0a1536
commit 66ad3e9431
4 changed files with 17 additions and 4 deletions

View File

@@ -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<string, { count: number; windowStart: number }>();
@@ -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 });
}
}