@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/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, migrateSchema, schemaColumns, BaseReldbCrudRepository } from '@h-ai/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: {error}",
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: {error}",
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: {error}",
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: {error}",
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: {error}",
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: {error}",
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 [...new Headers(headers).keys()].every((name) => !transportHeader.test(name));
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([z.string(), z.array(jsonValue), z.record(z.string(), jsonValue)], reachM("reach_gatewayBodyInvalid")).optional()
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
- }, { path: ["url"], message: reachM("reach_gatewayUrlInvalid") });
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(error) {
371
+ function toReachError() {
360
372
  return {
361
- code: HaiReachError.SEND_FAILED.code,
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({ method: "POST", url, body: typeof body === "string" ? body : JSON.stringify(body), signal })),
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
- return { method: "POST", headers, body: typeof body === "string" ? body : JSON.stringify(body), redirect: "error", signal };
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", { params: { type: config.type } })
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", { params: { error: body.Message } })
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(error));
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
- code: HaiReachError.SEND_FAILED.code,
551
+ ...HaiReachError.SEND_FAILED,
529
552
  message: reachM("reach_apiSendFailed", {
530
- params: { error: reachM("reach_apiRequestFailed") }
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({ success: true, ...messageId === void 0 ? {} : { messageId } });
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", { params: { type: config.type } })
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", { endpoint: getSafeEndpoint(config.url), method: config.method });
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", { endpoint: getSafeEndpoint(apiConfig.url) });
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", { params: { error: `HTTP ${response.status}` } })
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(error) {
728
+ function toReachError3() {
692
729
  return {
693
- code: HaiReachError.SEND_FAILED.code,
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", { params: { type: config.type } })
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(error));
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(error));
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 { success: false, error: { ...HaiReachError.SEND_FAILED, message: reachM("reach_sensitiveDeliveryUnconfirmed"), ext: { receiptId } } };
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({ where: "queue_id = ?", params: [queueId], limit: 1 });
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 rendered = await templateRegistry2.render(message.template, message.vars ?? {});
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 || (template.success ? template.data.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", { params: { recipient: "" } })
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", { params: { provider: providerName } })
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 = { queue_id: db.config?.type === "mysql" ? "VARCHAR(255)" : "TEXT", retry_safe: "BOOLEAN NOT NULL DEFAULT FALSE", attempt_count: "BIGINT NOT NULL DEFAULT 0", max_attempts: "BIGINT NOT NULL DEFAULT 5", next_attempt_at: "BIGINT NOT NULL DEFAULT 0", retry_delay_ms: "BIGINT NOT NULL DEFAULT 60000", max_retry_delay_ms: "BIGINT NOT NULL DEFAULT 3600000", last_failure: "TEXT" };
1286
- const steps = [{
1287
- version: 1,
1288
- name: "durable-delivery-ledger",
1289
- async needed(sql) {
1290
- const found = await schemaColumns(db, "hai_reach_send_log", sql);
1291
- return found.success ? ok(found.data.length > 0 && Object.keys(definitions).some((name) => !found.data.includes(name))) : found;
1292
- },
1293
- async apply(sql) {
1294
- const found = await schemaColumns(db, "hai_reach_send_log", sql);
1295
- if (!found.success)
1296
- return found;
1297
- for (const [name, definition] of Object.entries(definitions)) {
1298
- if (found.data.includes(name))
1299
- continue;
1300
- const added = await sql.execute(`ALTER TABLE hai_reach_send_log ADD COLUMN ${name} ${definition}`);
1301
- if (!added.success)
1302
- return added;
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
- async apply(sql) {
1319
- 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");
1320
- if (!isolated.success)
1321
- return isolated;
1322
- while (true) {
1323
- 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");
1324
- if (!rows.success)
1325
- return rows;
1326
- for (const row of rows.data) {
1327
- const saved = await sql.execute("UPDATE hai_reach_send_log SET queue_id=? WHERE id=? AND queue_id IS NULL", [randomUUID(), row.id]);
1328
- if (!saved.success)
1329
- return saved;
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
- async apply(sql) {
1354
- 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");
1355
- }
1356
- }, {
1357
- version: 4,
1358
- name: "unique-operation-receipts",
1359
- async needed(sql) {
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
- return ok(true);
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
- async apply(sql) {
1384
- return sql.execute("CREATE UNIQUE INDEX uq_hai_reach_queue_v1 ON hai_reach_send_log(queue_id)");
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(db, "reach", steps, options);
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 ["pending", eligibleBefore, "processing", eligibleBefore, "sending", eligibleBefore];
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", { params: { error: detail } }),
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 ["id", "createdAt", "updatedAt", "lockedUntil", "nextAttemptAt", "attemptCount", "maxAttempts", "retryDelayMs", "maxRetryDelayMs"]) {
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", { params: { field } }));
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", { params: { error: initResult.error.message } }),
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
- [status, messageId ?? null, Date.now(), queueId]
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({ where: "status = ?", params: ["pending"], orderBy: "created_at ASC" }, tx);
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
- [...claimableParams(eligibleBefore), limit]
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
- ["processing", owner, lockMs, ...claimedIds]
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
- ["processing", owner, lockMs, row.id, ...claimableParams(eligibleBefore)]
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
- ["dead", "attempts_exhausted", "pending", "processing", "sending"]
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
- [dead ? "dead" : "pending", reason, ...dead ? [] : [delay], row.id, owner, row.attemptCount, "processing", "sending"]
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
- ["pending", queueId, "dead", expectedUpdatedAt]
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
- ["sending", flag, lockMs, id, "processing", owner]
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
- [lockMs, id, owner, "processing", "sending"]
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
- ["unknown", id, owner, "processing", "sending"]
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
- [resolution.status, resolution.messageId ?? null, queueId, "unknown"]
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
- ["sent", messageId ?? null, id, "sending", owner]
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", { params: { error: initResult.error.message } }),
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({ where: "name = ?", params: [name], limit: 1 }, tx);
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", { error: error instanceof Error ? error.message : String(error) });
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", { error: error instanceof Error ? error.message : String(error) });
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", { params: { error: parseResult.error.message } }),
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
- providers = newProviders;
2224
- currentConfig = parsed;
2225
- dndConfig = parsed.dnd;
2226
- templateRepo = await tryInitTemplateRepo();
2227
- templateRegistry = createTemplateRegistry(templateRepo);
2228
- if (parsed.templates && templateRepo) {
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", { error: saveResult.error.message });
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
- sendLogRepo = requiredQueue ?? await tryInitSendLogRepo();
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: { error: error instanceof Error ? error.message : String(error) }
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({ where: "queue_id = ?", params: [queueId], limit: 1 });
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", { params: { error: "init already in progress" } })
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 { ...provider, gateway: { url, allowInsecureHttp } };
2565
+ return {
2566
+ ...provider,
2567
+ gateway: { url, allowInsecureHttp }
2568
+ };
2353
2569
  });
2354
2570
  return core.sanitize.sanitizeSensitiveFields({ ...currentConfig, providers: safeProviders });
2355
2571
  },