Skip to content

Commit

Permalink
add skew protection to all api routes
Browse files Browse the repository at this point in the history
  • Loading branch information
abvthecity committed Oct 11, 2024
1 parent 718429f commit 286a1b1
Show file tree
Hide file tree
Showing 8 changed files with 53 additions and 40 deletions.
6 changes: 2 additions & 4 deletions packages/ui/app/src/atoms/launchdarkly.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { atom, useAtomValue, useSetAtom } from "jotai";
import * as LDClient from "launchdarkly-js-client-sdk";
import { useCallback, useEffect, useState } from "react";
import useSWR from "swr";
import { useApiRoute } from "../hooks/useApiRoute";
import { useApiRouteSWR } from "../hooks/useApiRouteSWR";

// NOTE do not export this file in any index.ts file so that it can be properly tree-shaken
// otherwise we risk importing launchdarkly-js-client-sdk in all of our bundles
Expand Down Expand Up @@ -85,9 +84,8 @@ export const useLaunchDarklyFlag = (flag: string, equals = true, not = false): b

// since useSWR is cached globally, we can use this hook in multiple components without worrying about multiple requests
function useInitLaunchDarklyClient() {
const route = useApiRoute("/api/fern-docs/integrations/launchdarkly");
const setInfo = useSetAtom(SET_LAUNCH_DARKLY_INFO_ATOM);
useSWR(route, (key): Promise<LaunchDarklyInfo> => fetch(key).then((res) => res.json()), {
useApiRouteSWR<LaunchDarklyInfo>("/api/fern-docs/integrations/launchdarkly", {
onSuccess(data) {
void setInfo(data);
},
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/app/src/hooks/useApiRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Getter, useAtomValue } from "jotai";
import urlJoin from "url-join";
import { BASEPATH_ATOM, TRAILING_SLASH_ATOM } from "../atoms";

type FernDocsApiRoute = `/api/fern-docs/${string}`;
export type FernDocsApiRoute = `/api/fern-docs/${string}`;

interface Options {
includeTrailingSlash?: boolean;
Expand Down
28 changes: 28 additions & 0 deletions packages/ui/app/src/hooks/useApiRouteSWR.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import useSWR, { Fetcher, SWRConfiguration, SWRResponse } from "swr";
import useSWRImmutable from "swr/immutable";
import { withSkewProtection } from "../util/withSkewProtection";
import { FernDocsApiRoute, useApiRoute } from "./useApiRoute";

export function useApiRouteSWR<T>(
route: FernDocsApiRoute,
options?: SWRConfiguration<T, Error, Fetcher<T>> & { disabled?: boolean },
): SWRResponse<T> {
const key = useApiRoute(route);
return useSWR(
options?.disabled ? null : key,
(url): Promise<T> => fetch(url, { headers: withSkewProtection() }).then((r) => r.json()),
options,
);
}

export function useApiRouteSWRImmutable<T>(
route: FernDocsApiRoute,
options?: SWRConfiguration<T, Error, Fetcher<T>> & { disabled?: boolean },
): SWRResponse<T> {
const key = useApiRoute(route);
return useSWRImmutable(
options?.disabled ? null : key,
(url): Promise<T> => fetch(url, { headers: withSkewProtection() }).then((r) => r.json()),
options,
);
}
9 changes: 3 additions & 6 deletions packages/ui/app/src/playground/hooks/useEndpointContext.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
import { createEndpointContext, type ApiDefinition, type EndpointContext } from "@fern-api/fdr-sdk/api-definition";
import type * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { useMemo } from "react";
import useSWRImmutable from "swr/immutable";
import { useWriteApiDefinitionAtom } from "../../atoms";
import { useApiRoute } from "../../hooks/useApiRoute";
import { useApiRouteSWRImmutable } from "../../hooks/useApiRouteSWR";

interface LoadableEndpointContext {
context: EndpointContext | undefined;
isLoading: boolean;
}

const fetcher = (url: string): Promise<ApiDefinition> => fetch(url).then((res) => res.json());

/**
* This hook leverages SWR to fetch and cache the definition for this endpoint.
* It should be refactored to store the resulting endpoint in a global state, so that it can be shared between components.
*/
export function useEndpointContext(node: FernNavigation.EndpointNode | undefined): LoadableEndpointContext {
const route = useApiRoute(
const { data: apiDefinition, isLoading } = useApiRouteSWRImmutable<ApiDefinition>(
`/api/fern-docs/api-definition/${encodeURIComponent(node?.apiDefinitionId ?? "")}/endpoint/${encodeURIComponent(node?.endpointId ?? "")}`,
{ disabled: node == null },
);
const { data: apiDefinition, isLoading } = useSWRImmutable(node != null ? route : null, fetcher);
const context = useMemo(() => createEndpointContext(node, apiDefinition), [node, apiDefinition]);
useWriteApiDefinitionAtom(apiDefinition);

Expand Down
9 changes: 3 additions & 6 deletions packages/ui/app/src/playground/hooks/useWebSocketContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,23 @@ import { createWebSocketContext } from "@fern-api/fdr-sdk/api-definition";
import type * as FernNavigation from "@fern-api/fdr-sdk/navigation";
import { useSetAtom } from "jotai";
import { useEffect, useMemo } from "react";
import useSWRImmutable from "swr/immutable";
import { WRITE_API_DEFINITION_ATOM } from "../../atoms";
import { useApiRoute } from "../../hooks/useApiRoute";
import { useApiRouteSWRImmutable } from "../../hooks/useApiRouteSWR";

interface LoadableWebSocketContext {
context: WebSocketContext | undefined;
isLoading: boolean;
}

const fetcher = (url: string): Promise<ApiDefinition> => fetch(url).then((res) => res.json());

/**
* This hook leverages SWR to fetch and cache the definition for this endpoint.
* It should be refactored to store the resulting endpoint in a global state, so that it can be shared between components.
*/
export function useWebSocketContext(node: FernNavigation.WebSocketNode): LoadableWebSocketContext {
const route = useApiRoute(
const { data: apiDefinition, isLoading } = useApiRouteSWRImmutable<ApiDefinition>(
`/api/fern-docs/api-definition/${encodeURIComponent(node.apiDefinitionId)}/websocket/${encodeURIComponent(node.webSocketId)}`,
{ disabled: node == null },
);
const { data: apiDefinition, isLoading } = useSWRImmutable(route, fetcher);
const context = useMemo(() => createWebSocketContext(node, apiDefinition), [node, apiDefinition]);

const set = useSetAtom(WRITE_API_DEFINITION_ATOM);
Expand Down
17 changes: 4 additions & 13 deletions packages/ui/app/src/services/useApiKeyInjectionConfig.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
import type { APIKeyInjectionConfig } from "@fern-ui/fern-docs-auth";
import useSWR from "swr";
import { useApiRoute } from "../hooks/useApiRoute";
import { useApiRouteSWR } from "../hooks/useApiRouteSWR";

const DEFAULT = { enabled: false as const };

export function useApiKeyInjectionConfig(): APIKeyInjectionConfig {
const key = useApiRoute("/api/fern-docs/auth/api-key-injection");
const { data } = useSWR<APIKeyInjectionConfig>(
key,
async (url: string) => {
const res = await fetch(url);
return res.json();
},
{
refreshInterval: (latestData) => (latestData?.enabled ? 1000 * 60 * 5 : 0), // refresh every 5 minutes
},
);
const { data } = useApiRouteSWR<APIKeyInjectionConfig>("/api/fern-docs/auth/api-key-injection", {
refreshInterval: (latestData) => (latestData?.enabled ? 1000 * 60 * 5 : 0), // refresh every 5 minutes
});
return data ?? DEFAULT;
}
13 changes: 3 additions & 10 deletions packages/ui/app/src/services/useSearchService.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
/* eslint-disable react-hooks/rules-of-hooks */
import type { SearchConfig } from "@fern-ui/search-utils";
import { useCallback } from "react";
import useSWR, { mutate } from "swr";
import { noop } from "ts-essentials";
import { useIsLocalPreview } from "../contexts/local-preview";
import { useApiRoute } from "../hooks/useApiRoute";
import { useApiRouteSWR } from "../hooks/useApiRouteSWR";

export type SearchCredentials = {
appId: string;
Expand Down Expand Up @@ -32,15 +30,10 @@ export function useSearchConfig(): [SearchConfig, refresh: () => void] {
return [{ isAvailable: false }, noop];
}

const key = useApiRoute("/api/fern-docs/search");
const { data } = useSWR<SearchConfig>(key, (url: string) => fetch(url).then((res) => res.json()), {
const { data } = useApiRouteSWR<SearchConfig>("/api/fern-docs/search", {
refreshInterval: 1000 * 60 * 60 * 2, // 2 hours
revalidateOnFocus: false,
});

const refresh = useCallback(() => {
void mutate(key);
}, [key]);

return [data ?? { isAvailable: false }, refresh];
return [data ?? { isAvailable: false }, noop];
}
9 changes: 9 additions & 0 deletions packages/ui/app/src/util/withSkewProtection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function withSkewProtection(headers?: HeadersInit): HeadersInit | undefined {
if (process.env.NEXT_DEPLOYMENT_ID == null) {
return headers;
}

const h = new Headers(headers);
h.set("x-deployment-id", process.env.NEXT_DEPLOYMENT_ID);
return h;
}

0 comments on commit 286a1b1

Please sign in to comment.