@h-ai/reach 1.0.0-beta.18 → 1.0.0-beta.19
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.en.md +51 -13
- package/README.ja.md +51 -13
- package/README.md +51 -13
- package/dist/index.d.ts +2 -1
- package/dist/index.js +496 -280
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { core, err, ok } from '@h-ai/core';
|
|
3
3
|
import process from 'process';
|
|
4
|
-
import { reldb,
|
|
4
|
+
import { reldb, BaseReldbCrudRepository } from '@h-ai/reldb';
|
|
5
5
|
import { randomUUID, createHash, createHmac } from 'crypto';
|
|
6
6
|
import { addAbortListener } from 'events';
|
|
7
7
|
import { createRequire } from 'module';
|
|
@@ -23,6 +23,7 @@ var en_US_default = {
|
|
|
23
23
|
reach_providerRequired: "No provider specified, set provider field in message or template",
|
|
24
24
|
reach_dndBlocked: "Message discarded by Do Not Disturb period",
|
|
25
25
|
reach_dndPersistenceRequired: "DND delay requires an initialized database and a ready queue table",
|
|
26
|
+
reach_templatePersistenceRequired: "Configured notification templates require an initialized database and successful template persistence",
|
|
26
27
|
reach_dndEnqueueFailed: "The message could not be reliably queued; try again later",
|
|
27
28
|
reach_sendRecordSaveFailed: "Failed to persist the reach send record",
|
|
28
29
|
reach_invalidStoredInteger: "Send record {field} is not a safe non-negative integer",
|
|
@@ -30,13 +31,13 @@ var en_US_default = {
|
|
|
30
31
|
reach_unknownResolutionFailed: "Unknown delivery was not found or has already been resolved",
|
|
31
32
|
reach_queueInputInvalid: "Invalid queue receipt or manual resolution parameters",
|
|
32
33
|
reach_deliveryUnknown: "The external delivery outcome is unknown; manual review is required and automatic redelivery has stopped",
|
|
33
|
-
reach_smsSendFailed: "SMS send failed
|
|
34
|
+
reach_smsSendFailed: "SMS send failed. Please try again later.",
|
|
34
35
|
reach_smsGatewayFailed: "The SMS gateway request failed or timed out. Delivery is unconfirmed",
|
|
35
36
|
reach_gatewayUrlInvalid: "Use a full HTTPS gateway URL without credentials, a query, or a fragment. For HTTP on a trusted private network, explicitly enable allowInsecureHttp",
|
|
36
37
|
reach_gatewayHeadersInvalid: "Gateway headers are invalid or include a header managed by the HTTP client",
|
|
37
38
|
reach_gatewayBodyInvalid: "The gateway request body must be a JSON object, an array, or text",
|
|
38
39
|
reach_gatewayHookInvalid: "Gateway hooks must be bound to an SMS provider with a gateway configured",
|
|
39
|
-
reach_emailSendFailed: "Email send failed
|
|
40
|
+
reach_emailSendFailed: "Email send failed. Please try again later.",
|
|
40
41
|
reach_apiSendFailed: "API callback send failed: {error}",
|
|
41
42
|
reach_configError: "Reach config validation failed: {error}",
|
|
42
43
|
reach_config_nameRequired: "Provider name is required",
|
|
@@ -81,6 +82,7 @@ var ja_JP_default = {
|
|
|
81
82
|
reach_providerRequired: "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u30E1\u30C3\u30BB\u30FC\u30B8\u307E\u305F\u306F\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306Eprovider\u3092\u8A2D\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
82
83
|
reach_dndBlocked: "\u901A\u77E5\u3092\u505C\u6B62\u3059\u308B\u6642\u9593\u5E2F\u306E\u305F\u3081\u3001\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u7834\u68C4\u3057\u307E\u3057\u305F",
|
|
83
84
|
reach_dndPersistenceRequired: "\u901A\u77E5\u5236\u9650\u6642\u9593\u5E2F\u306E\u914D\u4FE1\u5EF6\u671F\u306B\u306F\u521D\u671F\u5316\u6E08\u307F\u306E\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u3068\u6E96\u5099\u6E08\u307F\u306E\u30AD\u30E5\u30FC\u30C6\u30FC\u30D6\u30EB\u304C\u5FC5\u8981\u3067\u3059",
|
|
85
|
+
reach_templatePersistenceRequired: "\u901A\u77E5\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306E\u8A2D\u5B9A\u306B\u306F\u521D\u671F\u5316\u6E08\u307F\u306E\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u3068\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306E\u4FDD\u5B58\u6210\u529F\u304C\u5FC5\u8981\u3067\u3059",
|
|
84
86
|
reach_dndEnqueueFailed: "\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u78BA\u5B9F\u306B\u30AD\u30E5\u30FC\u3078\u767B\u9332\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
85
87
|
reach_sendRecordSaveFailed: "\u914D\u4FE1\u8A18\u9332\u306E\u6C38\u7D9A\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F",
|
|
86
88
|
reach_invalidStoredInteger: "\u914D\u4FE1\u8A18\u9332\u306E {field} \u306F\u5B89\u5168\u306A\u975E\u8CA0\u6574\u6570\u3067\u306F\u3042\u308A\u307E\u305B\u3093",
|
|
@@ -88,13 +90,13 @@ var ja_JP_default = {
|
|
|
88
90
|
reach_unknownResolutionFailed: "\u7D50\u679C\u4E0D\u660E\u306E\u914D\u4FE1\u304C\u898B\u3064\u304B\u3089\u306A\u3044\u304B\u3001\u65E2\u306B\u89E3\u6C7A\u3055\u308C\u3066\u3044\u307E\u3059",
|
|
89
91
|
reach_queueInputInvalid: "\u30AD\u30E5\u30FC\u306E\u53D7\u9818\u8A18\u9332\u307E\u305F\u306F\u624B\u52D5\u89E3\u6C7A\u306E\u30D1\u30E9\u30E1\u30FC\u30BF\u30FC\u304C\u7121\u52B9\u3067\u3059",
|
|
90
92
|
reach_deliveryUnknown: "\u5916\u90E8\u3078\u306E\u914D\u4FE1\u7D50\u679C\u306F\u4E0D\u660E\u3067\u3059\u3002\u624B\u52D5\u78BA\u8A8D\u304C\u5FC5\u8981\u306A\u305F\u3081\u3001\u81EA\u52D5\u518D\u914D\u4FE1\u3092\u505C\u6B62\u3057\u307E\u3057\u305F",
|
|
91
|
-
reach_smsSendFailed: "SMS\u306E\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F
|
|
93
|
+
reach_smsSendFailed: "SMS\u306E\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044",
|
|
92
94
|
reach_smsGatewayFailed: "SMS \u30B2\u30FC\u30C8\u30A6\u30A7\u30A4\u3078\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u5931\u6557\u307E\u305F\u306F\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\u3057\u3001\u914D\u4FE1\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F",
|
|
93
95
|
reach_gatewayUrlInvalid: "\u30B2\u30FC\u30C8\u30A6\u30A7\u30A4\u306B\u306F\u8A8D\u8A3C\u60C5\u5831\u3001\u30AF\u30A8\u30EA\u3001\u30D5\u30E9\u30B0\u30E1\u30F3\u30C8\u3092\u542B\u307E\u306A\u3044\u5B8C\u5168\u306A HTTPS URL \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u4FE1\u983C\u3067\u304D\u308B\u5185\u90E8\u30CD\u30C3\u30C8\u30EF\u30FC\u30AF\u3067 HTTP \u3092\u4F7F\u7528\u3059\u308B\u5834\u5408\u306F allowInsecureHttp \u3092\u660E\u793A\u7684\u306B\u6709\u52B9\u306B\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
94
96
|
reach_gatewayHeadersInvalid: "\u30B2\u30FC\u30C8\u30A6\u30A7\u30A4\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u30D8\u30C3\u30C0\u30FC\u304C\u7121\u52B9\u304B\u3001HTTP \u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u304C\u7BA1\u7406\u3059\u308B\u30D8\u30C3\u30C0\u30FC\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059",
|
|
95
97
|
reach_gatewayBodyInvalid: "\u30B2\u30FC\u30C8\u30A6\u30A7\u30A4\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u30DC\u30C7\u30A3\u306B\u306F JSON \u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3001\u914D\u5217\u3001\u307E\u305F\u306F\u30C6\u30AD\u30B9\u30C8\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
96
98
|
reach_gatewayHookInvalid: "\u30B2\u30FC\u30C8\u30A6\u30A7\u30A4\u306E\u30D5\u30C3\u30AF\u306F\u3001\u30B2\u30FC\u30C8\u30A6\u30A7\u30A4\u3092\u8A2D\u5B9A\u3057\u305F SMS Provider \u306B\u5272\u308A\u5F53\u3066\u3066\u304F\u3060\u3055\u3044",
|
|
97
|
-
reach_emailSendFailed: "\u30E1\u30FC\u30EB\u306E\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F
|
|
99
|
+
reach_emailSendFailed: "\u30E1\u30FC\u30EB\u306E\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u3057\u3070\u3089\u304F\u3057\u3066\u304B\u3089\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044",
|
|
98
100
|
reach_apiSendFailed: "API\u30B3\u30FC\u30EB\u30D0\u30C3\u30AF\u306E\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
99
101
|
reach_configError: "\u901A\u77E5\u8A2D\u5B9A\u306E\u691C\u8A3C\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
|
|
100
102
|
reach_config_nameRequired: "\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u540D\u306F\u5FC5\u9808\u3067\u3059",
|
|
@@ -139,6 +141,7 @@ var zh_CN_default = {
|
|
|
139
141
|
reach_providerRequired: "\u672A\u6307\u5B9A Provider\uFF0C\u8BF7\u5728\u6D88\u606F\u6216\u6A21\u677F\u4E2D\u8BBE\u7F6E provider \u5B57\u6BB5",
|
|
140
142
|
reach_dndBlocked: "\u5F53\u524D\u5904\u4E8E\u514D\u6253\u6270\u65F6\u6BB5\uFF0C\u6D88\u606F\u5DF2\u4E22\u5F03",
|
|
141
143
|
reach_dndPersistenceRequired: "\u542F\u7528\u514D\u6253\u6270\u5EF6\u8FDF\u53D1\u9001\u9700\u8981\u5148\u521D\u59CB\u5316\u6570\u636E\u5E93\u5E76\u786E\u4FDD\u961F\u5217\u8868\u53EF\u7528",
|
|
144
|
+
reach_templatePersistenceRequired: "\u914D\u7F6E\u901A\u77E5\u6A21\u677F\u524D\u9700\u8981\u5148\u521D\u59CB\u5316\u6570\u636E\u5E93\uFF0C\u5E76\u786E\u4FDD\u6A21\u677F\u80FD\u591F\u4FDD\u5B58",
|
|
142
145
|
reach_dndEnqueueFailed: "\u6D88\u606F\u672A\u80FD\u53EF\u9760\u4FDD\u5B58\u5230\u5EF6\u8FDF\u961F\u5217\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
143
146
|
reach_sendRecordSaveFailed: "\u4FDD\u5B58\u89E6\u8FBE\u53D1\u9001\u8BB0\u5F55\u5931\u8D25",
|
|
144
147
|
reach_invalidStoredInteger: "\u53D1\u9001\u8BB0\u5F55\u7684 {field} \u4E0D\u662F\u5B89\u5168\u7684\u975E\u8D1F\u6574\u6570",
|
|
@@ -146,13 +149,13 @@ var zh_CN_default = {
|
|
|
146
149
|
reach_unknownResolutionFailed: "\u672A\u77E5\u6295\u9012\u7ED3\u679C\u4E0D\u5B58\u5728\u6216\u5DF2\u88AB\u5176\u5B83\u64CD\u4F5C\u88C1\u5B9A",
|
|
147
150
|
reach_queueInputInvalid: "\u961F\u5217\u56DE\u6267\u6216\u4EBA\u5DE5\u88C1\u5B9A\u53C2\u6570\u65E0\u6548",
|
|
148
151
|
reach_deliveryUnknown: "\u5916\u90E8\u6295\u9012\u7ED3\u679C\u672A\u77E5\uFF0C\u9700\u8981\u4EBA\u5DE5\u6838\u5BF9\uFF0C\u5DF2\u505C\u6B62\u81EA\u52A8\u91CD\u53D1",
|
|
149
|
-
reach_smsSendFailed: "\u77ED\u4FE1\u53D1\u9001\u5931\u8D25
|
|
152
|
+
reach_smsSendFailed: "\u77ED\u4FE1\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
150
153
|
reach_smsGatewayFailed: "\u77ED\u4FE1\u7F51\u5173\u8BF7\u6C42\u5931\u8D25\u6216\u8D85\u65F6\uFF0C\u6295\u9012\u7ED3\u679C\u672A\u786E\u8BA4",
|
|
151
154
|
reach_gatewayUrlInvalid: "\u7F51\u5173\u987B\u4F7F\u7528\u5B8C\u6574\u7684 HTTPS URL\uFF0C\u4E14\u4E0D\u80FD\u5305\u542B\u51ED\u636E\u3001\u67E5\u8BE2\u53C2\u6570\u6216\u7247\u6BB5\u3002\u53D7\u4FE1\u5185\u7F51\u4F7F\u7528 HTTP \u65F6\u987B\u663E\u5F0F\u542F\u7528 allowInsecureHttp",
|
|
152
155
|
reach_gatewayHeadersInvalid: "\u7F51\u5173\u8BF7\u6C42\u5934\u683C\u5F0F\u65E0\u6548\uFF0C\u6216\u5305\u542B\u7531 HTTP \u5BA2\u6237\u7AEF\u7BA1\u7406\u7684\u8BF7\u6C42\u5934",
|
|
153
156
|
reach_gatewayBodyInvalid: "\u7F51\u5173\u8BF7\u6C42\u4F53\u5FC5\u987B\u662F JSON \u5BF9\u8C61\u3001\u6570\u7EC4\u6216\u6587\u672C",
|
|
154
157
|
reach_gatewayHookInvalid: "\u7F51\u5173\u51FD\u6570\u5FC5\u987B\u7ED1\u5B9A\u5230\u542F\u7528\u4E86\u7F51\u5173\u7684\u77ED\u4FE1 Provider",
|
|
155
|
-
reach_emailSendFailed: "\u90AE\u4EF6\u53D1\u9001\u5931\u8D25
|
|
158
|
+
reach_emailSendFailed: "\u90AE\u4EF6\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5",
|
|
156
159
|
reach_apiSendFailed: "API \u56DE\u8C03\u53D1\u9001\u5931\u8D25: {error}",
|
|
157
160
|
reach_configError: "\u89E6\u8FBE\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A{error}",
|
|
158
161
|
reach_config_nameRequired: "Provider \u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A",
|
|
@@ -197,13 +200,19 @@ var gatewayFields = {
|
|
|
197
200
|
/** 网关自定义请求头;连接与消息长度相关的头由 HTTP 客户端管理。 */
|
|
198
201
|
headers: z.record(z.string(), z.string()).refine((headers) => {
|
|
199
202
|
try {
|
|
200
|
-
return [
|
|
203
|
+
return [
|
|
204
|
+
...new Headers(headers).keys()
|
|
205
|
+
].every((name) => !transportHeader.test(name));
|
|
201
206
|
} catch {
|
|
202
207
|
return false;
|
|
203
208
|
}
|
|
204
209
|
}, reachM("reach_gatewayHeadersInvalid")).optional(),
|
|
205
210
|
/** 网关请求体;对象和数组序列化为 JSON,字符串原样发送。省略时不发请求体。 */
|
|
206
|
-
body: z.union([
|
|
211
|
+
body: z.union([
|
|
212
|
+
z.string(),
|
|
213
|
+
z.array(jsonValue),
|
|
214
|
+
z.record(z.string(), jsonValue)
|
|
215
|
+
], reachM("reach_gatewayBodyInvalid")).optional()
|
|
207
216
|
};
|
|
208
217
|
var ReachGatewayFieldsSchema = z.strictObject(gatewayFields);
|
|
209
218
|
var ReachGatewayConfigSchema = z.strictObject({
|
|
@@ -215,7 +224,10 @@ var ReachGatewayConfigSchema = z.strictObject({
|
|
|
215
224
|
}).refine(({ url, allowInsecureHttp }) => {
|
|
216
225
|
const target = URL.parse(url);
|
|
217
226
|
return target !== null && /^https?:\/\//i.test(url) && !/[?#]/.test(url) && !target.username && !target.password && (target.protocol === "https:" || target.protocol === "http:" && allowInsecureHttp);
|
|
218
|
-
}, {
|
|
227
|
+
}, {
|
|
228
|
+
path: ["url"],
|
|
229
|
+
message: reachM("reach_gatewayUrlInvalid")
|
|
230
|
+
});
|
|
219
231
|
|
|
220
232
|
// src/reach-config.ts
|
|
221
233
|
var ConsoleProviderConfigSchema = z.object({
|
|
@@ -356,13 +368,10 @@ var ISO_MILLISECONDS_SUFFIX_REGEX = /\.\d{3}Z$/;
|
|
|
356
368
|
var RFC3986_RESERVED_CHARACTERS_REGEX = /[!'()*]/g;
|
|
357
369
|
var GATEWAY_TIMEOUT_MS = 1e4;
|
|
358
370
|
var GATEWAY_RESPONSE_LIMIT_BYTES = 64 * 1024;
|
|
359
|
-
function toReachError(
|
|
371
|
+
function toReachError() {
|
|
360
372
|
return {
|
|
361
|
-
|
|
362
|
-
message: reachM("reach_smsSendFailed"
|
|
363
|
-
params: { error: error instanceof Error ? error.message : String(error) }
|
|
364
|
-
}),
|
|
365
|
-
cause: error
|
|
373
|
+
...HaiReachError.SEND_FAILED,
|
|
374
|
+
message: reachM("reach_smsSendFailed")
|
|
366
375
|
};
|
|
367
376
|
}
|
|
368
377
|
function percentEncode(str) {
|
|
@@ -389,7 +398,12 @@ async function prepareGatewayRequest(gateway, url, signal, hook) {
|
|
|
389
398
|
});
|
|
390
399
|
try {
|
|
391
400
|
const output = await Promise.race([
|
|
392
|
-
Promise.resolve().then(() => hook({
|
|
401
|
+
Promise.resolve().then(() => hook({
|
|
402
|
+
method: "POST",
|
|
403
|
+
url,
|
|
404
|
+
body: typeof body === "string" ? body : JSON.stringify(body),
|
|
405
|
+
signal
|
|
406
|
+
})),
|
|
393
407
|
aborted
|
|
394
408
|
]);
|
|
395
409
|
const patch = ReachGatewayFieldsSchema.parse(output === void 0 ? {} : output);
|
|
@@ -400,9 +414,16 @@ async function prepareGatewayRequest(gateway, url, signal, hook) {
|
|
|
400
414
|
listener?.[Symbol.dispose]();
|
|
401
415
|
}
|
|
402
416
|
}
|
|
403
|
-
if (body !== void 0 && !headers.has("Content-Type"))
|
|
417
|
+
if (body !== void 0 && !headers.has("Content-Type")) {
|
|
404
418
|
headers.set("Content-Type", typeof body === "string" ? "text/plain; charset=utf-8" : "application/json");
|
|
405
|
-
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers,
|
|
423
|
+
body: typeof body === "string" ? body : JSON.stringify(body),
|
|
424
|
+
redirect: "error",
|
|
425
|
+
signal
|
|
426
|
+
};
|
|
406
427
|
}
|
|
407
428
|
async function readGatewayResponse(response) {
|
|
408
429
|
if (response.ok && response.body) {
|
|
@@ -436,7 +457,9 @@ function createAliyunSmsProvider(gatewayHook) {
|
|
|
436
457
|
if (config.type !== "aliyun-sms") {
|
|
437
458
|
return err(
|
|
438
459
|
HaiReachError.CONFIG_ERROR,
|
|
439
|
-
reachM("reach_unsupportedType", {
|
|
460
|
+
reachM("reach_unsupportedType", {
|
|
461
|
+
params: { type: config.type }
|
|
462
|
+
})
|
|
440
463
|
);
|
|
441
464
|
}
|
|
442
465
|
smsConfig = config;
|
|
@@ -499,7 +522,7 @@ function createAliyunSmsProvider(gatewayHook) {
|
|
|
499
522
|
logger.warn("SMS send returned non-OK status", { code: body.Code });
|
|
500
523
|
return err(
|
|
501
524
|
HaiReachError.SEND_FAILED,
|
|
502
|
-
reachM("reach_smsSendFailed"
|
|
525
|
+
reachM("reach_smsSendFailed")
|
|
503
526
|
);
|
|
504
527
|
}
|
|
505
528
|
logger.info("SMS sent", { to: message.to, bizId: body.BizId });
|
|
@@ -510,7 +533,7 @@ function createAliyunSmsProvider(gatewayHook) {
|
|
|
510
533
|
return err(HaiReachError.SEND_FAILED, reachM("reach_smsGatewayFailed"));
|
|
511
534
|
}
|
|
512
535
|
logger.error("SMS send failed", { to: message.to, error });
|
|
513
|
-
return err(toReachError(
|
|
536
|
+
return err(toReachError());
|
|
514
537
|
}
|
|
515
538
|
}
|
|
516
539
|
};
|
|
@@ -525,9 +548,11 @@ function getSafeEndpoint(url) {
|
|
|
525
548
|
}
|
|
526
549
|
function toReachError2() {
|
|
527
550
|
return {
|
|
528
|
-
|
|
551
|
+
...HaiReachError.SEND_FAILED,
|
|
529
552
|
message: reachM("reach_apiSendFailed", {
|
|
530
|
-
params: {
|
|
553
|
+
params: {
|
|
554
|
+
error: reachM("reach_apiRequestFailed")
|
|
555
|
+
}
|
|
531
556
|
})
|
|
532
557
|
};
|
|
533
558
|
}
|
|
@@ -559,7 +584,10 @@ async function readAcknowledgement(response) {
|
|
|
559
584
|
const messageId = "messageId" in result ? result.messageId : void 0;
|
|
560
585
|
if (messageId !== void 0 && (typeof messageId !== "string" || messageId.length > 255))
|
|
561
586
|
return err(HaiReachError.SEND_FAILED, reachM("reach_apiResponseInvalid"));
|
|
562
|
-
return ok({
|
|
587
|
+
return ok({
|
|
588
|
+
success: true,
|
|
589
|
+
...messageId === void 0 ? {} : { messageId }
|
|
590
|
+
});
|
|
563
591
|
} catch {
|
|
564
592
|
return err(HaiReachError.SEND_FAILED, reachM("reach_apiResponseInvalid"));
|
|
565
593
|
} finally {
|
|
@@ -579,7 +607,9 @@ function createApiProvider() {
|
|
|
579
607
|
if (config.type !== "api") {
|
|
580
608
|
return err(
|
|
581
609
|
HaiReachError.CONFIG_ERROR,
|
|
582
|
-
reachM("reach_unsupportedType", {
|
|
610
|
+
reachM("reach_unsupportedType", {
|
|
611
|
+
params: { type: config.type }
|
|
612
|
+
})
|
|
583
613
|
);
|
|
584
614
|
}
|
|
585
615
|
apiConfig = config;
|
|
@@ -587,7 +617,10 @@ function createApiProvider() {
|
|
|
587
617
|
apiConfig = null;
|
|
588
618
|
return err(HaiReachError.CONFIG_ERROR, reachM("reach_apiHttpsRequired"));
|
|
589
619
|
}
|
|
590
|
-
logger2.info("API provider connected", {
|
|
620
|
+
logger2.info("API provider connected", {
|
|
621
|
+
endpoint: getSafeEndpoint(config.url),
|
|
622
|
+
method: config.method
|
|
623
|
+
});
|
|
591
624
|
return ok(void 0);
|
|
592
625
|
},
|
|
593
626
|
async close() {
|
|
@@ -604,7 +637,9 @@ function createApiProvider() {
|
|
|
604
637
|
reachM("reach_notInitialized")
|
|
605
638
|
);
|
|
606
639
|
}
|
|
607
|
-
logger2.debug("Sending via API callback", {
|
|
640
|
+
logger2.debug("Sending via API callback", {
|
|
641
|
+
endpoint: getSafeEndpoint(apiConfig.url)
|
|
642
|
+
});
|
|
608
643
|
const requireHttps = apiConfig.requireHttps ?? (message.sensitive === true && core.module.requiresSharedState(process.env));
|
|
609
644
|
if (requireHttps && new URL(apiConfig.url).protocol !== "https:")
|
|
610
645
|
return err(HaiReachError.CONFIG_ERROR, reachM("reach_apiHttpsRequired"));
|
|
@@ -638,7 +673,9 @@ function createApiProvider() {
|
|
|
638
673
|
const rejected = response.status >= 300 && response.status < 400 || response.status >= 400 && response.status < 500 && ![408, 425, 429].includes(response.status);
|
|
639
674
|
return err(
|
|
640
675
|
rejected ? HaiReachError.DELIVERY_REJECTED : HaiReachError.SEND_FAILED,
|
|
641
|
-
reachM("reach_apiSendFailed", {
|
|
676
|
+
reachM("reach_apiSendFailed", {
|
|
677
|
+
params: { error: `HTTP ${response.status}` }
|
|
678
|
+
})
|
|
642
679
|
);
|
|
643
680
|
}
|
|
644
681
|
const result = await readAcknowledgement(response);
|
|
@@ -688,13 +725,10 @@ function createConsoleProvider() {
|
|
|
688
725
|
};
|
|
689
726
|
}
|
|
690
727
|
var logger4 = core.logger.child({ module: "reach", scope: "provider-smtp" });
|
|
691
|
-
function toReachError3(
|
|
728
|
+
function toReachError3() {
|
|
692
729
|
return {
|
|
693
|
-
|
|
694
|
-
message: reachM("reach_emailSendFailed"
|
|
695
|
-
params: { error: error instanceof Error ? error.message : String(error) }
|
|
696
|
-
}),
|
|
697
|
-
cause: error
|
|
730
|
+
...HaiReachError.SEND_FAILED,
|
|
731
|
+
message: reachM("reach_emailSendFailed")
|
|
698
732
|
};
|
|
699
733
|
}
|
|
700
734
|
function createSmtpProvider() {
|
|
@@ -707,7 +741,9 @@ function createSmtpProvider() {
|
|
|
707
741
|
if (config.type !== "smtp") {
|
|
708
742
|
return err(
|
|
709
743
|
HaiReachError.CONFIG_ERROR,
|
|
710
|
-
reachM("reach_unsupportedType", {
|
|
744
|
+
reachM("reach_unsupportedType", {
|
|
745
|
+
params: { type: config.type }
|
|
746
|
+
})
|
|
711
747
|
);
|
|
712
748
|
}
|
|
713
749
|
try {
|
|
@@ -724,7 +760,7 @@ function createSmtpProvider() {
|
|
|
724
760
|
return ok(void 0);
|
|
725
761
|
} catch (error) {
|
|
726
762
|
logger4.error("SMTP connection failed", { error });
|
|
727
|
-
return err(toReachError3(
|
|
763
|
+
return err(toReachError3());
|
|
728
764
|
}
|
|
729
765
|
},
|
|
730
766
|
async close() {
|
|
@@ -758,7 +794,7 @@ function createSmtpProvider() {
|
|
|
758
794
|
return ok({ success: true, messageId: info.messageId });
|
|
759
795
|
} catch (error) {
|
|
760
796
|
logger4.error("Email send failed", { to: message.to, error });
|
|
761
|
-
return err(toReachError3(
|
|
797
|
+
return err(toReachError3());
|
|
762
798
|
}
|
|
763
799
|
}
|
|
764
800
|
};
|
|
@@ -850,14 +886,25 @@ function previousResult(row) {
|
|
|
850
886
|
return row.status === "sent" ? ok({ success: true, receiptId: row.queueId ?? void 0 }) : unconfirmed(row.queueId);
|
|
851
887
|
}
|
|
852
888
|
function unconfirmed(receiptId) {
|
|
853
|
-
return {
|
|
889
|
+
return {
|
|
890
|
+
success: false,
|
|
891
|
+
error: {
|
|
892
|
+
...HaiReachError.SEND_FAILED,
|
|
893
|
+
message: reachM("reach_sensitiveDeliveryUnconfirmed"),
|
|
894
|
+
ext: { receiptId }
|
|
895
|
+
}
|
|
896
|
+
};
|
|
854
897
|
}
|
|
855
898
|
async function sendSensitive(message, provider, repo, blocked) {
|
|
856
899
|
if (!repo || !message.idempotencyKey?.trim() || message.idempotencyKey.length > 255 || !Number.isSafeInteger(message.expiresAt))
|
|
857
900
|
return err(HaiReachError.CONFIG_ERROR, reachM("reach_sensitivePolicyRequired"));
|
|
858
901
|
const hash = createHash("sha256").update(JSON.stringify([message.provider, message.to, message.idempotencyKey])).digest("hex");
|
|
859
902
|
const queueId = `${hash.slice(0, 8)}-${hash.slice(8, 12)}-5${hash.slice(13, 16)}-a${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
|
|
860
|
-
const find = () => repo.findAll({
|
|
903
|
+
const find = () => repo.findAll({
|
|
904
|
+
where: "queue_id = ?",
|
|
905
|
+
params: [queueId],
|
|
906
|
+
limit: 1
|
|
907
|
+
});
|
|
861
908
|
const existing = await find();
|
|
862
909
|
if (!existing.success)
|
|
863
910
|
return existing;
|
|
@@ -906,6 +953,106 @@ async function sendSensitive(message, provider, repo, blocked) {
|
|
|
906
953
|
return unconfirmed(queueId);
|
|
907
954
|
return ok({ success: true, receiptId: queueId });
|
|
908
955
|
}
|
|
956
|
+
function renderString(template, vars) {
|
|
957
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
958
|
+
return Object.hasOwn(vars, key) ? vars[key] : match;
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
function renderTemplate(template, vars) {
|
|
962
|
+
try {
|
|
963
|
+
const rendered = {
|
|
964
|
+
subject: template.subject ? renderString(template.subject, vars) : void 0,
|
|
965
|
+
body: renderString(template.body, vars)
|
|
966
|
+
};
|
|
967
|
+
return ok(rendered);
|
|
968
|
+
} catch (error) {
|
|
969
|
+
return err(
|
|
970
|
+
HaiReachError.TEMPLATE_RENDER_FAILED,
|
|
971
|
+
reachM("reach_templateRenderFailed", {
|
|
972
|
+
params: {
|
|
973
|
+
error: error instanceof Error ? error.message : String(error)
|
|
974
|
+
}
|
|
975
|
+
}),
|
|
976
|
+
error
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function noDbError(msgKey) {
|
|
981
|
+
return err(
|
|
982
|
+
HaiReachError.SEND_FAILED,
|
|
983
|
+
reachM(msgKey, {
|
|
984
|
+
params: { error: "database not available" }
|
|
985
|
+
})
|
|
986
|
+
);
|
|
987
|
+
}
|
|
988
|
+
function createTemplateRegistry(repo) {
|
|
989
|
+
return {
|
|
990
|
+
async resolve(name) {
|
|
991
|
+
if (!repo) {
|
|
992
|
+
return err(
|
|
993
|
+
HaiReachError.TEMPLATE_NOT_FOUND,
|
|
994
|
+
reachM("reach_templateNotFound", {
|
|
995
|
+
params: { template: name }
|
|
996
|
+
})
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
const dbResult = await repo.findByName(name);
|
|
1000
|
+
if (!dbResult.success) {
|
|
1001
|
+
return dbResult;
|
|
1002
|
+
}
|
|
1003
|
+
if (dbResult.data) {
|
|
1004
|
+
return ok({
|
|
1005
|
+
name: dbResult.data.name,
|
|
1006
|
+
provider: dbResult.data.provider,
|
|
1007
|
+
subject: dbResult.data.subject ?? void 0,
|
|
1008
|
+
body: dbResult.data.body
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
return err(
|
|
1012
|
+
HaiReachError.TEMPLATE_NOT_FOUND,
|
|
1013
|
+
reachM("reach_templateNotFound", {
|
|
1014
|
+
params: { template: name }
|
|
1015
|
+
})
|
|
1016
|
+
);
|
|
1017
|
+
},
|
|
1018
|
+
async save(template) {
|
|
1019
|
+
if (!repo) {
|
|
1020
|
+
return noDbError("reach_templateSaveFailed");
|
|
1021
|
+
}
|
|
1022
|
+
return repo.upsert(template);
|
|
1023
|
+
},
|
|
1024
|
+
async saveBatch(templates) {
|
|
1025
|
+
if (!repo) {
|
|
1026
|
+
return noDbError("reach_templateSaveFailed");
|
|
1027
|
+
}
|
|
1028
|
+
const results = await Promise.all(templates.map((t) => repo.upsert(t)));
|
|
1029
|
+
const failed = results.find((r) => !r.success);
|
|
1030
|
+
if (failed) {
|
|
1031
|
+
return failed;
|
|
1032
|
+
}
|
|
1033
|
+
return ok(void 0);
|
|
1034
|
+
},
|
|
1035
|
+
async remove(name) {
|
|
1036
|
+
if (!repo) {
|
|
1037
|
+
return noDbError("reach_templateDeleteFailed");
|
|
1038
|
+
}
|
|
1039
|
+
return repo.deleteByName(name);
|
|
1040
|
+
},
|
|
1041
|
+
async list() {
|
|
1042
|
+
if (!repo) {
|
|
1043
|
+
return ok([]);
|
|
1044
|
+
}
|
|
1045
|
+
return repo.listTemplates();
|
|
1046
|
+
},
|
|
1047
|
+
async render(name, vars) {
|
|
1048
|
+
const resolved = await this.resolve(name);
|
|
1049
|
+
if (!resolved.success) {
|
|
1050
|
+
return resolved;
|
|
1051
|
+
}
|
|
1052
|
+
return renderTemplate(resolved.data, vars);
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
909
1056
|
|
|
910
1057
|
// src/reach-send.ts
|
|
911
1058
|
var logger6 = core.logger.child({ module: "reach", scope: "send" });
|
|
@@ -947,14 +1094,16 @@ async function preprocessMessage(message, templateRegistry2) {
|
|
|
947
1094
|
if (!message.template) {
|
|
948
1095
|
return ok(message);
|
|
949
1096
|
}
|
|
950
|
-
const
|
|
1097
|
+
const template = await templateRegistry2.resolve(message.template);
|
|
1098
|
+
if (!template.success)
|
|
1099
|
+
return template;
|
|
1100
|
+
const rendered = renderTemplate(template.data, message.vars ?? {});
|
|
951
1101
|
if (!rendered.success) {
|
|
952
1102
|
return rendered;
|
|
953
1103
|
}
|
|
954
|
-
const template = await templateRegistry2.resolve(message.template);
|
|
955
1104
|
const processed = {
|
|
956
1105
|
...message,
|
|
957
|
-
provider: message.provider ||
|
|
1106
|
+
provider: message.provider || template.data.provider || "",
|
|
958
1107
|
subject: message.subject ?? rendered.data.subject,
|
|
959
1108
|
body: message.body ?? rendered.data.body
|
|
960
1109
|
};
|
|
@@ -1118,7 +1267,9 @@ async function executeSend(message, providers2, templateRegistry2, dndConfig2, r
|
|
|
1118
1267
|
if (!message.to) {
|
|
1119
1268
|
return err(
|
|
1120
1269
|
HaiReachError.INVALID_RECIPIENT,
|
|
1121
|
-
reachM("reach_invalidRecipient", {
|
|
1270
|
+
reachM("reach_invalidRecipient", {
|
|
1271
|
+
params: { recipient: "" }
|
|
1272
|
+
})
|
|
1122
1273
|
);
|
|
1123
1274
|
}
|
|
1124
1275
|
const preprocessed = await preprocessMessage(message, templateRegistry2);
|
|
@@ -1135,7 +1286,9 @@ async function executeSend(message, providers2, templateRegistry2, dndConfig2, r
|
|
|
1135
1286
|
if (!providers2.has(providerName)) {
|
|
1136
1287
|
return err(
|
|
1137
1288
|
HaiReachError.PROVIDER_NOT_FOUND,
|
|
1138
|
-
reachM("reach_providerNotFound", {
|
|
1289
|
+
reachM("reach_providerNotFound", {
|
|
1290
|
+
params: { provider: providerName }
|
|
1291
|
+
})
|
|
1139
1292
|
);
|
|
1140
1293
|
}
|
|
1141
1294
|
if (preprocessed.data.sensitive)
|
|
@@ -1189,202 +1342,130 @@ async function executeSend(message, providers2, templateRegistry2, dndConfig2, r
|
|
|
1189
1342
|
}
|
|
1190
1343
|
return result;
|
|
1191
1344
|
}
|
|
1192
|
-
function renderString(template, vars) {
|
|
1193
|
-
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
1194
|
-
return Object.hasOwn(vars, key) ? vars[key] : match;
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1197
|
-
function renderTemplate(template, vars) {
|
|
1198
|
-
try {
|
|
1199
|
-
const rendered = {
|
|
1200
|
-
subject: template.subject ? renderString(template.subject, vars) : void 0,
|
|
1201
|
-
body: renderString(template.body, vars)
|
|
1202
|
-
};
|
|
1203
|
-
return ok(rendered);
|
|
1204
|
-
} catch (error) {
|
|
1205
|
-
return err(
|
|
1206
|
-
HaiReachError.TEMPLATE_RENDER_FAILED,
|
|
1207
|
-
reachM("reach_templateRenderFailed", {
|
|
1208
|
-
params: { error: error instanceof Error ? error.message : String(error) }
|
|
1209
|
-
}),
|
|
1210
|
-
error
|
|
1211
|
-
);
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
function noDbError(msgKey) {
|
|
1215
|
-
return err(
|
|
1216
|
-
HaiReachError.SEND_FAILED,
|
|
1217
|
-
reachM(msgKey, { params: { error: "database not available" } })
|
|
1218
|
-
);
|
|
1219
|
-
}
|
|
1220
|
-
function createTemplateRegistry(repo) {
|
|
1221
|
-
return {
|
|
1222
|
-
async resolve(name) {
|
|
1223
|
-
if (!repo) {
|
|
1224
|
-
return err(
|
|
1225
|
-
HaiReachError.TEMPLATE_NOT_FOUND,
|
|
1226
|
-
reachM("reach_templateNotFound", { params: { template: name } })
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
const dbResult = await repo.findByName(name);
|
|
1230
|
-
if (!dbResult.success) {
|
|
1231
|
-
return dbResult;
|
|
1232
|
-
}
|
|
1233
|
-
if (dbResult.data) {
|
|
1234
|
-
return ok({
|
|
1235
|
-
name: dbResult.data.name,
|
|
1236
|
-
provider: dbResult.data.provider,
|
|
1237
|
-
subject: dbResult.data.subject ?? void 0,
|
|
1238
|
-
body: dbResult.data.body
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1241
|
-
return err(
|
|
1242
|
-
HaiReachError.TEMPLATE_NOT_FOUND,
|
|
1243
|
-
reachM("reach_templateNotFound", { params: { template: name } })
|
|
1244
|
-
);
|
|
1245
|
-
},
|
|
1246
|
-
async save(template) {
|
|
1247
|
-
if (!repo) {
|
|
1248
|
-
return noDbError("reach_templateSaveFailed");
|
|
1249
|
-
}
|
|
1250
|
-
return repo.upsert(template);
|
|
1251
|
-
},
|
|
1252
|
-
async saveBatch(templates) {
|
|
1253
|
-
if (!repo) {
|
|
1254
|
-
return noDbError("reach_templateSaveFailed");
|
|
1255
|
-
}
|
|
1256
|
-
const results = await Promise.all(templates.map((t) => repo.upsert(t)));
|
|
1257
|
-
const failed = results.find((r) => !r.success);
|
|
1258
|
-
if (failed) {
|
|
1259
|
-
return failed;
|
|
1260
|
-
}
|
|
1261
|
-
return ok(void 0);
|
|
1262
|
-
},
|
|
1263
|
-
async remove(name) {
|
|
1264
|
-
if (!repo) {
|
|
1265
|
-
return noDbError("reach_templateDeleteFailed");
|
|
1266
|
-
}
|
|
1267
|
-
return repo.deleteByName(name);
|
|
1268
|
-
},
|
|
1269
|
-
async list() {
|
|
1270
|
-
if (!repo) {
|
|
1271
|
-
return ok([]);
|
|
1272
|
-
}
|
|
1273
|
-
return repo.listTemplates();
|
|
1274
|
-
},
|
|
1275
|
-
async render(name, vars) {
|
|
1276
|
-
const resolved = await this.resolve(name);
|
|
1277
|
-
if (!resolved.success) {
|
|
1278
|
-
return resolved;
|
|
1279
|
-
}
|
|
1280
|
-
return renderTemplate(resolved.data, vars);
|
|
1281
|
-
}
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
1345
|
function migrateReach(options = {}, db = reldb) {
|
|
1285
|
-
const definitions = {
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1302
|
-
|
|
1346
|
+
const definitions = {
|
|
1347
|
+
queue_id: db.config?.type === "mysql" ? "VARCHAR(255)" : "TEXT",
|
|
1348
|
+
retry_safe: "BOOLEAN NOT NULL DEFAULT FALSE",
|
|
1349
|
+
attempt_count: "BIGINT NOT NULL DEFAULT 0",
|
|
1350
|
+
max_attempts: "BIGINT NOT NULL DEFAULT 5",
|
|
1351
|
+
next_attempt_at: "BIGINT NOT NULL DEFAULT 0",
|
|
1352
|
+
retry_delay_ms: "BIGINT NOT NULL DEFAULT 60000",
|
|
1353
|
+
max_retry_delay_ms: "BIGINT NOT NULL DEFAULT 3600000",
|
|
1354
|
+
last_failure: "TEXT"
|
|
1355
|
+
};
|
|
1356
|
+
const steps = [
|
|
1357
|
+
{
|
|
1358
|
+
version: 1,
|
|
1359
|
+
name: "durable-delivery-ledger",
|
|
1360
|
+
async needed(sql) {
|
|
1361
|
+
const found = await db.schemaColumns("hai_reach_send_log", sql);
|
|
1362
|
+
return found.success ? ok(found.data.length > 0 && Object.keys(definitions).some((name) => !found.data.includes(name))) : found;
|
|
1363
|
+
},
|
|
1364
|
+
async apply(sql) {
|
|
1365
|
+
const found = await db.schemaColumns("hai_reach_send_log", sql);
|
|
1366
|
+
if (!found.success)
|
|
1367
|
+
return found;
|
|
1368
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
1369
|
+
if (found.data.includes(name))
|
|
1370
|
+
continue;
|
|
1371
|
+
const added = await sql.execute(`ALTER TABLE hai_reach_send_log ADD COLUMN ${name} ${definition}`);
|
|
1372
|
+
if (!added.success)
|
|
1373
|
+
return added;
|
|
1374
|
+
}
|
|
1375
|
+
return ok(void 0);
|
|
1303
1376
|
}
|
|
1304
|
-
return ok(void 0);
|
|
1305
|
-
}
|
|
1306
|
-
}, {
|
|
1307
|
-
version: 2,
|
|
1308
|
-
name: "legacy-delivery-isolation",
|
|
1309
|
-
async needed(sql) {
|
|
1310
|
-
const found = await schemaColumns(db, "hai_reach_send_log", sql);
|
|
1311
|
-
if (!found.success || !found.data.length)
|
|
1312
|
-
return found.success ? ok(false) : found;
|
|
1313
|
-
if (!found.data.includes("queue_id"))
|
|
1314
|
-
return ok(true);
|
|
1315
|
-
const row = await sql.get("SELECT id FROM hai_reach_send_log WHERE queue_id IS NULL AND status IN ('pending','processing','unknown') LIMIT 1");
|
|
1316
|
-
return row.success ? ok(Boolean(row.data)) : row;
|
|
1317
1377
|
},
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1378
|
+
{
|
|
1379
|
+
version: 2,
|
|
1380
|
+
name: "legacy-delivery-isolation",
|
|
1381
|
+
async needed(sql) {
|
|
1382
|
+
const found = await db.schemaColumns("hai_reach_send_log", sql);
|
|
1383
|
+
if (!found.success || !found.data.length) {
|
|
1384
|
+
return found.success ? ok(false) : found;
|
|
1385
|
+
}
|
|
1386
|
+
if (!found.data.includes("queue_id"))
|
|
1387
|
+
return ok(true);
|
|
1388
|
+
const row = await sql.get("SELECT id FROM hai_reach_send_log WHERE queue_id IS NULL AND status IN ('pending','processing','unknown') LIMIT 1");
|
|
1389
|
+
return row.success ? ok(Boolean(row.data)) : row;
|
|
1390
|
+
},
|
|
1391
|
+
async apply(sql) {
|
|
1392
|
+
const isolated = await sql.execute("UPDATE hai_reach_send_log SET status='unknown',processing_owner=NULL,locked_until=NULL WHERE status='processing' AND queue_id IS NULL");
|
|
1393
|
+
if (!isolated.success)
|
|
1394
|
+
return isolated;
|
|
1395
|
+
while (true) {
|
|
1396
|
+
const rows = await sql.query("SELECT id FROM hai_reach_send_log WHERE queue_id IS NULL AND status IN ('pending','unknown') ORDER BY id LIMIT 100");
|
|
1397
|
+
if (!rows.success)
|
|
1398
|
+
return rows;
|
|
1399
|
+
for (const row of rows.data) {
|
|
1400
|
+
const saved = await sql.execute("UPDATE hai_reach_send_log SET queue_id=? WHERE id=? AND queue_id IS NULL", [
|
|
1401
|
+
randomUUID(),
|
|
1402
|
+
row.id
|
|
1403
|
+
]);
|
|
1404
|
+
if (!saved.success)
|
|
1405
|
+
return saved;
|
|
1406
|
+
}
|
|
1407
|
+
if (rows.data.length < 100)
|
|
1408
|
+
return ok(void 0);
|
|
1330
1409
|
}
|
|
1331
|
-
if (rows.data.length < 100)
|
|
1332
|
-
return ok(void 0);
|
|
1333
1410
|
}
|
|
1334
|
-
}
|
|
1335
|
-
}, {
|
|
1336
|
-
version: 3,
|
|
1337
|
-
name: "credential-payload-retirement",
|
|
1338
|
-
async needed(sql) {
|
|
1339
|
-
const found = await schemaColumns(db, "hai_reach_send_log", sql);
|
|
1340
|
-
if (!found.success || !found.data.length)
|
|
1341
|
-
return found.success ? ok(false) : found;
|
|
1342
|
-
const ledger = await schemaColumns(db, "hai_schema_migrations", sql);
|
|
1343
|
-
if (!ledger.success)
|
|
1344
|
-
return ledger;
|
|
1345
|
-
const version = ledger.data.length ? await sql.get("SELECT version FROM hai_schema_migrations WHERE module=?", ["reach"]) : ok(null);
|
|
1346
|
-
if (!version.success)
|
|
1347
|
-
return version;
|
|
1348
|
-
if (Number(version.data?.version ?? 0) >= 3)
|
|
1349
|
-
return ok(false);
|
|
1350
|
-
const row = await sql.get("SELECT id FROM hai_reach_send_log WHERE subject IS NOT NULL OR body IS NOT NULL OR template IS NOT NULL OR vars_json IS NOT NULL OR extra_json IS NOT NULL LIMIT 1");
|
|
1351
|
-
return row.success ? ok(Boolean(row.data)) : row;
|
|
1352
1411
|
},
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
const columns = await schemaColumns(db, "hai_reach_send_log", sql);
|
|
1361
|
-
if (!columns.success || !columns.data.length)
|
|
1362
|
-
return columns.success ? ok(false) : columns;
|
|
1363
|
-
if (!columns.data.includes("queue_id"))
|
|
1364
|
-
return ok(true);
|
|
1365
|
-
if (db.config?.type === "sqlite") {
|
|
1366
|
-
const indexes = await sql.query("PRAGMA index_list(hai_reach_send_log)");
|
|
1367
|
-
if (!indexes.success)
|
|
1368
|
-
return indexes;
|
|
1369
|
-
for (const index of indexes.data) {
|
|
1370
|
-
if (!index.unique || !/^\w+$/.test(index.name))
|
|
1371
|
-
continue;
|
|
1372
|
-
const fields = await sql.query(`PRAGMA index_info(${index.name})`);
|
|
1373
|
-
if (!fields.success)
|
|
1374
|
-
return fields;
|
|
1375
|
-
if (fields.data.length === 1 && fields.data[0]?.name === "queue_id")
|
|
1376
|
-
return ok(false);
|
|
1412
|
+
{
|
|
1413
|
+
version: 3,
|
|
1414
|
+
name: "credential-payload-retirement",
|
|
1415
|
+
async needed(sql) {
|
|
1416
|
+
const found = await db.schemaColumns("hai_reach_send_log", sql);
|
|
1417
|
+
if (!found.success || !found.data.length) {
|
|
1418
|
+
return found.success ? ok(false) : found;
|
|
1377
1419
|
}
|
|
1378
|
-
|
|
1420
|
+
const ledger = await db.schemaColumns("hai_schema_migrations", sql);
|
|
1421
|
+
if (!ledger.success)
|
|
1422
|
+
return ledger;
|
|
1423
|
+
const version = ledger.data.length ? await sql.get("SELECT version FROM hai_schema_migrations WHERE module=?", ["reach"]) : ok(null);
|
|
1424
|
+
if (!version.success)
|
|
1425
|
+
return version;
|
|
1426
|
+
if (Number(version.data?.version ?? 0) >= 3)
|
|
1427
|
+
return ok(false);
|
|
1428
|
+
const row = await sql.get("SELECT id FROM hai_reach_send_log WHERE subject IS NOT NULL OR body IS NOT NULL OR template IS NOT NULL OR vars_json IS NOT NULL OR extra_json IS NOT NULL LIMIT 1");
|
|
1429
|
+
return row.success ? ok(Boolean(row.data)) : row;
|
|
1430
|
+
},
|
|
1431
|
+
async apply(sql) {
|
|
1432
|
+
return sql.execute("UPDATE hai_reach_send_log SET body=NULL,subject=NULL,vars_json=NULL,extra_json=NULL,template=NULL,status=CASE WHEN status IN ('pending','processing') THEN 'discarded' WHEN status='sending' THEN 'unknown' ELSE status END,processing_owner=NULL,locked_until=NULL WHERE subject IS NOT NULL OR body IS NOT NULL OR template IS NOT NULL OR vars_json IS NOT NULL OR extra_json IS NOT NULL");
|
|
1379
1433
|
}
|
|
1380
|
-
const found = db.config?.type === "mysql" ? await sql.get("SELECT index_name FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? AND non_unique=0 GROUP BY index_name HAVING COUNT(*)=1 AND MAX(column_name)=?", ["hai_reach_send_log", "queue_id"]) : await sql.get("SELECT i.indexrelid FROM pg_index i JOIN pg_class t ON t.oid=i.indrelid JOIN pg_namespace n ON n.oid=t.relnamespace JOIN pg_attribute a ON a.attrelid=t.oid AND a.attnum=ANY(i.indkey) WHERE t.relname=? AND n.nspname=current_schema() AND i.indisunique AND i.indnatts=1 AND a.attname=?", ["hai_reach_send_log", "queue_id"]);
|
|
1381
|
-
return found.success ? ok(!found.data) : found;
|
|
1382
1434
|
},
|
|
1383
|
-
|
|
1384
|
-
|
|
1435
|
+
{
|
|
1436
|
+
version: 4,
|
|
1437
|
+
name: "unique-operation-receipts",
|
|
1438
|
+
async needed(sql) {
|
|
1439
|
+
const columns = await db.schemaColumns("hai_reach_send_log", sql);
|
|
1440
|
+
if (!columns.success || !columns.data.length) {
|
|
1441
|
+
return columns.success ? ok(false) : columns;
|
|
1442
|
+
}
|
|
1443
|
+
if (!columns.data.includes("queue_id"))
|
|
1444
|
+
return ok(true);
|
|
1445
|
+
if (db.config?.type === "sqlite") {
|
|
1446
|
+
const indexes = await sql.query("PRAGMA index_list(hai_reach_send_log)");
|
|
1447
|
+
if (!indexes.success)
|
|
1448
|
+
return indexes;
|
|
1449
|
+
for (const index of indexes.data) {
|
|
1450
|
+
if (!index.unique || !/^\w+$/.test(index.name))
|
|
1451
|
+
continue;
|
|
1452
|
+
const fields = await sql.query(`PRAGMA index_info(${index.name})`);
|
|
1453
|
+
if (!fields.success)
|
|
1454
|
+
return fields;
|
|
1455
|
+
if (fields.data.length === 1 && fields.data[0]?.name === "queue_id")
|
|
1456
|
+
return ok(false);
|
|
1457
|
+
}
|
|
1458
|
+
return ok(true);
|
|
1459
|
+
}
|
|
1460
|
+
const found = db.config?.type === "mysql" ? await sql.get("SELECT index_name FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name=? AND non_unique=0 GROUP BY index_name HAVING COUNT(*)=1 AND MAX(column_name)=?", ["hai_reach_send_log", "queue_id"]) : await sql.get("SELECT i.indexrelid FROM pg_index i JOIN pg_class t ON t.oid=i.indrelid JOIN pg_namespace n ON n.oid=t.relnamespace JOIN pg_attribute a ON a.attrelid=t.oid AND a.attnum=ANY(i.indkey) WHERE t.relname=? AND n.nspname=current_schema() AND i.indisunique AND i.indnatts=1 AND a.attname=?", ["hai_reach_send_log", "queue_id"]);
|
|
1461
|
+
return found.success ? ok(!found.data) : found;
|
|
1462
|
+
},
|
|
1463
|
+
async apply(sql) {
|
|
1464
|
+
return sql.execute("CREATE UNIQUE INDEX uq_hai_reach_queue_v1 ON hai_reach_send_log(queue_id)");
|
|
1465
|
+
}
|
|
1385
1466
|
}
|
|
1386
|
-
|
|
1387
|
-
return migrateSchema(
|
|
1467
|
+
];
|
|
1468
|
+
return db.migrateSchema("reach", steps, options);
|
|
1388
1469
|
}
|
|
1389
1470
|
|
|
1390
1471
|
// src/repositories/reach-repository-send-log.ts
|
|
@@ -1395,25 +1476,47 @@ function claimableWhere(clock) {
|
|
|
1395
1476
|
return `(attempt_count < max_attempts AND ((status = ? AND next_attempt_at <= ?) OR (status = ? AND updated_at < ? AND (locked_until IS NULL OR locked_until <= ${clock})) OR (status = ? AND updated_at < ? AND retry_safe = TRUE AND locked_until <= ${clock})))`;
|
|
1396
1477
|
}
|
|
1397
1478
|
function claimableParams(eligibleBefore) {
|
|
1398
|
-
return [
|
|
1479
|
+
return [
|
|
1480
|
+
"pending",
|
|
1481
|
+
eligibleBefore,
|
|
1482
|
+
"processing",
|
|
1483
|
+
eligibleBefore,
|
|
1484
|
+
"sending",
|
|
1485
|
+
eligibleBefore
|
|
1486
|
+
];
|
|
1399
1487
|
}
|
|
1400
1488
|
function buildSendLogError(detail, cause) {
|
|
1401
1489
|
return err(
|
|
1402
1490
|
HaiReachError.SEND_FAILED,
|
|
1403
|
-
reachM("reach_sendFailed", {
|
|
1491
|
+
reachM("reach_sendFailed", {
|
|
1492
|
+
params: { error: detail }
|
|
1493
|
+
}),
|
|
1404
1494
|
cause
|
|
1405
1495
|
);
|
|
1406
1496
|
}
|
|
1407
1497
|
function normalizeSendLog(row) {
|
|
1408
1498
|
const normalized = { ...row };
|
|
1409
|
-
for (const field of [
|
|
1499
|
+
for (const field of [
|
|
1500
|
+
"id",
|
|
1501
|
+
"createdAt",
|
|
1502
|
+
"updatedAt",
|
|
1503
|
+
"lockedUntil",
|
|
1504
|
+
"nextAttemptAt",
|
|
1505
|
+
"attemptCount",
|
|
1506
|
+
"maxAttempts",
|
|
1507
|
+
"retryDelayMs",
|
|
1508
|
+
"maxRetryDelayMs"
|
|
1509
|
+
]) {
|
|
1410
1510
|
const value = row[field];
|
|
1411
1511
|
if (value === void 0 && (field === "updatedAt" || field === "lockedUntil"))
|
|
1412
1512
|
continue;
|
|
1413
1513
|
const validType = typeof value === "number" || typeof value === "bigint" || typeof value === "string" && /^\d+$/.test(value);
|
|
1414
1514
|
const integer = validType ? Number(value) : Number.NaN;
|
|
1415
|
-
if (!Number.isSafeInteger(integer) || integer < 0)
|
|
1416
|
-
return buildSendLogError(reachM("reach_invalidStoredInteger", {
|
|
1515
|
+
if (!Number.isSafeInteger(integer) || integer < 0) {
|
|
1516
|
+
return buildSendLogError(reachM("reach_invalidStoredInteger", {
|
|
1517
|
+
params: { field }
|
|
1518
|
+
}));
|
|
1519
|
+
}
|
|
1417
1520
|
Object.assign(normalized, { [field]: integer });
|
|
1418
1521
|
}
|
|
1419
1522
|
return ok(normalized);
|
|
@@ -1597,7 +1700,9 @@ async function createSendLogRepository(db) {
|
|
|
1597
1700
|
if (!initResult.success) {
|
|
1598
1701
|
return err(
|
|
1599
1702
|
HaiReachError.SEND_FAILED,
|
|
1600
|
-
reachM("reach_sendFailed", {
|
|
1703
|
+
reachM("reach_sendFailed", {
|
|
1704
|
+
params: { error: initResult.error.message }
|
|
1705
|
+
}),
|
|
1601
1706
|
initResult.error
|
|
1602
1707
|
);
|
|
1603
1708
|
}
|
|
@@ -1621,7 +1726,12 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1621
1726
|
async settleSensitive(queueId, status, messageId) {
|
|
1622
1727
|
const settled = await this.sql().execute(
|
|
1623
1728
|
`UPDATE ${TABLE_NAME} SET status = ?, message_id = ?, locked_until = NULL, updated_at = ? WHERE queue_id = ? AND status = 'sending' AND body IS NULL AND vars_json IS NULL AND retry_safe = FALSE`,
|
|
1624
|
-
[
|
|
1729
|
+
[
|
|
1730
|
+
status,
|
|
1731
|
+
messageId ?? null,
|
|
1732
|
+
Date.now(),
|
|
1733
|
+
queueId
|
|
1734
|
+
]
|
|
1625
1735
|
);
|
|
1626
1736
|
if (!settled.success)
|
|
1627
1737
|
return settled;
|
|
@@ -1663,7 +1773,11 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1663
1773
|
}
|
|
1664
1774
|
/** 获取所有待发送记录 */
|
|
1665
1775
|
async findPending(tx) {
|
|
1666
|
-
const result = await this.findAll({
|
|
1776
|
+
const result = await this.findAll({
|
|
1777
|
+
where: "status = ?",
|
|
1778
|
+
params: ["pending"],
|
|
1779
|
+
orderBy: "created_at ASC"
|
|
1780
|
+
}, tx);
|
|
1667
1781
|
if (!result.success) {
|
|
1668
1782
|
return result;
|
|
1669
1783
|
}
|
|
@@ -1685,7 +1799,10 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1685
1799
|
ORDER BY created_at ASC
|
|
1686
1800
|
LIMIT ?
|
|
1687
1801
|
FOR UPDATE SKIP LOCKED`,
|
|
1688
|
-
[
|
|
1802
|
+
[
|
|
1803
|
+
...claimableParams(eligibleBefore),
|
|
1804
|
+
limit
|
|
1805
|
+
]
|
|
1689
1806
|
);
|
|
1690
1807
|
if (!lockedRowsResult.success) {
|
|
1691
1808
|
throw lockedRowsResult.error;
|
|
@@ -1705,7 +1822,12 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1705
1822
|
`UPDATE ${TABLE_NAME}
|
|
1706
1823
|
SET status = ?, processing_owner = ?, locked_until = ${clock} + ?, updated_at = ${clock}, attempt_count = attempt_count + 1
|
|
1707
1824
|
WHERE id IN (${placeholders})`,
|
|
1708
|
-
[
|
|
1825
|
+
[
|
|
1826
|
+
"processing",
|
|
1827
|
+
owner,
|
|
1828
|
+
lockMs,
|
|
1829
|
+
...claimedIds
|
|
1830
|
+
]
|
|
1709
1831
|
);
|
|
1710
1832
|
if (!updateResult.success) {
|
|
1711
1833
|
throw updateResult.error;
|
|
@@ -1735,7 +1857,13 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1735
1857
|
`UPDATE ${TABLE_NAME}
|
|
1736
1858
|
SET status = ?, processing_owner = ?, locked_until = ${clock} + ?, updated_at = ${clock}, attempt_count = attempt_count + 1
|
|
1737
1859
|
WHERE id = ? AND ${claimableWhere(clock)}`,
|
|
1738
|
-
[
|
|
1860
|
+
[
|
|
1861
|
+
"processing",
|
|
1862
|
+
owner,
|
|
1863
|
+
lockMs,
|
|
1864
|
+
row.id,
|
|
1865
|
+
...claimableParams(eligibleBefore)
|
|
1866
|
+
]
|
|
1739
1867
|
);
|
|
1740
1868
|
if (!updateResult.success) {
|
|
1741
1869
|
throw updateResult.error;
|
|
@@ -1789,7 +1917,13 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1789
1917
|
WHERE attempt_count >= max_attempts
|
|
1790
1918
|
AND (status = ? OR (status = ? AND (locked_until IS NULL OR locked_until <= ${clock}))
|
|
1791
1919
|
OR (status = ? AND retry_safe = TRUE AND locked_until <= ${clock}))`,
|
|
1792
|
-
[
|
|
1920
|
+
[
|
|
1921
|
+
"dead",
|
|
1922
|
+
"attempts_exhausted",
|
|
1923
|
+
"pending",
|
|
1924
|
+
"processing",
|
|
1925
|
+
"sending"
|
|
1926
|
+
]
|
|
1793
1927
|
);
|
|
1794
1928
|
if (!exhausted.success)
|
|
1795
1929
|
throw exhausted.error;
|
|
@@ -1829,7 +1963,16 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1829
1963
|
SET status = ?, last_failure = ?, next_attempt_at = ${dead ? "0" : `${clock} + ?`}, processing_owner = NULL, locked_until = NULL, updated_at = ${clock}
|
|
1830
1964
|
WHERE id = ? AND processing_owner = ? AND locked_until > ${clock} AND attempt_count = ?
|
|
1831
1965
|
AND (status = ? OR (status = ? AND retry_safe = TRUE))`,
|
|
1832
|
-
[
|
|
1966
|
+
[
|
|
1967
|
+
dead ? "dead" : "pending",
|
|
1968
|
+
reason,
|
|
1969
|
+
...dead ? [] : [delay],
|
|
1970
|
+
row.id,
|
|
1971
|
+
owner,
|
|
1972
|
+
row.attemptCount,
|
|
1973
|
+
"processing",
|
|
1974
|
+
"sending"
|
|
1975
|
+
]
|
|
1833
1976
|
);
|
|
1834
1977
|
if (!result.success) {
|
|
1835
1978
|
return buildSendLogError(result.error.message, result.error);
|
|
@@ -1845,7 +1988,12 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1845
1988
|
const result = await this.sql().execute(
|
|
1846
1989
|
`UPDATE ${TABLE_NAME} SET status = ?, attempt_count = 0, next_attempt_at = ${clock}, last_failure = NULL, updated_at = CASE WHEN updated_at >= ${clock} THEN updated_at + 1 ELSE ${clock} END
|
|
1847
1990
|
WHERE queue_id = ? AND status = ? AND updated_at = ?`,
|
|
1848
|
-
[
|
|
1991
|
+
[
|
|
1992
|
+
"pending",
|
|
1993
|
+
queueId,
|
|
1994
|
+
"dead",
|
|
1995
|
+
expectedUpdatedAt
|
|
1996
|
+
]
|
|
1849
1997
|
);
|
|
1850
1998
|
return result.success && result.data.changes === 1 ? ok(void 0) : buildSendLogError(reachM("reach_deadRequeueFailed"));
|
|
1851
1999
|
}
|
|
@@ -1856,7 +2004,14 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1856
2004
|
const result = await this.sql().execute(
|
|
1857
2005
|
`UPDATE ${TABLE_NAME} SET status = ?, retry_safe = ?, locked_until = ${clock} + ?, updated_at = ${clock}
|
|
1858
2006
|
WHERE id = ? AND status = ? AND processing_owner = ? AND locked_until > ${clock}`,
|
|
1859
|
-
[
|
|
2007
|
+
[
|
|
2008
|
+
"sending",
|
|
2009
|
+
flag,
|
|
2010
|
+
lockMs,
|
|
2011
|
+
id,
|
|
2012
|
+
"processing",
|
|
2013
|
+
owner
|
|
2014
|
+
]
|
|
1860
2015
|
);
|
|
1861
2016
|
return result.success && result.data.changes === 1 ? ok(void 0) : buildSendLogError(reachM("reach_claimLost"));
|
|
1862
2017
|
}
|
|
@@ -1866,7 +2021,13 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1866
2021
|
const result = await this.sql().execute(
|
|
1867
2022
|
`UPDATE ${TABLE_NAME} SET locked_until = ${clock} + ?, updated_at = ${clock}
|
|
1868
2023
|
WHERE id = ? AND processing_owner = ? AND status IN (?, ?) AND locked_until > ${clock}`,
|
|
1869
|
-
[
|
|
2024
|
+
[
|
|
2025
|
+
lockMs,
|
|
2026
|
+
id,
|
|
2027
|
+
owner,
|
|
2028
|
+
"processing",
|
|
2029
|
+
"sending"
|
|
2030
|
+
]
|
|
1870
2031
|
);
|
|
1871
2032
|
return result.success && result.data.changes === 1 ? ok(void 0) : buildSendLogError(reachM("reach_claimLost"));
|
|
1872
2033
|
}
|
|
@@ -1875,7 +2036,13 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1875
2036
|
const result = await this.sql().execute(
|
|
1876
2037
|
`UPDATE ${TABLE_NAME} SET status = ?, processing_owner = NULL, locked_until = NULL, next_attempt_at = 0, updated_at = ${this.clockSql()}
|
|
1877
2038
|
WHERE id = ? AND processing_owner = ? AND status IN (?, ?)`,
|
|
1878
|
-
[
|
|
2039
|
+
[
|
|
2040
|
+
"unknown",
|
|
2041
|
+
id,
|
|
2042
|
+
owner,
|
|
2043
|
+
"processing",
|
|
2044
|
+
"sending"
|
|
2045
|
+
]
|
|
1879
2046
|
);
|
|
1880
2047
|
return result.success && result.data.changes === 1 ? ok(void 0) : buildSendLogError(reachM("reach_claimLost"));
|
|
1881
2048
|
}
|
|
@@ -1883,7 +2050,12 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1883
2050
|
async resolveUnknown(queueId, resolution) {
|
|
1884
2051
|
const result = await this.sql().execute(
|
|
1885
2052
|
`UPDATE ${TABLE_NAME} SET status = ?, message_id = ?, next_attempt_at = 0, updated_at = ${this.clockSql()} WHERE queue_id = ? AND status = ?`,
|
|
1886
|
-
[
|
|
2053
|
+
[
|
|
2054
|
+
resolution.status,
|
|
2055
|
+
resolution.messageId ?? null,
|
|
2056
|
+
queueId,
|
|
2057
|
+
"unknown"
|
|
2058
|
+
]
|
|
1887
2059
|
);
|
|
1888
2060
|
return result.success && result.data.changes === 1 ? ok(void 0) : buildSendLogError(reachM("reach_unknownResolutionFailed"));
|
|
1889
2061
|
}
|
|
@@ -1894,7 +2066,13 @@ var DbSendLogRepository = class extends BaseReldbCrudRepository {
|
|
|
1894
2066
|
`UPDATE ${TABLE_NAME}
|
|
1895
2067
|
SET status = ?, message_id = ?, processing_owner = NULL, locked_until = NULL, next_attempt_at = 0, updated_at = ${clock}
|
|
1896
2068
|
WHERE id = ? AND status = ? AND processing_owner = ? AND locked_until > ${clock}`,
|
|
1897
|
-
[
|
|
2069
|
+
[
|
|
2070
|
+
"sent",
|
|
2071
|
+
messageId ?? null,
|
|
2072
|
+
id,
|
|
2073
|
+
"sending",
|
|
2074
|
+
owner
|
|
2075
|
+
]
|
|
1898
2076
|
);
|
|
1899
2077
|
if (!result.success) {
|
|
1900
2078
|
return buildSendLogError(result.error.message, result.error);
|
|
@@ -1978,7 +2156,9 @@ async function createTemplateRepository(db) {
|
|
|
1978
2156
|
if (!initResult.success) {
|
|
1979
2157
|
return err(
|
|
1980
2158
|
HaiReachError.SEND_FAILED,
|
|
1981
|
-
reachM("reach_templateDbInitFailed", {
|
|
2159
|
+
reachM("reach_templateDbInitFailed", {
|
|
2160
|
+
params: { error: initResult.error.message }
|
|
2161
|
+
}),
|
|
1982
2162
|
initResult.error
|
|
1983
2163
|
);
|
|
1984
2164
|
}
|
|
@@ -2006,7 +2186,11 @@ var DbTemplateRepository = class extends BaseReldbCrudRepository {
|
|
|
2006
2186
|
}
|
|
2007
2187
|
/** 按名称查找模板 */
|
|
2008
2188
|
async findByName(name, tx) {
|
|
2009
|
-
const result = await this.findAll({
|
|
2189
|
+
const result = await this.findAll({
|
|
2190
|
+
where: "name = ?",
|
|
2191
|
+
params: [name],
|
|
2192
|
+
limit: 1
|
|
2193
|
+
}, tx);
|
|
2010
2194
|
if (!result.success) {
|
|
2011
2195
|
return result;
|
|
2012
2196
|
}
|
|
@@ -2083,7 +2267,6 @@ var currentConfig = null;
|
|
|
2083
2267
|
var dndConfig;
|
|
2084
2268
|
var templateRegistry = createTemplateRegistry();
|
|
2085
2269
|
var sendLogRepo = null;
|
|
2086
|
-
var templateRepo = null;
|
|
2087
2270
|
var initInProgress = false;
|
|
2088
2271
|
var initCompletion = null;
|
|
2089
2272
|
var closeInProgress = null;
|
|
@@ -2115,7 +2298,9 @@ async function tryInitSendLogRepo() {
|
|
|
2115
2298
|
}
|
|
2116
2299
|
return result.data;
|
|
2117
2300
|
} catch (error) {
|
|
2118
|
-
logger7.debug("Send log repository not initialized", {
|
|
2301
|
+
logger7.debug("Send log repository not initialized", {
|
|
2302
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2303
|
+
});
|
|
2119
2304
|
return null;
|
|
2120
2305
|
}
|
|
2121
2306
|
}
|
|
@@ -2132,7 +2317,9 @@ async function tryInitTemplateRepo() {
|
|
|
2132
2317
|
}
|
|
2133
2318
|
return result.data;
|
|
2134
2319
|
} catch (error) {
|
|
2135
|
-
logger7.debug("Template repository not initialized", {
|
|
2320
|
+
logger7.debug("Template repository not initialized", {
|
|
2321
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2322
|
+
});
|
|
2136
2323
|
return null;
|
|
2137
2324
|
}
|
|
2138
2325
|
}
|
|
@@ -2158,7 +2345,6 @@ async function doClose() {
|
|
|
2158
2345
|
dndConfig = void 0;
|
|
2159
2346
|
templateRegistry = createTemplateRegistry();
|
|
2160
2347
|
sendLogRepo = null;
|
|
2161
|
-
templateRepo = null;
|
|
2162
2348
|
resetSendLogRepoSingleton();
|
|
2163
2349
|
resetTemplateRepoSingleton();
|
|
2164
2350
|
resetSendState();
|
|
@@ -2175,7 +2361,9 @@ async function doInit(config, options) {
|
|
|
2175
2361
|
logger7.error("Reach config validation failed", { error: parseResult.error.message });
|
|
2176
2362
|
return err(
|
|
2177
2363
|
HaiReachError.CONFIG_ERROR,
|
|
2178
|
-
reachM("reach_configError", {
|
|
2364
|
+
reachM("reach_configError", {
|
|
2365
|
+
params: { error: parseResult.error.message }
|
|
2366
|
+
}),
|
|
2179
2367
|
parseResult.error
|
|
2180
2368
|
);
|
|
2181
2369
|
}
|
|
@@ -2187,6 +2375,10 @@ async function doInit(config, options) {
|
|
|
2187
2375
|
}
|
|
2188
2376
|
if (core.module.requiresSharedState(process.env) && parsed.providers.some((provider) => provider.type === "console"))
|
|
2189
2377
|
return err(HaiReachError.CONFIG_ERROR, reachM("reach_productionProviderRequired"));
|
|
2378
|
+
if (parsed.templates?.length && !reldb.isInitialized)
|
|
2379
|
+
return err(HaiReachError.CONFIG_ERROR, reachM("reach_templatePersistenceRequired"));
|
|
2380
|
+
const newProviders = /* @__PURE__ */ new Map();
|
|
2381
|
+
let published = false;
|
|
2190
2382
|
try {
|
|
2191
2383
|
let requiredQueue = null;
|
|
2192
2384
|
if (parsed.dnd?.enabled && parsed.dnd.strategy === "delay") {
|
|
@@ -2197,9 +2389,9 @@ async function doInit(config, options) {
|
|
|
2197
2389
|
return err(HaiReachError.CONFIG_ERROR, reachM("reach_dndPersistenceRequired"), queue.error);
|
|
2198
2390
|
requiredQueue = queue.data;
|
|
2199
2391
|
}
|
|
2200
|
-
const newProviders = /* @__PURE__ */ new Map();
|
|
2201
2392
|
for (const providerConfig of parsed.providers) {
|
|
2202
2393
|
const provider = createProvider(providerConfig, gatewayHooks.get(providerConfig.name));
|
|
2394
|
+
newProviders.set(providerConfig.name, provider);
|
|
2203
2395
|
const connectResult = await provider.connect(providerConfig);
|
|
2204
2396
|
if (!connectResult.success) {
|
|
2205
2397
|
logger7.error("Provider initialization failed", {
|
|
@@ -2208,30 +2400,27 @@ async function doInit(config, options) {
|
|
|
2208
2400
|
code: connectResult.error.code,
|
|
2209
2401
|
message: connectResult.error.message
|
|
2210
2402
|
});
|
|
2211
|
-
logger7.info("Rolling back connected providers", { count: newProviders.size });
|
|
2212
|
-
for (const [name, p] of newProviders.entries()) {
|
|
2213
|
-
try {
|
|
2214
|
-
await p.close();
|
|
2215
|
-
} catch (rollbackError) {
|
|
2216
|
-
logger7.error("Provider rollback close failed", { name, error: rollbackError });
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
2403
|
return connectResult;
|
|
2220
2404
|
}
|
|
2221
|
-
newProviders.set(providerConfig.name, provider);
|
|
2222
2405
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
const saveResult = await templateRegistry.saveBatch(parsed.templates);
|
|
2406
|
+
const candidateTemplateRepo = await tryInitTemplateRepo();
|
|
2407
|
+
if (parsed.templates?.length && !candidateTemplateRepo)
|
|
2408
|
+
return err(HaiReachError.CONFIG_ERROR, reachM("reach_templatePersistenceRequired"));
|
|
2409
|
+
const candidateRegistry = createTemplateRegistry(candidateTemplateRepo);
|
|
2410
|
+
if (parsed.templates?.length) {
|
|
2411
|
+
const saveResult = await candidateRegistry.saveBatch(parsed.templates);
|
|
2230
2412
|
if (!saveResult.success) {
|
|
2231
|
-
logger7.warn("Failed to save config templates to database", {
|
|
2413
|
+
logger7.warn("Failed to save config templates to database", { errorCode: saveResult.error.code });
|
|
2414
|
+
return err(HaiReachError.CONFIG_ERROR, reachM("reach_templatePersistenceRequired"), saveResult.error);
|
|
2232
2415
|
}
|
|
2233
2416
|
}
|
|
2234
|
-
|
|
2417
|
+
const candidateSendLogRepo = requiredQueue ?? await tryInitSendLogRepo();
|
|
2418
|
+
providers = newProviders;
|
|
2419
|
+
currentConfig = parsed;
|
|
2420
|
+
dndConfig = parsed.dnd;
|
|
2421
|
+
templateRegistry = candidateRegistry;
|
|
2422
|
+
sendLogRepo = candidateSendLogRepo;
|
|
2423
|
+
published = true;
|
|
2235
2424
|
if (dndConfig) {
|
|
2236
2425
|
startDndScheduler(dndConfig, providers, sendLogRepo);
|
|
2237
2426
|
}
|
|
@@ -2243,10 +2432,22 @@ async function doInit(config, options) {
|
|
|
2243
2432
|
return err(
|
|
2244
2433
|
HaiReachError.CONFIG_ERROR,
|
|
2245
2434
|
reachM("reach_initFailed", {
|
|
2246
|
-
params: {
|
|
2435
|
+
params: {
|
|
2436
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2437
|
+
}
|
|
2247
2438
|
}),
|
|
2248
2439
|
error
|
|
2249
2440
|
);
|
|
2441
|
+
} finally {
|
|
2442
|
+
if (!published) {
|
|
2443
|
+
for (const [name, provider] of newProviders) {
|
|
2444
|
+
try {
|
|
2445
|
+
await provider.close();
|
|
2446
|
+
} catch {
|
|
2447
|
+
logger7.error("Candidate provider cleanup failed", { name });
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2250
2451
|
}
|
|
2251
2452
|
}
|
|
2252
2453
|
function queueEntry(row) {
|
|
@@ -2271,7 +2472,11 @@ var reach = {
|
|
|
2271
2472
|
return err(HaiReachError.CONFIG_ERROR, reachM("reach_dndPersistenceRequired"));
|
|
2272
2473
|
if (!ReachQueueIdSchema.safeParse(queueId).success)
|
|
2273
2474
|
return err(HaiReachError.CONFIG_ERROR, reachM("reach_queueInputInvalid"));
|
|
2274
|
-
const rows = await sendLogRepo.findAll({
|
|
2475
|
+
const rows = await sendLogRepo.findAll({
|
|
2476
|
+
where: "queue_id = ?",
|
|
2477
|
+
params: [queueId],
|
|
2478
|
+
limit: 1
|
|
2479
|
+
});
|
|
2275
2480
|
if (!rows.success)
|
|
2276
2481
|
return rows;
|
|
2277
2482
|
const row = rows.data[0];
|
|
@@ -2320,7 +2525,9 @@ var reach = {
|
|
|
2320
2525
|
logger7.warn("Reach module init already in progress, skipping");
|
|
2321
2526
|
return err(
|
|
2322
2527
|
HaiReachError.CONFIG_ERROR,
|
|
2323
|
-
reachM("reach_configError", {
|
|
2528
|
+
reachM("reach_configError", {
|
|
2529
|
+
params: { error: "init already in progress" }
|
|
2530
|
+
})
|
|
2324
2531
|
);
|
|
2325
2532
|
}
|
|
2326
2533
|
initInProgress = true;
|
|
@@ -2346,10 +2553,19 @@ var reach = {
|
|
|
2346
2553
|
if (!currentConfig)
|
|
2347
2554
|
return null;
|
|
2348
2555
|
const safeProviders = currentConfig.providers.map((provider) => {
|
|
2556
|
+
if (provider.type === "api" && provider.headers) {
|
|
2557
|
+
return {
|
|
2558
|
+
...provider,
|
|
2559
|
+
headers: Object.fromEntries(Object.keys(provider.headers).map((name) => [name, core.sanitize.defaultSensitiveReplacement]))
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2349
2562
|
if (provider.type !== "aliyun-sms" || !provider.gateway)
|
|
2350
2563
|
return provider;
|
|
2351
2564
|
const { url, allowInsecureHttp } = provider.gateway;
|
|
2352
|
-
return {
|
|
2565
|
+
return {
|
|
2566
|
+
...provider,
|
|
2567
|
+
gateway: { url, allowInsecureHttp }
|
|
2568
|
+
};
|
|
2353
2569
|
});
|
|
2354
2570
|
return core.sanitize.sanitizeSensitiveFields({ ...currentConfig, providers: safeProviders });
|
|
2355
2571
|
},
|