agentlink-sdk 1.0.3 → 1.0.5
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 +357 -0
- package/dist/index.d.mts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +93 -19
- package/dist/index.mjs +94 -20
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# AgentLink SDK
|
|
2
|
+
|
|
3
|
+
AgentLink 客户端 SDK,用于跨域数据同步,通过 URL hash 传递数据,支持 Token 验证和白名单机制。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
### npm
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install agentlink-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### yarn
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
yarn add agentlink-sdk
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### pnpm
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm add agentlink-sdk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### CDN
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<script type="module">
|
|
29
|
+
import { AgentLinkClient } from 'https://cdn.jsdelivr.net/npm/agentlink-sdk@latest/dist/index.mjs';
|
|
30
|
+
</script>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 快速开始
|
|
34
|
+
|
|
35
|
+
### 基本使用
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { AgentLinkClient } from 'agentlink-sdk';
|
|
39
|
+
|
|
40
|
+
// 创建客户端实例
|
|
41
|
+
const client = new AgentLinkClient({
|
|
42
|
+
serverUrl: 'https://agent-link-server.vercel.app'
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// 发送数据
|
|
46
|
+
await client.sendData(
|
|
47
|
+
'https://target-domain.com/page',
|
|
48
|
+
{ message: 'Hello from AgentLink' },
|
|
49
|
+
'greeting'
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// 接收数据
|
|
53
|
+
client.receiveData((data, type, senderInfo) => {
|
|
54
|
+
console.log('收到数据:', data);
|
|
55
|
+
console.log('数据类型:', type);
|
|
56
|
+
console.log('发送方信息:', senderInfo);
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API 文档
|
|
61
|
+
|
|
62
|
+
### AgentLinkClient
|
|
63
|
+
|
|
64
|
+
#### 构造函数
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
new AgentLinkClient(options: AgentLinkClientOptions)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**参数:**
|
|
71
|
+
|
|
72
|
+
- `options.serverUrl` (string, 必需): 服务端验证地址,例如 `'https://agentlink-server.vercel.app'`
|
|
73
|
+
|
|
74
|
+
#### 方法
|
|
75
|
+
|
|
76
|
+
##### sendData
|
|
77
|
+
|
|
78
|
+
发送数据到目标应用。
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
async sendData(
|
|
82
|
+
targetUrl: string,
|
|
83
|
+
data: any,
|
|
84
|
+
type: string,
|
|
85
|
+
windowName?: string
|
|
86
|
+
): Promise<void>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**参数:**
|
|
90
|
+
|
|
91
|
+
- `targetUrl` (string, 必需): 目标应用的完整 URL
|
|
92
|
+
- `data` (any, 必需): 要发送的数据对象
|
|
93
|
+
- `type` (string, 必需): 数据类型标识
|
|
94
|
+
- `windowName` (string, 可选): 窗口名称,用于复用窗口,默认为 `'agentlink-window'`
|
|
95
|
+
|
|
96
|
+
**示例:**
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
await client.sendData(
|
|
100
|
+
'https://example.com/receive',
|
|
101
|
+
{
|
|
102
|
+
message: 'Hello',
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
user: { id: 123, name: '张三' }
|
|
105
|
+
},
|
|
106
|
+
'user-message'
|
|
107
|
+
);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
##### receiveData
|
|
111
|
+
|
|
112
|
+
监听来自 URL hash 的数据。
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
receiveData(
|
|
116
|
+
callback: (
|
|
117
|
+
data: any,
|
|
118
|
+
type: string,
|
|
119
|
+
senderInfo?: SenderInfo,
|
|
120
|
+
verification?: string
|
|
121
|
+
) => void
|
|
122
|
+
): () => void
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**参数:**
|
|
126
|
+
|
|
127
|
+
- `callback` (function, 必需): 接收到数据时的回调函数
|
|
128
|
+
- `data`: 接收到的数据
|
|
129
|
+
- `type`: 数据类型
|
|
130
|
+
- `senderInfo`: 发送方信息(包含 domain 和 description)
|
|
131
|
+
- `verification`: 验证状态信息
|
|
132
|
+
|
|
133
|
+
**返回值:**
|
|
134
|
+
|
|
135
|
+
返回一个取消监听的函数。
|
|
136
|
+
|
|
137
|
+
**示例:**
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const unsubscribe = client.receiveData((data, type, senderInfo, verification) => {
|
|
141
|
+
console.log('收到数据:', data);
|
|
142
|
+
console.log('数据类型:', type);
|
|
143
|
+
if (senderInfo) {
|
|
144
|
+
console.log('发送方域名:', senderInfo.domain);
|
|
145
|
+
console.log('发送方描述:', senderInfo.description);
|
|
146
|
+
}
|
|
147
|
+
console.log('验证状态:', verification);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// 取消监听
|
|
151
|
+
unsubscribe();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
##### getDataFromUrl
|
|
155
|
+
|
|
156
|
+
从当前 URL 或指定 URL 获取数据并验证发送方。
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
async getDataFromUrl(url?: string): Promise<{
|
|
160
|
+
data: any;
|
|
161
|
+
type: string;
|
|
162
|
+
senderInfo?: SenderInfo;
|
|
163
|
+
verification?: string;
|
|
164
|
+
} | null>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**参数:**
|
|
168
|
+
|
|
169
|
+
- `url` (string, 可选): 要解析的 URL,默认为当前页面的 URL
|
|
170
|
+
|
|
171
|
+
**返回值:**
|
|
172
|
+
|
|
173
|
+
如果 URL 中包含数据,返回包含 `data`、`type`、`senderInfo` 和 `verification` 的对象;否则返回 `null`。
|
|
174
|
+
|
|
175
|
+
**示例:**
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
const result = await client.getDataFromUrl();
|
|
179
|
+
if (result) {
|
|
180
|
+
console.log('数据:', result.data);
|
|
181
|
+
console.log('类型:', result.type);
|
|
182
|
+
console.log('发送方:', result.senderInfo);
|
|
183
|
+
console.log('验证状态:', result.verification);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
##### getWhitelistInfo
|
|
188
|
+
|
|
189
|
+
获取白名单信息。
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
async getWhitelistInfo(includeAll?: boolean): Promise<WhitelistResponse>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**参数:**
|
|
196
|
+
|
|
197
|
+
- `includeAll` (boolean, 可选): 是否获取所有白名单信息,默认为 `false`(仅获取当前域名)
|
|
198
|
+
|
|
199
|
+
**返回值:**
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
{
|
|
203
|
+
whitelist: WhitelistInfo | WhitelistInfo[] | null;
|
|
204
|
+
origin: string | null;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**示例:**
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// 获取当前域名的白名单信息
|
|
212
|
+
const info = await client.getWhitelistInfo(false);
|
|
213
|
+
console.log('当前域名白名单:', info.whitelist);
|
|
214
|
+
|
|
215
|
+
// 获取所有白名单信息
|
|
216
|
+
const allInfo = await client.getWhitelistInfo(true);
|
|
217
|
+
console.log('所有白名单:', allInfo.whitelist);
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## 类型定义
|
|
221
|
+
|
|
222
|
+
### AgentLinkClientOptions
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
interface AgentLinkClientOptions {
|
|
226
|
+
serverUrl: string;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### SenderInfo
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
interface SenderInfo {
|
|
234
|
+
domain: string;
|
|
235
|
+
description: string | null;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### WhitelistInfo
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
interface WhitelistInfo {
|
|
243
|
+
domain: string;
|
|
244
|
+
description?: string | null;
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### WhitelistResponse
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
interface WhitelistResponse {
|
|
252
|
+
whitelist: WhitelistInfo | WhitelistInfo[] | null;
|
|
253
|
+
origin: string | null;
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## 特性
|
|
258
|
+
|
|
259
|
+
- ✅ **跨域数据同步**: 通过 URL hash 实现跨域数据传输
|
|
260
|
+
- ✅ **Token 验证**: 自动获取和验证 Token,确保数据来源可信
|
|
261
|
+
- ✅ **白名单机制**: 支持域名白名单验证
|
|
262
|
+
- ✅ **数据压缩**: 自动压缩大型数据,优化 URL 长度
|
|
263
|
+
- ✅ **缓存优化**: Token 和白名单验证结果缓存,减少服务器请求
|
|
264
|
+
- ✅ **TypeScript 支持**: 完整的 TypeScript 类型定义
|
|
265
|
+
- ✅ **窗口复用**: 支持复用窗口,避免频繁打开新窗口
|
|
266
|
+
|
|
267
|
+
## 工作原理
|
|
268
|
+
|
|
269
|
+
1. **发送数据**:
|
|
270
|
+
- SDK 自动从服务器获取当前域名的 Token
|
|
271
|
+
- 将数据编码到 URL hash 中(包含 Token 和验证信息)
|
|
272
|
+
- 通过 `window.open` 打开目标页面并传递数据
|
|
273
|
+
|
|
274
|
+
2. **接收数据**:
|
|
275
|
+
- 监听 `hashchange` 事件
|
|
276
|
+
- 从 URL hash 中解码数据
|
|
277
|
+
- 验证发送方的 Token 和 Origin
|
|
278
|
+
- 返回数据及发送方信息
|
|
279
|
+
|
|
280
|
+
3. **安全机制**:
|
|
281
|
+
- Token 验证:确保数据来自已注册的域名
|
|
282
|
+
- 白名单验证:检查域名是否在白名单中
|
|
283
|
+
- 缓存机制:减少重复验证请求
|
|
284
|
+
|
|
285
|
+
## 示例
|
|
286
|
+
|
|
287
|
+
完整示例请参考 [example.html](./example.html)
|
|
288
|
+
|
|
289
|
+
### 发送数据示例
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const client = new AgentLinkClient({
|
|
293
|
+
serverUrl: 'https://agent-link-server.vercel.app'
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// 发送复杂数据
|
|
297
|
+
await client.sendData(
|
|
298
|
+
'https://target-app.com/receive',
|
|
299
|
+
{
|
|
300
|
+
action: 'update',
|
|
301
|
+
payload: {
|
|
302
|
+
userId: 123,
|
|
303
|
+
items: ['item1', 'item2', 'item3']
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
'user-action'
|
|
307
|
+
);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### 接收数据示例
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
const client = new AgentLinkClient({
|
|
314
|
+
serverUrl: 'https://agent-link-server.vercel.app'
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// 监听数据
|
|
318
|
+
client.receiveData((data, type, senderInfo, verification) => {
|
|
319
|
+
if (senderInfo) {
|
|
320
|
+
console.log(`来自 ${senderInfo.domain} 的数据`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
switch (type) {
|
|
324
|
+
case 'user-action':
|
|
325
|
+
handleUserAction(data);
|
|
326
|
+
break;
|
|
327
|
+
case 'greeting':
|
|
328
|
+
handleGreeting(data);
|
|
329
|
+
break;
|
|
330
|
+
default:
|
|
331
|
+
console.log('未知数据类型:', type);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## 注意事项
|
|
337
|
+
|
|
338
|
+
1. **弹窗拦截**: 某些浏览器可能会拦截 `window.open`,确保在用户交互事件(如点击)中调用 `sendData`
|
|
339
|
+
2. **URL 长度限制**: 虽然 SDK 会自动压缩数据,但过大的数据仍可能超出浏览器 URL 长度限制
|
|
340
|
+
3. **HTTPS 要求**: 生产环境建议使用 HTTPS,确保数据传输安全
|
|
341
|
+
4. **Token 缓存**: Token 在实例级别缓存,同一实例的多次调用会复用 Token
|
|
342
|
+
|
|
343
|
+
## 浏览器支持
|
|
344
|
+
|
|
345
|
+
- Chrome/Edge (最新版本)
|
|
346
|
+
- Firefox (最新版本)
|
|
347
|
+
- Safari (最新版本)
|
|
348
|
+
|
|
349
|
+
## 许可证
|
|
350
|
+
|
|
351
|
+
MIT
|
|
352
|
+
|
|
353
|
+
## 相关链接
|
|
354
|
+
|
|
355
|
+
- [示例文件](./example.html)
|
|
356
|
+
- [GitHub 仓库](https://github.com/your-org/AgentLink)
|
|
357
|
+
|
package/dist/index.d.mts
CHANGED
|
@@ -26,6 +26,7 @@ declare class AgentLinkClient {
|
|
|
26
26
|
private token;
|
|
27
27
|
private tokenPromise;
|
|
28
28
|
private static tokenCache;
|
|
29
|
+
private static whitelistCache;
|
|
29
30
|
private static CACHE_TTL;
|
|
30
31
|
constructor(options: AgentLinkClientOptions);
|
|
31
32
|
/**
|
|
@@ -36,6 +37,10 @@ declare class AgentLinkClient {
|
|
|
36
37
|
* 从服务器获取 Token
|
|
37
38
|
*/
|
|
38
39
|
private fetchToken;
|
|
40
|
+
/**
|
|
41
|
+
* 检查域名是否在白名单中
|
|
42
|
+
*/
|
|
43
|
+
private checkWhitelist;
|
|
39
44
|
/**
|
|
40
45
|
* 验证发送端的 Token 和 Origin
|
|
41
46
|
*/
|
|
@@ -50,16 +55,18 @@ declare class AgentLinkClient {
|
|
|
50
55
|
sendData(targetUrl: string, data: any, type: string, windowName?: string): Promise<void>;
|
|
51
56
|
/**
|
|
52
57
|
* 监听来自 URL 的数据
|
|
53
|
-
* @param callback
|
|
58
|
+
* @param callback 接收到数据时的回调函数,现在包含验证信息
|
|
54
59
|
*/
|
|
55
|
-
receiveData(callback: (data: any, type: string, senderInfo?: SenderInfo) => void): () => void;
|
|
60
|
+
receiveData(callback: (data: any, type: string, senderInfo?: SenderInfo, verification?: string) => void): () => void;
|
|
56
61
|
/**
|
|
57
62
|
* 从当前 URL 获取数据并验证发送方
|
|
63
|
+
* 即使验证失败也返回数据,但会标记验证状态
|
|
58
64
|
*/
|
|
59
65
|
getDataFromUrl(url?: string): Promise<{
|
|
60
66
|
data: any;
|
|
61
67
|
type: string;
|
|
62
68
|
senderInfo?: SenderInfo;
|
|
69
|
+
verification?: string;
|
|
63
70
|
} | null>;
|
|
64
71
|
/**
|
|
65
72
|
* 获取白名单信息
|
|
@@ -81,6 +88,7 @@ declare function decompress(compressedData: Uint8Array): Promise<string>;
|
|
|
81
88
|
declare function uint8ArrayToBase64(arr: Uint8Array): string;
|
|
82
89
|
/**
|
|
83
90
|
* 将 URL 安全的 base64 字符串转换为 Uint8Array
|
|
91
|
+
* 如果输入无效,会抛出错误(由调用者处理)
|
|
84
92
|
*/
|
|
85
93
|
declare function base64ToUint8Array(base64: string): Uint8Array;
|
|
86
94
|
|
|
@@ -89,11 +97,12 @@ interface URLData {
|
|
|
89
97
|
type: string;
|
|
90
98
|
token: string;
|
|
91
99
|
origin: string;
|
|
100
|
+
verification?: string;
|
|
92
101
|
}
|
|
93
102
|
/**
|
|
94
103
|
* 将数据编码为 URL hash
|
|
95
104
|
*/
|
|
96
|
-
declare function encodeDataToUrl(data: any, type: string, token: string, origin: string, baseUrl: string): Promise<string>;
|
|
105
|
+
declare function encodeDataToUrl(data: any, type: string, token: string, origin: string, baseUrl: string, verification?: string): Promise<string>;
|
|
97
106
|
/**
|
|
98
107
|
* 从 URL 中解码数据
|
|
99
108
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ declare class AgentLinkClient {
|
|
|
26
26
|
private token;
|
|
27
27
|
private tokenPromise;
|
|
28
28
|
private static tokenCache;
|
|
29
|
+
private static whitelistCache;
|
|
29
30
|
private static CACHE_TTL;
|
|
30
31
|
constructor(options: AgentLinkClientOptions);
|
|
31
32
|
/**
|
|
@@ -36,6 +37,10 @@ declare class AgentLinkClient {
|
|
|
36
37
|
* 从服务器获取 Token
|
|
37
38
|
*/
|
|
38
39
|
private fetchToken;
|
|
40
|
+
/**
|
|
41
|
+
* 检查域名是否在白名单中
|
|
42
|
+
*/
|
|
43
|
+
private checkWhitelist;
|
|
39
44
|
/**
|
|
40
45
|
* 验证发送端的 Token 和 Origin
|
|
41
46
|
*/
|
|
@@ -50,16 +55,18 @@ declare class AgentLinkClient {
|
|
|
50
55
|
sendData(targetUrl: string, data: any, type: string, windowName?: string): Promise<void>;
|
|
51
56
|
/**
|
|
52
57
|
* 监听来自 URL 的数据
|
|
53
|
-
* @param callback
|
|
58
|
+
* @param callback 接收到数据时的回调函数,现在包含验证信息
|
|
54
59
|
*/
|
|
55
|
-
receiveData(callback: (data: any, type: string, senderInfo?: SenderInfo) => void): () => void;
|
|
60
|
+
receiveData(callback: (data: any, type: string, senderInfo?: SenderInfo, verification?: string) => void): () => void;
|
|
56
61
|
/**
|
|
57
62
|
* 从当前 URL 获取数据并验证发送方
|
|
63
|
+
* 即使验证失败也返回数据,但会标记验证状态
|
|
58
64
|
*/
|
|
59
65
|
getDataFromUrl(url?: string): Promise<{
|
|
60
66
|
data: any;
|
|
61
67
|
type: string;
|
|
62
68
|
senderInfo?: SenderInfo;
|
|
69
|
+
verification?: string;
|
|
63
70
|
} | null>;
|
|
64
71
|
/**
|
|
65
72
|
* 获取白名单信息
|
|
@@ -81,6 +88,7 @@ declare function decompress(compressedData: Uint8Array): Promise<string>;
|
|
|
81
88
|
declare function uint8ArrayToBase64(arr: Uint8Array): string;
|
|
82
89
|
/**
|
|
83
90
|
* 将 URL 安全的 base64 字符串转换为 Uint8Array
|
|
91
|
+
* 如果输入无效,会抛出错误(由调用者处理)
|
|
84
92
|
*/
|
|
85
93
|
declare function base64ToUint8Array(base64: string): Uint8Array;
|
|
86
94
|
|
|
@@ -89,11 +97,12 @@ interface URLData {
|
|
|
89
97
|
type: string;
|
|
90
98
|
token: string;
|
|
91
99
|
origin: string;
|
|
100
|
+
verification?: string;
|
|
92
101
|
}
|
|
93
102
|
/**
|
|
94
103
|
* 将数据编码为 URL hash
|
|
95
104
|
*/
|
|
96
|
-
declare function encodeDataToUrl(data: any, type: string, token: string, origin: string, baseUrl: string): Promise<string>;
|
|
105
|
+
declare function encodeDataToUrl(data: any, type: string, token: string, origin: string, baseUrl: string, verification?: string): Promise<string>;
|
|
97
106
|
/**
|
|
98
107
|
* 从 URL 中解码数据
|
|
99
108
|
*/
|
package/dist/index.js
CHANGED
|
@@ -55,19 +55,45 @@ function uint8ArrayToBase64(arr) {
|
|
|
55
55
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
56
56
|
}
|
|
57
57
|
function base64ToUint8Array(base64) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
if (!base64 || typeof base64 !== "string") {
|
|
59
|
+
const error = new Error("Invalid base64 string: input must be a non-empty string");
|
|
60
|
+
error.isInvalidBase64 = true;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const cleaned = base64.trim().replace(/[\s\n\r]/g, "");
|
|
64
|
+
if (!cleaned) {
|
|
65
|
+
const error = new Error("Invalid base64 string: empty after cleaning");
|
|
66
|
+
error.isInvalidBase64 = true;
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
const base64Regex = /^[A-Za-z0-9\-_]+$/;
|
|
70
|
+
if (!base64Regex.test(cleaned)) {
|
|
71
|
+
const error = new Error(`Invalid base64 string: contains invalid characters`);
|
|
72
|
+
error.isInvalidBase64 = true;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const padding = "=".repeat((4 - cleaned.length % 4) % 4);
|
|
77
|
+
const standardBase64 = cleaned.replace(/-/g, "+").replace(/_/g, "/") + padding;
|
|
78
|
+
const binary = atob(standardBase64);
|
|
79
|
+
const arr = new Uint8Array(binary.length);
|
|
80
|
+
for (let i = 0; i < binary.length; i++) {
|
|
81
|
+
arr[i] = binary.charCodeAt(i);
|
|
82
|
+
}
|
|
83
|
+
return arr;
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const base64Error = error instanceof Error ? error : new Error("Failed to decode base64");
|
|
86
|
+
base64Error.isInvalidBase64 = true;
|
|
87
|
+
throw base64Error;
|
|
64
88
|
}
|
|
65
|
-
return arr;
|
|
66
89
|
}
|
|
67
90
|
|
|
68
91
|
// src/utils/url.ts
|
|
69
|
-
async function encodeDataToUrl(data, type, token, origin, baseUrl) {
|
|
92
|
+
async function encodeDataToUrl(data, type, token, origin, baseUrl, verification) {
|
|
70
93
|
const payload = { data, type, token, origin };
|
|
94
|
+
if (verification !== void 0) {
|
|
95
|
+
payload.verification = verification;
|
|
96
|
+
}
|
|
71
97
|
const jsonString = JSON.stringify(payload);
|
|
72
98
|
const compressed = await compress(jsonString);
|
|
73
99
|
const base64 = uint8ArrayToBase64(compressed);
|
|
@@ -79,12 +105,24 @@ async function decodeDataFromUrl(urlStr) {
|
|
|
79
105
|
try {
|
|
80
106
|
const url = new URL(urlStr);
|
|
81
107
|
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
82
|
-
if (!hash)
|
|
108
|
+
if (!hash || hash.trim().length === 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (hash.length < 10) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
83
114
|
const compressed = base64ToUint8Array(hash);
|
|
84
115
|
const jsonString = await decompress(compressed);
|
|
85
116
|
return JSON.parse(jsonString);
|
|
86
117
|
} catch (error) {
|
|
87
|
-
|
|
118
|
+
if (error && typeof error === "object" && error.isInvalidBase64) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
if (error instanceof Error) {
|
|
122
|
+
if (false) {
|
|
123
|
+
console.debug("[AgentLink] Failed to decode data from URL:", error.message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
88
126
|
return null;
|
|
89
127
|
}
|
|
90
128
|
}
|
|
@@ -170,6 +208,21 @@ var _AgentLinkClient = class _AgentLinkClient {
|
|
|
170
208
|
this.token = token;
|
|
171
209
|
return token;
|
|
172
210
|
}
|
|
211
|
+
/**
|
|
212
|
+
* 检查域名是否在白名单中
|
|
213
|
+
*/
|
|
214
|
+
async checkWhitelist(origin) {
|
|
215
|
+
const cached = _AgentLinkClient.whitelistCache.get(origin);
|
|
216
|
+
if (cached && Date.now() - cached.timestamp < _AgentLinkClient.CACHE_TTL) {
|
|
217
|
+
return cached.verified;
|
|
218
|
+
}
|
|
219
|
+
const verified = await verifyWhitelist(this.serverUrl, origin);
|
|
220
|
+
_AgentLinkClient.whitelistCache.set(origin, {
|
|
221
|
+
verified,
|
|
222
|
+
timestamp: Date.now()
|
|
223
|
+
});
|
|
224
|
+
return verified;
|
|
225
|
+
}
|
|
173
226
|
/**
|
|
174
227
|
* 验证发送端的 Token 和 Origin
|
|
175
228
|
*/
|
|
@@ -205,7 +258,9 @@ var _AgentLinkClient = class _AgentLinkClient {
|
|
|
205
258
|
async sendData(targetUrl, data, type, windowName = _AgentLinkClient.DEFAULT_WINDOW_NAME) {
|
|
206
259
|
const token = await this.ensureToken();
|
|
207
260
|
const origin = window.location.origin;
|
|
208
|
-
const
|
|
261
|
+
const isWhitelisted = await this.checkWhitelist(origin);
|
|
262
|
+
const verification = isWhitelisted ? "mixlab launchpad\u52A0\u901F\u8BA1\u5212" : "\u672A\u9A8C\u8BC1";
|
|
263
|
+
const encodedUrl = await encodeDataToUrl(data, type, token, origin, targetUrl, verification);
|
|
209
264
|
const targetWindow = window.open(encodedUrl, windowName);
|
|
210
265
|
if (!targetWindow) {
|
|
211
266
|
throw new Error("[AgentLink] Failed to open target window. It might be blocked by a popup blocker.");
|
|
@@ -214,23 +269,36 @@ var _AgentLinkClient = class _AgentLinkClient {
|
|
|
214
269
|
}
|
|
215
270
|
/**
|
|
216
271
|
* 监听来自 URL 的数据
|
|
217
|
-
* @param callback
|
|
272
|
+
* @param callback 接收到数据时的回调函数,现在包含验证信息
|
|
218
273
|
*/
|
|
219
274
|
receiveData(callback) {
|
|
220
275
|
const handleHashChange = async () => {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
276
|
+
try {
|
|
277
|
+
const result = await this.getDataFromUrl();
|
|
278
|
+
if (result) {
|
|
279
|
+
callback(result.data, result.type, result.senderInfo, result.verification);
|
|
280
|
+
}
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (false) {
|
|
283
|
+
console.debug("[AgentLink] Error in handleHashChange (silently handled):", error);
|
|
284
|
+
}
|
|
224
285
|
}
|
|
225
286
|
};
|
|
226
287
|
window.addEventListener("hashchange", handleHashChange);
|
|
227
|
-
|
|
288
|
+
try {
|
|
289
|
+
handleHashChange();
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (false) {
|
|
292
|
+
console.debug("[AgentLink] Error in initial hash check (silently handled):", error);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
228
295
|
return () => {
|
|
229
296
|
window.removeEventListener("hashchange", handleHashChange);
|
|
230
297
|
};
|
|
231
298
|
}
|
|
232
299
|
/**
|
|
233
300
|
* 从当前 URL 获取数据并验证发送方
|
|
301
|
+
* 即使验证失败也返回数据,但会标记验证状态
|
|
234
302
|
*/
|
|
235
303
|
async getDataFromUrl(url) {
|
|
236
304
|
const targetUrl = url || window.location.href;
|
|
@@ -239,14 +307,18 @@ var _AgentLinkClient = class _AgentLinkClient {
|
|
|
239
307
|
return null;
|
|
240
308
|
}
|
|
241
309
|
const senderInfo = await this.verifySender(urlData.token, urlData.origin);
|
|
310
|
+
let verification = urlData.verification;
|
|
311
|
+
if (!verification) {
|
|
312
|
+
verification = senderInfo ? "mixlab launchpad\u52A0\u901F\u8BA1\u5212" : "\u672A\u9A8C\u8BC1";
|
|
313
|
+
}
|
|
242
314
|
if (!senderInfo) {
|
|
243
|
-
console.warn(`[AgentLink] Data received but sender verification failed`);
|
|
244
|
-
return null;
|
|
315
|
+
console.warn(`[AgentLink] Data received but sender verification failed. Verification: ${verification}`);
|
|
245
316
|
}
|
|
246
317
|
return {
|
|
247
318
|
data: urlData.data,
|
|
248
319
|
type: urlData.type,
|
|
249
|
-
senderInfo
|
|
320
|
+
senderInfo: senderInfo || void 0,
|
|
321
|
+
verification
|
|
250
322
|
};
|
|
251
323
|
}
|
|
252
324
|
/**
|
|
@@ -259,6 +331,8 @@ var _AgentLinkClient = class _AgentLinkClient {
|
|
|
259
331
|
_AgentLinkClient.DEFAULT_WINDOW_NAME = "agentlink-window";
|
|
260
332
|
// 接收端验证结果缓存(静态,跨实例共享)
|
|
261
333
|
_AgentLinkClient.tokenCache = /* @__PURE__ */ new Map();
|
|
334
|
+
// 白名单验证缓存(避免重复请求)
|
|
335
|
+
_AgentLinkClient.whitelistCache = /* @__PURE__ */ new Map();
|
|
262
336
|
// 缓存过期时间:1 小时
|
|
263
337
|
_AgentLinkClient.CACHE_TTL = 60 * 60 * 1e3;
|
|
264
338
|
var AgentLinkClient = _AgentLinkClient;
|
package/dist/index.mjs
CHANGED
|
@@ -21,19 +21,45 @@ function uint8ArrayToBase64(arr) {
|
|
|
21
21
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
22
22
|
}
|
|
23
23
|
function base64ToUint8Array(base64) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
if (!base64 || typeof base64 !== "string") {
|
|
25
|
+
const error = new Error("Invalid base64 string: input must be a non-empty string");
|
|
26
|
+
error.isInvalidBase64 = true;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
const cleaned = base64.trim().replace(/[\s\n\r]/g, "");
|
|
30
|
+
if (!cleaned) {
|
|
31
|
+
const error = new Error("Invalid base64 string: empty after cleaning");
|
|
32
|
+
error.isInvalidBase64 = true;
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
const base64Regex = /^[A-Za-z0-9\-_]+$/;
|
|
36
|
+
if (!base64Regex.test(cleaned)) {
|
|
37
|
+
const error = new Error(`Invalid base64 string: contains invalid characters`);
|
|
38
|
+
error.isInvalidBase64 = true;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const padding = "=".repeat((4 - cleaned.length % 4) % 4);
|
|
43
|
+
const standardBase64 = cleaned.replace(/-/g, "+").replace(/_/g, "/") + padding;
|
|
44
|
+
const binary = atob(standardBase64);
|
|
45
|
+
const arr = new Uint8Array(binary.length);
|
|
46
|
+
for (let i = 0; i < binary.length; i++) {
|
|
47
|
+
arr[i] = binary.charCodeAt(i);
|
|
48
|
+
}
|
|
49
|
+
return arr;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
const base64Error = error instanceof Error ? error : new Error("Failed to decode base64");
|
|
52
|
+
base64Error.isInvalidBase64 = true;
|
|
53
|
+
throw base64Error;
|
|
54
|
+
}
|
|
32
55
|
}
|
|
33
56
|
|
|
34
57
|
// src/utils/url.ts
|
|
35
|
-
async function encodeDataToUrl(data, type, token, origin, baseUrl) {
|
|
58
|
+
async function encodeDataToUrl(data, type, token, origin, baseUrl, verification) {
|
|
36
59
|
const payload = { data, type, token, origin };
|
|
60
|
+
if (verification !== void 0) {
|
|
61
|
+
payload.verification = verification;
|
|
62
|
+
}
|
|
37
63
|
const jsonString = JSON.stringify(payload);
|
|
38
64
|
const compressed = await compress(jsonString);
|
|
39
65
|
const base64 = uint8ArrayToBase64(compressed);
|
|
@@ -45,12 +71,24 @@ async function decodeDataFromUrl(urlStr) {
|
|
|
45
71
|
try {
|
|
46
72
|
const url = new URL(urlStr);
|
|
47
73
|
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
|
|
48
|
-
if (!hash)
|
|
74
|
+
if (!hash || hash.trim().length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
if (hash.length < 10) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
49
80
|
const compressed = base64ToUint8Array(hash);
|
|
50
81
|
const jsonString = await decompress(compressed);
|
|
51
82
|
return JSON.parse(jsonString);
|
|
52
83
|
} catch (error) {
|
|
53
|
-
|
|
84
|
+
if (error && typeof error === "object" && error.isInvalidBase64) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
if (error instanceof Error) {
|
|
88
|
+
if (false) {
|
|
89
|
+
console.debug("[AgentLink] Failed to decode data from URL:", error.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
54
92
|
return null;
|
|
55
93
|
}
|
|
56
94
|
}
|
|
@@ -136,6 +174,21 @@ var _AgentLinkClient = class _AgentLinkClient {
|
|
|
136
174
|
this.token = token;
|
|
137
175
|
return token;
|
|
138
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* 检查域名是否在白名单中
|
|
179
|
+
*/
|
|
180
|
+
async checkWhitelist(origin) {
|
|
181
|
+
const cached = _AgentLinkClient.whitelistCache.get(origin);
|
|
182
|
+
if (cached && Date.now() - cached.timestamp < _AgentLinkClient.CACHE_TTL) {
|
|
183
|
+
return cached.verified;
|
|
184
|
+
}
|
|
185
|
+
const verified = await verifyWhitelist(this.serverUrl, origin);
|
|
186
|
+
_AgentLinkClient.whitelistCache.set(origin, {
|
|
187
|
+
verified,
|
|
188
|
+
timestamp: Date.now()
|
|
189
|
+
});
|
|
190
|
+
return verified;
|
|
191
|
+
}
|
|
139
192
|
/**
|
|
140
193
|
* 验证发送端的 Token 和 Origin
|
|
141
194
|
*/
|
|
@@ -171,7 +224,9 @@ var _AgentLinkClient = class _AgentLinkClient {
|
|
|
171
224
|
async sendData(targetUrl, data, type, windowName = _AgentLinkClient.DEFAULT_WINDOW_NAME) {
|
|
172
225
|
const token = await this.ensureToken();
|
|
173
226
|
const origin = window.location.origin;
|
|
174
|
-
const
|
|
227
|
+
const isWhitelisted = await this.checkWhitelist(origin);
|
|
228
|
+
const verification = isWhitelisted ? "mixlab launchpad\u52A0\u901F\u8BA1\u5212" : "\u672A\u9A8C\u8BC1";
|
|
229
|
+
const encodedUrl = await encodeDataToUrl(data, type, token, origin, targetUrl, verification);
|
|
175
230
|
const targetWindow = window.open(encodedUrl, windowName);
|
|
176
231
|
if (!targetWindow) {
|
|
177
232
|
throw new Error("[AgentLink] Failed to open target window. It might be blocked by a popup blocker.");
|
|
@@ -180,23 +235,36 @@ var _AgentLinkClient = class _AgentLinkClient {
|
|
|
180
235
|
}
|
|
181
236
|
/**
|
|
182
237
|
* 监听来自 URL 的数据
|
|
183
|
-
* @param callback
|
|
238
|
+
* @param callback 接收到数据时的回调函数,现在包含验证信息
|
|
184
239
|
*/
|
|
185
240
|
receiveData(callback) {
|
|
186
241
|
const handleHashChange = async () => {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
242
|
+
try {
|
|
243
|
+
const result = await this.getDataFromUrl();
|
|
244
|
+
if (result) {
|
|
245
|
+
callback(result.data, result.type, result.senderInfo, result.verification);
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
if (false) {
|
|
249
|
+
console.debug("[AgentLink] Error in handleHashChange (silently handled):", error);
|
|
250
|
+
}
|
|
190
251
|
}
|
|
191
252
|
};
|
|
192
253
|
window.addEventListener("hashchange", handleHashChange);
|
|
193
|
-
|
|
254
|
+
try {
|
|
255
|
+
handleHashChange();
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (false) {
|
|
258
|
+
console.debug("[AgentLink] Error in initial hash check (silently handled):", error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
194
261
|
return () => {
|
|
195
262
|
window.removeEventListener("hashchange", handleHashChange);
|
|
196
263
|
};
|
|
197
264
|
}
|
|
198
265
|
/**
|
|
199
266
|
* 从当前 URL 获取数据并验证发送方
|
|
267
|
+
* 即使验证失败也返回数据,但会标记验证状态
|
|
200
268
|
*/
|
|
201
269
|
async getDataFromUrl(url) {
|
|
202
270
|
const targetUrl = url || window.location.href;
|
|
@@ -205,14 +273,18 @@ var _AgentLinkClient = class _AgentLinkClient {
|
|
|
205
273
|
return null;
|
|
206
274
|
}
|
|
207
275
|
const senderInfo = await this.verifySender(urlData.token, urlData.origin);
|
|
276
|
+
let verification = urlData.verification;
|
|
277
|
+
if (!verification) {
|
|
278
|
+
verification = senderInfo ? "mixlab launchpad\u52A0\u901F\u8BA1\u5212" : "\u672A\u9A8C\u8BC1";
|
|
279
|
+
}
|
|
208
280
|
if (!senderInfo) {
|
|
209
|
-
console.warn(`[AgentLink] Data received but sender verification failed`);
|
|
210
|
-
return null;
|
|
281
|
+
console.warn(`[AgentLink] Data received but sender verification failed. Verification: ${verification}`);
|
|
211
282
|
}
|
|
212
283
|
return {
|
|
213
284
|
data: urlData.data,
|
|
214
285
|
type: urlData.type,
|
|
215
|
-
senderInfo
|
|
286
|
+
senderInfo: senderInfo || void 0,
|
|
287
|
+
verification
|
|
216
288
|
};
|
|
217
289
|
}
|
|
218
290
|
/**
|
|
@@ -225,6 +297,8 @@ var _AgentLinkClient = class _AgentLinkClient {
|
|
|
225
297
|
_AgentLinkClient.DEFAULT_WINDOW_NAME = "agentlink-window";
|
|
226
298
|
// 接收端验证结果缓存(静态,跨实例共享)
|
|
227
299
|
_AgentLinkClient.tokenCache = /* @__PURE__ */ new Map();
|
|
300
|
+
// 白名单验证缓存(避免重复请求)
|
|
301
|
+
_AgentLinkClient.whitelistCache = /* @__PURE__ */ new Map();
|
|
228
302
|
// 缓存过期时间:1 小时
|
|
229
303
|
_AgentLinkClient.CACHE_TTL = 60 * 60 * 1e3;
|
|
230
304
|
var AgentLinkClient = _AgentLinkClient;
|