@svadmin/sso 0.11.21 → 0.12.7

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.
@@ -1,23 +1,23 @@
1
1
  /**
2
- * 浏览器端 OIDC/OAuth2 认证辅助函数。
2
+ * Browser-side OIDC/OAuth2 authentication helpers.
3
3
  *
4
- * 提供 PKCE 生成、OIDC Discovery 端点解析、重定向校验、
5
- * 反向代理 origin 恢复等通用工具。
4
+ * Provides PKCE generation, OIDC Discovery endpoint resolution, redirect validation,
5
+ * and reverse-proxy origin recovery.
6
6
  */
7
- /** 生成随机的 OAuth state 值 */
7
+ /** Generates a random OAuth state value. */
8
8
  export declare function generateState(): string;
9
- /** 生成 PKCE code verifier */
9
+ /** Generates a PKCE code verifier. */
10
10
  export declare function generateVerifier(): string;
11
- /** 基于 code verifier 算得 code challenge (S256) */
11
+ /** Computes the S256 code challenge from a code verifier. */
12
12
  export declare function generateChallenge(verifier: string): Promise<string>;
13
13
  /**
14
- * 校验 returnTo 重定向地址,防止开放重定向漏洞。
15
- * 只允许同源相对路径(必须以 / 开头且非 // 开头)。
14
+ * Validates a returnTo redirect and prevents open-redirect vulnerabilities.
15
+ * Allows only same-origin relative paths (starting with / but not //).
16
16
  */
17
17
  export declare function isValidReturnTo(url: string): boolean;
18
- /** 从反向代理头恢复公网 origin */
18
+ /** Recovers the public origin from reverse-proxy headers. */
19
19
  export declare function getForwardedOrigin(req: Request, fallbackUrl: URL): string;
20
- /** 通过 OIDC Discovery 解析授权端点 URL */
20
+ /** Resolves the authorization endpoint URL through OIDC Discovery. */
21
21
  export declare function resolveAuthorizeUrl(issuer: string, overrideUrl?: string, fetcher?: typeof fetch): Promise<string>;
22
- /** 通过 OIDC Discovery 解析 token 端点 URL */
22
+ /** Resolves the token endpoint URL through OIDC Discovery. */
23
23
  export declare function resolveTokenUrl(issuer: string, overrideUrlOrFetcher?: string | typeof fetch, fetcherOverride?: typeof fetch): Promise<string>;
@@ -1,29 +1,29 @@
1
1
  /**
2
- * 浏览器端 OIDC/OAuth2 认证辅助函数。
2
+ * Browser-side OIDC/OAuth2 authentication helpers.
3
3
  *
4
- * 提供 PKCE 生成、OIDC Discovery 端点解析、重定向校验、
5
- * 反向代理 origin 恢复等通用工具。
4
+ * Provides PKCE generation, OIDC Discovery endpoint resolution, redirect validation,
5
+ * and reverse-proxy origin recovery.
6
6
  */
7
- /** 将 Uint8Array 编码为 base64url 格式 (纯 Web API) */
7
+ /** Encodes a Uint8Array as base64url using only Web APIs. */
8
8
  function base64url(array) {
9
9
  let binary = "";
10
10
  for (const byte of array)
11
11
  binary += String.fromCharCode(byte);
12
12
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
13
13
  }
14
- /** 生成随机的 OAuth state 值 */
14
+ /** Generates a random OAuth state value. */
15
15
  export function generateState() {
16
16
  const array = new Uint8Array(16);
17
17
  crypto.getRandomValues(array);
18
18
  return base64url(array);
19
19
  }
20
- /** 生成 PKCE code verifier */
20
+ /** Generates a PKCE code verifier. */
21
21
  export function generateVerifier() {
22
22
  const array = new Uint8Array(32);
23
23
  crypto.getRandomValues(array);
24
24
  return base64url(array);
25
25
  }
26
- /** 基于 code verifier 算得 code challenge (S256) */
26
+ /** Computes the S256 code challenge from a code verifier. */
27
27
  export async function generateChallenge(verifier) {
28
28
  const encoder = new TextEncoder();
29
29
  const data = encoder.encode(verifier);
@@ -31,8 +31,8 @@ export async function generateChallenge(verifier) {
31
31
  return base64url(new Uint8Array(hash));
32
32
  }
33
33
  /**
34
- * 校验 returnTo 重定向地址,防止开放重定向漏洞。
35
- * 只允许同源相对路径(必须以 / 开头且非 // 开头)。
34
+ * Validates a returnTo redirect and prevents open-redirect vulnerabilities.
35
+ * Allows only same-origin relative paths (starting with / but not //).
36
36
  */
37
37
  export function isValidReturnTo(url) {
38
38
  if (!url)
@@ -41,7 +41,7 @@ export function isValidReturnTo(url) {
41
41
  return false;
42
42
  return /^\/[^/].*/.test(url) || url === "/";
43
43
  }
44
- /** 从反向代理头恢复公网 origin */
44
+ /** Recovers the public origin from reverse-proxy headers. */
45
45
  export function getForwardedOrigin(req, fallbackUrl) {
46
46
  const forwardedProto = req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
47
47
  const forwardedHost = req.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
@@ -49,7 +49,7 @@ export function getForwardedOrigin(req, fallbackUrl) {
49
49
  const protocol = (forwardedProto || fallbackUrl.protocol.replace(/:$/, "")).replace(/:$/, "");
50
50
  return `${protocol}://${host}`;
51
51
  }
52
- /** 通过 OIDC Discovery 解析授权端点 URL */
52
+ /** Resolves the authorization endpoint URL through OIDC Discovery. */
53
53
  export async function resolveAuthorizeUrl(issuer, overrideUrl = "", fetcher = fetch) {
54
54
  const normalizedIssuer = issuer.replace(/\/$/, "");
55
55
  const normalizedOverride = overrideUrl.trim();
@@ -65,14 +65,14 @@ export async function resolveAuthorizeUrl(issuer, overrideUrl = "", fetcher = fe
65
65
  }
66
66
  }
67
67
  catch {
68
- // 忽略 discovery 失败,走 fallback
68
+ // Ignore discovery failures and use the fallback.
69
69
  }
70
70
  }
71
71
  if (normalizedIssuer)
72
72
  return `${normalizedIssuer}/oauth/authorize`;
73
73
  return "";
74
74
  }
75
- /** 通过 OIDC Discovery 解析 token 端点 URL */
75
+ /** Resolves the token endpoint URL through OIDC Discovery. */
76
76
  export async function resolveTokenUrl(issuer, overrideUrlOrFetcher = "", fetcherOverride = fetch) {
77
77
  const normalizedIssuer = issuer.replace(/\/$/, "");
78
78
  const fetcher = typeof overrideUrlOrFetcher === "function" ? overrideUrlOrFetcher : fetcherOverride;
@@ -91,7 +91,7 @@ export async function resolveTokenUrl(issuer, overrideUrlOrFetcher = "", fetcher
91
91
  }
92
92
  }
93
93
  catch {
94
- // 忽略 discovery 失败
94
+ // Ignore discovery failures.
95
95
  }
96
96
  return `${normalizedIssuer}/oauth/token`;
97
97
  }
@@ -14,26 +14,30 @@ const REQUIRED_OIDC_ENDPOINTS = [
14
14
  'userinfo_endpoint',
15
15
  ];
16
16
  function readOIDCConfig(body) {
17
- const discovery = typeof body === 'object' && body !== null && !Array.isArray(body)
18
- ? body
19
- : {};
17
+ const discovery = isUnknownRecord(body) ? body : {};
20
18
  const missingEndpoints = REQUIRED_OIDC_ENDPOINTS.filter((endpoint) => {
21
19
  const value = discovery[endpoint];
22
20
  return typeof value !== 'string' || value.length === 0;
23
21
  });
24
- if (missingEndpoints.length > 0) {
22
+ const authorizationEndpoint = discovery['authorization_endpoint'];
23
+ const tokenEndpoint = discovery['token_endpoint'];
24
+ const userinfoEndpoint = discovery['userinfo_endpoint'];
25
+ if (missingEndpoints.length > 0 ||
26
+ typeof authorizationEndpoint !== 'string' ||
27
+ typeof tokenEndpoint !== 'string' ||
28
+ typeof userinfoEndpoint !== 'string') {
25
29
  throw new SSOAuthError(`OIDC discovery returned incomplete endpoints: missing ${missingEndpoints.join(', ')}`, 502, {
26
30
  code: 'invalid_discovery_document',
27
31
  body,
28
32
  });
29
33
  }
30
34
  return {
31
- authorization_endpoint: discovery.authorization_endpoint,
32
- token_endpoint: discovery.token_endpoint,
33
- userinfo_endpoint: discovery.userinfo_endpoint,
34
- ...(typeof discovery.end_session_endpoint === 'string'
35
- && discovery.end_session_endpoint.length > 0
36
- ? { end_session_endpoint: discovery.end_session_endpoint }
35
+ authorization_endpoint: authorizationEndpoint,
36
+ token_endpoint: tokenEndpoint,
37
+ userinfo_endpoint: userinfoEndpoint,
38
+ ...(typeof discovery['end_session_endpoint'] === 'string'
39
+ && discovery['end_session_endpoint'].length > 0
40
+ ? { end_session_endpoint: discovery['end_session_endpoint'] }
37
41
  : {}),
38
42
  };
39
43
  }
@@ -44,18 +48,24 @@ function decodeJwtPayload(token) {
44
48
  if (parts.length < 2)
45
49
  return null;
46
50
  const payloadStr = parts[1];
51
+ if (!payloadStr)
52
+ return null;
47
53
  const padded = payloadStr + '='.repeat((4 - (payloadStr.length % 4)) % 4);
48
54
  const binString = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
49
55
  const bytes = Uint8Array.from(binString, (value) => value.codePointAt(0) ?? 0);
50
- return JSON.parse(new TextDecoder().decode(bytes));
56
+ const payload = JSON.parse(new TextDecoder().decode(bytes));
57
+ return isUnknownRecord(payload) ? payload : null;
58
+ }
59
+ function isUnknownRecord(value) {
60
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
51
61
  }
52
62
  function getAccessTokenExpiry(data, accessToken) {
53
- if (typeof data.expires_in === 'number' && Number.isFinite(data.expires_in)) {
54
- return Math.floor(Date.now() / 1000) + data.expires_in;
63
+ if (typeof data['expires_in'] === 'number' && Number.isFinite(data['expires_in'])) {
64
+ return Math.floor(Date.now() / 1000) + data['expires_in'];
55
65
  }
56
66
  try {
57
67
  const payload = decodeJwtPayload(accessToken);
58
- return typeof payload?.exp === 'number' ? payload.exp : undefined;
68
+ return typeof payload?.['exp'] === 'number' ? payload['exp'] : undefined;
59
69
  }
60
70
  catch {
61
71
  return undefined;
@@ -69,7 +79,7 @@ function createInvalidTokenResponseError(fallbackMessage, field, body) {
69
79
  });
70
80
  }
71
81
  function readTokenResponse(body, fallbackMessage) {
72
- if (typeof body === 'object' && body !== null && !Array.isArray(body)) {
82
+ if (isUnknownRecord(body)) {
73
83
  return body;
74
84
  }
75
85
  throw createInvalidTokenResponseError(fallbackMessage, 'response body', body);
@@ -91,11 +101,14 @@ function authorizationScheme(tokenType) {
91
101
  function normalizeTokenResponse(body, defaults, fallbackMessage) {
92
102
  const response = readTokenResponse(body, fallbackMessage);
93
103
  const accessToken = requireTokenString(response, 'access_token', fallbackMessage);
104
+ const idToken = typeof response['id_token'] === 'string' ? response['id_token'] : defaults.idToken;
105
+ const refreshToken = readOptionalTokenString(response, 'refresh_token', defaults.refreshToken, fallbackMessage);
106
+ const expiresAt = getAccessTokenExpiry(response, accessToken);
94
107
  return {
95
108
  access_token: accessToken,
96
- id_token: typeof response.id_token === 'string' ? response.id_token : defaults.idToken,
97
- refresh_token: readOptionalTokenString(response, 'refresh_token', defaults.refreshToken, fallbackMessage),
98
- expires_at: getAccessTokenExpiry(response, accessToken),
109
+ ...(idToken === undefined ? {} : { id_token: idToken }),
110
+ ...(refreshToken === undefined ? {} : { refresh_token: refreshToken }),
111
+ ...(expiresAt === undefined ? {} : { expires_at: expiresAt }),
99
112
  token_type: authorizationScheme(readOptionalTokenString(response, 'token_type', defaults.tokenType, fallbackMessage) ?? defaults.tokenType),
100
113
  };
101
114
  }
@@ -261,7 +274,8 @@ export function createSSOAuthProvider(config) {
261
274
  if (!response.ok)
262
275
  throw await createSSOAuthResponseError(response, fallbackMessage);
263
276
  try {
264
- return await response.json();
277
+ const body = await response.json();
278
+ return body;
265
279
  }
266
280
  catch (error) {
267
281
  throw new SSOAuthError(`${fallbackMessage}: invalid JSON response`, 502, {
@@ -284,7 +298,7 @@ export function createSSOAuthProvider(config) {
284
298
  refresh_token: current.refresh_token,
285
299
  }), 'Token refresh failed');
286
300
  return normalizeTokenResponse(response, {
287
- idToken: current.id_token,
301
+ ...(current.id_token === undefined ? {} : { idToken: current.id_token }),
288
302
  refreshToken: current.refresh_token,
289
303
  tokenType: current.token_type,
290
304
  }, 'Token refresh failed');
@@ -294,9 +308,9 @@ export function createSSOAuthProvider(config) {
294
308
  storageKey,
295
309
  autoRefresh: config.autoRefresh ?? true,
296
310
  refreshBuffer,
297
- refreshLock: config.refreshLock,
311
+ ...(config.refreshLock === undefined ? {} : { refreshLock: config.refreshLock }),
298
312
  refresh: performRefresh,
299
- legacyStorageKey: config.legacyStorageKey,
313
+ ...(config.legacyStorageKey === undefined ? {} : { legacyStorageKey: config.legacyStorageKey }),
300
314
  });
301
315
  function assertAuthorizationExchangeCurrent(authGeneration, expectedState) {
302
316
  if (sessions.getAuthGeneration() === authGeneration
@@ -341,12 +355,26 @@ export function createSSOAuthProvider(config) {
341
355
  return session;
342
356
  });
343
357
  }
344
- const mapIdentity = config.mapIdentity ?? ((userinfo) => ({
345
- id: userinfo.sub ?? '',
346
- name: userinfo.name ?? userinfo.preferred_username ?? '',
347
- avatar: userinfo.picture ?? undefined,
348
- email: userinfo.email ?? undefined,
349
- }));
358
+ const mapIdentity = config.mapIdentity ?? ((userinfo) => {
359
+ const id = userinfo['sub'];
360
+ if (typeof id !== 'string' || !id)
361
+ throw new Error('Invalid userinfo subject');
362
+ const name = userinfo['name'];
363
+ const username = userinfo['preferred_username'];
364
+ const avatar = userinfo['picture'];
365
+ const email = userinfo['email'];
366
+ for (const value of [name, username, avatar, email]) {
367
+ if (value !== undefined && typeof value !== 'string') {
368
+ throw new Error('Invalid userinfo profile field');
369
+ }
370
+ }
371
+ return {
372
+ id,
373
+ ...(typeof name === 'string' ? { name } : typeof username === 'string' ? { name: username } : {}),
374
+ ...(typeof avatar === 'string' ? { avatar } : {}),
375
+ ...(typeof email === 'string' ? { email } : {}),
376
+ };
377
+ });
350
378
  const authorizationSource = {
351
379
  getAuthorizationHeader: async (options) => {
352
380
  const accessToken = await sessions.getAccessToken(options);
@@ -399,7 +427,7 @@ export function createSSOAuthProvider(config) {
399
427
  await clearOwnedLoginState(state);
400
428
  }
401
429
  catch {
402
- // 保留触发失败的原始认证错误;清理失败不能掩盖根因。
430
+ // Preserve the original authentication error; cleanup failures must not hide the root cause.
403
431
  }
404
432
  return { success: false, error: getErrorResult(error) };
405
433
  }
@@ -415,7 +443,7 @@ export function createSSOAuthProvider(config) {
415
443
  catch (error) {
416
444
  if (!(error instanceof SSOAuthError) || error.code !== 'auth_lock_failed')
417
445
  throw error;
418
- // 无跨上下文锁时 callback commit 同样会失败;本地登出仍应清理会话。
446
+ // Callback commit also fails without a cross-context lock; local sign-out must still clear the session.
419
447
  sessions.clearSession();
420
448
  }
421
449
  try {
@@ -435,7 +463,7 @@ export function createSSOAuthProvider(config) {
435
463
  }
436
464
  }
437
465
  catch {
438
- // 本地会话已清理,发现或跳转失败不能恢复为登录态。
466
+ // The local session is cleared; discovery or redirect failures must not restore an authenticated state.
439
467
  }
440
468
  return {
441
469
  success: true,
@@ -469,7 +497,7 @@ export function createSSOAuthProvider(config) {
469
497
  await clearOwnedLoginState(savedState);
470
498
  }
471
499
  catch {
472
- // 错误回调仍返回 IdP 的原始错误;清理失败不掩盖它。
500
+ // Error callbacks still return the IdP's original error; cleanup failures must not hide it.
473
501
  }
474
502
  }
475
503
  return {
@@ -499,7 +527,7 @@ export function createSSOAuthProvider(config) {
499
527
  await clearOwnedLoginState(savedState);
500
528
  }
501
529
  catch {
502
- // 保留 token exchange 的原始错误。
530
+ // Preserve the original token exchange error.
503
531
  }
504
532
  return { authenticated: false, error: getErrorResult(error) };
505
533
  }
@@ -546,6 +574,8 @@ export function createSSOAuthProvider(config) {
546
574
  if (!response.ok)
547
575
  return null;
548
576
  const userinfo = await response.json();
577
+ if (!isUnknownRecord(userinfo))
578
+ return null;
549
579
  if (sessions.getAuthGeneration() !== authGeneration
550
580
  || !sessions.getSession())
551
581
  return null;
@@ -46,7 +46,7 @@ export function createAuthenticatedFetch(source, fetcher, options = {}) {
46
46
  cause: error,
47
47
  });
48
48
  }
49
- // 两份副本都在首次发送前创建,避免流式 body 被消费后静默丢失。
49
+ // Create both copies before the first send so a streamed body is not silently lost after consumption.
50
50
  const firstAttempt = cloneReplayableRequest(request);
51
51
  const retryAttempt = cloneReplayableRequest(request);
52
52
  const authorization = await source.getAuthorizationHeader();
package/dist/errors.d.ts CHANGED
@@ -7,7 +7,7 @@ export interface SSOAuthErrorOptions {
7
7
  }
8
8
  export declare class SSOAuthError extends Error {
9
9
  readonly statusCode: number;
10
- readonly code?: string;
10
+ readonly code: string | undefined;
11
11
  readonly details?: unknown;
12
12
  readonly body?: unknown;
13
13
  readonly retryable: boolean;
package/dist/errors.js CHANGED
@@ -31,9 +31,10 @@ export class SSOAuthError extends Error {
31
31
  }
32
32
  }
33
33
  function asRecord(value) {
34
- return typeof value === 'object' && value !== null
35
- ? value
36
- : null;
34
+ return isRecord(value) ? value : null;
35
+ }
36
+ function isRecord(value) {
37
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
37
38
  }
38
39
  function firstString(...values) {
39
40
  return values.find((value) => typeof value === 'string');
@@ -43,7 +44,8 @@ async function readResponseBody(response) {
43
44
  if (!text)
44
45
  return undefined;
45
46
  try {
46
- return JSON.parse(text);
47
+ const value = JSON.parse(text);
48
+ return value;
47
49
  }
48
50
  catch {
49
51
  return text;
@@ -52,15 +54,15 @@ async function readResponseBody(response) {
52
54
  export async function createSSOAuthResponseError(response, fallbackMessage) {
53
55
  const body = await readResponseBody(response);
54
56
  const record = asRecord(body);
55
- const nestedError = asRecord(record?.error);
56
- const code = firstString(record?.code, record?.error_code, nestedError?.code, nestedError?.error_code, record?.error);
57
- const description = firstString(record?.error_description, nestedError?.error_description);
58
- const responseMessage = firstString(record?.message, nestedError?.message);
57
+ const nestedError = asRecord(record?.['error']);
58
+ const code = firstString(record?.['code'], record?.['error_code'], nestedError?.['code'], nestedError?.['error_code'], record?.['error']);
59
+ const description = firstString(record?.['error_description'], nestedError?.['error_description']);
60
+ const responseMessage = firstString(record?.['message'], nestedError?.['message']);
59
61
  const textMessage = typeof body === 'string' ? body : undefined;
60
62
  const statusMessage = response.statusText || `HTTP ${response.status}`;
61
63
  return new SSOAuthError(description ?? responseMessage ?? textMessage ?? `${fallbackMessage}: ${statusMessage}`, response.status, {
62
- code,
63
- details: record?.details ?? nestedError?.details,
64
+ ...(code === undefined ? {} : { code }),
65
+ details: record?.['details'] ?? nestedError?.['details'],
64
66
  body,
65
67
  });
66
68
  }
@@ -81,6 +83,6 @@ export function createSSOAuthNetworkError(error, fallbackMessage) {
81
83
  }
82
84
  export function isTerminalSessionError(error) {
83
85
  const record = asRecord(error);
84
- const code = firstString(record?.code, record?.error_code, asRecord(record?.error)?.code, asRecord(record?.error)?.error_code);
86
+ const code = firstString(record?.['code'], record?.['error_code'], asRecord(record?.['error'])?.['code'], asRecord(record?.['error'])?.['error_code']);
85
87
  return code !== undefined && TERMINAL_OAUTH_CODES.has(code);
86
88
  }
package/dist/presets.d.ts CHANGED
@@ -68,15 +68,13 @@ export declare function createKeycloakAuth(baseUrl: string, realm: string, clien
68
68
  export declare function createAuth0Auth(domain: string, clientId: string, opts?: PresetOptions): import("./auth-provider.js").SSOAuthProvider;
69
69
  export type { PresetOptions };
70
70
  /**
71
- * Supauth hosted OIDC Sign-In.
71
+ * Supauth OIDC Sign-In (GoTrue-compatible identity provider).
72
72
  *
73
- * Targets the SupAuth contract hosted by SupaCloud or SupaOAuth: OIDC endpoints
74
- * live below `/auth/v1`, while browser sign-out lives at the public root
75
- * `/logout`. Other GoTrue deployments should provide explicit endpoints or use
76
- * `createSSOAuthProvider()` directly.
73
+ * Compatible with SupaCloud, SupaOAuth, and any GoTrue-based auth backend.
74
+ * Uses standard OIDC Authorization Code Flow with PKCE.
77
75
  *
76
+ * @param issuer - Supauth issuer URL (e.g., 'https://auth.example.com')
78
77
  * @param clientId - OAuth2 Client ID
79
- * @param opts - SSO options. `opts.issuer` accepts a Supauth public root or `/auth/v1` issuer URL.
80
78
  */
81
79
  export declare function createSupauthAuth(clientId: string, opts?: PresetOptions & {
82
80
  issuer?: string;
package/dist/presets.js CHANGED
@@ -83,10 +83,10 @@ export function createGitHubAuth(clientId, opts) {
83
83
  userinfo_endpoint: 'https://api.github.com/user',
84
84
  },
85
85
  mapIdentity: (userinfo) => ({
86
- id: String(userinfo.id ?? ''),
87
- name: userinfo.name ?? userinfo.login ?? '',
88
- avatar: userinfo.avatar_url ?? undefined,
89
- email: userinfo.email ?? undefined,
86
+ id: String(userinfo['id'] ?? ''),
87
+ name: userinfo['name'] ?? userinfo['login'] ?? '',
88
+ avatar: userinfo['avatar_url'] ?? undefined,
89
+ email: userinfo['email'] ?? undefined,
90
90
  }),
91
91
  ...opts,
92
92
  });
@@ -144,43 +144,22 @@ export function createAuth0Auth(domain, clientId, opts) {
144
144
  });
145
145
  }
146
146
  // ─── Supauth / GoTrue ────────────────────────────────────────
147
- function normalizeSupauthIssuer(configuredIssuer) {
148
- const normalizedIssuer = configuredIssuer.replace(/\/+$/, '');
149
- if (!normalizedIssuer || normalizedIssuer.endsWith('/auth/v1')) {
150
- return {
151
- issuer: normalizedIssuer,
152
- publicRoot: normalizedIssuer.slice(0, -'/auth/v1'.length),
153
- };
154
- }
155
- return { issuer: `${normalizedIssuer}/auth/v1`, publicRoot: normalizedIssuer };
156
- }
157
147
  /**
158
- * Supauth hosted OIDC Sign-In.
148
+ * Supauth OIDC Sign-In (GoTrue-compatible identity provider).
159
149
  *
160
- * Targets the SupAuth contract hosted by SupaCloud or SupaOAuth: OIDC endpoints
161
- * live below `/auth/v1`, while browser sign-out lives at the public root
162
- * `/logout`. Other GoTrue deployments should provide explicit endpoints or use
163
- * `createSSOAuthProvider()` directly.
150
+ * Compatible with SupaCloud, SupaOAuth, and any GoTrue-based auth backend.
151
+ * Uses standard OIDC Authorization Code Flow with PKCE.
164
152
  *
153
+ * @param issuer - Supauth issuer URL (e.g., 'https://auth.example.com')
165
154
  * @param clientId - OAuth2 Client ID
166
- * @param opts - SSO options. `opts.issuer` accepts a Supauth public root or `/auth/v1` issuer URL.
167
155
  */
168
156
  export function createSupauthAuth(clientId, opts) {
169
- const manualLogoutEndpoint = opts?.manualEndpoints?.end_session_endpoint;
170
- const { issuer: configuredIssuer = '', endSessionEndpoint: configuredLogoutEndpoint, storageKey: configuredStorageKey, ...providerOptions } = opts ?? {};
171
- const { issuer, publicRoot } = normalizeSupauthIssuer(configuredIssuer);
172
- const storageKey = configuredStorageKey
173
- ?? `svadmin_sso:${encodeURIComponent(configuredIssuer)}:${encodeURIComponent(clientId)}`;
174
- const endSessionEndpoint = configuredLogoutEndpoint
175
- ?? manualLogoutEndpoint
176
- ?? (publicRoot ? `${publicRoot}/logout` : undefined);
157
+ const issuer = opts?.issuer ?? '';
177
158
  return createSSOAuthProvider({
178
159
  issuer,
179
160
  clientId,
180
161
  scopes: ['openid', 'profile', 'email'],
181
162
  redirectUri: resolveRedirectUri(opts),
182
- storageKey,
183
- ...(endSessionEndpoint === undefined ? {} : { endSessionEndpoint }),
184
- ...providerOptions,
163
+ ...opts,
185
164
  });
186
165
  }
@@ -239,7 +239,7 @@ export function createSessionManager(options) {
239
239
  function saveSession(session, event) {
240
240
  const raw = JSON.stringify(session);
241
241
  try {
242
- // 单次 setItem 写入完整 token set,避免轮换只落盘一半。
242
+ // Write the complete token set in one setItem call to avoid partial rotation persistence.
243
243
  options.storage.setItem(keys.tokens, raw);
244
244
  }
245
245
  catch (error) {
@@ -286,8 +286,8 @@ export function createSessionManager(options) {
286
286
  }
287
287
  function clearTokenSession() {
288
288
  const persistenceFailure = clearStorageValue(options.storage, keys.tokens);
289
- // 仅 token 失效不能取消已经持有 PKCE/state 的新认证尝试。
290
- // 显式 logout 会走 clearSession(),仍会清理完整状态并推进 generation。
289
+ // Token invalidation alone must not cancel a new authentication attempt holding PKCE/state.
290
+ // Explicit logout calls clearSession(), which clears all state and advances the generation.
291
291
  const preservesAuthAttempt = hasPendingAuthAttempt();
292
292
  if (!preservesAuthAttempt) {
293
293
  authGeneration += 1;
@@ -417,8 +417,8 @@ export function createSessionManager(options) {
417
417
  }
418
418
  throw error;
419
419
  }
420
- // 网络请求只持有 refresh 锁;最终 CAS 与写入再短暂持有 auth 锁,
421
- // 使跨标签页 logout/callback commit 与 token rotation 线性化。
420
+ // The network request holds only the refresh lock; final CAS and persistence briefly hold the auth lock,
421
+ // linearizing cross-tab logout/callback commits with token rotation.
422
422
  return runAuthMutation(async () => {
423
423
  const writeRace = resolveRefreshRace(initialRaw);
424
424
  if (writeRace.changed)
@@ -477,7 +477,7 @@ export function createSessionManager(options) {
477
477
  if (!force && raw === lastObservedRaw)
478
478
  return;
479
479
  if (raw === null) {
480
- // token-only 失效可与新 callback 并发;共享 state 存在时不能把它误判为 logout。
480
+ // Token-only invalidation may race with a new callback; shared state means it must not be treated as logout.
481
481
  const shouldPreserveAuthAttempt = preserveAuthAttempt && hasPendingAuthAttempt();
482
482
  if (!shouldPreserveAuthAttempt) {
483
483
  authGeneration += 1;
@@ -494,7 +494,7 @@ export function createSessionManager(options) {
494
494
  syncRemoteChange('SIGNED_OUT', null);
495
495
  return;
496
496
  }
497
- // 一次明确的本地登出只有当前上下文的新登录才能撤销;远端刷新不能复活它。
497
+ // Only a new login in the current context can undo an explicit local sign-out; remote refresh must not resurrect it.
498
498
  if (locallySignedOut)
499
499
  return;
500
500
  authGeneration += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/sso",
3
- "version": "0.11.21",
3
+ "version": "0.12.7",
4
4
  "type": "module",
5
5
  "description": "OIDC/OAuth2 SSO AuthProvider plugin for svadmin — supports Okta, Azure AD, Cognito, Keycloak, Google",
6
6
  "license": "MIT",
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "https://github.com/vibeunion/svadmin",
21
+ "url": "git+https://github.com/vibeunion/svadmin.git",
22
22
  "directory": "packages/sso"
23
23
  },
24
24
  "sideEffects": false,
@@ -34,7 +34,7 @@
34
34
  "dist"
35
35
  ],
36
36
  "peerDependencies": {
37
- "@svadmin/core": "^0.50.0"
37
+ "@svadmin/core": "^0.53.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "typescript": "^7.0.2"