@sema-agent/settings-schema 10.1.0 → 11.0.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 +419 -0
- package/README.md +141 -65
- package/bin/settings-schema.mjs +13 -6
- package/conformance/VERSION +1 -1
- package/conformance/baseline.json +3 -1
- package/conformance/cases/c01-entry-salvage-then-typo/expected.json +3 -1
- package/conformance/cases/c02-hosts-clamp-stacked/expected.json +12 -5
- package/conformance/cases/c02-hosts-clamp-stacked/meta.json +1 -1
- package/conformance/cases/c03-literal-dotted-top-key/expected.json +22 -2
- package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +3 -1
- package/conformance/cases/c06-legacy-governance-lift/expected.json +11 -101
- package/conformance/cases/c06-legacy-governance-lift/meta.json +1 -1
- package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +3 -1
- package/conformance/cases/c08-limits-unknown-carried-64/expected.json +203 -2
- package/conformance/cases/c09-limits-unknown-carried-65/expected.json +204 -2
- package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +3 -1
- package/conformance/cases/c11-unicode-unknown-keys/expected.json +20 -2
- package/conformance/cases/c12-prototype-name-top-keys/expected.json +3 -1
- package/conformance/cases/c13-models-field-null/expected.json +3 -1
- package/conformance/cases/c14-models-field-empty-array/expected.json +6 -1
- package/conformance/cases/c15-models-field-number/expected.json +3 -1
- package/conformance/cases/c16-deep-nesting-205/expected.json +14 -2
- package/conformance/cases/c17-empty-domain-file/expected.json +3 -1
- package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +3 -1
- package/conformance/cases/c19-wire-envelope-no-version/expected.json +3 -1
- package/conformance/cases/c20-governance-absent-lifts/expected.json +11 -86
- package/conformance/cases/c20-governance-absent-lifts/meta.json +1 -1
- package/conformance/cases/c21-governance-empty-object-wins/expected.json +6 -5
- package/conformance/cases/c21-governance-empty-object-wins/input.json +1 -4
- package/conformance/cases/c21-governance-empty-object-wins/meta.json +1 -1
- package/conformance/cases/c23-mixed-validity-models/expected.json +3 -1
- package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +22 -2
- package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +6 -1
- package/conformance/cases/c26-hosts-array-1024/expected.json +3 -1
- package/conformance/cases/c27-hosts-array-1025/expected.json +6 -7175
- package/conformance/cases/c27-hosts-array-1025/meta.json +1 -1
- package/conformance/cases/c28-large-document-over-1mb/expected.json +14 -2
- package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +3 -1
- package/conformance/cases/c30-version-triple-a-first/expected.json +6 -1
- package/conformance/cases/c31-version-triple-b-changed/expected.json +6 -1
- package/conformance/cases/c32-version-triple-a-restored/expected.json +6 -1
- package/conformance/cases/c33-limits-nested-carried-key/expected.json +18 -2
- package/conformance/cases/c34-skills-content-vs-manifest/expected.json +19 -3
- package/conformance/cases/c35-portable-domain-file-is-null/expected.json +3 -1
- package/conformance/cases/c36-skills-entry-without-content/expected.json +6 -2
- package/conformance/cases/c37-skills-field-null/expected.json +6 -2
- package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +3 -1
- package/conformance/cases/c39-hidden-file-in-config-d/expected.json +3 -1
- package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +11 -4
- package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +9 -3
- package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +39 -2
- package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +4 -1
- package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +3 -1
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +19 -10
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +1 -1
- package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/expected.json +12 -5
- package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/meta.json +9 -0
- package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/expected.json +23 -3
- package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/input.json +6 -0
- package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/meta.json +10 -0
- package/conformance/cases/c48-s935-model-headers-transported/expected.json +6 -1
- package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +3 -1
- package/conformance/cases/c50-s784-lane-host-legal/expected.json +3 -1
- package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +3 -1
- package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +3 -1
- package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +3 -1
- package/conformance/cases/c54-s784-lane-adb-legal/expected.json +3 -1
- package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +3 -1
- package/conformance/cases/c56-s784-lane-device-legal/expected.json +3 -1
- package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +3 -1
- package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +3 -1
- package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +3 -1
- package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +3 -1
- package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +3 -1
- package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +3 -1
- package/conformance/cases/c63-s784-host-two-replicas/expected.json +3 -1
- package/conformance/cases/c64-s784-multitenant-local-store/expected.json +3 -1
- package/conformance/cases/c65-s784-pg-without-db/expected.json +3 -1
- package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +3 -1
- package/conformance/cases/c67-s784-retired-field-loud/expected.json +3 -1
- package/conformance/cases/c67-s784-retired-field-loud/meta.json +1 -1
- package/conformance/cases/c68-s784-device-local-store/expected.json +3 -1
- package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +3 -1
- package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +3 -1
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/expected.json +119 -0
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/input.json +24 -0
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/meta.json +10 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +111 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/input.json +26 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/meta.json +6 -0
- package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +3 -1
- package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +3 -1
- package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +6 -1
- package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +11 -1
- package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +11 -1
- package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +16 -2
- package/conformance/cases/cli04-env-value-with-newline/expected.json +3 -1
- package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +6 -1
- package/conformance/cases/t01-absent-domain-key-not-null/expected.json +3 -1
- package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +29 -2
- package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +3 -1
- package/conformance/cases/z01-every-domain-empty-object/expected.json +8 -4
- package/conformance/cases/z01-every-domain-empty-object/input.json +0 -1
- package/conformance/cases/z01-every-domain-empty-object/meta.json +1 -1
- package/conformance/cases/z02-every-domain-unknown-key/expected.json +163 -23
- package/conformance/cases/z02-every-domain-unknown-key/input.json +0 -3
- package/conformance/cases/z02-every-domain-unknown-key/meta.json +1 -1
- package/dist/api/auth-bridge.d.ts +51 -181
- package/dist/api/auth-bridge.js +58 -33
- package/dist/api/auth.d.ts +37 -146
- package/dist/api/auth.js +16 -26
- package/dist/api/feedback.d.ts +87 -0
- package/dist/api/feedback.js +66 -0
- package/dist/api/pull-token.d.ts +32 -0
- package/dist/api/pull-token.js +88 -0
- package/dist/api/scopes.d.ts +82 -174
- package/dist/api/scopes.js +23 -18
- package/dist/bundle.d.ts +15 -7
- package/dist/bundle.js +28 -27
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +132 -81
- package/dist/config-d-file.d.ts +14 -0
- package/dist/config-d-file.js +67 -0
- package/dist/config-fns.d.ts +87 -186
- package/dist/config-fns.js +207 -262
- package/dist/conformance/contract-adapter.d.ts +9 -2
- package/dist/conformance/contract-adapter.js +15 -7
- package/dist/conformance/corpus.d.ts +3 -0
- package/dist/conformance/corpus.js +5 -0
- package/dist/conformance/derive.d.ts +3 -0
- package/dist/conformance/index.d.ts +2 -2
- package/dist/conformance/index.js +2 -2
- package/dist/conformance/run.js +67 -22
- package/dist/conformance/types.d.ts +31 -15
- package/dist/conformance/types.js +5 -0
- package/dist/conformance/warnings.d.ts +1 -1
- package/dist/conformance/warnings.js +1 -4
- package/dist/cross-domain.d.ts +66 -22
- package/dist/cross-domain.js +164 -93
- package/dist/cross-session.d.ts +29 -11
- package/dist/default-model.d.ts +43 -0
- package/dist/default-model.js +34 -0
- package/dist/execution-lane-index.d.ts +4 -8
- package/dist/execution-lane-index.js +4 -8
- package/dist/execution-lanes.d.ts +31 -8
- package/dist/execution-lanes.js +23 -5
- package/dist/file-edit.js +5 -2
- package/dist/file-store.d.ts +7 -6
- package/dist/file-store.js +37 -50
- package/dist/fleet.d.ts +364 -762
- package/dist/fleet.js +277 -224
- package/dist/hooks.d.ts +1151 -4737
- package/dist/hooks.js +59 -15
- package/dist/index.d.ts +10 -4
- package/dist/index.js +13 -7
- package/dist/integer.d.ts +11 -0
- package/dist/integer.js +11 -0
- package/dist/issue-path.d.ts +10 -0
- package/dist/issue-path.js +10 -0
- package/dist/loopback.d.ts +26 -0
- package/dist/loopback.js +54 -0
- package/dist/migration-receipt.d.ts +42 -0
- package/dist/migration-receipt.js +12 -0
- package/dist/one-time-migrations.d.ts +56 -0
- package/dist/one-time-migrations.js +819 -0
- package/dist/prompts-digest.d.ts +1 -1
- package/dist/prompts-digest.js +3 -2
- package/dist/reader.d.ts +2 -2
- package/dist/remote-exec.d.ts +45 -187
- package/dist/remote-exec.js +13 -7
- package/dist/resolve-roster.d.ts +18 -3
- package/dist/resolve-roster.js +28 -25
- package/dist/retired-envs.d.ts +24 -0
- package/dist/retired-envs.js +114 -0
- package/dist/retired-form.d.ts +31 -0
- package/dist/retired-form.js +28 -0
- package/dist/safety-merge-spec.d.ts +131 -191
- package/dist/safety-merge-spec.js +18 -14
- package/dist/scheduler-lifetime.d.ts +5 -0
- package/dist/scheduler-lifetime.js +7 -0
- package/dist/scheduler-store-migration.d.ts +14 -0
- package/dist/scheduler-store-migration.js +14 -0
- package/dist/scheduler-store-node.d.ts +24 -9
- package/dist/scheduler-store-node.js +25 -15
- package/dist/scheduler-store.d.ts +47 -14
- package/dist/scheduler-store.js +32 -61
- package/dist/secret-refs.d.ts +3 -3
- package/dist/secret-refs.js +1 -1
- package/dist/secret-scan.d.ts +14 -24
- package/dist/secret-scan.js +45 -23
- package/dist/sha256-core.d.ts +16 -0
- package/dist/sha256-core.js +138 -0
- package/dist/sha256.d.ts +6 -1
- package/dist/sha256.js +8 -98
- package/dist/skills-manifest.js +1 -1
- package/dist/types.d.ts +1501 -13536
- package/dist/types.js +494 -438
- package/dist/url-shape.d.ts +34 -0
- package/dist/url-shape.js +61 -0
- package/dist/web-admin-credential.d.ts +96 -0
- package/dist/{center-credential.js → web-admin-credential.js} +46 -43
- package/dist/web-admin-env.d.ts +33 -0
- package/dist/web-admin-env.js +33 -0
- package/dist/worker-row-verdict.d.ts +16 -0
- package/dist/worker-row-verdict.js +31 -0
- package/dist/workers-rows.d.ts +34 -0
- package/dist/workers-rows.js +42 -0
- package/dist/zod-unwrap.d.ts +11 -0
- package/dist/zod-unwrap.js +25 -0
- package/package.json +17 -9
- package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +0 -6
- package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +0 -6
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +0 -49
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +0 -26
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +0 -4
- package/conformance/migrate-workers/m02-g5-idempotent/expected.json +0 -38
- package/conformance/migrate-workers/m02-g5-idempotent/input.json +0 -33
- package/conformance/migrate-workers/m02-g5-idempotent/meta.json +0 -5
- package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +0 -71
- package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +0 -42
- package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +0 -4
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +0 -52
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +0 -30
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +0 -4
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/expected.json +0 -82
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/input.json +0 -68
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/meta.json +0 -4
- package/dist/center-credential.d.ts +0 -116
- package/dist/issue-text.d.ts +0 -10
- package/dist/issue-text.js +0 -8
- package/dist/migrate-workers.d.ts +0 -71
- package/dist/migrate-workers.js +0 -351
- package/dist/migrate.d.ts +0 -255
- package/dist/migrate.js +0 -542
- /package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/input.json +0 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **「这个配置值是不是一个交给 `fetch` 的 http(s) URL、哪些部件禁」的唯一形判官**(11.0.0;lift 自 server `src/url-shape.ts`,
|
|
3
|
+
* 规则与拒因词闭集逐字搬)。本包 `NON_SECRET_URL` 由它定义;server 的同名判官改 re-export 本模块,client-core 改 import ——
|
|
4
|
+
* 同一个值在 web-admin 目录、server env 与客户端只有一个答案。
|
|
5
|
+
*
|
|
6
|
+
* ## 规则(一条)
|
|
7
|
+
* 一个座位问的只有两件事:① 它是不是一个**层级式的 http(s) URL**(`http(s)://host[:port][/path]` —— `//` authority 在场、
|
|
8
|
+
* scheme ∈ {http, https});② 这个座位禁掉的那几件部件(userinfo / query / fragment,闭集)在不在。答案是一串**规则词**
|
|
9
|
+
* ({@link httpUrlFlaws}),规则句由 {@link httpUrlRule} 给,句子只说规则、不含值。
|
|
10
|
+
*
|
|
11
|
+
* 部件按 WHATWG 序列化串(`href`)认:空 query `x?` 与空 fragment `x#` 在 `href` 里保留分隔符,`search` / `hash` 却是空串 ——
|
|
12
|
+
* 只看后者会把它们放过去(改前本包 `NON_SECRET_URL` 正是这样收下了 `https://gw/v1#`)。
|
|
13
|
+
*
|
|
14
|
+
* 拒句里值的样子(展示铸点)不在本模块:那是各宿主自己的展示律(server `buildRefusalShownUrl`)。本模块零 import,纯函数,浏览器安全。
|
|
15
|
+
*/
|
|
16
|
+
/** 座位可以禁掉的部件(闭集;判序 = 本数组序)。 */
|
|
17
|
+
export declare const HTTP_URL_PARTS: readonly ["userinfo", "query", "fragment"];
|
|
18
|
+
export type HttpUrlPart = (typeof HTTP_URL_PARTS)[number];
|
|
19
|
+
/** 判官的拒因词(闭集):形不对,或一件被禁的部件在场。 */
|
|
20
|
+
export type HttpUrlFlaw = "not-http-url" | HttpUrlPart;
|
|
21
|
+
/** 「不是一个 http(s) URL」的规则句 —— 不透明形、不可解析、层级式非 http(s) 同一句;只点名规则,不回显 scheme。 */
|
|
22
|
+
export declare const HTTP_URL_SHAPE_RULE = "is not a parseable http(s) URL (scheme must be http or https; it must look like http(s)://host[:port][/path])";
|
|
23
|
+
/**
|
|
24
|
+
* 层级式绝对 URL(`scheme://host…`,任意 scheme):能解析、且序列化后 `scheme:` 之后紧跟 `//`。`host:8888` / `alice:pw@host`
|
|
25
|
+
* 这类会被解析成**不透明** URL 的形(冒号前那一段被读成 scheme)不算。特殊 scheme(http / https …)恒层级式(WHATWG 补 `//`)。
|
|
26
|
+
*/
|
|
27
|
+
export declare function isHierarchicalUrl(raw: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* 判:合格 ⇒ `[]`;否则按判序交回规则词。形不对且不透明 ⇒ 只有 `not-http-url`(部件在不透明形上无从判,也不需要判 —— 那一形整只不收);
|
|
30
|
+
* 层级式非 http(s) ⇒ `not-http-url` 之后照判部件。返回类型按 `forbid` 收窄:座位没禁的部件不会出现在答案里。
|
|
31
|
+
*/
|
|
32
|
+
export declare function httpUrlFlaws<P extends HttpUrlPart>(raw: string, forbid: readonly P[]): ReadonlyArray<"not-http-url" | P>;
|
|
33
|
+
/** 一个拒因词的规则句(穷举;新词不补 = 编译红)。句子只说规则,不含值。 */
|
|
34
|
+
export declare function httpUrlRule(flaw: HttpUrlFlaw): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **「这个配置值是不是一个交给 `fetch` 的 http(s) URL、哪些部件禁」的唯一形判官**(11.0.0;lift 自 server `src/url-shape.ts`,
|
|
3
|
+
* 规则与拒因词闭集逐字搬)。本包 `NON_SECRET_URL` 由它定义;server 的同名判官改 re-export 本模块,client-core 改 import ——
|
|
4
|
+
* 同一个值在 web-admin 目录、server env 与客户端只有一个答案。
|
|
5
|
+
*
|
|
6
|
+
* ## 规则(一条)
|
|
7
|
+
* 一个座位问的只有两件事:① 它是不是一个**层级式的 http(s) URL**(`http(s)://host[:port][/path]` —— `//` authority 在场、
|
|
8
|
+
* scheme ∈ {http, https});② 这个座位禁掉的那几件部件(userinfo / query / fragment,闭集)在不在。答案是一串**规则词**
|
|
9
|
+
* ({@link httpUrlFlaws}),规则句由 {@link httpUrlRule} 给,句子只说规则、不含值。
|
|
10
|
+
*
|
|
11
|
+
* 部件按 WHATWG 序列化串(`href`)认:空 query `x?` 与空 fragment `x#` 在 `href` 里保留分隔符,`search` / `hash` 却是空串 ——
|
|
12
|
+
* 只看后者会把它们放过去(改前本包 `NON_SECRET_URL` 正是这样收下了 `https://gw/v1#`)。
|
|
13
|
+
*
|
|
14
|
+
* 拒句里值的样子(展示铸点)不在本模块:那是各宿主自己的展示律(server `buildRefusalShownUrl`)。本模块零 import,纯函数,浏览器安全。
|
|
15
|
+
*/
|
|
16
|
+
/** 座位可以禁掉的部件(闭集;判序 = 本数组序)。 */
|
|
17
|
+
export const HTTP_URL_PARTS = ["userinfo", "query", "fragment"];
|
|
18
|
+
/** 「不是一个 http(s) URL」的规则句 —— 不透明形、不可解析、层级式非 http(s) 同一句;只点名规则,不回显 scheme。 */
|
|
19
|
+
export const HTTP_URL_SHAPE_RULE = "is not a parseable http(s) URL (scheme must be http or https; it must look like http(s)://host[:port][/path])";
|
|
20
|
+
/**
|
|
21
|
+
* 层级式绝对 URL(`scheme://host…`,任意 scheme):能解析、且序列化后 `scheme:` 之后紧跟 `//`。`host:8888` / `alice:pw@host`
|
|
22
|
+
* 这类会被解析成**不透明** URL 的形(冒号前那一段被读成 scheme)不算。特殊 scheme(http / https …)恒层级式(WHATWG 补 `//`)。
|
|
23
|
+
*/
|
|
24
|
+
export function isHierarchicalUrl(raw) {
|
|
25
|
+
if (!URL.canParse(raw))
|
|
26
|
+
return false;
|
|
27
|
+
const u = new URL(raw);
|
|
28
|
+
return u.href.startsWith(`${u.protocol}//`);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 判:合格 ⇒ `[]`;否则按判序交回规则词。形不对且不透明 ⇒ 只有 `not-http-url`(部件在不透明形上无从判,也不需要判 —— 那一形整只不收);
|
|
32
|
+
* 层级式非 http(s) ⇒ `not-http-url` 之后照判部件。返回类型按 `forbid` 收窄:座位没禁的部件不会出现在答案里。
|
|
33
|
+
*/
|
|
34
|
+
export function httpUrlFlaws(raw, forbid) {
|
|
35
|
+
if (!isHierarchicalUrl(raw))
|
|
36
|
+
return ["not-http-url"];
|
|
37
|
+
const u = new URL(raw);
|
|
38
|
+
// 分隔符按 WHATWG 序列化串认:userinfo / path / query 里的 `#`、userinfo / path 里的 `?` 都被百分号编码,
|
|
39
|
+
// 所以 href 里第一个 `#` 就是 fragment 的分隔符、它之前第一个 `?` 就是 query 的分隔符。
|
|
40
|
+
const hashAt = u.href.indexOf("#");
|
|
41
|
+
const present = {
|
|
42
|
+
userinfo: u.username !== "" || u.password !== "",
|
|
43
|
+
query: (hashAt < 0 ? u.href : u.href.slice(0, hashAt)).includes("?"),
|
|
44
|
+
fragment: hashAt >= 0,
|
|
45
|
+
};
|
|
46
|
+
const shape = u.protocol === "http:" || u.protocol === "https:" ? [] : ["not-http-url"];
|
|
47
|
+
return [...shape, ...HTTP_URL_PARTS.filter((p) => forbid.includes(p) && present[p])];
|
|
48
|
+
}
|
|
49
|
+
/** 一个拒因词的规则句(穷举;新词不补 = 编译红)。句子只说规则,不含值。 */
|
|
50
|
+
export function httpUrlRule(flaw) {
|
|
51
|
+
switch (flaw) {
|
|
52
|
+
case "not-http-url":
|
|
53
|
+
return HTTP_URL_SHAPE_RULE;
|
|
54
|
+
case "userinfo":
|
|
55
|
+
return "must not carry userinfo (credentials in the URL are logged, reported and handed to fetch; they belong in a credential setting)";
|
|
56
|
+
case "query":
|
|
57
|
+
return "must not carry a query (request paths are appended to this base)";
|
|
58
|
+
case "fragment":
|
|
59
|
+
return "must not carry a fragment (it is never sent, and anything appended after it is dropped with it)";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* web-admin 凭证的**文件形契约** —— S-698(design/325 §1)。
|
|
3
|
+
*
|
|
4
|
+
* 引擎向 web-admin 出站时用的凭证由部署声明(见 server 的凭证模块,单属主);声明成文件时
|
|
5
|
+
* (`SEMA_WEB_ADMIN_TOKEN_FILE`),文件内容有两种**种类**,不是一种语义的两种拼法:
|
|
6
|
+
*
|
|
7
|
+
* · **裸行**(一个静态令牌):首行 = 静态 bearer,只收可打印 ASCII({@link TokenString} 的律)。它是用户 JWT 还是 worker
|
|
8
|
+
* 拉取令牌,由读方用 `looksLikeJwt`(`./api/auth-bridge`)自判 —— 本文件不重判种类。
|
|
9
|
+
* · **信封**:一个 JSON 对象,形 = {@link WebAdminCredentialEnvelope}。今只有一员 `kind:"user-session"`
|
|
10
|
+
* (人的登录会话:可轮换的访问令牌 + 单次有效的刷新令牌 + 到期时刻 + 签发它的 web-admin 根 URL)。worker 拉取
|
|
11
|
+
* 令牌**不进信封**:它不轮换、不属于人,保持裸行。
|
|
12
|
+
*
|
|
13
|
+
* 判别律(一条,{@link parseWebAdminCredentialFile} 是它的**唯一**实现):内容 `trim()` 后以 `{` 开头 ⇒ 只按
|
|
14
|
+
* 信封读,读不成 = `unreadable`(**不回落**成裸行 —— 一个 `{` 开头的裸 token 不存在,回落只会把一段坏 JSON
|
|
15
|
+
* 当 bearer 发出去);否则裸行;空 = `unreadable`。同一份内容只有一种读法(零双读)。
|
|
16
|
+
*
|
|
17
|
+
* 声明面(本文件尾段):凭证的二选一判官 {@link judgeWebAdminCredentialDeclaration},读 env 名表 {@link WEB_ADMIN_ENV}
|
|
18
|
+
* (`./web-admin-env`,八键)里的 `token` / `tokenFile` 两键。
|
|
19
|
+
*
|
|
20
|
+
* 纯契约:零 IO(读文件、写文件、续签都是消费方的事),browser-safe,与本包其余部分同一条纯度律。
|
|
21
|
+
*/
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
/** 信封 `kind` 的闭集今只有一员(`SEALED_BOX_ALG` 同式的字面常量:schema 的字面与读方的比较都引它)。 */
|
|
24
|
+
export declare const WEB_ADMIN_CREDENTIAL_ENVELOPE_KIND: "user-session";
|
|
25
|
+
/**
|
|
26
|
+
* web-admin 凭证**信封**(`SEMA_WEB_ADMIN_TOKEN_FILE` 指向的文件是一个 JSON 对象时的唯一形)。`.strict()`:
|
|
27
|
+
* 多一个键就不是这份契约(拼错的 `refresh_token` 被静默剥掉,续签就会拿着缺席的刷新令牌去撞 web-admin)。
|
|
28
|
+
*
|
|
29
|
+
* · `accessToken` —— 出站 bearer(web-admin 签的用户 JWT,形判 `looksLikeJwt`);读方拿它做身份分箱的方式与裸行
|
|
30
|
+
* `user-jwt` 逐字同。令牌形与裸行同一条律({@link TokenString})。
|
|
31
|
+
* · `refreshToken` —— 单次有效、严格轮换(`POST /api/v1/auth/token/refresh`,见 `./api/auth`);续签前**重读
|
|
32
|
+
* 文件**取当前值,不用内存旧值(另一个写者可能刚轮换过)。
|
|
33
|
+
* · `expiresAt` —— 访问令牌到期时刻(RFC 3339)。
|
|
34
|
+
* · `issuer` —— 签发它的 web-admin 的**基础 URL**(与读方连 web-admin 的根 URL 同形,可带路径前缀):续签只向同源的 web-admin 发,
|
|
35
|
+
* 同不同源由读方比。URL 形判据与 `remoteExec` 的端点同一只 {@link NON_SECRET_URL}(http(s)、有 host、userinfo 与
|
|
36
|
+
* query / fragment 里不许夹带凭据)—— 本包不另写一份更窄的 URL 律。
|
|
37
|
+
*/
|
|
38
|
+
export declare const WebAdminCredentialEnvelope: z.ZodObject<{
|
|
39
|
+
kind: z.ZodLiteral<"user-session">;
|
|
40
|
+
accessToken: z.ZodString;
|
|
41
|
+
refreshToken: z.ZodString;
|
|
42
|
+
expiresAt: z.ZodISODateTime;
|
|
43
|
+
issuer: z.ZodString;
|
|
44
|
+
}, z.core.$strict>;
|
|
45
|
+
export type WebAdminCredentialEnvelope = z.infer<typeof WebAdminCredentialEnvelope>;
|
|
46
|
+
/** {@link parseWebAdminCredentialFile} 的三态结果(判别键 `kind`)。`unreadable.reason` 只说**哪里**不对
|
|
47
|
+
* (信封字段名 / zod 问题码 / 未知键**个数**),从不回显文件内容 —— 文件里装的就是凭据。 */
|
|
48
|
+
export type WebAdminCredentialFile = {
|
|
49
|
+
kind: "bare";
|
|
50
|
+
bearer: string;
|
|
51
|
+
} | {
|
|
52
|
+
kind: "envelope";
|
|
53
|
+
envelope: WebAdminCredentialEnvelope;
|
|
54
|
+
} | {
|
|
55
|
+
kind: "unreadable";
|
|
56
|
+
reason: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* 凭证文件内容 → 三态({@link WebAdminCredentialFile})。判别律见模块头注,本函数是它的唯一实现:
|
|
60
|
+
*
|
|
61
|
+
* 1. `trim()`(含 BOM);空 ⇒ `unreadable("empty")`。
|
|
62
|
+
* 2. 以 `{` 开头 ⇒ 只按 {@link WebAdminCredentialEnvelope} 读:JSON 不成立或 schema 不过 ⇒ `unreadable`
|
|
63
|
+
* (**不回落**裸行)。
|
|
64
|
+
* 3. 否则 ⇒ 首行(第一个 CR 或 LF 之前,再剥一次空白)。它是令牌({@link TokenString} 的律)⇒ `bare`;
|
|
65
|
+
* 不是 ⇒ `unreadable`(放不进 Authorization 头的行不是 bearer)。多行文件只认首行。
|
|
66
|
+
*
|
|
67
|
+
* 纯函数、零 IO;读方负责读文件(以及文件不可读时自己的那条 `unreadable` 臂)。
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseWebAdminCredentialFile(text: string): WebAdminCredentialFile;
|
|
70
|
+
/** 引擎向 web-admin 出示的凭证 —— 闭集;每一份由 {@link WEB_ADMIN_ENV} 里的**一对** env 名声明(值键 | 文件键):
|
|
71
|
+
* `access` = 主凭证(`token` / `tokenFile`),`assertion` = 断言令牌(`assertionToken` / `assertionTokenFile`)。 */
|
|
72
|
+
export declare const WEB_ADMIN_CREDENTIALS: readonly ["access", "assertion"];
|
|
73
|
+
export type WebAdminCredential = (typeof WEB_ADMIN_CREDENTIALS)[number];
|
|
74
|
+
/** {@link judgeWebAdminCredentialDeclaration} 的结果:凭证在哪里声明。`env` 形带**值** —— 它就是凭据,只在读方
|
|
75
|
+
* 自己的凭证模块里流转,不进日志。 */
|
|
76
|
+
export type WebAdminCredentialDeclaration = {
|
|
77
|
+
from: "env";
|
|
78
|
+
token: string;
|
|
79
|
+
} | {
|
|
80
|
+
from: "file";
|
|
81
|
+
path: string;
|
|
82
|
+
} | {
|
|
83
|
+
from: "absent";
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* 读 `credential` 那一对声明键,判这份凭证在哪里声明 —— 二选一规则的**唯一**实现(每份凭证同一条规则、同一句拒句,只换键名):
|
|
87
|
+
*
|
|
88
|
+
* · 空串 = 未设(`undefined` 同);其余一律算「设了」(不 trim —— 与规则上提前 server 的读法逐字同)。
|
|
89
|
+
* · 两个都设 ⇒ **throw**(读方拒启):两个写者写同一个语义面,哪个赢都是猜。拒句点名两个键、不回显值。
|
|
90
|
+
* · 只设 `tokenFile` ⇒ `{from:"file", path}`,`path` **原样**:相对路径按 cwd 解析是读方的事(纯契约不碰
|
|
91
|
+
* `node:path`,也不知道读方的 cwd)。
|
|
92
|
+
* · 只设 `token` ⇒ `{from:"env", token}`;都不设 ⇒ `{from:"absent"}`。
|
|
93
|
+
*
|
|
94
|
+
* 只读这一对键,别的 env(另一份凭证的那一对也在内)一概不看。判哪一份由调用方说 —— 没有缺省。
|
|
95
|
+
*/
|
|
96
|
+
export declare function judgeWebAdminCredentialDeclaration(env: Readonly<Record<string, string | undefined>>, credential: WebAdminCredential): WebAdminCredentialDeclaration;
|
|
@@ -1,61 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* web-admin 凭证的**文件形契约** —— S-698(design/325 §1)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* (`
|
|
4
|
+
* 引擎向 web-admin 出站时用的凭证由部署声明(见 server 的凭证模块,单属主);声明成文件时
|
|
5
|
+
* (`SEMA_WEB_ADMIN_TOKEN_FILE`),文件内容有两种**种类**,不是一种语义的两种拼法:
|
|
6
6
|
*
|
|
7
|
-
* · **裸行**(
|
|
7
|
+
* · **裸行**(一个静态令牌):首行 = 静态 bearer,只收可打印 ASCII({@link TokenString} 的律)。它是用户 JWT 还是 worker
|
|
8
8
|
* 拉取令牌,由读方用 `looksLikeJwt`(`./api/auth-bridge`)自判 —— 本文件不重判种类。
|
|
9
|
-
* ·
|
|
10
|
-
* (人的登录会话:可轮换的访问令牌 + 单次有效的刷新令牌 + 到期时刻 +
|
|
9
|
+
* · **信封**:一个 JSON 对象,形 = {@link WebAdminCredentialEnvelope}。今只有一员 `kind:"user-session"`
|
|
10
|
+
* (人的登录会话:可轮换的访问令牌 + 单次有效的刷新令牌 + 到期时刻 + 签发它的 web-admin 根 URL)。worker 拉取
|
|
11
11
|
* 令牌**不进信封**:它不轮换、不属于人,保持裸行。
|
|
12
12
|
*
|
|
13
|
-
* 判别律(一条,{@link
|
|
13
|
+
* 判别律(一条,{@link parseWebAdminCredentialFile} 是它的**唯一**实现):内容 `trim()` 后以 `{` 开头 ⇒ 只按
|
|
14
14
|
* 信封读,读不成 = `unreadable`(**不回落**成裸行 —— 一个 `{` 开头的裸 token 不存在,回落只会把一段坏 JSON
|
|
15
15
|
* 当 bearer 发出去);否则裸行;空 = `unreadable`。同一份内容只有一种读法(零双读)。
|
|
16
16
|
*
|
|
17
|
-
* 声明面(
|
|
18
|
-
*
|
|
17
|
+
* 声明面(本文件尾段):凭证的二选一判官 {@link judgeWebAdminCredentialDeclaration},读 env 名表 {@link WEB_ADMIN_ENV}
|
|
18
|
+
* (`./web-admin-env`,八键)里的 `token` / `tokenFile` 两键。
|
|
19
19
|
*
|
|
20
20
|
* 纯契约:零 IO(读文件、写文件、续签都是消费方的事),browser-safe,与本包其余部分同一条纯度律。
|
|
21
21
|
*/
|
|
22
22
|
import { z } from "zod";
|
|
23
23
|
import { looksLikeJwt } from "./api/auth-bridge.js";
|
|
24
24
|
import { NON_SECRET_URL } from "./types.js";
|
|
25
|
+
import { WEB_ADMIN_ENV } from "./web-admin-env.js";
|
|
25
26
|
/** 信封 `kind` 的闭集今只有一员(`SEALED_BOX_ALG` 同式的字面常量:schema 的字面与读方的比较都引它)。 */
|
|
26
|
-
export const
|
|
27
|
-
/** RFC 3339 `date-time` 的**剖面**:zod 的 `datetime({offset:true})` 管日历有效性(月日范围、闰年、时分秒范围),
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
export const WEB_ADMIN_CREDENTIAL_ENVELOPE_KIND = "user-session";
|
|
28
|
+
/** RFC 3339 `date-time` 的**剖面**:zod 的 `z.iso.datetime({offset:true})` 管日历有效性(月日范围、闰年、时分秒范围),
|
|
29
|
+
* 这里的正则自己判另三处 —— 秒必备、数字偏移必须是 `±hh:mm`、偏移的时 00–23 / 分 00–59(`10:00Z`、`+0800` 与 `+99:99`
|
|
30
|
+
* 都不是 RFC 3339,后者 `Date.parse` 读成 NaN)。这三处的判据在本正则,不随 zod 哪一版收不收而变。大写 `T` / `Z`、不收闰秒 `:60`:读方普遍用
|
|
30
31
|
* `Date.parse` 取时刻,这是那组形里它处处读得准的子集。 */
|
|
31
|
-
const RFC3339_DATE_TIME = z
|
|
32
|
-
.string()
|
|
32
|
+
const RFC3339_DATE_TIME = z.iso
|
|
33
33
|
.datetime({ offset: true })
|
|
34
34
|
.regex(/T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, "must be an RFC 3339 date-time: seconds and a Z / ±hh:mm offset (hh 00-23, mm 00-59) are required");
|
|
35
35
|
/** 一枚**令牌串**的形 —— 裸行 bearer、信封里的 `accessToken` / `refreshToken` 共用这**一条**律:可打印 ASCII
|
|
36
36
|
* (0x20–0x7E),首尾是可见字符(0x21–0x7E)、中间可有空格(共享令牌可以是一句口令 —— 验修轮 N2:今天走得通的
|
|
37
37
|
* 这种令牌不许被本律拒掉)。控制字符、制表、非 ASCII、不可见字符一律不是令牌:判别器把它判 `unreadable`,而不是
|
|
38
38
|
* 发出去 —— 更不会把一段前面粘了不可见字符的信封 JSON 当 bearer 送出门(对抗轮 #7)。刻意比 RFC 6750 `b64token`
|
|
39
|
-
* 宽:部署手写的静态令牌可以带 `!` / `%` / 空格之类。**如实登记**:Latin-1 令牌(HTTP 头的 ByteString
|
|
39
|
+
* 宽:部署手写的静态令牌可以带 `!` / `%` / 空格之类。**如实登记**:Latin-1 令牌(HTTP 头的 ByteString 收、web-admin 也
|
|
40
40
|
* 逐字比得上)在这里判 `unreadable` —— 非 ASCII 与不可见字符(`\u0085` / `\u00AD`)分不开,取 fail-closed。 */
|
|
41
41
|
const TOKEN_CHARS = /^[\x21-\x7E](?:[\x20-\x7E]*[\x21-\x7E])?$/;
|
|
42
42
|
const TokenString = z.string().regex(TOKEN_CHARS, "must be a token: printable ASCII, no leading / trailing space, no control or non-ASCII characters");
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
* 多一个键就不是这份契约(拼错的 `refresh_token`
|
|
44
|
+
* web-admin 凭证**信封**(`SEMA_WEB_ADMIN_TOKEN_FILE` 指向的文件是一个 JSON 对象时的唯一形)。`.strict()`:
|
|
45
|
+
* 多一个键就不是这份契约(拼错的 `refresh_token` 被静默剥掉,续签就会拿着缺席的刷新令牌去撞 web-admin)。
|
|
46
46
|
*
|
|
47
|
-
* · `accessToken` —— 出站 bearer(
|
|
47
|
+
* · `accessToken` —— 出站 bearer(web-admin 签的用户 JWT,形判 `looksLikeJwt`);读方拿它做身份分箱的方式与裸行
|
|
48
48
|
* `user-jwt` 逐字同。令牌形与裸行同一条律({@link TokenString})。
|
|
49
49
|
* · `refreshToken` —— 单次有效、严格轮换(`POST /api/v1/auth/token/refresh`,见 `./api/auth`);续签前**重读
|
|
50
50
|
* 文件**取当前值,不用内存旧值(另一个写者可能刚轮换过)。
|
|
51
51
|
* · `expiresAt` —— 访问令牌到期时刻(RFC 3339)。
|
|
52
|
-
* · `issuer` ——
|
|
52
|
+
* · `issuer` —— 签发它的 web-admin 的**基础 URL**(与读方连 web-admin 的根 URL 同形,可带路径前缀):续签只向同源的 web-admin 发,
|
|
53
53
|
* 同不同源由读方比。URL 形判据与 `remoteExec` 的端点同一只 {@link NON_SECRET_URL}(http(s)、有 host、userinfo 与
|
|
54
54
|
* query / fragment 里不许夹带凭据)—— 本包不另写一份更窄的 URL 律。
|
|
55
55
|
*/
|
|
56
|
-
export const
|
|
56
|
+
export const WebAdminCredentialEnvelope = z
|
|
57
57
|
.object({
|
|
58
|
-
kind: z.literal(
|
|
58
|
+
kind: z.literal(WEB_ADMIN_CREDENTIAL_ENVELOPE_KIND),
|
|
59
59
|
accessToken: TokenString.refine(looksLikeJwt, "must be a JWT (three dot-separated segments) — a worker pull token never goes into an envelope"),
|
|
60
60
|
refreshToken: TokenString,
|
|
61
61
|
expiresAt: RFC3339_DATE_TIME,
|
|
@@ -63,13 +63,13 @@ export const CenterCredentialEnvelope = z
|
|
|
63
63
|
})
|
|
64
64
|
.strict();
|
|
65
65
|
/** 信封自己的字段名(schema 的键 —— reason 里只许出现这几个名字,不许出现用户写的键)。 */
|
|
66
|
-
const ENVELOPE_FIELDS = new Set(Object.keys(
|
|
66
|
+
const ENVELOPE_FIELDS = new Set(Object.keys(WebAdminCredentialEnvelope.shape));
|
|
67
67
|
/** 一次 schema 失败 → 不含文件内容的一句话。 */
|
|
68
68
|
function envelopeRejection(error) {
|
|
69
69
|
const fields = new Set(); // 一个字段挂两道判据(datetime + 剖面正则)时只说一次
|
|
70
70
|
let unknownKeys = 0;
|
|
71
71
|
for (const issue of error.issues) {
|
|
72
|
-
if (issue.code ===
|
|
72
|
+
if (issue.code === "unrecognized_keys") {
|
|
73
73
|
unknownKeys += issue.keys.length;
|
|
74
74
|
continue;
|
|
75
75
|
}
|
|
@@ -78,20 +78,20 @@ function envelopeRejection(error) {
|
|
|
78
78
|
fields.add(`${field} (${issue.code})`);
|
|
79
79
|
}
|
|
80
80
|
const parts = [...(fields.size > 0 ? [`fields rejected: ${[...fields].join(", ")}`] : []), ...(unknownKeys > 0 ? [`unknown keys: ${unknownKeys}`] : [])];
|
|
81
|
-
return `envelope does not match
|
|
81
|
+
return `envelope does not match WebAdminCredentialEnvelope — ${parts.join("; ")}`;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* 凭证文件内容 → 三态({@link
|
|
84
|
+
* 凭证文件内容 → 三态({@link WebAdminCredentialFile})。判别律见模块头注,本函数是它的唯一实现:
|
|
85
85
|
*
|
|
86
86
|
* 1. `trim()`(含 BOM);空 ⇒ `unreadable("empty")`。
|
|
87
|
-
* 2. 以 `{` 开头 ⇒ 只按 {@link
|
|
87
|
+
* 2. 以 `{` 开头 ⇒ 只按 {@link WebAdminCredentialEnvelope} 读:JSON 不成立或 schema 不过 ⇒ `unreadable`
|
|
88
88
|
* (**不回落**裸行)。
|
|
89
89
|
* 3. 否则 ⇒ 首行(第一个 CR 或 LF 之前,再剥一次空白)。它是令牌({@link TokenString} 的律)⇒ `bare`;
|
|
90
90
|
* 不是 ⇒ `unreadable`(放不进 Authorization 头的行不是 bearer)。多行文件只认首行。
|
|
91
91
|
*
|
|
92
92
|
* 纯函数、零 IO;读方负责读文件(以及文件不可读时自己的那条 `unreadable` 臂)。
|
|
93
93
|
*/
|
|
94
|
-
export function
|
|
94
|
+
export function parseWebAdminCredentialFile(text) {
|
|
95
95
|
const trimmed = text.trim();
|
|
96
96
|
if (trimmed === "")
|
|
97
97
|
return { kind: "unreadable", reason: "empty" };
|
|
@@ -104,7 +104,7 @@ export function parseCenterCredentialFile(text) {
|
|
|
104
104
|
// JSON.parse 的报错会引用出错位置附近的原文 —— 不转述它。
|
|
105
105
|
return { kind: "unreadable", reason: "envelope is not valid JSON" };
|
|
106
106
|
}
|
|
107
|
-
const parsed =
|
|
107
|
+
const parsed = WebAdminCredentialEnvelope.safeParse(json);
|
|
108
108
|
return parsed.success ? { kind: "envelope", envelope: parsed.data } : { kind: "unreadable", reason: envelopeRejection(parsed.error) };
|
|
109
109
|
}
|
|
110
110
|
const firstLine = trimmed.split(/[\r\n]/, 1)[0].trim(); // 行尾 = 第一个 CR 或 LF(CRLF / LF / 孤 CR 同判)
|
|
@@ -112,18 +112,17 @@ export function parseCenterCredentialFile(text) {
|
|
|
112
112
|
return { kind: "unreadable", reason: "bare line is not a token — printable ASCII only (no control or non-ASCII characters)" };
|
|
113
113
|
return { kind: "bare", bearer: firstLine };
|
|
114
114
|
}
|
|
115
|
-
// ──
|
|
115
|
+
// ── 凭证二选一判官(每份凭证读 {@link WEB_ADMIN_ENV} 里的一对键:值键 | 文件键)────────────────────────────
|
|
116
|
+
/** 引擎向 web-admin 出示的凭证 —— 闭集;每一份由 {@link WEB_ADMIN_ENV} 里的**一对** env 名声明(值键 | 文件键):
|
|
117
|
+
* `access` = 主凭证(`token` / `tokenFile`),`assertion` = 断言令牌(`assertionToken` / `assertionTokenFile`)。 */
|
|
118
|
+
export const WEB_ADMIN_CREDENTIALS = ["access", "assertion"];
|
|
119
|
+
/** 每份凭证的那一对键名。键是闭集的每一员(`Record` 穷举:闭集加一员而这里不给一对 = 编译错)。 */
|
|
120
|
+
const CREDENTIAL_ENV_PAIRS = {
|
|
121
|
+
access: { value: WEB_ADMIN_ENV.token, file: WEB_ADMIN_ENV.tokenFile },
|
|
122
|
+
assertion: { value: WEB_ADMIN_ENV.assertionToken, file: WEB_ADMIN_ENV.assertionTokenFile },
|
|
123
|
+
};
|
|
116
124
|
/**
|
|
117
|
-
*
|
|
118
|
-
* 引擎写)—— 键名是 wire 面,手抄的键名会静默漂。
|
|
119
|
-
*
|
|
120
|
-
* · `token`(`SEMA_REGISTRY_TOKEN`)—— 值即凭证(部署注入;读方在进程生命期内读一次)。
|
|
121
|
-
* · `tokenFile`(`SEMA_REGISTRY_TOKEN_FILE`)—— 凭证文件的路径,读方**每次取值都读文件**(轮换 / 换账号只需
|
|
122
|
-
* 改写文件,不重启);文件内容的读法见 {@link parseCenterCredentialFile}。
|
|
123
|
-
*/
|
|
124
|
-
export const CENTER_CREDENTIAL_ENV = { token: "SEMA_REGISTRY_TOKEN", tokenFile: "SEMA_REGISTRY_TOKEN_FILE" };
|
|
125
|
-
/**
|
|
126
|
-
* 读两个声明键,判凭证在哪里声明 —— 二选一规则的**唯一**实现:
|
|
125
|
+
* 读 `credential` 那一对声明键,判这份凭证在哪里声明 —— 二选一规则的**唯一**实现(每份凭证同一条规则、同一句拒句,只换键名):
|
|
127
126
|
*
|
|
128
127
|
* · 空串 = 未设(`undefined` 同);其余一律算「设了」(不 trim —— 与规则上提前 server 的读法逐字同)。
|
|
129
128
|
* · 两个都设 ⇒ **throw**(读方拒启):两个写者写同一个语义面,哪个赢都是猜。拒句点名两个键、不回显值。
|
|
@@ -131,10 +130,14 @@ export const CENTER_CREDENTIAL_ENV = { token: "SEMA_REGISTRY_TOKEN", tokenFile:
|
|
|
131
130
|
* `node:path`,也不知道读方的 cwd)。
|
|
132
131
|
* · 只设 `token` ⇒ `{from:"env", token}`;都不设 ⇒ `{from:"absent"}`。
|
|
133
132
|
*
|
|
134
|
-
*
|
|
133
|
+
* 只读这一对键,别的 env(另一份凭证的那一对也在内)一概不看。判哪一份由调用方说 —— 没有缺省。
|
|
135
134
|
*/
|
|
136
|
-
export function
|
|
137
|
-
const
|
|
135
|
+
export function judgeWebAdminCredentialDeclaration(env, credential) {
|
|
136
|
+
const pair = CREDENTIAL_ENV_PAIRS[credential];
|
|
137
|
+
// 类型面上不可达;JS 调用方漏传 / 传错第二参时响亮拒(不猜是哪一份凭证)。
|
|
138
|
+
if (pair === undefined)
|
|
139
|
+
throw new Error(`unknown web-admin credential ${JSON.stringify(credential)} — one of ${WEB_ADMIN_CREDENTIALS.join(" / ")}`);
|
|
140
|
+
const { value: tokenKey, file: fileKey } = pair;
|
|
138
141
|
const token = env[tokenKey] ?? "";
|
|
139
142
|
const file = env[fileKey] ?? "";
|
|
140
143
|
if (token !== "" && file !== "") {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 引擎连 web-admin 读的**全部 env 名** —— 一处定义(叶模块,零 import:凭证判官、墓碑表与 worker 保留名表都引它);server 读、web-admin 给 worker 渲染、壳给本机引擎写,三方 import。
|
|
3
|
+
* 键名是部署面,手抄的键名会静默漂(此前只有 `token` / `tokenFile` 有契约位,其余六个各仓手抄)。读方只读这张表里的名字,
|
|
4
|
+
* 旧名一概不读(零双读):旧名是 `RETIRED_ENVS` 里的墓碑,在场即拒启。
|
|
5
|
+
*
|
|
6
|
+
* · `url` —— web-admin 的根 URL(在场 = 配置走远端:引擎向它拉 effective)。
|
|
7
|
+
* · `token` —— 值即凭证(部署注入;读方在进程生命期内读一次)。与 `tokenFile` 二选一(判官
|
|
8
|
+
* `judgeWebAdminCredentialDeclaration(env, "access")`)。
|
|
9
|
+
* · `tokenFile` —— 凭证文件的路径,读方**每次取值都读文件**(轮换 / 换账号只需改写文件,不重启);文件内容的读法见
|
|
10
|
+
* `parseWebAdminCredentialFile`。
|
|
11
|
+
* · `assertionToken` / `assertionTokenFile` —— web-admin 断言令牌(`wat_` 前缀)的值 / 文件路径:与 `token` / `tokenFile` 同一条
|
|
12
|
+
* 二选一规则、同一只判官(`judgeWebAdminCredentialDeclaration(env, "assertion")`),两个都设 ⇒ 拒;文件同样每次取值都读。
|
|
13
|
+
* · `worker` —— 本引擎在 web-admin 上的 worker 名(按 worker 取 effective、实例登记、公钥上报)。
|
|
14
|
+
* · `liveUrl` —— fleet 活性路由组(`FLEET_LIVE_PATHS`:心跳 / 注销 / 续租)另走一条不看就绪的入口时的根 URL;缺席 = 用 `url`。
|
|
15
|
+
* · `webUrl` —— web-admin 人用的界面根 URL(引擎原样自报到 `/health`,壳按它拼深链;缺席即缺席)。
|
|
16
|
+
* · `dryRun` —— 只拉取,不应用。
|
|
17
|
+
* · `adminToken` —— 引擎以管理身份写 web-admin 时用的令牌(起服上报执行公钥)。
|
|
18
|
+
*
|
|
19
|
+
* 渲染器给每台 worker 写的三键(`url` / `worker` / `token`)同时是 worker env 的**保留名**(`RESERVED_WORKER_ENV`):运维在 worker 规格的
|
|
20
|
+
* `env` 里写它们,会与渲染器写的值打架。
|
|
21
|
+
*/
|
|
22
|
+
export declare const WEB_ADMIN_ENV: {
|
|
23
|
+
readonly url: "SEMA_WEB_ADMIN_URL";
|
|
24
|
+
readonly token: "SEMA_WEB_ADMIN_TOKEN";
|
|
25
|
+
readonly tokenFile: "SEMA_WEB_ADMIN_TOKEN_FILE";
|
|
26
|
+
readonly worker: "SEMA_WEB_ADMIN_WORKER";
|
|
27
|
+
readonly liveUrl: "SEMA_WEB_ADMIN_LIVE_URL";
|
|
28
|
+
readonly webUrl: "SEMA_WEB_ADMIN_WEB_URL";
|
|
29
|
+
readonly dryRun: "SEMA_WEB_ADMIN_DRY_RUN";
|
|
30
|
+
readonly adminToken: "SEMA_WEB_ADMIN_ADMIN_TOKEN";
|
|
31
|
+
readonly assertionToken: "SEMA_WEB_ADMIN_ASSERTION_TOKEN";
|
|
32
|
+
readonly assertionTokenFile: "SEMA_WEB_ADMIN_ASSERTION_TOKEN_FILE";
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 引擎连 web-admin 读的**全部 env 名** —— 一处定义(叶模块,零 import:凭证判官、墓碑表与 worker 保留名表都引它);server 读、web-admin 给 worker 渲染、壳给本机引擎写,三方 import。
|
|
3
|
+
* 键名是部署面,手抄的键名会静默漂(此前只有 `token` / `tokenFile` 有契约位,其余六个各仓手抄)。读方只读这张表里的名字,
|
|
4
|
+
* 旧名一概不读(零双读):旧名是 `RETIRED_ENVS` 里的墓碑,在场即拒启。
|
|
5
|
+
*
|
|
6
|
+
* · `url` —— web-admin 的根 URL(在场 = 配置走远端:引擎向它拉 effective)。
|
|
7
|
+
* · `token` —— 值即凭证(部署注入;读方在进程生命期内读一次)。与 `tokenFile` 二选一(判官
|
|
8
|
+
* `judgeWebAdminCredentialDeclaration(env, "access")`)。
|
|
9
|
+
* · `tokenFile` —— 凭证文件的路径,读方**每次取值都读文件**(轮换 / 换账号只需改写文件,不重启);文件内容的读法见
|
|
10
|
+
* `parseWebAdminCredentialFile`。
|
|
11
|
+
* · `assertionToken` / `assertionTokenFile` —— web-admin 断言令牌(`wat_` 前缀)的值 / 文件路径:与 `token` / `tokenFile` 同一条
|
|
12
|
+
* 二选一规则、同一只判官(`judgeWebAdminCredentialDeclaration(env, "assertion")`),两个都设 ⇒ 拒;文件同样每次取值都读。
|
|
13
|
+
* · `worker` —— 本引擎在 web-admin 上的 worker 名(按 worker 取 effective、实例登记、公钥上报)。
|
|
14
|
+
* · `liveUrl` —— fleet 活性路由组(`FLEET_LIVE_PATHS`:心跳 / 注销 / 续租)另走一条不看就绪的入口时的根 URL;缺席 = 用 `url`。
|
|
15
|
+
* · `webUrl` —— web-admin 人用的界面根 URL(引擎原样自报到 `/health`,壳按它拼深链;缺席即缺席)。
|
|
16
|
+
* · `dryRun` —— 只拉取,不应用。
|
|
17
|
+
* · `adminToken` —— 引擎以管理身份写 web-admin 时用的令牌(起服上报执行公钥)。
|
|
18
|
+
*
|
|
19
|
+
* 渲染器给每台 worker 写的三键(`url` / `worker` / `token`)同时是 worker env 的**保留名**(`RESERVED_WORKER_ENV`):运维在 worker 规格的
|
|
20
|
+
* `env` 里写它们,会与渲染器写的值打架。
|
|
21
|
+
*/
|
|
22
|
+
export const WEB_ADMIN_ENV = {
|
|
23
|
+
url: "SEMA_WEB_ADMIN_URL",
|
|
24
|
+
token: "SEMA_WEB_ADMIN_TOKEN",
|
|
25
|
+
tokenFile: "SEMA_WEB_ADMIN_TOKEN_FILE",
|
|
26
|
+
worker: "SEMA_WEB_ADMIN_WORKER",
|
|
27
|
+
liveUrl: "SEMA_WEB_ADMIN_LIVE_URL",
|
|
28
|
+
webUrl: "SEMA_WEB_ADMIN_WEB_URL",
|
|
29
|
+
dryRun: "SEMA_WEB_ADMIN_DRY_RUN",
|
|
30
|
+
adminToken: "SEMA_WEB_ADMIN_ADMIN_TOKEN",
|
|
31
|
+
assertionToken: "SEMA_WEB_ADMIN_ASSERTION_TOKEN",
|
|
32
|
+
assertionTokenFile: "SEMA_WEB_ADMIN_ASSERTION_TOKEN_FILE",
|
|
33
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶、不进 exports 表):workers 域**一行**的终判与它的拒句拼装 —— 只此一处。两个调用方各判一种形:读口 `readWorkersRows`
|
|
3
|
+
* 判**存储形**的行(读不进 ⇒ 扣下),一次性迁移 SS-M4 判**改写后**的行(读不进 ⇒ 不写回半成品,收据 `refused`)。
|
|
4
|
+
*
|
|
5
|
+
* 判据只有读侧那一只 schema:只含这一行的 workers 域读不读得进(`WorkersConfig` —— 与 `parseDomain("workers")` 同一只,不另抄任何规则)。
|
|
6
|
+
* 拒句 = `<主语> is not a valid WorkerSpec — <至多三条 issue 的「坐标: 句子」>( (+n more))`,主语点名判的是哪一形(闭集两词);句中坐标已过出门那一道。
|
|
7
|
+
* 交回的 `field`(第一条 issue 的路径,相对该行)与整句**还没**过出门:出门归各自的铸点(读口的 `held`、迁移的收据),各过一次。
|
|
8
|
+
*/
|
|
9
|
+
import { type ConfigPath } from "./config-fns.js";
|
|
10
|
+
/** 判的是哪一形的行(闭集):`stored` = 店里存着的那一行;`migrated` = 一次性迁移改写出来、还没写回的那一行。 */
|
|
11
|
+
export type WorkerRowForm = "stored" | "migrated";
|
|
12
|
+
/** 一行的终判:读得进 ⇒ `undefined`;读不进 ⇒ 第一条 issue 的路径(相对该行)+ 拒句(见文件头)。 */
|
|
13
|
+
export declare function workerRowVerdict(row: Readonly<Record<string, unknown>>, form: WorkerRowForm): {
|
|
14
|
+
readonly field: ConfigPath;
|
|
15
|
+
readonly reason: string;
|
|
16
|
+
} | undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶、不进 exports 表):workers 域**一行**的终判与它的拒句拼装 —— 只此一处。两个调用方各判一种形:读口 `readWorkersRows`
|
|
3
|
+
* 判**存储形**的行(读不进 ⇒ 扣下),一次性迁移 SS-M4 判**改写后**的行(读不进 ⇒ 不写回半成品,收据 `refused`)。
|
|
4
|
+
*
|
|
5
|
+
* 判据只有读侧那一只 schema:只含这一行的 workers 域读不读得进(`WorkersConfig` —— 与 `parseDomain("workers")` 同一只,不另抄任何规则)。
|
|
6
|
+
* 拒句 = `<主语> is not a valid WorkerSpec — <至多三条 issue 的「坐标: 句子」>( (+n more))`,主语点名判的是哪一形(闭集两词);句中坐标已过出门那一道。
|
|
7
|
+
* 交回的 `field`(第一条 issue 的路径,相对该行)与整句**还没**过出门:出门归各自的铸点(读口的 `held`、迁移的收据),各过一次。
|
|
8
|
+
*/
|
|
9
|
+
import { formatConfigPath, outgoingPath } from "./config-fns.js";
|
|
10
|
+
import { issuePath } from "./issue-path.js";
|
|
11
|
+
import { WorkersConfig } from "./types.js";
|
|
12
|
+
/** 每一形的拒句主语 —— 穷举映射型:加一形不补这里即编译红。 */
|
|
13
|
+
const SUBJECT = {
|
|
14
|
+
stored: "the stored worker",
|
|
15
|
+
migrated: "after migration the worker",
|
|
16
|
+
};
|
|
17
|
+
/** 一只单行域里的 issue 路径 → 相对那一行(去掉 `["workers", 0]` 前缀;不在那一行下的 issue 原样)。 */
|
|
18
|
+
function rowRelative(path) {
|
|
19
|
+
return path[0] === "workers" && path[1] === 0 ? path.slice(2) : path;
|
|
20
|
+
}
|
|
21
|
+
/** 一行的终判:读得进 ⇒ `undefined`;读不进 ⇒ 第一条 issue 的路径(相对该行)+ 拒句(见文件头)。 */
|
|
22
|
+
export function workerRowVerdict(row, form) {
|
|
23
|
+
const verdict = WorkersConfig.safeParse({ workers: [row] });
|
|
24
|
+
if (verdict.success)
|
|
25
|
+
return undefined;
|
|
26
|
+
const issues = verdict.error.issues;
|
|
27
|
+
const shown = issues.slice(0, 3).map((i) => `${formatConfigPath(outgoingPath(rowRelative(issuePath(i))))}: ${i.message}`);
|
|
28
|
+
const more = issues.length > shown.length ? ` (+${issues.length - shown.length} more)` : "";
|
|
29
|
+
const first = issues[0];
|
|
30
|
+
return { field: first === undefined ? [] : rowRelative(issuePath(first)), reason: `${SUBJECT[form]} is not a valid WorkerSpec — ${shown.join("; ")}${more}` };
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workers 域的**逐行终判**(11.0.0 从 7.0.0 的一次性迁移里拆出 —— 那只函数同时装着「6.x 形改写」与「逐行终判」两件事;改写搬进
|
|
3
|
+
* 一次性迁移 SS-M4,终判留在这里,是 workers 域的**永久读语义**,不是迁移)。纯函数、无 I/O、不改入参。
|
|
4
|
+
*
|
|
5
|
+
* ## 为什么要逐行
|
|
6
|
+
* workers 是 gate 域(读即抛):整域 `parseDomain("workers")` 遇一行坏就整域抛,舰队清单整份读不出 —— 一只坏 worker 拖黑整个舰队。
|
|
7
|
+
* web-admin 的 workers 读口先过本函数:每一只**认得出名字**的 worker 单独过读侧自己的判据(只含这一行的 workers 域读不读得进,
|
|
8
|
+
* `WorkersConfig` —— 与 `parseDomain("workers")` 同一只 schema,不另抄任何规则;判据与拒句拼装在包内私有的 `./worker-row-verdict`,
|
|
9
|
+
* 一次性迁移 SS-M4 判改写后的行调同一只);读不进的那一行扣下(`held`,带原文与拒因),
|
|
10
|
+
* 其余照常进 `doc`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(不是对象 / 没有字符串名字 —— 原样留在 `doc`)与跨行规则
|
|
11
|
+
* (名字 / 路由前缀唯一 —— 两行单独都读得进、合起来才不行,这里不替运维挑留哪一行)。
|
|
12
|
+
*
|
|
13
|
+
* 被扣下的行**不进** `doc`,但随 `held[].original` 逐字带回:`doc` ∪ `held[].original` 覆盖输入的全部 worker。读口不写回 —— 原文留在盘上,
|
|
14
|
+
* web-admin 在列表里标红点名 `field` / `reason`。拒因只点名字段、不回显值。
|
|
15
|
+
*/
|
|
16
|
+
import { type ConfigPath } from "./config-fns.js";
|
|
17
|
+
/** 被扣下的一行:`field` 相对该 worker 条目(读侧 schema issue 的路径),`reason` 只点名字段、不回显值,`original` = 输入里那一行逐字(同一个值)。 */
|
|
18
|
+
export interface HeldWorkerRow {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly index: number;
|
|
21
|
+
readonly field: ConfigPath;
|
|
22
|
+
readonly reason: string;
|
|
23
|
+
readonly original: unknown;
|
|
24
|
+
}
|
|
25
|
+
/** {@link readWorkersRows} 的结果:`doc` = 扣下坏行后的文档(什么都没扣 ⇒ 输入的同一引用),`held` = 被扣下的行。 */
|
|
26
|
+
export interface WorkersRowsRead {
|
|
27
|
+
readonly doc: unknown;
|
|
28
|
+
readonly held: readonly HeldWorkerRow[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 逐行读一份 workers 域**原始**文档(`{ workers: [...] }`)。不是这个形 ⇒ 原样返回、零扣下(判形是 `parseDomain` 的事)。
|
|
32
|
+
* 规则见文件头。
|
|
33
|
+
*/
|
|
34
|
+
export declare function readWorkersRows(raw: unknown): WorkersRowsRead;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workers 域的**逐行终判**(11.0.0 从 7.0.0 的一次性迁移里拆出 —— 那只函数同时装着「6.x 形改写」与「逐行终判」两件事;改写搬进
|
|
3
|
+
* 一次性迁移 SS-M4,终判留在这里,是 workers 域的**永久读语义**,不是迁移)。纯函数、无 I/O、不改入参。
|
|
4
|
+
*
|
|
5
|
+
* ## 为什么要逐行
|
|
6
|
+
* workers 是 gate 域(读即抛):整域 `parseDomain("workers")` 遇一行坏就整域抛,舰队清单整份读不出 —— 一只坏 worker 拖黑整个舰队。
|
|
7
|
+
* web-admin 的 workers 读口先过本函数:每一只**认得出名字**的 worker 单独过读侧自己的判据(只含这一行的 workers 域读不读得进,
|
|
8
|
+
* `WorkersConfig` —— 与 `parseDomain("workers")` 同一只 schema,不另抄任何规则;判据与拒句拼装在包内私有的 `./worker-row-verdict`,
|
|
9
|
+
* 一次性迁移 SS-M4 判改写后的行调同一只);读不进的那一行扣下(`held`,带原文与拒因),
|
|
10
|
+
* 其余照常进 `doc`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(不是对象 / 没有字符串名字 —— 原样留在 `doc`)与跨行规则
|
|
11
|
+
* (名字 / 路由前缀唯一 —— 两行单独都读得进、合起来才不行,这里不替运维挑留哪一行)。
|
|
12
|
+
*
|
|
13
|
+
* 被扣下的行**不进** `doc`,但随 `held[].original` 逐字带回:`doc` ∪ `held[].original` 覆盖输入的全部 worker。读口不写回 —— 原文留在盘上,
|
|
14
|
+
* web-admin 在列表里标红点名 `field` / `reason`。拒因只点名字段、不回显值。
|
|
15
|
+
*/
|
|
16
|
+
import { outgoingPath, redactSecrets } from "./config-fns.js";
|
|
17
|
+
import { workerRowVerdict } from "./worker-row-verdict.js";
|
|
18
|
+
const isRec = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
19
|
+
/**
|
|
20
|
+
* 逐行读一份 workers 域**原始**文档(`{ workers: [...] }`)。不是这个形 ⇒ 原样返回、零扣下(判形是 `parseDomain` 的事)。
|
|
21
|
+
* 规则见文件头。
|
|
22
|
+
*/
|
|
23
|
+
export function readWorkersRows(raw) {
|
|
24
|
+
if (!isRec(raw) || !Array.isArray(raw.workers))
|
|
25
|
+
return { doc: raw, held: [] };
|
|
26
|
+
const rows = raw.workers;
|
|
27
|
+
const kept = [];
|
|
28
|
+
const held = [];
|
|
29
|
+
rows.forEach((row, index) => {
|
|
30
|
+
if (!isRec(row) || typeof row.name !== "string") {
|
|
31
|
+
kept.push(row); // 认不出名字:整域读侧判
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const v = workerRowVerdict(row, "stored");
|
|
35
|
+
// 坐标与拒因都走出门那一道:issue 路径里的 Record 键是用户写的(例:一个不合法的 env 键名可能就是粘错位置的凭据)。
|
|
36
|
+
if (v === undefined)
|
|
37
|
+
kept.push(row);
|
|
38
|
+
else
|
|
39
|
+
held.push({ name: row.name, index, field: outgoingPath(v.field), reason: redactSecrets(v.reason), original: row });
|
|
40
|
+
});
|
|
41
|
+
return held.length === 0 ? { doc: raw, held } : { doc: { ...raw, workers: kept }, held };
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶):zod 包装层的**一只**读法 —— 逐条容错(`salvageEntries`,config-fns)与一次性迁移(SS-M7 找 URL 座位)共用。
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
/** 展开 zod 的包装层,拿到里面真正的那个 schema。包装不改变「条目能不能单独解析」这件事。
|
|
6
|
+
*
|
|
7
|
+
* 包装层闭集:`.optional()` / `.nullable()` / `.default()` / `.prefault()` / `.catch()`(都是 `.unwrap()` 一层),以及
|
|
8
|
+
* **一侧是变换**的管道 —— `.transform(f)` 是「schema → 变换」、`z.preprocess(f, schema)` 是「变换 → schema」,两者都取
|
|
9
|
+
* 不是变换的那一侧。两侧都是 schema 的管道(`a.pipe(b)`)不是包装:到此为止(按它本身的形判,认不出就不抢救)。
|
|
10
|
+
* `.refine()` / `.superRefine()` 在 zod 4 里不是包装层(判据挂在同一只 schema 上),无须展开。 */
|
|
11
|
+
export declare function unwrapSchema(schema: z.ZodType): z.ZodType;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶):zod 包装层的**一只**读法 —— 逐条容错(`salvageEntries`,config-fns)与一次性迁移(SS-M7 找 URL 座位)共用。
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
/** 展开 zod 的包装层,拿到里面真正的那个 schema。包装不改变「条目能不能单独解析」这件事。
|
|
6
|
+
*
|
|
7
|
+
* 包装层闭集:`.optional()` / `.nullable()` / `.default()` / `.prefault()` / `.catch()`(都是 `.unwrap()` 一层),以及
|
|
8
|
+
* **一侧是变换**的管道 —— `.transform(f)` 是「schema → 变换」、`z.preprocess(f, schema)` 是「变换 → schema」,两者都取
|
|
9
|
+
* 不是变换的那一侧。两侧都是 schema 的管道(`a.pipe(b)`)不是包装:到此为止(按它本身的形判,认不出就不抢救)。
|
|
10
|
+
* `.refine()` / `.superRefine()` 在 zod 4 里不是包装层(判据挂在同一只 schema 上),无须展开。 */
|
|
11
|
+
export function unwrapSchema(schema) {
|
|
12
|
+
let cur = schema;
|
|
13
|
+
// 有界:包装层数是 schema 作者写出来的常数,8 层远超实际(防被畸形 schema 拖住)。
|
|
14
|
+
for (let i = 0; i < 8; i++) {
|
|
15
|
+
if (cur instanceof z.ZodOptional || cur instanceof z.ZodNullable || cur instanceof z.ZodDefault || cur instanceof z.ZodPrefault || cur instanceof z.ZodCatch)
|
|
16
|
+
cur = cur.unwrap();
|
|
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
|
|
22
|
+
return cur;
|
|
23
|
+
}
|
|
24
|
+
return cur;
|
|
25
|
+
}
|