@trustbaseai/protocol 0.1.0

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.
Files changed (42) hide show
  1. package/README.md +348 -0
  2. package/dist/bundle-hash.d.ts +54 -0
  3. package/dist/bundle-hash.d.ts.map +1 -0
  4. package/dist/bundle-hash.js +105 -0
  5. package/dist/bundle-hash.js.map +1 -0
  6. package/dist/canonical.d.ts +65 -0
  7. package/dist/canonical.d.ts.map +1 -0
  8. package/dist/canonical.js +215 -0
  9. package/dist/canonical.js.map +1 -0
  10. package/dist/constants.d.ts +200 -0
  11. package/dist/constants.d.ts.map +1 -0
  12. package/dist/constants.js +216 -0
  13. package/dist/constants.js.map +1 -0
  14. package/dist/endpoint-protocol.d.ts +143 -0
  15. package/dist/endpoint-protocol.d.ts.map +1 -0
  16. package/dist/endpoint-protocol.js +236 -0
  17. package/dist/endpoint-protocol.js.map +1 -0
  18. package/dist/errors.d.ts +13 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +21 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/index.d.ts +24 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +121 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/registry.d.ts +116 -0
  27. package/dist/registry.d.ts.map +1 -0
  28. package/dist/registry.js +637 -0
  29. package/dist/registry.js.map +1 -0
  30. package/dist/sp-framework.d.ts +686 -0
  31. package/dist/sp-framework.d.ts.map +1 -0
  32. package/dist/sp-framework.js +187 -0
  33. package/dist/sp-framework.js.map +1 -0
  34. package/dist/validation.d.ts +37 -0
  35. package/dist/validation.d.ts.map +1 -0
  36. package/dist/validation.js +85 -0
  37. package/dist/validation.js.map +1 -0
  38. package/dist/ws-protocol.d.ts +242 -0
  39. package/dist/ws-protocol.d.ts.map +1 -0
  40. package/dist/ws-protocol.js +289 -0
  41. package/dist/ws-protocol.js.map +1 -0
  42. package/package.json +34 -0
package/README.md ADDED
@@ -0,0 +1,348 @@
1
+ # @trustbaseai/protocol
2
+
3
+ > TrustBase 的**跨侧唯一事实来源**:链上 / 索引器 / PWA 三方共用的类型、常量与纯函数。
4
+
5
+ - **零副作用**、**零 Node 内建依赖**:Node 与浏览器同一份代码、同一份结果
6
+ - 只有类型 + 纯函数 + 常量;**不含私钥、不签名、不联网、不落盘**
7
+ - 运行时依赖只有 `@noble/hashes`(sha256 与 hex/utf8 工具)
8
+
9
+ ```bash
10
+ npm install @trustbaseai/protocol
11
+ ```
12
+
13
+ ```ts
14
+ import {
15
+ canonicalize,
16
+ endpointBundleHashHex,
17
+ getMsgType,
18
+ validateQueryRequest,
19
+ } from '@trustbaseai/protocol';
20
+ ```
21
+
22
+ ---
23
+
24
+ ## 1. 为什么先做这个包
25
+
26
+ SDK 里最能"静默失败"的东西都在这儿:
27
+
28
+ | 症状 | 根因 | 本包的防线 |
29
+ |---|---|---|
30
+ | `unable to resolve type URL /trustchain.order.v1.MsgXxx` | type URL 写错/模块未上线 | `registry` 逐条核对 proto,带 `文件:行号` 出处 |
31
+ | 交易被拒但报错像"权限不足" | `signer` 注释与字段填错位 | 每个 message 标注 `cosmos.msg.v1.signer` 指名字段 |
32
+ | **所有见证票被拒、链上永远没票** | 三方对同一个 bundle 算出不同 hash | `canonicalize` + 金标准向量(换算法 = 破坏兼容) |
33
+ | 浏览器里 `ws` 查询参数被拒 | 客户端与索引器上限不一致 | `WS_QUERY_LIMITS` 与索引器实现同值,测试锁死 |
34
+ | 页面在浏览器里崩(Node API) | 误引 Node 内建模块 | `test/isomorphic.test.ts` 静态扫描守门 |
35
+
36
+ ---
37
+
38
+ ## 2. `bundle_hash` 规范化(红线,冻结成一份)
39
+
40
+ ```
41
+ bundle_hash = sha256( utf8( canonicalize(bundle) ) ) // 32 字节
42
+ ```
43
+
44
+ 链上 `x/endpoint` **只校验两件事**:长度是 32 字节、且等于当前指针里的哈希
45
+ (`x/endpoint/keeper/msg_server.go:44`(轮换) / `:142`(见证) 长度校验,`:161` 哈希相等)。
46
+ **链码不强制算法本身** —— 所以一旦三方算不一致,症状是
47
+ 「所有见证票被拒、链上永远没票」,而链上只会回一句看起来像"对方写错"的错误。
48
+
49
+ ### 规范化规则 `trustbase-canonical-json-v1`
50
+
51
+ | # | 规则 |
52
+ |---|---|
53
+ | 1 | 只接受 JSON 数据模型:null / boolean / 整数 / 字符串 / 数组 / 纯对象 |
54
+ | 2 | 对象键按 **Unicode 码点升序** |
55
+ | 3 | 数字**必须是整数**且 `|n| <= 2^53-1`;`NaN` / `±Infinity` / 浮点 / `-0` / 超大整数一律抛错 |
56
+ | 4 | 字符串按 UTF-8 编码;非 ASCII 不转义;只转义 `"` `\` 与 C0 控制符;拒绝落单代理项 |
57
+ | 5 | 数组**保序**;拒绝稀疏数组与数组上的非下标属性 |
58
+ | 6 | 对象只读 enumerable own 属性;拒绝 accessor (getter)、symbol 键 |
59
+ | 7 | 输出无任何空白 |
60
+ | 8 | 拒绝 `undefined` / bigint / symbol / function / 循环引用 / 深度 > 64 |
61
+
62
+ **与 `JSON.stringify` 的三个差异**(都是刻意的,都是"静默不一致"的入口):
63
+
64
+ - 不调用 `toJSON()` —— 遇到就报错,而不是把对象悄悄换成别的东西
65
+ - 不做 `-0 → 0` 的静默归一 —— 直接拒(否则"谁先归一"决定哈希)
66
+ - 不丢 `undefined` 键 —— 直接拒(丢键会让"少写一个字段"看起来像正常数据)
67
+
68
+ **跨语言实现要点**:Go 的 `sort.Strings` 与 Python 的 `sorted()` 天然是码点序;
69
+ JS 默认 `sort()` 是 **UTF-16 码元序**,对 emoji 这类增补平面字符会排错
70
+ (本包提供 `compareByCodePoint`,并用金标准向量 `unicode-key-order` 把这个坑锁死)。
71
+
72
+ ### 函数签名
73
+
74
+ ```ts
75
+ function canonicalize(value: unknown): string; // 不满足规则 → 抛 CanonicalizationError
76
+ function endpointBundleHash(bundle: unknown): Uint8Array; // 32 字节,喂给链上 bytes 字段
77
+ function endpointBundleHashHex(bundle: unknown): string; // 64 位小写 hex,线协议用
78
+ function announcementSigningPayload<T extends { sig?: unknown }>(announce: T): Omit<T, 'sig'>;
79
+ function announcementHashHex(announce: unknown): string; // = 去掉 sig 后取 hash
80
+ function isBundleHashHex(value: unknown): value is string; // 严格:必须全小写
81
+ function normalizeBundleHashHex(value: string): string; // 宽松:大小写都收 → 归一为小写
82
+ function bundleHashFromHex(value: string): Uint8Array; // hex → 32 字节
83
+ function bundleHashToHex(bytes: Uint8Array): string; // 32 字节 → hex
84
+ ```
85
+
86
+ 线协议里 hash 用 **64 位小写 hex**(与链侧 `credential_hash` / `evidence_hash` 同格式);
87
+ 链上 `bytes` 字段的 wire JSON 是 **base64** —— 发链前记得转,这是本仓库真机踩过的坑
88
+ (`routes/chain-writes-products.js:205`)。
89
+
90
+ ### ⚠️ 换算法 = 破坏兼容
91
+
92
+ 链上只比对字节,不做算法协商。任何一方换算法(换规范化规则 / 换哈希 / 换编码)
93
+ 都必须**链上 + 索引器 + PWA 三方同时升级**,并 bump `CANONICALIZATION_VERSION`
94
+ (该值应进协议握手与广播包版本;旧包直接拒收)。
95
+
96
+ ---
97
+
98
+ ## 3. 金标准向量
99
+
100
+ `test-vectors/bundle-hash.golden.json` —— **冻结的常量**,不是运行时算出来的对照。
101
+ 测试对它做三层校验:
102
+
103
+ 1. 逐条比 `canonical` 与 `sha256_hex`
104
+ 2. **独立复算**:用 `node:crypto` 对同一份规范化字符串再算一次 sha256(绕开 `@noble/hashes`)
105
+ 3. 关系断言:键序不同 → 必须同 hash;数组反序 → 必须不同 hash
106
+
107
+ | id | 分组 | 说明 |
108
+ |---|---|---|
109
+ | `key-order-a` / `key-order-b` | key-order-equivalence | 内容相同、键序不同 → 同 hash |
110
+ | `nested` | nested-objects | 每层都排序,数字/布尔/null 正确序列化 |
111
+ | `array-order-asc` / `array-order-desc` | array-order-preserved | 数组保序 → 反序必须不同 hash |
112
+ | `non-ascii` | non-ascii | 中文字段不转义 |
113
+ | `unicode-key-order` | unicode-key-order | 码点序而非 UTF-16 码元序(锁住 emoji 排序坑) |
114
+ | `empty-collections` | empty-collections | 空数组 / 空对象 / 空字符串互不混淆 |
115
+ | `boundary-length` | boundary-length | 4096 字符字符串 + 单字符键 |
116
+ | `realistic-announce` / `-reordered` | realistic-announce + 键序等价 | 真实广播包形态 + 整包键序打乱仍同 hash |
117
+
118
+ 重新生成向量的脚本是 `tools/gen-bundle-hash-vectors.cjs`,**故意不挂进 npm scripts**:
119
+ 必须显式敲路径才跑得到,且会打印醒目警告。**因为测试红了才去刷向量 = 让三方静默分裂**。
120
+
121
+ ---
122
+
123
+ ## 4. 链消息注册表
124
+
125
+ `MSG_TYPES` 收录商家/买家/服务商实际要发的消息(gov-only 的 `MsgUpdateParams` 不收)。
126
+ 每条给:`typeUrl`、`protoMessage`、`signer`、`fields[]`(名/类型/是否必填)、
127
+ `source`(`proto 文件:行号`)、`provenBy`(真机跑通的参考实现,可选)。
128
+
129
+ 字段 `kind` 与 wire 形态:
130
+
131
+ - `uint32` / `uint64` / `int64` → JSON 里是**字符串**
132
+ - `bytes` → JSON 里是 **base64**(不是 hex)
133
+ - `enum` → JSON 里是枚举**名**(如 `PRODUCT_TYPE_PHYSICAL`)
134
+
135
+ **出处可机器校验**(不是手抄):
136
+
137
+ ```bash
138
+ npm run build
139
+ node tools/check-proto-citations.cjs /path/to/trustchain-relmerge # 校验
140
+ node tools/check-proto-citations.cjs /path/to/trustchain-relmerge --fix # 收紧行号
141
+ ```
142
+
143
+ 它逐条核对 message 名、字段名、**字段类型**、`cosmos.msg.v1.signer` 注解,并做**反向完整性检查**
144
+ (proto 里有、注册表漏登记 = 失败)。该工具**不在 `npm test` 里**:proto 不在本仓库
145
+ (SDK 开源、链码单仓),CI 跑不了不是 bug —— 有链码 checkout 的维护者手动跑。
146
+
147
+ 已收录(33 条,按 proto 核对,快照 `trustchain-relmerge@40d1654` + 2026-09-23 工作区改动):
148
+
149
+ ```
150
+ /trustchain.order.v1.MsgCreateOrder
151
+ /trustchain.order.v1.MsgConfirmPayment
152
+ /trustchain.order.v1.MsgSettleOrder
153
+ /trustchain.order.v1.MsgTransferCredits
154
+ /trustchain.order.v1.MsgShipOrder
155
+ /trustchain.order.v1.MsgConfirmOrder
156
+ /trustchain.order.v1.MsgCancelOrder
157
+ /trustchain.order.v1.MsgDisputeOrder
158
+ /trustchain.order.v1.MsgResolveDispute
159
+ /trustchain.marketplace.v1.MsgListSKU
160
+ /trustchain.marketplace.v1.MsgAddStock
161
+ /trustchain.marketplace.v1.MsgReduceStock
162
+ /trustchain.marketplace.v1.MsgUpdateSKU
163
+ /trustchain.marketplace.v1.MsgPauseSKU
164
+ /trustchain.marketplace.v1.MsgResumeSKU
165
+ /trustchain.marketplace.v1.MsgDelistSKU
166
+ /trustchain.marketplace.v1.MsgConfiscateStake
167
+ /trustchain.verification.v1.MsgSubmitVerification
168
+ /trustchain.verification.v1.MsgApproveVerification
169
+ /trustchain.verification.v1.MsgRejectVerification
170
+ /trustchain.verification.v1.MsgRevokeVerification
171
+ /trustchain.verification.v1.MsgVerifyByGovernance
172
+ /trustchain.serviceprovider.v1.MsgRegisterServiceProvider
173
+ /trustchain.serviceprovider.v1.MsgSubmitPaymentFact
174
+ /trustchain.serviceprovider.v1.MsgConfirmPaymentFact
175
+ /trustchain.serviceprovider.v1.MsgClaimProviderReward
176
+ /trustchain.insurance.v1.MsgSubmitClaim
177
+ /trustchain.reward.v1.MsgRewardPoolTransfer
178
+ /trustchain.endpoint.v1.MsgUpdateEndpointPointer
179
+ /trustchain.endpoint.v1.MsgAttestEndpoint
180
+ /trustchain.subchain.v1.MsgFundBootstrap
181
+ /trustchain.subchain.v1.MsgDisburseBootstrap
182
+ /trustchain.economy.v1.MsgGrantCredits
183
+ ```
184
+
185
+ **`signer` 别读成"签名者身份"**。`signer` 只表示"签名写在这条消息的哪个字段里";
186
+ 谁被允许签**由 keeper 判定**。典型例子:`MsgConfirmOrder` / `MsgDisputeOrder` 的 `signer`
187
+ 是 `submitter`,但 submitter 可以是订单卖家、`params.dispute_authority`(平台服务商),
188
+ 或超时兜底后的**任意账户** —— 三条路径见 `registry.ts` 里这两条的注释与
189
+ `x/order/keeper/order_submit_auth.go`。
190
+
191
+ ```ts
192
+ function getMsgType(typeUrl: string): MsgTypeSpec | undefined;
193
+ function getMsgTypeByProtoMessage(protoMessage: string): MsgTypeSpec | undefined;
194
+ function isKnownMsgType(typeUrl: unknown): typeUrl is string;
195
+ ```
196
+
197
+ 建议用法:每次构造交易前过一遍 `getMsgType(typeUrl)`。返回 `undefined` 就先当作
198
+ 「type URL 拼错 or 链上模块未上线」处理,别把二进制丢进广播 ——
199
+ 真机上这两种情况的报错信息长得一模一样。
200
+
201
+ ---
202
+
203
+ ## 5. 协议类型(WS 查询 / 端点广播 / 探测背书)
204
+
205
+ ### 5.1 索引器 WS 查询协议
206
+
207
+ 已有实现的**镜像**(`trustchain-relmerge/indexer/src/core/websocket.ts` 与
208
+ `ws-query-provider.ts`),不是新设计。三条容易踩的语义:
209
+
210
+ 1. **`events` 优先于 `topics`**(老字段兼容策略;两个都给时按 `events` 算)
211
+ 2. **非法 topic 是"过滤"不是"报错"** —— 服务端照样回 `subscribed`,
212
+ 以响应里的 `events`(当前全部订阅集合)为准
213
+ 3. **连接建立即默认全订** `block/transaction/product/order`;`subscribe` 是**增量**语义
214
+
215
+ ```ts
216
+ validateClientMessage(input): Validation<WsClientMessage>;
217
+ validateSubscribeRequest(input): Validation<ValidatedSubscriptionRequest>;
218
+ validateUnsubscribeRequest(input): Validation<ValidatedSubscriptionRequest>;
219
+ validateQueryRequest(input): Validation<ValidatedQueryRequest>;
220
+ validateQueryArgs(name, args): Validation<WsQueryArgsByName[name]>;
221
+ validateServerMessage(input): Validation<WsServerMessage>;
222
+ extractTopics(request): string[];
223
+ ```
224
+
225
+ 校验失败**不抛异常**,返回 `{ ok: false, error, message }`,其中 `error` 与索引器实际回的错误码同值
226
+ (`invalid_id` / `unknown_query` / `invalid_params` / …),客户端一套分支就能同时处理本地预检与服务端回包。
227
+
228
+ 上限常量与索引器**同值**(改一个就得改另一边):
229
+
230
+ | 常量 | 值 | 出处 |
231
+ |---|---|---|
232
+ | `maxQueryIdLength` | 128 | `ws-query-provider.ts:23` |
233
+ | `maxLimit` / `defaultLimit` | 100 / 20 | `:18-19` |
234
+ | `maxOffset` | 1 000 000 | `:20` |
235
+ | `maxKeywordLength` / `maxIdLength` | 200 | `:21-22` |
236
+ | `maxMessageBytes` | 64 KiB | `:24` |
237
+ | `maxPendingQueries` / `queryTimeoutMs` | 8 / 5000 | `websocket.ts:70-71` |
238
+
239
+ ### 5.2 端点广播包 / 探测背书包
240
+
241
+ ```ts
242
+ validateEndpointAnnouncement(input): Validation<EndpointAnnouncement>;
243
+ validateProbeReport(input): Validation<ProbeReport>;
244
+ shouldAcceptAnnouncement(local, incoming, now?): AnnouncementAcceptance;
245
+ isEndpointDescriptor(input): input is EndpointDescriptor;
246
+ ```
247
+
248
+ - 两个校验函数**通过时返回入参本身**(同一引用),绝不重建对象 ——
249
+ hash 是对**原文**算的,"顺手补个默认字段"就会让哈希对不上
250
+ - 不做密码学:`sig` / `pubkey` 只做结构校验;验签与"pubkey 是否等于链上登记身份"
251
+ 属于 `@trustbase/identity` / `@trustbase/p2p`(planned)
252
+ - `announcementHashHex()` 定义的哈希域 = 签名域 = `announce 去掉 sig`,
253
+ 探测背书包的 `endpoint_hash` 必须等于它(§6.4 第 2 层:防"签名留白、内容调包")
254
+ - 接收判定:`issued_at + ttl_ms <= now` → 拒;本地无记录 → 收;
255
+ `seq > 本地 seq` → 收;否则拒(防重放 / 防回滚到旧端点)
256
+
257
+ > **诚实标注**:`endpoint_announce` / `probe_report` / gossip topic
258
+ > `trustbase/endpoints/v1` 在链侧仓库里**尚无实现**(grep 为空),属 P2 阶段落地。
259
+ > 本包把它们**先冻结成契约**,让索引器与 PWA 从一开始照同一份写。
260
+ > 其中"签名域"是**新定义**,不是已存在的事实。
261
+ > 相比之下 `x/endpoint` 的指针与见证是**链上已实现**的,`bundle_hash` 的
262
+ > 长度与相等校验都是硬约束。
263
+
264
+ ---
265
+
266
+ ## 6. 常量
267
+
268
+ ```ts
269
+ DEFAULT_CHAIN_ID // 'trustchain-1'
270
+ BECH32_PREFIX // 'tct'
271
+ DENOM_UTCT // 'utct'(基本 denom)
272
+ TCT_DECIMALS // 6
273
+ UTCT_PER_TCT // 1_000_000(1 TCT = 10^6 utct)
274
+ WS_QUERY_LIMITS // 见上表
275
+ WS_QUERY_NAMES // ['health','catalog','product','endpoints']
276
+ WS_TOPICS // ['block','transaction','product','order']
277
+ PointerState // ACTIVE / UNUSABLE / EXPIRED(=算出) / NONE(=无指针)
278
+ MAX_LATENCY_MS // 3_600_000(= x/endpoint/types/keys.go:44)
279
+ EndpointEventType // endpoint_pointer_updated 等 6 个链上事件名
280
+ GOSSIP_TOPIC_ENDPOINTS // 'trustbase/endpoints/v1'(待实现)
281
+ ```
282
+
283
+ `EXPIRED` **不落链**,是按 `rotated_at_height + ttl_blocks` 实时算的;
284
+ `UNUSABLE` 保留不删除(可审计、可恢复)。出处:`x/endpoint/keeper/keeper.go:168-174`、
285
+ `proto/trustchain/endpoint/v1/endpoint.proto:36-38`。
286
+
287
+ ---
288
+
289
+ ## 7. 构建与测试
290
+
291
+ ```bash
292
+ npm run build # tsc → dist/(CommonJS + .d.ts)
293
+ npm test # jest
294
+ npm run typecheck # tsc --noEmit
295
+ ```
296
+
297
+ 当前产物是 **CommonJS**:Node 直接可用,浏览器经任一打包器可用。
298
+ 源码本身零 Node 依赖,ESM 双产物排在 S1(见下)。
299
+ `test/isomorphic.test.ts` 会静态扫描 `src/`,出现 `node:` 导入 / `process.` / `Buffer`
300
+ 即测试失败 —— 这条纪律靠测试守,不靠自觉。
301
+
302
+ ### 维护者工具(不在 `npm test` 里)
303
+
304
+ | 工具 | 作用 | 何时用 |
305
+ |---|---|---|
306
+ | `tools/check-proto-citations.cjs <repo> [--fix]` | 拿真实 proto 逐条核对注册表:message 名 / 字段名 / **字段类型** / `signer` 注解 + 反向完整性(proto 有而注册表漏 = 失败)。行号漂移会判 FAIL,`--fix` 按 **message 名**在整份 proto 里重定位后重排(不会指错,因为名字是权威) | 改过 proto、或改过 `registry.ts` |
307
+ | `tools/gen-bundle-hash-vectors.cjs` | ⛔ 重新生成金标准向量 | **只有协议大版本升级时**(测试红了别跑它) |
308
+
309
+ 校验工具对空结果输出 `all citations verified`,失败逐条列 `✗`。
310
+ **它读的是 `dist/`** —— 改完源码必须先 `npm run build` 再跑,否则校验的是旧产物(这个坑踩过两次)。
311
+ 本轮校对结果:33 条 type URL 全部通过(2026-09-23,含 D2/D3 两批改动)。
312
+
313
+ ## 8. 尚未做(明写,别当已实现)
314
+
315
+ - **ESM 产物**:当前只有 CJS。浏览器原生 `<script type="module">` 直接 import 尚不可用
316
+ (打包器可以)。
317
+ - **`sig` 的密码学验证**:只做结构校验。
318
+ - **`since_height` 语义**:字段已定义,但索引器当前忽略它(真机未见实现)。
319
+ - **WS 清单文件 / 端口发现的客户端**:常量已冻结(`ws-port.json`、`INDEXER_WS_PORT`),
320
+ 读取与发现逻辑属于 `@trustbase/index-client`(planned)。
321
+ - **`endpoint` 订阅 topic**:设计文档示例里有,索引器尚未实现(`PLANNED_WS_TOPICS`)。
322
+ - **`MsgConfiscateStake` 无法作为交易提交**:proto 里有 message,但 `service Msg` 没有对应 rpc
323
+ (生成的 `MsgServer` 接口也没有该方法),keeper 里却已实现 handler —— 实际只能模块内部调用。
324
+ 本包照实登记,不假装它能发。
325
+
326
+ ## 9. 事实核对发现(不一致清单)
327
+
328
+ 写这一版时逐文件核对 proto / 索引器 / 卖家后端,发现下面这些**不一致**。
329
+ 它们不影响本包的正确性(本包按"链上真实行为"登记),但**会坑到调用方**,所以记在这里:
330
+
331
+ | # | 发现 | 证据 | 影响 / 应对 |
332
+ |---|---|---|---|
333
+ | 1 | **`chain-id` 默认值有两个** | `routes/chain-writes.js:58` = `trustchain-1`;`index.js:334`、`run-relationship.js:22`、`run-unified.js:283,298`、`mcp/server.js:19` = `trustchain-local`;`routes/order.js:151` 的 CLI 示例又写 `trustchain` | 签名 chain-id 不一致 = 交易被拒。索引器 `config/manager.ts:179` 认定正确值是 `trustchain-1`,并在 `:225-227` 显式纠正历史误配。本包取 `trustchain-1` |
334
+ | 2 | ~~**`MsgCreateOrder` 的 signer 语义被注释写反**~~ **已解决(2026-09-22 方案 A)** | 链侧已把 proto 与 Go 统一到 `seller`(`order/v1/tx.proto:46`、`x/order/types/signers.go:57-58`),`routes/chain-writes.js` 的注释不再是错的 | 本包随之为 `signer: 'seller'`。**但 D2 又改了另外三条**(见 #9/#10),所以"signer 不一定是买家/卖家"这个直觉仍然要不得 |
335
+ | 3 | **服务商消息名与设计文档不一致** | 设计/口语里说 `MsgSubmitFact` / `MsgConfirmFact`;proto 实为 `MsgSubmitPaymentFact` / `MsgConfirmPaymentFact`(`serviceprovider/v1/tx.proto:47,96`) | 按口语写 type URL 必报 `unable to resolve type URL`。注册表用 proto 真名 |
336
+ | 4 | **`MsgConfiscateStake` 有 message 无 rpc** | `marketplace/v1/tx.proto:119` 有 message,`:13-24` 的 `service Msg` 没有 rpc;`x/marketplace/types/tx.pb.go:1201-1210` 的 `MsgServer` 接口无此方法,但 `x/marketplace/keeper/confiscate.go:81` 实现了 handler | 该消息**发不出去**(只能模块内部调 `Keeper.ConfiscateStake`)。谁要发它,先补 rpc |
337
+ | 5 | **`bytes` 字段 wire 形态是 base64,仓库里存的是 hex** | `chain-writes-products.js:205` `Buffer.from(hex,'hex').toString('base64')` 后才发链 | 直接把索引器里的 hex `content_hash` 塞进交易 = 链上收到错误字节(不报错)。本包在字段 `note` 里标出来 |
338
+ | 6 | **`endpoint` topic 只在设计文档里** | 索引器全仓 grep 无 `endpoint` 广播路径;默认订阅只有 block/transaction/product/order(`websocket.ts` `handleConnection()`) | 订阅 `endpoint` 不会报错,但**永远收不到东西**。见 `PLANNED_WS_TOPICS` |
339
+ | 7 | **`endpoint_announce` / `probe_report` / gossip topic 尚无实现** | 链侧仓库 grep 无这三个名字 | 本包的这套类型是**先冻结契约**,不是既有事实;签名域属新定义 |
340
+ | 8 | **外部仓库行号会漂移** | 编写本包期间 `indexer/src/core/websocket.ts` 与 `routes/chain-writes.js` 都被其它人改过(行号移动了几十行);2026-09-23 又在 `order/v1/tx.proto` 上漂移了一轮 | 索引器/后端的引用一律**以函数名/符号名为准**;proto 的行号由 `tools/check-proto-citations.cjs` 机器校验(漂移会被判 FAIL,`--fix` 可按 message 名自动重排) |
341
+ | 9 | **`MsgConfirmOrder` / `MsgDisputeOrder` 的 signer 字段从 `seller` 改名为 `submitter`**(同字段号 5) | 链侧先加 `seller = 5`(2026-09-22 中间态),随后 D2 定稿为 `submitter = 5`(`order/v1/tx.proto:90-98` / `:109-117`) | **对调用方是破坏性变更**:字段号不变、类型不变(wire 层仍是 string),但**字段名变了** —— 谁按旧名 `seller` 发,链上读到的 `submitter` 就是空值,签名者取不到地址,交易必被拒。必须与卖家后端同批升级。注册表已改为 `submitter` |
342
+ | 10 | **`signer` 字段名 ≠ 签名者身份**(本次最容易误读的一条) | `MsgConfirmOrder` / `MsgDisputeOrder` 的 submitter 有**三条放行路径**(订单卖家 / `params.dispute_authority` / 超时兜底后的任意账户),判定在 keeper `x/order/keeper/order_submit_auth.go` 的 `authorizeOrderSubmit()` | 任何"signer 一定是卖家"的客户端假设都会错。注册表这两条的注释里写明了三条路径;`params.dispute_authority` 是**治理可调且空串 = 关闭**的参数(`order/v1/params.proto:36-39`),客户端不能硬编码它 |
343
+ | 11 | **`signers.go` 尾部注释与新代码自相矛盾** | `x/order/types/signers.go:17-24` 仍留着旧说明「DisputeOrder: actor is `disputer`」「keeper 应校验 disputer 是买家」,与同文件 `:73-78` 的实现(`submitterAddr(m.Submitter)`)直接冲突 | 链侧文档债(本包不改链仓):读 signers.go 时以**代码**为准。已记录,建议链侧清掉旧注释 |
344
+ | 12 | **`MsgSubmitClaim.order_id` 是"新增必填",不是兼容新增** | `insurance/v1/tx.proto:77-90`(D3 修复);Go `x/insurance/types/tx.pb.go:494-505` 同 | 老客户端不带 `order_id`(= 0)会被链上拒。理赔入口必须同批升级,否则用户侧表现为"提交失败"而无明显原因 |
345
+ | 13 | **`Claim` 状态类型在 `query.proto`,不在 `tx.proto`** | `insurance/v1/query.proto:14` 的 `message Claim`;D3 新增字段在同文件 `:28`(`uint64 order_id = 12`)与 `:34`(`string beneficiary = 13`);`insurance/v1/insurance.proto` 里只有 `InsuranceApplication` / `InsurerStake` / `SKUDeclaration` | 按 `insurance.proto` 找 `Claim` 是找不到的。本包注册表只收 `Msg*` 交易消息,故**未收录 `Claim`**(它是查询侧状态类型)。另注:`beneficiary` **老数据为空串时回退 claimant**(升级前的历史索赔)—— 读链上数据的一方要处理这个回退,别把空串当"无赔付对象" |
346
+ | 14 | **`x/insurance` 只收录了 1 条消息** | 注册表里 `trustchain.insurance.*` 仅 `MsgSubmitClaim`(D3 修复点);同模块另有 9 条(`MsgRequestInsurance` / `MsgConfirmInsurance` / `MsgCancelInsuranceApplication` / `MsgResolveClaim` / `MsgDepositInsuranceStake` / `MsgWithdrawInsuranceStake` / `MsgDeclareSKU` / `MsgRevokeInsurance` / `MsgUpdateParams`)未收录 | 本包**不假装覆盖整个保险模块**。要做保险买家/卖家侧 SDK 时需先补齐(有 `MSG_TYPES` 的反向完整性检查在,补的时候不会漏字段) |
347
+
348
+
@@ -0,0 +1,54 @@
1
+ /**
2
+ * `bundle_hash` —— 端点包哈希,链上 `x/endpoint` 的跨侧契约。
3
+ *
4
+ * ```
5
+ * endpoint_bundle_hash(bundle) = sha256( utf8( canonicalize(bundle) ) )
6
+ * ```
7
+ *
8
+ * 32 字节,链上以 `bytes` 字段承载(`proto/trustchain/endpoint/v1/tx.proto:38` 与 `:70`);
9
+ * 线协议(gossip / WS)里以 **64 字符小写 hex** 承载(与链侧其它哈希字段一致,
10
+ * 如 `credential_hash`、`evidence_hash` 都是 hex)。
11
+ *
12
+ * ⚠️ **换算法 = 破坏兼容**:链上只比对字节,不做算法协商。任何一方换算法
13
+ * (换规范化规则、换哈希、换编码)都必须链上/索引器/PWA 三方同时升级,
14
+ * 并且 bump `CANONICALIZATION_VERSION`。参见 canonical.ts 顶部。
15
+ */
16
+ /** sha256 输出长度(字节)。链上常量同名:`x/endpoint/types/keys.go:41` */
17
+ export declare const ENDPOINT_BUNDLE_HASH_LEN = 32;
18
+ /** 线协议里 hash 的 hex 长度 = 32 字节 × 2 */
19
+ export declare const ENDPOINT_BUNDLE_HASH_HEX_LEN: number;
20
+ /** 取签名/哈希域:广播包去掉 `sig` 之后的全部字段(`sig` 本身不可自签) */
21
+ export declare function announcementSigningPayload<T extends {
22
+ sig?: unknown;
23
+ }>(announce: T): Omit<T, 'sig'>;
24
+ /**
25
+ * 端点包的规范化字符串(= 被哈希的字节的文本形态)。
26
+ * 顶层必须是纯对象:bundle 语义上就是一个对象,原样哈希原始值属于调用方写错。
27
+ */
28
+ export declare function endpointBundleCanonical(bundle: unknown): string;
29
+ /** `bundle_hash` 的原始字节(32B)—— 直接喂给 `MsgUpdateEndpointPointer.bundle_hash` / `MsgAttestEndpoint.bundle_hash` */
30
+ export declare function endpointBundleHash(bundle: unknown): Uint8Array;
31
+ /** `bundle_hash` 的小写 hex(64 字符)—— 线协议 / gossip / WS `endpoint_hash` 用这个 */
32
+ export declare function endpointBundleHashHex(bundle: unknown): string;
33
+ /**
34
+ * 广播包哈希 —— 探测背书包 `endpoint_hash` 必须等于它。
35
+ *
36
+ * 定义:`endpointBundleHashHex(announce 去掉 sig)`。
37
+ * 与签名域同一份输入,保证"签的"和"哈希的"是同一串字节(§6.4 第 2 层:
38
+ * `endpoint_hash` 绑定具体端点内容,防"签名留白、内容调包")。
39
+ */
40
+ export declare function announcementHashHex(announce: unknown): string;
41
+ /** 严格校验:必须恰好 64 个**小写** hex 字符(线协议里做字符串比对,大小写必须唯一) */
42
+ export declare function isBundleHashHex(value: unknown): value is string;
43
+ /**
44
+ * 宽松归一:接受大小写 hex,输出小写;长度或字符不合法则抛错。
45
+ *
46
+ * 参数在类型上是 `string`,运行时仍按 `unknown` 处理 —— SDK 会被 JS 调用方
47
+ * 直接传值,类型系统在这里不构成防线。
48
+ */
49
+ export declare function normalizeBundleHashHex(value: string): string;
50
+ /** hex → 32 字节(大小写都收)。给链上 `bytes` 字段用。 */
51
+ export declare function bundleHashFromHex(value: string): Uint8Array;
52
+ /** 32 字节 → 小写 hex。长度不对直接抛(链上会拒,早失败早发现)。 */
53
+ export declare function bundleHashToHex(bytes: Uint8Array): string;
54
+ //# sourceMappingURL=bundle-hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-hash.d.ts","sourceRoot":"","sources":["../src/bundle-hash.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,2DAA2D;AAC3D,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,qCAAqC;AACrC,eAAO,MAAM,4BAA4B,QAA+B,CAAC;AAEzE,gDAAgD;AAChD,wBAAgB,0BAA0B,CAAC,CAAC,SAAS;IAAE,GAAG,CAAC,EAAE,OAAO,CAAA;CAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAUnG;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAS/D;AAED,+GAA+G;AAC/G,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,OAAO,GAAG,UAAU,CAE9D;AAED,2EAA2E;AAC3E,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,CAE7D;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAE7D;AAED,sDAAsD;AACtD,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE/D;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAW5D;AAED,0CAA0C;AAC1C,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAU3D;AAED,2CAA2C;AAC3C,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CASzD"}
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ /**
3
+ * `bundle_hash` —— 端点包哈希,链上 `x/endpoint` 的跨侧契约。
4
+ *
5
+ * ```
6
+ * endpoint_bundle_hash(bundle) = sha256( utf8( canonicalize(bundle) ) )
7
+ * ```
8
+ *
9
+ * 32 字节,链上以 `bytes` 字段承载(`proto/trustchain/endpoint/v1/tx.proto:38` 与 `:70`);
10
+ * 线协议(gossip / WS)里以 **64 字符小写 hex** 承载(与链侧其它哈希字段一致,
11
+ * 如 `credential_hash`、`evidence_hash` 都是 hex)。
12
+ *
13
+ * ⚠️ **换算法 = 破坏兼容**:链上只比对字节,不做算法协商。任何一方换算法
14
+ * (换规范化规则、换哈希、换编码)都必须链上/索引器/PWA 三方同时升级,
15
+ * 并且 bump `CANONICALIZATION_VERSION`。参见 canonical.ts 顶部。
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.ENDPOINT_BUNDLE_HASH_HEX_LEN = exports.ENDPOINT_BUNDLE_HASH_LEN = void 0;
19
+ exports.announcementSigningPayload = announcementSigningPayload;
20
+ exports.endpointBundleCanonical = endpointBundleCanonical;
21
+ exports.endpointBundleHash = endpointBundleHash;
22
+ exports.endpointBundleHashHex = endpointBundleHashHex;
23
+ exports.announcementHashHex = announcementHashHex;
24
+ exports.isBundleHashHex = isBundleHashHex;
25
+ exports.normalizeBundleHashHex = normalizeBundleHashHex;
26
+ exports.bundleHashFromHex = bundleHashFromHex;
27
+ exports.bundleHashToHex = bundleHashToHex;
28
+ const sha256_1 = require("@noble/hashes/sha256");
29
+ const utils_1 = require("@noble/hashes/utils");
30
+ const canonical_1 = require("./canonical");
31
+ const validation_1 = require("./validation");
32
+ /** sha256 输出长度(字节)。链上常量同名:`x/endpoint/types/keys.go:41` */
33
+ exports.ENDPOINT_BUNDLE_HASH_LEN = 32;
34
+ /** 线协议里 hash 的 hex 长度 = 32 字节 × 2 */
35
+ exports.ENDPOINT_BUNDLE_HASH_HEX_LEN = exports.ENDPOINT_BUNDLE_HASH_LEN * 2;
36
+ /** 取签名/哈希域:广播包去掉 `sig` 之后的全部字段(`sig` 本身不可自签) */
37
+ function announcementSigningPayload(announce) {
38
+ if (!(0, validation_1.isPlainObject)(announce)) {
39
+ throw new canonical_1.CanonicalizationError('bundle_not_object', 'endpoint bundle must be a plain object', '$');
40
+ }
41
+ const { sig: _sig, ...rest } = announce;
42
+ return rest;
43
+ }
44
+ /**
45
+ * 端点包的规范化字符串(= 被哈希的字节的文本形态)。
46
+ * 顶层必须是纯对象:bundle 语义上就是一个对象,原样哈希原始值属于调用方写错。
47
+ */
48
+ function endpointBundleCanonical(bundle) {
49
+ if (!(0, validation_1.isPlainObject)(bundle)) {
50
+ throw new canonical_1.CanonicalizationError('bundle_not_object', 'endpoint bundle must be a plain object', '$');
51
+ }
52
+ return (0, canonical_1.canonicalize)(bundle);
53
+ }
54
+ /** `bundle_hash` 的原始字节(32B)—— 直接喂给 `MsgUpdateEndpointPointer.bundle_hash` / `MsgAttestEndpoint.bundle_hash` */
55
+ function endpointBundleHash(bundle) {
56
+ return (0, sha256_1.sha256)((0, utils_1.utf8ToBytes)(endpointBundleCanonical(bundle)));
57
+ }
58
+ /** `bundle_hash` 的小写 hex(64 字符)—— 线协议 / gossip / WS `endpoint_hash` 用这个 */
59
+ function endpointBundleHashHex(bundle) {
60
+ return (0, utils_1.bytesToHex)(endpointBundleHash(bundle));
61
+ }
62
+ /**
63
+ * 广播包哈希 —— 探测背书包 `endpoint_hash` 必须等于它。
64
+ *
65
+ * 定义:`endpointBundleHashHex(announce 去掉 sig)`。
66
+ * 与签名域同一份输入,保证"签的"和"哈希的"是同一串字节(§6.4 第 2 层:
67
+ * `endpoint_hash` 绑定具体端点内容,防"签名留白、内容调包")。
68
+ */
69
+ function announcementHashHex(announce) {
70
+ return endpointBundleHashHex(announcementSigningPayload(announce));
71
+ }
72
+ /** 严格校验:必须恰好 64 个**小写** hex 字符(线协议里做字符串比对,大小写必须唯一) */
73
+ function isBundleHashHex(value) {
74
+ return (0, validation_1.isHexOfLength)(value, exports.ENDPOINT_BUNDLE_HASH_HEX_LEN) && value === value.toLowerCase();
75
+ }
76
+ /**
77
+ * 宽松归一:接受大小写 hex,输出小写;长度或字符不合法则抛错。
78
+ *
79
+ * 参数在类型上是 `string`,运行时仍按 `unknown` 处理 —— SDK 会被 JS 调用方
80
+ * 直接传值,类型系统在这里不构成防线。
81
+ */
82
+ function normalizeBundleHashHex(value) {
83
+ const raw = value;
84
+ if (!(0, validation_1.isHexOfLength)(raw, exports.ENDPOINT_BUNDLE_HASH_HEX_LEN)) {
85
+ const length = typeof raw === 'string' ? raw.length : -1;
86
+ throw new canonical_1.CanonicalizationError('invalid_bundle_hash_hex', `bundle hash must be ${exports.ENDPOINT_BUNDLE_HASH_HEX_LEN} hex characters, got ${length} chars`, '$');
87
+ }
88
+ return raw.toLowerCase();
89
+ }
90
+ /** hex → 32 字节(大小写都收)。给链上 `bytes` 字段用。 */
91
+ function bundleHashFromHex(value) {
92
+ const bytes = (0, utils_1.hexToBytes)(normalizeBundleHashHex(value));
93
+ if (bytes.length !== exports.ENDPOINT_BUNDLE_HASH_LEN) {
94
+ throw new canonical_1.CanonicalizationError('invalid_bundle_hash_length', `bundle hash must decode to ${exports.ENDPOINT_BUNDLE_HASH_LEN} bytes, got ${bytes.length}`, '$');
95
+ }
96
+ return bytes;
97
+ }
98
+ /** 32 字节 → 小写 hex。长度不对直接抛(链上会拒,早失败早发现)。 */
99
+ function bundleHashToHex(bytes) {
100
+ if (bytes.length !== exports.ENDPOINT_BUNDLE_HASH_LEN) {
101
+ throw new canonical_1.CanonicalizationError('invalid_bundle_hash_length', `bundle hash must be ${exports.ENDPOINT_BUNDLE_HASH_LEN} bytes, got ${bytes.length}`, '$');
102
+ }
103
+ return (0, utils_1.bytesToHex)(bytes);
104
+ }
105
+ //# sourceMappingURL=bundle-hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bundle-hash.js","sourceRoot":"","sources":["../src/bundle-hash.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * 规范化 JSON (canonical JSON) —— `bundle_hash` 的唯一算法,跨侧冻结。
3
+ *
4
+ * ## 为什么需要它
5
+ *
6
+ * 链上 `x/endpoint` 只校验两件事:`bundle_hash` 是 32 字节、且等于当前指针里的哈希
7
+ * (长度校验 `x/endpoint/keeper/msg_server.go:44`(轮换) / `:142`(见证),
8
+ * 哈希相等校验 `msg_server.go:161`)。**链码不强制算法本身** —— 也就是说:
9
+ *
10
+ * 链上写指针的是 A 方,投见证票的是 B 方,浏览器 PWA 算 `endpoint_hash` 的是 C 方。
11
+ * 三方任何一方把同一个 bundle 算成不同字节 → 所有见证票被拒、链上永远没票,
12
+ * 而链上只会回 `bundle_hash must equal current pointer` 这种看起来像"对方写错"的错。
13
+ *
14
+ * 所以这份规则是**红线**:换算法 = 破坏兼容,必须链上/索引器/PWA 三方同时升级,
15
+ * 并且必须 bump `CANONICALIZATION_VERSION`(写进协议版本,旧包直接拒收)。
16
+ *
17
+ * ## 规则(`trustbase-canonical-json-v1`)
18
+ *
19
+ * | # | 规则 |
20
+ * |---|---|
21
+ * | 1 | 只接受 JSON 数据模型:null / boolean / 整数 / 字符串 / 数组 / 纯对象 |
22
+ * | 2 | 对象键按 **Unicode 码点升序**(不是 UTF-16 码元序;两者仅对增补平面字符不同) |
23
+ * | 3 | 数字**必须是整数**且 `|n| <= 2^53-1`;`NaN` / `±Infinity` / 浮点 / `-0` / 超大整数一律抛错 |
24
+ * | 4 | 字符串按 UTF-8 编码;非 ASCII **不转义**(中文原样输出);只转义 `"` `\` 与 C0 控制符;拒绝落单的代理项 |
25
+ * | 5 | 数组**保序**;拒绝稀疏数组与数组上的非下标属性 |
26
+ * | 6 | 对象只读 **enumerable own 属性**;拒绝 accessor (getter)、symbol 键 |
27
+ * | 7 | 输出**无任何空白**:`{"a":1,"b":[1,2]}` |
28
+ * | 8 | 拒绝 `undefined` / bigint / symbol / function / 循环引用 / 深度 > 64 |
29
+ *
30
+ * **与 `JSON.stringify` 的三个差异**(踩过就知道疼):
31
+ * - 不调用 `toJSON()`(Date 之类会直接被拒,而不是悄悄变成 ISO 字符串)
32
+ * - 不做 `-0 → 0` 的静默归一,直接拒(否则两份"相等"的输入算出不同 hash 取决于谁先归一)
33
+ * - 不做 `undefined` 键的静默丢弃,直接拒(丢键会让"少写一个字段"看起来像正常数据)
34
+ *
35
+ * **跨语言实现要点**:Go 用 `sort.Strings`(== UTF-8 字节序 == 码点序,天然一致);
36
+ * Python 用默认 `sorted()`(码点序);JS 必须用本文件的 `compareByCodePoint`,
37
+ * 因为 JS 默认 `sort()` 是 UTF-16 码元序,遇到 emoji 这类增补平面字符会排错。
38
+ */
39
+ import { ProtocolError } from './errors';
40
+ /** 规则集版本。换算法必须 bump 这个值(会进协议握手/广播包,旧版本直接拒收)。 */
41
+ export declare const CANONICALIZATION_VERSION = "trustbase-canonical-json-v1";
42
+ /** 最大嵌套深度。防递归爆栈 / 防对方构造深树打爆内存。 */
43
+ export declare const MAX_CANONICAL_DEPTH = 64;
44
+ export declare class CanonicalizationError extends ProtocolError {
45
+ /** 出错位置,JSONPath 风格,如 `$.endpoints[0].hints` */
46
+ readonly path: string;
47
+ constructor(code: string, message: string, path: string);
48
+ }
49
+ /**
50
+ * 码点升序比较器(跨语言一致)。
51
+ *
52
+ * 不能用 `a < b`:JS 的字符串比较是 UTF-16 码元序,`'\u{1F600}' < '\uFFFD'` 的结果
53
+ * 与码点序相反,而 Go/Python 都是码点序。这类差异只在少数键上暴露,是典型的
54
+ * "本地测试全绿、跨端全红"。
55
+ */
56
+ export declare function compareByCodePoint(a: string, b: string): number;
57
+ /**
58
+ * 规范化 JSON:把任意值写成**字节唯一**的字符串表示。
59
+ *
60
+ * 同一个逻辑值无论键序、来源、运行时,输出必须逐字节相同。
61
+ *
62
+ * @throws {CanonicalizationError} 输入不满足 canonical JSON 数据模型时
63
+ */
64
+ export declare function canonicalize(value: unknown): string;
65
+ //# sourceMappingURL=canonical.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../src/canonical.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,gDAAgD;AAChD,eAAO,MAAM,wBAAwB,gCAAgC,CAAC;AAEtE,kCAAkC;AAClC,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,qBAAa,qBAAsB,SAAQ,aAAa;IACtD,gDAAgD;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;CAKxD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAa/D;AA2LD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD"}