diff --git a/examples/nextjs-example/package-lock.json b/examples/nextjs-example/package-lock.json index 0dbd72cb..c8c2c4c7 100644 --- a/examples/nextjs-example/package-lock.json +++ b/examples/nextjs-example/package-lock.json @@ -18,7 +18,8 @@ "next": "15.5.4", "react": "19.1.0", "react-dom": "19.1.0", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "usehooks-ts": "^3.1.1" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -38,7 +39,7 @@ }, "../..": { "name": "@openrouter/sdk", - "version": "0.0.0-beta.9", + "version": "0.0.0-beta.38", "dependencies": { "zod": "^3.20.0" }, @@ -50,7 +51,8 @@ "globals": "^15.14.0", "tshy": "^2.0.0", "typescript": "~5.8.3", - "typescript-eslint": "^8.26.0" + "typescript-eslint": "^8.26.0", + "vitest": "^3.2.4" }, "peerDependencies": { "@tanstack/react-query": "^5", @@ -5124,6 +5126,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6780,6 +6788,21 @@ } } }, + "node_modules/usehooks-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", + "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==", + "license": "MIT", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/examples/nextjs-example/package.json b/examples/nextjs-example/package.json index 6cd64bdc..31273f15 100644 --- a/examples/nextjs-example/package.json +++ b/examples/nextjs-example/package.json @@ -19,7 +19,8 @@ "next": "15.5.4", "react": "19.1.0", "react-dom": "19.1.0", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "usehooks-ts": "^3.1.1" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/examples/nextjs-example/src/app/chat/_dialogs/not-connected.tsx b/examples/nextjs-example/src/app/(app)/chat/_dialogs/not-connected.tsx similarity index 64% rename from examples/nextjs-example/src/app/chat/_dialogs/not-connected.tsx rename to examples/nextjs-example/src/app/(app)/chat/_dialogs/not-connected.tsx index 68b0b9b5..b9d4aa64 100644 --- a/examples/nextjs-example/src/app/chat/_dialogs/not-connected.tsx +++ b/examples/nextjs-example/src/app/(app)/chat/_dialogs/not-connected.tsx @@ -8,7 +8,8 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { getOAuthKeyUrl } from "@/lib/config"; +import { OAUTH_CALLBACK_URL, OPENROUTER_CODE_VERIFIER_KEY } from "@/lib/config"; +import { useOpenRouter } from "@/lib/hooks/use-openrouter-client"; import { ExternalLink } from "lucide-react"; interface NotConnectedDialogProps { @@ -16,6 +17,22 @@ interface NotConnectedDialogProps { } export function NotConnectedDialog({ open }: NotConnectedDialogProps) { + const { client } = useOpenRouter(); + + const handleGotoOAuth = async () => { + const { codeChallenge, codeVerifier } = + await client.oAuth.createSHA256CodeChallenge(); + + const url = await client.oAuth.createAuthorizationUrl({ + codeChallenge, + callbackUrl: OAUTH_CALLBACK_URL, + codeChallengeMethod: "S256", + }); + + localStorage.setItem(OPENROUTER_CODE_VERIFIER_KEY, codeVerifier); + window.location.href = url; + }; + return ( {}}> - ); } - diff --git a/examples/nextjs-example/src/app/chat/page.tsx b/examples/nextjs-example/src/app/(app)/chat/page.tsx similarity index 83% rename from examples/nextjs-example/src/app/chat/page.tsx rename to examples/nextjs-example/src/app/(app)/chat/page.tsx index a29c8fd8..894fbb70 100644 --- a/examples/nextjs-example/src/app/chat/page.tsx +++ b/examples/nextjs-example/src/app/(app)/chat/page.tsx @@ -2,7 +2,6 @@ import type React from "react"; -import { useState, useRef, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -13,12 +12,11 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { MessageSquare, Send, Settings, User, Bot } from "lucide-react"; -import { OpenRouterCore } from "@openrouter/sdk/core"; -import { chatSend, SendAcceptEnum } from "@openrouter/sdk/funcs/chatSend"; -import { OPENROUTER_KEY_LOCALSTORAGE_KEY } from "@/lib/config"; +import { useApiKey } from "@/lib/hooks/use-api-key"; +import { useOpenRouter } from "@/lib/hooks/use-openrouter-client"; import { Message as OpenRouterMessageRequest } from "@openrouter/sdk/models"; -import useLocalStorage from "@/lib/hooks/use-local-storage"; +import { Bot, MessageSquare, Send, Settings, User } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; import { NotConnectedDialog } from "./_dialogs/not-connected"; type Message = OpenRouterMessageRequest & { @@ -27,9 +25,8 @@ type Message = OpenRouterMessageRequest & { }; export default function Page() { - const { value: apiKey, isPending: isApiKeyPending } = useLocalStorage< - string | null - >(OPENROUTER_KEY_LOCALSTORAGE_KEY, null); + const { client: openRouter } = useOpenRouter(); + const [apiKey] = useApiKey(); const [messages, setMessages] = useState([ { @@ -39,11 +36,12 @@ export default function Page() { "Hello! I'm your AI assistant powered by OpenRouter. I can help you with a wide variety of tasks. What would you like to know or discuss today?", }, ]); - const [input, setInput] = useState(""); const [selectedModel, setSelectedModel] = useState("gpt-4"); const [isLoading, setIsLoading] = useState(false); const messagesEndRef = useRef(null); + const inputRef = useRef(null); + const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }; @@ -53,6 +51,9 @@ export default function Page() { }, [messages]); const handleSend = async () => { + const input = inputRef.current?.value; + if (!input) return; + setIsLoading(true); if (!input.trim() || isLoading) return; @@ -72,7 +73,7 @@ export default function Page() { ]; setMessages(updatedMessages); - setInput(""); + inputRef.current!.value = ""; // Add an empty assistant message to stream into const assistantMessage: Message = { @@ -88,27 +89,16 @@ export default function Page() { throw new Error("API key is required but not present."); } - const openRouter = new OpenRouterCore({ apiKey }); - - const result = await chatSend( - openRouter, - { - model: "openai/gpt-4o", - maxTokens: 1000, - messages: updatedMessages, - stream: true, - }, - { acceptHeaderOverride: SendAcceptEnum.textEventStream }, - ); - - if (!result.ok) { - alert("Error: " + result.error.message); - return setIsLoading(false); - } + const result = await openRouter.chat.send({ + model: "openai/gpt-4o", + maxTokens: 1000, + messages: updatedMessages, + stream: true, + }); // Stream chunks into the latest message const chunks: string[] = []; - for await (const chunk of result.value) { + for await (const chunk of result) { chunks.push(chunk.data.choices[0].delta.content || ""); setMessages((prev) => { const newMessages = [...prev]; @@ -134,7 +124,7 @@ export default function Page() { return ( <> - +
{/* Header */}
@@ -221,18 +211,13 @@ export default function Page() {
setInput(e.target.value)} + ref={inputRef} onKeyPress={handleKeyPress} placeholder="Type your message..." className="flex-1" disabled={isLoading} /> -
diff --git a/examples/nextjs-example/src/app/(app)/layout.tsx b/examples/nextjs-example/src/app/(app)/layout.tsx new file mode 100644 index 00000000..da6fa9c1 --- /dev/null +++ b/examples/nextjs-example/src/app/(app)/layout.tsx @@ -0,0 +1,11 @@ +"use client"; + +import { OpenRouterClientProvider } from "@/lib/hooks/use-openrouter-client"; + +export default function Layout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return {children}; +} diff --git a/examples/nextjs-example/src/app/page.tsx b/examples/nextjs-example/src/app/(app)/page.tsx similarity index 72% rename from examples/nextjs-example/src/app/page.tsx rename to examples/nextjs-example/src/app/(app)/page.tsx index eb6b4de2..ba5d2db5 100644 --- a/examples/nextjs-example/src/app/page.tsx +++ b/examples/nextjs-example/src/app/(app)/page.tsx @@ -10,10 +10,12 @@ import { CardTitle, } from "@/components/ui/card"; import { - getOAuthKeyUrl, + OAUTH_CALLBACK_URL, + OPENROUTER_CODE_VERIFIER_KEY, OPENROUTER_KEY_LOCALSTORAGE_KEY, - OPENROUTER_USER_ID_LOCALSTORAGE_KEY, } from "@/lib/config"; +import { useApiKey } from "@/lib/hooks/use-api-key"; +import { useOpenRouter } from "@/lib/hooks/use-openrouter-client"; import { ArrowRightIcon, ExternalLink, @@ -21,11 +23,9 @@ import { MessageSquare, Zap, } from "lucide-react"; -import { useEffect, useState } from "react"; -import { OpenRouterCore } from "@openrouter/sdk/core"; -import { oAuthPostAuthKeys } from "@openrouter/sdk/funcs/oAuthPostAuthKeys"; -import { useRouter } from "next/navigation"; import Link from "next/link"; +import { useRouter } from "next/navigation"; +import { useEffect, useState } from "react"; export default function Page({ searchParams }: PageProps<"/">) { const [connectionState, setConnectionState] = useState< @@ -77,24 +77,45 @@ function InitializingPageContent() { function ConnectingPageContent(props: { code: string }) { const router = useRouter(); - const openRouter = new OpenRouterCore(); + const { client: openRouter } = useOpenRouter(); + const [, setApiKey] = useApiKey(); + + useEffect(() => { + const exchangeCode = async () => { + const codeVerifier = localStorage.getItem(OPENROUTER_CODE_VERIFIER_KEY); - oAuthPostAuthKeys(openRouter, { code: props.code }).then((result) => { - if (!result.ok) return; + if (!codeVerifier) { + console.error("Code verifier not found in localStorage"); + router.push("/?error=missing_verifier"); + return; + } - if ("key" in result.value) { - localStorage.setItem(OPENROUTER_KEY_LOCALSTORAGE_KEY, result.value.key); - } + try { + // Exchange the authorization code for an API key using PKCE + const result = await openRouter.oAuth.exchangeAuthorizationCode({ + code: props.code, + codeVerifier, + codeChallengeMethod: "S256", + }); - if (result.value.userId) { - localStorage.setItem( - OPENROUTER_USER_ID_LOCALSTORAGE_KEY, - result.value.userId, - ); - } + // Store the key and user ID + if (result.key) { + setApiKey(result.key); + } + + // Clean up the code verifier + localStorage.removeItem(OPENROUTER_CODE_VERIFIER_KEY); + + // Redirect to chat + router.push("/chat"); + } catch (error) { + console.error("Failed to exchange authorization code:", error); + router.push("/?error=exchange_failed"); + } + }; - router.push("/chat"); - }); + exchangeCode(); + }, [props.code, router]); return (
@@ -152,6 +173,33 @@ function ConnectedPageContent() { } function DisconnectedPageContent() { + const [authUrl, setAuthUrl] = useState(null); + const { client: openRouter } = useOpenRouter(); + + useEffect(() => { + const generateAuthUrl = async () => { + // Generate PKCE code challenge + const challenge = await openRouter.oAuth.createSHA256CodeChallenge(); + + // Store the code verifier for later use in the callback + localStorage.setItem( + OPENROUTER_CODE_VERIFIER_KEY, + challenge.codeVerifier, + ); + + // Generate authorization URL with PKCE + const url = await openRouter.oAuth.createAuthorizationUrl({ + callbackUrl: OAUTH_CALLBACK_URL, + codeChallenge: challenge.codeChallenge, + codeChallengeMethod: "S256", + }); + + setAuthUrl(url); + }; + + generateAuthUrl(); + }, []); + return (
@@ -171,7 +219,8 @@ function DisconnectedPageContent() { OpenRouter Integration Demo
- This app demonstrates how to connect to OpenRouter using OAuth 2.0. + This app demonstrates how to connect to OpenRouter using OAuth 2.0 + with PKCE for enhanced security. @@ -191,7 +240,7 @@ function DisconnectedPageContent() {

Secure OAuth

- Safe authentication flow + Safe authentication with PKCE

@@ -206,14 +255,20 @@ function DisconnectedPageContent() {
- - + + ) : ( + - + )}
diff --git a/examples/nextjs-example/src/app/layout.tsx b/examples/nextjs-example/src/app/layout.tsx index f7fa87eb..ec03ad13 100644 --- a/examples/nextjs-example/src/app/layout.tsx +++ b/examples/nextjs-example/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; diff --git a/examples/nextjs-example/src/app/oauth/callback/route.ts b/examples/nextjs-example/src/app/oauth/callback/route.ts index 5e733eff..f2167de8 100644 --- a/examples/nextjs-example/src/app/oauth/callback/route.ts +++ b/examples/nextjs-example/src/app/oauth/callback/route.ts @@ -1,34 +1,14 @@ import { redirect } from "next/navigation"; -import { cookies } from "next/headers"; export async function GET(request: Request) { - const codeParam = new URL(request.url).searchParams.get("code"); + const url = new URL(request.url); + const code = url.searchParams.get("code"); - if (codeParam === null) { - return new Response("Missing code parameter", { status: 400 }); + if (!code) { + return redirect("/?error=missing_code"); } - const response = await fetch("https://openrouter.ai/api/v1/auth/keys", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - code: codeParam, - }), - }); - - const json = await response.json(); - - const cookieStore = await cookies(); - - cookieStore.set("openrouter_key", json.key, { - maxAge: 3_600, // 1 hour - }); - - cookieStore.set("openrouter_user", json.user_id, { - maxAge: 3_600, // 1 hour - }); - - redirect("/"); + // Redirect to the main page with the code + // The client-side code will handle the exchange using the stored code verifier + redirect(`/?code=${code}`); } diff --git a/examples/nextjs-example/src/lib/config.ts b/examples/nextjs-example/src/lib/config.ts index 2660ec6a..7abe3124 100644 --- a/examples/nextjs-example/src/lib/config.ts +++ b/examples/nextjs-example/src/lib/config.ts @@ -1,10 +1,5 @@ -export const OAUTH_CALLBACK_URL = "http://localhost:3000/"; +export const OAUTH_CALLBACK_URL = "http://localhost:3000/oauth/callback"; export const OPENROUTER_KEY_LOCALSTORAGE_KEY = "openrouter_key"; export const OPENROUTER_USER_ID_LOCALSTORAGE_KEY = "openrouter_user_id"; - -export function getOAuthKeyUrl() { - const url = new URL("https://openrouter.ai/auth"); - url.searchParams.append("callback_url", OAUTH_CALLBACK_URL); - return url.toString(); -} +export const OPENROUTER_CODE_VERIFIER_KEY = "openrouter_code_verifier"; diff --git a/examples/nextjs-example/src/lib/hooks/use-api-key.tsx b/examples/nextjs-example/src/lib/hooks/use-api-key.tsx new file mode 100644 index 00000000..11ef86fc --- /dev/null +++ b/examples/nextjs-example/src/lib/hooks/use-api-key.tsx @@ -0,0 +1,27 @@ +import { useLocalStorage } from "usehooks-ts"; +import { OPENROUTER_KEY_LOCALSTORAGE_KEY } from "../config"; + +const EMPTY_VALUE = ""; +const FALLBACK_VALUE = undefined; + +/** + * A hook to manage the OpenRouter API key in local storage. + * + * @returns A tuple containing the API key and a function to update it. + * + * @example + * ```tsx + * const [apiKey, setApiKey, removeApiKey] = useApiKey(); + * ``` + */ +export function useApiKey() { + return useLocalStorage( + OPENROUTER_KEY_LOCALSTORAGE_KEY, + undefined, + { + deserializer: (value) => (value !== EMPTY_VALUE ? value : FALLBACK_VALUE), + serializer: (value) => value || EMPTY_VALUE, + initializeWithValue: false, + }, + ); +} diff --git a/examples/nextjs-example/src/lib/hooks/use-local-storage.tsx b/examples/nextjs-example/src/lib/hooks/use-local-storage.tsx deleted file mode 100644 index a2471fc3..00000000 --- a/examples/nextjs-example/src/lib/hooks/use-local-storage.tsx +++ /dev/null @@ -1,81 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback } from "react"; - -type SetValue = T | ((val: T) => T); - -function useLocalStorage( - key: string, - initialValue: T, -): { value: T; setValue: (val: SetValue) => void; isPending: boolean } { - // Check if we're in the browser - const isBrowser = typeof window !== "undefined"; - - // State to track if we're still loading from localStorage - const [isPending, setIsPending] = useState(true); - - // State to store our value - const [storedValue, setStoredValue] = useState(() => { - if (!isBrowser) { - return initialValue; - } - - const item = window.localStorage.getItem(key); - if (item) return item as T; - return initialValue; - }); - - // Return a wrapped version of useState's setter function that persists the new value to localStorage - const setValue = useCallback( - (value: SetValue) => { - try { - // Allow value to be a function so we have the same API as useState - const valueToStore = - value instanceof Function ? value(storedValue) : value; - - // Save state - setStoredValue(valueToStore); - - // Save to local storage - if (isBrowser) { - window.localStorage.setItem(key, JSON.stringify(valueToStore)); - } - } catch (error) { - console.warn(`Error setting localStorage key "${key}":`, error); - } - }, - [key, storedValue, isBrowser], - ); - - // Set isPending to false after initial load - useEffect(() => { - if (isBrowser) { - setIsPending(false); - } - }, [isBrowser]); - - // Listen for changes to this key from other tabs/windows - useEffect(() => { - if (!isBrowser) return; - - const handleStorageChange = (e: StorageEvent) => { - if (e.key === key && e.newValue !== null) { - try { - setStoredValue(JSON.parse(e.newValue)); - } catch (error) { - console.warn( - `Error parsing localStorage value for key "${key}":`, - error, - ); - } - } - }; - - window.addEventListener("storage", handleStorageChange); - return () => window.removeEventListener("storage", handleStorageChange); - }, [key, isBrowser]); - - return { value: storedValue, setValue, isPending }; -} - -export default useLocalStorage; diff --git a/examples/nextjs-example/src/lib/hooks/use-openrouter-client.tsx b/examples/nextjs-example/src/lib/hooks/use-openrouter-client.tsx new file mode 100644 index 00000000..e2f26d55 --- /dev/null +++ b/examples/nextjs-example/src/lib/hooks/use-openrouter-client.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { OpenRouter } from "@openrouter/sdk"; +import React from "react"; +import { useApiKey } from "./use-api-key"; + +interface OpenRouterClientContextApi { + client: OpenRouter; +} + +const OpenRouterClientContext = React.createContext( + null!, +); + +export function OpenRouterClientProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [apiKey] = useApiKey(); + + const client = React.useMemo(() => { + return new OpenRouter({ + apiKey: apiKey, + }); + }, [apiKey]); + + return ( + + {children} + + ); +} + +export function useOpenRouter() { + const context = React.useContext(OpenRouterClientContext); + if (!context) { + throw new Error( + "useOpenRouter must be used within an OpenRouterClientProvider", + ); + } + + return context; +} diff --git a/src/funcs/custom/oAuthCreateAuthorizationUrl.ts b/src/funcs/oAuthCreateAuthorizationUrl.ts similarity index 92% rename from src/funcs/custom/oAuthCreateAuthorizationUrl.ts rename to src/funcs/oAuthCreateAuthorizationUrl.ts index add4da57..3be2ae74 100644 --- a/src/funcs/custom/oAuthCreateAuthorizationUrl.ts +++ b/src/funcs/oAuthCreateAuthorizationUrl.ts @@ -1,7 +1,7 @@ import z from "zod/v3"; -import { OpenRouterCore } from "../../core.js"; -import { serverURLFromOptions } from "../../lib/config.js"; -import { Result } from "../../types/fp.js"; +import { OpenRouterCore } from "../core.js"; +import { serverURLFromOptions } from "../lib/config.js"; +import { Result } from "../types/fp.js"; const CreateAuthorizationUrlBaseSchema = z.object({ callbackUrl: z.union([z.string().url(), z.instanceof(URL)]), diff --git a/src/funcs/custom/oAuthCreateSHA256CodeChallenge.ts b/src/funcs/oAuthCreateSHA256CodeChallenge.ts similarity index 98% rename from src/funcs/custom/oAuthCreateSHA256CodeChallenge.ts rename to src/funcs/oAuthCreateSHA256CodeChallenge.ts index 7939e147..95b3345a 100644 --- a/src/funcs/custom/oAuthCreateSHA256CodeChallenge.ts +++ b/src/funcs/oAuthCreateSHA256CodeChallenge.ts @@ -1,5 +1,5 @@ import z from "zod/v3"; -import { Result } from "../../types/fp.js"; +import { Result } from "../types/fp.js"; const CreateSHA256CodeChallengeRequestSchema = z.object({ /** diff --git a/src/sdk/oauth.ts b/src/sdk/oauth.ts index a57c74ab..62164e34 100644 --- a/src/sdk/oauth.ts +++ b/src/sdk/oauth.ts @@ -11,12 +11,12 @@ import { unwrapAsync } from "../types/fp.js"; import { CreateAuthorizationUrlRequest, oAuthCreateAuthorizationUrl, -} from "../funcs/custom/oAuthCreateAuthorizationUrl.js"; +} from "../funcs/oAuthCreateAuthorizationUrl.js"; import { CreateSHA256CodeChallengeRequest, CreateSHA256CodeChallengeResponse, oAuthCreateSHA256CodeChallenge, -} from "../funcs/custom/oAuthCreateSHA256CodeChallenge.js"; +} from "../funcs/oAuthCreateSHA256CodeChallenge.js"; // #endregion imports export class OAuth extends ClientSDK { diff --git a/src/funcs/custom/oAuthCreateAuthorizationUrl.test.ts b/tests/funcs/oAuthCreateAuthorizationUrl.test.ts similarity index 98% rename from src/funcs/custom/oAuthCreateAuthorizationUrl.test.ts rename to tests/funcs/oAuthCreateAuthorizationUrl.test.ts index 0874d3da..85538716 100644 --- a/src/funcs/custom/oAuthCreateAuthorizationUrl.test.ts +++ b/tests/funcs/oAuthCreateAuthorizationUrl.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import { OpenRouterCore } from "../../core.js"; -import { oAuthCreateAuthorizationUrl } from "./oAuthCreateAuthorizationUrl.js"; +import { OpenRouterCore } from "../../src/core"; +import { oAuthCreateAuthorizationUrl } from "../../src/funcs/oAuthCreateAuthorizationUrl"; describe("oAuthCreateAuthorizationUrl", () => { const createMockClient = (serverURL?: string) => { diff --git a/src/funcs/custom/oAuthCreateSHA256CodeChallenge.test.ts b/tests/funcs/oAuthCreateSHA256CodeChallenge.test.ts similarity index 91% rename from src/funcs/custom/oAuthCreateSHA256CodeChallenge.test.ts rename to tests/funcs/oAuthCreateSHA256CodeChallenge.test.ts index 7e4eb838..a626eb2b 100644 --- a/src/funcs/custom/oAuthCreateSHA256CodeChallenge.test.ts +++ b/tests/funcs/oAuthCreateSHA256CodeChallenge.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { oAuthCreateSHA256CodeChallenge } from "./oAuthCreateSHA256CodeChallenge.js"; +import { oAuthCreateSHA256CodeChallenge } from "../../src/funcs/oAuthCreateSHA256CodeChallenge"; describe("oAuthCreateSHA256CodeChallenge", () => { it("should generate code challenge from provided code verifier", async () => { @@ -101,7 +101,9 @@ describe("oAuthCreateSHA256CodeChallenge", () => { expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toBeDefined(); - expect((result.error as Error).message).toContain("at least 43 characters"); + expect((result.error as Error).message).toContain( + "at least 43 characters", + ); } }); @@ -113,7 +115,9 @@ describe("oAuthCreateSHA256CodeChallenge", () => { expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toBeDefined(); - expect((result.error as Error).message).toContain("at least 43 characters"); + expect((result.error as Error).message).toContain( + "at least 43 characters", + ); } }); @@ -126,7 +130,9 @@ describe("oAuthCreateSHA256CodeChallenge", () => { expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toBeDefined(); - expect((result.error as Error).message).toContain("at most 128 characters"); + expect((result.error as Error).message).toContain( + "at most 128 characters", + ); } }); @@ -170,7 +176,9 @@ describe("oAuthCreateSHA256CodeChallenge", () => { expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toBeDefined(); - expect((result.error as Error).message).toContain("unreserved characters"); + expect((result.error as Error).message).toContain( + "unreserved characters", + ); } }); @@ -183,7 +191,9 @@ describe("oAuthCreateSHA256CodeChallenge", () => { expect(result.ok).toBe(false); if (!result.ok) { expect(result.error).toBeDefined(); - expect((result.error as Error).message).toContain("unreserved characters"); + expect((result.error as Error).message).toContain( + "unreserved characters", + ); } }); }); diff --git a/tests/sdk/oauth.test.ts b/tests/sdk/oauth.test.ts new file mode 100644 index 00000000..dfb160a6 --- /dev/null +++ b/tests/sdk/oauth.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it, vi } from "vitest"; +import { OAuth as OpenRouterOAuthSDK } from "../../src/sdk/oauth"; + +describe("oauth", () => { + const oauthSdkInstance = new OpenRouterOAuthSDK({ + serverURL: "https://test-server-url", + }); + + describe("createSHA256CodeChallenge", () => { + it("is defined on the oauth sdk object", () => { + expect(oauthSdkInstance.createSHA256CodeChallenge).toBeDefined(); + }); + + it("calls the custom oAuthCreateSHA256CodeChallenge function", async () => { + const spy = vi.spyOn( + await import("../../src/funcs/oAuthCreateSHA256CodeChallenge"), + "oAuthCreateSHA256CodeChallenge", + ); + + oauthSdkInstance.createSHA256CodeChallenge(); + + expect(spy).toHaveBeenCalled(); + }); + }); + + describe("createAuthorizationUrl", () => { + it("is defined on the oauth sdk object", () => { + expect(oauthSdkInstance.createAuthorizationUrl).toBeDefined(); + }); + + it("calls the custom oAuthCreateAuthorizationUrl function", async () => { + const spy = vi.spyOn( + await import("../../src/funcs/oAuthCreateAuthorizationUrl"), + "oAuthCreateAuthorizationUrl", + ); + + oauthSdkInstance.createAuthorizationUrl({ + callbackUrl: "https://example.com/callback", + }); + + expect(spy).toHaveBeenCalled(); + }); + }); +});