@sema-agent/settings-schema 14.6.0 → 14.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +266 -0
- package/README.md +31 -3
- package/conformance/VERSION +1 -1
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
- package/dist/api/auth-bridge.d.ts +16 -2
- package/dist/api/auth-bridge.js +59 -24
- package/dist/api/credentials.d.ts +281 -0
- package/dist/api/credentials.js +149 -0
- package/dist/api/scopes.js +5 -1
- package/dist/config-d-file.js +33 -15
- package/dist/config-fns.js +142 -78
- 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.js +43 -6
- package/dist/cross-session.d.ts +4 -4
- package/dist/declared-capture.d.ts +41 -0
- package/dist/declared-capture.js +344 -0
- package/dist/file-edit.js +11 -15
- package/dist/fleet.d.ts +11 -0
- package/dist/fleet.js +59 -26
- 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 +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp-request-entry.d.ts +9 -7
- package/dist/mcp-request-entry.js +89 -82
- package/dist/one-time-migrations.js +207 -95
- package/dist/own-key-read.d.ts +247 -0
- package/dist/own-key-read.js +449 -0
- package/dist/plain-json-object.d.ts +11 -1
- package/dist/plain-json-object.js +37 -1
- package/dist/remote-exec.js +9 -1
- 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/types.d.ts +6 -2
- package/dist/types.js +11 -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 +51 -1
- 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 +35 -4
- package/docs/CONSUMER-INTEGRATION.md +178 -0
- package/llms-full.txt +23 -4
- package/llms.txt +3 -2
- package/package.json +1 -1
- package/dist/definition-copy.d.ts +0 -23
- package/dist/definition-copy.js +0 -52
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
*/
|
|
67
67
|
import { z } from "zod";
|
|
68
68
|
import { MAX_HASH_DEPTH, stableStringify } from "./hash.js";
|
|
69
|
-
import { captureStringArray, dataDescriptor } from "./
|
|
70
|
-
import {
|
|
69
|
+
import { captureStringArray, dataDescriptor, isAccessorDescriptor, isolatedObject } from "./own-key-read.js";
|
|
70
|
+
import { jsonShapeOf } from "./plain-json-object.js";
|
|
71
71
|
/** How values from different layers/principals combine for a category. */
|
|
72
72
|
export const MergeShape = z.enum(["denyFirst", "replace", "runAll", "overrideByName", "concat"]);
|
|
73
73
|
/** The trust-tier VOCABULARY as a RUNTIME VALUE — the ONE source of the ladder's words, in the same form as
|
|
@@ -183,10 +183,11 @@ function orderVerdict(diff) {
|
|
|
183
183
|
return diff > 0 ? "stricter" : diff < 0 ? "looser" : "equal";
|
|
184
184
|
}
|
|
185
185
|
/** A string list as a set (order and duplicates do not count); `undefined` when the value is not an array of strings
|
|
186
|
-
* — a sparse array, an accessor
|
|
187
|
-
*
|
|
186
|
+
* — a sparse array, an accessor or non-enumerable element (the verdict is about the caller's own value: an accessor is not a stored value, it may answer
|
|
187
|
+
* differently on the next read — the `none` rule's domain says the same; no getter is called), a proxy that cannot be read (revoked, a trap that throws) is not one:
|
|
188
|
+
* all through the package's one string-array reading, strict ({@link captureStringArray}), so the comparator never throws on them and never splits a string. */
|
|
188
189
|
function stringSetOf(v) {
|
|
189
|
-
const list = captureStringArray(v);
|
|
190
|
+
const list = captureStringArray(v, "strict");
|
|
190
191
|
return list === undefined ? undefined : new Set(list);
|
|
191
192
|
}
|
|
192
193
|
/** `superset ⇒ stricter` for setUnion, the mirror for setNarrower. */
|
|
@@ -213,45 +214,57 @@ const NOT_STRICT_JSON = { ok: false };
|
|
|
213
214
|
function strictJsonCopy(v, depth = 0) {
|
|
214
215
|
if (depth > MAX_HASH_DEPTH)
|
|
215
216
|
return NOT_STRICT_JSON;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const member = strictJsonStored(Object.getOwnPropertyDescriptor(v, i), depth);
|
|
229
|
-
if (!member.ok)
|
|
217
|
+
const shape = jsonShapeOf(v); // the shape is asked ONCE, of the seat (total): no `typeof` split, no second prototype read
|
|
218
|
+
switch (shape) {
|
|
219
|
+
case "null":
|
|
220
|
+
return { ok: true, value: v };
|
|
221
|
+
case "primitive":
|
|
222
|
+
if (typeof v === "string" || typeof v === "boolean")
|
|
223
|
+
return { ok: true, value: v };
|
|
224
|
+
return typeof v === "number" && Number.isFinite(v) ? { ok: true, value: v } : NOT_STRICT_JSON; // undefined / symbol / bigint / NaN / ±Infinity
|
|
225
|
+
case "array": {
|
|
226
|
+
const array = v;
|
|
227
|
+
const length = array.length;
|
|
228
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0)
|
|
230
229
|
return NOT_STRICT_JSON;
|
|
231
|
-
|
|
230
|
+
const out = [];
|
|
231
|
+
for (let i = 0; i < length; i++) {
|
|
232
|
+
const member = strictJsonStored(Object.getOwnPropertyDescriptor(array, i), depth);
|
|
233
|
+
if (!member.ok)
|
|
234
|
+
return NOT_STRICT_JSON;
|
|
235
|
+
Object.defineProperty(out, i, dataDescriptor(member.value));
|
|
236
|
+
}
|
|
237
|
+
return { ok: true, value: out };
|
|
232
238
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
239
|
+
case "plain": {
|
|
240
|
+
const record = v;
|
|
241
|
+
const out = isolatedObject();
|
|
242
|
+
for (const key of Reflect.ownKeys(record)) {
|
|
243
|
+
if (typeof key === "symbol")
|
|
244
|
+
continue; // never serialized
|
|
245
|
+
const desc = Object.getOwnPropertyDescriptor(record, key);
|
|
246
|
+
if (desc !== undefined && desc.enumerable !== true)
|
|
247
|
+
continue; // never serialized (what `Object.keys` leaves out)
|
|
248
|
+
const member = strictJsonStored(desc, depth);
|
|
249
|
+
if (!member.ok)
|
|
250
|
+
return NOT_STRICT_JSON;
|
|
251
|
+
Object.defineProperty(out, key, dataDescriptor(member.value));
|
|
252
|
+
}
|
|
253
|
+
return { ok: true, value: out };
|
|
254
|
+
}
|
|
255
|
+
case "non_plain_object": // function / Date / Map / class instance
|
|
256
|
+
case "uninspectable": // a proxy that cannot be inspected: outside the domain
|
|
246
257
|
return NOT_STRICT_JSON;
|
|
247
|
-
|
|
258
|
+
default: {
|
|
259
|
+
const unhandled = shape;
|
|
260
|
+
throw new Error(`strictJsonCopy: unhandled shape ${String(unhandled)}`);
|
|
261
|
+
}
|
|
248
262
|
}
|
|
249
|
-
return { ok: true, value: out };
|
|
250
263
|
}
|
|
251
264
|
/** One member, judged by the descriptor read ONCE: a stored value (a hole has no descriptor; an accessor has no `value`) that is itself strict JSON. */
|
|
252
265
|
function strictJsonStored(desc, depth) {
|
|
253
|
-
if (desc === undefined ||
|
|
254
|
-
return NOT_STRICT_JSON; //
|
|
266
|
+
if (desc === undefined || isAccessorDescriptor(desc))
|
|
267
|
+
return NOT_STRICT_JSON; // 访问器不是存着的值(描述符的字段只认自有的,见内核)
|
|
255
268
|
return strictJsonCopy(desc.value, depth + 1);
|
|
256
269
|
}
|
|
257
270
|
/** `none`: compares the DESCRIPTOR VIEW, read once ({@link strictJsonCopy}), and serializes only the copies — member values never go through `[[Get]]`. The ONE guard around the
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 沙箱 git 平面的 **origin 形** 与 **有效配置块 `sandboxGit`**(14.7.0)。纯、浏览器安全。
|
|
3
|
+
*
|
|
4
|
+
* ## 律(一条)
|
|
5
|
+
* **一个 origin 就是它自己的 WHATWG origin**:串等于 `new URL(串).origin`,且 scheme ∈ {http, https}。规范形 ——
|
|
6
|
+
* 小写 host、去缺省端口(https 的 443、http 的 80)、无尾斜杠、无 userinfo / 路径 / 查询 / 片段 —— 不是一张手写规则表,而是这一个等式的推论:
|
|
7
|
+
* 凡 WHATWG 会改写的串(大写、缺省端口、IPv4 简写、百分号编码的 host、串内制表符、前后空白、反斜杠……)都不等于自己的 origin,所以都不是规范形。
|
|
8
|
+
* 判官只有 {@link isSandboxGitOrigin} 一只;请求体的 `origins`、应答行的 `origin`、有效配置块的 `origin` 全读它(经 {@link SandboxGitOrigin})。
|
|
9
|
+
* 「这个串是不是 http(s) URL」归包内唯一的 URL 形判官 `httpUrlFlaws`(`./url-shape`),本模块只加「等于自己的 origin」这一格。
|
|
10
|
+
* origin 不表达子路径部署(`https://host/gitea` 不是 origin)—— 规范化器不存在:把带路径的串静默削成 origin 会把凭据送到运维没写的地方。
|
|
11
|
+
*
|
|
12
|
+
* ## 有效配置块 `sandboxGit`
|
|
13
|
+
* `/effective` 载荷上的**非域顶层键**(web-admin 按部署登记册投影,不是存储域):可选、无默认 —— 缺席 = 这个部署没有投影这一块。
|
|
14
|
+
* `readEffectiveWire` 对它走 open-world 逐字承运(从不校验、从不丢,`EffectiveWire.sandboxGit` 是 `unknown`);**判它的座位只有 {@link SandboxGitConfig}**,由消费方在用之前判。
|
|
15
|
+
*
|
|
16
|
+
* · `origins[i].origin` —— 本部署代理哪些 origin(规范形,见上);≤ {@link SANDBOX_GIT_MAX_ORIGINS} 项、两两不同。
|
|
17
|
+
* · `origins[i].connectUrl` —— 控制台「连接 Gitea(git)」页的绝对 https URL(非秘密:{@link SandboxGitConnectUrl})。
|
|
18
|
+
*
|
|
19
|
+
* 🔴 **块里 http origin 可读**:它为什么送不出凭据(传输不加密)是逐 origin 应答里的一个词(`insecure_transport`,`./api/credentials` 的判定序表第 1 行)
|
|
20
|
+
* 与能力读数里的一个词,不是块的校验拒。块若因一个 http origin 整块不可读,同块里的 https origin 一并失效,「逐 origin 说拒因」就无从谈起;
|
|
21
|
+
* http origin 只配路由、永不取值,是消费方的事。
|
|
22
|
+
*
|
|
23
|
+
* 解析剥未知键(与 `SessionMirrorPolicy` 同律:加字段恒为加性)。
|
|
24
|
+
*/
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
/** 一次最多代理的 origin 数(请求体、应答、有效配置块同一个上界)。 */
|
|
27
|
+
export declare const SANDBOX_GIT_MAX_ORIGINS = 16;
|
|
28
|
+
/**
|
|
29
|
+
* 判:`raw` 是不是规范形的 http(s) origin。合格 ⇔ 它是层级式 http(s) URL 且**逐字等于**自己的 WHATWG origin。
|
|
30
|
+
* 纯函数、不抛;非串由 {@link SandboxGitOrigin} 的 zod 形先拒。
|
|
31
|
+
*/
|
|
32
|
+
export declare function isSandboxGitOrigin(raw: string): boolean;
|
|
33
|
+
/** 一个规范形的 origin(`scheme://host[:port]`,scheme ∈ {http, https})。判官 {@link isSandboxGitOrigin};拒句是定句,不回显值。 */
|
|
34
|
+
export declare const SandboxGitOrigin: z.ZodString;
|
|
35
|
+
export type SandboxGitOrigin = z.infer<typeof SandboxGitOrigin>;
|
|
36
|
+
/**
|
|
37
|
+
* 一张 origin 表:每项是规范形、两两不同、至多 {@link SANDBOX_GIT_MAX_ORIGINS} 项。**空表合法**(有效配置块可以没有 origin);要求至少一项的座位(申领请求)自己 `.min(1)`。
|
|
38
|
+
* 重复的那一项被点名(坐标 = 第二次出现的下标)。
|
|
39
|
+
*/
|
|
40
|
+
export declare const SandboxGitOriginList: z.ZodArray<z.ZodString>;
|
|
41
|
+
export type SandboxGitOriginList = z.infer<typeof SandboxGitOriginList>;
|
|
42
|
+
/**
|
|
43
|
+
* 控制台「连接 Gitea(git)」页的绝对 URL:**https**,且是 {@link NON_SECRET_URL_NO_MIGRATION} 那一类(层级式 URL、无 userinfo、无片段、路径与查询里没有秘密形的值)。
|
|
44
|
+
* 它是 wire 输入,不是存量文档里的值,所以走「无迁移指路」那一只。
|
|
45
|
+
*/
|
|
46
|
+
export declare const SandboxGitConnectUrl: z.ZodString;
|
|
47
|
+
export type SandboxGitConnectUrl = z.infer<typeof SandboxGitConnectUrl>;
|
|
48
|
+
/** 有效配置块里的一个 origin 条目。 */
|
|
49
|
+
export declare const SandboxGitOriginEntry: z.ZodObject<{
|
|
50
|
+
origin: z.ZodString;
|
|
51
|
+
connectUrl: z.ZodString;
|
|
52
|
+
}, z.core.$strip>;
|
|
53
|
+
export type SandboxGitOriginEntry = z.infer<typeof SandboxGitOriginEntry>;
|
|
54
|
+
/**
|
|
55
|
+
* 有效配置块 `sandboxGit`(`/effective` 载荷的非域顶层键,可选、无默认)。判它的**唯一座位**:消费方在用之前对 `wire.sandboxGit` 跑 `safeParse`。
|
|
56
|
+
* `origins` ≤ {@link SANDBOX_GIT_MAX_ORIGINS} 项,按 `origin` 两两不同(重复的那一项的 `origin` 字段被点名);空表合法。
|
|
57
|
+
*/
|
|
58
|
+
export declare const SandboxGitConfig: z.ZodObject<{
|
|
59
|
+
origins: z.ZodArray<z.ZodObject<{
|
|
60
|
+
origin: z.ZodString;
|
|
61
|
+
connectUrl: z.ZodString;
|
|
62
|
+
}, z.core.$strip>>;
|
|
63
|
+
}, z.core.$strip>;
|
|
64
|
+
export type SandboxGitConfig = z.infer<typeof SandboxGitConfig>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 沙箱 git 平面的 **origin 形** 与 **有效配置块 `sandboxGit`**(14.7.0)。纯、浏览器安全。
|
|
3
|
+
*
|
|
4
|
+
* ## 律(一条)
|
|
5
|
+
* **一个 origin 就是它自己的 WHATWG origin**:串等于 `new URL(串).origin`,且 scheme ∈ {http, https}。规范形 ——
|
|
6
|
+
* 小写 host、去缺省端口(https 的 443、http 的 80)、无尾斜杠、无 userinfo / 路径 / 查询 / 片段 —— 不是一张手写规则表,而是这一个等式的推论:
|
|
7
|
+
* 凡 WHATWG 会改写的串(大写、缺省端口、IPv4 简写、百分号编码的 host、串内制表符、前后空白、反斜杠……)都不等于自己的 origin,所以都不是规范形。
|
|
8
|
+
* 判官只有 {@link isSandboxGitOrigin} 一只;请求体的 `origins`、应答行的 `origin`、有效配置块的 `origin` 全读它(经 {@link SandboxGitOrigin})。
|
|
9
|
+
* 「这个串是不是 http(s) URL」归包内唯一的 URL 形判官 `httpUrlFlaws`(`./url-shape`),本模块只加「等于自己的 origin」这一格。
|
|
10
|
+
* origin 不表达子路径部署(`https://host/gitea` 不是 origin)—— 规范化器不存在:把带路径的串静默削成 origin 会把凭据送到运维没写的地方。
|
|
11
|
+
*
|
|
12
|
+
* ## 有效配置块 `sandboxGit`
|
|
13
|
+
* `/effective` 载荷上的**非域顶层键**(web-admin 按部署登记册投影,不是存储域):可选、无默认 —— 缺席 = 这个部署没有投影这一块。
|
|
14
|
+
* `readEffectiveWire` 对它走 open-world 逐字承运(从不校验、从不丢,`EffectiveWire.sandboxGit` 是 `unknown`);**判它的座位只有 {@link SandboxGitConfig}**,由消费方在用之前判。
|
|
15
|
+
*
|
|
16
|
+
* · `origins[i].origin` —— 本部署代理哪些 origin(规范形,见上);≤ {@link SANDBOX_GIT_MAX_ORIGINS} 项、两两不同。
|
|
17
|
+
* · `origins[i].connectUrl` —— 控制台「连接 Gitea(git)」页的绝对 https URL(非秘密:{@link SandboxGitConnectUrl})。
|
|
18
|
+
*
|
|
19
|
+
* 🔴 **块里 http origin 可读**:它为什么送不出凭据(传输不加密)是逐 origin 应答里的一个词(`insecure_transport`,`./api/credentials` 的判定序表第 1 行)
|
|
20
|
+
* 与能力读数里的一个词,不是块的校验拒。块若因一个 http origin 整块不可读,同块里的 https origin 一并失效,「逐 origin 说拒因」就无从谈起;
|
|
21
|
+
* http origin 只配路由、永不取值,是消费方的事。
|
|
22
|
+
*
|
|
23
|
+
* 解析剥未知键(与 `SessionMirrorPolicy` 同律:加字段恒为加性)。
|
|
24
|
+
*/
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { buildQuotedIssue } from "./quoted-issue.js";
|
|
27
|
+
import { NON_SECRET_URL_NO_MIGRATION } from "./types.js";
|
|
28
|
+
import { httpUrlFlaws } from "./url-shape.js";
|
|
29
|
+
/** 一次最多代理的 origin 数(请求体、应答、有效配置块同一个上界)。 */
|
|
30
|
+
export const SANDBOX_GIT_MAX_ORIGINS = 16;
|
|
31
|
+
/**
|
|
32
|
+
* 判:`raw` 是不是规范形的 http(s) origin。合格 ⇔ 它是层级式 http(s) URL 且**逐字等于**自己的 WHATWG origin。
|
|
33
|
+
* 纯函数、不抛;非串由 {@link SandboxGitOrigin} 的 zod 形先拒。
|
|
34
|
+
*/
|
|
35
|
+
export function isSandboxGitOrigin(raw) {
|
|
36
|
+
if (httpUrlFlaws(raw, []).length > 0)
|
|
37
|
+
return false;
|
|
38
|
+
return new URL(raw).origin === raw;
|
|
39
|
+
}
|
|
40
|
+
/** 一个规范形的 origin(`scheme://host[:port]`,scheme ∈ {http, https})。判官 {@link isSandboxGitOrigin};拒句是定句,不回显值。 */
|
|
41
|
+
export const SandboxGitOrigin = z
|
|
42
|
+
.string()
|
|
43
|
+
.refine(isSandboxGitOrigin, buildQuotedIssue `must be a canonical http(s) origin — scheme://host[:port] equal to its own WHATWG origin: lowercase host, no default port, no userinfo, path, query, fragment or trailing slash`);
|
|
44
|
+
/** 给定下标的 origin 在它之前已经出现过 ⇒ 下标。重复判在**规范形**之上(两个都是规范形的串,相等即同一个 origin)。 */
|
|
45
|
+
function repeatedAt(origins) {
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const repeats = [];
|
|
48
|
+
origins.forEach((o, i) => {
|
|
49
|
+
if (seen.has(o))
|
|
50
|
+
repeats.push(i);
|
|
51
|
+
seen.add(o);
|
|
52
|
+
});
|
|
53
|
+
return repeats;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 一张 origin 表:每项是规范形、两两不同、至多 {@link SANDBOX_GIT_MAX_ORIGINS} 项。**空表合法**(有效配置块可以没有 origin);要求至少一项的座位(申领请求)自己 `.min(1)`。
|
|
57
|
+
* 重复的那一项被点名(坐标 = 第二次出现的下标)。
|
|
58
|
+
*/
|
|
59
|
+
export const SandboxGitOriginList = z
|
|
60
|
+
.array(SandboxGitOrigin)
|
|
61
|
+
.max(SANDBOX_GIT_MAX_ORIGINS)
|
|
62
|
+
.superRefine((origins, ctx) => {
|
|
63
|
+
for (const i of repeatedAt(origins))
|
|
64
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate origin`, path: [i] });
|
|
65
|
+
});
|
|
66
|
+
/**
|
|
67
|
+
* 控制台「连接 Gitea(git)」页的绝对 URL:**https**,且是 {@link NON_SECRET_URL_NO_MIGRATION} 那一类(层级式 URL、无 userinfo、无片段、路径与查询里没有秘密形的值)。
|
|
68
|
+
* 它是 wire 输入,不是存量文档里的值,所以走「无迁移指路」那一只。
|
|
69
|
+
*/
|
|
70
|
+
export const SandboxGitConnectUrl = NON_SECRET_URL_NO_MIGRATION.refine((u) => URL.canParse(u) && new URL(u).protocol === "https:", buildQuotedIssue `must be an https URL — the connect page is opened by a person and carries their session`);
|
|
71
|
+
/** 有效配置块里的一个 origin 条目。 */
|
|
72
|
+
export const SandboxGitOriginEntry = z.object({
|
|
73
|
+
/** 本部署代理的 origin(规范形)。http 在这里可读(见模块头注)。 */
|
|
74
|
+
origin: SandboxGitOrigin,
|
|
75
|
+
/** 该 origin 的「连接」页,绝对 https URL。 */
|
|
76
|
+
connectUrl: SandboxGitConnectUrl,
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* 有效配置块 `sandboxGit`(`/effective` 载荷的非域顶层键,可选、无默认)。判它的**唯一座位**:消费方在用之前对 `wire.sandboxGit` 跑 `safeParse`。
|
|
80
|
+
* `origins` ≤ {@link SANDBOX_GIT_MAX_ORIGINS} 项,按 `origin` 两两不同(重复的那一项的 `origin` 字段被点名);空表合法。
|
|
81
|
+
*/
|
|
82
|
+
export const SandboxGitConfig = z
|
|
83
|
+
.object({
|
|
84
|
+
origins: z.array(SandboxGitOriginEntry).max(SANDBOX_GIT_MAX_ORIGINS),
|
|
85
|
+
})
|
|
86
|
+
.superRefine((block, ctx) => {
|
|
87
|
+
for (const i of repeatedAt(block.origins.map((e) => e.origin)))
|
|
88
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate origin`, path: ["origins", i, "origin"] });
|
|
89
|
+
});
|
package/dist/scheduler-store.js
CHANGED
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ownDeclaredValueOf } from './own-key-read.js';
|
|
2
|
+
import { isJsonArray, isPlainJsonObject } from './plain-json-object.js';
|
|
2
3
|
import { isSessionLifetime } from './scheduler-lifetime.js';
|
|
3
4
|
import { migrateSchedulerRecords } from './scheduler-store-migration.js';
|
|
4
5
|
/** Structural validation of one record (required fields + a well-formed `when`). Unknown fields are allowed. */
|
|
5
6
|
export function isSchedulerRecord(v) {
|
|
6
7
|
if (!isPlainJsonObject(v))
|
|
7
8
|
return false;
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
// a type guard, so every field is read STRICTLY from the record's own keys (own, enumerable, data — the kernel's `ownDeclaredValueOf`): an inherited field is absent
|
|
10
|
+
// (a polluted `Object.prototype.prompt` is not a prompt the record carries); an accessor / hidden field, or one that cannot be read ⇒ not a record — no getter is called,
|
|
11
|
+
// so what the caller reads from the record next is what was judged
|
|
12
|
+
const field = (o, key) => {
|
|
13
|
+
const read = ownDeclaredValueOf(o, key);
|
|
14
|
+
return read.ok ? read.value : undefined;
|
|
15
|
+
};
|
|
16
|
+
if (typeof field(v, 'id') !== 'string' || typeof field(v, 'scope') !== 'string' || typeof field(v, 'prompt') !== 'string' || typeof field(v, 'createdMs') !== 'number')
|
|
10
17
|
return false;
|
|
11
|
-
const w =
|
|
18
|
+
const w = field(v, 'when');
|
|
12
19
|
if (!isPlainJsonObject(w))
|
|
13
20
|
return false;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
const kind = field(w, 'kind');
|
|
22
|
+
if (kind === 'cron')
|
|
23
|
+
return typeof field(w, 'expr') === 'string';
|
|
24
|
+
if (kind === 'at')
|
|
25
|
+
return typeof field(w, 'atMs') === 'number';
|
|
26
|
+
if (kind === 'delay')
|
|
27
|
+
return typeof field(w, 'delaySec') === 'number';
|
|
20
28
|
return false;
|
|
21
29
|
}
|
|
22
30
|
/** 一条会话链上的记录过期多久算「永驻孤儿」:`lifetime: "session"` ∧ `sessionId` 在场 ∧ `nextRunMs` 已过去这么久 ⇒ 读口丢弃并告警(7 天)。 */
|
|
@@ -38,7 +46,7 @@ export function parseSchedulerStore(text, opts) {
|
|
|
38
46
|
catch {
|
|
39
47
|
return { records: [], warnings: [{ kind: 'unreadable-file', why: 'not-json' }] };
|
|
40
48
|
}
|
|
41
|
-
if (!
|
|
49
|
+
if (!isJsonArray(parsed))
|
|
42
50
|
return { records: [], warnings: [{ kind: 'unreadable-file', why: 'not-an-array' }] };
|
|
43
51
|
const warnings = [];
|
|
44
52
|
const kept = [];
|
package/dist/settings-units.js
CHANGED
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* the engine's env catalog, whose one implementation is the server's.
|
|
43
43
|
*/
|
|
44
44
|
import { classifyHooksConfig } from "./hooks-config.js";
|
|
45
|
+
import { captureJson, captureStringArray, ownEntriesOf } from "./own-key-read.js";
|
|
45
46
|
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
46
47
|
import { SAFETY_MERGE_SPEC, resolveMemberTighten, tightenVerdict } from "./safety-merge-spec.js";
|
|
47
48
|
/** The settings-document units (see the file header). `path` is the unit's place in a settings document. */
|
|
@@ -98,20 +99,17 @@ const UNREADABLE = { ok: false, reason: "unreadable" };
|
|
|
98
99
|
const LAUNCH_ONLY = { ok: false, reason: "launch_only" };
|
|
99
100
|
/** `stringList`: an array whose every element is a non-empty string (a hole reads as `undefined`, so a sparse array is not one). */
|
|
100
101
|
function isNonEmptyStringList(v) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
for (let i = 0; i < v.length; i++) {
|
|
104
|
-
const x = v[i];
|
|
105
|
-
if (typeof x !== "string" || x.length === 0)
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
return true;
|
|
102
|
+
const list = captureStringArray(v, "strict"); // the package's one string-array reading, strict: the value is handed back as is, so an accessor / hidden index ⇒ not a list (no getter is called)
|
|
103
|
+
return list !== undefined && list.every((x) => x.length > 0);
|
|
109
104
|
}
|
|
110
|
-
/** `envMap`: a plain object (prototype `Object.prototype` or `null`), non-empty keys, string values.
|
|
105
|
+
/** `envMap`: a plain object (prototype `Object.prototype` or `null`), non-empty keys, string values. The value is handed back AS IS, so its members are read strictly —
|
|
106
|
+
* the package's one whole-container read, strict (`ownEntriesOf`: one `ownKeys`, each own string key's descriptor asked once — symbol keys never; never `[[Get]]`, never a
|
|
107
|
+
* proxy's `get` trap): an accessor member or a non-enumerable member ⇒ not an env map (no getter is called — what the caller reads next is what was judged); unreadable ⇒ not one. */
|
|
111
108
|
function isEnvMap(v) {
|
|
112
109
|
if (!isPlainJsonObject(v))
|
|
113
110
|
return false;
|
|
114
|
-
|
|
111
|
+
const members = ownEntriesOf(v);
|
|
112
|
+
return members.ok && members.value.every(([k, x]) => k.length > 0 && typeof x === "string");
|
|
115
113
|
}
|
|
116
114
|
/** What a declared member's value class adds on top of its comparator's domain — read through the comparator
|
|
117
115
|
* {@link resolveMemberTighten} derived from the row, so the seat is interpreted in one place: `list` ⇒ setUnion / setNarrower,
|
|
@@ -144,8 +142,12 @@ function shapedReading(unit, row, rule, raw) {
|
|
|
144
142
|
case "killSwitch":
|
|
145
143
|
// CC spells ON "disable"; the boolean itself is the member comparator's domain
|
|
146
144
|
return { ok: true, value: raw === "disable" ? true : raw };
|
|
147
|
-
case "hooks":
|
|
148
|
-
|
|
145
|
+
case "hooks": {
|
|
146
|
+
// the value is handed back AS IS, so it is read strictly (as every unit is): the whole tree is snapshotted by the kernel's strict whole-container read first —
|
|
147
|
+
// an accessor anywhere or a non-enumerable member ⇒ not readable (no getter is called) — and the classifier judges the snapshot, not the caller's object
|
|
148
|
+
const snapshot = captureJson(raw);
|
|
149
|
+
return snapshot.ok && classifyHooksConfig({ hooks: snapshot.value }).outcome === "loaded" ? { ok: true, value: raw } : UNREADABLE;
|
|
150
|
+
}
|
|
149
151
|
case "envMap":
|
|
150
152
|
return isEnvMap(raw) ? { ok: true, value: raw } : UNREADABLE;
|
|
151
153
|
case "nonEmptyString":
|
package/dist/types.d.ts
CHANGED
|
@@ -1830,11 +1830,11 @@ export interface SkillsManifest {
|
|
|
1830
1830
|
* legal hash scheme. */
|
|
1831
1831
|
export declare const SkillsManifestSchema: z.ZodObject<{
|
|
1832
1832
|
skills: z.ZodPrefault<z.ZodArray<z.ZodObject<{
|
|
1833
|
+
description: z.ZodDefault<z.ZodString>;
|
|
1833
1834
|
name: z.ZodString;
|
|
1834
1835
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
1835
1836
|
notes: z.ZodOptional<z.ZodString>;
|
|
1836
1837
|
rosters: z.ZodPrefault<z.ZodArray<z.ZodString>>;
|
|
1837
|
-
description: z.ZodDefault<z.ZodString>;
|
|
1838
1838
|
scenarios: z.ZodPrefault<z.ZodArray<z.ZodString>>;
|
|
1839
1839
|
allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1840
1840
|
disallowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -3219,10 +3219,14 @@ export type EffectiveKey = z.infer<typeof EffectiveKey>;
|
|
|
3219
3219
|
* `prompts` is the second wire divergence: on the wire it is {@link EffectivePromptsWire} (`{schemaVersion: 2, artifact}` — the one
|
|
3220
3220
|
* artifact {@link promptArtifactFor} selected for the pulling worker, stamped with its publish provenance), carried verbatim by the
|
|
3221
3221
|
* read leg and judged by the consumer (that shape, then the engine's verify) — optional (absent = nothing published, or the publish
|
|
3222
|
-
* gate is closed), never the storage shape.
|
|
3222
|
+
* gate is closed), never the storage shape.
|
|
3223
|
+
* `sandboxGit` (14.7.0) is the third wire-only key and the third carried verbatim: the sandbox git plane's origin block — web-admin projects it from the
|
|
3224
|
+
* deployment's registry, it is NOT a stored domain, so `buildEffective` never produces it and `readEffectiveWire` neither validates nor drops it. Optional with
|
|
3225
|
+
* no default (absent = the deployment projects no such block); the ONE seat that judges it is `SandboxGitConfig` (`./sandbox-git`), run by the consumer before use. */
|
|
3223
3226
|
export type EffectiveWire = Omit<EffectiveConfig, "skills" | "prompts"> & {
|
|
3224
3227
|
skills: SkillsManifest;
|
|
3225
3228
|
prompts?: unknown;
|
|
3229
|
+
sandboxGit?: unknown;
|
|
3226
3230
|
};
|
|
3227
3231
|
/** Reserved audit "domain" for RBAC/user-management events (not a config domain → no snapshot, not
|
|
3228
3232
|
* rollback-able). Lets user changes share the one audit stream the Audit page already renders. */
|
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;
|
|
@@ -605,7 +606,7 @@ const RoleMap = z.partialRecord(ModelRole, RoleTarget, {
|
|
|
605
606
|
// 枚举键的 record 在运行期以 `unrecognized_keys`(带 `keys`)报未知键,zod 的型面只列了 invalid_type / invalid_key —— 按运行期形读(钉:role-words-12.0.0)。
|
|
606
607
|
error: (iss) => {
|
|
607
608
|
const raw = iss;
|
|
608
|
-
return raw.code === "unrecognized_keys" &&
|
|
609
|
+
return raw.code === "unrecognized_keys" && isJsonArray(raw.keys) ? retiredRoleWordsRefusal(raw.keys) : undefined;
|
|
609
610
|
},
|
|
610
611
|
});
|
|
611
612
|
/** {@link AtMention} 的模式闭集:`all` = 每个启用的模型都可 `@`;`none` = 一个都不许;`only` = 只许 `models` 里点名的那几个。 */
|
|
@@ -1607,7 +1608,11 @@ export function applyProjectPatch(doc, projectId, patch) {
|
|
|
1607
1608
|
const current = Object.prototype.hasOwnProperty.call(doc.projects, projectId) ? doc.projects[projectId] : undefined;
|
|
1608
1609
|
if (current === undefined)
|
|
1609
1610
|
throw new Error(`applyProjectPatch: project "${projectId}" is not registered in this document`);
|
|
1610
|
-
|
|
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;
|
|
1611
1616
|
if (!parsed.success) {
|
|
1612
1617
|
const issues = parsed.error.issues.map((i) => ({ ...i, path: ["projects", projectId, ...i.path] }));
|
|
1613
1618
|
return { success: false, error: new z.ZodError(issues) };
|
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;
|