@sema-agent/settings-schema 14.6.0 → 14.8.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 +475 -0
- package/README.md +89 -5
- 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.d.ts +15 -0
- package/dist/atomic-write.js +109 -5
- package/dist/catalog-manages.d.ts +15 -0
- package/dist/catalog-manages.js +60 -0
- package/dist/center-refs.d.ts +68 -0
- package/dist/center-refs.js +70 -0
- package/dist/config-d-file.js +33 -15
- package/dist/config-dir-migration.js +1 -0
- package/dist/config-fns.js +147 -81
- package/dist/config-root-documents.d.ts +106 -0
- package/dist/config-root-documents.js +93 -0
- package/dist/config-root-node.d.ts +117 -0
- package/dist/config-root-node.js +260 -0
- package/dist/conformance/canonical.js +4 -4
- package/dist/conformance/contract-adapter.js +26 -1
- package/dist/conformance/corpus.js +4 -4
- package/dist/conformance/derive.js +2 -2
- package/dist/conformance/run.d.ts +3 -0
- package/dist/conformance/run.js +53 -12
- package/dist/conformance/temp-roots.d.ts +31 -0
- package/dist/conformance/temp-roots.js +184 -0
- 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/default-model.d.ts +1 -1
- package/dist/default-model.js +5 -4
- package/dist/env-model-table.d.ts +163 -0
- package/dist/env-model-table.js +257 -0
- package/dist/file-edit.js +11 -15
- package/dist/fleet.d.ts +36 -7
- package/dist/fleet.js +83 -32
- package/dist/hash.d.ts +0 -9
- package/dist/hash.js +64 -7
- package/dist/hooks-config.js +18 -3
- package/dist/http-hook-allowlists.d.ts +22 -9
- package/dist/http-hook-allowlists.js +169 -91
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/mcp-request-entry.d.ts +67 -13
- package/dist/mcp-request-entry.js +132 -93
- package/dist/mcp-server-name.d.ts +17 -0
- package/dist/mcp-server-name.js +19 -0
- package/dist/migration-receipt.d.ts +10 -6
- package/dist/migration-receipt.js +5 -3
- package/dist/node.d.ts +1 -0
- package/dist/node.js +2 -0
- package/dist/one-time-migrations.d.ts +4 -2
- package/dist/one-time-migrations.js +236 -96
- package/dist/own-key-read.d.ts +247 -0
- package/dist/own-key-read.js +449 -0
- package/dist/plain-json-object.d.ts +11 -1
- package/dist/plain-json-object.js +37 -1
- package/dist/remote-exec.js +9 -1
- package/dist/retired-form.d.ts +19 -0
- package/dist/retired-form.js +24 -1
- package/dist/safety-merge-spec.d.ts +6 -6
- package/dist/safety-merge-spec.js +50 -37
- package/dist/sandbox-git.d.ts +64 -0
- package/dist/sandbox-git.js +89 -0
- package/dist/scheduler-store.js +19 -11
- package/dist/settings-units.js +14 -12
- package/dist/tier-words.d.ts +33 -0
- package/dist/tier-words.js +43 -0
- package/dist/types.d.ts +12 -2
- package/dist/types.js +35 -6
- package/dist/url-shape.d.ts +16 -0
- package/dist/url-shape.js +18 -0
- 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 +58 -2
- package/dist/workers-rows.js +38 -10
- package/dist/zod-unwrap.d.ts +6 -0
- package/dist/zod-unwrap.js +14 -7
- package/docs/API-SURFACE.md +100 -5
- package/docs/CONSUMER-INTEGRATION.md +250 -2
- package/llms-full.txt +26 -4
- package/llms.txt +11 -4
- package/package.json +2 -2
- package/dist/definition-copy.d.ts +0 -23
- package/dist/definition-copy.js +0 -52
package/dist/types.js
CHANGED
|
@@ -22,6 +22,7 @@ import { MAX_EXTRA_BODY_DEPTH } from "./hash.js"; // reconcile the extraBody dep
|
|
|
22
22
|
import { DispatchPolicy, FLEET_GENERATION_ENV, FLEET_SCOPE_ENV } from "./fleet.js"; // WorkerSpec.fleet.dispatch (scheduling framework D3)
|
|
23
23
|
import { isLoopbackAuthHost } from "./loopback.js";
|
|
24
24
|
import { isJsonArray, isPlainJsonObject } from "./plain-json-object.js"; // THE "is this a JSON object" predicate + the total array test
|
|
25
|
+
import { appendOwn, guardedSafeParse, isAccessorDescriptor } from "./own-key-read.js"; // the one spelling of "is this descriptor an accessor" (its own fields only) + the one guarded zod verdict
|
|
25
26
|
import { httpUrlFlaws, httpUrlRule } from "./url-shape.js"; // 11.0.0: THE URL shape judge // 11.0.0: the one narrow「本机」predicate (auth relaxation)
|
|
26
27
|
import { secretScanForms } from "./secret-scan.js"; // package-private normalization — the one set of forms both scanners read (not in the barrel)
|
|
27
28
|
import { WEB_ADMIN_ENV } from "./web-admin-env.js"; // the three connection keys the config renderer writes are reserved worker env
|
|
@@ -255,9 +256,9 @@ export function extraBodyUnsafe(v) {
|
|
|
255
256
|
for (const [k, desc] of Object.entries(Object.getOwnPropertyDescriptors(cur))) {
|
|
256
257
|
if (k === "length")
|
|
257
258
|
continue; // the array's own `length` data prop — not an element
|
|
258
|
-
if (
|
|
259
|
+
if (isAccessorDescriptor(desc))
|
|
259
260
|
return true; // accessor element — do NOT invoke
|
|
260
|
-
stack
|
|
261
|
+
appendOwn(stack, [desc.value, depth + 1]); // 定义语义压栈:`push` 会被继承的数字键 setter 吞掉,遍历一层都不下、秘密静默漏判
|
|
261
262
|
}
|
|
262
263
|
continue;
|
|
263
264
|
}
|
|
@@ -265,11 +266,11 @@ export function extraBodyUnsafe(v) {
|
|
|
265
266
|
return true; // Map/Set/Date/class/toJSON-bearing → not plain
|
|
266
267
|
// descriptor walk: never read a value via [] (would invoke a getter); reject accessors outright.
|
|
267
268
|
for (const [k, desc] of Object.entries(Object.getOwnPropertyDescriptors(cur))) {
|
|
268
|
-
if (
|
|
269
|
+
if (isAccessorDescriptor(desc))
|
|
269
270
|
return true; // accessor (get/set) — do NOT invoke
|
|
270
271
|
if (containsSecretToken(k))
|
|
271
272
|
return true; // secret-shaped KEY (tokenized). R8
|
|
272
|
-
stack
|
|
273
|
+
appendOwn(stack, [desc.value, depth + 1]); // 定义语义压栈:`push` 会被继承的数字键 setter 吞掉,遍历一层都不下、秘密静默漏判
|
|
273
274
|
}
|
|
274
275
|
}
|
|
275
276
|
return false;
|
|
@@ -455,6 +456,20 @@ export const PARSE_TRANSPARENT_KEYS = [
|
|
|
455
456
|
validatedBy: "consumer",
|
|
456
457
|
why: "它的形由同一条 override 的 `key` 决定(一个 key 一种形),执法点在读那个 key 的消费方;在这里写死形等于把所有可覆盖键的形表搬进本包。",
|
|
457
458
|
},
|
|
459
|
+
{
|
|
460
|
+
domain: "mcp",
|
|
461
|
+
path: "servers[].sessionMetaKey",
|
|
462
|
+
since: "14.8.0",
|
|
463
|
+
validatedBy: "engine-ingest",
|
|
464
|
+
why: "引擎在发给这台服务器的请求 `_meta` 里放发送腿会话 id 的那把键;它只有一只判官 = 引擎根导出 `judgeSessionMetaKey`(类型、非空、上界、MCP `_meta` 键名文法、MCP 保留前缀、`progressToken`、引擎的 `sema/` 命名空间一并判;坏键在引擎的准备门整条拒、点名这把键),投影它的消费方在交给引擎之前可调同一只;本包只承载 —— 这里哪怕只判类型与长度,也是第二只判官的一部分,与引擎那只各答各的。",
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
domain: "mcp",
|
|
468
|
+
path: "servers[].startupTimeoutMs",
|
|
469
|
+
since: "14.8.0",
|
|
470
|
+
validatedBy: "engine-ingest",
|
|
471
|
+
why: "引擎的逐服务器启动界(握手与首次工具列举各带它);它只有一只判官 = 引擎根导出的 `mcpStartupTimeoutMs`(是不是数、整数性、范围一并判;坏值在引擎的拨号门拒这一台服务器、点名 `config.mcp_startup_timeout_invalid`,其余服务器照挂),投影它的消费方在交给引擎之前可调同一只;本包只承载 —— 这里哪怕只判「是不是数」,也是第二只判官的一部分,与引擎那只各答各的。",
|
|
472
|
+
},
|
|
458
473
|
];
|
|
459
474
|
/** A catalog model's price / capability tier — fuels core 1.25 `selectModel({ tier })` role resolution. ONE closed set for both
|
|
460
475
|
* places that name it (`ModelEntry.tier`, `RoleTarget.select.tier`). Not the tier-group ladder of {@link TierBindings}
|
|
@@ -605,7 +620,7 @@ const RoleMap = z.partialRecord(ModelRole, RoleTarget, {
|
|
|
605
620
|
// 枚举键的 record 在运行期以 `unrecognized_keys`(带 `keys`)报未知键,zod 的型面只列了 invalid_type / invalid_key —— 按运行期形读(钉:role-words-12.0.0)。
|
|
606
621
|
error: (iss) => {
|
|
607
622
|
const raw = iss;
|
|
608
|
-
return raw.code === "unrecognized_keys" &&
|
|
623
|
+
return raw.code === "unrecognized_keys" && isJsonArray(raw.keys) ? retiredRoleWordsRefusal(raw.keys) : undefined;
|
|
609
624
|
},
|
|
610
625
|
});
|
|
611
626
|
/** {@link AtMention} 的模式闭集:`all` = 每个启用的模型都可 `@`;`none` = 一个都不许;`only` = 只许 `models` 里点名的那几个。 */
|
|
@@ -1607,7 +1622,11 @@ export function applyProjectPatch(doc, projectId, patch) {
|
|
|
1607
1622
|
const current = Object.prototype.hasOwnProperty.call(doc.projects, projectId) ? doc.projects[projectId] : undefined;
|
|
1608
1623
|
if (current === undefined)
|
|
1609
1624
|
throw new Error(`applyProjectPatch: project "${projectId}" is not registered in this document`);
|
|
1610
|
-
|
|
1625
|
+
// the patch is the caller's value: the parse and the first read of its error sit in one guard (another realm's odd value, a getter / trap that throws) ⇒ a rejection, never a throw
|
|
1626
|
+
const patchRead = guardedSafeParse(() => ProjectRegistrationPatch.safeParse(patch));
|
|
1627
|
+
if (!patchRead.ok)
|
|
1628
|
+
return { success: false, error: new z.ZodError([{ code: "custom", ...buildQuotedIssue `the patch could not be read during validation`, path: ["projects", projectId], input: undefined }]) };
|
|
1629
|
+
const parsed = patchRead.value;
|
|
1611
1630
|
if (!parsed.success) {
|
|
1612
1631
|
const issues = parsed.error.issues.map((i) => ({ ...i, path: ["projects", projectId, ...i.path] }));
|
|
1613
1632
|
return { success: false, error: new z.ZodError(issues) };
|
|
@@ -2481,6 +2500,16 @@ export const McpServerSpec = z.object({
|
|
|
2481
2500
|
* declared `"local"`, the engine read ABSENT. A value outside the table is REJECTED at parse — a mistyped trust
|
|
2482
2501
|
* word must never be read as a legal one (the consumer's gate is equally severe: no case-folding). */
|
|
2483
2502
|
contentOrigin: z.enum(MCP_CONTENT_ORIGINS).optional(),
|
|
2503
|
+
/** 引擎在发给这台服务器的每个 `tools/call` / `resources/read` 的 `params._meta[key]` 里放发送腿自己的会话 id 的那把键 —— 一一对应引擎的
|
|
2504
|
+
* `McpServerSpec.sessionMetaKey`。缺席 = 什么都不送(`.optional()`,不铸默认:会话 id 出进程是部署自己选的关联)。
|
|
2505
|
+
*
|
|
2506
|
+
* **解析透明** —— 形归谁校验、为什么不放这里:{@link PARSE_TRANSPARENT_KEYS}(`mcp.servers[].sessionMetaKey`)。 */
|
|
2507
|
+
sessionMetaKey: z.unknown().optional(),
|
|
2508
|
+
/** 这一台服务器自己的启动界(整数毫秒):握手(连接 + initialize)与首次工具列举各带它 —— 一一对应引擎的 `McpServerSpec.startupTimeoutMs`。
|
|
2509
|
+
* 缺席 = 引擎读进程级的缺省界(`.optional()`,不铸默认:没声明就是没声明);它只界启动阶段,不动单次调用的超时。
|
|
2510
|
+
*
|
|
2511
|
+
* **解析透明** —— 形归谁校验、为什么不放这里:{@link PARSE_TRANSPARENT_KEYS}(`mcp.servers[].startupTimeoutMs`)。 */
|
|
2512
|
+
startupTimeoutMs: z.unknown().optional(),
|
|
2484
2513
|
/** Declaration-plane GROUPING LABEL for this server — maps 1:1 onto the engine's `McpServerSpec.source`
|
|
2485
2514
|
* (core ≥7.5.0 echoes it verbatim on `wiring_manifest.mcp[].source`, single-line + 80-char cap, so a
|
|
2486
2515
|
* consumer can group servers by where they were declared). An OPEN set: CC's `plugin` / `user` /
|
package/dist/url-shape.d.ts
CHANGED
|
@@ -41,6 +41,22 @@ export declare function isHierarchicalUrl(raw: string): boolean;
|
|
|
41
41
|
* 层级式非 http(s) ⇒ `not-http-url` 之后照判部件。返回类型按 `forbid` 收窄:座位没禁的部件不会出现在答案里。
|
|
42
42
|
*/
|
|
43
43
|
export declare function httpUrlFlaws<P extends HttpUrlPart>(raw: string, forbid: readonly P[]): ReadonlyArray<"not-http-url" | P>;
|
|
44
|
+
/**
|
|
45
|
+
* **「后面要拼路径的基址」座位禁用的部件**(闭集、冻结、判序)—— 14.7.0 起的唯一一张。一个值若是别人要往它后面拼路径的基址
|
|
46
|
+
* (中心基址三席、本实例对中心公告的地址、会话网页链接的基址……):`userinfo` 把凭据放进一个会被记日志、转述、拼路径的值;`query` / `fragment`
|
|
47
|
+
* 挂在基址上,一拼路径就落到错的位置(`http://x?a=1` + `/v1/…` = `http://x?a=1/v1/…`)。**路径前缀不禁** —— 要拼的路径接在前缀后面。
|
|
48
|
+
* 这是**策略集,独立于判官词表**({@link HTTP_URL_PARTS} 说的是 URL 有哪些成分,本集说的是这种座位禁哪些):今天两者碰巧同三词,但本集不由词表派生 ——
|
|
49
|
+
* 词表加词时此处要显式裁(加进来还是明确不加),不会静默多禁一件。冻结:判官读的就是消费方手里的这一只。
|
|
50
|
+
* 按座位的本质命名,不按某个消费键名:谁的座位是「后面要拼路径的基址」,谁读它,不各自抄词。
|
|
51
|
+
*/
|
|
52
|
+
export declare const PATH_BASE_URL_FORBIDDEN_PARTS: readonly ["userinfo", "query", "fragment"];
|
|
53
|
+
/**
|
|
54
|
+
* 「后面要拼路径的基址」座位的判:`[]` = 合格,否则按判序交回规则词 —— 就是 {@link httpUrlFlaws} 配 {@link PATH_BASE_URL_FORBIDDEN_PARTS},
|
|
55
|
+
* 不另写判法;规则词的句子取 {@link httpUrlRule}。答案只有词,不含值的任何片段。判**占位展开之后**的值:`http://$(POD_IP):8090` 过
|
|
56
|
+
* (占位在主机位),`http://[$(POD_IP)]:8090` 拒(方括号里必须是 IPv6 字面量),展开后的 `http://[fd00::1]:8090` 过。
|
|
57
|
+
* 缺席 / 空串由调用方在判之前处理(对 `""` 本函数答 `not-http-url`)。
|
|
58
|
+
*/
|
|
59
|
+
export declare function pathBaseUrlFlaws(url: string): ReadonlyArray<HttpUrlFlaw>;
|
|
44
60
|
/** 以 `http://` / `https://` 起头(大小写不敏感)—— 一道**语法前缀**守卫,不是形判官:不解析、不看 host / 部件,所以 `https://` 这样的裸 scheme 它也收(见头注「第二个答案」)。
|
|
45
61
|
* 唯一读者是请求面 MCP 传输读器(`readRequestMcpTransport`);S-2106 收口时它退成那个读器座位的「照字面写全」规则,与形判官合取。 */
|
|
46
62
|
export declare function startsWithHttpScheme(raw: string): boolean;
|
package/dist/url-shape.js
CHANGED
|
@@ -57,6 +57,24 @@ export function httpUrlFlaws(raw, forbid) {
|
|
|
57
57
|
const shape = u.protocol === "http:" || u.protocol === "https:" ? [] : ["not-http-url"];
|
|
58
58
|
return [...shape, ...HTTP_URL_PARTS.filter((p) => forbid.includes(p) && present[p])];
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* **「后面要拼路径的基址」座位禁用的部件**(闭集、冻结、判序)—— 14.7.0 起的唯一一张。一个值若是别人要往它后面拼路径的基址
|
|
62
|
+
* (中心基址三席、本实例对中心公告的地址、会话网页链接的基址……):`userinfo` 把凭据放进一个会被记日志、转述、拼路径的值;`query` / `fragment`
|
|
63
|
+
* 挂在基址上,一拼路径就落到错的位置(`http://x?a=1` + `/v1/…` = `http://x?a=1/v1/…`)。**路径前缀不禁** —— 要拼的路径接在前缀后面。
|
|
64
|
+
* 这是**策略集,独立于判官词表**({@link HTTP_URL_PARTS} 说的是 URL 有哪些成分,本集说的是这种座位禁哪些):今天两者碰巧同三词,但本集不由词表派生 ——
|
|
65
|
+
* 词表加词时此处要显式裁(加进来还是明确不加),不会静默多禁一件。冻结:判官读的就是消费方手里的这一只。
|
|
66
|
+
* 按座位的本质命名,不按某个消费键名:谁的座位是「后面要拼路径的基址」,谁读它,不各自抄词。
|
|
67
|
+
*/
|
|
68
|
+
export const PATH_BASE_URL_FORBIDDEN_PARTS = Object.freeze(["userinfo", "query", "fragment"]);
|
|
69
|
+
/**
|
|
70
|
+
* 「后面要拼路径的基址」座位的判:`[]` = 合格,否则按判序交回规则词 —— 就是 {@link httpUrlFlaws} 配 {@link PATH_BASE_URL_FORBIDDEN_PARTS},
|
|
71
|
+
* 不另写判法;规则词的句子取 {@link httpUrlRule}。答案只有词,不含值的任何片段。判**占位展开之后**的值:`http://$(POD_IP):8090` 过
|
|
72
|
+
* (占位在主机位),`http://[$(POD_IP)]:8090` 拒(方括号里必须是 IPv6 字面量),展开后的 `http://[fd00::1]:8090` 过。
|
|
73
|
+
* 缺席 / 空串由调用方在判之前处理(对 `""` 本函数答 `not-http-url`)。
|
|
74
|
+
*/
|
|
75
|
+
export function pathBaseUrlFlaws(url) {
|
|
76
|
+
return httpUrlFlaws(url, PATH_BASE_URL_FORBIDDEN_PARTS);
|
|
77
|
+
}
|
|
60
78
|
/** 以 `http://` / `https://` 起头(大小写不敏感)—— 一道**语法前缀**守卫,不是形判官:不解析、不看 host / 部件,所以 `https://` 这样的裸 scheme 它也收(见头注「第二个答案」)。
|
|
61
79
|
* 唯一读者是请求面 MCP 传输读器(`readRequestMcpTransport`);S-2106 收口时它退成那个读器座位的「照字面写全」规则,与形判官合取。 */
|
|
62
80
|
export function startsWithHttpScheme(raw) {
|
|
@@ -25,6 +25,7 @@ import { NON_SECRET_URL_NO_MIGRATION } from "./types.js";
|
|
|
25
25
|
import { buildQuotedIssue } from "./quoted-issue.js";
|
|
26
26
|
import { RFC3339_DATE_TIME } from "./rfc3339.js"; // the package's one RFC 3339 date-time judge (a project registration's `archivedAt` reads the same one)
|
|
27
27
|
import { WEB_ADMIN_ENV } from "./web-admin-env.js";
|
|
28
|
+
import { guardedSafeParse } from "./own-key-read.js"; // the one guarded zod verdict (parse + building its error in one guard)
|
|
28
29
|
/** 信封 `kind` 的闭集今只有一员(`SEALED_BOX_ALG` 同式的字面常量:schema 的字面与读方的比较都引它)。 */
|
|
29
30
|
export const WEB_ADMIN_CREDENTIAL_ENVELOPE_KIND = "user-session";
|
|
30
31
|
/** 一枚**令牌串**的形 —— 裸行 bearer、信封里的 `accessToken` / `refreshToken` 共用这**一条**律:可打印 ASCII
|
|
@@ -99,8 +100,10 @@ export function parseWebAdminCredentialFile(text) {
|
|
|
99
100
|
// JSON.parse 的报错会引用出错位置附近的原文 —— 不转述它。
|
|
100
101
|
return { kind: "unreadable", reason: "envelope is not valid JSON" };
|
|
101
102
|
}
|
|
102
|
-
const parsed = WebAdminCredentialEnvelope.safeParse(json);
|
|
103
|
-
|
|
103
|
+
const parsed = guardedSafeParse(() => WebAdminCredentialEnvelope.safeParse(json)); // 拼拒句时抛(被污染的运行时)⇒ 读不出,不抛出去
|
|
104
|
+
if (!parsed.ok)
|
|
105
|
+
return { kind: "unreadable", reason: "envelope could not be read during validation" };
|
|
106
|
+
return parsed.value.success ? { kind: "envelope", envelope: parsed.value.data } : { kind: "unreadable", reason: envelopeRejection(parsed.value.error) };
|
|
104
107
|
}
|
|
105
108
|
const firstLine = trimmed.split(/[\r\n]/, 1)[0].trim(); // 行尾 = 第一个 CR 或 LF(CRLF / LF / 孤 CR 同判)
|
|
106
109
|
if (!TOKEN_CHARS.test(firstLine))
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 会话不在那一台上就落空。所以模板函数拒绝铸只带一位的链接;解析却**宽读**(旧链接、手输的链接可能缺 `agent`),缺的那一位就是缺席。
|
|
12
12
|
* 链接没有 scope 位(跨空间深链不在这一形里);日后加 scope 时作可选第三位、同一函数,不另开函数。
|
|
13
13
|
*/
|
|
14
|
-
import {
|
|
14
|
+
import { httpUrlRule, pathBaseUrlFlaws } from "./url-shape.js";
|
|
15
15
|
/** 链接的路径与两个参数名 —— 本文件私有,唯一一处。 */
|
|
16
16
|
const SESSION_LINK_PATH = "/chat";
|
|
17
17
|
const WORKER_PARAM = "agent";
|
|
@@ -29,7 +29,7 @@ function requiredPart(label, value) {
|
|
|
29
29
|
export function webAdminSessionUrl(webBaseUrl, target) {
|
|
30
30
|
if (typeof webBaseUrl !== "string")
|
|
31
31
|
throw new TypeError("webAdminSessionUrl: webBaseUrl must be a string (an absolute http(s) URL)");
|
|
32
|
-
const flaws =
|
|
32
|
+
const flaws = pathBaseUrlFlaws(webBaseUrl); // 「后面要拼路径的基址」座位的唯一判官(禁 userinfo / query / fragment,路径前缀放行)
|
|
33
33
|
if (flaws.length > 0)
|
|
34
34
|
throw new TypeError(`webAdminSessionUrl: webBaseUrl ${httpUrlRule(flaws[0])}`);
|
|
35
35
|
const worker = requiredPart("worker", target?.worker);
|
|
@@ -9,6 +9,17 @@
|
|
|
9
9
|
import { type ConfigPath } from "./config-fns.js";
|
|
10
10
|
/** 判的是哪一形的行(闭集):`stored` = 店里存着的那一行;`migrated` = 一次性迁移改写出来、还没写回的那一行。 */
|
|
11
11
|
export type WorkerRowForm = "stored" | "migrated";
|
|
12
|
+
/**
|
|
13
|
+
* zod 的 `invalid_type` 句子(`Invalid input: expected <类型>, received <类型>`)—— 与 zod 自己的话**逐字同**(对每种 JSON 值;`test/migration-row-verdict-14.7.0` 逐值对着 zod 的原句核,升级 zod 改措辞即红),
|
|
14
|
+
* 区别只有一处:zod 对「原型不是 Object.prototype 的对象」读 `input.constructor.name` 当类型名 —— 原型不是 `Object.prototype` 的(隔离原型)行快照上,用户写的**自有 `constructor` 键**就成了类型名:
|
|
15
|
+
* 整次调用抛(`constructor.name` 是个对象)、用户文本进拒因(`received hello world`)、句子变样。这里不读 constructor。别的 issue 不管(返回 `undefined` ⇒ 沿用 zod 的 / 包自己写的句子)。
|
|
16
|
+
* 只给本文件的终判调用(`safeParse` 的 `error` 参数);它在 schema 自带的句子之后、zod 内置句子之前生效。
|
|
17
|
+
*/
|
|
18
|
+
export declare function safeIssueMessage(issue: {
|
|
19
|
+
readonly code: string;
|
|
20
|
+
readonly expected?: unknown;
|
|
21
|
+
readonly input?: unknown;
|
|
22
|
+
}): string | undefined;
|
|
12
23
|
/** 一行的终判:读得进 ⇒ `undefined`;读不进 ⇒ 第一条 issue 的路径(相对该行)+ 拒句(见文件头)。 */
|
|
13
24
|
export declare function workerRowVerdict(row: Readonly<Record<string, unknown>>, form: WorkerRowForm): {
|
|
14
25
|
readonly field: ConfigPath;
|
|
@@ -8,23 +8,79 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { formatConfigPath, outgoingPath } from "./config-fns.js";
|
|
10
10
|
import { issuePath } from "./issue-path.js";
|
|
11
|
+
import { guardedSafeParse } from "./own-key-read.js";
|
|
12
|
+
import { jsonShapeOf } from "./plain-json-object.js";
|
|
13
|
+
import { retiredKeyHeldSentence, retiredKeyOfSentence } from "./retired-form.js";
|
|
11
14
|
import { WorkersConfig } from "./types.js";
|
|
12
15
|
/** 每一形的拒句主语 —— 穷举映射型:加一形不补这里即编译红。 */
|
|
13
16
|
const SUBJECT = {
|
|
14
17
|
stored: "the stored worker",
|
|
15
18
|
migrated: "after migration the worker",
|
|
16
19
|
};
|
|
20
|
+
/** 输入的「类型名」(zod 的 `invalid_type` 句子里 `received` 后面那个词):typeof / array / null,**从不读 `constructor`**。形状只问座位;数恒答 `number`
|
|
21
|
+
* (非有限的数到不了这里:{@link safeIssueMessage} 把它交还 zod 自己的句子)。 */
|
|
22
|
+
function receivedWord(input) {
|
|
23
|
+
switch (typeof input) {
|
|
24
|
+
case "number":
|
|
25
|
+
return "number";
|
|
26
|
+
case "object": {
|
|
27
|
+
const shape = jsonShapeOf(input);
|
|
28
|
+
switch (shape) {
|
|
29
|
+
case "null":
|
|
30
|
+
return "null";
|
|
31
|
+
case "array":
|
|
32
|
+
return "array";
|
|
33
|
+
case "plain":
|
|
34
|
+
case "non_plain_object":
|
|
35
|
+
case "uninspectable":
|
|
36
|
+
case "primitive":
|
|
37
|
+
return "object";
|
|
38
|
+
default: {
|
|
39
|
+
const unhandled = shape;
|
|
40
|
+
throw new Error(`receivedWord: unhandled shape ${String(unhandled)}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
return typeof input;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* zod 的 `invalid_type` 句子(`Invalid input: expected <类型>, received <类型>`)—— 与 zod 自己的话**逐字同**(对每种 JSON 值;`test/migration-row-verdict-14.7.0` 逐值对着 zod 的原句核,升级 zod 改措辞即红),
|
|
50
|
+
* 区别只有一处:zod 对「原型不是 Object.prototype 的对象」读 `input.constructor.name` 当类型名 —— 原型不是 `Object.prototype` 的(隔离原型)行快照上,用户写的**自有 `constructor` 键**就成了类型名:
|
|
51
|
+
* 整次调用抛(`constructor.name` 是个对象)、用户文本进拒因(`received hello world`)、句子变样。这里不读 constructor。别的 issue 不管(返回 `undefined` ⇒ 沿用 zod 的 / 包自己写的句子)。
|
|
52
|
+
* 只给本文件的终判调用(`safeParse` 的 `error` 参数);它在 schema 自带的句子之后、zod 内置句子之前生效。
|
|
53
|
+
*/
|
|
54
|
+
export function safeIssueMessage(issue) {
|
|
55
|
+
if (issue.code !== "invalid_type")
|
|
56
|
+
return undefined;
|
|
57
|
+
// 非有限的数(`NaN` / `±Infinity` —— JSON 写不出 `NaN`;`±Infinity` 只由溢出的数字字面量如 `1e400` 解析出来)的类型名 zod 自己怎么说各版本不同:不抢,交还 zod 自己的句子(它不读 constructor)
|
|
58
|
+
if (typeof issue.input === "number" && !Number.isFinite(issue.input))
|
|
59
|
+
return undefined;
|
|
60
|
+
const expected = issue.expected === "nan" ? "NaN" : String(issue.expected);
|
|
61
|
+
return `Invalid input: expected ${expected}, received ${receivedWord(issue.input)}`;
|
|
62
|
+
}
|
|
17
63
|
/** 一只单行域里的 issue 路径 → 相对那一行(去掉 `["workers", 0]` 前缀;不在那一行下的 issue 原样)。 */
|
|
18
64
|
function rowRelative(path) {
|
|
19
65
|
return path[0] === "workers" && path[1] === 0 ? path.slice(2) : path;
|
|
20
66
|
}
|
|
67
|
+
/** 一条 issue 在拒句里的样子:`<坐标>: <句子>`;墓碑 issue(旧形字段还在盘上)换成读面的说法 —— 坐标就是退役键名,拒句的文字里不点名它(结构化的 `field` 已经指到它),指现行位与出路。 */
|
|
68
|
+
function issueText(i) {
|
|
69
|
+
const tombstone = retiredKeyOfSentence(i.message);
|
|
70
|
+
return tombstone !== undefined ? retiredKeyHeldSentence(tombstone.form) : `${formatConfigPath(outgoingPath(rowRelative(issuePath(i))))}: ${i.message}`;
|
|
71
|
+
}
|
|
21
72
|
/** 一行的终判:读得进 ⇒ `undefined`;读不进 ⇒ 第一条 issue 的路径(相对该行)+ 拒句(见文件头)。 */
|
|
22
73
|
export function workerRowVerdict(row, form) {
|
|
23
|
-
|
|
74
|
+
// 校验过程抛(会抛的 getter / 陷阱:读侧交进来的是调用方的活对象;zod 4.6 首读 `error` 时拼句子抛 —— 被污染的运行时、另一 realm 的怪形值)⇒ 这一行判「读不出」
|
|
75
|
+
// (定句,不含任何用户文本),不抛出调用。首读 `error` 与 `safeParse` 同在一只守护里(`guardedSafeParse`),两版 zod 同答
|
|
76
|
+
const judged = guardedSafeParse(() => WorkersConfig.safeParse({ workers: [row] }, { error: (iss) => safeIssueMessage(iss) }));
|
|
77
|
+
if (!judged.ok)
|
|
78
|
+
return { field: [], reason: `${SUBJECT[form]} is not a valid WorkerSpec — a value could not be read during validation` };
|
|
79
|
+
const verdict = judged.value;
|
|
24
80
|
if (verdict.success)
|
|
25
81
|
return undefined;
|
|
26
82
|
const issues = verdict.error.issues;
|
|
27
|
-
const shown = issues.slice(0, 3).map(
|
|
83
|
+
const shown = issues.slice(0, 3).map(issueText);
|
|
28
84
|
const more = issues.length > shown.length ? ` (+${issues.length - shown.length} more)` : "";
|
|
29
85
|
const first = issues[0];
|
|
30
86
|
return { field: first === undefined ? [] : rowRelative(issuePath(first)), reason: `${SUBJECT[form]} is not a valid WorkerSpec — ${shown.join("; ")}${more}` };
|
package/dist/workers-rows.js
CHANGED
|
@@ -15,29 +15,57 @@
|
|
|
15
15
|
* web-admin 在列表里标红点名 `field` / `reason`。拒因只点名字段、不回显值。
|
|
16
16
|
*/
|
|
17
17
|
import { outgoingPath, redactSecrets } from "./config-fns.js";
|
|
18
|
+
import { appendOwn, dataDescriptor, ownDeclaredValueOf, ownElementsOf, ownEntriesOf } from "./own-key-read.js";
|
|
18
19
|
import { isJsonArray, isPlainJsonObject } from "./plain-json-object.js";
|
|
19
20
|
import { workerRowVerdict } from "./worker-row-verdict.js";
|
|
21
|
+
/**
|
|
22
|
+
* 扣下坏行后的文档:调用方文档的其余自有键 + 换掉的 `workers`。**不对调用方的整份文档做展开**(`{ ...raw }` 会对它做 `[[OwnPropertyKeys]]` + 逐键 `[[Get]]`:无关键上会抛的 getter / `ownKeys` 陷阱
|
|
23
|
+
* 把嵌入方自己抛的值带出调用)—— 其余键经内核的 `ownEntriesOf` 读(整容器律:每个自有串键一份描述符、只从描述符取值,全或无):**其余键里只要有一个读不成纯数据**(访问器 / 隐藏键)
|
|
24
|
+
* 或整份读不出(`ownKeys` / `getOwnPropertyDescriptor` 陷阱抛)⇒ 其余键**一个都不带出**,只剩 `workers`;符号键从不带出(内核只认串键)。workers 域只有 `workers` 一个键,
|
|
25
|
+
* 其余键本来就是未知键(读侧剥掉、只进未知键告警),所以这一答只影响那条告警点名哪些键。交出的是寻常对象。
|
|
26
|
+
*/
|
|
27
|
+
function withWorkers(raw, kept) {
|
|
28
|
+
const doc = {};
|
|
29
|
+
const entries = ownEntriesOf(raw);
|
|
30
|
+
// 键序同展开(`{ ...raw, workers: kept }`):`workers` 留在它原来的位置
|
|
31
|
+
if (entries.ok)
|
|
32
|
+
for (const [key, value] of entries.value)
|
|
33
|
+
Object.defineProperty(doc, key, dataDescriptor(key === "workers" ? kept : value));
|
|
34
|
+
if (!Object.hasOwn(doc, "workers"))
|
|
35
|
+
Object.defineProperty(doc, "workers", dataDescriptor(kept));
|
|
36
|
+
return doc;
|
|
37
|
+
}
|
|
20
38
|
/**
|
|
21
39
|
* 逐行读一份 workers 域**原始**文档(`{ workers: [...] }`)。不是这个形 ⇒ 原样返回、零扣下(判形是 `parseDomain` 的事)。
|
|
22
40
|
* 规则见文件头。
|
|
23
41
|
*/
|
|
24
42
|
export function readWorkersRows(raw) {
|
|
25
|
-
|
|
43
|
+
// 读调用方的对象一律经内核(一份描述符里取值,不问 `get` 陷阱;原型链上被污染的 `workers` / `name` 不是文档里写着的):
|
|
44
|
+
// · `workers` 键与每行的 `name` —— 严归类(逐键一次描述符,键缺席时再问一次 `has`;访问器 / 隐藏键 ⇒ 读不成纯数据);
|
|
45
|
+
// · `workers` 数组的元素 —— 按校验器解析数组的读法(内核的 `ownElementsOf`:只看下标键,数据下标取值、隐藏下标照读、访问器下标调它自己的 getter 一次;挂在数组上的非下标键不看)。
|
|
46
|
+
// `workers` 读不成纯数据 / 元素读不出(结构陷阱抛、下标的 getter 抛)⇒ 「不是这个形」,原样放行交整域读侧判(不按条丢:丢一行 = 对账方把那只 worker 当作已撤销);行的 `name` 读不出 ⇒ 「认不出名字」,该行原样留在 doc
|
|
47
|
+
if (!isPlainJsonObject(raw))
|
|
48
|
+
return { doc: raw, held: [] };
|
|
49
|
+
const workers = ownDeclaredValueOf(raw, "workers");
|
|
50
|
+
if (!workers.ok || !isJsonArray(workers.value))
|
|
51
|
+
return { doc: raw, held: [] };
|
|
52
|
+
const rows = ownElementsOf(workers.value);
|
|
53
|
+
if (!rows.ok)
|
|
26
54
|
return { doc: raw, held: [] };
|
|
27
|
-
const rows = raw.workers;
|
|
28
55
|
const kept = [];
|
|
29
56
|
const held = [];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
for (const [index, row] of rows.value) {
|
|
58
|
+
const name = isPlainJsonObject(row) ? ownDeclaredValueOf(row, "name") : undefined;
|
|
59
|
+
if (!isPlainJsonObject(row) || name === undefined || !name.ok || typeof name.value !== "string") {
|
|
60
|
+
appendOwn(kept, row); // 认不出名字:整域读侧判(这一读口的追加都走定义语义:`push` 会被继承的数字键 setter 吞掉)
|
|
61
|
+
continue;
|
|
34
62
|
}
|
|
35
63
|
const v = workerRowVerdict(row, "stored");
|
|
36
64
|
// 坐标与拒因都走出门那一道:issue 路径里的 Record 键是用户写的(例:一个不合法的 env 键名可能就是粘错位置的凭据)。
|
|
37
65
|
if (v === undefined)
|
|
38
|
-
kept
|
|
66
|
+
appendOwn(kept, row);
|
|
39
67
|
else
|
|
40
|
-
held
|
|
41
|
-
}
|
|
42
|
-
return held.length === 0 ? { doc: raw, held } : { doc:
|
|
68
|
+
appendOwn(held, { name: name.value, index, field: outgoingPath(v.field), reason: redactSecrets(v.reason), original: row });
|
|
69
|
+
}
|
|
70
|
+
return held.length === 0 ? { doc: raw, held } : { doc: withWorkers(raw, kept), held };
|
|
43
71
|
}
|
package/dist/zod-unwrap.d.ts
CHANGED
|
@@ -9,3 +9,9 @@ import { z } from "zod";
|
|
|
9
9
|
* 不是变换的那一侧。两侧都是 schema 的管道(`a.pipe(b)`)不是包装:到此为止(按它本身的形判,认不出就不抢救)。
|
|
10
10
|
* `.refine()` / `.superRefine()` 在 zod 4 里不是包装层(判据挂在同一只 schema 上),无须展开。 */
|
|
11
11
|
export declare function unwrapSchema(schema: z.ZodType): z.ZodType;
|
|
12
|
+
/** 剥一层包装:`wrapper` = `.optional()` 一类(`.unwrap()`);`preprocess` = `z.preprocess(f, schema)`(变换在**输入**侧,先于里面的 schema);`postprocess` = `.transform(f)`(变换在输出侧)。不是包装 ⇒ `undefined`。
|
|
13
|
+
* `unwrapSchema` 把三种都一视同仁地剥掉(「条目能不能单独解析」与变换无关);要区分的读者(按声明键投影:输入侧的变换会看到整个值,投影不了)用它。 */
|
|
14
|
+
export declare function unwrapOnce(cur: z.ZodType): {
|
|
15
|
+
readonly next: z.ZodType;
|
|
16
|
+
readonly via: "wrapper" | "preprocess" | "postprocess";
|
|
17
|
+
} | undefined;
|
package/dist/zod-unwrap.js
CHANGED
|
@@ -12,14 +12,21 @@ export function unwrapSchema(schema) {
|
|
|
12
12
|
let cur = schema;
|
|
13
13
|
// 有界:包装层数是 schema 作者写出来的常数,8 层远超实际(防被畸形 schema 拖住)。
|
|
14
14
|
for (let i = 0; i < 8; i++) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
else if (cur instanceof z.ZodPipe && cur.in instanceof z.ZodTransform)
|
|
18
|
-
cur = cur.out;
|
|
19
|
-
else if (cur instanceof z.ZodPipe && cur.out instanceof z.ZodTransform)
|
|
20
|
-
cur = cur.in;
|
|
21
|
-
else
|
|
15
|
+
const step = unwrapOnce(cur);
|
|
16
|
+
if (step === undefined)
|
|
22
17
|
return cur;
|
|
18
|
+
cur = step.next;
|
|
23
19
|
}
|
|
24
20
|
return cur;
|
|
25
21
|
}
|
|
22
|
+
/** 剥一层包装:`wrapper` = `.optional()` 一类(`.unwrap()`);`preprocess` = `z.preprocess(f, schema)`(变换在**输入**侧,先于里面的 schema);`postprocess` = `.transform(f)`(变换在输出侧)。不是包装 ⇒ `undefined`。
|
|
23
|
+
* `unwrapSchema` 把三种都一视同仁地剥掉(「条目能不能单独解析」与变换无关);要区分的读者(按声明键投影:输入侧的变换会看到整个值,投影不了)用它。 */
|
|
24
|
+
export function unwrapOnce(cur) {
|
|
25
|
+
if (cur instanceof z.ZodOptional || cur instanceof z.ZodNullable || cur instanceof z.ZodDefault || cur instanceof z.ZodPrefault || cur instanceof z.ZodCatch)
|
|
26
|
+
return { next: cur.unwrap(), via: "wrapper" };
|
|
27
|
+
if (cur instanceof z.ZodPipe && cur.in instanceof z.ZodTransform)
|
|
28
|
+
return { next: cur.out, via: "preprocess" };
|
|
29
|
+
if (cur instanceof z.ZodPipe && cur.out instanceof z.ZodTransform)
|
|
30
|
+
return { next: cur.in, via: "postprocess" };
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|