@yaotoshi/auth-sdk 0.1.1 → 0.2.1

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/dist/index.cjs CHANGED
@@ -100,24 +100,38 @@ var AuthStorage = class {
100
100
  }
101
101
  }
102
102
  clearAll() {
103
- this.remove("code_verifier");
104
- this.remove("state");
105
- this.removePersistent("access_token");
103
+ try {
104
+ 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
+ }
109
+ } catch {
110
+ }
106
111
  }
107
112
  };
108
113
 
109
114
  // src/client.ts
110
115
  var YaotoshiAuth = class {
111
116
  constructor(config) {
117
+ this.processing = false;
112
118
  this.config = {
113
119
  scopes: ["openid", "email"],
114
120
  postLogoutRedirectUri: void 0,
115
121
  storagePrefix: "yaotoshi_auth",
122
+ apiPathPrefix: "/api/proxy",
116
123
  ...config
117
124
  };
118
125
  this.storage = new AuthStorage(this.config.storagePrefix);
119
126
  }
127
+ apiUrl(path) {
128
+ const prefix = this.config.apiPathPrefix ?? "/api/proxy";
129
+ return `${this.config.accountsUrl}${prefix}${path}`;
130
+ }
120
131
  async login() {
132
+ if (typeof window === "undefined") {
133
+ throw new Error("login() requires a browser environment");
134
+ }
121
135
  const codeVerifier = generateCodeVerifier();
122
136
  const codeChallenge = await generateCodeChallenge(codeVerifier);
123
137
  const state = generateState();
@@ -127,7 +141,7 @@ var YaotoshiAuth = class {
127
141
  response_type: "code",
128
142
  client_id: this.config.clientId,
129
143
  redirect_uri: this.config.redirectUri,
130
- scope: (this.config.scopes ?? ["openid", "email"]).join(" "),
144
+ scope: this.config.scopes.join(" "),
131
145
  state,
132
146
  code_challenge: codeChallenge,
133
147
  code_challenge_method: "S256"
@@ -135,75 +149,118 @@ var YaotoshiAuth = class {
135
149
  window.location.href = `${this.config.accountsUrl}/authorize?${params.toString()}`;
136
150
  }
137
151
  async handleCallback() {
138
- const params = new URLSearchParams(window.location.search);
139
- const code = params.get("code");
140
- const state = params.get("state");
141
- const error = params.get("error");
142
- if (error) {
143
- throw new Error(`Authorization error: ${error}`);
144
- }
145
- if (!code || !state) {
146
- throw new Error("Missing code or state in callback");
147
- }
148
- const savedState = this.storage.get("state");
149
- if (state !== savedState) {
150
- throw new Error("State mismatch \u2014 possible CSRF attack");
151
- }
152
- const codeVerifier = this.storage.get("code_verifier");
153
- if (!codeVerifier) {
154
- throw new Error("Missing code verifier \u2014 login flow may have been interrupted");
155
- }
156
- const tokenResponse = await fetch(`${this.config.accountsUrl}/api/proxy/token`, {
157
- method: "POST",
158
- headers: { "Content-Type": "application/json" },
159
- body: JSON.stringify({
160
- grant_type: "authorization_code",
161
- code,
162
- client_id: this.config.clientId,
163
- redirect_uri: this.config.redirectUri,
164
- code_verifier: codeVerifier
165
- })
166
- });
167
- if (!tokenResponse.ok) {
168
- const err = await tokenResponse.json().catch(() => ({}));
169
- throw new Error(err.message || "Token exchange failed");
152
+ if (this.processing) {
153
+ throw new Error("Callback is already being processed");
154
+ }
155
+ this.processing = true;
156
+ try {
157
+ const params = new URLSearchParams(window.location.search);
158
+ const code = params.get("code");
159
+ const state = params.get("state");
160
+ const error = params.get("error");
161
+ if (error) {
162
+ const errorDescription = params.get("error_description");
163
+ throw new Error(`Authorization error: ${error}${errorDescription ? ` \u2014 ${errorDescription}` : ""}`);
164
+ }
165
+ if (!code || !state) {
166
+ throw new Error("Missing code or state in callback");
167
+ }
168
+ const savedState = this.storage.get("state");
169
+ if (state !== savedState) {
170
+ throw new Error("State mismatch \u2014 possible CSRF attack");
171
+ }
172
+ const codeVerifier = this.storage.get("code_verifier");
173
+ if (!codeVerifier) {
174
+ throw new Error("Missing code verifier \u2014 login flow may have been interrupted");
175
+ }
176
+ const tokenResponse = await fetch(this.apiUrl("/token"), {
177
+ method: "POST",
178
+ headers: { "Content-Type": "application/json" },
179
+ credentials: "include",
180
+ body: JSON.stringify({
181
+ grant_type: "authorization_code",
182
+ code,
183
+ client_id: this.config.clientId,
184
+ redirect_uri: this.config.redirectUri,
185
+ code_verifier: codeVerifier
186
+ })
187
+ });
188
+ if (!tokenResponse.ok) {
189
+ const err = await tokenResponse.json().catch(() => ({}));
190
+ const message = Array.isArray(err.message) ? err.message.join(", ") : err.message || "Token exchange failed";
191
+ throw new Error(message);
192
+ }
193
+ const tokenData = await tokenResponse.json();
194
+ this.storage.remove("code_verifier");
195
+ this.storage.remove("state");
196
+ this.storage.setPersistent("access_token", tokenData.access_token);
197
+ this.storage.setPersistent("token_expires_at", String(Date.now() + tokenData.expires_in * 1e3));
198
+ const user = await this.getUser(tokenData.access_token);
199
+ return {
200
+ accessToken: tokenData.access_token,
201
+ scope: tokenData.scope,
202
+ expiresIn: tokenData.expires_in,
203
+ user
204
+ };
205
+ } finally {
206
+ this.processing = false;
170
207
  }
171
- const tokenData = await tokenResponse.json();
172
- this.storage.remove("code_verifier");
173
- this.storage.remove("state");
174
- this.storage.setPersistent("access_token", tokenData.access_token);
175
- const user = await this.getUser(tokenData.access_token);
176
- return { accessToken: tokenData.access_token, user };
177
208
  }
178
209
  async getUser(token) {
179
210
  const accessToken = token || this.getAccessToken();
180
211
  if (!accessToken) {
181
212
  throw new Error("No access token available");
182
213
  }
183
- const response = await fetch(`${this.config.accountsUrl}/api/proxy/me`, {
184
- headers: { Authorization: `Bearer ${accessToken}` }
214
+ const response = await fetch(this.apiUrl("/me"), {
215
+ headers: { Authorization: `Bearer ${accessToken}` },
216
+ credentials: "include"
185
217
  });
186
218
  if (!response.ok) {
187
219
  if (response.status === 401) {
188
220
  this.storage.removePersistent("access_token");
221
+ this.storage.removePersistent("token_expires_at");
189
222
  }
190
223
  throw new Error("Failed to fetch user info");
191
224
  }
192
- return response.json();
225
+ const data = await response.json();
226
+ if (!data.sub || !data.email) {
227
+ throw new Error("Invalid user info response");
228
+ }
229
+ return data;
193
230
  }
194
- logout() {
231
+ async logout() {
232
+ if (typeof window === "undefined") {
233
+ throw new Error("logout() requires a browser environment");
234
+ }
195
235
  const token = this.getAccessToken();
196
- this.storage.clearAll();
197
- const params = new URLSearchParams();
198
- if (token) params.set("token", token);
199
- if (this.config.clientId) params.set("client_id", this.config.clientId);
236
+ try {
237
+ await fetch(this.apiUrl("/logout"), {
238
+ method: "POST",
239
+ headers: { "Content-Type": "application/json" },
240
+ credentials: "include",
241
+ body: JSON.stringify({
242
+ ...token && { token },
243
+ ...this.config.clientId && { client_id: this.config.clientId },
244
+ ...this.config.postLogoutRedirectUri && { post_logout_redirect_uri: this.config.postLogoutRedirectUri }
245
+ })
246
+ });
247
+ } finally {
248
+ this.storage.clearAll();
249
+ }
200
250
  if (this.config.postLogoutRedirectUri) {
201
- params.set("post_logout_redirect_uri", this.config.postLogoutRedirectUri);
251
+ window.location.href = this.config.postLogoutRedirectUri;
202
252
  }
203
- window.location.href = `${this.config.accountsUrl}/api/proxy/logout?${params.toString()}`;
204
253
  }
205
254
  isAuthenticated() {
206
- return !!this.getAccessToken();
255
+ const token = this.getAccessToken();
256
+ if (!token) return false;
257
+ const expiresAt = this.storage.getPersistent("token_expires_at");
258
+ if (expiresAt && Date.now() > Number(expiresAt)) {
259
+ this.storage.removePersistent("access_token");
260
+ this.storage.removePersistent("token_expires_at");
261
+ return false;
262
+ }
263
+ return true;
207
264
  }
208
265
  getAccessToken() {
209
266
  return this.storage.getPersistent("access_token");
@@ -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 this.remove('code_verifier');\n this.remove('state');\n this.removePersistent('access_token');\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\n constructor(config: YaotoshiAuthConfig) {\n this.config = {\n scopes: ['openid', 'email'],\n postLogoutRedirectUri: undefined,\n storagePrefix: 'yaotoshi_auth',\n ...config,\n };\n this.storage = new AuthStorage(this.config.storagePrefix);\n }\n\n async login(): Promise<void> {\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 ?? ['openid', 'email']).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 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 throw new Error(`Authorization error: ${error}`);\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 // Exchange code for token\n const tokenResponse = await fetch(`${this.config.accountsUrl}/api/proxy/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\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 throw new Error(err.message || 'Token exchange failed');\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\n // Fetch user info\n const user = await this.getUser(tokenData.access_token);\n\n return { accessToken: tokenData.access_token, user };\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.config.accountsUrl}/api/proxy/me`, {\n headers: { Authorization: `Bearer ${accessToken}` },\n });\n\n if (!response.ok) {\n if (response.status === 401) {\n this.storage.removePersistent('access_token');\n }\n throw new Error('Failed to fetch user info');\n }\n\n return response.json();\n }\n\n logout(): void {\n const token = this.getAccessToken();\n this.storage.clearAll();\n\n const params = new URLSearchParams();\n if (token) params.set('token', token);\n if (this.config.clientId) params.set('client_id', this.config.clientId);\n if (this.config.postLogoutRedirectUri) {\n params.set('post_logout_redirect_uri', this.config.postLogoutRedirectUri);\n }\n\n window.location.href = `${this.config.accountsUrl}/api/proxy/logout?${params.toString()}`;\n }\n\n isAuthenticated(): boolean {\n return !!this.getAccessToken();\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 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":[]}
package/dist/index.d.cts CHANGED
@@ -1,11 +1,22 @@
1
1
  /** Configuration options for the YaotoshiAuth client. */
2
2
  interface YaotoshiAuthConfig {
3
+ /** OAuth client ID */
3
4
  clientId: string;
5
+ /** OAuth redirect URI for callback */
4
6
  redirectUri: string;
7
+ /** URI to redirect to after logout */
5
8
  postLogoutRedirectUri?: string;
9
+ /** Base URL of the accounts service (e.g., "https://accounts.example.com") */
6
10
  accountsUrl: string;
11
+ /** OAuth scopes to request (default: ['openid', 'email']) */
7
12
  scopes?: string[];
13
+ /** Prefix for storage keys (default: 'yaotoshi_auth') */
8
14
  storagePrefix?: string;
15
+ /**
16
+ * API path prefix. Set to '/api/proxy' when using the Next.js proxy (default),
17
+ * or '' when connecting directly to the API.
18
+ */
19
+ apiPathPrefix?: string;
9
20
  }
10
21
  interface TokenResponse {
11
22
  access_token: string;
@@ -13,6 +24,7 @@ interface TokenResponse {
13
24
  expires_in: number;
14
25
  scope: string;
15
26
  }
27
+ /** User info returned by the /me endpoint. `sub` is the user's unique ID (cuid). */
16
28
  interface UserInfo {
17
29
  sub: string;
18
30
  email: string;
@@ -20,17 +32,23 @@ interface UserInfo {
20
32
  }
21
33
  interface AuthResult {
22
34
  accessToken: string;
35
+ /** Granted scopes (may differ from requested scopes) */
36
+ scope: string;
37
+ /** Token lifetime in seconds */
38
+ expiresIn: number;
23
39
  user: UserInfo;
24
40
  }
25
41
 
26
42
  declare class YaotoshiAuth {
27
43
  private config;
28
44
  private storage;
45
+ private processing;
29
46
  constructor(config: YaotoshiAuthConfig);
47
+ private apiUrl;
30
48
  login(): Promise<void>;
31
49
  handleCallback(): Promise<AuthResult>;
32
50
  getUser(token?: string): Promise<UserInfo>;
33
- logout(): void;
51
+ logout(): Promise<void>;
34
52
  isAuthenticated(): boolean;
35
53
  getAccessToken(): string | null;
36
54
  }
package/dist/index.d.ts CHANGED
@@ -1,11 +1,22 @@
1
1
  /** Configuration options for the YaotoshiAuth client. */
2
2
  interface YaotoshiAuthConfig {
3
+ /** OAuth client ID */
3
4
  clientId: string;
5
+ /** OAuth redirect URI for callback */
4
6
  redirectUri: string;
7
+ /** URI to redirect to after logout */
5
8
  postLogoutRedirectUri?: string;
9
+ /** Base URL of the accounts service (e.g., "https://accounts.example.com") */
6
10
  accountsUrl: string;
11
+ /** OAuth scopes to request (default: ['openid', 'email']) */
7
12
  scopes?: string[];
13
+ /** Prefix for storage keys (default: 'yaotoshi_auth') */
8
14
  storagePrefix?: string;
15
+ /**
16
+ * API path prefix. Set to '/api/proxy' when using the Next.js proxy (default),
17
+ * or '' when connecting directly to the API.
18
+ */
19
+ apiPathPrefix?: string;
9
20
  }
10
21
  interface TokenResponse {
11
22
  access_token: string;
@@ -13,6 +24,7 @@ interface TokenResponse {
13
24
  expires_in: number;
14
25
  scope: string;
15
26
  }
27
+ /** User info returned by the /me endpoint. `sub` is the user's unique ID (cuid). */
16
28
  interface UserInfo {
17
29
  sub: string;
18
30
  email: string;
@@ -20,17 +32,23 @@ interface UserInfo {
20
32
  }
21
33
  interface AuthResult {
22
34
  accessToken: string;
35
+ /** Granted scopes (may differ from requested scopes) */
36
+ scope: string;
37
+ /** Token lifetime in seconds */
38
+ expiresIn: number;
23
39
  user: UserInfo;
24
40
  }
25
41
 
26
42
  declare class YaotoshiAuth {
27
43
  private config;
28
44
  private storage;
45
+ private processing;
29
46
  constructor(config: YaotoshiAuthConfig);
47
+ private apiUrl;
30
48
  login(): Promise<void>;
31
49
  handleCallback(): Promise<AuthResult>;
32
50
  getUser(token?: string): Promise<UserInfo>;
33
- logout(): void;
51
+ logout(): Promise<void>;
34
52
  isAuthenticated(): boolean;
35
53
  getAccessToken(): string | null;
36
54
  }
package/dist/index.js CHANGED
@@ -74,24 +74,38 @@ var AuthStorage = class {
74
74
  }
75
75
  }
76
76
  clearAll() {
77
- this.remove("code_verifier");
78
- this.remove("state");
79
- this.removePersistent("access_token");
77
+ try {
78
+ 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
+ }
83
+ } catch {
84
+ }
80
85
  }
81
86
  };
82
87
 
83
88
  // src/client.ts
84
89
  var YaotoshiAuth = class {
85
90
  constructor(config) {
91
+ this.processing = false;
86
92
  this.config = {
87
93
  scopes: ["openid", "email"],
88
94
  postLogoutRedirectUri: void 0,
89
95
  storagePrefix: "yaotoshi_auth",
96
+ apiPathPrefix: "/api/proxy",
90
97
  ...config
91
98
  };
92
99
  this.storage = new AuthStorage(this.config.storagePrefix);
93
100
  }
101
+ apiUrl(path) {
102
+ const prefix = this.config.apiPathPrefix ?? "/api/proxy";
103
+ return `${this.config.accountsUrl}${prefix}${path}`;
104
+ }
94
105
  async login() {
106
+ if (typeof window === "undefined") {
107
+ throw new Error("login() requires a browser environment");
108
+ }
95
109
  const codeVerifier = generateCodeVerifier();
96
110
  const codeChallenge = await generateCodeChallenge(codeVerifier);
97
111
  const state = generateState();
@@ -101,7 +115,7 @@ var YaotoshiAuth = class {
101
115
  response_type: "code",
102
116
  client_id: this.config.clientId,
103
117
  redirect_uri: this.config.redirectUri,
104
- scope: (this.config.scopes ?? ["openid", "email"]).join(" "),
118
+ scope: this.config.scopes.join(" "),
105
119
  state,
106
120
  code_challenge: codeChallenge,
107
121
  code_challenge_method: "S256"
@@ -109,75 +123,118 @@ var YaotoshiAuth = class {
109
123
  window.location.href = `${this.config.accountsUrl}/authorize?${params.toString()}`;
110
124
  }
111
125
  async handleCallback() {
112
- const params = new URLSearchParams(window.location.search);
113
- const code = params.get("code");
114
- const state = params.get("state");
115
- const error = params.get("error");
116
- if (error) {
117
- throw new Error(`Authorization error: ${error}`);
118
- }
119
- if (!code || !state) {
120
- throw new Error("Missing code or state in callback");
121
- }
122
- const savedState = this.storage.get("state");
123
- if (state !== savedState) {
124
- throw new Error("State mismatch \u2014 possible CSRF attack");
125
- }
126
- const codeVerifier = this.storage.get("code_verifier");
127
- if (!codeVerifier) {
128
- throw new Error("Missing code verifier \u2014 login flow may have been interrupted");
129
- }
130
- const tokenResponse = await fetch(`${this.config.accountsUrl}/api/proxy/token`, {
131
- method: "POST",
132
- headers: { "Content-Type": "application/json" },
133
- body: JSON.stringify({
134
- grant_type: "authorization_code",
135
- code,
136
- client_id: this.config.clientId,
137
- redirect_uri: this.config.redirectUri,
138
- code_verifier: codeVerifier
139
- })
140
- });
141
- if (!tokenResponse.ok) {
142
- const err = await tokenResponse.json().catch(() => ({}));
143
- throw new Error(err.message || "Token exchange failed");
126
+ if (this.processing) {
127
+ throw new Error("Callback is already being processed");
128
+ }
129
+ this.processing = true;
130
+ try {
131
+ const params = new URLSearchParams(window.location.search);
132
+ const code = params.get("code");
133
+ const state = params.get("state");
134
+ const error = params.get("error");
135
+ if (error) {
136
+ const errorDescription = params.get("error_description");
137
+ throw new Error(`Authorization error: ${error}${errorDescription ? ` \u2014 ${errorDescription}` : ""}`);
138
+ }
139
+ if (!code || !state) {
140
+ throw new Error("Missing code or state in callback");
141
+ }
142
+ const savedState = this.storage.get("state");
143
+ if (state !== savedState) {
144
+ throw new Error("State mismatch \u2014 possible CSRF attack");
145
+ }
146
+ const codeVerifier = this.storage.get("code_verifier");
147
+ if (!codeVerifier) {
148
+ throw new Error("Missing code verifier \u2014 login flow may have been interrupted");
149
+ }
150
+ const tokenResponse = await fetch(this.apiUrl("/token"), {
151
+ method: "POST",
152
+ headers: { "Content-Type": "application/json" },
153
+ credentials: "include",
154
+ body: JSON.stringify({
155
+ grant_type: "authorization_code",
156
+ code,
157
+ client_id: this.config.clientId,
158
+ redirect_uri: this.config.redirectUri,
159
+ code_verifier: codeVerifier
160
+ })
161
+ });
162
+ if (!tokenResponse.ok) {
163
+ const err = await tokenResponse.json().catch(() => ({}));
164
+ const message = Array.isArray(err.message) ? err.message.join(", ") : err.message || "Token exchange failed";
165
+ throw new Error(message);
166
+ }
167
+ const tokenData = await tokenResponse.json();
168
+ this.storage.remove("code_verifier");
169
+ this.storage.remove("state");
170
+ this.storage.setPersistent("access_token", tokenData.access_token);
171
+ this.storage.setPersistent("token_expires_at", String(Date.now() + tokenData.expires_in * 1e3));
172
+ const user = await this.getUser(tokenData.access_token);
173
+ return {
174
+ accessToken: tokenData.access_token,
175
+ scope: tokenData.scope,
176
+ expiresIn: tokenData.expires_in,
177
+ user
178
+ };
179
+ } finally {
180
+ this.processing = false;
144
181
  }
145
- const tokenData = await tokenResponse.json();
146
- this.storage.remove("code_verifier");
147
- this.storage.remove("state");
148
- this.storage.setPersistent("access_token", tokenData.access_token);
149
- const user = await this.getUser(tokenData.access_token);
150
- return { accessToken: tokenData.access_token, user };
151
182
  }
152
183
  async getUser(token) {
153
184
  const accessToken = token || this.getAccessToken();
154
185
  if (!accessToken) {
155
186
  throw new Error("No access token available");
156
187
  }
157
- const response = await fetch(`${this.config.accountsUrl}/api/proxy/me`, {
158
- headers: { Authorization: `Bearer ${accessToken}` }
188
+ const response = await fetch(this.apiUrl("/me"), {
189
+ headers: { Authorization: `Bearer ${accessToken}` },
190
+ credentials: "include"
159
191
  });
160
192
  if (!response.ok) {
161
193
  if (response.status === 401) {
162
194
  this.storage.removePersistent("access_token");
195
+ this.storage.removePersistent("token_expires_at");
163
196
  }
164
197
  throw new Error("Failed to fetch user info");
165
198
  }
166
- return response.json();
199
+ const data = await response.json();
200
+ if (!data.sub || !data.email) {
201
+ throw new Error("Invalid user info response");
202
+ }
203
+ return data;
167
204
  }
168
- logout() {
205
+ async logout() {
206
+ if (typeof window === "undefined") {
207
+ throw new Error("logout() requires a browser environment");
208
+ }
169
209
  const token = this.getAccessToken();
170
- this.storage.clearAll();
171
- const params = new URLSearchParams();
172
- if (token) params.set("token", token);
173
- if (this.config.clientId) params.set("client_id", this.config.clientId);
210
+ try {
211
+ await fetch(this.apiUrl("/logout"), {
212
+ method: "POST",
213
+ headers: { "Content-Type": "application/json" },
214
+ credentials: "include",
215
+ body: JSON.stringify({
216
+ ...token && { token },
217
+ ...this.config.clientId && { client_id: this.config.clientId },
218
+ ...this.config.postLogoutRedirectUri && { post_logout_redirect_uri: this.config.postLogoutRedirectUri }
219
+ })
220
+ });
221
+ } finally {
222
+ this.storage.clearAll();
223
+ }
174
224
  if (this.config.postLogoutRedirectUri) {
175
- params.set("post_logout_redirect_uri", this.config.postLogoutRedirectUri);
225
+ window.location.href = this.config.postLogoutRedirectUri;
176
226
  }
177
- window.location.href = `${this.config.accountsUrl}/api/proxy/logout?${params.toString()}`;
178
227
  }
179
228
  isAuthenticated() {
180
- return !!this.getAccessToken();
229
+ const token = this.getAccessToken();
230
+ if (!token) return false;
231
+ const expiresAt = this.storage.getPersistent("token_expires_at");
232
+ if (expiresAt && Date.now() > Number(expiresAt)) {
233
+ this.storage.removePersistent("access_token");
234
+ this.storage.removePersistent("token_expires_at");
235
+ return false;
236
+ }
237
+ return true;
181
238
  }
182
239
  getAccessToken() {
183
240
  return this.storage.getPersistent("access_token");
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 this.remove('code_verifier');\n this.remove('state');\n this.removePersistent('access_token');\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\n constructor(config: YaotoshiAuthConfig) {\n this.config = {\n scopes: ['openid', 'email'],\n postLogoutRedirectUri: undefined,\n storagePrefix: 'yaotoshi_auth',\n ...config,\n };\n this.storage = new AuthStorage(this.config.storagePrefix);\n }\n\n async login(): Promise<void> {\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 ?? ['openid', 'email']).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 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 throw new Error(`Authorization error: ${error}`);\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 // Exchange code for token\n const tokenResponse = await fetch(`${this.config.accountsUrl}/api/proxy/token`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\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 throw new Error(err.message || 'Token exchange failed');\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\n // Fetch user info\n const user = await this.getUser(tokenData.access_token);\n\n return { accessToken: tokenData.access_token, user };\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.config.accountsUrl}/api/proxy/me`, {\n headers: { Authorization: `Bearer ${accessToken}` },\n });\n\n if (!response.ok) {\n if (response.status === 401) {\n this.storage.removePersistent('access_token');\n }\n throw new Error('Failed to fetch user info');\n }\n\n return response.json();\n }\n\n logout(): void {\n const token = this.getAccessToken();\n this.storage.clearAll();\n\n const params = new URLSearchParams();\n if (token) params.set('token', token);\n if (this.config.clientId) params.set('client_id', this.config.clientId);\n if (this.config.postLogoutRedirectUri) {\n params.set('post_logout_redirect_uri', this.config.postLogoutRedirectUri);\n }\n\n window.location.href = `${this.config.accountsUrl}/api/proxy/logout?${params.toString()}`;\n }\n\n isAuthenticated(): boolean {\n return !!this.getAccessToken();\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 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":[]}
package/package.json CHANGED
@@ -1,9 +1,11 @@
1
1
  {
2
2
  "name": "@yaotoshi/auth-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Authentication SDK for Yaotoshi ecosystem apps",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "browser": "./dist/index.js",
8
+ "sideEffects": false,
7
9
  "main": "./dist/index.cjs",
8
10
  "module": "./dist/index.js",
9
11
  "types": "./dist/index.d.ts",