@idealyst/oauth-client 0.0.1 → 1.0.70
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/README.md +172 -182
- package/package.json +3 -9
- package/src/examples/simple-oauth-example.ts +60 -0
- package/src/index.native.ts +10 -0
- package/src/index.ts +8 -53
- package/src/index.web.ts +10 -0
- package/src/oauth-client.native.ts +130 -0
- package/src/oauth-client.web.ts +83 -0
- package/src/types.ts +8 -36
- package/src/examples/google-example.ts +0 -108
- package/src/native-client.ts +0 -159
- package/src/storage.ts +0 -60
- package/src/web-client.ts +0 -272
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { OAuthClient, OAuthConfig, OAuthResult } from './types'
|
|
2
|
+
import { Linking } from 'react-native'
|
|
3
|
+
|
|
4
|
+
export class NativeOAuthClient implements OAuthClient {
|
|
5
|
+
private config: OAuthConfig
|
|
6
|
+
|
|
7
|
+
constructor(config: OAuthConfig) {
|
|
8
|
+
this.config = config
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async authorize(): Promise<OAuthResult> {
|
|
12
|
+
const state = this.generateState()
|
|
13
|
+
const oauthUrl = this.buildOAuthUrl(state)
|
|
14
|
+
|
|
15
|
+
// Open OAuth URL in system browser
|
|
16
|
+
await Linking.openURL(oauthUrl)
|
|
17
|
+
|
|
18
|
+
// Wait for deep link callback
|
|
19
|
+
const callbackData = await this.waitForDeepLinkCallback()
|
|
20
|
+
|
|
21
|
+
if (callbackData.error) {
|
|
22
|
+
throw new Error(`OAuth error: ${callbackData.error}`)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (callbackData.code) {
|
|
26
|
+
return {
|
|
27
|
+
code: callbackData.code,
|
|
28
|
+
state: callbackData.state
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
throw new Error('No authorization code received')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async waitForDeepLinkCallback(): Promise<{ code?: string; error?: string; state?: string }> {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
let subscription: any
|
|
38
|
+
|
|
39
|
+
const handleUrl = (event: { url: string }) => {
|
|
40
|
+
const callbackData = this.parseDeepLink(event.url)
|
|
41
|
+
if (callbackData) {
|
|
42
|
+
cleanup()
|
|
43
|
+
resolve(callbackData)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const cleanup = () => {
|
|
48
|
+
if (subscription?.remove) {
|
|
49
|
+
subscription.remove()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check for initial URL (if app was opened from deep link)
|
|
54
|
+
Linking.getInitialURL().then((url: string | null) => {
|
|
55
|
+
if (url) {
|
|
56
|
+
const callbackData = this.parseDeepLink(url)
|
|
57
|
+
if (callbackData) {
|
|
58
|
+
cleanup()
|
|
59
|
+
resolve(callbackData)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Listen for subsequent deep links
|
|
66
|
+
subscription = Linking.addEventListener('url', handleUrl)
|
|
67
|
+
|
|
68
|
+
// Timeout after 5 minutes
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
cleanup()
|
|
71
|
+
reject(new Error('OAuth timeout - user did not complete authorization'))
|
|
72
|
+
}, 5 * 60 * 1000)
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private parseDeepLink(url: string): { code?: string; error?: string; state?: string } | null {
|
|
77
|
+
try {
|
|
78
|
+
const parsedUrl = new URL(url)
|
|
79
|
+
|
|
80
|
+
// Check if this is our OAuth callback
|
|
81
|
+
const expectedScheme = new URL(this.config.redirectUrl).protocol.slice(0, -1)
|
|
82
|
+
if (parsedUrl.protocol.slice(0, -1) !== expectedScheme) {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Extract OAuth parameters
|
|
87
|
+
const code = parsedUrl.searchParams.get('code')
|
|
88
|
+
const error = parsedUrl.searchParams.get('error')
|
|
89
|
+
const state = parsedUrl.searchParams.get('state')
|
|
90
|
+
|
|
91
|
+
if (!code && !error) {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
code: code || undefined,
|
|
97
|
+
error: error || undefined,
|
|
98
|
+
state: state || undefined
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private buildOAuthUrl(state: string): string {
|
|
106
|
+
const url = new URL(this.config.oauthUrl)
|
|
107
|
+
|
|
108
|
+
url.searchParams.set('redirect_uri', this.config.redirectUrl)
|
|
109
|
+
url.searchParams.set('state', state)
|
|
110
|
+
|
|
111
|
+
// Add additional parameters
|
|
112
|
+
if (this.config.additionalParameters) {
|
|
113
|
+
Object.entries(this.config.additionalParameters).forEach(([key, value]) => {
|
|
114
|
+
url.searchParams.set(key, value)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return url.toString()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private generateState(): string {
|
|
122
|
+
// Generate random state for CSRF protection
|
|
123
|
+
let result = ''
|
|
124
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'
|
|
125
|
+
for (let i = 0; i < 32; i++) {
|
|
126
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
127
|
+
}
|
|
128
|
+
return result
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { OAuthClient, OAuthConfig, OAuthResult } from './types'
|
|
2
|
+
|
|
3
|
+
export class WebOAuthClient implements OAuthClient {
|
|
4
|
+
private config: OAuthConfig
|
|
5
|
+
|
|
6
|
+
constructor(config: OAuthConfig) {
|
|
7
|
+
this.config = config
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async authorize(): Promise<OAuthResult> {
|
|
11
|
+
const state = this.generateState()
|
|
12
|
+
|
|
13
|
+
// Check if we're already in a callback
|
|
14
|
+
const callbackData = this.checkForCallback()
|
|
15
|
+
|
|
16
|
+
if (callbackData) {
|
|
17
|
+
const { code, error, returnedState } = callbackData
|
|
18
|
+
|
|
19
|
+
if (error) {
|
|
20
|
+
throw new Error(`OAuth error: ${error}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (code) {
|
|
24
|
+
// Clean up URL
|
|
25
|
+
window.history.replaceState({}, document.title, window.location.pathname)
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
code,
|
|
29
|
+
state: returnedState || undefined
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Build OAuth URL and redirect
|
|
35
|
+
const oauthUrl = this.buildOAuthUrl(state)
|
|
36
|
+
window.location.href = oauthUrl
|
|
37
|
+
|
|
38
|
+
// This won't be reached due to redirect
|
|
39
|
+
throw new Error('Authorization flow initiated')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private checkForCallback(): { code?: string; error?: string; returnedState?: string } | null {
|
|
43
|
+
const urlParams = new URLSearchParams(window.location.search)
|
|
44
|
+
const code = urlParams.get('code')
|
|
45
|
+
const error = urlParams.get('error')
|
|
46
|
+
const state = urlParams.get('state')
|
|
47
|
+
|
|
48
|
+
if (code || error) {
|
|
49
|
+
return {
|
|
50
|
+
code: code || undefined,
|
|
51
|
+
error: error || undefined,
|
|
52
|
+
returnedState: state || undefined,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private buildOAuthUrl(state: string): string {
|
|
60
|
+
const url = new URL(this.config.oauthUrl)
|
|
61
|
+
|
|
62
|
+
url.searchParams.set('redirect_uri', this.config.redirectUrl)
|
|
63
|
+
url.searchParams.set('state', state)
|
|
64
|
+
|
|
65
|
+
// Add additional parameters
|
|
66
|
+
if (this.config.additionalParameters) {
|
|
67
|
+
Object.entries(this.config.additionalParameters).forEach(([key, value]) => {
|
|
68
|
+
url.searchParams.set(key, value)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return url.toString()
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private generateState(): string {
|
|
76
|
+
const array = new Uint8Array(16)
|
|
77
|
+
crypto.getRandomValues(array)
|
|
78
|
+
return btoa(String.fromCharCode.apply(null, Array.from(array)))
|
|
79
|
+
.replace(/\+/g, '-')
|
|
80
|
+
.replace(/\//g, '_')
|
|
81
|
+
.replace(/=/g, '')
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,47 +1,19 @@
|
|
|
1
1
|
export interface OAuthConfig {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
additionalParameters?: Record<string, string>
|
|
5
|
-
customHeaders?: Record<string, string>
|
|
6
|
-
scopes?: string[]
|
|
2
|
+
// OAuth endpoint URL (e.g., "https://api.yourapp.com/auth/google")
|
|
3
|
+
oauthUrl: string
|
|
7
4
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
authorizationEndpoint?: string
|
|
11
|
-
tokenEndpoint?: string
|
|
12
|
-
revocationEndpoint?: string
|
|
13
|
-
endSessionEndpoint?: string
|
|
5
|
+
// Redirect URL for the client app (e.g., "com.yourapp://oauth/callback")
|
|
6
|
+
redirectUrl: string
|
|
14
7
|
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
iosUrlScheme?: string
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface OAuthTokens {
|
|
21
|
-
accessToken: string
|
|
22
|
-
refreshToken?: string
|
|
23
|
-
idToken?: string
|
|
24
|
-
tokenType?: string
|
|
25
|
-
expiresAt?: Date
|
|
26
|
-
scopes?: string[]
|
|
8
|
+
// Optional additional parameters to send to OAuth endpoint
|
|
9
|
+
additionalParameters?: Record<string, string>
|
|
27
10
|
}
|
|
28
11
|
|
|
29
12
|
export interface OAuthResult {
|
|
30
|
-
|
|
31
|
-
|
|
13
|
+
code: string
|
|
14
|
+
state?: string
|
|
32
15
|
}
|
|
33
16
|
|
|
34
17
|
export interface OAuthClient {
|
|
35
18
|
authorize(): Promise<OAuthResult>
|
|
36
|
-
refresh(refreshToken: string): Promise<OAuthResult>
|
|
37
|
-
revoke(token: string): Promise<void>
|
|
38
|
-
logout(): Promise<void>
|
|
39
|
-
getStoredTokens(): Promise<OAuthTokens | null>
|
|
40
|
-
clearStoredTokens(): Promise<void>
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface StorageAdapter {
|
|
44
|
-
getItem(key: string): Promise<string | null>
|
|
45
|
-
setItem(key: string, value: string): Promise<void>
|
|
46
|
-
removeItem(key: string): Promise<void>
|
|
47
19
|
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { createOAuthClient, providers } from '../index'
|
|
2
|
-
|
|
3
|
-
// Example: Google OAuth - works on both web and mobile
|
|
4
|
-
export async function setupGoogleOAuth() {
|
|
5
|
-
const client = createOAuthClient({
|
|
6
|
-
...providers.google,
|
|
7
|
-
clientId: 'your-google-client-id',
|
|
8
|
-
redirectUrl: 'com.yourapp://oauth/callback', // Mobile
|
|
9
|
-
// redirectUrl: 'https://yourapp.com/auth/callback', // Web
|
|
10
|
-
scopes: ['openid', 'profile', 'email'],
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
// Authorize user - same API on both platforms!
|
|
15
|
-
const result = await client.authorize()
|
|
16
|
-
console.log('Access token:', result.tokens.accessToken)
|
|
17
|
-
console.log('User info:', result.user)
|
|
18
|
-
|
|
19
|
-
// Token management works the same on both platforms
|
|
20
|
-
const storedTokens = await client.getStoredTokens()
|
|
21
|
-
if (storedTokens) {
|
|
22
|
-
console.log('Stored tokens:', storedTokens)
|
|
23
|
-
|
|
24
|
-
// Check if token needs refresh
|
|
25
|
-
if (storedTokens.expiresAt && storedTokens.expiresAt < new Date()) {
|
|
26
|
-
if (storedTokens.refreshToken) {
|
|
27
|
-
const refreshed = await client.refresh(storedTokens.refreshToken)
|
|
28
|
-
console.log('Refreshed tokens:', refreshed.tokens)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Logout when done
|
|
34
|
-
await client.logout()
|
|
35
|
-
console.log('Logged out successfully')
|
|
36
|
-
|
|
37
|
-
} catch (error) {
|
|
38
|
-
console.error('OAuth error:', error)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Example: Multiple providers
|
|
43
|
-
export async function setupMultipleProviders() {
|
|
44
|
-
const providers = [
|
|
45
|
-
{ name: 'Google', config: { ...providers.google, clientId: 'google-client-id' } },
|
|
46
|
-
{ name: 'GitHub', config: { ...providers.github, clientId: 'github-client-id' } },
|
|
47
|
-
{ name: 'Microsoft', config: { ...providers.microsoft, clientId: 'ms-client-id' } },
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
for (const provider of providers) {
|
|
51
|
-
try {
|
|
52
|
-
const client = createOAuthClient({
|
|
53
|
-
...provider.config,
|
|
54
|
-
redirectUrl: 'com.yourapp://oauth/callback',
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
const result = await client.authorize()
|
|
58
|
-
console.log(`${provider.name} login successful:`, result.tokens.accessToken)
|
|
59
|
-
return { provider: provider.name, tokens: result.tokens }
|
|
60
|
-
} catch (error) {
|
|
61
|
-
console.warn(`${provider.name} login failed:`, error)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
throw new Error('All OAuth providers failed')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Example: Custom provider configuration
|
|
69
|
-
export async function setupCustomProvider() {
|
|
70
|
-
const client = createOAuthClient({
|
|
71
|
-
issuer: 'https://your-oauth-server.com',
|
|
72
|
-
clientId: 'your-client-id',
|
|
73
|
-
redirectUrl: 'com.yourapp://oauth/callback',
|
|
74
|
-
scopes: ['read', 'write'],
|
|
75
|
-
additionalParameters: {
|
|
76
|
-
prompt: 'consent',
|
|
77
|
-
},
|
|
78
|
-
customHeaders: {
|
|
79
|
-
'X-Custom-Header': 'value',
|
|
80
|
-
},
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const result = await client.authorize()
|
|
85
|
-
return result.tokens
|
|
86
|
-
} catch (error) {
|
|
87
|
-
console.error('Custom OAuth error:', error)
|
|
88
|
-
throw error
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Example: Platform-specific configuration
|
|
93
|
-
export async function setupPlatformSpecificOAuth() {
|
|
94
|
-
// Check if we're on mobile or web and configure accordingly
|
|
95
|
-
const isMobile = typeof navigator !== 'undefined' && navigator.product === 'ReactNative'
|
|
96
|
-
|
|
97
|
-
const client = createOAuthClient({
|
|
98
|
-
...providers.google,
|
|
99
|
-
clientId: 'your-google-client-id',
|
|
100
|
-
redirectUrl: isMobile
|
|
101
|
-
? 'com.yourapp://oauth/callback' // Mobile deep link
|
|
102
|
-
: 'https://yourapp.com/auth/callback', // Web callback
|
|
103
|
-
scopes: ['openid', 'profile', 'email'],
|
|
104
|
-
// Note: No client secret needed - PKCE provides security for public clients
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
return await client.authorize()
|
|
108
|
-
}
|
package/src/native-client.ts
DELETED
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { authorize, refresh, revoke, type AuthConfiguration, type AuthorizeResult } from 'react-native-app-auth'
|
|
2
|
-
import type { OAuthClient, OAuthConfig, OAuthResult, OAuthTokens, StorageAdapter } from './types'
|
|
3
|
-
|
|
4
|
-
export class NativeOAuthClient implements OAuthClient {
|
|
5
|
-
private config: OAuthConfig
|
|
6
|
-
private storage: StorageAdapter
|
|
7
|
-
private storageKey: string
|
|
8
|
-
private authConfig: AuthConfiguration
|
|
9
|
-
|
|
10
|
-
constructor(config: OAuthConfig, storage: StorageAdapter) {
|
|
11
|
-
this.config = config
|
|
12
|
-
this.storage = storage
|
|
13
|
-
this.storageKey = `oauth_tokens_${config.clientId}`
|
|
14
|
-
|
|
15
|
-
this.authConfig = {
|
|
16
|
-
issuer: config.issuer,
|
|
17
|
-
clientId: config.clientId,
|
|
18
|
-
redirectUrl: config.redirectUrl,
|
|
19
|
-
scopes: config.scopes || [],
|
|
20
|
-
additionalParameters: config.additionalParameters || {},
|
|
21
|
-
customHeaders: config.customHeaders || {},
|
|
22
|
-
usesPkce: true,
|
|
23
|
-
usesStateParam: true,
|
|
24
|
-
|
|
25
|
-
// Optional endpoint overrides
|
|
26
|
-
...(config.authorizationEndpoint && {
|
|
27
|
-
authorizationEndpoint: config.authorizationEndpoint
|
|
28
|
-
}),
|
|
29
|
-
...(config.tokenEndpoint && {
|
|
30
|
-
tokenEndpoint: config.tokenEndpoint
|
|
31
|
-
}),
|
|
32
|
-
...(config.revocationEndpoint && {
|
|
33
|
-
revocationEndpoint: config.revocationEndpoint
|
|
34
|
-
}),
|
|
35
|
-
...(config.endSessionEndpoint && {
|
|
36
|
-
endSessionEndpoint: config.endSessionEndpoint
|
|
37
|
-
}),
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async authorize(): Promise<OAuthResult> {
|
|
42
|
-
try {
|
|
43
|
-
const result: AuthorizeResult = await authorize(this.authConfig)
|
|
44
|
-
|
|
45
|
-
const tokens: OAuthTokens = {
|
|
46
|
-
accessToken: result.accessToken,
|
|
47
|
-
refreshToken: result.refreshToken,
|
|
48
|
-
idToken: result.idToken,
|
|
49
|
-
tokenType: result.tokenType || 'Bearer',
|
|
50
|
-
expiresAt: result.accessTokenExpirationDate
|
|
51
|
-
? new Date(result.accessTokenExpirationDate)
|
|
52
|
-
: undefined,
|
|
53
|
-
scopes: result.scopes,
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
await this.storeTokens(tokens)
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
tokens,
|
|
60
|
-
user: result.additionalParameters
|
|
61
|
-
}
|
|
62
|
-
} catch (error: any) {
|
|
63
|
-
throw new Error(`Authorization failed: ${error.message || error}`)
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async refresh(refreshToken: string): Promise<OAuthResult> {
|
|
68
|
-
try {
|
|
69
|
-
const result = await refresh(this.authConfig, {
|
|
70
|
-
refreshToken,
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
const tokens: OAuthTokens = {
|
|
74
|
-
accessToken: result.accessToken,
|
|
75
|
-
refreshToken: result.refreshToken || refreshToken, // Keep original if not returned
|
|
76
|
-
idToken: result.idToken,
|
|
77
|
-
tokenType: result.tokenType || 'Bearer',
|
|
78
|
-
expiresAt: result.accessTokenExpirationDate
|
|
79
|
-
? new Date(result.accessTokenExpirationDate)
|
|
80
|
-
: undefined,
|
|
81
|
-
scopes: result.scopes,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
await this.storeTokens(tokens)
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
tokens,
|
|
88
|
-
user: result.additionalParameters
|
|
89
|
-
}
|
|
90
|
-
} catch (error: any) {
|
|
91
|
-
throw new Error(`Token refresh failed: ${error.message || error}`)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
async revoke(token: string): Promise<void> {
|
|
96
|
-
try {
|
|
97
|
-
await revoke(this.authConfig, {
|
|
98
|
-
tokenToRevoke: token,
|
|
99
|
-
sendClientId: true,
|
|
100
|
-
})
|
|
101
|
-
} catch (error: any) {
|
|
102
|
-
// Some providers return errors for already revoked tokens
|
|
103
|
-
// Don't throw unless it's a network error
|
|
104
|
-
if (error.message?.includes('network') || error.message?.includes('connection')) {
|
|
105
|
-
throw new Error(`Token revocation failed: ${error.message || error}`)
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
async logout(): Promise<void> {
|
|
111
|
-
const tokens = await this.getStoredTokens()
|
|
112
|
-
|
|
113
|
-
// Revoke tokens if available
|
|
114
|
-
if (tokens?.accessToken) {
|
|
115
|
-
try {
|
|
116
|
-
await this.revoke(tokens.accessToken)
|
|
117
|
-
} catch (error) {
|
|
118
|
-
console.warn('Failed to revoke access token:', error)
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (tokens?.refreshToken) {
|
|
123
|
-
try {
|
|
124
|
-
await this.revoke(tokens.refreshToken)
|
|
125
|
-
} catch (error) {
|
|
126
|
-
console.warn('Failed to revoke refresh token:', error)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
await this.clearStoredTokens()
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async getStoredTokens(): Promise<OAuthTokens | null> {
|
|
134
|
-
const stored = await this.storage.getItem(this.storageKey)
|
|
135
|
-
if (!stored) return null
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
const tokens = JSON.parse(stored)
|
|
139
|
-
return {
|
|
140
|
-
...tokens,
|
|
141
|
-
expiresAt: tokens.expiresAt ? new Date(tokens.expiresAt) : undefined,
|
|
142
|
-
}
|
|
143
|
-
} catch {
|
|
144
|
-
return null
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async clearStoredTokens(): Promise<void> {
|
|
149
|
-
await this.storage.removeItem(this.storageKey)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
private async storeTokens(tokens: OAuthTokens): Promise<void> {
|
|
153
|
-
const serializable = {
|
|
154
|
-
...tokens,
|
|
155
|
-
expiresAt: tokens.expiresAt?.toISOString(),
|
|
156
|
-
}
|
|
157
|
-
await this.storage.setItem(this.storageKey, JSON.stringify(serializable))
|
|
158
|
-
}
|
|
159
|
-
}
|
package/src/storage.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { StorageAdapter } from './types'
|
|
2
|
-
|
|
3
|
-
export class WebStorage implements StorageAdapter {
|
|
4
|
-
async getItem(key: string): Promise<string | null> {
|
|
5
|
-
if (typeof localStorage === 'undefined') {
|
|
6
|
-
return null
|
|
7
|
-
}
|
|
8
|
-
return localStorage.getItem(key)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async setItem(key: string, value: string): Promise<void> {
|
|
12
|
-
if (typeof localStorage === 'undefined') {
|
|
13
|
-
return
|
|
14
|
-
}
|
|
15
|
-
localStorage.setItem(key, value)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async removeItem(key: string): Promise<void> {
|
|
19
|
-
if (typeof localStorage === 'undefined') {
|
|
20
|
-
return
|
|
21
|
-
}
|
|
22
|
-
localStorage.removeItem(key)
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class ReactNativeStorage implements StorageAdapter {
|
|
27
|
-
private AsyncStorage: any
|
|
28
|
-
|
|
29
|
-
constructor() {
|
|
30
|
-
try {
|
|
31
|
-
this.AsyncStorage = require('@react-native-async-storage/async-storage').default
|
|
32
|
-
} catch {
|
|
33
|
-
throw new Error(
|
|
34
|
-
'AsyncStorage is required for React Native. Please install @react-native-async-storage/async-storage'
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async getItem(key: string): Promise<string | null> {
|
|
40
|
-
return await this.AsyncStorage.getItem(key)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async setItem(key: string, value: string): Promise<void> {
|
|
44
|
-
await this.AsyncStorage.setItem(key, value)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async removeItem(key: string): Promise<void> {
|
|
48
|
-
await this.AsyncStorage.removeItem(key)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function createDefaultStorage(): StorageAdapter {
|
|
53
|
-
// Check if we're in React Native environment
|
|
54
|
-
if (typeof navigator !== 'undefined' && navigator.product === 'ReactNative') {
|
|
55
|
-
return new ReactNativeStorage()
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Default to web storage
|
|
59
|
-
return new WebStorage()
|
|
60
|
-
}
|