@rei-standard/amsg-server 1.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/dist/index.cjs ADDED
@@ -0,0 +1,980 @@
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; } }
2
+
3
+ var _chunkM2CNZRROcjs = require('./chunk-M2CNZRRO.cjs');
4
+
5
+ // src/server/adapters/factory.js
6
+ async function createAdapter(config) {
7
+ if (!config || !config.driver) {
8
+ throw new Error(
9
+ '[rei-standard-amsg-server] "driver" is required in the db config. Supported drivers: neon, pg'
10
+ );
11
+ }
12
+ if (!config.connectionString) {
13
+ throw new Error(
14
+ '[rei-standard-amsg-server] "connectionString" is required in the db config.'
15
+ );
16
+ }
17
+ switch (config.driver) {
18
+ case "neon": {
19
+ const { NeonAdapter } = await Promise.resolve().then(() => _interopRequireWildcard(require("./neon-FRQJDC3A.cjs")));
20
+ return new NeonAdapter(config.connectionString);
21
+ }
22
+ case "pg": {
23
+ const { PgAdapter } = await Promise.resolve().then(() => _interopRequireWildcard(require("./pg-PBITGIEU.cjs")));
24
+ return new PgAdapter(config.connectionString);
25
+ }
26
+ default:
27
+ throw new Error(
28
+ `[rei-standard-amsg-server] Unsupported driver "${config.driver}". Supported drivers: neon, pg`
29
+ );
30
+ }
31
+ }
32
+
33
+ // src/server/lib/request.js
34
+ var REQUEST_ERRORS = {
35
+ INVALID_JSON: { code: "INVALID_JSON", message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u6709\u6548\u7684 JSON" },
36
+ INVALID_REQUEST_BODY: { code: "INVALID_REQUEST_BODY", message: "\u8BF7\u6C42\u4F53\u683C\u5F0F\u65E0\u6548" },
37
+ INVALID_ENCRYPTED_PAYLOAD: { code: "INVALID_ENCRYPTED_PAYLOAD", message: "\u52A0\u5BC6\u6570\u636E\u683C\u5F0F\u9519\u8BEF" }
38
+ };
39
+ function parseBodyAsObject(body, options = {}) {
40
+ const invalidJson = options.invalidJson || REQUEST_ERRORS.INVALID_JSON;
41
+ const invalidType = options.invalidType || REQUEST_ERRORS.INVALID_REQUEST_BODY;
42
+ let parsed = body;
43
+ if (typeof parsed === "string") {
44
+ try {
45
+ parsed = JSON.parse(parsed);
46
+ } catch (e) {
47
+ return { ok: false, error: invalidJson };
48
+ }
49
+ }
50
+ if (!isPlainObject(parsed)) {
51
+ return { ok: false, error: invalidType };
52
+ }
53
+ return { ok: true, data: parsed };
54
+ }
55
+ function parseJsonBody(body) {
56
+ return parseBodyAsObject(body, {
57
+ invalidJson: REQUEST_ERRORS.INVALID_JSON,
58
+ invalidType: REQUEST_ERRORS.INVALID_REQUEST_BODY
59
+ });
60
+ }
61
+ function isPlainObject(value) {
62
+ return typeof value === "object" && value !== null && !Array.isArray(value);
63
+ }
64
+ function isEncryptedEnvelope(payload) {
65
+ if (!isPlainObject(payload)) return false;
66
+ return typeof payload.iv === "string" && typeof payload.authTag === "string" && typeof payload.encryptedData === "string";
67
+ }
68
+ function parseEncryptedBody(body) {
69
+ const parsedBody = parseBodyAsObject(body, {
70
+ invalidJson: REQUEST_ERRORS.INVALID_ENCRYPTED_PAYLOAD,
71
+ invalidType: REQUEST_ERRORS.INVALID_ENCRYPTED_PAYLOAD
72
+ });
73
+ if (!parsedBody.ok) {
74
+ return parsedBody;
75
+ }
76
+ if (!isEncryptedEnvelope(parsedBody.data)) {
77
+ return { ok: false, error: REQUEST_ERRORS.INVALID_ENCRYPTED_PAYLOAD };
78
+ }
79
+ return parsedBody;
80
+ }
81
+
82
+ // src/server/handlers/init-database.js
83
+ function createInitDatabaseHandler(ctx) {
84
+ async function GET(headers) {
85
+ if (!ctx.initSecret) {
86
+ return {
87
+ status: 500,
88
+ body: { success: false, error: { code: "INIT_SECRET_MISSING", message: "initSecret \u672A\u914D\u7F6E\uFF0C\u8BF7\u5728 createReiServer \u914D\u7F6E\u4E2D\u63D0\u4F9B initSecret" } }
89
+ };
90
+ }
91
+ const authHeader = (headers["authorization"] || "").trim();
92
+ const expectedAuth = `Bearer ${ctx.initSecret}`;
93
+ if (authHeader !== expectedAuth) {
94
+ return {
95
+ status: 401,
96
+ body: { success: false, error: { code: "UNAUTHORIZED", message: "\u9700\u8981\u8BA4\u8BC1\u3002\u8BF7\u5728\u8BF7\u6C42\u5934\u4E2D\u6DFB\u52A0: Authorization: Bearer {INIT_SECRET}" } }
97
+ };
98
+ }
99
+ const result = await ctx.db.initSchema();
100
+ const columnNames = result.columns.map((c) => c.name);
101
+ const missingColumns = _chunkM2CNZRROcjs.REQUIRED_COLUMNS.filter((col) => !columnNames.includes(col));
102
+ if (missingColumns.length > 0) {
103
+ console.warn("[init-database] \u26A0\uFE0F Missing columns:", missingColumns);
104
+ }
105
+ return {
106
+ status: 200,
107
+ body: {
108
+ success: true,
109
+ message: "\u6570\u636E\u5E93\u521D\u59CB\u5316\u6210\u529F\uFF01\u5EFA\u8BAE\u7ACB\u5373\u5220\u9664\u6B64 API \u6587\u4EF6\u3002",
110
+ data: {
111
+ table: "scheduled_messages",
112
+ columnsCreated: result.columnsCreated,
113
+ indexesCreated: result.indexesCreated,
114
+ indexesFailed: result.indexesFailed,
115
+ details: { columns: result.columns, indexes: result.indexes },
116
+ nextSteps: [
117
+ "1. \u9A8C\u8BC1\u8868\u548C\u7D22\u5F15\u5DF2\u6B63\u786E\u521B\u5EFA",
118
+ "2. \u7ACB\u5373\u5220\u9664 /app/api/v1/init-database/route.js \u6587\u4EF6",
119
+ "3. \u4ECE .env \u4E2D\u5220\u9664 INIT_SECRET\uFF08\u53EF\u9009\uFF09",
120
+ "4. \u5F00\u59CB\u4F7F\u7528 ReiStandard API"
121
+ ]
122
+ }
123
+ }
124
+ };
125
+ }
126
+ async function POST(headers, body) {
127
+ if (!ctx.initSecret) {
128
+ return {
129
+ status: 500,
130
+ body: { success: false, error: { code: "INIT_SECRET_MISSING", message: "initSecret \u672A\u914D\u7F6E\uFF0C\u8BF7\u5728 createReiServer \u914D\u7F6E\u4E2D\u63D0\u4F9B initSecret" } }
131
+ };
132
+ }
133
+ const authHeader = (headers["authorization"] || "").trim();
134
+ const expectedAuth = `Bearer ${ctx.initSecret}`;
135
+ if (authHeader !== expectedAuth) {
136
+ return {
137
+ status: 401,
138
+ body: { success: false, error: { code: "UNAUTHORIZED", message: "\u9700\u8981\u8BA4\u8BC1" } }
139
+ };
140
+ }
141
+ const parsedBody = parseJsonBody(body);
142
+ if (!parsedBody.ok) {
143
+ return {
144
+ status: 400,
145
+ body: { success: false, error: parsedBody.error }
146
+ };
147
+ }
148
+ if (parsedBody.data.confirm !== "DELETE_ALL_DATA") {
149
+ return {
150
+ status: 400,
151
+ body: { success: false, error: { code: "CONFIRMATION_REQUIRED", message: '\u9700\u8981\u5728\u8BF7\u6C42\u4F53\u4E2D\u63D0\u4F9B\u786E\u8BA4\u53C2\u6570: { "confirm": "DELETE_ALL_DATA" }' } }
152
+ };
153
+ }
154
+ await ctx.db.dropSchema();
155
+ return GET(headers);
156
+ }
157
+ return { GET, POST };
158
+ }
159
+
160
+ // src/server/handlers/get-master-key.js
161
+ function createGetMasterKeyHandler(ctx) {
162
+ async function GET(headers) {
163
+ const userId = headers["x-user-id"];
164
+ if (!userId) {
165
+ return {
166
+ status: 400,
167
+ body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } }
168
+ };
169
+ }
170
+ return {
171
+ status: 200,
172
+ body: {
173
+ success: true,
174
+ data: {
175
+ masterKey: ctx.encryptionKey,
176
+ version: 1
177
+ }
178
+ }
179
+ };
180
+ }
181
+ return { GET };
182
+ }
183
+
184
+ // src/server/handlers/schedule-message.js
185
+ var _crypto = require('crypto');
186
+
187
+ // src/server/lib/encryption.js
188
+
189
+ function deriveUserEncryptionKey(userId, masterKey) {
190
+ return _crypto.createHash.call(void 0, "sha256").update(masterKey + userId).digest("hex").slice(0, 64);
191
+ }
192
+ function decryptPayload(encryptedPayload, encryptionKey) {
193
+ const { iv, authTag, encryptedData } = encryptedPayload;
194
+ const decipher = _crypto.createDecipheriv.call(void 0,
195
+ "aes-256-gcm",
196
+ Buffer.from(encryptionKey, "hex"),
197
+ Buffer.from(iv, "base64")
198
+ );
199
+ decipher.setAuthTag(Buffer.from(authTag, "base64"));
200
+ const decrypted = Buffer.concat([
201
+ decipher.update(Buffer.from(encryptedData, "base64")),
202
+ decipher.final()
203
+ ]);
204
+ return JSON.parse(decrypted.toString("utf8"));
205
+ }
206
+ function encryptPayload(payload, encryptionKey) {
207
+ const plaintext = typeof payload === "string" ? payload : JSON.stringify(payload);
208
+ const iv = _crypto.randomBytes.call(void 0, 12);
209
+ const cipher = _crypto.createCipheriv.call(void 0, "aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
210
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
211
+ const authTag = cipher.getAuthTag();
212
+ return {
213
+ iv: iv.toString("base64"),
214
+ authTag: authTag.toString("base64"),
215
+ encryptedData: encrypted.toString("base64")
216
+ };
217
+ }
218
+ function encryptForStorage(text, encryptionKey) {
219
+ const iv = _crypto.randomBytes.call(void 0, 16);
220
+ const cipher = _crypto.createCipheriv.call(void 0, "aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
221
+ const encrypted = cipher.update(text, "utf8", "hex") + cipher.final("hex");
222
+ const authTag = cipher.getAuthTag();
223
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
224
+ }
225
+ function decryptFromStorage(encryptedText, encryptionKey) {
226
+ const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(":");
227
+ const decipher = _crypto.createDecipheriv.call(void 0,
228
+ "aes-256-gcm",
229
+ Buffer.from(encryptionKey, "hex"),
230
+ Buffer.from(ivHex, "hex")
231
+ );
232
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
233
+ return decipher.update(encryptedDataHex, "hex", "utf8") + decipher.final("utf8");
234
+ }
235
+
236
+ // src/server/lib/db-errors.js
237
+ function isUniqueViolation(error) {
238
+ if (!error || typeof error !== "object") return false;
239
+ const code = error.code;
240
+ if (code === "23505") return true;
241
+ const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
242
+ return message.includes("duplicate key") || message.includes("unique constraint");
243
+ }
244
+
245
+ // src/server/lib/validation.js
246
+ function isValidISO8601(dateString) {
247
+ const date = new Date(dateString);
248
+ return date instanceof Date && !isNaN(date.getTime());
249
+ }
250
+ function isValidUrl(urlString) {
251
+ try {
252
+ new URL(urlString);
253
+ return true;
254
+ } catch (e2) {
255
+ return false;
256
+ }
257
+ }
258
+ function isValidUUID(uuid) {
259
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
260
+ return uuidRegex.test(uuid);
261
+ }
262
+ function validateScheduleMessagePayload(payload) {
263
+ if (!payload.contactName || typeof payload.contactName !== "string") {
264
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["contactName"] } };
265
+ }
266
+ if (!payload.messageType || !["fixed", "prompted", "auto", "instant"].includes(payload.messageType)) {
267
+ return { valid: false, errorCode: "INVALID_MESSAGE_TYPE", errorMessage: "\u6D88\u606F\u7C7B\u578B\u65E0\u6548", details: { providedType: payload.messageType, allowedTypes: ["fixed", "prompted", "auto", "instant"] } };
268
+ }
269
+ if (!payload.firstSendTime || !isValidISO8601(payload.firstSendTime)) {
270
+ return { valid: false, errorCode: "INVALID_TIMESTAMP", errorMessage: "\u65F6\u95F4\u683C\u5F0F\u65E0\u6548", details: { field: "firstSendTime" } };
271
+ }
272
+ if (payload.firstSendTime && new Date(payload.firstSendTime) <= /* @__PURE__ */ new Date()) {
273
+ return { valid: false, errorCode: "INVALID_TIMESTAMP", errorMessage: "\u65F6\u95F4\u5FC5\u987B\u5728\u672A\u6765", details: { field: "firstSendTime", reason: "must be in the future" } };
274
+ }
275
+ if (!payload.pushSubscription || typeof payload.pushSubscription !== "object") {
276
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: ["pushSubscription"] } };
277
+ }
278
+ if (payload.recurrenceType && !["none", "daily", "weekly"].includes(payload.recurrenceType)) {
279
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } };
280
+ }
281
+ if (payload.messageType === "fixed") {
282
+ if (!payload.userMessage) {
283
+ 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)"] } };
284
+ }
285
+ }
286
+ if (payload.messageType === "prompted" || payload.messageType === "auto") {
287
+ const missingAiFields = [];
288
+ if (!payload.completePrompt) missingAiFields.push("completePrompt");
289
+ if (!payload.apiUrl) missingAiFields.push("apiUrl");
290
+ if (!payload.apiKey) missingAiFields.push("apiKey");
291
+ if (!payload.primaryModel) missingAiFields.push("primaryModel");
292
+ if (missingAiFields.length > 0) {
293
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { missingFields: missingAiFields } };
294
+ }
295
+ }
296
+ if (payload.messageType === "instant") {
297
+ if (payload.recurrenceType && payload.recurrenceType !== "none") {
298
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "instant \u7C7B\u578B\u7684 recurrenceType \u5FC5\u987B\u4E3A none", details: { invalidFields: ['recurrenceType (must be "none" for instant type)'] } };
299
+ }
300
+ const hasAiConfig = payload.completePrompt && payload.apiUrl && payload.apiKey && payload.primaryModel;
301
+ const hasUserMessage = payload.userMessage;
302
+ if (!hasAiConfig && !hasUserMessage) {
303
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "instant \u7C7B\u578B\u5FC5\u987B\u63D0\u4F9B userMessage \u6216\u5B8C\u6574\u7684 AI \u914D\u7F6E", details: { missingFields: ["userMessage or (completePrompt + apiUrl + apiKey + primaryModel)"] } };
304
+ }
305
+ }
306
+ if (payload.avatarUrl && !isValidUrl(payload.avatarUrl)) {
307
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["avatarUrl (invalid URL format)"] } };
308
+ }
309
+ if (payload.uuid && !isValidUUID(payload.uuid)) {
310
+ 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)"] } };
311
+ }
312
+ if (payload.messageSubtype && !["chat", "forum", "moment"].includes(payload.messageSubtype)) {
313
+ return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["messageSubtype"] } };
314
+ }
315
+ return { valid: true };
316
+ }
317
+
318
+ // src/server/lib/message-processor.js
319
+
320
+ async function processSingleMessage(task, ctx) {
321
+ try {
322
+ const userKey = deriveUserEncryptionKey(task.user_id, ctx.encryptionKey);
323
+ const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
324
+ let messageContent;
325
+ if (decryptedPayload.messageType === "fixed") {
326
+ messageContent = decryptedPayload.userMessage;
327
+ } else if (decryptedPayload.messageType === "instant") {
328
+ if (decryptedPayload.completePrompt && decryptedPayload.apiUrl && decryptedPayload.apiKey && decryptedPayload.primaryModel) {
329
+ messageContent = await _callAI(decryptedPayload);
330
+ } else if (decryptedPayload.userMessage) {
331
+ messageContent = decryptedPayload.userMessage;
332
+ } else {
333
+ throw new Error("Invalid instant message: no content source available");
334
+ }
335
+ } else if (decryptedPayload.messageType === "prompted" || decryptedPayload.messageType === "auto") {
336
+ messageContent = await _callAI(decryptedPayload);
337
+ } else {
338
+ throw new Error("Invalid message configuration: no content source available");
339
+ }
340
+ const sentences = messageContent.split(/([。!?!?]+)/).reduce((acc, part, i, arr) => {
341
+ if (i % 2 === 0 && part.trim()) {
342
+ const punctuation = arr[i + 1] || "";
343
+ acc.push(part.trim() + punctuation);
344
+ }
345
+ return acc;
346
+ }, []).filter((s) => s.length > 0);
347
+ const messages = sentences.length > 0 ? sentences : [messageContent];
348
+ if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
349
+ throw new Error("VAPID configuration missing - push notifications cannot be sent");
350
+ }
351
+ const pushSubscription = decryptedPayload.pushSubscription;
352
+ for (let i = 0; i < messages.length; i++) {
353
+ const notificationPayload = {
354
+ title: `\u6765\u81EA ${decryptedPayload.contactName}`,
355
+ message: messages[i],
356
+ contactName: decryptedPayload.contactName,
357
+ messageId: `msg_${_crypto.randomUUID.call(void 0, )}_${task.id || "instant"}_${i}`,
358
+ messageIndex: i + 1,
359
+ totalMessages: messages.length,
360
+ messageType: decryptedPayload.messageType,
361
+ messageSubtype: decryptedPayload.messageSubtype || "chat",
362
+ taskId: task.id || null,
363
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
364
+ source: decryptedPayload.messageType === "instant" ? "instant" : "scheduled",
365
+ avatarUrl: decryptedPayload.avatarUrl || null,
366
+ metadata: decryptedPayload.metadata || {}
367
+ };
368
+ await ctx.webpush.sendNotification(pushSubscription, JSON.stringify(notificationPayload));
369
+ if (i < messages.length - 1) {
370
+ await new Promise((resolve) => setTimeout(resolve, 1500));
371
+ }
372
+ }
373
+ return { success: true, messagesSent: messages.length };
374
+ } catch (error) {
375
+ return { success: false, messagesSent: 0, error: error.message };
376
+ }
377
+ }
378
+ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId) {
379
+ let retryCount = 0;
380
+ while (retryCount <= maxRetries) {
381
+ let task;
382
+ try {
383
+ task = userId ? await ctx.db.getTaskByUuid(uuid, userId) : await ctx.db.getTaskByUuidOnly(uuid);
384
+ } catch (error) {
385
+ if (retryCount < maxRetries) {
386
+ retryCount++;
387
+ await new Promise((resolve) => setTimeout(resolve, 1e3 * retryCount));
388
+ continue;
389
+ }
390
+ return {
391
+ success: false,
392
+ error: { code: "INTERNAL_ERROR", message: error.message, retriesAttempted: retryCount }
393
+ };
394
+ }
395
+ if (!task) {
396
+ return { success: false, error: { code: "TASK_NOT_FOUND", message: "\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u5904\u7406" } };
397
+ }
398
+ const result = await processSingleMessage(task, ctx);
399
+ if (!result.success) {
400
+ if (retryCount < maxRetries) {
401
+ retryCount++;
402
+ await new Promise((resolve) => setTimeout(resolve, 1e3 * retryCount));
403
+ continue;
404
+ }
405
+ try {
406
+ await ctx.db.updateTaskById(task.id, { status: "failed", retry_count: retryCount });
407
+ } catch (_updateError) {
408
+ }
409
+ return {
410
+ success: false,
411
+ error: { code: "PROCESSING_ERROR", message: result.error, retriesAttempted: retryCount }
412
+ };
413
+ }
414
+ try {
415
+ await ctx.db.deleteTaskById(task.id);
416
+ } catch (error) {
417
+ try {
418
+ await ctx.db.updateTaskById(task.id, { status: "sent", retry_count: 0 });
419
+ } catch (_markSentError) {
420
+ }
421
+ return {
422
+ success: false,
423
+ error: {
424
+ code: "POST_SEND_CLEANUP_FAILED",
425
+ message: "\u6D88\u606F\u5DF2\u53D1\u9001\uFF0C\u4F46\u4EFB\u52A1\u6E05\u7406\u5931\u8D25",
426
+ details: { error: error.message }
427
+ }
428
+ };
429
+ }
430
+ return { success: true, messagesSent: result.messagesSent, retriesUsed: retryCount };
431
+ }
432
+ }
433
+ async function _callAI(payload) {
434
+ const aiResponse = await fetch(payload.apiUrl, {
435
+ method: "POST",
436
+ headers: {
437
+ "Content-Type": "application/json",
438
+ "Authorization": `Bearer ${payload.apiKey}`
439
+ },
440
+ body: JSON.stringify({
441
+ model: payload.primaryModel,
442
+ messages: [{ role: "user", content: payload.completePrompt }],
443
+ max_tokens: 500,
444
+ temperature: 0.8
445
+ }),
446
+ signal: AbortSignal.timeout(3e5)
447
+ });
448
+ if (!aiResponse.ok) {
449
+ throw new Error(`AI API error: ${aiResponse.status} ${aiResponse.statusText}`);
450
+ }
451
+ const aiData = await aiResponse.json();
452
+ return aiData.choices[0].message.content.trim();
453
+ }
454
+
455
+ // src/server/handlers/schedule-message.js
456
+ function createScheduleMessageHandler(ctx) {
457
+ async function POST(headers, body) {
458
+ const isEncrypted = headers["x-payload-encrypted"] === "true";
459
+ const encryptionVersion = headers["x-encryption-version"];
460
+ const userId = headers["x-user-id"];
461
+ if (!isEncrypted) {
462
+ return { status: 400, body: { success: false, error: { code: "ENCRYPTION_REQUIRED", message: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6" } } };
463
+ }
464
+ if (!userId) {
465
+ return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
466
+ }
467
+ if (encryptionVersion !== "1") {
468
+ return { status: 400, body: { success: false, error: { code: "UNSUPPORTED_ENCRYPTION_VERSION", message: "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301" } } };
469
+ }
470
+ const parsedBody = parseEncryptedBody(body);
471
+ if (!parsedBody.ok) {
472
+ return { status: 400, body: { success: false, error: parsedBody.error } };
473
+ }
474
+ const encryptedBody = parsedBody.data;
475
+ let payload;
476
+ try {
477
+ const userKey2 = deriveUserEncryptionKey(userId, ctx.encryptionKey);
478
+ payload = decryptPayload(encryptedBody, userKey2);
479
+ } catch (error) {
480
+ if (error instanceof SyntaxError) {
481
+ return { status: 400, body: { success: false, error: { code: "INVALID_PAYLOAD_FORMAT", message: "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u4E0D\u662F\u6709\u6548 JSON" } } };
482
+ }
483
+ const message = typeof error.message === "string" ? error.message : "";
484
+ if (message.includes("auth") || message.includes("Unsupported state")) {
485
+ return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
486
+ }
487
+ return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
488
+ }
489
+ if (!isPlainObject(payload)) {
490
+ return { status: 400, body: { success: false, error: { code: "INVALID_PAYLOAD_FORMAT", message: "\u89E3\u5BC6\u540E\u7684\u6570\u636E\u5FC5\u987B\u662F JSON \u5BF9\u8C61" } } };
491
+ }
492
+ const validationResult = validateScheduleMessagePayload(payload);
493
+ if (!validationResult.valid) {
494
+ return { status: 400, body: { success: false, error: { code: validationResult.errorCode, message: validationResult.errorMessage, details: validationResult.details } } };
495
+ }
496
+ const taskUuid = payload.uuid || _crypto.randomUUID.call(void 0, );
497
+ const userKey = deriveUserEncryptionKey(userId, ctx.encryptionKey);
498
+ const fullTaskData = {
499
+ contactName: payload.contactName,
500
+ avatarUrl: payload.avatarUrl || null,
501
+ messageType: payload.messageType,
502
+ messageSubtype: payload.messageSubtype || "chat",
503
+ userMessage: payload.userMessage || null,
504
+ firstSendTime: payload.firstSendTime,
505
+ recurrenceType: payload.recurrenceType || "none",
506
+ apiUrl: payload.apiUrl || null,
507
+ apiKey: payload.apiKey || null,
508
+ primaryModel: payload.primaryModel || null,
509
+ completePrompt: payload.completePrompt || null,
510
+ pushSubscription: payload.pushSubscription,
511
+ metadata: payload.metadata || {}
512
+ };
513
+ const encryptedPayload = encryptForStorage(JSON.stringify(fullTaskData), userKey);
514
+ if (payload.messageType === "instant") {
515
+ if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
516
+ return {
517
+ status: 500,
518
+ body: {
519
+ success: false,
520
+ error: {
521
+ code: "VAPID_CONFIG_ERROR",
522
+ message: "VAPID \u914D\u7F6E\u7F3A\u5931\uFF0C\u65E0\u6CD5\u53D1\u9001\u5373\u65F6\u6D88\u606F",
523
+ details: {
524
+ missingKeys: [
525
+ !ctx.vapid.email && "VAPID_EMAIL",
526
+ !ctx.vapid.publicKey && "NEXT_PUBLIC_VAPID_PUBLIC_KEY",
527
+ !ctx.vapid.privateKey && "VAPID_PRIVATE_KEY"
528
+ ].filter(Boolean)
529
+ }
530
+ }
531
+ }
532
+ };
533
+ }
534
+ }
535
+ let dbResult;
536
+ try {
537
+ dbResult = await ctx.db.createTask({
538
+ user_id: userId,
539
+ uuid: taskUuid,
540
+ encrypted_payload: encryptedPayload,
541
+ next_send_at: payload.firstSendTime,
542
+ message_type: payload.messageType
543
+ });
544
+ } catch (error) {
545
+ if (isUniqueViolation(error)) {
546
+ return {
547
+ status: 409,
548
+ body: {
549
+ success: false,
550
+ error: {
551
+ code: "TASK_UUID_CONFLICT",
552
+ message: "\u4EFB\u52A1 UUID \u5DF2\u5B58\u5728\uFF0C\u8BF7\u4F7F\u7528\u65B0\u7684 uuid \u91CD\u65B0\u63D0\u4EA4"
553
+ }
554
+ }
555
+ };
556
+ }
557
+ throw error;
558
+ }
559
+ if (!dbResult) {
560
+ return { status: 500, body: { success: false, error: { code: "TASK_CREATE_FAILED", message: "\u521B\u5EFA\u4EFB\u52A1\u5931\u8D25" } } };
561
+ }
562
+ if (payload.messageType === "instant") {
563
+ try {
564
+ const sendResult = await processMessagesByUuid(taskUuid, ctx, 2, userId);
565
+ if (!sendResult.success) {
566
+ return { status: 500, body: { success: false, error: { code: "MESSAGE_SEND_FAILED", message: "\u6D88\u606F\u53D1\u9001\u5931\u8D25", details: sendResult.error } } };
567
+ }
568
+ return {
569
+ status: 200,
570
+ body: {
571
+ success: true,
572
+ data: {
573
+ uuid: taskUuid,
574
+ contactName: payload.contactName,
575
+ messagesSent: sendResult.messagesSent,
576
+ sentAt: (/* @__PURE__ */ new Date()).toISOString(),
577
+ status: "sent",
578
+ retriesUsed: sendResult.retriesUsed || 0
579
+ }
580
+ }
581
+ };
582
+ } catch (error) {
583
+ return { status: 500, body: { success: false, error: { code: "MESSAGE_SEND_FAILED", message: "\u6D88\u606F\u53D1\u9001\u5931\u8D25", details: { error: error.message } } } };
584
+ }
585
+ }
586
+ return {
587
+ status: 201,
588
+ body: {
589
+ success: true,
590
+ data: {
591
+ id: dbResult.id,
592
+ uuid: dbResult.uuid,
593
+ contactName: payload.contactName,
594
+ nextSendAt: dbResult.next_send_at,
595
+ status: dbResult.status,
596
+ createdAt: dbResult.created_at
597
+ }
598
+ }
599
+ };
600
+ }
601
+ return { POST };
602
+ }
603
+
604
+ // src/server/handlers/send-notifications.js
605
+ function createSendNotificationsHandler(ctx) {
606
+ async function POST(headers) {
607
+ if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
608
+ return {
609
+ status: 500,
610
+ body: {
611
+ success: false,
612
+ error: {
613
+ code: "VAPID_CONFIG_ERROR",
614
+ message: "VAPID \u914D\u7F6E\u7F3A\u5931\uFF0C\u65E0\u6CD5\u53D1\u9001\u63A8\u9001\u901A\u77E5",
615
+ details: {
616
+ missingKeys: [
617
+ !ctx.vapid.email && "VAPID_EMAIL",
618
+ !ctx.vapid.publicKey && "NEXT_PUBLIC_VAPID_PUBLIC_KEY",
619
+ !ctx.vapid.privateKey && "VAPID_PRIVATE_KEY"
620
+ ].filter(Boolean)
621
+ }
622
+ }
623
+ }
624
+ };
625
+ }
626
+ const authHeader = (headers["authorization"] || "").trim();
627
+ const expectedAuth = `Bearer ${ctx.cronSecret}`;
628
+ if (authHeader !== expectedAuth) {
629
+ return { status: 401, body: { success: false, error: { code: "UNAUTHORIZED", message: "Cron Secret \u9A8C\u8BC1\u5931\u8D25" } } };
630
+ }
631
+ const startTime = Date.now();
632
+ const tasks = await ctx.db.getPendingTasks(50);
633
+ const MAX_CONCURRENT = 8;
634
+ const results = {
635
+ totalTasks: tasks.length,
636
+ successCount: 0,
637
+ failedCount: 0,
638
+ deletedOnceOffTasks: 0,
639
+ updatedRecurringTasks: 0,
640
+ failedTasks: []
641
+ };
642
+ async function handleDeliveryFailure(task, reason) {
643
+ results.failedCount++;
644
+ try {
645
+ if (task.retry_count >= 3) {
646
+ await ctx.db.updateTaskById(task.id, { status: "failed" });
647
+ results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "permanently_failed" });
648
+ } else {
649
+ const nextRetryTime = new Date(Date.now() + (task.retry_count + 1) * 2 * 60 * 1e3);
650
+ await ctx.db.updateTaskById(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
651
+ results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count + 1, nextRetryAt: nextRetryTime.toISOString() });
652
+ }
653
+ } catch (updateError) {
654
+ results.failedTasks.push({
655
+ taskId: task.id,
656
+ reason,
657
+ status: "retry_update_failed",
658
+ updateError: updateError.message
659
+ });
660
+ }
661
+ }
662
+ async function handlePostSendPersistenceFailure(task, reason) {
663
+ results.failedCount++;
664
+ let markedSent = false;
665
+ try {
666
+ await ctx.db.updateTaskById(task.id, { status: "sent", retry_count: 0 });
667
+ markedSent = true;
668
+ } catch (_markSentError) {
669
+ markedSent = false;
670
+ }
671
+ results.failedTasks.push({
672
+ taskId: task.id,
673
+ reason,
674
+ status: markedSent ? "post_send_cleanup_failed_marked_sent" : "post_send_cleanup_failed",
675
+ messageDelivered: true
676
+ });
677
+ }
678
+ async function processTask(task) {
679
+ let sendResult;
680
+ try {
681
+ sendResult = await processSingleMessage(task, ctx);
682
+ } catch (error) {
683
+ await handleDeliveryFailure(task, error.message || "\u6D88\u606F\u53D1\u9001\u5931\u8D25");
684
+ return;
685
+ }
686
+ if (!sendResult.success) {
687
+ await handleDeliveryFailure(task, sendResult.error || "\u6D88\u606F\u53D1\u9001\u5931\u8D25");
688
+ return;
689
+ }
690
+ try {
691
+ const userKey = deriveUserEncryptionKey(task.user_id, ctx.encryptionKey);
692
+ const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
693
+ if (decryptedPayload.recurrenceType === "none") {
694
+ await ctx.db.deleteTaskById(task.id);
695
+ results.deletedOnceOffTasks++;
696
+ } else {
697
+ let nextSendAt;
698
+ const currentSendAt = new Date(task.next_send_at);
699
+ if (decryptedPayload.recurrenceType === "daily") {
700
+ nextSendAt = new Date(currentSendAt.getTime() + 24 * 60 * 60 * 1e3);
701
+ } else if (decryptedPayload.recurrenceType === "weekly") {
702
+ nextSendAt = new Date(currentSendAt.getTime() + 7 * 24 * 60 * 60 * 1e3);
703
+ }
704
+ await ctx.db.updateTaskById(task.id, { next_send_at: nextSendAt.toISOString(), retry_count: 0 });
705
+ results.updatedRecurringTasks++;
706
+ }
707
+ results.successCount++;
708
+ } catch (error) {
709
+ await handlePostSendPersistenceFailure(task, error.message || "\u53D1\u9001\u540E\u72B6\u6001\u66F4\u65B0\u5931\u8D25");
710
+ }
711
+ }
712
+ const taskQueue = [...tasks];
713
+ const processing = [];
714
+ while (taskQueue.length > 0 || processing.length > 0) {
715
+ while (processing.length < MAX_CONCURRENT && taskQueue.length > 0) {
716
+ const task = taskQueue.shift();
717
+ const promise = processTask(task);
718
+ processing.push(promise);
719
+ promise.finally(() => {
720
+ const index = processing.indexOf(promise);
721
+ if (index > -1) processing.splice(index, 1);
722
+ });
723
+ }
724
+ if (processing.length > 0) {
725
+ await Promise.race(processing);
726
+ }
727
+ }
728
+ await ctx.db.cleanupOldTasks(7);
729
+ const executionTime = Date.now() - startTime;
730
+ return {
731
+ status: 200,
732
+ body: {
733
+ success: true,
734
+ data: {
735
+ totalTasks: results.totalTasks,
736
+ successCount: results.successCount,
737
+ failedCount: results.failedCount,
738
+ processedAt: (/* @__PURE__ */ new Date()).toISOString(),
739
+ executionTime,
740
+ details: {
741
+ deletedOnceOffTasks: results.deletedOnceOffTasks,
742
+ updatedRecurringTasks: results.updatedRecurringTasks,
743
+ failedTasks: results.failedTasks
744
+ }
745
+ }
746
+ }
747
+ };
748
+ }
749
+ return { POST };
750
+ }
751
+
752
+ // src/server/handlers/update-message.js
753
+ function createUpdateMessageHandler(ctx) {
754
+ async function PUT(url, headers, body) {
755
+ const u = new URL(url, "https://dummy");
756
+ const taskUuid = u.searchParams.get("id");
757
+ if (!taskUuid) {
758
+ return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
759
+ }
760
+ const userId = headers["x-user-id"];
761
+ if (!userId) {
762
+ return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
763
+ }
764
+ const isEncrypted = headers["x-payload-encrypted"] === "true";
765
+ const encryptionVersion = headers["x-encryption-version"];
766
+ if (!isEncrypted) {
767
+ return { status: 400, body: { success: false, error: { code: "ENCRYPTION_REQUIRED", message: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6" } } };
768
+ }
769
+ if (encryptionVersion !== "1") {
770
+ return { status: 400, body: { success: false, error: { code: "UNSUPPORTED_ENCRYPTION_VERSION", message: "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301" } } };
771
+ }
772
+ const parsedBody = parseEncryptedBody(body);
773
+ if (!parsedBody.ok) {
774
+ return { status: 400, body: { success: false, error: parsedBody.error } };
775
+ }
776
+ const encryptedBody = parsedBody.data;
777
+ const userKey = deriveUserEncryptionKey(userId, ctx.encryptionKey);
778
+ let updates;
779
+ try {
780
+ updates = decryptPayload(encryptedBody, userKey);
781
+ } catch (_error) {
782
+ return { status: 400, body: { success: false, error: { code: "DECRYPTION_FAILED", message: "\u8BF7\u6C42\u4F53\u89E3\u5BC6\u5931\u8D25" } } };
783
+ }
784
+ if (!isPlainObject(updates)) {
785
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF" } } };
786
+ }
787
+ if (updates.nextSendAt && !isValidISO8601(updates.nextSendAt)) {
788
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["nextSendAt"] } } } };
789
+ }
790
+ if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
791
+ return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
792
+ }
793
+ const existingTask = await ctx.db.getTaskByUuid(taskUuid, userId);
794
+ if (!existingTask) {
795
+ const taskStatus = await ctx.db.getTaskStatus(taskUuid, userId);
796
+ if (!taskStatus) {
797
+ return { status: 404, body: { success: false, error: { code: "TASK_NOT_FOUND", message: "\u6307\u5B9A\u7684\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664" } } };
798
+ }
799
+ 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" } } };
800
+ }
801
+ const existingData = JSON.parse(decryptFromStorage(existingTask.encrypted_payload, userKey));
802
+ const updatedData = {
803
+ ...existingData,
804
+ ...updates.completePrompt && { completePrompt: updates.completePrompt },
805
+ ...updates.userMessage && { userMessage: updates.userMessage },
806
+ ...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
807
+ ...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
808
+ ...updates.metadata && { metadata: updates.metadata }
809
+ };
810
+ const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
811
+ const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
812
+ const result = await ctx.db.updateTaskByUuid(taskUuid, userId, encryptedPayload, extraFields);
813
+ if (!result) {
814
+ return { status: 409, body: { success: false, error: { code: "UPDATE_CONFLICT", message: "\u4EFB\u52A1\u66F4\u65B0\u5931\u8D25\uFF0C\u4EFB\u52A1\u53EF\u80FD\u5DF2\u88AB\u4FEE\u6539\u6216\u5220\u9664" } } };
815
+ }
816
+ return {
817
+ status: 200,
818
+ body: {
819
+ success: true,
820
+ data: {
821
+ uuid: taskUuid,
822
+ updatedFields: Object.keys(updates),
823
+ updatedAt: result.updated_at
824
+ }
825
+ }
826
+ };
827
+ }
828
+ return { PUT };
829
+ }
830
+
831
+ // src/server/handlers/cancel-message.js
832
+ function createCancelMessageHandler(ctx) {
833
+ async function DELETE(url, headers) {
834
+ const u = new URL(url, "https://dummy");
835
+ const taskUuid = u.searchParams.get("id");
836
+ if (!taskUuid) {
837
+ return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
838
+ }
839
+ const userId = headers["x-user-id"];
840
+ if (!userId) {
841
+ return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
842
+ }
843
+ const deleted = await ctx.db.deleteTaskByUuid(taskUuid, userId);
844
+ if (!deleted) {
845
+ return {
846
+ status: 404,
847
+ body: { success: false, error: { code: "TASK_NOT_FOUND", message: "\u6307\u5B9A\u7684\u4EFB\u52A1\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5220\u9664" } }
848
+ };
849
+ }
850
+ return {
851
+ status: 200,
852
+ body: {
853
+ success: true,
854
+ data: { uuid: taskUuid, message: "\u4EFB\u52A1\u5DF2\u6210\u529F\u53D6\u6D88", deletedAt: (/* @__PURE__ */ new Date()).toISOString() }
855
+ }
856
+ };
857
+ }
858
+ return { DELETE };
859
+ }
860
+
861
+ // src/server/handlers/messages.js
862
+ function createMessagesHandler(ctx) {
863
+ async function GET(url, headers) {
864
+ const userId = headers["x-user-id"];
865
+ if (!userId) {
866
+ return {
867
+ status: 400,
868
+ body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u5FC5\u987B\u63D0\u4F9B X-User-Id \u8BF7\u6C42\u5934" } }
869
+ };
870
+ }
871
+ const u = new URL(url, "https://dummy");
872
+ const status = u.searchParams.get("status") || "all";
873
+ const limit = Math.min(parseInt(u.searchParams.get("limit") || "20", 10), 100);
874
+ const offset = parseInt(u.searchParams.get("offset") || "0", 10);
875
+ if (isNaN(limit) || limit < 1) {
876
+ return { status: 400, body: { success: false, error: { code: "INVALID_PARAMETERS", message: "limit \u53C2\u6570\u65E0\u6548\uFF0C\u5FC5\u987B\u4E3A\u6B63\u6574\u6570" } } };
877
+ }
878
+ if (isNaN(offset) || offset < 0) {
879
+ return { status: 400, body: { success: false, error: { code: "INVALID_PARAMETERS", message: "offset \u53C2\u6570\u65E0\u6548\uFF0C\u5FC5\u987B\u4E3A\u975E\u8D1F\u6574\u6570" } } };
880
+ }
881
+ const { tasks, total } = await ctx.db.listTasks(userId, { status, limit, offset });
882
+ const userKey = deriveUserEncryptionKey(userId, ctx.encryptionKey);
883
+ const decryptedTasks = tasks.map((task) => {
884
+ const decrypted = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
885
+ return {
886
+ id: task.id,
887
+ uuid: task.uuid,
888
+ contactName: decrypted.contactName,
889
+ messageType: task.message_type,
890
+ messageSubtype: decrypted.messageSubtype,
891
+ nextSendAt: task.next_send_at,
892
+ recurrenceType: decrypted.recurrenceType,
893
+ status: task.status,
894
+ retryCount: task.retry_count,
895
+ createdAt: task.created_at,
896
+ updatedAt: task.updated_at
897
+ };
898
+ });
899
+ const responsePayload = {
900
+ tasks: decryptedTasks,
901
+ pagination: { total, limit, offset, hasMore: offset + limit < total }
902
+ };
903
+ const encryptedResponse = encryptPayload(responsePayload, userKey);
904
+ return {
905
+ status: 200,
906
+ body: {
907
+ success: true,
908
+ encrypted: true,
909
+ version: 1,
910
+ data: encryptedResponse
911
+ }
912
+ };
913
+ }
914
+ return { GET };
915
+ }
916
+
917
+ // src/server/index.js
918
+ function normalizeVapidSubject(email) {
919
+ const trimmedEmail = String(email || "").trim();
920
+ if (!trimmedEmail) return "";
921
+ return /^mailto:/i.test(trimmedEmail) ? trimmedEmail : `mailto:${trimmedEmail}`;
922
+ }
923
+ async function createReiServer(config) {
924
+ if (!config) throw new Error("[rei-standard-amsg-server] config is required");
925
+ if (!config.encryptionKey) throw new Error("[rei-standard-amsg-server] encryptionKey is required");
926
+ const adapter = await createAdapter(config.db);
927
+ let webpushModule;
928
+ try {
929
+ const webpushImport = await Promise.resolve().then(() => _interopRequireWildcard(require("web-push")));
930
+ webpushModule = webpushImport.default || webpushImport;
931
+ } catch (_err) {
932
+ throw new Error(
933
+ "[rei-standard-amsg-server] web-push is required. Install it with: npm install web-push"
934
+ );
935
+ }
936
+ const vapid = config.vapid || {};
937
+ if (vapid.email && vapid.publicKey && vapid.privateKey) {
938
+ webpushModule.setVapidDetails(
939
+ normalizeVapidSubject(vapid.email),
940
+ vapid.publicKey,
941
+ vapid.privateKey
942
+ );
943
+ }
944
+ const ctx = {
945
+ db: adapter,
946
+ encryptionKey: config.encryptionKey,
947
+ cronSecret: config.cronSecret || "",
948
+ initSecret: config.initSecret || "",
949
+ vapid: {
950
+ email: vapid.email || "",
951
+ publicKey: vapid.publicKey || "",
952
+ privateKey: vapid.privateKey || ""
953
+ },
954
+ webpush: webpushModule
955
+ };
956
+ return {
957
+ handlers: {
958
+ initDatabase: createInitDatabaseHandler(ctx),
959
+ getMasterKey: createGetMasterKeyHandler(ctx),
960
+ scheduleMessage: createScheduleMessageHandler(ctx),
961
+ sendNotifications: createSendNotificationsHandler(ctx),
962
+ updateMessage: createUpdateMessageHandler(ctx),
963
+ cancelMessage: createCancelMessageHandler(ctx),
964
+ messages: createMessagesHandler(ctx)
965
+ },
966
+ adapter
967
+ };
968
+ }
969
+
970
+
971
+
972
+
973
+
974
+
975
+
976
+
977
+
978
+
979
+
980
+ exports.createAdapter = createAdapter; exports.createReiServer = createReiServer; exports.decryptFromStorage = decryptFromStorage; exports.decryptPayload = decryptPayload; exports.deriveUserEncryptionKey = deriveUserEncryptionKey; exports.encryptForStorage = encryptForStorage; exports.isValidISO8601 = isValidISO8601; exports.isValidUUID = isValidUUID; exports.isValidUrl = isValidUrl; exports.validateScheduleMessagePayload = validateScheduleMessagePayload;