@rei-standard/amsg-server 2.5.1 → 2.5.3

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 CHANGED
@@ -54,15 +54,25 @@ const rei = await createReiServer({
54
54
 
55
55
  ## 关于 `messageType: 'instant'`
56
56
 
57
- > **Note**:新代码的 instant 消息请用 [@rei-standard/amsg-instant](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/instant/README.md),跳过本端点的"建任务 → 处理 → 删任务" DB 来回。本端点的 `instant` 分支为兼容保留,行为不变、不会有运行时警告。
57
+ > **两条 instant 路径,按各自特点选一条(都是正式支持路径):**
58
+ > - **本端点的 `messageType: 'instant'`**(create task → process by UUID → delete task):任务先写进数据库再处理,投递不绑在请求连接上——客户端断开也没关系,任务行还在,能继续跑、能重试,想跑多久跑多久。适合**有数据库、需要长时间生成或保证消息零丢失**的场景。
59
+ > - **[@rei-standard/amsg-instant](https://github.com/Tosd0/ReiStandard/blob/main/packages/rei-standard-amsg/instant/README.md)**:纯 SSE 流 + Web Push backup,不需要数据库,适合无状态边缘运行时(如 Cloudflare Workers)。它的处理挂在响应连接上,客户端一断开就只剩平台给的那点宽限期把活干完(Deno Deploy 实测 ≈20-30s),所以适合**能快速跑完的短即时消息**。
58
60
 
59
61
  ## AI 接口 `apiUrl` 约束
60
62
 
61
63
  当 `messageType` 为 `prompted` / `auto`,或 `instant` 使用 AI 配置时:
62
64
 
63
- - `apiUrl` 必须是完整聊天端点(例如:`https://api.openai.com/v1/chat/completions`)。
64
- - SDK 会自动做最小规范化:去首尾空白、去路径尾部多余 `/`。
65
- - SDK **不会**自动补全 `/v1`、`/chat/completions` 等路径。
65
+ - `apiUrl` 是聊天端点 URL(例如:`https://api.openai.com/v1/chat/completions`),必须能 `new URL(...)` 解析。
66
+ - SDK 对 OpenAI 风格路径做**幂等**补全(去首尾空白、去尾部多余 `/` 后):
67
+
68
+ | 输入 | 输出 |
69
+ |---|---|
70
+ | `https://api.openai.com`(裸域名) | `https://api.openai.com/v1/chat/completions` |
71
+ | `https://api.openai.com/v1`(版本段结尾) | `https://api.openai.com/v1/chat/completions`(不重复加 `/v1`) |
72
+ | `https://api.openai.com/v1/chat/completions` | 原样返回 |
73
+ | `https://api.anthropic.com/v1/messages`(其他自定义路径) | 原样返回,不猜 |
74
+
75
+ - 规则幂等,传完整 URL 不会被改坏;代理路径很特殊时直接传完整 `…/chat/completions` 绕开补全。
66
76
  - `maxTokens` 为可选字段:传了就映射为 `max_tokens`;不传则不指定(由上游模型默认策略决定)。
67
77
 
68
78
  如果上游返回 `405 Method Not Allowed`,通常表示 `apiUrl` 指向了基础域名而非聊天端点,请优先检查配置值。
package/dist/index.cjs CHANGED
@@ -27,6 +27,7 @@ async function createAdapter(config) {
27
27
  }
28
28
 
29
29
  // src/server/lib/validation.js
30
+ var _amsgshared = require('@rei-standard/amsg-shared');
30
31
  function isValidISO8601(dateString) {
31
32
  const date = new Date(dateString);
32
33
  return date instanceof Date && !isNaN(date.getTime());
@@ -48,23 +49,6 @@ function isValidUUIDv4(uuid) {
48
49
  return uuidV4Regex.test(uuid);
49
50
  }
50
51
  var VALID_LLM_MESSAGE_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
51
- var AVATAR_URL_MAX_LENGTH = 2048;
52
- function validateAvatarUrl(value) {
53
- if (value === void 0 || value === null) return null;
54
- if (typeof value !== "string") {
55
- return "avatarUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32";
56
- }
57
- if (/^data:/i.test(value)) {
58
- return "\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL";
59
- }
60
- if (value.length > AVATAR_URL_MAX_LENGTH) {
61
- return `\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`;
62
- }
63
- if (!isValidUrl(value)) {
64
- return "avatarUrl \u4E0D\u662F\u5408\u6CD5 URL";
65
- }
66
- return null;
67
- }
68
52
  var SPLIT_PATTERN_MAX_LENGTH = 200;
69
53
  var SPLIT_PATTERN_MAX_ITEMS = 10;
70
54
  function validateSplitPattern(value) {
@@ -188,7 +172,7 @@ function validateScheduleMessagePayload(payload) {
188
172
  if (payload.temperature !== void 0 && payload.temperature !== null && (typeof payload.temperature !== "number" || !Number.isFinite(payload.temperature))) {
189
173
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["temperature (must be a finite number)"] } };
190
174
  }
191
- const avatarErr = validateAvatarUrl(payload.avatarUrl);
175
+ const avatarErr = _amsgshared.validateAvatarUrl.call(void 0, payload.avatarUrl);
192
176
  if (avatarErr) {
193
177
  console.warn("[amsg-server] avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u7F6E\u7A7A\uFF1A", avatarErr);
194
178
  payload.avatarUrl = null;
@@ -486,8 +470,11 @@ function isUniqueViolation(error) {
486
470
 
487
471
 
488
472
 
489
- var _amsgshared = require('@rei-standard/amsg-shared');
473
+
474
+
475
+
490
476
  var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
477
+ var SLEEP_BETWEEN_MESSAGES_MS = 1500;
491
478
  function splitOnceByRegex(chunk, regex) {
492
479
  const out = chunk.split(regex).reduce((acc, part, i, arr) => {
493
480
  if (i % 2 === 0 && part.trim()) {
@@ -507,38 +494,6 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
507
494
  }
508
495
  return chunks.length > 0 ? chunks : [messageContent];
509
496
  }
510
- function readReasoningContent(llmResponse) {
511
- if (!llmResponse || typeof llmResponse !== "object") return null;
512
- const choices = (
513
- /** @type {{ choices?: unknown }} */
514
- llmResponse.choices
515
- );
516
- if (!Array.isArray(choices) || choices.length === 0) return null;
517
- const message = (
518
- /** @type {{ message?: { reasoning_content?: unknown, content?: unknown } }} */
519
- _optionalChain([choices, 'access', _2 => _2[0], 'optionalAccess', _3 => _3.message])
520
- );
521
- const raw = _optionalChain([message, 'optionalAccess', _4 => _4.reasoning_content]);
522
- if (typeof raw === "string") {
523
- const trimmed = raw.trim();
524
- if (trimmed.length > 0) return trimmed;
525
- }
526
- const content = _optionalChain([message, 'optionalAccess', _5 => _5.content]);
527
- if (typeof content === "string") {
528
- const match = content.match(REASONING_TAG_RE);
529
- if (match) {
530
- const trimmed = match[2].trim();
531
- if (trimmed.length > 0) return trimmed;
532
- }
533
- }
534
- return null;
535
- }
536
- var REASONING_TAG_RE = /<(think|thinking|thought)>([\s\S]*?)<\/\1>/i;
537
- var REASONING_TAG_RE_G = /<(think|thinking|thought)>[\s\S]*?<\/\1>/gi;
538
- function stripReasoningTags(content) {
539
- if (typeof content !== "string" || !content.includes("<")) return content;
540
- return content.replace(REASONING_TAG_RE_G, "").trim();
541
- }
542
497
  async function processSingleMessage(task, ctx, providedMasterKey) {
543
498
  try {
544
499
  const masterKey = providedMasterKey || ctx.masterKey;
@@ -569,9 +524,9 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
569
524
  } else {
570
525
  throw new Error("Invalid message configuration: no content source available");
571
526
  }
572
- const reasoning = readReasoningContent(llmResponse);
527
+ const reasoning = _amsgshared.readReasoningContent.call(void 0, llmResponse);
573
528
  if (reasoning) {
574
- messageContent = stripReasoningTags(messageContent);
529
+ messageContent = _amsgshared.stripReasoningTags.call(void 0, messageContent);
575
530
  }
576
531
  const messages = splitMessageIntoSentences(messageContent, _nullishCoalesce(decryptedPayload.splitPattern, () => ( null)));
577
532
  if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
@@ -599,7 +554,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
599
554
  metadata
600
555
  });
601
556
  await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
602
- await new Promise((resolve) => setTimeout(resolve, 1500));
557
+ await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
603
558
  }
604
559
  for (let i = 0; i < messages.length; i++) {
605
560
  const contentPush = _amsgshared.buildContentPush.call(void 0, {
@@ -620,7 +575,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
620
575
  });
621
576
  await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
622
577
  if (i < messages.length - 1) {
623
- await new Promise((resolve) => setTimeout(resolve, 1500));
578
+ await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
624
579
  }
625
580
  }
626
581
  return { success: true, messagesSent: messages.length };
@@ -713,7 +668,7 @@ async function _callAI(payload) {
713
668
  );
714
669
  }
715
670
  const aiData = await aiResponse.json();
716
- const content = _optionalChain([aiData, 'optionalAccess', _6 => _6.choices, 'optionalAccess', _7 => _7[0], 'optionalAccess', _8 => _8.message, 'optionalAccess', _9 => _9.content]);
671
+ const content = _optionalChain([aiData, 'optionalAccess', _2 => _2.choices, 'optionalAccess', _3 => _3[0], 'optionalAccess', _4 => _4.message, 'optionalAccess', _5 => _5.content]);
717
672
  if (typeof content !== "string" || !content.trim()) {
718
673
  throw new Error("AI API error: response missing choices[0].message.content");
719
674
  }
@@ -1170,7 +1125,7 @@ function createUpdateMessageHandler(ctx) {
1170
1125
  }
1171
1126
  }
1172
1127
  if (Object.prototype.hasOwnProperty.call(updates, "avatarUrl")) {
1173
- const avatarErr = validateAvatarUrl(updates.avatarUrl);
1128
+ const avatarErr = _amsgshared.validateAvatarUrl.call(void 0, updates.avatarUrl);
1174
1129
  if (avatarErr) {
1175
1130
  console.warn("[amsg-server] update-message avatarUrl \u4E0D\u5408\u6CD5\uFF0C\u5DF2\u5FFD\u7565\uFF1A", avatarErr);
1176
1131
  delete updates.avatarUrl;
@@ -1397,7 +1352,7 @@ async function resolveBlobStore(namespace) {
1397
1352
  }
1398
1353
  try {
1399
1354
  const blobModule = await Promise.resolve().then(() => _interopRequireWildcard(require("@netlify/blobs")));
1400
- const getStore = blobModule.getStore || _optionalChain([blobModule, 'access', _10 => _10.default, 'optionalAccess', _11 => _11.getStore]);
1355
+ const getStore = blobModule.getStore || _optionalChain([blobModule, 'access', _6 => _6.default, 'optionalAccess', _7 => _7.getStore]);
1401
1356
  if (typeof getStore === "function") {
1402
1357
  return getStore({ name: namespace });
1403
1358
  }
@@ -1654,11 +1609,7 @@ function createTenantContextManager(options) {
1654
1609
  }
1655
1610
 
1656
1611
  // src/server/index.js
1657
- function normalizeVapidSubject(email) {
1658
- const trimmedEmail = String(email || "").trim();
1659
- if (!trimmedEmail) return "";
1660
- return /^mailto:/i.test(trimmedEmail) ? trimmedEmail : `mailto:${trimmedEmail}`;
1661
- }
1612
+
1662
1613
  async function createReiServer(config) {
1663
1614
  if (!config) throw new Error("[rei-standard-amsg-server] config is required");
1664
1615
  if (!config.tenant) throw new Error("[rei-standard-amsg-server] tenant config is required");
@@ -1674,7 +1625,7 @@ async function createReiServer(config) {
1674
1625
  const vapid = config.vapid || {};
1675
1626
  if (vapid.email && vapid.publicKey && vapid.privateKey) {
1676
1627
  webpushModule.setVapidDetails(
1677
- normalizeVapidSubject(vapid.email),
1628
+ _amsgshared.normalizeVapidSubject.call(void 0, vapid.email),
1678
1629
  vapid.publicKey,
1679
1630
  vapid.privateKey
1680
1631
  );
@@ -1731,4 +1682,4 @@ async function createReiServer(config) {
1731
1682
 
1732
1683
 
1733
1684
 
1734
- exports.createAdapter = createAdapter; exports.createReiServer = createReiServer; exports.createTenantToken = createTenantToken; exports.decryptFromStorage = decryptFromStorage; exports.decryptPayload = decryptPayload; exports.deriveUserEncryptionKey = deriveUserEncryptionKey; exports.encryptForStorage = encryptForStorage; exports.isValidISO8601 = isValidISO8601; exports.isValidUUID = isValidUUID; exports.isValidUUIDv4 = isValidUUIDv4; exports.isValidUrl = isValidUrl; exports.validateAvatarUrl = validateAvatarUrl; exports.validateLlmMessagesArray = validateLlmMessagesArray; exports.validateScheduleMessagePayload = validateScheduleMessagePayload; exports.validateSplitPattern = validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
1685
+ exports.createAdapter = createAdapter; exports.createReiServer = createReiServer; exports.createTenantToken = createTenantToken; exports.decryptFromStorage = decryptFromStorage; exports.decryptPayload = decryptPayload; exports.deriveUserEncryptionKey = deriveUserEncryptionKey; exports.encryptForStorage = encryptForStorage; exports.isValidISO8601 = isValidISO8601; exports.isValidUUID = isValidUUID; exports.isValidUUIDv4 = isValidUUIDv4; exports.isValidUrl = isValidUrl; exports.validateAvatarUrl = _amsgshared.validateAvatarUrl; exports.validateLlmMessagesArray = validateLlmMessagesArray; exports.validateScheduleMessagePayload = validateScheduleMessagePayload; exports.validateSplitPattern = validateSplitPattern; exports.verifyTenantToken = verifyTenantToken;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
+ import { validateAvatarUrl, readReasoningContent, stripReasoningTags, buildReasoningPush, buildContentPush, normalizeVapidSubject } from '@rei-standard/amsg-shared';
2
+ export { validateAvatarUrl } from '@rei-standard/amsg-shared';
1
3
  import { createDecipheriv, createHash, randomBytes, createCipheriv, randomUUID, timingSafeEqual, createHmac } from 'crypto';
2
- import { buildReasoningPush, buildContentPush } from '@rei-standard/amsg-shared';
3
4
 
4
5
  /**
5
6
  * Adapter Factory
@@ -58,6 +59,7 @@ async function createAdapter(config) {
58
59
  * ReiStandard SDK v2.0.1
59
60
  */
60
61
 
62
+
61
63
  /**
62
64
  * Validate ISO 8601 date string.
63
65
  * @param {string} dateString
@@ -104,43 +106,12 @@ function isValidUUIDv4(uuid) {
104
106
 
105
107
  const VALID_LLM_MESSAGE_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
106
108
 
107
- const AVATAR_URL_MAX_LENGTH = 2048;
108
-
109
- /**
110
- * Validate the optional `avatarUrl` field. Rejects `data:` URIs (typically
111
- * base64-encoded inline images) and anything longer than 2048 chars, both
112
- * of which are the dominant trigger for downstream 413 / Web Push 4 KB
113
- * payload errors. Returns an error message string, or null when valid.
114
- *
115
- * Mirrors @rei-standard/amsg-instant's `validateAvatarUrl` (kept in lockstep
116
- * on purpose — both packages forward `avatarUrl` to the same SW push payload).
117
- *
118
- * @param {unknown} value
119
- * @returns {string | null}
120
- */
121
- function validateAvatarUrl(value) {
122
- if (value === undefined || value === null) return null;
123
- if (typeof value !== 'string') {
124
- return 'avatarUrl 必须是字符串';
125
- }
126
- if (/^data:/i.test(value)) {
127
- return '头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL';
128
- }
129
- if (value.length > AVATAR_URL_MAX_LENGTH) {
130
- return `头像 URL 长度 ${value.length} 字符超过 ${AVATAR_URL_MAX_LENGTH} 上限,请改为更短的图片 URL`;
131
- }
132
- if (!isValidUrl(value)) {
133
- return 'avatarUrl 不是合法 URL';
134
- }
135
- return null;
136
- }
137
-
138
109
  const SPLIT_PATTERN_MAX_LENGTH = 200;
139
110
  const SPLIT_PATTERN_MAX_ITEMS = 10;
140
111
 
141
112
  /**
142
- * Validate the optional `splitPattern` field. Mirrors
143
- * @rei-standard/amsg-instant's `validateSplitPattern` (kept in lockstep).
113
+ * Validate the optional `splitPattern` field (amsg-server scheduled tasks
114
+ * only; amsg-instant 0.8.0 dropped its request-level `splitPattern`).
144
115
  * Accepts `string`, `string[]`, or absent/null. Returns an error message
145
116
  * string, or null when valid.
146
117
  *
@@ -801,13 +772,13 @@ function isUniqueViolation(error) {
801
772
  * ReiStandard amsg-server v2.4.0
802
773
  *
803
774
  * Handles single message content generation and Web Push delivery for
804
- * scheduled tasks (`fixed` / `prompted` / `auto`) and the legacy
805
- * via-server instant path (`messageType: 'instant'`).
775
+ * scheduled tasks (`fixed` / `prompted` / `auto`) and the
776
+ * in-server instant path (`messageType: 'instant'`).
806
777
  *
807
778
  * Push wire shape comes from `@rei-standard/amsg-shared`'s
808
779
  * discriminated union (`AmsgPush`). The SW (`@rei-standard/amsg-sw`)
809
780
  * routes on `messageKind`. Server-driven pushes always carry
810
- * `source: 'instant'` (for the legacy in-server instant) or
781
+ * `source: 'instant'` (for the in-server instant path) or
811
782
  * `source: 'scheduled'` (for everything else).
812
783
  *
813
784
  * v2.4.0: when the LLM response carries non-empty
@@ -820,6 +791,11 @@ function isUniqueViolation(error) {
820
791
 
821
792
  const DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
822
793
 
794
+ // Pacing between consecutive Web Push deliveries (reasoning → content, and
795
+ // between content sentences) so the client renders a natural typing cadence.
796
+ // Kept equal to amsg-instant's SLEEP_BETWEEN_MESSAGES_MS default.
797
+ const SLEEP_BETWEEN_MESSAGES_MS = 1500;
798
+
823
799
  /**
824
800
  * Split a single chunk by one regex; on no-match return [chunk] so a later
825
801
  * regex in a cascade can still take a swing at it.
@@ -839,11 +815,10 @@ function splitOnceByRegex(chunk, regex) {
839
815
  }
840
816
 
841
817
  /**
842
- * Sentence splitter — kept byte-for-byte equivalent to
843
- * `@rei-standard/amsg-instant`'s `splitMessageIntoSentences`. Server carries
844
- * its own copy to avoid an architectural dependency on the instant package.
845
- * Name matches the instant export on purpose so cross-package grep finds
846
- * both copies; if you fix a bug here, fix it in the instant copy too.
818
+ * Sentence splitter for amsg-server's scheduled `splitPattern` feature
819
+ * (see standards §6.1). Server-only: amsg-instant 0.8.0 dropped its
820
+ * request-level `splitPattern`, so there is no instant counterpart to keep
821
+ * in lockstep.
847
822
  *
848
823
  * @param {string} messageContent
849
824
  * @param {string | string[] | null} [splitPattern=null]
@@ -867,54 +842,6 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
867
842
  return chunks.length > 0 ? chunks : [messageContent];
868
843
  }
869
844
 
870
- /**
871
- * Read `choices[0].message.reasoning_content` as a non-empty trimmed
872
- * string, or null when absent / empty. Mirrors
873
- * `amsg-instant/src/message-processor.js#readReasoningContent`.
874
- *
875
- * @param {unknown} llmResponse
876
- * @returns {string | null}
877
- */
878
- function readReasoningContent(llmResponse) {
879
- if (!llmResponse || typeof llmResponse !== 'object') return null;
880
- const choices = /** @type {{ choices?: unknown }} */ (llmResponse).choices;
881
- if (!Array.isArray(choices) || choices.length === 0) return null;
882
- const message = /** @type {{ message?: { reasoning_content?: unknown, content?: unknown } }} */ (choices[0])?.message;
883
-
884
- const raw = message?.reasoning_content;
885
- if (typeof raw === 'string') {
886
- const trimmed = raw.trim();
887
- if (trimmed.length > 0) return trimmed;
888
- }
889
-
890
- const content = message?.content;
891
- if (typeof content === 'string') {
892
- const match = content.match(REASONING_TAG_RE);
893
- if (match) {
894
- const trimmed = match[2].trim();
895
- if (trimmed.length > 0) return trimmed;
896
- }
897
- }
898
-
899
- return null;
900
- }
901
-
902
- const REASONING_TAG_RE = /<(think|thinking|thought)>([\s\S]*?)<\/\1>/i;
903
- const REASONING_TAG_RE_G = /<(think|thinking|thought)>[\s\S]*?<\/\1>/gi;
904
-
905
- /**
906
- * Mirrors `amsg-instant/src/message-processor.js#stripReasoningTags`.
907
- * Removes any private chain-of-thought markup leaking through
908
- * `message.content` so it does not also ship inside ContentPush.
909
- *
910
- * @param {string} content
911
- * @returns {string}
912
- */
913
- function stripReasoningTags(content) {
914
- if (typeof content !== 'string' || !content.includes('<')) return content;
915
- return content.replace(REASONING_TAG_RE_G, '').trim();
916
- }
917
-
918
845
  /**
919
846
  * @typedef {Object} ProcessorContext
920
847
  * @property {Object} webpush - The web-push module instance (already VAPID-configured).
@@ -1004,7 +931,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
1004
931
  // `messageId` format — deterministic when we have a task.id so a
1005
932
  // retry produces the same id for the same (task, sentence) pair
1006
933
  // (downstream dedupers can key on it). Falls back to a UUID for
1007
- // the legacy in-server instant path that has no row id.
934
+ // the in-server instant path that has no row id.
1008
935
  const messageIdBase = task.id != null
1009
936
  ? `msg_task_${task.id}`
1010
937
  : `msg_${randomUUID()}_instant`;
@@ -1028,7 +955,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
1028
955
  metadata,
1029
956
  });
1030
957
  await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
1031
- await new Promise(resolve => setTimeout(resolve, 1500));
958
+ await new Promise(resolve => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
1032
959
  }
1033
960
 
1034
961
  for (let i = 0; i < messages.length; i++) {
@@ -1052,7 +979,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
1052
979
  await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
1053
980
 
1054
981
  if (i < messages.length - 1) {
1055
- await new Promise(resolve => setTimeout(resolve, 1500));
982
+ await new Promise(resolve => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
1056
983
  }
1057
984
  }
1058
985
 
@@ -1398,12 +1325,20 @@ function createScheduleMessageHandler(ctx) {
1398
1325
  const encryptedPayload = encryptForStorage(JSON.stringify(fullTaskData), userKey);
1399
1326
 
1400
1327
  /**
1401
- * @deprecated Soft-deprecated. For new code, use @rei-standard/amsg-instant.
1402
- * This branch is kept for backward compatibility and the existing behavior
1403
- * (create task → process → delete) is unchanged. The dedicated amsg-instant
1404
- * package is stateless (no DB roundtrip), deployable to Cloudflare Workers,
1405
- * and locks the encryption + push-payload contract behind a single version.
1406
- * See packages/rei-standard-amsg/instant/README.md.
1328
+ * In-server instant path. Delivers an instant message through this
1329
+ * server's own task queue (create task → process by UUID → delete task).
1330
+ * The task is written to the database before processing, so delivery is
1331
+ * not tied to the request connection: even if the client disconnects, the
1332
+ * row stays and the generation keeps running (and can be retried) for as
1333
+ * long as it needs. Use this when you have a database and want long or
1334
+ * guaranteed-complete generations with no dropped messages.
1335
+ *
1336
+ * The stateless alternative is `@rei-standard/amsg-instant`: it streams
1337
+ * over SSE with a Web Push backup and needs no database, which makes it a
1338
+ * good fit for edge runtimes (e.g. Cloudflare Workers). Its work rides the
1339
+ * response connection, so after the client disconnects it only has the
1340
+ * platform's brief grace window to finish (≈20-30s observed on Deno
1341
+ * Deploy) — ideal for short instant messages that complete quickly.
1407
1342
  */
1408
1343
  // Instant type: check VAPID before creating the task to avoid orphaned rows
1409
1344
  if (payload.messageType === 'instant') {
@@ -1459,11 +1394,20 @@ function createScheduleMessageHandler(ctx) {
1459
1394
  }
1460
1395
 
1461
1396
  /**
1462
- * @deprecated Soft-deprecated. For new code, use @rei-standard/amsg-instant.
1463
- * The "create-task → process-by-uuid → delete-task" sequence below is
1464
- * preserved verbatim so existing clients keep working. New integrations
1465
- * should call the dedicated amsg-instant endpoint instead — it skips this
1466
- * DB round-trip entirely. See packages/rei-standard-amsg/instant/README.md.
1397
+ * In-server instant path. Delivers an instant message through this
1398
+ * server's own task queue (create task → process by UUID → delete task).
1399
+ * The task is written to the database before processing, so delivery is
1400
+ * not tied to the request connection: even if the client disconnects, the
1401
+ * row stays and the generation keeps running (and can be retried) for as
1402
+ * long as it needs. Use this when you have a database and want long or
1403
+ * guaranteed-complete generations with no dropped messages.
1404
+ *
1405
+ * The stateless alternative is `@rei-standard/amsg-instant`: it streams
1406
+ * over SSE with a Web Push backup and needs no database, which makes it a
1407
+ * good fit for edge runtimes (e.g. Cloudflare Workers). Its work rides the
1408
+ * response connection, so after the client disconnects it only has the
1409
+ * platform's brief grace window to finish (≈20-30s observed on Deno
1410
+ * Deploy) — ideal for short instant messages that complete quickly.
1467
1411
  */
1468
1412
  // Instant type: send immediately
1469
1413
  if (payload.messageType === 'instant') {
@@ -2481,12 +2425,6 @@ function createTenantContextManager(options) {
2481
2425
  */
2482
2426
 
2483
2427
 
2484
- function normalizeVapidSubject(email) {
2485
- const trimmedEmail = String(email || '').trim();
2486
- if (!trimmedEmail) return '';
2487
- return /^mailto:/i.test(trimmedEmail) ? trimmedEmail : `mailto:${trimmedEmail}`;
2488
- }
2489
-
2490
2428
  /**
2491
2429
  * @typedef {Object} VapidConfig
2492
2430
  * @property {string} [email] - VAPID contact email (e.g. mailto:…).
@@ -2598,4 +2536,4 @@ async function createReiServer(config) {
2598
2536
  };
2599
2537
  }
2600
2538
 
2601
- export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateAvatarUrl, validateLlmMessagesArray, validateScheduleMessagePayload, validateSplitPattern, verifyTenantToken };
2539
+ export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateLlmMessagesArray, validateScheduleMessagePayload, validateSplitPattern, verifyTenantToken };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { validateAvatarUrl, readReasoningContent, stripReasoningTags, buildReasoningPush, buildContentPush, normalizeVapidSubject } from '@rei-standard/amsg-shared';
2
+ export { validateAvatarUrl } from '@rei-standard/amsg-shared';
1
3
  import { createDecipheriv, createHash, randomBytes, createCipheriv, randomUUID, timingSafeEqual, createHmac } from 'crypto';
2
- import { buildReasoningPush, buildContentPush } from '@rei-standard/amsg-shared';
3
4
 
4
5
  /**
5
6
  * Adapter Factory
@@ -58,6 +59,7 @@ async function createAdapter(config) {
58
59
  * ReiStandard SDK v2.0.1
59
60
  */
60
61
 
62
+
61
63
  /**
62
64
  * Validate ISO 8601 date string.
63
65
  * @param {string} dateString
@@ -104,43 +106,12 @@ function isValidUUIDv4(uuid) {
104
106
 
105
107
  const VALID_LLM_MESSAGE_ROLES = new Set(['system', 'user', 'assistant', 'tool']);
106
108
 
107
- const AVATAR_URL_MAX_LENGTH = 2048;
108
-
109
- /**
110
- * Validate the optional `avatarUrl` field. Rejects `data:` URIs (typically
111
- * base64-encoded inline images) and anything longer than 2048 chars, both
112
- * of which are the dominant trigger for downstream 413 / Web Push 4 KB
113
- * payload errors. Returns an error message string, or null when valid.
114
- *
115
- * Mirrors @rei-standard/amsg-instant's `validateAvatarUrl` (kept in lockstep
116
- * on purpose — both packages forward `avatarUrl` to the same SW push payload).
117
- *
118
- * @param {unknown} value
119
- * @returns {string | null}
120
- */
121
- function validateAvatarUrl(value) {
122
- if (value === undefined || value === null) return null;
123
- if (typeof value !== 'string') {
124
- return 'avatarUrl 必须是字符串';
125
- }
126
- if (/^data:/i.test(value)) {
127
- return '头像不支持传入 data: URI,请改为公网可访问的 https:// 图片 URL';
128
- }
129
- if (value.length > AVATAR_URL_MAX_LENGTH) {
130
- return `头像 URL 长度 ${value.length} 字符超过 ${AVATAR_URL_MAX_LENGTH} 上限,请改为更短的图片 URL`;
131
- }
132
- if (!isValidUrl(value)) {
133
- return 'avatarUrl 不是合法 URL';
134
- }
135
- return null;
136
- }
137
-
138
109
  const SPLIT_PATTERN_MAX_LENGTH = 200;
139
110
  const SPLIT_PATTERN_MAX_ITEMS = 10;
140
111
 
141
112
  /**
142
- * Validate the optional `splitPattern` field. Mirrors
143
- * @rei-standard/amsg-instant's `validateSplitPattern` (kept in lockstep).
113
+ * Validate the optional `splitPattern` field (amsg-server scheduled tasks
114
+ * only; amsg-instant 0.8.0 dropped its request-level `splitPattern`).
144
115
  * Accepts `string`, `string[]`, or absent/null. Returns an error message
145
116
  * string, or null when valid.
146
117
  *
@@ -801,13 +772,13 @@ function isUniqueViolation(error) {
801
772
  * ReiStandard amsg-server v2.4.0
802
773
  *
803
774
  * Handles single message content generation and Web Push delivery for
804
- * scheduled tasks (`fixed` / `prompted` / `auto`) and the legacy
805
- * via-server instant path (`messageType: 'instant'`).
775
+ * scheduled tasks (`fixed` / `prompted` / `auto`) and the
776
+ * in-server instant path (`messageType: 'instant'`).
806
777
  *
807
778
  * Push wire shape comes from `@rei-standard/amsg-shared`'s
808
779
  * discriminated union (`AmsgPush`). The SW (`@rei-standard/amsg-sw`)
809
780
  * routes on `messageKind`. Server-driven pushes always carry
810
- * `source: 'instant'` (for the legacy in-server instant) or
781
+ * `source: 'instant'` (for the in-server instant path) or
811
782
  * `source: 'scheduled'` (for everything else).
812
783
  *
813
784
  * v2.4.0: when the LLM response carries non-empty
@@ -820,6 +791,11 @@ function isUniqueViolation(error) {
820
791
 
821
792
  const DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
822
793
 
794
+ // Pacing between consecutive Web Push deliveries (reasoning → content, and
795
+ // between content sentences) so the client renders a natural typing cadence.
796
+ // Kept equal to amsg-instant's SLEEP_BETWEEN_MESSAGES_MS default.
797
+ const SLEEP_BETWEEN_MESSAGES_MS = 1500;
798
+
823
799
  /**
824
800
  * Split a single chunk by one regex; on no-match return [chunk] so a later
825
801
  * regex in a cascade can still take a swing at it.
@@ -839,11 +815,10 @@ function splitOnceByRegex(chunk, regex) {
839
815
  }
840
816
 
841
817
  /**
842
- * Sentence splitter — kept byte-for-byte equivalent to
843
- * `@rei-standard/amsg-instant`'s `splitMessageIntoSentences`. Server carries
844
- * its own copy to avoid an architectural dependency on the instant package.
845
- * Name matches the instant export on purpose so cross-package grep finds
846
- * both copies; if you fix a bug here, fix it in the instant copy too.
818
+ * Sentence splitter for amsg-server's scheduled `splitPattern` feature
819
+ * (see standards §6.1). Server-only: amsg-instant 0.8.0 dropped its
820
+ * request-level `splitPattern`, so there is no instant counterpart to keep
821
+ * in lockstep.
847
822
  *
848
823
  * @param {string} messageContent
849
824
  * @param {string | string[] | null} [splitPattern=null]
@@ -867,54 +842,6 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
867
842
  return chunks.length > 0 ? chunks : [messageContent];
868
843
  }
869
844
 
870
- /**
871
- * Read `choices[0].message.reasoning_content` as a non-empty trimmed
872
- * string, or null when absent / empty. Mirrors
873
- * `amsg-instant/src/message-processor.js#readReasoningContent`.
874
- *
875
- * @param {unknown} llmResponse
876
- * @returns {string | null}
877
- */
878
- function readReasoningContent(llmResponse) {
879
- if (!llmResponse || typeof llmResponse !== 'object') return null;
880
- const choices = /** @type {{ choices?: unknown }} */ (llmResponse).choices;
881
- if (!Array.isArray(choices) || choices.length === 0) return null;
882
- const message = /** @type {{ message?: { reasoning_content?: unknown, content?: unknown } }} */ (choices[0])?.message;
883
-
884
- const raw = message?.reasoning_content;
885
- if (typeof raw === 'string') {
886
- const trimmed = raw.trim();
887
- if (trimmed.length > 0) return trimmed;
888
- }
889
-
890
- const content = message?.content;
891
- if (typeof content === 'string') {
892
- const match = content.match(REASONING_TAG_RE);
893
- if (match) {
894
- const trimmed = match[2].trim();
895
- if (trimmed.length > 0) return trimmed;
896
- }
897
- }
898
-
899
- return null;
900
- }
901
-
902
- const REASONING_TAG_RE = /<(think|thinking|thought)>([\s\S]*?)<\/\1>/i;
903
- const REASONING_TAG_RE_G = /<(think|thinking|thought)>[\s\S]*?<\/\1>/gi;
904
-
905
- /**
906
- * Mirrors `amsg-instant/src/message-processor.js#stripReasoningTags`.
907
- * Removes any private chain-of-thought markup leaking through
908
- * `message.content` so it does not also ship inside ContentPush.
909
- *
910
- * @param {string} content
911
- * @returns {string}
912
- */
913
- function stripReasoningTags(content) {
914
- if (typeof content !== 'string' || !content.includes('<')) return content;
915
- return content.replace(REASONING_TAG_RE_G, '').trim();
916
- }
917
-
918
845
  /**
919
846
  * @typedef {Object} ProcessorContext
920
847
  * @property {Object} webpush - The web-push module instance (already VAPID-configured).
@@ -1004,7 +931,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
1004
931
  // `messageId` format — deterministic when we have a task.id so a
1005
932
  // retry produces the same id for the same (task, sentence) pair
1006
933
  // (downstream dedupers can key on it). Falls back to a UUID for
1007
- // the legacy in-server instant path that has no row id.
934
+ // the in-server instant path that has no row id.
1008
935
  const messageIdBase = task.id != null
1009
936
  ? `msg_task_${task.id}`
1010
937
  : `msg_${randomUUID()}_instant`;
@@ -1028,7 +955,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
1028
955
  metadata,
1029
956
  });
1030
957
  await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
1031
- await new Promise(resolve => setTimeout(resolve, 1500));
958
+ await new Promise(resolve => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
1032
959
  }
1033
960
 
1034
961
  for (let i = 0; i < messages.length; i++) {
@@ -1052,7 +979,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
1052
979
  await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
1053
980
 
1054
981
  if (i < messages.length - 1) {
1055
- await new Promise(resolve => setTimeout(resolve, 1500));
982
+ await new Promise(resolve => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
1056
983
  }
1057
984
  }
1058
985
 
@@ -1398,12 +1325,20 @@ function createScheduleMessageHandler(ctx) {
1398
1325
  const encryptedPayload = encryptForStorage(JSON.stringify(fullTaskData), userKey);
1399
1326
 
1400
1327
  /**
1401
- * @deprecated Soft-deprecated. For new code, use @rei-standard/amsg-instant.
1402
- * This branch is kept for backward compatibility and the existing behavior
1403
- * (create task → process → delete) is unchanged. The dedicated amsg-instant
1404
- * package is stateless (no DB roundtrip), deployable to Cloudflare Workers,
1405
- * and locks the encryption + push-payload contract behind a single version.
1406
- * See packages/rei-standard-amsg/instant/README.md.
1328
+ * In-server instant path. Delivers an instant message through this
1329
+ * server's own task queue (create task → process by UUID → delete task).
1330
+ * The task is written to the database before processing, so delivery is
1331
+ * not tied to the request connection: even if the client disconnects, the
1332
+ * row stays and the generation keeps running (and can be retried) for as
1333
+ * long as it needs. Use this when you have a database and want long or
1334
+ * guaranteed-complete generations with no dropped messages.
1335
+ *
1336
+ * The stateless alternative is `@rei-standard/amsg-instant`: it streams
1337
+ * over SSE with a Web Push backup and needs no database, which makes it a
1338
+ * good fit for edge runtimes (e.g. Cloudflare Workers). Its work rides the
1339
+ * response connection, so after the client disconnects it only has the
1340
+ * platform's brief grace window to finish (≈20-30s observed on Deno
1341
+ * Deploy) — ideal for short instant messages that complete quickly.
1407
1342
  */
1408
1343
  // Instant type: check VAPID before creating the task to avoid orphaned rows
1409
1344
  if (payload.messageType === 'instant') {
@@ -1459,11 +1394,20 @@ function createScheduleMessageHandler(ctx) {
1459
1394
  }
1460
1395
 
1461
1396
  /**
1462
- * @deprecated Soft-deprecated. For new code, use @rei-standard/amsg-instant.
1463
- * The "create-task → process-by-uuid → delete-task" sequence below is
1464
- * preserved verbatim so existing clients keep working. New integrations
1465
- * should call the dedicated amsg-instant endpoint instead — it skips this
1466
- * DB round-trip entirely. See packages/rei-standard-amsg/instant/README.md.
1397
+ * In-server instant path. Delivers an instant message through this
1398
+ * server's own task queue (create task → process by UUID → delete task).
1399
+ * The task is written to the database before processing, so delivery is
1400
+ * not tied to the request connection: even if the client disconnects, the
1401
+ * row stays and the generation keeps running (and can be retried) for as
1402
+ * long as it needs. Use this when you have a database and want long or
1403
+ * guaranteed-complete generations with no dropped messages.
1404
+ *
1405
+ * The stateless alternative is `@rei-standard/amsg-instant`: it streams
1406
+ * over SSE with a Web Push backup and needs no database, which makes it a
1407
+ * good fit for edge runtimes (e.g. Cloudflare Workers). Its work rides the
1408
+ * response connection, so after the client disconnects it only has the
1409
+ * platform's brief grace window to finish (≈20-30s observed on Deno
1410
+ * Deploy) — ideal for short instant messages that complete quickly.
1467
1411
  */
1468
1412
  // Instant type: send immediately
1469
1413
  if (payload.messageType === 'instant') {
@@ -2481,12 +2425,6 @@ function createTenantContextManager(options) {
2481
2425
  */
2482
2426
 
2483
2427
 
2484
- function normalizeVapidSubject(email) {
2485
- const trimmedEmail = String(email || '').trim();
2486
- if (!trimmedEmail) return '';
2487
- return /^mailto:/i.test(trimmedEmail) ? trimmedEmail : `mailto:${trimmedEmail}`;
2488
- }
2489
-
2490
2428
  /**
2491
2429
  * @typedef {Object} VapidConfig
2492
2430
  * @property {string} [email] - VAPID contact email (e.g. mailto:…).
@@ -2598,4 +2536,4 @@ async function createReiServer(config) {
2598
2536
  };
2599
2537
  }
2600
2538
 
2601
- export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateAvatarUrl, validateLlmMessagesArray, validateScheduleMessagePayload, validateSplitPattern, verifyTenantToken };
2539
+ export { createAdapter, createReiServer, createTenantToken, decryptFromStorage, decryptPayload, deriveUserEncryptionKey, encryptForStorage, isValidISO8601, isValidUUID, isValidUUIDv4, isValidUrl, validateLlmMessagesArray, validateScheduleMessagePayload, validateSplitPattern, verifyTenantToken };
package/dist/index.mjs CHANGED
@@ -27,6 +27,7 @@ async function createAdapter(config) {
27
27
  }
28
28
 
29
29
  // src/server/lib/validation.js
30
+ import { validateAvatarUrl } from "@rei-standard/amsg-shared";
30
31
  function isValidISO8601(dateString) {
31
32
  const date = new Date(dateString);
32
33
  return date instanceof Date && !isNaN(date.getTime());
@@ -48,23 +49,6 @@ function isValidUUIDv4(uuid) {
48
49
  return uuidV4Regex.test(uuid);
49
50
  }
50
51
  var VALID_LLM_MESSAGE_ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant", "tool"]);
51
- var AVATAR_URL_MAX_LENGTH = 2048;
52
- function validateAvatarUrl(value) {
53
- if (value === void 0 || value === null) return null;
54
- if (typeof value !== "string") {
55
- return "avatarUrl \u5FC5\u987B\u662F\u5B57\u7B26\u4E32";
56
- }
57
- if (/^data:/i.test(value)) {
58
- return "\u5934\u50CF\u4E0D\u652F\u6301\u4F20\u5165 data: URI\uFF0C\u8BF7\u6539\u4E3A\u516C\u7F51\u53EF\u8BBF\u95EE\u7684 https:// \u56FE\u7247 URL";
59
- }
60
- if (value.length > AVATAR_URL_MAX_LENGTH) {
61
- return `\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`;
62
- }
63
- if (!isValidUrl(value)) {
64
- return "avatarUrl \u4E0D\u662F\u5408\u6CD5 URL";
65
- }
66
- return null;
67
- }
68
52
  var SPLIT_PATTERN_MAX_LENGTH = 200;
69
53
  var SPLIT_PATTERN_MAX_ITEMS = 10;
70
54
  function validateSplitPattern(value) {
@@ -485,9 +469,12 @@ function isUniqueViolation(error) {
485
469
  import { randomUUID } from "crypto";
486
470
  import {
487
471
  buildContentPush,
488
- buildReasoningPush
472
+ buildReasoningPush,
473
+ readReasoningContent,
474
+ stripReasoningTags
489
475
  } from "@rei-standard/amsg-shared";
490
476
  var DEFAULT_SPLIT_REGEX = /([。!?!?]+)/;
477
+ var SLEEP_BETWEEN_MESSAGES_MS = 1500;
491
478
  function splitOnceByRegex(chunk, regex) {
492
479
  const out = chunk.split(regex).reduce((acc, part, i, arr) => {
493
480
  if (i % 2 === 0 && part.trim()) {
@@ -507,38 +494,6 @@ function splitMessageIntoSentences(messageContent, splitPattern = null) {
507
494
  }
508
495
  return chunks.length > 0 ? chunks : [messageContent];
509
496
  }
510
- function readReasoningContent(llmResponse) {
511
- if (!llmResponse || typeof llmResponse !== "object") return null;
512
- const choices = (
513
- /** @type {{ choices?: unknown }} */
514
- llmResponse.choices
515
- );
516
- if (!Array.isArray(choices) || choices.length === 0) return null;
517
- const message = (
518
- /** @type {{ message?: { reasoning_content?: unknown, content?: unknown } }} */
519
- choices[0]?.message
520
- );
521
- const raw = message?.reasoning_content;
522
- if (typeof raw === "string") {
523
- const trimmed = raw.trim();
524
- if (trimmed.length > 0) return trimmed;
525
- }
526
- const content = message?.content;
527
- if (typeof content === "string") {
528
- const match = content.match(REASONING_TAG_RE);
529
- if (match) {
530
- const trimmed = match[2].trim();
531
- if (trimmed.length > 0) return trimmed;
532
- }
533
- }
534
- return null;
535
- }
536
- var REASONING_TAG_RE = /<(think|thinking|thought)>([\s\S]*?)<\/\1>/i;
537
- var REASONING_TAG_RE_G = /<(think|thinking|thought)>[\s\S]*?<\/\1>/gi;
538
- function stripReasoningTags(content) {
539
- if (typeof content !== "string" || !content.includes("<")) return content;
540
- return content.replace(REASONING_TAG_RE_G, "").trim();
541
- }
542
497
  async function processSingleMessage(task, ctx, providedMasterKey) {
543
498
  try {
544
499
  const masterKey = providedMasterKey || ctx.masterKey;
@@ -599,7 +554,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
599
554
  metadata
600
555
  });
601
556
  await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(reasoningPush));
602
- await new Promise((resolve) => setTimeout(resolve, 1500));
557
+ await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
603
558
  }
604
559
  for (let i = 0; i < messages.length; i++) {
605
560
  const contentPush = buildContentPush({
@@ -620,7 +575,7 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
620
575
  });
621
576
  await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(contentPush));
622
577
  if (i < messages.length - 1) {
623
- await new Promise((resolve) => setTimeout(resolve, 1500));
578
+ await new Promise((resolve) => setTimeout(resolve, SLEEP_BETWEEN_MESSAGES_MS));
624
579
  }
625
580
  }
626
581
  return { success: true, messagesSent: messages.length };
@@ -1654,11 +1609,7 @@ function createTenantContextManager(options) {
1654
1609
  }
1655
1610
 
1656
1611
  // src/server/index.js
1657
- function normalizeVapidSubject(email) {
1658
- const trimmedEmail = String(email || "").trim();
1659
- if (!trimmedEmail) return "";
1660
- return /^mailto:/i.test(trimmedEmail) ? trimmedEmail : `mailto:${trimmedEmail}`;
1661
- }
1612
+ import { normalizeVapidSubject } from "@rei-standard/amsg-shared";
1662
1613
  async function createReiServer(config) {
1663
1614
  if (!config) throw new Error("[rei-standard-amsg-server] config is required");
1664
1615
  if (!config.tenant) throw new Error("[rei-standard-amsg-server] tenant config is required");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-server",
3
- "version": "2.5.1",
3
+ "version": "2.5.3",
4
4
  "description": "ReiStandard Active Messaging server SDK with pluggable database adapters. Three-axis push schema (messageKind / messageType / messageSubtype) from @rei-standard/amsg-shared. Auto-emits ReasoningPush when the LLM response carries reasoning_content.",
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
  "web-push": "^3.6.7",
38
38
  "@netlify/blobs": "^8.1.0"
39
39
  },