mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-07 07:42:13 +03:00
Release v3.8.41 — 52 commits since v3.8.40 (19 CHANGELOG bullets, 11 contributors). All gating CI green: Unit×8, Coverage×8, Vitest, Package Artifact, Quality Ratchet, CodeQL, Lint, Docs Sync (Strict), Node 24/26 compat, E2E×9, Integration, Electron smoke. Advisory checks overridden (main unprotected): PR Test Policy = test-masking heuristic on the cumulative 52-commit assert delta (legitimate dead-code-sweep removals + consolidations, reviewed per-PR); SonarCloud/SonarQube = new-code maintainability/coverage quality gate (CodeQL/Semgrep/Security/npm-audit/Dependabot all clean — not a security finding).
115 lines
4.0 KiB
TypeScript
115 lines
4.0 KiB
TypeScript
/**
|
|
* Shared upsert for OAuth provider connections, used by both the authenticated
|
|
* OAuth route (`device-complete`) and the public Codex device-flow completion
|
|
* endpoint. Mirrors the exchange/poll/poll-callback persistence: normalize the
|
|
* display name, compute expiry, match an existing connection by id or email
|
|
* (+ Codex workspaceId) and update it, else create a new one, then sync to Cloud.
|
|
*/
|
|
import { timingSafeEqual } from "crypto";
|
|
import {
|
|
createProviderConnection,
|
|
updateProviderConnection,
|
|
getProviderConnections,
|
|
isCloudEnabled,
|
|
} from "@/models";
|
|
import { getConsistentMachineId } from "@/shared/utils/machineId";
|
|
import { syncToCloud } from "@/lib/cloudSync";
|
|
|
|
/**
|
|
* Constant-time string comparison to prevent timing-oracle attacks (CWE-208).
|
|
* Handles null/undefined safely and different-length strings.
|
|
*/
|
|
function safeEqual(a: string | null | undefined, b: string | null | undefined): boolean {
|
|
if (a == null || b == null) return a === b;
|
|
const ba = Buffer.from(String(a));
|
|
const bb = Buffer.from(String(b));
|
|
if (ba.length !== bb.length) return false;
|
|
return timingSafeEqual(ba, bb);
|
|
}
|
|
|
|
/**
|
|
* Build the create payload for a brand-new OAuth connection.
|
|
*
|
|
* #5326: mirror the freshly computed `expiresAt` into `tokenExpiresAt` at creation
|
|
* time. The dashboard token-health badge prefers `tokenExpiresAt` over `expiresAt`
|
|
* (ConnectionRow.tsx: `connection.tokenExpiresAt || connection.expiresAt`). If
|
|
* `tokenExpiresAt` stays null on a freshly created connection, the badge falls back
|
|
* to the original grant clock and can flash a false amber/"Token Expired" until the
|
|
* first background refresh writes both fields together. All refresh paths already
|
|
* persist `expiresAt` and `tokenExpiresAt` in lockstep
|
|
* (tokenHealthCheck onPersist, tokenRefresh.updateProviderCredentials); this makes
|
|
* creation consistent with them.
|
|
*/
|
|
export function buildOAuthConnectionCreatePayload(
|
|
provider: string,
|
|
tokenData: Record<string, any>,
|
|
expiresAt: string | null
|
|
) {
|
|
return {
|
|
provider,
|
|
authType: "oauth" as const,
|
|
...tokenData,
|
|
expiresAt,
|
|
tokenExpiresAt: expiresAt,
|
|
testStatus: "active" as const,
|
|
};
|
|
}
|
|
|
|
async function syncToCloudIfEnabled(): Promise<void> {
|
|
try {
|
|
const cloudEnabled = await isCloudEnabled();
|
|
if (!cloudEnabled) return;
|
|
const machineId = await getConsistentMachineId();
|
|
await syncToCloud(machineId);
|
|
} catch (error) {
|
|
console.log("Error syncing to cloud after OAuth:", error);
|
|
}
|
|
}
|
|
|
|
export async function persistOAuthConnection(
|
|
provider: string,
|
|
tokenData: any,
|
|
connectionId?: string
|
|
) {
|
|
// Normalize: if name is missing, use email or displayName as fallback label.
|
|
if (!tokenData.name && (tokenData.email || tokenData.displayName)) {
|
|
tokenData.name = tokenData.email || tokenData.displayName;
|
|
}
|
|
|
|
const expiresAt = tokenData.expiresIn
|
|
? new Date(Date.now() + tokenData.expiresIn * 1000).toISOString()
|
|
: null;
|
|
|
|
let connection: any;
|
|
if (tokenData.email) {
|
|
const existing = await getProviderConnections({ provider });
|
|
const match = existing.find((c: any) => {
|
|
if (c.id && safeEqual(connectionId, c.id)) return true;
|
|
if (!safeEqual(c.email, tokenData.email) || c.authType !== "oauth") return false;
|
|
// For Codex, also check workspaceId to avoid overwriting a different workspace.
|
|
if (provider === "codex" && tokenData.providerSpecificData?.workspaceId) {
|
|
const existingWorkspace = c.providerSpecificData?.workspaceId;
|
|
return safeEqual(existingWorkspace, tokenData.providerSpecificData.workspaceId);
|
|
}
|
|
return true;
|
|
});
|
|
const matchId = typeof match?.id === "string" ? match.id : null;
|
|
if (matchId) {
|
|
connection = await updateProviderConnection(matchId, {
|
|
...tokenData,
|
|
expiresAt,
|
|
testStatus: "active",
|
|
isActive: true,
|
|
});
|
|
}
|
|
}
|
|
if (!connection) {
|
|
connection = await createProviderConnection(
|
|
buildOAuthConnectionCreatePayload(provider, tokenData, expiresAt)
|
|
);
|
|
}
|
|
|
|
await syncToCloudIfEnabled();
|
|
return connection;
|
|
}
|