@rei-standard/amsg-client 2.3.0-next.0 → 2.3.0-next.2
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 +11 -11
- package/dist/index.cjs +42 -36
- package/dist/index.d.cts +45 -36
- package/dist/index.d.ts +45 -36
- package/dist/index.mjs +42 -36
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -180,24 +180,24 @@ await client.sendInstant({
|
|
|
180
180
|
- 传**正则 source**,不要带 `/.../` 也不要尾 flag。`'/foo/i'` 会被当字面量斜杠 + 字面量 `i`,不是大小写不敏感的 `foo`。大小写不敏感请用 `[Aa]` 字符类替代。
|
|
181
181
|
- 想让分隔符回贴到前一段(默认行为),把分隔符包进 `(...)` 捕获组。库**不会自动包**——传 `'\\n+'` 而不是 `'(\\n+)'` 会得到首尾相连、分隔符丢失的奇怪结果。
|
|
182
182
|
|
|
183
|
-
###
|
|
183
|
+
### 本地软清空:`avatarUrl` 与 payload 体积(2.2.4+ / 2.3.0-next.1+)
|
|
184
184
|
|
|
185
|
-
`scheduleMessage` / `sendInstant` / `updateMessage`
|
|
185
|
+
`scheduleMessage` / `sendInstant` / `updateMessage` 在发请求**之前**会在本地做两项保护:
|
|
186
186
|
|
|
187
|
-
| 触发条件 |
|
|
187
|
+
| 触发条件 | 处理方式 | 触发原因(背景说明,不在 message 里) |
|
|
188
188
|
| --- | --- | --- |
|
|
189
|
-
| `payload.avatarUrl` 以 `data:` 开头(含 `data:image/...;base64,...`) | `
|
|
190
|
-
| `payload.avatarUrl` 长度 > 2048 字符 |
|
|
191
|
-
| `payload.avatarUrl` 不是字符串 |
|
|
192
|
-
| `JSON.stringify(payload)` UTF-8 字节数 > 3072 | `
|
|
189
|
+
| `payload.avatarUrl` 以 `data:` 开头(含 `data:image/...;base64,...`) | `console.warn` + 在 payload 上把 `avatarUrl` 置为 `null`,请求照发(`updateMessage` 从 patch 里删除该字段,保留服务端原头像) | base64 内嵌头像把单个 push payload 撑到几十 KB,远端 Web Push 服务直接返回 4KB 超限 / 网关 `413`。 |
|
|
190
|
+
| `payload.avatarUrl` 长度 > 2048 字符 | 同上 | 同上。建议用 CDN 缩略图 URL。 |
|
|
191
|
+
| `payload.avatarUrl` 不是字符串 | 同上 | 类型错误。 |
|
|
192
|
+
| `JSON.stringify(payload)` UTF-8 字节数 > 3072 | 抛出 `Error.code === 'PAYLOAD_TOO_LARGE_LOCAL'`,错误对象带 `.details = { method, actualBytes, limitBytes }` | 远端网关 / Web Push 4KB 硬上限的本地兜底。 |
|
|
193
|
+
|
|
194
|
+
头像是装饰字段,单个不合规 URL 不再让整次调度 / 推送挂掉;想拦到错误请监听 `console.warn`,或在调用前自己用 `validateAvatarUrl` 预检(server / instant 包都有导出)。`PAYLOAD_TOO_LARGE_LOCAL` 仍然是真正的"整包过大"信号,照常用 try/catch 捕获:
|
|
193
195
|
|
|
194
196
|
```js
|
|
195
197
|
try {
|
|
196
198
|
await client.sendInstant(payload);
|
|
197
199
|
} catch (err) {
|
|
198
|
-
if (err.code === '
|
|
199
|
-
// err.message 形如「头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL」
|
|
200
|
-
} else if (err.code === 'PAYLOAD_TOO_LARGE_LOCAL') {
|
|
200
|
+
if (err.code === 'PAYLOAD_TOO_LARGE_LOCAL') {
|
|
201
201
|
// err.details = { method: 'sendInstant', actualBytes: 8732, limitBytes: 3072 }
|
|
202
202
|
} else {
|
|
203
203
|
throw err;
|
|
@@ -205,7 +205,7 @@ try {
|
|
|
205
205
|
}
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
-
服务端(`@rei-standard/amsg-instant` 0.
|
|
208
|
+
服务端(`@rei-standard/amsg-instant` 0.7.1+ / 0.8.0-next.1+,`@rei-standard/amsg-server` 2.3.3+ / 2.4.0-next.1+)有同样的软清空二道防线,client 这一道主要省一次远端往返。
|
|
209
209
|
|
|
210
210
|
## 导出 API(Exports)
|
|
211
211
|
|
package/dist/index.cjs
CHANGED
|
@@ -117,16 +117,16 @@ var ReiClient = class {
|
|
|
117
117
|
*
|
|
118
118
|
* The payload is automatically encrypted before transmission.
|
|
119
119
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
120
|
+
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
121
|
+
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
122
|
+
* schedule still ships, just without an avatar. The only throw left is
|
|
123
|
+
* `PAYLOAD_TOO_LARGE_LOCAL` — JSON-serialized payload exceeds 3 KB.
|
|
124
124
|
*
|
|
125
125
|
* @param {Object} payload - Schedule message payload.
|
|
126
126
|
* @returns {Promise<Object>} API response body.
|
|
127
127
|
*/
|
|
128
128
|
async scheduleMessage(payload) {
|
|
129
|
-
this.
|
|
129
|
+
this._sanitizeAvatarUrl(payload);
|
|
130
130
|
const json = JSON.stringify(payload);
|
|
131
131
|
this._assertPayloadSize(json, "scheduleMessage");
|
|
132
132
|
const encrypted = await this._encrypt(json);
|
|
@@ -163,10 +163,10 @@ var ReiClient = class {
|
|
|
163
163
|
*
|
|
164
164
|
* Routes to `customBaseUrls.instant` if configured, otherwise `baseUrl`.
|
|
165
165
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
166
|
+
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
167
|
+
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
168
|
+
* push still ships, just without an icon. The only throw left is
|
|
169
|
+
* `PAYLOAD_TOO_LARGE_LOCAL` — JSON-serialized payload exceeds 3 KB.
|
|
170
170
|
*
|
|
171
171
|
* @param {Object} payload - Instant message payload.
|
|
172
172
|
* @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
|
|
@@ -174,7 +174,7 @@ var ReiClient = class {
|
|
|
174
174
|
* @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
|
|
175
175
|
*/
|
|
176
176
|
async sendInstant(payload, endpointPath = "/instant", opts = {}) {
|
|
177
|
-
this.
|
|
177
|
+
this._sanitizeAvatarUrl(payload);
|
|
178
178
|
const json = JSON.stringify(payload);
|
|
179
179
|
this._assertPayloadSize(json, "sendInstant");
|
|
180
180
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -205,17 +205,20 @@ var ReiClient = class {
|
|
|
205
205
|
/**
|
|
206
206
|
* Update an existing scheduled message.
|
|
207
207
|
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
208
|
+
* If `updates.avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string),
|
|
209
|
+
* the client soft-strips it from the patch and emits a `console.warn` —
|
|
210
|
+
* the rest of the update still applies, and the stored avatar is left
|
|
211
|
+
* untouched. The only throw left is `PAYLOAD_TOO_LARGE_LOCAL` —
|
|
212
|
+
* JSON-serialized updates exceed 3 KB.
|
|
212
213
|
*
|
|
213
214
|
* @param {string} uuid - Task UUID.
|
|
214
215
|
* @param {Object} updates - Fields to update.
|
|
215
216
|
* @returns {Promise<Object>}
|
|
216
217
|
*/
|
|
217
218
|
async updateMessage(uuid, updates) {
|
|
218
|
-
this.
|
|
219
|
+
if (this._sanitizeAvatarUrl(updates)) {
|
|
220
|
+
delete updates.avatarUrl;
|
|
221
|
+
}
|
|
219
222
|
const json = JSON.stringify(updates);
|
|
220
223
|
this._assertPayloadSize(json, "updateMessage");
|
|
221
224
|
const encrypted = await this._encrypt(json);
|
|
@@ -295,33 +298,36 @@ var ReiClient = class {
|
|
|
295
298
|
}
|
|
296
299
|
// ─── Local preflight (no network) ────────────────────────────────
|
|
297
300
|
/**
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
*
|
|
301
|
+
* Sanitize `avatarUrl` on an outgoing payload. If the value is unusable
|
|
302
|
+
* (`data:` URI / oversized / non-string), set the field to `null` on the
|
|
303
|
+
* payload, log a `console.warn`, and let the rest of the request go
|
|
304
|
+
* through. Avatar is cosmetic — failing the entire schedule / instant
|
|
305
|
+
* call over a bad image URL is too punishing. Mirrors the server-side
|
|
306
|
+
* soft-strip in `@rei-standard/amsg-server` 2.3.3+ and `@rei-standard/amsg-instant`
|
|
307
|
+
* 0.7.1+. See standards §6.2.
|
|
303
308
|
*
|
|
304
309
|
* @private
|
|
305
|
-
* @param {
|
|
310
|
+
* @param {object|null|undefined} target - Payload-like object holding `avatarUrl`.
|
|
311
|
+
* @returns {boolean} `true` if the field was stripped, `false` otherwise.
|
|
306
312
|
*/
|
|
307
|
-
|
|
308
|
-
if (
|
|
313
|
+
_sanitizeAvatarUrl(target) {
|
|
314
|
+
if (!target || typeof target !== "object") return false;
|
|
315
|
+
const value = target.avatarUrl;
|
|
316
|
+
if (value === void 0 || value === null) return false;
|
|
317
|
+
let reason = null;
|
|
309
318
|
if (typeof value !== "string") {
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
"\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL"
|
|
316
|
-
);
|
|
319
|
+
reason = "avatarUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32";
|
|
320
|
+
} else if (/^data:/i.test(value)) {
|
|
321
|
+
reason = "\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL";
|
|
322
|
+
} else if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
323
|
+
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`;
|
|
317
324
|
}
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
{ actualLength: value.length, limit: AVATAR_URL_MAX_LENGTH }
|
|
323
|
-
);
|
|
325
|
+
if (reason) {
|
|
326
|
+
console.warn("[rei-standard-amsg-client] avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u7F6E\u7A7A\uFF1A", reason);
|
|
327
|
+
target.avatarUrl = null;
|
|
328
|
+
return true;
|
|
324
329
|
}
|
|
330
|
+
return false;
|
|
325
331
|
}
|
|
326
332
|
/**
|
|
327
333
|
* Reject outgoing payloads larger than 3 KB pre-encryption. Spares the
|
package/dist/index.d.cts
CHANGED
|
@@ -181,16 +181,16 @@ class ReiClient {
|
|
|
181
181
|
*
|
|
182
182
|
* The payload is automatically encrypted before transmission.
|
|
183
183
|
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
184
|
+
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
185
|
+
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
186
|
+
* schedule still ships, just without an avatar. The only throw left is
|
|
187
|
+
* `PAYLOAD_TOO_LARGE_LOCAL` — JSON-serialized payload exceeds 3 KB.
|
|
188
188
|
*
|
|
189
189
|
* @param {Object} payload - Schedule message payload.
|
|
190
190
|
* @returns {Promise<Object>} API response body.
|
|
191
191
|
*/
|
|
192
192
|
async scheduleMessage(payload) {
|
|
193
|
-
this.
|
|
193
|
+
this._sanitizeAvatarUrl(payload);
|
|
194
194
|
const json = JSON.stringify(payload);
|
|
195
195
|
this._assertPayloadSize(json, 'scheduleMessage');
|
|
196
196
|
const encrypted = await this._encrypt(json);
|
|
@@ -230,10 +230,10 @@ class ReiClient {
|
|
|
230
230
|
*
|
|
231
231
|
* Routes to `customBaseUrls.instant` if configured, otherwise `baseUrl`.
|
|
232
232
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
233
|
+
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
234
|
+
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
235
|
+
* push still ships, just without an icon. The only throw left is
|
|
236
|
+
* `PAYLOAD_TOO_LARGE_LOCAL` — JSON-serialized payload exceeds 3 KB.
|
|
237
237
|
*
|
|
238
238
|
* @param {Object} payload - Instant message payload.
|
|
239
239
|
* @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
|
|
@@ -241,7 +241,7 @@ class ReiClient {
|
|
|
241
241
|
* @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
|
|
242
242
|
*/
|
|
243
243
|
async sendInstant(payload, endpointPath = '/instant', opts = {}) {
|
|
244
|
-
this.
|
|
244
|
+
this._sanitizeAvatarUrl(payload);
|
|
245
245
|
const json = JSON.stringify(payload);
|
|
246
246
|
this._assertPayloadSize(json, 'sendInstant');
|
|
247
247
|
|
|
@@ -278,17 +278,23 @@ class ReiClient {
|
|
|
278
278
|
/**
|
|
279
279
|
* Update an existing scheduled message.
|
|
280
280
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
281
|
+
* If `updates.avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string),
|
|
282
|
+
* the client soft-strips it from the patch and emits a `console.warn` —
|
|
283
|
+
* the rest of the update still applies, and the stored avatar is left
|
|
284
|
+
* untouched. The only throw left is `PAYLOAD_TOO_LARGE_LOCAL` —
|
|
285
|
+
* JSON-serialized updates exceed 3 KB.
|
|
285
286
|
*
|
|
286
287
|
* @param {string} uuid - Task UUID.
|
|
287
288
|
* @param {Object} updates - Fields to update.
|
|
288
289
|
* @returns {Promise<Object>}
|
|
289
290
|
*/
|
|
290
291
|
async updateMessage(uuid, updates) {
|
|
291
|
-
|
|
292
|
+
// Match server-side semantics: a stripped patch shouldn't overwrite the
|
|
293
|
+
// stored avatar with `null`. When sanitize fires, remove the field
|
|
294
|
+
// entirely so the existing image is preserved.
|
|
295
|
+
if (this._sanitizeAvatarUrl(updates)) {
|
|
296
|
+
delete updates.avatarUrl;
|
|
297
|
+
}
|
|
292
298
|
const json = JSON.stringify(updates);
|
|
293
299
|
this._assertPayloadSize(json, 'updateMessage');
|
|
294
300
|
const encrypted = await this._encrypt(json);
|
|
@@ -381,33 +387,36 @@ class ReiClient {
|
|
|
381
387
|
// ─── Local preflight (no network) ────────────────────────────────
|
|
382
388
|
|
|
383
389
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
390
|
+
* Sanitize `avatarUrl` on an outgoing payload. If the value is unusable
|
|
391
|
+
* (`data:` URI / oversized / non-string), set the field to `null` on the
|
|
392
|
+
* payload, log a `console.warn`, and let the rest of the request go
|
|
393
|
+
* through. Avatar is cosmetic — failing the entire schedule / instant
|
|
394
|
+
* call over a bad image URL is too punishing. Mirrors the server-side
|
|
395
|
+
* soft-strip in `@rei-standard/amsg-server` 2.3.3+ and `@rei-standard/amsg-instant`
|
|
396
|
+
* 0.7.1+. See standards §6.2.
|
|
389
397
|
*
|
|
390
398
|
* @private
|
|
391
|
-
* @param {
|
|
399
|
+
* @param {object|null|undefined} target - Payload-like object holding `avatarUrl`.
|
|
400
|
+
* @returns {boolean} `true` if the field was stripped, `false` otherwise.
|
|
392
401
|
*/
|
|
393
|
-
|
|
394
|
-
if (
|
|
402
|
+
_sanitizeAvatarUrl(target) {
|
|
403
|
+
if (!target || typeof target !== 'object') return false;
|
|
404
|
+
const value = target.avatarUrl;
|
|
405
|
+
if (value === undefined || value === null) return false;
|
|
406
|
+
let reason = null;
|
|
395
407
|
if (typeof value !== 'string') {
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
'头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL'
|
|
402
|
-
);
|
|
408
|
+
reason = 'avatarUrl 必须是字符串';
|
|
409
|
+
} else if (/^data:/i.test(value)) {
|
|
410
|
+
reason = '头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL';
|
|
411
|
+
} else if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
412
|
+
reason = `头像 URL 长度 ${value.length} 字符超过 ${AVATAR_URL_MAX_LENGTH} 上限,请改为更短的图片 URL`;
|
|
403
413
|
}
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
{ actualLength: value.length, limit: AVATAR_URL_MAX_LENGTH }
|
|
409
|
-
);
|
|
414
|
+
if (reason) {
|
|
415
|
+
console.warn('[rei-standard-amsg-client] avatarUrl 不合法,已置空:', reason);
|
|
416
|
+
target.avatarUrl = null;
|
|
417
|
+
return true;
|
|
410
418
|
}
|
|
419
|
+
return false;
|
|
411
420
|
}
|
|
412
421
|
|
|
413
422
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -181,16 +181,16 @@ class ReiClient {
|
|
|
181
181
|
*
|
|
182
182
|
* The payload is automatically encrypted before transmission.
|
|
183
183
|
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
184
|
+
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
185
|
+
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
186
|
+
* schedule still ships, just without an avatar. The only throw left is
|
|
187
|
+
* `PAYLOAD_TOO_LARGE_LOCAL` — JSON-serialized payload exceeds 3 KB.
|
|
188
188
|
*
|
|
189
189
|
* @param {Object} payload - Schedule message payload.
|
|
190
190
|
* @returns {Promise<Object>} API response body.
|
|
191
191
|
*/
|
|
192
192
|
async scheduleMessage(payload) {
|
|
193
|
-
this.
|
|
193
|
+
this._sanitizeAvatarUrl(payload);
|
|
194
194
|
const json = JSON.stringify(payload);
|
|
195
195
|
this._assertPayloadSize(json, 'scheduleMessage');
|
|
196
196
|
const encrypted = await this._encrypt(json);
|
|
@@ -230,10 +230,10 @@ class ReiClient {
|
|
|
230
230
|
*
|
|
231
231
|
* Routes to `customBaseUrls.instant` if configured, otherwise `baseUrl`.
|
|
232
232
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
233
|
+
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
234
|
+
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
235
|
+
* push still ships, just without an icon. The only throw left is
|
|
236
|
+
* `PAYLOAD_TOO_LARGE_LOCAL` — JSON-serialized payload exceeds 3 KB.
|
|
237
237
|
*
|
|
238
238
|
* @param {Object} payload - Instant message payload.
|
|
239
239
|
* @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
|
|
@@ -241,7 +241,7 @@ class ReiClient {
|
|
|
241
241
|
* @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
|
|
242
242
|
*/
|
|
243
243
|
async sendInstant(payload, endpointPath = '/instant', opts = {}) {
|
|
244
|
-
this.
|
|
244
|
+
this._sanitizeAvatarUrl(payload);
|
|
245
245
|
const json = JSON.stringify(payload);
|
|
246
246
|
this._assertPayloadSize(json, 'sendInstant');
|
|
247
247
|
|
|
@@ -278,17 +278,23 @@ class ReiClient {
|
|
|
278
278
|
/**
|
|
279
279
|
* Update an existing scheduled message.
|
|
280
280
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
281
|
+
* If `updates.avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string),
|
|
282
|
+
* the client soft-strips it from the patch and emits a `console.warn` —
|
|
283
|
+
* the rest of the update still applies, and the stored avatar is left
|
|
284
|
+
* untouched. The only throw left is `PAYLOAD_TOO_LARGE_LOCAL` —
|
|
285
|
+
* JSON-serialized updates exceed 3 KB.
|
|
285
286
|
*
|
|
286
287
|
* @param {string} uuid - Task UUID.
|
|
287
288
|
* @param {Object} updates - Fields to update.
|
|
288
289
|
* @returns {Promise<Object>}
|
|
289
290
|
*/
|
|
290
291
|
async updateMessage(uuid, updates) {
|
|
291
|
-
|
|
292
|
+
// Match server-side semantics: a stripped patch shouldn't overwrite the
|
|
293
|
+
// stored avatar with `null`. When sanitize fires, remove the field
|
|
294
|
+
// entirely so the existing image is preserved.
|
|
295
|
+
if (this._sanitizeAvatarUrl(updates)) {
|
|
296
|
+
delete updates.avatarUrl;
|
|
297
|
+
}
|
|
292
298
|
const json = JSON.stringify(updates);
|
|
293
299
|
this._assertPayloadSize(json, 'updateMessage');
|
|
294
300
|
const encrypted = await this._encrypt(json);
|
|
@@ -381,33 +387,36 @@ class ReiClient {
|
|
|
381
387
|
// ─── Local preflight (no network) ────────────────────────────────
|
|
382
388
|
|
|
383
389
|
/**
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
390
|
+
* Sanitize `avatarUrl` on an outgoing payload. If the value is unusable
|
|
391
|
+
* (`data:` URI / oversized / non-string), set the field to `null` on the
|
|
392
|
+
* payload, log a `console.warn`, and let the rest of the request go
|
|
393
|
+
* through. Avatar is cosmetic — failing the entire schedule / instant
|
|
394
|
+
* call over a bad image URL is too punishing. Mirrors the server-side
|
|
395
|
+
* soft-strip in `@rei-standard/amsg-server` 2.3.3+ and `@rei-standard/amsg-instant`
|
|
396
|
+
* 0.7.1+. See standards §6.2.
|
|
389
397
|
*
|
|
390
398
|
* @private
|
|
391
|
-
* @param {
|
|
399
|
+
* @param {object|null|undefined} target - Payload-like object holding `avatarUrl`.
|
|
400
|
+
* @returns {boolean} `true` if the field was stripped, `false` otherwise.
|
|
392
401
|
*/
|
|
393
|
-
|
|
394
|
-
if (
|
|
402
|
+
_sanitizeAvatarUrl(target) {
|
|
403
|
+
if (!target || typeof target !== 'object') return false;
|
|
404
|
+
const value = target.avatarUrl;
|
|
405
|
+
if (value === undefined || value === null) return false;
|
|
406
|
+
let reason = null;
|
|
395
407
|
if (typeof value !== 'string') {
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
'头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL'
|
|
402
|
-
);
|
|
408
|
+
reason = 'avatarUrl 必须是字符串';
|
|
409
|
+
} else if (/^data:/i.test(value)) {
|
|
410
|
+
reason = '头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL';
|
|
411
|
+
} else if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
412
|
+
reason = `头像 URL 长度 ${value.length} 字符超过 ${AVATAR_URL_MAX_LENGTH} 上限,请改为更短的图片 URL`;
|
|
403
413
|
}
|
|
404
|
-
if (
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
{ actualLength: value.length, limit: AVATAR_URL_MAX_LENGTH }
|
|
409
|
-
);
|
|
414
|
+
if (reason) {
|
|
415
|
+
console.warn('[rei-standard-amsg-client] avatarUrl 不合法,已置空:', reason);
|
|
416
|
+
target.avatarUrl = null;
|
|
417
|
+
return true;
|
|
410
418
|
}
|
|
419
|
+
return false;
|
|
411
420
|
}
|
|
412
421
|
|
|
413
422
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -95,16 +95,16 @@ var ReiClient = class {
|
|
|
95
95
|
*
|
|
96
96
|
* The payload is automatically encrypted before transmission.
|
|
97
97
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
98
|
+
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
99
|
+
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
100
|
+
* schedule still ships, just without an avatar. The only throw left is
|
|
101
|
+
* `PAYLOAD_TOO_LARGE_LOCAL` — JSON-serialized payload exceeds 3 KB.
|
|
102
102
|
*
|
|
103
103
|
* @param {Object} payload - Schedule message payload.
|
|
104
104
|
* @returns {Promise<Object>} API response body.
|
|
105
105
|
*/
|
|
106
106
|
async scheduleMessage(payload) {
|
|
107
|
-
this.
|
|
107
|
+
this._sanitizeAvatarUrl(payload);
|
|
108
108
|
const json = JSON.stringify(payload);
|
|
109
109
|
this._assertPayloadSize(json, "scheduleMessage");
|
|
110
110
|
const encrypted = await this._encrypt(json);
|
|
@@ -141,10 +141,10 @@ var ReiClient = class {
|
|
|
141
141
|
*
|
|
142
142
|
* Routes to `customBaseUrls.instant` if configured, otherwise `baseUrl`.
|
|
143
143
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
144
|
+
* If `avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string), the
|
|
145
|
+
* client soft-strips it on the payload and emits a `console.warn` — the
|
|
146
|
+
* push still ships, just without an icon. The only throw left is
|
|
147
|
+
* `PAYLOAD_TOO_LARGE_LOCAL` — JSON-serialized payload exceeds 3 KB.
|
|
148
148
|
*
|
|
149
149
|
* @param {Object} payload - Instant message payload.
|
|
150
150
|
* @param {string} [endpointPath] - Path under the resolved base URL. Default '/instant'.
|
|
@@ -152,7 +152,7 @@ var ReiClient = class {
|
|
|
152
152
|
* @returns {Promise<Object>} `{ success, data?: { messagesSent, sentAt }, error? }`
|
|
153
153
|
*/
|
|
154
154
|
async sendInstant(payload, endpointPath = "/instant", opts = {}) {
|
|
155
|
-
this.
|
|
155
|
+
this._sanitizeAvatarUrl(payload);
|
|
156
156
|
const json = JSON.stringify(payload);
|
|
157
157
|
this._assertPayloadSize(json, "sendInstant");
|
|
158
158
|
const headers = { "Content-Type": "application/json" };
|
|
@@ -183,17 +183,20 @@ var ReiClient = class {
|
|
|
183
183
|
/**
|
|
184
184
|
* Update an existing scheduled message.
|
|
185
185
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
186
|
+
* If `updates.avatarUrl` is unusable (`data:` URI, > 2 KB, or non-string),
|
|
187
|
+
* the client soft-strips it from the patch and emits a `console.warn` —
|
|
188
|
+
* the rest of the update still applies, and the stored avatar is left
|
|
189
|
+
* untouched. The only throw left is `PAYLOAD_TOO_LARGE_LOCAL` —
|
|
190
|
+
* JSON-serialized updates exceed 3 KB.
|
|
190
191
|
*
|
|
191
192
|
* @param {string} uuid - Task UUID.
|
|
192
193
|
* @param {Object} updates - Fields to update.
|
|
193
194
|
* @returns {Promise<Object>}
|
|
194
195
|
*/
|
|
195
196
|
async updateMessage(uuid, updates) {
|
|
196
|
-
this.
|
|
197
|
+
if (this._sanitizeAvatarUrl(updates)) {
|
|
198
|
+
delete updates.avatarUrl;
|
|
199
|
+
}
|
|
197
200
|
const json = JSON.stringify(updates);
|
|
198
201
|
this._assertPayloadSize(json, "updateMessage");
|
|
199
202
|
const encrypted = await this._encrypt(json);
|
|
@@ -273,33 +276,36 @@ var ReiClient = class {
|
|
|
273
276
|
}
|
|
274
277
|
// ─── Local preflight (no network) ────────────────────────────────
|
|
275
278
|
/**
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
*
|
|
279
|
+
* Sanitize `avatarUrl` on an outgoing payload. If the value is unusable
|
|
280
|
+
* (`data:` URI / oversized / non-string), set the field to `null` on the
|
|
281
|
+
* payload, log a `console.warn`, and let the rest of the request go
|
|
282
|
+
* through. Avatar is cosmetic — failing the entire schedule / instant
|
|
283
|
+
* call over a bad image URL is too punishing. Mirrors the server-side
|
|
284
|
+
* soft-strip in `@rei-standard/amsg-server` 2.3.3+ and `@rei-standard/amsg-instant`
|
|
285
|
+
* 0.7.1+. See standards §6.2.
|
|
281
286
|
*
|
|
282
287
|
* @private
|
|
283
|
-
* @param {
|
|
288
|
+
* @param {object|null|undefined} target - Payload-like object holding `avatarUrl`.
|
|
289
|
+
* @returns {boolean} `true` if the field was stripped, `false` otherwise.
|
|
284
290
|
*/
|
|
285
|
-
|
|
286
|
-
if (
|
|
291
|
+
_sanitizeAvatarUrl(target) {
|
|
292
|
+
if (!target || typeof target !== "object") return false;
|
|
293
|
+
const value = target.avatarUrl;
|
|
294
|
+
if (value === void 0 || value === null) return false;
|
|
295
|
+
let reason = null;
|
|
287
296
|
if (typeof value !== "string") {
|
|
288
|
-
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
"\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL"
|
|
294
|
-
);
|
|
297
|
+
reason = "avatarUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32";
|
|
298
|
+
} else if (/^data:/i.test(value)) {
|
|
299
|
+
reason = "\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL";
|
|
300
|
+
} else if (value.length > AVATAR_URL_MAX_LENGTH) {
|
|
301
|
+
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`;
|
|
295
302
|
}
|
|
296
|
-
if (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
{ actualLength: value.length, limit: AVATAR_URL_MAX_LENGTH }
|
|
301
|
-
);
|
|
303
|
+
if (reason) {
|
|
304
|
+
console.warn("[rei-standard-amsg-client] avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u7F6E\u7A7A\uFF1A", reason);
|
|
305
|
+
target.avatarUrl = null;
|
|
306
|
+
return true;
|
|
302
307
|
}
|
|
308
|
+
return false;
|
|
303
309
|
}
|
|
304
310
|
/**
|
|
305
311
|
* Reject outgoing payloads larger than 3 KB pre-encryption. Spares the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rei-standard/amsg-client",
|
|
3
|
-
"version": "2.3.0-next.
|
|
3
|
+
"version": "2.3.0-next.2",
|
|
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.1.0-next.
|
|
36
|
+
"@rei-standard/amsg-shared": "0.1.0-next.4"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"tsup": "^8.0.0",
|