@stravigor/socialite 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/CHANGELOG.md +12 -0
- package/package.json +19 -0
- package/src/abstract_provider.ts +173 -0
- package/src/helpers.ts +13 -0
- package/src/index.ts +7 -0
- package/src/providers/discord_provider.ts +54 -0
- package/src/providers/github_provider.ts +66 -0
- package/src/providers/google_provider.ts +46 -0
- package/src/socialite_manager.ts +68 -0
- package/src/types.ts +31 -0
- package/tsconfig.json +4 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Initial release with OAuth 2.0 social authentication
|
|
8
|
+
- Built-in providers: Google, GitHub, Discord
|
|
9
|
+
- `SocialiteManager` with driver-based architecture and custom provider extensibility
|
|
10
|
+
- Session-based CSRF state verification with `stateless()` opt-out
|
|
11
|
+
- `socialite` helper for fluent API access
|
|
12
|
+
- `AbstractProvider` base class for building custom providers
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stravigor/socialite",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OAuth social authentication for the Strav framework",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./*": "./src/*.ts"
|
|
10
|
+
},
|
|
11
|
+
"files": ["src/", "package.json", "tsconfig.json", "CHANGELOG.md"],
|
|
12
|
+
"peerDependencies": {
|
|
13
|
+
"@stravigor/core": "0.2.2"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "bun test tests/",
|
|
17
|
+
"typecheck": "tsc --noEmit"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type Context from '@stravigor/core/http/context'
|
|
2
|
+
import type Session from '@stravigor/core/session/session'
|
|
3
|
+
import { randomHex } from '@stravigor/core/helpers/crypto'
|
|
4
|
+
import { ExternalServiceError } from '@stravigor/core/exceptions/errors'
|
|
5
|
+
import type { ProviderConfig, SocialiteUser, TokenResponse } from './types.ts'
|
|
6
|
+
|
|
7
|
+
const STATE_KEY = 'socialite_state'
|
|
8
|
+
|
|
9
|
+
export abstract class AbstractProvider {
|
|
10
|
+
abstract readonly name: string
|
|
11
|
+
|
|
12
|
+
protected config: ProviderConfig
|
|
13
|
+
protected _scopes: string[]
|
|
14
|
+
protected _stateless = false
|
|
15
|
+
protected _parameters: Record<string, string> = {}
|
|
16
|
+
|
|
17
|
+
constructor(config: ProviderConfig) {
|
|
18
|
+
this.config = config
|
|
19
|
+
this._scopes = config.scopes ?? this.getDefaultScopes()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Template methods — each provider implements these
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
protected abstract getDefaultScopes(): string[]
|
|
27
|
+
protected abstract getAuthUrl(): string
|
|
28
|
+
protected abstract getTokenUrl(): string
|
|
29
|
+
protected abstract getUserByToken(token: string): Promise<Record<string, unknown>>
|
|
30
|
+
protected abstract mapUserToObject(data: Record<string, unknown>): SocialiteUser
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Fluent API
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
scopes(scopes: string[]): this {
|
|
37
|
+
this._scopes = [...new Set([...this._scopes, ...scopes])]
|
|
38
|
+
return this
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setScopes(scopes: string[]): this {
|
|
42
|
+
this._scopes = scopes
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
stateless(): this {
|
|
47
|
+
this._stateless = true
|
|
48
|
+
return this
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
with(params: Record<string, string>): this {
|
|
52
|
+
this._parameters = { ...this._parameters, ...params }
|
|
53
|
+
return this
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// OAuth flow
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
redirect(ctx: Context): Response {
|
|
61
|
+
let state: string | undefined
|
|
62
|
+
|
|
63
|
+
if (!this._stateless) {
|
|
64
|
+
state = randomHex(32)
|
|
65
|
+
const session = ctx.get<Session>('session')
|
|
66
|
+
session.set(STATE_KEY, state)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const url = this.buildAuthUrl(state)
|
|
70
|
+
return ctx.redirect(url)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async user(ctx: Context): Promise<SocialiteUser> {
|
|
74
|
+
if (!this._stateless) {
|
|
75
|
+
const session = ctx.get<Session>('session')
|
|
76
|
+
const expectedState = session.get<string>(STATE_KEY)
|
|
77
|
+
const returnedState = ctx.query.get('state')
|
|
78
|
+
|
|
79
|
+
if (!expectedState || expectedState !== returnedState) {
|
|
80
|
+
throw new SocialiteError('Invalid state parameter. Possible CSRF attack.')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
session.forget(STATE_KEY)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const code = ctx.query.get('code')
|
|
87
|
+
if (!code) {
|
|
88
|
+
const error = ctx.query.get('error')
|
|
89
|
+
throw new SocialiteError(error ? `OAuth error: ${error}` : 'Missing authorization code.')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const token = await this.getAccessToken(code)
|
|
93
|
+
const data = await this.getUserByToken(token.accessToken)
|
|
94
|
+
const user = this.mapUserToObject(data)
|
|
95
|
+
|
|
96
|
+
user.token = token.accessToken
|
|
97
|
+
user.refreshToken = token.refreshToken
|
|
98
|
+
user.expiresIn = token.expiresIn
|
|
99
|
+
user.approvedScopes = token.scope ? token.scope.split(/[\s,]+/) : this._scopes
|
|
100
|
+
user.raw = data
|
|
101
|
+
|
|
102
|
+
return user
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async userFromToken(token: string): Promise<SocialiteUser> {
|
|
106
|
+
const data = await this.getUserByToken(token)
|
|
107
|
+
const user = this.mapUserToObject(data)
|
|
108
|
+
|
|
109
|
+
user.token = token
|
|
110
|
+
user.refreshToken = null
|
|
111
|
+
user.expiresIn = null
|
|
112
|
+
user.approvedScopes = this._scopes
|
|
113
|
+
user.raw = data
|
|
114
|
+
|
|
115
|
+
return user
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Internal
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
protected async getAccessToken(code: string): Promise<TokenResponse> {
|
|
123
|
+
const response = await fetch(this.getTokenUrl(), {
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: {
|
|
126
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
127
|
+
Accept: 'application/json',
|
|
128
|
+
},
|
|
129
|
+
body: new URLSearchParams({
|
|
130
|
+
grant_type: 'authorization_code',
|
|
131
|
+
client_id: this.config.clientId,
|
|
132
|
+
client_secret: this.config.clientSecret,
|
|
133
|
+
code,
|
|
134
|
+
redirect_uri: this.config.redirectUrl,
|
|
135
|
+
}),
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const text = await response.text()
|
|
140
|
+
throw new ExternalServiceError(this.name, response.status, text)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const data = (await response.json()) as Record<string, unknown>
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
accessToken: data.access_token as string,
|
|
147
|
+
refreshToken: (data.refresh_token as string) ?? null,
|
|
148
|
+
expiresIn: (data.expires_in as number) ?? null,
|
|
149
|
+
scope: (data.scope as string) ?? null,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected buildAuthUrl(state?: string): string {
|
|
154
|
+
const params = new URLSearchParams({
|
|
155
|
+
client_id: this.config.clientId,
|
|
156
|
+
redirect_uri: this.config.redirectUrl,
|
|
157
|
+
response_type: 'code',
|
|
158
|
+
scope: this._scopes.join(' '),
|
|
159
|
+
...this._parameters,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if (state) params.set('state', state)
|
|
163
|
+
|
|
164
|
+
return `${this.getAuthUrl()}?${params.toString()}`
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export class SocialiteError extends Error {
|
|
169
|
+
constructor(message: string) {
|
|
170
|
+
super(message)
|
|
171
|
+
this.name = 'SocialiteError'
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AbstractProvider } from './abstract_provider.ts'
|
|
2
|
+
import SocialiteManager from './socialite_manager.ts'
|
|
3
|
+
import type { ProviderConfig } from './types.ts'
|
|
4
|
+
|
|
5
|
+
export const socialite = {
|
|
6
|
+
driver(name: string): AbstractProvider {
|
|
7
|
+
return SocialiteManager.driver(name)
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
extend(name: string, factory: (config: ProviderConfig) => AbstractProvider): void {
|
|
11
|
+
SocialiteManager.extend(name, factory)
|
|
12
|
+
},
|
|
13
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { default, default as SocialiteManager } from './socialite_manager.ts'
|
|
2
|
+
export { socialite } from './helpers.ts'
|
|
3
|
+
export { AbstractProvider, SocialiteError } from './abstract_provider.ts'
|
|
4
|
+
export { GoogleProvider } from './providers/google_provider.ts'
|
|
5
|
+
export { GitHubProvider } from './providers/github_provider.ts'
|
|
6
|
+
export { DiscordProvider } from './providers/discord_provider.ts'
|
|
7
|
+
export type { SocialiteUser, SocialiteConfig, ProviderConfig, TokenResponse } from './types.ts'
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ExternalServiceError } from '@stravigor/core/exceptions/errors'
|
|
2
|
+
import { AbstractProvider } from '../abstract_provider.ts'
|
|
3
|
+
import type { SocialiteUser } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
export class DiscordProvider extends AbstractProvider {
|
|
6
|
+
readonly name = 'Discord'
|
|
7
|
+
|
|
8
|
+
protected getDefaultScopes(): string[] {
|
|
9
|
+
return ['identify', 'email']
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected getAuthUrl(): string {
|
|
13
|
+
return 'https://discord.com/api/oauth2/authorize'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protected getTokenUrl(): string {
|
|
17
|
+
return 'https://discord.com/api/oauth2/token'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected async getUserByToken(token: string): Promise<Record<string, unknown>> {
|
|
21
|
+
const response = await fetch('https://discord.com/api/v10/users/@me', {
|
|
22
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new ExternalServiceError('Discord', response.status, await response.text())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (await response.json()) as Record<string, unknown>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected mapUserToObject(data: Record<string, unknown>): SocialiteUser {
|
|
33
|
+
let avatar: string | null = null
|
|
34
|
+
if (data.avatar) {
|
|
35
|
+
avatar = `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png`
|
|
36
|
+
} else if (data.id) {
|
|
37
|
+
const index = Number((BigInt(data.id as string) >> 22n) % 6n)
|
|
38
|
+
avatar = `https://cdn.discordapp.com/embed/avatars/${index}.png`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id: data.id as string,
|
|
43
|
+
name: (data.global_name as string) ?? null,
|
|
44
|
+
email: (data.email as string) ?? null,
|
|
45
|
+
avatar,
|
|
46
|
+
nickname: (data.username as string) ?? null,
|
|
47
|
+
token: '',
|
|
48
|
+
refreshToken: null,
|
|
49
|
+
expiresIn: null,
|
|
50
|
+
approvedScopes: [],
|
|
51
|
+
raw: data,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { ExternalServiceError } from '@stravigor/core/exceptions/errors'
|
|
2
|
+
import { AbstractProvider } from '../abstract_provider.ts'
|
|
3
|
+
import type { SocialiteUser } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
export class GitHubProvider extends AbstractProvider {
|
|
6
|
+
readonly name = 'GitHub'
|
|
7
|
+
|
|
8
|
+
protected getDefaultScopes(): string[] {
|
|
9
|
+
return ['read:user', 'user:email']
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected getAuthUrl(): string {
|
|
13
|
+
return 'https://github.com/login/oauth/authorize'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protected getTokenUrl(): string {
|
|
17
|
+
return 'https://github.com/login/oauth/access_token'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected async getUserByToken(token: string): Promise<Record<string, unknown>> {
|
|
21
|
+
const headers = {
|
|
22
|
+
Authorization: `Bearer ${token}`,
|
|
23
|
+
Accept: 'application/json',
|
|
24
|
+
'User-Agent': 'Strav-Socialite',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const [userResponse, emailsResponse] = await Promise.all([
|
|
28
|
+
fetch('https://api.github.com/user', { headers }),
|
|
29
|
+
fetch('https://api.github.com/user/emails', { headers }),
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
if (!userResponse.ok) {
|
|
33
|
+
throw new ExternalServiceError('GitHub', userResponse.status, await userResponse.text())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const user = (await userResponse.json()) as Record<string, unknown>
|
|
37
|
+
|
|
38
|
+
// If the user's profile email is private, fall back to the primary verified email
|
|
39
|
+
if (!user.email && emailsResponse.ok) {
|
|
40
|
+
const emails = (await emailsResponse.json()) as Array<{
|
|
41
|
+
email: string
|
|
42
|
+
primary: boolean
|
|
43
|
+
verified: boolean
|
|
44
|
+
}>
|
|
45
|
+
const primary = emails.find(e => e.primary && e.verified)
|
|
46
|
+
if (primary) user.email = primary.email
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return user
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
protected mapUserToObject(data: Record<string, unknown>): SocialiteUser {
|
|
53
|
+
return {
|
|
54
|
+
id: String(data.id),
|
|
55
|
+
name: (data.name as string) ?? null,
|
|
56
|
+
email: (data.email as string) ?? null,
|
|
57
|
+
avatar: (data.avatar_url as string) ?? null,
|
|
58
|
+
nickname: (data.login as string) ?? null,
|
|
59
|
+
token: '',
|
|
60
|
+
refreshToken: null,
|
|
61
|
+
expiresIn: null,
|
|
62
|
+
approvedScopes: [],
|
|
63
|
+
raw: data,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { ExternalServiceError } from '@stravigor/core/exceptions/errors'
|
|
2
|
+
import { AbstractProvider } from '../abstract_provider.ts'
|
|
3
|
+
import type { SocialiteUser } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
export class GoogleProvider extends AbstractProvider {
|
|
6
|
+
readonly name = 'Google'
|
|
7
|
+
|
|
8
|
+
protected getDefaultScopes(): string[] {
|
|
9
|
+
return ['openid', 'email', 'profile']
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected getAuthUrl(): string {
|
|
13
|
+
return 'https://accounts.google.com/o/oauth2/v2/auth'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
protected getTokenUrl(): string {
|
|
17
|
+
return 'https://oauth2.googleapis.com/token'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
protected async getUserByToken(token: string): Promise<Record<string, unknown>> {
|
|
21
|
+
const response = await fetch('https://www.googleapis.com/oauth2/v3/userinfo', {
|
|
22
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new ExternalServiceError('Google', response.status, await response.text())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return (await response.json()) as Record<string, unknown>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
protected mapUserToObject(data: Record<string, unknown>): SocialiteUser {
|
|
33
|
+
return {
|
|
34
|
+
id: data.sub as string,
|
|
35
|
+
name: (data.name as string) ?? null,
|
|
36
|
+
email: (data.email as string) ?? null,
|
|
37
|
+
avatar: (data.picture as string) ?? null,
|
|
38
|
+
nickname: null,
|
|
39
|
+
token: '',
|
|
40
|
+
refreshToken: null,
|
|
41
|
+
expiresIn: null,
|
|
42
|
+
approvedScopes: [],
|
|
43
|
+
raw: data,
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { inject } from '@stravigor/core/core'
|
|
2
|
+
import type Configuration from '@stravigor/core/config/configuration'
|
|
3
|
+
import { ConfigurationError } from '@stravigor/core/exceptions/errors'
|
|
4
|
+
import type { AbstractProvider } from './abstract_provider.ts'
|
|
5
|
+
import type { ProviderConfig, SocialiteConfig } from './types.ts'
|
|
6
|
+
import { GoogleProvider } from './providers/google_provider.ts'
|
|
7
|
+
import { GitHubProvider } from './providers/github_provider.ts'
|
|
8
|
+
import { DiscordProvider } from './providers/discord_provider.ts'
|
|
9
|
+
|
|
10
|
+
@inject
|
|
11
|
+
export default class SocialiteManager {
|
|
12
|
+
private static _config: SocialiteConfig
|
|
13
|
+
private static _extensions = new Map<string, (config: ProviderConfig) => AbstractProvider>()
|
|
14
|
+
|
|
15
|
+
constructor(config: Configuration) {
|
|
16
|
+
SocialiteManager._config = {
|
|
17
|
+
providers: config.get('socialite.providers', {}) as Record<string, ProviderConfig>,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static get config(): SocialiteConfig {
|
|
22
|
+
if (!SocialiteManager._config) {
|
|
23
|
+
throw new ConfigurationError(
|
|
24
|
+
'SocialiteManager not configured. Resolve it through the container first.'
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
return SocialiteManager._config
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get a fresh provider instance by name.
|
|
32
|
+
* Returns a new instance each call because fluent methods mutate state.
|
|
33
|
+
*/
|
|
34
|
+
static driver(name: string): AbstractProvider {
|
|
35
|
+
const providerConfig = SocialiteManager._config?.providers[name]
|
|
36
|
+
if (!providerConfig) {
|
|
37
|
+
throw new ConfigurationError(`Socialite provider "${name}" is not configured.`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const driverName = providerConfig.driver ?? name
|
|
41
|
+
|
|
42
|
+
const extension = SocialiteManager._extensions.get(driverName)
|
|
43
|
+
if (extension) return extension(providerConfig)
|
|
44
|
+
|
|
45
|
+
switch (driverName) {
|
|
46
|
+
case 'google':
|
|
47
|
+
return new GoogleProvider(providerConfig)
|
|
48
|
+
case 'github':
|
|
49
|
+
return new GitHubProvider(providerConfig)
|
|
50
|
+
case 'discord':
|
|
51
|
+
return new DiscordProvider(providerConfig)
|
|
52
|
+
default:
|
|
53
|
+
throw new ConfigurationError(
|
|
54
|
+
`Unknown socialite driver "${driverName}". Register it with SocialiteManager.extend().`
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Register a custom provider factory. */
|
|
60
|
+
static extend(name: string, factory: (config: ProviderConfig) => AbstractProvider): void {
|
|
61
|
+
SocialiteManager._extensions.set(name, factory)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Clear all custom extensions (useful for testing). */
|
|
65
|
+
static reset(): void {
|
|
66
|
+
SocialiteManager._extensions.clear()
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface SocialiteUser {
|
|
2
|
+
id: string
|
|
3
|
+
name: string | null
|
|
4
|
+
email: string | null
|
|
5
|
+
avatar: string | null
|
|
6
|
+
nickname: string | null
|
|
7
|
+
token: string
|
|
8
|
+
refreshToken: string | null
|
|
9
|
+
expiresIn: number | null
|
|
10
|
+
approvedScopes: string[]
|
|
11
|
+
raw: Record<string, unknown>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ProviderConfig {
|
|
15
|
+
driver?: string
|
|
16
|
+
clientId: string
|
|
17
|
+
clientSecret: string
|
|
18
|
+
redirectUrl: string
|
|
19
|
+
scopes?: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SocialiteConfig {
|
|
23
|
+
providers: Record<string, ProviderConfig>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TokenResponse {
|
|
27
|
+
accessToken: string
|
|
28
|
+
refreshToken: string | null
|
|
29
|
+
expiresIn: number | null
|
|
30
|
+
scope: string | null
|
|
31
|
+
}
|
package/tsconfig.json
ADDED