@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 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 value = target.avatarUrl;
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 contentType = res.headers.get("content-type") || "";
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: res.headers.get("content-type")
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 value = target.avatarUrl;
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 contentType = res.headers.get('content-type') || '';
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: res.headers.get('content-type'),
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 value = target.avatarUrl;
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 contentType = res.headers.get('content-type') || '';
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: res.headers.get('content-type'),
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 value = target.avatarUrl;
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 contentType = res.headers.get("content-type") || "";
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: res.headers.get("content-type")
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.7.0",
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.2.0"
36
+ "@rei-standard/amsg-shared": "^0.3.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "tsup": "^8.0.0",