@strav/social 0.4.30 → 1.0.0-alpha.22
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/package.json +23 -16
- package/src/drivers/index.ts +2 -0
- package/src/drivers/mock_driver.ts +170 -0
- package/src/drivers/unsupported.ts +17 -0
- package/src/dto/index.ts +4 -0
- package/src/dto/oauth_tokens.ts +26 -0
- package/src/dto/social_profile.ts +37 -0
- package/src/facebook/facebook_config.ts +68 -0
- package/src/facebook/facebook_driver.ts +321 -0
- package/src/facebook/facebook_provider.ts +29 -0
- package/src/facebook/index.ts +12 -0
- package/src/google/google_config.ts +44 -0
- package/src/google/google_driver.ts +317 -0
- package/src/google/google_provider.ts +33 -0
- package/src/google/index.ts +12 -0
- package/src/index.ts +61 -14
- package/src/ledger/apply_social_account_migration.ts +66 -0
- package/src/ledger/index.ts +12 -0
- package/src/ledger/social_account.ts +32 -0
- package/src/ledger/social_account_repository.ts +216 -0
- package/src/ledger/social_account_schema.ts +75 -0
- package/src/line/index.ts +12 -0
- package/src/line/line_config.ts +47 -0
- package/src/line/line_driver.ts +310 -0
- package/src/line/line_provider.ts +34 -0
- package/src/pkce.ts +63 -0
- package/src/social_capabilities.ts +35 -0
- package/src/social_driver.ts +105 -0
- package/src/social_error.ts +155 -0
- package/src/social_manager.ts +92 -74
- package/src/social_provider.ts +41 -7
- package/src/tenanted/apply_tenanted_social_account_migration.ts +45 -0
- package/src/tenanted/index.ts +18 -0
- package/src/tenanted/tenanted_social_account.ts +30 -0
- package/src/tenanted/tenanted_social_account_repository.ts +149 -0
- package/src/tenanted/tenanted_social_account_schema.ts +44 -0
- package/src/types.ts +15 -43
- package/CHANGELOG.md +0 -19
- package/README.md +0 -78
- package/src/abstract_provider.ts +0 -182
- package/src/helpers.ts +0 -31
- package/src/providers/discord_provider.ts +0 -59
- package/src/providers/facebook_provider.ts +0 -69
- package/src/providers/github_provider.ts +0 -75
- package/src/providers/google_provider.ts +0 -51
- package/src/providers/line_provider.ts +0 -73
- package/src/providers/linkedin_provider.ts +0 -51
- package/src/schema.ts +0 -13
- package/src/social_account.ts +0 -238
- package/stubs/config/social.ts +0 -22
- package/stubs/schemas/social_account.ts +0 -13
- package/tsconfig.json +0 -5
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `FacebookSocialDriver` — Facebook Login via the Graph API.
|
|
3
|
+
*
|
|
4
|
+
* Notable divergences from Line / Google:
|
|
5
|
+
*
|
|
6
|
+
* - **No OIDC.** Facebook does not issue `id_token`; the
|
|
7
|
+
* driver omits the `openid` capability. Apps that want a
|
|
8
|
+
* verifiable JWT use a different provider.
|
|
9
|
+
*
|
|
10
|
+
* - **No refresh tokens.** Facebook hands out short-lived
|
|
11
|
+
* (~1–2h) access tokens and a separate "long-lived token
|
|
12
|
+
* exchange" path that swaps the access token itself for a
|
|
13
|
+
* ~60-day variant. That's not a refresh-token grant in the
|
|
14
|
+
* framework's sense, so the driver omits the `tokens.refresh`
|
|
15
|
+
* capability and throws `ProviderUnsupportedError` from
|
|
16
|
+
* `refresh()`. Use `exchangeForLongLivedToken()` directly for
|
|
17
|
+
* the Facebook-specific flow.
|
|
18
|
+
*
|
|
19
|
+
* - **Email needs App Review.** Apps that ship the `email`
|
|
20
|
+
* scope to non-developer users have to go through Meta's
|
|
21
|
+
* review. The capability flag is declared because the
|
|
22
|
+
* driver CAN return email when granted — but apps gate the
|
|
23
|
+
* scope picker / button visibility on their own deployment
|
|
24
|
+
* state.
|
|
25
|
+
*
|
|
26
|
+
* - **`emailVerified` not asserted.** Facebook does not
|
|
27
|
+
* surface verification state; apps that need verified email
|
|
28
|
+
* send their own confirmation.
|
|
29
|
+
*
|
|
30
|
+
* - **Revoke** issues `DELETE /me/permissions`, which clears
|
|
31
|
+
* ALL granted scopes for the user. There's no per-scope
|
|
32
|
+
* revoke through the framework; apps that need it call the
|
|
33
|
+
* Graph API directly.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import type { OAuthTokens, SocialProfile } from '../dto/index.ts'
|
|
37
|
+
import type { SocialCapability } from '../social_capabilities.ts'
|
|
38
|
+
import type {
|
|
39
|
+
AuthorizeInput,
|
|
40
|
+
AuthorizeResult,
|
|
41
|
+
ExchangeInput,
|
|
42
|
+
RefreshInput,
|
|
43
|
+
SocialDriver,
|
|
44
|
+
} from '../social_driver.ts'
|
|
45
|
+
import {
|
|
46
|
+
InvalidTokenError,
|
|
47
|
+
OAuthExchangeError,
|
|
48
|
+
ProviderUnsupportedError,
|
|
49
|
+
SocialProviderError,
|
|
50
|
+
StateMismatchError,
|
|
51
|
+
} from '../social_error.ts'
|
|
52
|
+
import { codeChallengeFor, randomCodeVerifier, randomState } from '../pkce.ts'
|
|
53
|
+
import {
|
|
54
|
+
DEFAULT_FACEBOOK_PROFILE_FIELDS,
|
|
55
|
+
facebookEndpoints,
|
|
56
|
+
type FacebookProviderConfig,
|
|
57
|
+
} from './facebook_config.ts'
|
|
58
|
+
|
|
59
|
+
const PROVIDER = 'facebook'
|
|
60
|
+
|
|
61
|
+
const CAPS: readonly SocialCapability[] = [
|
|
62
|
+
// No `openid` — Facebook is plain OAuth2, no id_token.
|
|
63
|
+
'pkce.support',
|
|
64
|
+
'profile.id', 'profile.email', 'profile.name', 'profile.avatar', 'profile.locale',
|
|
65
|
+
// No `profile.emailVerified` — Facebook doesn't assert verification.
|
|
66
|
+
'tokens.exchange',
|
|
67
|
+
// No `tokens.refresh` — see `exchangeForLongLivedToken` instead.
|
|
68
|
+
'tokens.revoke', 'tokens.introspect',
|
|
69
|
+
'scopes.discoverable',
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
const SCOPES: readonly string[] = ['public_profile', 'email']
|
|
73
|
+
|
|
74
|
+
export interface FacebookDriverOptions {
|
|
75
|
+
instanceName: string
|
|
76
|
+
config: FacebookProviderConfig
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface TokenResponse {
|
|
80
|
+
access_token: string
|
|
81
|
+
expires_in?: number
|
|
82
|
+
token_type: string
|
|
83
|
+
scope?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface PicturePayload {
|
|
87
|
+
data?: { url?: string; is_silhouette?: boolean }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface MeResponse {
|
|
91
|
+
id: string
|
|
92
|
+
name?: string
|
|
93
|
+
email?: string
|
|
94
|
+
first_name?: string
|
|
95
|
+
last_name?: string
|
|
96
|
+
picture?: PicturePayload
|
|
97
|
+
locale?: string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface DebugTokenResponse {
|
|
101
|
+
data?: {
|
|
102
|
+
user_id?: string
|
|
103
|
+
app_id?: string
|
|
104
|
+
is_valid?: boolean
|
|
105
|
+
expires_at?: number
|
|
106
|
+
scopes?: string[]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export class FacebookSocialDriver implements SocialDriver {
|
|
111
|
+
readonly name = PROVIDER
|
|
112
|
+
readonly instanceName: string
|
|
113
|
+
readonly capabilities: ReadonlySet<SocialCapability> = new Set(CAPS)
|
|
114
|
+
readonly availableScopes = SCOPES
|
|
115
|
+
|
|
116
|
+
private readonly config: FacebookProviderConfig
|
|
117
|
+
private readonly fetchFn: typeof fetch
|
|
118
|
+
private readonly endpoints: ReturnType<typeof facebookEndpoints>
|
|
119
|
+
private readonly profileFields: readonly string[]
|
|
120
|
+
|
|
121
|
+
constructor(options: FacebookDriverOptions) {
|
|
122
|
+
this.instanceName = options.instanceName
|
|
123
|
+
this.config = options.config
|
|
124
|
+
this.fetchFn = options.config.fetch ?? fetch
|
|
125
|
+
this.endpoints = {
|
|
126
|
+
...facebookEndpoints(options.config.graphVersion),
|
|
127
|
+
...(options.config.endpoints ?? {}),
|
|
128
|
+
}
|
|
129
|
+
this.profileFields = options.config.profileFields ?? DEFAULT_FACEBOOK_PROFILE_FIELDS
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async authorize(input: AuthorizeInput): Promise<AuthorizeResult> {
|
|
133
|
+
const state = input.state ?? randomState()
|
|
134
|
+
const optOut = input.extra?.no_pkce === '1'
|
|
135
|
+
const codeVerifier = optOut
|
|
136
|
+
? undefined
|
|
137
|
+
: input.codeVerifier ?? randomCodeVerifier()
|
|
138
|
+
const challenge = codeVerifier ? await codeChallengeFor(codeVerifier) : undefined
|
|
139
|
+
|
|
140
|
+
const scopes = input.scopes ?? ['public_profile']
|
|
141
|
+
const params = new URLSearchParams({
|
|
142
|
+
response_type: 'code',
|
|
143
|
+
client_id: this.config.clientId,
|
|
144
|
+
redirect_uri: input.redirectUri,
|
|
145
|
+
scope: scopes.join(','), // Facebook accepts comma OR space; comma is canonical.
|
|
146
|
+
state,
|
|
147
|
+
...(challenge ? { code_challenge: challenge, code_challenge_method: 'S256' } : {}),
|
|
148
|
+
...(input.extra ?? {}),
|
|
149
|
+
})
|
|
150
|
+
params.delete('no_pkce')
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
url: `${this.endpoints.authorize}?${params.toString()}`,
|
|
154
|
+
state,
|
|
155
|
+
...(codeVerifier ? { codeVerifier } : {}),
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async exchange(input: ExchangeInput): Promise<OAuthTokens> {
|
|
160
|
+
if (input.expectedState !== undefined && input.state !== input.expectedState) {
|
|
161
|
+
throw new StateMismatchError()
|
|
162
|
+
}
|
|
163
|
+
// Facebook accepts GET with query string OR POST with form;
|
|
164
|
+
// we use POST form for symmetry with Line/Google.
|
|
165
|
+
const body = new URLSearchParams({
|
|
166
|
+
client_id: this.config.clientId,
|
|
167
|
+
client_secret: this.config.clientSecret,
|
|
168
|
+
redirect_uri: input.redirectUri,
|
|
169
|
+
code: input.code,
|
|
170
|
+
...(input.codeVerifier ? { code_verifier: input.codeVerifier } : {}),
|
|
171
|
+
})
|
|
172
|
+
const res = await this.fetchFn(this.endpoints.token, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
175
|
+
body,
|
|
176
|
+
})
|
|
177
|
+
if (!res.ok) {
|
|
178
|
+
const text = await res.text()
|
|
179
|
+
throw new OAuthExchangeError(
|
|
180
|
+
`FacebookSocialDriver.exchange: token endpoint returned ${res.status}.`,
|
|
181
|
+
{ context: { status: res.status, body: text } },
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
return this.toOAuthTokens((await res.json()) as TokenResponse)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async profile(accessToken: string): Promise<SocialProfile> {
|
|
188
|
+
const url = `${this.endpoints.me}?${new URLSearchParams({
|
|
189
|
+
fields: this.profileFields.join(','),
|
|
190
|
+
access_token: accessToken,
|
|
191
|
+
}).toString()}`
|
|
192
|
+
const res = await this.fetchFn(url)
|
|
193
|
+
if (res.status === 401 || res.status === 400) {
|
|
194
|
+
// Facebook returns 400 for revoked / expired tokens (not 401).
|
|
195
|
+
throw new InvalidTokenError('FacebookSocialDriver.profile: access token rejected.')
|
|
196
|
+
}
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
const text = await res.text()
|
|
199
|
+
throw new SocialProviderError(
|
|
200
|
+
`FacebookSocialDriver.profile: Graph API returned ${res.status}.`,
|
|
201
|
+
{ provider: PROVIDER, operation: 'profile', context: { status: res.status, body: text } },
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
const me = (await res.json()) as MeResponse
|
|
205
|
+
return {
|
|
206
|
+
id: me.id,
|
|
207
|
+
provider: PROVIDER,
|
|
208
|
+
...(me.email ? { email: me.email } : {}),
|
|
209
|
+
...(me.name ? { name: me.name } : {}),
|
|
210
|
+
...(me.picture?.data?.url ? { avatarUrl: me.picture.data.url } : {}),
|
|
211
|
+
...(me.locale ? { locale: me.locale } : {}),
|
|
212
|
+
metadata: {
|
|
213
|
+
...(me.first_name ? { firstName: me.first_name } : {}),
|
|
214
|
+
...(me.last_name ? { lastName: me.last_name } : {}),
|
|
215
|
+
...(me.picture?.data?.is_silhouette
|
|
216
|
+
? { isSilhouette: me.picture.data.is_silhouette }
|
|
217
|
+
: {}),
|
|
218
|
+
},
|
|
219
|
+
raw: me,
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
refresh(_input: RefreshInput): Promise<OAuthTokens> {
|
|
224
|
+
throw new ProviderUnsupportedError(PROVIDER, 'tokens.refresh', {
|
|
225
|
+
reason:
|
|
226
|
+
'Facebook does not issue refresh tokens. Use `exchangeForLongLivedToken(accessToken, driver)` to swap a short-lived access token for the ~60-day variant — note this trades the access token itself, not a separate refresh token.',
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async revoke(token: string): Promise<void> {
|
|
231
|
+
// DELETE /me/permissions clears every scope this user
|
|
232
|
+
// granted to the app. Per-scope revoke isn't bridged; apps
|
|
233
|
+
// that need it call the Graph API directly.
|
|
234
|
+
const url = `${this.endpoints.permissions}?${new URLSearchParams({
|
|
235
|
+
access_token: token,
|
|
236
|
+
}).toString()}`
|
|
237
|
+
const res = await this.fetchFn(url, { method: 'DELETE' })
|
|
238
|
+
if (!res.ok) {
|
|
239
|
+
const text = await res.text()
|
|
240
|
+
throw new SocialProviderError(
|
|
241
|
+
`FacebookSocialDriver.revoke: Graph API returned ${res.status}.`,
|
|
242
|
+
{ provider: PROVIDER, operation: 'revoke', context: { status: res.status, body: text } },
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ─── Facebook-specific helpers (advanced) ───────────────────────────
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Trade a short-lived access token (~1–2h) for a long-lived
|
|
251
|
+
* one (~60 days). Facebook calls this `fb_exchange_token`; the
|
|
252
|
+
* framework's `refresh()` throws because it doesn't fit the
|
|
253
|
+
* refresh-token contract. Apps that hold tokens across sessions
|
|
254
|
+
* call this once after the initial exchange.
|
|
255
|
+
*/
|
|
256
|
+
async exchangeForLongLivedToken(accessToken: string): Promise<OAuthTokens> {
|
|
257
|
+
const body = new URLSearchParams({
|
|
258
|
+
grant_type: 'fb_exchange_token',
|
|
259
|
+
client_id: this.config.clientId,
|
|
260
|
+
client_secret: this.config.clientSecret,
|
|
261
|
+
fb_exchange_token: accessToken,
|
|
262
|
+
})
|
|
263
|
+
const res = await this.fetchFn(this.endpoints.token, {
|
|
264
|
+
method: 'POST',
|
|
265
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
266
|
+
body,
|
|
267
|
+
})
|
|
268
|
+
if (res.status === 400 || res.status === 401) {
|
|
269
|
+
throw new InvalidTokenError(
|
|
270
|
+
`FacebookSocialDriver.exchangeForLongLivedToken: short-lived token rejected.`,
|
|
271
|
+
{ context: { status: res.status } },
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
if (!res.ok) {
|
|
275
|
+
throw new SocialProviderError(
|
|
276
|
+
`FacebookSocialDriver.exchangeForLongLivedToken: Graph API returned ${res.status}.`,
|
|
277
|
+
{ provider: PROVIDER, operation: 'long_lived_exchange', context: { status: res.status } },
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
return this.toOAuthTokens((await res.json()) as TokenResponse)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Inspect a token via the Graph API's `debug_token` endpoint.
|
|
285
|
+
* Returns the raw payload — apps read `is_valid`, `expires_at`,
|
|
286
|
+
* `scopes`, etc. Implementation note: the `input_token` is what
|
|
287
|
+
* we're checking; the `access_token` is the *app* token used to
|
|
288
|
+
* authenticate the call (`client_id|client_secret`).
|
|
289
|
+
*/
|
|
290
|
+
async debugToken(token: string): Promise<DebugTokenResponse> {
|
|
291
|
+
const appToken = `${this.config.clientId}|${this.config.clientSecret}`
|
|
292
|
+
const url = `${this.endpoints.debugToken}?${new URLSearchParams({
|
|
293
|
+
input_token: token,
|
|
294
|
+
access_token: appToken,
|
|
295
|
+
}).toString()}`
|
|
296
|
+
const res = await this.fetchFn(url)
|
|
297
|
+
if (!res.ok) {
|
|
298
|
+
throw new SocialProviderError(
|
|
299
|
+
`FacebookSocialDriver.debugToken: Graph API returned ${res.status}.`,
|
|
300
|
+
{ provider: PROVIDER, operation: 'introspect', context: { status: res.status } },
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
return (await res.json()) as DebugTokenResponse
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ─── Internals ────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
private toOAuthTokens(t: TokenResponse): OAuthTokens {
|
|
309
|
+
const expiresAt =
|
|
310
|
+
typeof t.expires_in === 'number'
|
|
311
|
+
? new Date(Date.now() + t.expires_in * 1000)
|
|
312
|
+
: undefined
|
|
313
|
+
return {
|
|
314
|
+
accessToken: t.access_token,
|
|
315
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
316
|
+
...(t.scope ? { scope: t.scope } : {}),
|
|
317
|
+
tokenType: t.token_type ?? 'Bearer',
|
|
318
|
+
raw: t,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `FacebookSocialProvider` — `ServiceProvider` that registers
|
|
3
|
+
* the Facebook driver factory on the `SocialManager`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { type Application, ServiceProvider } from '@strav/kernel'
|
|
7
|
+
import { SocialConfigError } from '../social_error.ts'
|
|
8
|
+
import { SocialManager } from '../social_manager.ts'
|
|
9
|
+
import type { FacebookProviderConfig } from './facebook_config.ts'
|
|
10
|
+
import { FacebookSocialDriver } from './facebook_driver.ts'
|
|
11
|
+
|
|
12
|
+
export class FacebookSocialProvider extends ServiceProvider {
|
|
13
|
+
override readonly name = 'social-facebook'
|
|
14
|
+
override readonly dependencies = ['social']
|
|
15
|
+
|
|
16
|
+
override register(app: Application): void {
|
|
17
|
+
const manager = app.resolve(SocialManager)
|
|
18
|
+
manager.extend('facebook', ({ instanceName, config }) => {
|
|
19
|
+
const cfg = config as FacebookProviderConfig
|
|
20
|
+
if (!cfg.clientId || !cfg.clientSecret) {
|
|
21
|
+
throw new SocialConfigError(
|
|
22
|
+
`FacebookSocialProvider: \`clientId\` and \`clientSecret\` are required for provider "${instanceName}".`,
|
|
23
|
+
{ context: { instanceName } },
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
return new FacebookSocialDriver({ instanceName, config: cfg })
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Public API of `@strav/social/facebook`.
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
DEFAULT_FACEBOOK_PROFILE_FIELDS,
|
|
5
|
+
facebookEndpoints,
|
|
6
|
+
type FacebookProviderConfig,
|
|
7
|
+
} from './facebook_config.ts'
|
|
8
|
+
export {
|
|
9
|
+
FacebookSocialDriver,
|
|
10
|
+
type FacebookDriverOptions,
|
|
11
|
+
} from './facebook_driver.ts'
|
|
12
|
+
export { FacebookSocialProvider } from './facebook_provider.ts'
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google-specific provider config. Apps put one of these inside
|
|
3
|
+
* `config.social.providers[name]` with `driver: 'google'`.
|
|
4
|
+
*
|
|
5
|
+
* Get credentials from https://console.cloud.google.com → APIs &
|
|
6
|
+
* Services → Credentials → "OAuth 2.0 Client IDs". For server-
|
|
7
|
+
* side apps choose "Web application"; for mobile / SPA see the
|
|
8
|
+
* dedicated client types (PKCE is mandatory there).
|
|
9
|
+
*
|
|
10
|
+
* The Google Workspace `hd` (hosted-domain) constraint can be
|
|
11
|
+
* enforced per-authorize via `authorize({ extra: { hd: 'example.com' } })`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ProviderConfig } from '../types.ts'
|
|
15
|
+
|
|
16
|
+
export interface GoogleProviderConfig extends ProviderConfig {
|
|
17
|
+
driver: 'google'
|
|
18
|
+
clientId: string
|
|
19
|
+
clientSecret: string
|
|
20
|
+
/**
|
|
21
|
+
* Default to requesting refresh tokens. Default `true`. When
|
|
22
|
+
* `false`, the authorize URL omits `access_type=offline` and
|
|
23
|
+
* `refresh()` will fail later — only useful for short-lived
|
|
24
|
+
* "sign in once" flows where the app never holds long-term tokens.
|
|
25
|
+
*/
|
|
26
|
+
offlineAccess?: boolean
|
|
27
|
+
/** Override endpoints for testing — never set in production. */
|
|
28
|
+
endpoints?: {
|
|
29
|
+
authorize?: string
|
|
30
|
+
token?: string
|
|
31
|
+
userInfo?: string
|
|
32
|
+
revoke?: string
|
|
33
|
+
tokenInfo?: string
|
|
34
|
+
}
|
|
35
|
+
fetch?: typeof fetch
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const GOOGLE_ENDPOINTS = {
|
|
39
|
+
authorize: 'https://accounts.google.com/o/oauth2/v2/auth',
|
|
40
|
+
token: 'https://oauth2.googleapis.com/token',
|
|
41
|
+
userInfo: 'https://openidconnect.googleapis.com/v1/userinfo',
|
|
42
|
+
revoke: 'https://oauth2.googleapis.com/revoke',
|
|
43
|
+
tokenInfo: 'https://oauth2.googleapis.com/tokeninfo',
|
|
44
|
+
} as const
|