@rei-standard/amsg-server 1.2.1 → 2.0.1

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.mjs CHANGED
@@ -1,7 +1,3 @@
1
- import {
2
- REQUIRED_COLUMNS
3
- } from "./chunk-H5EQVHAY.mjs";
4
-
5
1
  // src/server/adapters/factory.js
6
2
  async function createAdapter(config) {
7
3
  if (!config || !config.driver) {
@@ -16,11 +12,11 @@ async function createAdapter(config) {
16
12
  }
17
13
  switch (config.driver) {
18
14
  case "neon": {
19
- const { NeonAdapter } = await import("./neon-YS3VCCSU.mjs");
15
+ const { NeonAdapter } = await import("./neon-CU5N3CSW.mjs");
20
16
  return new NeonAdapter(config.connectionString);
21
17
  }
22
18
  case "pg": {
23
- const { PgAdapter } = await import("./pg-2CLYF2XS.mjs");
19
+ const { PgAdapter } = await import("./pg-QO6NKTGL.mjs");
24
20
  return new PgAdapter(config.connectionString);
25
21
  }
26
22
  default:
@@ -30,123 +26,6 @@ async function createAdapter(config) {
30
26
  }
31
27
  }
32
28
 
33
- // src/server/handlers/init-database.js
34
- function createInitDatabaseHandler(ctx) {
35
- async function GET() {
36
- const result = await ctx.db.initSchema();
37
- const columnNames = result.columns.map((c) => c.name);
38
- const missingColumns = REQUIRED_COLUMNS.filter((col) => !columnNames.includes(col));
39
- if (missingColumns.length > 0) {
40
- console.warn("[init-database] \u26A0\uFE0F Missing columns:", missingColumns);
41
- }
42
- return {
43
- status: 200,
44
- body: {
45
- success: true,
46
- message: "\u6570\u636E\u5E93\u521D\u59CB\u5316\u6210\u529F\uFF01\u5EFA\u8BAE\u7ACB\u5373\u5220\u9664\u6B64 API \u6587\u4EF6\u3002",
47
- data: {
48
- tables: ["scheduled_messages", "system_config"],
49
- columnsCreated: result.columnsCreated,
50
- indexesCreated: result.indexesCreated,
51
- indexesFailed: result.indexesFailed,
52
- details: { columns: result.columns, indexes: result.indexes },
53
- nextSteps: [
54
- "1. \u8C03\u7528 /api/v1/init-master-key \u4E00\u6B21\u6027\u751F\u6210\u4E3B\u5BC6\u94A5\u5E76\u79BB\u7EBF\u4FDD\u5B58",
55
- "2. \u5BA2\u6237\u7AEF\u4F7F\u7528 UUID v4 \u4F5C\u4E3A X-User-Id",
56
- "3. \u5BA2\u6237\u7AEF\u8C03\u7528 /api/v1/get-user-key \u83B7\u53D6 userKey \u5E76\u7F13\u5B58"
57
- ]
58
- }
59
- }
60
- };
61
- }
62
- return { GET };
63
- }
64
-
65
- // src/server/handlers/init-master-key.js
66
- import { createHash, randomBytes } from "crypto";
67
- function makeFingerprint(masterKey) {
68
- return createHash("sha256").update(masterKey).digest("hex").slice(0, 16);
69
- }
70
- function createInitMasterKeyHandler(ctx) {
71
- async function POST() {
72
- const masterKey = randomBytes(32).toString("hex");
73
- const inserted = await ctx.db.setMasterKeyOnce(masterKey);
74
- if (!inserted) {
75
- return {
76
- status: 409,
77
- body: {
78
- success: false,
79
- error: {
80
- code: "MASTER_KEY_ALREADY_INITIALIZED",
81
- message: "\u4E3B\u5BC6\u94A5\u5DF2\u521D\u59CB\u5316\uFF0C\u65E0\u6CD5\u518D\u6B21\u83B7\u53D6"
82
- }
83
- }
84
- };
85
- }
86
- return {
87
- status: 201,
88
- body: {
89
- success: true,
90
- data: {
91
- masterKey,
92
- fingerprint: makeFingerprint(masterKey),
93
- version: 1
94
- }
95
- }
96
- };
97
- }
98
- return { POST };
99
- }
100
-
101
- // src/server/lib/encryption.js
102
- import { createCipheriv, createDecipheriv, createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
103
- function deriveUserEncryptionKey(userId, masterKey) {
104
- return createHash2("sha256").update(masterKey + userId).digest("hex").slice(0, 64);
105
- }
106
- function decryptPayload(encryptedPayload, encryptionKey) {
107
- const { iv, authTag, encryptedData } = encryptedPayload;
108
- const decipher = createDecipheriv(
109
- "aes-256-gcm",
110
- Buffer.from(encryptionKey, "hex"),
111
- Buffer.from(iv, "base64")
112
- );
113
- decipher.setAuthTag(Buffer.from(authTag, "base64"));
114
- const decrypted = Buffer.concat([
115
- decipher.update(Buffer.from(encryptedData, "base64")),
116
- decipher.final()
117
- ]);
118
- return JSON.parse(decrypted.toString("utf8"));
119
- }
120
- function encryptPayload(payload, encryptionKey) {
121
- const plaintext = typeof payload === "string" ? payload : JSON.stringify(payload);
122
- const iv = randomBytes2(12);
123
- const cipher = createCipheriv("aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
124
- const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
125
- const authTag = cipher.getAuthTag();
126
- return {
127
- iv: iv.toString("base64"),
128
- authTag: authTag.toString("base64"),
129
- encryptedData: encrypted.toString("base64")
130
- };
131
- }
132
- function encryptForStorage(text, encryptionKey) {
133
- const iv = randomBytes2(16);
134
- const cipher = createCipheriv("aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
135
- const encrypted = cipher.update(text, "utf8", "hex") + cipher.final("hex");
136
- const authTag = cipher.getAuthTag();
137
- return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
138
- }
139
- function decryptFromStorage(encryptedText, encryptionKey) {
140
- const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(":");
141
- const decipher = createDecipheriv(
142
- "aes-256-gcm",
143
- Buffer.from(encryptionKey, "hex"),
144
- Buffer.from(ivHex, "hex")
145
- );
146
- decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
147
- return decipher.update(encryptedDataHex, "hex", "utf8") + decipher.final("utf8");
148
- }
149
-
150
29
  // src/server/lib/validation.js
151
30
  function isValidISO8601(dateString) {
152
31
  const date = new Date(dateString);
@@ -187,6 +66,9 @@ function validateScheduleMessagePayload(payload) {
187
66
  if (payload.recurrenceType && !["none", "daily", "weekly"].includes(payload.recurrenceType)) {
188
67
  return { valid: false, errorCode: "INVALID_PARAMETERS", errorMessage: "\u7F3A\u5C11\u5FC5\u9700\u53C2\u6570\u6216\u53C2\u6570\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } };
189
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
+ }
190
72
  if (payload.messageType === "fixed") {
191
73
  if (!payload.userMessage) {
192
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)"] } };
@@ -224,55 +106,6 @@ function validateScheduleMessagePayload(payload) {
224
106
  return { valid: true };
225
107
  }
226
108
 
227
- // src/server/handlers/get-user-key.js
228
- function createGetUserKeyHandler(ctx) {
229
- async function GET(headers) {
230
- const userId = headers["x-user-id"];
231
- if (!userId) {
232
- return {
233
- status: 400,
234
- body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } }
235
- };
236
- }
237
- if (!isValidUUIDv4(userId)) {
238
- return {
239
- status: 400,
240
- body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } }
241
- };
242
- }
243
- const masterKey = await ctx.db.getMasterKey();
244
- if (!masterKey) {
245
- return {
246
- status: 503,
247
- body: { success: false, error: { code: "MASTER_KEY_NOT_INITIALIZED", message: "\u4E3B\u5BC6\u94A5\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 /api/v1/init-master-key" } }
248
- };
249
- }
250
- return {
251
- status: 200,
252
- body: {
253
- success: true,
254
- data: {
255
- userKey: deriveUserEncryptionKey(userId, masterKey),
256
- version: 1
257
- }
258
- }
259
- };
260
- }
261
- return { GET };
262
- }
263
-
264
- // src/server/handlers/schedule-message.js
265
- import { randomUUID as randomUUID2 } from "crypto";
266
-
267
- // src/server/lib/db-errors.js
268
- function isUniqueViolation(error) {
269
- if (!error || typeof error !== "object") return false;
270
- const code = error.code;
271
- if (code === "23505") return true;
272
- const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
273
- return message.includes("duplicate key") || message.includes("unique constraint");
274
- }
275
-
276
109
  // src/server/lib/request.js
277
110
  var REQUEST_ERRORS = {
278
111
  INVALID_JSON: { code: "INVALID_JSON", message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u6709\u6548\u7684 JSON" },
@@ -295,6 +128,12 @@ function parseBodyAsObject(body, options = {}) {
295
128
  }
296
129
  return { ok: true, data: parsed };
297
130
  }
131
+ function parseJsonBody(body) {
132
+ return parseBodyAsObject(body, {
133
+ invalidJson: REQUEST_ERRORS.INVALID_JSON,
134
+ invalidType: REQUEST_ERRORS.INVALID_REQUEST_BODY
135
+ });
136
+ }
298
137
  function isPlainObject(value) {
299
138
  return typeof value === "object" && value !== null && !Array.isArray(value);
300
139
  }
@@ -315,14 +154,240 @@ function parseEncryptedBody(body) {
315
154
  }
316
155
  return parsedBody;
317
156
  }
157
+ function getHeader(headers = {}, name) {
158
+ if (!headers || typeof headers !== "object") {
159
+ return "";
160
+ }
161
+ const lowerName = String(name || "").toLowerCase();
162
+ if (!lowerName) return "";
163
+ for (const [key, value] of Object.entries(headers)) {
164
+ if (String(key).toLowerCase() === lowerName) {
165
+ return String(value || "").trim();
166
+ }
167
+ }
168
+ return "";
169
+ }
170
+
171
+ // src/server/handlers/init-tenant.js
172
+ function createInitTenantHandler(ctx) {
173
+ async function POST(headers, body) {
174
+ const initSecret = getHeader(headers, "x-init-secret");
175
+ if (ctx.tenant.initSecret && initSecret !== ctx.tenant.initSecret) {
176
+ return {
177
+ status: 401,
178
+ body: {
179
+ success: false,
180
+ error: {
181
+ code: "INVALID_INIT_AUTH",
182
+ message: "\u521D\u59CB\u5316\u9274\u6743\u5931\u8D25"
183
+ }
184
+ }
185
+ };
186
+ }
187
+ const parsed = parseJsonBody(body);
188
+ if (!parsed.ok) {
189
+ return {
190
+ status: 400,
191
+ body: {
192
+ success: false,
193
+ error: parsed.error
194
+ }
195
+ };
196
+ }
197
+ const payload = parsed.data;
198
+ const tenantId = payload.tenantId || void 0;
199
+ const driver = payload.driver;
200
+ const databaseUrl = payload.databaseUrl || payload.connectionString;
201
+ if (tenantId && !isValidUUIDv4(tenantId)) {
202
+ return {
203
+ status: 400,
204
+ body: {
205
+ success: false,
206
+ error: {
207
+ code: "INVALID_TENANT_ID",
208
+ message: "tenantId \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F"
209
+ }
210
+ }
211
+ };
212
+ }
213
+ if (!databaseUrl || !driver) {
214
+ return {
215
+ status: 400,
216
+ body: {
217
+ success: false,
218
+ error: {
219
+ code: "INVALID_PARAMETERS",
220
+ message: "\u7F3A\u5C11 driver \u6216 databaseUrl \u53C2\u6570"
221
+ }
222
+ }
223
+ };
224
+ }
225
+ try {
226
+ const result = await ctx.tenantManager.initializeTenant({
227
+ tenantId,
228
+ driver,
229
+ connectionString: databaseUrl
230
+ });
231
+ return {
232
+ status: 201,
233
+ body: {
234
+ success: true,
235
+ data: {
236
+ tenantId: result.tenantId,
237
+ tenantToken: result.tenantToken,
238
+ cronToken: result.cronToken,
239
+ cronWebhookUrl: result.cronWebhookUrl,
240
+ masterKeyFingerprint: result.masterKeyFingerprint
241
+ }
242
+ }
243
+ };
244
+ } catch (error) {
245
+ if (error.message === "INVALID_DRIVER") {
246
+ return {
247
+ status: 400,
248
+ body: {
249
+ success: false,
250
+ error: {
251
+ code: "INVALID_DRIVER",
252
+ message: "driver \u5FC5\u987B\u662F neon \u6216 pg"
253
+ }
254
+ }
255
+ };
256
+ }
257
+ if (error.message === "INVALID_DATABASE_URL") {
258
+ return {
259
+ status: 400,
260
+ body: {
261
+ success: false,
262
+ error: {
263
+ code: "INVALID_DATABASE_URL",
264
+ message: "databaseUrl \u4E0D\u80FD\u4E3A\u7A7A"
265
+ }
266
+ }
267
+ };
268
+ }
269
+ if (error.message === "TENANT_ALREADY_INITIALIZED") {
270
+ return {
271
+ status: 409,
272
+ body: {
273
+ success: false,
274
+ error: {
275
+ code: "TENANT_ALREADY_INITIALIZED",
276
+ message: "tenantId \u5DF2\u5B58\u5728\uFF0C\u4E0D\u80FD\u91CD\u590D\u521D\u59CB\u5316"
277
+ }
278
+ }
279
+ };
280
+ }
281
+ throw error;
282
+ }
283
+ }
284
+ return { POST };
285
+ }
286
+
287
+ // src/server/lib/encryption.js
288
+ import { createCipheriv, createDecipheriv, createHash, randomBytes } from "crypto";
289
+ function deriveUserEncryptionKey(userId, masterKey) {
290
+ return createHash("sha256").update(masterKey + userId).digest("hex").slice(0, 64);
291
+ }
292
+ function decryptPayload(encryptedPayload, encryptionKey) {
293
+ const { iv, authTag, encryptedData } = encryptedPayload;
294
+ const decipher = createDecipheriv(
295
+ "aes-256-gcm",
296
+ Buffer.from(encryptionKey, "hex"),
297
+ Buffer.from(iv, "base64")
298
+ );
299
+ decipher.setAuthTag(Buffer.from(authTag, "base64"));
300
+ const decrypted = Buffer.concat([
301
+ decipher.update(Buffer.from(encryptedData, "base64")),
302
+ decipher.final()
303
+ ]);
304
+ return JSON.parse(decrypted.toString("utf8"));
305
+ }
306
+ function encryptPayload(payload, encryptionKey) {
307
+ const plaintext = typeof payload === "string" ? payload : JSON.stringify(payload);
308
+ const iv = randomBytes(12);
309
+ const cipher = createCipheriv("aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
310
+ const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
311
+ const authTag = cipher.getAuthTag();
312
+ return {
313
+ iv: iv.toString("base64"),
314
+ authTag: authTag.toString("base64"),
315
+ encryptedData: encrypted.toString("base64")
316
+ };
317
+ }
318
+ function encryptForStorage(text, encryptionKey) {
319
+ const iv = randomBytes(16);
320
+ const cipher = createCipheriv("aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
321
+ const encrypted = cipher.update(text, "utf8", "hex") + cipher.final("hex");
322
+ const authTag = cipher.getAuthTag();
323
+ return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
324
+ }
325
+ function decryptFromStorage(encryptedText, encryptionKey) {
326
+ const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(":");
327
+ const decipher = createDecipheriv(
328
+ "aes-256-gcm",
329
+ Buffer.from(encryptionKey, "hex"),
330
+ Buffer.from(ivHex, "hex")
331
+ );
332
+ decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
333
+ return decipher.update(encryptedDataHex, "hex", "utf8") + decipher.final("utf8");
334
+ }
335
+
336
+ // src/server/handlers/get-user-key.js
337
+ function createGetUserKeyHandler(ctx) {
338
+ async function GET(url, headers) {
339
+ const effectiveHeaders = headers || url || {};
340
+ const tenantResult = await ctx.tenantManager.resolveTenant(effectiveHeaders);
341
+ if (!tenantResult.ok) {
342
+ return tenantResult.error;
343
+ }
344
+ const { masterKey } = tenantResult.context;
345
+ const userId = getHeader(effectiveHeaders, "x-user-id");
346
+ if (!userId) {
347
+ return {
348
+ status: 400,
349
+ body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } }
350
+ };
351
+ }
352
+ if (!isValidUUIDv4(userId)) {
353
+ return {
354
+ status: 400,
355
+ body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } }
356
+ };
357
+ }
358
+ return {
359
+ status: 200,
360
+ body: {
361
+ success: true,
362
+ data: {
363
+ userKey: deriveUserEncryptionKey(userId, masterKey),
364
+ version: 1
365
+ }
366
+ }
367
+ };
368
+ }
369
+ return { GET };
370
+ }
371
+
372
+ // src/server/handlers/schedule-message.js
373
+ import { randomUUID as randomUUID2 } from "crypto";
374
+
375
+ // src/server/lib/db-errors.js
376
+ function isUniqueViolation(error) {
377
+ if (!error || typeof error !== "object") return false;
378
+ const code = error.code;
379
+ if (code === "23505") return true;
380
+ const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
381
+ return message.includes("duplicate key") || message.includes("unique constraint");
382
+ }
318
383
 
319
384
  // src/server/lib/message-processor.js
320
385
  import { randomUUID } from "crypto";
321
386
  async function processSingleMessage(task, ctx, providedMasterKey) {
322
387
  try {
323
- const masterKey = providedMasterKey || await ctx.db.getMasterKey();
388
+ const masterKey = providedMasterKey || ctx.masterKey;
324
389
  if (!masterKey) {
325
- return { success: false, messagesSent: 0, error: "MASTER_KEY_NOT_INITIALIZED" };
390
+ return { success: false, messagesSent: 0, error: "TENANT_MASTER_KEY_MISSING" };
326
391
  }
327
392
  const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
328
393
  const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
@@ -382,11 +447,11 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
382
447
  }
383
448
  async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, providedMasterKey) {
384
449
  let retryCount = 0;
385
- const masterKey = providedMasterKey || await ctx.db.getMasterKey();
450
+ const masterKey = providedMasterKey || ctx.masterKey;
386
451
  if (!masterKey) {
387
452
  return {
388
453
  success: false,
389
- error: { code: "MASTER_KEY_NOT_INITIALIZED", message: "\u4E3B\u5BC6\u94A5\u5C1A\u672A\u521D\u59CB\u5316" }
454
+ error: { code: "TENANT_MASTER_KEY_MISSING", message: "\u79DF\u6237\u4E3B\u5BC6\u94A5\u4E0D\u5B58\u5728\u6216\u914D\u7F6E\u5F02\u5E38" }
390
455
  };
391
456
  }
392
457
  while (retryCount <= maxRetries) {
@@ -443,33 +508,81 @@ async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, provided
443
508
  }
444
509
  }
445
510
  async function _callAI(payload) {
446
- const aiResponse = await fetch(payload.apiUrl, {
511
+ const normalizedApiUrl = normalizeAiApiUrl(payload.apiUrl);
512
+ const requestBody = buildAiRequestBody(payload);
513
+ const aiResponse = await fetch(normalizedApiUrl, {
447
514
  method: "POST",
448
515
  headers: {
449
516
  "Content-Type": "application/json",
450
517
  "Authorization": `Bearer ${payload.apiKey}`
451
518
  },
452
- body: JSON.stringify({
453
- model: payload.primaryModel,
454
- messages: [{ role: "user", content: payload.completePrompt }],
455
- max_tokens: 500,
456
- temperature: 0.8
457
- }),
519
+ body: JSON.stringify(requestBody),
458
520
  signal: AbortSignal.timeout(3e5)
459
521
  });
460
522
  if (!aiResponse.ok) {
461
- 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
+ );
462
531
  }
463
532
  const aiData = await aiResponse.json();
464
- 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();
465
571
  }
466
572
 
467
573
  // src/server/handlers/schedule-message.js
468
574
  function createScheduleMessageHandler(ctx) {
469
575
  async function POST(headers, body) {
470
- const isEncrypted = headers["x-payload-encrypted"] === "true";
471
- const encryptionVersion = headers["x-encryption-version"];
472
- const userId = headers["x-user-id"];
576
+ const tenantResult = await ctx.tenantManager.resolveTenant(headers);
577
+ if (!tenantResult.ok) {
578
+ return tenantResult.error;
579
+ }
580
+ const tenantCtx = tenantResult.context;
581
+ const db = tenantCtx.db;
582
+ const masterKey = tenantCtx.masterKey;
583
+ const isEncrypted = getHeader(headers, "x-payload-encrypted") === "true";
584
+ const encryptionVersion = getHeader(headers, "x-encryption-version");
585
+ const userId = getHeader(headers, "x-user-id");
473
586
  if (!isEncrypted) {
474
587
  return { status: 400, body: { success: false, error: { code: "ENCRYPTION_REQUIRED", message: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6" } } };
475
588
  }
@@ -482,10 +595,6 @@ function createScheduleMessageHandler(ctx) {
482
595
  if (encryptionVersion !== "1") {
483
596
  return { status: 400, body: { success: false, error: { code: "UNSUPPORTED_ENCRYPTION_VERSION", message: "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301" } } };
484
597
  }
485
- const masterKey = await ctx.db.getMasterKey();
486
- if (!masterKey) {
487
- return { status: 503, body: { success: false, error: { code: "MASTER_KEY_NOT_INITIALIZED", message: "\u4E3B\u5BC6\u94A5\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 /api/v1/init-master-key" } } };
488
- }
489
598
  const parsedBody = parseEncryptedBody(body);
490
599
  if (!parsedBody.ok) {
491
600
  return { status: 400, body: { success: false, error: parsedBody.error } };
@@ -526,6 +635,7 @@ function createScheduleMessageHandler(ctx) {
526
635
  apiKey: payload.apiKey || null,
527
636
  primaryModel: payload.primaryModel || null,
528
637
  completePrompt: payload.completePrompt || null,
638
+ maxTokens: payload.maxTokens ?? null,
529
639
  pushSubscription: payload.pushSubscription,
530
640
  metadata: payload.metadata || {}
531
641
  };
@@ -553,7 +663,7 @@ function createScheduleMessageHandler(ctx) {
553
663
  }
554
664
  let dbResult;
555
665
  try {
556
- dbResult = await ctx.db.createTask({
666
+ dbResult = await db.createTask({
557
667
  user_id: userId,
558
668
  uuid: taskUuid,
559
669
  encrypted_payload: encryptedPayload,
@@ -580,7 +690,11 @@ function createScheduleMessageHandler(ctx) {
580
690
  }
581
691
  if (payload.messageType === "instant") {
582
692
  try {
583
- const sendResult = await processMessagesByUuid(taskUuid, ctx, 2, userId, masterKey);
693
+ const sendResult = await processMessagesByUuid(taskUuid, {
694
+ ...ctx,
695
+ db,
696
+ masterKey
697
+ }, 2, userId, masterKey);
584
698
  if (!sendResult.success) {
585
699
  return { status: 500, body: { success: false, error: { code: "MESSAGE_SEND_FAILED", message: "\u6D88\u606F\u53D1\u9001\u5931\u8D25", details: sendResult.error } } };
586
700
  }
@@ -622,7 +736,19 @@ function createScheduleMessageHandler(ctx) {
622
736
 
623
737
  // src/server/handlers/send-notifications.js
624
738
  function createSendNotificationsHandler(ctx) {
625
- async function POST(headers) {
739
+ async function POST(urlOrHeaders, maybeHeaders) {
740
+ const url = typeof urlOrHeaders === "string" ? urlOrHeaders : "";
741
+ const headers = maybeHeaders || (typeof urlOrHeaders === "object" ? urlOrHeaders : {});
742
+ const tenantResult = await ctx.tenantManager.resolveTenant(headers, {
743
+ allowCronToken: true,
744
+ url
745
+ });
746
+ if (!tenantResult.ok) {
747
+ return tenantResult.error;
748
+ }
749
+ const tenantCtx = tenantResult.context;
750
+ const db = tenantCtx.db;
751
+ const masterKey = tenantCtx.masterKey;
626
752
  if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
627
753
  return {
628
754
  status: 500,
@@ -642,26 +768,8 @@ function createSendNotificationsHandler(ctx) {
642
768
  }
643
769
  };
644
770
  }
645
- const authHeader = (headers["authorization"] || "").trim();
646
- const expectedAuth = `Bearer ${ctx.cronSecret}`;
647
- if (authHeader !== expectedAuth) {
648
- return { status: 401, body: { success: false, error: { code: "UNAUTHORIZED", message: "Cron Secret \u9A8C\u8BC1\u5931\u8D25" } } };
649
- }
650
- const masterKey = await ctx.db.getMasterKey();
651
- if (!masterKey) {
652
- return {
653
- status: 503,
654
- body: {
655
- success: false,
656
- error: {
657
- code: "MASTER_KEY_NOT_INITIALIZED",
658
- message: "\u4E3B\u5BC6\u94A5\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 /api/v1/init-master-key"
659
- }
660
- }
661
- };
662
- }
663
771
  const startTime = Date.now();
664
- const tasks = await ctx.db.getPendingTasks(50);
772
+ const tasks = await db.getPendingTasks(50);
665
773
  const MAX_CONCURRENT = 8;
666
774
  const results = {
667
775
  totalTasks: tasks.length,
@@ -675,11 +783,11 @@ function createSendNotificationsHandler(ctx) {
675
783
  results.failedCount++;
676
784
  try {
677
785
  if (task.retry_count >= 3) {
678
- await ctx.db.updateTaskById(task.id, { status: "failed" });
786
+ await db.updateTaskById(task.id, { status: "failed" });
679
787
  results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "permanently_failed" });
680
788
  } else {
681
789
  const nextRetryTime = new Date(Date.now() + (task.retry_count + 1) * 2 * 60 * 1e3);
682
- await ctx.db.updateTaskById(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
790
+ await db.updateTaskById(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
683
791
  results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count + 1, nextRetryAt: nextRetryTime.toISOString() });
684
792
  }
685
793
  } catch (updateError) {
@@ -695,7 +803,7 @@ function createSendNotificationsHandler(ctx) {
695
803
  results.failedCount++;
696
804
  let markedSent = false;
697
805
  try {
698
- await ctx.db.updateTaskById(task.id, { status: "sent", retry_count: 0 });
806
+ await db.updateTaskById(task.id, { status: "sent", retry_count: 0 });
699
807
  markedSent = true;
700
808
  } catch (_markSentError) {
701
809
  markedSent = false;
@@ -710,7 +818,11 @@ function createSendNotificationsHandler(ctx) {
710
818
  async function processTask(task) {
711
819
  let sendResult;
712
820
  try {
713
- sendResult = await processSingleMessage(task, ctx);
821
+ sendResult = await processSingleMessage(task, {
822
+ ...ctx,
823
+ db,
824
+ masterKey
825
+ }, masterKey);
714
826
  } catch (error) {
715
827
  await handleDeliveryFailure(task, error.message || "\u6D88\u606F\u53D1\u9001\u5931\u8D25");
716
828
  return;
@@ -723,7 +835,7 @@ function createSendNotificationsHandler(ctx) {
723
835
  const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
724
836
  const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
725
837
  if (decryptedPayload.recurrenceType === "none") {
726
- await ctx.db.deleteTaskById(task.id);
838
+ await db.deleteTaskById(task.id);
727
839
  results.deletedOnceOffTasks++;
728
840
  } else {
729
841
  let nextSendAt;
@@ -733,7 +845,7 @@ function createSendNotificationsHandler(ctx) {
733
845
  } else if (decryptedPayload.recurrenceType === "weekly") {
734
846
  nextSendAt = new Date(currentSendAt.getTime() + 7 * 24 * 60 * 60 * 1e3);
735
847
  }
736
- await ctx.db.updateTaskById(task.id, { next_send_at: nextSendAt.toISOString(), retry_count: 0 });
848
+ await db.updateTaskById(task.id, { next_send_at: nextSendAt.toISOString(), retry_count: 0 });
737
849
  results.updatedRecurringTasks++;
738
850
  }
739
851
  results.successCount++;
@@ -757,7 +869,7 @@ function createSendNotificationsHandler(ctx) {
757
869
  await Promise.race(processing);
758
870
  }
759
871
  }
760
- await ctx.db.cleanupOldTasks(7);
872
+ await db.cleanupOldTasks(7);
761
873
  const executionTime = Date.now() - startTime;
762
874
  return {
763
875
  status: 200,
@@ -784,30 +896,33 @@ function createSendNotificationsHandler(ctx) {
784
896
  // src/server/handlers/update-message.js
785
897
  function createUpdateMessageHandler(ctx) {
786
898
  async function PUT(url, headers, body) {
899
+ const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
900
+ if (!tenantResult.ok) {
901
+ return tenantResult.error;
902
+ }
903
+ const tenantCtx = tenantResult.context;
904
+ const db = tenantCtx.db;
905
+ const masterKey = tenantCtx.masterKey;
787
906
  const u = new URL(url, "https://dummy");
788
907
  const taskUuid = u.searchParams.get("id");
789
908
  if (!taskUuid) {
790
909
  return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
791
910
  }
792
- const userId = headers["x-user-id"];
911
+ const userId = getHeader(headers, "x-user-id");
793
912
  if (!userId) {
794
913
  return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
795
914
  }
796
915
  if (!isValidUUIDv4(userId)) {
797
916
  return { status: 400, body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } } };
798
917
  }
799
- const isEncrypted = headers["x-payload-encrypted"] === "true";
800
- const encryptionVersion = headers["x-encryption-version"];
918
+ const isEncrypted = getHeader(headers, "x-payload-encrypted") === "true";
919
+ const encryptionVersion = getHeader(headers, "x-encryption-version");
801
920
  if (!isEncrypted) {
802
921
  return { status: 400, body: { success: false, error: { code: "ENCRYPTION_REQUIRED", message: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6" } } };
803
922
  }
804
923
  if (encryptionVersion !== "1") {
805
924
  return { status: 400, body: { success: false, error: { code: "UNSUPPORTED_ENCRYPTION_VERSION", message: "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301" } } };
806
925
  }
807
- const masterKey = await ctx.db.getMasterKey();
808
- if (!masterKey) {
809
- return { status: 503, body: { success: false, error: { code: "MASTER_KEY_NOT_INITIALIZED", message: "\u4E3B\u5BC6\u94A5\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 /api/v1/init-master-key" } } };
810
- }
811
926
  const parsedBody = parseEncryptedBody(body);
812
927
  if (!parsedBody.ok) {
813
928
  return { status: 400, body: { success: false, error: parsedBody.error } };
@@ -829,9 +944,12 @@ function createUpdateMessageHandler(ctx) {
829
944
  if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
830
945
  return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
831
946
  }
832
- const existingTask = await ctx.db.getTaskByUuid(taskUuid, userId);
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
+ }
950
+ const existingTask = await db.getTaskByUuid(taskUuid, userId);
833
951
  if (!existingTask) {
834
- const taskStatus = await ctx.db.getTaskStatus(taskUuid, userId);
952
+ const taskStatus = await db.getTaskStatus(taskUuid, userId);
835
953
  if (!taskStatus) {
836
954
  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" } } };
837
955
  }
@@ -844,11 +962,12 @@ function createUpdateMessageHandler(ctx) {
844
962
  ...updates.userMessage && { userMessage: updates.userMessage },
845
963
  ...updates.recurrenceType && { recurrenceType: updates.recurrenceType },
846
964
  ...updates.avatarUrl && { avatarUrl: updates.avatarUrl },
847
- ...updates.metadata && { metadata: updates.metadata }
965
+ ...updates.metadata && { metadata: updates.metadata },
966
+ ...Object.prototype.hasOwnProperty.call(updates, "maxTokens") && { maxTokens: updates.maxTokens ?? null }
848
967
  };
849
968
  const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
850
969
  const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
851
- const result = await ctx.db.updateTaskByUuid(taskUuid, userId, encryptedPayload, extraFields);
970
+ const result = await db.updateTaskByUuid(taskUuid, userId, encryptedPayload, extraFields);
852
971
  if (!result) {
853
972
  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" } } };
854
973
  }
@@ -870,19 +989,24 @@ function createUpdateMessageHandler(ctx) {
870
989
  // src/server/handlers/cancel-message.js
871
990
  function createCancelMessageHandler(ctx) {
872
991
  async function DELETE(url, headers) {
992
+ const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
993
+ if (!tenantResult.ok) {
994
+ return tenantResult.error;
995
+ }
996
+ const db = tenantResult.context.db;
873
997
  const u = new URL(url, "https://dummy");
874
998
  const taskUuid = u.searchParams.get("id");
875
999
  if (!taskUuid) {
876
1000
  return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
877
1001
  }
878
- const userId = headers["x-user-id"];
1002
+ const userId = getHeader(headers, "x-user-id");
879
1003
  if (!userId) {
880
1004
  return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
881
1005
  }
882
1006
  if (!isValidUUIDv4(userId)) {
883
1007
  return { status: 400, body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } } };
884
1008
  }
885
- const deleted = await ctx.db.deleteTaskByUuid(taskUuid, userId);
1009
+ const deleted = await db.deleteTaskByUuid(taskUuid, userId);
886
1010
  if (!deleted) {
887
1011
  return {
888
1012
  status: 404,
@@ -903,7 +1027,14 @@ function createCancelMessageHandler(ctx) {
903
1027
  // src/server/handlers/messages.js
904
1028
  function createMessagesHandler(ctx) {
905
1029
  async function GET(url, headers) {
906
- const userId = headers["x-user-id"];
1030
+ const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
1031
+ if (!tenantResult.ok) {
1032
+ return tenantResult.error;
1033
+ }
1034
+ const tenantCtx = tenantResult.context;
1035
+ const db = tenantCtx.db;
1036
+ const masterKey = tenantCtx.masterKey;
1037
+ const userId = getHeader(headers, "x-user-id");
907
1038
  if (!userId) {
908
1039
  return {
909
1040
  status: 400,
@@ -926,14 +1057,7 @@ function createMessagesHandler(ctx) {
926
1057
  if (isNaN(offset) || offset < 0) {
927
1058
  return { status: 400, body: { success: false, error: { code: "INVALID_PARAMETERS", message: "offset \u53C2\u6570\u65E0\u6548\uFF0C\u5FC5\u987B\u4E3A\u975E\u8D1F\u6574\u6570" } } };
928
1059
  }
929
- const { tasks, total } = await ctx.db.listTasks(userId, { status, limit, offset });
930
- const masterKey = await ctx.db.getMasterKey();
931
- if (!masterKey) {
932
- return {
933
- status: 503,
934
- body: { success: false, error: { code: "MASTER_KEY_NOT_INITIALIZED", message: "\u4E3B\u5BC6\u94A5\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 /api/v1/init-master-key" } }
935
- };
936
- }
1060
+ const { tasks, total } = await db.listTasks(userId, { status, limit, offset });
937
1061
  const userKey = deriveUserEncryptionKey(userId, masterKey);
938
1062
  const decryptedTasks = tasks.map((task) => {
939
1063
  const decrypted = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
@@ -969,6 +1093,318 @@ function createMessagesHandler(ctx) {
969
1093
  return { GET };
970
1094
  }
971
1095
 
1096
+ // src/server/tenant/blob-store.js
1097
+ import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
1098
+ var inMemoryNamespaces = /* @__PURE__ */ new Map();
1099
+ function base64UrlEncode(input) {
1100
+ return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1101
+ }
1102
+ function base64UrlDecode(input) {
1103
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
1104
+ const padLength = (4 - normalized.length % 4) % 4;
1105
+ return Buffer.from(normalized + "=".repeat(padLength), "base64");
1106
+ }
1107
+ function getKekBuffer(kek) {
1108
+ const value = String(kek || "").trim();
1109
+ if (!value) {
1110
+ throw new Error("[rei-standard-amsg-server] tenant.kek is required");
1111
+ }
1112
+ return createHash2("sha256").update(value).digest();
1113
+ }
1114
+ function encryptConfig(config, kekBuffer) {
1115
+ const iv = randomBytes2(12);
1116
+ const cipher = createCipheriv2("aes-256-gcm", kekBuffer, iv);
1117
+ const plaintext = Buffer.from(JSON.stringify(config), "utf8");
1118
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1119
+ const authTag = cipher.getAuthTag();
1120
+ return `v1.${base64UrlEncode(iv)}.${base64UrlEncode(authTag)}.${base64UrlEncode(ciphertext)}`;
1121
+ }
1122
+ function decryptConfig(encrypted, kekBuffer) {
1123
+ const parts = String(encrypted || "").split(".");
1124
+ if (parts.length !== 4 || parts[0] !== "v1") {
1125
+ throw new Error("INVALID_TENANT_CONFIG");
1126
+ }
1127
+ const iv = base64UrlDecode(parts[1]);
1128
+ const authTag = base64UrlDecode(parts[2]);
1129
+ const ciphertext = base64UrlDecode(parts[3]);
1130
+ const decipher = createDecipheriv2("aes-256-gcm", kekBuffer, iv);
1131
+ decipher.setAuthTag(authTag);
1132
+ const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
1133
+ return JSON.parse(plaintext);
1134
+ }
1135
+ function createInMemoryStore(namespace) {
1136
+ if (!inMemoryNamespaces.has(namespace)) {
1137
+ inMemoryNamespaces.set(namespace, /* @__PURE__ */ new Map());
1138
+ }
1139
+ const namespaceMap = inMemoryNamespaces.get(namespace);
1140
+ return {
1141
+ async get(key) {
1142
+ return namespaceMap.has(key) ? namespaceMap.get(key) : null;
1143
+ },
1144
+ async set(key, value) {
1145
+ namespaceMap.set(key, value);
1146
+ },
1147
+ async delete(key) {
1148
+ namespaceMap.delete(key);
1149
+ }
1150
+ };
1151
+ }
1152
+ async function resolveBlobStore(namespace) {
1153
+ if (globalThis.__REI_BLOB_STORE__ && typeof globalThis.__REI_BLOB_STORE__.get === "function") {
1154
+ return globalThis.__REI_BLOB_STORE__;
1155
+ }
1156
+ try {
1157
+ const blobModule = await import("@netlify/blobs");
1158
+ const getStore = blobModule.getStore || blobModule.default?.getStore;
1159
+ if (typeof getStore === "function") {
1160
+ return getStore({ name: namespace });
1161
+ }
1162
+ } catch (_error) {
1163
+ }
1164
+ return createInMemoryStore(namespace);
1165
+ }
1166
+ function createTenantBlobStore(options) {
1167
+ const namespace = String(options.namespace || "rei-tenants").trim() || "rei-tenants";
1168
+ const kekBuffer = getKekBuffer(options.kek);
1169
+ function getTenantKey(tenantId) {
1170
+ return `tenant/${tenantId}`;
1171
+ }
1172
+ return {
1173
+ async getTenantConfig(tenantId) {
1174
+ const store = await resolveBlobStore(namespace);
1175
+ const encrypted = await store.get(getTenantKey(tenantId), { type: "text" });
1176
+ if (!encrypted) return null;
1177
+ return decryptConfig(encrypted, kekBuffer);
1178
+ },
1179
+ async setTenantConfig(config) {
1180
+ const store = await resolveBlobStore(namespace);
1181
+ const encrypted = encryptConfig(config, kekBuffer);
1182
+ await store.set(getTenantKey(config.tenantId), encrypted);
1183
+ },
1184
+ async deleteTenantConfig(tenantId) {
1185
+ const store = await resolveBlobStore(namespace);
1186
+ await store.delete(getTenantKey(tenantId));
1187
+ }
1188
+ };
1189
+ }
1190
+
1191
+ // src/server/tenant/context.js
1192
+ import { createHash as createHash3, randomBytes as randomBytes3, randomUUID as randomUUID3 } from "crypto";
1193
+
1194
+ // src/server/tenant/token.js
1195
+ import { createHmac, timingSafeEqual } from "crypto";
1196
+ function base64UrlEncode2(input) {
1197
+ return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
1198
+ }
1199
+ function base64UrlDecode2(input) {
1200
+ const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
1201
+ const padLength = (4 - normalized.length % 4) % 4;
1202
+ return Buffer.from(normalized + "=".repeat(padLength), "base64");
1203
+ }
1204
+ function sign(input, secret) {
1205
+ return base64UrlEncode2(createHmac("sha256", secret).update(input).digest());
1206
+ }
1207
+ function nowEpochSeconds() {
1208
+ return Math.floor(Date.now() / 1e3);
1209
+ }
1210
+ function createTenantToken(params, secret) {
1211
+ const issuedAt = nowEpochSeconds();
1212
+ const expiresInSeconds = Number(params.expiresInSeconds || 60 * 60 * 24 * 30);
1213
+ const payload = {
1214
+ tid: params.tenantId,
1215
+ typ: params.type,
1216
+ iat: issuedAt,
1217
+ exp: issuedAt + expiresInSeconds,
1218
+ v: 1
1219
+ };
1220
+ const header = { alg: "HS256", typ: "JWT" };
1221
+ const encodedHeader = base64UrlEncode2(JSON.stringify(header));
1222
+ const encodedPayload = base64UrlEncode2(JSON.stringify(payload));
1223
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
1224
+ const signature = sign(signingInput, secret);
1225
+ return `${signingInput}.${signature}`;
1226
+ }
1227
+ function verifyTenantToken(token, secret, options = {}) {
1228
+ if (!token || typeof token !== "string") {
1229
+ throw new Error("INVALID_TENANT_AUTH");
1230
+ }
1231
+ const parts = token.split(".");
1232
+ if (parts.length !== 3) {
1233
+ throw new Error("INVALID_TENANT_AUTH");
1234
+ }
1235
+ const [encodedHeader, encodedPayload, receivedSignature] = parts;
1236
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
1237
+ const expectedSignature = sign(signingInput, secret);
1238
+ const receivedBuffer = Buffer.from(receivedSignature);
1239
+ const expectedBuffer = Buffer.from(expectedSignature);
1240
+ if (receivedBuffer.length !== expectedBuffer.length || !timingSafeEqual(receivedBuffer, expectedBuffer)) {
1241
+ throw new Error("INVALID_TENANT_AUTH");
1242
+ }
1243
+ let payload;
1244
+ try {
1245
+ payload = JSON.parse(base64UrlDecode2(encodedPayload).toString("utf8"));
1246
+ } catch {
1247
+ throw new Error("INVALID_TENANT_AUTH");
1248
+ }
1249
+ if (!payload || typeof payload !== "object" || payload.v !== 1) {
1250
+ throw new Error("INVALID_TENANT_AUTH");
1251
+ }
1252
+ if (!payload.tid || !payload.typ || !payload.exp) {
1253
+ throw new Error("INVALID_TENANT_AUTH");
1254
+ }
1255
+ if (payload.exp <= nowEpochSeconds()) {
1256
+ throw new Error("INVALID_TENANT_AUTH");
1257
+ }
1258
+ const expectedTypes = options.expectedTypes || [];
1259
+ if (expectedTypes.length > 0 && !expectedTypes.includes(payload.typ)) {
1260
+ throw new Error("INVALID_TENANT_AUTH");
1261
+ }
1262
+ return payload;
1263
+ }
1264
+ function extractBearerToken(headers = {}) {
1265
+ let authorization = "";
1266
+ for (const [key, value] of Object.entries(headers || {})) {
1267
+ if (String(key).toLowerCase() === "authorization") {
1268
+ authorization = String(value || "").trim();
1269
+ break;
1270
+ }
1271
+ }
1272
+ if (!authorization.toLowerCase().startsWith("bearer ")) {
1273
+ return "";
1274
+ }
1275
+ return authorization.slice(7).trim();
1276
+ }
1277
+
1278
+ // src/server/tenant/context.js
1279
+ function fingerprint(masterKey) {
1280
+ return createHash3("sha256").update(masterKey).digest("hex").slice(0, 16);
1281
+ }
1282
+ function sanitizeDbConfig(driver, connectionString) {
1283
+ const normalizedDriver = String(driver || "").trim();
1284
+ const normalizedConnectionString = String(connectionString || "").trim();
1285
+ if (!["neon", "pg"].includes(normalizedDriver)) {
1286
+ throw new Error("INVALID_DRIVER");
1287
+ }
1288
+ if (!normalizedConnectionString) {
1289
+ throw new Error("INVALID_DATABASE_URL");
1290
+ }
1291
+ return {
1292
+ driver: normalizedDriver,
1293
+ connectionString: normalizedConnectionString
1294
+ };
1295
+ }
1296
+ function parseTokenFromUrl(url) {
1297
+ if (!url || typeof url !== "string") {
1298
+ return "";
1299
+ }
1300
+ try {
1301
+ const parsed = new URL(url, "https://dummy.local");
1302
+ return String(parsed.searchParams.get("token") || "").trim();
1303
+ } catch {
1304
+ return "";
1305
+ }
1306
+ }
1307
+ function createTenantContextManager(options) {
1308
+ const tenantStore = options.tenantStore;
1309
+ const tokenSigningKey = String(options.tokenSigningKey || "").trim();
1310
+ const publicBaseUrl = String(options.publicBaseUrl || "").replace(/\/$/, "");
1311
+ const adapterFactory = options.adapterFactory || createAdapter;
1312
+ const adapterCache = /* @__PURE__ */ new Map();
1313
+ if (!tokenSigningKey) {
1314
+ throw new Error("[rei-standard-amsg-server] tenant.tokenSigningKey is required");
1315
+ }
1316
+ async function getOrCreateAdapter(dbConfig) {
1317
+ const cacheKey = `${dbConfig.driver}:${dbConfig.connectionString}`;
1318
+ if (!adapterCache.has(cacheKey)) {
1319
+ const adapter = await adapterFactory(dbConfig);
1320
+ adapterCache.set(cacheKey, adapter);
1321
+ }
1322
+ return adapterCache.get(cacheKey);
1323
+ }
1324
+ async function initializeTenant({ driver, connectionString, tenantId: providedTenantId }) {
1325
+ const dbConfig = sanitizeDbConfig(driver, connectionString);
1326
+ const tenantId = String(providedTenantId || randomUUID3()).trim();
1327
+ const existing = await tenantStore.getTenantConfig(tenantId);
1328
+ if (existing) {
1329
+ throw new Error("TENANT_ALREADY_INITIALIZED");
1330
+ }
1331
+ const adapter = await getOrCreateAdapter(dbConfig);
1332
+ await adapter.initSchema();
1333
+ const masterKey = randomBytes3(32).toString("hex");
1334
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1335
+ await tenantStore.setTenantConfig({
1336
+ tenantId,
1337
+ db: dbConfig,
1338
+ masterKey,
1339
+ createdAt: now,
1340
+ updatedAt: now
1341
+ });
1342
+ const tenantToken = createTenantToken({ tenantId, type: "tenant" }, tokenSigningKey);
1343
+ const cronToken = createTenantToken({ tenantId, type: "cron" }, tokenSigningKey);
1344
+ return {
1345
+ tenantId,
1346
+ masterKeyFingerprint: fingerprint(masterKey),
1347
+ tenantToken,
1348
+ cronToken,
1349
+ cronWebhookUrl: publicBaseUrl ? `${publicBaseUrl}/api/v1/send-notifications?token=${cronToken}` : ""
1350
+ };
1351
+ }
1352
+ async function resolveTenant(headers, options2 = {}) {
1353
+ const allowCronToken = options2.allowCronToken === true;
1354
+ const url = options2.url || "";
1355
+ const bearerToken = extractBearerToken(headers);
1356
+ const tokenFromQuery = allowCronToken ? parseTokenFromUrl(url) : "";
1357
+ const token = bearerToken || tokenFromQuery;
1358
+ if (!token) {
1359
+ return {
1360
+ ok: false,
1361
+ error: {
1362
+ status: 401,
1363
+ body: { success: false, error: { code: "INVALID_TENANT_AUTH", message: "\u7F3A\u5C11\u79DF\u6237\u9274\u6743\u4FE1\u606F" } }
1364
+ }
1365
+ };
1366
+ }
1367
+ let payload;
1368
+ try {
1369
+ payload = verifyTenantToken(token, tokenSigningKey, {
1370
+ expectedTypes: allowCronToken ? ["cron"] : ["tenant"]
1371
+ });
1372
+ } catch (_error) {
1373
+ return {
1374
+ ok: false,
1375
+ error: {
1376
+ status: 401,
1377
+ body: { success: false, error: { code: "INVALID_TENANT_AUTH", message: "\u79DF\u6237\u4EE4\u724C\u65E0\u6548\u6216\u5DF2\u8FC7\u671F" } }
1378
+ }
1379
+ };
1380
+ }
1381
+ const tenantConfig = await tenantStore.getTenantConfig(payload.tid);
1382
+ if (!tenantConfig) {
1383
+ return {
1384
+ ok: false,
1385
+ error: {
1386
+ status: 401,
1387
+ body: { success: false, error: { code: "INVALID_TENANT_AUTH", message: "\u79DF\u6237\u914D\u7F6E\u4E0D\u5B58\u5728\u6216\u5DF2\u5931\u6548" } }
1388
+ }
1389
+ };
1390
+ }
1391
+ const adapter = await getOrCreateAdapter(tenantConfig.db);
1392
+ return {
1393
+ ok: true,
1394
+ context: {
1395
+ tenantId: tenantConfig.tenantId,
1396
+ tokenType: payload.typ,
1397
+ db: adapter,
1398
+ masterKey: tenantConfig.masterKey
1399
+ }
1400
+ };
1401
+ }
1402
+ return {
1403
+ initializeTenant,
1404
+ resolveTenant
1405
+ };
1406
+ }
1407
+
972
1408
  // src/server/index.js
973
1409
  function normalizeVapidSubject(email) {
974
1410
  const trimmedEmail = String(email || "").trim();
@@ -977,7 +1413,7 @@ function normalizeVapidSubject(email) {
977
1413
  }
978
1414
  async function createReiServer(config) {
979
1415
  if (!config) throw new Error("[rei-standard-amsg-server] config is required");
980
- const adapter = await createAdapter(config.db);
1416
+ if (!config.tenant) throw new Error("[rei-standard-amsg-server] tenant config is required");
981
1417
  let webpushModule;
982
1418
  try {
983
1419
  const webpushImport = await import("web-push");
@@ -995,33 +1431,45 @@ async function createReiServer(config) {
995
1431
  vapid.privateKey
996
1432
  );
997
1433
  }
1434
+ const tenantStore = createTenantBlobStore({
1435
+ namespace: config.tenant.blobNamespace || "rei-tenants",
1436
+ kek: config.tenant.kek
1437
+ });
1438
+ const tenantManager = createTenantContextManager({
1439
+ tenantStore,
1440
+ tokenSigningKey: config.tenant.tokenSigningKey,
1441
+ publicBaseUrl: config.tenant.publicBaseUrl,
1442
+ adapterFactory: config.tenant.adapterFactory
1443
+ });
1444
+ const initSecret = String(config.tenant.initSecret || "").trim();
998
1445
  const ctx = {
999
- db: adapter,
1000
- cronSecret: config.cronSecret || "",
1001
1446
  vapid: {
1002
1447
  email: vapid.email || "",
1003
1448
  publicKey: vapid.publicKey || "",
1004
1449
  privateKey: vapid.privateKey || ""
1005
1450
  },
1006
- webpush: webpushModule
1451
+ webpush: webpushModule,
1452
+ tenant: {
1453
+ initSecret
1454
+ },
1455
+ tenantManager
1007
1456
  };
1008
1457
  return {
1009
1458
  handlers: {
1010
- initDatabase: createInitDatabaseHandler(ctx),
1011
- initMasterKey: createInitMasterKeyHandler(ctx),
1459
+ initTenant: createInitTenantHandler(ctx),
1012
1460
  getUserKey: createGetUserKeyHandler(ctx),
1013
1461
  scheduleMessage: createScheduleMessageHandler(ctx),
1014
1462
  sendNotifications: createSendNotificationsHandler(ctx),
1015
1463
  updateMessage: createUpdateMessageHandler(ctx),
1016
1464
  cancelMessage: createCancelMessageHandler(ctx),
1017
1465
  messages: createMessagesHandler(ctx)
1018
- },
1019
- adapter
1466
+ }
1020
1467
  };
1021
1468
  }
1022
1469
  export {
1023
1470
  createAdapter,
1024
1471
  createReiServer,
1472
+ createTenantToken,
1025
1473
  decryptFromStorage,
1026
1474
  decryptPayload,
1027
1475
  deriveUserEncryptionKey,
@@ -1030,5 +1478,6 @@ export {
1030
1478
  isValidUUID,
1031
1479
  isValidUUIDv4,
1032
1480
  isValidUrl,
1033
- validateScheduleMessagePayload
1481
+ validateScheduleMessagePayload,
1482
+ verifyTenantToken
1034
1483
  };