@rei-standard/amsg-server 2.6.0-next.0 → 2.6.0-next.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -162,53 +162,127 @@ function validateScheduleMessagePayload(payload) {
162
162
  return { valid: true };
163
163
  }
164
164
 
165
+ // src/server/lib/webcrypto-utils.js
166
+ import { toUint8, concatBytes, base64UrlToBytes } from "@rei-standard/amsg-shared";
167
+ var TEXT_ENCODER = new TextEncoder();
168
+ var TEXT_DECODER = new TextDecoder("utf-8", { fatal: false });
169
+ function utf8(str) {
170
+ return TEXT_ENCODER.encode(String(str));
171
+ }
172
+ function utf8Decode(buf) {
173
+ return TEXT_DECODER.decode(toUint8(buf));
174
+ }
175
+ function bytesToBase64(buf) {
176
+ const bytes = toUint8(buf);
177
+ let bin = "";
178
+ for (let i = 0; i < bytes.length; i++) {
179
+ bin += String.fromCharCode(bytes[i]);
180
+ }
181
+ return typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
182
+ }
183
+ function bytesToBase64Url(buf) {
184
+ return bytesToBase64(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
185
+ }
186
+ function bytesToHex(buf) {
187
+ const bytes = toUint8(buf);
188
+ let out = "";
189
+ for (let i = 0; i < bytes.length; i++) {
190
+ out += bytes[i].toString(16).padStart(2, "0");
191
+ }
192
+ return out;
193
+ }
194
+ function hexToBytes(hex) {
195
+ const out = new Uint8Array(hex.length / 2);
196
+ for (let i = 0; i < out.length; i++) {
197
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
198
+ }
199
+ return out;
200
+ }
201
+ function jsonToBase64Url(value) {
202
+ return bytesToBase64Url(utf8(JSON.stringify(value)));
203
+ }
204
+ function randomUUID() {
205
+ return globalThis.crypto.randomUUID();
206
+ }
207
+ function randomBytes(n) {
208
+ const out = new Uint8Array(n);
209
+ globalThis.crypto.getRandomValues(out);
210
+ return out;
211
+ }
212
+
165
213
  // src/server/lib/encryption.js
166
- import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
167
- function deriveUserEncryptionKey(userId, masterKey) {
168
- return createHash("sha256").update(masterKey + userId).digest("hex").slice(0, 64);
214
+ var base64ToBytes = base64UrlToBytes;
215
+ var TAG_LENGTH_BYTES = 16;
216
+ function importAesKey(hexKey, usage) {
217
+ return globalThis.crypto.subtle.importKey(
218
+ "raw",
219
+ hexToBytes(hexKey),
220
+ { name: "AES-GCM" },
221
+ false,
222
+ [usage]
223
+ );
224
+ }
225
+ async function aesGcmSeal(hexKey, iv, plaintextBytes) {
226
+ const key = await importAesKey(hexKey, "encrypt");
227
+ const sealed = new Uint8Array(await globalThis.crypto.subtle.encrypt(
228
+ { name: "AES-GCM", iv, tagLength: TAG_LENGTH_BYTES * 8 },
229
+ key,
230
+ plaintextBytes
231
+ ));
232
+ return {
233
+ ciphertext: sealed.slice(0, sealed.length - TAG_LENGTH_BYTES),
234
+ authTag: sealed.slice(sealed.length - TAG_LENGTH_BYTES)
235
+ };
236
+ }
237
+ async function aesGcmOpen(hexKey, iv, ciphertext, authTag) {
238
+ const key = await importAesKey(hexKey, "decrypt");
239
+ const tagBits = authTag.length * 8;
240
+ const tagLength = tagBits >= 96 && tagBits <= 128 ? tagBits : 128;
241
+ const plain = await globalThis.crypto.subtle.decrypt(
242
+ { name: "AES-GCM", iv, tagLength },
243
+ key,
244
+ concatBytes(ciphertext, authTag)
245
+ );
246
+ return new Uint8Array(plain);
169
247
  }
170
- function decryptPayload(encryptedPayload, encryptionKey) {
248
+ async function deriveUserEncryptionKey(userId, masterKey) {
249
+ const digest = await globalThis.crypto.subtle.digest("SHA-256", utf8(masterKey + userId));
250
+ return bytesToHex(new Uint8Array(digest)).slice(0, 64);
251
+ }
252
+ async function decryptPayload(encryptedPayload, encryptionKey) {
171
253
  const { iv, authTag, encryptedData } = encryptedPayload;
172
- const decipher = createDecipheriv(
173
- "aes-256-gcm",
174
- Buffer.from(encryptionKey, "hex"),
175
- Buffer.from(iv, "base64")
254
+ const plain = await aesGcmOpen(
255
+ encryptionKey,
256
+ base64ToBytes(iv),
257
+ base64ToBytes(encryptedData),
258
+ base64ToBytes(authTag)
176
259
  );
177
- decipher.setAuthTag(Buffer.from(authTag, "base64"));
178
- const decrypted = Buffer.concat([
179
- decipher.update(Buffer.from(encryptedData, "base64")),
180
- decipher.final()
181
- ]);
182
- return JSON.parse(decrypted.toString("utf8"));
260
+ return JSON.parse(utf8Decode(plain));
183
261
  }
184
- function encryptPayload(payload, encryptionKey) {
262
+ async function encryptPayload(payload, encryptionKey) {
185
263
  const plaintext = typeof payload === "string" ? payload : JSON.stringify(payload);
186
264
  const iv = randomBytes(12);
187
- const cipher = createCipheriv("aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
188
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
189
- const authTag = cipher.getAuthTag();
265
+ const { ciphertext, authTag } = await aesGcmSeal(encryptionKey, iv, utf8(plaintext));
190
266
  return {
191
- iv: iv.toString("base64"),
192
- authTag: authTag.toString("base64"),
193
- encryptedData: encrypted.toString("base64")
267
+ iv: bytesToBase64(iv),
268
+ authTag: bytesToBase64(authTag),
269
+ encryptedData: bytesToBase64(ciphertext)
194
270
  };
195
271
  }
196
- function encryptForStorage(text, encryptionKey) {
272
+ async function encryptForStorage(text, encryptionKey) {
197
273
  const iv = randomBytes(16);
198
- const cipher = createCipheriv("aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
199
- const encrypted = cipher.update(text, "utf8", "hex") + cipher.final("hex");
200
- const authTag = cipher.getAuthTag();
201
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
274
+ const { ciphertext, authTag } = await aesGcmSeal(encryptionKey, iv, utf8(text));
275
+ return `${bytesToHex(iv)}:${bytesToHex(authTag)}:${bytesToHex(ciphertext)}`;
202
276
  }
203
- function decryptFromStorage(encryptedText, encryptionKey) {
277
+ async function decryptFromStorage(encryptedText, encryptionKey) {
204
278
  const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(":");
205
- const decipher = createDecipheriv(
206
- "aes-256-gcm",
207
- Buffer.from(encryptionKey, "hex"),
208
- Buffer.from(ivHex, "hex")
279
+ const plain = await aesGcmOpen(
280
+ encryptionKey,
281
+ hexToBytes(ivHex),
282
+ hexToBytes(encryptedDataHex),
283
+ hexToBytes(authTagHex)
209
284
  );
210
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
211
- return decipher.update(encryptedDataHex, "hex", "utf8") + decipher.final("utf8");
285
+ return utf8Decode(plain);
212
286
  }
213
287
 
214
288
  // src/server/lib/request.js
@@ -300,7 +374,7 @@ function createGetUserKeyHandler(ctx) {
300
374
  body: {
301
375
  success: true,
302
376
  data: {
303
- userKey: deriveUserEncryptionKey(userId, masterKey),
377
+ userKey: await deriveUserEncryptionKey(userId, masterKey),
304
378
  version: 1
305
379
  }
306
380
  }
@@ -309,9 +383,6 @@ function createGetUserKeyHandler(ctx) {
309
383
  return { GET };
310
384
  }
311
385
 
312
- // src/server/handlers/schedule-message.js
313
- import { randomUUID as randomUUID2 } from "crypto";
314
-
315
386
  // src/server/lib/db-errors.js
316
387
  function isUniqueViolation(error) {
317
388
  if (!error || typeof error !== "object") return false;
@@ -322,7 +393,6 @@ function isUniqueViolation(error) {
322
393
  }
323
394
 
324
395
  // src/server/lib/message-processor.js
325
- import { randomUUID } from "crypto";
326
396
  import {
327
397
  buildContentPush,
328
398
  buildReasoningPush,
@@ -356,8 +426,8 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
356
426
  if (!masterKey) {
357
427
  return { success: false, messagesSent: 0, error: "TENANT_MASTER_KEY_MISSING" };
358
428
  }
359
- const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
360
- const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
429
+ const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
430
+ const decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
361
431
  let messageContent;
362
432
  let llmResponse = null;
363
433
  if (decryptedPayload.messageType === "fixed") {
@@ -606,10 +676,10 @@ function createScheduleMessageHandler(ctx) {
606
676
  return { status: 400, body: { success: false, error: parsedBody.error } };
607
677
  }
608
678
  const encryptedBody = parsedBody.data;
679
+ const userKey = await deriveUserEncryptionKey(userId, masterKey);
609
680
  let payload;
610
681
  try {
611
- const userKey2 = deriveUserEncryptionKey(userId, masterKey);
612
- payload = decryptPayload(encryptedBody, userKey2);
682
+ payload = await decryptPayload(encryptedBody, userKey);
613
683
  } catch (error) {
614
684
  if (error instanceof SyntaxError) {
615
685
  return { status: 400, body: { success: false, error: { code: "INVALID_PAYLOAD_FORMAT", message: "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u4E0D\u662F\u6709\u6548 JSON" } } };
@@ -627,8 +697,7 @@ function createScheduleMessageHandler(ctx) {
627
697
  if (!validationResult.valid) {
628
698
  return { status: 400, body: { success: false, error: { code: validationResult.errorCode, message: validationResult.errorMessage, details: validationResult.details } } };
629
699
  }
630
- const taskUuid = payload.uuid || randomUUID2();
631
- const userKey = deriveUserEncryptionKey(userId, masterKey);
700
+ const taskUuid = payload.uuid || randomUUID();
632
701
  const fullTaskData = {
633
702
  contactName: payload.contactName,
634
703
  avatarUrl: payload.avatarUrl || null,
@@ -654,7 +723,7 @@ function createScheduleMessageHandler(ctx) {
654
723
  pushSubscription: payload.pushSubscription,
655
724
  metadata: payload.metadata || {}
656
725
  };
657
- const encryptedPayload = encryptForStorage(JSON.stringify(fullTaskData), userKey);
726
+ const encryptedPayload = await encryptForStorage(JSON.stringify(fullTaskData), userKey);
658
727
  if (payload.messageType === "instant") {
659
728
  if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
660
729
  return {
@@ -808,8 +877,8 @@ async function runScheduledTick(ctx) {
808
877
  return;
809
878
  }
810
879
  try {
811
- const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
812
- const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
880
+ const userKey = await deriveUserEncryptionKey(task.user_id, masterKey);
881
+ const decryptedPayload = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
813
882
  if (decryptedPayload.recurrenceType === "none") {
814
883
  await db.deleteTaskById(task.id);
815
884
  results.deletedOnceOffTasks++;
@@ -896,10 +965,10 @@ function createUpdateMessageHandler(ctx) {
896
965
  return { status: 400, body: { success: false, error: parsedBody.error } };
897
966
  }
898
967
  const encryptedBody = parsedBody.data;
899
- const userKey = deriveUserEncryptionKey(userId, masterKey);
968
+ const userKey = await deriveUserEncryptionKey(userId, masterKey);
900
969
  let updates;
901
970
  try {
902
- updates = decryptPayload(encryptedBody, userKey);
971
+ updates = await decryptPayload(encryptedBody, userKey);
903
972
  } catch (_error) {
904
973
  return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
905
974
  }
@@ -948,7 +1017,7 @@ function createUpdateMessageHandler(ctx) {
948
1017
  }
949
1018
  return { status: 409, body: { success: false, error: { code: "TASK_ALREADY_COMPLETED", message: "\u4EFB\u52A1\u5DF2\u5B8C\u6210\u6216\u5DF2\u5931\u8D25\uFF0C\u65E0\u6CD5\u66F4\u65B0" } } };
950
1019
  }
951
- const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
1020
+ const existingData = JSON.parse(await decryptFromStorage(existingTask.encrypted_payload, userKey));
952
1021
  const promptUpdates = {};
953
1022
  if (updates.completePrompt) {
954
1023
  promptUpdates.completePrompt = updates.completePrompt;
@@ -971,7 +1040,7 @@ function createUpdateMessageHandler(ctx) {
971
1040
  // string fields above are.
972
1041
  ...Object.prototype.hasOwnProperty.call(updates, "splitPattern") && { splitPattern: updates.splitPattern ?? null }
973
1042
  };
974
- const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
1043
+ const encryptedPayload = await encryptForStorage(JSON.stringify(updatedData), userKey);
975
1044
  const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
976
1045
  const result = await db.updateTaskByUuid(taskUuid, userId, encryptedPayload, extraFields);
977
1046
  if (!result) {
@@ -1064,9 +1133,9 @@ function createMessagesHandler(ctx) {
1064
1133
  return { status: 400, body: { success: false, error: { code: "INVALID_PARAMETERS", message: "offset \u53C2\u6570\u65E0\u6548\uFF0C\u5FC5\u987B\u4E3A\u975E\u8D1F\u6574\u6570" } } };
1065
1134
  }
1066
1135
  const { tasks, total } = await db.listTasks(userId, { status, limit, offset });
1067
- const userKey = deriveUserEncryptionKey(userId, masterKey);
1068
- const decryptedTasks = tasks.map((task) => {
1069
- const decrypted = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
1136
+ const userKey = await deriveUserEncryptionKey(userId, masterKey);
1137
+ const decryptedTasks = await Promise.all(tasks.map(async (task) => {
1138
+ const decrypted = JSON.parse(await decryptFromStorage(task.encrypted_payload, userKey));
1070
1139
  return {
1071
1140
  id: task.id,
1072
1141
  uuid: task.uuid,
@@ -1080,12 +1149,12 @@ function createMessagesHandler(ctx) {
1080
1149
  createdAt: task.created_at,
1081
1150
  updatedAt: task.updated_at
1082
1151
  };
1083
- });
1152
+ }));
1084
1153
  const responsePayload = {
1085
1154
  tasks: decryptedTasks,
1086
1155
  pagination: { total, limit, offset, hasMore: offset + limit < total }
1087
1156
  };
1088
- const encryptedResponse = encryptPayload(responsePayload, userKey);
1157
+ const encryptedResponse = await encryptPayload(responsePayload, userKey);
1089
1158
  return {
1090
1159
  status: 200,
1091
1160
  body: {
@@ -1425,6 +1494,35 @@ function createSingleUserInitHandler(ctx) {
1425
1494
  return { POST };
1426
1495
  }
1427
1496
 
1497
+ // src/server/handlers/vapid-public-key.js
1498
+ function createVapidPublicKeyHandler(ctx) {
1499
+ async function GET(url, headers) {
1500
+ const effectiveHeaders = headers || url || {};
1501
+ const tenantResult = await ctx.tenantManager.resolveTenant(effectiveHeaders);
1502
+ if (!tenantResult.ok) {
1503
+ return tenantResult.error;
1504
+ }
1505
+ const publicKey = ctx.vapid && ctx.vapid.publicKey;
1506
+ if (!publicKey) {
1507
+ return {
1508
+ status: 503,
1509
+ body: {
1510
+ success: false,
1511
+ error: {
1512
+ code: "VAPID_NOT_CONFIGURED",
1513
+ message: "VAPID \u516C\u94A5\u672A\u914D\u7F6E\uFF1A\u8BF7\u4E3A\u672C Worker \u8BBE\u7F6E VAPID_PUBLIC_KEY"
1514
+ }
1515
+ }
1516
+ };
1517
+ }
1518
+ return {
1519
+ status: 200,
1520
+ body: { success: true, publicKey }
1521
+ };
1522
+ }
1523
+ return { GET };
1524
+ }
1525
+
1428
1526
  // src/server/single-user.js
1429
1527
  function createSingleUserServer(config) {
1430
1528
  if (!config || !config.db) throw new Error("[amsg-server single-user] config.db is required");
@@ -1452,36 +1550,12 @@ function createSingleUserServer(config) {
1452
1550
  scheduleMessage: createScheduleMessageHandler(ctx),
1453
1551
  updateMessage: createUpdateMessageHandler(ctx),
1454
1552
  cancelMessage: createCancelMessageHandler(ctx),
1455
- messages: createMessagesHandler(ctx)
1553
+ messages: createMessagesHandler(ctx),
1554
+ vapidPublicKey: createVapidPublicKeyHandler(ctx)
1456
1555
  }
1457
1556
  };
1458
1557
  }
1459
1558
 
1460
- // src/server/lib/webcrypto-utils.js
1461
- import { toUint8, concatBytes, base64UrlToBytes } from "@rei-standard/amsg-shared";
1462
- var TEXT_ENCODER = new TextEncoder();
1463
- var TEXT_DECODER = new TextDecoder("utf-8", { fatal: false });
1464
- function utf8(str) {
1465
- return TEXT_ENCODER.encode(String(str));
1466
- }
1467
- function bytesToBase64Url(buf) {
1468
- const bytes = toUint8(buf);
1469
- let bin = "";
1470
- for (let i = 0; i < bytes.length; i++) {
1471
- bin += String.fromCharCode(bytes[i]);
1472
- }
1473
- const b64 = typeof btoa === "function" ? btoa(bin) : Buffer.from(bin, "binary").toString("base64");
1474
- return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1475
- }
1476
- function jsonToBase64Url(value) {
1477
- return bytesToBase64Url(utf8(JSON.stringify(value)));
1478
- }
1479
- function randomBytes2(n) {
1480
- const out = new Uint8Array(n);
1481
- globalThis.crypto.getRandomValues(out);
1482
- return out;
1483
- }
1484
-
1485
1559
  // src/server/lib/webpush-webcrypto.js
1486
1560
  import { normalizeVapidSubject } from "@rei-standard/amsg-shared";
1487
1561
  var KEY_INFO_PREFIX = utf8("WebPush: info\0");
@@ -1569,7 +1643,7 @@ async function encryptPushPayload({ plaintext, uaPublicKey, authSecret }) {
1569
1643
  );
1570
1644
  const keyInfo = concatBytes(KEY_INFO_PREFIX, uaPublicKey, asPublicRaw);
1571
1645
  const ikm = await hkdfSha256(authSecret, ecdhSecret, keyInfo, 32);
1572
- const salt = randomBytes2(16);
1646
+ const salt = randomBytes(16);
1573
1647
  const cekBytes = await hkdfSha256(salt, ikm, CEK_INFO, 16);
1574
1648
  const cek = await globalThis.crypto.subtle.importKey(
1575
1649
  "raw",
@@ -1741,6 +1815,8 @@ function createSingleUserCloudflareWorker(buildConfig) {
1741
1815
  result = await server.handlers.updateMessage.PUT(url, headers, await request.text());
1742
1816
  } else if (method === "DELETE" && pathname.endsWith("/cancel-message")) {
1743
1817
  result = await server.handlers.cancelMessage.DELETE(url, headers);
1818
+ } else if (method === "GET" && pathname.endsWith("/vapid-public-key")) {
1819
+ result = await server.handlers.vapidPublicKey.GET(url, headers);
1744
1820
  } else {
1745
1821
  result = { status: 404, body: { success: false, error: { code: "NOT_FOUND", message: "Unknown route" } } };
1746
1822
  }