/** * db/webSessionDedup.ts — pure helpers for de-duplicating web-session * (cookie/token) provider credentials. Extracted from providers.ts so the * cookie-dedup wiring there stays thin (#3368 PR6). No DB access here. */ /** * Reduce a `provider_specific_data` record to a single comparable credential * value. Cookie/token credentials are mirrored across a provider's storage * keys (e.g. `cookie`, `sessionToken`, `token`) with the same secret value, so * any one of them identifies the session. Returns the trimmed value, or null * when no usable string credential is present. */ const PREFERRED_CREDENTIAL_KEYS = [ "cookie", "token", "sessionToken", "session-token", "sso", "access_token", "accessToken", ]; /** First trimmed non-empty string value among `keys` of `rec`, else null. */ function firstNonEmptyString(rec: Record, keys: readonly string[]): string | null { for (const key of keys) { const value = rec[key]; if (typeof value === "string" && value.trim()) return value.trim(); } return null; } export function webSessionCredentialKey(psd: unknown): string | null { if (!psd || typeof psd !== "object") return null; const rec = psd as Record; // Prefer canonical credential keys, then fall back to the first non-empty // string value (sorted for determinism). return ( firstNonEmptyString(rec, PREFERRED_CREDENTIAL_KEYS) ?? firstNonEmptyString(rec, Object.keys(rec).sort()) ); } /** Parse a stored `provider_specific_data` column (JSON string or object). */ export function parseProviderSpecificData(raw: unknown): Record | null { if (!raw) return null; if (typeof raw === "object") return raw as Record; if (typeof raw === "string") { try { const parsed = JSON.parse(raw); return parsed && typeof parsed === "object" ? (parsed as Record) : null; } catch { return null; } } return null; } /** Trimmed non-empty string, else null — local to avoid a cross-module import for one coercion. */ function nonEmptyString(value: unknown): string | null { return typeof value === "string" && value.trim() ? value.trim() : null; } /** * Two-sided disambiguator match: `true` when both sides agree, `false` when * both carry a value and it differs, `undefined` when the field can't decide * (at most one side carries it) — the caller then defers to other fields. */ function fieldMatch(incoming: string | null, existing: string | null): boolean | undefined { if (incoming && existing) return incoming === existing; if (incoming || existing) return false; return undefined; } /** * Decide whether `row` (an existing `provider_connections` record) is the * same OAuth identity as an incoming connection carrying `incomingUsername` * and `incomingProfileArn` (#10815). * * Two independent disambiguators, either of which can prove "different * account": `providerSpecificData.username` (Raycast-style IdP dedup) and * `providerSpecificData.profileArn` (Kiro/AWS profile dedup — Kiro never * sets `username`). A field only rules a match IN/OUT when both the * incoming and existing record carry it; when neither carries either field * the legacy bare-email match still applies unchanged. */ export function isMatchingOauthIdentity( row: { provider_specific_data?: unknown }, incomingUsername: string | null, incomingProfileArn: string | null ): boolean { const existingPsd = parseProviderSpecificData(row.provider_specific_data); const usernameMatch = fieldMatch(incomingUsername, nonEmptyString(existingPsd?.username)); const profileArnMatch = fieldMatch(incomingProfileArn, nonEmptyString(existingPsd?.profileArn)); if (usernameMatch === false || profileArnMatch === false) return false; return true; }