@h-ai/reach 1.0.0-beta.22 → 1.0.0-beta.23

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 CHANGED
@@ -175,7 +175,7 @@ Production rejects console providers and requires SMTP, SMS or an actual API ser
175
175
 
176
176
  Use `reach.send({ ..., sensitive: true, expiresAt, idempotencyKey })` for passwords, OTPs and reset tokens. expiresAt is Unix milliseconds; stable operation keys must contain no credentials. A shared ledger stores only metadata receipts/status and redacted destinations, never bodies, variables or credential hashes. There is no delayed credential queue. Success returns receiptId; DND/expired messages are not sent, and uncertain delivery is never automatically repeated. Explicit development console output is isolated from persistent observers. Production Console restrictions remain. Normal messages may use deferPolicy: reject to avoid a second queue.
177
177
 
178
- `migrateReach({ dryRun: true })` previews persistence preparation performed during repository initialization. Stop on failure, back up and stop conflicting writers first, preserve the sending ledger and never treat schema preparation as permission to replay credentials.
178
+ `reach.init()` creates and validates the send log and its queue identifier constraint. Incompatible tables cause initialization to fail. Preserve send receipts and reconcile uncertain outcomes before attempting further credential delivery.
179
179
 
180
180
  ### API acknowledgements and destination boundaries
181
181
 
package/README.ja.md CHANGED
@@ -175,7 +175,7 @@ if (state.data?.status === 'dead' && state.data.updatedAt !== undefined) {
175
175
 
176
176
  パスワード、OTP、リセットトークンは `reach.send({ ..., sensitive: true, expiresAt, idempotencyKey })` を使います。expiresAt は Unix ミリ秒、操作キーに秘密値を含めません。共有台帳は本文・変数・認証値のハッシュを保存せず、脱機密化した宛先と受領状態のみを保持します。遅延キューに入れず、DND/期限切れは送信せず、不明な配信を自動再送しません。成功時は receiptId を返します。明示した開発用コンソール出力は永続観測から分離し、本番 Console 制限は維持します。通常通知は deferPolicy: reject で二重キューを防げます。
177
177
 
178
- `migrateReach({ dryRun: true })` で永続構造を確認し、リポジトリ初期化で準備します。失敗時は停止し、事前バックアップと競合書込の停止を行い、送信台帳を保持します。構造準備を資格情報の再送許可と解釈しないでください。
178
+ `reach.init()` は送信ログとキュー識別子の制約を作成・検証し、構造が一致しない場合は初期化に失敗します。送信台帳を保存し、結果が不明な資格情報通知は永続レシートで状態を確認してください。
179
179
 
180
180
  ### API 応答と送信先の境界
181
181
 
package/README.md CHANGED
@@ -175,7 +175,7 @@ if (state.data?.status === 'dead' && state.data.updatedAt !== undefined) {
175
175
 
176
176
  发送密码、OTP 或重置令牌时使用 `reach.send({ ..., sensitive: true, expiresAt, idempotencyKey })`。expiresAt 是 Unix 毫秒,稳定操作键不可包含凭据。必须有共享发送账本:只持久化无载荷回执/状态,接收方脱敏,不存正文、变量或低熵凭据摘要,也不进入延迟队列。发送成功返回 receiptId;DND/过期不投递,异常或不明状态不自动重投。明确启用的开发控制台仍可显示业务允许的通知,持久采集观察器被隔离;生产 Console Provider 限制不变。普通通知可用 deferPolicy: reject 阻止二次排队。
177
177
 
178
- `migrateReach({ dryRun: true })` 预检持久结构,仓储初始化执行支持的准备步骤;失败时停止初始化。执行前备份并停止冲突写入,保留发送账本,不把结构准备作为重新发送凭据的理由。
178
+ `reach.init()` 创建并校验发送日志表及队列标识约束,结构不符时返回初始化失败。保留发送账本,按持久回执核对状态,避免重发结果不明的凭据通知。
179
179
 
180
180
  ### API 回执与目标边界
181
181
 
package/dist/index.d.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import * as _h_ai_core from '@h-ai/core';
3
3
  import { HaiResult } from '@h-ai/core';
4
- import * as _h_ai_reldb from '@h-ai/reldb';
5
- import { ReldbFunctions } from '@h-ai/reldb';
6
4
 
7
5
  /**
8
6
  * @h-ai/reach — 配置 Schema
@@ -1377,17 +1375,5 @@ interface ReachFunctions extends SendOperations {
1377
1375
  */
1378
1376
  declare const reach: ReachFunctions;
1379
1377
 
1380
- /**
1381
- * 预检或升级 Reach 队列。旧凭据相关载荷清除,待发凭据丢弃,保留发送状态和历史标识。
1382
- * @example
1383
- * ```ts
1384
- * import { migrateReach } from '@h-ai/reach'
1385
- * const plan = await migrateReach({ dryRun: true })
1386
- * ```
1387
- */
1388
- declare function migrateReach(options?: {
1389
- dryRun?: boolean;
1390
- }, db?: ReldbFunctions): Promise<_h_ai_core.HaiResult<_h_ai_reldb.SchemaMigrationPlan>>;
1391
-
1392
- export { AliyunSmsProviderConfigSchema, ApiProviderConfigSchema, ConsoleProviderConfigSchema, DndConfigSchema, HaiReachError, ProviderConfigSchema, ReachConfigSchema, ReachDeadRequeueSchema, ReachQueueConfigSchema, ReachQueueIdSchema, ReachQueueListSchema, ReachUnknownResolutionSchema, SmtpProviderConfigSchema, TemplateConfigSchema, migrateReach, reach };
1378
+ export { AliyunSmsProviderConfigSchema, ApiProviderConfigSchema, ConsoleProviderConfigSchema, DndConfigSchema, HaiReachError, ProviderConfigSchema, ReachConfigSchema, ReachDeadRequeueSchema, ReachQueueConfigSchema, ReachQueueIdSchema, ReachQueueListSchema, ReachUnknownResolutionSchema, SmtpProviderConfigSchema, TemplateConfigSchema, reach };
1393
1379
  export type { AliyunSmsProviderConfig, ApiProviderConfig, ConsoleProviderConfig, DndConfig, ProviderConfig, ReachChannel, ReachConfig, ReachConfigInput, ReachDeadRequeue, ReachFunctions, ReachGatewayHook, ReachInitOptions, ReachMessage, ReachQueueConfig, ReachQueueEntry, ReachQueueFailure, ReachQueueListOptions, ReachQueueOperations, ReachQueueStatus, ReachTemplate, ReachTemplateRegistry, ReachUnknownResolution, RenderedTemplate, SendOperations, SendResult, SmtpProviderConfig, TemplateConfig };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
  import { core, err, ok } from '@h-ai/core';
3
3
  import process from 'process';
4
4
  import { reldb, BaseReldbCrudRepository } from '@h-ai/reldb';
5
- import { randomUUID, createHash, createHmac } from 'crypto';
5
+ import { createHash, randomUUID, createHmac } from 'crypto';
6
6
  import { addAbortListener } from 'events';
7
7
  import { createRequire } from 'module';
8
8
  import { cache } from '@h-ai/cache';
@@ -11,6 +11,7 @@ import { cache } from '@h-ai/cache';
11
11
 
12
12
  // messages/en-US.json
13
13
  var en_US_default = {
14
+ reach_sendLogIndexRequired: "The send log table requires a unique index on receipt IDs",
14
15
  $schema: "https://inlang.com/schema/inlang-message-format",
15
16
  reach_notInitialized: "Reach module not initialized, call reach.init() first",
16
17
  reach_initFailed: "Reach module initialization failed: {error}",
@@ -70,6 +71,7 @@ var en_US_default = {
70
71
 
71
72
  // messages/ja-JP.json
72
73
  var ja_JP_default = {
74
+ reach_sendLogIndexRequired: "\u9001\u4FE1\u30ED\u30B0\u30C6\u30FC\u30D6\u30EB\u306B\u306F\u53D7\u4ED8 ID \u306E\u4E00\u610F\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u304C\u5FC5\u8981\u3067\u3059",
73
75
  $schema: "https://inlang.com/schema/inlang-message-format",
74
76
  reach_notInitialized: "\u901A\u77E5\u30E2\u30B8\u30E5\u30FC\u30EB\u304C\u521D\u671F\u5316\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u5148\u306Breach.init()\u3092\u547C\u3073\u51FA\u3057\u3066\u304F\u3060\u3055\u3044",
75
77
  reach_initFailed: "\u901A\u77E5\u30E2\u30B8\u30E5\u30FC\u30EB\u306E\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F: {error}",
@@ -129,6 +131,7 @@ var ja_JP_default = {
129
131
 
130
132
  // messages/zh-CN.json
131
133
  var zh_CN_default = {
134
+ reach_sendLogIndexRequired: "\u53D1\u9001\u65E5\u5FD7\u8868\u5FC5\u987B\u4E3A\u56DE\u6267\u7F16\u53F7\u5EFA\u7ACB\u552F\u4E00\u7D22\u5F15",
132
135
  $schema: "https://inlang.com/schema/inlang-message-format",
133
136
  reach_notInitialized: "\u89E6\u8FBE\u6A21\u5757\u5C1A\u672A\u521D\u59CB\u5316\uFF0C\u8BF7\u5148\u8C03\u7528 reach.init()",
134
137
  reach_initFailed: "\u89E6\u8FBE\u6A21\u5757\u521D\u59CB\u5316\u5931\u8D25: {error}",
@@ -1352,133 +1355,6 @@ async function executeSend(message, providers2, templateRegistry2, dndConfig2, r
1352
1355
  }
1353
1356
  return result;
1354
1357
  }
1355
- function migrateReach(options = {}, db = reldb) {
1356
- const definitions = {
1357
- queue_id: db.config?.type === "mysql" ? "VARCHAR(255)" : "TEXT",
1358
- retry_safe: "BOOLEAN NOT NULL DEFAULT FALSE",
1359
- attempt_count: "BIGINT NOT NULL DEFAULT 0",
1360
- max_attempts: "BIGINT NOT NULL DEFAULT 5",
1361
- next_attempt_at: "BIGINT NOT NULL DEFAULT 0",
1362
- retry_delay_ms: "BIGINT NOT NULL DEFAULT 60000",
1363
- max_retry_delay_ms: "BIGINT NOT NULL DEFAULT 3600000",
1364
- last_failure: "TEXT"
1365
- };
1366
- const steps = [
1367
- {
1368
- version: 1,
1369
- name: "durable-delivery-ledger",
1370
- async needed(sql) {
1371
- const found = await db.schemaColumns("hai_reach_send_log", sql);
1372
- return found.success ? ok(found.data.length > 0 && Object.keys(definitions).some((name) => !found.data.includes(name))) : found;
1373
- },
1374
- async apply(sql) {
1375
- const found = await db.schemaColumns("hai_reach_send_log", sql);
1376
- if (!found.success)
1377
- return found;
1378
- for (const [name, definition] of Object.entries(definitions)) {
1379
- if (found.data.includes(name))
1380
- continue;
1381
- const added = await sql.execute(`ALTER TABLE hai_reach_send_log ADD COLUMN ${name} ${definition}`);
1382
- if (!added.success)
1383
- return added;
1384
- }
1385
- return ok(void 0);
1386
- }
1387
- },
1388
- {
1389
- version: 2,
1390
- name: "legacy-delivery-isolation",
1391
- async needed(sql) {
1392
- const found = await db.schemaColumns("hai_reach_send_log", sql);
1393
- if (!found.success || !found.data.length) {
1394
- return found.success ? ok(false) : found;
1395
- }
1396
- if (!found.data.includes("queue_id"))
1397
- return ok(true);
1398
- 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");
1399
- return row.success ? ok(Boolean(row.data)) : row;
1400
- },
1401
- async apply(sql) {
1402
- 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");
1403
- if (!isolated.success)
1404
- return isolated;
1405
- while (true) {
1406
- 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");
1407
- if (!rows.success)
1408
- return rows;
1409
- for (const row of rows.data) {
1410
- const saved = await sql.execute("UPDATE hai_reach_send_log SET queue_id=? WHERE id=? AND queue_id IS NULL", [
1411
- randomUUID(),
1412
- row.id
1413
- ]);
1414
- if (!saved.success)
1415
- return saved;
1416
- }
1417
- if (rows.data.length < 100)
1418
- return ok(void 0);
1419
- }
1420
- }
1421
- },
1422
- {
1423
- version: 3,
1424
- name: "credential-payload-retirement",
1425
- async needed(sql) {
1426
- const found = await db.schemaColumns("hai_reach_send_log", sql);
1427
- if (!found.success || !found.data.length) {
1428
- return found.success ? ok(false) : found;
1429
- }
1430
- const ledger = await db.schemaColumns("hai_schema_migrations", sql);
1431
- if (!ledger.success)
1432
- return ledger;
1433
- const version = ledger.data.length ? await sql.get("SELECT version FROM hai_schema_migrations WHERE module=?", ["reach"]) : ok(null);
1434
- if (!version.success)
1435
- return version;
1436
- if (Number(version.data?.version ?? 0) >= 3)
1437
- return ok(false);
1438
- 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");
1439
- return row.success ? ok(Boolean(row.data)) : row;
1440
- },
1441
- async apply(sql) {
1442
- 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");
1443
- }
1444
- },
1445
- {
1446
- version: 4,
1447
- name: "unique-operation-receipts",
1448
- async needed(sql) {
1449
- const columns = await db.schemaColumns("hai_reach_send_log", sql);
1450
- if (!columns.success || !columns.data.length) {
1451
- return columns.success ? ok(false) : columns;
1452
- }
1453
- if (!columns.data.includes("queue_id"))
1454
- return ok(true);
1455
- if (db.config?.type === "sqlite") {
1456
- const indexes = await sql.query("PRAGMA index_list(hai_reach_send_log)");
1457
- if (!indexes.success)
1458
- return indexes;
1459
- for (const index of indexes.data) {
1460
- if (!index.unique || !/^\w+$/.test(index.name))
1461
- continue;
1462
- const fields = await sql.query(`PRAGMA index_info(${index.name})`);
1463
- if (!fields.success)
1464
- return fields;
1465
- if (fields.data.length === 1 && fields.data[0]?.name === "queue_id")
1466
- return ok(false);
1467
- }
1468
- return ok(true);
1469
- }
1470
- 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"]);
1471
- return found.success ? ok(!found.data) : found;
1472
- },
1473
- async apply(sql) {
1474
- return sql.execute("CREATE UNIQUE INDEX uq_hai_reach_queue_v1 ON hai_reach_send_log(queue_id)");
1475
- }
1476
- }
1477
- ];
1478
- return db.migrateSchema("reach", steps, options);
1479
- }
1480
-
1481
- // src/repositories/reach-repository-send-log.ts
1482
1358
  var TABLE_NAME = "hai_reach_send_log";
1483
1359
  var DEFAULT_CLAIM_BATCH_SIZE = 100;
1484
1360
  var DEFAULT_CLAIM_LOCK_MS = 10 * 60 * 1e3;
@@ -1702,11 +1578,8 @@ function resetSendLogRepoSingleton() {
1702
1578
  async function createSendLogRepository(db) {
1703
1579
  if (sendLogRepoInstance && sendLogRepoSqlOps === db.sql)
1704
1580
  return ok(sendLogRepoInstance);
1705
- const migration = await migrateReach({}, db);
1706
- if (!migration.success)
1707
- return migration;
1708
1581
  const repo = new DbSendLogRepository(db);
1709
- const initResult = await repo.count();
1582
+ const initResult = await repo.ready();
1710
1583
  if (!initResult.success) {
1711
1584
  return err(
1712
1585
  HaiReachError.SEND_FAILED,
@@ -1721,6 +1594,17 @@ async function createSendLogRepository(db) {
1721
1594
  return ok(repo);
1722
1595
  }
1723
1596
  var DbSendLogRepository = class extends BaseReldbCrudRepository {
1597
+ /** 唯一回执约束保证多个节点不能为同一操作排入不同的发送记录。 */
1598
+ async ready() {
1599
+ const schema = await this.ensureSchema();
1600
+ if (!schema.success)
1601
+ return schema;
1602
+ const type = this.db.config?.type;
1603
+ const index = type === "sqlite" ? await this.db.sql.get('SELECT i.name FROM pragma_index_list(?) i JOIN pragma_index_info(i.name) f WHERE i."unique"=1 AND i.partial=0 GROUP BY i.name HAVING COUNT(*)=1 AND MAX(f.name)=?', [TABLE_NAME, "queue_id"]) : type === "mysql" ? await this.db.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)=?", [TABLE_NAME, "queue_id"]) : await this.db.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=i.indkey[0] WHERE t.relname=? AND n.nspname=current_schema() AND i.indisunique AND i.indisvalid AND i.indpred IS NULL AND i.indnkeyatts=1 AND a.attname=?", [TABLE_NAME, "queue_id"]);
1604
+ if (!index.success)
1605
+ return index;
1606
+ return index.data ? ok(void 0) : err(HaiReachError.SEND_FAILED, reachM("reach_sendLogIndexRequired"));
1607
+ }
1724
1608
  /** 共享后端取真正执行时钟;本地 SQLite 保留可控的单进程测试时钟。 */
1725
1609
  clockSql(localNow) {
1726
1610
  if (this.db.config?.type === "postgresql")
@@ -2597,6 +2481,6 @@ var reach = {
2597
2481
  }
2598
2482
  };
2599
2483
 
2600
- export { AliyunSmsProviderConfigSchema, ApiProviderConfigSchema, ConsoleProviderConfigSchema, DndConfigSchema, HaiReachError, ProviderConfigSchema, ReachConfigSchema, ReachDeadRequeueSchema, ReachQueueConfigSchema, ReachQueueIdSchema, ReachQueueListSchema, ReachUnknownResolutionSchema, SmtpProviderConfigSchema, TemplateConfigSchema, migrateReach, reach };
2484
+ export { AliyunSmsProviderConfigSchema, ApiProviderConfigSchema, ConsoleProviderConfigSchema, DndConfigSchema, HaiReachError, ProviderConfigSchema, ReachConfigSchema, ReachDeadRequeueSchema, ReachQueueConfigSchema, ReachQueueIdSchema, ReachQueueListSchema, ReachUnknownResolutionSchema, SmtpProviderConfigSchema, TemplateConfigSchema, reach };
2601
2485
  //# sourceMappingURL=index.js.map
2602
2486
  //# sourceMappingURL=index.js.map