refactor(web): centralize OAuth callback cookie contract (fixes #1362)

Replace the duplicated `OAUTH_RESULT_COOKIE` constant and inline payload
type across the callback route and connector dialog hook with a shared
`contracts/types/oauth.types.ts` module that exports:

- OAUTH_RESULT_COOKIE constant
- oauthCallbackResultSchema Zod schema
- OAuthCallbackResult type (inferred from the schema)
- parseOAuthCallbackResult() helper that returns null on invalid JSON
  or shape mismatch

The route handler now uses the shared type to constrain the cookie
payload at compile time. The consumer hook validates the cookie value
through the helper instead of an unchecked JSON.parse, removing the
silent runtime risk when the cookie is tampered with or its shape
drifts.
This commit is contained in:
suryo12 2026-05-23 23:22:18 +07:00
parent fc3b39483b
commit aa86534a52
3 changed files with 39 additions and 18 deletions

View file

@ -1,6 +1,5 @@
import { type NextRequest, NextResponse } from "next/server";
const OAUTH_RESULT_COOKIE = "connector_oauth_result";
import { OAUTH_RESULT_COOKIE, type OAuthCallbackResult } from "@/contracts/types/oauth.types";
export async function GET(
request: NextRequest,
@ -9,12 +8,13 @@ export async function GET(
const { search_space_id } = await params;
const searchParams = request.nextUrl.searchParams;
const result = JSON.stringify({
const payload: OAuthCallbackResult = {
success: searchParams.get("success"),
error: searchParams.get("error"),
connector: searchParams.get("connector"),
connectorId: searchParams.get("connectorId"),
});
};
const result = JSON.stringify(payload);
const redirectUrl = new URL(`/dashboard/${search_space_id}/new-chat`, request.url);

View file

@ -14,7 +14,9 @@ import { activeSearchSpaceIdAtom } from "@/atoms/search-spaces/search-space-quer
import { EnumConnectorName } from "@/contracts/enums/connector";
import type { SearchSourceConnector } from "@/contracts/types/connector.types";
import { searchSourceConnector } from "@/contracts/types/connector.types";
import { OAUTH_RESULT_COOKIE, parseOAuthCallbackResult } from "@/contracts/types/oauth.types";
import { authenticatedFetch } from "@/lib/auth-utils";
import { BACKEND_URL } from "@/lib/env-config";
import {
trackConnectorConnected,
trackConnectorDeleted,
@ -36,15 +38,12 @@ import {
OAUTH_CONNECTORS,
OTHER_CONNECTORS,
} from "../constants/connector-constants";
import {
dateRangeSchema,
frequencyMinutesSchema,
parseOAuthAuthResponse,
validateIndexingConfigState,
} from "../constants/connector-popup.schemas";
import { BACKEND_URL } from "@/lib/env-config";
const OAUTH_RESULT_COOKIE = "connector_oauth_result";
function readOAuthResultCookie(): string | null {
const match = document.cookie
@ -211,17 +210,8 @@ export const useConnectorDialog = () => {
if (!raw || !searchSpaceId) return;
clearOAuthResultCookie();
let result: {
success: string | null;
error: string | null;
connector: string | null;
connectorId: string | null;
};
try {
result = JSON.parse(raw);
} catch {
return;
}
const result = parseOAuthCallbackResult(raw);
if (!result) return;
if (result.error) {
const oauthConnector = result.connector

View file

@ -0,0 +1,31 @@
import { z } from "zod";
export const OAUTH_RESULT_COOKIE = "connector_oauth_result";
/**
* Schema for the payload written to the `connector_oauth_result` cookie by the
* OAuth callback route and read back by the connector dialog hook.
*/
export const oauthCallbackResultSchema = z.object({
success: z.string().nullable(),
error: z.string().nullable(),
connector: z.string().nullable(),
connectorId: z.string().nullable(),
});
export type OAuthCallbackResult = z.infer<typeof oauthCallbackResultSchema>;
/**
* Safely decode and validate the OAuth callback cookie value. Returns `null`
* when the value is not valid JSON or does not match the expected shape.
*/
export function parseOAuthCallbackResult(raw: string): OAuthCallbackResult | null {
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return null;
}
const result = oauthCallbackResultSchema.safeParse(parsed);
return result.success ? result.data : null;
}