@rei-standard/amsg-client 2.7.0 → 2.9.0-next.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/dist/index.cjs +24 -21
- package/dist/index.d.cts +34 -29
- package/dist/index.d.ts +34 -29
- package/dist/index.mjs +25 -22
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -36,7 +36,6 @@ module.exports = __toCommonJS(src_exports);
|
|
|
36
36
|
var import_amsg_shared = require("@rei-standard/amsg-shared");
|
|
37
37
|
var import_amsg_shared2 = require("@rei-standard/amsg-shared");
|
|
38
38
|
var TEXT_ENCODER = new TextEncoder();
|
|
39
|
-
var AVATAR_URL_MAX_LENGTH = 2048;
|
|
40
39
|
function makeLocalError(code, message, details) {
|
|
41
40
|
const err = new Error(`[rei-standard-amsg-client] ${message}`);
|
|
42
41
|
err.code = code;
|
|
@@ -98,6 +97,7 @@ var ReiClient = class {
|
|
|
98
97
|
this._userKey = null;
|
|
99
98
|
this._instantEncryption = instantEncryption;
|
|
100
99
|
this._instantClientToken = typeof config.instantClientToken === "string" && config.instantClientToken ? config.instantClientToken : "";
|
|
100
|
+
this._serverToken = typeof config.serverToken === "string" && config.serverToken ? config.serverToken : "";
|
|
101
101
|
this._maxPayloadBytes = normalizeMaxPayloadBytes(config.maxPayloadBytes);
|
|
102
102
|
this._lowLevelWarned = /* @__PURE__ */ new Set();
|
|
103
103
|
}
|
|
@@ -111,6 +111,17 @@ var ReiClient = class {
|
|
|
111
111
|
_resolveBaseUrl(endpointName) {
|
|
112
112
|
return this._customBaseUrls[endpointName] || this._baseUrl;
|
|
113
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* Attach the single-user shared secret to amsg-server endpoint requests.
|
|
116
|
+
* Never applied to the instant path (that uses instantClientToken).
|
|
117
|
+
* @private
|
|
118
|
+
* @param {Record<string, string>} headers
|
|
119
|
+
* @returns {Record<string, string>}
|
|
120
|
+
*/
|
|
121
|
+
_withServerToken(headers) {
|
|
122
|
+
if (this._serverToken) headers["X-Client-Token"] = this._serverToken;
|
|
123
|
+
return headers;
|
|
124
|
+
}
|
|
114
125
|
// ─── Initialisation ─────────────────────────────────────────────
|
|
115
126
|
/**
|
|
116
127
|
* Fetch the user-specific encryption key.
|
|
@@ -129,7 +140,7 @@ var ReiClient = class {
|
|
|
129
140
|
}
|
|
130
141
|
const res = await fetch(`${this._baseUrl}/get-user-key`, {
|
|
131
142
|
method: "GET",
|
|
132
|
-
headers: { "X-User-Id": this._userId }
|
|
143
|
+
headers: this._withServerToken({ "X-User-Id": this._userId })
|
|
133
144
|
});
|
|
134
145
|
const json = await res.json();
|
|
135
146
|
if (!json.success) throw new Error(json.error?.message || "Failed to fetch user key");
|
|
@@ -166,12 +177,12 @@ var ReiClient = class {
|
|
|
166
177
|
const encrypted = await this._encrypt(json);
|
|
167
178
|
const res = await fetch(`${this._baseUrl}/schedule-message`, {
|
|
168
179
|
method: "POST",
|
|
169
|
-
headers: {
|
|
180
|
+
headers: this._withServerToken({
|
|
170
181
|
"Content-Type": "application/json",
|
|
171
182
|
"X-User-Id": this._userId,
|
|
172
183
|
"X-Payload-Encrypted": "true",
|
|
173
184
|
"X-Encryption-Version": "1"
|
|
174
|
-
},
|
|
185
|
+
}),
|
|
175
186
|
body: JSON.stringify(encrypted)
|
|
176
187
|
});
|
|
177
188
|
return res.json();
|
|
@@ -509,12 +520,12 @@ var ReiClient = class {
|
|
|
509
520
|
const encrypted = await this._encrypt(json);
|
|
510
521
|
const res = await fetch(`${this._baseUrl}/update-message?id=${encodeURIComponent(uuid)}`, {
|
|
511
522
|
method: "PUT",
|
|
512
|
-
headers: {
|
|
523
|
+
headers: this._withServerToken({
|
|
513
524
|
"Content-Type": "application/json",
|
|
514
525
|
"X-User-Id": this._userId,
|
|
515
526
|
"X-Payload-Encrypted": "true",
|
|
516
527
|
"X-Encryption-Version": "1"
|
|
517
|
-
},
|
|
528
|
+
}),
|
|
518
529
|
body: JSON.stringify(encrypted)
|
|
519
530
|
});
|
|
520
531
|
return res.json();
|
|
@@ -528,7 +539,7 @@ var ReiClient = class {
|
|
|
528
539
|
async cancelMessage(uuid) {
|
|
529
540
|
const res = await fetch(`${this._baseUrl}/cancel-message?id=${encodeURIComponent(uuid)}`, {
|
|
530
541
|
method: "DELETE",
|
|
531
|
-
headers: { "X-User-Id": this._userId }
|
|
542
|
+
headers: this._withServerToken({ "X-User-Id": this._userId })
|
|
532
543
|
});
|
|
533
544
|
return res.json();
|
|
534
545
|
}
|
|
@@ -550,11 +561,11 @@ var ReiClient = class {
|
|
|
550
561
|
const url = `${this._baseUrl}/messages${qs ? "?" + qs : ""}`;
|
|
551
562
|
const res = await fetch(url, {
|
|
552
563
|
method: "GET",
|
|
553
|
-
headers: {
|
|
564
|
+
headers: this._withServerToken({
|
|
554
565
|
"X-User-Id": this._userId,
|
|
555
566
|
"X-Response-Encrypted": "true",
|
|
556
567
|
"X-Encryption-Version": "1"
|
|
557
|
-
}
|
|
568
|
+
})
|
|
558
569
|
});
|
|
559
570
|
const json = await res.json();
|
|
560
571
|
if (!json?.success || json?.encrypted !== true) return json;
|
|
@@ -597,16 +608,7 @@ var ReiClient = class {
|
|
|
597
608
|
*/
|
|
598
609
|
_sanitizeAvatarUrl(target) {
|
|
599
610
|
if (!target || typeof target !== "object") return false;
|
|
600
|
-
const
|
|
601
|
-
if (value === void 0 || value === null) return false;
|
|
602
|
-
let reason = null;
|
|
603
|
-
if (typeof value !== "string") {
|
|
604
|
-
reason = "avatarUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32";
|
|
605
|
-
} else if (/^data:/i.test(value)) {
|
|
606
|
-
reason = "\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL";
|
|
607
|
-
} else if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
608
|
-
reason = `\u5934\u50CF URL \u957F\u5EA6 ${value.length} \u5B57\u7B26\u8D85\u8FC7 ${AVATAR_URL_MAX_LENGTH} \u4E0A\u9650\uFF0C\u8BF7\u6539\u4E3A\u66F4\u77ED\u7684\u56FE\u7247 URL`;
|
|
609
|
-
}
|
|
611
|
+
const reason = (0, import_amsg_shared.validateAvatarUrl)(target.avatarUrl);
|
|
610
612
|
if (reason) {
|
|
611
613
|
console.warn("[rei-standard-amsg-client] avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u7F6E\u7A7A\uFF1A", reason);
|
|
612
614
|
target.avatarUrl = null;
|
|
@@ -695,7 +697,8 @@ var ReiClient = class {
|
|
|
695
697
|
err.status = res.status;
|
|
696
698
|
throw err;
|
|
697
699
|
}
|
|
698
|
-
const
|
|
700
|
+
const rawContentType = res.headers.get("content-type");
|
|
701
|
+
const contentType = rawContentType || "";
|
|
699
702
|
const kind = classifyContentType(contentType);
|
|
700
703
|
if (kind === "sse") {
|
|
701
704
|
if (!res.body) throw new Error("Response body is null");
|
|
@@ -705,7 +708,7 @@ var ReiClient = class {
|
|
|
705
708
|
responseMeta: {
|
|
706
709
|
status: res.status,
|
|
707
710
|
contentEncoding: res.headers.get("content-encoding"),
|
|
708
|
-
contentType:
|
|
711
|
+
contentType: rawContentType
|
|
709
712
|
}
|
|
710
713
|
});
|
|
711
714
|
return { kind: "sse" };
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { base64UrlToBytes } from '@rei-standard/amsg-shared';
|
|
1
|
+
import { base64UrlToBytes, validateAvatarUrl } from '@rei-standard/amsg-shared';
|
|
2
2
|
export { MESSAGE_KIND, MESSAGE_TYPE, PUSH_SOURCE, buildContentPush, buildErrorPush, buildReasoningPush, buildToolRequestPush, isContentPush, isErrorPush, isReasoningPush, isToolRequestPush } from '@rei-standard/amsg-shared';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -75,6 +75,11 @@ const TEXT_ENCODER = new TextEncoder();
|
|
|
75
75
|
* inside any frontend bundle that uses it, so
|
|
76
76
|
* devtools can read it. Use for casual URL-direct
|
|
77
77
|
* abuse only.
|
|
78
|
+
* @property {string} [serverToken] - Optional shared secret for a single-user amsg-server.
|
|
79
|
+
* When set, sent as the `X-Client-Token` header on
|
|
80
|
+
* amsg-server endpoints (schedule / messages / update /
|
|
81
|
+
* cancel / user-key / init). Not applied to the instant
|
|
82
|
+
* path (that uses `instantClientToken`).
|
|
78
83
|
* @property {number|null} [maxPayloadBytes=null] - Optional local UTF-8 byte cap for outgoing request
|
|
79
84
|
* payloads before encryption. `null` / omitted means
|
|
80
85
|
* no SDK-level request-size limit.
|
|
@@ -231,14 +236,6 @@ const TEXT_ENCODER = new TextEncoder();
|
|
|
231
236
|
* @property {number} [status] - `res.status`. First call only.
|
|
232
237
|
*/
|
|
233
238
|
|
|
234
|
-
/**
|
|
235
|
-
* Max length of `avatarUrl` accepted by local preflight (2 KB). Mirrors
|
|
236
|
-
* `@rei-standard/amsg-instant` / `@rei-standard/amsg-server` server-side
|
|
237
|
-
* limits — kept in lockstep on purpose so client-side rejects match what
|
|
238
|
-
* the server would reject.
|
|
239
|
-
*/
|
|
240
|
-
const AVATAR_URL_MAX_LENGTH = 2048;
|
|
241
|
-
|
|
242
239
|
function makeLocalError(code, message, details) {
|
|
243
240
|
const err = new Error(`[rei-standard-amsg-client] ${message}`);
|
|
244
241
|
err.code = code;
|
|
@@ -375,6 +372,10 @@ class ReiClient {
|
|
|
375
372
|
? config.instantClientToken
|
|
376
373
|
: '';
|
|
377
374
|
/** @private */
|
|
375
|
+
this._serverToken = typeof config.serverToken === 'string' && config.serverToken
|
|
376
|
+
? config.serverToken
|
|
377
|
+
: '';
|
|
378
|
+
/** @private */
|
|
378
379
|
this._maxPayloadBytes = normalizeMaxPayloadBytes(config.maxPayloadBytes);
|
|
379
380
|
/**
|
|
380
381
|
* Per-instance latch (set of method names already warned). The
|
|
@@ -395,6 +396,18 @@ class ReiClient {
|
|
|
395
396
|
return this._customBaseUrls[endpointName] || this._baseUrl;
|
|
396
397
|
}
|
|
397
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Attach the single-user shared secret to amsg-server endpoint requests.
|
|
401
|
+
* Never applied to the instant path (that uses instantClientToken).
|
|
402
|
+
* @private
|
|
403
|
+
* @param {Record<string, string>} headers
|
|
404
|
+
* @returns {Record<string, string>}
|
|
405
|
+
*/
|
|
406
|
+
_withServerToken(headers) {
|
|
407
|
+
if (this._serverToken) headers['X-Client-Token'] = this._serverToken;
|
|
408
|
+
return headers;
|
|
409
|
+
}
|
|
410
|
+
|
|
398
411
|
// ─── Initialisation ─────────────────────────────────────────────
|
|
399
412
|
|
|
400
413
|
/**
|
|
@@ -415,7 +428,7 @@ class ReiClient {
|
|
|
415
428
|
|
|
416
429
|
const res = await fetch(`${this._baseUrl}/get-user-key`, {
|
|
417
430
|
method: 'GET',
|
|
418
|
-
headers: { 'X-User-Id': this._userId }
|
|
431
|
+
headers: this._withServerToken({ 'X-User-Id': this._userId })
|
|
419
432
|
});
|
|
420
433
|
|
|
421
434
|
const json = await res.json();
|
|
@@ -458,12 +471,12 @@ class ReiClient {
|
|
|
458
471
|
|
|
459
472
|
const res = await fetch(`${this._baseUrl}/schedule-message`, {
|
|
460
473
|
method: 'POST',
|
|
461
|
-
headers: {
|
|
474
|
+
headers: this._withServerToken({
|
|
462
475
|
'Content-Type': 'application/json',
|
|
463
476
|
'X-User-Id': this._userId,
|
|
464
477
|
'X-Payload-Encrypted': 'true',
|
|
465
478
|
'X-Encryption-Version': '1'
|
|
466
|
-
},
|
|
479
|
+
}),
|
|
467
480
|
body: JSON.stringify(encrypted)
|
|
468
481
|
});
|
|
469
482
|
|
|
@@ -891,12 +904,12 @@ class ReiClient {
|
|
|
891
904
|
|
|
892
905
|
const res = await fetch(`${this._baseUrl}/update-message?id=${encodeURIComponent(uuid)}`, {
|
|
893
906
|
method: 'PUT',
|
|
894
|
-
headers: {
|
|
907
|
+
headers: this._withServerToken({
|
|
895
908
|
'Content-Type': 'application/json',
|
|
896
909
|
'X-User-Id': this._userId,
|
|
897
910
|
'X-Payload-Encrypted': 'true',
|
|
898
911
|
'X-Encryption-Version': '1'
|
|
899
|
-
},
|
|
912
|
+
}),
|
|
900
913
|
body: JSON.stringify(encrypted)
|
|
901
914
|
});
|
|
902
915
|
|
|
@@ -912,7 +925,7 @@ class ReiClient {
|
|
|
912
925
|
async cancelMessage(uuid) {
|
|
913
926
|
const res = await fetch(`${this._baseUrl}/cancel-message?id=${encodeURIComponent(uuid)}`, {
|
|
914
927
|
method: 'DELETE',
|
|
915
|
-
headers: { 'X-User-Id': this._userId }
|
|
928
|
+
headers: this._withServerToken({ 'X-User-Id': this._userId })
|
|
916
929
|
});
|
|
917
930
|
|
|
918
931
|
return res.json();
|
|
@@ -938,11 +951,11 @@ class ReiClient {
|
|
|
938
951
|
|
|
939
952
|
const res = await fetch(url, {
|
|
940
953
|
method: 'GET',
|
|
941
|
-
headers: {
|
|
954
|
+
headers: this._withServerToken({
|
|
942
955
|
'X-User-Id': this._userId,
|
|
943
956
|
'X-Response-Encrypted': 'true',
|
|
944
957
|
'X-Encryption-Version': '1'
|
|
945
|
-
}
|
|
958
|
+
})
|
|
946
959
|
});
|
|
947
960
|
|
|
948
961
|
const json = await res.json();
|
|
@@ -991,16 +1004,7 @@ class ReiClient {
|
|
|
991
1004
|
*/
|
|
992
1005
|
_sanitizeAvatarUrl(target) {
|
|
993
1006
|
if (!target || typeof target !== 'object') return false;
|
|
994
|
-
const
|
|
995
|
-
if (value === undefined || value === null) return false;
|
|
996
|
-
let reason = null;
|
|
997
|
-
if (typeof value !== 'string') {
|
|
998
|
-
reason = 'avatarUrl 必须是字符串';
|
|
999
|
-
} else if (/^data:/i.test(value)) {
|
|
1000
|
-
reason = '头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL';
|
|
1001
|
-
} else if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
1002
|
-
reason = `头像 URL 长度 ${value.length} 字符超过 ${AVATAR_URL_MAX_LENGTH} 上限,请改为更短的图片 URL`;
|
|
1003
|
-
}
|
|
1007
|
+
const reason = validateAvatarUrl(target.avatarUrl);
|
|
1004
1008
|
if (reason) {
|
|
1005
1009
|
console.warn('[rei-standard-amsg-client] avatarUrl 不合法,已置空:', reason);
|
|
1006
1010
|
target.avatarUrl = null;
|
|
@@ -1105,7 +1109,8 @@ class ReiClient {
|
|
|
1105
1109
|
throw err;
|
|
1106
1110
|
}
|
|
1107
1111
|
|
|
1108
|
-
const
|
|
1112
|
+
const rawContentType = res.headers.get('content-type');
|
|
1113
|
+
const contentType = rawContentType || '';
|
|
1109
1114
|
const kind = classifyContentType(contentType);
|
|
1110
1115
|
if (kind === 'sse') {
|
|
1111
1116
|
if (!res.body) throw new Error('Response body is null');
|
|
@@ -1115,7 +1120,7 @@ class ReiClient {
|
|
|
1115
1120
|
responseMeta: {
|
|
1116
1121
|
status: res.status,
|
|
1117
1122
|
contentEncoding: res.headers.get('content-encoding'),
|
|
1118
|
-
contentType:
|
|
1123
|
+
contentType: rawContentType,
|
|
1119
1124
|
},
|
|
1120
1125
|
});
|
|
1121
1126
|
return { kind: 'sse' };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { base64UrlToBytes } from '@rei-standard/amsg-shared';
|
|
1
|
+
import { base64UrlToBytes, validateAvatarUrl } from '@rei-standard/amsg-shared';
|
|
2
2
|
export { MESSAGE_KIND, MESSAGE_TYPE, PUSH_SOURCE, buildContentPush, buildErrorPush, buildReasoningPush, buildToolRequestPush, isContentPush, isErrorPush, isReasoningPush, isToolRequestPush } from '@rei-standard/amsg-shared';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -75,6 +75,11 @@ const TEXT_ENCODER = new TextEncoder();
|
|
|
75
75
|
* inside any frontend bundle that uses it, so
|
|
76
76
|
* devtools can read it. Use for casual URL-direct
|
|
77
77
|
* abuse only.
|
|
78
|
+
* @property {string} [serverToken] - Optional shared secret for a single-user amsg-server.
|
|
79
|
+
* When set, sent as the `X-Client-Token` header on
|
|
80
|
+
* amsg-server endpoints (schedule / messages / update /
|
|
81
|
+
* cancel / user-key / init). Not applied to the instant
|
|
82
|
+
* path (that uses `instantClientToken`).
|
|
78
83
|
* @property {number|null} [maxPayloadBytes=null] - Optional local UTF-8 byte cap for outgoing request
|
|
79
84
|
* payloads before encryption. `null` / omitted means
|
|
80
85
|
* no SDK-level request-size limit.
|
|
@@ -231,14 +236,6 @@ const TEXT_ENCODER = new TextEncoder();
|
|
|
231
236
|
* @property {number} [status] - `res.status`. First call only.
|
|
232
237
|
*/
|
|
233
238
|
|
|
234
|
-
/**
|
|
235
|
-
* Max length of `avatarUrl` accepted by local preflight (2 KB). Mirrors
|
|
236
|
-
* `@rei-standard/amsg-instant` / `@rei-standard/amsg-server` server-side
|
|
237
|
-
* limits — kept in lockstep on purpose so client-side rejects match what
|
|
238
|
-
* the server would reject.
|
|
239
|
-
*/
|
|
240
|
-
const AVATAR_URL_MAX_LENGTH = 2048;
|
|
241
|
-
|
|
242
239
|
function makeLocalError(code, message, details) {
|
|
243
240
|
const err = new Error(`[rei-standard-amsg-client] ${message}`);
|
|
244
241
|
err.code = code;
|
|
@@ -375,6 +372,10 @@ class ReiClient {
|
|
|
375
372
|
? config.instantClientToken
|
|
376
373
|
: '';
|
|
377
374
|
/** @private */
|
|
375
|
+
this._serverToken = typeof config.serverToken === 'string' && config.serverToken
|
|
376
|
+
? config.serverToken
|
|
377
|
+
: '';
|
|
378
|
+
/** @private */
|
|
378
379
|
this._maxPayloadBytes = normalizeMaxPayloadBytes(config.maxPayloadBytes);
|
|
379
380
|
/**
|
|
380
381
|
* Per-instance latch (set of method names already warned). The
|
|
@@ -395,6 +396,18 @@ class ReiClient {
|
|
|
395
396
|
return this._customBaseUrls[endpointName] || this._baseUrl;
|
|
396
397
|
}
|
|
397
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Attach the single-user shared secret to amsg-server endpoint requests.
|
|
401
|
+
* Never applied to the instant path (that uses instantClientToken).
|
|
402
|
+
* @private
|
|
403
|
+
* @param {Record<string, string>} headers
|
|
404
|
+
* @returns {Record<string, string>}
|
|
405
|
+
*/
|
|
406
|
+
_withServerToken(headers) {
|
|
407
|
+
if (this._serverToken) headers['X-Client-Token'] = this._serverToken;
|
|
408
|
+
return headers;
|
|
409
|
+
}
|
|
410
|
+
|
|
398
411
|
// ─── Initialisation ─────────────────────────────────────────────
|
|
399
412
|
|
|
400
413
|
/**
|
|
@@ -415,7 +428,7 @@ class ReiClient {
|
|
|
415
428
|
|
|
416
429
|
const res = await fetch(`${this._baseUrl}/get-user-key`, {
|
|
417
430
|
method: 'GET',
|
|
418
|
-
headers: { 'X-User-Id': this._userId }
|
|
431
|
+
headers: this._withServerToken({ 'X-User-Id': this._userId })
|
|
419
432
|
});
|
|
420
433
|
|
|
421
434
|
const json = await res.json();
|
|
@@ -458,12 +471,12 @@ class ReiClient {
|
|
|
458
471
|
|
|
459
472
|
const res = await fetch(`${this._baseUrl}/schedule-message`, {
|
|
460
473
|
method: 'POST',
|
|
461
|
-
headers: {
|
|
474
|
+
headers: this._withServerToken({
|
|
462
475
|
'Content-Type': 'application/json',
|
|
463
476
|
'X-User-Id': this._userId,
|
|
464
477
|
'X-Payload-Encrypted': 'true',
|
|
465
478
|
'X-Encryption-Version': '1'
|
|
466
|
-
},
|
|
479
|
+
}),
|
|
467
480
|
body: JSON.stringify(encrypted)
|
|
468
481
|
});
|
|
469
482
|
|
|
@@ -891,12 +904,12 @@ class ReiClient {
|
|
|
891
904
|
|
|
892
905
|
const res = await fetch(`${this._baseUrl}/update-message?id=${encodeURIComponent(uuid)}`, {
|
|
893
906
|
method: 'PUT',
|
|
894
|
-
headers: {
|
|
907
|
+
headers: this._withServerToken({
|
|
895
908
|
'Content-Type': 'application/json',
|
|
896
909
|
'X-User-Id': this._userId,
|
|
897
910
|
'X-Payload-Encrypted': 'true',
|
|
898
911
|
'X-Encryption-Version': '1'
|
|
899
|
-
},
|
|
912
|
+
}),
|
|
900
913
|
body: JSON.stringify(encrypted)
|
|
901
914
|
});
|
|
902
915
|
|
|
@@ -912,7 +925,7 @@ class ReiClient {
|
|
|
912
925
|
async cancelMessage(uuid) {
|
|
913
926
|
const res = await fetch(`${this._baseUrl}/cancel-message?id=${encodeURIComponent(uuid)}`, {
|
|
914
927
|
method: 'DELETE',
|
|
915
|
-
headers: { 'X-User-Id': this._userId }
|
|
928
|
+
headers: this._withServerToken({ 'X-User-Id': this._userId })
|
|
916
929
|
});
|
|
917
930
|
|
|
918
931
|
return res.json();
|
|
@@ -938,11 +951,11 @@ class ReiClient {
|
|
|
938
951
|
|
|
939
952
|
const res = await fetch(url, {
|
|
940
953
|
method: 'GET',
|
|
941
|
-
headers: {
|
|
954
|
+
headers: this._withServerToken({
|
|
942
955
|
'X-User-Id': this._userId,
|
|
943
956
|
'X-Response-Encrypted': 'true',
|
|
944
957
|
'X-Encryption-Version': '1'
|
|
945
|
-
}
|
|
958
|
+
})
|
|
946
959
|
});
|
|
947
960
|
|
|
948
961
|
const json = await res.json();
|
|
@@ -991,16 +1004,7 @@ class ReiClient {
|
|
|
991
1004
|
*/
|
|
992
1005
|
_sanitizeAvatarUrl(target) {
|
|
993
1006
|
if (!target || typeof target !== 'object') return false;
|
|
994
|
-
const
|
|
995
|
-
if (value === undefined || value === null) return false;
|
|
996
|
-
let reason = null;
|
|
997
|
-
if (typeof value !== 'string') {
|
|
998
|
-
reason = 'avatarUrl 必须是字符串';
|
|
999
|
-
} else if (/^data:/i.test(value)) {
|
|
1000
|
-
reason = '头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL';
|
|
1001
|
-
} else if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
1002
|
-
reason = `头像 URL 长度 ${value.length} 字符超过 ${AVATAR_URL_MAX_LENGTH} 上限,请改为更短的图片 URL`;
|
|
1003
|
-
}
|
|
1007
|
+
const reason = validateAvatarUrl(target.avatarUrl);
|
|
1004
1008
|
if (reason) {
|
|
1005
1009
|
console.warn('[rei-standard-amsg-client] avatarUrl 不合法,已置空:', reason);
|
|
1006
1010
|
target.avatarUrl = null;
|
|
@@ -1105,7 +1109,8 @@ class ReiClient {
|
|
|
1105
1109
|
throw err;
|
|
1106
1110
|
}
|
|
1107
1111
|
|
|
1108
|
-
const
|
|
1112
|
+
const rawContentType = res.headers.get('content-type');
|
|
1113
|
+
const contentType = rawContentType || '';
|
|
1109
1114
|
const kind = classifyContentType(contentType);
|
|
1110
1115
|
if (kind === 'sse') {
|
|
1111
1116
|
if (!res.body) throw new Error('Response body is null');
|
|
@@ -1115,7 +1120,7 @@ class ReiClient {
|
|
|
1115
1120
|
responseMeta: {
|
|
1116
1121
|
status: res.status,
|
|
1117
1122
|
contentEncoding: res.headers.get('content-encoding'),
|
|
1118
|
-
contentType:
|
|
1123
|
+
contentType: rawContentType,
|
|
1119
1124
|
},
|
|
1120
1125
|
});
|
|
1121
1126
|
return { kind: 'sse' };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.js
|
|
2
|
-
import { base64UrlToBytes } from "@rei-standard/amsg-shared";
|
|
2
|
+
import { base64UrlToBytes, validateAvatarUrl } from "@rei-standard/amsg-shared";
|
|
3
3
|
import {
|
|
4
4
|
MESSAGE_KIND,
|
|
5
5
|
MESSAGE_TYPE,
|
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
isErrorPush
|
|
15
15
|
} from "@rei-standard/amsg-shared";
|
|
16
16
|
var TEXT_ENCODER = new TextEncoder();
|
|
17
|
-
var AVATAR_URL_MAX_LENGTH = 2048;
|
|
18
17
|
function makeLocalError(code, message, details) {
|
|
19
18
|
const err = new Error(`[rei-standard-amsg-client] ${message}`);
|
|
20
19
|
err.code = code;
|
|
@@ -76,6 +75,7 @@ var ReiClient = class {
|
|
|
76
75
|
this._userKey = null;
|
|
77
76
|
this._instantEncryption = instantEncryption;
|
|
78
77
|
this._instantClientToken = typeof config.instantClientToken === "string" && config.instantClientToken ? config.instantClientToken : "";
|
|
78
|
+
this._serverToken = typeof config.serverToken === "string" && config.serverToken ? config.serverToken : "";
|
|
79
79
|
this._maxPayloadBytes = normalizeMaxPayloadBytes(config.maxPayloadBytes);
|
|
80
80
|
this._lowLevelWarned = /* @__PURE__ */ new Set();
|
|
81
81
|
}
|
|
@@ -89,6 +89,17 @@ var ReiClient = class {
|
|
|
89
89
|
_resolveBaseUrl(endpointName) {
|
|
90
90
|
return this._customBaseUrls[endpointName] || this._baseUrl;
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Attach the single-user shared secret to amsg-server endpoint requests.
|
|
94
|
+
* Never applied to the instant path (that uses instantClientToken).
|
|
95
|
+
* @private
|
|
96
|
+
* @param {Record<string, string>} headers
|
|
97
|
+
* @returns {Record<string, string>}
|
|
98
|
+
*/
|
|
99
|
+
_withServerToken(headers) {
|
|
100
|
+
if (this._serverToken) headers["X-Client-Token"] = this._serverToken;
|
|
101
|
+
return headers;
|
|
102
|
+
}
|
|
92
103
|
// ─── Initialisation ─────────────────────────────────────────────
|
|
93
104
|
/**
|
|
94
105
|
* Fetch the user-specific encryption key.
|
|
@@ -107,7 +118,7 @@ var ReiClient = class {
|
|
|
107
118
|
}
|
|
108
119
|
const res = await fetch(`${this._baseUrl}/get-user-key`, {
|
|
109
120
|
method: "GET",
|
|
110
|
-
headers: { "X-User-Id": this._userId }
|
|
121
|
+
headers: this._withServerToken({ "X-User-Id": this._userId })
|
|
111
122
|
});
|
|
112
123
|
const json = await res.json();
|
|
113
124
|
if (!json.success) throw new Error(json.error?.message || "Failed to fetch user key");
|
|
@@ -144,12 +155,12 @@ var ReiClient = class {
|
|
|
144
155
|
const encrypted = await this._encrypt(json);
|
|
145
156
|
const res = await fetch(`${this._baseUrl}/schedule-message`, {
|
|
146
157
|
method: "POST",
|
|
147
|
-
headers: {
|
|
158
|
+
headers: this._withServerToken({
|
|
148
159
|
"Content-Type": "application/json",
|
|
149
160
|
"X-User-Id": this._userId,
|
|
150
161
|
"X-Payload-Encrypted": "true",
|
|
151
162
|
"X-Encryption-Version": "1"
|
|
152
|
-
},
|
|
163
|
+
}),
|
|
153
164
|
body: JSON.stringify(encrypted)
|
|
154
165
|
});
|
|
155
166
|
return res.json();
|
|
@@ -487,12 +498,12 @@ var ReiClient = class {
|
|
|
487
498
|
const encrypted = await this._encrypt(json);
|
|
488
499
|
const res = await fetch(`${this._baseUrl}/update-message?id=${encodeURIComponent(uuid)}`, {
|
|
489
500
|
method: "PUT",
|
|
490
|
-
headers: {
|
|
501
|
+
headers: this._withServerToken({
|
|
491
502
|
"Content-Type": "application/json",
|
|
492
503
|
"X-User-Id": this._userId,
|
|
493
504
|
"X-Payload-Encrypted": "true",
|
|
494
505
|
"X-Encryption-Version": "1"
|
|
495
|
-
},
|
|
506
|
+
}),
|
|
496
507
|
body: JSON.stringify(encrypted)
|
|
497
508
|
});
|
|
498
509
|
return res.json();
|
|
@@ -506,7 +517,7 @@ var ReiClient = class {
|
|
|
506
517
|
async cancelMessage(uuid) {
|
|
507
518
|
const res = await fetch(`${this._baseUrl}/cancel-message?id=${encodeURIComponent(uuid)}`, {
|
|
508
519
|
method: "DELETE",
|
|
509
|
-
headers: { "X-User-Id": this._userId }
|
|
520
|
+
headers: this._withServerToken({ "X-User-Id": this._userId })
|
|
510
521
|
});
|
|
511
522
|
return res.json();
|
|
512
523
|
}
|
|
@@ -528,11 +539,11 @@ var ReiClient = class {
|
|
|
528
539
|
const url = `${this._baseUrl}/messages${qs ? "?" + qs : ""}`;
|
|
529
540
|
const res = await fetch(url, {
|
|
530
541
|
method: "GET",
|
|
531
|
-
headers: {
|
|
542
|
+
headers: this._withServerToken({
|
|
532
543
|
"X-User-Id": this._userId,
|
|
533
544
|
"X-Response-Encrypted": "true",
|
|
534
545
|
"X-Encryption-Version": "1"
|
|
535
|
-
}
|
|
546
|
+
})
|
|
536
547
|
});
|
|
537
548
|
const json = await res.json();
|
|
538
549
|
if (!json?.success || json?.encrypted !== true) return json;
|
|
@@ -575,16 +586,7 @@ var ReiClient = class {
|
|
|
575
586
|
*/
|
|
576
587
|
_sanitizeAvatarUrl(target) {
|
|
577
588
|
if (!target || typeof target !== "object") return false;
|
|
578
|
-
const
|
|
579
|
-
if (value === void 0 || value === null) return false;
|
|
580
|
-
let reason = null;
|
|
581
|
-
if (typeof value !== "string") {
|
|
582
|
-
reason = "avatarUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32";
|
|
583
|
-
} else if (/^data:/i.test(value)) {
|
|
584
|
-
reason = "\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL";
|
|
585
|
-
} else if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
586
|
-
reason = `\u5934\u50CF URL \u957F\u5EA6 ${value.length} \u5B57\u7B26\u8D85\u8FC7 ${AVATAR_URL_MAX_LENGTH} \u4E0A\u9650\uFF0C\u8BF7\u6539\u4E3A\u66F4\u77ED\u7684\u56FE\u7247 URL`;
|
|
587
|
-
}
|
|
589
|
+
const reason = validateAvatarUrl(target.avatarUrl);
|
|
588
590
|
if (reason) {
|
|
589
591
|
console.warn("[rei-standard-amsg-client] avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u7F6E\u7A7A\uFF1A", reason);
|
|
590
592
|
target.avatarUrl = null;
|
|
@@ -673,7 +675,8 @@ var ReiClient = class {
|
|
|
673
675
|
err.status = res.status;
|
|
674
676
|
throw err;
|
|
675
677
|
}
|
|
676
|
-
const
|
|
678
|
+
const rawContentType = res.headers.get("content-type");
|
|
679
|
+
const contentType = rawContentType || "";
|
|
677
680
|
const kind = classifyContentType(contentType);
|
|
678
681
|
if (kind === "sse") {
|
|
679
682
|
if (!res.body) throw new Error("Response body is null");
|
|
@@ -683,7 +686,7 @@ var ReiClient = class {
|
|
|
683
686
|
responseMeta: {
|
|
684
687
|
status: res.status,
|
|
685
688
|
contentEncoding: res.headers.get("content-encoding"),
|
|
686
|
-
contentType:
|
|
689
|
+
contentType: rawContentType
|
|
687
690
|
}
|
|
688
691
|
});
|
|
689
692
|
return { kind: "sse" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0-next.0",
|
|
4
4
|
"description": "ReiStandard Active Messaging browser client SDK — also re-exports shared push types, builders, and guards from @rei-standard/amsg-shared",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"node": ">=20"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@rei-standard/amsg-shared": "0.
|
|
36
|
+
"@rei-standard/amsg-shared": "^0.3.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"tsup": "^8.0.0",
|