@h-ai/reach 0.1.0-alpha.72 → 0.1.0-alpha.74
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 +6 -0
- package/README.ja.md +6 -0
- package/README.md +6 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +77 -15
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/README.en.md
CHANGED
|
@@ -217,6 +217,12 @@ Use `reach.send({ ..., sensitive: true, expiresAt, idempotencyKey })` for passwo
|
|
|
217
217
|
|
|
218
218
|
`migrateReach({ dryRun: true })` previews supported migrations, applied during repository initialization. Statuses/identifiers remain; legacy in-flight work becomes unknown. Legacy payloads are retired once and pending old payloads discarded. Back up and stop old writers before upgrading; never replay old credentials.
|
|
219
219
|
|
|
220
|
+
## API acknowledgements and destination boundaries
|
|
221
|
+
|
|
222
|
+
An empty successful 2xx response, including 204/205, means receiver acceptance; messageId is optional and acceptance does not prove final delivery. Nonempty responses must be JSON objects with an optional messageId string of at most 255 characters. Responses are bounded to 64 KiB. Invalid responses, timeouts and network failures never become false success or expose receiver content.
|
|
223
|
+
|
|
224
|
+
API `redirect` defaults to `error`; ordinary messages may explicitly use `follow`. Sensitive messages always reject redirects without forwarding payloads or private headers. `requireHttps:true` enforces initial HTTPS and cannot be combined with follow. When omitted, production sensitive messages require HTTPS; explicit false is only for an operator-approved trusted internal connection. Unknown source-side effects still require reconciliation and sensitive messages are never retried automatically. Application allowlists and network egress controls remain necessary.
|
|
225
|
+
|
|
220
226
|
## Development and testing
|
|
221
227
|
|
|
222
228
|
```bash
|
package/README.ja.md
CHANGED
|
@@ -217,6 +217,12 @@ if (state.data?.status === 'dead' && state.data.updatedAt !== undefined) {
|
|
|
217
217
|
|
|
218
218
|
`migrateReach({ dryRun: true })` で事前確認し、リポジトリー初期化で対応する移行を実行します。状態と識別子を保持し、旧処理中は unknown、旧ペイロードは一度廃棄し、旧待機ペイロードは discarded にします。更新前は旧書込を停止しバックアップし、古い認証情報を再送しないでください。
|
|
219
219
|
|
|
220
|
+
## API 応答と送信先の境界
|
|
221
|
+
|
|
222
|
+
204/205 を含む空の成功 2xx は受付成功を表し、messageId は任意で、最終配信の証明ではありません。本文がある場合は JSON オブジェクトとし、messageId は255文字以下の任意文字列です。応答上限は64 KiBです。不正応答・タイムアウト・通信失敗を成功扱いにせず、応答本文をログへ出しません。
|
|
223
|
+
|
|
224
|
+
API の `redirect` は既定で `error`、通常通知のみ明示的に `follow` を選択できます。sensitive は常に転送を拒否し、本文や独自ヘッダーを転送しません。`requireHttps:true` は開始URLの HTTPS を必須とし follow と併用できません。省略時は本番の機密通知に HTTPS を要求し、明示 false は運用者が確認した信頼できる内部接続専用です。元の送信先での副作用が不明なら確認が必要で、機密通知は自動再送しません。アプリの許可リストとネットワーク出口制御は引き続き必要です。
|
|
225
|
+
|
|
220
226
|
## 開発とテスト
|
|
221
227
|
|
|
222
228
|
```bash
|
package/README.md
CHANGED
|
@@ -217,6 +217,12 @@ if (state.data?.status === 'dead' && state.data.updatedAt !== undefined) {
|
|
|
217
217
|
|
|
218
218
|
`migrateReach({ dryRun: true })` 预检;仓库初始化自动执行受支持步骤。升级保留发送状态与标识,旧在途转 unknown;旧正文/主题/模板变量/extra 一次性退役,待发旧载荷 discarded。生产需先备份停旧写入,不能恢复旧凭据重新投递。
|
|
219
219
|
|
|
220
|
+
## API 回执与目标边界
|
|
221
|
+
|
|
222
|
+
空正文的成功 2xx(含 204/205)表示接收端受理,messageId 可缺省,不代表最终送达。非空正文须为 JSON 对象,messageId 若存在须为不超过 255 字符的字符串;响应最多 64 KiB,非法响应、超时和网络失败不会假装成功或输出第三方正文。
|
|
223
|
+
|
|
224
|
+
API Provider 的 `redirect` 默认为 `error`;只有普通消息可显式设为 `follow`。`sensitive:true` 始终拒绝跳转,不转发正文和私有头。`requireHttps:true` 强制初始 HTTPS 且与 follow 互斥;省略时生产敏感消息要求 HTTPS,显式 false 仅适用于操作者确认的受信内网。超时或源端已产生的未知副作用仍须核对,敏感消息不自动重投。域名授权、内网地址限制和出口策略由应用及部署环境负责。
|
|
225
|
+
|
|
220
226
|
## 开发测试
|
|
221
227
|
|
|
222
228
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -80,6 +80,11 @@ declare const ApiProviderConfigSchema: z.ZodObject<{
|
|
|
80
80
|
}>>;
|
|
81
81
|
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
82
82
|
timeout: z.ZodDefault<z.ZodNumber>;
|
|
83
|
+
redirect: z.ZodDefault<z.ZodEnum<{
|
|
84
|
+
error: "error";
|
|
85
|
+
follow: "follow";
|
|
86
|
+
}>>;
|
|
87
|
+
requireHttps: z.ZodOptional<z.ZodBoolean>;
|
|
83
88
|
idempotent: z.ZodDefault<z.ZodBoolean>;
|
|
84
89
|
}, z.core.$strip>;
|
|
85
90
|
/**
|
|
@@ -120,6 +125,11 @@ declare const ProviderConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
120
125
|
}>>;
|
|
121
126
|
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
122
127
|
timeout: z.ZodDefault<z.ZodNumber>;
|
|
128
|
+
redirect: z.ZodDefault<z.ZodEnum<{
|
|
129
|
+
error: "error";
|
|
130
|
+
follow: "follow";
|
|
131
|
+
}>>;
|
|
132
|
+
requireHttps: z.ZodOptional<z.ZodBoolean>;
|
|
123
133
|
idempotent: z.ZodDefault<z.ZodBoolean>;
|
|
124
134
|
}, z.core.$strip>], "type">;
|
|
125
135
|
/**
|
|
@@ -344,6 +354,11 @@ declare const ReachConfigSchema: z.ZodObject<{
|
|
|
344
354
|
}>>;
|
|
345
355
|
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
346
356
|
timeout: z.ZodDefault<z.ZodNumber>;
|
|
357
|
+
redirect: z.ZodDefault<z.ZodEnum<{
|
|
358
|
+
error: "error";
|
|
359
|
+
follow: "follow";
|
|
360
|
+
}>>;
|
|
361
|
+
requireHttps: z.ZodOptional<z.ZodBoolean>;
|
|
347
362
|
idempotent: z.ZodDefault<z.ZodBoolean>;
|
|
348
363
|
}, z.core.$strip>], "type">>;
|
|
349
364
|
templates: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
package/dist/index.js
CHANGED
|
@@ -53,7 +53,11 @@ var en_US_default = {
|
|
|
53
53
|
reach_productionProviderRequired: "Production requires a delivery provider; console is available only in development or explicit test mode",
|
|
54
54
|
reach_sensitivePolicyRequired: "Credential notifications require a database receipt, stable idempotency key and expiry",
|
|
55
55
|
reach_sensitiveDeliveryUnconfirmed: "Credential delivery is unconfirmed and will not be retried automatically. Check the receipt or request new credentials.",
|
|
56
|
-
reach_messageExpired: "The credentials expired before delivery"
|
|
56
|
+
reach_messageExpired: "The credentials expired before delivery",
|
|
57
|
+
reach_apiResponseTooLarge: "API acknowledgement exceeds the 64 KiB limit",
|
|
58
|
+
reach_apiResponseInvalid: "API acknowledgement must be empty or a JSON object with an optional messageId string",
|
|
59
|
+
reach_apiRequestFailed: "API request failed or timed out; delivery is unconfirmed",
|
|
60
|
+
reach_apiHttpsRequired: "This API policy requires HTTPS and disallows automatic redirects"
|
|
57
61
|
};
|
|
58
62
|
|
|
59
63
|
// messages/ja-JP.json
|
|
@@ -101,7 +105,11 @@ var ja_JP_default = {
|
|
|
101
105
|
reach_productionProviderRequired: "\u672C\u756A\u74B0\u5883\u306B\u306F\u914D\u4FE1\u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u304C\u5FC5\u8981\u3067\u3059\u3002console \u306F\u958B\u767A\u74B0\u5883\u307E\u305F\u306F\u660E\u793A\u7684\u306A\u30C6\u30B9\u30C8\u30E2\u30FC\u30C9\u3067\u306E\u307F\u4F7F\u7528\u3067\u304D\u307E\u3059",
|
|
102
106
|
reach_sensitivePolicyRequired: "\u8A8D\u8A3C\u60C5\u5831\u306E\u901A\u77E5\u306B\u306F\u30C7\u30FC\u30BF\u30D9\u30FC\u30B9\u306E\u53D7\u9818\u8A18\u9332\u3001\u56FA\u5B9A\u306E\u51AA\u7B49\u30AD\u30FC\u3068\u6709\u52B9\u671F\u9650\u304C\u5FC5\u8981\u3067\u3059",
|
|
103
107
|
reach_sensitiveDeliveryUnconfirmed: "\u8A8D\u8A3C\u60C5\u5831\u306E\u914D\u4FE1\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3002\u81EA\u52D5\u518D\u9001\u305B\u305A\u3001\u53D7\u9818\u8A18\u9332\u306E\u78BA\u8A8D\u307E\u305F\u306F\u8A8D\u8A3C\u60C5\u5831\u306E\u518D\u767A\u884C\u304C\u5FC5\u8981\u3067\u3059\u3002",
|
|
104
|
-
reach_messageExpired: "\u8A8D\u8A3C\u60C5\u5831\u306E\u671F\u9650\u304C\u5207\u308C\u305F\u305F\u3081\u914D\u4FE1\u3057\u307E\u305B\u3093\u3067\u3057\u305F"
|
|
108
|
+
reach_messageExpired: "\u8A8D\u8A3C\u60C5\u5831\u306E\u671F\u9650\u304C\u5207\u308C\u305F\u305F\u3081\u914D\u4FE1\u3057\u307E\u305B\u3093\u3067\u3057\u305F",
|
|
109
|
+
reach_apiResponseTooLarge: "API \u5FDC\u7B54\u304C 64 KiB \u306E\u4E0A\u9650\u3092\u8D85\u3048\u3066\u3044\u307E\u3059",
|
|
110
|
+
reach_apiResponseInvalid: "API \u5FDC\u7B54\u306F\u7A7A\u304B\u3001\u7701\u7565\u53EF\u80FD\u306A messageId \u6587\u5B57\u5217\u3092\u6301\u3064 JSON \u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059",
|
|
111
|
+
reach_apiRequestFailed: "API \u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u5931\u6557\u307E\u305F\u306F\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\u3057\u307E\u3057\u305F\u3002\u914D\u4FE1\u7D50\u679C\u306F\u672A\u78BA\u8A8D\u3067\u3059",
|
|
112
|
+
reach_apiHttpsRequired: "\u3053\u306E API \u30DD\u30EA\u30B7\u30FC\u3067\u306F HTTPS \u304C\u5FC5\u8981\u3067\u3001\u81EA\u52D5\u30EA\u30C0\u30A4\u30EC\u30AF\u30C8\u306F\u7981\u6B62\u3055\u308C\u3066\u3044\u307E\u3059"
|
|
105
113
|
};
|
|
106
114
|
|
|
107
115
|
// messages/zh-CN.json
|
|
@@ -149,7 +157,11 @@ var zh_CN_default = {
|
|
|
149
157
|
reach_productionProviderRequired: "\u751F\u4EA7\u73AF\u5883\u5FC5\u987B\u914D\u7F6E\u771F\u5B9E\u89E6\u8FBE\u670D\u52A1\uFF1Bconsole \u4EC5\u53EF\u7528\u4E8E\u5F00\u53D1\u6216\u663E\u5F0F\u6D4B\u8BD5\u6A21\u5F0F",
|
|
150
158
|
reach_sensitivePolicyRequired: "\u51ED\u636E\u901A\u77E5\u9700\u8981\u6570\u636E\u5E93\u56DE\u6267\u3001\u7A33\u5B9A\u5E42\u7B49\u952E\u548C\u6709\u6548\u671F",
|
|
151
159
|
reach_sensitiveDeliveryUnconfirmed: "\u51ED\u636E\u6295\u9012\u5C1A\u672A\u786E\u8BA4\uFF1B\u4E0D\u4F1A\u81EA\u52A8\u91CD\u590D\u53D1\u9001\uFF0C\u8BF7\u67E5\u8BE2\u56DE\u6267\u6216\u7533\u8BF7\u65B0\u51ED\u636E",
|
|
152
|
-
reach_messageExpired: "\u51ED\u636E\u5DF2\u8FC7\u671F\uFF0C\u672A\u6295\u9012"
|
|
160
|
+
reach_messageExpired: "\u51ED\u636E\u5DF2\u8FC7\u671F\uFF0C\u672A\u6295\u9012",
|
|
161
|
+
reach_apiResponseTooLarge: "API \u56DE\u6267\u8D85\u8FC7 64 KiB \u4E0A\u9650",
|
|
162
|
+
reach_apiResponseInvalid: "API \u56DE\u6267\u5FC5\u987B\u4E3A\u7A7A\u6216\u542B\u53EF\u9009 messageId \u5B57\u7B26\u4E32\u7684 JSON \u5BF9\u8C61",
|
|
163
|
+
reach_apiRequestFailed: "API \u8BF7\u6C42\u5931\u8D25\u6216\u8D85\u65F6\uFF0C\u6295\u9012\u7ED3\u679C\u672A\u786E\u8BA4",
|
|
164
|
+
reach_apiHttpsRequired: "\u6B64 API \u7B56\u7565\u8981\u6C42 HTTPS \u4E14\u7981\u6B62\u81EA\u52A8\u91CD\u5B9A\u5411"
|
|
153
165
|
};
|
|
154
166
|
|
|
155
167
|
// src/reach-i18n.ts
|
|
@@ -209,6 +221,10 @@ var ApiProviderConfigSchema = z.object({
|
|
|
209
221
|
headers: z.record(z.string(), z.string()).optional(),
|
|
210
222
|
/** 请求超时毫秒数(默认 10000) */
|
|
211
223
|
timeout: z.number().int().min(0).default(1e4),
|
|
224
|
+
/** HTTP 重定向策略;默认拒绝。敏感发送始终拒绝,即使普通发送显式允许。 */
|
|
225
|
+
redirect: z.enum(["error", "follow"]).default("error"),
|
|
226
|
+
/** 强制 HTTPS;省略时生产敏感发送要求 HTTPS,显式 false 仅用于受信内网传输。 */
|
|
227
|
+
requireHttps: z.boolean().optional(),
|
|
212
228
|
/** 接收端按 Idempotency-Key 原子持久去重,且覆盖所有重试与旧节点迟到请求时才启用;默认 false。 */
|
|
213
229
|
idempotent: z.boolean().default(false)
|
|
214
230
|
});
|
|
@@ -387,15 +403,51 @@ function getSafeEndpoint(url) {
|
|
|
387
403
|
return "[invalid-url]";
|
|
388
404
|
}
|
|
389
405
|
}
|
|
390
|
-
function toReachError2(
|
|
406
|
+
function toReachError2() {
|
|
391
407
|
return {
|
|
392
408
|
code: HaiReachError.SEND_FAILED.code,
|
|
393
409
|
message: reachM("reach_apiSendFailed", {
|
|
394
|
-
params: { error:
|
|
395
|
-
})
|
|
396
|
-
cause: error
|
|
410
|
+
params: { error: reachM("reach_apiRequestFailed") }
|
|
411
|
+
})
|
|
397
412
|
};
|
|
398
413
|
}
|
|
414
|
+
async function readAcknowledgement(response) {
|
|
415
|
+
const reader = response.body?.getReader();
|
|
416
|
+
let complete = false;
|
|
417
|
+
try {
|
|
418
|
+
let text = "";
|
|
419
|
+
let bytes = 0;
|
|
420
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
421
|
+
if (reader) {
|
|
422
|
+
while (true) {
|
|
423
|
+
const chunk = await reader.read();
|
|
424
|
+
if (chunk.done)
|
|
425
|
+
break;
|
|
426
|
+
bytes += chunk.value.byteLength;
|
|
427
|
+
if (bytes > 65536)
|
|
428
|
+
return err(HaiReachError.SEND_FAILED, reachM("reach_apiResponseTooLarge"));
|
|
429
|
+
text += decoder.decode(chunk.value, { stream: true });
|
|
430
|
+
}
|
|
431
|
+
text += decoder.decode();
|
|
432
|
+
}
|
|
433
|
+
complete = true;
|
|
434
|
+
if (text.length === 0)
|
|
435
|
+
return ok({ success: true });
|
|
436
|
+
const result = JSON.parse(text);
|
|
437
|
+
if (!result || typeof result !== "object" || Array.isArray(result))
|
|
438
|
+
return err(HaiReachError.SEND_FAILED, reachM("reach_apiResponseInvalid"));
|
|
439
|
+
const messageId = "messageId" in result ? result.messageId : void 0;
|
|
440
|
+
if (messageId !== void 0 && (typeof messageId !== "string" || messageId.length > 255))
|
|
441
|
+
return err(HaiReachError.SEND_FAILED, reachM("reach_apiResponseInvalid"));
|
|
442
|
+
return ok({ success: true, ...messageId === void 0 ? {} : { messageId } });
|
|
443
|
+
} catch {
|
|
444
|
+
return err(HaiReachError.SEND_FAILED, reachM("reach_apiResponseInvalid"));
|
|
445
|
+
} finally {
|
|
446
|
+
if (!complete)
|
|
447
|
+
await reader?.cancel().catch(() => void 0);
|
|
448
|
+
reader?.releaseLock();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
399
451
|
function createApiProvider() {
|
|
400
452
|
let apiConfig = null;
|
|
401
453
|
return {
|
|
@@ -411,6 +463,10 @@ function createApiProvider() {
|
|
|
411
463
|
);
|
|
412
464
|
}
|
|
413
465
|
apiConfig = config;
|
|
466
|
+
if (config.requireHttps && new URL(config.url).protocol !== "https:" || config.requireHttps && config.redirect === "follow") {
|
|
467
|
+
apiConfig = null;
|
|
468
|
+
return err(HaiReachError.CONFIG_ERROR, reachM("reach_apiHttpsRequired"));
|
|
469
|
+
}
|
|
414
470
|
logger2.info("API provider connected", { endpoint: getSafeEndpoint(config.url), method: config.method });
|
|
415
471
|
return ok(void 0);
|
|
416
472
|
},
|
|
@@ -429,6 +485,9 @@ function createApiProvider() {
|
|
|
429
485
|
);
|
|
430
486
|
}
|
|
431
487
|
logger2.debug("Sending via API callback", { endpoint: getSafeEndpoint(apiConfig.url) });
|
|
488
|
+
const requireHttps = apiConfig.requireHttps ?? (message.sensitive === true && core.module.requiresSharedState(process.env));
|
|
489
|
+
if (requireHttps && new URL(apiConfig.url).protocol !== "https:")
|
|
490
|
+
return err(HaiReachError.CONFIG_ERROR, reachM("reach_apiHttpsRequired"));
|
|
432
491
|
try {
|
|
433
492
|
const payload = {
|
|
434
493
|
to: message.to,
|
|
@@ -450,25 +509,28 @@ function createApiProvider() {
|
|
|
450
509
|
method: apiConfig.method,
|
|
451
510
|
headers,
|
|
452
511
|
body: JSON.stringify(payload),
|
|
453
|
-
signal: controller.signal
|
|
512
|
+
signal: controller.signal,
|
|
513
|
+
redirect: message.sensitive || apiConfig.redirect !== "follow" ? "manual" : "follow"
|
|
454
514
|
});
|
|
455
515
|
if (!response.ok) {
|
|
516
|
+
await response.body?.cancel();
|
|
456
517
|
logger2.warn("API callback returned non-OK status", { status: response.status });
|
|
457
|
-
const rejected = response.status >= 400 && response.status < 500 && ![408, 425, 429].includes(response.status);
|
|
518
|
+
const rejected = response.status >= 300 && response.status < 400 || response.status >= 400 && response.status < 500 && ![408, 425, 429].includes(response.status);
|
|
458
519
|
return err(
|
|
459
520
|
rejected ? HaiReachError.DELIVERY_REJECTED : HaiReachError.SEND_FAILED,
|
|
460
521
|
reachM("reach_apiSendFailed", { params: { error: `HTTP ${response.status}` } })
|
|
461
522
|
);
|
|
462
523
|
}
|
|
463
|
-
const result = await response
|
|
464
|
-
|
|
465
|
-
|
|
524
|
+
const result = await readAcknowledgement(response);
|
|
525
|
+
if (result.success)
|
|
526
|
+
logger2.info("API callback accepted", { hasMessageId: result.data.messageId !== void 0 });
|
|
527
|
+
return result;
|
|
466
528
|
} finally {
|
|
467
529
|
clearTimeout(timeoutId);
|
|
468
530
|
}
|
|
469
|
-
} catch
|
|
470
|
-
logger2.error("API callback send failed", {
|
|
471
|
-
return err(toReachError2(
|
|
531
|
+
} catch {
|
|
532
|
+
logger2.error("API callback send failed", { errorCode: HaiReachError.SEND_FAILED.code });
|
|
533
|
+
return err(toReachError2());
|
|
472
534
|
}
|
|
473
535
|
}
|
|
474
536
|
};
|