agentlink-sdk 1.0.4 → 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 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
@@ -88,6 +88,7 @@ declare function decompress(compressedData: Uint8Array): Promise<string>;
88
88
  declare function uint8ArrayToBase64(arr: Uint8Array): string;
89
89
  /**
90
90
  * 将 URL 安全的 base64 字符串转换为 Uint8Array
91
+ * 如果输入无效,会抛出错误(由调用者处理)
91
92
  */
92
93
  declare function base64ToUint8Array(base64: string): Uint8Array;
93
94
 
package/dist/index.d.ts CHANGED
@@ -88,6 +88,7 @@ declare function decompress(compressedData: Uint8Array): Promise<string>;
88
88
  declare function uint8ArrayToBase64(arr: Uint8Array): string;
89
89
  /**
90
90
  * 将 URL 安全的 base64 字符串转换为 Uint8Array
91
+ * 如果输入无效,会抛出错误(由调用者处理)
91
92
  */
92
93
  declare function base64ToUint8Array(base64: string): Uint8Array;
93
94
 
package/dist/index.js CHANGED
@@ -55,14 +55,37 @@ function uint8ArrayToBase64(arr) {
55
55
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
56
56
  }
57
57
  function base64ToUint8Array(base64) {
58
- const padding = "=".repeat((4 - base64.length % 4) % 4);
59
- const standardBase64 = base64.replace(/-/g, "+").replace(/_/g, "/") + padding;
60
- const binary = atob(standardBase64);
61
- const arr = new Uint8Array(binary.length);
62
- for (let i = 0; i < binary.length; i++) {
63
- arr[i] = binary.charCodeAt(i);
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
@@ -82,12 +105,24 @@ async function decodeDataFromUrl(urlStr) {
82
105
  try {
83
106
  const url = new URL(urlStr);
84
107
  const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
85
- if (!hash) return null;
108
+ if (!hash || hash.trim().length === 0) {
109
+ return null;
110
+ }
111
+ if (hash.length < 10) {
112
+ return null;
113
+ }
86
114
  const compressed = base64ToUint8Array(hash);
87
115
  const jsonString = await decompress(compressed);
88
116
  return JSON.parse(jsonString);
89
117
  } catch (error) {
90
- console.error("[AgentLink] Failed to decode data from URL:", error);
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
+ }
91
126
  return null;
92
127
  }
93
128
  }
@@ -238,13 +273,25 @@ var _AgentLinkClient = class _AgentLinkClient {
238
273
  */
239
274
  receiveData(callback) {
240
275
  const handleHashChange = async () => {
241
- const result = await this.getDataFromUrl();
242
- if (result) {
243
- callback(result.data, result.type, result.senderInfo, result.verification);
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
+ }
244
285
  }
245
286
  };
246
287
  window.addEventListener("hashchange", handleHashChange);
247
- handleHashChange();
288
+ try {
289
+ handleHashChange();
290
+ } catch (error) {
291
+ if (false) {
292
+ console.debug("[AgentLink] Error in initial hash check (silently handled):", error);
293
+ }
294
+ }
248
295
  return () => {
249
296
  window.removeEventListener("hashchange", handleHashChange);
250
297
  };
package/dist/index.mjs CHANGED
@@ -21,14 +21,37 @@ function uint8ArrayToBase64(arr) {
21
21
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
22
22
  }
23
23
  function base64ToUint8Array(base64) {
24
- const padding = "=".repeat((4 - base64.length % 4) % 4);
25
- const standardBase64 = base64.replace(/-/g, "+").replace(/_/g, "/") + padding;
26
- const binary = atob(standardBase64);
27
- const arr = new Uint8Array(binary.length);
28
- for (let i = 0; i < binary.length; i++) {
29
- arr[i] = binary.charCodeAt(i);
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;
30
54
  }
31
- return arr;
32
55
  }
33
56
 
34
57
  // src/utils/url.ts
@@ -48,12 +71,24 @@ async function decodeDataFromUrl(urlStr) {
48
71
  try {
49
72
  const url = new URL(urlStr);
50
73
  const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
51
- if (!hash) return null;
74
+ if (!hash || hash.trim().length === 0) {
75
+ return null;
76
+ }
77
+ if (hash.length < 10) {
78
+ return null;
79
+ }
52
80
  const compressed = base64ToUint8Array(hash);
53
81
  const jsonString = await decompress(compressed);
54
82
  return JSON.parse(jsonString);
55
83
  } catch (error) {
56
- console.error("[AgentLink] Failed to decode data from URL:", error);
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
+ }
57
92
  return null;
58
93
  }
59
94
  }
@@ -204,13 +239,25 @@ var _AgentLinkClient = class _AgentLinkClient {
204
239
  */
205
240
  receiveData(callback) {
206
241
  const handleHashChange = async () => {
207
- const result = await this.getDataFromUrl();
208
- if (result) {
209
- callback(result.data, result.type, result.senderInfo, result.verification);
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
+ }
210
251
  }
211
252
  };
212
253
  window.addEventListener("hashchange", handleHashChange);
213
- handleHashChange();
254
+ try {
255
+ handleHashChange();
256
+ } catch (error) {
257
+ if (false) {
258
+ console.debug("[AgentLink] Error in initial hash check (silently handled):", error);
259
+ }
260
+ }
214
261
  return () => {
215
262
  window.removeEventListener("hashchange", handleHashChange);
216
263
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlink-sdk",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "AgentLink client SDK for cross-domain data synchronization via URL hash",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.esm.js",