@dshtrading/authority 0.0.0-stage → 0.6.1

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/LICENSE ADDED
@@ -0,0 +1,75 @@
1
+ Copyright (c) 2026 zhu1090093659 (dsh-trading)
2
+
3
+ # PolyForm Noncommercial License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncommercial Purposes
34
+
35
+ Any noncommercial purpose is a permitted purpose.
36
+
37
+ ## Personal Uses
38
+
39
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
40
+
41
+ ## Noncommercial Organizations
42
+
43
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
44
+
45
+ ## Fair Use
46
+
47
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
48
+
49
+ ## No Other Rights
50
+
51
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
52
+
53
+ ## Patent Defense
54
+
55
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
56
+
57
+ ## Violations
58
+
59
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
60
+
61
+ ## No Liability
62
+
63
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
64
+
65
+ ## Definitions
66
+
67
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
68
+
69
+ **You** refers to the individual or entity agreeing to these terms.
70
+
71
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
72
+
73
+ **Your licenses** are all the licenses granted to you for the software under these terms.
74
+
75
+ **Use** means anything you do with the software requiring one of your licenses.
package/lib/index.d.ts ADDED
@@ -0,0 +1,198 @@
1
+ //#region src/index.d.ts
2
+ /** 覆盖授权平面根目录的环境变量(**必须显式配置**;没有默认位置)。 */
3
+ declare const AUTHORITY_DIR_ENV = "DSH_TRADING_AUTHORITY_DIR";
4
+ /** 可选:平面必须归这个 uid(「允许的属主」钉死;缺省只要求 ≠ 运行 uid)。 */
5
+ declare const AUTHORITY_OWNER_UID_ENV = "DSH_TRADING_AUTHORITY_OWNER_UID";
6
+ /** 开发形态显式 opt-in(名字里带 dev):同 uid 平面放行,但每次判定留痕。 */
7
+ declare const AUTHORITY_DEV_ENV = "DSH_TRADING_AUTHORITY_DEV_SAME_UID";
8
+ /** 运营 CLI 用:声明 trading agent 运行在哪个 uid 下(平面归它就是自铸)。 */
9
+ declare const AUTHORITY_AGENT_UID_ENV = "DSH_TRADING_AUTHORITY_AGENT_UID";
10
+ /** 人工签署的实盘授权文件名。 */
11
+ declare const GRANT_FILENAME = "live-trading.grant.json";
12
+ /** 受信任公钥目录文件名。 */
13
+ declare const TRUSTED_KEYS_FILENAME = "trusted-keys.json";
14
+ /** 授权文档协议版本(逐帧校验,未来换格式时旧文档必须被拒而不是被猜)。 */
15
+ declare const GRANT_PROTOCOL_VERSION = 1;
16
+ interface GrantPayload {
17
+ /** 恒为 true;false 或缺席都按「不是授权」拒绝(不可表达即不可自我扩权)。 */
18
+ liveTrading: boolean;
19
+ /** ISO-8601 签发时刻。 */
20
+ issuedAt: string;
21
+ /** ISO-8601 到期时刻;**必填**(设计文档 §12.3:授权带到期,续签 = 重新签名)。 */
22
+ expiresAt: string;
23
+ /** 签署人标识(自由文本,只用于审计与展示)。 */
24
+ operator?: string;
25
+ /** 备注(自由文本,只用于审计与展示)。 */
26
+ note?: string;
27
+ /**
28
+ * 开发形态标记(在**签名覆盖的 payload 里**):由运营 CLI 在 dev 平面签出。
29
+ * 读取端没有同一个显式 opt-in 时拒绝它 —— 开发形态的授权不会变成生产授权。
30
+ */
31
+ dev?: boolean;
32
+ }
33
+ interface TrustedKey {
34
+ keyId: string;
35
+ alg: 'ed25519';
36
+ publicKeyPem: string;
37
+ }
38
+ /** 拒绝原因(机器可读;新增取值必须同时更新用例的期望表)。 */
39
+ type LiveTradingDenyReason = 'granted' | 'dir-not-configured' | 'plane-not-isolated' | 'dev-grant-not-accepted' | 'no-grant-document' | 'no-trusted-keys' | 'malformed' | 'unknown-key' | 'bad-signature' | 'not-a-grant' | 'expired' | 'not-yet-valid';
40
+ interface GrantVerification {
41
+ ok: boolean;
42
+ reason: LiveTradingDenyReason;
43
+ detail: string;
44
+ payload?: GrantPayload;
45
+ keyId?: string;
46
+ }
47
+ /** 平面目录的来源:显式选项 / 环境变量 / 未配置(未配置 ⇒ 拒绝)。 */
48
+ type AuthorityDirSource = 'option' | 'env' | 'unset';
49
+ interface AuthorityLocation {
50
+ /** 未显式配置时是 undefined —— 本模块没有默认位置。 */
51
+ dir: string | undefined;
52
+ source: AuthorityDirSource;
53
+ }
54
+ /** 平面隔离检查的机器可读结论。 */
55
+ type PlaneIsolationCode =
56
+ /** 归另一个 uid、无 group/other 写位、祖先链干净。 */
57
+ 'isolated' |
58
+ /** 显式 dev opt-in:跳过检查(留痕见 devMode)。 */
59
+ 'dev-opt-in' |
60
+ /** 目录还不存在(还谈不上隔离;判定会继续走到「没有信任锚」)。 */
61
+ 'dir-missing' |
62
+ /** 平台没有 uid 语义(process.geteuid 不可用)⇒ 无法证明隔离。 */
63
+ 'no-uid-semantics' |
64
+ /** 平面目录/文件归运行 uid 所有。 */
65
+ 'owner-is-agent-uid' |
66
+ /** 平面目录/文件属主不是显式配置的「允许的属主」。 */
67
+ 'owner-not-allowed' |
68
+ /** 「允许的属主」被配成了运行 uid 自身(等于没有边界)。 */
69
+ 'configured-owner-is-agent-uid' |
70
+ /** 平面目录/文件带 group/other 写位。 */
71
+ 'group-or-other-writable' |
72
+ /** 祖先目录归运行 uid 所有,或对 group/other 可写且无 sticky 位。 */
73
+ 'ancestor-agent-writable';
74
+ interface PlaneIsolationReport {
75
+ isolated: boolean;
76
+ code: PlaneIsolationCode;
77
+ detail: string;
78
+ /** 本次判定是否在显式开发形态下做出(生产必须为 false)。 */
79
+ devMode: boolean;
80
+ }
81
+ interface LiveTradingDecision extends GrantVerification {
82
+ /** 授权平面是否授予(与 allowed 的区别:镜像可以再收紧一层)。 */
83
+ granted: boolean;
84
+ /** 最终判定:镜像与授权平面取合取(两个都为真才放行)。 */
85
+ allowed: boolean;
86
+ /** preset 资产里的镜像值(非权威,只能收紧)。 */
87
+ mirror: boolean;
88
+ /** 镜像为 true 而授权平面未授予 —— 资产被改写或与平面漂移,必须可见。 */
89
+ mismatch: boolean;
90
+ /** 判定所用的平面目录;未显式配置时 undefined(没有默认位置,见 authorityDir)。 */
91
+ dir: string | undefined;
92
+ /** 平面目录的来源。 */
93
+ dirSource: AuthorityDirSource;
94
+ /** 判定是否在显式开发形态下做出。 */
95
+ devMode: boolean;
96
+ /** 平面隔离检查结论;目录未配置时 undefined。 */
97
+ isolation: PlaneIsolationReport | undefined;
98
+ }
99
+ interface AuthorityOptions {
100
+ /** 显式指定平面目录(测试与 CLI 用);缺省见 authorityDir —— 没有默认位置。 */
101
+ dir?: string;
102
+ /** 环境变量映射,缺省 process.env。 */
103
+ env?: Record<string, string | undefined>;
104
+ /** 判定时刻(毫秒),缺省 Date.now();测试用来固定时间轴。 */
105
+ now?: number;
106
+ /**
107
+ * 判定进程的 euid,缺省 process.geteuid()。测试用来**模拟生产形态**(平面归另一个
108
+ * uid、进程跑在 agent uid 下)——本机同 uid 时无法真实制造这种文件属主。
109
+ *
110
+ * 只在有 uid 语义的平台上有效:见 uidSemantics。
111
+ */
112
+ euid?: number;
113
+ /**
114
+ * 本平台是否有 uid 语义(`process.geteuid` 可用);缺省 hasUidSemantics()。
115
+ *
116
+ * 测试用 false 表达「没有 uid 语义的平台(Windows)」。此时注入 euid 也不构成隔离证据
117
+ * —— Windows 的 `fs.Stats.uid` 恒为 0、`mode` 是按只读属性合成的,拿它们比对只会把
118
+ * 「无从证明」说成某个具体结论。
119
+ */
120
+ uidSemantics?: boolean;
121
+ }
122
+ /** 显式 opt-in 的开发形态(名字里带 dev,且值必须是精确的 '1')。 */
123
+ declare function devOptIn(env?: Record<string, string | undefined>): boolean;
124
+ /** 当前进程的 euid;没有 uid 语义的平台(Windows)返回 undefined ⇒ 读取端按未隔离拒绝。 */
125
+ declare function processEuid(): number | undefined;
126
+ /**
127
+ * 本平台是否有 uid 语义(`process.geteuid` 可用)——属主检查的**能力前提**。
128
+ *
129
+ * Windows 上没有:`fs.Stats.uid` 恒为 0、`mode` 是按只读属性合成的,两者都不是属主证据。
130
+ * 「平面归另一个 uid 所有」在 Windows 上无从证明,所以判定按能力而非按注入值走 —— 否则拿
131
+ * 合成的 stat 去比对只会得到 `ancestor-agent-writable` 这类假阳性结论(把「不知道」说成
132
+ * 「agent 可写」)。没有这条语义就一定 fail-closed。
133
+ */
134
+ declare function hasUidSemantics(): boolean;
135
+ /**
136
+ * 授权平面根目录:显式 dir > $DSH_TRADING_AUTHORITY_DIR > **未配置(undefined)**。
137
+ *
138
+ * 这里**没有**默认位置:旧的缺省 $DSH_HOME/authority 落在 agent 自己 uid 可写的 home
139
+ * 里,等于把「谁能授予实盘」交给 agent(验收发现 #1)。要授权就必须显式把平面指到
140
+ * 一个不属于 agent uid 的目录。
141
+ */
142
+ declare function resolveAuthorityLocation(options?: AuthorityOptions): AuthorityLocation;
143
+ /** 便捷形式:等价 resolveAuthorityLocation(options).dir(未配置 ⇒ undefined)。 */
144
+ declare function authorityDir(options?: AuthorityOptions): string | undefined;
145
+ /**
146
+ * 平面隔离检查:平面及其祖先链是否落在「运行 uid(= agent uid)可写」的位置上。
147
+ *
148
+ * 检查项逐条见 PlaneIsolationCode;任何一条不过都返回 isolated=false 与机器可读 code。
149
+ * 这是唯一**agent 无法伪造**的一层:它写得进文件,但改不了属主,也没法把自己的 uid
150
+ * 从祖先链里去掉。
151
+ */
152
+ declare function inspectPlaneIsolation(dir: string, options?: AuthorityOptions): PlaneIsolationReport;
153
+ /**
154
+ * 规范化序列化:对象键递归排序、无空白、undefined 丢弃。
155
+ * 签名覆盖的是这个字节串,**不是**文件字节 —— 否则换个格式化工具(缩进/键序)
156
+ * 就会让一份有效授权变成验签失败,或让「重排键序」变成一次静默的语义篡改。
157
+ */
158
+ declare function canonicalize(value: unknown): string;
159
+ /** 解析受信任公钥目录文本;形状不对一律当「没有可用信任锚」。 */
160
+ declare function parseTrustedKeys(text: string | undefined): TrustedKey[];
161
+ interface VerifyOptions {
162
+ /** 是否接受带 payload.dev=true 的开发形态授权;缺省 false(fail-closed)。 */
163
+ allowDevGrant?: boolean;
164
+ }
165
+ /**
166
+ * 纯验签:给定授权文档文本与信任锚文本,判定是否授予实盘。
167
+ * 不做任何 IO、不读环境、不抛异常 —— 全部失败路径都变成 { ok: false, reason }。
168
+ * 开发形态授权(payload.dev=true)缺省一律拒绝,只有显式 allowDevGrant 才接受。
169
+ */
170
+ declare function verifyGrantDocument(grantText: string | undefined, trustedKeysText: string | undefined, now?: number, options?: VerifyOptions): GrantVerification;
171
+ interface PlaneInspection {
172
+ isolation: PlaneIsolationReport;
173
+ verification: GrantVerification;
174
+ }
175
+ /**
176
+ * 判定一个平面目录:先做隔离检查,再验签(两者都按 stat 指纹缓存,文件/属主一变立刻重算)。
177
+ * 目录不存在时隔离检查返回 dir-missing,验签照旧走到「没有信任锚」——该结论在有 uid 语义的
178
+ * 平台上成立;无 uid 语义的平台(Windows)在能力门就先返回 no-uid-semantics。
179
+ */
180
+ declare function inspectPlane(dir: string, options?: AuthorityOptions): PlaneInspection;
181
+ /** 判定平面当前状态(按 stat 指纹缓存,文件一变立刻重算)。dev 形态授权缺省被拒。 */
182
+ declare function verifyAtDirectory(dir: string, now?: number): GrantVerification;
183
+ /** 替换告警出口(测试记录用;生产缺省走 process.emitWarning)。 */
184
+ declare function setAuthorityMismatchSink(sink: ((message: string) => void) | undefined): void;
185
+ /**
186
+ * 实盘判定。`mirror` 是 preset 资产/插件 config 里的 liveTrading 值。
187
+ *
188
+ * 合取语义(只收紧):allowed = mirror===true && 授权平面授予。
189
+ * 镜像为 false 一律拒绝;镜像为 true 但平面未授予 ⇒ 拒绝 + 一次可见告警(mismatch)。
190
+ * 平面未配置 / 未与 agent uid 隔离 ⇒ 直接拒绝,**不读任何文件**(没有默认位置可读)。
191
+ */
192
+ declare function liveTradingDecision(mirror: boolean | undefined, options?: AuthorityOptions): LiveTradingDecision;
193
+ /** 判定便捷形式:只看放行/不放行。 */
194
+ declare function liveTradingEnabled(mirror: boolean | undefined, options?: AuthorityOptions): boolean;
195
+ /** 清空 stat 指纹缓存(测试与「删掉平面后立刻复验」的场景用)。 */
196
+ declare function resetAuthorityCache(): void;
197
+ //#endregion
198
+ export { AUTHORITY_AGENT_UID_ENV, AUTHORITY_DEV_ENV, AUTHORITY_DIR_ENV, AUTHORITY_OWNER_UID_ENV, AuthorityDirSource, AuthorityLocation, AuthorityOptions, GRANT_FILENAME, GRANT_PROTOCOL_VERSION, GrantPayload, GrantVerification, LiveTradingDecision, LiveTradingDenyReason, PlaneInspection, PlaneIsolationCode, PlaneIsolationReport, TRUSTED_KEYS_FILENAME, TrustedKey, VerifyOptions, authorityDir, canonicalize, devOptIn, hasUidSemantics, inspectPlane, inspectPlaneIsolation, liveTradingDecision, liveTradingEnabled, parseTrustedKeys, processEuid, resetAuthorityCache, resolveAuthorityLocation, setAuthorityMismatchSink, verifyAtDirectory, verifyGrantDocument };
package/lib/index.js ADDED
@@ -0,0 +1,537 @@
1
+ import { createPublicKey, verify } from "node:crypto";
2
+ import { readFileSync, realpathSync, statSync } from "node:fs";
3
+ import { basename, dirname, join, resolve } from "node:path";
4
+ //#region src/index.ts
5
+ /**
6
+ * 实盘授权状态平面(live-trading authority plane)——设计文档 §5 与不变量 13.3 的执行点。
7
+ *
8
+ * 为什么存在(红队 RT-04,已复核):今天「实盘开关」写在 agent 可写的 preset 资产 YAML
9
+ * (packages/<market>/assets/preset/<market>-trader/agent.cordis.yml)与
10
+ * packages/base/src/presets.ts 里 —— agent 改一个文件就能抹掉实盘闸门的第一段。
11
+ * **文件权限不是答案**(同 uid 下没有边界,见设计文档 §2.1),签名才是:
12
+ *
13
+ * 实盘许可 = 人工用私钥签署的授权记录(权威)
14
+ * preset 资产里的 liveTrading = 非权威镜像,只能收紧、永远不能授予
15
+ *
16
+ * 状态平面(位于 agent 可写路径之外,见 authorityDir):
17
+ * <dir>/trusted-keys.json 受信任公钥目录(人类持有私钥;本模块只读)
18
+ * <dir>/live-trading.grant.json 人工签署的实盘授权(本模块只读)
19
+ *
20
+ * 位置与隔离规则(fail-closed;**没有默认位置**):
21
+ * 1. 平面目录必须**显式配置**(dir 选项或 $DSH_TRADING_AUTHORITY_DIR)。未配置就是拒绝
22
+ * (dir-not-configured)—— 曾经缺省落在 $DSH_HOME/authority,那等于让权威住在
23
+ * agent 自己 uid 可写的目录里(验收发现 #1:同 uid 可自铸信任锚并自我授权)。
24
+ * 2. 平面必须与**运行本判定的进程 uid**(= trading agent 的 uid)隔离:平面目录、两份
25
+ * 文件、以及从根到平面目录的整条祖先链都不得归该 uid 所有(祖先归 agent ⇒ 它可以
26
+ * unlink 后替换整个平面),目录与文件还不得带 group/other 写位。任何一条不满足 ⇒
27
+ * plane-not-isolated。**这条是 agent 伪造不了的**:它写得进文件,改不了文件的属主。
28
+ * 平台前提是 POSIX 属主/权限语义:**没有 uid 语义的平台(Windows 上 process.geteuid
29
+ * 不可用,fs.Stats.uid 恒为 0、mode 按只读属性合成)无法证明隔离** ⇒ 一律
30
+ * no-uid-semantics 拒绝(fail-closed);此时注入 euid 也不构成证据(见 hasUidSemantics)。
31
+ * 3. 可选加固:$DSH_TRADING_AUTHORITY_OWNER_UID 把「允许的属主」钉成某个具体 uid
32
+ * (而不是「只要不是 agent 就行」);把它配成运行 uid 自身按配置错误拒绝。
33
+ * 4. 开发形态必须**显式 opt-in**:$DSH_TRADING_AUTHORITY_DEV_SAME_UID=1(名字带 dev),
34
+ * 跳过 2/3,并**每次判定留痕**(decision.devMode=true + 一次可见告警)。dev 形态下
35
+ * 签出的授权文档带 payload.dev=true,而读取端在没有同一个 opt-in 时拒绝它
36
+ * (dev-grant-not-accepted)—— 「开发形态签的东西不构成生产授权」写在签名里,
37
+ * 抹掉这个标记会让签名失效。
38
+ *
39
+ * fail-closed:任何「缺失 / 读不到 / 解析失败 / 验签失败 / 过期 / 尚未生效 / 未配置 /
40
+ * 未隔离」一律 granted=false 并给出机器可读 reason —— 绝不因为读不到而放行。
41
+ *
42
+ * 撤销语义:删掉 grant 文件即撤销。缓存按文件 stat 指纹失效(不是 TTL),所以撤销
43
+ * 在下一次判定就生效,不需要重启任何进程。
44
+ *
45
+ * 诚实边界:本模块挡得住「agent 改写 preset 资产」与「agent 用自己 uid 自铸平面」,
46
+ * 挡不住「agent 拿到签署私钥」。私钥必须由人在另一个 uid 下持有(0600);生产形态里
47
+ * 该目录归执行核 tradectl(P2)之外的 authority uid,本包届时仍是核心与连接器共用的
48
+ * 读取端,路径与隔离规则不变。
49
+ */
50
+ /** 覆盖授权平面根目录的环境变量(**必须显式配置**;没有默认位置)。 */
51
+ const AUTHORITY_DIR_ENV = "DSH_TRADING_AUTHORITY_DIR";
52
+ /** 可选:平面必须归这个 uid(「允许的属主」钉死;缺省只要求 ≠ 运行 uid)。 */
53
+ const AUTHORITY_OWNER_UID_ENV = "DSH_TRADING_AUTHORITY_OWNER_UID";
54
+ /** 开发形态显式 opt-in(名字里带 dev):同 uid 平面放行,但每次判定留痕。 */
55
+ const AUTHORITY_DEV_ENV = "DSH_TRADING_AUTHORITY_DEV_SAME_UID";
56
+ /** 运营 CLI 用:声明 trading agent 运行在哪个 uid 下(平面归它就是自铸)。 */
57
+ const AUTHORITY_AGENT_UID_ENV = "DSH_TRADING_AUTHORITY_AGENT_UID";
58
+ /** 人工签署的实盘授权文件名。 */
59
+ const GRANT_FILENAME = "live-trading.grant.json";
60
+ /** 受信任公钥目录文件名。 */
61
+ const TRUSTED_KEYS_FILENAME = "trusted-keys.json";
62
+ /** 授权文档协议版本(逐帧校验,未来换格式时旧文档必须被拒而不是被猜)。 */
63
+ const GRANT_PROTOCOL_VERSION = 1;
64
+ /** 显式 opt-in 的开发形态(名字里带 dev,且值必须是精确的 '1')。 */
65
+ function devOptIn(env = process.env) {
66
+ return env[AUTHORITY_DEV_ENV] === "1";
67
+ }
68
+ /** 当前进程的 euid;没有 uid 语义的平台(Windows)返回 undefined ⇒ 读取端按未隔离拒绝。 */
69
+ function processEuid() {
70
+ const geteuid = process.geteuid;
71
+ return typeof geteuid === "function" ? geteuid() : void 0;
72
+ }
73
+ /**
74
+ * 本平台是否有 uid 语义(`process.geteuid` 可用)——属主检查的**能力前提**。
75
+ *
76
+ * Windows 上没有:`fs.Stats.uid` 恒为 0、`mode` 是按只读属性合成的,两者都不是属主证据。
77
+ * 「平面归另一个 uid 所有」在 Windows 上无从证明,所以判定按能力而非按注入值走 —— 否则拿
78
+ * 合成的 stat 去比对只会得到 `ancestor-agent-writable` 这类假阳性结论(把「不知道」说成
79
+ * 「agent 可写」)。没有这条语义就一定 fail-closed。
80
+ */
81
+ function hasUidSemantics() {
82
+ return processEuid() !== void 0;
83
+ }
84
+ function parseUid(value) {
85
+ if (value === void 0 || value.trim().length === 0) return {};
86
+ const uid = Number(value.trim());
87
+ if (!Number.isInteger(uid) || uid < 0) return { error: "uid 配置不合法(" + JSON.stringify(value) + ")——必须是非负整数" };
88
+ return { uid };
89
+ }
90
+ /**
91
+ * 授权平面根目录:显式 dir > $DSH_TRADING_AUTHORITY_DIR > **未配置(undefined)**。
92
+ *
93
+ * 这里**没有**默认位置:旧的缺省 $DSH_HOME/authority 落在 agent 自己 uid 可写的 home
94
+ * 里,等于把「谁能授予实盘」交给 agent(验收发现 #1)。要授权就必须显式把平面指到
95
+ * 一个不属于 agent uid 的目录。
96
+ */
97
+ function resolveAuthorityLocation(options = {}) {
98
+ if (options.dir !== void 0 && options.dir.trim().length > 0) return {
99
+ dir: options.dir,
100
+ source: "option"
101
+ };
102
+ const explicit = (options.env ?? process.env)[AUTHORITY_DIR_ENV];
103
+ if (explicit !== void 0 && explicit.trim().length > 0) return {
104
+ dir: explicit,
105
+ source: "env"
106
+ };
107
+ return {
108
+ dir: void 0,
109
+ source: "unset"
110
+ };
111
+ }
112
+ /** 便捷形式:等价 resolveAuthorityLocation(options).dir(未配置 ⇒ undefined)。 */
113
+ function authorityDir(options = {}) {
114
+ return resolveAuthorityLocation(options).dir;
115
+ }
116
+ /** 解析真实路径:目录不存在时,用「最深已存在祖先的 realpath + 剩余段」继续走祖先链。 */
117
+ function resolveRealPath(path) {
118
+ const missing = [];
119
+ let cursor = resolve(path);
120
+ for (;;) try {
121
+ const real = realpathSync(cursor);
122
+ return missing.length === 0 ? real : join(real, ...missing.reverse());
123
+ } catch {
124
+ const parent = dirname(cursor);
125
+ if (parent === cursor) return resolve(path);
126
+ missing.push(basename(cursor));
127
+ cursor = parent;
128
+ }
129
+ }
130
+ function statOrUndefined(path) {
131
+ try {
132
+ return statSync(path);
133
+ } catch {
134
+ return;
135
+ }
136
+ }
137
+ /**
138
+ * 平面隔离检查:平面及其祖先链是否落在「运行 uid(= agent uid)可写」的位置上。
139
+ *
140
+ * 检查项逐条见 PlaneIsolationCode;任何一条不过都返回 isolated=false 与机器可读 code。
141
+ * 这是唯一**agent 无法伪造**的一层:它写得进文件,但改不了属主,也没法把自己的 uid
142
+ * 从祖先链里去掉。
143
+ */
144
+ function inspectPlaneIsolation(dir, options = {}) {
145
+ const env = options.env ?? process.env;
146
+ if (devOptIn(env)) return {
147
+ isolated: true,
148
+ code: "dev-opt-in",
149
+ devMode: true,
150
+ detail: "开发形态:显式设置了 " + AUTHORITY_DEV_ENV + "=1,跳过属主/权限检查 —— 这条判定不构成生产授权(每次判定都留痕)"
151
+ };
152
+ const euid = options.uidSemantics ?? hasUidSemantics() ? options.euid ?? processEuid() : void 0;
153
+ if (euid === void 0) return {
154
+ isolated: false,
155
+ code: "no-uid-semantics",
156
+ devMode: false,
157
+ detail: "本平台没有 uid 语义(process.geteuid 不可用:fs.Stats.uid 恒为 0、mode 按只读属性合成),无法证明平面不归 agent 所有 ⇒ 按未隔离拒绝(开发形态请显式设置 " + AUTHORITY_DEV_ENV + "=1)"
158
+ };
159
+ const pinned = parseUid(env[AUTHORITY_OWNER_UID_ENV]);
160
+ if (pinned.error !== void 0) return {
161
+ isolated: false,
162
+ code: "owner-not-allowed",
163
+ devMode: false,
164
+ detail: AUTHORITY_OWNER_UID_ENV + " " + pinned.error
165
+ };
166
+ if (pinned.uid !== void 0 && pinned.uid === euid) return {
167
+ isolated: false,
168
+ code: "configured-owner-is-agent-uid",
169
+ devMode: false,
170
+ detail: AUTHORITY_OWNER_UID_ENV + "=" + pinned.uid + " 就是运行 uid 自身 —— 允许属主不能是 agent(那等于没有边界)"
171
+ };
172
+ const real = resolveRealPath(dir);
173
+ const ancestors = [];
174
+ for (let cursor = dirname(real);; cursor = dirname(cursor)) {
175
+ ancestors.push(cursor);
176
+ if (dirname(cursor) === cursor) break;
177
+ }
178
+ for (const ancestor of ancestors) {
179
+ const stat = statOrUndefined(ancestor);
180
+ if (stat === void 0) continue;
181
+ if (stat.uid === euid) return {
182
+ isolated: false,
183
+ code: "ancestor-agent-writable",
184
+ devMode: false,
185
+ detail: "祖先目录 " + ancestor + " 归运行 uid " + euid + " 所有 —— agent 可以删掉/替换整个平面,授权平面必须放在它够不到的位置"
186
+ };
187
+ if ((stat.mode & 18) !== 0 && (stat.mode & 512) === 0) return {
188
+ isolated: false,
189
+ code: "ancestor-agent-writable",
190
+ devMode: false,
191
+ detail: "祖先目录 " + ancestor + " 对 group/other 可写且没有 sticky 位(mode " + (stat.mode & 4095).toString(8) + ")—— agent 可以替换平面里的条目"
192
+ };
193
+ }
194
+ const offenders = [];
195
+ const dirStat = statOrUndefined(real);
196
+ if (dirStat === void 0) return {
197
+ isolated: false,
198
+ code: "dir-missing",
199
+ devMode: false,
200
+ detail: "授权平面目录不存在:" + dir
201
+ };
202
+ offenders.push({
203
+ path: real,
204
+ stat: dirStat
205
+ });
206
+ for (const name of [TRUSTED_KEYS_FILENAME, GRANT_FILENAME]) {
207
+ const stat = statOrUndefined(join(real, name));
208
+ if (stat !== void 0) offenders.push({
209
+ path: join(real, name),
210
+ stat
211
+ });
212
+ }
213
+ for (const entry of offenders) {
214
+ if (entry.stat.uid === euid) return {
215
+ isolated: false,
216
+ code: "owner-is-agent-uid",
217
+ devMode: false,
218
+ detail: entry.path + " 归运行 uid " + euid + " 所有 —— 这正是「agent 自铸信任锚」的形态;平面必须由人在另一个 uid 下持有"
219
+ };
220
+ if (pinned.uid !== void 0 && entry.stat.uid !== pinned.uid) return {
221
+ isolated: false,
222
+ code: "owner-not-allowed",
223
+ devMode: false,
224
+ detail: entry.path + " 的属主 uid 是 " + entry.stat.uid + ",而 DSH_TRADING_AUTHORITY_OWNER_UID 要求 " + pinned.uid
225
+ };
226
+ if ((entry.stat.mode & 18) !== 0) return {
227
+ isolated: false,
228
+ code: "group-or-other-writable",
229
+ devMode: false,
230
+ detail: entry.path + " 带 group/other 写位(mode " + (entry.stat.mode & 4095).toString(8) + ")—— 别的 uid 能改它就不是权威"
231
+ };
232
+ }
233
+ return {
234
+ isolated: true,
235
+ code: "isolated",
236
+ devMode: false,
237
+ detail: "平面归 uid " + dirStat.uid + "(≠ 运行 uid " + euid + "),目录与文件无 group/other 写位,祖先链无 agent 可写目录"
238
+ };
239
+ }
240
+ /**
241
+ * 规范化序列化:对象键递归排序、无空白、undefined 丢弃。
242
+ * 签名覆盖的是这个字节串,**不是**文件字节 —— 否则换个格式化工具(缩进/键序)
243
+ * 就会让一份有效授权变成验签失败,或让「重排键序」变成一次静默的语义篡改。
244
+ */
245
+ function canonicalize(value) {
246
+ if (value === void 0) return "null";
247
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
248
+ if (Array.isArray(value)) return "[" + value.map((item) => canonicalize(item)).join(",") + "]";
249
+ const record = value;
250
+ return "{" + Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => JSON.stringify(key) + ":" + canonicalize(record[key])).join(",") + "}";
251
+ }
252
+ function isTrustedKey(value) {
253
+ if (value === null || typeof value !== "object") return false;
254
+ const key = value;
255
+ return typeof key.keyId === "string" && key.keyId.length > 0 && key.alg === "ed25519" && typeof key.publicKeyPem === "string" && key.publicKeyPem.includes("BEGIN PUBLIC KEY");
256
+ }
257
+ /** 解析受信任公钥目录文本;形状不对一律当「没有可用信任锚」。 */
258
+ function parseTrustedKeys(text) {
259
+ if (text === void 0) return [];
260
+ let doc;
261
+ try {
262
+ doc = JSON.parse(text);
263
+ } catch {
264
+ return [];
265
+ }
266
+ const keys = doc?.keys;
267
+ if (!Array.isArray(keys)) return [];
268
+ return keys.filter(isTrustedKey);
269
+ }
270
+ function isGrantPayload(value) {
271
+ if (value === null || typeof value !== "object") return false;
272
+ const payload = value;
273
+ return typeof payload.liveTrading === "boolean" && typeof payload.issuedAt === "string" && typeof payload.expiresAt === "string";
274
+ }
275
+ /**
276
+ * 纯验签:给定授权文档文本与信任锚文本,判定是否授予实盘。
277
+ * 不做任何 IO、不读环境、不抛异常 —— 全部失败路径都变成 { ok: false, reason }。
278
+ * 开发形态授权(payload.dev=true)缺省一律拒绝,只有显式 allowDevGrant 才接受。
279
+ */
280
+ function verifyGrantDocument(grantText, trustedKeysText, now = Date.now(), options = {}) {
281
+ const trustedKeys = parseTrustedKeys(trustedKeysText);
282
+ if (trustedKeys.length === 0) return {
283
+ ok: false,
284
+ reason: "no-trusted-keys",
285
+ detail: "受信任公钥目录缺失或为空(trusted-keys.json)——没有信任锚就没有授权"
286
+ };
287
+ if (grantText === void 0) return {
288
+ ok: false,
289
+ reason: "no-grant-document",
290
+ detail: "授权平面里没有实盘授权文档(live-trading.grant.json)"
291
+ };
292
+ let doc;
293
+ try {
294
+ doc = JSON.parse(grantText);
295
+ } catch (error) {
296
+ return {
297
+ ok: false,
298
+ reason: "malformed",
299
+ detail: "授权文档不是合法 JSON:" + String(error?.message ?? error)
300
+ };
301
+ }
302
+ const record = doc;
303
+ if (record === null || typeof record !== "object") return {
304
+ ok: false,
305
+ reason: "malformed",
306
+ detail: "授权文档顶层必须是对象"
307
+ };
308
+ if (record.protocolVersion !== 1) return {
309
+ ok: false,
310
+ reason: "malformed",
311
+ detail: "授权文档 protocolVersion 期望 1,实际 " + JSON.stringify(record.protocolVersion) + "(版本不匹配一律拒绝,不做兼容猜测)"
312
+ };
313
+ const signature = record.signature;
314
+ if (signature === void 0 || typeof signature.sig !== "string" || typeof signature.keyId !== "string" || signature.alg !== "ed25519") return {
315
+ ok: false,
316
+ reason: "malformed",
317
+ detail: "授权文档 signature 形状不合法(需要 alg=ed25519 / keyId / sig)"
318
+ };
319
+ if (!isGrantPayload(record.payload)) return {
320
+ ok: false,
321
+ reason: "malformed",
322
+ detail: "授权文档 payload 形状不合法(需要 liveTrading / issuedAt / expiresAt)"
323
+ };
324
+ const payload = record.payload;
325
+ const trusted = trustedKeys.find((key) => key.keyId === signature.keyId);
326
+ if (trusted === void 0) return {
327
+ ok: false,
328
+ reason: "unknown-key",
329
+ detail: "签发密钥 " + signature.keyId + " 不在受信任公钥目录里"
330
+ };
331
+ let signatureOk = false;
332
+ try {
333
+ signatureOk = verify(null, Buffer.from(canonicalize(payload), "utf8"), createPublicKey(trusted.publicKeyPem), Buffer.from(signature.sig, "base64"));
334
+ } catch (error) {
335
+ return {
336
+ ok: false,
337
+ reason: "bad-signature",
338
+ detail: "验签过程失败(公钥或签名不是合法 Ed25519 材料):" + String(error?.message ?? error)
339
+ };
340
+ }
341
+ if (!signatureOk) return {
342
+ ok: false,
343
+ reason: "bad-signature",
344
+ detail: "Ed25519 验签不通过(payload 被改过,或不是这把私钥签的)",
345
+ keyId: trusted.keyId
346
+ };
347
+ if (payload.liveTrading !== true) return {
348
+ ok: false,
349
+ reason: "not-a-grant",
350
+ detail: "授权文档 payload.liveTrading 不是 true —— 这份签名不构成实盘授权",
351
+ payload,
352
+ keyId: trusted.keyId
353
+ };
354
+ if (payload.dev === true && options.allowDevGrant !== true) return {
355
+ ok: false,
356
+ reason: "dev-grant-not-accepted",
357
+ detail: "这份授权是开发形态(payload.dev=true)签出的:它只对同样显式设置 " + AUTHORITY_DEV_ENV + "=1 的读取端生效。生产形态请在另一个 uid 下重新签署(dev 标记在签名覆盖范围内,抹掉它会让签名失效)",
358
+ payload,
359
+ keyId: trusted.keyId
360
+ };
361
+ const issuedAt = Date.parse(payload.issuedAt);
362
+ const expiresAt = Date.parse(payload.expiresAt);
363
+ if (Number.isNaN(issuedAt) || Number.isNaN(expiresAt)) return {
364
+ ok: false,
365
+ reason: "malformed",
366
+ detail: "issuedAt / expiresAt 不是可解析的时间戳",
367
+ payload,
368
+ keyId: trusted.keyId
369
+ };
370
+ if (now < issuedAt) return {
371
+ ok: false,
372
+ reason: "not-yet-valid",
373
+ detail: "授权尚未生效(issuedAt " + payload.issuedAt + ")",
374
+ payload,
375
+ keyId: trusted.keyId
376
+ };
377
+ if (now >= expiresAt) return {
378
+ ok: false,
379
+ reason: "expired",
380
+ detail: "授权已过期(expiresAt " + payload.expiresAt + ")—— 续签 = 重新签名,不做静默延期",
381
+ payload,
382
+ keyId: trusted.keyId
383
+ };
384
+ return {
385
+ ok: true,
386
+ reason: "granted",
387
+ detail: "人工签署的实盘授权有效(签署人 " + (payload.operator ?? "未署名") + ",到期 " + payload.expiresAt + ")",
388
+ payload,
389
+ keyId: trusted.keyId
390
+ };
391
+ }
392
+ function readTextOrUndefined(file) {
393
+ try {
394
+ return readFileSync(file, "utf8");
395
+ } catch {
396
+ return;
397
+ }
398
+ }
399
+ /** 指纹含属主与权限位:光看 mtime/size 会让一次 chown/chmod 逃过缓存。 */
400
+ function stampOf(file) {
401
+ try {
402
+ const stat = statSync(file);
403
+ return stat.uid + ":" + stat.mode + ":" + stat.mtimeMs + ":" + stat.size;
404
+ } catch {
405
+ return "missing";
406
+ }
407
+ }
408
+ const cache = /* @__PURE__ */ new Map();
409
+ function planeKeyOf(euid, env, semantics = true) {
410
+ return String(semantics) + "|" + String(euid) + "|" + String(env["DSH_TRADING_AUTHORITY_OWNER_UID"] ?? "") + "|" + String(devOptIn(env));
411
+ }
412
+ /**
413
+ * 判定一个平面目录:先做隔离检查,再验签(两者都按 stat 指纹缓存,文件/属主一变立刻重算)。
414
+ * 目录不存在时隔离检查返回 dir-missing,验签照旧走到「没有信任锚」——该结论在有 uid 语义的
415
+ * 平台上成立;无 uid 语义的平台(Windows)在能力门就先返回 no-uid-semantics。
416
+ */
417
+ function inspectPlane(dir, options = {}) {
418
+ const env = options.env ?? process.env;
419
+ const now = options.now ?? Date.now();
420
+ const grantFile = join(dir, GRANT_FILENAME);
421
+ const keysFile = join(dir, TRUSTED_KEYS_FILENAME);
422
+ const grantStamp = stampOf(grantFile);
423
+ const keysStamp = stampOf(keysFile);
424
+ const dirStamp = stampOf(dir);
425
+ const semantics = options.uidSemantics ?? hasUidSemantics();
426
+ const planeKey = planeKeyOf(options.euid ?? processEuid(), env, semantics);
427
+ const cached = cache.get(dir);
428
+ if (cached !== void 0 && cached.grantStamp === grantStamp && cached.keysStamp === keysStamp && cached.dirStamp === dirStamp && cached.planeKey === planeKey) {
429
+ const payload = cached.verification.payload;
430
+ if (cached.verification.reason !== "granted" || payload === void 0 || now >= Date.parse(payload.issuedAt) && now < Date.parse(payload.expiresAt)) return {
431
+ isolation: cached.isolation,
432
+ verification: cached.verification
433
+ };
434
+ }
435
+ const isolation = inspectPlaneIsolation(dir, {
436
+ ...options,
437
+ env
438
+ });
439
+ const verification = verifyGrantDocument(readTextOrUndefined(grantFile), readTextOrUndefined(keysFile), now, { allowDevGrant: devOptIn(env) });
440
+ cache.set(dir, {
441
+ grantStamp,
442
+ keysStamp,
443
+ dirStamp,
444
+ planeKey,
445
+ isolation,
446
+ verification
447
+ });
448
+ return {
449
+ isolation,
450
+ verification
451
+ };
452
+ }
453
+ /** 判定平面当前状态(按 stat 指纹缓存,文件一变立刻重算)。dev 形态授权缺省被拒。 */
454
+ function verifyAtDirectory(dir, now = Date.now()) {
455
+ return inspectPlane(dir, { now }).verification;
456
+ }
457
+ let warningSink = (message) => {
458
+ process.emitWarning(message, { code: "DSH_TRADING_LIVE_AUTHORITY_MISMATCH" });
459
+ };
460
+ /** 替换告警出口(测试记录用;生产缺省走 process.emitWarning)。 */
461
+ function setAuthorityMismatchSink(sink) {
462
+ warningSink = sink ?? ((message) => process.emitWarning(message, { code: "DSH_TRADING_LIVE_AUTHORITY_MISMATCH" }));
463
+ }
464
+ const reportedMismatch = /* @__PURE__ */ new Set();
465
+ /** 同一个 (目录, 原因) 只吼一次,避免热路径把 stderr 刷满。 */
466
+ function reportMismatch(dir, mirror, verification) {
467
+ const key = dir + "|" + verification.reason;
468
+ if (reportedMismatch.has(key)) return;
469
+ reportedMismatch.add(key);
470
+ warningSink("[dsh-trading] 实盘镜像与授权平面不一致:preset 资产里 liveTrading=" + String(mirror) + ",但 " + dir + " 未授予实盘(" + verification.reason + ":" + verification.detail + ")。授权平面是唯一授予者,镜像不能授予 —— 资产写 true 只会产生这条告警,不会打开实盘。");
471
+ }
472
+ const reportedDev = /* @__PURE__ */ new Set();
473
+ /** 开发形态每次判定留痕(同一个目录只吼一次,避免热路径刷屏)。 */
474
+ function reportDevMode(dir, isolation) {
475
+ if (reportedDev.has(dir)) return;
476
+ reportedDev.add(dir);
477
+ warningSink("[dsh-trading][DEV] 实盘授权平面在开发形态下判定:" + dir + " —— " + isolation.detail + "。生产形态必须把平面放在不属于 agent uid 的目录下(DSH_TRADING_AUTHORITY_DIR + 属主隔离)。");
478
+ }
479
+ /**
480
+ * 实盘判定。`mirror` 是 preset 资产/插件 config 里的 liveTrading 值。
481
+ *
482
+ * 合取语义(只收紧):allowed = mirror===true && 授权平面授予。
483
+ * 镜像为 false 一律拒绝;镜像为 true 但平面未授予 ⇒ 拒绝 + 一次可见告警(mismatch)。
484
+ * 平面未配置 / 未与 agent uid 隔离 ⇒ 直接拒绝,**不读任何文件**(没有默认位置可读)。
485
+ */
486
+ function liveTradingDecision(mirror, options = {}) {
487
+ const env = options.env ?? process.env;
488
+ const location = resolveAuthorityLocation(options);
489
+ const mirrorValue = mirror === true;
490
+ const dev = devOptIn(env);
491
+ let isolation;
492
+ let verification;
493
+ if (location.dir === void 0) verification = {
494
+ ok: false,
495
+ reason: "dir-not-configured",
496
+ detail: "授权平面目录未显式配置(DSH_TRADING_AUTHORITY_DIR 或 dir 选项)—— 本模块没有默认位置:缺省落在 $DSH_HOME/authority 等于把权威放进 agent 自己 uid 可写的目录(验收发现 #1)。请把平面指向一个不属于 agent uid 的目录。"
497
+ };
498
+ else {
499
+ const inspection = inspectPlane(location.dir, {
500
+ ...options,
501
+ env
502
+ });
503
+ isolation = inspection.isolation;
504
+ verification = inspection.verification;
505
+ if (!inspection.isolation.isolated && inspection.isolation.code !== "dir-missing") verification = {
506
+ ok: false,
507
+ reason: "plane-not-isolated",
508
+ detail: "授权平面未与 agent uid 隔离(" + inspection.isolation.code + "):" + inspection.isolation.detail + "。签名机制只解决「谁签的」,属主隔离才解决「平面归谁」—— 自铸的信任锚在这一步被挡下。"
509
+ };
510
+ }
511
+ if (dev && location.dir !== void 0 && isolation !== void 0) reportDevMode(location.dir, isolation);
512
+ const mismatch = mirrorValue && !verification.ok;
513
+ if (mismatch && location.dir !== void 0) reportMismatch(location.dir, mirrorValue, verification);
514
+ return {
515
+ ...verification,
516
+ granted: verification.ok,
517
+ allowed: mirrorValue && verification.ok,
518
+ mirror: mirrorValue,
519
+ mismatch,
520
+ dir: location.dir,
521
+ dirSource: location.source,
522
+ devMode: dev,
523
+ isolation
524
+ };
525
+ }
526
+ /** 判定便捷形式:只看放行/不放行。 */
527
+ function liveTradingEnabled(mirror, options = {}) {
528
+ return liveTradingDecision(mirror, options).allowed;
529
+ }
530
+ /** 清空 stat 指纹缓存(测试与「删掉平面后立刻复验」的场景用)。 */
531
+ function resetAuthorityCache() {
532
+ cache.clear();
533
+ reportedMismatch.clear();
534
+ reportedDev.clear();
535
+ }
536
+ //#endregion
537
+ export { AUTHORITY_AGENT_UID_ENV, AUTHORITY_DEV_ENV, AUTHORITY_DIR_ENV, AUTHORITY_OWNER_UID_ENV, GRANT_FILENAME, GRANT_PROTOCOL_VERSION, TRUSTED_KEYS_FILENAME, authorityDir, canonicalize, devOptIn, hasUidSemantics, inspectPlane, inspectPlaneIsolation, liveTradingDecision, liveTradingEnabled, parseTrustedKeys, processEuid, resetAuthorityCache, resolveAuthorityLocation, setAuthorityMismatchSink, verifyAtDirectory, verifyGrantDocument };
package/lib/sign.d.ts ADDED
@@ -0,0 +1,106 @@
1
+ import { AUTHORITY_AGENT_UID_ENV, AUTHORITY_DEV_ENV, AUTHORITY_OWNER_UID_ENV, GrantPayload, TrustedKey } from "./index.js";
2
+ //#region src/sign.d.ts
3
+ interface OperatorKeyPair {
4
+ keyId: string;
5
+ publicKeyPem: string;
6
+ privateKeyPem: string;
7
+ }
8
+ /** 生成一把运营方(人类)Ed25519 密钥对。默认 keyId = operator-1。 */
9
+ declare function generateOperatorKeyPair(keyId?: string): OperatorKeyPair;
10
+ /** 受信任公钥目录文档(人工把它写进授权平面;本包运行期只读它)。 */
11
+ declare function buildTrustedKeysDocument(keys: TrustedKey[]): string;
12
+ /**
13
+ * 签署一份实盘授权。签名覆盖 canonicalize(payload)(键序无关、格式无关),
14
+ * 所以重排键序不改变语义、改动任何一个字段(含 dev 标记)都会验签失败。
15
+ */
16
+ declare function signLiveTradingGrant(payload: GrantPayload, privateKeyPem: string, keyId: string): string;
17
+ /** 到期时刻 = 签发时刻 + 天数(设计文档 §12.3 默认 30 天)。 */
18
+ declare function expiryFromDays(issuedAtMs: number, days: number): string;
19
+ /** 运营侧拒绝原因(机器可读;CLI 把 code 原样打出来)。 */
20
+ type OperatorDenyCode = 'agent-uid-required' | 'agent-uid-invalid' | 'agent-uid-equals-operator' | 'no-uid-semantics' | 'key-exists' | 'key-missing' | 'trust-anchor-exists' | 'trust-anchor-missing' | 'plane-dir-missing' | 'plane-not-isolated' | 'key-not-trusted';
21
+ declare class OperatorGuardError extends Error {
22
+ readonly code: OperatorDenyCode;
23
+ constructor(code: OperatorDenyCode, message: string);
24
+ }
25
+ interface OperatorContext {
26
+ /** 显式声明 trading agent 运行在哪个 uid 下(--agent-uid 或 $DSH_TRADING_AUTHORITY_AGENT_UID)。 */
27
+ agentUid?: number;
28
+ /** 显式开发形态(--force-dev,或 $DSH_TRADING_AUTHORITY_DEV_SAME_UID=1)。 */
29
+ forceDev?: boolean;
30
+ /** 环境变量映射,缺省 process.env。 */
31
+ env?: Record<string, string | undefined>;
32
+ /** 覆盖运行 uid(测试用来模拟「人在另一个 uid 下操作」)。 */
33
+ euid?: number;
34
+ }
35
+ interface OperatorIdentity {
36
+ /** 运行本命令的 uid;无 uid 语义的平台上是 undefined。 */
37
+ euid: number | undefined;
38
+ /** 声明的 agent uid;显式 dev 形态下允许缺席。 */
39
+ agentUid: number | undefined;
40
+ devMode: boolean;
41
+ detail: string;
42
+ }
43
+ /**
44
+ * 解析运营侧身份并**先拒绝自铸形态**:不声明 agent uid 就没有「这不是 agent 自己写的」
45
+ * 这句话;声明成自己就是自铸。两条都过不去时只有显式 dev 形态能继续。
46
+ */
47
+ declare function resolveOperatorIdentity(context?: OperatorContext): OperatorIdentity;
48
+ interface InitTrustAnchorRequest extends OperatorContext {
49
+ /** 授权平面目录(必须显式给出;本工具没有默认位置)。 */
50
+ dir: string;
51
+ /** 私钥落盘路径。 */
52
+ keyFile: string;
53
+ /** 密钥标识,缺省 operator-1。 */
54
+ keyId?: string;
55
+ /** 是否把 trusted-keys.json 写进平面;false = 只回内容给人手写(缺省 true)。 */
56
+ writeTrust?: boolean;
57
+ }
58
+ interface InitTrustAnchorResult {
59
+ keyId: string;
60
+ keyFile: string;
61
+ trustFile: string;
62
+ publicKeyPem: string;
63
+ /** 信任锚是否由本次调用写入(false = 调用方自己写)。 */
64
+ trustWritten: boolean;
65
+ devMode: boolean;
66
+ detail: string;
67
+ }
68
+ /**
69
+ * 建一对运营密钥并把信任锚写进平面。守卫见文件头:不声明 agent uid ⇒ 拒绝;声明的
70
+ * agent uid = 当前 uid ⇒ 拒绝(除非 --force-dev);信任锚已存在 ⇒ 拒绝(除非 --force-dev)。
71
+ */
72
+ declare function initTrustAnchor(request: InitTrustAnchorRequest): InitTrustAnchorResult;
73
+ interface SignGrantRequest extends OperatorContext {
74
+ /** 授权平面目录(必须显式给出;本工具没有默认位置)。 */
75
+ dir: string;
76
+ /** 运营私钥路径。 */
77
+ keyFile: string;
78
+ keyId?: string;
79
+ days?: number;
80
+ operator?: string;
81
+ note?: string;
82
+ /** 判定时刻(毫秒),缺省 Date.now()。 */
83
+ now?: number;
84
+ }
85
+ interface SignGrantResult {
86
+ grantFile: string;
87
+ payload: GrantPayload;
88
+ keyId: string;
89
+ devMode: boolean;
90
+ detail: string;
91
+ }
92
+ /**
93
+ * 把一份带到期的实盘授权签进**已存在**的平面。
94
+ *
95
+ * 守卫:平面目录必须已存在(不在 agent 可写路径上先建好)、信任锚必须已存在、私钥的
96
+ * 公钥半边必须在锚里登记;不声明 agent uid 或声明成自己一律拒绝(除非 --force-dev)。
97
+ */
98
+ declare function signGrantIntoPlane(request: SignGrantRequest): SignGrantResult;
99
+ /** 平面里两份文件的现状(status 打印用;只读)。 */
100
+ declare function describePlaneFiles(dir: string): Array<{
101
+ name: string;
102
+ path: string;
103
+ present: boolean;
104
+ }>;
105
+ //#endregion
106
+ export { AUTHORITY_AGENT_UID_ENV, AUTHORITY_DEV_ENV, AUTHORITY_OWNER_UID_ENV, InitTrustAnchorRequest, InitTrustAnchorResult, OperatorContext, OperatorDenyCode, OperatorGuardError, OperatorIdentity, OperatorKeyPair, SignGrantRequest, SignGrantResult, buildTrustedKeysDocument, describePlaneFiles, expiryFromDays, generateOperatorKeyPair, initTrustAnchor, resolveOperatorIdentity, signGrantIntoPlane, signLiveTradingGrant };
package/lib/sign.js ADDED
@@ -0,0 +1,233 @@
1
+ import { AUTHORITY_AGENT_UID_ENV, AUTHORITY_DEV_ENV, AUTHORITY_OWNER_UID_ENV, GRANT_FILENAME, TRUSTED_KEYS_FILENAME, canonicalize, devOptIn, inspectPlaneIsolation, parseTrustedKeys, processEuid } from "./index.js";
2
+ import { createPrivateKey, createPublicKey, generateKeyPairSync, sign } from "node:crypto";
3
+ import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ //#region src/sign.ts
6
+ /**
7
+ * 授权平面的**签署侧**:只给运营 CLI 与测试用。
8
+ *
9
+ * 运行期(连接器、宿主、未来的 tradectl 判定路径)**不得** import 本模块 —— 见
10
+ * test/live-trading-authority.test.ts 的依赖方向断言。把它留在运行期代码路径之外,
11
+ * 「谁能签发」就不依赖运行期纪律,而依赖模块图。
12
+ *
13
+ * 这里同时是**运营侧守卫**的家(验收发现 #1:同 uid 可以自己 init 一对密钥、自己写
14
+ * trusted-keys.json、自己 sign 一份 grant,然后 status 输出放行):
15
+ *
16
+ * - 写平面的人必须**声明 agent 跑在哪个 uid 下**(--agent-uid / $DSH_TRADING_AUTHORITY_AGENT_UID)。
17
+ * 声明不了就不写 —— 否则「这个平面不是 agent 自己写的」无法成立。
18
+ * - 声明的 agent uid **就是当前进程 uid** ⇒ 拒绝(agent-uid-equals-operator):这正是
19
+ * 自铸形态。只有在**显式开发形态**(--force-dev,名字里带 dev)下才放行,且签出的
20
+ * 授权文档会带 payload.dev=true —— 读取端没有同一个 opt-in 时拒绝它。
21
+ * - `init` 拒绝覆盖已存在的 trusted-keys.json(trust-anchor-exists),除非显式 --force-dev。
22
+ * - `sign` 要求信任锚已存在、且签名私钥的公钥半边确实登记在锚里(key-not-trusted),
23
+ * 并且拒绝写一个「归声明的 agent uid 所有」的平面(plane-not-isolated)。
24
+ *
25
+ * 注意:私钥不出现在本包的运行期导出里;CLI 从磁盘读私钥文件(由人在另一个 uid 下
26
+ * 持有,0600)。运营侧守卫是纵深防御,不是安全边界本身 —— 边界在读取端的属主检查
27
+ * (见 ./index.ts 的 inspectPlaneIsolation):agent 写得进文件,改不了文件的属主。
28
+ */
29
+ /** 生成一把运营方(人类)Ed25519 密钥对。默认 keyId = operator-1。 */
30
+ function generateOperatorKeyPair(keyId = "operator-1") {
31
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
32
+ return {
33
+ keyId,
34
+ publicKeyPem: publicKey.export({
35
+ type: "spki",
36
+ format: "pem"
37
+ }).toString(),
38
+ privateKeyPem: privateKey.export({
39
+ type: "pkcs8",
40
+ format: "pem"
41
+ }).toString()
42
+ };
43
+ }
44
+ /** 受信任公钥目录文档(人工把它写进授权平面;本包运行期只读它)。 */
45
+ function buildTrustedKeysDocument(keys) {
46
+ return JSON.stringify({
47
+ version: 1,
48
+ keys
49
+ }, null, 2) + "\n";
50
+ }
51
+ /**
52
+ * 签署一份实盘授权。签名覆盖 canonicalize(payload)(键序无关、格式无关),
53
+ * 所以重排键序不改变语义、改动任何一个字段(含 dev 标记)都会验签失败。
54
+ */
55
+ function signLiveTradingGrant(payload, privateKeyPem, keyId) {
56
+ const signature = sign(null, Buffer.from(canonicalize(payload), "utf8"), createPrivateKey(privateKeyPem)).toString("base64");
57
+ return JSON.stringify({
58
+ protocolVersion: 1,
59
+ payload,
60
+ signature: {
61
+ alg: "ed25519",
62
+ keyId,
63
+ sig: signature
64
+ }
65
+ }, null, 2) + "\n";
66
+ }
67
+ /** 到期时刻 = 签发时刻 + 天数(设计文档 §12.3 默认 30 天)。 */
68
+ function expiryFromDays(issuedAtMs, days) {
69
+ if (!Number.isFinite(days) || days <= 0) throw new Error("days 必须是正数");
70
+ return new Date(issuedAtMs + days * 24 * 60 * 60 * 1e3).toISOString();
71
+ }
72
+ var OperatorGuardError = class extends Error {
73
+ code;
74
+ constructor(code, message) {
75
+ super(message);
76
+ this.name = "OperatorGuardError";
77
+ this.code = code;
78
+ }
79
+ };
80
+ function parseAgentUid(context, env) {
81
+ if (context.agentUid !== void 0) {
82
+ if (!Number.isInteger(context.agentUid) || context.agentUid < 0) throw new OperatorGuardError("agent-uid-invalid", "--agent-uid 必须是非负整数,实际 " + JSON.stringify(context.agentUid));
83
+ return context.agentUid;
84
+ }
85
+ const raw = env[AUTHORITY_AGENT_UID_ENV];
86
+ if (raw === void 0 || raw.trim().length === 0) return void 0;
87
+ const uid = Number(raw.trim());
88
+ if (!Number.isInteger(uid) || uid < 0) throw new OperatorGuardError("agent-uid-invalid", AUTHORITY_AGENT_UID_ENV + " 必须是非负整数,实际 " + JSON.stringify(raw));
89
+ return uid;
90
+ }
91
+ /**
92
+ * 解析运营侧身份并**先拒绝自铸形态**:不声明 agent uid 就没有「这不是 agent 自己写的」
93
+ * 这句话;声明成自己就是自铸。两条都过不去时只有显式 dev 形态能继续。
94
+ */
95
+ function resolveOperatorIdentity(context = {}) {
96
+ const env = context.env ?? process.env;
97
+ const devMode = context.forceDev === true || devOptIn(env);
98
+ const euid = context.euid ?? processEuid();
99
+ const agentUid = parseAgentUid(context, env);
100
+ if (devMode) return {
101
+ euid,
102
+ agentUid,
103
+ devMode: true,
104
+ detail: "开发形态(" + (context.forceDev === true ? "--force-dev" : AUTHORITY_DEV_ENV + "=1") + "):允许在 agent uid 下写平面,签出的授权带 payload.dev=true,读取端没有同一个 opt-in 时拒绝它"
105
+ };
106
+ if (agentUid === void 0) throw new OperatorGuardError("agent-uid-required", "必须声明 trading agent 的 uid(--agent-uid <uid> 或 " + AUTHORITY_AGENT_UID_ENV + "=<uid>):不声明就无法证明这个平面不是 agent 自己写的。只有在显式开发形态(--force-dev)下才允许省略。");
107
+ if (euid === void 0) throw new OperatorGuardError("no-uid-semantics", "本平台没有 uid 语义(process.geteuid 不可用),无法证明「写平面的不是 agent」⇒ 拒绝。开发形态请显式 --force-dev。");
108
+ if (euid === agentUid) throw new OperatorGuardError("agent-uid-equals-operator", "当前进程 uid " + euid + " 就是声明的 agent uid —— 这正是「agent 自铸信任锚」的形态:自己生成密钥、自己写 trusted-keys.json、自己签 grant。生产形态请在另一个 uid 下运行本命令(或由人工把私钥与信任锚带过去);显式开发形态请用 --force-dev,它会在授权文档里留下 dev 标记。");
109
+ return {
110
+ euid,
111
+ agentUid,
112
+ devMode: false,
113
+ detail: "运行 uid " + euid + " ≠ 声明的 agent uid " + agentUid + ":平面不归 agent 所有"
114
+ };
115
+ }
116
+ /** 从读取端(agent uid)角度看,这个目录是否可写 —— 可写就不许写信任锚/授权。 */
117
+ function assertWriterPlaneIsolated(dir, identity, context) {
118
+ if (identity.devMode || identity.agentUid === void 0) return;
119
+ const env = { ...context.env ?? process.env };
120
+ delete env[AUTHORITY_DEV_ENV];
121
+ const report = inspectPlaneIsolation(dir, {
122
+ env,
123
+ euid: identity.agentUid
124
+ });
125
+ if (!report.isolated && report.code !== "dir-missing") throw new OperatorGuardError("plane-not-isolated", "平面目录 " + dir + " 对 agent uid " + identity.agentUid + " 可写(" + report.code + "):" + report.detail + "。换到 agent 够不到的位置(先由人在该 uid 下建目录),或用 --force-dev 显式声明开发形态。");
126
+ }
127
+ function publicKeyPemOf(privateKeyPem) {
128
+ try {
129
+ return createPublicKey(createPrivateKey(privateKeyPem)).export({
130
+ type: "spki",
131
+ format: "pem"
132
+ }).toString();
133
+ } catch {
134
+ return;
135
+ }
136
+ }
137
+ /**
138
+ * 建一对运营密钥并把信任锚写进平面。守卫见文件头:不声明 agent uid ⇒ 拒绝;声明的
139
+ * agent uid = 当前 uid ⇒ 拒绝(除非 --force-dev);信任锚已存在 ⇒ 拒绝(除非 --force-dev)。
140
+ */
141
+ function initTrustAnchor(request) {
142
+ const identity = resolveOperatorIdentity(request);
143
+ const keyId = request.keyId ?? "operator-1";
144
+ const trustFile = join(request.dir, TRUSTED_KEYS_FILENAME);
145
+ if (existsSync(request.keyFile)) throw new OperatorGuardError("key-exists", "私钥文件已存在,拒绝覆盖:" + request.keyFile);
146
+ if (existsSync(trustFile) && !identity.devMode) throw new OperatorGuardError("trust-anchor-exists", "信任锚已存在,拒绝覆盖:" + trustFile + " —— 覆盖信任锚 = 换掉「谁说了算」,正是 agent 自铸的形态。要重建请先由人工移除该文件;显式开发形态请用 --force-dev。");
147
+ if (existsSync(request.dir)) assertWriterPlaneIsolated(request.dir, identity, request);
148
+ const pair = generateOperatorKeyPair(keyId);
149
+ mkdirSync(request.dir, {
150
+ recursive: true,
151
+ mode: 493
152
+ });
153
+ writeFileSync(request.keyFile, pair.privateKeyPem, { mode: 384 });
154
+ chmodSync(request.keyFile, 384);
155
+ const keysDoc = buildTrustedKeysDocument([{
156
+ keyId,
157
+ alg: "ed25519",
158
+ publicKeyPem: pair.publicKeyPem
159
+ }]);
160
+ const trustWritten = request.writeTrust !== false;
161
+ if (trustWritten) {
162
+ writeFileSync(trustFile, keysDoc, { mode: 420 });
163
+ chmodSync(trustFile, 420);
164
+ }
165
+ return {
166
+ keyId,
167
+ keyFile: request.keyFile,
168
+ trustFile,
169
+ publicKeyPem: pair.publicKeyPem,
170
+ trustWritten,
171
+ devMode: identity.devMode,
172
+ detail: identity.detail
173
+ };
174
+ }
175
+ /**
176
+ * 把一份带到期的实盘授权签进**已存在**的平面。
177
+ *
178
+ * 守卫:平面目录必须已存在(不在 agent 可写路径上先建好)、信任锚必须已存在、私钥的
179
+ * 公钥半边必须在锚里登记;不声明 agent uid 或声明成自己一律拒绝(除非 --force-dev)。
180
+ */
181
+ function signGrantIntoPlane(request) {
182
+ const identity = resolveOperatorIdentity(request);
183
+ const keyId = request.keyId ?? "operator-1";
184
+ if (!existsSync(request.dir)) throw new OperatorGuardError("plane-dir-missing", "授权平面目录不存在:" + request.dir + " —— sign 只写入已经存在的平面(先由人在允许属主下建目录并写信任锚)。");
185
+ assertWriterPlaneIsolated(request.dir, identity, request);
186
+ const trustFile = join(request.dir, TRUSTED_KEYS_FILENAME);
187
+ if (!existsSync(trustFile)) throw new OperatorGuardError("trust-anchor-missing", "平面里没有信任锚(" + trustFile + ")—— 先由人工在允许属主下建立信任锚;sign 不会顺手造一个(那正是自铸)。");
188
+ if (!existsSync(request.keyFile)) throw new OperatorGuardError("key-missing", "私钥文件不存在:" + request.keyFile);
189
+ const trusted = parseTrustedKeys(readFileSync(trustFile, "utf8")).find((key) => key.keyId === keyId);
190
+ if (trusted === void 0) throw new OperatorGuardError("key-not-trusted", "密钥 " + keyId + " 不在信任锚里(" + trustFile + ")—— 签出来也没人认;先用运营 CLI 的 init 登记公钥。");
191
+ const privateKeyPem = readFileSync(request.keyFile, "utf8");
192
+ const derived = publicKeyPemOf(privateKeyPem);
193
+ if (derived === void 0 || derived.trim() !== trusted.publicKeyPem.trim()) throw new OperatorGuardError("key-not-trusted", "私钥 " + request.keyFile + " 的公钥半边与信任锚里 " + keyId + " 登记的不是同一把 —— 拒绝签一份验不过的授权。");
194
+ const issuedAtMs = request.now ?? Date.now();
195
+ const days = request.days ?? 30;
196
+ const payload = {
197
+ liveTrading: true,
198
+ issuedAt: new Date(issuedAtMs).toISOString(),
199
+ expiresAt: expiryFromDays(issuedAtMs, days),
200
+ operator: request.operator ?? process.env.USER ?? "operator",
201
+ note: request.note ?? "",
202
+ ...identity.devMode ? { dev: true } : {}
203
+ };
204
+ const grantFile = join(request.dir, GRANT_FILENAME);
205
+ writeFileSync(grantFile, signLiveTradingGrant(payload, privateKeyPem, keyId), { mode: 420 });
206
+ chmodSync(grantFile, 420);
207
+ return {
208
+ grantFile,
209
+ payload,
210
+ keyId,
211
+ devMode: identity.devMode,
212
+ detail: identity.detail
213
+ };
214
+ }
215
+ /** 平面里两份文件的现状(status 打印用;只读)。 */
216
+ function describePlaneFiles(dir) {
217
+ return [TRUSTED_KEYS_FILENAME, GRANT_FILENAME].map((name) => {
218
+ const path = join(dir, name);
219
+ let present = false;
220
+ try {
221
+ present = statSync(path).isFile();
222
+ } catch {
223
+ present = false;
224
+ }
225
+ return {
226
+ name,
227
+ path,
228
+ present
229
+ };
230
+ });
231
+ }
232
+ //#endregion
233
+ export { AUTHORITY_AGENT_UID_ENV, AUTHORITY_DEV_ENV, AUTHORITY_OWNER_UID_ENV, OperatorGuardError, buildTrustedKeysDocument, describePlaneFiles, expiryFromDays, generateOperatorKeyPair, initTrustAnchor, resolveOperatorIdentity, signGrantIntoPlane, signLiveTradingGrant };
@@ -0,0 +1,40 @@
1
+ import { AuthorityOptions } from "./index.js";
2
+ //#region src/testing.d.ts
3
+ interface TestAuthority {
4
+ /** 临时授权平面目录(用完即删)。 */
5
+ dir: string;
6
+ keyId: string;
7
+ privateKeyPem: string;
8
+ publicKeyPem: string;
9
+ /** 夹具形态:true = 显式开发形态,false = 生产形态(无 dev opt-in)。 */
10
+ devMode: boolean;
11
+ /**
12
+ * 把这份平面当成**生产形态**判定的选项:注入一个与文件属主不同的 euid,等价于
13
+ * 「平面归人在的另一个 uid、判定跑在 agent uid 下」。dev 形态的夹具也可用它。
14
+ */
15
+ authorityOptions: AuthorityOptions;
16
+ /** 重签一份授权(换到期天数或签署人)。 */
17
+ sign(overrides?: {
18
+ days?: number;
19
+ operator?: string;
20
+ }): void;
21
+ /** 撤销:删掉授权文档、保留信任锚。 */
22
+ revoke(): void;
23
+ /** 还原环境变量、清缓存、删临时目录。 */
24
+ uninstall(): void;
25
+ }
26
+ interface TestAuthorityOptions {
27
+ /** 授权有效期(天),缺省 30。 */
28
+ days?: number;
29
+ /** 签署人标识,缺省 test-operator。 */
30
+ operator?: string;
31
+ /** 环境变量表,缺省 process.env(决定 DSH_TRADING_AUTHORITY_DIR 写在哪)。 */
32
+ env?: Record<string, string | undefined>;
33
+ /** 只放信任锚不签授权(覆盖「平面就绪但未授权」的形态)。 */
34
+ withoutGrant?: boolean;
35
+ /** 夹具形态,缺省 true(显式开发形态);false = 生产形态见文件头。 */
36
+ dev?: boolean;
37
+ }
38
+ declare function installTestAuthority(options?: TestAuthorityOptions): TestAuthority;
39
+ //#endregion
40
+ export { TestAuthority, TestAuthorityOptions, installTestAuthority };
package/lib/testing.js ADDED
@@ -0,0 +1,86 @@
1
+ import { AUTHORITY_DEV_ENV, AUTHORITY_DIR_ENV, GRANT_FILENAME, TRUSTED_KEYS_FILENAME, processEuid, resetAuthorityCache } from "./index.js";
2
+ import { buildTrustedKeysDocument, expiryFromDays, generateOperatorKeyPair, signLiveTradingGrant } from "./sign.js";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ //#region src/testing.ts
7
+ /**
8
+ * 授权平面的测试夹具(只经 ./testing 子路径导出,运行期入口不引用它)。
9
+ *
10
+ * 它替换的不是被测逻辑,而是「人已经签过授权」这个**前置条件**:在临时目录里搭一份
11
+ * 真实的 Ed25519 密钥、真实的 trusted-keys.json、真实的签名授权文档,并把
12
+ * DSH_TRADING_AUTHORITY_DIR 指向它。
13
+ *
14
+ * 为什么不是 mock:禁 mock 的棘轮与设计文档同向——判定逻辑必须读真实文件、验真实签名,
15
+ * 否则「镜像 true + 已授权 → 放行」这条路径就只能靠伪造判定来覆盖,等于没测。
16
+ *
17
+ * **两种形态(验收发现 #1 之后必须显式选)**:
18
+ * - dev: true(缺省)= 开发形态:同 uid 平面 + $DSH_TRADING_AUTHORITY_DEV_SAME_UID=1,
19
+ * 授权文档带 payload.dev=true。名字里带 dev、每次判定留痕(decision.devMode=true +
20
+ * 一次可见告警)。连接器用例走的就是这一形态——因为它们的判定调用不传选项。
21
+ * - dev: false = 生产形态:不设 dev opt-in、签名不带 dev 标记。本机同 uid 下制造不出
22
+ * 「平面归另一个 uid」的真实属主,所以要用 fixture.authorityOptions(注入 euid 模拟
23
+ * 生产形态的 uid 关系)去判定;直接 liveTradingEnabled(true) 会按 plane-not-isolated
24
+ * 拒绝 —— 那正是本机同 uid 的诚实结论。
25
+ */
26
+ function installTestAuthority(options = {}) {
27
+ const env = options.env ?? process.env;
28
+ const devMode = options.dev !== false;
29
+ const previousDir = env[AUTHORITY_DIR_ENV];
30
+ const previousDev = env[AUTHORITY_DEV_ENV];
31
+ const dir = mkdtempSync(join(tmpdir(), "dsh-authority-fixture-"));
32
+ const keyId = "operator-test";
33
+ const pair = generateOperatorKeyPair(keyId);
34
+ writeFileSync(join(dir, TRUSTED_KEYS_FILENAME), buildTrustedKeysDocument([{
35
+ keyId,
36
+ alg: "ed25519",
37
+ publicKeyPem: pair.publicKeyPem
38
+ }]));
39
+ const sign = (overrides = {}) => {
40
+ const now = Date.now();
41
+ writeFileSync(join(dir, GRANT_FILENAME), signLiveTradingGrant({
42
+ liveTrading: true,
43
+ issuedAt: (/* @__PURE__ */ new Date(now - 6e4)).toISOString(),
44
+ expiresAt: expiryFromDays(now, overrides.days ?? options.days ?? 30),
45
+ operator: overrides.operator ?? options.operator ?? "test-operator",
46
+ ...devMode ? { dev: true } : {}
47
+ }, pair.privateKeyPem, keyId));
48
+ resetAuthorityCache();
49
+ };
50
+ if (options.withoutGrant !== true) sign();
51
+ env[AUTHORITY_DIR_ENV] = dir;
52
+ if (devMode) env[AUTHORITY_DEV_ENV] = "1";
53
+ else delete env[AUTHORITY_DEV_ENV];
54
+ resetAuthorityCache();
55
+ const syntheticEuid = (processEuid() ?? 0) + 12345;
56
+ return {
57
+ dir,
58
+ keyId,
59
+ privateKeyPem: pair.privateKeyPem,
60
+ publicKeyPem: pair.publicKeyPem,
61
+ devMode,
62
+ authorityOptions: {
63
+ dir,
64
+ env: devMode ? { [AUTHORITY_DEV_ENV]: "1" } : {},
65
+ euid: syntheticEuid
66
+ },
67
+ sign,
68
+ revoke: () => {
69
+ rmSync(join(dir, GRANT_FILENAME), { force: true });
70
+ resetAuthorityCache();
71
+ },
72
+ uninstall: () => {
73
+ if (previousDir === void 0) delete env[AUTHORITY_DIR_ENV];
74
+ else env[AUTHORITY_DIR_ENV] = previousDir;
75
+ if (previousDev === void 0) delete env[AUTHORITY_DEV_ENV];
76
+ else env[AUTHORITY_DEV_ENV] = previousDev;
77
+ resetAuthorityCache();
78
+ rmSync(dir, {
79
+ recursive: true,
80
+ force: true
81
+ });
82
+ }
83
+ };
84
+ }
85
+ //#endregion
86
+ export { installTestAuthority };
package/package.json CHANGED
@@ -1,6 +1,41 @@
1
1
  {
2
2
  "name": "@dshtrading/authority",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Live-trading authority plane for dsh-trading: a fail-closed, human-signed (Ed25519) grant outside every agent-writable path. The preset asset's liveTrading flag is demoted to a non-authoritative mirror that can only deny (design doc §5 / invariant 13.3).",
4
+ "version": "0.6.1",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./sign": {
14
+ "types": "./lib/sign.d.ts",
15
+ "default": "./lib/sign.js"
16
+ },
17
+ "./testing": {
18
+ "types": "./lib/testing.d.ts",
19
+ "default": "./lib/testing.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "lib"
25
+ ],
26
+ "license": "PolyForm-Noncommercial-1.0.0",
27
+ "dependencies": {
28
+ "@dshtrading/dsh-home": "^0.6.1"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^24.0.0",
32
+ "tsdown": "^0.22.0",
33
+ "typescript": "^5.9.3",
34
+ "vitest": "^3.0.0",
35
+ "yaml": "^2.9.0"
36
+ },
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "test": "vitest run"
40
+ }
6
41
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.