/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * Clipboard sniffing for the BYOK key modal. * * After the user clicks our "Open Console" button, they create a key on the * provider's site, copy it, and return to this tab. If transient activation * (or clipboard-read permission) is still valid, we can detect the key and * offer one-click insertion instead of forcing a manual paste. * * Reads are best-effort and silently no-op on: * - missing Clipboard API (older Safari, insecure context, sandboxed iframe) * - permission denied (Firefox without explicit user grant) * - read failure for any other reason * * We never throw on these — the modal just falls back to its manual paste UX. */ export type BYOKProvider = 'anthropic' | 'openai'; /** * Provider-specific shape checks. Tight enough to avoid false positives on * random clipboard contents, permissive enough to cover the key formats each * provider currently issues. * * Anthropic console keys: `sk-ant-api03-` + ≥50 chars of [A-Za-z0-9_-] * OpenAI keys: `sk-`, `sk-proj-`, `sk-svcacct-`, `sk-admin-` + ≥20 chars * * The OpenAI pattern uses a negative lookahead for `ant-` so an Anthropic key * doesn't accidentally satisfy the OpenAI tab — they both start with `sk-`. */ const PROVIDER_PATTERNS: Record = { anthropic: /^sk-ant-api03-[A-Za-z0-9_-]{50,}$/, openai: /^sk-(?!ant-)(?:proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}$/, }; /** * Try to read a candidate provider key from the system clipboard. * * Returns the trimmed key string if the clipboard contents match the provider * pattern. Returns `null` for everything else (no match, permission denied, * Clipboard API unavailable, etc.) — callers should treat `null` as "fall back * to manual paste". */ export async function readClipboardKey(provider: BYOKProvider): Promise { if (typeof navigator === 'undefined' || !navigator.clipboard?.readText) { return null; } try { const text = await navigator.clipboard.readText(); const trimmed = text.trim(); if (PROVIDER_PATTERNS[provider].test(trimmed)) { return trimmed; } return null; } catch (err) { // Permission denied / insecure context / no transient activation — these // are expected on Firefox and Safari without an explicit user grant, and // the manual-paste fallback still works. Log at debug level for diagnostic // but never escalate to the UI. console.debug('[byok] clipboard read failed', err); return null; } } /** * Render-safe key mask. Preserves the provider prefix so the user can confirm * the right key is detected, hides the secret middle, shows the last 4 chars * for disambiguation. * * sk-ant-api03-abcdef…XYZA * sk-proj-••••XYZA * sk-••••XYZA */ export function maskKey(key: string): string { if (key.length < 12) return '••••'; const dashIdx = key.lastIndexOf('-'); const prefixEnd = dashIdx > 0 && dashIdx < 14 ? dashIdx + 1 : Math.min(14, key.length - 4); return `${key.slice(0, prefixEnd)}••••${key.slice(-4)}`; } /** * Light shape check shared with `readClipboardKey` so callers can validate * manual paste contents the same way before saving. */ export function looksLikeProviderKey(provider: BYOKProvider, value: string): boolean { return PROVIDER_PATTERNS[provider].test(value.trim()); }