SyncPayload["data"]["l4ProxyHosts"] is optional (Array | undefined),
so indexing with [number] fails tsc. NonNullable<...>[number] resolves
the correct element type.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Banner (L4PortsApplyBanner):
- Accept refreshSignal prop; re-fetch /api/l4-ports when it changes
- Signal fires immediately after create/edit/delete/toggle in L4ProxyHostsClient
without waiting for a page reload
Master-slave replication (instance-sync):
- Add l4ProxyHosts to SyncPayload.data (optional for backward compat
with older master instances that don't include it)
- buildSyncPayload: query and include l4ProxyHosts, sanitize ownerUserId
- applySyncPayload: clear and re-insert l4ProxyHosts in transaction;
call applyL4Ports() if port diff requires it so the slave's sidecar
recreates caddy with the correct ports
- Sync route: add isL4ProxyHost validator; backfill missing field from
old masters; validate array when present
Tests (25 new tests):
- instance-sync.test.ts: buildSyncPayload includes L4 data, sanitizes ownerUserId;
applySyncPayload replaces L4 hosts, handles missing field, writes trigger
when ports differ, skips trigger when ports already match
- l4-ports-apply-banner.test.ts: banner refreshSignal contract + client
increments counter on all mutation paths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>