@rei-standard/amsg-server 1.2.0 → 2.0.0-pre1
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 +71 -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 +643 -243
- package/dist/index.d.cts +2053 -2
- package/dist/index.d.ts +2053 -2
- package/dist/index.mjs +642 -242
- package/dist/{neon-ZQS3KT6C.cjs → neon-BFUS25UX.cjs} +7 -38
- package/dist/{neon-YS3VCCSU.mjs → neon-CU5N3CSW.mjs} +3 -34
- package/dist/{neon-EZ9wrhOE.d.ts → neon-Ch9Abtfp.d.cts} +4 -38
- package/dist/{neon-Drv0o9hb.d.cts → neon-DyBfYFkz.d.ts} +4 -38
- package/dist/{pg-BNsQBOUp.d.ts → pg-BpYu3O81.d.cts} +4 -36
- package/dist/{pg-BHyMmb-s.d.cts → pg-DJWS_HZ3.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-D-ldTuIC.d.cts +74 -0
- package/dist/schema-D-ldTuIC.d.ts +74 -0
- package/package.json +3 -2
- package/dist/index-DvXRyTLZ.d.ts +0 -1633
- package/dist/index-ueyor1u6.d.cts +0 -1633
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } }
|
|
2
|
-
|
|
3
|
-
var _chunk74H4WV6Qcjs = require('./chunk-74H4WV6Q.cjs');
|
|
4
|
-
|
|
5
|
-
// src/server/adapters/factory.js
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }// src/server/adapters/factory.js
|
|
6
2
|
async function createAdapter(config) {
|
|
7
3
|
if (!config || !config.driver) {
|
|
8
4
|
throw new Error(
|
|
@@ -16,11 +12,11 @@ async function createAdapter(config) {
|
|
|
16
12
|
}
|
|
17
13
|
switch (config.driver) {
|
|
18
14
|
case "neon": {
|
|
19
|
-
const { NeonAdapter } = await Promise.resolve().then(() => _interopRequireWildcard(require("./neon-
|
|
15
|
+
const { NeonAdapter } = await Promise.resolve().then(() => _interopRequireWildcard(require("./neon-BFUS25UX.cjs")));
|
|
20
16
|
return new NeonAdapter(config.connectionString);
|
|
21
17
|
}
|
|
22
18
|
case "pg": {
|
|
23
|
-
const { PgAdapter } = await Promise.resolve().then(() => _interopRequireWildcard(require("./pg-
|
|
19
|
+
const { PgAdapter } = await Promise.resolve().then(() => _interopRequireWildcard(require("./pg-IIH3M4OM.cjs")));
|
|
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 = _chunk74H4WV6Qcjs.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
|
-
var _crypto = require('crypto');
|
|
67
|
-
function makeFingerprint(masterKey) {
|
|
68
|
-
return _crypto.createHash.call(void 0, "sha256").update(masterKey).digest("hex").slice(0, 16);
|
|
69
|
-
}
|
|
70
|
-
function createInitMasterKeyHandler(ctx) {
|
|
71
|
-
async function POST() {
|
|
72
|
-
const masterKey = _crypto.randomBytes.call(void 0, 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
|
-
|
|
103
|
-
function deriveUserEncryptionKey(userId, masterKey) {
|
|
104
|
-
return _crypto.createHash.call(void 0, "sha256").update(masterKey + userId).digest("hex").slice(0, 64);
|
|
105
|
-
}
|
|
106
|
-
function decryptPayload(encryptedPayload, encryptionKey) {
|
|
107
|
-
const { iv, authTag, encryptedData } = encryptedPayload;
|
|
108
|
-
const decipher = _crypto.createDecipheriv.call(void 0,
|
|
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 = _crypto.randomBytes.call(void 0, 12);
|
|
123
|
-
const cipher = _crypto.createCipheriv.call(void 0, "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 = _crypto.randomBytes.call(void 0, 16);
|
|
134
|
-
const cipher = _crypto.createCipheriv.call(void 0, "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 = _crypto.createDecipheriv.call(void 0,
|
|
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);
|
|
@@ -224,55 +103,6 @@ function validateScheduleMessagePayload(payload) {
|
|
|
224
103
|
return { valid: true };
|
|
225
104
|
}
|
|
226
105
|
|
|
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
|
-
|
|
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
106
|
// src/server/lib/request.js
|
|
277
107
|
var REQUEST_ERRORS = {
|
|
278
108
|
INVALID_JSON: { code: "INVALID_JSON", message: "\u8BF7\u6C42\u4F53\u4E0D\u662F\u6709\u6548\u7684 JSON" },
|
|
@@ -295,6 +125,12 @@ function parseBodyAsObject(body, options = {}) {
|
|
|
295
125
|
}
|
|
296
126
|
return { ok: true, data: parsed };
|
|
297
127
|
}
|
|
128
|
+
function parseJsonBody(body) {
|
|
129
|
+
return parseBodyAsObject(body, {
|
|
130
|
+
invalidJson: REQUEST_ERRORS.INVALID_JSON,
|
|
131
|
+
invalidType: REQUEST_ERRORS.INVALID_REQUEST_BODY
|
|
132
|
+
});
|
|
133
|
+
}
|
|
298
134
|
function isPlainObject(value) {
|
|
299
135
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
300
136
|
}
|
|
@@ -315,14 +151,240 @@ function parseEncryptedBody(body) {
|
|
|
315
151
|
}
|
|
316
152
|
return parsedBody;
|
|
317
153
|
}
|
|
154
|
+
function getHeader(headers = {}, name) {
|
|
155
|
+
if (!headers || typeof headers !== "object") {
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
const lowerName = String(name || "").toLowerCase();
|
|
159
|
+
if (!lowerName) return "";
|
|
160
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
161
|
+
if (String(key).toLowerCase() === lowerName) {
|
|
162
|
+
return String(value || "").trim();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/server/handlers/init-tenant.js
|
|
169
|
+
function createInitTenantHandler(ctx) {
|
|
170
|
+
async function POST(headers, body) {
|
|
171
|
+
const initSecret = getHeader(headers, "x-init-secret");
|
|
172
|
+
if (ctx.tenant.initSecret && initSecret !== ctx.tenant.initSecret) {
|
|
173
|
+
return {
|
|
174
|
+
status: 401,
|
|
175
|
+
body: {
|
|
176
|
+
success: false,
|
|
177
|
+
error: {
|
|
178
|
+
code: "INVALID_INIT_AUTH",
|
|
179
|
+
message: "\u521D\u59CB\u5316\u9274\u6743\u5931\u8D25"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
const parsed = parseJsonBody(body);
|
|
185
|
+
if (!parsed.ok) {
|
|
186
|
+
return {
|
|
187
|
+
status: 400,
|
|
188
|
+
body: {
|
|
189
|
+
success: false,
|
|
190
|
+
error: parsed.error
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const payload = parsed.data;
|
|
195
|
+
const tenantId = payload.tenantId || void 0;
|
|
196
|
+
const driver = payload.driver;
|
|
197
|
+
const databaseUrl = payload.databaseUrl || payload.connectionString;
|
|
198
|
+
if (tenantId && !isValidUUIDv4(tenantId)) {
|
|
199
|
+
return {
|
|
200
|
+
status: 400,
|
|
201
|
+
body: {
|
|
202
|
+
success: false,
|
|
203
|
+
error: {
|
|
204
|
+
code: "INVALID_TENANT_ID",
|
|
205
|
+
message: "tenantId \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F"
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (!databaseUrl || !driver) {
|
|
211
|
+
return {
|
|
212
|
+
status: 400,
|
|
213
|
+
body: {
|
|
214
|
+
success: false,
|
|
215
|
+
error: {
|
|
216
|
+
code: "INVALID_PARAMETERS",
|
|
217
|
+
message: "\u7F3A\u5C11 driver \u6216 databaseUrl \u53C2\u6570"
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const result = await ctx.tenantManager.initializeTenant({
|
|
224
|
+
tenantId,
|
|
225
|
+
driver,
|
|
226
|
+
connectionString: databaseUrl
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
status: 201,
|
|
230
|
+
body: {
|
|
231
|
+
success: true,
|
|
232
|
+
data: {
|
|
233
|
+
tenantId: result.tenantId,
|
|
234
|
+
tenantToken: result.tenantToken,
|
|
235
|
+
cronToken: result.cronToken,
|
|
236
|
+
cronWebhookUrl: result.cronWebhookUrl,
|
|
237
|
+
masterKeyFingerprint: result.masterKeyFingerprint
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error.message === "INVALID_DRIVER") {
|
|
243
|
+
return {
|
|
244
|
+
status: 400,
|
|
245
|
+
body: {
|
|
246
|
+
success: false,
|
|
247
|
+
error: {
|
|
248
|
+
code: "INVALID_DRIVER",
|
|
249
|
+
message: "driver \u5FC5\u987B\u662F neon \u6216 pg"
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (error.message === "INVALID_DATABASE_URL") {
|
|
255
|
+
return {
|
|
256
|
+
status: 400,
|
|
257
|
+
body: {
|
|
258
|
+
success: false,
|
|
259
|
+
error: {
|
|
260
|
+
code: "INVALID_DATABASE_URL",
|
|
261
|
+
message: "databaseUrl \u4E0D\u80FD\u4E3A\u7A7A"
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
if (error.message === "TENANT_ALREADY_INITIALIZED") {
|
|
267
|
+
return {
|
|
268
|
+
status: 409,
|
|
269
|
+
body: {
|
|
270
|
+
success: false,
|
|
271
|
+
error: {
|
|
272
|
+
code: "TENANT_ALREADY_INITIALIZED",
|
|
273
|
+
message: "tenantId \u5DF2\u5B58\u5728\uFF0C\u4E0D\u80FD\u91CD\u590D\u521D\u59CB\u5316"
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { POST };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/server/lib/encryption.js
|
|
285
|
+
var _crypto = require('crypto');
|
|
286
|
+
function deriveUserEncryptionKey(userId, masterKey) {
|
|
287
|
+
return _crypto.createHash.call(void 0, "sha256").update(masterKey + userId).digest("hex").slice(0, 64);
|
|
288
|
+
}
|
|
289
|
+
function decryptPayload(encryptedPayload, encryptionKey) {
|
|
290
|
+
const { iv, authTag, encryptedData } = encryptedPayload;
|
|
291
|
+
const decipher = _crypto.createDecipheriv.call(void 0,
|
|
292
|
+
"aes-256-gcm",
|
|
293
|
+
Buffer.from(encryptionKey, "hex"),
|
|
294
|
+
Buffer.from(iv, "base64")
|
|
295
|
+
);
|
|
296
|
+
decipher.setAuthTag(Buffer.from(authTag, "base64"));
|
|
297
|
+
const decrypted = Buffer.concat([
|
|
298
|
+
decipher.update(Buffer.from(encryptedData, "base64")),
|
|
299
|
+
decipher.final()
|
|
300
|
+
]);
|
|
301
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
302
|
+
}
|
|
303
|
+
function encryptPayload(payload, encryptionKey) {
|
|
304
|
+
const plaintext = typeof payload === "string" ? payload : JSON.stringify(payload);
|
|
305
|
+
const iv = _crypto.randomBytes.call(void 0, 12);
|
|
306
|
+
const cipher = _crypto.createCipheriv.call(void 0, "aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
|
|
307
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
308
|
+
const authTag = cipher.getAuthTag();
|
|
309
|
+
return {
|
|
310
|
+
iv: iv.toString("base64"),
|
|
311
|
+
authTag: authTag.toString("base64"),
|
|
312
|
+
encryptedData: encrypted.toString("base64")
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function encryptForStorage(text, encryptionKey) {
|
|
316
|
+
const iv = _crypto.randomBytes.call(void 0, 16);
|
|
317
|
+
const cipher = _crypto.createCipheriv.call(void 0, "aes-256-gcm", Buffer.from(encryptionKey, "hex"), iv);
|
|
318
|
+
const encrypted = cipher.update(text, "utf8", "hex") + cipher.final("hex");
|
|
319
|
+
const authTag = cipher.getAuthTag();
|
|
320
|
+
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
|
|
321
|
+
}
|
|
322
|
+
function decryptFromStorage(encryptedText, encryptionKey) {
|
|
323
|
+
const [ivHex, authTagHex, encryptedDataHex] = encryptedText.split(":");
|
|
324
|
+
const decipher = _crypto.createDecipheriv.call(void 0,
|
|
325
|
+
"aes-256-gcm",
|
|
326
|
+
Buffer.from(encryptionKey, "hex"),
|
|
327
|
+
Buffer.from(ivHex, "hex")
|
|
328
|
+
);
|
|
329
|
+
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
330
|
+
return decipher.update(encryptedDataHex, "hex", "utf8") + decipher.final("utf8");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/server/handlers/get-user-key.js
|
|
334
|
+
function createGetUserKeyHandler(ctx) {
|
|
335
|
+
async function GET(url, headers) {
|
|
336
|
+
const effectiveHeaders = headers || url || {};
|
|
337
|
+
const tenantResult = await ctx.tenantManager.resolveTenant(effectiveHeaders);
|
|
338
|
+
if (!tenantResult.ok) {
|
|
339
|
+
return tenantResult.error;
|
|
340
|
+
}
|
|
341
|
+
const { masterKey } = tenantResult.context;
|
|
342
|
+
const userId = getHeader(effectiveHeaders, "x-user-id");
|
|
343
|
+
if (!userId) {
|
|
344
|
+
return {
|
|
345
|
+
status: 400,
|
|
346
|
+
body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } }
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
if (!isValidUUIDv4(userId)) {
|
|
350
|
+
return {
|
|
351
|
+
status: 400,
|
|
352
|
+
body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } }
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
status: 200,
|
|
357
|
+
body: {
|
|
358
|
+
success: true,
|
|
359
|
+
data: {
|
|
360
|
+
userKey: deriveUserEncryptionKey(userId, masterKey),
|
|
361
|
+
version: 1
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return { GET };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/server/handlers/schedule-message.js
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
// src/server/lib/db-errors.js
|
|
373
|
+
function isUniqueViolation(error) {
|
|
374
|
+
if (!error || typeof error !== "object") return false;
|
|
375
|
+
const code = error.code;
|
|
376
|
+
if (code === "23505") return true;
|
|
377
|
+
const message = typeof error.message === "string" ? error.message.toLowerCase() : "";
|
|
378
|
+
return message.includes("duplicate key") || message.includes("unique constraint");
|
|
379
|
+
}
|
|
318
380
|
|
|
319
381
|
// src/server/lib/message-processor.js
|
|
320
382
|
|
|
321
383
|
async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
322
384
|
try {
|
|
323
|
-
const masterKey = providedMasterKey ||
|
|
385
|
+
const masterKey = providedMasterKey || ctx.masterKey;
|
|
324
386
|
if (!masterKey) {
|
|
325
|
-
return { success: false, messagesSent: 0, error: "
|
|
387
|
+
return { success: false, messagesSent: 0, error: "TENANT_MASTER_KEY_MISSING" };
|
|
326
388
|
}
|
|
327
389
|
const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
|
|
328
390
|
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
@@ -382,11 +444,11 @@ async function processSingleMessage(task, ctx, providedMasterKey) {
|
|
|
382
444
|
}
|
|
383
445
|
async function processMessagesByUuid(uuid, ctx, maxRetries = 2, userId, providedMasterKey) {
|
|
384
446
|
let retryCount = 0;
|
|
385
|
-
const masterKey = providedMasterKey ||
|
|
447
|
+
const masterKey = providedMasterKey || ctx.masterKey;
|
|
386
448
|
if (!masterKey) {
|
|
387
449
|
return {
|
|
388
450
|
success: false,
|
|
389
|
-
error: { code: "
|
|
451
|
+
error: { code: "TENANT_MASTER_KEY_MISSING", message: "\u79DF\u6237\u4E3B\u5BC6\u94A5\u4E0D\u5B58\u5728\u6216\u914D\u7F6E\u5F02\u5E38" }
|
|
390
452
|
};
|
|
391
453
|
}
|
|
392
454
|
while (retryCount <= maxRetries) {
|
|
@@ -467,9 +529,16 @@ async function _callAI(payload) {
|
|
|
467
529
|
// src/server/handlers/schedule-message.js
|
|
468
530
|
function createScheduleMessageHandler(ctx) {
|
|
469
531
|
async function POST(headers, body) {
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
532
|
+
const tenantResult = await ctx.tenantManager.resolveTenant(headers);
|
|
533
|
+
if (!tenantResult.ok) {
|
|
534
|
+
return tenantResult.error;
|
|
535
|
+
}
|
|
536
|
+
const tenantCtx = tenantResult.context;
|
|
537
|
+
const db = tenantCtx.db;
|
|
538
|
+
const masterKey = tenantCtx.masterKey;
|
|
539
|
+
const isEncrypted = getHeader(headers, "x-payload-encrypted") === "true";
|
|
540
|
+
const encryptionVersion = getHeader(headers, "x-encryption-version");
|
|
541
|
+
const userId = getHeader(headers, "x-user-id");
|
|
473
542
|
if (!isEncrypted) {
|
|
474
543
|
return { status: 400, body: { success: false, error: { code: "ENCRYPTION_REQUIRED", message: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6" } } };
|
|
475
544
|
}
|
|
@@ -482,10 +551,6 @@ function createScheduleMessageHandler(ctx) {
|
|
|
482
551
|
if (encryptionVersion !== "1") {
|
|
483
552
|
return { status: 400, body: { success: false, error: { code: "UNSUPPORTED_ENCRYPTION_VERSION", message: "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301" } } };
|
|
484
553
|
}
|
|
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
554
|
const parsedBody = parseEncryptedBody(body);
|
|
490
555
|
if (!parsedBody.ok) {
|
|
491
556
|
return { status: 400, body: { success: false, error: parsedBody.error } };
|
|
@@ -553,7 +618,7 @@ function createScheduleMessageHandler(ctx) {
|
|
|
553
618
|
}
|
|
554
619
|
let dbResult;
|
|
555
620
|
try {
|
|
556
|
-
dbResult = await
|
|
621
|
+
dbResult = await db.createTask({
|
|
557
622
|
user_id: userId,
|
|
558
623
|
uuid: taskUuid,
|
|
559
624
|
encrypted_payload: encryptedPayload,
|
|
@@ -580,7 +645,11 @@ function createScheduleMessageHandler(ctx) {
|
|
|
580
645
|
}
|
|
581
646
|
if (payload.messageType === "instant") {
|
|
582
647
|
try {
|
|
583
|
-
const sendResult = await processMessagesByUuid(taskUuid,
|
|
648
|
+
const sendResult = await processMessagesByUuid(taskUuid, {
|
|
649
|
+
...ctx,
|
|
650
|
+
db,
|
|
651
|
+
masterKey
|
|
652
|
+
}, 2, userId, masterKey);
|
|
584
653
|
if (!sendResult.success) {
|
|
585
654
|
return { status: 500, body: { success: false, error: { code: "MESSAGE_SEND_FAILED", message: "\u6D88\u606F\u53D1\u9001\u5931\u8D25", details: sendResult.error } } };
|
|
586
655
|
}
|
|
@@ -622,7 +691,19 @@ function createScheduleMessageHandler(ctx) {
|
|
|
622
691
|
|
|
623
692
|
// src/server/handlers/send-notifications.js
|
|
624
693
|
function createSendNotificationsHandler(ctx) {
|
|
625
|
-
async function POST(
|
|
694
|
+
async function POST(urlOrHeaders, maybeHeaders) {
|
|
695
|
+
const url = typeof urlOrHeaders === "string" ? urlOrHeaders : "";
|
|
696
|
+
const headers = maybeHeaders || (typeof urlOrHeaders === "object" ? urlOrHeaders : {});
|
|
697
|
+
const tenantResult = await ctx.tenantManager.resolveTenant(headers, {
|
|
698
|
+
allowCronToken: true,
|
|
699
|
+
url
|
|
700
|
+
});
|
|
701
|
+
if (!tenantResult.ok) {
|
|
702
|
+
return tenantResult.error;
|
|
703
|
+
}
|
|
704
|
+
const tenantCtx = tenantResult.context;
|
|
705
|
+
const db = tenantCtx.db;
|
|
706
|
+
const masterKey = tenantCtx.masterKey;
|
|
626
707
|
if (!ctx.vapid.email || !ctx.vapid.publicKey || !ctx.vapid.privateKey) {
|
|
627
708
|
return {
|
|
628
709
|
status: 500,
|
|
@@ -642,26 +723,8 @@ function createSendNotificationsHandler(ctx) {
|
|
|
642
723
|
}
|
|
643
724
|
};
|
|
644
725
|
}
|
|
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
726
|
const startTime = Date.now();
|
|
664
|
-
const tasks = await
|
|
727
|
+
const tasks = await db.getPendingTasks(50);
|
|
665
728
|
const MAX_CONCURRENT = 8;
|
|
666
729
|
const results = {
|
|
667
730
|
totalTasks: tasks.length,
|
|
@@ -675,11 +738,11 @@ function createSendNotificationsHandler(ctx) {
|
|
|
675
738
|
results.failedCount++;
|
|
676
739
|
try {
|
|
677
740
|
if (task.retry_count >= 3) {
|
|
678
|
-
await
|
|
741
|
+
await db.updateTaskById(task.id, { status: "failed" });
|
|
679
742
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count, status: "permanently_failed" });
|
|
680
743
|
} else {
|
|
681
744
|
const nextRetryTime = new Date(Date.now() + (task.retry_count + 1) * 2 * 60 * 1e3);
|
|
682
|
-
await
|
|
745
|
+
await db.updateTaskById(task.id, { next_send_at: nextRetryTime.toISOString(), retry_count: task.retry_count + 1 });
|
|
683
746
|
results.failedTasks.push({ taskId: task.id, reason, retryCount: task.retry_count + 1, nextRetryAt: nextRetryTime.toISOString() });
|
|
684
747
|
}
|
|
685
748
|
} catch (updateError) {
|
|
@@ -695,7 +758,7 @@ function createSendNotificationsHandler(ctx) {
|
|
|
695
758
|
results.failedCount++;
|
|
696
759
|
let markedSent = false;
|
|
697
760
|
try {
|
|
698
|
-
await
|
|
761
|
+
await db.updateTaskById(task.id, { status: "sent", retry_count: 0 });
|
|
699
762
|
markedSent = true;
|
|
700
763
|
} catch (_markSentError) {
|
|
701
764
|
markedSent = false;
|
|
@@ -710,7 +773,11 @@ function createSendNotificationsHandler(ctx) {
|
|
|
710
773
|
async function processTask(task) {
|
|
711
774
|
let sendResult;
|
|
712
775
|
try {
|
|
713
|
-
sendResult = await processSingleMessage(task,
|
|
776
|
+
sendResult = await processSingleMessage(task, {
|
|
777
|
+
...ctx,
|
|
778
|
+
db,
|
|
779
|
+
masterKey
|
|
780
|
+
}, masterKey);
|
|
714
781
|
} catch (error) {
|
|
715
782
|
await handleDeliveryFailure(task, error.message || "\u6D88\u606F\u53D1\u9001\u5931\u8D25");
|
|
716
783
|
return;
|
|
@@ -723,7 +790,7 @@ function createSendNotificationsHandler(ctx) {
|
|
|
723
790
|
const userKey = deriveUserEncryptionKey(task.user_id, masterKey);
|
|
724
791
|
const decryptedPayload = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
725
792
|
if (decryptedPayload.recurrenceType === "none") {
|
|
726
|
-
await
|
|
793
|
+
await db.deleteTaskById(task.id);
|
|
727
794
|
results.deletedOnceOffTasks++;
|
|
728
795
|
} else {
|
|
729
796
|
let nextSendAt;
|
|
@@ -733,7 +800,7 @@ function createSendNotificationsHandler(ctx) {
|
|
|
733
800
|
} else if (decryptedPayload.recurrenceType === "weekly") {
|
|
734
801
|
nextSendAt = new Date(currentSendAt.getTime() + 7 * 24 * 60 * 60 * 1e3);
|
|
735
802
|
}
|
|
736
|
-
await
|
|
803
|
+
await db.updateTaskById(task.id, { next_send_at: nextSendAt.toISOString(), retry_count: 0 });
|
|
737
804
|
results.updatedRecurringTasks++;
|
|
738
805
|
}
|
|
739
806
|
results.successCount++;
|
|
@@ -757,7 +824,7 @@ function createSendNotificationsHandler(ctx) {
|
|
|
757
824
|
await Promise.race(processing);
|
|
758
825
|
}
|
|
759
826
|
}
|
|
760
|
-
await
|
|
827
|
+
await db.cleanupOldTasks(7);
|
|
761
828
|
const executionTime = Date.now() - startTime;
|
|
762
829
|
return {
|
|
763
830
|
status: 200,
|
|
@@ -784,30 +851,33 @@ function createSendNotificationsHandler(ctx) {
|
|
|
784
851
|
// src/server/handlers/update-message.js
|
|
785
852
|
function createUpdateMessageHandler(ctx) {
|
|
786
853
|
async function PUT(url, headers, body) {
|
|
854
|
+
const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
|
|
855
|
+
if (!tenantResult.ok) {
|
|
856
|
+
return tenantResult.error;
|
|
857
|
+
}
|
|
858
|
+
const tenantCtx = tenantResult.context;
|
|
859
|
+
const db = tenantCtx.db;
|
|
860
|
+
const masterKey = tenantCtx.masterKey;
|
|
787
861
|
const u = new URL(url, "https://dummy");
|
|
788
862
|
const taskUuid = u.searchParams.get("id");
|
|
789
863
|
if (!taskUuid) {
|
|
790
864
|
return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
|
|
791
865
|
}
|
|
792
|
-
const userId = headers
|
|
866
|
+
const userId = getHeader(headers, "x-user-id");
|
|
793
867
|
if (!userId) {
|
|
794
868
|
return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
|
|
795
869
|
}
|
|
796
870
|
if (!isValidUUIDv4(userId)) {
|
|
797
871
|
return { status: 400, body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } } };
|
|
798
872
|
}
|
|
799
|
-
const isEncrypted = headers
|
|
800
|
-
const encryptionVersion = headers
|
|
873
|
+
const isEncrypted = getHeader(headers, "x-payload-encrypted") === "true";
|
|
874
|
+
const encryptionVersion = getHeader(headers, "x-encryption-version");
|
|
801
875
|
if (!isEncrypted) {
|
|
802
876
|
return { status: 400, body: { success: false, error: { code: "ENCRYPTION_REQUIRED", message: "\u8BF7\u6C42\u4F53\u5FC5\u987B\u52A0\u5BC6" } } };
|
|
803
877
|
}
|
|
804
878
|
if (encryptionVersion !== "1") {
|
|
805
879
|
return { status: 400, body: { success: false, error: { code: "UNSUPPORTED_ENCRYPTION_VERSION", message: "\u52A0\u5BC6\u7248\u672C\u4E0D\u652F\u6301" } } };
|
|
806
880
|
}
|
|
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
881
|
const parsedBody = parseEncryptedBody(body);
|
|
812
882
|
if (!parsedBody.ok) {
|
|
813
883
|
return { status: 400, body: { success: false, error: parsedBody.error } };
|
|
@@ -829,9 +899,9 @@ function createUpdateMessageHandler(ctx) {
|
|
|
829
899
|
if (updates.recurrenceType && !["none", "daily", "weekly"].includes(updates.recurrenceType)) {
|
|
830
900
|
return { status: 400, body: { success: false, error: { code: "INVALID_UPDATE_DATA", message: "\u66F4\u65B0\u6570\u636E\u683C\u5F0F\u9519\u8BEF", details: { invalidFields: ["recurrenceType"] } } } };
|
|
831
901
|
}
|
|
832
|
-
const existingTask = await
|
|
902
|
+
const existingTask = await db.getTaskByUuid(taskUuid, userId);
|
|
833
903
|
if (!existingTask) {
|
|
834
|
-
const taskStatus = await
|
|
904
|
+
const taskStatus = await db.getTaskStatus(taskUuid, userId);
|
|
835
905
|
if (!taskStatus) {
|
|
836
906
|
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
907
|
}
|
|
@@ -848,7 +918,7 @@ function createUpdateMessageHandler(ctx) {
|
|
|
848
918
|
};
|
|
849
919
|
const encryptedPayload = encryptForStorage(JSON.stringify(updatedData), userKey);
|
|
850
920
|
const extraFields = updates.nextSendAt ? { next_send_at: updates.nextSendAt } : void 0;
|
|
851
|
-
const result = await
|
|
921
|
+
const result = await db.updateTaskByUuid(taskUuid, userId, encryptedPayload, extraFields);
|
|
852
922
|
if (!result) {
|
|
853
923
|
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
924
|
}
|
|
@@ -870,19 +940,24 @@ function createUpdateMessageHandler(ctx) {
|
|
|
870
940
|
// src/server/handlers/cancel-message.js
|
|
871
941
|
function createCancelMessageHandler(ctx) {
|
|
872
942
|
async function DELETE(url, headers) {
|
|
943
|
+
const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
|
|
944
|
+
if (!tenantResult.ok) {
|
|
945
|
+
return tenantResult.error;
|
|
946
|
+
}
|
|
947
|
+
const db = tenantResult.context.db;
|
|
873
948
|
const u = new URL(url, "https://dummy");
|
|
874
949
|
const taskUuid = u.searchParams.get("id");
|
|
875
950
|
if (!taskUuid) {
|
|
876
951
|
return { status: 400, body: { success: false, error: { code: "TASK_ID_REQUIRED", message: "\u7F3A\u5C11\u4EFB\u52A1ID" } } };
|
|
877
952
|
}
|
|
878
|
-
const userId = headers
|
|
953
|
+
const userId = getHeader(headers, "x-user-id");
|
|
879
954
|
if (!userId) {
|
|
880
955
|
return { status: 400, body: { success: false, error: { code: "USER_ID_REQUIRED", message: "\u7F3A\u5C11\u7528\u6237\u6807\u8BC6\u7B26" } } };
|
|
881
956
|
}
|
|
882
957
|
if (!isValidUUIDv4(userId)) {
|
|
883
958
|
return { status: 400, body: { success: false, error: { code: "INVALID_USER_ID_FORMAT", message: "X-User-Id \u5FC5\u987B\u662F UUID v4 \u683C\u5F0F" } } };
|
|
884
959
|
}
|
|
885
|
-
const deleted = await
|
|
960
|
+
const deleted = await db.deleteTaskByUuid(taskUuid, userId);
|
|
886
961
|
if (!deleted) {
|
|
887
962
|
return {
|
|
888
963
|
status: 404,
|
|
@@ -903,7 +978,14 @@ function createCancelMessageHandler(ctx) {
|
|
|
903
978
|
// src/server/handlers/messages.js
|
|
904
979
|
function createMessagesHandler(ctx) {
|
|
905
980
|
async function GET(url, headers) {
|
|
906
|
-
const
|
|
981
|
+
const tenantResult = await ctx.tenantManager.resolveTenant(headers, { url });
|
|
982
|
+
if (!tenantResult.ok) {
|
|
983
|
+
return tenantResult.error;
|
|
984
|
+
}
|
|
985
|
+
const tenantCtx = tenantResult.context;
|
|
986
|
+
const db = tenantCtx.db;
|
|
987
|
+
const masterKey = tenantCtx.masterKey;
|
|
988
|
+
const userId = getHeader(headers, "x-user-id");
|
|
907
989
|
if (!userId) {
|
|
908
990
|
return {
|
|
909
991
|
status: 400,
|
|
@@ -926,14 +1008,7 @@ function createMessagesHandler(ctx) {
|
|
|
926
1008
|
if (isNaN(offset) || offset < 0) {
|
|
927
1009
|
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
1010
|
}
|
|
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
|
-
}
|
|
1011
|
+
const { tasks, total } = await db.listTasks(userId, { status, limit, offset });
|
|
937
1012
|
const userKey = deriveUserEncryptionKey(userId, masterKey);
|
|
938
1013
|
const decryptedTasks = tasks.map((task) => {
|
|
939
1014
|
const decrypted = JSON.parse(decryptFromStorage(task.encrypted_payload, userKey));
|
|
@@ -969,6 +1044,318 @@ function createMessagesHandler(ctx) {
|
|
|
969
1044
|
return { GET };
|
|
970
1045
|
}
|
|
971
1046
|
|
|
1047
|
+
// src/server/tenant/blob-store.js
|
|
1048
|
+
|
|
1049
|
+
var inMemoryNamespaces = /* @__PURE__ */ new Map();
|
|
1050
|
+
function base64UrlEncode(input) {
|
|
1051
|
+
return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
1052
|
+
}
|
|
1053
|
+
function base64UrlDecode(input) {
|
|
1054
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1055
|
+
const padLength = (4 - normalized.length % 4) % 4;
|
|
1056
|
+
return Buffer.from(normalized + "=".repeat(padLength), "base64");
|
|
1057
|
+
}
|
|
1058
|
+
function getKekBuffer(kek) {
|
|
1059
|
+
const value = String(kek || "").trim();
|
|
1060
|
+
if (!value) {
|
|
1061
|
+
throw new Error("[rei-standard-amsg-server] tenant.kek is required");
|
|
1062
|
+
}
|
|
1063
|
+
return _crypto.createHash.call(void 0, "sha256").update(value).digest();
|
|
1064
|
+
}
|
|
1065
|
+
function encryptConfig(config, kekBuffer) {
|
|
1066
|
+
const iv = _crypto.randomBytes.call(void 0, 12);
|
|
1067
|
+
const cipher = _crypto.createCipheriv.call(void 0, "aes-256-gcm", kekBuffer, iv);
|
|
1068
|
+
const plaintext = Buffer.from(JSON.stringify(config), "utf8");
|
|
1069
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1070
|
+
const authTag = cipher.getAuthTag();
|
|
1071
|
+
return `v1.${base64UrlEncode(iv)}.${base64UrlEncode(authTag)}.${base64UrlEncode(ciphertext)}`;
|
|
1072
|
+
}
|
|
1073
|
+
function decryptConfig(encrypted, kekBuffer) {
|
|
1074
|
+
const parts = String(encrypted || "").split(".");
|
|
1075
|
+
if (parts.length !== 4 || parts[0] !== "v1") {
|
|
1076
|
+
throw new Error("INVALID_TENANT_CONFIG");
|
|
1077
|
+
}
|
|
1078
|
+
const iv = base64UrlDecode(parts[1]);
|
|
1079
|
+
const authTag = base64UrlDecode(parts[2]);
|
|
1080
|
+
const ciphertext = base64UrlDecode(parts[3]);
|
|
1081
|
+
const decipher = _crypto.createDecipheriv.call(void 0, "aes-256-gcm", kekBuffer, iv);
|
|
1082
|
+
decipher.setAuthTag(authTag);
|
|
1083
|
+
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
1084
|
+
return JSON.parse(plaintext);
|
|
1085
|
+
}
|
|
1086
|
+
function createInMemoryStore(namespace) {
|
|
1087
|
+
if (!inMemoryNamespaces.has(namespace)) {
|
|
1088
|
+
inMemoryNamespaces.set(namespace, /* @__PURE__ */ new Map());
|
|
1089
|
+
}
|
|
1090
|
+
const namespaceMap = inMemoryNamespaces.get(namespace);
|
|
1091
|
+
return {
|
|
1092
|
+
async get(key) {
|
|
1093
|
+
return namespaceMap.has(key) ? namespaceMap.get(key) : null;
|
|
1094
|
+
},
|
|
1095
|
+
async set(key, value) {
|
|
1096
|
+
namespaceMap.set(key, value);
|
|
1097
|
+
},
|
|
1098
|
+
async delete(key) {
|
|
1099
|
+
namespaceMap.delete(key);
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
async function resolveBlobStore(namespace) {
|
|
1104
|
+
if (globalThis.__REI_BLOB_STORE__ && typeof globalThis.__REI_BLOB_STORE__.get === "function") {
|
|
1105
|
+
return globalThis.__REI_BLOB_STORE__;
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
const blobModule = await Promise.resolve().then(() => _interopRequireWildcard(require("@netlify/blobs")));
|
|
1109
|
+
const getStore = blobModule.getStore || _optionalChain([blobModule, 'access', _ => _.default, 'optionalAccess', _2 => _2.getStore]);
|
|
1110
|
+
if (typeof getStore === "function") {
|
|
1111
|
+
return getStore({ name: namespace });
|
|
1112
|
+
}
|
|
1113
|
+
} catch (_error) {
|
|
1114
|
+
}
|
|
1115
|
+
return createInMemoryStore(namespace);
|
|
1116
|
+
}
|
|
1117
|
+
function createTenantBlobStore(options) {
|
|
1118
|
+
const namespace = String(options.namespace || "rei-tenants").trim() || "rei-tenants";
|
|
1119
|
+
const kekBuffer = getKekBuffer(options.kek);
|
|
1120
|
+
function getTenantKey(tenantId) {
|
|
1121
|
+
return `tenant/${tenantId}`;
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
async getTenantConfig(tenantId) {
|
|
1125
|
+
const store = await resolveBlobStore(namespace);
|
|
1126
|
+
const encrypted = await store.get(getTenantKey(tenantId), { type: "text" });
|
|
1127
|
+
if (!encrypted) return null;
|
|
1128
|
+
return decryptConfig(encrypted, kekBuffer);
|
|
1129
|
+
},
|
|
1130
|
+
async setTenantConfig(config) {
|
|
1131
|
+
const store = await resolveBlobStore(namespace);
|
|
1132
|
+
const encrypted = encryptConfig(config, kekBuffer);
|
|
1133
|
+
await store.set(getTenantKey(config.tenantId), encrypted);
|
|
1134
|
+
},
|
|
1135
|
+
async deleteTenantConfig(tenantId) {
|
|
1136
|
+
const store = await resolveBlobStore(namespace);
|
|
1137
|
+
await store.delete(getTenantKey(tenantId));
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// src/server/tenant/context.js
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
// src/server/tenant/token.js
|
|
1146
|
+
|
|
1147
|
+
function base64UrlEncode2(input) {
|
|
1148
|
+
return Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
1149
|
+
}
|
|
1150
|
+
function base64UrlDecode2(input) {
|
|
1151
|
+
const normalized = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
1152
|
+
const padLength = (4 - normalized.length % 4) % 4;
|
|
1153
|
+
return Buffer.from(normalized + "=".repeat(padLength), "base64");
|
|
1154
|
+
}
|
|
1155
|
+
function sign(input, secret) {
|
|
1156
|
+
return base64UrlEncode2(_crypto.createHmac.call(void 0, "sha256", secret).update(input).digest());
|
|
1157
|
+
}
|
|
1158
|
+
function nowEpochSeconds() {
|
|
1159
|
+
return Math.floor(Date.now() / 1e3);
|
|
1160
|
+
}
|
|
1161
|
+
function createTenantToken(params, secret) {
|
|
1162
|
+
const issuedAt = nowEpochSeconds();
|
|
1163
|
+
const expiresInSeconds = Number(params.expiresInSeconds || 60 * 60 * 24 * 30);
|
|
1164
|
+
const payload = {
|
|
1165
|
+
tid: params.tenantId,
|
|
1166
|
+
typ: params.type,
|
|
1167
|
+
iat: issuedAt,
|
|
1168
|
+
exp: issuedAt + expiresInSeconds,
|
|
1169
|
+
v: 1
|
|
1170
|
+
};
|
|
1171
|
+
const header = { alg: "HS256", typ: "JWT" };
|
|
1172
|
+
const encodedHeader = base64UrlEncode2(JSON.stringify(header));
|
|
1173
|
+
const encodedPayload = base64UrlEncode2(JSON.stringify(payload));
|
|
1174
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
1175
|
+
const signature = sign(signingInput, secret);
|
|
1176
|
+
return `${signingInput}.${signature}`;
|
|
1177
|
+
}
|
|
1178
|
+
function verifyTenantToken(token, secret, options = {}) {
|
|
1179
|
+
if (!token || typeof token !== "string") {
|
|
1180
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1181
|
+
}
|
|
1182
|
+
const parts = token.split(".");
|
|
1183
|
+
if (parts.length !== 3) {
|
|
1184
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1185
|
+
}
|
|
1186
|
+
const [encodedHeader, encodedPayload, receivedSignature] = parts;
|
|
1187
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
1188
|
+
const expectedSignature = sign(signingInput, secret);
|
|
1189
|
+
const receivedBuffer = Buffer.from(receivedSignature);
|
|
1190
|
+
const expectedBuffer = Buffer.from(expectedSignature);
|
|
1191
|
+
if (receivedBuffer.length !== expectedBuffer.length || !_crypto.timingSafeEqual.call(void 0, receivedBuffer, expectedBuffer)) {
|
|
1192
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1193
|
+
}
|
|
1194
|
+
let payload;
|
|
1195
|
+
try {
|
|
1196
|
+
payload = JSON.parse(base64UrlDecode2(encodedPayload).toString("utf8"));
|
|
1197
|
+
} catch (e3) {
|
|
1198
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1199
|
+
}
|
|
1200
|
+
if (!payload || typeof payload !== "object" || payload.v !== 1) {
|
|
1201
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1202
|
+
}
|
|
1203
|
+
if (!payload.tid || !payload.typ || !payload.exp) {
|
|
1204
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1205
|
+
}
|
|
1206
|
+
if (payload.exp <= nowEpochSeconds()) {
|
|
1207
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1208
|
+
}
|
|
1209
|
+
const expectedTypes = options.expectedTypes || [];
|
|
1210
|
+
if (expectedTypes.length > 0 && !expectedTypes.includes(payload.typ)) {
|
|
1211
|
+
throw new Error("INVALID_TENANT_AUTH");
|
|
1212
|
+
}
|
|
1213
|
+
return payload;
|
|
1214
|
+
}
|
|
1215
|
+
function extractBearerToken(headers = {}) {
|
|
1216
|
+
let authorization = "";
|
|
1217
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
1218
|
+
if (String(key).toLowerCase() === "authorization") {
|
|
1219
|
+
authorization = String(value || "").trim();
|
|
1220
|
+
break;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (!authorization.toLowerCase().startsWith("bearer ")) {
|
|
1224
|
+
return "";
|
|
1225
|
+
}
|
|
1226
|
+
return authorization.slice(7).trim();
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// src/server/tenant/context.js
|
|
1230
|
+
function fingerprint(masterKey) {
|
|
1231
|
+
return _crypto.createHash.call(void 0, "sha256").update(masterKey).digest("hex").slice(0, 16);
|
|
1232
|
+
}
|
|
1233
|
+
function sanitizeDbConfig(driver, connectionString) {
|
|
1234
|
+
const normalizedDriver = String(driver || "").trim();
|
|
1235
|
+
const normalizedConnectionString = String(connectionString || "").trim();
|
|
1236
|
+
if (!["neon", "pg"].includes(normalizedDriver)) {
|
|
1237
|
+
throw new Error("INVALID_DRIVER");
|
|
1238
|
+
}
|
|
1239
|
+
if (!normalizedConnectionString) {
|
|
1240
|
+
throw new Error("INVALID_DATABASE_URL");
|
|
1241
|
+
}
|
|
1242
|
+
return {
|
|
1243
|
+
driver: normalizedDriver,
|
|
1244
|
+
connectionString: normalizedConnectionString
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
function parseTokenFromUrl(url) {
|
|
1248
|
+
if (!url || typeof url !== "string") {
|
|
1249
|
+
return "";
|
|
1250
|
+
}
|
|
1251
|
+
try {
|
|
1252
|
+
const parsed = new URL(url, "https://dummy.local");
|
|
1253
|
+
return String(parsed.searchParams.get("token") || "").trim();
|
|
1254
|
+
} catch (e4) {
|
|
1255
|
+
return "";
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
function createTenantContextManager(options) {
|
|
1259
|
+
const tenantStore = options.tenantStore;
|
|
1260
|
+
const tokenSigningKey = String(options.tokenSigningKey || "").trim();
|
|
1261
|
+
const publicBaseUrl = String(options.publicBaseUrl || "").replace(/\/$/, "");
|
|
1262
|
+
const adapterFactory = options.adapterFactory || createAdapter;
|
|
1263
|
+
const adapterCache = /* @__PURE__ */ new Map();
|
|
1264
|
+
if (!tokenSigningKey) {
|
|
1265
|
+
throw new Error("[rei-standard-amsg-server] tenant.tokenSigningKey is required");
|
|
1266
|
+
}
|
|
1267
|
+
async function getOrCreateAdapter(dbConfig) {
|
|
1268
|
+
const cacheKey = `${dbConfig.driver}:${dbConfig.connectionString}`;
|
|
1269
|
+
if (!adapterCache.has(cacheKey)) {
|
|
1270
|
+
const adapter = await adapterFactory(dbConfig);
|
|
1271
|
+
adapterCache.set(cacheKey, adapter);
|
|
1272
|
+
}
|
|
1273
|
+
return adapterCache.get(cacheKey);
|
|
1274
|
+
}
|
|
1275
|
+
async function initializeTenant({ driver, connectionString, tenantId: providedTenantId }) {
|
|
1276
|
+
const dbConfig = sanitizeDbConfig(driver, connectionString);
|
|
1277
|
+
const tenantId = String(providedTenantId || _crypto.randomUUID.call(void 0, )).trim();
|
|
1278
|
+
const existing = await tenantStore.getTenantConfig(tenantId);
|
|
1279
|
+
if (existing) {
|
|
1280
|
+
throw new Error("TENANT_ALREADY_INITIALIZED");
|
|
1281
|
+
}
|
|
1282
|
+
const adapter = await getOrCreateAdapter(dbConfig);
|
|
1283
|
+
await adapter.initSchema();
|
|
1284
|
+
const masterKey = _crypto.randomBytes.call(void 0, 32).toString("hex");
|
|
1285
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1286
|
+
await tenantStore.setTenantConfig({
|
|
1287
|
+
tenantId,
|
|
1288
|
+
db: dbConfig,
|
|
1289
|
+
masterKey,
|
|
1290
|
+
createdAt: now,
|
|
1291
|
+
updatedAt: now
|
|
1292
|
+
});
|
|
1293
|
+
const tenantToken = createTenantToken({ tenantId, type: "tenant" }, tokenSigningKey);
|
|
1294
|
+
const cronToken = createTenantToken({ tenantId, type: "cron" }, tokenSigningKey);
|
|
1295
|
+
return {
|
|
1296
|
+
tenantId,
|
|
1297
|
+
masterKeyFingerprint: fingerprint(masterKey),
|
|
1298
|
+
tenantToken,
|
|
1299
|
+
cronToken,
|
|
1300
|
+
cronWebhookUrl: publicBaseUrl ? `${publicBaseUrl}/api/v1/send-notifications?token=${cronToken}` : ""
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
async function resolveTenant(headers, options2 = {}) {
|
|
1304
|
+
const allowCronToken = options2.allowCronToken === true;
|
|
1305
|
+
const url = options2.url || "";
|
|
1306
|
+
const bearerToken = extractBearerToken(headers);
|
|
1307
|
+
const tokenFromQuery = allowCronToken ? parseTokenFromUrl(url) : "";
|
|
1308
|
+
const token = bearerToken || tokenFromQuery;
|
|
1309
|
+
if (!token) {
|
|
1310
|
+
return {
|
|
1311
|
+
ok: false,
|
|
1312
|
+
error: {
|
|
1313
|
+
status: 401,
|
|
1314
|
+
body: { success: false, error: { code: "INVALID_TENANT_AUTH", message: "\u7F3A\u5C11\u79DF\u6237\u9274\u6743\u4FE1\u606F" } }
|
|
1315
|
+
}
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
let payload;
|
|
1319
|
+
try {
|
|
1320
|
+
payload = verifyTenantToken(token, tokenSigningKey, {
|
|
1321
|
+
expectedTypes: allowCronToken ? ["cron"] : ["tenant"]
|
|
1322
|
+
});
|
|
1323
|
+
} catch (_error) {
|
|
1324
|
+
return {
|
|
1325
|
+
ok: false,
|
|
1326
|
+
error: {
|
|
1327
|
+
status: 401,
|
|
1328
|
+
body: { success: false, error: { code: "INVALID_TENANT_AUTH", message: "\u79DF\u6237\u4EE4\u724C\u65E0\u6548\u6216\u5DF2\u8FC7\u671F" } }
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const tenantConfig = await tenantStore.getTenantConfig(payload.tid);
|
|
1333
|
+
if (!tenantConfig) {
|
|
1334
|
+
return {
|
|
1335
|
+
ok: false,
|
|
1336
|
+
error: {
|
|
1337
|
+
status: 401,
|
|
1338
|
+
body: { success: false, error: { code: "INVALID_TENANT_AUTH", message: "\u79DF\u6237\u914D\u7F6E\u4E0D\u5B58\u5728\u6216\u5DF2\u5931\u6548" } }
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
const adapter = await getOrCreateAdapter(tenantConfig.db);
|
|
1343
|
+
return {
|
|
1344
|
+
ok: true,
|
|
1345
|
+
context: {
|
|
1346
|
+
tenantId: tenantConfig.tenantId,
|
|
1347
|
+
tokenType: payload.typ,
|
|
1348
|
+
db: adapter,
|
|
1349
|
+
masterKey: tenantConfig.masterKey
|
|
1350
|
+
}
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
return {
|
|
1354
|
+
initializeTenant,
|
|
1355
|
+
resolveTenant
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
|
|
972
1359
|
// src/server/index.js
|
|
973
1360
|
function normalizeVapidSubject(email) {
|
|
974
1361
|
const trimmedEmail = String(email || "").trim();
|
|
@@ -977,7 +1364,7 @@ function normalizeVapidSubject(email) {
|
|
|
977
1364
|
}
|
|
978
1365
|
async function createReiServer(config) {
|
|
979
1366
|
if (!config) throw new Error("[rei-standard-amsg-server] config is required");
|
|
980
|
-
|
|
1367
|
+
if (!config.tenant) throw new Error("[rei-standard-amsg-server] tenant config is required");
|
|
981
1368
|
let webpushModule;
|
|
982
1369
|
try {
|
|
983
1370
|
const webpushImport = await Promise.resolve().then(() => _interopRequireWildcard(require("web-push")));
|
|
@@ -995,28 +1382,39 @@ async function createReiServer(config) {
|
|
|
995
1382
|
vapid.privateKey
|
|
996
1383
|
);
|
|
997
1384
|
}
|
|
1385
|
+
const tenantStore = createTenantBlobStore({
|
|
1386
|
+
namespace: config.tenant.blobNamespace || "rei-tenants",
|
|
1387
|
+
kek: config.tenant.kek
|
|
1388
|
+
});
|
|
1389
|
+
const tenantManager = createTenantContextManager({
|
|
1390
|
+
tenantStore,
|
|
1391
|
+
tokenSigningKey: config.tenant.tokenSigningKey,
|
|
1392
|
+
publicBaseUrl: config.tenant.publicBaseUrl,
|
|
1393
|
+
adapterFactory: config.tenant.adapterFactory
|
|
1394
|
+
});
|
|
1395
|
+
const initSecret = String(config.tenant.initSecret || "").trim();
|
|
998
1396
|
const ctx = {
|
|
999
|
-
db: adapter,
|
|
1000
|
-
cronSecret: config.cronSecret || "",
|
|
1001
1397
|
vapid: {
|
|
1002
1398
|
email: vapid.email || "",
|
|
1003
1399
|
publicKey: vapid.publicKey || "",
|
|
1004
1400
|
privateKey: vapid.privateKey || ""
|
|
1005
1401
|
},
|
|
1006
|
-
webpush: webpushModule
|
|
1402
|
+
webpush: webpushModule,
|
|
1403
|
+
tenant: {
|
|
1404
|
+
initSecret
|
|
1405
|
+
},
|
|
1406
|
+
tenantManager
|
|
1007
1407
|
};
|
|
1008
1408
|
return {
|
|
1009
1409
|
handlers: {
|
|
1010
|
-
|
|
1011
|
-
initMasterKey: createInitMasterKeyHandler(ctx),
|
|
1410
|
+
initTenant: createInitTenantHandler(ctx),
|
|
1012
1411
|
getUserKey: createGetUserKeyHandler(ctx),
|
|
1013
1412
|
scheduleMessage: createScheduleMessageHandler(ctx),
|
|
1014
1413
|
sendNotifications: createSendNotificationsHandler(ctx),
|
|
1015
1414
|
updateMessage: createUpdateMessageHandler(ctx),
|
|
1016
1415
|
cancelMessage: createCancelMessageHandler(ctx),
|
|
1017
1416
|
messages: createMessagesHandler(ctx)
|
|
1018
|
-
}
|
|
1019
|
-
adapter
|
|
1417
|
+
}
|
|
1020
1418
|
};
|
|
1021
1419
|
}
|
|
1022
1420
|
|
|
@@ -1031,4 +1429,6 @@ async function createReiServer(config) {
|
|
|
1031
1429
|
|
|
1032
1430
|
|
|
1033
1431
|
|
|
1034
|
-
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
exports.createAdapter = createAdapter; exports.createReiServer = createReiServer; exports.createTenantToken = createTenantToken; exports.decryptFromStorage = decryptFromStorage; exports.decryptPayload = decryptPayload; exports.deriveUserEncryptionKey = deriveUserEncryptionKey; exports.encryptForStorage = encryptForStorage; exports.isValidISO8601 = isValidISO8601; exports.isValidUUID = isValidUUID; exports.isValidUUIDv4 = isValidUUIDv4; exports.isValidUrl = isValidUrl; exports.validateScheduleMessagePayload = validateScheduleMessagePayload; exports.verifyTenantToken = verifyTenantToken;
|