@cloudbase/auth 2.25.0 → 2.25.2

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/src/index.ts CHANGED
@@ -12,6 +12,8 @@ import {
12
12
  EVENTS,
13
13
  AUTH_STATE_CHANGED_TYPE,
14
14
  OAUTH_TYPE,
15
+ weappJwtDecodeAll,
16
+ AuthError,
15
17
  } from '@cloudbase/oauth'
16
18
  import { useAuthAdapter } from './adapter'
17
19
  import {
@@ -25,7 +27,41 @@ import {
25
27
  adapterForWxMp,
26
28
  useDefaultAdapter,
27
29
  } from './utilities'
28
- import { SbaseApi } from './sbaseApi'
30
+ import { saveToBrowserSession, getBrowserSession, removeBrowserSession, addUrlSearch } from './utils'
31
+ import { utils } from '@cloudbase/utilities'
32
+ import {
33
+ CommonRes,
34
+ DeleteMeReq,
35
+ GetClaimsRes,
36
+ GetUserIdentitiesRes,
37
+ GetUserRes,
38
+ LinkIdentityReq,
39
+ LinkIdentityRes,
40
+ OnAuthStateChangeCallback,
41
+ ReauthenticateRes,
42
+ ResendReq,
43
+ ResendRes,
44
+ ResetPasswordForEmailRes,
45
+ ResetPasswordForOldReq,
46
+ SetSessionReq,
47
+ SignInAnonymouslyReq,
48
+ SignInOAuthRes,
49
+ SignInRes,
50
+ SignInWithIdTokenReq,
51
+ SignInWithOAuthReq,
52
+ SignInWithOtpReq,
53
+ SignInWithOtpRes,
54
+ SignInWithPasswordReq,
55
+ SignUpRes,
56
+ UnlinkIdentityReq,
57
+ UpdateUserAttributes,
58
+ UpdateUserReq,
59
+ UpdateUserWithVerificationRes,
60
+ VerifyOAuthReq,
61
+ VerifyOtpReq,
62
+ } from './type'
63
+
64
+ const isBrowser = () => typeof window !== 'undefined' && typeof document !== 'undefined'
29
65
 
30
66
  export type {
31
67
  SignInRes,
@@ -78,7 +114,6 @@ interface UserInfo {
78
114
  created_from?: string
79
115
  }
80
116
 
81
-
82
117
  const onCredentialsError = eventBus => (params) => {
83
118
  eventBus.fire(EVENTS.LOGIN_STATE_CHANGED, { ...params, eventType: LOGIN_STATE_CHANGED_TYPE.CREDENTIALS_ERROR })
84
119
  }
@@ -327,13 +362,22 @@ export class LoginState implements ILoginState {
327
362
  }
328
363
  }
329
364
 
330
- class Auth extends SbaseApi {
365
+ class Auth {
366
+ readonly config: ICloudbaseAuthConfig
367
+ oauthInstance: CloudbaseOAuth
368
+ readonly cache: ICloudbaseCache
369
+ private listeners: Map<string, Set<OnAuthStateChangeCallback>> = new Map()
370
+ private hasListenerSetUp = false
371
+
331
372
  constructor(config: ICloudbaseAuthConfig & {
332
373
  cache: ICloudbaseCache
333
374
  request?: ICloudbaseRequest
334
375
  runtime?: string
335
376
  },) {
336
- super(config)
377
+ this.config = config
378
+ this.oauthInstance = config.oauthInstance
379
+ this.cache = config.cache
380
+ this.init()
337
381
  this.setAccessKey()
338
382
  }
339
383
 
@@ -1407,6 +1451,1224 @@ class Auth extends SbaseApi {
1407
1451
  }
1408
1452
  }
1409
1453
  }
1454
+
1455
+ // ========== SbaseApi methods merged below ==========
1456
+
1457
+ /**
1458
+ * https://supabase.com/docs/reference/javascript/auth-signinanonymously
1459
+ * Sign in a user anonymously.
1460
+ * const { data, error } = await auth.signInAnonymously();
1461
+ * @param params
1462
+ * @returns Promise<SignInRes>
1463
+ */
1464
+ async signInAnonymously(params: SignInAnonymouslyReq): Promise<SignInRes> {
1465
+ try {
1466
+ await this.oauthInstance.authApi.signInAnonymously(params)
1467
+ const loginState = await this.createLoginState()
1468
+
1469
+ const { data: { session } = {} } = await this.getSession()
1470
+
1471
+ // loginState返回是为了兼容v2版本
1472
+ return { ...(loginState as any), data: { user: session.user, session }, error: null }
1473
+ } catch (error) {
1474
+ return { data: {}, error: new AuthError(error) }
1475
+ }
1476
+ }
1477
+
1478
+ /**
1479
+ * https://supabase.com/docs/reference/javascript/auth-signup
1480
+ * Sign up a new user with email or phone using a one-time password (OTP). If the account not exist, a new account will be created.
1481
+ * @param params
1482
+ * @returns Promise<SignUpRes>
1483
+ */
1484
+ async signUp(params: authModels.SignUpRequest): Promise<SignUpRes> {
1485
+ if (params.phone_number || params.verification_code || params.verification_token || params.provider_token) {
1486
+ await this.oauthInstance.authApi.signUp(params)
1487
+ return this.createLoginState() as any
1488
+ }
1489
+ try {
1490
+ // 参数校验:email或phone必填其一
1491
+ this.validateAtLeastOne(params, [['email'], ['phone']], 'You must provide either an email or phone number')
1492
+
1493
+ // 第一步:发送验证码并存储 verificationInfo
1494
+ const verificationInfo = await this.getVerification(params.email ? { email: params.email } : { phone_number: this.formatPhone(params.phone) },)
1495
+
1496
+ return {
1497
+ data: {
1498
+ // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件)
1499
+ verifyOtp: async ({ token, messageId = verificationInfo.verification_id }): Promise<SignInRes> => {
1500
+ try {
1501
+ // 第三步:待用户输入完验证码之后,验证短信验证码
1502
+ const verificationTokenRes = await this.verify({
1503
+ verification_id: messageId || verificationInfo.verification_id,
1504
+ verification_code: token,
1505
+ })
1506
+
1507
+ // 第四步:注册并登录或直接登录
1508
+ // 如果用户已经存在,直接登录
1509
+ if (verificationInfo.is_user) {
1510
+ await this.signIn({
1511
+ username: params.email || this.formatPhone(params.phone),
1512
+ verification_token: verificationTokenRes.verification_token,
1513
+ })
1514
+ } else {
1515
+ // 如果用户不存在,注册用户
1516
+ const data = JSON.parse(JSON.stringify(params))
1517
+ delete data.email
1518
+ delete data.phone
1519
+
1520
+ await this.oauthInstance.authApi.signUp({
1521
+ ...data,
1522
+ ...(params.email ? { email: params.email } : { phone_number: this.formatPhone(params.phone) }),
1523
+ verification_token: verificationTokenRes.verification_token,
1524
+ verification_code: token,
1525
+ })
1526
+ await this.createLoginState()
1527
+ }
1528
+
1529
+ const { data: { session } = {} } = await this.getSession()
1530
+
1531
+ return { data: { user: session.user, session }, error: null }
1532
+ } catch (error) {
1533
+ return { data: {}, error: new AuthError(error) }
1534
+ }
1535
+ },
1536
+ },
1537
+ error: null,
1538
+ }
1539
+ } catch (error) {
1540
+ return { data: {}, error: new AuthError(error) }
1541
+ }
1542
+ }
1543
+
1544
+ /**
1545
+ * https://supabase.com/docs/reference/javascript/auth-signout
1546
+ * const result = await auth.signOut();
1547
+ *
1548
+ * @param params
1549
+ */
1550
+ async signOut(params?: authModels.SignoutRequest): Promise<void | authModels.SignoutReponse> {
1551
+ try {
1552
+ const { userInfoKey } = this.cache.keys
1553
+ const res = await this.oauthInstance.authApi.signOut(params)
1554
+ await this.cache.removeStoreAsync(userInfoKey)
1555
+ this.setAccessKey()
1556
+
1557
+ this.config.eventBus?.fire(EVENTS.LOGIN_STATE_CHANGED, { eventType: LOGIN_STATE_CHANGED_TYPE.SIGN_OUT })
1558
+
1559
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.SIGNED_OUT })
1560
+
1561
+ // res返回是为了兼容v2版本
1562
+ return { ...res, data: {}, error: null }
1563
+ } catch (error) {
1564
+ return { data: {}, error: new AuthError(error) }
1565
+ }
1566
+ }
1567
+
1568
+ /**
1569
+ * https://supabase.com/docs/reference/javascript/auth-onauthstatechange
1570
+ * Receive a notification every time an auth event happens.
1571
+ * @param callback
1572
+ * @returns Promise<{ data: { subscription: Subscription }, error: Error | null }>
1573
+ */
1574
+ onAuthStateChange(callback: OnAuthStateChangeCallback) {
1575
+ if (!this.hasListenerSetUp) {
1576
+ this.setupListeners()
1577
+ this.hasListenerSetUp = true
1578
+ }
1579
+
1580
+ const id = Math.random().toString(36)
1581
+
1582
+ if (!this.listeners.has(id)) {
1583
+ this.listeners.set(id, new Set())
1584
+ }
1585
+
1586
+ this.listeners.get(id)!.add(callback)
1587
+
1588
+ // 返回 Subscription 对象
1589
+ const subscription = {
1590
+ id,
1591
+ callback,
1592
+ unsubscribe: () => {
1593
+ const callbacks = this.listeners.get(id)
1594
+ if (callbacks) {
1595
+ callbacks.delete(callback)
1596
+ if (callbacks.size === 0) {
1597
+ this.listeners.delete(id)
1598
+ }
1599
+ }
1600
+ },
1601
+ }
1602
+
1603
+ return {
1604
+ data: { subscription },
1605
+ }
1606
+ }
1607
+
1608
+ /**
1609
+ * https://supabase.com/docs/reference/javascript/auth-signinwithpassword
1610
+ * Log in an existing user with an email and password or phone and password or username and password.
1611
+ * @param params
1612
+ * @returns Promise<SignInRes>
1613
+ */
1614
+ async signInWithPassword(params: SignInWithPasswordReq): Promise<SignInRes> {
1615
+ try {
1616
+ // 参数校验:username/email/phone三选一,password必填
1617
+ this.validateAtLeastOne(
1618
+ params,
1619
+ [['username'], ['email'], ['phone']],
1620
+ 'You must provide either username, email, or phone',
1621
+ )
1622
+ this.validateParams(params, {
1623
+ password: { required: true, message: 'Password is required' },
1624
+ })
1625
+
1626
+ await this.signIn({
1627
+ username: params.username || params.email || this.formatPhone(params.phone),
1628
+ password: params.password,
1629
+ ...(params.is_encrypt ? { isEncrypt: true, version: 'v2' } : {}),
1630
+ })
1631
+ const { data: { session } = {} } = await this.getSession()
1632
+
1633
+ return { data: { user: session.user, session }, error: null }
1634
+ } catch (error) {
1635
+ return { data: {}, error: new AuthError(error) }
1636
+ }
1637
+ }
1638
+
1639
+ /**
1640
+ * https://supabase.com/docs/reference/javascript/auth-signinwithidtoken
1641
+ * 第三方平台登录。如果用户不存在,会根据云开发平台-登录方式中对应身份源的登录模式配置,判断是否自动注册
1642
+ * @param params
1643
+ * @returns Promise<SignInRes>
1644
+ */
1645
+ async signInWithIdToken(params: SignInWithIdTokenReq): Promise<SignInRes> {
1646
+ try {
1647
+ // 参数校验:token必填
1648
+ this.validateParams(params, {
1649
+ token: { required: true, message: 'Token is required' },
1650
+ })
1651
+
1652
+ await this.signInWithProvider({
1653
+ provider_token: params.token,
1654
+ })
1655
+ const { data: { session } = {} } = await this.getSession()
1656
+
1657
+ return { data: { user: session.user, session }, error: null }
1658
+ } catch (error) {
1659
+ return { data: {}, error: new AuthError(error) }
1660
+ }
1661
+ }
1662
+
1663
+ /**
1664
+ * https://supabase.com/docs/reference/javascript/auth-signinwithotp
1665
+ * Log in a user using a one-time password (OTP).
1666
+ * @param params
1667
+ * @returns Promise<SignInWithOtpRes>
1668
+ */
1669
+ async signInWithOtp(params: SignInWithOtpReq): Promise<SignInWithOtpRes> {
1670
+ try {
1671
+ // 参数校验:email或phone必填其一
1672
+ this.validateAtLeastOne(params, [['email'], ['phone']], 'You must provide either an email or phone number')
1673
+
1674
+ // 第一步:发送验证码并存储 verificationInfo
1675
+ const verificationInfo = await this.getVerification(params.email ? { email: params.email } : { phone_number: this.formatPhone(params.phone) },)
1676
+
1677
+ return {
1678
+ data: {
1679
+ user: null,
1680
+ session: null,
1681
+ // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件)
1682
+ verifyOtp: async ({ token, messageId = verificationInfo.verification_id }): Promise<SignInRes> => this.verifyOtp({
1683
+ email: params.email,
1684
+ phone: params.phone,
1685
+ token,
1686
+ messageId,
1687
+ }),
1688
+ },
1689
+ error: null,
1690
+ }
1691
+ } catch (error) {
1692
+ return { data: {}, error: new AuthError(error) }
1693
+ }
1694
+ }
1695
+
1696
+ /**
1697
+ * 校验第三方平台授权登录回调
1698
+ * @param params
1699
+ * @returns Promise<SignInRes>
1700
+ */
1701
+ async verifyOAuth(params?: VerifyOAuthReq): Promise<SignInRes | LinkIdentityRes> {
1702
+ const data: any = {}
1703
+ try {
1704
+ // 回调至 provider_redirect_uri 地址(url query中携带 授权code,state等参数),此时检查 state 是否符合预期(如 自己设置的 wx_open)
1705
+ const code = params?.code || utils.getQuery('code')
1706
+ const state = params?.state || utils.getQuery('state')
1707
+
1708
+ // 参数校验:code和state必填
1709
+ if (!code) {
1710
+ return { data: {}, error: new AuthError({ message: 'Code is required' }) }
1711
+ }
1712
+
1713
+ if (!state) {
1714
+ return { data: {}, error: new AuthError({ message: 'State is required' }) }
1715
+ }
1716
+
1717
+ const cacheData = getBrowserSession(state)
1718
+ data.type = cacheData?.type
1719
+
1720
+ const provider = params?.provider || cacheData?.provider || utils.getQuery('provider')
1721
+
1722
+ if (!provider) {
1723
+ return { data, error: new AuthError({ message: 'Provider is required' }) }
1724
+ }
1725
+
1726
+ // state符合预期,则获取该三方平台token
1727
+ const { provider_token: token } = await this.grantProviderToken({
1728
+ provider_id: provider,
1729
+ provider_redirect_uri: location.origin + location.pathname, // 指定三方平台跳回的 url 地址
1730
+ provider_code: code, // 第三方平台跳转回页面时,url param 中携带的 code 参数
1731
+ })
1732
+
1733
+ let res: SignInRes | LinkIdentityRes
1734
+
1735
+ if (cacheData.type === OAUTH_TYPE.BIND_IDENTITY) {
1736
+ res = await this.oauthInstance.authApi.toBindIdentity({ provider_token: token, provider, fireEvent: true })
1737
+ } else {
1738
+ // 通过 provider_token 仅登录或登录并注册(与云开发平台-登录方式-身份源登录模式配置有关)
1739
+ res = await this.signInWithIdToken({
1740
+ token,
1741
+ })
1742
+ res.data = { ...data, ...res.data }
1743
+ }
1744
+
1745
+ const localSearch = new URLSearchParams(location?.search)
1746
+ localSearch.delete('code')
1747
+ localSearch.delete('state')
1748
+ addUrlSearch(
1749
+ cacheData?.search === undefined ? `?${localSearch.toString()}` : cacheData?.search,
1750
+ cacheData?.hash || location.hash,
1751
+ )
1752
+ removeBrowserSession(state)
1753
+
1754
+ return res
1755
+ } catch (error) {
1756
+ return { data, error: new AuthError(error) }
1757
+ }
1758
+ }
1759
+
1760
+ /**
1761
+ * https://supabase.com/docs/reference/javascript/auth-signinwithoauth
1762
+ * 生成第三方平台授权 Uri (如微信二维码扫码授权网页)
1763
+ * @param params
1764
+ * @returns Promise<SignInOAuthRes>
1765
+ */
1766
+ async signInWithOAuth(params: SignInWithOAuthReq): Promise<SignInOAuthRes> {
1767
+ try {
1768
+ // 参数校验:provider必填
1769
+ this.validateParams(params, {
1770
+ provider: { required: true, message: 'Provider is required' },
1771
+ })
1772
+
1773
+ const href = params.options?.redirectTo || location.href
1774
+
1775
+ const urlObject = new URL(href)
1776
+
1777
+ const provider_redirect_uri = urlObject.origin + urlObject.pathname
1778
+
1779
+ const state = params.options?.state || `prd-${params.provider}-${Math.random().toString(36)
1780
+ .slice(2)}`
1781
+
1782
+ const { uri } = await this.genProviderRedirectUri({
1783
+ provider_id: params.provider,
1784
+ provider_redirect_uri,
1785
+ state,
1786
+ })
1787
+
1788
+ // 对 URL 进行解码
1789
+ const decodedUri = decodeURIComponent(uri)
1790
+
1791
+ // 合并额外的查询参数
1792
+ let finalUri = decodedUri
1793
+
1794
+ if (params.options?.queryParams) {
1795
+ const url = new URL(decodedUri)
1796
+ Object.entries(params.options.queryParams).forEach(([key, value]) => {
1797
+ url.searchParams.set(key, value)
1798
+ })
1799
+ finalUri = url.toString()
1800
+ }
1801
+
1802
+ saveToBrowserSession(state, {
1803
+ provider: params.provider,
1804
+ search: urlObject.search,
1805
+ hash: urlObject.hash,
1806
+ type: params.options?.type || OAUTH_TYPE.SIGN_IN,
1807
+ })
1808
+
1809
+ if (isBrowser() && !params.options?.skipBrowserRedirect) {
1810
+ window.location.assign(finalUri)
1811
+ }
1812
+
1813
+ return { data: { url: finalUri, provider: params.provider }, error: null }
1814
+ } catch (error) {
1815
+ return { data: {}, error: new AuthError(error) }
1816
+ }
1817
+ }
1818
+
1819
+ // https://supabase.com/docs/reference/javascript/auth-signinwithsso
1820
+ async signInWithSSO() {
1821
+ //
1822
+ }
1823
+
1824
+ // https://supabase.com/docs/reference/javascript/auth-signinwithweb3
1825
+ async signInWithWeb3() {
1826
+ //
1827
+ }
1828
+
1829
+ // https://supabase.com/docs/reference/javascript/auth-getclaims
1830
+ async getClaims(): Promise<GetClaimsRes> {
1831
+ try {
1832
+ const { accessToken } = await this.getAccessToken()
1833
+ const parsedToken = weappJwtDecodeAll(accessToken)
1834
+ return { data: parsedToken, error: null }
1835
+ } catch (error) {
1836
+ return { data: {}, error: new AuthError(error) }
1837
+ }
1838
+ }
1839
+
1840
+ /**
1841
+ * https://supabase.com/docs/reference/javascript/auth-resetpasswordforemail
1842
+ * 通过 email 或手机号重置密码
1843
+ * @param emailOrPhone 邮箱或手机号
1844
+ * @returns Promise<ResetPasswordForEmailRes>
1845
+ */
1846
+ async resetPasswordForEmail(
1847
+ emailOrPhone: string,
1848
+ options?: { redirectTo?: string },
1849
+ ): Promise<ResetPasswordForEmailRes> {
1850
+ try {
1851
+ // 参数校验:emailOrPhone必填
1852
+ this.validateParams(
1853
+ { emailOrPhone },
1854
+ {
1855
+ emailOrPhone: { required: true, message: 'Email or phone is required' },
1856
+ },
1857
+ )
1858
+
1859
+ const { redirectTo } = options || {}
1860
+
1861
+ // 判断是邮箱还是手机号
1862
+ const isEmail = emailOrPhone.includes('@')
1863
+ let verificationParams: { email?: string; phone_number?: string }
1864
+
1865
+ if (isEmail) {
1866
+ verificationParams = { email: emailOrPhone }
1867
+ } else {
1868
+ // 正规化手机号
1869
+ const formattedPhone = this.formatPhone(emailOrPhone)
1870
+ verificationParams = { phone_number: formattedPhone }
1871
+ }
1872
+
1873
+ // 第一步:发送验证码并存储 verificationInfo
1874
+ const verificationInfo = await this.getVerification(verificationParams)
1875
+
1876
+ return {
1877
+ data: {
1878
+ // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件)
1879
+ updateUser: async (attributes: UpdateUserAttributes): Promise<SignInRes> => {
1880
+ this.validateParams(attributes, {
1881
+ nonce: { required: true, message: 'Nonce is required' },
1882
+ password: { required: true, message: 'Password is required' },
1883
+ })
1884
+ try {
1885
+ // 第三步:待用户输入完验证码之后,验证验证码
1886
+ const verificationTokenRes = await this.verify({
1887
+ verification_id: verificationInfo.verification_id,
1888
+ verification_code: attributes.nonce,
1889
+ })
1890
+
1891
+ await this.oauthInstance.authApi.resetPassword({
1892
+ email: isEmail ? emailOrPhone : undefined,
1893
+ phone: !isEmail ? emailOrPhone : undefined,
1894
+ new_password: attributes.password,
1895
+ verification_token: verificationTokenRes.verification_token,
1896
+ })
1897
+
1898
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.PASSWORD_RECOVERY })
1899
+
1900
+ const res = await this.signInWithPassword({
1901
+ email: isEmail ? emailOrPhone : undefined,
1902
+ phone: !isEmail ? emailOrPhone : undefined,
1903
+ password: attributes.password,
1904
+ })
1905
+
1906
+ if (redirectTo && isBrowser()) {
1907
+ window.location.assign(redirectTo)
1908
+ }
1909
+
1910
+ return res
1911
+ } catch (error) {
1912
+ return { data: {}, error: new AuthError(error) }
1913
+ }
1914
+ },
1915
+ },
1916
+ error: null,
1917
+ }
1918
+ } catch (error) {
1919
+ return { data: {}, error: new AuthError(error) }
1920
+ }
1921
+ }
1922
+
1923
+ /**
1924
+ * 通过旧密码重置密码
1925
+ * @param new_password
1926
+ * @param old_password
1927
+ * @returns
1928
+ */
1929
+ async resetPasswordForOld(params: ResetPasswordForOldReq) {
1930
+ try {
1931
+ await this.oauthInstance.authApi.updatePasswordByOld({
1932
+ old_password: params.old_password,
1933
+ new_password: params.new_password,
1934
+ })
1935
+
1936
+ const { data: { session } = {} } = await this.getSession()
1937
+
1938
+ return { data: { user: session.user, session }, error: null }
1939
+ } catch (error) {
1940
+ return { data: {}, error: new AuthError(error) }
1941
+ }
1942
+ }
1943
+
1944
+ /**
1945
+ * https://supabase.com/docs/reference/javascript/auth-verifyotp
1946
+ * Log in a user given a User supplied OTP and verificationId received through mobile or email.
1947
+ * @param params
1948
+ * @returns Promise<SignInRes>
1949
+ */
1950
+ async verifyOtp(params: VerifyOtpReq): Promise<SignInRes> {
1951
+ try {
1952
+ const { type } = params
1953
+ // 参数校验:token和verificationInfo必填
1954
+ this.validateParams(params, {
1955
+ token: { required: true, message: 'Token is required' },
1956
+ messageId: { required: true, message: 'messageId is required' },
1957
+ })
1958
+
1959
+ if (['phone_change', 'email_change'].includes(type)) {
1960
+ await this.verify({
1961
+ verification_id: params.messageId,
1962
+ verification_code: params.token,
1963
+ })
1964
+ } else {
1965
+ await this.signInWithUsername({
1966
+ verificationInfo: { verification_id: params.messageId, is_user: true },
1967
+ verificationCode: params.token,
1968
+ username: params.email || this.formatPhone(params.phone) || '',
1969
+ loginType: params.email ? 'email' : 'phone',
1970
+ })
1971
+ }
1972
+
1973
+ const { data: { session } = {} } = await this.getSession()
1974
+
1975
+ return { data: { user: session.user, session }, error: null }
1976
+ } catch (error) {
1977
+ return { data: {}, error: new AuthError(error) }
1978
+ }
1979
+ }
1980
+
1981
+ /**
1982
+ * https://supabase.com/docs/reference/javascript/auth-getSession
1983
+ * Returns the session, refreshing it if necessary.
1984
+ * @returns Promise<SignInRes>
1985
+ */
1986
+ async getSession(): Promise<SignInRes> {
1987
+ try {
1988
+ const credentials: Credentials = await this.oauthInstance.oauth2client.getCredentials()
1989
+
1990
+ if (!credentials || credentials.scope === 'accessKey') {
1991
+ return { data: { session: null }, error: null }
1992
+ }
1993
+
1994
+ const { data: { user } = {} } = await this.getUser()
1995
+
1996
+ return { data: { session: { ...credentials, user }, user }, error: null }
1997
+ } catch (error) {
1998
+ return { data: {}, error: new AuthError(error) }
1999
+ }
2000
+ }
2001
+
2002
+ /**
2003
+ * https://supabase.com/docs/reference/javascript/auth-refreshsession
2004
+ * 无论过期状态如何,都返回一个新的会话
2005
+ * @param refresh_token
2006
+ * @returns Promise<SignInRes>
2007
+ */
2008
+ async refreshSession(refresh_token?: string): Promise<SignInRes> {
2009
+ try {
2010
+ const credentials: Credentials = await this.oauthInstance.oauth2client.localCredentials.getCredentials()
2011
+ credentials.refresh_token = refresh_token || credentials.refresh_token
2012
+ const newTokens = await this.oauthInstance.oauth2client.refreshToken(credentials)
2013
+ const { data: { user } = {} } = await this.getUser()
2014
+
2015
+ return { data: { user, session: { ...newTokens, user } }, error: null }
2016
+ } catch (error) {
2017
+ return { data: {}, error: new AuthError(error) }
2018
+ }
2019
+ }
2020
+
2021
+ /**
2022
+ * https://supabase.com/docs/reference/javascript/auth-getuser
2023
+ * 如果存在现有会话,则获取当前用户详细信息
2024
+ * @returns Promise<GetUserRes>
2025
+ */
2026
+ async getUser(): Promise<GetUserRes> {
2027
+ try {
2028
+ const user = this.convertToUser(await this.getUserInfo())
2029
+ return { data: { user }, error: null }
2030
+ } catch (error) {
2031
+ return { data: {}, error: new AuthError(error) }
2032
+ }
2033
+ }
2034
+
2035
+ /**
2036
+ * 刷新用户信息
2037
+ * @returns Promise<CommonRes>
2038
+ */
2039
+ async refreshUser(): Promise<CommonRes> {
2040
+ try {
2041
+ await this.currentUser.refresh()
2042
+
2043
+ const { data: { session } = {} } = await this.getSession()
2044
+ return { data: { user: session.user, session }, error: null }
2045
+ } catch (error) {
2046
+ return { data: {}, error: new AuthError(error) }
2047
+ }
2048
+ }
2049
+
2050
+ /**
2051
+ * https://supabase.com/docs/reference/javascript/auth-updateuser
2052
+ * 更新用户信息
2053
+ * @param params
2054
+ * @returns Promise<GetUserRes | UpdateUserWithVerificationRes>
2055
+ */
2056
+ async updateUser(params: UpdateUserReq): Promise<GetUserRes | UpdateUserWithVerificationRes> {
2057
+ try {
2058
+ // 参数校验:至少有一个更新字段被提供
2059
+ const hasValue = Object.keys(params).some(key => params[key] !== undefined && params[key] !== null && params[key] !== '',)
2060
+ if (!hasValue) {
2061
+ throw new AuthError({ message: 'At least one field must be provided for update' })
2062
+ }
2063
+
2064
+ const { email, phone, ...restParams } = params
2065
+
2066
+ // 检查是否需要更新 email 或 phone
2067
+ const needsEmailVerification = email !== undefined
2068
+ const needsPhoneVerification = phone !== undefined
2069
+
2070
+ let extraRes = {}
2071
+
2072
+ if (needsEmailVerification || needsPhoneVerification) {
2073
+ // 需要发送验证码
2074
+ let verificationParams: { email?: string; phone_number?: string }
2075
+ let verificationType: 'email_change' | 'phone_change'
2076
+
2077
+ if (needsEmailVerification) {
2078
+ verificationParams = { email: params.email }
2079
+ verificationType = 'email_change'
2080
+ } else {
2081
+ // 正规化手机号
2082
+ const formattedPhone = this.formatPhone(params.phone)
2083
+ verificationParams = { phone_number: formattedPhone }
2084
+ verificationType = 'phone_change'
2085
+ }
2086
+
2087
+ // 发送验证码
2088
+ const verificationInfo = await this.getVerification(verificationParams)
2089
+
2090
+ Object.keys(restParams).length > 0 && await this.updateUserBasicInfo(restParams)
2091
+
2092
+ extraRes = {
2093
+ messageId: verificationInfo.verification_id,
2094
+ verifyOtp: async (verifyParams: { email?: string; phone?: string; token: string }): Promise<GetUserRes> => {
2095
+ try {
2096
+ if (verifyParams.email && params.email === verifyParams.email) {
2097
+ // 验证码验证
2098
+ await this.verifyOtp({
2099
+ type: 'email_change',
2100
+ email: params.email,
2101
+ token: verifyParams.token,
2102
+ messageId: verificationInfo.verification_id,
2103
+ })
2104
+ await this.updateUserBasicInfo({ email: params.email })
2105
+ } else if (verifyParams.phone && params.phone === verifyParams.phone) {
2106
+ // 验证码验证
2107
+ await this.verifyOtp({
2108
+ type: 'phone_change',
2109
+ phone: params.phone,
2110
+ token: verifyParams.token,
2111
+ messageId: verificationInfo.verification_id,
2112
+ })
2113
+ await this.updateUserBasicInfo({ phone: this.formatPhone(params.phone) })
2114
+ } else {
2115
+ await this.verifyOtp({
2116
+ type: verificationType,
2117
+ email: needsEmailVerification ? params.email : undefined,
2118
+ phone: !needsEmailVerification ? params.phone : undefined,
2119
+ token: verifyParams.token,
2120
+ messageId: verificationInfo.verification_id,
2121
+ })
2122
+ // 验证成功后更新用户信息
2123
+ await this.updateUserBasicInfo(params)
2124
+ }
2125
+
2126
+ const {
2127
+ data: { user },
2128
+ } = await this.getUser()
2129
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.USER_UPDATED })
2130
+
2131
+ return { data: { user }, error: null }
2132
+ } catch (error) {
2133
+ return { data: {}, error: new AuthError(error) }
2134
+ }
2135
+ },
2136
+ }
2137
+ } else {
2138
+ // 不需要验证,直接更新
2139
+ await this.updateUserBasicInfo(params)
2140
+ }
2141
+ const {
2142
+ data: { user },
2143
+ } = await this.getUser()
2144
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.USER_UPDATED })
2145
+
2146
+ return { data: { user, ...extraRes }, error: null }
2147
+ } catch (error) {
2148
+ return { data: {}, error: new AuthError(error) }
2149
+ }
2150
+ }
2151
+
2152
+ /**
2153
+ * https://supabase.com/docs/reference/javascript/auth-getuseridentities
2154
+ * 获取所有身份源
2155
+ * @returns Promise<GetUserIdentitiesRes>
2156
+ */
2157
+ async getUserIdentities(): Promise<GetUserIdentitiesRes> {
2158
+ try {
2159
+ const providers = await this.oauthInstance.authApi.getProviders()
2160
+
2161
+ return { data: { identities: providers?.data?.filter(v => !!v.bind) }, error: null }
2162
+ } catch (error) {
2163
+ return { data: {}, error: new AuthError(error) }
2164
+ }
2165
+ }
2166
+
2167
+ /**
2168
+ * https://supabase.com/docs/reference/javascript/auth-linkidentity
2169
+ * 绑定身份源到当前用户
2170
+ * @param params
2171
+ * @returns Promise<LinkIdentityRes>
2172
+ */
2173
+ async linkIdentity(params: LinkIdentityReq): Promise<LinkIdentityRes> {
2174
+ try {
2175
+ // 参数校验:provider必填
2176
+ this.validateParams(params, {
2177
+ provider: { required: true, message: 'Provider is required' },
2178
+ })
2179
+
2180
+ await this.signInWithOAuth({
2181
+ provider: params.provider,
2182
+ options: {
2183
+ type: OAUTH_TYPE.BIND_IDENTITY,
2184
+ },
2185
+ })
2186
+
2187
+ return { data: { provider: params.provider }, error: null }
2188
+ } catch (error) {
2189
+ return { data: {}, error: new AuthError(error) }
2190
+ }
2191
+ }
2192
+
2193
+ /**
2194
+ * https://supabase.com/docs/reference/javascript/auth-unlinkidentity
2195
+ * 解绑身份源
2196
+ * @param params
2197
+ * @returns Promise<CommonRes>
2198
+ */
2199
+ async unlinkIdentity(params: UnlinkIdentityReq): Promise<CommonRes> {
2200
+ try {
2201
+ // 参数校验:provider必填
2202
+ this.validateParams(params, {
2203
+ provider: { required: true, message: 'Provider is required' },
2204
+ })
2205
+
2206
+ await this.oauthInstance.authApi.unbindProvider({ provider_id: params.provider })
2207
+
2208
+ return { data: {}, error: null }
2209
+ } catch (error) {
2210
+ return { data: {}, error: new AuthError(error) }
2211
+ }
2212
+ }
2213
+
2214
+ /**
2215
+ * https://supabase.com/docs/reference/javascript/auth-reauthentication
2216
+ * 重新认证
2217
+ * @returns Promise<ReauthenticateRes>
2218
+ */
2219
+ async reauthenticate(): Promise<ReauthenticateRes> {
2220
+ try {
2221
+ const {
2222
+ data: { user },
2223
+ } = await this.getUser()
2224
+
2225
+ this.validateAtLeastOne(user, [['email', 'phone']], 'You must provide either an email or phone number')
2226
+ const userInfo = user.email ? { email: user.email } : { phone_number: this.formatPhone(user.phone) }
2227
+
2228
+ // 第一步:发送验证码并存储 verificationInfo
2229
+ const verificationInfo = await this.getVerification(userInfo)
2230
+
2231
+ return {
2232
+ data: {
2233
+ // 第二步:等待用户输入验证码(通过 Promise 包装用户输入事件)
2234
+ updateUser: async (attributes: UpdateUserAttributes): Promise<SignInRes> => {
2235
+ this.validateParams(attributes, {
2236
+ nonce: { required: true, message: 'Nonce is required' },
2237
+ })
2238
+ try {
2239
+ if (attributes.password) {
2240
+ // 第三步:待用户输入完验证码之后,验证验证码
2241
+ const verificationTokenRes = await this.verify({
2242
+ verification_id: verificationInfo.verification_id,
2243
+ verification_code: attributes.nonce,
2244
+ })
2245
+
2246
+ // 第四步:获取 sudo_token
2247
+ const sudoRes = await this.oauthInstance.authApi.sudo({
2248
+ verification_token: verificationTokenRes.verification_token,
2249
+ })
2250
+
2251
+ await this.oauthInstance.authApi.setPassword({
2252
+ new_password: attributes.password,
2253
+ sudo_token: sudoRes.sudo_token,
2254
+ })
2255
+ } else {
2256
+ await this.signInWithUsername({
2257
+ verificationInfo,
2258
+ verificationCode: attributes.nonce,
2259
+ ...userInfo,
2260
+ loginType: userInfo.email ? 'email' : 'phone',
2261
+ })
2262
+ }
2263
+
2264
+ const { data: { session } = {} } = await this.getSession()
2265
+
2266
+ return { data: { user: session.user, session }, error: null }
2267
+ } catch (error) {
2268
+ return { data: {}, error: new AuthError(error) }
2269
+ }
2270
+ },
2271
+ },
2272
+ error: null,
2273
+ }
2274
+ } catch (error) {
2275
+ return { data: {}, error: new AuthError(error) }
2276
+ }
2277
+ }
2278
+
2279
+ /**
2280
+ * https://supabase.com/docs/reference/javascript/auth-resend
2281
+ * 重新发送验证码
2282
+ * @param params
2283
+ * @returns Promise<ResendRes>
2284
+ */
2285
+ async resend(params: ResendReq): Promise<ResendRes> {
2286
+ try {
2287
+ // 参数校验:email或phone必填其一
2288
+ this.validateAtLeastOne(params, [['email'], ['phone']], 'You must provide either an email or phone number')
2289
+
2290
+ const target = params.type === 'signup' ? 'ANY' : 'USER'
2291
+ const data: { email?: string; phone_number?: string; target: 'USER' | 'ANY' } = { target }
2292
+ if ('email' in params) {
2293
+ data.email = params.email
2294
+ }
2295
+
2296
+ if ('phone' in params) {
2297
+ data.phone_number = this.formatPhone(params.phone)
2298
+ }
2299
+
2300
+ // 重新发送验证码
2301
+ const { verification_id: verificationId } = await this.oauthInstance.authApi.getVerification(data)
2302
+
2303
+ return {
2304
+ data: { messageId: verificationId },
2305
+ error: null,
2306
+ }
2307
+ } catch (error: any) {
2308
+ return {
2309
+ data: {},
2310
+ error: new AuthError(error),
2311
+ }
2312
+ }
2313
+ }
2314
+
2315
+ /**
2316
+ * https://supabase.com/docs/reference/javascript/auth-setsession
2317
+ * 使用access_token和refresh_token来设置会话
2318
+ * @param params
2319
+ * @returns Promise<SignInRes>
2320
+ */
2321
+ async setSession(params: SetSessionReq): Promise<SignInRes> {
2322
+ try {
2323
+ this.validateParams(params, {
2324
+ access_token: { required: true, message: 'Access token is required' },
2325
+ refresh_token: { required: true, message: 'Refresh token is required' },
2326
+ })
2327
+
2328
+ await this.oauthInstance.oauth2client.refreshToken(params, { throwOnError: true })
2329
+
2330
+ const { data: { session } = {} } = await this.getSession()
2331
+
2332
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.SIGNED_IN })
2333
+
2334
+ return { data: { user: session.user, session }, error: null }
2335
+ } catch (error) {
2336
+ return { data: {}, error: new AuthError(error) }
2337
+ }
2338
+ }
2339
+
2340
+ // https://supabase.com/docs/reference/javascript/auth-exchangecodeforsession
2341
+ async exchangeCodeForSession() {
2342
+ //
2343
+ }
2344
+
2345
+ /**
2346
+ * 删除当前用户
2347
+ * @param params
2348
+ * @returns
2349
+ */
2350
+ async deleteUser(params: DeleteMeReq): Promise<CommonRes> {
2351
+ try {
2352
+ this.validateParams(params, {
2353
+ password: { required: true, message: 'Password is required' },
2354
+ })
2355
+
2356
+ const { sudo_token } = await this.oauthInstance.authApi.sudo(params)
2357
+
2358
+ await this.oauthInstance.authApi.deleteMe({ sudo_token })
2359
+
2360
+ return { data: {}, error: null }
2361
+ } catch (error) {
2362
+ return { data: {}, error: new AuthError(error) }
2363
+ }
2364
+ }
2365
+
2366
+ /**
2367
+ * 跳转系统默认登录页
2368
+ * @returns {Promise<authModels.ToDefaultLoginPage>}
2369
+ * @memberof Auth
2370
+ */
2371
+ async toDefaultLoginPage(params: authModels.ToDefaultLoginPage = {}): Promise<CommonRes> {
2372
+ try {
2373
+ const configVersion = params.config_version || 'env'
2374
+ const query = Object.keys(params.query || {})
2375
+ .map(key => `${key}=${params.query[key]}`)
2376
+ .join('&')
2377
+
2378
+ if (adapterForWxMp.isMatch()) {
2379
+ wx.navigateTo({ url: `/packages/$wd_system/pages/login/index${query ? `?${query}` : ''}` })
2380
+ } else {
2381
+ const redirectUri = params.redirect_uri || window.location.href
2382
+ const urlObj = new URL(redirectUri)
2383
+ const loginPage = `${urlObj.origin}/__auth/?app_id=${params.app_id || ''}&env_id=${this.config.env}&client_id=${
2384
+ this.config.clientId || this.config.env
2385
+ }&config_version=${configVersion}&redirect_uri=${encodeURIComponent(redirectUri)}${query ? `&${query}` : ''}`
2386
+ window.location.href = loginPage
2387
+ }
2388
+ return { data: {}, error: null }
2389
+ } catch (error) {
2390
+ return { data: {}, error: new AuthError(error) }
2391
+ }
2392
+ }
2393
+
2394
+ /**
2395
+ * 自定义登录
2396
+ * @param getTickFn () => Promise<string>, 获取自定义登录 ticket 的函数
2397
+ * @returns
2398
+ */
2399
+ async signInWithCustomTicket(getTickFn?: authModels.GetCustomSignTicketFn): Promise<SignInRes> {
2400
+ if (getTickFn) {
2401
+ this.setCustomSignFunc(getTickFn)
2402
+ }
2403
+
2404
+ try {
2405
+ await this.oauthInstance.authApi.signInWithCustomTicket()
2406
+ const loginState = await this.createLoginState()
2407
+
2408
+ const { data: { session } = {} } = await this.getSession()
2409
+
2410
+ // loginState返回是为了兼容v2版本
2411
+ return { ...(loginState as any), data: { user: session.user, session }, error: null }
2412
+ } catch (error) {
2413
+ return { data: {}, error: new AuthError(error) }
2414
+ }
2415
+ }
2416
+
2417
+ /**
2418
+ * 小程序openId静默登录
2419
+ * @param params
2420
+ * @returns Promise<SignInRes>
2421
+ */
2422
+ async signInWithOpenId({ useWxCloud = true } = {}): Promise<SignInRes> {
2423
+ if (!adapterForWxMp.isMatch()) {
2424
+ throw Error('wx api undefined')
2425
+ }
2426
+ const wxInfo = wx.getAccountInfoSync().miniProgram
2427
+
2428
+ const mainFunc = async (code) => {
2429
+ let result: authModels.GrantProviderTokenResponse | undefined = undefined
2430
+ let credentials: Credentials | undefined = undefined
2431
+
2432
+ try {
2433
+ result = await this.oauthInstance.authApi.grantProviderToken(
2434
+ {
2435
+ provider_id: wxInfo?.appId,
2436
+ provider_code: code,
2437
+ provider_params: {
2438
+ provider_code_type: 'open_id',
2439
+ appid: wxInfo?.appId,
2440
+ },
2441
+ },
2442
+ useWxCloud,
2443
+ )
2444
+
2445
+ if ((result as any)?.error_code || !result.provider_token) {
2446
+ throw result
2447
+ }
2448
+
2449
+ credentials = await this.oauthInstance.authApi.signInWithProvider(
2450
+ { provider_token: result.provider_token },
2451
+ useWxCloud,
2452
+ )
2453
+
2454
+ if ((credentials as any)?.error_code) {
2455
+ throw credentials
2456
+ }
2457
+ } catch (error) {
2458
+ throw error
2459
+ }
2460
+ await this.oauthInstance.oauth2client.setCredentials(credentials as Credentials)
2461
+ }
2462
+
2463
+ try {
2464
+ await new Promise((resolve, reject) => {
2465
+ wx.login({
2466
+ success: async (res: { code: string }) => {
2467
+ try {
2468
+ await mainFunc(res.code)
2469
+ resolve(true)
2470
+ } catch (error) {
2471
+ reject(error)
2472
+ }
2473
+ },
2474
+ fail: (res: any) => {
2475
+ const error = new Error(res?.errMsg)
2476
+ ;(error as any).code = res?.errno
2477
+ reject(error)
2478
+ },
2479
+ })
2480
+ })
2481
+
2482
+ const loginState = await this.createLoginState()
2483
+
2484
+ const { data: { session } = {} } = await this.getSession()
2485
+
2486
+ // loginState返回是为了兼容v2版本
2487
+ return { ...(loginState as any), data: { user: session.user, session }, error: null }
2488
+ } catch (error) {
2489
+ return { data: {}, error: new AuthError(error) }
2490
+ }
2491
+ }
2492
+
2493
+ /**
2494
+ * 小程序手机号授权登录,目前只支持全托管手机号授权登录
2495
+ * @param params
2496
+ * @returns Promise<SignInRes>
2497
+ */
2498
+ async signInWithPhoneAuth({ phoneCode = '' }): Promise<SignInRes> {
2499
+ if (!adapterForWxMp.isMatch()) {
2500
+ return { data: {}, error: new AuthError({ message: 'wx api undefined' }) }
2501
+ }
2502
+ const wxInfo = wx.getAccountInfoSync().miniProgram
2503
+ const providerInfo = {
2504
+ provider_params: { provider_code_type: 'phone' },
2505
+ provider_id: wxInfo.appId,
2506
+ }
2507
+
2508
+ const { code } = await wx.login()
2509
+ ;(providerInfo as any).provider_code = code
2510
+
2511
+ try {
2512
+ let providerToken = await this.oauthInstance.authApi.grantProviderToken(providerInfo)
2513
+ if (providerToken.error_code) {
2514
+ throw providerToken
2515
+ }
2516
+
2517
+ providerToken = await this.oauthInstance.authApi.patchProviderToken({
2518
+ provider_token: providerToken.provider_token,
2519
+ provider_id: wxInfo.appId,
2520
+ provider_params: {
2521
+ code: phoneCode,
2522
+ provider_code_type: 'phone',
2523
+ },
2524
+ })
2525
+ if (providerToken.error_code) {
2526
+ throw providerToken
2527
+ }
2528
+
2529
+ const signInRes = await this.oauthInstance.authApi.signInWithProvider({
2530
+ provider_token: providerToken.provider_token,
2531
+ })
2532
+
2533
+ if ((signInRes as any)?.error_code) {
2534
+ throw signInRes
2535
+ }
2536
+ } catch (error) {
2537
+ return { data: {}, error: new AuthError(error) }
2538
+ }
2539
+
2540
+ const loginState = await this.createLoginState()
2541
+
2542
+ const { data: { session } = {} } = await this.getSession()
2543
+
2544
+ // loginState返回是为了兼容v2版本
2545
+ return { ...(loginState as any), data: { user: session.user, session }, error: null }
2546
+ }
2547
+
2548
+ private formatPhone(phone: string) {
2549
+ if (!/\s+/.test(phone) && /^\+\d{1,3}\d+/.test(phone)) {
2550
+ return phone.replace(/^(\+\d{1,2})(\d+)$/, '$1 $2')
2551
+ }
2552
+ return /^\+\d{1,3}\s+/.test(phone) ? phone : `+86 ${phone}`
2553
+ }
2554
+
2555
+ private notifyListeners(event, session, info): OnAuthStateChangeCallback {
2556
+ this.listeners.forEach((callbacks) => {
2557
+ callbacks.forEach((callback) => {
2558
+ try {
2559
+ callback(event, session, info)
2560
+ } catch (error) {
2561
+ console.error('Error in auth state change callback:', error)
2562
+ }
2563
+ })
2564
+ })
2565
+ return
2566
+ }
2567
+
2568
+ private setupListeners() {
2569
+ this.config.eventBus?.on(EVENTS.AUTH_STATE_CHANGED, async (params) => {
2570
+ const event = params?.data?.event
2571
+ const info = params?.data?.info
2572
+ const {
2573
+ data: { session },
2574
+ } = await this.getSession()
2575
+
2576
+ this.notifyListeners(event, session, info)
2577
+ })
2578
+ }
2579
+
2580
+ private convertToUser(userInfo: authModels.UserInfo & Partial<User>) {
2581
+ if (!userInfo) return null
2582
+
2583
+ // 优先使用 userInfo 中的数据(V3 API)
2584
+ const email = userInfo?.email || ''
2585
+ const phone = userInfo?.phone_number || ''
2586
+ const userId = userInfo?.sub || userInfo?.uid || ''
2587
+
2588
+ return {
2589
+ id: userId,
2590
+ aud: 'authenticated',
2591
+ role: userInfo.groups,
2592
+ email: email || '',
2593
+ email_confirmed_at: userInfo?.email_verified ? userInfo.created_at : userInfo.created_at,
2594
+ phone,
2595
+ phone_confirmed_at: phone ? userInfo.created_at : undefined,
2596
+ confirmed_at: userInfo.created_at,
2597
+ last_sign_in_at: userInfo.last_sign_in_at,
2598
+ app_metadata: {
2599
+ provider: userInfo.loginType?.toLowerCase() || 'cloudbase',
2600
+ providers: [userInfo.loginType?.toLowerCase() || 'cloudbase'],
2601
+ },
2602
+ user_metadata: {
2603
+ // V3 API 用户信息
2604
+ name: userInfo?.name,
2605
+ picture: userInfo?.picture,
2606
+ username: userInfo?.username, // 用户名称,长度 5-24 位,支持字符中英文、数字、特殊字符(仅支持_-),不支持中文
2607
+ gender: userInfo?.gender,
2608
+ locale: userInfo?.locale,
2609
+ // V2 API 兼容(使用 any 避免类型错误)
2610
+ uid: userInfo.uid,
2611
+ nickName: userInfo.nickName,
2612
+ avatarUrl: userInfo.avatarUrl || userInfo.picture,
2613
+ location: userInfo.location,
2614
+ hasPassword: userInfo.hasPassword,
2615
+ },
2616
+ identities:
2617
+ userInfo?.providers?.map(p => ({
2618
+ id: p.id || '',
2619
+ identity_id: p.id || '',
2620
+ user_id: userId,
2621
+ identity_data: {
2622
+ provider_id: p.id,
2623
+ provider_user_id: p.provider_user_id,
2624
+ name: p.name,
2625
+ },
2626
+ provider: p.id || 'cloudbase',
2627
+ created_at: userInfo.created_at,
2628
+ updated_at: userInfo.updated_at,
2629
+ last_sign_in_at: userInfo.last_sign_in_at,
2630
+ })) || [],
2631
+ created_at: userInfo.created_at,
2632
+ updated_at: userInfo.updated_at,
2633
+ is_anonymous: userInfo.name === 'anonymous',
2634
+ }
2635
+ }
2636
+
2637
+ /**
2638
+ * 参数校验辅助方法
2639
+ */
2640
+ private validateParams(params: any, rules: { [key: string]: { required?: boolean; message: string } }): void {
2641
+ for (const [key, rule] of Object.entries(rules)) {
2642
+ if (rule.required && (params?.[key] === undefined || params?.[key] === null || params?.[key] === '')) {
2643
+ throw new AuthError({ message: rule.message })
2644
+ }
2645
+ }
2646
+ }
2647
+
2648
+ /**
2649
+ * 校验必填参数组(至少有一个参数必须有值)
2650
+ */
2651
+ private validateAtLeastOne(params: any, fieldGroups: string[][], message: string): void {
2652
+ const hasValue = fieldGroups.some(group => group.some(field => params?.[field] !== undefined && params?.[field] !== null && params?.[field] !== ''),)
2653
+
2654
+ if (!hasValue) {
2655
+ throw new AuthError({ message })
2656
+ }
2657
+ }
2658
+
2659
+
2660
+ private async init(): Promise<{ error: Error | null }> {
2661
+ try {
2662
+ const credentials: Credentials = await this.oauthInstance.oauth2client.localCredentials.getCredentials()
2663
+ if (credentials) {
2664
+ this.config.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.INITIAL_SESSION })
2665
+ }
2666
+ } catch (error) {
2667
+ // Ignore errors when checking for existing credentials
2668
+ }
2669
+
2670
+ return { error: null }
2671
+ }
1410
2672
  }
1411
2673
 
1412
2674
  type TInitAuthOptions = Pick<ICloudbaseAuthConfig, 'region' | 'persistence' | 'i18n' | 'accessKey' | 'useWxCloud'> &