@rei-standard/amsg-server 2.0.0-pre1 → 2.1.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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @rei-standard/amsg-server
2
2
 
3
- `@rei-standard/amsg-server` 是 ReiStandard 主动消息标准的服务端 SDK(v2.0.0-pre1),提供 Blob 租户配置、租户 token 鉴权和标准路由处理器。
3
+ `@rei-standard/amsg-server` 是 ReiStandard 主动消息标准的服务端 SDK(v2.0.1),提供 Blob 租户配置、租户 token 鉴权和标准路由处理器。
4
4
 
5
- ## v2.0.0-pre1 变更摘要
5
+ ## v2.0.1 变更摘要
6
6
 
7
7
  - 初始化流程合并为 `POST /api/v1/init-tenant`
8
8
  - 移除旧端点:`init-database`、`init-master-key`
@@ -50,6 +50,17 @@ const rei = await createReiServer({
50
50
  // GET /api/v1/messages -> rei.handlers.messages.GET
51
51
  ```
52
52
 
53
+ ## AI 接口 `apiUrl` 约束
54
+
55
+ 当 `messageType` 为 `prompted` / `auto`,或 `instant` 使用 AI 配置时:
56
+
57
+ - `apiUrl` 必须是完整聊天端点(例如:`https://api.openai.com/v1/chat/completions`)。
58
+ - SDK 会自动做最小规范化:去首尾空白、去路径尾部多余 `/`。
59
+ - SDK **不会**自动补全 `/v1`、`/chat/completions` 等路径。
60
+ - `maxTokens` 为可选字段:传了就映射为 `max_tokens`;不传则不指定(由上游模型默认策略决定)。
61
+
62
+ 如果上游返回 `405 Method Not Allowed`,通常表示 `apiUrl` 指向了基础域名而非聊天端点,请优先检查配置值。
63
+
53
64
  ## 一体化初始化流程
54
65
 
55
66
  1. 管理员配置环境变量(VAPID + tenant secrets)
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/server/adapters/factory.js
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/server/adapters/factory.js
2
2
  async function createAdapter(config) {
3
3
  if (!config || !config.driver) {
4
4
  throw new Error(
@@ -66,6 +66,9 @@ function validateScheduleMessagePayload(payload) {
66
66
  if (payload.recurrenceType && !["none", "daily", "weekly"].includes(payload.recurrenceType)) {
67
67
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } };
68
68
  }
69
+ if (payload.maxTokens !== void 0 && payload.maxTokens !== null && (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)) {
70
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } };
71
+ }
69
72
  if (payload.messageType === "fixed") {
70
73
  if (!payload.userMessage) {
71
74
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["userMessage (required for fixed type)"] } };
@@ -97,7 +100,7 @@ function validateScheduleMessagePayload(payload) {
97
100
  if (payload.uuid && !isValidUUID(payload.uuid)) {
98
101
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["uuid (invalid UUID format)"] } };
99
102
  }
100
- if (payload.messageSubtype && !["chat", "forum", "moment"].includes(payload.messageSubtype)) {
103
+ if (payload.messageSubtype !== void 0 && payload.messageSubtype !== null && typeof payload.messageSubtype !== "string") {
101
104
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["messageSubtype"] } };
102
105
  }
103
106
  return { valid: true };
@@ -505,25 +508,66 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
505
508
  }
506
509
  }
507
510
  async function _callAI(payload) {
508
- const aiResponse = await fetch(payload.apiUrl, {
511
+ const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
512
+ const requestBody = buildAiRequestBody(payload);
513
+ const aiResponse = await fetch(normalizedApiUrl, {
509
514
  method: "POST",
510
515
  headers: {
511
516
  "Content-Type": "application/json",
512
517
  "Authorization": `Bearer ${payload.apiKey}`
513
518
  },
514
- body: JSON.stringify({
515
- model: payload.primaryModel,
516
- messages: [{ role: "user", content: payload.completePrompt }],
517
- max_tokens: 500,
518
- temperature: 0.8
519
- }),
519
+ body: JSON.stringify(requestBody),
520
520
  signal: AbortSignal.timeout(3e5)
521
521
  });
522
522
  if (!aiResponse.ok) {
523
- throw new Error(`AI API error: ${aiResponse.status} ${aiResponse.statusText}`);
523
+ if (aiResponse.status === 405) {
524
+ throw new Error(
525
+ `AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
526
+ );
527
+ }
528
+ throw new Error(
529
+ `AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
530
+ );
524
531
  }
525
532
  const aiData = await aiResponse.json();
526
- return aiData.choices[0].message.content.trim();
533
+ const content = _optionalChain([aiData, 'optionalAccess', _ => _.choices, 'optionalAccess', _2 => _2[0], 'optionalAccess', _3 => _3.message, 'optionalAccess', _4 => _4.content]);
534
+ if (typeof content !== "string" || !content.trim()) {
535
+ throw new Error("AI API error: response missing choices[0].message.content");
536
+ }
537
+ return content.trim();
538
+ }
539
+ function buildAiRequestBody(payload) {
540
+ const requestBody = {
541
+ model: payload.primaryModel,
542
+ messages: [{ role: "user", content: payload.completePrompt }],
543
+ temperature: 0.8
544
+ };
545
+ if (payload.maxTokens === void 0 || payload.maxTokens === null) {
546
+ return requestBody;
547
+ }
548
+ if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
549
+ throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
550
+ }
551
+ requestBody.max_tokens = payload.maxTokens;
552
+ return requestBody;
553
+ }
554
+ function normalizeAiApiUrl(apiUrl) {
555
+ if (typeof apiUrl !== "string" || !apiUrl.trim()) {
556
+ throw new Error(
557
+ "Invalid apiUrl: apiUrl is required. Please provide a full chat endpoint URL (for example: https://api.openai.com/v1/chat/completions)."
558
+ );
559
+ }
560
+ const trimmedApiUrl = apiUrl.trim();
561
+ let parsedUrl;
562
+ try {
563
+ parsedUrl = new URL(trimmedApiUrl);
564
+ } catch (e3) {
565
+ throw new Error(
566
+ `Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL that points to a full chat endpoint.`
567
+ );
568
+ }
569
+ parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, "") || "/";
570
+ return parsedUrl.toString();
527
571
  }
528
572
 
529
573
  // src/server/handlers/schedule-message.js
@@ -591,6 +635,7 @@ function createScheduleMessageHandler(ctx) {
591
635
  apiKey: payload.apiKey || null,
592
636
  primaryModel: payload.primaryModel || null,
593
637
  completePrompt: payload.completePrompt || null,
638
+ maxTokens: _nullishCoalesce(payload.maxTokens, () => ( null)),
594
639
  pushSubscription: payload.pushSubscription,
595
640
  metadata: payload.metadata || {}
596
641
  };
@@ -899,6 +944,9 @@ function createUpdateMessageHandler(ctx) {
899
944
  if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
900
945
  return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
901
946
  }
947
+ if (Object.prototype.hasOwnProperty.call(updates, "maxTokens") && updates.maxTokens !== null && (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)) {
948
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } } } };
949
+ }
902
950
  const existingTask = await db.getTaskByUuid(taskUuid, userId);
903
951
  if (!existingTask) {
904
952
  const taskStatus = await db.getTaskStatus(taskUuid, userId);
@@ -914,7 +962,8 @@ function createUpdateMessageHandler(ctx) {
914
962
  ...updates.userMessage && { userMessage: updates.userMessage },
915
963
  ...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
916
964
  ...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
917
- ...updates.metadata && { metadata: updates.metadata }
965
+ ...updates.metadata && { metadata: updates.metadata },
966
+ ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: _nullishCoalesce(updates.maxTokens, () => ( null)) }
918
967
  };
919
968
  const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
920
969
  const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
@@ -1106,7 +1155,7 @@ async function resolveBlobStore(namespace) {
1106
1155
  }
1107
1156
  try {
1108
1157
  const blobModule = await Promise.resolve().then(() => _interopRequireWildcard(require("@netlify/blobs")));
1109
- const getStore = blobModule.getStore || _optionalChain([blobModule, 'access', _ => _.default, 'optionalAccess', _2 => _2.getStore]);
1158
+ const getStore = blobModule.getStore || _optionalChain([blobModule, 'access', _5 => _5.default, 'optionalAccess', _6 => _6.getStore]);
1110
1159
  if (typeof getStore === "function") {
1111
1160
  return getStore({ name: namespace });
1112
1161
  }
@@ -1186,15 +1235,21 @@ function verifyTenantToken(token, secret, options = {}) {
1186
1235
  const [encodedHeader, encodedPayload, receivedSignature] = parts;
1187
1236
  const signingInput = `${encodedHeader}.${encodedPayload}`;
1188
1237
  const expectedSignature = sign(signingInput, secret);
1189
- const receivedBuffer = Buffer.from(receivedSignature);
1190
- const expectedBuffer = Buffer.from(expectedSignature);
1238
+ let receivedBuffer;
1239
+ let expectedBuffer;
1240
+ try {
1241
+ receivedBuffer = base64UrlDecode2(receivedSignature);
1242
+ expectedBuffer = base64UrlDecode2(expectedSignature);
1243
+ } catch (e4) {
1244
+ throw new Error("INVALID_TENANT_AUTH");
1245
+ }
1191
1246
  if (receivedBuffer.length !== expectedBuffer.length || !_crypto.timingSafeEqual.call(void 0, receivedBuffer, expectedBuffer)) {
1192
1247
  throw new Error("INVALID_TENANT_AUTH");
1193
1248
  }
1194
1249
  let payload;
1195
1250
  try {
1196
1251
  payload = JSON.parse(base64UrlDecode2(encodedPayload).toString("utf8"));
1197
- } catch (e3) {
1252
+ } catch (e5) {
1198
1253
  throw new Error("INVALID_TENANT_AUTH");
1199
1254
  }
1200
1255
  if (!payload || typeof payload !== "object" || payload.v !== 1) {
@@ -1251,7 +1306,7 @@ function parseTokenFromUrl(url) {
1251
1306
  try {
1252
1307
  const parsed = new URL(url, "https://dummy.local");
1253
1308
  return String(parsed.searchParams.get("token") || "").trim();
1254
- } catch (e4) {
1309
+ } catch (e6) {
1255
1310
  return "";
1256
1311
  }
1257
1312
  }
package/dist/index.d.cts CHANGED
@@ -2,7 +2,7 @@ import { createDecipheriv, createHash, randomBytes, createCipheriv, randomUUID,
2
2
 
3
3
  /**
4
4
  * Adapter Factory
5
- * ReiStandard SDK v2.0.0-pre1
5
+ * ReiStandard SDK v2.0.1
6
6
  *
7
7
  * Creates a database adapter instance based on the supplied configuration.
8
8
  *
@@ -35,12 +35,12 @@ async function createAdapter(config) {
35
35
 
36
36
  switch (config.driver) {
37
37
  case 'neon': {
38
- const { NeonAdapter } = await import('./neon-Ch9Abtfp.d.cts');
38
+ const { NeonAdapter } = await import('./neon-CJl66EGy.d.cts');
39
39
  return new NeonAdapter(config.connectionString);
40
40
  }
41
41
 
42
42
  case 'pg': {
43
- const { PgAdapter } = await import('./pg-BpYu3O81.d.cts');
43
+ const { PgAdapter } = await import('./pg-07S-u_H4.d.cts');
44
44
  return new PgAdapter(config.connectionString);
45
45
  }
46
46
 
@@ -54,7 +54,7 @@ async function createAdapter(config) {
54
54
 
55
55
  /**
56
56
  * Validation utility library (SDK version)
57
- * ReiStandard SDK v2.0.0-pre1
57
+ * ReiStandard SDK v2.0.1
58
58
  */
59
59
 
60
60
  /**
@@ -132,6 +132,14 @@ function validateScheduleMessagePayload(payload) {
132
132
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['recurrenceType'] } };
133
133
  }
134
134
 
135
+ if (
136
+ payload.maxTokens !== undefined &&
137
+ payload.maxTokens !== null &&
138
+ (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)
139
+ ) {
140
+ return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['maxTokens'] } };
141
+ }
142
+
135
143
  if (payload.messageType === 'fixed') {
136
144
  if (!payload.userMessage) {
137
145
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { missingFields: ['userMessage (required for fixed type)'] } };
@@ -166,7 +174,14 @@ function validateScheduleMessagePayload(payload) {
166
174
  if (payload.uuid && !isValidUUID(payload.uuid)) {
167
175
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['uuid (invalid UUID format)'] } };
168
176
  }
169
- if (payload.messageSubtype && !['chat', 'forum', 'moment'].includes(payload.messageSubtype)) {
177
+ // messageSubtype is a free-form string tag forwarded to SW-side push payload
178
+ // for classification. Only the type is enforced; the taxonomy is the
179
+ // consumer's call (previously the enum was chat/forum/moment).
180
+ if (
181
+ payload.messageSubtype !== undefined &&
182
+ payload.messageSubtype !== null &&
183
+ typeof payload.messageSubtype !== 'string'
184
+ ) {
170
185
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['messageSubtype'] } };
171
186
  }
172
187
 
@@ -320,7 +335,7 @@ function getHeader(headers = {}, name) {
320
335
 
321
336
  /**
322
337
  * Handler: init-tenant
323
- * ReiStandard SDK v2.0.0-pre1
338
+ * ReiStandard SDK v2.0.1
324
339
  *
325
340
  * @param {Object} ctx - Server context.
326
341
  * @returns {{ POST: function }}
@@ -456,7 +471,7 @@ function createInitTenantHandler(ctx) {
456
471
 
457
472
  /**
458
473
  * Encryption utility library (SDK version)
459
- * ReiStandard SDK v2.0.0-pre1
474
+ * ReiStandard SDK v2.0.1
460
475
  *
461
476
  * Wraps AES-256-GCM operations for request/response and storage encryption.
462
477
  */
@@ -558,7 +573,7 @@ function decryptFromStorage(encryptedText, encryptionKey) {
558
573
 
559
574
  /**
560
575
  * Handler: get-user-key
561
- * ReiStandard SDK v2.0.0-pre1
576
+ * ReiStandard SDK v2.0.1
562
577
  *
563
578
  * @param {Object} ctx - Server context.
564
579
  * @returns {{ GET: function }}
@@ -627,7 +642,7 @@ function isUniqueViolation(error) {
627
642
 
628
643
  /**
629
644
  * Message Processor (SDK version)
630
- * ReiStandard SDK v2.0.0-pre1
645
+ * ReiStandard SDK v2.0.1
631
646
  *
632
647
  * Handles single message content generation and Web Push delivery.
633
648
  * Receives its dependencies (encryption helpers, webpush, VAPID config)
@@ -824,32 +839,110 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
824
839
  * @private
825
840
  */
826
841
  async function _callAI(payload) {
827
- const aiResponse = await fetch(payload.apiUrl, {
842
+ const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
843
+ const requestBody = buildAiRequestBody(payload);
844
+
845
+ const aiResponse = await fetch(normalizedApiUrl, {
828
846
  method: 'POST',
829
847
  headers: {
830
848
  'Content-Type': 'application/json',
831
849
  'Authorization': `Bearer ${payload.apiKey}`
832
850
  },
833
- body: JSON.stringify({
834
- model: payload.primaryModel,
835
- messages: [{ role: 'user', content: payload.completePrompt }],
836
- max_tokens: 500,
837
- temperature: 0.8
838
- }),
851
+ body: JSON.stringify(requestBody),
839
852
  signal: AbortSignal.timeout(300000)
840
853
  });
841
854
 
842
855
  if (!aiResponse.ok) {
843
- throw new Error(`AI API error: ${aiResponse.status} ${aiResponse.statusText}`);
856
+ if (aiResponse.status === 405) {
857
+ throw new Error(
858
+ `AI API error: 405 Method Not Allowed. ` +
859
+ `apiUrl must point to a full chat endpoint (for example: /chat/completions). ` +
860
+ `Received: ${normalizedApiUrl}`
861
+ );
862
+ }
863
+
864
+ throw new Error(
865
+ `AI API error: ${aiResponse.status} ${aiResponse.statusText || 'Unknown Error'}. ` +
866
+ `Request URL: ${normalizedApiUrl}`
867
+ );
844
868
  }
845
869
 
846
870
  const aiData = await aiResponse.json();
847
- return aiData.choices[0].message.content.trim();
871
+ const content = aiData?.choices?.[0]?.message?.content;
872
+ if (typeof content !== 'string' || !content.trim()) {
873
+ throw new Error('AI API error: response missing choices[0].message.content');
874
+ }
875
+
876
+ return content.trim();
877
+ }
878
+
879
+ /**
880
+ * Build OpenAI-compatible request body.
881
+ *
882
+ * `max_tokens` is optional:
883
+ * - include it only when payload.maxTokens is provided
884
+ * - omit it when payload.maxTokens is undefined / null
885
+ *
886
+ * @param {Object} payload
887
+ * @returns {Object}
888
+ */
889
+ function buildAiRequestBody(payload) {
890
+ const requestBody = {
891
+ model: payload.primaryModel,
892
+ messages: [{ role: 'user', content: payload.completePrompt }],
893
+ temperature: 0.8
894
+ };
895
+
896
+ if (payload.maxTokens === undefined || payload.maxTokens === null) {
897
+ return requestBody;
898
+ }
899
+
900
+ if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
901
+ throw new Error('Invalid maxTokens: maxTokens must be a positive integer when provided.');
902
+ }
903
+
904
+ requestBody.max_tokens = payload.maxTokens;
905
+ return requestBody;
906
+ }
907
+
908
+ /**
909
+ * Normalize AI API URL with minimal, non-breaking rules:
910
+ * - trim whitespace
911
+ * - remove redundant trailing slashes from pathname
912
+ * - keep query string untouched
913
+ *
914
+ * Note: This function does NOT auto-append /v1 or /chat/completions.
915
+ *
916
+ * @param {string} apiUrl
917
+ * @returns {string}
918
+ */
919
+ function normalizeAiApiUrl(apiUrl) {
920
+ if (typeof apiUrl !== 'string' || !apiUrl.trim()) {
921
+ throw new Error(
922
+ 'Invalid apiUrl: apiUrl is required. ' +
923
+ 'Please provide a full chat endpoint URL (for example: https://api.openai.com/v1/chat/completions).'
924
+ );
925
+ }
926
+
927
+ const trimmedApiUrl = apiUrl.trim();
928
+ let parsedUrl;
929
+
930
+ try {
931
+ parsedUrl = new URL(trimmedApiUrl);
932
+ } catch {
933
+ throw new Error(
934
+ `Invalid apiUrl: "${apiUrl}". ` +
935
+ 'Please provide a valid absolute URL that points to a full chat endpoint.'
936
+ );
937
+ }
938
+
939
+ parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, '') || '/';
940
+ return parsedUrl.toString();
848
941
  }
849
942
 
850
943
  /**
851
944
  * Handler: schedule-message
852
- * ReiStandard SDK v2.0.0-pre1
945
+ * ReiStandard SDK v2.0.1
853
946
  *
854
947
  * @param {Object} ctx - Server context.
855
948
  * @returns {{ POST: function }}
@@ -933,6 +1026,7 @@ function createScheduleMessageHandler(ctx) {
933
1026
  apiKey: payload.apiKey || null,
934
1027
  primaryModel: payload.primaryModel || null,
935
1028
  completePrompt: payload.completePrompt || null,
1029
+ maxTokens: payload.maxTokens ?? null,
936
1030
  pushSubscription: payload.pushSubscription,
937
1031
  metadata: payload.metadata || {}
938
1032
  };
@@ -1046,7 +1140,7 @@ function createScheduleMessageHandler(ctx) {
1046
1140
 
1047
1141
  /**
1048
1142
  * Handler: send-notifications
1049
- * ReiStandard SDK v2.0.0-pre1
1143
+ * ReiStandard SDK v2.0.1
1050
1144
  *
1051
1145
  * @param {Object} ctx - Server context.
1052
1146
  * @returns {{ POST: function }}
@@ -1236,7 +1330,7 @@ function createSendNotificationsHandler(ctx) {
1236
1330
 
1237
1331
  /**
1238
1332
  * Handler: update-message
1239
- * ReiStandard SDK v2.0.0-pre1
1333
+ * ReiStandard SDK v2.0.1
1240
1334
  *
1241
1335
  * @param {Object} ctx - Server context.
1242
1336
  * @returns {{ PUT: function }}
@@ -1306,6 +1400,14 @@ function createUpdateMessageHandler(ctx) {
1306
1400
  return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['recurrenceType'] } } } };
1307
1401
  }
1308
1402
 
1403
+ if (
1404
+ Object.prototype.hasOwnProperty.call(updates, 'maxTokens') &&
1405
+ updates.maxTokens !== null &&
1406
+ (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)
1407
+ ) {
1408
+ return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['maxTokens'] } } } };
1409
+ }
1410
+
1309
1411
  // Fetch existing task
1310
1412
  const existingTask = await db.getTaskByUuid(taskUuid, userId);
1311
1413
 
@@ -1325,7 +1427,8 @@ function createUpdateMessageHandler(ctx) {
1325
1427
  ...(updates.userMessage && { userMessage: updates.userMessage }),
1326
1428
  ...(updates.recurrenceType && { recurrenceType: updates.recurrenceType }),
1327
1429
  ...(updates.avatarUrl && { avatarUrl: updates.avatarUrl }),
1328
- ...(updates.metadata && { metadata: updates.metadata })
1430
+ ...(updates.metadata && { metadata: updates.metadata }),
1431
+ ...(Object.prototype.hasOwnProperty.call(updates, 'maxTokens') && { maxTokens: updates.maxTokens ?? null })
1329
1432
  };
1330
1433
 
1331
1434
  const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
@@ -1355,7 +1458,7 @@ function createUpdateMessageHandler(ctx) {
1355
1458
 
1356
1459
  /**
1357
1460
  * Handler: cancel-message
1358
- * ReiStandard SDK v2.0.0-pre1
1461
+ * ReiStandard SDK v2.0.1
1359
1462
  *
1360
1463
  * @param {Object} ctx - Server context.
1361
1464
  * @returns {{ DELETE: function }}
@@ -1408,7 +1511,7 @@ function createCancelMessageHandler(ctx) {
1408
1511
 
1409
1512
  /**
1410
1513
  * Handler: messages
1411
- * ReiStandard SDK v2.0.0-pre1
1514
+ * ReiStandard SDK v2.0.1
1412
1515
  *
1413
1516
  * @param {Object} ctx - Server context.
1414
1517
  * @returns {{ GET: function }}
@@ -1696,8 +1799,17 @@ function verifyTenantToken(token, secret, options = {}) {
1696
1799
  const signingInput = `${encodedHeader}.${encodedPayload}`;
1697
1800
  const expectedSignature = sign(signingInput, secret);
1698
1801
 
1699
- const receivedBuffer = Buffer.from(receivedSignature);
1700
- const expectedBuffer = Buffer.from(expectedSignature);
1802
+ // Compare raw HMAC bytes (decoded from base64url) rather than the
1803
+ // encoded character bytes — semantically clearer and avoids the
1804
+ // implicit-UTF-8 default of Buffer.from(string).
1805
+ let receivedBuffer;
1806
+ let expectedBuffer;
1807
+ try {
1808
+ receivedBuffer = base64UrlDecode(receivedSignature);
1809
+ expectedBuffer = base64UrlDecode(expectedSignature);
1810
+ } catch {
1811
+ throw new Error('INVALID_TENANT_AUTH');
1812
+ }
1701
1813
  if (
1702
1814
  receivedBuffer.length !== expectedBuffer.length ||
1703
1815
  !timingSafeEqual(receivedBuffer, expectedBuffer)
@@ -1911,7 +2023,7 @@ function createTenantContextManager(options) {
1911
2023
 
1912
2024
  /**
1913
2025
  * ReiStandard Server SDK Entry Point
1914
- * v2.0.0-pre1
2026
+ * v2.0.1
1915
2027
  *
1916
2028
  * Usage:
1917
2029
  * import { createReiServer } from '@rei-standard/amsg-server';
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import { createDecipheriv, createHash, randomBytes, createCipheriv, randomUUID,
2
2
 
3
3
  /**
4
4
  * Adapter Factory
5
- * ReiStandard SDK v2.0.0-pre1
5
+ * ReiStandard SDK v2.0.1
6
6
  *
7
7
  * Creates a database adapter instance based on the supplied configuration.
8
8
  *
@@ -35,12 +35,12 @@ async function createAdapter(config) {
35
35
 
36
36
  switch (config.driver) {
37
37
  case 'neon': {
38
- const { NeonAdapter } = await import('./neon-DyBfYFkz.d.ts');
38
+ const { NeonAdapter } = await import('./neon-DYvGnCzx.d.ts');
39
39
  return new NeonAdapter(config.connectionString);
40
40
  }
41
41
 
42
42
  case 'pg': {
43
- const { PgAdapter } = await import('./pg-DJWS_HZ3.d.ts');
43
+ const { PgAdapter } = await import('./pg-Du-pN_UT.d.ts');
44
44
  return new PgAdapter(config.connectionString);
45
45
  }
46
46
 
@@ -54,7 +54,7 @@ async function createAdapter(config) {
54
54
 
55
55
  /**
56
56
  * Validation utility library (SDK version)
57
- * ReiStandard SDK v2.0.0-pre1
57
+ * ReiStandard SDK v2.0.1
58
58
  */
59
59
 
60
60
  /**
@@ -132,6 +132,14 @@ function validateScheduleMessagePayload(payload) {
132
132
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['recurrenceType'] } };
133
133
  }
134
134
 
135
+ if (
136
+ payload.maxTokens !== undefined &&
137
+ payload.maxTokens !== null &&
138
+ (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)
139
+ ) {
140
+ return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['maxTokens'] } };
141
+ }
142
+
135
143
  if (payload.messageType === 'fixed') {
136
144
  if (!payload.userMessage) {
137
145
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { missingFields: ['userMessage (required for fixed type)'] } };
@@ -166,7 +174,14 @@ function validateScheduleMessagePayload(payload) {
166
174
  if (payload.uuid && !isValidUUID(payload.uuid)) {
167
175
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['uuid (invalid UUID format)'] } };
168
176
  }
169
- if (payload.messageSubtype && !['chat', 'forum', 'moment'].includes(payload.messageSubtype)) {
177
+ // messageSubtype is a free-form string tag forwarded to SW-side push payload
178
+ // for classification. Only the type is enforced; the taxonomy is the
179
+ // consumer's call (previously the enum was chat/forum/moment).
180
+ if (
181
+ payload.messageSubtype !== undefined &&
182
+ payload.messageSubtype !== null &&
183
+ typeof payload.messageSubtype !== 'string'
184
+ ) {
170
185
  return { valid: false, errorCode: 'INVALID_PARAMETERS', errorMessage: '缺少必需参数或参数格式错误', details: { invalidFields: ['messageSubtype'] } };
171
186
  }
172
187
 
@@ -320,7 +335,7 @@ function getHeader(headers = {}, name) {
320
335
 
321
336
  /**
322
337
  * Handler: init-tenant
323
- * ReiStandard SDK v2.0.0-pre1
338
+ * ReiStandard SDK v2.0.1
324
339
  *
325
340
  * @param {Object} ctx - Server context.
326
341
  * @returns {{ POST: function }}
@@ -456,7 +471,7 @@ function createInitTenantHandler(ctx) {
456
471
 
457
472
  /**
458
473
  * Encryption utility library (SDK version)
459
- * ReiStandard SDK v2.0.0-pre1
474
+ * ReiStandard SDK v2.0.1
460
475
  *
461
476
  * Wraps AES-256-GCM operations for request/response and storage encryption.
462
477
  */
@@ -558,7 +573,7 @@ function decryptFromStorage(encryptedText, encryptionKey) {
558
573
 
559
574
  /**
560
575
  * Handler: get-user-key
561
- * ReiStandard SDK v2.0.0-pre1
576
+ * ReiStandard SDK v2.0.1
562
577
  *
563
578
  * @param {Object} ctx - Server context.
564
579
  * @returns {{ GET: function }}
@@ -627,7 +642,7 @@ function isUniqueViolation(error) {
627
642
 
628
643
  /**
629
644
  * Message Processor (SDK version)
630
- * ReiStandard SDK v2.0.0-pre1
645
+ * ReiStandard SDK v2.0.1
631
646
  *
632
647
  * Handles single message content generation and Web Push delivery.
633
648
  * Receives its dependencies (encryption helpers, webpush, VAPID config)
@@ -824,32 +839,110 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
824
839
  * @private
825
840
  */
826
841
  async function _callAI(payload) {
827
- const aiResponse = await fetch(payload.apiUrl, {
842
+ const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
843
+ const requestBody = buildAiRequestBody(payload);
844
+
845
+ const aiResponse = await fetch(normalizedApiUrl, {
828
846
  method: 'POST',
829
847
  headers: {
830
848
  'Content-Type': 'application/json',
831
849
  'Authorization': `Bearer ${payload.apiKey}`
832
850
  },
833
- body: JSON.stringify({
834
- model: payload.primaryModel,
835
- messages: [{ role: 'user', content: payload.completePrompt }],
836
- max_tokens: 500,
837
- temperature: 0.8
838
- }),
851
+ body: JSON.stringify(requestBody),
839
852
  signal: AbortSignal.timeout(300000)
840
853
  });
841
854
 
842
855
  if (!aiResponse.ok) {
843
- throw new Error(`AI API error: ${aiResponse.status} ${aiResponse.statusText}`);
856
+ if (aiResponse.status === 405) {
857
+ throw new Error(
858
+ `AI API error: 405 Method Not Allowed. ` +
859
+ `apiUrl must point to a full chat endpoint (for example: /chat/completions). ` +
860
+ `Received: ${normalizedApiUrl}`
861
+ );
862
+ }
863
+
864
+ throw new Error(
865
+ `AI API error: ${aiResponse.status} ${aiResponse.statusText || 'Unknown Error'}. ` +
866
+ `Request URL: ${normalizedApiUrl}`
867
+ );
844
868
  }
845
869
 
846
870
  const aiData = await aiResponse.json();
847
- return aiData.choices[0].message.content.trim();
871
+ const content = aiData?.choices?.[0]?.message?.content;
872
+ if (typeof content !== 'string' || !content.trim()) {
873
+ throw new Error('AI API error: response missing choices[0].message.content');
874
+ }
875
+
876
+ return content.trim();
877
+ }
878
+
879
+ /**
880
+ * Build OpenAI-compatible request body.
881
+ *
882
+ * `max_tokens` is optional:
883
+ * - include it only when payload.maxTokens is provided
884
+ * - omit it when payload.maxTokens is undefined / null
885
+ *
886
+ * @param {Object} payload
887
+ * @returns {Object}
888
+ */
889
+ function buildAiRequestBody(payload) {
890
+ const requestBody = {
891
+ model: payload.primaryModel,
892
+ messages: [{ role: 'user', content: payload.completePrompt }],
893
+ temperature: 0.8
894
+ };
895
+
896
+ if (payload.maxTokens === undefined || payload.maxTokens === null) {
897
+ return requestBody;
898
+ }
899
+
900
+ if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
901
+ throw new Error('Invalid maxTokens: maxTokens must be a positive integer when provided.');
902
+ }
903
+
904
+ requestBody.max_tokens = payload.maxTokens;
905
+ return requestBody;
906
+ }
907
+
908
+ /**
909
+ * Normalize AI API URL with minimal, non-breaking rules:
910
+ * - trim whitespace
911
+ * - remove redundant trailing slashes from pathname
912
+ * - keep query string untouched
913
+ *
914
+ * Note: This function does NOT auto-append /v1 or /chat/completions.
915
+ *
916
+ * @param {string} apiUrl
917
+ * @returns {string}
918
+ */
919
+ function normalizeAiApiUrl(apiUrl) {
920
+ if (typeof apiUrl !== 'string' || !apiUrl.trim()) {
921
+ throw new Error(
922
+ 'Invalid apiUrl: apiUrl is required. ' +
923
+ 'Please provide a full chat endpoint URL (for example: https://api.openai.com/v1/chat/completions).'
924
+ );
925
+ }
926
+
927
+ const trimmedApiUrl = apiUrl.trim();
928
+ let parsedUrl;
929
+
930
+ try {
931
+ parsedUrl = new URL(trimmedApiUrl);
932
+ } catch {
933
+ throw new Error(
934
+ `Invalid apiUrl: "${apiUrl}". ` +
935
+ 'Please provide a valid absolute URL that points to a full chat endpoint.'
936
+ );
937
+ }
938
+
939
+ parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, '') || '/';
940
+ return parsedUrl.toString();
848
941
  }
849
942
 
850
943
  /**
851
944
  * Handler: schedule-message
852
- * ReiStandard SDK v2.0.0-pre1
945
+ * ReiStandard SDK v2.0.1
853
946
  *
854
947
  * @param {Object} ctx - Server context.
855
948
  * @returns {{ POST: function }}
@@ -933,6 +1026,7 @@ function createScheduleMessageHandler(ctx) {
933
1026
  apiKey: payload.apiKey || null,
934
1027
  primaryModel: payload.primaryModel || null,
935
1028
  completePrompt: payload.completePrompt || null,
1029
+ maxTokens: payload.maxTokens ?? null,
936
1030
  pushSubscription: payload.pushSubscription,
937
1031
  metadata: payload.metadata || {}
938
1032
  };
@@ -1046,7 +1140,7 @@ function createScheduleMessageHandler(ctx) {
1046
1140
 
1047
1141
  /**
1048
1142
  * Handler: send-notifications
1049
- * ReiStandard SDK v2.0.0-pre1
1143
+ * ReiStandard SDK v2.0.1
1050
1144
  *
1051
1145
  * @param {Object} ctx - Server context.
1052
1146
  * @returns {{ POST: function }}
@@ -1236,7 +1330,7 @@ function createSendNotificationsHandler(ctx) {
1236
1330
 
1237
1331
  /**
1238
1332
  * Handler: update-message
1239
- * ReiStandard SDK v2.0.0-pre1
1333
+ * ReiStandard SDK v2.0.1
1240
1334
  *
1241
1335
  * @param {Object} ctx - Server context.
1242
1336
  * @returns {{ PUT: function }}
@@ -1306,6 +1400,14 @@ function createUpdateMessageHandler(ctx) {
1306
1400
  return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['recurrenceType'] } } } };
1307
1401
  }
1308
1402
 
1403
+ if (
1404
+ Object.prototype.hasOwnProperty.call(updates, 'maxTokens') &&
1405
+ updates.maxTokens !== null &&
1406
+ (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)
1407
+ ) {
1408
+ return { status: 400, body: { success: false, error: { code: 'INVALID_UPDATE_DATA', message: '更新数据格式错误', details: { invalidFields: ['maxTokens'] } } } };
1409
+ }
1410
+
1309
1411
  // Fetch existing task
1310
1412
  const existingTask = await db.getTaskByUuid(taskUuid, userId);
1311
1413
 
@@ -1325,7 +1427,8 @@ function createUpdateMessageHandler(ctx) {
1325
1427
  ...(updates.userMessage && { userMessage: updates.userMessage }),
1326
1428
  ...(updates.recurrenceType && { recurrenceType: updates.recurrenceType }),
1327
1429
  ...(updates.avatarUrl && { avatarUrl: updates.avatarUrl }),
1328
- ...(updates.metadata && { metadata: updates.metadata })
1430
+ ...(updates.metadata && { metadata: updates.metadata }),
1431
+ ...(Object.prototype.hasOwnProperty.call(updates, 'maxTokens') && { maxTokens: updates.maxTokens ?? null })
1329
1432
  };
1330
1433
 
1331
1434
  const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
@@ -1355,7 +1458,7 @@ function createUpdateMessageHandler(ctx) {
1355
1458
 
1356
1459
  /**
1357
1460
  * Handler: cancel-message
1358
- * ReiStandard SDK v2.0.0-pre1
1461
+ * ReiStandard SDK v2.0.1
1359
1462
  *
1360
1463
  * @param {Object} ctx - Server context.
1361
1464
  * @returns {{ DELETE: function }}
@@ -1408,7 +1511,7 @@ function createCancelMessageHandler(ctx) {
1408
1511
 
1409
1512
  /**
1410
1513
  * Handler: messages
1411
- * ReiStandard SDK v2.0.0-pre1
1514
+ * ReiStandard SDK v2.0.1
1412
1515
  *
1413
1516
  * @param {Object} ctx - Server context.
1414
1517
  * @returns {{ GET: function }}
@@ -1696,8 +1799,17 @@ function verifyTenantToken(token, secret, options = {}) {
1696
1799
  const signingInput = `${encodedHeader}.${encodedPayload}`;
1697
1800
  const expectedSignature = sign(signingInput, secret);
1698
1801
 
1699
- const receivedBuffer = Buffer.from(receivedSignature);
1700
- const expectedBuffer = Buffer.from(expectedSignature);
1802
+ // Compare raw HMAC bytes (decoded from base64url) rather than the
1803
+ // encoded character bytes — semantically clearer and avoids the
1804
+ // implicit-UTF-8 default of Buffer.from(string).
1805
+ let receivedBuffer;
1806
+ let expectedBuffer;
1807
+ try {
1808
+ receivedBuffer = base64UrlDecode(receivedSignature);
1809
+ expectedBuffer = base64UrlDecode(expectedSignature);
1810
+ } catch {
1811
+ throw new Error('INVALID_TENANT_AUTH');
1812
+ }
1701
1813
  if (
1702
1814
  receivedBuffer.length !== expectedBuffer.length ||
1703
1815
  !timingSafeEqual(receivedBuffer, expectedBuffer)
@@ -1911,7 +2023,7 @@ function createTenantContextManager(options) {
1911
2023
 
1912
2024
  /**
1913
2025
  * ReiStandard Server SDK Entry Point
1914
- * v2.0.0-pre1
2026
+ * v2.0.1
1915
2027
  *
1916
2028
  * Usage:
1917
2029
  * import { createReiServer } from '@rei-standard/amsg-server';
package/dist/index.mjs CHANGED
@@ -66,6 +66,9 @@ function validateScheduleMessagePayload(payload) {
66
66
  if (payload.recurrenceType && !["none", "daily", "weekly"].includes(payload.recurrenceType)) {
67
67
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } };
68
68
  }
69
+ if (payload.maxTokens !== void 0 && payload.maxTokens !== null && (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0)) {
70
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } };
71
+ }
69
72
  if (payload.messageType === "fixed") {
70
73
  if (!payload.userMessage) {
71
74
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["userMessage (required for fixed type)"] } };
@@ -97,7 +100,7 @@ function validateScheduleMessagePayload(payload) {
97
100
  if (payload.uuid && !isValidUUID(payload.uuid)) {
98
101
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["uuid (invalid UUID format)"] } };
99
102
  }
100
- if (payload.messageSubtype && !["chat", "forum", "moment"].includes(payload.messageSubtype)) {
103
+ if (payload.messageSubtype !== void 0 && payload.messageSubtype !== null && typeof payload.messageSubtype !== "string") {
101
104
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["messageSubtype"] } };
102
105
  }
103
106
  return { valid: true };
@@ -505,25 +508,66 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
505
508
  }
506
509
  }
507
510
  async function _callAI(payload) {
508
- const aiResponse = await fetch(payload.apiUrl, {
511
+ const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
512
+ const requestBody = buildAiRequestBody(payload);
513
+ const aiResponse = await fetch(normalizedApiUrl, {
509
514
  method: "POST",
510
515
  headers: {
511
516
  "Content-Type": "application/json",
512
517
  "Authorization": `Bearer ${payload.apiKey}`
513
518
  },
514
- body: JSON.stringify({
515
- model: payload.primaryModel,
516
- messages: [{ role: "user", content: payload.completePrompt }],
517
- max_tokens: 500,
518
- temperature: 0.8
519
- }),
519
+ body: JSON.stringify(requestBody),
520
520
  signal: AbortSignal.timeout(3e5)
521
521
  });
522
522
  if (!aiResponse.ok) {
523
- throw new Error(`AI API error: ${aiResponse.status} ${aiResponse.statusText}`);
523
+ if (aiResponse.status === 405) {
524
+ throw new Error(
525
+ `AI API error: 405 Method Not Allowed. apiUrl must point to a full chat endpoint (for example: /chat/completions). Received: ${normalizedApiUrl}`
526
+ );
527
+ }
528
+ throw new Error(
529
+ `AI API error: ${aiResponse.status} ${aiResponse.statusText || "Unknown Error"}. Request URL: ${normalizedApiUrl}`
530
+ );
524
531
  }
525
532
  const aiData = await aiResponse.json();
526
- return aiData.choices[0].message.content.trim();
533
+ const content = aiData?.choices?.[0]?.message?.content;
534
+ if (typeof content !== "string" || !content.trim()) {
535
+ throw new Error("AI API error: response missing choices[0].message.content");
536
+ }
537
+ return content.trim();
538
+ }
539
+ function buildAiRequestBody(payload) {
540
+ const requestBody = {
541
+ model: payload.primaryModel,
542
+ messages: [{ role: "user", content: payload.completePrompt }],
543
+ temperature: 0.8
544
+ };
545
+ if (payload.maxTokens === void 0 || payload.maxTokens === null) {
546
+ return requestBody;
547
+ }
548
+ if (!Number.isInteger(payload.maxTokens) || payload.maxTokens <= 0) {
549
+ throw new Error("Invalid maxTokens: maxTokens must be a positive integer when provided.");
550
+ }
551
+ requestBody.max_tokens = payload.maxTokens;
552
+ return requestBody;
553
+ }
554
+ function normalizeAiApiUrl(apiUrl) {
555
+ if (typeof apiUrl !== "string" || !apiUrl.trim()) {
556
+ throw new Error(
557
+ "Invalid apiUrl: apiUrl is required. Please provide a full chat endpoint URL (for example: https://api.openai.com/v1/chat/completions)."
558
+ );
559
+ }
560
+ const trimmedApiUrl = apiUrl.trim();
561
+ let parsedUrl;
562
+ try {
563
+ parsedUrl = new URL(trimmedApiUrl);
564
+ } catch {
565
+ throw new Error(
566
+ `Invalid apiUrl: "${apiUrl}". Please provide a valid absolute URL that points to a full chat endpoint.`
567
+ );
568
+ }
569
+ parsedUrl.pathname = parsedUrl.pathname.replace(/\/+$/, "") || "/";
570
+ return parsedUrl.toString();
527
571
  }
528
572
 
529
573
  // src/server/handlers/schedule-message.js
@@ -591,6 +635,7 @@ function createScheduleMessageHandler(ctx) {
591
635
  apiKey: payload.apiKey || null,
592
636
  primaryModel: payload.primaryModel || null,
593
637
  completePrompt: payload.completePrompt || null,
638
+ maxTokens: payload.maxTokens ?? null,
594
639
  pushSubscription: payload.pushSubscription,
595
640
  metadata: payload.metadata || {}
596
641
  };
@@ -899,6 +944,9 @@ function createUpdateMessageHandler(ctx) {
899
944
  if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
900
945
  return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
901
946
  }
947
+ if (Object.prototype.hasOwnProperty.call(updates, "maxTokens") && updates.maxTokens !== null && (!Number.isInteger(updates.maxTokens) || updates.maxTokens <= 0)) {
948
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["maxTokens"] } } } };
949
+ }
902
950
  const existingTask = await db.getTaskByUuid(taskUuid, userId);
903
951
  if (!existingTask) {
904
952
  const taskStatus = await db.getTaskStatus(taskUuid, userId);
@@ -914,7 +962,8 @@ function createUpdateMessageHandler(ctx) {
914
962
  ...updates.userMessage && { userMessage: updates.userMessage },
915
963
  ...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
916
964
  ...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
917
- ...updates.metadata && { metadata: updates.metadata }
965
+ ...updates.metadata && { metadata: updates.metadata },
966
+ ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: updates.maxTokens ?? null }
918
967
  };
919
968
  const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
920
969
  const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
@@ -1186,8 +1235,14 @@ function verifyTenantToken(token, secret, options = {}) {
1186
1235
  const [encodedHeader, encodedPayload, receivedSignature] = parts;
1187
1236
  const signingInput = `${encodedHeader}.${encodedPayload}`;
1188
1237
  const expectedSignature = sign(signingInput, secret);
1189
- const receivedBuffer = Buffer.from(receivedSignature);
1190
- const expectedBuffer = Buffer.from(expectedSignature);
1238
+ let receivedBuffer;
1239
+ let expectedBuffer;
1240
+ try {
1241
+ receivedBuffer = base64UrlDecode2(receivedSignature);
1242
+ expectedBuffer = base64UrlDecode2(expectedSignature);
1243
+ } catch {
1244
+ throw new Error("INVALID_TENANT_AUTH");
1245
+ }
1191
1246
  if (receivedBuffer.length !== expectedBuffer.length || !timingSafeEqual(receivedBuffer, expectedBuffer)) {
1192
1247
  throw new Error("INVALID_TENANT_AUTH");
1193
1248
  }
@@ -1,9 +1,9 @@
1
1
  import { neon } from '@neondatabase/serverless';
2
- import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-D-ldTuIC.js';
2
+ import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-C8OnYk6j.cjs';
3
3
 
4
4
  /**
5
5
  * Neon Serverless Database Adapter
6
- * ReiStandard SDK v2.0.0-pre1
6
+ * ReiStandard SDK v2.0.1
7
7
  *
8
8
  * @implements {import('./interface.js').DbAdapter}
9
9
  */
@@ -1,9 +1,9 @@
1
1
  import { neon } from '@neondatabase/serverless';
2
- import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-D-ldTuIC.cjs';
2
+ import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-C8OnYk6j.js';
3
3
 
4
4
  /**
5
5
  * Neon Serverless Database Adapter
6
- * ReiStandard SDK v2.0.0-pre1
6
+ * ReiStandard SDK v2.0.1
7
7
  *
8
8
  * @implements {import('./interface.js').DbAdapter}
9
9
  */
@@ -1,9 +1,9 @@
1
1
  import { Pool } from 'pg';
2
- import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-D-ldTuIC.js';
2
+ import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-C8OnYk6j.cjs';
3
3
 
4
4
  /**
5
5
  * PostgreSQL (pg) Database Adapter
6
- * ReiStandard SDK v2.0.0-pre1
6
+ * ReiStandard SDK v2.0.1
7
7
  *
8
8
  * Uses the standard 'pg' npm package.
9
9
  *
@@ -1,9 +1,9 @@
1
1
  import { Pool } from 'pg';
2
- import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-D-ldTuIC.cjs';
2
+ import { T as TABLE_SQL, I as INDEXES, V as VERIFY_TABLE_SQL, C as COLUMNS_SQL } from './schema-C8OnYk6j.js';
3
3
 
4
4
  /**
5
5
  * PostgreSQL (pg) Database Adapter
6
- * ReiStandard SDK v2.0.0-pre1
6
+ * ReiStandard SDK v2.0.1
7
7
  *
8
8
  * Uses the standard 'pg' npm package.
9
9
  *
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Shared SQL schema constants
3
- * ReiStandard SDK v2.0.0-pre1
3
+ * ReiStandard SDK v2.0.1
4
4
  */
5
5
 
6
6
  const TABLE_SQL = `
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Shared SQL schema constants
3
- * ReiStandard SDK v2.0.0-pre1
3
+ * ReiStandard SDK v2.0.1
4
4
  */
5
5
 
6
6
  const TABLE_SQL = `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rei-standard/amsg-server",
3
- "version": "2.0.0-pre1",
3
+ "version": "2.1.0",
4
4
  "description": "ReiStandard Active Messaging server SDK with pluggable database adapters",
5
5
  "repository": {
6
6
  "type": "git",