@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
@@ -0,0 +1,178 @@
1
+ import { CaptchaToken } from '@cloudbase/adapter-interface'
2
+ import { CloudbaseEventEmitter } from '@cloudbase/utilities/dist/cjs/libs/events'
3
+ import { parseCaptcha } from '@cloudbase/utilities/dist/cjs/libs/util'
4
+ import { Auth } from '../auth/apis'
5
+
6
+ // 防抖函数实现
7
+ const debounce = (func: Function, delay: number) => {
8
+ let timeoutId: NodeJS.Timeout
9
+ return (...args: any[]) => {
10
+ clearTimeout(timeoutId)
11
+ timeoutId = setTimeout(() => func.apply(null, args), delay)
12
+ }
13
+ }
14
+
15
+ const eventBus = new CloudbaseEventEmitter()
16
+
17
+ const CAPTCHA_EVENT = {
18
+ CAPTCHA_DATA_CHANGE: 'captchaDataChange',
19
+ RESOLVE_CAPTCHA_DATA: 'resolveCaptchaData',
20
+ }
21
+
22
+ // 图形验证码弹窗样式类
23
+ const CAPTCHA_STYLES = {
24
+ overlay: 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:999',
25
+ modal:
26
+ 'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:320px;padding:20px;background:#fff;border-radius:8px;box-shadow:0 0 10px rgba(0,0,0,0.2);z-index:1000',
27
+ prompt: 'margin-bottom:15px',
28
+ captchaWrap: 'display:flex;align-items:center;justify-content:space-between;margin-bottom:15px',
29
+ captchaImg: 'width:100%;height:auto;border:1px solid #eee',
30
+ refreshBtn:
31
+ 'padding:10px 8px;background:#007bff;color:#fff;border:none;border-radius:4px;cursor:pointer;flex:1;white-space:nowrap;margin-left:10px',
32
+ input:
33
+ 'width:100%;padding:14px 16px;margin-bottom:15px;box-sizing:border-box;border:2px solid #e8ecf4;border-radius:10px;font-size:15px',
34
+ confirmBtn: 'width:100%;padding:10px;background:#007bff;color:#fff;border:none;border-radius:4px;cursor:pointer',
35
+ loading: 'background:#6c757d;cursor:not-allowed',
36
+ }
37
+
38
+ /**
39
+ * 显示图形图形验证码
40
+ * @param param0
41
+ */
42
+ const genCaptchaDom = ({ oauthInstance, captchaState }) => {
43
+ const CAPTCHA_IMG_ID = 'captcha-image'
44
+
45
+ // 刷新图形验证码函数
46
+ const refreshCaptcha = async () => {
47
+ try {
48
+ const result = await oauthInstance.createCaptchaData({ state: captchaState.state })
49
+ captchaState = { ...captchaState, captchaData: result.data, token: result.token }
50
+ ;(document.getElementById(CAPTCHA_IMG_ID) as HTMLImageElement).src = result.data
51
+ } catch (error) {
52
+ console.error('刷新图形验证码失败', error)
53
+ }
54
+ }
55
+
56
+ // 创建元素并设置样式
57
+ const createElement = (tag: string, styles?: string, text?: string) => {
58
+ const el = document.createElement(tag)
59
+ if (styles) el.style.cssText = styles
60
+ if (text) el.textContent = text
61
+ return el
62
+ }
63
+
64
+ // 创建遮罩层和弹窗
65
+ const overlay = createElement('div', CAPTCHA_STYLES.overlay)
66
+ const modal = createElement('div', CAPTCHA_STYLES.modal)
67
+
68
+ // 添加提示文字
69
+ modal.appendChild(createElement('p', CAPTCHA_STYLES.prompt, '请输入你看到的字符:'))
70
+
71
+ // 图形验证码区域
72
+ const captchaWrap = createElement('div', CAPTCHA_STYLES.captchaWrap)
73
+ const captchaImg = createElement('img', CAPTCHA_STYLES.captchaImg) as HTMLImageElement
74
+ captchaImg.id = CAPTCHA_IMG_ID
75
+ captchaImg.src = captchaState.captchaData
76
+ captchaWrap.appendChild(captchaImg)
77
+
78
+ // 刷新按钮
79
+ const refreshBtn = createElement('button', CAPTCHA_STYLES.refreshBtn, '刷新') as HTMLButtonElement
80
+ refreshBtn.onclick = debounce(async () => {
81
+ refreshBtn.textContent = '加载中...'
82
+ refreshBtn.disabled = true
83
+ refreshBtn.style.cssText = `${CAPTCHA_STYLES.refreshBtn};${CAPTCHA_STYLES.loading}`
84
+
85
+ try {
86
+ await refreshCaptcha()
87
+ } finally {
88
+ refreshBtn.textContent = '刷新'
89
+ refreshBtn.disabled = false
90
+ refreshBtn.style.cssText = CAPTCHA_STYLES.refreshBtn
91
+ }
92
+ }, 500)
93
+ captchaWrap.appendChild(refreshBtn)
94
+ modal.appendChild(captchaWrap)
95
+
96
+ // 输入框
97
+ const input = createElement('input', CAPTCHA_STYLES.input) as HTMLInputElement
98
+ input.type = 'text'
99
+ input.placeholder = '输入字符'
100
+ input.maxLength = 4
101
+ modal.appendChild(input)
102
+
103
+ // 错误提示元素
104
+ const errorMsg = createElement(
105
+ 'div',
106
+ 'color:#dc3545;font-size:12px;margin-bottom:10px;display:none;padding:8px;background:#f8d7da;border:1px solid #f5c6cb;border-radius:4px',
107
+ )
108
+ modal.appendChild(errorMsg)
109
+
110
+ // 显示错误提示
111
+ const showError = (message: string) => {
112
+ errorMsg.textContent = message
113
+ errorMsg.style.display = 'block'
114
+ }
115
+
116
+ // 确定按钮
117
+ const confirmBtn = createElement('button', CAPTCHA_STYLES.confirmBtn, '确定') as HTMLButtonElement
118
+ confirmBtn.onclick = debounce(async () => {
119
+ const inputValue = input.value.trim()
120
+ if (!inputValue) {
121
+ showError('请输入字符')
122
+ return
123
+ }
124
+
125
+ // 隐藏之前的错误提示
126
+ errorMsg.style.display = 'none'
127
+
128
+ confirmBtn.textContent = '验证中...'
129
+ confirmBtn.disabled = true
130
+ confirmBtn.style.cssText = `${CAPTCHA_STYLES.confirmBtn};${CAPTCHA_STYLES.loading}`
131
+
132
+ try {
133
+ const verifyResult = await oauthInstance.verifyCaptchaData({
134
+ token: captchaState.token,
135
+ key: inputValue,
136
+ })
137
+ eventBus.fire(CAPTCHA_EVENT.RESOLVE_CAPTCHA_DATA, verifyResult)
138
+ console.log('图形验证码校验成功')
139
+ document.body.removeChild(overlay)
140
+ document.body.removeChild(modal)
141
+ } catch (error) {
142
+ console.error('图形验证码校验失败', error)
143
+ // 根据错误类型显示不同的错误提示
144
+ const errorMessage = error.error_description || '图形验证码校验失败'
145
+ showError(errorMessage)
146
+
147
+ // 清空输入框并刷新图形验证码
148
+ input.value = ''
149
+ input.focus()
150
+ await refreshCaptcha()
151
+ } finally {
152
+ confirmBtn.textContent = '确定'
153
+ confirmBtn.disabled = false
154
+ confirmBtn.style.cssText = CAPTCHA_STYLES.confirmBtn
155
+ }
156
+ }, 500)
157
+ modal.appendChild(confirmBtn)
158
+
159
+ // 插入页面
160
+ document.body.appendChild(overlay)
161
+ document.body.appendChild(modal)
162
+ }
163
+
164
+ export const openURIWithCallback = async (url: string, oauthInstance?: Auth): Promise<CaptchaToken> => {
165
+ // 解析URL中的图形验证码参数
166
+ const { captchaData, state, token } = parseCaptcha(url)
167
+
168
+ genCaptchaDom({ oauthInstance, captchaState: { captchaData, state, token } })
169
+
170
+ // 监听图形验证码校验结果
171
+ return new Promise((resolve) => {
172
+ console.log('等待图形验证码校验结果...')
173
+ eventBus.on(CAPTCHA_EVENT.RESOLVE_CAPTCHA_DATA, (res) => {
174
+ // auth.verifyCaptchaData的校验结果
175
+ resolve(res?.data)
176
+ })
177
+ })
178
+ }
@@ -2,17 +2,20 @@ import { ApiUrls, ErrorType } from '../auth/consts'
2
2
  import { SimpleStorage, RequestFunction } from '../oauth2client/interface'
3
3
  import { AuthClientRequestOptions } from '../oauth2client/models'
4
4
  import { defaultStorage } from '../oauth2client/oauth2client'
5
- import { isInMpWebView, isMp } from '../utils/mp'
6
- import MyURLSearchParams from '../utils/urlSearchParams'
5
+ import { isMp } from '../utils/mp'
7
6
  import { SDKAdapterInterface } from '@cloudbase/adapter-interface'
7
+ import { openURIWithCallback } from './captcha-dom'
8
+ import { Auth } from '../auth/apis'
8
9
 
9
10
  export interface CaptchaOptions {
11
+ env: string;
10
12
  clientId: string
11
13
  request: RequestFunction
12
14
  storage: SimpleStorage
13
15
  // 打开网页并通过URL回调获取 CaptchaToken,针对不通的平台,该函数可以自定义实现, 默认集成浏览器端认证
14
16
  openURIWithCallback?: OpenURIWithCallbackFuction
15
17
  adapter?: SDKAdapterInterface & { isMatch?: () => boolean }
18
+ oauthInstance?: Auth
16
19
  }
17
20
 
18
21
  type OpenURIWithCallbackFuction = (url: string) => Promise<CaptchaToken>
@@ -50,7 +53,7 @@ export class Captcha {
50
53
  }
51
54
 
52
55
  this.config = opts
53
- this.tokenSectionName = `captcha_${opts.clientId}`
56
+ this.tokenSectionName = `captcha_${opts.clientId || opts.env}`
54
57
  }
55
58
 
56
59
  public isMatch() {
@@ -89,75 +92,9 @@ export class Captcha {
89
92
  }
90
93
 
91
94
  private getDefaultOpenURIWithCallback(): OpenURIWithCallbackFuction {
92
- if (!this.isMatch() && !isInMpWebView()) {
93
- if (window.location.search.indexOf('__captcha') > 0) {
94
- document.body.style.display = 'none'
95
- }
96
- if (document.getElementById('captcha_panel_wrap') === null) {
97
- const elementDiv = document.createElement('div')
98
- elementDiv.style.cssText = 'background-color: rgba(0, 0, 0, 0.7);position: fixed;left: 0px;right: 0px;top: 0px;bottom: 0px;padding: 9vw 0 0 0;display: none;z-index:100;'
99
- elementDiv.setAttribute('id', 'captcha_panel_wrap')
100
- setTimeout(() => {
101
- document.body.appendChild(elementDiv)
102
- }, 0)
103
- }
104
- }
105
- return this.defaultOpenURIWithCallback
95
+ return (url: string) => openURIWithCallback(url, this.config.oauthInstance)
106
96
  }
107
97
 
108
- /**
109
- * 默认通过浏览器打开网页并获取回调
110
- */
111
- private async defaultOpenURIWithCallback(
112
- url: string,
113
- opts?: { width?: string; height?: string },
114
- ): Promise<CaptchaToken> {
115
- const { width = '355px', height = '355px' } = opts || {}
116
-
117
- const matched = url.match(/^(data:.*)$/)
118
- if (matched) {
119
- return Promise.reject({
120
- error: ErrorType.UNIMPLEMENTED,
121
- error_description: 'need to impl captcha data',
122
- })
123
- }
124
-
125
- const target = document.getElementById('captcha_panel_wrap')
126
- const iframe = document.createElement('iframe')
127
- target.innerHTML = ''
128
-
129
- iframe.setAttribute('src', url)
130
- iframe.setAttribute('id', 'review-panel-iframe')
131
- iframe.style.cssText = `min-width:${width};display:block;height:${height};margin:0 auto;background-color: rgb(255, 255, 255);border: none;`
132
- target.appendChild(iframe)
133
- target.style.display = 'block'
134
- return new Promise<CaptchaToken>((resolve, reject) => {
135
- iframe.onload = function () {
136
- try {
137
- const windowLocation = window.location
138
- const iframeLocation = iframe.contentWindow.location
139
- if (iframeLocation.host + iframeLocation.pathname === windowLocation.host + windowLocation.pathname) {
140
- target.style.display = 'none'
141
- const iframeUrlParams = new MyURLSearchParams(iframeLocation.search)
142
- const captchToken = iframeUrlParams.get('captcha_token')
143
- if (captchToken) {
144
- return resolve({
145
- captcha_token: captchToken,
146
- expires_in: Number(iframeUrlParams.get('expires_in')),
147
- })
148
- }
149
- return reject({
150
- error: iframeUrlParams.get('error'),
151
- error_description: iframeUrlParams.get('error_description'),
152
- })
153
- }
154
- target.style.display = 'block'
155
- } catch (error) {
156
- target.style.display = 'block'
157
- }
158
- }
159
- })
160
- }
161
98
  /**
162
99
  * getCaptchaToken 获取captchaToken
163
100
  */
@@ -169,53 +106,26 @@ export class Captcha {
169
106
  return captchaToken
170
107
  }
171
108
  }
172
- let captchaTokenResp: {
173
- url?: string
174
- captcha_token?: string
175
- expires_in?: number
176
- }
177
- if (!this.isMatch() && !isInMpWebView()) {
178
- // eslint-disable-next-line @typescript-eslint/naming-convention
179
- const redirect_uri = `${window.location.origin + window.location.pathname}?__captcha=on`
180
- captchaTokenResp = await this.config.request<GetCaptchaResponse>(ApiUrls.GET_CAPTCHA_URL, {
181
- method: 'POST',
182
- body: {
183
- client_id: this.config.clientId,
184
- redirect_uri,
185
- state,
186
- },
187
- withCredentials: false,
188
- })
189
- if (captchaTokenResp.captcha_token) {
190
- const captchaToken = {
191
- captcha_token: captchaTokenResp.captcha_token,
192
- expires_in: captchaTokenResp.expires_in,
193
- }
194
- this.saveCaptchaToken(captchaToken)
195
- return captchaTokenResp.captcha_token
196
- }
197
- } else {
198
- /**
109
+
110
+ /**
199
111
  * https://iwiki.woa.com/p/4010699417
200
112
  */
201
- const captchaDataResp = await this.config.request<{
202
- data: string
203
- type: 'image'
204
- token: string
205
- expires_in: number
206
- }>(ApiUrls.CAPTCHA_DATA_URL, {
207
- method: 'POST',
208
- body: {
209
- state,
210
- redirect_uri: '',
211
- },
212
- withCredentials: false,
213
- })
214
- captchaTokenResp = {
215
- url: `${captchaDataResp.data}?state=${encodeURIComponent(state)}&token=${encodeURIComponent(captchaDataResp.token,)}`,
216
- }
217
- }
218
- const captchaToken = await this.config.openURIWithCallback(captchaTokenResp.url)
113
+ const captchaDataResp = await this.config.request<{
114
+ data: string
115
+ type: 'image'
116
+ token: string
117
+ expires_in: number
118
+ }>(ApiUrls.CAPTCHA_DATA_URL, {
119
+ method: 'POST',
120
+ body: {
121
+ state,
122
+ redirect_uri: '',
123
+ },
124
+ withCredentials: false,
125
+ })
126
+ const captchaTokenUrl = `${captchaDataResp.data}?state=${encodeURIComponent(state)}&token=${encodeURIComponent(captchaDataResp.token,)}`
127
+
128
+ const captchaToken = await this.config.openURIWithCallback(captchaTokenUrl)
219
129
  this.saveCaptchaToken(captchaToken)
220
130
  return captchaToken.captcha_token
221
131
  }
package/src/index.ts CHANGED
@@ -1,20 +1,44 @@
1
1
  import { OAuth2Client } from './oauth2client/oauth2client'
2
2
  import { AuthOptions, Auth } from './auth/apis'
3
3
  import { AUTH_API_PREFIX } from './auth/consts'
4
+ import { Credentials } from './oauth2client/models'
4
5
  export { Auth } from './auth/apis'
5
- export { AUTH_API_PREFIX } from './auth/consts'
6
+ export { AUTH_API_PREFIX, OAUTH_TYPE } from './auth/consts'
7
+ export { AuthError } from './auth/auth-error'
6
8
  export * as authModels from './auth/models'
7
- export type { ProviderProfile } from './auth/models'
9
+ export type { ProviderProfile, UserInfo, ModifyUserBasicInfoRequest } from './auth/models'
8
10
  export type { Credentials, OAuth2ClientOptions, ResponseError, AuthClientRequestOptions } from './oauth2client/models'
9
11
  export type { AuthOptions } from './auth/apis'
12
+ export { weappJwtDecodeAll } from './utils/base64'
13
+ export { LOGIN_STATE_CHANGED_TYPE, EVENTS, AUTH_STATE_CHANGED_TYPE } from './auth/consts'
10
14
 
11
15
  export class CloudbaseOAuth {
12
16
  public oauth2client: OAuth2Client
13
17
  public authApi: Auth
18
+ private detectSessionInUrl: boolean
14
19
 
15
20
  constructor(authOptions: AuthOptions) {
16
21
  // eslint-disable-next-line max-len
17
- const { apiOrigin, apiPath = AUTH_API_PREFIX, clientId, env, storage, request, baseRequest, anonymousSignInFunc, wxCloud, adapter, onCredentialsError, headers, i18n, useWxCloud } = authOptions
22
+ const {
23
+ apiOrigin,
24
+ apiPath = AUTH_API_PREFIX,
25
+ clientId,
26
+ env,
27
+ storage,
28
+ request,
29
+ baseRequest,
30
+ anonymousSignInFunc,
31
+ wxCloud,
32
+ adapter,
33
+ onCredentialsError,
34
+ headers,
35
+ i18n,
36
+ useWxCloud,
37
+ eventBus,
38
+ detectSessionInUrl,
39
+ debug,
40
+ } = authOptions
41
+ this.detectSessionInUrl = detectSessionInUrl ?? false
18
42
  this.oauth2client = new OAuth2Client({
19
43
  apiOrigin,
20
44
  apiPath,
@@ -28,6 +52,8 @@ export class CloudbaseOAuth {
28
52
  headers: headers || {},
29
53
  i18n,
30
54
  useWxCloud,
55
+ eventBus,
56
+ debug,
31
57
  })
32
58
 
33
59
  this.authApi = new Auth({
@@ -37,5 +63,27 @@ export class CloudbaseOAuth {
37
63
  request: request ? this.oauth2client.request.bind(this.oauth2client) : undefined,
38
64
  adapter,
39
65
  })
66
+
67
+ // Set the getInitialSession callback after Auth is created
68
+ // This allows Auth.getInitialSession to handle URL detection and OAuth verification
69
+ if (detectSessionInUrl) {
70
+ this.oauth2client.setGetInitialSession(this.authApi.getInitialSession.bind(this.authApi))
71
+ }
72
+ // Note: Do NOT auto-call initialize() here.
73
+ // Upper layer (packages/auth) should call initializeSession() after setting up onInitialSessionObtained callback
74
+ }
75
+
76
+ /**
77
+ * Setup the onInitialSessionObtained callback and trigger initialization.
78
+ * This should be called by upper layer (packages/auth) after creating Auth instance.
79
+ * @param onUserObtained Callback to handle user info storage after session is obtained
80
+ */
81
+ public initializeSession(onUserObtained?: (data: { session: Credentials; user?: any }) => void | Promise<void>): void {
82
+ if (!this.detectSessionInUrl) {
83
+ return
84
+ }
85
+ this.oauth2client.setOnInitialSessionObtained(onUserObtained)
86
+ // Pass callback directly to initialize() to ensure it's set before initialization starts
87
+ this.oauth2client.initialize()
40
88
  }
41
89
  }
@@ -15,7 +15,7 @@ export abstract class AuthClient {
15
15
  /**
16
16
  * Sets the auth credentials.
17
17
  */
18
- abstract setCredentials(credentials?: Credentials): void;
18
+ abstract setCredentials(credentials?: Credentials): Promise<void>;
19
19
 
20
20
  /**
21
21
  * get the current accessToken from AuthClient, you can use this to detect login status
@@ -46,6 +46,12 @@ export interface AuthClientRequestOptions extends RequestOptions {
46
46
  withBasicAuth?: boolean;
47
47
  retry?: number;
48
48
  useWxCloud?: boolean;
49
+ /**
50
+ * Custom getCredentials function for this request.
51
+ * When provided, uses this function instead of the default getCredentials() call
52
+ * to avoid potential deadlocks during initialization.
53
+ */
54
+ getCredentials?: () => Credentials | Promise<Credentials | null> | null;
49
55
 
50
56
  [key: string]: any;
51
57
  }
@@ -72,4 +78,26 @@ export interface OAuth2ClientOptions {
72
78
  onCredentialsError?: AuthOptions['onCredentialsError']
73
79
  i18n?: ICloudbaseConfig['i18n']
74
80
  useWxCloud?: boolean
81
+ eventBus?: any
82
+ /**
83
+ * Enable debug logging
84
+ */
85
+ debug?: boolean
86
+ /**
87
+ * Callback function to get initial session (e.g., from OAuth callback in URL).
88
+ * Called by OAuth2Client.initialize() if set.
89
+ * The callback should handle URL detection, OAuth verification, and return credentials.
90
+ * Returns { data: { session: Credentials, user?: any }, error: null } on success,
91
+ * or { data: null, error: Error } on failure.
92
+ */
93
+ getInitialSession?: () => Promise<{
94
+ data: { session: Credentials; user?: any } | null
95
+ error: Error | null
96
+ }>
97
+ /**
98
+ * Callback invoked after initial session is obtained and stored.
99
+ * Used to let upper layer (e.g., packages/auth) handle user info storage.
100
+ * Called with session and user data.
101
+ */
102
+ onInitialSessionObtained?: (data: { session: Credentials; user?: any }) => void | Promise<void>
75
103
  }