@seaverse/auth-sdk 0.3.8 → 0.4.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.
- package/README.md +264 -8
- package/dist/index.cjs +142 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +102 -2
- package/dist/index.js +142 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -94,7 +94,261 @@ 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
|
+
在调用 iframe 相关方法前,建议先检查应用是否在 iframe 中运行:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { SeaVerseBackendAPIClient } from '@seaverse/auth-sdk';
|
|
107
|
+
|
|
108
|
+
// 方式 1: 使用静态方法(无需 client 实例)
|
|
109
|
+
if (SeaVerseBackendAPIClient.isInIframe()) {
|
|
110
|
+
console.log('应用正在 iframe 中运行');
|
|
111
|
+
} else {
|
|
112
|
+
console.log('应用不在 iframe 中运行');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 方式 2: 使用实例方法
|
|
116
|
+
const client = new SeaVerseBackendAPIClient({
|
|
117
|
+
appId: 'your app id',
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (client.isInIframe()) {
|
|
121
|
+
console.log('应用正在 iframe 中运行');
|
|
122
|
+
// 可以安全地调用 getIframeToken()
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### 子页面 (iframe 内) 使用方式
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { SeaVerseBackendAPIClient } from '@seaverse/auth-sdk';
|
|
130
|
+
|
|
131
|
+
const client = new SeaVerseBackendAPIClient({
|
|
132
|
+
appId: 'your app id',
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 先检查是否在 iframe 中
|
|
136
|
+
if (!client.isInIframe()) {
|
|
137
|
+
console.log('应用不在 iframe 中,跳过 token 获取');
|
|
138
|
+
// 可以使用其他登录方式
|
|
139
|
+
} else {
|
|
140
|
+
// 在 iframe 中请求父页面的 token
|
|
141
|
+
try {
|
|
142
|
+
const token = await client.getIframeToken({
|
|
143
|
+
timeout: 10000 // 可选,默认 30 秒
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// 设置 token 到 client
|
|
147
|
+
client.setToken(token);
|
|
148
|
+
localStorage.setItem('token', token);
|
|
149
|
+
|
|
150
|
+
console.log('成功从父页面获取 token');
|
|
151
|
+
|
|
152
|
+
// 现在可以使用需要认证的 API
|
|
153
|
+
const user = await client.getCurrentUser();
|
|
154
|
+
console.log('当前用户:', user);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
console.error('获取 token 失败:', error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### 父页面监听并响应 token 请求
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// 父页面需要监听来自 iframe 的 token 请求
|
|
165
|
+
window.addEventListener('message', (event) => {
|
|
166
|
+
// 检查消息类型
|
|
167
|
+
if (event.data.type === 'seaverse:get_token') {
|
|
168
|
+
const requestId = event.data.requestId;
|
|
169
|
+
|
|
170
|
+
// 获取你的 token (从 localStorage、API 等)
|
|
171
|
+
const token = localStorage.getItem('token');
|
|
172
|
+
|
|
173
|
+
if (token) {
|
|
174
|
+
// 发送 token 给 iframe
|
|
175
|
+
event.source.postMessage({
|
|
176
|
+
type: 'seaverse:token',
|
|
177
|
+
requestId: requestId,
|
|
178
|
+
payload: {
|
|
179
|
+
accessToken: token,
|
|
180
|
+
expiresIn: 3600,
|
|
181
|
+
},
|
|
182
|
+
}, '*');
|
|
183
|
+
} else {
|
|
184
|
+
// 发送错误消息
|
|
185
|
+
event.source.postMessage({
|
|
186
|
+
type: 'seaverse:token_error',
|
|
187
|
+
requestId: requestId,
|
|
188
|
+
error: 'No token available',
|
|
189
|
+
}, '*');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
#### 完整的 iframe 登录流程示例
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// 子页面 (iframe)
|
|
199
|
+
import { SeaVerseBackendAPIClient } from '@seaverse/auth-sdk';
|
|
200
|
+
|
|
201
|
+
const client = new SeaVerseBackendAPIClient({
|
|
202
|
+
appId: 'your app id',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
async function initIframeApp() {
|
|
206
|
+
// 0. 检查是否在 iframe 中
|
|
207
|
+
if (!client.isInIframe()) {
|
|
208
|
+
console.log('应用不在 iframe 中,使用常规登录流程');
|
|
209
|
+
// 使用常规登录流程
|
|
210
|
+
showLoginForm();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
// 1. 尝试从父页面获取 token
|
|
216
|
+
const token = await client.getIframeToken();
|
|
217
|
+
client.setToken(token);
|
|
218
|
+
localStorage.setItem('token', token);
|
|
219
|
+
|
|
220
|
+
// 2. 验证 token 并获取用户信息
|
|
221
|
+
const user = await client.getCurrentUser();
|
|
222
|
+
console.log('已登录:', user);
|
|
223
|
+
|
|
224
|
+
// 3. 继续应用逻辑
|
|
225
|
+
startApp(user);
|
|
226
|
+
} catch (error) {
|
|
227
|
+
// 4. 如果获取失败,通知父页面需要登录
|
|
228
|
+
window.parent.postMessage({
|
|
229
|
+
type: 'seaverse:need_login',
|
|
230
|
+
}, '*');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
initIframeApp();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// 父页面
|
|
239
|
+
window.addEventListener('message', (event) => {
|
|
240
|
+
// 处理 token 请求
|
|
241
|
+
if (event.data.type === 'seaverse:get_token') {
|
|
242
|
+
const requestId = event.data.requestId;
|
|
243
|
+
const token = localStorage.getItem('token');
|
|
244
|
+
|
|
245
|
+
if (token) {
|
|
246
|
+
event.source.postMessage({
|
|
247
|
+
type: 'seaverse:token',
|
|
248
|
+
requestId: requestId,
|
|
249
|
+
payload: {
|
|
250
|
+
accessToken: token,
|
|
251
|
+
expiresIn: 3600,
|
|
252
|
+
},
|
|
253
|
+
}, '*');
|
|
254
|
+
} else {
|
|
255
|
+
// 如果没有 token,唤起登录
|
|
256
|
+
showLoginModal();
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 处理需要登录的消息
|
|
261
|
+
if (event.data.type === 'seaverse:need_login') {
|
|
262
|
+
showLoginModal();
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// 登录成功后,通知 iframe
|
|
267
|
+
function onLoginSuccess(newToken) {
|
|
268
|
+
const iframe = document.querySelector('iframe');
|
|
269
|
+
iframe.contentWindow.postMessage({
|
|
270
|
+
type: 'seaverse:token',
|
|
271
|
+
requestId: 'auto', // 自动登录时可以使用固定 ID
|
|
272
|
+
payload: {
|
|
273
|
+
accessToken: newToken,
|
|
274
|
+
expiresIn: 3600,
|
|
275
|
+
},
|
|
276
|
+
}, '*');
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
#### TypeScript 类型定义
|
|
281
|
+
|
|
282
|
+
SDK 提供了完整的 TypeScript 类型支持:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
import type {
|
|
286
|
+
IframeTokenRequestMessage,
|
|
287
|
+
IframeTokenResponseMessage,
|
|
288
|
+
IframeTokenErrorMessage,
|
|
289
|
+
} from '@seaverse/auth-sdk';
|
|
290
|
+
|
|
291
|
+
// Token 请求消息
|
|
292
|
+
const request: IframeTokenRequestMessage = {
|
|
293
|
+
type: 'seaverse:get_token',
|
|
294
|
+
requestId: 'unique-id',
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Token 响应消息
|
|
298
|
+
const response: IframeTokenResponseMessage = {
|
|
299
|
+
type: 'seaverse:token',
|
|
300
|
+
requestId: 'unique-id',
|
|
301
|
+
payload: {
|
|
302
|
+
accessToken: 'jwt-token',
|
|
303
|
+
expiresIn: 3600,
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// 错误消息
|
|
308
|
+
const error: IframeTokenErrorMessage = {
|
|
309
|
+
type: 'seaverse:token_error',
|
|
310
|
+
requestId: 'unique-id',
|
|
311
|
+
error: 'Token not available',
|
|
312
|
+
};
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
#### 安全注意事项
|
|
316
|
+
|
|
317
|
+
1. **使用特定 origin**: 在生产环境中,建议使用特定的 origin 而不是 `'*'`:
|
|
318
|
+
```typescript
|
|
319
|
+
// 父页面
|
|
320
|
+
const ALLOWED_ORIGIN = 'https://your-iframe-domain.com';
|
|
321
|
+
event.source.postMessage(message, ALLOWED_ORIGIN);
|
|
322
|
+
|
|
323
|
+
// 子页面发送时也可以指定
|
|
324
|
+
window.parent.postMessage(message, 'https://parent-domain.com');
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
2. **验证消息来源**: 在处理 postMessage 时验证 origin:
|
|
328
|
+
```typescript
|
|
329
|
+
window.addEventListener('message', (event) => {
|
|
330
|
+
// 验证来源
|
|
331
|
+
if (event.origin !== 'https://trusted-domain.com') {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// 处理消息...
|
|
335
|
+
});
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
3. **Token 过期处理**: 记得处理 token 过期的情况:
|
|
339
|
+
```typescript
|
|
340
|
+
try {
|
|
341
|
+
const user = await client.getCurrentUser();
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error.response?.status === 401) {
|
|
344
|
+
// Token 过期,重新获取
|
|
345
|
+
const newToken = await client.getIframeToken();
|
|
346
|
+
client.setToken(newToken);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### 3. 用户认证
|
|
98
352
|
|
|
99
353
|
```typescript
|
|
100
354
|
// 注册新用户
|
|
@@ -150,7 +404,7 @@ await client.resetPassword({
|
|
|
150
404
|
});
|
|
151
405
|
```
|
|
152
406
|
|
|
153
|
-
###
|
|
407
|
+
### 4. OAuth 第三方登录 (Backend Proxy Mode)
|
|
154
408
|
|
|
155
409
|
SDK 使用 Backend Proxy Mode,Client Secret 永不暴露给前端,安全性更高。
|
|
156
410
|
|
|
@@ -220,7 +474,7 @@ await client.unlinkDiscord();
|
|
|
220
474
|
await client.unlinkGithub();
|
|
221
475
|
```
|
|
222
476
|
|
|
223
|
-
###
|
|
477
|
+
### 5. 使用登录UI组件
|
|
224
478
|
|
|
225
479
|
SDK 提供精美的登录弹窗组件,支持深色和浅色两种主题:
|
|
226
480
|
|
|
@@ -436,7 +690,7 @@ if (token) {
|
|
|
436
690
|
}
|
|
437
691
|
```
|
|
438
692
|
|
|
439
|
-
###
|
|
693
|
+
### 6. 容器管理
|
|
440
694
|
|
|
441
695
|
```typescript
|
|
442
696
|
// 列出所有容器
|
|
@@ -471,7 +725,7 @@ await client.unregisterContainer({
|
|
|
471
725
|
const stats = await client.getContainerStats();
|
|
472
726
|
```
|
|
473
727
|
|
|
474
|
-
###
|
|
728
|
+
### 7. 技能市场
|
|
475
729
|
|
|
476
730
|
```typescript
|
|
477
731
|
// 列出市场技能
|
|
@@ -500,7 +754,7 @@ await client.uninstallSkill({
|
|
|
500
754
|
});
|
|
501
755
|
```
|
|
502
756
|
|
|
503
|
-
###
|
|
757
|
+
### 8. 邀请码管理
|
|
504
758
|
|
|
505
759
|
```typescript
|
|
506
760
|
// 申请邀请码(当用户没有邀请码时)
|
|
@@ -550,7 +804,7 @@ const usages = await client.getInviteUsages('inv_abc123', {
|
|
|
550
804
|
console.log('使用记录:', usages.data.usages);
|
|
551
805
|
```
|
|
552
806
|
|
|
553
|
-
###
|
|
807
|
+
### 9. 邮箱验证与邀请码绑定
|
|
554
808
|
|
|
555
809
|
#### 邮箱验证(自动登录)
|
|
556
810
|
|
|
@@ -734,7 +988,7 @@ const inviteCodeInfo = AuthModal.handleInviteCodeRequired(
|
|
|
734
988
|
);
|
|
735
989
|
```
|
|
736
990
|
|
|
737
|
-
###
|
|
991
|
+
### 10. 其他功能
|
|
738
992
|
|
|
739
993
|
```typescript
|
|
740
994
|
// 获取API Service Token
|
|
@@ -937,6 +1191,8 @@ SDK支持以下环境:
|
|
|
937
1191
|
| `forgotPassword()` | `{ email, frontend_url? }` | `SuccessResponse` | 忘记密码,frontend_url 默认为 window.location.href |
|
|
938
1192
|
| `resetPassword()` | `{ token, new_password }` | `SuccessResponse` | 重置密码 |
|
|
939
1193
|
| `setToken()` | `token: string` | `void` | 设置认证 token(OAuth 登录后使用) |
|
|
1194
|
+
| `isInIframe()` | - | `boolean` | 检查应用是否在 iframe 中运行(支持静态方法和实例方法) |
|
|
1195
|
+
| `getIframeToken()` | `{ timeout? }` | `Promise<string>` | 从父页面获取 token(仅 iframe 场景),timeout 默认 30000ms |
|
|
940
1196
|
| `getApiServiceToken()` | - | `ApiServiceTokenResponse` | 获取API Token |
|
|
941
1197
|
|
|
942
1198
|
#### 注册流程说明
|
package/dist/index.cjs
CHANGED
|
@@ -1285,6 +1285,148 @@ class SeaVerseBackendAPIClient {
|
|
|
1285
1285
|
// Update the httpClient's auth
|
|
1286
1286
|
this.httpClient.auth = auth;
|
|
1287
1287
|
}
|
|
1288
|
+
/**
|
|
1289
|
+
* Check if the application is running inside an iframe
|
|
1290
|
+
* This is a utility method to detect iframe context before calling iframe-specific methods
|
|
1291
|
+
*
|
|
1292
|
+
* @returns true if running in an iframe, false otherwise
|
|
1293
|
+
*
|
|
1294
|
+
* @example
|
|
1295
|
+
* // Check if in iframe before requesting token
|
|
1296
|
+
* if (client.isInIframe()) {
|
|
1297
|
+
* const token = await client.getIframeToken();
|
|
1298
|
+
* client.setToken(token);
|
|
1299
|
+
* } else {
|
|
1300
|
+
* console.log('Not running in an iframe');
|
|
1301
|
+
* }
|
|
1302
|
+
*
|
|
1303
|
+
* @example
|
|
1304
|
+
* // Use as a static method without client instance
|
|
1305
|
+
* if (SeaVerseBackendAPIClient.isInIframe()) {
|
|
1306
|
+
* console.log('Application is embedded in an iframe');
|
|
1307
|
+
* }
|
|
1308
|
+
*/
|
|
1309
|
+
static isInIframe() {
|
|
1310
|
+
// Only works in browser environment
|
|
1311
|
+
if (typeof window === 'undefined') {
|
|
1312
|
+
return false;
|
|
1313
|
+
}
|
|
1314
|
+
// Check if window.self is not equal to window.top
|
|
1315
|
+
// This is the standard way to detect if running in an iframe
|
|
1316
|
+
try {
|
|
1317
|
+
return window.self !== window.top;
|
|
1318
|
+
}
|
|
1319
|
+
catch (e) {
|
|
1320
|
+
// If we get a SecurityError (cross-origin iframe), we're definitely in an iframe
|
|
1321
|
+
return true;
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Instance method to check if running in an iframe
|
|
1326
|
+
* Calls the static method for convenience
|
|
1327
|
+
*
|
|
1328
|
+
* @returns true if running in an iframe, false otherwise
|
|
1329
|
+
*/
|
|
1330
|
+
isInIframe() {
|
|
1331
|
+
return SeaVerseBackendAPIClient.isInIframe();
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Get token from parent window via iframe communication
|
|
1335
|
+
* This method is designed for scenarios where the application is embedded in an iframe
|
|
1336
|
+
* and needs to obtain authentication tokens from the parent page
|
|
1337
|
+
*
|
|
1338
|
+
* @param options - Configuration options
|
|
1339
|
+
* @param options.timeout - Timeout in milliseconds (default: 30000)
|
|
1340
|
+
* @returns Promise that resolves with the access token
|
|
1341
|
+
*
|
|
1342
|
+
* @example
|
|
1343
|
+
* // In an iframe application
|
|
1344
|
+
* try {
|
|
1345
|
+
* const token = await client.getIframeToken({ timeout: 10000 });
|
|
1346
|
+
* client.setToken(token);
|
|
1347
|
+
* localStorage.setItem('token', token);
|
|
1348
|
+
* console.log('Successfully obtained token from parent');
|
|
1349
|
+
* } catch (error) {
|
|
1350
|
+
* console.error('Failed to get token from parent:', error);
|
|
1351
|
+
* }
|
|
1352
|
+
*
|
|
1353
|
+
* @example
|
|
1354
|
+
* // Parent page should listen for token requests and respond:
|
|
1355
|
+
* window.addEventListener('message', (event) => {
|
|
1356
|
+
* if (event.data.type === 'seaverse:get_token') {
|
|
1357
|
+
* const requestId = event.data.requestId;
|
|
1358
|
+
* // Get your token (from localStorage, API, etc.)
|
|
1359
|
+
* const token = localStorage.getItem('token');
|
|
1360
|
+
* // Send token back to iframe
|
|
1361
|
+
* event.source.postMessage({
|
|
1362
|
+
* type: 'seaverse:token',
|
|
1363
|
+
* requestId: requestId,
|
|
1364
|
+
* payload: {
|
|
1365
|
+
* accessToken: token,
|
|
1366
|
+
* expiresIn: 3600,
|
|
1367
|
+
* },
|
|
1368
|
+
* }, '*');
|
|
1369
|
+
* }
|
|
1370
|
+
* });
|
|
1371
|
+
*/
|
|
1372
|
+
getIframeToken(options) {
|
|
1373
|
+
// Only works in browser environment
|
|
1374
|
+
if (typeof window === 'undefined') {
|
|
1375
|
+
return Promise.reject(new Error('getIframeToken() can only be used in browser environment'));
|
|
1376
|
+
}
|
|
1377
|
+
// Check if we're in an iframe
|
|
1378
|
+
if (!this.isInIframe()) {
|
|
1379
|
+
return Promise.reject(new Error('getIframeToken() can only be used inside an iframe'));
|
|
1380
|
+
}
|
|
1381
|
+
const timeout = options?.timeout || 30000; // Default 30 seconds
|
|
1382
|
+
const requestId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
1383
|
+
return new Promise((resolve, reject) => {
|
|
1384
|
+
let timeoutId = null;
|
|
1385
|
+
let messageHandler = null;
|
|
1386
|
+
// Cleanup function
|
|
1387
|
+
const cleanup = () => {
|
|
1388
|
+
if (timeoutId) {
|
|
1389
|
+
clearTimeout(timeoutId);
|
|
1390
|
+
timeoutId = null;
|
|
1391
|
+
}
|
|
1392
|
+
if (messageHandler) {
|
|
1393
|
+
window.removeEventListener('message', messageHandler);
|
|
1394
|
+
messageHandler = null;
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
// Set up timeout
|
|
1398
|
+
timeoutId = setTimeout(() => {
|
|
1399
|
+
cleanup();
|
|
1400
|
+
reject(new Error(`Token request timeout after ${timeout}ms`));
|
|
1401
|
+
}, timeout);
|
|
1402
|
+
// Set up message listener
|
|
1403
|
+
messageHandler = (event) => {
|
|
1404
|
+
const data = event.data;
|
|
1405
|
+
// Check if this is a response to our request
|
|
1406
|
+
if (data &&
|
|
1407
|
+
typeof data === 'object' &&
|
|
1408
|
+
data.requestId === requestId) {
|
|
1409
|
+
if (data.type === 'seaverse:token') {
|
|
1410
|
+
cleanup();
|
|
1411
|
+
const tokenData = data;
|
|
1412
|
+
resolve(tokenData.payload.accessToken);
|
|
1413
|
+
}
|
|
1414
|
+
else if (data.type === 'seaverse:token_error') {
|
|
1415
|
+
cleanup();
|
|
1416
|
+
const errorData = data;
|
|
1417
|
+
reject(new Error(errorData.error || 'Failed to get token from parent'));
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
};
|
|
1421
|
+
window.addEventListener('message', messageHandler);
|
|
1422
|
+
// Send request to parent
|
|
1423
|
+
const requestMessage = {
|
|
1424
|
+
type: 'seaverse:get_token',
|
|
1425
|
+
requestId,
|
|
1426
|
+
};
|
|
1427
|
+
window.parent.postMessage(requestMessage, '*');
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1288
1430
|
// ============================================================================
|
|
1289
1431
|
// Authentication & OAuth APIs
|
|
1290
1432
|
// ============================================================================
|