@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.
Files changed (77) hide show
  1. package/CHANGELOG.md +531 -0
  2. package/README.md +46 -3
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
  5. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
  6. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
  7. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
  8. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
  9. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
  10. package/dist/api/auth-bridge.d.ts +16 -2
  11. package/dist/api/auth-bridge.js +59 -24
  12. package/dist/api/credentials.d.ts +281 -0
  13. package/dist/api/credentials.js +149 -0
  14. package/dist/api/scopes.js +5 -1
  15. package/dist/atomic-write.js +10 -7
  16. package/dist/bundle.d.ts +1 -1
  17. package/dist/bundle.js +1 -1
  18. package/dist/config-d-file.js +33 -15
  19. package/dist/config-dir-migration.js +2 -1
  20. package/dist/config-fns.js +142 -78
  21. package/dist/conformance/canonical.js +7 -10
  22. package/dist/conformance/contract-adapter.js +26 -1
  23. package/dist/conformance/corpus.js +5 -5
  24. package/dist/conformance/derive.js +6 -5
  25. package/dist/conformance/run.js +44 -6
  26. package/dist/cross-session.d.ts +4 -4
  27. package/dist/declared-capture.d.ts +41 -0
  28. package/dist/declared-capture.js +344 -0
  29. package/dist/file-edit.js +11 -15
  30. package/dist/file-store.d.ts +1 -31
  31. package/dist/file-store.js +3 -30
  32. package/dist/fleet.d.ts +11 -0
  33. package/dist/fleet.js +60 -26
  34. package/dist/fs-failure.d.ts +12 -7
  35. package/dist/fs-failure.js +53 -29
  36. package/dist/hash.d.ts +0 -9
  37. package/dist/hash.js +64 -7
  38. package/dist/hooks-config.d.ts +2 -2
  39. package/dist/hooks-config.js +129 -33
  40. package/dist/http-hook-allowlists.d.ts +27 -10
  41. package/dist/http-hook-allowlists.js +189 -25
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +4 -1
  44. package/dist/mcp-request-entry.d.ts +13 -8
  45. package/dist/mcp-request-entry.js +246 -107
  46. package/dist/one-time-migrations.js +207 -95
  47. package/dist/own-key-read.d.ts +247 -0
  48. package/dist/own-key-read.js +449 -0
  49. package/dist/plain-json-object.d.ts +38 -3
  50. package/dist/plain-json-object.js +84 -7
  51. package/dist/portable-domains.d.ts +31 -0
  52. package/dist/portable-domains.js +30 -0
  53. package/dist/remote-exec.js +9 -1
  54. package/dist/safety-merge-spec.d.ts +88 -6
  55. package/dist/safety-merge-spec.js +246 -17
  56. package/dist/sandbox-git.d.ts +64 -0
  57. package/dist/sandbox-git.js +89 -0
  58. package/dist/scheduler-store.js +21 -12
  59. package/dist/settings-units.d.ts +85 -0
  60. package/dist/settings-units.js +177 -0
  61. package/dist/types.d.ts +6 -2
  62. package/dist/types.js +65 -56
  63. package/dist/url-shape.d.ts +27 -5
  64. package/dist/url-shape.js +29 -5
  65. package/dist/web-admin-credential.js +5 -2
  66. package/dist/web-admin-session-url.js +2 -2
  67. package/dist/worker-row-verdict.d.ts +11 -0
  68. package/dist/worker-row-verdict.js +51 -1
  69. package/dist/workers-rows.d.ts +2 -1
  70. package/dist/workers-rows.js +41 -12
  71. package/dist/zod-unwrap.d.ts +6 -0
  72. package/dist/zod-unwrap.js +14 -7
  73. package/docs/API-SURFACE.md +50 -5
  74. package/docs/CONSUMER-INTEGRATION.md +178 -0
  75. package/llms-full.txt +34 -5
  76. package/llms.txt +6 -3
  77. package/package.json +1 -1
package/dist/types.js CHANGED
@@ -21,6 +21,8 @@ import { INTEGER } from "./integer.js";
21
21
  import { MAX_EXTRA_BODY_DEPTH } from "./hash.js"; // reconcile the extraBody depth cap with the hash cap (R18)
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
+ 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
24
26
  import { httpUrlFlaws, httpUrlRule } from "./url-shape.js"; // 11.0.0: THE URL shape judge // 11.0.0: the one narrow「本机」predicate (auth relaxation)
25
27
  import { secretScanForms } from "./secret-scan.js"; // package-private normalization — the one set of forms both scanners read (not in the barrel)
26
28
  import { WEB_ADMIN_ENV } from "./web-admin-env.js"; // the three connection keys the config renderer writes are reserved worker env
@@ -216,62 +218,66 @@ export const NON_SECRET_URL_NO_MIGRATION = createNonSecretUrl(false);
216
218
  * is proven plain JSON (no toJSON/getters), the stored original == what JSON.stringify persists — closing the
217
219
  * non-idempotent-toJSON / accessor-leak / non-plain-container bypasses. Round 2-5 hardening. */
218
220
  export function extraBodyUnsafe(v) {
219
- const stack = [[v, 0]];
220
- const seen = new WeakSet();
221
- let budget = 100_000;
222
- while (stack.length) {
223
- if (budget-- <= 0)
224
- return true; // too large to vet → fail-closed
225
- const [cur, depth] = stack.pop();
226
- if (depth > MAX_EXTRA_BODY_DEPTH)
227
- return true; // too deep to hash later — cap = hash cap MINUS the wrapper reserve, since extraBody is always hashed NESTED (R18: a value passing at the raw hash cap would still overflow once wrapped → write fail-open + read hard-DoS)
228
- if (cur === null)
229
- continue;
230
- const t = typeof cur;
231
- if (t === "string") {
232
- if (containsSecretToken(cur))
233
- return true; // tokenized (mid-string secret too) — sibling of URL/coords/mcp. R8
234
- continue;
235
- }
236
- if (t === "number") {
237
- if (!Number.isFinite(cur))
238
- return true; // Infinity/NaN → JSON null on persist
239
- continue;
240
- }
241
- if (t === "boolean")
242
- continue;
243
- if (t !== "object")
244
- return true; // undefined / function / symbol / bigint
245
- if (seen.has(cur))
246
- return true; // cycle → reject (would also break stableStringify)
247
- seen.add(cur);
248
- if (Array.isArray(cur)) {
249
- if (cur.length !== Object.keys(cur).length)
250
- return true; // sparse array (holes → null on persist)
251
- // descriptor walk for arrays too (NOT `for…of`/index access — that would INVOKE an accessor index's
252
- // getter, which could throw a secret-bearing message). Reject accessor elements. R11.
221
+ try {
222
+ const stack = [[v, 0]];
223
+ const seen = new WeakSet();
224
+ let budget = 100_000;
225
+ while (stack.length) {
226
+ if (budget-- <= 0)
227
+ return true; // too large to vet → fail-closed
228
+ const [cur, depth] = stack.pop();
229
+ if (depth > MAX_EXTRA_BODY_DEPTH)
230
+ return true; // too deep to hash later — cap = hash cap MINUS the wrapper reserve, since extraBody is always hashed NESTED (R18: a value passing at the raw hash cap would still overflow once wrapped → write fail-open + read hard-DoS)
231
+ if (cur === null)
232
+ continue;
233
+ const t = typeof cur;
234
+ if (t === "string") {
235
+ if (containsSecretToken(cur))
236
+ return true; // tokenized (mid-string secret too) — sibling of URL/coords/mcp. R8
237
+ continue;
238
+ }
239
+ if (t === "number") {
240
+ if (!Number.isFinite(cur))
241
+ return true; // Infinity/NaN → JSON null on persist
242
+ continue;
243
+ }
244
+ if (t === "boolean")
245
+ continue;
246
+ if (t !== "object")
247
+ return true; // undefined / function / symbol / bigint
248
+ if (seen.has(cur))
249
+ return true; // cycle → reject (would also break stableStringify)
250
+ seen.add(cur);
251
+ if (isJsonArray(cur)) {
252
+ if (cur.length !== Object.keys(cur).length)
253
+ return true; // sparse array (holes → null on persist)
254
+ // descriptor walk for arrays too (NOT `for…of`/index access — that would INVOKE an accessor index's
255
+ // getter, which could throw a secret-bearing message). Reject accessor elements. R11.
256
+ for (const [k, desc] of Object.entries(Object.getOwnPropertyDescriptors(cur))) {
257
+ if (k === "length")
258
+ continue; // the array's own `length` data prop — not an element
259
+ if (isAccessorDescriptor(desc))
260
+ return true; // accessor element — do NOT invoke
261
+ appendOwn(stack, [desc.value, depth + 1]); // 定义语义压栈:`push` 会被继承的数字键 setter 吞掉,遍历一层都不下、秘密静默漏判
262
+ }
263
+ continue;
264
+ }
265
+ if (!isPlainJsonObject(cur))
266
+ return true; // Map/Set/Date/class/toJSON-bearing → not plain
267
+ // descriptor walk: never read a value via [] (would invoke a getter); reject accessors outright.
253
268
  for (const [k, desc] of Object.entries(Object.getOwnPropertyDescriptors(cur))) {
254
- if (k === "length")
255
- continue; // the array's own `length` data prop — not an element
256
- if (!("value" in desc))
257
- return true; // accessor element — do NOT invoke
258
- stack.push([desc.value, depth + 1]);
269
+ if (isAccessorDescriptor(desc))
270
+ return true; // accessor (get/set) — do NOT invoke
271
+ if (containsSecretToken(k))
272
+ return true; // secret-shaped KEY (tokenized). R8
273
+ appendOwn(stack, [desc.value, depth + 1]); // 定义语义压栈:`push` 会被继承的数字键 setter 吞掉,遍历一层都不下、秘密静默漏判
259
274
  }
260
- continue;
261
- }
262
- const proto = Object.getPrototypeOf(cur);
263
- if (proto !== Object.prototype && proto !== null)
264
- return true; // Map/Set/Date/class/toJSON-bearing → not plain
265
- // descriptor walk: never read a value via [] (would invoke a getter); reject accessors outright.
266
- for (const [k, desc] of Object.entries(Object.getOwnPropertyDescriptors(cur))) {
267
- if (!("value" in desc))
268
- return true; // accessor (get/set) — do NOT invoke
269
- if (containsSecretToken(k))
270
- return true; // secret-shaped KEY (tokenized). R8
271
- stack.push([desc.value, depth + 1]);
272
275
  }
276
+ return false;
277
+ }
278
+ catch {
279
+ return true; // a value the walk cannot read (a proxy that has been revoked, a trap that throws) cannot be vetted ⇒ UNSAFE (fail-closed), never a throw
273
280
  }
274
- return false;
275
281
  }
276
282
  /** THE vetting pipeline for every FREE-FORM JSON value that lands in the persisted config — `ModelEntry.extraBody`,
277
283
  * `ModelEntry.compat` and `ModelEntry.headers` share this ONE function (1.10.0; before that it was inline in `extraBody`
@@ -518,8 +524,7 @@ export const ModelEntry = z.object({
518
524
  .transform((v, ctx) => {
519
525
  if (v === undefined)
520
526
  return undefined;
521
- const proto = typeof v === "object" && v !== null ? Object.getPrototypeOf(v) : undefined;
522
- if (typeof v !== "object" || v === null || Array.isArray(v) || (proto !== Object.prototype && proto !== null)) {
527
+ if (!isPlainJsonObject(v)) {
523
528
  ctx.addIssue({ code: "custom", ...buildQuotedIssue `extraBody must be a plain JSON object` });
524
529
  return z.NEVER;
525
530
  }
@@ -601,7 +606,7 @@ const RoleMap = z.partialRecord(ModelRole, RoleTarget, {
601
606
  // 枚举键的 record 在运行期以 `unrecognized_keys`(带 `keys`)报未知键,zod 的型面只列了 invalid_type / invalid_key —— 按运行期形读(钉:role-words-12.0.0)。
602
607
  error: (iss) => {
603
608
  const raw = iss;
604
- return raw.code === "unrecognized_keys" && Array.isArray(raw.keys) ? retiredRoleWordsRefusal(raw.keys) : undefined;
609
+ return raw.code === "unrecognized_keys" && isJsonArray(raw.keys) ? retiredRoleWordsRefusal(raw.keys) : undefined;
605
610
  },
606
611
  });
607
612
  /** {@link AtMention} 的模式闭集:`all` = 每个启用的模型都可 `@`;`none` = 一个都不许;`only` = 只许 `models` 里点名的那几个。 */
@@ -1603,7 +1608,11 @@ export function applyProjectPatch(doc, projectId, patch) {
1603
1608
  const current = Object.prototype.hasOwnProperty.call(doc.projects, projectId) ? doc.projects[projectId] : undefined;
1604
1609
  if (current === undefined)
1605
1610
  throw new Error(`applyProjectPatch: project "${projectId}" is not registered in this document`);
1606
- const parsed = ProjectRegistrationPatch.safeParse(patch);
1611
+ // 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
1612
+ const patchRead = guardedSafeParse(() => ProjectRegistrationPatch.safeParse(patch));
1613
+ if (!patchRead.ok)
1614
+ return { success: false, error: new z.ZodError([{ code: "custom", ...buildQuotedIssue `the patch could not be read during validation`, path: ["projects", projectId], input: undefined }]) };
1615
+ const parsed = patchRead.value;
1607
1616
  if (!parsed.success) {
1608
1617
  const issues = parsed.error.issues.map((i) => ({ ...i, path: ["projects", projectId, ...i.path] }));
1609
1618
  return { success: false, error: new z.ZodError(issues) };
@@ -13,10 +13,16 @@
13
13
  *
14
14
  * 拒句里值的样子(展示铸点)不在本模块:那是各宿主自己的展示律(server `buildRefusalShownUrl`)。本模块零 import,纯函数,浏览器安全。
15
15
  *
16
- * ## 本模块里的第二个答案(如实,14.0.0)
16
+ * ## 本模块里的第二个答案(如实,14.0.0 起;14.6.0 把分歧写成读数)
17
17
  * 请求面 MCP 传输的 url(任务提交体 `mcpServers[i].transport.url`,`./mcp-request-entry`)今天不走上面的形判官,而是只看前缀
18
- * ({@link startsWithHttpScheme}):那是消费方请求腿既有的读法,入包时语义逐字冻结。两者对同一个值可以答得不同(空 host、带 userinfo 的
19
- * url 前缀判收、形判官拒)—— 一项已知待收的分歧;放在这里,是为了让「本包里判 URL 的正则」只有这一个模块。
18
+ * ({@link startsWithHttpScheme}):那是消费方请求腿既有的读法,入包时语义逐字冻结。两者对同一个串的答案是两张表(读数逐串钉在 `test/request-url-two-answers-14.6.0.test.ts`):
19
+ * · **读器收、判官拒** —— 前缀对、WHATWG 解析不了的串:裸 scheme `https://`、host 里有空格、未闭合的 IPv6 字面量、非法端口……读器收下,判官答 `not-http-url`;
20
+ * · **判官收、读器拒** —— 不是照字面写全 `http(s)://` 的串:`https:host`、前导空白。WHATWG 会把它们修成合法 URL,读器按字面规则拒;
21
+ * · **部件** —— 读器的座位今天不禁 userinfo / query / fragment(全收),判官在禁这些部件的座位上点名它们。
22
+ * 这是一项**已知待收、收口是收紧所以不走 minor** 的分歧(S-2106):读器收下的是此前能解析出传输的提交体(含重放存量提交体时读它的那一路),把读器收严 = 同一份配置解析出不同结果,发版纪律里是 major,
23
+ * 不是 minor;而「读器直接换成判官」还会放宽第二张表,更不行。S-2106 的收口形与 `PLUGIN_ORIGIN` 座位(`./types`)同构 —— 读器收 ⟺ `startsWithHttpScheme(url)`(座位自己的「照字面写全」规则)∧
24
+ * `httpUrlFlaws(url, [座位禁的部件]).length === 0`(形与部件归本判官)—— 只收紧,不放宽;座位禁不禁 userinfo 是同一次 major 里要定的另一格(读器的拒句已写着凭据别放 URL)。
25
+ * 这个前缀守卫放在这里,是为了让「本包里判 URL 的正则」只有这一个模块。
20
26
  */
21
27
  /** 座位可以禁掉的部件(闭集;判序 = 本数组序)。 */
22
28
  export declare const HTTP_URL_PARTS: readonly ["userinfo", "query", "fragment"];
@@ -35,8 +41,24 @@ export declare function isHierarchicalUrl(raw: string): boolean;
35
41
  * 层级式非 http(s) ⇒ `not-http-url` 之后照判部件。返回类型按 `forbid` 收窄:座位没禁的部件不会出现在答案里。
36
42
  */
37
43
  export declare function httpUrlFlaws<P extends HttpUrlPart>(raw: string, forbid: readonly P[]): ReadonlyArray<"not-http-url" | P>;
38
- /** 以 `http://` / `https://` 起头(大小写不敏感)—— 一道**语法前缀**守卫,不是形判官:不解析、不看 host / 部件(见头注「第二个答案」)。
39
- * 唯一读者是请求面 MCP 传输读器(`readRequestMcpTransport`)。 */
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>;
60
+ /** 以 `http://` / `https://` 起头(大小写不敏感)—— 一道**语法前缀**守卫,不是形判官:不解析、不看 host / 部件,所以 `https://` 这样的裸 scheme 它也收(见头注「第二个答案」)。
61
+ * 唯一读者是请求面 MCP 传输读器(`readRequestMcpTransport`);S-2106 收口时它退成那个读器座位的「照字面写全」规则,与形判官合取。 */
40
62
  export declare function startsWithHttpScheme(raw: string): boolean;
41
63
  /** 一个拒因词的规则句(穷举;新词不补 = 编译红)。句子只说规则,不含值。 */
42
64
  export declare function httpUrlRule(flaw: HttpUrlFlaw): string;
package/dist/url-shape.js CHANGED
@@ -13,10 +13,16 @@
13
13
  *
14
14
  * 拒句里值的样子(展示铸点)不在本模块:那是各宿主自己的展示律(server `buildRefusalShownUrl`)。本模块零 import,纯函数,浏览器安全。
15
15
  *
16
- * ## 本模块里的第二个答案(如实,14.0.0)
16
+ * ## 本模块里的第二个答案(如实,14.0.0 起;14.6.0 把分歧写成读数)
17
17
  * 请求面 MCP 传输的 url(任务提交体 `mcpServers[i].transport.url`,`./mcp-request-entry`)今天不走上面的形判官,而是只看前缀
18
- * ({@link startsWithHttpScheme}):那是消费方请求腿既有的读法,入包时语义逐字冻结。两者对同一个值可以答得不同(空 host、带 userinfo 的
19
- * url 前缀判收、形判官拒)—— 一项已知待收的分歧;放在这里,是为了让「本包里判 URL 的正则」只有这一个模块。
18
+ * ({@link startsWithHttpScheme}):那是消费方请求腿既有的读法,入包时语义逐字冻结。两者对同一个串的答案是两张表(读数逐串钉在 `test/request-url-two-answers-14.6.0.test.ts`):
19
+ * · **读器收、判官拒** —— 前缀对、WHATWG 解析不了的串:裸 scheme `https://`、host 里有空格、未闭合的 IPv6 字面量、非法端口……读器收下,判官答 `not-http-url`;
20
+ * · **判官收、读器拒** —— 不是照字面写全 `http(s)://` 的串:`https:host`、前导空白。WHATWG 会把它们修成合法 URL,读器按字面规则拒;
21
+ * · **部件** —— 读器的座位今天不禁 userinfo / query / fragment(全收),判官在禁这些部件的座位上点名它们。
22
+ * 这是一项**已知待收、收口是收紧所以不走 minor** 的分歧(S-2106):读器收下的是此前能解析出传输的提交体(含重放存量提交体时读它的那一路),把读器收严 = 同一份配置解析出不同结果,发版纪律里是 major,
23
+ * 不是 minor;而「读器直接换成判官」还会放宽第二张表,更不行。S-2106 的收口形与 `PLUGIN_ORIGIN` 座位(`./types`)同构 —— 读器收 ⟺ `startsWithHttpScheme(url)`(座位自己的「照字面写全」规则)∧
24
+ * `httpUrlFlaws(url, [座位禁的部件]).length === 0`(形与部件归本判官)—— 只收紧,不放宽;座位禁不禁 userinfo 是同一次 major 里要定的另一格(读器的拒句已写着凭据别放 URL)。
25
+ * 这个前缀守卫放在这里,是为了让「本包里判 URL 的正则」只有这一个模块。
20
26
  */
21
27
  /** 座位可以禁掉的部件(闭集;判序 = 本数组序)。 */
22
28
  export const HTTP_URL_PARTS = ["userinfo", "query", "fragment"];
@@ -51,8 +57,26 @@ export function httpUrlFlaws(raw, forbid) {
51
57
  const shape = u.protocol === "http:" || u.protocol === "https:" ? [] : ["not-http-url"];
52
58
  return [...shape, ...HTTP_URL_PARTS.filter((p) => forbid.includes(p) && present[p])];
53
59
  }
54
- /** 以 `http://` / `https://` 起头(大小写不敏感)—— 一道**语法前缀**守卫,不是形判官:不解析、不看 host / 部件(见头注「第二个答案」)。
55
- * 唯一读者是请求面 MCP 传输读器(`readRequestMcpTransport`)。 */
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
+ }
78
+ /** 以 `http://` / `https://` 起头(大小写不敏感)—— 一道**语法前缀**守卫,不是形判官:不解析、不看 host / 部件,所以 `https://` 这样的裸 scheme 它也收(见头注「第二个答案」)。
79
+ * 唯一读者是请求面 MCP 传输读器(`readRequestMcpTransport`);S-2106 收口时它退成那个读器座位的「照字面写全」规则,与形判官合取。 */
56
80
  export function startsWithHttpScheme(raw) {
57
81
  return /^https?:\/\//i.test(raw);
58
82
  }
@@ -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
- return parsed.success ? { kind: "envelope", envelope: parsed.data } : { kind: "unreadable", reason: envelopeRejection(parsed.error) };
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 { httpUrlFlaws, httpUrlRule } from "./url-shape.js";
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 = httpUrlFlaws(webBaseUrl, ["userinfo", "query", "fragment"]);
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,19 +8,69 @@
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";
11
13
  import { WorkersConfig } from "./types.js";
12
14
  /** 每一形的拒句主语 —— 穷举映射型:加一形不补这里即编译红。 */
13
15
  const SUBJECT = {
14
16
  stored: "the stored worker",
15
17
  migrated: "after migration the worker",
16
18
  };
19
+ /** 输入的「类型名」(zod 的 `invalid_type` 句子里 `received` 后面那个词):typeof / array / null,**从不读 `constructor`**。形状只问座位;数恒答 `number`
20
+ * (非有限的数到不了这里:{@link safeIssueMessage} 把它交还 zod 自己的句子)。 */
21
+ function receivedWord(input) {
22
+ switch (typeof input) {
23
+ case "number":
24
+ return "number";
25
+ case "object": {
26
+ const shape = jsonShapeOf(input);
27
+ switch (shape) {
28
+ case "null":
29
+ return "null";
30
+ case "array":
31
+ return "array";
32
+ case "plain":
33
+ case "non_plain_object":
34
+ case "uninspectable":
35
+ case "primitive":
36
+ return "object";
37
+ default: {
38
+ const unhandled = shape;
39
+ throw new Error(`receivedWord: unhandled shape ${String(unhandled)}`);
40
+ }
41
+ }
42
+ }
43
+ default:
44
+ return typeof input;
45
+ }
46
+ }
47
+ /**
48
+ * zod 的 `invalid_type` 句子(`Invalid input: expected <类型>, received <类型>`)—— 与 zod 自己的话**逐字同**(对每种 JSON 值;`test/migration-row-verdict-14.7.0` 逐值对着 zod 的原句核,升级 zod 改措辞即红),
49
+ * 区别只有一处:zod 对「原型不是 Object.prototype 的对象」读 `input.constructor.name` 当类型名 —— 原型不是 `Object.prototype` 的(隔离原型)行快照上,用户写的**自有 `constructor` 键**就成了类型名:
50
+ * 整次调用抛(`constructor.name` 是个对象)、用户文本进拒因(`received hello world`)、句子变样。这里不读 constructor。别的 issue 不管(返回 `undefined` ⇒ 沿用 zod 的 / 包自己写的句子)。
51
+ * 只给本文件的终判调用(`safeParse` 的 `error` 参数);它在 schema 自带的句子之后、zod 内置句子之前生效。
52
+ */
53
+ export function safeIssueMessage(issue) {
54
+ if (issue.code !== "invalid_type")
55
+ return undefined;
56
+ // 非有限的数(`NaN` / `±Infinity` —— JSON 写不出 `NaN`;`±Infinity` 只由溢出的数字字面量如 `1e400` 解析出来)的类型名 zod 自己怎么说各版本不同:不抢,交还 zod 自己的句子(它不读 constructor)
57
+ if (typeof issue.input === "number" && !Number.isFinite(issue.input))
58
+ return undefined;
59
+ const expected = issue.expected === "nan" ? "NaN" : String(issue.expected);
60
+ return `Invalid input: expected ${expected}, received ${receivedWord(issue.input)}`;
61
+ }
17
62
  /** 一只单行域里的 issue 路径 → 相对那一行(去掉 `["workers", 0]` 前缀;不在那一行下的 issue 原样)。 */
18
63
  function rowRelative(path) {
19
64
  return path[0] === "workers" && path[1] === 0 ? path.slice(2) : path;
20
65
  }
21
66
  /** 一行的终判:读得进 ⇒ `undefined`;读不进 ⇒ 第一条 issue 的路径(相对该行)+ 拒句(见文件头)。 */
22
67
  export function workerRowVerdict(row, form) {
23
- const verdict = WorkersConfig.safeParse({ workers: [row] });
68
+ // 校验过程抛(会抛的 getter / 陷阱:读侧交进来的是调用方的活对象;zod 4.6 首读 `error` 时拼句子抛 —— 被污染的运行时、另一 realm 的怪形值)⇒ 这一行判「读不出」
69
+ // (定句,不含任何用户文本),不抛出调用。首读 `error` 与 `safeParse` 同在一只守护里(`guardedSafeParse`),两版 zod 同答
70
+ const judged = guardedSafeParse(() => WorkersConfig.safeParse({ workers: [row] }, { error: (iss) => safeIssueMessage(iss) }));
71
+ if (!judged.ok)
72
+ return { field: [], reason: `${SUBJECT[form]} is not a valid WorkerSpec — a value could not be read during validation` };
73
+ const verdict = judged.value;
24
74
  if (verdict.success)
25
75
  return undefined;
26
76
  const issues = verdict.error.issues;
@@ -7,7 +7,8 @@
7
7
  * web-admin 的 workers 读口先过本函数:每一只**认得出名字**的 worker 单独过读侧自己的判据(只含这一行的 workers 域读不读得进,
8
8
  * `WorkersConfig` —— 与 `parseDomain("workers")` 同一只 schema,不另抄任何规则;判据与拒句拼装在包内私有的 `./worker-row-verdict`,
9
9
  * 一次性迁移 SS-M4 判改写后的行调同一只);读不进的那一行扣下(`held`,带原文与拒因),
10
- * 其余照常进 `doc`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(不是对象 / 没有字符串名字 —— 原样留在 `doc`)与跨行规则
10
+ * 其余照常进 `doc`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(不是朴素 JSON 对象〔类实例 / `Map` / `Date` 一类,JSON 解析不出来,走到这里只能是程序拼的〕/ 没有字符串名字 —— 原样留在 `doc`;
11
+ * 非朴素的**坏**行因此不在这里单行扣下,而是交整域 `parseDomain("workers")` 判,整张舰队清单读不出 —— 响亮、fail-closed,只落在非 JSON 值上)与跨行规则
11
12
  * (名字 / 路由前缀唯一 —— 两行单独都读得进、合起来才不行,这里不替运维挑留哪一行)。
12
13
  *
13
14
  * 被扣下的行**不进** `doc`,但随 `held[].original` 逐字带回:`doc` ∪ `held[].original` 覆盖输入的全部 worker。读口不写回 —— 原文留在盘上,
@@ -7,36 +7,65 @@
7
7
  * web-admin 的 workers 读口先过本函数:每一只**认得出名字**的 worker 单独过读侧自己的判据(只含这一行的 workers 域读不读得进,
8
8
  * `WorkersConfig` —— 与 `parseDomain("workers")` 同一只 schema,不另抄任何规则;判据与拒句拼装在包内私有的 `./worker-row-verdict`,
9
9
  * 一次性迁移 SS-M4 判改写后的行调同一只);读不进的那一行扣下(`held`,带原文与拒因),
10
- * 其余照常进 `doc`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(不是对象 / 没有字符串名字 —— 原样留在 `doc`)与跨行规则
10
+ * 其余照常进 `doc`。整域读侧仍是最后围栏,只剩两类归它:认不出名字的条目(不是朴素 JSON 对象〔类实例 / `Map` / `Date` 一类,JSON 解析不出来,走到这里只能是程序拼的〕/ 没有字符串名字 —— 原样留在 `doc`;
11
+ * 非朴素的**坏**行因此不在这里单行扣下,而是交整域 `parseDomain("workers")` 判,整张舰队清单读不出 —— 响亮、fail-closed,只落在非 JSON 值上)与跨行规则
11
12
  * (名字 / 路由前缀唯一 —— 两行单独都读得进、合起来才不行,这里不替运维挑留哪一行)。
12
13
  *
13
14
  * 被扣下的行**不进** `doc`,但随 `held[].original` 逐字带回:`doc` ∪ `held[].original` 覆盖输入的全部 worker。读口不写回 —— 原文留在盘上,
14
15
  * web-admin 在列表里标红点名 `field` / `reason`。拒因只点名字段、不回显值。
15
16
  */
16
17
  import { outgoingPath, redactSecrets } from "./config-fns.js";
18
+ import { appendOwn, dataDescriptor, ownDeclaredValueOf, ownElementsOf, ownEntriesOf } from "./own-key-read.js";
19
+ import { isJsonArray, isPlainJsonObject } from "./plain-json-object.js";
17
20
  import { workerRowVerdict } from "./worker-row-verdict.js";
18
- const isRec = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
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
+ }
19
38
  /**
20
39
  * 逐行读一份 workers 域**原始**文档(`{ workers: [...] }`)。不是这个形 ⇒ 原样返回、零扣下(判形是 `parseDomain` 的事)。
21
40
  * 规则见文件头。
22
41
  */
23
42
  export function readWorkersRows(raw) {
24
- if (!isRec(raw) || !Array.isArray(raw.workers))
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)
25
54
  return { doc: raw, held: [] };
26
- const rows = raw.workers;
27
55
  const kept = [];
28
56
  const held = [];
29
- rows.forEach((row, index) => {
30
- if (!isRec(row) || typeof row.name !== "string") {
31
- kept.push(row); // 认不出名字:整域读侧判
32
- return;
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;
33
62
  }
34
63
  const v = workerRowVerdict(row, "stored");
35
64
  // 坐标与拒因都走出门那一道:issue 路径里的 Record 键是用户写的(例:一个不合法的 env 键名可能就是粘错位置的凭据)。
36
65
  if (v === undefined)
37
- kept.push(row);
66
+ appendOwn(kept, row);
38
67
  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 };
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 };
42
71
  }
@@ -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;
@@ -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
- 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
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
+ }