@bsuite/auth

中文文档
1.0.1发布于 2026/09/18UNLICENSEDTypeScriptES Module近 30 天下载 2318

BSuite OAuth 2.1 PKCE client — shared across CRM7, R80.4, Braden, Conduit, and Throughput. Uses JWKS for token verification via jose.

原始使用文档
文档目录11 节
  1. Concurrent sign-in and callback recovery
  2. OAuth server environment
  3. Install
  4. Usage
  5. API
  6. Behaviour notes
  7. Storage keys
  8. Design notes
  9. Per-app OAuth client IDs
  10. Related docs
  11. License

@bsuite/auth

OAuth 2.1 PKCE client shared across BSuite apps (CRM7, Conduit, R80.4, Braden, Throughput). BSU is the OAuth server; this package is the client every other app uses to consume it.

Concurrent sign-in and callback recovery

The opt-in session ownership coordinator serializes consumer SDK writes and logout across same-origin tabs. Consumer adapter wiring, transaction snapshot binding and deployed verification remain required.

Login initiators should use attemptSilentAuthDetailed(). Continue to the destination on authenticated; stop on redirecting or superseded; offer interactive recovery on failed. The old boolean helper remains compatible but cannot distinguish a scheduled navigation from failure. Assigning window.location does not stop subsequent JavaScript.

Route callbacks with hasPendingBusinessSuiteTransaction(state), which requires this origin's complete, fresh transaction state. Do not dispatch from state shape or the presence of an unrelated legacy key. Legacy transactions also require their verifier, nonce and timestamp. Incomplete transactions need a fresh sign-in.

BusinessSuiteOAuthExchangeUncertainError.recovery is fresh-sign-in: the server may have consumed the single-use code before a network or response-body failure. Never automatically replay it. Callback error handlers must preserve unrelated transactions and newer sessions. Capture the consumer session/logout generation before starting an exchange and check it again after verification and before committing tokens or navigating. An exchange already in flight can return after sign-out; its result must not restore the previous session. After successful exchange, bridge through the consumer's supabase.auth.setSession() before protected data reads. Host-local session storage remains separate from cross-app OAuth SSO.

The package is intentionally small — it defaults to the production BSuite Supabase host (https://tuybltdrdefjblnplpqo.supabase.co), supports an explicit OAuth-server override for persistent development Supabase branches, resolves redirect URIs from VITE_APP_URL (or NEXT_PUBLIC_APP_URL) when available, and falls back to ${window.location.origin}/auth/callback. It depends only on jose for JWKS verification.

OAuth server environment

Production consumers need no extra configuration; the OAuth server defaults to the production BSuite Supabase project. Long-lived development deployments that use a persistent Supabase branch/project must set one of these variables to that branch URL:

  • VITE_BSU_OAUTH_SUPABASE_URL for Vite browser builds
  • NEXT_PUBLIC_BSU_OAUTH_SUPABASE_URL for Next.js browser builds
  • BSU_OAUTH_SUPABASE_URL for server/build-time environments

Accepted aliases are VITE_BUSINESS_SUITE_SUPABASE_URL, NEXT_PUBLIC_BUSINESS_SUITE_SUPABASE_URL, and BUSINESS_SUITE_SUPABASE_URL. Values must be HTTPS Supabase project URLs in the form https://<project-ref>.supabase.co; invalid overrides throw before redirecting users into a broken OAuth flow.

Install

pnpm add @bsuite/auth

Usage

Every consumer app creates a thin wrapper that re-exports the shared client bound to its OAuth client ID. Example from CRM7 (the canonical reference — same pattern in R80.4, braden, conduit, throughput):

// src/lib/business-suite-oauth.ts
import { createOAuthClient } from '@bsuite/auth';
export type { BusinessSuiteTokens, VerifiedUser } from '@bsuite/auth';

export const {
  signInWithBusinessSuite,
  exchangeCodeForTokens,
  refreshBusinessSuiteToken,
  verifyAccessToken,
  getUserInfo,
  clearBSTokens,
  startBSTokenRefresh,
  attemptSilentAuthDetailed,
} = createOAuthClient('30f76744-3e0b-40bf-abb8-8c587389802e'); // your app's OAuth client ID

Then wire it into the auth lifecycle:

// On a login button click — schedules navigation to the OAuth server
void signInWithBusinessSuite();

// In your /auth/callback route, with `code` and `state` parsed from the URL
const { tokens, user } = await exchangeCodeForTokens(code, state);
localStorage.setItem('bs_access_token', tokens.access_token);
localStorage.setItem('bs_refresh_token', tokens.refresh_token);
localStorage.setItem('bs_user', JSON.stringify(user));
if (tokens.id_token) localStorage.setItem('bs_id_token', tokens.id_token);

// On app mount (e.g. AuthContext / AuthProvider) — try OIDC silent re-auth before showing the login UI
const result = await attemptSilentAuthDetailed();
// Stop on redirecting/superseded; navigate only on authenticated.

// Start the auto-refresh loop and capture the cleanup function
const stopRefresh: () => void = startBSTokenRefresh();
// later, e.g. on AuthContext unmount or sign-out:
stopRefresh();

// Sign-out — clear all 4 BS OAuth localStorage keys
clearBSTokens();

API

import type { OAuthClient, BusinessSuiteTokens, VerifiedUser } from '@bsuite/auth';

declare function createOAuthClient(clientId: string): OAuthClient;

interface OAuthClient {
  signInWithBusinessSuite(): Promise<void>;
  exchangeCodeForTokens(
    code: string,
    state: string,
  ): Promise<{ tokens: BusinessSuiteTokens; user: VerifiedUser }>;
  refreshBusinessSuiteToken(
    refreshToken: string,
  ): Promise<{ tokens: BusinessSuiteTokens; user: VerifiedUser }>;
  verifyAccessToken(token: string): Promise<VerifiedUser>;
  getUserInfo(accessToken: string): Promise<Record<string, unknown>>;
  clearBSTokens(): void;
  startBSTokenRefresh(): () => void; // returns a cleanup function
  attemptSilentAuth(): Promise<boolean>;
}

interface BusinessSuiteTokens {
  access_token: string;
  refresh_token: string;
  token_type: string;
  expires_in: number;
  id_token?: string;
}

interface VerifiedUser {
  sub: string;
  email?: string;
  name?: string;
  picture?: string;
  client_id?: string;
  role?: string;
}

Behaviour notes

  • signInWithBusinessSuite generates a PKCE code_verifier, code_challenge (S256), state, and OIDC nonce, stores them in localStorage with a 10-minute TTL sentinel, then sets window.location.href to the BSU /auth/v1/oauth/authorize URL. The returned Promise never resolves — the page navigates away.
  • exchangeCodeForTokens validates state (CSRF), reads the stored PKCE verifier, posts to /auth/v1/oauth/token, then JWKS-verifies the access token AND the OIDC id_token nonce (replay protection per OIDC Core §3.1.2.2). Throws on state mismatch, missing verifier, non-2xx response, or nonce mismatch.
  • refreshBusinessSuiteToken posts to /auth/v1/oauth/token with grant_type=refresh_token and verifies the new access token via JWKS before returning.
  • verifyAccessToken uses cached JWKS (jose's createRemoteJWKSet) with issuer: "<supabase>/auth/v1" and audience: "authenticated". RS256/ES256 only.
  • getUserInfo GETs /auth/v1/oauth/userinfo with the supplied bearer token and returns the parsed JSON body.
  • startBSTokenRefresh triggers an immediate check, then polls every 60 seconds and refreshes tokens 5 minutes before expiry. Returns a cleanup function that stops the interval. Always capture the cleanup to avoid leaking timers in components that mount/unmount (e.g. AuthProvider).
  • attemptSilentAuth returns true if there's a valid access token already in localStorage OR a refresh token that successfully exchanges. Returns false otherwise. Never throws — errors are swallowed so the login UI can still render.
  • clearBSTokens removes bs_access_token, bs_refresh_token, bs_user, bs_id_token from localStorage. Always call on sign-out.

Storage keys

StorageKeyPurposeLifetime
localStoragebs_access_tokenJWT access token (JWKS-verified)until refresh / sign-out
localStoragebs_refresh_tokenRefresh tokenuntil refresh / sign-out
localStoragebs_userJSON-serialised VerifiedUseruntil refresh / sign-out
localStoragebs_id_tokenOIDC id_token (optional)until refresh / sign-out
localStoragebs_oauth_code_verifierPKCE code verifiersign-in flow only, TTL-guarded
localStoragebs_oauth_stateCSRF statesign-in flow only, TTL-guarded
localStoragebs_oauth_nonceOIDC replay-protection noncesign-in flow only, TTL-guarded
localStoragebs_oauth_started_atPKCE TTL sentinelsign-in flow only
localStoragebs_oauth_flowsState-keyed PKCE transactionsten minutes / completion / sign-out
localStoragebs_oauth_inflight_codesPer-code claims with owner identityten minutes / owning completion / sign-out

Design notes

  • Per-domain localStorage — tokens never leave the app's own origin. Cross-app SSO is achieved via OIDC prompt=none silent re-auth (attemptSilentAuth), NOT cross-domain cookies. The deprecated business_suite_auth cookie SSO scheme was removed 2025-02-27; do not reintroduce it.
  • JWKS verification — verifyAccessToken fetches the Supabase /.well-known/jwks.json once and caches it via jose's createRemoteJWKSet. RS256/ES256 only.
  • PKCE S256 mandatory — no implicit flow, no plain challenge.
  • OIDC nonce verification — if id_token is returned, its nonce claim is verified against the nonce in the matching state-keyed transaction (or eligible legacy transaction). A missing or mismatched nonce throws “id_token nonce mismatch — possible replay attack” (OIDC Core §3.1.2.2).
  • Token expiry event — if the auto-refresh loop fails or a sibling tab removes bs_access_token, the package dispatches a bs-oauth-expired CustomEvent on window so apps can react (e.g. show a banner) before tokens are cleared. Reasons: network_error, refresh_rejected, cross_tab_logout.

Per-app OAuth client IDs

AppClient ID
CRM730f76744-3e0b-40bf-abb8-8c587389802e
R80.35d804d20-cd1b-4724-9107-86d2a9e51e09
Bradendcb7af18-254a-4946-b94d-5c606b01fc3f
Throughput35f0db49-ef62-4115-baba-7b961f034cc3
Conduitda925c19-8f32-40a0-b74d-4eb9540c422f
  • Parent repo canonical auth architecture: AUTH_CANONICAL.md
  • Per-app integration: each consumer's src/lib/business-suite-oauth.ts

License

UNLICENSED (internal BSuite use only).

版本与发布记录

@bsuite/auth 共发布 14 个版本,首次发布于 2026/04/20;过去 12 个月发布了 14 个版本。

版本发布日期距今
1.0.1latest2026/09/181 个月前
1.0.1-next.1next2026/09/091 个月前
1.0.1-next.02026/09/091 个月前
1.0.02026/08/182 个月前
0.2.82026/08/053 个月前
0.2.72026/07/064 个月前

关于 @bsuite/auth 的常见问题

@bsuite/auth 是什么?

BSuite OAuth 2.1 PKCE client — shared across CRM7, R80.4, Braden, Conduit, and Throughput. Uses JWKS for token verification via jose.

如何安装 @bsuite/auth?

在项目目录运行 npm install @bsuite/auth,或使用 pnpm add / yarn add / bun add。

@bsuite/auth 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@bsuite/auth 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@bsuite/auth 可以商用吗?

@bsuite/auth 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@bsuite/auth 的下载量有多少?

根据 npm 官方下载统计,@bsuite/auth 在 2026/10/09 – 2026/11/07 这 30 天内被下载 2,318 次,日均约 77 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bsuite/auth 还在维护吗?

最新版本 1.0.1 发布于 2026/09/18(1 个月前);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/04/20。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。