@h-ai/reach 1.0.0-beta.22 → 1.0.0-beta.24
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 +1 -1
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/index.d.ts +1 -15
- package/dist/index.js +17 -133
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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 {
|
|
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.
|
|
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,
|
|
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
|