@sema-agent/settings-schema 14.5.0 → 14.7.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.
- package/CHANGELOG.md +531 -0
- package/README.md +46 -3
- package/conformance/VERSION +1 -1
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
- package/dist/api/auth-bridge.d.ts +16 -2
- package/dist/api/auth-bridge.js +59 -24
- package/dist/api/credentials.d.ts +281 -0
- package/dist/api/credentials.js +149 -0
- package/dist/api/scopes.js +5 -1
- package/dist/atomic-write.js +10 -7
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/config-d-file.js +33 -15
- package/dist/config-dir-migration.js +2 -1
- package/dist/config-fns.js +142 -78
- package/dist/conformance/canonical.js +7 -10
- package/dist/conformance/contract-adapter.js +26 -1
- package/dist/conformance/corpus.js +5 -5
- package/dist/conformance/derive.js +6 -5
- package/dist/conformance/run.js +44 -6
- package/dist/cross-session.d.ts +4 -4
- package/dist/declared-capture.d.ts +41 -0
- package/dist/declared-capture.js +344 -0
- package/dist/file-edit.js +11 -15
- package/dist/file-store.d.ts +1 -31
- package/dist/file-store.js +3 -30
- package/dist/fleet.d.ts +11 -0
- package/dist/fleet.js +60 -26
- package/dist/fs-failure.d.ts +12 -7
- package/dist/fs-failure.js +53 -29
- package/dist/hash.d.ts +0 -9
- package/dist/hash.js +64 -7
- package/dist/hooks-config.d.ts +2 -2
- package/dist/hooks-config.js +129 -33
- package/dist/http-hook-allowlists.d.ts +27 -10
- package/dist/http-hook-allowlists.js +189 -25
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/dist/mcp-request-entry.d.ts +13 -8
- package/dist/mcp-request-entry.js +246 -107
- package/dist/one-time-migrations.js +207 -95
- package/dist/own-key-read.d.ts +247 -0
- package/dist/own-key-read.js +449 -0
- package/dist/plain-json-object.d.ts +38 -3
- package/dist/plain-json-object.js +84 -7
- package/dist/portable-domains.d.ts +31 -0
- package/dist/portable-domains.js +30 -0
- package/dist/remote-exec.js +9 -1
- package/dist/safety-merge-spec.d.ts +88 -6
- package/dist/safety-merge-spec.js +246 -17
- package/dist/sandbox-git.d.ts +64 -0
- package/dist/sandbox-git.js +89 -0
- package/dist/scheduler-store.js +21 -12
- package/dist/settings-units.d.ts +85 -0
- package/dist/settings-units.js +177 -0
- package/dist/types.d.ts +6 -2
- package/dist/types.js +65 -56
- package/dist/url-shape.d.ts +27 -5
- package/dist/url-shape.js +29 -5
- package/dist/web-admin-credential.js +5 -2
- package/dist/web-admin-session-url.js +2 -2
- package/dist/worker-row-verdict.d.ts +11 -0
- package/dist/worker-row-verdict.js +51 -1
- package/dist/workers-rows.d.ts +2 -1
- package/dist/workers-rows.js +41 -12
- package/dist/zod-unwrap.d.ts +6 -0
- package/dist/zod-unwrap.js +14 -7
- package/docs/API-SURFACE.md +50 -5
- package/docs/CONSUMER-INTEGRATION.md +178 -0
- package/llms-full.txt +34 -5
- package/llms.txt +6 -3
- package/package.json +1 -1
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `api/credentials` —— 凭据取值口(`POST /api/v1/credentials/handles` → `POST /api/v1/credentials/resolve`)里 **`sandbox_git`** 这一种凭据的 wire 型(14.7.0)。
|
|
3
|
+
* 一份 zod,发这些形的一方(web-admin)与收这些形的一方(引擎的数据面)同 import。纯、浏览器安全。
|
|
4
|
+
*
|
|
5
|
+
* 两步:① 带调用方凭据申领一枚短期、一次性的**句柄**(请求 {@link SandboxGitCredentialRequest},应答 {@link CredentialHandleAnswer});
|
|
6
|
+
* ② 凭句柄兑换(请求 {@link CredentialResolveRequest},应答 {@link SandboxGitCredentialAnswer};不合格则整次拒答 {@link SandboxGitCredentialRefusal})。
|
|
7
|
+
* 本模块只有**形与词表**:调用方鉴权、委托、活表复核、行 MAC、租约算术、续期状态机、「应答与请求逐条对应 / 回显相等」的核对都是各方自己的事,不在这里。
|
|
8
|
+
* 取值口的另两种凭据(`model` / `mcp_static`)的请求与应答形不在本模块;它们落地时与本模块的 `sandbox_git` 形并成按 `kind` 判别的联合,
|
|
9
|
+
* 本模块的形不动(`kind` 字面量已经在)。
|
|
10
|
+
*
|
|
11
|
+
* ## 三张词表,各一张,每个词只在本文件里拼一次
|
|
12
|
+
* · {@link SANDBOX_GIT_ORIGIN_VERDICTS} —— **逐 origin 判定序表**,11 行,序即判定序(首条命中即答;一个 origin 的结果不影响别的 origin)。
|
|
13
|
+
* 状态词({@link SANDBOX_GIT_ORIGIN_STATES},六个)与原因词({@link SANDBOX_GIT_ORIGIN_REASONS},十个)由它派生,没有第二张词表;`active` 行没有原因。
|
|
14
|
+
* 应答行({@link SandboxGitOriginAnswer})每个状态一个臂,臂里 `reason` 的值域 = 表里该状态的行 —— 状态与原因的配对由表定,臂不手写。
|
|
15
|
+
* 每一行的**条件**(什么事实使它命中)是 web-admin 的判定逻辑,写在 `docs/CONSUMER-INTEGRATION.md` 里,不进运行期表。
|
|
16
|
+
* · {@link SANDBOX_GIT_REFUSALS} —— **整次拒答词**(十个)→ HTTP 状态集与 `permanence`;拒体里的 `permanence` 是错误词的函数,必须等于这一行的。
|
|
17
|
+
* · {@link SANDBOX_GIT_PERMANENCE} —— `terminal`(不按定时重试)/ `transient`(退避重试)两词,语义在本表定义。
|
|
18
|
+
*
|
|
19
|
+
* ## 解析一律剥未知键
|
|
20
|
+
* 与包内 api 契约的前向兼容同律(新键恒为加性)。同时这也是一条安全律:`secret` 只在 `active` 臂的形里 —— 别的臂即使收到也剥掉,
|
|
21
|
+
* 解析出的值里不会带着谁多塞的秘密。`secret` 是应答里唯一的秘密位;`credentialId` / `version` / `auditId` 是不带值的审计位。
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
/**
|
|
25
|
+
* 逐 origin 判定序表(11 行,序即判定序)。每行 `{ state, reason? }`:命中这一行的 origin 的应答是这个状态与这个原因。
|
|
26
|
+
* 条件(各行什么事实命中)见 `docs/CONSUMER-INTEGRATION.md` 的判定序表;`active` 是兜底行(行 `active` 且刷新不成问题),没有原因。
|
|
27
|
+
* 加一行 / 改序都是改契约:状态词、原因词、应答臂的原因值域、类型全部随这一张表走。
|
|
28
|
+
*/
|
|
29
|
+
export declare const SANDBOX_GIT_ORIGIN_VERDICTS: readonly [{
|
|
30
|
+
readonly state: "insecure_transport";
|
|
31
|
+
readonly reason: "not_https";
|
|
32
|
+
}, {
|
|
33
|
+
readonly state: "not_offered";
|
|
34
|
+
readonly reason: "not_in_registry";
|
|
35
|
+
}, {
|
|
36
|
+
readonly state: "not_connected";
|
|
37
|
+
readonly reason: "no_row";
|
|
38
|
+
}, {
|
|
39
|
+
readonly state: "not_connected";
|
|
40
|
+
readonly reason: "revoked";
|
|
41
|
+
}, {
|
|
42
|
+
readonly state: "needs_reauth";
|
|
43
|
+
readonly reason: "revoking";
|
|
44
|
+
}, {
|
|
45
|
+
readonly state: "needs_reauth";
|
|
46
|
+
readonly reason: "suspended";
|
|
47
|
+
}, {
|
|
48
|
+
readonly state: "needs_reauth";
|
|
49
|
+
readonly reason: "row_needs_reauth";
|
|
50
|
+
}, {
|
|
51
|
+
readonly state: "needs_reauth";
|
|
52
|
+
readonly reason: "integrity_failed";
|
|
53
|
+
}, {
|
|
54
|
+
readonly state: "needs_reauth";
|
|
55
|
+
readonly reason: "refresh_rejected";
|
|
56
|
+
}, {
|
|
57
|
+
readonly state: "temporarily_unavailable";
|
|
58
|
+
readonly reason: "refresh_unavailable";
|
|
59
|
+
}, {
|
|
60
|
+
readonly state: "active";
|
|
61
|
+
}];
|
|
62
|
+
type Verdict = (typeof SANDBOX_GIT_ORIGIN_VERDICTS)[number];
|
|
63
|
+
/** 逐 origin 状态词(闭集,六个)—— 类型由判定序表派生。 */
|
|
64
|
+
export type SandboxGitOriginState = Verdict["state"];
|
|
65
|
+
/** 逐 origin 原因词(闭集,十个)—— 类型由判定序表派生;`active` 行没有原因。 */
|
|
66
|
+
export type SandboxGitOriginReason = Extract<Verdict, {
|
|
67
|
+
readonly reason: string;
|
|
68
|
+
}>["reason"];
|
|
69
|
+
/** 状态词表:判定序表里状态首次出现的次序。 */
|
|
70
|
+
export declare const SANDBOX_GIT_ORIGIN_STATES: readonly [SandboxGitOriginState, ...SandboxGitOriginState[]];
|
|
71
|
+
/** 原因词表:判定序表里原因出现的次序(= 判定序)。 */
|
|
72
|
+
export declare const SANDBOX_GIT_ORIGIN_REASONS: readonly [SandboxGitOriginReason, ...SandboxGitOriginReason[]];
|
|
73
|
+
/** 申领 `sandbox_git` 句柄的请求体。`origins` = 要取凭据的 origin(规范形、1–16 个、两两不同;http origin 也收 —— 中心对它答判定序第 1 行);`sid` = 会话 id(审计标签,不是安全边界)。 */
|
|
74
|
+
export declare const SandboxGitCredentialRequest: z.ZodObject<{
|
|
75
|
+
origins: z.ZodArray<z.ZodString>;
|
|
76
|
+
sid: z.ZodString;
|
|
77
|
+
principal: z.ZodString;
|
|
78
|
+
principalInc: z.ZodString;
|
|
79
|
+
scope: z.ZodString;
|
|
80
|
+
kind: z.ZodLiteral<"sandbox_git">;
|
|
81
|
+
}, z.core.$strip>;
|
|
82
|
+
export type SandboxGitCredentialRequest = z.infer<typeof SandboxGitCredentialRequest>;
|
|
83
|
+
/** 申领成功的应答:句柄(不透明串,一次性)与它的到期时刻(毫秒)。与凭据种类无关。 */
|
|
84
|
+
export declare const CredentialHandleAnswer: z.ZodObject<{
|
|
85
|
+
handle: z.ZodString;
|
|
86
|
+
expiresAt: z.ZodNumber;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
export type CredentialHandleAnswer = z.infer<typeof CredentialHandleAnswer>;
|
|
89
|
+
/** 兑换请求:只有句柄 —— 调用方凭据在传输层,不在体里。与凭据种类无关。 */
|
|
90
|
+
export declare const CredentialResolveRequest: z.ZodObject<{
|
|
91
|
+
handle: z.ZodString;
|
|
92
|
+
}, z.core.$strip>;
|
|
93
|
+
export type CredentialResolveRequest = z.infer<typeof CredentialResolveRequest>;
|
|
94
|
+
/**
|
|
95
|
+
* 一个 origin 的应答行,按 `state` 判别。键集(判定序表之上的形):
|
|
96
|
+
* · `active` 独有 `credentialId` `version` `username` `secret` `expiresAt`(OAuth 令牌:到期毫秒;个人访问令牌:`null`);没有 `reason`;
|
|
97
|
+
* · `needs_reauth`(序 5–9)带 `credentialId`(不带值的审计位);
|
|
98
|
+
* · 其余只有 `origin` / `state` / `reason`。
|
|
99
|
+
* 多给的键一律剥掉(见模块头注)。
|
|
100
|
+
*/
|
|
101
|
+
export declare const SandboxGitOriginAnswer: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
102
|
+
origin: z.ZodString;
|
|
103
|
+
state: z.ZodLiteral<"insecure_transport">;
|
|
104
|
+
reason: z.ZodEnum<{
|
|
105
|
+
not_https: "not_https";
|
|
106
|
+
}>;
|
|
107
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
108
|
+
origin: z.ZodString;
|
|
109
|
+
state: z.ZodLiteral<"not_offered">;
|
|
110
|
+
reason: z.ZodEnum<{
|
|
111
|
+
not_in_registry: "not_in_registry";
|
|
112
|
+
}>;
|
|
113
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
114
|
+
origin: z.ZodString;
|
|
115
|
+
state: z.ZodLiteral<"not_connected">;
|
|
116
|
+
reason: z.ZodEnum<{
|
|
117
|
+
no_row: "no_row";
|
|
118
|
+
revoked: "revoked";
|
|
119
|
+
}>;
|
|
120
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
121
|
+
origin: z.ZodString;
|
|
122
|
+
state: z.ZodLiteral<"needs_reauth">;
|
|
123
|
+
reason: z.ZodEnum<{
|
|
124
|
+
revoking: "revoking";
|
|
125
|
+
suspended: "suspended";
|
|
126
|
+
row_needs_reauth: "row_needs_reauth";
|
|
127
|
+
integrity_failed: "integrity_failed";
|
|
128
|
+
refresh_rejected: "refresh_rejected";
|
|
129
|
+
}>;
|
|
130
|
+
credentialId: z.ZodString;
|
|
131
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
132
|
+
origin: z.ZodString;
|
|
133
|
+
state: z.ZodLiteral<"temporarily_unavailable">;
|
|
134
|
+
reason: z.ZodEnum<{
|
|
135
|
+
refresh_unavailable: "refresh_unavailable";
|
|
136
|
+
}>;
|
|
137
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
138
|
+
origin: z.ZodString;
|
|
139
|
+
state: z.ZodLiteral<"active">;
|
|
140
|
+
credentialId: z.ZodString;
|
|
141
|
+
version: z.ZodNumber;
|
|
142
|
+
username: z.ZodString;
|
|
143
|
+
secret: z.ZodString;
|
|
144
|
+
expiresAt: z.ZodNullable<z.ZodNumber>;
|
|
145
|
+
}, z.core.$strip>], "state">;
|
|
146
|
+
export type SandboxGitOriginAnswer = z.infer<typeof SandboxGitOriginAnswer>;
|
|
147
|
+
/**
|
|
148
|
+
* 取值应答(200)。`principal` / `principalInc` / `scope` 是请求的**回显**;`answeredAt`(应答时刻)、`cacheUntil`(句柄到期,收方不得把凭据缓存过它)与行里的 `expiresAt`
|
|
149
|
+
* 是**同一只钟**(中心的)给的毫秒 —— 收方只该用它们的**差**,不拿它们和自己的钟比。`auditId` = 审计行 id(不带值)。
|
|
150
|
+
* `origins` 与请求逐条对应(同序同长)是收方要核的事,本形只钉 1–{@link SANDBOX_GIT_MAX_ORIGINS} 条。
|
|
151
|
+
*/
|
|
152
|
+
export declare const SandboxGitCredentialAnswer: z.ZodObject<{
|
|
153
|
+
auditId: z.ZodString;
|
|
154
|
+
answeredAt: z.ZodNumber;
|
|
155
|
+
cacheUntil: z.ZodNumber;
|
|
156
|
+
origins: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
157
|
+
origin: z.ZodString;
|
|
158
|
+
state: z.ZodLiteral<"insecure_transport">;
|
|
159
|
+
reason: z.ZodEnum<{
|
|
160
|
+
not_https: "not_https";
|
|
161
|
+
}>;
|
|
162
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
163
|
+
origin: z.ZodString;
|
|
164
|
+
state: z.ZodLiteral<"not_offered">;
|
|
165
|
+
reason: z.ZodEnum<{
|
|
166
|
+
not_in_registry: "not_in_registry";
|
|
167
|
+
}>;
|
|
168
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
169
|
+
origin: z.ZodString;
|
|
170
|
+
state: z.ZodLiteral<"not_connected">;
|
|
171
|
+
reason: z.ZodEnum<{
|
|
172
|
+
no_row: "no_row";
|
|
173
|
+
revoked: "revoked";
|
|
174
|
+
}>;
|
|
175
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
176
|
+
origin: z.ZodString;
|
|
177
|
+
state: z.ZodLiteral<"needs_reauth">;
|
|
178
|
+
reason: z.ZodEnum<{
|
|
179
|
+
revoking: "revoking";
|
|
180
|
+
suspended: "suspended";
|
|
181
|
+
row_needs_reauth: "row_needs_reauth";
|
|
182
|
+
integrity_failed: "integrity_failed";
|
|
183
|
+
refresh_rejected: "refresh_rejected";
|
|
184
|
+
}>;
|
|
185
|
+
credentialId: z.ZodString;
|
|
186
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
187
|
+
origin: z.ZodString;
|
|
188
|
+
state: z.ZodLiteral<"temporarily_unavailable">;
|
|
189
|
+
reason: z.ZodEnum<{
|
|
190
|
+
refresh_unavailable: "refresh_unavailable";
|
|
191
|
+
}>;
|
|
192
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
193
|
+
origin: z.ZodString;
|
|
194
|
+
state: z.ZodLiteral<"active">;
|
|
195
|
+
credentialId: z.ZodString;
|
|
196
|
+
version: z.ZodNumber;
|
|
197
|
+
username: z.ZodString;
|
|
198
|
+
secret: z.ZodString;
|
|
199
|
+
expiresAt: z.ZodNullable<z.ZodNumber>;
|
|
200
|
+
}, z.core.$strip>], "state">>;
|
|
201
|
+
principal: z.ZodString;
|
|
202
|
+
principalInc: z.ZodString;
|
|
203
|
+
scope: z.ZodString;
|
|
204
|
+
kind: z.ZodLiteral<"sandbox_git">;
|
|
205
|
+
}, z.core.$strip>;
|
|
206
|
+
export type SandboxGitCredentialAnswer = z.infer<typeof SandboxGitCredentialAnswer>;
|
|
207
|
+
/** `permanence` 两词:`terminal` = 不按定时重试;`transient` = 退避重试。 */
|
|
208
|
+
export declare const SANDBOX_GIT_PERMANENCE: readonly ["terminal", "transient"];
|
|
209
|
+
export type SandboxGitPermanence = (typeof SANDBOX_GIT_PERMANENCE)[number];
|
|
210
|
+
/**
|
|
211
|
+
* 整次拒答词 → 它的 HTTP 状态集(中心答这个词时用其一)与 `permanence`。**一个错误词的 permanence 是固定的**,拒体里再带一份只为让收方不必查表 ——
|
|
212
|
+
* 两处不一致就是畸形应答({@link SandboxGitCredentialRefusal} 拒)。收方怎么处置某个词(例如哪些 terminal 可由用户动作恢复)是收方的状态机,不在这里。
|
|
213
|
+
*/
|
|
214
|
+
export declare const SANDBOX_GIT_REFUSALS: {
|
|
215
|
+
readonly caller_not_admitted: {
|
|
216
|
+
readonly statuses: readonly [401, 403];
|
|
217
|
+
readonly permanence: "terminal";
|
|
218
|
+
};
|
|
219
|
+
readonly scope_mismatch: {
|
|
220
|
+
readonly statuses: readonly [403];
|
|
221
|
+
readonly permanence: "terminal";
|
|
222
|
+
};
|
|
223
|
+
readonly incarnation_mismatch: {
|
|
224
|
+
readonly statuses: readonly [403];
|
|
225
|
+
readonly permanence: "terminal";
|
|
226
|
+
};
|
|
227
|
+
readonly request_invalid: {
|
|
228
|
+
readonly statuses: readonly [400];
|
|
229
|
+
readonly permanence: "terminal";
|
|
230
|
+
};
|
|
231
|
+
readonly principal_not_live: {
|
|
232
|
+
readonly statuses: readonly [403];
|
|
233
|
+
readonly permanence: "terminal";
|
|
234
|
+
};
|
|
235
|
+
readonly delegation_required: {
|
|
236
|
+
readonly statuses: readonly [403];
|
|
237
|
+
readonly permanence: "terminal";
|
|
238
|
+
};
|
|
239
|
+
readonly vault_disabled: {
|
|
240
|
+
readonly statuses: readonly [503];
|
|
241
|
+
readonly permanence: "terminal";
|
|
242
|
+
};
|
|
243
|
+
readonly vault_frozen: {
|
|
244
|
+
readonly statuses: readonly [503];
|
|
245
|
+
readonly permanence: "terminal";
|
|
246
|
+
};
|
|
247
|
+
readonly handle_invalid: {
|
|
248
|
+
readonly statuses: readonly [401];
|
|
249
|
+
readonly permanence: "transient";
|
|
250
|
+
};
|
|
251
|
+
readonly vault_unavailable: {
|
|
252
|
+
readonly statuses: readonly [503];
|
|
253
|
+
readonly permanence: "transient";
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
/** 整次拒答词(闭集,十个)—— 类型由 {@link SANDBOX_GIT_REFUSALS} 派生。 */
|
|
257
|
+
export type SandboxGitRefusalError = keyof typeof SANDBOX_GIT_REFUSALS;
|
|
258
|
+
/** 整次拒答词表(运行期;表的键,按表序)。 */
|
|
259
|
+
export declare const SANDBOX_GIT_REFUSAL_ERRORS: readonly [SandboxGitRefusalError, ...SandboxGitRefusalError[]];
|
|
260
|
+
/** 整次拒答的体:`{ error, permanence, message }`。`message` 是中心的诊断句(收方不据它判,可为空串)。 */
|
|
261
|
+
export declare const SandboxGitCredentialRefusal: z.ZodObject<{
|
|
262
|
+
error: z.ZodEnum<{
|
|
263
|
+
caller_not_admitted: "caller_not_admitted";
|
|
264
|
+
scope_mismatch: "scope_mismatch";
|
|
265
|
+
incarnation_mismatch: "incarnation_mismatch";
|
|
266
|
+
request_invalid: "request_invalid";
|
|
267
|
+
principal_not_live: "principal_not_live";
|
|
268
|
+
delegation_required: "delegation_required";
|
|
269
|
+
vault_disabled: "vault_disabled";
|
|
270
|
+
vault_frozen: "vault_frozen";
|
|
271
|
+
handle_invalid: "handle_invalid";
|
|
272
|
+
vault_unavailable: "vault_unavailable";
|
|
273
|
+
}>;
|
|
274
|
+
permanence: z.ZodEnum<{
|
|
275
|
+
terminal: "terminal";
|
|
276
|
+
transient: "transient";
|
|
277
|
+
}>;
|
|
278
|
+
message: z.ZodString;
|
|
279
|
+
}, z.core.$strip>;
|
|
280
|
+
export type SandboxGitCredentialRefusal = z.infer<typeof SandboxGitCredentialRefusal>;
|
|
281
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `api/credentials` —— 凭据取值口(`POST /api/v1/credentials/handles` → `POST /api/v1/credentials/resolve`)里 **`sandbox_git`** 这一种凭据的 wire 型(14.7.0)。
|
|
3
|
+
* 一份 zod,发这些形的一方(web-admin)与收这些形的一方(引擎的数据面)同 import。纯、浏览器安全。
|
|
4
|
+
*
|
|
5
|
+
* 两步:① 带调用方凭据申领一枚短期、一次性的**句柄**(请求 {@link SandboxGitCredentialRequest},应答 {@link CredentialHandleAnswer});
|
|
6
|
+
* ② 凭句柄兑换(请求 {@link CredentialResolveRequest},应答 {@link SandboxGitCredentialAnswer};不合格则整次拒答 {@link SandboxGitCredentialRefusal})。
|
|
7
|
+
* 本模块只有**形与词表**:调用方鉴权、委托、活表复核、行 MAC、租约算术、续期状态机、「应答与请求逐条对应 / 回显相等」的核对都是各方自己的事,不在这里。
|
|
8
|
+
* 取值口的另两种凭据(`model` / `mcp_static`)的请求与应答形不在本模块;它们落地时与本模块的 `sandbox_git` 形并成按 `kind` 判别的联合,
|
|
9
|
+
* 本模块的形不动(`kind` 字面量已经在)。
|
|
10
|
+
*
|
|
11
|
+
* ## 三张词表,各一张,每个词只在本文件里拼一次
|
|
12
|
+
* · {@link SANDBOX_GIT_ORIGIN_VERDICTS} —— **逐 origin 判定序表**,11 行,序即判定序(首条命中即答;一个 origin 的结果不影响别的 origin)。
|
|
13
|
+
* 状态词({@link SANDBOX_GIT_ORIGIN_STATES},六个)与原因词({@link SANDBOX_GIT_ORIGIN_REASONS},十个)由它派生,没有第二张词表;`active` 行没有原因。
|
|
14
|
+
* 应答行({@link SandboxGitOriginAnswer})每个状态一个臂,臂里 `reason` 的值域 = 表里该状态的行 —— 状态与原因的配对由表定,臂不手写。
|
|
15
|
+
* 每一行的**条件**(什么事实使它命中)是 web-admin 的判定逻辑,写在 `docs/CONSUMER-INTEGRATION.md` 里,不进运行期表。
|
|
16
|
+
* · {@link SANDBOX_GIT_REFUSALS} —— **整次拒答词**(十个)→ HTTP 状态集与 `permanence`;拒体里的 `permanence` 是错误词的函数,必须等于这一行的。
|
|
17
|
+
* · {@link SANDBOX_GIT_PERMANENCE} —— `terminal`(不按定时重试)/ `transient`(退避重试)两词,语义在本表定义。
|
|
18
|
+
*
|
|
19
|
+
* ## 解析一律剥未知键
|
|
20
|
+
* 与包内 api 契约的前向兼容同律(新键恒为加性)。同时这也是一条安全律:`secret` 只在 `active` 臂的形里 —— 别的臂即使收到也剥掉,
|
|
21
|
+
* 解析出的值里不会带着谁多塞的秘密。`secret` 是应答里唯一的秘密位;`credentialId` / `version` / `auditId` 是不带值的审计位。
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { INTEGER } from "../integer.js";
|
|
25
|
+
import { buildQuotedIssue } from "../quoted-issue.js";
|
|
26
|
+
import { SANDBOX_GIT_MAX_ORIGINS, SandboxGitOrigin, SandboxGitOriginList } from "../sandbox-git.js";
|
|
27
|
+
import { PRINCIPAL_MAX_LENGTH, PrincipalIncarnation } from "./auth-bridge.js";
|
|
28
|
+
// ── 逐 origin 判定序表 ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* 逐 origin 判定序表(11 行,序即判定序)。每行 `{ state, reason? }`:命中这一行的 origin 的应答是这个状态与这个原因。
|
|
31
|
+
* 条件(各行什么事实命中)见 `docs/CONSUMER-INTEGRATION.md` 的判定序表;`active` 是兜底行(行 `active` 且刷新不成问题),没有原因。
|
|
32
|
+
* 加一行 / 改序都是改契约:状态词、原因词、应答臂的原因值域、类型全部随这一张表走。
|
|
33
|
+
*/
|
|
34
|
+
export const SANDBOX_GIT_ORIGIN_VERDICTS = [
|
|
35
|
+
{ state: "insecure_transport", reason: "not_https" },
|
|
36
|
+
{ state: "not_offered", reason: "not_in_registry" },
|
|
37
|
+
{ state: "not_connected", reason: "no_row" },
|
|
38
|
+
{ state: "not_connected", reason: "revoked" },
|
|
39
|
+
{ state: "needs_reauth", reason: "revoking" },
|
|
40
|
+
{ state: "needs_reauth", reason: "suspended" },
|
|
41
|
+
{ state: "needs_reauth", reason: "row_needs_reauth" },
|
|
42
|
+
{ state: "needs_reauth", reason: "integrity_failed" },
|
|
43
|
+
{ state: "needs_reauth", reason: "refresh_rejected" },
|
|
44
|
+
{ state: "temporarily_unavailable", reason: "refresh_unavailable" },
|
|
45
|
+
{ state: "active" },
|
|
46
|
+
];
|
|
47
|
+
/** 状态词表:判定序表里状态首次出现的次序。 */
|
|
48
|
+
export const SANDBOX_GIT_ORIGIN_STATES = [...new Set(SANDBOX_GIT_ORIGIN_VERDICTS.map((v) => v.state))];
|
|
49
|
+
/** 原因词表:判定序表里原因出现的次序(= 判定序)。 */
|
|
50
|
+
export const SANDBOX_GIT_ORIGIN_REASONS = SANDBOX_GIT_ORIGIN_VERDICTS.flatMap((v) => ("reason" in v ? [v.reason] : []));
|
|
51
|
+
/** 判定序表里某个状态下的原因词,按表序。 */
|
|
52
|
+
function reasonsOf(state) {
|
|
53
|
+
return SANDBOX_GIT_ORIGIN_VERDICTS.flatMap((v) => (v.state === state && "reason" in v ? [v.reason] : []));
|
|
54
|
+
}
|
|
55
|
+
// ── 公共字段 ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
/** 本模块里每个数值位(毫秒时刻、凭据行版本)的形:非负安全整数 —— 包内的整数判据 + 安全整数上界;浮点、负数、超出安全整数的都不是。 */
|
|
57
|
+
const SafeCount = INTEGER.nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
58
|
+
const NonEmptyString = z.string().min(1);
|
|
59
|
+
/**
|
|
60
|
+
* 请求与应答都带的「这是谁的 run」字段:主体(长度界同令牌 `sub` 的界,{@link PRINCIPAL_MAX_LENGTH})、化身号({@link PrincipalIncarnation},与令牌 `sub_inc` 同一只 schema)、
|
|
61
|
+
* scope(非空串 —— 与 `readTokenScope` 读 token 声明的规则同一条,不另立 slug 规则)。应答里这三键是**回显**,等不等于请求由收方核。
|
|
62
|
+
*/
|
|
63
|
+
const RunIdentity = {
|
|
64
|
+
principal: z.string().min(1).max(PRINCIPAL_MAX_LENGTH),
|
|
65
|
+
principalInc: PrincipalIncarnation,
|
|
66
|
+
scope: NonEmptyString,
|
|
67
|
+
};
|
|
68
|
+
// ── ① 申领句柄、② 兑换:请求与句柄应答 ───────────────────────────────────────────────────────────────────────────
|
|
69
|
+
/** 申领 `sandbox_git` 句柄的请求体。`origins` = 要取凭据的 origin(规范形、1–16 个、两两不同;http origin 也收 —— 中心对它答判定序第 1 行);`sid` = 会话 id(审计标签,不是安全边界)。 */
|
|
70
|
+
export const SandboxGitCredentialRequest = z.object({
|
|
71
|
+
kind: z.literal("sandbox_git"),
|
|
72
|
+
...RunIdentity,
|
|
73
|
+
origins: SandboxGitOriginList.min(1),
|
|
74
|
+
sid: NonEmptyString,
|
|
75
|
+
});
|
|
76
|
+
/** 申领成功的应答:句柄(不透明串,一次性)与它的到期时刻(毫秒)。与凭据种类无关。 */
|
|
77
|
+
export const CredentialHandleAnswer = z.object({ handle: NonEmptyString, expiresAt: SafeCount });
|
|
78
|
+
/** 兑换请求:只有句柄 —— 调用方凭据在传输层,不在体里。与凭据种类无关。 */
|
|
79
|
+
export const CredentialResolveRequest = z.object({ handle: NonEmptyString });
|
|
80
|
+
// ── 应答:逐 origin 一行 ─────────────────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
/** 非 `active` 状态的应答臂:`{ origin, state, reason }`,`reason` 的值域 = 判定序表里该状态的行。 */
|
|
82
|
+
function verdictArm(state) {
|
|
83
|
+
return z.object({ origin: SandboxGitOrigin, state: z.literal(state), reason: z.enum(reasonsOf(state)) });
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 一个 origin 的应答行,按 `state` 判别。键集(判定序表之上的形):
|
|
87
|
+
* · `active` 独有 `credentialId` `version` `username` `secret` `expiresAt`(OAuth 令牌:到期毫秒;个人访问令牌:`null`);没有 `reason`;
|
|
88
|
+
* · `needs_reauth`(序 5–9)带 `credentialId`(不带值的审计位);
|
|
89
|
+
* · 其余只有 `origin` / `state` / `reason`。
|
|
90
|
+
* 多给的键一律剥掉(见模块头注)。
|
|
91
|
+
*/
|
|
92
|
+
export const SandboxGitOriginAnswer = z.discriminatedUnion("state", [
|
|
93
|
+
verdictArm("insecure_transport"),
|
|
94
|
+
verdictArm("not_offered"),
|
|
95
|
+
verdictArm("not_connected"),
|
|
96
|
+
verdictArm("needs_reauth").extend({ credentialId: NonEmptyString }),
|
|
97
|
+
verdictArm("temporarily_unavailable"),
|
|
98
|
+
z.object({
|
|
99
|
+
origin: SandboxGitOrigin,
|
|
100
|
+
state: z.literal("active"),
|
|
101
|
+
credentialId: NonEmptyString,
|
|
102
|
+
version: SafeCount,
|
|
103
|
+
username: NonEmptyString,
|
|
104
|
+
secret: NonEmptyString,
|
|
105
|
+
expiresAt: SafeCount.nullable(),
|
|
106
|
+
}),
|
|
107
|
+
]);
|
|
108
|
+
/**
|
|
109
|
+
* 取值应答(200)。`principal` / `principalInc` / `scope` 是请求的**回显**;`answeredAt`(应答时刻)、`cacheUntil`(句柄到期,收方不得把凭据缓存过它)与行里的 `expiresAt`
|
|
110
|
+
* 是**同一只钟**(中心的)给的毫秒 —— 收方只该用它们的**差**,不拿它们和自己的钟比。`auditId` = 审计行 id(不带值)。
|
|
111
|
+
* `origins` 与请求逐条对应(同序同长)是收方要核的事,本形只钉 1–{@link SANDBOX_GIT_MAX_ORIGINS} 条。
|
|
112
|
+
*/
|
|
113
|
+
export const SandboxGitCredentialAnswer = z.object({
|
|
114
|
+
kind: z.literal("sandbox_git"),
|
|
115
|
+
...RunIdentity,
|
|
116
|
+
auditId: NonEmptyString,
|
|
117
|
+
answeredAt: SafeCount,
|
|
118
|
+
cacheUntil: SafeCount,
|
|
119
|
+
origins: z.array(SandboxGitOriginAnswer).min(1).max(SANDBOX_GIT_MAX_ORIGINS),
|
|
120
|
+
});
|
|
121
|
+
// ── 整次拒答 ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
/** `permanence` 两词:`terminal` = 不按定时重试;`transient` = 退避重试。 */
|
|
123
|
+
export const SANDBOX_GIT_PERMANENCE = ["terminal", "transient"];
|
|
124
|
+
/**
|
|
125
|
+
* 整次拒答词 → 它的 HTTP 状态集(中心答这个词时用其一)与 `permanence`。**一个错误词的 permanence 是固定的**,拒体里再带一份只为让收方不必查表 ——
|
|
126
|
+
* 两处不一致就是畸形应答({@link SandboxGitCredentialRefusal} 拒)。收方怎么处置某个词(例如哪些 terminal 可由用户动作恢复)是收方的状态机,不在这里。
|
|
127
|
+
*/
|
|
128
|
+
export const SANDBOX_GIT_REFUSALS = {
|
|
129
|
+
caller_not_admitted: { statuses: [401, 403], permanence: "terminal" },
|
|
130
|
+
scope_mismatch: { statuses: [403], permanence: "terminal" },
|
|
131
|
+
incarnation_mismatch: { statuses: [403], permanence: "terminal" },
|
|
132
|
+
request_invalid: { statuses: [400], permanence: "terminal" },
|
|
133
|
+
principal_not_live: { statuses: [403], permanence: "terminal" },
|
|
134
|
+
delegation_required: { statuses: [403], permanence: "terminal" },
|
|
135
|
+
vault_disabled: { statuses: [503], permanence: "terminal" },
|
|
136
|
+
vault_frozen: { statuses: [503], permanence: "terminal" },
|
|
137
|
+
handle_invalid: { statuses: [401], permanence: "transient" },
|
|
138
|
+
vault_unavailable: { statuses: [503], permanence: "transient" },
|
|
139
|
+
};
|
|
140
|
+
/** 整次拒答词表(运行期;表的键,按表序)。 */
|
|
141
|
+
export const SANDBOX_GIT_REFUSAL_ERRORS = Object.keys(SANDBOX_GIT_REFUSALS);
|
|
142
|
+
/** 整次拒答的体:`{ error, permanence, message }`。`message` 是中心的诊断句(收方不据它判,可为空串)。 */
|
|
143
|
+
export const SandboxGitCredentialRefusal = z
|
|
144
|
+
.object({
|
|
145
|
+
error: z.enum(SANDBOX_GIT_REFUSAL_ERRORS),
|
|
146
|
+
permanence: z.enum(SANDBOX_GIT_PERMANENCE),
|
|
147
|
+
message: z.string(),
|
|
148
|
+
})
|
|
149
|
+
.refine((body) => SANDBOX_GIT_REFUSALS[body.error].permanence === body.permanence, buildQuotedIssue `permanence must be the one the refusal table gives for this error`);
|
package/dist/api/scopes.js
CHANGED
|
@@ -47,6 +47,7 @@ import { z } from "zod";
|
|
|
47
47
|
import { INTEGER } from "../integer.js";
|
|
48
48
|
import { buildQuotedIssue, fixed } from "../quoted-issue.js";
|
|
49
49
|
import { USER_ROLES } from "../config-fns.js";
|
|
50
|
+
import { ownDeclaredValueOf } from "../own-key-read.js";
|
|
50
51
|
import { ACCESS_TOKEN_TTL_SECONDS } from "./auth.js";
|
|
51
52
|
// ── constants ────────────────────────────────────────────────────────────────────────────────────────────
|
|
52
53
|
/** The reserved default scope. Always exists. */
|
|
@@ -230,5 +231,8 @@ export const TOKEN_SCOPE_REFUSALS = ["scope_claim_missing"];
|
|
|
230
231
|
* re-mints one with the claim — a token without it is not a token this contract reads (it used to be read as `"global"`).
|
|
231
232
|
*/
|
|
232
233
|
export function readTokenScope(claims) {
|
|
233
|
-
|
|
234
|
+
// the claim is read from the claims' OWN keys (a polluted `Object.prototype.scope` is not a scope the token carries); a claim that cannot be read is a claim that is missing
|
|
235
|
+
const read = ownDeclaredValueOf(claims, "scope");
|
|
236
|
+
const scope = read.ok ? read.value : undefined;
|
|
237
|
+
return typeof scope === "string" && scope.length > 0 ? { scope } : { refused: "scope_claim_missing" };
|
|
234
238
|
}
|
package/dist/atomic-write.js
CHANGED
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
* 悬空链接在读阶段与写阶段是**同一句**({@link danglingSymlinkSentence})。
|
|
22
22
|
*
|
|
23
23
|
* ## 写失败(12.0.0;句子归文件系统失败句的唯一铸点)
|
|
24
|
-
*
|
|
25
|
-
* 文件本身没错);`rename` 失败 ⇒ 「cannot replace <file> (<errno>): …」(点名真文件)
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* 三步各一句({@link FsFailure},句子在 `./fs-failure`):临时文件建不出 ⇒ 「cannot write into directory <dir> (<errno>): …」(点名目录 —— 目录不可写时
|
|
25
|
+
* 文件本身没错);沿用原模式的文件给临时文件设模式失败 ⇒ 「cannot set the mode of <file> (<errno>): …」(点名设模式这一步 —— 文件系统不让改模式,目录没有问题);`rename` 失败 ⇒ 「cannot replace <file> (<errno>): …」(点名真文件)。
|
|
26
|
+
* 认得的 errno 给对应出路,别的给 Node 自己的句子;`code` / `cause` 留着。
|
|
27
|
+
* 可写目录里的只读文件照常写得进(`rename` 换的是目录项)。权限类 errno 的出路要一个只有 Node 一侧读得到的事实 —— 失败那一刻目录的权限位对本进程是否可写({@link directoryAdmitsWrite},模式判在本文件、与模式表同处),
|
|
28
|
+
* 写器在建临时文件那一步读了经落点递给铸点(`EPERM` ∧ 可写 ⇒ 真因是不可变标志,首选出路是清标志;`EACCES` ∧ 可写 ⇒ 真因是 ACL / 安全模块;不可写 ⇒ 先让目录可写);`chmod` / `rename` 那两步不读 —— 临时文件刚在同目录建成,可写是已证明的事实,铸点自己给。
|
|
28
29
|
*/
|
|
29
30
|
import { randomBytes } from "node:crypto";
|
|
30
31
|
import { chmodSync, lstatSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -91,8 +92,10 @@ export function writeFileAtomic(target, text, kind) {
|
|
|
91
92
|
let site = { landing: "dir-create-temp", path: tmp };
|
|
92
93
|
try {
|
|
93
94
|
writeFileSync(tmp, text, { encoding: "utf8", mode: kept ?? spec.newMode });
|
|
94
|
-
if (kept !== undefined)
|
|
95
|
+
if (kept !== undefined) {
|
|
96
|
+
site = { landing: "chmod", path: real }; // the temporary file now exists in the directory: a failure from here on is not the directory's; it names the step (the real file whose mode is being kept)
|
|
95
97
|
chmodSync(tmp, kept); // a kept mode is restored exactly (the creation mode above went through the umask)
|
|
98
|
+
}
|
|
96
99
|
site = { landing: "rename", path: real };
|
|
97
100
|
renameSync(tmp, real);
|
|
98
101
|
}
|
|
@@ -103,8 +106,8 @@ export function writeFileAtomic(target, text, kind) {
|
|
|
103
106
|
catch {
|
|
104
107
|
/* 临时文件没建成:没什么可清 */
|
|
105
108
|
}
|
|
106
|
-
// the sentence names the step that failed and its way out (`./fs-failure`, the one mint); the way-out
|
|
107
|
-
// read only at the create step: by the time `rename`
|
|
109
|
+
// the sentence names the step that failed and its way out (`./fs-failure`, the one mint); the way-out table needs one Node-side fact — do the directory's mode bits admit this process —
|
|
110
|
+
// read only at the create step: by the time `chmod` / `rename` run the temporary file has just been created in the same directory, so "writable" is proven there and the mint says so itself
|
|
108
111
|
throw new FsFailure(site.landing === "dir-create-temp" ? { ...site, directoryWritable: directoryAdmitsWrite(dir) } : site, e);
|
|
109
112
|
}
|
|
110
113
|
}
|
package/dist/bundle.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PortableDomain } from "./
|
|
1
|
+
import { type PortableDomain } from "./portable-domains.js";
|
|
2
2
|
import type { ConfigReader } from "./reader.js";
|
|
3
3
|
/** A bundle carries PORTABLE domains only — the non-portable ones (`NON_PORTABLE_DOMAIN_REASONS`: workers / hosts / systems / projects /
|
|
4
4
|
* entitlement) never travel, and the three bundle functions are that table's other face (types narrow to {@link PortableDomain}; a
|
package/dist/bundle.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Node-only (writeBundle/readBundle touch the filesystem) → imported via `@sema-agent/settings-schema/node`.
|
|
10
10
|
*/
|
|
11
11
|
import { writeDomainFile } from "./file-edit.js";
|
|
12
|
-
import { PORTABLE_DOMAINS } from "./
|
|
12
|
+
import { PORTABLE_DOMAINS } from "./portable-domains.js";
|
|
13
13
|
import { configDPath, readConfigDFile, refuseRetiredDomainFiles } from "./config-d-file.js";
|
|
14
14
|
/** The runtime guard of the same rule (a name that is not even a domain is refused first — no path traversal). */
|
|
15
15
|
function assertPortable(d) {
|
package/dist/config-d-file.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
import { readFile } from "node:fs/promises";
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { refuseRetiredDomain, sanitizeZodError } from "./config-fns.js";
|
|
15
|
-
import {
|
|
15
|
+
import { UNINSPECTABLE, guardedSafeParse } from "./own-key-read.js";
|
|
16
|
+
import { jsonShapeOf } from "./plain-json-object.js";
|
|
16
17
|
import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
|
|
17
18
|
import { RETIRED_DOMAINS, isDomainName, isRetiredDomainName } from "./types.js";
|
|
18
19
|
/** 一份 `config.d` 文档写盘的**文本形** —— 两格缩进的 JSON 加一个换行。写域文件(`writeDomainFile`)、写 `remote-exec.json`(`writeRemoteExec`)与目录迁移写回
|
|
@@ -23,16 +24,12 @@ export function configDDocumentText(value) {
|
|
|
23
24
|
/** 判一份 remote-exec 文档的值(`RemoteExecSpec`),错误**脱敏**(zod 会回显出错的值 —— 粘错位置的密钥不许随抛出的错走);反射访问抛(恶意 getter /
|
|
24
25
|
* Proxy)⇒ 定句。`loadRemoteExec` / `writeRemoteExec` 与目录迁移的整文档终判调同一只。 */
|
|
25
26
|
export function parseRemoteExecDocument(raw) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
if (r.success)
|
|
34
|
-
return r.data;
|
|
35
|
-
throw sanitizeZodError(r.error);
|
|
27
|
+
const judged = guardedSafeParse(() => RemoteExecSpec.safeParse(raw), sanitizeZodError); // 错误对象的构造与脱敏都在守护里(zod 4.6 首读才拼句子)
|
|
28
|
+
if (!judged.ok)
|
|
29
|
+
throw new TypeError("invalid remoteExec (a value could not be read during validation)");
|
|
30
|
+
if (judged.value.success)
|
|
31
|
+
return judged.value.data;
|
|
32
|
+
throw judged.value.error;
|
|
36
33
|
}
|
|
37
34
|
/** `<root>/config.d/<名>.json` 的路径 —— 名字的判也在这里(**一处**):不是配置域也不是 retired 域 ⇒ 拒(不拿用户给的名拼路径,防 `../` 穿越)。 */
|
|
38
35
|
export function configDPath(root, name) {
|
|
@@ -64,11 +61,32 @@ export function judgeConfigDText(name, text, file) {
|
|
|
64
61
|
* 非朴素对象 ⇒ 抛。{@link judgeConfigDText} 的最后一道判就是这只(文本解析出来之后过它),所以「不是对象」一种坏形一句话,不论文档是从文件文本来的还是调用方直接给的;
|
|
65
62
|
* `label` = 句首的称呼(文本路径给文件的真实路径,值路径给 `the <名> document`)。 */
|
|
66
63
|
export function judgeConfigDValue(name, value, label) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
const shape = jsonShapeOf(value);
|
|
65
|
+
if (shape === "plain")
|
|
66
|
+
return value;
|
|
67
|
+
// `shape` is narrowed to the five non-plain words: an exhaustive switch (a sixth word is a compile error here)
|
|
68
|
+
let kind;
|
|
69
|
+
switch (shape) {
|
|
70
|
+
case "null":
|
|
71
|
+
kind = "null";
|
|
72
|
+
break;
|
|
73
|
+
case "array":
|
|
74
|
+
kind = "array";
|
|
75
|
+
break;
|
|
76
|
+
case "non_plain_object":
|
|
77
|
+
kind = typeof value === "function" ? "function" : "non-plain object";
|
|
78
|
+
break;
|
|
79
|
+
case "uninspectable": // 读不出(撤销的 Proxy / 陷阱抛)不是「不是对象」:调用方给的对象读不出是调用方的错 ⇒ `TypeError`(v14.6.0:引擎对它抛的就是 TypeError),句子带全包唯一那一句
|
|
80
|
+
throw new TypeError(`${label} must be a JSON object (got ${UNINSPECTABLE}) — ${documentWayOut(name)}`);
|
|
81
|
+
case "primitive":
|
|
82
|
+
kind = typeof value;
|
|
83
|
+
break;
|
|
84
|
+
default: {
|
|
85
|
+
const unhandled = shape;
|
|
86
|
+
throw new Error(`judgeConfigDValue: unhandled shape ${String(unhandled)}`);
|
|
87
|
+
}
|
|
70
88
|
}
|
|
71
|
-
|
|
89
|
+
throw new Error(`${label} must be a JSON object (got ${kind}) — ${documentWayOut(name)}`);
|
|
72
90
|
}
|
|
73
91
|
/** 读一个 `config.d/<名>.json`:不存在(ENOENT)⇒ `undefined`;在场 ⇒ {@link judgeConfigDText}。 */
|
|
74
92
|
export async function readConfigDFile(root, name) {
|
|
@@ -31,6 +31,7 @@ import { z } from "zod";
|
|
|
31
31
|
import { formatConfigPath, parseDomain } from "./config-fns.js";
|
|
32
32
|
import { configDDocumentText, judgeConfigDText, judgeConfigDValue, parseRemoteExecDocument } from "./config-d-file.js";
|
|
33
33
|
import { fsFailureMessage } from "./fs-failure.js";
|
|
34
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
34
35
|
import { migrateStoredDocuments } from "./one-time-migrations.js";
|
|
35
36
|
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
36
37
|
import { RUNTIME_LIFT_TARGETS } from "./runtime-lift-targets.js";
|
|
@@ -150,7 +151,7 @@ function migrateCovered(caller, docs, covered) {
|
|
|
150
151
|
* 覆盖面外的键不读、不判、不进结果;覆盖面内在场(非 `undefined`)的文档必须是 JSON 对象,否则回 `unreadable`(不抛)。`docs` 不是对象 / 覆盖面拼错 ⇒ `TypeError`(调用方的错,不是数据的错)。
|
|
151
152
|
*/
|
|
152
153
|
export function migrateConfigDocuments(docs, opts) {
|
|
153
|
-
if (
|
|
154
|
+
if (!isPlainJsonObject(docs))
|
|
154
155
|
throw new TypeError("migrateConfigDocuments: `docs` must be a { document: value } object");
|
|
155
156
|
const covered = coveredDocuments("migrateConfigDocuments", opts.documents);
|
|
156
157
|
const present = {};
|