Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand Down Expand Up @@ -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:** <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>

Happy coding! 🚀
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ 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

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
Expand Down
4 changes: 2 additions & 2 deletions docs/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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-...' }`.
Expand Down
4 changes: 2 additions & 2 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,11 @@ export const BergetAuthPlugin = async ({ client }: PluginInput): Promise<Hooks>
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,
},
],
Expand Down
3 changes: 2 additions & 1 deletion src/plugin/auth.test.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
16 changes: 15 additions & 1 deletion src/plugin/pkce-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}));
Expand Down Expand Up @@ -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', () => {
Expand Down
18 changes: 13 additions & 5 deletions src/plugin/pkce-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>,
Expand Down Expand Up @@ -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);
Expand Down
Loading