opencode-google-auth 0.0.1
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/LICENSE +21 -0
- package/README.md +8 -0
- package/dist/main.mjs +716 -0
- package/dist/main.mjs.map +1 -0
- package/package.json +66 -0
- package/src/lib/logger.ts +43 -0
- package/src/lib/runtime.ts +34 -0
- package/src/lib/services/config.test.ts +194 -0
- package/src/lib/services/config.ts +378 -0
- package/src/lib/services/oauth.ts +135 -0
- package/src/lib/services/opencode.ts +7 -0
- package/src/lib/services/session.ts +212 -0
- package/src/main.ts +273 -0
- package/src/models.json +198 -0
- package/src/transform/request.test.ts +176 -0
- package/src/transform/request.ts +102 -0
- package/src/transform/response.test.ts +75 -0
- package/src/transform/response.ts +27 -0
- package/src/transform/stream.test.ts +108 -0
- package/src/transform/stream.ts +61 -0
- package/src/types.ts +66 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import type { GoogleGenerativeAIProviderOptions } from "@ai-sdk/google"
|
|
2
|
+
import { Context, Effect, pipe } from "effect"
|
|
3
|
+
import type {
|
|
4
|
+
ModelsDev,
|
|
5
|
+
OpenCodeModel,
|
|
6
|
+
OpenCodeProvider,
|
|
7
|
+
Provider,
|
|
8
|
+
} from "../../types"
|
|
9
|
+
|
|
10
|
+
export interface WrappedBody {
|
|
11
|
+
readonly project: string
|
|
12
|
+
readonly request: unknown
|
|
13
|
+
readonly model: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface RequestContext {
|
|
17
|
+
readonly body: WrappedBody
|
|
18
|
+
readonly headers: Headers
|
|
19
|
+
readonly url: URL
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProviderConfigShape {
|
|
23
|
+
readonly SERVICE_NAME: string
|
|
24
|
+
readonly DISPLAY_NAME: string
|
|
25
|
+
readonly ENDPOINTS: readonly string[]
|
|
26
|
+
readonly HEADERS: Readonly<Record<string, string>>
|
|
27
|
+
readonly SCOPES: readonly string[]
|
|
28
|
+
readonly CLIENT_ID: string
|
|
29
|
+
readonly CLIENT_SECRET: string
|
|
30
|
+
readonly getConfig: (modelsDev: ModelsDev) => OpenCodeProvider
|
|
31
|
+
readonly transformRequest?: (context: RequestContext) => Effect.Effect<{
|
|
32
|
+
body: Record<string, unknown>
|
|
33
|
+
headers: Headers
|
|
34
|
+
url: URL
|
|
35
|
+
}>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ProviderConfig extends Context.Tag("ProviderConfig")<
|
|
39
|
+
ProviderConfig,
|
|
40
|
+
ProviderConfigShape
|
|
41
|
+
>() {}
|
|
42
|
+
|
|
43
|
+
export const CODE_ASSIST_VERSION = "v1internal"
|
|
44
|
+
|
|
45
|
+
export const CLIENT_METADATA = {
|
|
46
|
+
ideType: "IDE_UNSPECIFIED",
|
|
47
|
+
platform: "PLATFORM_UNSPECIFIED",
|
|
48
|
+
pluginType: "GEMINI",
|
|
49
|
+
} as const
|
|
50
|
+
|
|
51
|
+
export const GEMINI_CLI_MODELS = [
|
|
52
|
+
"gemini-2.5-pro",
|
|
53
|
+
"gemini-2.5-flash",
|
|
54
|
+
"gemini-2.5-flash-lite",
|
|
55
|
+
"gemini-3-pro-preview",
|
|
56
|
+
"gemini-3-flash-preview",
|
|
57
|
+
] as const
|
|
58
|
+
|
|
59
|
+
export const ANTIGRAVITY_MODELS = [
|
|
60
|
+
"gemini-3-flash",
|
|
61
|
+
"gemini-3-pro-low",
|
|
62
|
+
"gemini-3-pro-high",
|
|
63
|
+
"claude-sonnet-4-5",
|
|
64
|
+
"claude-sonnet-4-5-thinking",
|
|
65
|
+
"claude-opus-4-5-thinking",
|
|
66
|
+
] as const
|
|
67
|
+
|
|
68
|
+
export const geminiCliConfig = (): ProviderConfigShape => ({
|
|
69
|
+
SERVICE_NAME: "gemini-cli",
|
|
70
|
+
DISPLAY_NAME: "Gemini CLI",
|
|
71
|
+
CLIENT_ID:
|
|
72
|
+
"681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com",
|
|
73
|
+
CLIENT_SECRET: "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",
|
|
74
|
+
SCOPES: [
|
|
75
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
76
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
77
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
78
|
+
],
|
|
79
|
+
ENDPOINTS: ["https://cloudcode-pa.googleapis.com"],
|
|
80
|
+
HEADERS: {
|
|
81
|
+
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
82
|
+
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
83
|
+
"Client-Metadata":
|
|
84
|
+
"ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
|
|
85
|
+
},
|
|
86
|
+
getConfig: (modelsDev) => {
|
|
87
|
+
const provider = modelsDev.google as Provider
|
|
88
|
+
const filteredModels = pipe(
|
|
89
|
+
provider.models,
|
|
90
|
+
(models) => Object.entries(models),
|
|
91
|
+
(entries) =>
|
|
92
|
+
entries.filter(([key]) =>
|
|
93
|
+
(GEMINI_CLI_MODELS as readonly string[]).includes(key),
|
|
94
|
+
),
|
|
95
|
+
(filtered) => Object.fromEntries(filtered),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...provider,
|
|
100
|
+
id: geminiCliConfig().SERVICE_NAME,
|
|
101
|
+
name: geminiCliConfig().DISPLAY_NAME,
|
|
102
|
+
api: geminiCliConfig().ENDPOINTS.at(0) as string,
|
|
103
|
+
models: filteredModels as Record<string, OpenCodeModel>,
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
export const antigravityConfig = (): ProviderConfigShape => ({
|
|
109
|
+
SERVICE_NAME: "antigravity",
|
|
110
|
+
DISPLAY_NAME: "Antigravity",
|
|
111
|
+
CLIENT_ID:
|
|
112
|
+
"1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
|
|
113
|
+
CLIENT_SECRET: "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",
|
|
114
|
+
SCOPES: [
|
|
115
|
+
"https://www.googleapis.com/auth/cloud-platform",
|
|
116
|
+
"https://www.googleapis.com/auth/userinfo.email",
|
|
117
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
118
|
+
"https://www.googleapis.com/auth/cclog",
|
|
119
|
+
"https://www.googleapis.com/auth/experimentsandconfigs",
|
|
120
|
+
],
|
|
121
|
+
ENDPOINTS: [
|
|
122
|
+
"https://daily-cloudcode-pa.sandbox.googleapis.com",
|
|
123
|
+
"https://autopush-cloudcode-pa.sandbox.googleapis.com",
|
|
124
|
+
"https://cloudcode-pa.googleapis.com",
|
|
125
|
+
],
|
|
126
|
+
HEADERS: {
|
|
127
|
+
"User-Agent": "antigravity/1.11.5 windows/amd64",
|
|
128
|
+
"X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
|
|
129
|
+
"Client-Metadata":
|
|
130
|
+
'{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}',
|
|
131
|
+
},
|
|
132
|
+
getConfig: (modelsDev) => {
|
|
133
|
+
const googleProvider = modelsDev.google as Provider
|
|
134
|
+
const googleVertextProvider = modelsDev[
|
|
135
|
+
"google-vertex-anthropic"
|
|
136
|
+
] as Provider
|
|
137
|
+
|
|
138
|
+
const geminiFlash = googleProvider.models[
|
|
139
|
+
"gemini-3-flash-preview"
|
|
140
|
+
] as OpenCodeModel
|
|
141
|
+
const geminiPro = googleProvider.models[
|
|
142
|
+
"gemini-3-pro-preview"
|
|
143
|
+
] as OpenCodeModel
|
|
144
|
+
const claudeSonnet = googleVertextProvider.models[
|
|
145
|
+
"claude-sonnet-4-5@20250929"
|
|
146
|
+
] as OpenCodeModel
|
|
147
|
+
const claudeOpus = googleVertextProvider.models[
|
|
148
|
+
"claude-opus-4-5@20251101"
|
|
149
|
+
] as OpenCodeModel
|
|
150
|
+
|
|
151
|
+
const models: Record<string, OpenCodeModel> = {
|
|
152
|
+
"gemini-3-flash": {
|
|
153
|
+
...geminiFlash,
|
|
154
|
+
id: "gemini-3-flash",
|
|
155
|
+
},
|
|
156
|
+
"gemini-3-pro-low": {
|
|
157
|
+
...geminiPro,
|
|
158
|
+
id: "gemini-3-pro-low",
|
|
159
|
+
name: "Gemini 3 Pro (Low)",
|
|
160
|
+
temperature: false,
|
|
161
|
+
options: {
|
|
162
|
+
thinkingConfig: {
|
|
163
|
+
thinkingLevel: "low",
|
|
164
|
+
},
|
|
165
|
+
} satisfies GoogleGenerativeAIProviderOptions,
|
|
166
|
+
},
|
|
167
|
+
"gemini-3-pro-high": {
|
|
168
|
+
...geminiPro,
|
|
169
|
+
id: "gemini-3-pro-high",
|
|
170
|
+
name: "Gemini 3 Pro (High)",
|
|
171
|
+
temperature: false,
|
|
172
|
+
options: {
|
|
173
|
+
thinkingConfig: {
|
|
174
|
+
thinkingLevel: "high",
|
|
175
|
+
},
|
|
176
|
+
} satisfies GoogleGenerativeAIProviderOptions,
|
|
177
|
+
},
|
|
178
|
+
"claude-sonnet-4-5": {
|
|
179
|
+
...claudeSonnet,
|
|
180
|
+
id: "claude-sonnet-4-5",
|
|
181
|
+
reasoning: false,
|
|
182
|
+
options: {
|
|
183
|
+
thinkingConfig: {
|
|
184
|
+
includeThoughts: false,
|
|
185
|
+
},
|
|
186
|
+
} satisfies GoogleGenerativeAIProviderOptions,
|
|
187
|
+
},
|
|
188
|
+
"claude-sonnet-4-5-thinking": {
|
|
189
|
+
...claudeSonnet,
|
|
190
|
+
id: "claude-sonnet-4-5-thinking",
|
|
191
|
+
name: "Claude Sonnet 4.5 (Reasoning)",
|
|
192
|
+
},
|
|
193
|
+
"claude-opus-4-5-thinking": {
|
|
194
|
+
...claudeOpus,
|
|
195
|
+
id: "claude-opus-4-5-thinking",
|
|
196
|
+
name: "Claude Opus 4.5 (Reasoning)",
|
|
197
|
+
},
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
...googleProvider,
|
|
202
|
+
id: antigravityConfig().SERVICE_NAME,
|
|
203
|
+
name: antigravityConfig().DISPLAY_NAME,
|
|
204
|
+
api: antigravityConfig().ENDPOINTS.at(2) as string,
|
|
205
|
+
models,
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
transformRequest: Effect.fn(function* (context) {
|
|
209
|
+
yield* Effect.log(
|
|
210
|
+
"Transforming request for: ",
|
|
211
|
+
antigravityConfig().SERVICE_NAME,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
const { body, headers, url } = context
|
|
215
|
+
const innerRequest = body.request as Record<string, unknown>
|
|
216
|
+
|
|
217
|
+
let sessionId: string | undefined
|
|
218
|
+
if (
|
|
219
|
+
innerRequest.labels
|
|
220
|
+
&& typeof innerRequest.labels === "object"
|
|
221
|
+
&& "sessionId" in innerRequest.labels
|
|
222
|
+
) {
|
|
223
|
+
const labels = innerRequest.labels as Record<string, unknown>
|
|
224
|
+
sessionId = labels.sessionId as string
|
|
225
|
+
delete labels.sessionId
|
|
226
|
+
if (Object.keys(labels).length === 0) {
|
|
227
|
+
delete innerRequest.labels
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Handle thinkingConfig for Claude models
|
|
232
|
+
const isClaude = body.model.toLowerCase().includes("claude")
|
|
233
|
+
const isThinking = body.model.toLowerCase().includes("thinking")
|
|
234
|
+
|
|
235
|
+
if (isClaude && body.request && typeof body.request === "object") {
|
|
236
|
+
const request = body.request as Record<string, unknown>
|
|
237
|
+
const generationConfig = request.generationConfig as
|
|
238
|
+
| Record<string, unknown>
|
|
239
|
+
| undefined
|
|
240
|
+
|
|
241
|
+
innerRequest.toolConfig = {
|
|
242
|
+
functionCallingConfig: {
|
|
243
|
+
mode: "VALIDATED",
|
|
244
|
+
},
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// For non-thinking Claude, remove thinkingConfig entirely
|
|
248
|
+
if (!isThinking && generationConfig?.thinkingConfig) {
|
|
249
|
+
delete generationConfig.thinkingConfig
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// For thinking Claude, convert camelCase to snake_case and add default budget
|
|
253
|
+
if (isThinking && generationConfig?.thinkingConfig) {
|
|
254
|
+
const thinkingConfig = generationConfig.thinkingConfig as Record<
|
|
255
|
+
string,
|
|
256
|
+
unknown
|
|
257
|
+
>
|
|
258
|
+
|
|
259
|
+
if (thinkingConfig.includeThoughts !== undefined) {
|
|
260
|
+
thinkingConfig.include_thoughts = thinkingConfig.includeThoughts
|
|
261
|
+
delete thinkingConfig.includeThoughts
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (thinkingConfig.thinkingBudget !== undefined) {
|
|
265
|
+
thinkingConfig.thinking_budget = thinkingConfig.thinkingBudget
|
|
266
|
+
delete thinkingConfig.thinkingBudget
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Add default thinking_budget if not present (required for Claude thinking)
|
|
270
|
+
if (thinkingConfig.thinking_budget === undefined) {
|
|
271
|
+
thinkingConfig.thinking_budget = 32768 // Default to high tier
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (isThinking) {
|
|
276
|
+
headers.set("anthropic-beta", "interleaved-thinking-2025-05-14")
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (sessionId) {
|
|
281
|
+
const hashedSession = yield* Effect.promise(() => hash(sessionId))
|
|
282
|
+
|
|
283
|
+
const finalSessionId = [
|
|
284
|
+
`-${crypto.randomUUID()}`,
|
|
285
|
+
body.model,
|
|
286
|
+
body.project,
|
|
287
|
+
`seed-${hashedSession}`,
|
|
288
|
+
].join(":")
|
|
289
|
+
|
|
290
|
+
innerRequest.sessionId = finalSessionId
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (innerRequest.tools && Array.isArray(innerRequest.tools)) {
|
|
294
|
+
const tools = innerRequest.tools as Array<Record<string, unknown>>
|
|
295
|
+
for (const tool of tools) {
|
|
296
|
+
if (
|
|
297
|
+
tool.functionDeclarations
|
|
298
|
+
&& Array.isArray(tool.functionDeclarations)
|
|
299
|
+
) {
|
|
300
|
+
const functionDeclarations = tool.functionDeclarations as Array<
|
|
301
|
+
Record<string, unknown>
|
|
302
|
+
>
|
|
303
|
+
for (let i = 0; i < functionDeclarations.length; i++) {
|
|
304
|
+
const declaration = functionDeclarations[i]
|
|
305
|
+
if (declaration && declaration.name === "todoread") {
|
|
306
|
+
functionDeclarations[i] = {
|
|
307
|
+
...functionDeclarations[i],
|
|
308
|
+
parameters: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
_placeholder: {
|
|
312
|
+
type: "boolean",
|
|
313
|
+
description: "Placeholder. Always pass true.",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
required: ["_placeholder"],
|
|
317
|
+
additionalProperties: false,
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// if (
|
|
327
|
+
// innerRequest.systemInstruction
|
|
328
|
+
// && typeof innerRequest.systemInstruction === "object"
|
|
329
|
+
// ) {
|
|
330
|
+
// const systemInstruction = innerRequest.systemInstruction as Record<
|
|
331
|
+
// string,
|
|
332
|
+
// unknown
|
|
333
|
+
// >
|
|
334
|
+
|
|
335
|
+
// if (systemInstruction.parts && Array.isArray(systemInstruction.parts)) {
|
|
336
|
+
// let parts = systemInstruction.parts as Array<{ text: string }>
|
|
337
|
+
|
|
338
|
+
// parts.unshift({
|
|
339
|
+
// text: "You are Antigravity, a powerful agentic AI coding assistant designed by the Google DeepMind team working on Advanced Agentic Coding.",
|
|
340
|
+
// })
|
|
341
|
+
// }
|
|
342
|
+
// }
|
|
343
|
+
|
|
344
|
+
if (
|
|
345
|
+
innerRequest.systemInstruction
|
|
346
|
+
&& typeof innerRequest.systemInstruction === "object"
|
|
347
|
+
) {
|
|
348
|
+
const systemInstruction = innerRequest.systemInstruction as Record<
|
|
349
|
+
string,
|
|
350
|
+
unknown
|
|
351
|
+
>
|
|
352
|
+
|
|
353
|
+
systemInstruction.role = "user"
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
headers,
|
|
358
|
+
url,
|
|
359
|
+
body: {
|
|
360
|
+
...body,
|
|
361
|
+
requestType: "agent",
|
|
362
|
+
userAgent: "antigravity",
|
|
363
|
+
requestId: `agent-${crypto.randomUUID()}`,
|
|
364
|
+
},
|
|
365
|
+
}
|
|
366
|
+
}),
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
async function hash(str: string) {
|
|
370
|
+
const encoder = new TextEncoder()
|
|
371
|
+
const data = encoder.encode(str)
|
|
372
|
+
|
|
373
|
+
const hashBuffer = await crypto.subtle.digest("SHA-256", data)
|
|
374
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
375
|
+
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
|
|
376
|
+
|
|
377
|
+
return hashHex.slice(0, 16)
|
|
378
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth service
|
|
3
|
+
*
|
|
4
|
+
* Handles initial OAuth authentication flow only.
|
|
5
|
+
* Token refresh is handled by the Session service.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
HttpRouter,
|
|
10
|
+
HttpServer,
|
|
11
|
+
HttpServerRequest,
|
|
12
|
+
HttpServerResponse,
|
|
13
|
+
} from "@effect/platform"
|
|
14
|
+
import { BunHttpServer } from "@effect/platform-bun"
|
|
15
|
+
import { Data, Deferred, Effect, Fiber, Schema } from "effect"
|
|
16
|
+
import { OAuth2Client } from "google-auth-library"
|
|
17
|
+
import type { BunServeOptions } from "../../types"
|
|
18
|
+
import { ProviderConfig } from "./config"
|
|
19
|
+
|
|
20
|
+
export class OAuthError extends Data.TaggedError("OAuthError")<{
|
|
21
|
+
readonly reason: "browser" | "callback" | "state_mismatch" | "token_exchange"
|
|
22
|
+
readonly message: string
|
|
23
|
+
readonly cause?: unknown
|
|
24
|
+
}> {}
|
|
25
|
+
|
|
26
|
+
const SuccessParamsSchema = Schema.Struct({
|
|
27
|
+
code: Schema.String,
|
|
28
|
+
state: Schema.String,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const FailureParamsSchema = Schema.Struct({
|
|
32
|
+
error: Schema.String,
|
|
33
|
+
error_description: Schema.optional(Schema.String),
|
|
34
|
+
state: Schema.optional(Schema.String),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const isFailureParams = Schema.is(FailureParamsSchema)
|
|
38
|
+
|
|
39
|
+
const ParamsSchema = Schema.Union(SuccessParamsSchema, FailureParamsSchema)
|
|
40
|
+
|
|
41
|
+
class OAuth extends Effect.Service<OAuth>()("OAuth", {
|
|
42
|
+
effect: Effect.gen(function* () {
|
|
43
|
+
const config = yield* ProviderConfig
|
|
44
|
+
|
|
45
|
+
const client = new OAuth2Client({
|
|
46
|
+
clientId: config.CLIENT_ID,
|
|
47
|
+
clientSecret: config.CLIENT_SECRET,
|
|
48
|
+
})
|
|
49
|
+
const serverOptions: BunServeOptions = { port: 0 }
|
|
50
|
+
const ServerLive = BunHttpServer.layerServer(serverOptions)
|
|
51
|
+
|
|
52
|
+
const authenticate = Effect.gen(function* () {
|
|
53
|
+
yield* HttpServer.logAddress
|
|
54
|
+
|
|
55
|
+
const deferredParams = yield* Deferred.make<
|
|
56
|
+
typeof SuccessParamsSchema.Type,
|
|
57
|
+
OAuthError
|
|
58
|
+
>()
|
|
59
|
+
|
|
60
|
+
const redirectUri = yield* HttpServer.addressFormattedWith((address) =>
|
|
61
|
+
Effect.succeed(`${address}/oauth2callback`),
|
|
62
|
+
)
|
|
63
|
+
const state = crypto.randomUUID()
|
|
64
|
+
|
|
65
|
+
const authUrl = client.generateAuthUrl({
|
|
66
|
+
state,
|
|
67
|
+
redirect_uri: redirectUri,
|
|
68
|
+
access_type: "offline",
|
|
69
|
+
scope: config.SCOPES as unknown as string[],
|
|
70
|
+
prompt: "consent",
|
|
71
|
+
})
|
|
72
|
+
yield* Effect.log(`OAuth2 authorization URL: ${authUrl}`)
|
|
73
|
+
|
|
74
|
+
const serverFiber = yield* HttpRouter.empty.pipe(
|
|
75
|
+
HttpRouter.get(
|
|
76
|
+
"/oauth2callback",
|
|
77
|
+
Effect.gen(function* () {
|
|
78
|
+
const params =
|
|
79
|
+
yield* HttpServerRequest.schemaSearchParams(ParamsSchema)
|
|
80
|
+
|
|
81
|
+
if (isFailureParams(params)) {
|
|
82
|
+
yield* Deferred.fail(
|
|
83
|
+
deferredParams,
|
|
84
|
+
new OAuthError({
|
|
85
|
+
reason: "callback",
|
|
86
|
+
message: `${params.error} - ${params.error_description ?? "No additional details provided"}`,
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
} else {
|
|
90
|
+
yield* Deferred.succeed(deferredParams, params)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return yield* HttpServerResponse.text("You may now close this tab.")
|
|
94
|
+
}).pipe(Effect.tapError(Effect.logError)),
|
|
95
|
+
),
|
|
96
|
+
HttpServer.serveEffect(),
|
|
97
|
+
Effect.fork,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
yield* Effect.log("Started OAuth2 callback server")
|
|
101
|
+
|
|
102
|
+
const search = yield* Deferred.await(deferredParams)
|
|
103
|
+
yield* Effect.log("Received OAuth2 callback with params", search)
|
|
104
|
+
|
|
105
|
+
yield* Fiber.interrupt(serverFiber)
|
|
106
|
+
|
|
107
|
+
if (state !== search.state) {
|
|
108
|
+
return yield* new OAuthError({
|
|
109
|
+
reason: "state_mismatch",
|
|
110
|
+
message: "Invalid state parameter. Possible CSRF attack.",
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result = yield* Effect.tryPromise({
|
|
115
|
+
try: () =>
|
|
116
|
+
client.getToken({
|
|
117
|
+
code: search.code,
|
|
118
|
+
redirect_uri: redirectUri,
|
|
119
|
+
}),
|
|
120
|
+
catch: (cause) =>
|
|
121
|
+
new OAuthError({
|
|
122
|
+
reason: "token_exchange",
|
|
123
|
+
message: "Failed to exchange authorization code for tokens",
|
|
124
|
+
cause,
|
|
125
|
+
}),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return result.tokens
|
|
129
|
+
}).pipe(Effect.provide(ServerLive), Effect.scoped)
|
|
130
|
+
|
|
131
|
+
return { authenticate }
|
|
132
|
+
}),
|
|
133
|
+
}) {}
|
|
134
|
+
|
|
135
|
+
export { OAuth }
|