openllmprovider 0.1.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/README.md +192 -0
- package/dist/auth/index.cjs +6 -0
- package/dist/auth/index.d.cts +3 -0
- package/dist/auth/index.d.mts +3 -0
- package/dist/auth/index.mjs +3 -0
- package/dist/auto-C2hXJY13.d.cts +33 -0
- package/dist/auto-C2hXJY13.d.cts.map +1 -0
- package/dist/auto-CBqNYBXs.mjs +48 -0
- package/dist/auto-CBqNYBXs.mjs.map +1 -0
- package/dist/auto-CInerwvs.d.mts +33 -0
- package/dist/auto-CInerwvs.d.mts.map +1 -0
- package/dist/auto-D77wgMqO.cjs +59 -0
- package/dist/auto-D77wgMqO.cjs.map +1 -0
- package/dist/file-DB-rxfzi.mjs +77 -0
- package/dist/file-DB-rxfzi.mjs.map +1 -0
- package/dist/file-DZ7FGcSW.cjs +73 -0
- package/dist/file-DZ7FGcSW.cjs.map +1 -0
- package/dist/index.cjs +1909 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1239 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +1241 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +1891 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger-BsHpI_fH.mjs +11 -0
- package/dist/logger-BsHpI_fH.mjs.map +1 -0
- package/dist/logger-jRimlMFR.cjs +69 -0
- package/dist/logger-jRimlMFR.cjs.map +1 -0
- package/dist/plugin/index.cjs +29 -0
- package/dist/plugin/index.cjs.map +1 -0
- package/dist/plugin/index.d.cts +10 -0
- package/dist/plugin/index.d.cts.map +1 -0
- package/dist/plugin/index.d.mts +10 -0
- package/dist/plugin/index.d.mts.map +1 -0
- package/dist/plugin/index.mjs +25 -0
- package/dist/plugin/index.mjs.map +1 -0
- package/dist/plugin-BkeUu5LW.d.mts +46 -0
- package/dist/plugin-BkeUu5LW.d.mts.map +1 -0
- package/dist/plugin-wK7RmJhZ.d.cts +46 -0
- package/dist/plugin-wK7RmJhZ.d.cts.map +1 -0
- package/dist/resolver-BA7LWSJO.mjs +645 -0
- package/dist/resolver-BA7LWSJO.mjs.map +1 -0
- package/dist/resolver-BMTvzTt9.cjs +662 -0
- package/dist/resolver-BMTvzTt9.cjs.map +1 -0
- package/dist/resolver-MgJryMWG.d.cts +75 -0
- package/dist/resolver-MgJryMWG.d.cts.map +1 -0
- package/dist/resolver-_gfXzr_S.d.mts +76 -0
- package/dist/resolver-_gfXzr_S.d.mts.map +1 -0
- package/dist/storage/index.cjs +7 -0
- package/dist/storage/index.d.cts +12 -0
- package/dist/storage/index.d.cts.map +1 -0
- package/dist/storage/index.d.mts +12 -0
- package/dist/storage/index.d.mts.map +1 -0
- package/dist/storage/index.mjs +4 -0
- package/package.json +137 -0
- package/src/auth/.gitkeep +0 -0
- package/src/auth/index.ts +10 -0
- package/src/auth/resolver.ts +46 -0
- package/src/auth/scanners.ts +462 -0
- package/src/auth/store.ts +357 -0
- package/src/catalog/.gitkeep +0 -0
- package/src/catalog/catalog.ts +302 -0
- package/src/catalog/index.ts +17 -0
- package/src/catalog/mapper.ts +129 -0
- package/src/catalog/merger.ts +99 -0
- package/src/index.ts +37 -0
- package/src/logger.ts +7 -0
- package/src/plugin/.gitkeep +0 -0
- package/src/plugin/anthropic.test.ts +505 -0
- package/src/plugin/anthropic.ts +324 -0
- package/src/plugin/codex.ts +656 -0
- package/src/plugin/copilot.ts +161 -0
- package/src/plugin/google.ts +454 -0
- package/src/plugin/index.ts +30 -0
- package/src/provider/.gitkeep +0 -0
- package/src/provider/bundled.ts +59 -0
- package/src/provider/index.ts +249 -0
- package/src/provider/state.ts +163 -0
- package/src/storage/.gitkeep +0 -0
- package/src/storage/auto.ts +32 -0
- package/src/storage/file.ts +84 -0
- package/src/storage/index.ts +10 -0
- package/src/storage/memory.ts +23 -0
- package/src/types/.gitkeep +0 -0
- package/src/types/auth.ts +18 -0
- package/src/types/errors.ts +87 -0
- package/src/types/index.ts +26 -0
- package/src/types/model.ts +88 -0
- package/src/types/plugin.ts +49 -0
- package/src/types/provider.ts +48 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createLogger } from '../logger.js'
|
|
2
|
+
import type { AuthCredential, AuthHook } from '../types/plugin.js'
|
|
3
|
+
|
|
4
|
+
const log = createLogger('plugin:copilot')
|
|
5
|
+
|
|
6
|
+
const GITHUB_CLIENT_ID = 'Ov23li8tweQw6odWQebz'
|
|
7
|
+
const GITHUB_DEFAULT_DOMAIN = 'github.com'
|
|
8
|
+
|
|
9
|
+
function normalizeDomain(url: string): string {
|
|
10
|
+
return url.replace(/^https?:\/\//, '').replace(/\/$/, '')
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getUrls(domain: string): { deviceCodeUrl: string; accessTokenUrl: string } {
|
|
14
|
+
return {
|
|
15
|
+
deviceCodeUrl: `https://${domain}/login/device/code`,
|
|
16
|
+
accessTokenUrl: `https://${domain}/login/oauth/access_token`,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveEnterpriseDomainFromEnv(): string {
|
|
21
|
+
const raw = process.env.OPENLLMPROVIDER_COPILOT_ENTERPRISE_URL?.trim()
|
|
22
|
+
if (!raw) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'Missing OPENLLMPROVIDER_COPILOT_ENTERPRISE_URL. Example: github.company.com or https://github.company.com'
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
return normalizeDomain(raw)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function runDeviceFlow(domain: string): Promise<AuthCredential> {
|
|
31
|
+
const urls = getUrls(domain)
|
|
32
|
+
const deviceRes = await globalThis.fetch(urls.deviceCodeUrl, {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: {
|
|
35
|
+
'Content-Type': 'application/json',
|
|
36
|
+
Accept: 'application/json',
|
|
37
|
+
},
|
|
38
|
+
body: JSON.stringify({
|
|
39
|
+
client_id: GITHUB_CLIENT_ID,
|
|
40
|
+
scope: 'read:user',
|
|
41
|
+
}),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
if (!deviceRes.ok) {
|
|
45
|
+
const body = await deviceRes.text()
|
|
46
|
+
throw new Error(`Device flow init failed: ${deviceRes.status} ${deviceRes.statusText} ${body}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const deviceData = (await deviceRes.json()) as {
|
|
50
|
+
device_code: string
|
|
51
|
+
user_code: string
|
|
52
|
+
verification_uri: string
|
|
53
|
+
interval: number
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
log(
|
|
57
|
+
'device flow: domain=%s user_code=%s verification_uri=%s',
|
|
58
|
+
domain,
|
|
59
|
+
deviceData.user_code,
|
|
60
|
+
deviceData.verification_uri
|
|
61
|
+
)
|
|
62
|
+
const token = await pollForToken(urls.accessTokenUrl, deviceData.device_code, deviceData.interval)
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
type: 'oauth',
|
|
66
|
+
refresh: token,
|
|
67
|
+
key: token,
|
|
68
|
+
expires: 0,
|
|
69
|
+
...(domain !== GITHUB_DEFAULT_DOMAIN ? { enterpriseUrl: domain } : {}),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const copilotPlugin: AuthHook = {
|
|
74
|
+
provider: 'github-copilot',
|
|
75
|
+
|
|
76
|
+
async loader(getAuth, _provider) {
|
|
77
|
+
const auth = await getAuth()
|
|
78
|
+
const enterpriseUrl = typeof auth.enterpriseUrl === 'string' ? auth.enterpriseUrl : undefined
|
|
79
|
+
const baseURL = enterpriseUrl
|
|
80
|
+
? `https://copilot-api.${normalizeDomain(enterpriseUrl)}`
|
|
81
|
+
: 'https://api.githubcopilot.com'
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
baseURL,
|
|
85
|
+
apiKey: '',
|
|
86
|
+
async fetch(...[request, init]: Parameters<typeof globalThis.fetch>): ReturnType<typeof globalThis.fetch> {
|
|
87
|
+
const auth = await getAuth()
|
|
88
|
+
const headers = new Headers(init?.headers)
|
|
89
|
+
// Remove SDK-set auth headers
|
|
90
|
+
headers.delete('x-api-key')
|
|
91
|
+
headers.delete('Authorization')
|
|
92
|
+
// Set copilot-specific auth
|
|
93
|
+
headers.set('Authorization', `Bearer ${auth.refresh ?? auth.key ?? ''}`)
|
|
94
|
+
headers.set('Openai-Intent', 'conversation-edits')
|
|
95
|
+
log('copilot fetch: injecting auth headers')
|
|
96
|
+
return globalThis.fetch(request, { ...init, headers })
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
methods: [
|
|
102
|
+
{
|
|
103
|
+
type: 'oauth',
|
|
104
|
+
label: 'GitHub Copilot (GitHub.com)',
|
|
105
|
+
async handler(): Promise<AuthCredential> {
|
|
106
|
+
log('starting github.com device flow')
|
|
107
|
+
return runDeviceFlow(GITHUB_DEFAULT_DOMAIN)
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
type: 'device-flow',
|
|
112
|
+
label: 'GitHub Copilot Enterprise (Device Flow)',
|
|
113
|
+
async handler(): Promise<AuthCredential> {
|
|
114
|
+
log('starting enterprise device flow')
|
|
115
|
+
const domain = resolveEnterpriseDomainFromEnv()
|
|
116
|
+
return runDeviceFlow(domain)
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
],
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function pollForToken(accessTokenUrl: string, deviceCode: string, interval: number): Promise<string> {
|
|
123
|
+
const pollInterval = Math.max(interval, 5) * 1000
|
|
124
|
+
|
|
125
|
+
while (true) {
|
|
126
|
+
await new Promise<void>((resolve) => setTimeout(resolve, pollInterval))
|
|
127
|
+
|
|
128
|
+
const res = await globalThis.fetch(accessTokenUrl, {
|
|
129
|
+
method: 'POST',
|
|
130
|
+
headers: {
|
|
131
|
+
'Content-Type': 'application/json',
|
|
132
|
+
Accept: 'application/json',
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
client_id: GITHUB_CLIENT_ID,
|
|
136
|
+
device_code: deviceCode,
|
|
137
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
138
|
+
}),
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const data = (await res.json()) as { access_token?: string; error?: string }
|
|
142
|
+
|
|
143
|
+
if (data.access_token) {
|
|
144
|
+
log('device flow: token obtained')
|
|
145
|
+
return data.access_token
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (data.error === 'authorization_pending') {
|
|
149
|
+
log('device flow: waiting for user authorization...')
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (data.error === 'slow_down') {
|
|
154
|
+
log('device flow: slowing down')
|
|
155
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 5000))
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
throw new Error(`Device flow failed: ${data.error ?? 'unknown error'}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { createHash, randomBytes, randomUUID } from 'node:crypto'
|
|
3
|
+
import { stdin as input, stdout as output } from 'node:process'
|
|
4
|
+
import { createInterface } from 'node:readline/promises'
|
|
5
|
+
import { createLogger } from '../logger.js'
|
|
6
|
+
import type { AuthCredential, AuthHook, ProviderInfo } from '../types/plugin.js'
|
|
7
|
+
|
|
8
|
+
const log = createLogger('plugin:google')
|
|
9
|
+
|
|
10
|
+
const GEMINI_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com'
|
|
11
|
+
const GEMINI_CLIENT_SECRET = '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'
|
|
12
|
+
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token'
|
|
13
|
+
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
|
|
14
|
+
const GOOGLE_USERINFO_URL = 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json'
|
|
15
|
+
const GEMINI_REDIRECT_URI = 'http://localhost:8085/oauth2callback'
|
|
16
|
+
const GEMINI_SCOPES = [
|
|
17
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
18
|
+
'https://www.googleapis.com/auth/userinfo.email',
|
|
19
|
+
'https://www.googleapis.com/auth/userinfo.profile',
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com'
|
|
23
|
+
const CODE_ASSIST_HEADERS = {
|
|
24
|
+
'User-Agent': 'google-api-nodejs-client/9.15.1',
|
|
25
|
+
'X-Goog-Api-Client': 'gl-node/22.17.0',
|
|
26
|
+
'Client-Metadata': 'ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface TokenResponse {
|
|
30
|
+
access_token: string
|
|
31
|
+
refresh_token?: string
|
|
32
|
+
expires_in: number
|
|
33
|
+
token_type: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveProjectId(): string {
|
|
37
|
+
const explicit = process.env.OPENLLMPROVIDER_GOOGLE_PROJECT_ID?.trim()
|
|
38
|
+
if (explicit) return explicit
|
|
39
|
+
const gcp = process.env.GOOGLE_CLOUD_PROJECT?.trim() ?? process.env.GOOGLE_CLOUD_PROJECT_ID?.trim()
|
|
40
|
+
if (gcp) return gcp
|
|
41
|
+
return ''
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function refreshGoogleToken(refreshToken: string): Promise<TokenResponse> {
|
|
45
|
+
log('refreshing Google OAuth token')
|
|
46
|
+
|
|
47
|
+
const body = new URLSearchParams({
|
|
48
|
+
grant_type: 'refresh_token',
|
|
49
|
+
refresh_token: refreshToken,
|
|
50
|
+
client_id: GEMINI_CLIENT_ID,
|
|
51
|
+
client_secret: GEMINI_CLIENT_SECRET,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const res = await globalThis.fetch(GOOGLE_TOKEN_URL, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
57
|
+
body: body.toString(),
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
throw new Error(`Google token refresh failed: ${res.status} ${res.statusText}`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const raw = (await res.json()) as Record<string, unknown>
|
|
65
|
+
return {
|
|
66
|
+
access_token: String(raw.access_token ?? ''),
|
|
67
|
+
refresh_token: typeof raw.refresh_token === 'string' ? raw.refresh_token : undefined,
|
|
68
|
+
expires_in: Number(raw.expires_in ?? 3600),
|
|
69
|
+
token_type: String(raw.token_type ?? 'Bearer'),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toBase64Url(inputValue: Buffer): string {
|
|
74
|
+
return inputValue.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createPkce(): { verifier: string; challenge: string } {
|
|
78
|
+
const verifier = toBase64Url(randomBytes(32))
|
|
79
|
+
const challenge = toBase64Url(createHash('sha256').update(verifier).digest())
|
|
80
|
+
return { verifier, challenge }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildAuthorizationRequest(): { url: string; verifier: string; state: string } {
|
|
84
|
+
const { verifier, challenge } = createPkce()
|
|
85
|
+
const state = toBase64Url(randomBytes(24))
|
|
86
|
+
|
|
87
|
+
const url = new URL(GOOGLE_AUTH_URL)
|
|
88
|
+
url.searchParams.set('client_id', GEMINI_CLIENT_ID)
|
|
89
|
+
url.searchParams.set('response_type', 'code')
|
|
90
|
+
url.searchParams.set('redirect_uri', GEMINI_REDIRECT_URI)
|
|
91
|
+
url.searchParams.set('scope', GEMINI_SCOPES.join(' '))
|
|
92
|
+
url.searchParams.set('code_challenge', challenge)
|
|
93
|
+
url.searchParams.set('code_challenge_method', 'S256')
|
|
94
|
+
url.searchParams.set('state', state)
|
|
95
|
+
url.searchParams.set('access_type', 'offline')
|
|
96
|
+
url.searchParams.set('prompt', 'consent')
|
|
97
|
+
|
|
98
|
+
return { url: url.toString(), verifier, state }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function readCallbackInput(question: string): Promise<string> {
|
|
102
|
+
const rl = createInterface({ input, output })
|
|
103
|
+
try {
|
|
104
|
+
return (await rl.question(question)).trim()
|
|
105
|
+
} finally {
|
|
106
|
+
rl.close()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function parseCallbackInput(value: string): { code?: string; state?: string } {
|
|
111
|
+
const raw = value.trim()
|
|
112
|
+
if (raw.length === 0) return {}
|
|
113
|
+
|
|
114
|
+
if (/^https?:\/\//i.test(raw)) {
|
|
115
|
+
try {
|
|
116
|
+
const u = new URL(raw)
|
|
117
|
+
return {
|
|
118
|
+
code: u.searchParams.get('code') ?? undefined,
|
|
119
|
+
state: u.searchParams.get('state') ?? undefined,
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
return {}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const maybeQuery = raw.startsWith('?') ? raw.slice(1) : raw
|
|
127
|
+
if (maybeQuery.includes('=')) {
|
|
128
|
+
const params = new URLSearchParams(maybeQuery)
|
|
129
|
+
const code = params.get('code') ?? undefined
|
|
130
|
+
const state = params.get('state') ?? undefined
|
|
131
|
+
if (code !== undefined || state !== undefined) {
|
|
132
|
+
return { code, state }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { code: raw }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function openUrlInBrowser(url: string): void {
|
|
140
|
+
const platform = process.platform
|
|
141
|
+
const command = platform === 'darwin' ? 'open' : platform === 'win32' ? 'rundll32' : 'xdg-open'
|
|
142
|
+
const args = platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url]
|
|
143
|
+
try {
|
|
144
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' })
|
|
145
|
+
child.unref()
|
|
146
|
+
} catch {
|
|
147
|
+
log('failed to open browser automatically')
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function exchangeAuthorizationCode(code: string, verifier: string): Promise<TokenResponse> {
|
|
152
|
+
const body = new URLSearchParams({
|
|
153
|
+
client_id: GEMINI_CLIENT_ID,
|
|
154
|
+
client_secret: GEMINI_CLIENT_SECRET,
|
|
155
|
+
code,
|
|
156
|
+
grant_type: 'authorization_code',
|
|
157
|
+
redirect_uri: GEMINI_REDIRECT_URI,
|
|
158
|
+
code_verifier: verifier,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
const res = await globalThis.fetch(GOOGLE_TOKEN_URL, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
164
|
+
body: body.toString(),
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
const bodyText = await res.text()
|
|
169
|
+
throw new Error(`Google token exchange failed: ${res.status} ${res.statusText} ${bodyText}`)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const raw = (await res.json()) as Record<string, unknown>
|
|
173
|
+
return {
|
|
174
|
+
access_token: String(raw.access_token ?? ''),
|
|
175
|
+
refresh_token: typeof raw.refresh_token === 'string' ? raw.refresh_token : undefined,
|
|
176
|
+
expires_in: Number(raw.expires_in ?? 3600),
|
|
177
|
+
token_type: String(raw.token_type ?? 'Bearer'),
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function fetchGoogleEmail(accessToken: string): Promise<string | undefined> {
|
|
182
|
+
const res = await globalThis.fetch(GOOGLE_USERINFO_URL, {
|
|
183
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
184
|
+
})
|
|
185
|
+
if (!res.ok) return undefined
|
|
186
|
+
const raw = (await res.json()) as Record<string, unknown>
|
|
187
|
+
return typeof raw.email === 'string' ? raw.email : undefined
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type FetchInput = Parameters<typeof globalThis.fetch>[0]
|
|
191
|
+
type FetchInit = Parameters<typeof globalThis.fetch>[1]
|
|
192
|
+
type FetchBody = NonNullable<FetchInit>['body']
|
|
193
|
+
|
|
194
|
+
function toRequestUrlString(value: FetchInput | URL): string {
|
|
195
|
+
if (typeof value === 'string') return value
|
|
196
|
+
if (value instanceof URL) return value.toString()
|
|
197
|
+
return value.url
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseGenerativeAction(
|
|
201
|
+
input: FetchInput | URL
|
|
202
|
+
): { model: string; action: string; streaming: boolean } | undefined {
|
|
203
|
+
const url = toRequestUrlString(input)
|
|
204
|
+
const match = url.match(/\/models\/([^:]+):(\w+)/)
|
|
205
|
+
if (!match) return undefined
|
|
206
|
+
const model = match[1] ?? ''
|
|
207
|
+
const action = match[2] ?? ''
|
|
208
|
+
if (!model || !action) return undefined
|
|
209
|
+
return { model, action, streaming: action === 'streamGenerateContent' }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildCodeAssistUrl(action: string, streaming: boolean): string {
|
|
213
|
+
return `${CODE_ASSIST_ENDPOINT}/v1internal:${action}${streaming ? '?alt=sse' : ''}`
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function rewriteRequestBody(body: FetchBody, projectId: string, model: string): FetchBody {
|
|
217
|
+
if (typeof body !== 'string' || body.length === 0) return body
|
|
218
|
+
try {
|
|
219
|
+
const parsed = JSON.parse(body) as Record<string, unknown>
|
|
220
|
+
if (typeof parsed.project === 'string' && parsed.request !== undefined) {
|
|
221
|
+
const wrapped = { ...parsed, model }
|
|
222
|
+
return JSON.stringify(wrapped)
|
|
223
|
+
}
|
|
224
|
+
const { model: _ignored, ...requestPayload } = parsed
|
|
225
|
+
const userPromptId = randomUUID()
|
|
226
|
+
const wrapped = {
|
|
227
|
+
project: projectId,
|
|
228
|
+
model,
|
|
229
|
+
user_prompt_id: userPromptId,
|
|
230
|
+
request: requestPayload,
|
|
231
|
+
}
|
|
232
|
+
return JSON.stringify(wrapped)
|
|
233
|
+
} catch {
|
|
234
|
+
return body
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function rewriteStreamingLine(line: string): string {
|
|
239
|
+
if (!line.startsWith('data:')) return line
|
|
240
|
+
const payload = line.slice(5).trim()
|
|
241
|
+
if (!payload || payload === '[DONE]') return line
|
|
242
|
+
try {
|
|
243
|
+
const parsed = JSON.parse(payload) as Record<string, unknown>
|
|
244
|
+
const response = parsed.response
|
|
245
|
+
if (response !== undefined) {
|
|
246
|
+
return `data: ${JSON.stringify(response)}`
|
|
247
|
+
}
|
|
248
|
+
return line
|
|
249
|
+
} catch {
|
|
250
|
+
return line
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function rewriteStreamingBody(stream: ReadableStream<Uint8Array>): ReadableStream<Uint8Array> {
|
|
255
|
+
const decoder = new TextDecoder()
|
|
256
|
+
const encoder = new TextEncoder()
|
|
257
|
+
let buffer = ''
|
|
258
|
+
|
|
259
|
+
return new ReadableStream<Uint8Array>({
|
|
260
|
+
async start(controller) {
|
|
261
|
+
const reader = stream.getReader()
|
|
262
|
+
try {
|
|
263
|
+
while (true) {
|
|
264
|
+
const { done, value } = await reader.read()
|
|
265
|
+
if (done) break
|
|
266
|
+
buffer += decoder.decode(value, { stream: true })
|
|
267
|
+
let idx = buffer.indexOf('\n')
|
|
268
|
+
while (idx !== -1) {
|
|
269
|
+
const line = buffer.slice(0, idx)
|
|
270
|
+
buffer = buffer.slice(idx + 1)
|
|
271
|
+
controller.enqueue(encoder.encode(`${rewriteStreamingLine(line)}\n`))
|
|
272
|
+
idx = buffer.indexOf('\n')
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
buffer += decoder.decode()
|
|
276
|
+
if (buffer.length > 0) {
|
|
277
|
+
controller.enqueue(encoder.encode(rewriteStreamingLine(buffer)))
|
|
278
|
+
}
|
|
279
|
+
controller.close()
|
|
280
|
+
} catch (error) {
|
|
281
|
+
controller.error(error)
|
|
282
|
+
} finally {
|
|
283
|
+
reader.releaseLock()
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function normalizeCodeAssistResponse(response: Response, streaming: boolean): Promise<Response> {
|
|
290
|
+
const contentType = response.headers.get('content-type') ?? ''
|
|
291
|
+
|
|
292
|
+
if (streaming && response.ok && contentType.includes('text/event-stream') && response.body) {
|
|
293
|
+
return new Response(rewriteStreamingBody(response.body), {
|
|
294
|
+
status: response.status,
|
|
295
|
+
statusText: response.statusText,
|
|
296
|
+
headers: new Headers(response.headers),
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!contentType.includes('application/json')) {
|
|
301
|
+
return response
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const text = await response.text()
|
|
305
|
+
try {
|
|
306
|
+
const parsed = JSON.parse(text) as Record<string, unknown>
|
|
307
|
+
const next = parsed.response
|
|
308
|
+
if (next !== undefined) {
|
|
309
|
+
return new Response(JSON.stringify(next), {
|
|
310
|
+
status: response.status,
|
|
311
|
+
statusText: response.statusText,
|
|
312
|
+
headers: new Headers(response.headers),
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
return new Response(text, {
|
|
316
|
+
status: response.status,
|
|
317
|
+
statusText: response.statusText,
|
|
318
|
+
headers: new Headers(response.headers),
|
|
319
|
+
})
|
|
320
|
+
} catch {
|
|
321
|
+
return new Response(text, {
|
|
322
|
+
status: response.status,
|
|
323
|
+
statusText: response.statusText,
|
|
324
|
+
headers: new Headers(response.headers),
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export const googlePlugin: AuthHook = {
|
|
330
|
+
provider: 'google',
|
|
331
|
+
|
|
332
|
+
async loader(getAuth: () => Promise<AuthCredential>, _provider: ProviderInfo, setAuth: (credential: AuthCredential) => Promise<void>): Promise<Record<string, unknown>> {
|
|
333
|
+
const initialAuth = await getAuth()
|
|
334
|
+
|
|
335
|
+
if (initialAuth.type !== 'oauth') {
|
|
336
|
+
log('google loader: skipping (type=%s)', initialAuth.type)
|
|
337
|
+
return {}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
log('google loader: activating OAuth fetch wrapper')
|
|
341
|
+
return {
|
|
342
|
+
apiKey: 'google-oauth-placeholder',
|
|
343
|
+
async fetch(
|
|
344
|
+
inputValue: Parameters<typeof globalThis.fetch>[0],
|
|
345
|
+
init?: Parameters<typeof globalThis.fetch>[1]
|
|
346
|
+
): Promise<Response> {
|
|
347
|
+
const currentAuth = await getAuth()
|
|
348
|
+
if (currentAuth.type !== 'oauth') {
|
|
349
|
+
return globalThis.fetch(inputValue, init)
|
|
350
|
+
}
|
|
351
|
+
let currentToken = currentAuth.key ?? ''
|
|
352
|
+
if (
|
|
353
|
+
currentAuth.expires !== undefined &&
|
|
354
|
+
currentAuth.expires < Date.now() &&
|
|
355
|
+
typeof currentAuth.refresh === 'string'
|
|
356
|
+
) {
|
|
357
|
+
log('token expired, attempting refresh...')
|
|
358
|
+
try {
|
|
359
|
+
const tokens = await refreshGoogleToken(currentAuth.refresh)
|
|
360
|
+
currentToken = tokens.access_token
|
|
361
|
+
const updated: AuthCredential = {
|
|
362
|
+
...currentAuth,
|
|
363
|
+
key: tokens.access_token,
|
|
364
|
+
refresh: tokens.refresh_token ?? currentAuth.refresh,
|
|
365
|
+
expires: Date.now() + tokens.expires_in * 1000,
|
|
366
|
+
}
|
|
367
|
+
await setAuth(updated).catch((err) =>
|
|
368
|
+
log('failed to persist refreshed credential: %s', err instanceof Error ? err.message : String(err)),
|
|
369
|
+
)
|
|
370
|
+
log('token refreshed successfully, expires in %ds', tokens.expires_in)
|
|
371
|
+
} catch (err: unknown) {
|
|
372
|
+
log('token refresh failed: %s', err instanceof Error ? err.message : String(err))
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const rewritten = parseGenerativeAction(inputValue)
|
|
377
|
+
if (!rewritten) {
|
|
378
|
+
const headers = new Headers(init?.headers)
|
|
379
|
+
headers.delete('x-goog-api-key')
|
|
380
|
+
headers.delete('x-api-key')
|
|
381
|
+
headers.set('Authorization', `Bearer ${currentToken}`)
|
|
382
|
+
return globalThis.fetch(inputValue, { ...init, headers })
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const projectId = resolveProjectId()
|
|
386
|
+
if (!projectId) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
'Google OAuth via Code Assist requires project id. Set OPENLLMPROVIDER_GOOGLE_PROJECT_ID (or GOOGLE_CLOUD_PROJECT).'
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const headers = new Headers(init?.headers)
|
|
393
|
+
headers.delete('x-goog-api-key')
|
|
394
|
+
headers.delete('x-api-key')
|
|
395
|
+
headers.set('Authorization', `Bearer ${currentToken}`)
|
|
396
|
+
headers.set('User-Agent', CODE_ASSIST_HEADERS['User-Agent'])
|
|
397
|
+
headers.set('X-Goog-Api-Client', CODE_ASSIST_HEADERS['X-Goog-Api-Client'])
|
|
398
|
+
headers.set('Client-Metadata', CODE_ASSIST_HEADERS['Client-Metadata'])
|
|
399
|
+
headers.set('x-activity-request-id', randomUUID())
|
|
400
|
+
if (rewritten.streaming) {
|
|
401
|
+
headers.set('Accept', 'text/event-stream')
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const requestUrl = buildCodeAssistUrl(rewritten.action, rewritten.streaming)
|
|
405
|
+
const body = rewriteRequestBody(init?.body, projectId, rewritten.model)
|
|
406
|
+
const response = await globalThis.fetch(requestUrl, {
|
|
407
|
+
...init,
|
|
408
|
+
headers,
|
|
409
|
+
body,
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
return normalizeCodeAssistResponse(response, rewritten.streaming)
|
|
413
|
+
},
|
|
414
|
+
}
|
|
415
|
+
},
|
|
416
|
+
|
|
417
|
+
methods: [
|
|
418
|
+
{
|
|
419
|
+
type: 'oauth',
|
|
420
|
+
label: 'Google OAuth (Gemini)',
|
|
421
|
+
async handler(): Promise<AuthCredential> {
|
|
422
|
+
const authRequest = buildAuthorizationRequest()
|
|
423
|
+
|
|
424
|
+
console.log('Open this URL to continue Google OAuth:')
|
|
425
|
+
console.log(authRequest.url)
|
|
426
|
+
openUrlInBrowser(authRequest.url)
|
|
427
|
+
|
|
428
|
+
const callbackInput = await readCallbackInput('Paste the callback URL or authorization code: ')
|
|
429
|
+
const parsed = parseCallbackInput(callbackInput)
|
|
430
|
+
if (!parsed.code) {
|
|
431
|
+
throw new Error('Missing authorization code in callback input')
|
|
432
|
+
}
|
|
433
|
+
if (parsed.state !== undefined && parsed.state !== authRequest.state) {
|
|
434
|
+
throw new Error('OAuth state mismatch')
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const tokens = await exchangeAuthorizationCode(parsed.code, authRequest.verifier)
|
|
438
|
+
if (!tokens.refresh_token) {
|
|
439
|
+
throw new Error('Google OAuth did not return a refresh token; retry and grant consent')
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const email = await fetchGoogleEmail(tokens.access_token)
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
type: 'oauth',
|
|
446
|
+
key: tokens.access_token,
|
|
447
|
+
refresh: tokens.refresh_token,
|
|
448
|
+
expires: Date.now() + tokens.expires_in * 1000,
|
|
449
|
+
email,
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createLogger } from '../logger.js'
|
|
2
|
+
import type { AuthCredential, AuthHook, ProviderInfo } from '../types/plugin.js'
|
|
3
|
+
|
|
4
|
+
const log = createLogger('plugin')
|
|
5
|
+
const plugins: Map<string, AuthHook> = new Map()
|
|
6
|
+
|
|
7
|
+
export function registerPlugin(plugin: AuthHook): void {
|
|
8
|
+
log('registering plugin for provider: %s', plugin.provider)
|
|
9
|
+
plugins.set(plugin.provider, plugin)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getPlugins(): AuthHook[] {
|
|
13
|
+
return [...plugins.values()]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getPluginForProvider(providerId: string): AuthHook | undefined {
|
|
17
|
+
return plugins.get(providerId)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function loadPluginOptions(
|
|
21
|
+
providerId: string,
|
|
22
|
+
getAuth: () => Promise<AuthCredential>,
|
|
23
|
+
providerInfo: ProviderInfo,
|
|
24
|
+
setAuth: (credential: AuthCredential) => Promise<void>,
|
|
25
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
26
|
+
const plugin = plugins.get(providerId)
|
|
27
|
+
if (!plugin) return undefined
|
|
28
|
+
log('loading plugin options for provider: %s', providerId)
|
|
29
|
+
return plugin.loader(getAuth, providerInfo, setAuth)
|
|
30
|
+
}
|
|
File without changes
|