@yaotoshi/auth-sdk 0.2.1 → 0.2.3

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 ADDED
@@ -0,0 +1,171 @@
1
+ # @yaotoshi/auth-sdk
2
+
3
+ Browser SDK for integrating with a Yaotoshi Accounts service. Handles OAuth 2.0 + PKCE login, token management, and logout.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @yaotoshi/auth-sdk
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ import { YaotoshiAuth } from '@yaotoshi/auth-sdk';
15
+
16
+ const auth = new YaotoshiAuth({
17
+ clientId: 'your-client-id',
18
+ redirectUri: 'http://localhost:3000/callback',
19
+ postLogoutRedirectUri: 'http://localhost:3000',
20
+ accountsUrl: 'http://localhost:9999',
21
+ });
22
+ ```
23
+
24
+ ### Login
25
+
26
+ ```ts
27
+ // Redirects the user to the accounts service login page
28
+ auth.login();
29
+ ```
30
+
31
+ ### Handle Callback
32
+
33
+ On your `/callback` page:
34
+
35
+ ```ts
36
+ const { accessToken, user, scope, expiresIn } = await auth.handleCallback();
37
+ // user.email, user.sub (unique ID)
38
+ // expiresIn: token lifetime in seconds
39
+ ```
40
+
41
+ ### Check Auth Status
42
+
43
+ ```ts
44
+ // Returns false if no token or token is expired
45
+ auth.isAuthenticated();
46
+ ```
47
+
48
+ ### Get User Info
49
+
50
+ ```ts
51
+ const user = await auth.getUser();
52
+ // { sub: 'user-id', email: 'user@example.com', email_verified: true }
53
+ ```
54
+
55
+ ### Get Access Token
56
+
57
+ ```ts
58
+ const token = auth.getAccessToken();
59
+ // Use in Authorization header for your own API calls
60
+ ```
61
+
62
+ ### Logout
63
+
64
+ ```ts
65
+ // Revokes the session on the server, clears local token, redirects to postLogoutRedirectUri
66
+ await auth.logout();
67
+ ```
68
+
69
+ ## Config Options
70
+
71
+ | Option | Required | Default | Description |
72
+ |--------|----------|---------|-------------|
73
+ | `clientId` | Yes | — | OAuth client ID (from the accounts admin panel) |
74
+ | `redirectUri` | Yes | — | Your app's callback URL (must be registered in the OAuth client) |
75
+ | `accountsUrl` | Yes | — | Base URL of the accounts service |
76
+ | `postLogoutRedirectUri` | No | — | Where to redirect after logout. If not set, user stays on the accounts login page |
77
+ | `scopes` | No | `['openid', 'email']` | OAuth scopes to request |
78
+ | `apiPathPrefix` | No | `'/api/proxy'` | API path prefix. Use `''` if connecting directly to the API |
79
+ | `storagePrefix` | No | `'yaotoshi_auth'` | Prefix for localStorage/sessionStorage keys |
80
+
81
+ ## Setup
82
+
83
+ Before using the SDK, you need an OAuth client registered in the accounts service:
84
+
85
+ 1. Open the accounts admin panel
86
+ 2. Go to **Admin > Clients**
87
+ 3. Create a new client with:
88
+ - **Type**: `PUBLIC`
89
+ - **Redirect URIs**: your callback URL (e.g. `http://localhost:3000/callback`)
90
+ - **Post-Logout Redirect URIs**: your app URL (e.g. `http://localhost:3000`)
91
+ 4. Copy the **Client ID**
92
+
93
+ ## React Example
94
+
95
+ ```tsx
96
+ import { YaotoshiAuth } from '@yaotoshi/auth-sdk';
97
+ import { useEffect, useState } from 'react';
98
+
99
+ const auth = new YaotoshiAuth({
100
+ clientId: 'your-client-id',
101
+ redirectUri: 'http://localhost:3000/callback',
102
+ postLogoutRedirectUri: 'http://localhost:3000',
103
+ accountsUrl: 'http://localhost:9999',
104
+ });
105
+
106
+ function LoginPage() {
107
+ return <button onClick={() => auth.login()}>Sign in</button>;
108
+ }
109
+
110
+ function CallbackPage() {
111
+ useEffect(() => {
112
+ auth.handleCallback()
113
+ .then(() => window.location.href = '/dashboard')
114
+ .catch(err => console.error('Login failed:', err));
115
+ }, []);
116
+ return <p>Signing you in...</p>;
117
+ }
118
+
119
+ function Dashboard() {
120
+ const [user, setUser] = useState(null);
121
+
122
+ useEffect(() => {
123
+ if (!auth.isAuthenticated()) {
124
+ window.location.href = '/login';
125
+ return;
126
+ }
127
+ auth.getUser().then(setUser);
128
+ }, []);
129
+
130
+ if (!user) return <p>Loading...</p>;
131
+ return (
132
+ <div>
133
+ <p>Welcome, {user.email}</p>
134
+ <button onClick={() => auth.logout()}>Sign out</button>
135
+ </div>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ## Connecting Directly to the API
141
+
142
+ By default, the SDK sends requests through a Next.js proxy at `/api/proxy/*`. If your app connects directly to the accounts API:
143
+
144
+ ```ts
145
+ const auth = new YaotoshiAuth({
146
+ clientId: 'your-client-id',
147
+ redirectUri: 'http://localhost:3000/callback',
148
+ accountsUrl: 'http://localhost:9998', // direct API URL
149
+ apiPathPrefix: '', // no proxy
150
+ });
151
+ ```
152
+
153
+ ## Security
154
+
155
+ - Tokens are stored in `localStorage` (same approach as Auth0, Firebase)
156
+ - PKCE (S256) is used for all OAuth flows
157
+ - Token expiry is tracked — `isAuthenticated()` returns `false` when expired
158
+ - Mitigate XSS risk with a strong `Content-Security-Policy` header on your app
159
+
160
+ ## Browser Only
161
+
162
+ This SDK requires `window`, `localStorage`, `sessionStorage`, and `crypto.subtle`. It is designed for browser environments only.
163
+
164
+ ## Links
165
+
166
+ - [GitHub](https://github.com/onchainyaotoshi/accounts)
167
+ - [npm](https://www.npmjs.com/package/@yaotoshi/auth-sdk)
168
+
169
+ ## License
170
+
171
+ MIT
package/dist/index.cjs CHANGED
@@ -81,31 +81,19 @@ var AuthStorage = class {
81
81
  }
82
82
  }
83
83
  getPersistent(name) {
84
- try {
85
- return localStorage.getItem(this.key(name));
86
- } catch {
87
- return null;
88
- }
84
+ return this.get(name);
89
85
  }
90
86
  setPersistent(name, value) {
91
- try {
92
- localStorage.setItem(this.key(name), value);
93
- } catch {
94
- }
87
+ this.set(name, value);
95
88
  }
96
89
  removePersistent(name) {
97
- try {
98
- localStorage.removeItem(this.key(name));
99
- } catch {
100
- }
90
+ this.remove(name);
101
91
  }
102
92
  clearAll() {
103
93
  try {
104
94
  const prefix = this.prefix + "_";
105
- for (const store of [sessionStorage, localStorage]) {
106
- const keys = Object.keys(store).filter((k) => k.startsWith(prefix));
107
- keys.forEach((k) => store.removeItem(k));
108
- }
95
+ const keys = Object.keys(sessionStorage).filter((k) => k.startsWith(prefix));
96
+ keys.forEach((k) => sessionStorage.removeItem(k));
109
97
  } catch {
110
98
  }
111
99
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["export { YaotoshiAuth } from './client';\nexport type { YaotoshiAuthConfig, TokenResponse, UserInfo, AuthResult } from './types';\n","function generateRandomBytes(length: number): Uint8Array {\n const array = new Uint8Array(length);\n crypto.getRandomValues(array);\n return array;\n}\n\nfunction base64UrlEncode(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport function generateCodeVerifier(): string {\n const bytes = generateRandomBytes(32);\n return base64UrlEncode(bytes);\n}\n\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(digest);\n}\n\nexport function generateState(): string {\n const bytes = generateRandomBytes(16);\n return base64UrlEncode(bytes);\n}\n","export class AuthStorage {\n private prefix: string;\n\n constructor(prefix = 'yaotoshi_auth') {\n this.prefix = prefix;\n }\n\n private key(name: string): string {\n return `${this.prefix}_${name}`;\n }\n\n get(name: string): string | null {\n try {\n return sessionStorage.getItem(this.key(name));\n } catch {\n return null;\n }\n }\n\n set(name: string, value: string): void {\n try {\n sessionStorage.setItem(this.key(name), value);\n } catch {\n // Storage unavailable\n }\n }\n\n remove(name: string): void {\n try {\n sessionStorage.removeItem(this.key(name));\n } catch {\n // Storage unavailable\n }\n }\n\n getPersistent(name: string): string | null {\n try {\n return localStorage.getItem(this.key(name));\n } catch {\n return null;\n }\n }\n\n setPersistent(name: string, value: string): void {\n try {\n localStorage.setItem(this.key(name), value);\n } catch {\n // Storage unavailable\n }\n }\n\n removePersistent(name: string): void {\n try {\n localStorage.removeItem(this.key(name));\n } catch {\n // Storage unavailable\n }\n }\n\n clearAll(): void {\n try {\n const prefix = this.prefix + '_';\n for (const store of [sessionStorage, localStorage]) {\n const keys = Object.keys(store).filter(k => k.startsWith(prefix));\n keys.forEach(k => store.removeItem(k));\n }\n } catch {\n // Storage unavailable\n }\n }\n}\n","import { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce';\nimport { AuthStorage } from './storage';\nimport type { YaotoshiAuthConfig, TokenResponse, UserInfo, AuthResult } from './types';\n\nexport class YaotoshiAuth {\n private config: Required<Pick<YaotoshiAuthConfig, 'clientId' | 'redirectUri' | 'accountsUrl'>> &\n YaotoshiAuthConfig;\n private storage: AuthStorage;\n private processing = false;\n\n constructor(config: YaotoshiAuthConfig) {\n this.config = {\n scopes: ['openid', 'email'],\n postLogoutRedirectUri: undefined,\n storagePrefix: 'yaotoshi_auth',\n apiPathPrefix: '/api/proxy',\n ...config,\n };\n this.storage = new AuthStorage(this.config.storagePrefix);\n }\n\n private apiUrl(path: string): string {\n const prefix = this.config.apiPathPrefix ?? '/api/proxy';\n return `${this.config.accountsUrl}${prefix}${path}`;\n }\n\n async login(): Promise<void> {\n if (typeof window === 'undefined') {\n throw new Error('login() requires a browser environment');\n }\n\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n this.storage.set('code_verifier', codeVerifier);\n this.storage.set('state', state);\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n scope: this.config.scopes!.join(' '),\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n });\n\n window.location.href = `${this.config.accountsUrl}/authorize?${params.toString()}`;\n }\n\n async handleCallback(): Promise<AuthResult> {\n if (this.processing) {\n throw new Error('Callback is already being processed');\n }\n this.processing = true;\n\n try {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const state = params.get('state');\n const error = params.get('error');\n\n if (error) {\n const errorDescription = params.get('error_description');\n throw new Error(`Authorization error: ${error}${errorDescription ? ` — ${errorDescription}` : ''}`);\n }\n\n if (!code || !state) {\n throw new Error('Missing code or state in callback');\n }\n\n const savedState = this.storage.get('state');\n if (state !== savedState) {\n throw new Error('State mismatch — possible CSRF attack');\n }\n\n const codeVerifier = this.storage.get('code_verifier');\n if (!codeVerifier) {\n throw new Error('Missing code verifier — login flow may have been interrupted');\n }\n\n const tokenResponse = await fetch(this.apiUrl('/token'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({\n grant_type: 'authorization_code',\n code,\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!tokenResponse.ok) {\n const err = await tokenResponse.json().catch(() => ({}));\n const message = Array.isArray(err.message) ? err.message.join(', ') : (err.message || 'Token exchange failed');\n throw new Error(message);\n }\n\n const tokenData: TokenResponse = await tokenResponse.json();\n\n // Clean up PKCE state\n this.storage.remove('code_verifier');\n this.storage.remove('state');\n\n // Persist the access token\n this.storage.setPersistent('access_token', tokenData.access_token);\n this.storage.setPersistent('token_expires_at', String(Date.now() + tokenData.expires_in * 1000));\n\n // Fetch user info\n const user = await this.getUser(tokenData.access_token);\n\n return {\n accessToken: tokenData.access_token,\n scope: tokenData.scope,\n expiresIn: tokenData.expires_in,\n user,\n };\n } finally {\n this.processing = false;\n }\n }\n\n async getUser(token?: string): Promise<UserInfo> {\n const accessToken = token || this.getAccessToken();\n if (!accessToken) {\n throw new Error('No access token available');\n }\n\n const response = await fetch(this.apiUrl('/me'), {\n headers: { Authorization: `Bearer ${accessToken}` },\n credentials: 'include',\n });\n\n if (!response.ok) {\n if (response.status === 401) {\n this.storage.removePersistent('access_token');\n this.storage.removePersistent('token_expires_at');\n }\n throw new Error('Failed to fetch user info');\n }\n\n const data = await response.json();\n if (!data.sub || !data.email) {\n throw new Error('Invalid user info response');\n }\n\n return data;\n }\n\n async logout(): Promise<void> {\n if (typeof window === 'undefined') {\n throw new Error('logout() requires a browser environment');\n }\n\n const token = this.getAccessToken();\n\n try {\n await fetch(this.apiUrl('/logout'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({\n ...(token && { token }),\n ...(this.config.clientId && { client_id: this.config.clientId }),\n ...(this.config.postLogoutRedirectUri && { post_logout_redirect_uri: this.config.postLogoutRedirectUri }),\n }),\n });\n } finally {\n // Clear local state regardless of server response\n this.storage.clearAll();\n }\n\n // Redirect after successful logout\n if (this.config.postLogoutRedirectUri) {\n window.location.href = this.config.postLogoutRedirectUri;\n }\n }\n\n isAuthenticated(): boolean {\n const token = this.getAccessToken();\n if (!token) return false;\n\n const expiresAt = this.storage.getPersistent('token_expires_at');\n if (expiresAt && Date.now() > Number(expiresAt)) {\n this.storage.removePersistent('access_token');\n this.storage.removePersistent('token_expires_at');\n return false;\n }\n\n return true;\n }\n\n getAccessToken(): string | null {\n return this.storage.getPersistent('access_token');\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["export { YaotoshiAuth } from './client';\nexport type { YaotoshiAuthConfig, TokenResponse, UserInfo, AuthResult } from './types';\n","function generateRandomBytes(length: number): Uint8Array {\n const array = new Uint8Array(length);\n crypto.getRandomValues(array);\n return array;\n}\n\nfunction base64UrlEncode(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport function generateCodeVerifier(): string {\n const bytes = generateRandomBytes(32);\n return base64UrlEncode(bytes);\n}\n\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(digest);\n}\n\nexport function generateState(): string {\n const bytes = generateRandomBytes(16);\n return base64UrlEncode(bytes);\n}\n","export class AuthStorage {\n private prefix: string;\n\n constructor(prefix = 'yaotoshi_auth') {\n this.prefix = prefix;\n }\n\n private key(name: string): string {\n return `${this.prefix}_${name}`;\n }\n\n get(name: string): string | null {\n try {\n return sessionStorage.getItem(this.key(name));\n } catch {\n return null;\n }\n }\n\n set(name: string, value: string): void {\n try {\n sessionStorage.setItem(this.key(name), value);\n } catch {\n // Storage unavailable\n }\n }\n\n remove(name: string): void {\n try {\n sessionStorage.removeItem(this.key(name));\n } catch {\n // Storage unavailable\n }\n }\n\n getPersistent(name: string): string | null {\n return this.get(name);\n }\n\n setPersistent(name: string, value: string): void {\n this.set(name, value);\n }\n\n removePersistent(name: string): void {\n this.remove(name);\n }\n\n clearAll(): void {\n try {\n const prefix = this.prefix + '_';\n const keys = Object.keys(sessionStorage).filter(k => k.startsWith(prefix));\n keys.forEach(k => sessionStorage.removeItem(k));\n } catch {\n // Storage unavailable\n }\n }\n}\n","import { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce';\nimport { AuthStorage } from './storage';\nimport type { YaotoshiAuthConfig, TokenResponse, UserInfo, AuthResult } from './types';\n\nexport class YaotoshiAuth {\n private config: Required<Pick<YaotoshiAuthConfig, 'clientId' | 'redirectUri' | 'accountsUrl'>> &\n YaotoshiAuthConfig;\n private storage: AuthStorage;\n private processing = false;\n\n constructor(config: YaotoshiAuthConfig) {\n this.config = {\n scopes: ['openid', 'email'],\n postLogoutRedirectUri: undefined,\n storagePrefix: 'yaotoshi_auth',\n apiPathPrefix: '/api/proxy',\n ...config,\n };\n this.storage = new AuthStorage(this.config.storagePrefix);\n }\n\n private apiUrl(path: string): string {\n const prefix = this.config.apiPathPrefix ?? '/api/proxy';\n return `${this.config.accountsUrl}${prefix}${path}`;\n }\n\n async login(): Promise<void> {\n if (typeof window === 'undefined') {\n throw new Error('login() requires a browser environment');\n }\n\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n this.storage.set('code_verifier', codeVerifier);\n this.storage.set('state', state);\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n scope: this.config.scopes!.join(' '),\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n });\n\n window.location.href = `${this.config.accountsUrl}/authorize?${params.toString()}`;\n }\n\n async handleCallback(): Promise<AuthResult> {\n if (this.processing) {\n throw new Error('Callback is already being processed');\n }\n this.processing = true;\n\n try {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const state = params.get('state');\n const error = params.get('error');\n\n if (error) {\n const errorDescription = params.get('error_description');\n throw new Error(`Authorization error: ${error}${errorDescription ? ` — ${errorDescription}` : ''}`);\n }\n\n if (!code || !state) {\n throw new Error('Missing code or state in callback');\n }\n\n const savedState = this.storage.get('state');\n if (state !== savedState) {\n throw new Error('State mismatch — possible CSRF attack');\n }\n\n const codeVerifier = this.storage.get('code_verifier');\n if (!codeVerifier) {\n throw new Error('Missing code verifier — login flow may have been interrupted');\n }\n\n const tokenResponse = await fetch(this.apiUrl('/token'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({\n grant_type: 'authorization_code',\n code,\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!tokenResponse.ok) {\n const err = await tokenResponse.json().catch(() => ({}));\n const message = Array.isArray(err.message) ? err.message.join(', ') : (err.message || 'Token exchange failed');\n throw new Error(message);\n }\n\n const tokenData: TokenResponse = await tokenResponse.json();\n\n // Clean up PKCE state\n this.storage.remove('code_verifier');\n this.storage.remove('state');\n\n // Persist the access token\n this.storage.setPersistent('access_token', tokenData.access_token);\n this.storage.setPersistent('token_expires_at', String(Date.now() + tokenData.expires_in * 1000));\n\n // Fetch user info\n const user = await this.getUser(tokenData.access_token);\n\n return {\n accessToken: tokenData.access_token,\n scope: tokenData.scope,\n expiresIn: tokenData.expires_in,\n user,\n };\n } finally {\n this.processing = false;\n }\n }\n\n async getUser(token?: string): Promise<UserInfo> {\n const accessToken = token || this.getAccessToken();\n if (!accessToken) {\n throw new Error('No access token available');\n }\n\n const response = await fetch(this.apiUrl('/me'), {\n headers: { Authorization: `Bearer ${accessToken}` },\n credentials: 'include',\n });\n\n if (!response.ok) {\n if (response.status === 401) {\n this.storage.removePersistent('access_token');\n this.storage.removePersistent('token_expires_at');\n }\n throw new Error('Failed to fetch user info');\n }\n\n const data = await response.json();\n if (!data.sub || !data.email) {\n throw new Error('Invalid user info response');\n }\n\n return data;\n }\n\n async logout(): Promise<void> {\n if (typeof window === 'undefined') {\n throw new Error('logout() requires a browser environment');\n }\n\n const token = this.getAccessToken();\n\n try {\n await fetch(this.apiUrl('/logout'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({\n ...(token && { token }),\n ...(this.config.clientId && { client_id: this.config.clientId }),\n ...(this.config.postLogoutRedirectUri && { post_logout_redirect_uri: this.config.postLogoutRedirectUri }),\n }),\n });\n } finally {\n // Clear local state regardless of server response\n this.storage.clearAll();\n }\n\n // Redirect after successful logout\n if (this.config.postLogoutRedirectUri) {\n window.location.href = this.config.postLogoutRedirectUri;\n }\n }\n\n isAuthenticated(): boolean {\n const token = this.getAccessToken();\n if (!token) return false;\n\n const expiresAt = this.storage.getPersistent('token_expires_at');\n if (expiresAt && Date.now() > Number(expiresAt)) {\n this.storage.removePersistent('access_token');\n this.storage.removePersistent('token_expires_at');\n return false;\n }\n\n return true;\n }\n\n getAccessToken(): string | null {\n return this.storage.getPersistent('access_token');\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.js CHANGED
@@ -55,31 +55,19 @@ var AuthStorage = class {
55
55
  }
56
56
  }
57
57
  getPersistent(name) {
58
- try {
59
- return localStorage.getItem(this.key(name));
60
- } catch {
61
- return null;
62
- }
58
+ return this.get(name);
63
59
  }
64
60
  setPersistent(name, value) {
65
- try {
66
- localStorage.setItem(this.key(name), value);
67
- } catch {
68
- }
61
+ this.set(name, value);
69
62
  }
70
63
  removePersistent(name) {
71
- try {
72
- localStorage.removeItem(this.key(name));
73
- } catch {
74
- }
64
+ this.remove(name);
75
65
  }
76
66
  clearAll() {
77
67
  try {
78
68
  const prefix = this.prefix + "_";
79
- for (const store of [sessionStorage, localStorage]) {
80
- const keys = Object.keys(store).filter((k) => k.startsWith(prefix));
81
- keys.forEach((k) => store.removeItem(k));
82
- }
69
+ const keys = Object.keys(sessionStorage).filter((k) => k.startsWith(prefix));
70
+ keys.forEach((k) => sessionStorage.removeItem(k));
83
71
  } catch {
84
72
  }
85
73
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["function generateRandomBytes(length: number): Uint8Array {\n const array = new Uint8Array(length);\n crypto.getRandomValues(array);\n return array;\n}\n\nfunction base64UrlEncode(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport function generateCodeVerifier(): string {\n const bytes = generateRandomBytes(32);\n return base64UrlEncode(bytes);\n}\n\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(digest);\n}\n\nexport function generateState(): string {\n const bytes = generateRandomBytes(16);\n return base64UrlEncode(bytes);\n}\n","export class AuthStorage {\n private prefix: string;\n\n constructor(prefix = 'yaotoshi_auth') {\n this.prefix = prefix;\n }\n\n private key(name: string): string {\n return `${this.prefix}_${name}`;\n }\n\n get(name: string): string | null {\n try {\n return sessionStorage.getItem(this.key(name));\n } catch {\n return null;\n }\n }\n\n set(name: string, value: string): void {\n try {\n sessionStorage.setItem(this.key(name), value);\n } catch {\n // Storage unavailable\n }\n }\n\n remove(name: string): void {\n try {\n sessionStorage.removeItem(this.key(name));\n } catch {\n // Storage unavailable\n }\n }\n\n getPersistent(name: string): string | null {\n try {\n return localStorage.getItem(this.key(name));\n } catch {\n return null;\n }\n }\n\n setPersistent(name: string, value: string): void {\n try {\n localStorage.setItem(this.key(name), value);\n } catch {\n // Storage unavailable\n }\n }\n\n removePersistent(name: string): void {\n try {\n localStorage.removeItem(this.key(name));\n } catch {\n // Storage unavailable\n }\n }\n\n clearAll(): void {\n try {\n const prefix = this.prefix + '_';\n for (const store of [sessionStorage, localStorage]) {\n const keys = Object.keys(store).filter(k => k.startsWith(prefix));\n keys.forEach(k => store.removeItem(k));\n }\n } catch {\n // Storage unavailable\n }\n }\n}\n","import { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce';\nimport { AuthStorage } from './storage';\nimport type { YaotoshiAuthConfig, TokenResponse, UserInfo, AuthResult } from './types';\n\nexport class YaotoshiAuth {\n private config: Required<Pick<YaotoshiAuthConfig, 'clientId' | 'redirectUri' | 'accountsUrl'>> &\n YaotoshiAuthConfig;\n private storage: AuthStorage;\n private processing = false;\n\n constructor(config: YaotoshiAuthConfig) {\n this.config = {\n scopes: ['openid', 'email'],\n postLogoutRedirectUri: undefined,\n storagePrefix: 'yaotoshi_auth',\n apiPathPrefix: '/api/proxy',\n ...config,\n };\n this.storage = new AuthStorage(this.config.storagePrefix);\n }\n\n private apiUrl(path: string): string {\n const prefix = this.config.apiPathPrefix ?? '/api/proxy';\n return `${this.config.accountsUrl}${prefix}${path}`;\n }\n\n async login(): Promise<void> {\n if (typeof window === 'undefined') {\n throw new Error('login() requires a browser environment');\n }\n\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n this.storage.set('code_verifier', codeVerifier);\n this.storage.set('state', state);\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n scope: this.config.scopes!.join(' '),\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n });\n\n window.location.href = `${this.config.accountsUrl}/authorize?${params.toString()}`;\n }\n\n async handleCallback(): Promise<AuthResult> {\n if (this.processing) {\n throw new Error('Callback is already being processed');\n }\n this.processing = true;\n\n try {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const state = params.get('state');\n const error = params.get('error');\n\n if (error) {\n const errorDescription = params.get('error_description');\n throw new Error(`Authorization error: ${error}${errorDescription ? ` — ${errorDescription}` : ''}`);\n }\n\n if (!code || !state) {\n throw new Error('Missing code or state in callback');\n }\n\n const savedState = this.storage.get('state');\n if (state !== savedState) {\n throw new Error('State mismatch — possible CSRF attack');\n }\n\n const codeVerifier = this.storage.get('code_verifier');\n if (!codeVerifier) {\n throw new Error('Missing code verifier — login flow may have been interrupted');\n }\n\n const tokenResponse = await fetch(this.apiUrl('/token'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({\n grant_type: 'authorization_code',\n code,\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!tokenResponse.ok) {\n const err = await tokenResponse.json().catch(() => ({}));\n const message = Array.isArray(err.message) ? err.message.join(', ') : (err.message || 'Token exchange failed');\n throw new Error(message);\n }\n\n const tokenData: TokenResponse = await tokenResponse.json();\n\n // Clean up PKCE state\n this.storage.remove('code_verifier');\n this.storage.remove('state');\n\n // Persist the access token\n this.storage.setPersistent('access_token', tokenData.access_token);\n this.storage.setPersistent('token_expires_at', String(Date.now() + tokenData.expires_in * 1000));\n\n // Fetch user info\n const user = await this.getUser(tokenData.access_token);\n\n return {\n accessToken: tokenData.access_token,\n scope: tokenData.scope,\n expiresIn: tokenData.expires_in,\n user,\n };\n } finally {\n this.processing = false;\n }\n }\n\n async getUser(token?: string): Promise<UserInfo> {\n const accessToken = token || this.getAccessToken();\n if (!accessToken) {\n throw new Error('No access token available');\n }\n\n const response = await fetch(this.apiUrl('/me'), {\n headers: { Authorization: `Bearer ${accessToken}` },\n credentials: 'include',\n });\n\n if (!response.ok) {\n if (response.status === 401) {\n this.storage.removePersistent('access_token');\n this.storage.removePersistent('token_expires_at');\n }\n throw new Error('Failed to fetch user info');\n }\n\n const data = await response.json();\n if (!data.sub || !data.email) {\n throw new Error('Invalid user info response');\n }\n\n return data;\n }\n\n async logout(): Promise<void> {\n if (typeof window === 'undefined') {\n throw new Error('logout() requires a browser environment');\n }\n\n const token = this.getAccessToken();\n\n try {\n await fetch(this.apiUrl('/logout'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({\n ...(token && { token }),\n ...(this.config.clientId && { client_id: this.config.clientId }),\n ...(this.config.postLogoutRedirectUri && { post_logout_redirect_uri: this.config.postLogoutRedirectUri }),\n }),\n });\n } finally {\n // Clear local state regardless of server response\n this.storage.clearAll();\n }\n\n // Redirect after successful logout\n if (this.config.postLogoutRedirectUri) {\n window.location.href = this.config.postLogoutRedirectUri;\n }\n }\n\n isAuthenticated(): boolean {\n const token = this.getAccessToken();\n if (!token) return false;\n\n const expiresAt = this.storage.getPersistent('token_expires_at');\n if (expiresAt && Date.now() > Number(expiresAt)) {\n this.storage.removePersistent('access_token');\n this.storage.removePersistent('token_expires_at');\n return false;\n }\n\n return true;\n }\n\n getAccessToken(): string | null {\n return this.storage.getPersistent('access_token');\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/pkce.ts","../src/storage.ts","../src/client.ts"],"sourcesContent":["function generateRandomBytes(length: number): Uint8Array {\n const array = new Uint8Array(length);\n crypto.getRandomValues(array);\n return array;\n}\n\nfunction base64UrlEncode(buffer: ArrayBuffer): string {\n const bytes = new Uint8Array(buffer);\n let binary = '';\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport function generateCodeVerifier(): string {\n const bytes = generateRandomBytes(32);\n return base64UrlEncode(bytes);\n}\n\nexport async function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder();\n const data = encoder.encode(verifier);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return base64UrlEncode(digest);\n}\n\nexport function generateState(): string {\n const bytes = generateRandomBytes(16);\n return base64UrlEncode(bytes);\n}\n","export class AuthStorage {\n private prefix: string;\n\n constructor(prefix = 'yaotoshi_auth') {\n this.prefix = prefix;\n }\n\n private key(name: string): string {\n return `${this.prefix}_${name}`;\n }\n\n get(name: string): string | null {\n try {\n return sessionStorage.getItem(this.key(name));\n } catch {\n return null;\n }\n }\n\n set(name: string, value: string): void {\n try {\n sessionStorage.setItem(this.key(name), value);\n } catch {\n // Storage unavailable\n }\n }\n\n remove(name: string): void {\n try {\n sessionStorage.removeItem(this.key(name));\n } catch {\n // Storage unavailable\n }\n }\n\n getPersistent(name: string): string | null {\n return this.get(name);\n }\n\n setPersistent(name: string, value: string): void {\n this.set(name, value);\n }\n\n removePersistent(name: string): void {\n this.remove(name);\n }\n\n clearAll(): void {\n try {\n const prefix = this.prefix + '_';\n const keys = Object.keys(sessionStorage).filter(k => k.startsWith(prefix));\n keys.forEach(k => sessionStorage.removeItem(k));\n } catch {\n // Storage unavailable\n }\n }\n}\n","import { generateCodeVerifier, generateCodeChallenge, generateState } from './pkce';\nimport { AuthStorage } from './storage';\nimport type { YaotoshiAuthConfig, TokenResponse, UserInfo, AuthResult } from './types';\n\nexport class YaotoshiAuth {\n private config: Required<Pick<YaotoshiAuthConfig, 'clientId' | 'redirectUri' | 'accountsUrl'>> &\n YaotoshiAuthConfig;\n private storage: AuthStorage;\n private processing = false;\n\n constructor(config: YaotoshiAuthConfig) {\n this.config = {\n scopes: ['openid', 'email'],\n postLogoutRedirectUri: undefined,\n storagePrefix: 'yaotoshi_auth',\n apiPathPrefix: '/api/proxy',\n ...config,\n };\n this.storage = new AuthStorage(this.config.storagePrefix);\n }\n\n private apiUrl(path: string): string {\n const prefix = this.config.apiPathPrefix ?? '/api/proxy';\n return `${this.config.accountsUrl}${prefix}${path}`;\n }\n\n async login(): Promise<void> {\n if (typeof window === 'undefined') {\n throw new Error('login() requires a browser environment');\n }\n\n const codeVerifier = generateCodeVerifier();\n const codeChallenge = await generateCodeChallenge(codeVerifier);\n const state = generateState();\n\n this.storage.set('code_verifier', codeVerifier);\n this.storage.set('state', state);\n\n const params = new URLSearchParams({\n response_type: 'code',\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n scope: this.config.scopes!.join(' '),\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n });\n\n window.location.href = `${this.config.accountsUrl}/authorize?${params.toString()}`;\n }\n\n async handleCallback(): Promise<AuthResult> {\n if (this.processing) {\n throw new Error('Callback is already being processed');\n }\n this.processing = true;\n\n try {\n const params = new URLSearchParams(window.location.search);\n const code = params.get('code');\n const state = params.get('state');\n const error = params.get('error');\n\n if (error) {\n const errorDescription = params.get('error_description');\n throw new Error(`Authorization error: ${error}${errorDescription ? ` — ${errorDescription}` : ''}`);\n }\n\n if (!code || !state) {\n throw new Error('Missing code or state in callback');\n }\n\n const savedState = this.storage.get('state');\n if (state !== savedState) {\n throw new Error('State mismatch — possible CSRF attack');\n }\n\n const codeVerifier = this.storage.get('code_verifier');\n if (!codeVerifier) {\n throw new Error('Missing code verifier — login flow may have been interrupted');\n }\n\n const tokenResponse = await fetch(this.apiUrl('/token'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({\n grant_type: 'authorization_code',\n code,\n client_id: this.config.clientId,\n redirect_uri: this.config.redirectUri,\n code_verifier: codeVerifier,\n }),\n });\n\n if (!tokenResponse.ok) {\n const err = await tokenResponse.json().catch(() => ({}));\n const message = Array.isArray(err.message) ? err.message.join(', ') : (err.message || 'Token exchange failed');\n throw new Error(message);\n }\n\n const tokenData: TokenResponse = await tokenResponse.json();\n\n // Clean up PKCE state\n this.storage.remove('code_verifier');\n this.storage.remove('state');\n\n // Persist the access token\n this.storage.setPersistent('access_token', tokenData.access_token);\n this.storage.setPersistent('token_expires_at', String(Date.now() + tokenData.expires_in * 1000));\n\n // Fetch user info\n const user = await this.getUser(tokenData.access_token);\n\n return {\n accessToken: tokenData.access_token,\n scope: tokenData.scope,\n expiresIn: tokenData.expires_in,\n user,\n };\n } finally {\n this.processing = false;\n }\n }\n\n async getUser(token?: string): Promise<UserInfo> {\n const accessToken = token || this.getAccessToken();\n if (!accessToken) {\n throw new Error('No access token available');\n }\n\n const response = await fetch(this.apiUrl('/me'), {\n headers: { Authorization: `Bearer ${accessToken}` },\n credentials: 'include',\n });\n\n if (!response.ok) {\n if (response.status === 401) {\n this.storage.removePersistent('access_token');\n this.storage.removePersistent('token_expires_at');\n }\n throw new Error('Failed to fetch user info');\n }\n\n const data = await response.json();\n if (!data.sub || !data.email) {\n throw new Error('Invalid user info response');\n }\n\n return data;\n }\n\n async logout(): Promise<void> {\n if (typeof window === 'undefined') {\n throw new Error('logout() requires a browser environment');\n }\n\n const token = this.getAccessToken();\n\n try {\n await fetch(this.apiUrl('/logout'), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({\n ...(token && { token }),\n ...(this.config.clientId && { client_id: this.config.clientId }),\n ...(this.config.postLogoutRedirectUri && { post_logout_redirect_uri: this.config.postLogoutRedirectUri }),\n }),\n });\n } finally {\n // Clear local state regardless of server response\n this.storage.clearAll();\n }\n\n // Redirect after successful logout\n if (this.config.postLogoutRedirectUri) {\n window.location.href = this.config.postLogoutRedirectUri;\n }\n }\n\n isAuthenticated(): boolean {\n const token = this.getAccessToken();\n if (!token) return false;\n\n const expiresAt = this.storage.getPersistent('token_expires_at');\n if (expiresAt && Date.now() > Number(expiresAt)) {\n this.storage.removePersistent('access_token');\n this.storage.removePersistent('token_expires_at');\n return false;\n }\n\n return true;\n }\n\n getAccessToken(): string | null {\n return this.storage.getPersistent('access_token');\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaotoshi/auth-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Authentication SDK for Yaotoshi ecosystem apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -17,7 +17,8 @@
17
17
  }
18
18
  },
19
19
  "files": [
20
- "dist"
20
+ "dist",
21
+ "README.md"
21
22
  ],
22
23
  "scripts": {
23
24
  "build": "tsup",