@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.
Files changed (52) hide show
  1. package/package.json +23 -16
  2. package/src/drivers/facebook/facebook_config.ts +68 -0
  3. package/src/drivers/facebook/facebook_driver.ts +321 -0
  4. package/src/drivers/facebook/facebook_provider.ts +29 -0
  5. package/src/drivers/facebook/index.ts +12 -0
  6. package/src/drivers/google/google_config.ts +44 -0
  7. package/src/drivers/google/google_driver.ts +317 -0
  8. package/src/drivers/google/google_provider.ts +33 -0
  9. package/src/drivers/google/index.ts +12 -0
  10. package/src/drivers/index.ts +2 -0
  11. package/src/drivers/line/index.ts +12 -0
  12. package/src/drivers/line/line_config.ts +47 -0
  13. package/src/drivers/line/line_driver.ts +310 -0
  14. package/src/drivers/line/line_provider.ts +34 -0
  15. package/src/drivers/mock_driver.ts +170 -0
  16. package/src/drivers/unsupported.ts +17 -0
  17. package/src/dto/index.ts +4 -0
  18. package/src/dto/oauth_tokens.ts +26 -0
  19. package/src/dto/social_profile.ts +37 -0
  20. package/src/index.ts +61 -14
  21. package/src/ledger/apply_social_account_migration.ts +66 -0
  22. package/src/ledger/index.ts +12 -0
  23. package/src/ledger/social_account.ts +32 -0
  24. package/src/ledger/social_account_repository.ts +203 -0
  25. package/src/ledger/social_account_schema.ts +75 -0
  26. package/src/pkce.ts +63 -0
  27. package/src/social_capabilities.ts +35 -0
  28. package/src/social_driver.ts +143 -0
  29. package/src/social_error.ts +155 -0
  30. package/src/social_manager.ts +92 -74
  31. package/src/social_provider.ts +41 -7
  32. package/src/tenanted/apply_tenanted_social_account_migration.ts +45 -0
  33. package/src/tenanted/index.ts +18 -0
  34. package/src/tenanted/tenanted_social_account.ts +30 -0
  35. package/src/tenanted/tenanted_social_account_repository.ts +136 -0
  36. package/src/tenanted/tenanted_social_account_schema.ts +44 -0
  37. package/src/types.ts +15 -43
  38. package/CHANGELOG.md +0 -19
  39. package/README.md +0 -78
  40. package/src/abstract_provider.ts +0 -182
  41. package/src/helpers.ts +0 -31
  42. package/src/providers/discord_provider.ts +0 -59
  43. package/src/providers/facebook_provider.ts +0 -69
  44. package/src/providers/github_provider.ts +0 -75
  45. package/src/providers/google_provider.ts +0 -51
  46. package/src/providers/line_provider.ts +0 -73
  47. package/src/providers/linkedin_provider.ts +0 -51
  48. package/src/schema.ts +0 -13
  49. package/src/social_account.ts +0 -238
  50. package/stubs/config/social.ts +0 -22
  51. package/stubs/schemas/social_account.ts +0 -13
  52. package/tsconfig.json +0 -5
package/src/types.ts CHANGED
@@ -1,49 +1,21 @@
1
- export interface SocialUser {
2
- id: string
3
- name: string | null
4
- email: string | null
5
- /**
6
- * Whether the provider asserts the email has been verified by the user.
7
- *
8
- * Callers MUST check this before using `email` to match an existing
9
- * application user — linking by an unverified email is a known account-
10
- * takeover vector. See packages/social/CLAUDE.md ("Verified-email gate").
11
- */
12
- emailVerified: boolean
13
- avatar: string | null
14
- nickname: string | null
15
- token: string
16
- refreshToken: string | null
17
- expiresIn: number | null
18
- approvedScopes: string[]
19
- raw: Record<string, unknown>
20
- }
21
-
22
- export interface ProviderConfig {
23
- driver?: string
24
- clientId: string
25
- clientSecret: string
26
- redirectUrl: string
27
- scopes?: string[]
28
- /**
29
- * How to authenticate with the provider's token endpoint. Default
30
- * `'basic'` (HTTP Basic auth — RFC 6749 §2.3.1, MUST-support, keeps
31
- * `client_secret` out of body-logging surfaces). `'post'` falls back
32
- * to `client_secret` in the request body for providers that don't
33
- * accept Basic (e.g. Facebook). The `social` package picks the right
34
- * default per provider but you can override here if needed.
35
- */
36
- tokenEndpointAuthMethod?: 'basic' | 'post'
37
- }
1
+ /**
2
+ * `@strav/social` — runtime config shape.
3
+ *
4
+ * Multi-provider by default — apps register one entry per
5
+ * provider they want available (e.g. `line`, `google`,
6
+ * `facebook`). The driver field discriminates the adapter; the
7
+ * rest is driver-specific (`clientId`, `clientSecret`, …).
8
+ */
38
9
 
39
10
  export interface SocialConfig {
40
- userKey: string
11
+ /** Default routing target for unqualified `social.*` calls. Keyed into `providers`. */
12
+ default: string
41
13
  providers: Record<string, ProviderConfig>
42
14
  }
43
15
 
44
- export interface TokenResponse {
45
- accessToken: string
46
- refreshToken: string | null
47
- expiresIn: number | null
48
- scope: string | null
16
+ export interface ProviderConfig {
17
+ /** Driver identifier — must match a built-in (`'line'` / `'google'` / `'facebook'`) or a name registered via `manager.extend(name, factory)`. */
18
+ driver: string
19
+ /** Driver-specific fields — see each adapter's `*Config`. */
20
+ [key: string]: unknown
49
21
  }
package/CHANGELOG.md DELETED
@@ -1,19 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.1.1
4
-
5
- ### Added
6
-
7
- - Facebook provider (Graph API v21.0)
8
- - LinkedIn provider (OpenID Connect userinfo endpoint)
9
-
10
- ## 0.1.0
11
-
12
- ### Added
13
-
14
- - Initial release with OAuth 2.0 social authentication
15
- - Built-in providers: Google, GitHub, Discord
16
- - `SocialManager` with driver-based architecture and custom provider extensibility
17
- - Session-based CSRF state verification with `stateless()` opt-out
18
- - `social` helper for fluent API access
19
- - `AbstractProvider` base class for building custom providers
package/README.md DELETED
@@ -1,78 +0,0 @@
1
- # @strav/social
2
-
3
- OAuth social authentication for the [Strav](https://www.npmjs.com/package/@strav/core) framework. Sign in with Google, GitHub, Discord, Facebook, and LinkedIn using a fluent, driver-based API.
4
-
5
- ## Install
6
-
7
- ```bash
8
- bun add @strav/social
9
- bun strav install social
10
- ```
11
-
12
- Requires `@strav/core` as a peer dependency.
13
-
14
- ## Setup
15
-
16
- ```ts
17
- import { SocialProvider } from '@strav/social'
18
-
19
- app.use(new SocialProvider())
20
- ```
21
-
22
- ## Usage
23
-
24
- ```ts
25
- import { social } from '@strav/social'
26
-
27
- // Redirect to provider
28
- r.get('/auth/github', ctx => {
29
- return social.driver('github').redirect(ctx)
30
- })
31
-
32
- // Handle callback
33
- r.get('/auth/github/callback', async ctx => {
34
- const githubUser = await social.driver('github').user(ctx)
35
-
36
- // githubUser.id, .name, .email, .avatar, .nickname, .token
37
- let user = await User.findBy('email', githubUser.email)
38
- if (!user) {
39
- user = await User.create({ name: githubUser.name, email: githubUser.email })
40
- }
41
-
42
- await social.findOrCreate('github', githubUser, user)
43
- // authenticate session...
44
- })
45
- ```
46
-
47
- ## Providers
48
-
49
- - **Google** — OpenID Connect with Workspace domain restriction
50
- - **GitHub** — User profile with verified email fallback
51
- - **Discord** — Profile with computed avatar URLs
52
- - **Facebook** — Graph API v21.0
53
- - **LinkedIn** — OpenID Connect userinfo
54
-
55
- ## Fluent API
56
-
57
- ```ts
58
- social.driver('github').scopes(['repo', 'gist']).redirect(ctx)
59
- social.driver('google').with({ hd: 'example.com' }).redirect(ctx)
60
- const user = await social.driver('google').userFromToken(accessToken)
61
- ```
62
-
63
- ## Custom Providers
64
-
65
- ```ts
66
- import { AbstractProvider, social } from '@strav/social'
67
-
68
- class SpotifyProvider extends AbstractProvider { /* ... */ }
69
- social.extend('spotify', config => new SpotifyProvider(config))
70
- ```
71
-
72
- ## Documentation
73
-
74
- See the full [Social guide](../../guides/social.md).
75
-
76
- ## License
77
-
78
- MIT
@@ -1,182 +0,0 @@
1
- import type { Context, Session } from '@strav/http'
2
- import { randomHex, ExternalServiceError, scrubProviderError } from '@strav/kernel'
3
- import type { ProviderConfig, SocialUser, TokenResponse } from './types.ts'
4
-
5
- const STATE_KEY = 'social_state'
6
-
7
- export abstract class AbstractProvider {
8
- abstract readonly name: string
9
-
10
- protected config: ProviderConfig
11
- protected _scopes: string[]
12
- protected _parameters: Record<string, string> = {}
13
-
14
- constructor(config: ProviderConfig) {
15
- this.config = config
16
- this._scopes = config.scopes ?? this.getDefaultScopes()
17
- }
18
-
19
- // ---------------------------------------------------------------------------
20
- // Template methods — each provider implements these
21
- // ---------------------------------------------------------------------------
22
-
23
- protected abstract getDefaultScopes(): string[]
24
- protected abstract getAuthUrl(): string
25
- protected abstract getTokenUrl(): string
26
- protected abstract getUserByToken(token: string): Promise<Record<string, unknown>>
27
- protected abstract mapUserToObject(data: Record<string, unknown>): SocialUser
28
-
29
- // ---------------------------------------------------------------------------
30
- // Fluent API
31
- // ---------------------------------------------------------------------------
32
-
33
- scopes(scopes: string[]): this {
34
- this._scopes = [...new Set([...this._scopes, ...scopes])]
35
- return this
36
- }
37
-
38
- setScopes(scopes: string[]): this {
39
- this._scopes = scopes
40
- return this
41
- }
42
-
43
- with(params: Record<string, string>): this {
44
- this._parameters = { ...this._parameters, ...params }
45
- return this
46
- }
47
-
48
- // ---------------------------------------------------------------------------
49
- // OAuth flow
50
- // ---------------------------------------------------------------------------
51
-
52
- redirect(ctx: Context): Response {
53
- const state = randomHex(32)
54
- const session = ctx.get<Session>('session')
55
- session.set(STATE_KEY, state)
56
-
57
- const url = this.buildAuthUrl(state)
58
- return ctx.redirect(url)
59
- }
60
-
61
- async user(ctx: Context): Promise<SocialUser> {
62
- const session = ctx.get<Session>('session')
63
- const expectedState = session.get<string>(STATE_KEY)
64
- const returnedState = ctx.query.get('state')
65
-
66
- if (!expectedState || expectedState !== returnedState) {
67
- throw new SocialError('Invalid state parameter. Possible CSRF attack.')
68
- }
69
-
70
- session.forget(STATE_KEY)
71
-
72
- const code = ctx.query.get('code')
73
- if (!code) {
74
- const error = ctx.query.get('error')
75
- throw new SocialError(error ? `OAuth error: ${error}` : 'Missing authorization code.')
76
- }
77
-
78
- const token = await this.getAccessToken(code)
79
- const data = await this.getUserByToken(token.accessToken)
80
- const user = this.mapUserToObject(data)
81
-
82
- user.token = token.accessToken
83
- user.refreshToken = token.refreshToken
84
- user.expiresIn = token.expiresIn
85
- user.approvedScopes = token.scope ? token.scope.split(/[\s,]+/) : this._scopes
86
- user.raw = data
87
-
88
- return user
89
- }
90
-
91
- async userFromToken(token: string): Promise<SocialUser> {
92
- const data = await this.getUserByToken(token)
93
- const user = this.mapUserToObject(data)
94
-
95
- user.token = token
96
- user.refreshToken = null
97
- user.expiresIn = null
98
- user.approvedScopes = this._scopes
99
- user.raw = data
100
-
101
- return user
102
- }
103
-
104
- // ---------------------------------------------------------------------------
105
- // Internal
106
- // ---------------------------------------------------------------------------
107
-
108
- /**
109
- * Per-provider override of the default token-endpoint auth method.
110
- * Override this in subclasses for providers that require a non-`basic`
111
- * default (e.g. Facebook prefers `post`).
112
- */
113
- protected defaultTokenEndpointAuthMethod(): 'basic' | 'post' {
114
- return 'basic'
115
- }
116
-
117
- protected async getAccessToken(code: string): Promise<TokenResponse> {
118
- const method = this.config.tokenEndpointAuthMethod ?? this.defaultTokenEndpointAuthMethod()
119
-
120
- const headers: Record<string, string> = {
121
- 'Content-Type': 'application/x-www-form-urlencoded',
122
- Accept: 'application/json',
123
- }
124
-
125
- const body: Record<string, string> = {
126
- grant_type: 'authorization_code',
127
- client_id: this.config.clientId,
128
- code,
129
- redirect_uri: this.config.redirectUrl,
130
- }
131
-
132
- if (method === 'basic') {
133
- // RFC 6749 §2.3.1 — MUST-support form. Keeps client_secret out of
134
- // body-logging surfaces.
135
- const credentials = `${this.config.clientId}:${this.config.clientSecret}`
136
- headers.Authorization = `Basic ${Buffer.from(credentials, 'utf8').toString('base64')}`
137
- } else {
138
- // Fallback for providers that only accept secret-in-body.
139
- body.client_secret = this.config.clientSecret
140
- }
141
-
142
- const response = await fetch(this.getTokenUrl(), {
143
- method: 'POST',
144
- headers,
145
- body: new URLSearchParams(body),
146
- })
147
-
148
- if (!response.ok) {
149
- const text = await response.text()
150
- throw new ExternalServiceError(this.name, response.status, scrubProviderError(text))
151
- }
152
-
153
- const data = (await response.json()) as Record<string, unknown>
154
-
155
- return {
156
- accessToken: data.access_token as string,
157
- refreshToken: (data.refresh_token as string) ?? null,
158
- expiresIn: (data.expires_in as number) ?? null,
159
- scope: (data.scope as string) ?? null,
160
- }
161
- }
162
-
163
- protected buildAuthUrl(state: string): string {
164
- const params = new URLSearchParams({
165
- client_id: this.config.clientId,
166
- redirect_uri: this.config.redirectUrl,
167
- response_type: 'code',
168
- scope: this._scopes.join(' '),
169
- state,
170
- ...this._parameters,
171
- })
172
-
173
- return `${this.getAuthUrl()}?${params.toString()}`
174
- }
175
- }
176
-
177
- export class SocialError extends Error {
178
- constructor(message: string) {
179
- super(message)
180
- this.name = 'SocialError'
181
- }
182
- }
package/src/helpers.ts DELETED
@@ -1,31 +0,0 @@
1
- import type { AbstractProvider } from './abstract_provider.ts'
2
- import SocialAccount from './social_account.ts'
3
- import SocialManager from './social_manager.ts'
4
- import type { ProviderConfig, SocialUser } from './types.ts'
5
- import type { SocialAccountData } from './social_account.ts'
6
-
7
- export const social = {
8
- driver(name: string): AbstractProvider {
9
- return SocialManager.driver(name)
10
- },
11
-
12
- extend(name: string, factory: (config: ProviderConfig) => AbstractProvider): void {
13
- SocialManager.extend(name, factory)
14
- },
15
-
16
- /**
17
- * Find an existing social account by provider or create a new one.
18
- * If the account already exists, its tokens are updated.
19
- *
20
- * @example
21
- * const socialUser = await social.driver('google').user(ctx)
22
- * const { account, created } = await social.findOrCreate('google', socialUser, user)
23
- */
24
- findOrCreate(
25
- provider: string,
26
- socialUser: SocialUser,
27
- user: unknown
28
- ): Promise<{ account: SocialAccountData; created: boolean }> {
29
- return SocialAccount.findOrCreate(provider, socialUser, user)
30
- },
31
- }
@@ -1,59 +0,0 @@
1
- import { ExternalServiceError, scrubProviderError } from '@strav/kernel'
2
- import { AbstractProvider } from '../abstract_provider.ts'
3
- import type { SocialUser } 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(
27
- 'Discord',
28
- response.status,
29
- scrubProviderError(await response.text())
30
- )
31
- }
32
-
33
- return (await response.json()) as Record<string, unknown>
34
- }
35
-
36
- protected mapUserToObject(data: Record<string, unknown>): SocialUser {
37
- let avatar: string | null = null
38
- if (data.avatar) {
39
- avatar = `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png`
40
- } else if (data.id) {
41
- const index = Number((BigInt(data.id as string) >> 22n) % 6n)
42
- avatar = `https://cdn.discordapp.com/embed/avatars/${index}.png`
43
- }
44
-
45
- return {
46
- id: data.id as string,
47
- name: (data.global_name as string) ?? null,
48
- email: (data.email as string) ?? null,
49
- emailVerified: data.verified === true,
50
- avatar,
51
- nickname: (data.username as string) ?? null,
52
- token: '',
53
- refreshToken: null,
54
- expiresIn: null,
55
- approvedScopes: [],
56
- raw: data,
57
- }
58
- }
59
- }
@@ -1,69 +0,0 @@
1
- import { ExternalServiceError, scrubProviderError } from '@strav/kernel'
2
- import { AbstractProvider } from '../abstract_provider.ts'
3
- import type { SocialUser } from '../types.ts'
4
-
5
- const API_VERSION = 'v21.0'
6
-
7
- export class FacebookProvider extends AbstractProvider {
8
- readonly name = 'Facebook'
9
-
10
- protected getDefaultScopes(): string[] {
11
- return ['email', 'public_profile']
12
- }
13
-
14
- /**
15
- * Facebook's Graph API token endpoint reads `client_secret` from the
16
- * request body (its docs don't advertise HTTP Basic support reliably),
17
- * so default to `'post'` here. Apps can still override via
18
- * `ProviderConfig.tokenEndpointAuthMethod`.
19
- */
20
- protected override defaultTokenEndpointAuthMethod(): 'basic' | 'post' {
21
- return 'post'
22
- }
23
-
24
- protected getAuthUrl(): string {
25
- return `https://www.facebook.com/${API_VERSION}/dialog/oauth`
26
- }
27
-
28
- protected getTokenUrl(): string {
29
- return `https://graph.facebook.com/${API_VERSION}/oauth/access_token`
30
- }
31
-
32
- protected async getUserByToken(token: string): Promise<Record<string, unknown>> {
33
- const fields = 'id,name,email,picture.type(large)'
34
- const response = await fetch(
35
- `https://graph.facebook.com/${API_VERSION}/me?fields=${fields}&access_token=${token}`
36
- )
37
-
38
- if (!response.ok) {
39
- throw new ExternalServiceError(
40
- 'Facebook',
41
- response.status,
42
- scrubProviderError(await response.text())
43
- )
44
- }
45
-
46
- return (await response.json()) as Record<string, unknown>
47
- }
48
-
49
- protected mapUserToObject(data: Record<string, unknown>): SocialUser {
50
- const picture = data.picture as { data?: { url?: string } } | undefined
51
- const email = (data.email as string) ?? null
52
-
53
- return {
54
- id: data.id as string,
55
- name: (data.name as string) ?? null,
56
- email,
57
- // Facebook's Graph API only returns the user's verified primary email;
58
- // an unverified address is omitted from the response entirely.
59
- emailVerified: email !== null,
60
- avatar: picture?.data?.url ?? null,
61
- nickname: null,
62
- token: '',
63
- refreshToken: null,
64
- expiresIn: null,
65
- approvedScopes: [],
66
- raw: data,
67
- }
68
- }
69
- }
@@ -1,75 +0,0 @@
1
- import { ExternalServiceError, scrubProviderError } from '@strav/kernel'
2
- import { AbstractProvider } from '../abstract_provider.ts'
3
- import type { SocialUser } 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-Social',
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(
34
- 'GitHub',
35
- userResponse.status,
36
- scrubProviderError(await userResponse.text())
37
- )
38
- }
39
-
40
- const user = (await userResponse.json()) as Record<string, unknown>
41
-
42
- // If the user's profile email is private, fall back to the primary verified email
43
- if (!user.email && emailsResponse.ok) {
44
- const emails = (await emailsResponse.json()) as Array<{
45
- email: string
46
- primary: boolean
47
- verified: boolean
48
- }>
49
- const primary = emails.find(e => e.primary && e.verified)
50
- if (primary) user.email = primary.email
51
- }
52
-
53
- return user
54
- }
55
-
56
- protected mapUserToObject(data: Record<string, unknown>): SocialUser {
57
- const email = (data.email as string) ?? null
58
- return {
59
- id: String(data.id),
60
- name: (data.name as string) ?? null,
61
- email,
62
- // GitHub only exposes verified emails: the profile `email` is required
63
- // to be a verified address, and the fallback path in getUserByToken
64
- // filters /user/emails for `verified === true`.
65
- emailVerified: email !== null,
66
- avatar: (data.avatar_url as string) ?? null,
67
- nickname: (data.login as string) ?? null,
68
- token: '',
69
- refreshToken: null,
70
- expiresIn: null,
71
- approvedScopes: [],
72
- raw: data,
73
- }
74
- }
75
- }
@@ -1,51 +0,0 @@
1
- import { ExternalServiceError, scrubProviderError } from '@strav/kernel'
2
- import { AbstractProvider } from '../abstract_provider.ts'
3
- import type { SocialUser } 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(
27
- 'Google',
28
- response.status,
29
- scrubProviderError(await response.text())
30
- )
31
- }
32
-
33
- return (await response.json()) as Record<string, unknown>
34
- }
35
-
36
- protected mapUserToObject(data: Record<string, unknown>): SocialUser {
37
- return {
38
- id: data.sub as string,
39
- name: (data.name as string) ?? null,
40
- email: (data.email as string) ?? null,
41
- emailVerified: data.email_verified === true,
42
- avatar: (data.picture as string) ?? null,
43
- nickname: null,
44
- token: '',
45
- refreshToken: null,
46
- expiresIn: null,
47
- approvedScopes: [],
48
- raw: data,
49
- }
50
- }
51
- }