Request to Corti API
diff --git a/sdk/typescript/next-auth-examples/app/lib/forms.ts b/sdk/typescript/next-auth-examples/app/lib/forms.ts
new file mode 100644
index 0000000..c98e574
--- /dev/null
+++ b/sdk/typescript/next-auth-examples/app/lib/forms.ts
@@ -0,0 +1,52 @@
+export function getRequiredTrimmedFields<
+ T extends Record,
+ const K extends readonly (keyof T)[],
+>(
+ form: T,
+ keys: K,
+): { ok: true; values: { [P in K[number]]: string } } | { ok: false; missing: K[number][] } {
+ const values = {} as { [P in K[number]]: string };
+ const missing: K[number][] = [];
+
+ for (const key of keys) {
+ const value = form[key].trim();
+ if (!value) {
+ missing.push(key);
+ }
+ values[key] = value;
+ }
+
+ if (missing.length > 0) {
+ return { ok: false, missing };
+ }
+
+ return { ok: true, values };
+}
+
+export function getRequiredFormValues(formEl: HTMLFormElement):
+ | {
+ ok: true;
+ values: Record;
+ }
+ | {
+ ok: false;
+ missing: string[];
+ } {
+ const data = new FormData(formEl);
+ const values: Record = {};
+ const missing: string[] = [];
+
+ for (const [key, value] of data.entries()) {
+ const v = String(value).trim();
+ values[key] = v;
+ if (!v) {
+ missing.push(key);
+ }
+ }
+
+ if (missing.length > 0) {
+ return { ok: false, missing };
+ }
+
+ return { ok: true, values };
+}
diff --git a/sdk/typescript/next-auth-examples/app/lib/sessionJson.ts b/sdk/typescript/next-auth-examples/app/lib/sessionJson.ts
new file mode 100644
index 0000000..e63ca01
--- /dev/null
+++ b/sdk/typescript/next-auth-examples/app/lib/sessionJson.ts
@@ -0,0 +1,16 @@
+export function cacheFormValues(key: string, value: unknown): void {
+ sessionStorage.setItem(key, JSON.stringify(value));
+}
+
+export function consumeCachedFormValues(key: string): T | null {
+ const raw = sessionStorage.getItem(key);
+ if (!raw) {
+ return null;
+ }
+ sessionStorage.removeItem(key);
+ try {
+ return JSON.parse(raw) as T;
+ } catch {
+ return null;
+ }
+}
diff --git a/sdk/typescript/next-auth-examples/app/lib/tokenRequest.ts b/sdk/typescript/next-auth-examples/app/lib/tokenRequest.ts
index 6e6a056..6251fd7 100644
--- a/sdk/typescript/next-auth-examples/app/lib/tokenRequest.ts
+++ b/sdk/typescript/next-auth-examples/app/lib/tokenRequest.ts
@@ -7,8 +7,8 @@ export type TokenRequestResult =
export async function requestToken(
url: string,
body: Record,
- environment: string,
- tenant: string,
+ environment: unknown,
+ tenant: unknown,
defaultError: string,
): Promise {
try {
@@ -24,11 +24,12 @@ export async function requestToken(
error: data?.error ?? `Request failed (${res.status})`,
};
}
+
return {
ok: true,
data: data as TokenResponse,
- environment,
- tenant,
+ environment: environment as string,
+ tenant: tenant as string,
};
} catch (e) {
return {
diff --git a/sdk/typescript/next-auth-examples/app/lib/useAuthExampleState.ts b/sdk/typescript/next-auth-examples/app/lib/useAuthExampleState.ts
new file mode 100644
index 0000000..c48dd65
--- /dev/null
+++ b/sdk/typescript/next-auth-examples/app/lib/useAuthExampleState.ts
@@ -0,0 +1,156 @@
+"use client";
+
+import { useCallback, useEffect, useState } from "react";
+import {
+ initialAuthCodeForm,
+ initialForm,
+ initialPkceForm,
+ initialRopcForm,
+} from "@/app/lib/constants";
+import { consumeCachedFormValues } from "@/app/lib/sessionJson";
+import type {
+ AuthCodeFormState,
+ FormState,
+ PkceFormState,
+ RopcFormState,
+ TokenResponse,
+} from "@/app/lib/types";
+
+const AUTH_CODE_SESSION_KEY = "authcode_form";
+const PKCE_SESSION_KEY = "pkce_form";
+
+export type Flow = "cc" | "ropc" | "authCode" | "pkce" | null;
+
+export function useAuthExampleState() {
+ const [flow, setFlow] = useState(null);
+
+ const [form, setForm] = useState(initialForm);
+ const [ropcForm, setRopcForm] = useState(initialRopcForm);
+ const [authCodeForm, setAuthCodeForm] = useState(initialAuthCodeForm);
+ const [pkceForm, setPkceForm] = useState(initialPkceForm);
+
+ const [receivedCode, setReceivedCode] = useState(null);
+ const [pkceReceivedCode, setPkceReceivedCode] = useState(null);
+
+ const [tokenLoading, setTokenLoading] = useState(false);
+ const [tokenError, setTokenError] = useState(null);
+ const [token, setToken] = useState(null);
+ const [tokenEnvTenant, setTokenEnvTenant] = useState<{
+ environment: string;
+ tenant: string;
+ } | null>(null);
+ const [tokenClient, setTokenClient] = useState<{ clientId: string } | null>(null);
+
+ useEffect(() => {
+ const defaultRedirectUri = window.location.origin;
+
+ setAuthCodeForm((prev) => {
+ if (prev.redirectUri) {
+ return prev;
+ }
+
+ return { ...prev, redirectUri: defaultRedirectUri };
+ });
+
+ setPkceForm((prev) => {
+ if (prev.redirectUri) {
+ return prev;
+ }
+
+ return { ...prev, redirectUri: defaultRedirectUri };
+ });
+ }, []);
+
+ useEffect(() => {
+ const params = new URLSearchParams(window.location.search);
+ const code = params.get("code");
+ if (!code) return;
+
+ window.history.replaceState({}, "", window.location.pathname);
+
+ const pkceSaved = consumeCachedFormValues(PKCE_SESSION_KEY);
+ if (pkceSaved) {
+ setFlow("pkce");
+ setPkceForm(pkceSaved);
+ setPkceReceivedCode(code);
+ return;
+ }
+
+ const authCodeSaved = consumeCachedFormValues(AUTH_CODE_SESSION_KEY);
+ if (!authCodeSaved) return;
+
+ setFlow("authCode");
+ setAuthCodeForm(authCodeSaved);
+ setReceivedCode(code);
+ }, []);
+
+ const handleBack = useCallback(() => {
+ setFlow(null);
+ setToken(null);
+ setTokenEnvTenant(null);
+ setTokenClient(null);
+ setTokenError(null);
+ setReceivedCode(null);
+ setPkceReceivedCode(null);
+ }, []);
+
+ const withTokenStates = useCallback(async (fn: () => Promise, defaultError: string) => {
+ setTokenError(null);
+ setTokenLoading(true);
+
+ try {
+ await fn();
+ } catch (e) {
+ setTokenError(e instanceof Error ? e.message : defaultError);
+ } finally {
+ setTokenLoading(false);
+ }
+ }, []);
+
+ const setTokenResult = useCallback(
+ (tokenResponse: TokenResponse, environment: string, tenant: string, clientId?: string) => {
+ setToken(tokenResponse);
+ setTokenEnvTenant({ environment, tenant });
+
+ if (clientId) {
+ setTokenClient({ clientId });
+ } else {
+ setTokenClient(null);
+ }
+ },
+ [],
+ );
+
+ return {
+ flow,
+ setFlow,
+
+ form,
+ setForm,
+ ropcForm,
+ setRopcForm,
+ authCodeForm,
+ setAuthCodeForm,
+ pkceForm,
+ setPkceForm,
+
+ receivedCode,
+ setReceivedCode,
+ pkceReceivedCode,
+ setPkceReceivedCode,
+
+ tokenLoading,
+ setTokenLoading,
+ tokenError,
+ setTokenError,
+ token,
+ setToken,
+ tokenEnvTenant,
+ setTokenEnvTenant,
+ tokenClient,
+
+ handleBack,
+ withTokenStates,
+ setTokenResult,
+ };
+}
diff --git a/sdk/typescript/next-auth-examples/app/lib/utils.ts b/sdk/typescript/next-auth-examples/app/lib/utils.ts
index 04d0f07..9d25f62 100644
--- a/sdk/typescript/next-auth-examples/app/lib/utils.ts
+++ b/sdk/typescript/next-auth-examples/app/lib/utils.ts
@@ -9,9 +9,11 @@ export function isNonEmptyString(v: unknown): v is string {
return typeof v === "string" && v.trim().length > 0;
}
-export async function parseJsonBody(request: Request): Promise {
+export async function parseJsonBody(request: Request): Promise | null> {
try {
- return await request.json();
+ const body = await request.json();
+
+ return typeof body === "object" && body !== null ? (body as Record) : null;
} catch {
return null;
}
diff --git a/sdk/typescript/next-auth-examples/app/page.tsx b/sdk/typescript/next-auth-examples/app/page.tsx
index 7e2ecbd..2e0235a 100644
--- a/sdk/typescript/next-auth-examples/app/page.tsx
+++ b/sdk/typescript/next-auth-examples/app/page.tsx
@@ -2,7 +2,7 @@
import { CortiAuth } from "@corti/sdk";
import type { SubmitEvent } from "react";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback } from "react";
import { AuthCodeCredentialsForm } from "@/app/components/AuthCodeCredentialsForm";
import { AuthCodeReceivedView } from "@/app/components/AuthCodeReceivedView";
import { BackButton } from "@/app/components/BackButton";
@@ -12,42 +12,40 @@ import { PkceCredentialsForm } from "@/app/components/PkceCredentialsForm";
import { RopcCredentialsForm } from "@/app/components/RopcCredentialsForm";
import { SuccessView } from "@/app/components/SuccessView";
import { WarningBanner } from "@/app/components/WarningBanner";
-import {
- initialAuthCodeForm,
- initialForm,
- initialPkceForm,
- initialRopcForm,
-} from "@/app/lib/constants";
+import { getRequiredFormValues } from "@/app/lib/forms";
+import { cacheFormValues } from "@/app/lib/sessionJson";
import { requestToken } from "@/app/lib/tokenRequest";
-import type {
- AuthCodeFormState,
- FormState,
- PkceFormState,
- RopcFormState,
- TokenResponse,
-} from "@/app/lib/types";
+import { useAuthExampleState } from "@/app/lib/useAuthExampleState";
import { useInteractionsList } from "@/app/lib/useInteractionsList";
const AUTH_CODE_SESSION_KEY = "authcode_form";
const PKCE_SESSION_KEY = "pkce_form";
-type Flow = "cc" | "ropc" | "authCode" | "pkce" | null;
-
export default function Home() {
- const [flow, setFlow] = useState(null);
- const [form, setForm] = useState(initialForm);
- const [ropcForm, setRopcForm] = useState(initialRopcForm);
- const [authCodeForm, setAuthCodeForm] = useState(initialAuthCodeForm);
- const [pkceForm, setPkceForm] = useState(initialPkceForm);
- const [receivedCode, setReceivedCode] = useState(null);
- const [pkceReceivedCode, setPkceReceivedCode] = useState(null);
- const [tokenLoading, setTokenLoading] = useState(false);
- const [tokenError, setTokenError] = useState(null);
- const [token, setToken] = useState(null);
- const [tokenEnvTenant, setTokenEnvTenant] = useState<{
- environment: string;
- tenant: string;
- } | null>(null);
+ const {
+ flow,
+ setFlow,
+ form,
+ setForm,
+ ropcForm,
+ setRopcForm,
+ authCodeForm,
+ setAuthCodeForm,
+ pkceForm,
+ setPkceForm,
+ receivedCode,
+ setReceivedCode,
+ pkceReceivedCode,
+ tokenLoading,
+ tokenError,
+ setTokenError,
+ token,
+ tokenEnvTenant,
+ tokenClient,
+ handleBack,
+ withTokenStates,
+ setTokenResult,
+ } = useAuthExampleState();
const {
list: interactionsList,
@@ -59,218 +57,203 @@ export default function Home() {
tokenEnvTenant?.tenant ?? "",
);
- useEffect(() => {
- const params = new URLSearchParams(window.location.search);
- const code = params.get("code");
- if (!code) return;
-
- window.history.replaceState({}, "", window.location.pathname);
-
- const pkceRaw = sessionStorage.getItem(PKCE_SESSION_KEY);
- if (pkceRaw) {
- try {
- const saved = JSON.parse(pkceRaw) as PkceFormState;
- sessionStorage.removeItem(PKCE_SESSION_KEY);
- setFlow("pkce");
- setPkceForm(saved);
- setPkceReceivedCode(code);
- } catch {
- // ignore invalid stored state
- }
- return;
- }
-
- const authCodeRaw = sessionStorage.getItem(AUTH_CODE_SESSION_KEY);
- if (!authCodeRaw) return;
-
- try {
- const saved = JSON.parse(authCodeRaw) as AuthCodeFormState;
- sessionStorage.removeItem(AUTH_CODE_SESSION_KEY);
- setFlow("authCode");
- setAuthCodeForm(saved);
- setReceivedCode(code);
- } catch {
- // ignore invalid stored state
- }
- }, []);
-
const handleSubmit = useCallback(
- async (e: SubmitEvent) => {
+ async (e: SubmitEvent) => {
e.preventDefault();
- const clientId = form.clientId.trim();
- const clientSecret = form.clientSecret.trim();
- const environment = form.environment.trim();
- const tenant = form.tenant.trim();
- if (!clientId || !clientSecret || !environment || !tenant) {
+
+ const required = getRequiredFormValues(e.currentTarget);
+
+ if (!required.ok) {
setTokenError("All fields are required.");
return;
}
- setTokenError(null);
- setTokenLoading(true);
- const result = await requestToken(
- "/api/auth/token",
- { clientId, clientSecret, environment, tenant },
- environment,
- tenant,
- "Failed to get token",
- );
- setTokenLoading(false);
- if (result.ok) {
- setToken(result.data);
- setTokenEnvTenant({ environment: result.environment, tenant: result.tenant });
- } else {
- setTokenError(result.error);
- }
+
+ await withTokenStates(async () => {
+ const result = await requestToken(
+ "/api/auth/token",
+ required.values,
+ required.values.environment,
+ required.values.tenant,
+ "Failed to get token",
+ );
+
+ if (!result.ok) {
+ throw new Error(result.error);
+ }
+
+ setTokenResult(result.data, result.environment, result.tenant, required.values.clientId);
+ }, "Failed to get token");
},
- [form],
+ [setTokenError, setTokenResult, withTokenStates],
);
const handleRopcSubmit = useCallback(
- async (e: SubmitEvent) => {
+ async (e: SubmitEvent) => {
e.preventDefault();
- const clientId = ropcForm.clientId.trim();
- const environment = ropcForm.environment.trim();
- const tenant = ropcForm.tenant.trim();
- const username = ropcForm.username.trim();
- const password = ropcForm.password.trim();
- if (!clientId || !environment || !tenant || !username || !password) {
+ const required = getRequiredFormValues(e.currentTarget);
+
+ if (!required.ok) {
setTokenError("All fields are required.");
return;
}
- setTokenError(null);
- setTokenLoading(true);
- const result = await requestToken(
- "/api/auth/token/ropc",
- { clientId, environment, tenant, username, password },
- environment,
- tenant,
- "Failed to get token",
- );
- setTokenLoading(false);
- if (result.ok) {
- setToken(result.data);
- setTokenEnvTenant({ environment: result.environment, tenant: result.tenant });
- } else {
- setTokenError(result.error);
- }
+
+ await withTokenStates(async () => {
+ const result = await requestToken(
+ "/api/auth/token/ropc",
+ required.values,
+ required.values.environment,
+ required.values.tenant,
+ "Failed to get token",
+ );
+
+ if (!result.ok) {
+ throw new Error(result.error);
+ }
+
+ setTokenResult(result.data, result.environment, result.tenant, required.values.clientId);
+ }, "Failed to get token");
},
- [ropcForm],
+ [setTokenError, setTokenResult, withTokenStates],
);
const handleAuthCodeSubmit = useCallback(
- async (e: SubmitEvent) => {
+ async (e: SubmitEvent) => {
e.preventDefault();
- const clientId = authCodeForm.clientId.trim();
- const clientSecret = authCodeForm.clientSecret.trim();
- const environment = authCodeForm.environment.trim();
- const tenant = authCodeForm.tenant.trim();
- const redirectUri = authCodeForm.redirectUri.trim();
- if (!clientId || !clientSecret || !environment || !tenant || !redirectUri) {
+ const required = getRequiredFormValues(e.currentTarget);
+
+ if (!required.ok) {
setTokenError("All fields are required.");
return;
}
+
setTokenError(null);
- sessionStorage.setItem(
- AUTH_CODE_SESSION_KEY,
- JSON.stringify({ clientId, clientSecret, environment, tenant, redirectUri }),
- );
- const cortiAuth = new CortiAuth({ tenantName: tenant, environment });
- await cortiAuth.authorizeUrl({ clientId, redirectUri });
+ cacheFormValues(AUTH_CODE_SESSION_KEY, required.values);
+
+ const cortiAuth = new CortiAuth({
+ tenantName: required.values.tenant,
+ environment: required.values.environment,
+ });
+
+ await cortiAuth.authorizeURL({
+ clientId: required.values.clientId,
+ redirectUri: required.values.redirectUri,
+ });
},
- [authCodeForm],
+ [setTokenError],
);
const handlePkceSubmit = useCallback(
- async (e: SubmitEvent) => {
+ async (e: SubmitEvent) => {
e.preventDefault();
- const clientId = pkceForm.clientId.trim();
- const environment = pkceForm.environment.trim();
- const tenant = pkceForm.tenant.trim();
- const redirectUri = pkceForm.redirectUri.trim();
- if (!clientId || !environment || !tenant || !redirectUri) {
+ const required = getRequiredFormValues(e.currentTarget);
+
+ if (!required.ok) {
setTokenError("All fields are required.");
return;
}
+
setTokenError(null);
- sessionStorage.setItem(
- PKCE_SESSION_KEY,
- JSON.stringify({ clientId, environment, tenant, redirectUri }),
- );
- const cortiAuth = new CortiAuth({ tenantName: tenant, environment });
- await cortiAuth.authorizePkceUrl({ clientId, redirectUri });
+ cacheFormValues(PKCE_SESSION_KEY, required.values);
+
+ const cortiAuth = new CortiAuth({
+ tenantName: required.values.tenant,
+ environment: required.values.environment,
+ });
+
+ await cortiAuth.authorizePkceUrl({
+ clientId: required.values.clientId,
+ redirectUri: required.values.redirectUri,
+ });
},
- [pkceForm],
+ [setTokenError],
);
const handlePkceProceed = useCallback(async () => {
if (!pkceReceivedCode) {
return;
}
- setTokenError(null);
- setTokenLoading(true);
- const codeVerifier = CortiAuth.getCodeVerifier();
- const result = await requestToken(
- "/api/auth/token/pkce",
- {
- clientId: pkceForm.clientId,
+
+ await withTokenStates(async () => {
+ const cortiAuth = new CortiAuth({
+ tenantName: pkceForm.tenant,
environment: pkceForm.environment,
- tenant: pkceForm.tenant,
+ });
+
+ const tokenResponse = await cortiAuth.getPkceFlowToken({
+ clientId: pkceForm.clientId,
code: pkceReceivedCode,
redirectUri: pkceForm.redirectUri,
- codeVerifier,
- },
- pkceForm.environment,
- pkceForm.tenant,
- "Failed to exchange PKCE authorization code",
- );
- setTokenLoading(false);
- if (result.ok) {
- setPkceReceivedCode(null);
- setToken(result.data);
- setTokenEnvTenant({ environment: result.environment, tenant: result.tenant });
- } else {
- setTokenError(result.error);
- }
- }, [pkceReceivedCode, pkceForm]);
+ });
+
+ setTokenResult(tokenResponse, pkceForm.environment, pkceForm.tenant, pkceForm.clientId);
+ }, "Failed to exchange PKCE authorization code");
+ }, [pkceForm, pkceReceivedCode, setTokenResult, withTokenStates]);
const handleAuthCodeProceed = useCallback(async () => {
if (!receivedCode) {
return;
}
- setTokenError(null);
- setTokenLoading(true);
- const result = await requestToken(
- "/api/auth/token/authcode",
- {
- clientId: authCodeForm.clientId,
- clientSecret: authCodeForm.clientSecret,
- environment: authCodeForm.environment,
- tenant: authCodeForm.tenant,
- code: receivedCode,
- redirectUri: authCodeForm.redirectUri,
- },
- authCodeForm.environment,
- authCodeForm.tenant,
- "Failed to exchange authorization code",
- );
- setTokenLoading(false);
- if (result.ok) {
+ await withTokenStates(async () => {
+ const result = await requestToken(
+ "/api/auth/token/authcode",
+ {
+ clientId: authCodeForm.clientId,
+ clientSecret: authCodeForm.clientSecret,
+ environment: authCodeForm.environment,
+ tenant: authCodeForm.tenant,
+ code: receivedCode,
+ redirectUri: authCodeForm.redirectUri,
+ },
+ authCodeForm.environment,
+ authCodeForm.tenant,
+ "Failed to exchange authorization code",
+ );
+
+ if (!result.ok) {
+ throw new Error(result.error);
+ }
+
+ setTokenResult(result.data, result.environment, result.tenant, authCodeForm.clientId);
setReceivedCode(null);
- setToken(result.data);
- setTokenEnvTenant({ environment: result.environment, tenant: result.tenant });
- } else {
- setTokenError(result.error);
+ }, "Failed to exchange authorization code");
+ }, [authCodeForm, receivedCode, setReceivedCode, setTokenResult, withTokenStates]);
+
+ const handleRefreshToken = useCallback(() => {
+ const canRefresh =
+ token != null &&
+ token.refreshToken != null &&
+ tokenEnvTenant != null &&
+ tokenClient != null &&
+ !!tokenClient.clientId;
+
+ if (!canRefresh) {
+ return;
}
- }, [receivedCode, authCodeForm]);
- const handleBack = useCallback(() => {
- setFlow(null);
- setToken(null);
- setTokenEnvTenant(null);
- setTokenError(null);
- setReceivedCode(null);
- setPkceReceivedCode(null);
- }, []);
+ void withTokenStates(async () => {
+ const refreshToken = token.refreshToken;
+ if (!refreshToken) {
+ return;
+ }
+
+ const cortiAuth = new CortiAuth({
+ tenantName: tokenEnvTenant.tenant,
+ environment: tokenEnvTenant.environment,
+ });
+
+ const tokenResponse = await cortiAuth.refreshToken({
+ clientId: tokenClient.clientId,
+ refreshToken,
+ });
+
+ setTokenResult(
+ tokenResponse,
+ tokenEnvTenant.environment,
+ tokenEnvTenant.tenant,
+ tokenClient.clientId,
+ );
+ }, "Failed to refresh token");
+ }, [setTokenResult, token, tokenClient, tokenEnvTenant, withTokenStates]);
const showBack = flow != null || token != null;
@@ -350,6 +333,8 @@ export default function Home() {
interactionsList={interactionsList}
interactionsLoading={interactionsLoading}
interactionsError={interactionsError}
+ onRefreshToken={handleRefreshToken}
+ refreshTokenLoading={tokenLoading}
/>
)}