diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bfb869c..555523b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -156,15 +156,15 @@ ln -s /absolute/path/to/opencode-berget-auth ~/.config/opencode/plugins/berget-a 1. Run `opencode` in your test project 2. Type `/connect` -3. You should see **"Login with Berget"** as an auth option +3. You should see **"Use Berget Code plan"** as an auth option 4. Check logs — the plugin calls `logDebug('Initializing Berget Auth Plugin')` on startup ### Testing Auth Flows | Flow | How to Test | | ------------------ | ------------------------------------------------------------------------------------------------------- | -| **OAuth / SSO** | `/connect` → "Login with Berget" → browser opens → complete login | -| **API Key** | `/connect` → "Enter Berget API Key manually" → paste a key | +| **OAuth / SSO** | `/connect` → "Use Berget Code plan" → browser opens → complete login | +| **API Key** | `/connect` → "Use Berget AI API key" → paste a key | | **Token Refresh** | Start a session, wait for token expiry (or temporarily shorten expiry in code), then send a new message | | **Model Fetching** | After auth, check that Berget models appear in model picker | @@ -262,9 +262,9 @@ Do **not** publish manually from your local machine. ## Need Help? -- **OpenCode Plugin Docs:** https://opencode.ai/docs/plugins -- **OpenCode SDK Docs:** https://opencode.ai/docs/sdk -- **Discord:** https://opencode.ai/discord -- **Issues:** https://github.com/berget-ai/opencode-berget-auth/issues +- **OpenCode Plugin Docs:** +- **OpenCode SDK Docs:** +- **Discord:** +- **Issues:** Happy coding! 🚀 diff --git a/README.md b/README.md index d422cfa..99faa15 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ opencode For team members with a Berget Code seat: 1. Run `/connect` in OpenCode -2. Select "Login with Berget" +2. Select "Use Berget Code plan" 3. Authenticate in browser — token refresh is automatic ### API Key @@ -28,7 +28,7 @@ For team members with a Berget Code seat: For API key users: 1. Run `/connect` in OpenCode -2. Select "Enter Berget API Key manually" +2. Select "Use Berget AI API key" 3. Paste your key — persisted across sessions ## How It Works diff --git a/docs/auth.md b/docs/auth.md index 8ca47b4..279c8b8 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -91,7 +91,7 @@ sequenceDiagram participant K as Keycloak participant CB as Callback Server (localhost:8787) - U->>OC: /connect → select "Login with Berget" + U->>OC: /connect → select "Use Berget Code plan" OC->>BP: Call authorize() BP->>BP: Generate PKCE verifier, challenge (S256), state nonce BP->>K: Redirect browser to /auth with challenge, state, scopes @@ -314,7 +314,7 @@ This is by design — the port must match the `redirect_uri` registered with Key ## API Key Authentication -The simpler path. When the user selects "Enter Berget API Key manually": +The simpler path. When the user selects "Use Berget AI API key": 1. OpenCode stores the key in its secure credential store. 2. `getAuth()` returns `{ type: 'api', key: 'sk-...' }`. diff --git a/src/plugin.ts b/src/plugin.ts index b3eab45..5a0475f 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -131,11 +131,11 @@ export const BergetAuthPlugin = async ({ client }: PluginInput): Promise methods: [ { authorize: createPkceAuthorizeMethod(), - label: 'Login with Berget', + label: 'Use Berget Code plan', type: 'oauth' as const, }, { - label: 'Enter Berget API Key manually', + label: 'Use Berget AI API key', type: 'api' as const, }, ], diff --git a/src/plugin/auth.test.ts b/src/plugin/auth.test.ts index 2c3d124..4574ed8 100644 --- a/src/plugin/auth.test.ts +++ b/src/plugin/auth.test.ts @@ -1,8 +1,9 @@ import { describe, expect, it } from 'vitest'; +import type { OAuthAuthDetails } from './types'; + import { ACCESS_TOKEN_EXPIRY_BUFFER_MS } from '../constants'; import { accessTokenExpired } from './auth'; -import type { OAuthAuthDetails } from './types'; describe('accessTokenExpired - Issue #5', () => { // With raw expiry storage, the buffer lives ONLY in the check. diff --git a/src/plugin/pkce-flow.test.ts b/src/plugin/pkce-flow.test.ts index e5edb1c..6836649 100644 --- a/src/plugin/pkce-flow.test.ts +++ b/src/plugin/pkce-flow.test.ts @@ -4,10 +4,14 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { AuthOAuthResult } from './types'; +const constantsMocks = vi.hoisted(() => ({ + getKeycloakUrl: vi.fn(() => 'https://keycloak.berget.ai'), +})); + vi.mock('../constants', () => ({ + ...constantsMocks, ACCESS_TOKEN_EXPIRY_BUFFER_MS: 60_000, getKeycloakRealm: () => 'berget', - getKeycloakUrl: () => 'https://keycloak.berget.ai', KEYCLOAK_CLIENT_ID: 'berget-code', PKCE_CALLBACK_PORT: 8787, })); @@ -232,6 +236,16 @@ describe('createPkceAuthorizeMethod - Issue #1', () => { 'Port 8787 is already in use. Another OpenCode login may be in progress. Please wait and try again, or close other OpenCode sessions.', ); }); + + it('rejects with a clear error when the Keycloak URL is invalid (malformed BERGET_API_URL)', async () => { + process.env.CI = 'true'; + constantsMocks.getKeycloakUrl.mockReturnValueOnce('not a valid url'); + + const createPkceAuthorizeMethod = await loadSubject(); + const authorize = createPkceAuthorizeMethod(); + + await expect(authorize()).rejects.toThrow(/BERGET_API_URL/); + }); }); describe('exchangeCodeForTokens - Issue #3', () => { diff --git a/src/plugin/pkce-flow.ts b/src/plugin/pkce-flow.ts index 28af265..7454a36 100644 --- a/src/plugin/pkce-flow.ts +++ b/src/plugin/pkce-flow.ts @@ -24,7 +24,7 @@ import { logDebug } from './debug'; /** * Creates the OAuth authorize method using PKCE flow - * This is called when user selects "Login with Berget" in OpenCode + * This is called when user selects "Use Berget Code plan" in OpenCode */ export function createPkceAuthorizeMethod(): ( inputs?: Record, @@ -310,10 +310,18 @@ async function executePkceAuthorization( const state = generateRandomHex(16); const redirectUri = `http://localhost:${PKCE_CALLBACK_PORT}/callback`; - // Build authorization URL - const authUrl = new URL( - `${getKeycloakUrl()}/realms/${getKeycloakRealm()}/protocol/openid-connect/auth`, - ); + // Build authorization URL — guarded so a malformed BERGET_API_URL surfaces + // as a clear error instead of a raw TypeError + let authUrl: URL; + try { + authUrl = new URL( + `${getKeycloakUrl()}/realms/${getKeycloakRealm()}/protocol/openid-connect/auth`, + ); + } catch { + throw new Error( + `Invalid Keycloak URL "${getKeycloakUrl()}" — check the BERGET_API_URL environment variable`, + ); + } authUrl.searchParams.set('client_id', KEYCLOAK_CLIENT_ID); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set('redirect_uri', redirectUri);