diff --git a/skills/openrouter-oauth/README.md b/skills/openrouter-oauth/README.md index c636e2b..4eb1dc3 100644 --- a/skills/openrouter-oauth/README.md +++ b/skills/openrouter-oauth/README.md @@ -16,7 +16,7 @@ For other install methods (Claude Code plugin marketplace, Cursor Rules, etc.) s ## Compatibility -Browser environment — requires Web Crypto API, `localStorage`, and `sessionStorage`. +Browser, CLI, and Node.js environments. Browser flows require Web Crypto API, `localStorage`, and `sessionStorage`. Headless flows require Web Crypto API and can keep the verifier in process memory. ## What it covers @@ -27,3 +27,4 @@ See [SKILL.md](SKILL.md) for the full reference, including: - Copy-pasteable auth module with no dependencies - A ready-to-use "Sign In with OpenRouter" button component - Programmatic API-key acquisition flows (no UI required) +- Headless authorization for SSH sessions, remote development boxes, and containers without a reachable callback diff --git a/skills/openrouter-oauth/SKILL.md b/skills/openrouter-oauth/SKILL.md index 97e14eb..3aca4e8 100644 --- a/skills/openrouter-oauth/SKILL.md +++ b/skills/openrouter-oauth/SKILL.md @@ -1,8 +1,8 @@ --- name: openrouter-oauth -description: Implement "Sign In with OpenRouter" using OAuth PKCE — framework-agnostic, no SDK or client registration required. Use when the user wants to add OpenRouter login, authentication, sign-in buttons, OAuth, or AI model inference API keys for browser-based apps. No client registration, no backend, no secrets required. -version: 2.0.0 -compatibility: browser (requires Web Crypto API, localStorage, sessionStorage) +description: Implement "Sign In with OpenRouter" using OAuth PKCE — framework-agnostic, no SDK or client registration required. Use when the user wants to add OpenRouter login, authentication, sign-in buttons, OAuth, or AI model inference API keys for browser apps, CLI tools, or remote environments. No client registration, no backend, no secrets required. +version: 2.1.0 +compatibility: browser, CLI, or Node.js (requires Web Crypto API; browser flows also use localStorage and sessionStorage) --- # Sign In with OpenRouter @@ -16,7 +16,7 @@ Live demo: [openrouterteam.github.io/sign-in-with-openrouter](https://openrouter | User wants to… | Do this | |---|---| | Add sign-in / login to a web app | Follow the full PKCE flow + button guidance below | -| Get an API key programmatically (no UI) | Just implement the PKCE flow — skip the button section | +| Get an API key programmatically (no UI) | Use the headless / no-callback flow below | | Use the OpenRouter SDK after auth | Do PKCE here for the key, then see `openrouter-typescript-sdk` skill for `callModel`/streaming | --- @@ -48,6 +48,56 @@ https://openrouter.ai/auth?callback_url={url}&code_challenge={challenge}&code_ch | `code_challenge` | The S256 challenge from Step 1 | | `code_challenge_method` | Always `S256` | +### Headless / no-callback flow + +For SSH sessions, remote development boxes, containers, and other environments where a localhost callback cannot be reached, omit `callback_url` entirely. After the user authorizes, OpenRouter displays the authorization code on screen for them to paste into the app. + +``` +https://openrouter.ai/auth?code_challenge={challenge}&code_challenge_method=S256&key_label={label} +``` + +| Param | Value | +|---|---| +| `code_challenge` | The S256 challenge from Step 1 (required) | +| `code_challenge_method` | `S256` (required; `plain` is rejected) | +| `key_label` | Optional app title on the approval screen; defaults to `A command-line app` | + +- Do not include `callback_url` or `oauth_client_id`. A no-callback request with `oauth_client_id`, a missing `code_challenge`, or a `plain` challenge method is rejected and redirected to the OpenRouter home page instead of showing a code. +- Keep the `code_verifier` in the process until the user pastes the displayed code. `sessionStorage` and `localStorage` from the browser auth module do not apply in a CLI or Node.js context. +- The displayed code is single-use and expires 10 minutes after issuance. +- After the user pastes the code, continue with Step 4. The `POST /api/v1/auth/keys` exchange is unchanged. + +Minimal Node.js example: + +```js +import { createHash, randomBytes } from "node:crypto"; +import { createInterface } from "node:readline/promises"; + +const base64url = (bytes) => Buffer.from(bytes).toString("base64url"); +const code_verifier = base64url(randomBytes(32)); +const code_challenge = base64url(createHash("sha256").update(code_verifier).digest()); +const authorizeUrl = new URL("https://openrouter.ai/auth"); +authorizeUrl.searchParams.set("code_challenge", code_challenge); +authorizeUrl.searchParams.set("code_challenge_method", "S256"); +authorizeUrl.searchParams.set("key_label", "My command-line app"); +console.log(`Open this URL, authorize, then paste the code below:\n${authorizeUrl}`); + +const readline = createInterface({ input: process.stdin, output: process.stdout }); +const code = (await readline.question("Authorization code: ")).trim(); +readline.close(); +const response = await fetch("https://openrouter.ai/api/v1/auth/keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ code, code_verifier, code_challenge_method: "S256" }), +}); +if (!response.ok) { + console.error(`Key exchange failed (${response.status}): ${await response.text()}`); + process.exitCode = 1; +} else { + console.log(await response.json()); +} +``` + ### Step 3: Handle the redirect back User returns to your `callback_url` with `?code=` appended. Extract the `code` query parameter.