opencode-auth-proxy 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +265 -0
- package/opencode-auth-proxy.ts +588 -0
- package/package.json +45 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2024-01-01
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Initial release of OpenCode Auth Proxy Plugin
|
|
12
|
+
- Google OAuth 2.0 authentication with PKCE support
|
|
13
|
+
- Session management with HMAC-signed cookies
|
|
14
|
+
- Token persistence to local filesystem
|
|
15
|
+
- WebSocket proxy support
|
|
16
|
+
- Health check endpoints
|
|
17
|
+
- Configurable via environment variables or config file
|
|
18
|
+
|
|
19
|
+
### Security
|
|
20
|
+
- PKCE (Proof Key for Code Exchange) implementation
|
|
21
|
+
- HMAC-SHA256 session cookie signing
|
|
22
|
+
- HttpOnly and SameSite cookie attributes
|
|
23
|
+
- Automatic secure cookie detection for HTTPS
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 milc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# OpenCode Auth Proxy Plugin
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+
|
|
6
|
+
**Google OAuth authentication proxy for OpenCode.** This plugin provides a secure OAuth 2.0 flow with PKCE (Proof Key for Code Exchange) for authenticating with Google services, managing sessions, and proxying requests to protected endpoints.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- 🔐 Secure OAuth 2.0 flow with PKCE
|
|
11
|
+
- 🍪 Session management with HMAC-signed cookies
|
|
12
|
+
- 🔄 Automatic token refresh and persistence
|
|
13
|
+
- 🛡️ Request proxying with authentication checks
|
|
14
|
+
- 🌐 WebSocket support for real-time connections
|
|
15
|
+
- ⚙️ Configurable via environment variables or config file
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
- [Opencode CLI](https://opencode.ai) installed
|
|
20
|
+
- A Google Cloud project with OAuth 2.0 credentials
|
|
21
|
+
- Node.js 20 or higher
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Add the plugin to your OpenCode server configuration file (`opencode.server.json`):
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"$schema": "https://opencode.ai/config.json",
|
|
30
|
+
"plugin": ["opencode-auth-proxy"]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
The plugin can be configured via environment variables or the OpenCode config file.
|
|
37
|
+
|
|
38
|
+
### Environment Variables
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
PORT=4096 # Proxy server port (default: 4096)
|
|
42
|
+
TARGET_HOST=127.0.0.1 # Target server host (default: 127.0.0.1)
|
|
43
|
+
TARGET_PORT=4097 # Target server port (default: 4097)
|
|
44
|
+
GOOGLE_REDIRECT_CLIENT_ID=your-client-id # Google OAuth client ID (required)
|
|
45
|
+
GOOGLE_REDIRECT_CLIENT_SECRET=your-secret # Google OAuth client secret (required)
|
|
46
|
+
GOOGLE_REDIRECT_URI=http://localhost:4096/auth/google/callback # OAuth redirect URI
|
|
47
|
+
SESSION_SECRET=your-secret-key # Session signing secret (required)
|
|
48
|
+
SESSION_COOKIE_NAME=opencode_session # Session cookie name (default: opencode_session)
|
|
49
|
+
COOKIE_SECURE=false # Use secure cookies (default: auto-detect)
|
|
50
|
+
TOKEN_PATH=~/.opencode-proxy/google-auth.json # Token storage path
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Config File
|
|
54
|
+
|
|
55
|
+
Add configuration to your `opencode.json`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"$schema": "https://opencode.ai/config.json",
|
|
60
|
+
"authProxy": {
|
|
61
|
+
"port": 4096,
|
|
62
|
+
"targetHost": "127.0.0.1",
|
|
63
|
+
"targetPort": 4097,
|
|
64
|
+
"googleClientId": "your-client-id",
|
|
65
|
+
"googleClientSecret": "your-client-secret",
|
|
66
|
+
"googleRedirectUri": "http://localhost:4096/auth/google/callback",
|
|
67
|
+
"sessionSecret": "your-secret-key",
|
|
68
|
+
"sessionCookieName": "opencode_session",
|
|
69
|
+
"cookieSecure": false,
|
|
70
|
+
"tokenPath": "~/.opencode-proxy/google-auth.json"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Google Cloud Setup
|
|
76
|
+
|
|
77
|
+
1. Go to the [Google Cloud Console](https://console.cloud.google.com/)
|
|
78
|
+
2. Create or select a project
|
|
79
|
+
3. Enable the **Google+ API** and **OAuth2 API**
|
|
80
|
+
4. Go to **APIs & Services** > **Credentials**
|
|
81
|
+
5. Create an **OAuth 2.0 Client ID**
|
|
82
|
+
- Application type: **Web application**
|
|
83
|
+
- Authorized redirect URIs: `http://localhost:4096/auth/google/callback` (or your configured redirect URI)
|
|
84
|
+
6. Copy the **Client ID** and **Client Secret**
|
|
85
|
+
|
|
86
|
+
## Usage
|
|
87
|
+
|
|
88
|
+
### Starting Authentication
|
|
89
|
+
|
|
90
|
+
Navigate to `/auth/google/start` in your browser or use:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
curl http://localhost:4096/auth/google/start
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
You can optionally pass a `project` query parameter:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl "http://localhost:4096/auth/google/start?project=my-project-id"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### OAuth Flow
|
|
103
|
+
|
|
104
|
+
1. User visits `/auth/google/start`
|
|
105
|
+
2. Plugin redirects to Google OAuth consent screen
|
|
106
|
+
3. User authorizes the application
|
|
107
|
+
4. Google redirects to `/auth/google/callback`
|
|
108
|
+
5. Plugin exchanges authorization code for tokens
|
|
109
|
+
6. Session cookie is set
|
|
110
|
+
7. User is redirected to the application
|
|
111
|
+
|
|
112
|
+
### Accessing Protected Endpoints
|
|
113
|
+
|
|
114
|
+
Once authenticated, the plugin will:
|
|
115
|
+
- Check for a valid session cookie on all non-public paths
|
|
116
|
+
- Proxy authenticated requests to the target server
|
|
117
|
+
- Reject unauthenticated requests with 401 or redirect to login
|
|
118
|
+
|
|
119
|
+
### Public Endpoints
|
|
120
|
+
|
|
121
|
+
The following paths are publicly accessible (no authentication required):
|
|
122
|
+
|
|
123
|
+
- `/health` - Health check endpoint
|
|
124
|
+
- `/global/health` - Global health check
|
|
125
|
+
- `/auth/google/start` - Start OAuth flow
|
|
126
|
+
- `/auth/google/config` - OAuth configuration info
|
|
127
|
+
- `/auth/google/callback` - OAuth callback handler
|
|
128
|
+
|
|
129
|
+
### Token Management
|
|
130
|
+
|
|
131
|
+
Tokens are automatically persisted to the configured `tokenPath`. To retrieve the current token:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
curl http://localhost:4096/auth/google/token
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## API Endpoints
|
|
138
|
+
|
|
139
|
+
### `GET /health` or `GET /global/health`
|
|
140
|
+
|
|
141
|
+
Health check endpoint that also checks upstream server status.
|
|
142
|
+
|
|
143
|
+
**Response:**
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"status": "ok",
|
|
147
|
+
"healthy": true,
|
|
148
|
+
"target": "http://127.0.0.1:4097",
|
|
149
|
+
"upstream": {
|
|
150
|
+
"ok": true,
|
|
151
|
+
"status": 200,
|
|
152
|
+
"body": { ... }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### `GET /auth/google/config`
|
|
158
|
+
|
|
159
|
+
Returns OAuth configuration (client ID is partially masked).
|
|
160
|
+
|
|
161
|
+
**Response:**
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"redirect_uri": "http://localhost:4096/auth/google/callback",
|
|
165
|
+
"callback_path": "/auth/google/callback",
|
|
166
|
+
"client_id": "12345678901234567890...",
|
|
167
|
+
"client_secret_set": true,
|
|
168
|
+
"scopes": [
|
|
169
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
170
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
171
|
+
"https://www.googleapis.com/auth/userinfo.profile"
|
|
172
|
+
]
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### `GET /auth/google/start?project=<project-id>`
|
|
177
|
+
|
|
178
|
+
Initiates the OAuth flow. Redirects to Google's consent screen.
|
|
179
|
+
|
|
180
|
+
### `GET /auth/google/callback`
|
|
181
|
+
|
|
182
|
+
OAuth callback handler. Processes the authorization code and sets session cookie.
|
|
183
|
+
|
|
184
|
+
### `GET /auth/google/token`
|
|
185
|
+
|
|
186
|
+
Retrieves the stored OAuth token.
|
|
187
|
+
|
|
188
|
+
**Response:**
|
|
189
|
+
```json
|
|
190
|
+
{
|
|
191
|
+
"token": {
|
|
192
|
+
"type": "success",
|
|
193
|
+
"refresh": "refresh-token",
|
|
194
|
+
"access": "access-token",
|
|
195
|
+
"expires": 1234567890,
|
|
196
|
+
"email": "user@example.com",
|
|
197
|
+
"projectId": "project-id",
|
|
198
|
+
"storedAt": "2024-01-01T00:00:00.000Z"
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Security
|
|
204
|
+
|
|
205
|
+
- **PKCE**: Uses Proof Key for Code Exchange to prevent authorization code interception
|
|
206
|
+
- **HMAC Signing**: Session cookies are signed with HMAC-SHA256
|
|
207
|
+
- **HttpOnly Cookies**: Session cookies are marked HttpOnly to prevent XSS attacks
|
|
208
|
+
- **SameSite**: Cookies use SameSite=Lax to prevent CSRF attacks
|
|
209
|
+
- **Secure Cookies**: Automatically enabled for HTTPS redirect URIs
|
|
210
|
+
|
|
211
|
+
## Troubleshooting
|
|
212
|
+
|
|
213
|
+
### Port Already in Use
|
|
214
|
+
|
|
215
|
+
If the default port (4096) is in use, configure a different port:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"authProxy": {
|
|
220
|
+
"port": 5000
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Or set the `PORT` environment variable.
|
|
226
|
+
|
|
227
|
+
### Session Not Persisting
|
|
228
|
+
|
|
229
|
+
Ensure `SESSION_SECRET` is set and consistent across restarts. The secret is used to sign and verify session cookies.
|
|
230
|
+
|
|
231
|
+
### OAuth Errors
|
|
232
|
+
|
|
233
|
+
Check the OpenCode logs for detailed OAuth error messages. Common issues:
|
|
234
|
+
|
|
235
|
+
- **Invalid redirect URI**: Ensure the redirect URI in Google Cloud Console matches exactly
|
|
236
|
+
- **Missing scopes**: The plugin requests cloud-platform, userinfo.email, and userinfo.profile scopes
|
|
237
|
+
- **Client secret mismatch**: Verify the client secret is correct
|
|
238
|
+
|
|
239
|
+
### Debugging
|
|
240
|
+
|
|
241
|
+
Check OpenCode server logs for detailed plugin logs. The plugin logs all authentication attempts, proxy errors, and configuration issues.
|
|
242
|
+
|
|
243
|
+
## Development
|
|
244
|
+
|
|
245
|
+
To develop on this plugin locally:
|
|
246
|
+
|
|
247
|
+
1. **Clone or navigate to the plugin directory**
|
|
248
|
+
|
|
249
|
+
2. **Link**: Update your OpenCode server config to point to your local directory:
|
|
250
|
+
|
|
251
|
+
```json
|
|
252
|
+
{
|
|
253
|
+
"plugin": ["file:///absolute/path/to/opencode-auth-proxy"]
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
3. **Install dependencies**:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
npm install
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT
|
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import http, { IncomingMessage, ServerResponse } from "http"
|
|
3
|
+
import httpProxy from "http-proxy"
|
|
4
|
+
import fs from "fs"
|
|
5
|
+
import path from "path"
|
|
6
|
+
import crypto from "crypto"
|
|
7
|
+
import os from "os"
|
|
8
|
+
|
|
9
|
+
type AuthProxyConfig = {
|
|
10
|
+
port?: number
|
|
11
|
+
targetPort?: number
|
|
12
|
+
targetHost?: string
|
|
13
|
+
googleClientId?: string
|
|
14
|
+
googleClientSecret?: string
|
|
15
|
+
googleRedirectUri?: string
|
|
16
|
+
sessionSecret?: string
|
|
17
|
+
sessionCookieName?: string
|
|
18
|
+
cookieSecure?: boolean
|
|
19
|
+
tokenPath?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type TokenSuccess = {
|
|
23
|
+
type: "success"
|
|
24
|
+
refresh: string
|
|
25
|
+
access: string
|
|
26
|
+
expires: number
|
|
27
|
+
email?: string
|
|
28
|
+
projectId: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type TokenPayload = TokenSuccess | { type: "failed"; error: string }
|
|
32
|
+
|
|
33
|
+
const GOOGLE_SCOPES = [
|
|
34
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
35
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
36
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
function base64url(buf: Buffer): string {
|
|
40
|
+
return buf
|
|
41
|
+
.toString("base64")
|
|
42
|
+
.replace(/\+/g, "-")
|
|
43
|
+
.replace(/\//g, "_")
|
|
44
|
+
.replace(/=+$/, "")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function encodeState(payload: { verifier: string; projectId: string }): string {
|
|
48
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function decodeState(state: string): { verifier: string; projectId: string } {
|
|
52
|
+
const normalized = state.replace(/-/g, "+").replace(/_/g, "/")
|
|
53
|
+
const padded = normalized.padEnd(normalized.length + ((4 - (normalized.length % 4)) % 4), "=")
|
|
54
|
+
const json = Buffer.from(padded, "base64").toString("utf8")
|
|
55
|
+
const parsed = JSON.parse(json)
|
|
56
|
+
return { verifier: parsed.verifier || "", projectId: parsed.projectId || "" }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function generatePkce() {
|
|
60
|
+
const verifier = base64url(crypto.randomBytes(32))
|
|
61
|
+
const challenge = base64url(crypto.createHash("sha256").update(verifier).digest())
|
|
62
|
+
return { verifier, challenge }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseCookies(req: IncomingMessage): Record<string, string> {
|
|
66
|
+
const header = req.headers.cookie
|
|
67
|
+
if (!header) return {}
|
|
68
|
+
return header.split(";").reduce<Record<string, string>>((acc, part) => {
|
|
69
|
+
const [key, ...valueParts] = part.trim().split("=")
|
|
70
|
+
if (!key) return acc
|
|
71
|
+
acc[key] = valueParts.join("=")
|
|
72
|
+
return acc
|
|
73
|
+
}, {})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function signSession(sessionSecret: string): string {
|
|
77
|
+
const sessionData = Buffer.from("authenticated", "utf8").toString("base64url")
|
|
78
|
+
const signature = crypto.createHmac("sha256", sessionSecret).update(sessionData).digest("hex")
|
|
79
|
+
return `${sessionData}.${signature}`
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function verifySession(token: string, sessionSecret: string): boolean {
|
|
83
|
+
const [sessionPart, signature] = token.split(".")
|
|
84
|
+
if (!sessionPart || !signature) return false
|
|
85
|
+
const expected = crypto.createHmac("sha256", sessionSecret).update(sessionPart).digest("hex")
|
|
86
|
+
return crypto.timingSafeEqual(Buffer.from(signature, "utf8"), Buffer.from(expected, "utf8"))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function expandPath(filePath: string): string {
|
|
90
|
+
if (filePath.startsWith("~/")) {
|
|
91
|
+
return path.join(os.homedir(), filePath.slice(2))
|
|
92
|
+
}
|
|
93
|
+
return filePath
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadToken(tokenPath: string): TokenSuccess | null {
|
|
97
|
+
try {
|
|
98
|
+
const expandedPath = expandPath(tokenPath)
|
|
99
|
+
const raw = fs.readFileSync(expandedPath, "utf8")
|
|
100
|
+
const parsed = JSON.parse(raw)
|
|
101
|
+
if (typeof parsed !== "object" || !parsed) return null
|
|
102
|
+
if ("access" in parsed && "refresh" in parsed) {
|
|
103
|
+
return parsed as TokenSuccess
|
|
104
|
+
}
|
|
105
|
+
return null
|
|
106
|
+
} catch {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function persistToken(token: TokenSuccess, tokenPath: string) {
|
|
112
|
+
const expandedPath = expandPath(tokenPath)
|
|
113
|
+
const dir = path.dirname(expandedPath)
|
|
114
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
115
|
+
fs.writeFileSync(expandedPath, JSON.stringify(token, null, 2))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs = 10000): Promise<Response> {
|
|
119
|
+
const controller = new AbortController()
|
|
120
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
121
|
+
try {
|
|
122
|
+
return await fetch(url, { ...options, signal: controller.signal })
|
|
123
|
+
} finally {
|
|
124
|
+
clearTimeout(timeout)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function authorizeGoogle(
|
|
129
|
+
clientId: string,
|
|
130
|
+
redirectUri: string,
|
|
131
|
+
projectId = "",
|
|
132
|
+
): Promise<{ url: string; verifier: string; projectId: string }> {
|
|
133
|
+
const pkce = generatePkce()
|
|
134
|
+
const url = new URL("https://accounts.google.com/o/oauth2/v2/auth")
|
|
135
|
+
url.searchParams.set("client_id", clientId)
|
|
136
|
+
url.searchParams.set("response_type", "code")
|
|
137
|
+
url.searchParams.set("redirect_uri", redirectUri)
|
|
138
|
+
url.searchParams.set("scope", GOOGLE_SCOPES.join(" "))
|
|
139
|
+
url.searchParams.set("code_challenge", pkce.challenge)
|
|
140
|
+
url.searchParams.set("code_challenge_method", "S256")
|
|
141
|
+
url.searchParams.set("state", encodeState({ verifier: pkce.verifier, projectId: projectId || "" }))
|
|
142
|
+
url.searchParams.set("access_type", "offline")
|
|
143
|
+
url.searchParams.set("prompt", "consent")
|
|
144
|
+
return { url: url.toString(), verifier: pkce.verifier, projectId: projectId || "" }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function exchangeGoogle(
|
|
148
|
+
code: string,
|
|
149
|
+
state: string,
|
|
150
|
+
clientId: string,
|
|
151
|
+
clientSecret: string,
|
|
152
|
+
redirectUri: string,
|
|
153
|
+
log: (data: any) => Promise<void>,
|
|
154
|
+
): Promise<TokenPayload> {
|
|
155
|
+
try {
|
|
156
|
+
const { verifier } = decodeState(state || "")
|
|
157
|
+
const startTime = Date.now()
|
|
158
|
+
const requestBody = new URLSearchParams({
|
|
159
|
+
client_id: clientId,
|
|
160
|
+
client_secret: clientSecret,
|
|
161
|
+
code,
|
|
162
|
+
grant_type: "authorization_code",
|
|
163
|
+
redirect_uri: redirectUri,
|
|
164
|
+
code_verifier: verifier,
|
|
165
|
+
})
|
|
166
|
+
await log({
|
|
167
|
+
service: "opencode-auth-proxy",
|
|
168
|
+
level: "info",
|
|
169
|
+
message: "OAuth token exchange request",
|
|
170
|
+
extra: {
|
|
171
|
+
redirect_uri: redirectUri,
|
|
172
|
+
client_id: clientId,
|
|
173
|
+
client_secret_length: clientSecret?.length || 0,
|
|
174
|
+
code_length: code?.length || 0,
|
|
175
|
+
has_verifier: !!verifier,
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
const tokenResponse = await fetch("https://oauth2.googleapis.com/token", {
|
|
179
|
+
method: "POST",
|
|
180
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
181
|
+
body: requestBody,
|
|
182
|
+
})
|
|
183
|
+
if (!tokenResponse.ok) {
|
|
184
|
+
const errorText = await tokenResponse.text()
|
|
185
|
+
let parsedError: any = {}
|
|
186
|
+
try {
|
|
187
|
+
parsedError = JSON.parse(errorText)
|
|
188
|
+
} catch {
|
|
189
|
+
parsedError = { raw: errorText }
|
|
190
|
+
}
|
|
191
|
+
await log({
|
|
192
|
+
service: "opencode-auth-proxy",
|
|
193
|
+
level: "error",
|
|
194
|
+
message: "OAuth token exchange failed",
|
|
195
|
+
extra: {
|
|
196
|
+
status: tokenResponse.status,
|
|
197
|
+
statusText: tokenResponse.statusText,
|
|
198
|
+
error: parsedError,
|
|
199
|
+
redirect_uri: redirectUri,
|
|
200
|
+
redirect_uri_length: redirectUri.length,
|
|
201
|
+
client_id: clientId,
|
|
202
|
+
client_id_length: clientId.length,
|
|
203
|
+
client_secret_set: !!clientSecret,
|
|
204
|
+
client_secret_length: clientSecret?.length || 0,
|
|
205
|
+
},
|
|
206
|
+
})
|
|
207
|
+
const errorMessage = parsedError.error_description || parsedError.error || errorText
|
|
208
|
+
return { type: "failed", error: errorMessage }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const tokenPayload = (await tokenResponse.json()) as {
|
|
212
|
+
access_token: string
|
|
213
|
+
expires_in: number
|
|
214
|
+
refresh_token?: string
|
|
215
|
+
}
|
|
216
|
+
const userInfoResponse = await fetch("https://www.googleapis.com/oauth2/v1/userinfo?alt=json", {
|
|
217
|
+
headers: { Authorization: `Bearer ${tokenPayload.access_token}` },
|
|
218
|
+
})
|
|
219
|
+
const userInfo = userInfoResponse.ok ? ((await userInfoResponse.json()) as { email?: string }) : {}
|
|
220
|
+
|
|
221
|
+
const refreshToken = tokenPayload.refresh_token
|
|
222
|
+
if (!refreshToken) return { type: "failed", error: "Missing refresh token in response" }
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
type: "success",
|
|
226
|
+
refresh: refreshToken,
|
|
227
|
+
access: tokenPayload.access_token,
|
|
228
|
+
expires: startTime + (tokenPayload.expires_in || 0) * 1000,
|
|
229
|
+
email: userInfo.email,
|
|
230
|
+
projectId: "",
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
return { type: "failed", error: error instanceof Error ? error.message : "Unknown error" }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeRedirectUri(uri: string | undefined, defaultPort: number): string {
|
|
238
|
+
const defaultUri = `http://localhost:${defaultPort}/auth/google/callback`
|
|
239
|
+
const rawUri = uri?.trim() || defaultUri
|
|
240
|
+
try {
|
|
241
|
+
const url = new URL(rawUri)
|
|
242
|
+
if (url.pathname.endsWith("/") && url.pathname !== "/") {
|
|
243
|
+
url.pathname = url.pathname.replace(/\/+$/, "")
|
|
244
|
+
return url.toString()
|
|
245
|
+
}
|
|
246
|
+
return rawUri
|
|
247
|
+
} catch (err) {
|
|
248
|
+
return rawUri
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function getCallbackPath(redirectUri: string): string {
|
|
253
|
+
try {
|
|
254
|
+
const url = new URL(redirectUri)
|
|
255
|
+
return url.pathname || "/auth/google/callback"
|
|
256
|
+
} catch {
|
|
257
|
+
return "/auth/google/callback"
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const opencodeAuthProxy: Plugin = async ({ client, directory }) => {
|
|
262
|
+
await client.app.log({
|
|
263
|
+
service: "opencode-auth-proxy",
|
|
264
|
+
level: "info",
|
|
265
|
+
message: "Plugin initializing",
|
|
266
|
+
extra: {
|
|
267
|
+
hasEnvPort: !!process.env.PORT,
|
|
268
|
+
hasEnvSessionSecret: !!process.env.SESSION_SECRET,
|
|
269
|
+
hasEnvGoogleId: !!process.env.GOOGLE_REDIRECT_CLIENT_ID,
|
|
270
|
+
directory,
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
const config = (client.app.config as { authProxy?: AuthProxyConfig })?.authProxy
|
|
275
|
+
|
|
276
|
+
const PORT = config?.port ?? Number(process.env.PORT ?? 4096)
|
|
277
|
+
const TARGET_HOST = config?.targetHost ?? (process.env.TARGET_HOST ?? "127.0.0.1")
|
|
278
|
+
const TARGET_PORT = config?.targetPort ?? Number(process.env.TARGET_PORT ?? 4097)
|
|
279
|
+
const GOOGLE_CLIENT_ID =
|
|
280
|
+
process.env.GOOGLE_REDIRECT_CLIENT_ID?.trim() ?? config?.googleClientId?.trim() ?? ""
|
|
281
|
+
const GOOGLE_CLIENT_SECRET =
|
|
282
|
+
process.env.GOOGLE_REDIRECT_CLIENT_SECRET?.trim() ?? config?.googleClientSecret?.trim() ?? ""
|
|
283
|
+
const GOOGLE_REDIRECT_URI = normalizeRedirectUri(
|
|
284
|
+
config?.googleRedirectUri ?? process.env.GOOGLE_REDIRECT_URI,
|
|
285
|
+
PORT,
|
|
286
|
+
)
|
|
287
|
+
const providedSessionSecret = process.env.SESSION_SECRET?.trim() ?? config?.sessionSecret?.trim()
|
|
288
|
+
const SESSION_SECRET = providedSessionSecret ?? crypto.randomBytes(32).toString("hex")
|
|
289
|
+
if (!providedSessionSecret) {
|
|
290
|
+
await client.app.log({
|
|
291
|
+
service: "opencode-auth-proxy",
|
|
292
|
+
level: "warn",
|
|
293
|
+
message: "SESSION_SECRET not provided, auto-generating. Sessions will not persist across restarts.",
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
const SESSION_COOKIE_NAME = config?.sessionCookieName ?? process.env.SESSION_COOKIE_NAME ?? "opencode_session"
|
|
297
|
+
const TOKEN_PATH =
|
|
298
|
+
config?.tokenPath || process.env.TOKEN_PATH || path.join(os.homedir(), ".opencode-proxy", "google-auth.json")
|
|
299
|
+
|
|
300
|
+
const COOKIE_SECURE = (() => {
|
|
301
|
+
if (config?.cookieSecure !== undefined) return config.cookieSecure
|
|
302
|
+
if (process.env.COOKIE_SECURE !== undefined) return process.env.COOKIE_SECURE !== "false"
|
|
303
|
+
try {
|
|
304
|
+
const redirectUrl = new URL(GOOGLE_REDIRECT_URI)
|
|
305
|
+
return redirectUrl.protocol === "https:"
|
|
306
|
+
} catch {
|
|
307
|
+
return false
|
|
308
|
+
}
|
|
309
|
+
})()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if (!GOOGLE_CLIENT_ID || !GOOGLE_CLIENT_SECRET) {
|
|
313
|
+
await client.app.log({
|
|
314
|
+
service: "opencode-auth-proxy",
|
|
315
|
+
level: "error",
|
|
316
|
+
message: "Google OAuth credentials are required (GOOGLE_REDIRECT_CLIENT_ID and GOOGLE_REDIRECT_CLIENT_SECRET)",
|
|
317
|
+
extra: {
|
|
318
|
+
hasClientId: !!GOOGLE_CLIENT_ID,
|
|
319
|
+
hasClientSecret: !!GOOGLE_CLIENT_SECRET,
|
|
320
|
+
hasEnvClientId: !!process.env.GOOGLE_REDIRECT_CLIENT_ID,
|
|
321
|
+
hasEnvClientSecret: !!process.env.GOOGLE_REDIRECT_CLIENT_SECRET,
|
|
322
|
+
},
|
|
323
|
+
})
|
|
324
|
+
return {}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const target = `http://${TARGET_HOST}:${TARGET_PORT}`
|
|
328
|
+
const CALLBACK_PATH = getCallbackPath(GOOGLE_REDIRECT_URI)
|
|
329
|
+
const PUBLIC_PATHS = new Set([
|
|
330
|
+
"/health",
|
|
331
|
+
"/global/health",
|
|
332
|
+
"/auth/google/start",
|
|
333
|
+
"/auth/google/config",
|
|
334
|
+
CALLBACK_PATH,
|
|
335
|
+
"/auth/google/callback",
|
|
336
|
+
])
|
|
337
|
+
|
|
338
|
+
function isPublicPath(pathname: string): boolean {
|
|
339
|
+
return PUBLIC_PATHS.has(pathname)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const proxy = httpProxy.createProxyServer({
|
|
343
|
+
target,
|
|
344
|
+
changeOrigin: true,
|
|
345
|
+
ws: true,
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
proxy.on("error", async (err: any, req: IncomingMessage, res: any) => {
|
|
349
|
+
const url = req.url || "/"
|
|
350
|
+
const method = req.method || "UNKNOWN"
|
|
351
|
+
await client.app.log({
|
|
352
|
+
service: "opencode-auth-proxy",
|
|
353
|
+
level: "error",
|
|
354
|
+
message: "Proxy error",
|
|
355
|
+
extra: {
|
|
356
|
+
message: err?.message,
|
|
357
|
+
code: err?.code,
|
|
358
|
+
errno: err?.errno,
|
|
359
|
+
syscall: err?.syscall,
|
|
360
|
+
target,
|
|
361
|
+
url,
|
|
362
|
+
method,
|
|
363
|
+
stack: err?.stack,
|
|
364
|
+
},
|
|
365
|
+
})
|
|
366
|
+
const response = res as ServerResponse | undefined
|
|
367
|
+
if (response && !response.headersSent) {
|
|
368
|
+
response.writeHead(502, { "Content-Type": "application/json" })
|
|
369
|
+
response.end(
|
|
370
|
+
JSON.stringify({
|
|
371
|
+
error: "Proxy error",
|
|
372
|
+
message: err?.message,
|
|
373
|
+
code: err?.code,
|
|
374
|
+
target,
|
|
375
|
+
}),
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
const server = http.createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
381
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`)
|
|
382
|
+
const pathname = url.pathname || "/"
|
|
383
|
+
const accept = String(req.headers.accept || "")
|
|
384
|
+
const isSSE = accept.includes("text/event-stream") || pathname === "/event" || pathname === "/global/event"
|
|
385
|
+
|
|
386
|
+
const rejectRequest = async () => {
|
|
387
|
+
if (isSSE) {
|
|
388
|
+
await client.app.log({
|
|
389
|
+
service: "opencode-auth-proxy",
|
|
390
|
+
level: "warn",
|
|
391
|
+
message: "Unauthorized request",
|
|
392
|
+
extra: { method: req.method, path: pathname, kind: "sse" },
|
|
393
|
+
})
|
|
394
|
+
res.writeHead(401, { "Content-Type": "application/json" })
|
|
395
|
+
res.write(JSON.stringify({ error: "unauthorized" }))
|
|
396
|
+
} else if (req.method && req.method.toUpperCase() === "GET") {
|
|
397
|
+
await client.app.log({
|
|
398
|
+
service: "opencode-auth-proxy",
|
|
399
|
+
level: "warn",
|
|
400
|
+
message: "Unauthorized request",
|
|
401
|
+
extra: { method: req.method, path: pathname, kind: "redirect" },
|
|
402
|
+
})
|
|
403
|
+
res.writeHead(302, { Location: "/auth/google/start" })
|
|
404
|
+
} else {
|
|
405
|
+
await client.app.log({
|
|
406
|
+
service: "opencode-auth-proxy",
|
|
407
|
+
level: "warn",
|
|
408
|
+
message: "Unauthorized request",
|
|
409
|
+
extra: { method: req.method, path: pathname, kind: "api" },
|
|
410
|
+
})
|
|
411
|
+
res.writeHead(401, { "Content-Type": "application/json" })
|
|
412
|
+
res.write(JSON.stringify({ error: "unauthorized" }))
|
|
413
|
+
}
|
|
414
|
+
res.end()
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!isPublicPath(pathname)) {
|
|
418
|
+
const cookies = parseCookies(req)
|
|
419
|
+
const isAuthenticated = cookies[SESSION_COOKIE_NAME]
|
|
420
|
+
? verifySession(cookies[SESSION_COOKIE_NAME], SESSION_SECRET)
|
|
421
|
+
: false
|
|
422
|
+
if (!isAuthenticated) {
|
|
423
|
+
await rejectRequest()
|
|
424
|
+
return
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (pathname === "/health" || pathname === "/global/health") {
|
|
429
|
+
const upstream = await (async () => {
|
|
430
|
+
try {
|
|
431
|
+
const response = await fetchWithTimeout(`${target}/global/health`, { method: "GET" }, 2000)
|
|
432
|
+
const body = await response
|
|
433
|
+
.json()
|
|
434
|
+
.catch(async () => ({ text: await response.text().catch(() => "") }))
|
|
435
|
+
return { ok: response.ok, status: response.status, body }
|
|
436
|
+
} catch (e) {
|
|
437
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) }
|
|
438
|
+
}
|
|
439
|
+
})()
|
|
440
|
+
res.writeHead(200, { "Content-Type": "application/json" })
|
|
441
|
+
res.end(JSON.stringify({ status: "ok", healthy: true, target, upstream }))
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (pathname === "/auth/google/config") {
|
|
446
|
+
res.writeHead(200, { "Content-Type": "application/json" })
|
|
447
|
+
res.end(
|
|
448
|
+
JSON.stringify({
|
|
449
|
+
redirect_uri: GOOGLE_REDIRECT_URI,
|
|
450
|
+
callback_path: CALLBACK_PATH,
|
|
451
|
+
client_id: GOOGLE_CLIENT_ID.substring(0, 20) + "...",
|
|
452
|
+
client_secret_set: !!GOOGLE_CLIENT_SECRET,
|
|
453
|
+
scopes: GOOGLE_SCOPES,
|
|
454
|
+
}),
|
|
455
|
+
)
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (pathname === "/auth/google/start") {
|
|
460
|
+
try {
|
|
461
|
+
const projectId = url.searchParams.get("project") || ""
|
|
462
|
+
const result = await authorizeGoogle(GOOGLE_CLIENT_ID, GOOGLE_REDIRECT_URI, projectId)
|
|
463
|
+
res.writeHead(302, { Location: result.url })
|
|
464
|
+
res.end()
|
|
465
|
+
} catch (err) {
|
|
466
|
+
await client.app.log({
|
|
467
|
+
service: "opencode-auth-proxy",
|
|
468
|
+
level: "error",
|
|
469
|
+
message: "Auth start error",
|
|
470
|
+
extra: { error: err instanceof Error ? err.message : String(err) },
|
|
471
|
+
})
|
|
472
|
+
res.writeHead(500, { "Content-Type": "application/json" })
|
|
473
|
+
res.end(JSON.stringify({ error: (err as Error)?.message || "auth start failed" }))
|
|
474
|
+
}
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (pathname === CALLBACK_PATH || pathname === "/auth/google/callback") {
|
|
479
|
+
const code = url.searchParams.get("code")
|
|
480
|
+
const state = url.searchParams.get("state") || ""
|
|
481
|
+
if (!code) {
|
|
482
|
+
res.writeHead(400, { "Content-Type": "application/json" })
|
|
483
|
+
res.end(JSON.stringify({ error: "missing code" }))
|
|
484
|
+
return
|
|
485
|
+
}
|
|
486
|
+
try {
|
|
487
|
+
const result = await exchangeGoogle(
|
|
488
|
+
code,
|
|
489
|
+
state,
|
|
490
|
+
GOOGLE_CLIENT_ID,
|
|
491
|
+
GOOGLE_CLIENT_SECRET,
|
|
492
|
+
GOOGLE_REDIRECT_URI,
|
|
493
|
+
client.app.log.bind(client.app),
|
|
494
|
+
)
|
|
495
|
+
if (result.type !== "success") {
|
|
496
|
+
res.writeHead(500, { "Content-Type": "application/json" })
|
|
497
|
+
res.end(JSON.stringify(result))
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
const payload = { ...result, storedAt: new Date().toISOString() }
|
|
501
|
+
persistToken(payload, TOKEN_PATH)
|
|
502
|
+
|
|
503
|
+
const cookie = `${SESSION_COOKIE_NAME}=${signSession(SESSION_SECRET)}; Path=/; HttpOnly; SameSite=Lax${COOKIE_SECURE ? "; Secure" : ""}`
|
|
504
|
+
const redirectTo = url.searchParams.get("redirect")
|
|
505
|
+
const targetPath = redirectTo && redirectTo.startsWith("/") ? redirectTo : "/"
|
|
506
|
+
|
|
507
|
+
res.writeHead(302, { Location: targetPath, "Set-Cookie": cookie })
|
|
508
|
+
res.end()
|
|
509
|
+
} catch (err) {
|
|
510
|
+
await client.app.log({
|
|
511
|
+
service: "opencode-auth-proxy",
|
|
512
|
+
level: "error",
|
|
513
|
+
message: "Auth callback error",
|
|
514
|
+
extra: { error: err instanceof Error ? err.message : String(err) },
|
|
515
|
+
})
|
|
516
|
+
res.writeHead(500, { "Content-Type": "application/json" })
|
|
517
|
+
res.end(JSON.stringify({ error: (err as Error)?.message || "auth callback failed" }))
|
|
518
|
+
}
|
|
519
|
+
return
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (url.pathname === "/auth/google/token") {
|
|
523
|
+
const token = loadToken(TOKEN_PATH)
|
|
524
|
+
if (!token) {
|
|
525
|
+
res.writeHead(404, { "Content-Type": "application/json" })
|
|
526
|
+
res.end(JSON.stringify({ error: "token not found" }))
|
|
527
|
+
return
|
|
528
|
+
}
|
|
529
|
+
res.writeHead(200, { "Content-Type": "application/json" })
|
|
530
|
+
res.end(JSON.stringify({ token }))
|
|
531
|
+
return
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
proxy.web(req, res, { target })
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
server.on("upgrade", async (req, socket, head) => {
|
|
538
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`)
|
|
539
|
+
if (!isPublicPath(url.pathname || "/")) {
|
|
540
|
+
const cookies = parseCookies(req)
|
|
541
|
+
const isAuthenticated = cookies[SESSION_COOKIE_NAME]
|
|
542
|
+
? verifySession(cookies[SESSION_COOKIE_NAME], SESSION_SECRET)
|
|
543
|
+
: false
|
|
544
|
+
if (!isAuthenticated) {
|
|
545
|
+
await client.app.log({
|
|
546
|
+
service: "opencode-auth-proxy",
|
|
547
|
+
level: "warn",
|
|
548
|
+
message: "Unauthorized WebSocket upgrade",
|
|
549
|
+
extra: { method: "UPGRADE", path: url.pathname || "/" },
|
|
550
|
+
})
|
|
551
|
+
socket.destroy()
|
|
552
|
+
return
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
proxy.ws(req, socket, head)
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
server.listen(PORT, async () => {
|
|
560
|
+
await client.app.log({
|
|
561
|
+
service: "opencode-auth-proxy",
|
|
562
|
+
level: "info",
|
|
563
|
+
message: `Proxy server started`,
|
|
564
|
+
extra: {
|
|
565
|
+
port: PORT,
|
|
566
|
+
target,
|
|
567
|
+
},
|
|
568
|
+
})
|
|
569
|
+
})
|
|
570
|
+
} catch (error) {
|
|
571
|
+
await client.app.log({
|
|
572
|
+
service: "opencode-auth-proxy",
|
|
573
|
+
level: "error",
|
|
574
|
+
message: "Failed to start proxy server",
|
|
575
|
+
extra: {
|
|
576
|
+
error: error instanceof Error ? error.message : String(error),
|
|
577
|
+
port: PORT,
|
|
578
|
+
},
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
event: async () => {},
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export { opencodeAuthProxy }
|
|
588
|
+
export default opencodeAuthProxy
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencode-auth-proxy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Google OAuth authentication proxy plugin for OpenCode - provides secure OAuth flow with session management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "opencode-auth-proxy.ts",
|
|
7
|
+
"main": "opencode-auth-proxy.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./opencode-auth-proxy.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"opencode-auth-proxy.ts",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"opencode",
|
|
19
|
+
"google",
|
|
20
|
+
"oauth",
|
|
21
|
+
"plugin",
|
|
22
|
+
"auth",
|
|
23
|
+
"proxy",
|
|
24
|
+
"authentication"
|
|
25
|
+
],
|
|
26
|
+
"author": "milc",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/opencode-ai/opencode-auth-proxy.git"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20.0.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"typescript": "^5"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@opencode-ai/plugin": "1.0.220",
|
|
40
|
+
"http-proxy": "^1.18.1"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^20.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|