@seaverse/auth-sdk 0.3.8 → 0.3.9

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/README.md CHANGED
@@ -94,7 +94,222 @@ const health = await client.getHealth();
94
94
  console.log('Health:', health);
95
95
  ```
96
96
 
97
- ### 2. 用户认证
97
+ ### 2. Iframe 场景下的 Token 获取
98
+
99
+ 当你的应用被嵌入到 iframe 中时,可以使用 `getIframeToken()` 方法从父页面获取认证 token。这对于第三方登录重定向场景特别有用。
100
+
101
+ #### 子页面 (iframe 内) 使用方式
102
+
103
+ ```typescript
104
+ import { SeaVerseBackendAPIClient } from '@seaverse/auth-sdk';
105
+
106
+ const client = new SeaVerseBackendAPIClient({
107
+ appId: 'your app id',
108
+ });
109
+
110
+ // 在 iframe 中请求父页面的 token
111
+ try {
112
+ const token = await client.getIframeToken({
113
+ timeout: 10000 // 可选,默认 30 秒
114
+ });
115
+
116
+ // 设置 token 到 client
117
+ client.setToken(token);
118
+ localStorage.setItem('token', token);
119
+
120
+ console.log('成功从父页面获取 token');
121
+
122
+ // 现在可以使用需要认证的 API
123
+ const user = await client.getCurrentUser();
124
+ console.log('当前用户:', user);
125
+ } catch (error) {
126
+ console.error('获取 token 失败:', error);
127
+ }
128
+ ```
129
+
130
+ #### 父页面监听并响应 token 请求
131
+
132
+ ```typescript
133
+ // 父页面需要监听来自 iframe 的 token 请求
134
+ window.addEventListener('message', (event) => {
135
+ // 检查消息类型
136
+ if (event.data.type === 'seaverse:get_token') {
137
+ const requestId = event.data.requestId;
138
+
139
+ // 获取你的 token (从 localStorage、API 等)
140
+ const token = localStorage.getItem('token');
141
+
142
+ if (token) {
143
+ // 发送 token 给 iframe
144
+ event.source.postMessage({
145
+ type: 'seaverse:token',
146
+ requestId: requestId,
147
+ payload: {
148
+ accessToken: token,
149
+ expiresIn: 3600,
150
+ },
151
+ }, '*');
152
+ } else {
153
+ // 发送错误消息
154
+ event.source.postMessage({
155
+ type: 'seaverse:token_error',
156
+ requestId: requestId,
157
+ error: 'No token available',
158
+ }, '*');
159
+ }
160
+ }
161
+ });
162
+ ```
163
+
164
+ #### 完整的 iframe 登录流程示例
165
+
166
+ ```typescript
167
+ // 子页面 (iframe)
168
+ import { SeaVerseBackendAPIClient } from '@seaverse/auth-sdk';
169
+
170
+ const client = new SeaVerseBackendAPIClient({
171
+ appId: 'your app id',
172
+ });
173
+
174
+ async function initIframeApp() {
175
+ try {
176
+ // 1. 尝试从父页面获取 token
177
+ const token = await client.getIframeToken();
178
+ client.setToken(token);
179
+ localStorage.setItem('token', token);
180
+
181
+ // 2. 验证 token 并获取用户信息
182
+ const user = await client.getCurrentUser();
183
+ console.log('已登录:', user);
184
+
185
+ // 3. 继续应用逻辑
186
+ startApp(user);
187
+ } catch (error) {
188
+ // 4. 如果获取失败,通知父页面需要登录
189
+ window.parent.postMessage({
190
+ type: 'seaverse:need_login',
191
+ }, '*');
192
+ }
193
+ }
194
+
195
+ initIframeApp();
196
+ ```
197
+
198
+ ```typescript
199
+ // 父页面
200
+ window.addEventListener('message', (event) => {
201
+ // 处理 token 请求
202
+ if (event.data.type === 'seaverse:get_token') {
203
+ const requestId = event.data.requestId;
204
+ const token = localStorage.getItem('token');
205
+
206
+ if (token) {
207
+ event.source.postMessage({
208
+ type: 'seaverse:token',
209
+ requestId: requestId,
210
+ payload: {
211
+ accessToken: token,
212
+ expiresIn: 3600,
213
+ },
214
+ }, '*');
215
+ } else {
216
+ // 如果没有 token,唤起登录
217
+ showLoginModal();
218
+ }
219
+ }
220
+
221
+ // 处理需要登录的消息
222
+ if (event.data.type === 'seaverse:need_login') {
223
+ showLoginModal();
224
+ }
225
+ });
226
+
227
+ // 登录成功后,通知 iframe
228
+ function onLoginSuccess(newToken) {
229
+ const iframe = document.querySelector('iframe');
230
+ iframe.contentWindow.postMessage({
231
+ type: 'seaverse:token',
232
+ requestId: 'auto', // 自动登录时可以使用固定 ID
233
+ payload: {
234
+ accessToken: newToken,
235
+ expiresIn: 3600,
236
+ },
237
+ }, '*');
238
+ }
239
+ ```
240
+
241
+ #### TypeScript 类型定义
242
+
243
+ SDK 提供了完整的 TypeScript 类型支持:
244
+
245
+ ```typescript
246
+ import type {
247
+ IframeTokenRequestMessage,
248
+ IframeTokenResponseMessage,
249
+ IframeTokenErrorMessage,
250
+ } from '@seaverse/auth-sdk';
251
+
252
+ // Token 请求消息
253
+ const request: IframeTokenRequestMessage = {
254
+ type: 'seaverse:get_token',
255
+ requestId: 'unique-id',
256
+ };
257
+
258
+ // Token 响应消息
259
+ const response: IframeTokenResponseMessage = {
260
+ type: 'seaverse:token',
261
+ requestId: 'unique-id',
262
+ payload: {
263
+ accessToken: 'jwt-token',
264
+ expiresIn: 3600,
265
+ },
266
+ };
267
+
268
+ // 错误消息
269
+ const error: IframeTokenErrorMessage = {
270
+ type: 'seaverse:token_error',
271
+ requestId: 'unique-id',
272
+ error: 'Token not available',
273
+ };
274
+ ```
275
+
276
+ #### 安全注意事项
277
+
278
+ 1. **使用特定 origin**: 在生产环境中,建议使用特定的 origin 而不是 `'*'`:
279
+ ```typescript
280
+ // 父页面
281
+ const ALLOWED_ORIGIN = 'https://your-iframe-domain.com';
282
+ event.source.postMessage(message, ALLOWED_ORIGIN);
283
+
284
+ // 子页面发送时也可以指定
285
+ window.parent.postMessage(message, 'https://parent-domain.com');
286
+ ```
287
+
288
+ 2. **验证消息来源**: 在处理 postMessage 时验证 origin:
289
+ ```typescript
290
+ window.addEventListener('message', (event) => {
291
+ // 验证来源
292
+ if (event.origin !== 'https://trusted-domain.com') {
293
+ return;
294
+ }
295
+ // 处理消息...
296
+ });
297
+ ```
298
+
299
+ 3. **Token 过期处理**: 记得处理 token 过期的情况:
300
+ ```typescript
301
+ try {
302
+ const user = await client.getCurrentUser();
303
+ } catch (error) {
304
+ if (error.response?.status === 401) {
305
+ // Token 过期,重新获取
306
+ const newToken = await client.getIframeToken();
307
+ client.setToken(newToken);
308
+ }
309
+ }
310
+ ```
311
+
312
+ ### 3. 用户认证
98
313
 
99
314
  ```typescript
100
315
  // 注册新用户
@@ -150,7 +365,7 @@ await client.resetPassword({
150
365
  });
151
366
  ```
152
367
 
153
- ### 3. OAuth 第三方登录 (Backend Proxy Mode)
368
+ ### 4. OAuth 第三方登录 (Backend Proxy Mode)
154
369
 
155
370
  SDK 使用 Backend Proxy Mode,Client Secret 永不暴露给前端,安全性更高。
156
371
 
@@ -220,7 +435,7 @@ await client.unlinkDiscord();
220
435
  await client.unlinkGithub();
221
436
  ```
222
437
 
223
- ### 4. 使用登录UI组件
438
+ ### 5. 使用登录UI组件
224
439
 
225
440
  SDK 提供精美的登录弹窗组件,支持深色和浅色两种主题:
226
441
 
@@ -436,7 +651,7 @@ if (token) {
436
651
  }
437
652
  ```
438
653
 
439
- ### 5. 容器管理
654
+ ### 6. 容器管理
440
655
 
441
656
  ```typescript
442
657
  // 列出所有容器
@@ -471,7 +686,7 @@ await client.unregisterContainer({
471
686
  const stats = await client.getContainerStats();
472
687
  ```
473
688
 
474
- ### 6. 技能市场
689
+ ### 7. 技能市场
475
690
 
476
691
  ```typescript
477
692
  // 列出市场技能
@@ -500,7 +715,7 @@ await client.uninstallSkill({
500
715
  });
501
716
  ```
502
717
 
503
- ### 7. 邀请码管理
718
+ ### 8. 邀请码管理
504
719
 
505
720
  ```typescript
506
721
  // 申请邀请码(当用户没有邀请码时)
@@ -550,7 +765,7 @@ const usages = await client.getInviteUsages('inv_abc123', {
550
765
  console.log('使用记录:', usages.data.usages);
551
766
  ```
552
767
 
553
- ### 8. 邮箱验证与邀请码绑定
768
+ ### 9. 邮箱验证与邀请码绑定
554
769
 
555
770
  #### 邮箱验证(自动登录)
556
771
 
@@ -734,7 +949,7 @@ const inviteCodeInfo = AuthModal.handleInviteCodeRequired(
734
949
  );
735
950
  ```
736
951
 
737
- ### 9. 其他功能
952
+ ### 10. 其他功能
738
953
 
739
954
  ```typescript
740
955
  // 获取API Service Token
@@ -937,6 +1152,7 @@ SDK支持以下环境:
937
1152
  | `forgotPassword()` | `{ email, frontend_url? }` | `SuccessResponse` | 忘记密码,frontend_url 默认为 window.location.href |
938
1153
  | `resetPassword()` | `{ token, new_password }` | `SuccessResponse` | 重置密码 |
939
1154
  | `setToken()` | `token: string` | `void` | 设置认证 token(OAuth 登录后使用) |
1155
+ | `getIframeToken()` | `{ timeout? }` | `Promise<string>` | 从父页面获取 token(仅 iframe 场景),timeout 默认 30000ms |
940
1156
  | `getApiServiceToken()` | - | `ApiServiceTokenResponse` | 获取API Token |
941
1157
 
942
1158
  #### 注册流程说明
package/dist/index.cjs CHANGED
@@ -1285,6 +1285,103 @@ class SeaVerseBackendAPIClient {
1285
1285
  // Update the httpClient's auth
1286
1286
  this.httpClient.auth = auth;
1287
1287
  }
1288
+ /**
1289
+ * Get token from parent window via iframe communication
1290
+ * This method is designed for scenarios where the application is embedded in an iframe
1291
+ * and needs to obtain authentication tokens from the parent page
1292
+ *
1293
+ * @param options - Configuration options
1294
+ * @param options.timeout - Timeout in milliseconds (default: 30000)
1295
+ * @returns Promise that resolves with the access token
1296
+ *
1297
+ * @example
1298
+ * // In an iframe application
1299
+ * try {
1300
+ * const token = await client.getIframeToken({ timeout: 10000 });
1301
+ * client.setToken(token);
1302
+ * localStorage.setItem('token', token);
1303
+ * console.log('Successfully obtained token from parent');
1304
+ * } catch (error) {
1305
+ * console.error('Failed to get token from parent:', error);
1306
+ * }
1307
+ *
1308
+ * @example
1309
+ * // Parent page should listen for token requests and respond:
1310
+ * window.addEventListener('message', (event) => {
1311
+ * if (event.data.type === 'seaverse:get_token') {
1312
+ * const requestId = event.data.requestId;
1313
+ * // Get your token (from localStorage, API, etc.)
1314
+ * const token = localStorage.getItem('token');
1315
+ * // Send token back to iframe
1316
+ * event.source.postMessage({
1317
+ * type: 'seaverse:token',
1318
+ * requestId: requestId,
1319
+ * payload: {
1320
+ * accessToken: token,
1321
+ * expiresIn: 3600,
1322
+ * },
1323
+ * }, '*');
1324
+ * }
1325
+ * });
1326
+ */
1327
+ getIframeToken(options) {
1328
+ // Only works in browser environment
1329
+ if (typeof window === 'undefined') {
1330
+ return Promise.reject(new Error('getIframeToken() can only be used in browser environment'));
1331
+ }
1332
+ // Check if we're in an iframe
1333
+ if (window.self === window.top) {
1334
+ return Promise.reject(new Error('getIframeToken() can only be used inside an iframe'));
1335
+ }
1336
+ const timeout = options?.timeout || 30000; // Default 30 seconds
1337
+ const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1338
+ return new Promise((resolve, reject) => {
1339
+ let timeoutId = null;
1340
+ let messageHandler = null;
1341
+ // Cleanup function
1342
+ const cleanup = () => {
1343
+ if (timeoutId) {
1344
+ clearTimeout(timeoutId);
1345
+ timeoutId = null;
1346
+ }
1347
+ if (messageHandler) {
1348
+ window.removeEventListener('message', messageHandler);
1349
+ messageHandler = null;
1350
+ }
1351
+ };
1352
+ // Set up timeout
1353
+ timeoutId = setTimeout(() => {
1354
+ cleanup();
1355
+ reject(new Error(`Token request timeout after ${timeout}ms`));
1356
+ }, timeout);
1357
+ // Set up message listener
1358
+ messageHandler = (event) => {
1359
+ const data = event.data;
1360
+ // Check if this is a response to our request
1361
+ if (data &&
1362
+ typeof data === 'object' &&
1363
+ data.requestId === requestId) {
1364
+ if (data.type === 'seaverse:token') {
1365
+ cleanup();
1366
+ const tokenData = data;
1367
+ resolve(tokenData.payload.accessToken);
1368
+ }
1369
+ else if (data.type === 'seaverse:token_error') {
1370
+ cleanup();
1371
+ const errorData = data;
1372
+ reject(new Error(errorData.error || 'Failed to get token from parent'));
1373
+ }
1374
+ }
1375
+ };
1376
+ window.addEventListener('message', messageHandler);
1377
+ // Send request to parent
1378
+ const requestMessage = {
1379
+ type: 'seaverse:get_token',
1380
+ requestId,
1381
+ };
1382
+ window.parent.postMessage(requestMessage, '*');
1383
+ });
1384
+ }
1288
1385
  // ============================================================================
1289
1386
  // Authentication & OAuth APIs
1290
1387
  // ============================================================================