@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/README.md +82 -28
- package/dist/{chunk-H5EQVHAY.mjs → chunk-5ENJGVZX.mjs} +1 -36
- package/dist/{chunk-74H4WV6Q.cjs → chunk-RGECD4OH.cjs} +1 -36
- package/dist/index.cjs +702 -253
- package/dist/index.d.cts +2149 -2
- package/dist/index.d.ts +2149 -2
- package/dist/index.mjs +701 -252
- package/dist/{neon-ZQS3KT6C.cjs → neon-BFUS25UX.cjs} +7 -38
- package/dist/{neon-CK9Yty81.d.ts → neon-CJl66EGy.d.cts} +4 -38
- package/dist/{neon-YS3VCCSU.mjs → neon-CU5N3CSW.mjs} +3 -34
- package/dist/{neon-C29wdgUQ.d.cts → neon-DYvGnCzx.d.ts} +4 -38
- package/dist/{pg-DhwHwSPv.d.ts → pg-07S-u_H4.d.cts} +4 -36
- package/dist/{pg-DRLITtBW.d.cts → pg-Du-pN_UT.d.ts} +4 -36
- package/dist/{pg-2JJP632A.cjs → pg-IIH3M4OM.cjs} +7 -36
- package/dist/{pg-2CLYF2XS.mjs → pg-QO6NKTGL.mjs} +3 -32
- package/dist/schema-C8OnYk6j.d.cts +74 -0
- package/dist/schema-C8OnYk6j.d.ts +74 -0
- package/package.json +3 -2
- package/dist/index-BRvOilNH.d.cts +0 -1633
- package/dist/index-BXb6gptO.d.ts +0 -1633
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-
|
|
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-
|
|
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 ||
|
|
388
|
+
const masterKey = providedMasterKey || ctx.masterKey;
|
|
324
389
|
if (!masterKey) {
|
|
325
|
-
return { success: false, messagesSent: 0, error: "
|
|
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 ||
|
|
450
|
+
const masterKey = providedMasterKey || ctx.masterKey;
|
|
386
451
|
if (!masterKey) {
|
|
387
452
|
return {
|
|
388
453
|
success: false,
|
|
389
|
-
error: { code: "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
471
|
-
|
|
472
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
800
|
-
const encryptionVersion = headers
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|