@strav/social 0.4.31 → 1.0.0-alpha.24
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/facebook/facebook_config.ts +68 -0
- package/src/drivers/facebook/facebook_driver.ts +321 -0
- package/src/drivers/facebook/facebook_provider.ts +29 -0
- package/src/drivers/facebook/index.ts +12 -0
- package/src/drivers/google/google_config.ts +44 -0
- package/src/drivers/google/google_driver.ts +317 -0
- package/src/drivers/google/google_provider.ts +33 -0
- package/src/drivers/google/index.ts +12 -0
- package/src/drivers/index.ts +2 -0
- package/src/drivers/line/index.ts +12 -0
- package/src/drivers/line/line_config.ts +47 -0
- package/src/drivers/line/line_driver.ts +310 -0
- package/src/drivers/line/line_provider.ts +34 -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/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 +203 -0
- package/src/ledger/social_account_schema.ts +75 -0
- package/src/pkce.ts +63 -0
- package/src/social_capabilities.ts +35 -0
- package/src/social_driver.ts +143 -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 +136 -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,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GoogleSocialDriver` — Google Sign-In (OAuth 2.0 + OIDC) via
|
|
3
|
+
* the standard Web application client type.
|
|
4
|
+
*
|
|
5
|
+
* Notes worth knowing:
|
|
6
|
+
*
|
|
7
|
+
* - **Scopes**: `openid` (id_token), `profile`, `email`.
|
|
8
|
+
* Google accepts both short (`'email'`) and fully-qualified
|
|
9
|
+
* (`'https://www.googleapis.com/auth/userinfo.email'`) forms;
|
|
10
|
+
* we use the short forms.
|
|
11
|
+
*
|
|
12
|
+
* - **PKCE**: supported. Mandatory for `installed` / SPA
|
|
13
|
+
* client types; optional for `Web application`. The driver
|
|
14
|
+
* defaults PKCE on as defence in depth (matches Line + the
|
|
15
|
+
* OAuth 2.1 trajectory).
|
|
16
|
+
*
|
|
17
|
+
* - **Refresh tokens**: Google only issues `refresh_token` on
|
|
18
|
+
* `access_type=offline`. The driver defaults to including
|
|
19
|
+
* it. A `refresh_token` is returned ONLY on the user's
|
|
20
|
+
* first consent unless `prompt=consent` forces re-consent.
|
|
21
|
+
* Apps re-establishing offline access after revocation pass
|
|
22
|
+
* `extra: { prompt: 'consent' }`.
|
|
23
|
+
*
|
|
24
|
+
* - **id_token**: returned when `openid` is in the requested
|
|
25
|
+
* scope set. Decoding helper provided for `email` extraction,
|
|
26
|
+
* mirroring `emailFromLineIdToken`. Signature verification
|
|
27
|
+
* deferred to apps (use Google's `tokeninfo` endpoint or a
|
|
28
|
+
* JWT library + Google's JWKS).
|
|
29
|
+
*
|
|
30
|
+
* - **`hd` (hosted domain)**: Google Workspace constraint —
|
|
31
|
+
* pass `extra: { hd: 'example.com' }` on authorize to limit
|
|
32
|
+
* consent to one Workspace domain.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { OAuthTokens, SocialProfile } from '../../dto/index.ts'
|
|
36
|
+
import type { SocialCapability } from '../../social_capabilities.ts'
|
|
37
|
+
import type {
|
|
38
|
+
AuthorizeInput,
|
|
39
|
+
AuthorizeResult,
|
|
40
|
+
ExchangeInput,
|
|
41
|
+
RefreshInput,
|
|
42
|
+
SocialDriver,
|
|
43
|
+
} from '../../social_driver.ts'
|
|
44
|
+
import {
|
|
45
|
+
InvalidTokenError,
|
|
46
|
+
OAuthExchangeError,
|
|
47
|
+
SocialProviderError,
|
|
48
|
+
StateMismatchError,
|
|
49
|
+
} from '../../social_error.ts'
|
|
50
|
+
import { codeChallengeFor, randomCodeVerifier, randomState } from '../../pkce.ts'
|
|
51
|
+
import { GOOGLE_ENDPOINTS, type GoogleProviderConfig } from './google_config.ts'
|
|
52
|
+
|
|
53
|
+
const PROVIDER = 'google'
|
|
54
|
+
|
|
55
|
+
const CAPS: readonly SocialCapability[] = [
|
|
56
|
+
'openid', 'pkce.support',
|
|
57
|
+
'profile.id', 'profile.email', 'profile.emailVerified',
|
|
58
|
+
'profile.name', 'profile.avatar', 'profile.locale',
|
|
59
|
+
'tokens.exchange', 'tokens.refresh', 'tokens.revoke', 'tokens.introspect',
|
|
60
|
+
'scopes.discoverable',
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
const SCOPES: readonly string[] = ['openid', 'profile', 'email']
|
|
64
|
+
|
|
65
|
+
export interface GoogleDriverOptions {
|
|
66
|
+
instanceName: string
|
|
67
|
+
config: GoogleProviderConfig
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface TokenResponse {
|
|
71
|
+
access_token: string
|
|
72
|
+
expires_in: number
|
|
73
|
+
id_token?: string
|
|
74
|
+
refresh_token?: string
|
|
75
|
+
scope?: string
|
|
76
|
+
token_type: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface UserInfoResponse {
|
|
80
|
+
sub: string
|
|
81
|
+
email?: string
|
|
82
|
+
email_verified?: boolean
|
|
83
|
+
name?: string
|
|
84
|
+
given_name?: string
|
|
85
|
+
family_name?: string
|
|
86
|
+
picture?: string
|
|
87
|
+
locale?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface JwtPayload {
|
|
91
|
+
email?: string
|
|
92
|
+
email_verified?: boolean
|
|
93
|
+
sub?: string
|
|
94
|
+
[k: string]: unknown
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export class GoogleSocialDriver implements SocialDriver {
|
|
98
|
+
readonly name = PROVIDER
|
|
99
|
+
readonly instanceName: string
|
|
100
|
+
readonly capabilities: ReadonlySet<SocialCapability> = new Set(CAPS)
|
|
101
|
+
readonly availableScopes = SCOPES
|
|
102
|
+
|
|
103
|
+
private readonly config: GoogleProviderConfig
|
|
104
|
+
private readonly fetchFn: typeof fetch
|
|
105
|
+
private readonly endpoints: {
|
|
106
|
+
authorize: string
|
|
107
|
+
token: string
|
|
108
|
+
userInfo: string
|
|
109
|
+
revoke: string
|
|
110
|
+
tokenInfo: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
constructor(options: GoogleDriverOptions) {
|
|
114
|
+
this.instanceName = options.instanceName
|
|
115
|
+
this.config = options.config
|
|
116
|
+
this.fetchFn = options.config.fetch ?? fetch
|
|
117
|
+
this.endpoints = { ...GOOGLE_ENDPOINTS, ...(options.config.endpoints ?? {}) }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async authorize(input: AuthorizeInput): Promise<AuthorizeResult> {
|
|
121
|
+
const state = input.state ?? randomState()
|
|
122
|
+
const optOut = input.extra?.no_pkce === '1'
|
|
123
|
+
const codeVerifier = optOut
|
|
124
|
+
? undefined
|
|
125
|
+
: input.codeVerifier ?? randomCodeVerifier()
|
|
126
|
+
const challenge = codeVerifier ? await codeChallengeFor(codeVerifier) : undefined
|
|
127
|
+
|
|
128
|
+
const scopes = input.scopes ?? ['openid', 'profile', 'email']
|
|
129
|
+
const offline = this.config.offlineAccess !== false
|
|
130
|
+
|
|
131
|
+
const params = new URLSearchParams({
|
|
132
|
+
response_type: 'code',
|
|
133
|
+
client_id: this.config.clientId,
|
|
134
|
+
redirect_uri: input.redirectUri,
|
|
135
|
+
scope: scopes.join(' '),
|
|
136
|
+
state,
|
|
137
|
+
...(challenge ? { code_challenge: challenge, code_challenge_method: 'S256' } : {}),
|
|
138
|
+
...(offline ? { access_type: 'offline' } : {}),
|
|
139
|
+
// `include_granted_scopes=true` makes Google merge previous
|
|
140
|
+
// grants rather than replace — apps that progressively
|
|
141
|
+
// request scopes appreciate it; safe-by-default.
|
|
142
|
+
include_granted_scopes: 'true',
|
|
143
|
+
...(input.extra ?? {}),
|
|
144
|
+
})
|
|
145
|
+
params.delete('no_pkce')
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
url: `${this.endpoints.authorize}?${params.toString()}`,
|
|
149
|
+
state,
|
|
150
|
+
...(codeVerifier ? { codeVerifier } : {}),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async exchange(input: ExchangeInput): Promise<OAuthTokens> {
|
|
155
|
+
if (input.expectedState !== undefined && input.state !== input.expectedState) {
|
|
156
|
+
throw new StateMismatchError()
|
|
157
|
+
}
|
|
158
|
+
const body = new URLSearchParams({
|
|
159
|
+
grant_type: 'authorization_code',
|
|
160
|
+
code: input.code,
|
|
161
|
+
redirect_uri: input.redirectUri,
|
|
162
|
+
client_id: this.config.clientId,
|
|
163
|
+
client_secret: this.config.clientSecret,
|
|
164
|
+
...(input.codeVerifier ? { code_verifier: input.codeVerifier } : {}),
|
|
165
|
+
})
|
|
166
|
+
const res = await this.fetchFn(this.endpoints.token, {
|
|
167
|
+
method: 'POST',
|
|
168
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
169
|
+
body,
|
|
170
|
+
})
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
const text = await res.text()
|
|
173
|
+
throw new OAuthExchangeError(
|
|
174
|
+
`GoogleSocialDriver.exchange: token endpoint returned ${res.status}.`,
|
|
175
|
+
{ context: { status: res.status, body: text } },
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
return this.toOAuthTokens((await res.json()) as TokenResponse)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async profile(accessToken: string): Promise<SocialProfile> {
|
|
182
|
+
const res = await this.fetchFn(this.endpoints.userInfo, {
|
|
183
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
184
|
+
})
|
|
185
|
+
if (res.status === 401) {
|
|
186
|
+
throw new InvalidTokenError('GoogleSocialDriver.profile: access token rejected.')
|
|
187
|
+
}
|
|
188
|
+
if (!res.ok) {
|
|
189
|
+
const text = await res.text()
|
|
190
|
+
throw new SocialProviderError(
|
|
191
|
+
`GoogleSocialDriver.profile: userinfo endpoint returned ${res.status}.`,
|
|
192
|
+
{ provider: PROVIDER, operation: 'profile', context: { status: res.status, body: text } },
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
const u = (await res.json()) as UserInfoResponse
|
|
196
|
+
return {
|
|
197
|
+
id: u.sub,
|
|
198
|
+
provider: PROVIDER,
|
|
199
|
+
...(u.email ? { email: u.email } : {}),
|
|
200
|
+
...(u.email_verified !== undefined ? { emailVerified: u.email_verified } : {}),
|
|
201
|
+
...(u.name ? { name: u.name } : {}),
|
|
202
|
+
...(u.picture ? { avatarUrl: u.picture } : {}),
|
|
203
|
+
...(u.locale ? { locale: u.locale } : {}),
|
|
204
|
+
metadata: {
|
|
205
|
+
...(u.given_name ? { givenName: u.given_name } : {}),
|
|
206
|
+
...(u.family_name ? { familyName: u.family_name } : {}),
|
|
207
|
+
},
|
|
208
|
+
raw: u,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async refresh(input: RefreshInput): Promise<OAuthTokens> {
|
|
213
|
+
const body = new URLSearchParams({
|
|
214
|
+
grant_type: 'refresh_token',
|
|
215
|
+
refresh_token: input.refreshToken,
|
|
216
|
+
client_id: this.config.clientId,
|
|
217
|
+
client_secret: this.config.clientSecret,
|
|
218
|
+
})
|
|
219
|
+
const res = await this.fetchFn(this.endpoints.token, {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
222
|
+
body,
|
|
223
|
+
})
|
|
224
|
+
if (res.status === 400 || res.status === 401) {
|
|
225
|
+
const text = await res.text()
|
|
226
|
+
throw new InvalidTokenError(
|
|
227
|
+
`GoogleSocialDriver.refresh: refresh token rejected.`,
|
|
228
|
+
{ context: { status: res.status, body: text } },
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
if (!res.ok) {
|
|
232
|
+
const text = await res.text()
|
|
233
|
+
throw new SocialProviderError(
|
|
234
|
+
`GoogleSocialDriver.refresh: token endpoint returned ${res.status}.`,
|
|
235
|
+
{ provider: PROVIDER, operation: 'refresh', context: { status: res.status, body: text } },
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
// Google does NOT rotate refresh tokens; preserve the caller's
|
|
239
|
+
// current refresh token if the response omits it.
|
|
240
|
+
const json = (await res.json()) as TokenResponse
|
|
241
|
+
const tokens = this.toOAuthTokens(json)
|
|
242
|
+
if (!tokens.refreshToken) tokens.refreshToken = input.refreshToken
|
|
243
|
+
return tokens
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async revoke(token: string): Promise<void> {
|
|
247
|
+
// Google's revoke endpoint takes the token as a query
|
|
248
|
+
// parameter OR a form body; we POST form for consistency
|
|
249
|
+
// with the rest of the driver.
|
|
250
|
+
const body = new URLSearchParams({ token })
|
|
251
|
+
const res = await this.fetchFn(this.endpoints.revoke, {
|
|
252
|
+
method: 'POST',
|
|
253
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
254
|
+
body,
|
|
255
|
+
})
|
|
256
|
+
if (!res.ok) {
|
|
257
|
+
const text = await res.text()
|
|
258
|
+
throw new SocialProviderError(
|
|
259
|
+
`GoogleSocialDriver.revoke: revoke endpoint returned ${res.status}.`,
|
|
260
|
+
{ provider: PROVIDER, operation: 'revoke', context: { status: res.status, body: text } },
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ─── Internals ────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
private toOAuthTokens(t: TokenResponse): OAuthTokens {
|
|
268
|
+
const expiresAt =
|
|
269
|
+
typeof t.expires_in === 'number'
|
|
270
|
+
? new Date(Date.now() + t.expires_in * 1000)
|
|
271
|
+
: undefined
|
|
272
|
+
return {
|
|
273
|
+
accessToken: t.access_token,
|
|
274
|
+
...(t.refresh_token ? { refreshToken: t.refresh_token } : {}),
|
|
275
|
+
...(t.id_token ? { idToken: t.id_token } : {}),
|
|
276
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
277
|
+
...(t.scope ? { scope: t.scope } : {}),
|
|
278
|
+
tokenType: t.token_type ?? 'Bearer',
|
|
279
|
+
raw: t,
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Extract the `email` claim from a Google id_token (JWT). Same
|
|
286
|
+
* semantics as `emailFromLineIdToken` — decode-only, no JWS
|
|
287
|
+
* signature verification. Apps with stricter posture verify via
|
|
288
|
+
* Google's `tokeninfo?id_token=...` endpoint.
|
|
289
|
+
*
|
|
290
|
+
* Google's userinfo endpoint also returns `email` + `email_verified`,
|
|
291
|
+
* so most apps don't need this helper — it's here for paths that
|
|
292
|
+
* skip userinfo (e.g. server-only signin where the access token
|
|
293
|
+
* is discarded immediately after exchange).
|
|
294
|
+
*/
|
|
295
|
+
export function emailFromGoogleIdToken(idToken: string): string | null {
|
|
296
|
+
const segments = idToken.split('.')
|
|
297
|
+
if (segments.length !== 3) {
|
|
298
|
+
throw new InvalidTokenError(
|
|
299
|
+
'emailFromGoogleIdToken: id_token does not have 3 segments.',
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
const payload = decodeJwtSegment(segments[1]!)
|
|
303
|
+
return typeof payload.email === 'string' ? payload.email : null
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function decodeJwtSegment(segment: string): JwtPayload {
|
|
307
|
+
const pad = segment.length % 4
|
|
308
|
+
const padded = pad === 0 ? segment : `${segment}${'='.repeat(4 - pad)}`
|
|
309
|
+
const b64 = padded.replace(/-/g, '+').replace(/_/g, '/')
|
|
310
|
+
try {
|
|
311
|
+
return JSON.parse(atob(b64)) as JwtPayload
|
|
312
|
+
} catch (cause) {
|
|
313
|
+
throw new InvalidTokenError('decodeJwtSegment: failed to parse JWT segment.', {
|
|
314
|
+
cause,
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GoogleSocialProvider` — `ServiceProvider` that registers the
|
|
3
|
+
* Google driver factory on the `SocialManager`.
|
|
4
|
+
*
|
|
5
|
+
* Listed AFTER `SocialProvider` in `bootstrap/providers.ts`. The
|
|
6
|
+
* factory is invoked lazily on first `social.use(name)`;
|
|
7
|
+
* misconfigured credentials surface on first use.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { type Application, ServiceProvider } from '@strav/kernel'
|
|
11
|
+
import { SocialConfigError } from '../../social_error.ts'
|
|
12
|
+
import { SocialManager } from '../../social_manager.ts'
|
|
13
|
+
import type { GoogleProviderConfig } from './google_config.ts'
|
|
14
|
+
import { GoogleSocialDriver } from './google_driver.ts'
|
|
15
|
+
|
|
16
|
+
export class GoogleSocialProvider extends ServiceProvider {
|
|
17
|
+
override readonly name = 'social-google'
|
|
18
|
+
override readonly dependencies = ['social']
|
|
19
|
+
|
|
20
|
+
override register(app: Application): void {
|
|
21
|
+
const manager = app.resolve(SocialManager)
|
|
22
|
+
manager.extend('google', ({ instanceName, config }) => {
|
|
23
|
+
const cfg = config as GoogleProviderConfig
|
|
24
|
+
if (!cfg.clientId || !cfg.clientSecret) {
|
|
25
|
+
throw new SocialConfigError(
|
|
26
|
+
`GoogleSocialProvider: \`clientId\` and \`clientSecret\` are required for provider "${instanceName}".`,
|
|
27
|
+
{ context: { instanceName } },
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
return new GoogleSocialDriver({ instanceName, config: cfg })
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Public API of `@strav/social/google`.
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
GOOGLE_ENDPOINTS,
|
|
5
|
+
type GoogleProviderConfig,
|
|
6
|
+
} from './google_config.ts'
|
|
7
|
+
export {
|
|
8
|
+
emailFromGoogleIdToken,
|
|
9
|
+
GoogleSocialDriver,
|
|
10
|
+
type GoogleDriverOptions,
|
|
11
|
+
} from './google_driver.ts'
|
|
12
|
+
export { GoogleSocialProvider } from './google_provider.ts'
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Public API of `@strav/social/line`.
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
LINE_ENDPOINTS,
|
|
5
|
+
type LineProviderConfig,
|
|
6
|
+
} from './line_config.ts'
|
|
7
|
+
export {
|
|
8
|
+
emailFromLineIdToken,
|
|
9
|
+
LineSocialDriver,
|
|
10
|
+
type LineDriverOptions,
|
|
11
|
+
} from './line_driver.ts'
|
|
12
|
+
export { LineSocialProvider } from './line_provider.ts'
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-specific provider config. Apps put one of these inside
|
|
3
|
+
* `config.social.providers[name]` with `driver: 'line'`.
|
|
4
|
+
*
|
|
5
|
+
* Get credentials from https://developers.line.biz/console — a
|
|
6
|
+
* Line Login channel under a provider. The `email` scope
|
|
7
|
+
* additionally needs the "email permission" toggle to be enabled
|
|
8
|
+
* inside the channel (Line approval required for production).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ProviderConfig } from '../../types.ts'
|
|
12
|
+
|
|
13
|
+
export interface LineProviderConfig extends ProviderConfig {
|
|
14
|
+
driver: 'line'
|
|
15
|
+
/** Channel ID from the Line Developers console. */
|
|
16
|
+
clientId: string
|
|
17
|
+
/** Channel secret from the Line Developers console. */
|
|
18
|
+
clientSecret: string
|
|
19
|
+
/**
|
|
20
|
+
* Optional UI locale hint passed on every authorize URL —
|
|
21
|
+
* `'th-TH'`, `'ja-JP'`, `'en-US'`, … Apps that route by user
|
|
22
|
+
* locale override per-call via `authorize({ extra: { ui_locales } })`.
|
|
23
|
+
* Defaults to Line's autodetect.
|
|
24
|
+
*/
|
|
25
|
+
uiLocales?: string
|
|
26
|
+
/** Override endpoints for testing — never set in production. */
|
|
27
|
+
endpoints?: {
|
|
28
|
+
authorize?: string
|
|
29
|
+
token?: string
|
|
30
|
+
profile?: string
|
|
31
|
+
revoke?: string
|
|
32
|
+
verify?: string
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Custom `fetch` override (tests). Defaults to global `fetch`.
|
|
36
|
+
* The driver does no other I/O.
|
|
37
|
+
*/
|
|
38
|
+
fetch?: typeof fetch
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const LINE_ENDPOINTS = {
|
|
42
|
+
authorize: 'https://access.line.me/oauth2/v2.1/authorize',
|
|
43
|
+
token: 'https://api.line.me/oauth2/v2.1/token',
|
|
44
|
+
profile: 'https://api.line.me/v2/profile',
|
|
45
|
+
revoke: 'https://api.line.me/oauth2/v2.1/revoke',
|
|
46
|
+
verify: 'https://api.line.me/oauth2/v2.1/verify',
|
|
47
|
+
} as const
|