@cloudbase/oauth 2.23.3 → 2.24.0

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.
Files changed (43) hide show
  1. package/dist/cjs/auth/apis.js +170 -14
  2. package/dist/cjs/auth/auth-error.js +32 -0
  3. package/dist/cjs/auth/consts.js +24 -2
  4. package/dist/cjs/auth/models.js +1 -1
  5. package/dist/cjs/captcha/captcha-dom.js +223 -0
  6. package/dist/cjs/captcha/captcha.js +11 -102
  7. package/dist/cjs/index.js +25 -3
  8. package/dist/cjs/oauth2client/interface.js +1 -1
  9. package/dist/cjs/oauth2client/models.js +1 -1
  10. package/dist/cjs/oauth2client/oauth2client.js +384 -110
  11. package/dist/cjs/utils/base64.js +15 -2
  12. package/dist/esm/auth/apis.js +113 -2
  13. package/dist/esm/auth/auth-error.js +9 -0
  14. package/dist/esm/auth/consts.js +22 -0
  15. package/dist/esm/captcha/captcha-dom.js +129 -0
  16. package/dist/esm/captcha/captcha.js +14 -97
  17. package/dist/esm/index.js +18 -2
  18. package/dist/esm/oauth2client/oauth2client.js +162 -36
  19. package/dist/esm/utils/base64.js +12 -0
  20. package/dist/miniprogram/index.js +1 -1
  21. package/dist/types/auth/apis.d.ts +19 -1
  22. package/dist/types/auth/auth-error.d.ts +6 -0
  23. package/dist/types/auth/consts.d.ts +22 -0
  24. package/dist/types/auth/models.d.ts +2 -0
  25. package/dist/types/captcha/captcha-dom.d.ts +3 -0
  26. package/dist/types/captcha/captcha.d.ts +3 -1
  27. package/dist/types/index.d.ts +11 -2
  28. package/dist/types/oauth2client/interface.d.ts +1 -1
  29. package/dist/types/oauth2client/models.d.ts +14 -0
  30. package/dist/types/oauth2client/oauth2client.d.ts +58 -2
  31. package/dist/types/utils/base64.d.ts +5 -0
  32. package/package.json +4 -4
  33. package/src/auth/apis.ts +189 -4
  34. package/src/auth/auth-error.ts +21 -0
  35. package/src/auth/consts.ts +28 -0
  36. package/src/auth/models.ts +2 -0
  37. package/src/captcha/captcha-dom.ts +178 -0
  38. package/src/captcha/captcha.ts +25 -115
  39. package/src/index.ts +51 -3
  40. package/src/oauth2client/interface.ts +1 -1
  41. package/src/oauth2client/models.ts +28 -0
  42. package/src/oauth2client/oauth2client.ts +254 -34
  43. package/src/utils/base64.ts +12 -0
@@ -1,5 +1,5 @@
1
1
  import { ErrorType } from './consts'
2
- import { ApiUrls, ApiUrlsV2, AUTH_API_PREFIX } from '../auth/consts'
2
+ import { ApiUrls, ApiUrlsV2, AUTH_API_PREFIX, AUTH_STATE_CHANGED_TYPE, EVENTS } from '../auth/consts'
3
3
 
4
4
  import { AuthClient, SimpleStorage } from './interface'
5
5
 
@@ -315,6 +315,20 @@ export class OAuth2Client implements AuthClient {
315
315
  private static maxRetry = 5
316
316
  private static retryInterval = 1000
317
317
 
318
+ public localCredentials: LocalCredentials
319
+ /**
320
+ * Keeps track of the async client initialization.
321
+ * When null or not yet resolved the auth state is `unknown`
322
+ * Once resolved the auth state is known and it's safe to call any further client methods.
323
+ */
324
+ public initializePromise: Promise<{ error: Error | null }> | null = null
325
+ protected lockAcquired = false
326
+ protected pendingInLock: Promise<any>[] = []
327
+ protected logDebugMessages: boolean
328
+ protected getInitialSession?: () => Promise<{
329
+ data: { session: Credentials; user?: any } | null
330
+ error: Error | null
331
+ }>
318
332
  private apiOrigin: string
319
333
  private apiPath: string
320
334
  private clientId: string
@@ -322,7 +336,6 @@ export class OAuth2Client implements AuthClient {
322
336
  private retry: number
323
337
  private clientSecret?: string
324
338
  private baseRequest: <T>(url: string, options?: RequestOptions) => Promise<T>
325
- private localCredentials: LocalCredentials
326
339
  private storage: SimpleStorage
327
340
  private deviceID?: string
328
341
  private tokenInURL?: boolean
@@ -332,8 +345,10 @@ export class OAuth2Client implements AuthClient {
332
345
  private anonymousSignInFunc: (Credentials) => Promise<Credentials | void>
333
346
  private wxCloud: any
334
347
  private useWxCloud: boolean
348
+ private eventBus: any
335
349
  private basicAuth: string
336
350
  private onCredentialsError: AuthOptions['onCredentialsError'] | undefined
351
+ private onInitialSessionObtained?: (data: { session: Credentials; user?: any }, error?: any) => void | Promise<void>
337
352
 
338
353
  /**
339
354
  * constructor
@@ -352,6 +367,7 @@ export class OAuth2Client implements AuthClient {
352
367
  this.apiPath = options.apiPath || AUTH_API_PREFIX
353
368
  this.clientId = options.clientId
354
369
  this.i18n = options.i18n
370
+ this.eventBus = options.eventBus
355
371
  this.singlePromise = new SinglePromise({ clientId: this.clientId })
356
372
  this.retry = this.formatRetry(options.retry, OAuth2Client.defaultRetry)
357
373
  if (options.baseRequest) {
@@ -389,9 +405,56 @@ export class OAuth2Client implements AuthClient {
389
405
  this.anonymousSignInFunc = options.anonymousSignInFunc
390
406
  this.onCredentialsError = options.onCredentialsError
391
407
 
392
- langEvent.bus.on(langEvent.LANG_CHANGE_EVENT, (params) => {
408
+ // New options for session detection
409
+ this.getInitialSession = options.getInitialSession
410
+ this.onInitialSessionObtained = options.onInitialSessionObtained
411
+ this.logDebugMessages = options.debug ?? false
412
+
413
+ langEvent.bus.on(langEvent.LANG_CHANGE_EVENT, (params: any) => {
393
414
  this.i18n = params.data?.i18n || this.i18n
394
415
  })
416
+
417
+ // Note: Auto-initialize is NOT called here to allow CloudbaseOAuth to set getInitialSession first
418
+ // CloudbaseOAuth.constructor will call initialize() after setting up the callback
419
+ }
420
+
421
+ /**
422
+ * Sets the getInitialSession callback.
423
+ * This callback is called during initialize() to get the initial session (e.g., from OAuth callback in URL).
424
+ * @param callback The callback function to get initial session
425
+ */
426
+ public setGetInitialSession(callback: () => Promise<{
427
+ data: { session: Credentials; user?: any } | null
428
+ error: Error | null
429
+ }>,): void {
430
+ this.getInitialSession = callback
431
+ }
432
+
433
+ /**
434
+ * Sets the onInitialSessionObtained callback.
435
+ * This callback is invoked after initial session is obtained and stored,
436
+ * allowing upper layers to handle user info storage.
437
+ * @param callback The callback function to handle session and user data
438
+ */
439
+ public setOnInitialSessionObtained(callback: (data: { session: Credentials; user?: any }) => void | Promise<void>,): void {
440
+ this.onInitialSessionObtained = callback
441
+ }
442
+
443
+ /**
444
+ * Initializes the client session either from the url or from storage.
445
+ * This method is automatically called when instantiating the client with detectSessionInUrl=true,
446
+ * but should also be called manually when checking for an error from an auth redirect.
447
+ * @param onInitialSessionObtained Optional callback to set before initialization starts
448
+ */
449
+ async initialize(): Promise<{ error: Error | null }> {
450
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
451
+ if (this.initializePromise) {
452
+ return await this.initializePromise
453
+ }
454
+
455
+ this.initializePromise = (async () => await this._acquireLock(-1, async () => await this._initialize()))()
456
+
457
+ return await this.initializePromise
395
458
  }
396
459
 
397
460
  /**
@@ -399,8 +462,10 @@ export class OAuth2Client implements AuthClient {
399
462
  * @param {Credentials} credentials
400
463
  * @return {Promise<void>}
401
464
  */
402
- public setCredentials(credentials?: Credentials): Promise<void> {
403
- return this.localCredentials.setCredentials(credentials)
465
+ public async setCredentials(credentials?: Credentials): Promise<void> {
466
+ // If initialization is in progress, wait for it first
467
+ await this.initializePromise
468
+ return this._acquireLock(-1, async () => this.localCredentials.setCredentials(credentials))
404
469
  }
405
470
 
406
471
  /**
@@ -415,6 +480,8 @@ export class OAuth2Client implements AuthClient {
415
480
  * getAccessToken return a validate access token
416
481
  */
417
482
  public async getAccessToken(): Promise<string> {
483
+ // If initialization is in progress, wait for it first
484
+ await this.initializePromise
418
485
  const credentials: Credentials = await this.getCredentials()
419
486
  if (credentials?.access_token) {
420
487
  return Promise.resolve(credentials.access_token)
@@ -451,7 +518,9 @@ export class OAuth2Client implements AuthClient {
451
518
  options.headers.Authorization = this.basicAuth
452
519
  }
453
520
  if (options?.withCredentials) {
454
- const credentials = await this.getCredentials()
521
+ // Use custom getCredentials function if provided, otherwise use default
522
+ // Custom getCredentials avoids default getCredentials() call which may cause deadlock during initialization
523
+ const credentials = options.getCredentials ? await options.getCredentials() : await this.getCredentials()
455
524
  if (credentials) {
456
525
  if (this.tokenInURL) {
457
526
  if (url.indexOf('?') < 0) {
@@ -543,33 +612,10 @@ export class OAuth2Client implements AuthClient {
543
612
  * Get credentials.
544
613
  */
545
614
  public async getCredentials(): Promise<Credentials | null> {
546
- let credentials: Credentials = await this.localCredentials.getCredentials()
547
- if (!credentials) {
548
- const msg = 'credentials not found'
549
- this.onCredentialsError?.({ msg })
550
- return this.unAuthenticatedError(msg)
551
- }
552
- if (isCredentialsExpired(credentials)) {
553
- if (credentials.refresh_token) {
554
- try {
555
- credentials = await this.refreshToken(credentials)
556
- } catch (error) {
557
- if (credentials.scope === 'anonymous') {
558
- credentials = await this.anonymousLogin(credentials)
559
- } else {
560
- this.onCredentialsError?.({ msg: error.error_description })
561
- return Promise.reject(error)
562
- }
563
- }
564
- } else if (credentials.scope === 'anonymous') {
565
- credentials = await this.anonymousLogin(credentials)
566
- } else {
567
- const msg = 'no refresh token found in credentials'
568
- this.onCredentialsError?.({ msg })
569
- return this.unAuthenticatedError(msg)
570
- }
571
- }
572
- return credentials
615
+ // If initialization is in progress, wait for it first
616
+ await this.initializePromise
617
+
618
+ return this._acquireLock(-1, async () => this._getCredentials())
573
619
  }
574
620
 
575
621
  /**
@@ -609,7 +655,17 @@ export class OAuth2Client implements AuthClient {
609
655
  * @param {Credentials} credentials
610
656
  * @return {Promise<Credentials>}
611
657
  */
612
- public async refreshToken(credentials: Credentials): Promise<Credentials> {
658
+ public async refreshToken(credentials: Credentials, options?: { throwError?: boolean }): Promise<Credentials> {
659
+ // If initialization is in progress, wait for it first
660
+ await this.initializePromise
661
+
662
+ return this._acquireLock(-1, async () => this._refreshToken(credentials, options))
663
+ }
664
+
665
+ /**
666
+ * Internal refresh token method (called within lock)
667
+ */
668
+ private async _refreshToken(credentials: Credentials, options?: { throwError?: boolean }): Promise<Credentials> {
613
669
  return this.singlePromise.run('_refreshToken', async () => {
614
670
  if (!credentials || !credentials.refresh_token) {
615
671
  const msg = 'no refresh token found in credentials'
@@ -619,8 +675,14 @@ export class OAuth2Client implements AuthClient {
619
675
  try {
620
676
  const newCredentials: Credentials = await this.refreshTokenFunc(credentials.refresh_token, credentials)
621
677
  await this.localCredentials.setCredentials(newCredentials)
678
+
679
+ this.eventBus?.fire(EVENTS.AUTH_STATE_CHANGED, { event: AUTH_STATE_CHANGED_TYPE.TOKEN_REFRESHED })
680
+
622
681
  return newCredentials
623
682
  } catch (error) {
683
+ if (options?.throwError) {
684
+ throw error
685
+ }
624
686
  if (error.error === ErrorType.INVALID_GRANT) {
625
687
  await this.localCredentials.setCredentials(null)
626
688
  const msg = error.error_description
@@ -777,4 +839,162 @@ export class OAuth2Client implements AuthClient {
777
839
  }
778
840
  return Promise.reject(respErr)
779
841
  }
842
+
843
+ /**
844
+ * Debug logging helper
845
+ */
846
+ private _debug(...args: any[]): void {
847
+ if (this.logDebugMessages) {
848
+ console.log('[OAuth2Client]', ...args)
849
+ }
850
+ }
851
+
852
+ /**
853
+ * IMPORTANT:
854
+ * 1. Never throw in this method, as it is called from the constructor
855
+ * 2. Never return a session from this method as it would be cached over
856
+ * the whole lifetime of the client
857
+ */
858
+ private async _initialize(): Promise<{ error: Error | null }> {
859
+ try {
860
+ // If no getInitialSession callback is set, nothing to do
861
+ if (!this.getInitialSession) {
862
+ this._debug('#_initialize()', 'no getInitialSession callback set, skipping')
863
+ return { error: null }
864
+ }
865
+
866
+ this._debug('#_initialize()', 'calling getInitialSession callback')
867
+
868
+ try {
869
+ const { data, error } = await this.getInitialSession()
870
+
871
+ if (data?.session) {
872
+ this._debug('#_initialize()', 'session obtained from getInitialSession', data.session)
873
+ await this.localCredentials.setCredentials(data?.session)
874
+ }
875
+
876
+ // Invoke callback for upper layer to handle user storage
877
+ // This is called BEFORE lock is released so user storage is atomic with session
878
+ if (this.onInitialSessionObtained) {
879
+ this._debug('#_initialize()', 'calling onInitialSessionObtained callback')
880
+ try {
881
+ await this.onInitialSessionObtained(data, error)
882
+ } catch (callbackError) {
883
+ this._debug('#_initialize()', 'error in onInitialSessionObtained', callbackError)
884
+ // Don't fail initialization if callback fails
885
+ }
886
+ }
887
+ if (error) {
888
+ this._debug('#_initialize()', 'error from getInitialSession', error)
889
+ return { error }
890
+ }
891
+
892
+ return { error: null }
893
+ } catch (err) {
894
+ this._debug('#_initialize()', 'exception during getInitialSession', err)
895
+ return { error: err instanceof Error ? err : new Error(String(err)) }
896
+ }
897
+ } catch (error) {
898
+ this._debug('#_initialize()', 'unexpected error', error)
899
+ return { error: error instanceof Error ? error : new Error(String(error)) }
900
+ } finally {
901
+ this._debug('#_initialize()', 'end')
902
+ }
903
+ }
904
+
905
+ /**
906
+ * Acquires a global lock based on the client ID.
907
+ * This ensures that only one operation can modify credentials at a time.
908
+ */
909
+ private async _acquireLock<R>(acquireTimeout: number, fn: () => Promise<R>): Promise<R> {
910
+ this._debug('#_acquireLock', 'begin', acquireTimeout)
911
+
912
+ try {
913
+ if (this.lockAcquired) {
914
+ // Lock is already acquired, queue the operation
915
+ const last = this.pendingInLock.length ? this.pendingInLock[this.pendingInLock.length - 1] : Promise.resolve()
916
+
917
+ const result = (async () => {
918
+ await last
919
+ return await fn()
920
+ })()
921
+
922
+ this.pendingInLock.push((async () => {
923
+ try {
924
+ await result
925
+ } catch (_e: any) {
926
+ // we just care if it finished
927
+ }
928
+ })(),)
929
+
930
+ return result
931
+ }
932
+
933
+ // Acquire the lock
934
+ this._debug('#_acquireLock', 'acquiring lock for client', this.clientId)
935
+
936
+ try {
937
+ this.lockAcquired = true
938
+
939
+ const result = fn()
940
+
941
+ this.pendingInLock.push((async () => {
942
+ try {
943
+ await result
944
+ } catch (_e: any) {
945
+ // we just care if it finished
946
+ }
947
+ })(),)
948
+
949
+ await result
950
+
951
+ // keep draining the queue until there's nothing to wait on
952
+ while (this.pendingInLock.length) {
953
+ const waitOn = [...this.pendingInLock]
954
+ await Promise.all(waitOn)
955
+ this.pendingInLock.splice(0, waitOn.length)
956
+ }
957
+
958
+ return await result
959
+ } finally {
960
+ this._debug('#_acquireLock', 'releasing lock for client', this.clientId)
961
+ this.lockAcquired = false
962
+ }
963
+ } finally {
964
+ this._debug('#_acquireLock', 'end')
965
+ }
966
+ }
967
+
968
+ /**
969
+ * Internal method to get credentials (called within lock)
970
+ */
971
+ private async _getCredentials(): Promise<Credentials | null> {
972
+ let credentials: Credentials = await this.localCredentials.getCredentials()
973
+ if (!credentials) {
974
+ const msg = 'credentials not found'
975
+ this.onCredentialsError?.({ msg })
976
+ return this.unAuthenticatedError(msg)
977
+ }
978
+ if (isCredentialsExpired(credentials)) {
979
+ if (credentials.refresh_token) {
980
+ try {
981
+ credentials = await this._refreshToken(credentials)
982
+ } catch (error) {
983
+ if (credentials.scope === 'anonymous') {
984
+ credentials = await this.anonymousLogin(credentials)
985
+ } else {
986
+ this.onCredentialsError?.({ msg: error.error_description })
987
+ return Promise.reject(error)
988
+ }
989
+ }
990
+ } else if (credentials.scope === 'anonymous') {
991
+ credentials = await this.anonymousLogin(credentials)
992
+ } else {
993
+ const msg = 'no refresh token found in credentials'
994
+ this.onCredentialsError?.({ msg })
995
+ return this.unAuthenticatedError(msg)
996
+ }
997
+ }
998
+ return credentials
999
+ }
780
1000
  }
@@ -98,3 +98,15 @@ export function weappJwtDecode(token: string, options?: any) {
98
98
  throw new Error(`Invalid token specified: ${e}` ? (e as any).message : '')
99
99
  }
100
100
  }
101
+
102
+ export function weappJwtDecodeAll(token: string) {
103
+ if (typeof token !== 'string') {
104
+ throw new Error('Invalid token specified')
105
+ }
106
+ try {
107
+ const parts = token.split('.').map((segment, i) => i === 2 ? segment : JSON.parse(base64_url_decode(segment)))
108
+ return { claims: parts[1], header: parts[0], signature: parts[2]}
109
+ } catch (e) {
110
+ throw new Error(`Invalid token specified: ${e}` ? (e as any).message : '')
111
+ }
112
+ }