@sema-agent/settings-schema 14.4.0 → 14.6.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 +303 -0
- package/README.md +22 -1
- package/conformance/VERSION +1 -1
- package/dist/api/scopes.d.ts +1 -1
- package/dist/api/scopes.js +1 -1
- package/dist/atomic-write.d.ts +11 -0
- package/dist/atomic-write.js +38 -5
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/config-d-file.d.ts +4 -0
- package/dist/config-d-file.js +11 -4
- package/dist/config-dir-io.js +12 -2
- package/dist/config-dir-migration.d.ts +44 -0
- package/dist/config-dir-migration.js +91 -36
- package/dist/config-fns.js +3 -9
- package/dist/conformance/canonical.js +4 -7
- package/dist/conformance/corpus.js +3 -3
- package/dist/conformance/derive.js +6 -5
- package/dist/conformance/run.js +2 -1
- package/dist/definition-copy.d.ts +23 -0
- package/dist/definition-copy.js +52 -0
- package/dist/file-store.d.ts +1 -31
- package/dist/file-store.js +3 -30
- package/dist/fleet.js +2 -1
- package/dist/fs-failure.d.ts +17 -2
- package/dist/fs-failure.js +55 -15
- package/dist/hooks-config.d.ts +2 -2
- package/dist/hooks-config.js +112 -31
- package/dist/http-hook-allowlists.d.ts +7 -3
- package/dist/http-hook-allowlists.js +102 -16
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/mcp-request-entry.d.ts +9 -6
- package/dist/mcp-request-entry.js +225 -93
- package/dist/node.d.ts +2 -1
- package/dist/node.js +2 -1
- package/dist/one-time-migrations.js +36 -41
- package/dist/plain-json-object.d.ts +31 -0
- package/dist/plain-json-object.js +52 -0
- package/dist/portable-domains.d.ts +31 -0
- package/dist/portable-domains.js +30 -0
- package/dist/safety-merge-spec.d.ts +88 -6
- package/dist/safety-merge-spec.js +233 -17
- package/dist/scheduler-store.js +3 -2
- package/dist/settings-units.d.ts +85 -0
- package/dist/settings-units.js +175 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +61 -53
- package/dist/url-credential.d.ts +41 -0
- package/dist/url-credential.js +95 -0
- package/dist/url-shape.d.ts +11 -5
- package/dist/url-shape.js +11 -5
- package/dist/workers-rows.d.ts +2 -1
- package/dist/workers-rows.js +5 -4
- package/docs/API-SURFACE.md +30 -4
- package/docs/CONSUMER-INTEGRATION.md +2 -0
- package/llms-full.txt +24 -2
- package/llms.txt +6 -2
- package/package.json +2 -3
package/dist/file-store.js
CHANGED
|
@@ -29,25 +29,13 @@ import path from "node:path";
|
|
|
29
29
|
import { buildEffective, DOMAIN_READ_FALLBACK, parseDomainLoud } from "./config-fns.js";
|
|
30
30
|
import { configDPath, readConfigDFile, refuseRetiredDomainFiles } from "./config-d-file.js";
|
|
31
31
|
import { stableHash } from "./hash.js";
|
|
32
|
+
import { PORTABLE_DOMAINS } from "./portable-domains.js";
|
|
32
33
|
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
33
34
|
import { RETIRED_DOMAINS, isDomainName, isRetiredDomainName } from "./types.js";
|
|
34
35
|
/** The retired domain names (`RETIRED_DOMAINS` keys) — read from `config.d/` for their tombstone only. */
|
|
35
36
|
const RETIRED_DOMAIN_NAMES = Object.keys(RETIRED_DOMAINS);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
* {@link NON_PORTABLE_DOMAIN_REASONS} 里有一条成文理由(禁裸排除,#282 件5 附带裁定)。
|
|
39
|
-
*
|
|
40
|
-
* ⚠️ 口径诚实(对抗复审 复审 round1 F4):这是**默认集与本地读取集,不是 API 级硬边界** —— `exportBundle`
|
|
41
|
-
* / `readBundle` 的 `domains` 参数可显式传任意 `DomainName`(bundle.ts 不拒非便携域)。把非便携域搬进
|
|
42
|
-
* bundle 的那条口子归调用方纪律(以及各自域的导出面),不是本常量能挡住的;要不要把 bundle API 收窄到
|
|
43
|
-
* `PortableDomain` 是一个独立的行为面裁定(#282 未决项,需三问),本批不擅自改。
|
|
44
|
-
*
|
|
45
|
-
* 🔴 **开新顶层域时必须同步登记本表**(成案 = 消费方 #322 真事故):域表是**闭集**,不在这里的
|
|
46
|
-
* 域名,`config.d/<域>.json` 不会被读进 effective —— 消费方拿到的是「该域缺席」,和文件不存在**逐字
|
|
47
|
-
* 同形**。当年 `limits` 域就是这样在单机腿静默丢了一整片限额发布(远端腿同批可用,所以更难看出来)。
|
|
48
|
-
* 今日的兜底:漏登记的文件会被 {@link FileConfigStore} 的 `"unread-config-file"` 警告点名(fail-loud,
|
|
49
|
-
* 见 getEffective),但那只在消费方接了 `onWarning` 时才响 —— **登记仍是第一现场的义务**。 */
|
|
50
|
-
export const PORTABLE_DOMAINS = ["models", "rosters", "skills", "mcp", "plugins", "scenarios", "collab", "governance", "execution", "prompts", "limits"];
|
|
37
|
+
// 便携域表的定义住在纯叶 ./portable-domains.ts(14.6.0;零 node 依赖,主桶模块可 import);这里原名转出口,`./node` 子路径逐名不变。
|
|
38
|
+
export { NON_PORTABLE_DOMAIN_REASONS, PORTABLE_DOMAINS } from "./portable-domains.js";
|
|
51
39
|
/** `config.d/` 里**合法存在但不是域文档**的文件名(不进 `"unread-config-file"` 告警)。`remote-exec`
|
|
52
40
|
* 不是 `DOMAIN_SCHEMAS` 成员(resolver 不读它),按 service 表决住在这个目录,由 loadRemoteExec 读
|
|
53
41
|
* ——**它有自己的读取与校验路径**,所以既不是「未知文件」也不是「域文件」。
|
|
@@ -55,21 +43,6 @@ export const PORTABLE_DOMAINS = ["models", "rosters", "skills", "mcp", "plugins"
|
|
|
55
43
|
* **导出**(3.0.0):一致性门的语料派生要按同一张表给本地腿的文件分类(域文件 / 专用文件 / 忽略),
|
|
56
44
|
* 手抄一份必漂 —— 漂了的那一天,门会把一个合法的 `remote-exec.json` 报成未知文件。 */
|
|
57
45
|
export const NON_DOMAIN_CONFIG_FILES = new Set([`${REMOTE_EXEC_DOCUMENT}.json`]);
|
|
58
|
-
/**
|
|
59
|
-
* 非便携域的**排除理由表**(#282 件5 附带裁定:禁裸排除)。
|
|
60
|
-
*
|
|
61
|
-
* `satisfies Record<Exclude<DomainName, PortableDomain>, string>` 让键集在**编译期**双向咬死:新开一个
|
|
62
|
-
* 域却不放进 PORTABLE_DOMAINS ⇒ 这里缺行 ⇒ 编译错;把某域改成便携却忘了删行 ⇒ 多余键 ⇒ 编译错。
|
|
63
|
-
* (此前是「理由写在注释里」,机器只能做子串匹配——域名出现在文件任何角落都算数,等于没门;对抗复审
|
|
64
|
-
* 复审 round1 F3。)理由要写**为什么它不该被单机/跨部署搬运**,带出处。
|
|
65
|
-
*/
|
|
66
|
-
export const NON_PORTABLE_DOMAIN_REASONS = {
|
|
67
|
-
workers: "FLEET-only:单机没有 reconciler,`{workers:[]}` 的 schema 默认就是正确语义(缺席不需要 stub)。",
|
|
68
|
-
hosts: "FLEET-only:同 workers —— 单机无机器注册表/自动排布,空注册表即正确语义。",
|
|
69
|
-
systems: "FLEET-only:系统接入/凭据下发的元数据(nonce+路由)属于 web-admin 侧生命周期,单机 lane 无对应执法点。",
|
|
70
|
-
projects: "TENANT IDENTITY DATA,不是可搬运的配置:登记簿随包复制进另一个部署 = 凭空多出的身份登记(design/142 §1.4 威胁模型①「carried-in foreign projectId」换了身 import 的衣服)。要带走走 142 §5 的导出面(service 半场)。",
|
|
71
|
-
entitlement: "per-PRINCIPAL 商务/治理数据(tier·预算上限·kill-switch·allowlist),与 projects 同威胁模型(#282 件5 附带裁定,此前是唯一裸排除项):(a) 搬走 = 把一个租户的商务档复制进另一部署;(b) 反向也不成立 —— 单机 TOC 无 principal 轴(无鉴权语境、无 `/effective?principal=` 解析面),本地 entitlement.json 只会造出一份**永不被执法**的假管控面(真执法点在 service / web-admin 边界)。缺席语义如实:本地 buildEffective 给它 schema 默认值(空表),这**不等于**把远端 kill-switch/预算上限搬到了本地 —— 单机 lane 本就没有那个执法点。",
|
|
72
|
-
};
|
|
73
46
|
export class FileConfigStore {
|
|
74
47
|
root;
|
|
75
48
|
opts;
|
package/dist/fleet.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { INTEGER } from "./integer.js";
|
|
22
22
|
import { fnv1a64 } from "./hash.js";
|
|
23
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
23
24
|
import { buildQuotedIssue } from "./quoted-issue.js";
|
|
24
25
|
// ── Web-admin's fleet endpoints (paths, one spelling) ─────────────────────────────────────────────────────
|
|
25
26
|
/**
|
|
@@ -173,7 +174,7 @@ export const RestartSignal = z.object({
|
|
|
173
174
|
*/
|
|
174
175
|
function judgeSignalBody(body, pick, schema) {
|
|
175
176
|
try {
|
|
176
|
-
if (
|
|
177
|
+
if (!isPlainJsonObject(body))
|
|
177
178
|
return { kind: "not-an-object" };
|
|
178
179
|
const parsed = schema.safeParse(pick(body));
|
|
179
180
|
return parsed.success ? { kind: "read", value: parsed.data } : { kind: "invalid" };
|
package/dist/fs-failure.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 落点({@link FsFailureSite}.`landing`,闭集)按失败发生在哪一步分句 —— 同一个 errno 在不同落点指向不同的东西:
|
|
7
7
|
* · `dir-create-temp` —— 原子写器在目标所在目录里建临时文件。目录不让建 ⇒ 点名**目录**(文件本身没错);
|
|
8
|
+
* · `chmod` —— 原子写器给临时文件设模式(已有的文件沿用它原来的模式,临时文件建成后、`rename` 之前设)。这一步失败 ⇒ 点名**设模式**这件事(文件系统不让改文件模式;目录没有问题),
|
|
9
|
+
* 不提目录、不提不可变标志;`path` 是被保留模式的**真文件**(chmod 作用在替换它的临时副本上,用户认得的是真文件名);
|
|
8
10
|
* · `rename` —— 原子写器 `rename` 盖过目标。目录收了新文件、却换不掉这一份 ⇒ 点名**真文件**;
|
|
9
11
|
* · `remove` —— 删一个文件。删的是目录项:目录不让删 ⇒ 点名文件所在的**目录**(与建临时文件同一个因);
|
|
10
12
|
* · `read` —— 读一个目录或文件;
|
|
@@ -12,22 +14,35 @@
|
|
|
12
14
|
* 认得的 errno 给那一步对应的出路,别的 errno(磁盘满、I/O 错 …)给 Node 自己的句子 —— 照样响亮,只是不替它猜出路。两条与落点无关的律:
|
|
13
15
|
* · errno 只打一遍 —— 句首括号里一次,Node 句子自带的 `<errno>: ` 前缀去掉;
|
|
14
16
|
* · `ENAMETOOLONG`(路径某一节或整条超限)既不是目录的错也不是文件的错 ⇒ 一句 `path too long: <被操作系统拒的路径> (ENAMETOOLONG)`,不点名目录、不给权限类出路。
|
|
15
|
-
*
|
|
17
|
+
* 三个落点(`dir-create-temp` / `remove` / `rename`)的权限类出路走**同一张表**、一只函数({@link permissionWayOut}):落点表 {@link WAY_OUT_LANDING}(这个落点认哪些 errno、清标志点名谁、动已有目录项的落点才有粘滞目录一句)×
|
|
18
|
+
* errno 表 {@link WAY_OUT_BY_ERRNO}(穷举;每个 errno 两格 —— 目录权限位对本进程「可写」/「不可写或没读过」)。同一个 `EPERM` 既可能是目录模式不让写,也可能是不可变标志(macOS `chflags uchg` / Linux `chattr +i`),
|
|
19
|
+
* 同一个 `EACCES` 在模式位已经放行时只能是 ACL 条目或安全模块策略 —— 铸点是纯函数,分不清,但调用方在 Node 一侧能读目录模式,把「模式对本进程可写」这个事实经落点的 `directoryWritable` 递进来:
|
|
20
|
+
* 「可写」⇒ chmod 已经没用,`EPERM` 的首选出路 = 清标志(`chflags nouchg` / `chattr -i`),动已有目录项的落点(remove / rename)再接粘滞目录一句(那条属主规则给的就是 `EPERM`),`EACCES` 的出路 = 查 ACL / 安全模块(不接粘滞句);
|
|
21
|
+
* 否则(模式不可写,或调用方没读过模式 —— 注入的替身 I/O 抛的错,包看不到它背后)⇒ 先让目录可写,`EPERM` / `EROFS` 句尾提示「只读 / 不可变」的可能。`EROFS` 的出路不变(只读文件系统清不掉)。
|
|
22
|
+
* `rename` 落点的「可写」不是读出来的而是已证明的({@link RENAME_DIRECTORY_WRITABLE}:走到 rename 时临时文件刚在同一目录建成)—— 一个来源,`EPERM` 与 `EACCES` 同走「可写」那一列,所以 rename 句从不说「make the directory writable」。
|
|
16
23
|
*
|
|
17
24
|
* 原子写器抛 {@link FsFailure}(句子在 `message`,`code` 照抄底层 errno,原错误在 `cause`);它在写器内部已经按更细的落点铸过句子,
|
|
18
25
|
* 命令行 / 迁移用 {@link fsFailureMessage} 取句 —— 已经是 {@link FsFailure} 的原样用,否则按它所在的口铸。
|
|
19
26
|
*/
|
|
20
27
|
/** 一次失败发生的地方(闭集):落点加那一步交给操作系统的真实路径(`dir-create-temp` 是要建的临时文件,点名的目录是它的父目录;`rename` 是被盖过的真文件;
|
|
21
|
-
* `remove` / `write` 是那份文件;`read`
|
|
28
|
+
* `remove` / `write` 是那份文件;`read` 是读的目录或文件;`chmod` 是被保留模式的真文件〔这一格不是「交给操作系统的路径」口径:chmod 的对象是替换它的临时副本〕)。
|
|
29
|
+
* `directoryWritable`(只有 `dir-create-temp` / `remove` 两个落点有)—— 失败那一刻,目录的权限位对本进程是否可写(Node 一侧读的事实,见 `./atomic-write` 的 `directoryAdmitsWrite`);
|
|
30
|
+
* 缺席 = 没读过(注入的替身 I/O),出路顺序按「不可写」走。`rename` 落点没有这一格:走到 rename 时临时文件刚在同一目录建成,目录可写是已证明的事实,由铸点按 {@link RENAME_DIRECTORY_WRITABLE} 给出。
|
|
31
|
+
*/
|
|
22
32
|
export type FsFailureSite = {
|
|
23
33
|
readonly landing: "dir-create-temp";
|
|
24
34
|
readonly path: string;
|
|
35
|
+
readonly directoryWritable?: boolean;
|
|
36
|
+
} | {
|
|
37
|
+
readonly landing: "chmod";
|
|
38
|
+
readonly path: string;
|
|
25
39
|
} | {
|
|
26
40
|
readonly landing: "rename";
|
|
27
41
|
readonly path: string;
|
|
28
42
|
} | {
|
|
29
43
|
readonly landing: "remove";
|
|
30
44
|
readonly path: string;
|
|
45
|
+
readonly directoryWritable?: boolean;
|
|
31
46
|
} | {
|
|
32
47
|
readonly landing: "read";
|
|
33
48
|
readonly path: string;
|
package/dist/fs-failure.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 落点({@link FsFailureSite}.`landing`,闭集)按失败发生在哪一步分句 —— 同一个 errno 在不同落点指向不同的东西:
|
|
7
7
|
* · `dir-create-temp` —— 原子写器在目标所在目录里建临时文件。目录不让建 ⇒ 点名**目录**(文件本身没错);
|
|
8
|
+
* · `chmod` —— 原子写器给临时文件设模式(已有的文件沿用它原来的模式,临时文件建成后、`rename` 之前设)。这一步失败 ⇒ 点名**设模式**这件事(文件系统不让改文件模式;目录没有问题),
|
|
9
|
+
* 不提目录、不提不可变标志;`path` 是被保留模式的**真文件**(chmod 作用在替换它的临时副本上,用户认得的是真文件名);
|
|
8
10
|
* · `rename` —— 原子写器 `rename` 盖过目标。目录收了新文件、却换不掉这一份 ⇒ 点名**真文件**;
|
|
9
11
|
* · `remove` —— 删一个文件。删的是目录项:目录不让删 ⇒ 点名文件所在的**目录**(与建临时文件同一个因);
|
|
10
12
|
* · `read` —— 读一个目录或文件;
|
|
@@ -12,20 +14,58 @@
|
|
|
12
14
|
* 认得的 errno 给那一步对应的出路,别的 errno(磁盘满、I/O 错 …)给 Node 自己的句子 —— 照样响亮,只是不替它猜出路。两条与落点无关的律:
|
|
13
15
|
* · errno 只打一遍 —— 句首括号里一次,Node 句子自带的 `<errno>: ` 前缀去掉;
|
|
14
16
|
* · `ENAMETOOLONG`(路径某一节或整条超限)既不是目录的错也不是文件的错 ⇒ 一句 `path too long: <被操作系统拒的路径> (ENAMETOOLONG)`,不点名目录、不给权限类出路。
|
|
15
|
-
*
|
|
17
|
+
* 三个落点(`dir-create-temp` / `remove` / `rename`)的权限类出路走**同一张表**、一只函数({@link permissionWayOut}):落点表 {@link WAY_OUT_LANDING}(这个落点认哪些 errno、清标志点名谁、动已有目录项的落点才有粘滞目录一句)×
|
|
18
|
+
* errno 表 {@link WAY_OUT_BY_ERRNO}(穷举;每个 errno 两格 —— 目录权限位对本进程「可写」/「不可写或没读过」)。同一个 `EPERM` 既可能是目录模式不让写,也可能是不可变标志(macOS `chflags uchg` / Linux `chattr +i`),
|
|
19
|
+
* 同一个 `EACCES` 在模式位已经放行时只能是 ACL 条目或安全模块策略 —— 铸点是纯函数,分不清,但调用方在 Node 一侧能读目录模式,把「模式对本进程可写」这个事实经落点的 `directoryWritable` 递进来:
|
|
20
|
+
* 「可写」⇒ chmod 已经没用,`EPERM` 的首选出路 = 清标志(`chflags nouchg` / `chattr -i`),动已有目录项的落点(remove / rename)再接粘滞目录一句(那条属主规则给的就是 `EPERM`),`EACCES` 的出路 = 查 ACL / 安全模块(不接粘滞句);
|
|
21
|
+
* 否则(模式不可写,或调用方没读过模式 —— 注入的替身 I/O 抛的错,包看不到它背后)⇒ 先让目录可写,`EPERM` / `EROFS` 句尾提示「只读 / 不可变」的可能。`EROFS` 的出路不变(只读文件系统清不掉)。
|
|
22
|
+
* `rename` 落点的「可写」不是读出来的而是已证明的({@link RENAME_DIRECTORY_WRITABLE}:走到 rename 时临时文件刚在同一目录建成)—— 一个来源,`EPERM` 与 `EACCES` 同走「可写」那一列,所以 rename 句从不说「make the directory writable」。
|
|
16
23
|
*
|
|
17
24
|
* 原子写器抛 {@link FsFailure}(句子在 `message`,`code` 照抄底层 errno,原错误在 `cause`);它在写器内部已经按更细的落点铸过句子,
|
|
18
25
|
* 命令行 / 迁移用 {@link fsFailureMessage} 取句 —— 已经是 {@link FsFailure} 的原样用,否则按它所在的口铸。
|
|
19
26
|
*/
|
|
20
|
-
/** `
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
/** `rename` 落点上「目录对本进程可写」的事实:走到 rename 时临时文件刚在**同一目录**建成,这是已证明的,不是读出来的 —— 写器不对 rename 落点读模式位(一个来源)。 */
|
|
28
|
+
const RENAME_DIRECTORY_WRITABLE = true;
|
|
29
|
+
const MODE_BITS_ADMIT = "the directory's permission bits already admit this process, so chmod will not help";
|
|
30
|
+
const MAKE_DIRECTORY_WRITABLE = "make the directory writable";
|
|
31
|
+
const READ_ONLY_OR_IMMUTABLE = `${MAKE_DIRECTORY_WRITABLE}; directory may be read-only or immutable (chflags / chattr)`;
|
|
32
|
+
/** errno 表:每个权限类 errno 在目录权限位「可写」(`admits`:chmod 已无用,说还剩什么真因)与「不可写或没读过」(`otherwise`)两格的出路,以及「可写」时是否接粘滞目录一句(`stickyWhenAdmitted`)。
|
|
33
|
+
* `EROFS` / `ENOENT` 的两格同一句 —— 模式位可不可写不改变它们的出路(只读文件系统清不掉;目录不在,先让它在)。 */
|
|
34
|
+
const WAY_OUT_BY_ERRNO = {
|
|
35
|
+
EPERM: {
|
|
36
|
+
admits: (flagSubject) => `clear the immutable flag on ${flagSubject} (macOS: chflags nouchg; Linux: chattr -i) — ${MODE_BITS_ADMIT}`,
|
|
37
|
+
otherwise: READ_ONLY_OR_IMMUTABLE,
|
|
38
|
+
stickyWhenAdmitted: true,
|
|
39
|
+
},
|
|
40
|
+
EACCES: {
|
|
41
|
+
admits: () => `${MODE_BITS_ADMIT}; an ACL entry may still deny this process (macOS: ls -le; Linux: getfacl); so may a security-module policy (SELinux / AppArmor: the audit log names the rule)`,
|
|
42
|
+
otherwise: MAKE_DIRECTORY_WRITABLE,
|
|
43
|
+
stickyWhenAdmitted: false, // 粘滞目录的属主规则返回的是 EPERM(macOS `man 2 unlink` / `rename`;Linux 同,推断),EACCES 的句尾不指向一个产生不了它的因
|
|
44
|
+
},
|
|
45
|
+
EROFS: { admits: () => READ_ONLY_OR_IMMUTABLE, otherwise: READ_ONLY_OR_IMMUTABLE, stickyWhenAdmitted: false },
|
|
46
|
+
ENOENT: { admits: () => MAKE_DIRECTORY_WRITABLE, otherwise: MAKE_DIRECTORY_WRITABLE, stickyWhenAdmitted: false },
|
|
47
|
+
};
|
|
48
|
+
const WAY_OUT_LANDING = {
|
|
49
|
+
"dir-create-temp": { errnos: ["EACCES", "EPERM", "EROFS", "ENOENT"], flagSubject: "the directory" },
|
|
50
|
+
remove: { errnos: ["EACCES", "EPERM", "EROFS"], flagSubject: "the file or its directory", stickyVerb: "remove" },
|
|
51
|
+
rename: { errnos: ["EACCES", "EPERM"], flagSubject: "the file or its directory", stickyVerb: "replace" },
|
|
52
|
+
};
|
|
53
|
+
/** 权限类出路的**唯一一只函数**(三个落点共用,表驱动):`code` 不是这个落点认的权限类 errno ⇒ `undefined`(调用方给 Node 自己的句子)。
|
|
54
|
+
* 组合律一条:errno 表的格(按 `directoryWritable === true` 选「可写」列或「不可写或没读过」列);「可写」∧ 该 errno 带粘滞 ∧ 这个落点有 `stickyVerb` ⇒ 句尾接 `in a sticky directory only the file's owner may <verb> it`。 */
|
|
55
|
+
const permissionWayOut = (landing, code, directoryWritable) => {
|
|
56
|
+
const spec = WAY_OUT_LANDING[landing];
|
|
57
|
+
const errno = spec.errnos.find((e) => e === code);
|
|
58
|
+
if (errno === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
const row = WAY_OUT_BY_ERRNO[errno];
|
|
61
|
+
if (directoryWritable !== true)
|
|
62
|
+
return row.otherwise;
|
|
63
|
+
const way = row.admits(spec.flagSubject);
|
|
64
|
+
return row.stickyWhenAdmitted && spec.stickyVerb !== undefined ? `${way}; in a sticky directory only the file's owner may ${spec.stickyVerb} it` : way;
|
|
65
|
+
};
|
|
66
|
+
/** `chmod` 落点认的 errno:`EPERM` —— 创建者对自己刚建的临时文件 chmod 被拒,只能是文件系统 / 挂载不支持改模式。别的 errno 给 Node 自己的句子。 */
|
|
67
|
+
const CHMOD_REFUSED = new Set(["EPERM"]);
|
|
68
|
+
const CHMOD_WAY_OUT = "the filesystem does not allow changing file modes — keep this file on a filesystem that supports POSIX permissions";
|
|
29
69
|
const errnoOf = (e) => {
|
|
30
70
|
const code = e?.code;
|
|
31
71
|
return typeof code === "string" ? code : undefined;
|
|
@@ -49,13 +89,13 @@ export function fsFailureSentence(site, cause) {
|
|
|
49
89
|
const tag = code === undefined ? "" : ` (${code})`;
|
|
50
90
|
switch (site.landing) {
|
|
51
91
|
case "dir-create-temp":
|
|
52
|
-
return `cannot write into directory ${parentDirectory(site.path)}${tag}: ${code
|
|
92
|
+
return `cannot write into directory ${parentDirectory(site.path)}${tag}: ${permissionWayOut("dir-create-temp", code, site.directoryWritable) ?? nodeSentenceOf(cause, code)}`;
|
|
93
|
+
case "chmod":
|
|
94
|
+
return `cannot set the mode of ${site.path}${tag}: ${code !== undefined && CHMOD_REFUSED.has(code) ? CHMOD_WAY_OUT : nodeSentenceOf(cause, code)}`;
|
|
53
95
|
case "rename":
|
|
54
|
-
return code
|
|
55
|
-
? `cannot replace ${site.path}${tag}: make the file replaceable — clear an immutable flag; in a sticky directory only the file's owner may replace it`
|
|
56
|
-
: `cannot replace ${site.path}${tag}: ${nodeSentenceOf(cause, code)}`;
|
|
96
|
+
return `cannot replace ${site.path}${tag}: ${permissionWayOut("rename", code, RENAME_DIRECTORY_WRITABLE) ?? nodeSentenceOf(cause, code)}`;
|
|
57
97
|
case "remove":
|
|
58
|
-
return `cannot remove ${site.path} from directory ${parentDirectory(site.path)}${tag}: ${code
|
|
98
|
+
return `cannot remove ${site.path} from directory ${parentDirectory(site.path)}${tag}: ${permissionWayOut("remove", code, site.directoryWritable) ?? nodeSentenceOf(cause, code)}`;
|
|
59
99
|
case "read":
|
|
60
100
|
return `cannot read ${site.of === "directory" ? "directory " : ""}${site.path}${tag}: ${nodeSentenceOf(cause, code)}`;
|
|
61
101
|
case "write":
|
package/dist/hooks-config.d.ts
CHANGED
|
@@ -167,8 +167,8 @@ export declare const HOOKS_DROPPED_PIECE_CODES: readonly ["hooks-not-object", "u
|
|
|
167
167
|
export type HooksDroppedPieceCode = (typeof HOOKS_DROPPED_PIECE_CODES)[number];
|
|
168
168
|
/**
|
|
169
169
|
* Why the document was REJECTED (closed; each is "a guard may be in this piece"):
|
|
170
|
-
* · `guard-outside-hooks` — a guard sign outside `hooks` (top level, under another key, or the document itself matcher-shaped);
|
|
171
|
-
* · `hooks-is-matcher` — `hooks` is an array holding objects / arrays, a single matcher, or a single entry;
|
|
170
|
+
* · `guard-outside-hooks` — a guard sign outside `hooks` (top level, under another key, or the document itself matcher-shaped), or the document could not be read at all (path `[]`);
|
|
171
|
+
* · `hooks-is-matcher` — `hooks` is an array holding objects / arrays, a single matcher, or a single entry, or an object that is not a readable block (a class instance, a value that cannot be inspected) holding — or possibly holding — a guard sign;
|
|
172
172
|
* · `guard-under-unknown-event` — an unknown event name holding a guard sign;
|
|
173
173
|
* · `guard-event-not-array` — a guard event whose value is not an array (and not null), or a known event whose non-array value holds a sign;
|
|
174
174
|
* · `guard-matcher-invalid` — a bad matcher in a guard event, or a matcher holding a sign (in any event);
|
package/dist/hooks-config.js
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
* contract; cycles are tolerated, a throwing accessor propagates.
|
|
47
47
|
*/
|
|
48
48
|
import { z } from "zod";
|
|
49
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
49
50
|
import { buildQuotedIssue, fixed } from "./quoted-issue.js";
|
|
50
51
|
// ── event vocabulary ───────────────────────────────────────────────────────────────────────────────────────
|
|
51
52
|
// The two halves below are where each word came from (CC 2.1.198's own table, then the later additions) — provenance for the
|
|
@@ -141,8 +142,8 @@ export const HOOKS_CLASSIFICATION_OUTCOMES = ["loaded", "loaded-partial", "rejec
|
|
|
141
142
|
export const HOOKS_DROPPED_PIECE_CODES = ["hooks-not-object", "unknown-event", "event-not-array", "matcher-invalid", "entry-invalid"];
|
|
142
143
|
/**
|
|
143
144
|
* Why the document was REJECTED (closed; each is "a guard may be in this piece"):
|
|
144
|
-
* · `guard-outside-hooks` — a guard sign outside `hooks` (top level, under another key, or the document itself matcher-shaped);
|
|
145
|
-
* · `hooks-is-matcher` — `hooks` is an array holding objects / arrays, a single matcher, or a single entry;
|
|
145
|
+
* · `guard-outside-hooks` — a guard sign outside `hooks` (top level, under another key, or the document itself matcher-shaped), or the document could not be read at all (path `[]`);
|
|
146
|
+
* · `hooks-is-matcher` — `hooks` is an array holding objects / arrays, a single matcher, or a single entry, or an object that is not a readable block (a class instance, a value that cannot be inspected) holding — or possibly holding — a guard sign;
|
|
146
147
|
* · `guard-under-unknown-event` — an unknown event name holding a guard sign;
|
|
147
148
|
* · `guard-event-not-array` — a guard event whose value is not an array (and not null), or a known event whose non-array value holds a sign;
|
|
148
149
|
* · `guard-matcher-invalid` — a bad matcher in a guard event, or a matcher holding a sign (in any event);
|
|
@@ -156,12 +157,57 @@ export const HOOKS_REJECTION_CODES = [
|
|
|
156
157
|
"guard-matcher-invalid",
|
|
157
158
|
"guard-entry-invalid",
|
|
158
159
|
];
|
|
159
|
-
/**
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
160
|
+
/** The value's shape — six words: `plain` is a JSON object (not null, not an array, prototype null or a prototype whose own prototype is null: any realm's `Object.prototype`),
|
|
161
|
+
* `uninspectable` is a value whose shape cannot be read at all (a proxy that has been revoked, a throwing `getPrototypeOf` trap). This function IS `./plain-json-object`'s `jsonShapeOf`, character for
|
|
162
|
+
* character: the classifier is copied as source by consumers' generators, so its closure cannot import — the package has this one second copy, and the test is what keeps it from drifting. */
|
|
163
|
+
function hookShapeOf(v) {
|
|
164
|
+
try {
|
|
165
|
+
if (v === null)
|
|
166
|
+
return "null";
|
|
167
|
+
if (Array.isArray(v))
|
|
168
|
+
return "array";
|
|
169
|
+
if (typeof v === "function")
|
|
170
|
+
return "non_plain_object";
|
|
171
|
+
if (typeof v !== "object")
|
|
172
|
+
return "primitive";
|
|
173
|
+
const proto = Object.getPrototypeOf(v);
|
|
174
|
+
return proto === null || Object.getPrototypeOf(proto) === null ? "plain" : "non_plain_object";
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return "uninspectable"; // 读形状就抛(已撤销的 Proxy 连 `Array.isArray` 都抛、`getPrototypeOf` 陷阱抛):不是「不朴素」,是读不出 —— 读者按最严处置
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** `./plain-json-object` 的 `isPlainJsonObject`,调各自的形状分类核后逐字同文(闭包里不能 import)。 */
|
|
181
|
+
function hookIsRecord(v) {
|
|
182
|
+
const shape = hookShapeOf(v);
|
|
183
|
+
return shape === "plain";
|
|
184
|
+
}
|
|
185
|
+
/** Is the value object-like — anything but null / a primitive (arrays, functions and values whose shape cannot be read count). The shape is asked first, never a `typeof` split. */
|
|
186
|
+
function hookIsObjectLike(v) {
|
|
187
|
+
const shape = hookShapeOf(v);
|
|
188
|
+
switch (shape) {
|
|
189
|
+
case "null":
|
|
190
|
+
case "primitive":
|
|
191
|
+
return false;
|
|
192
|
+
case "plain":
|
|
193
|
+
case "non_plain_object":
|
|
194
|
+
case "array":
|
|
195
|
+
case "uninspectable":
|
|
196
|
+
return true;
|
|
197
|
+
default: {
|
|
198
|
+
const unhandled = shape;
|
|
199
|
+
throw new Error(`hookIsObjectLike: unhandled shape ${String(unhandled)}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** `./plain-json-object` 的 `isJsonArray`,逐字同文(闭包里不能 import):`Array.isArray` 的总版 —— 已撤销的 Proxy 读不出 ⇒ 不是数组,不抛。 */
|
|
204
|
+
function hookIsArray(v) {
|
|
205
|
+
try {
|
|
206
|
+
return Array.isArray(v);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false; // 已撤销的 Proxy:读不出 ⇒ 不是数组
|
|
210
|
+
}
|
|
165
211
|
}
|
|
166
212
|
/** The first own key of `node` (in key order) that names a guard event with a value other than null / undefined / `[]` (CC `ei`). */
|
|
167
213
|
function hookGuardKeyOf(node) {
|
|
@@ -169,7 +215,7 @@ function hookGuardKeyOf(node) {
|
|
|
169
215
|
if (!HOOK_GUARD_EVENTS.includes(key))
|
|
170
216
|
continue;
|
|
171
217
|
const value = node[key];
|
|
172
|
-
if (value !== null && value !== undefined && !(
|
|
218
|
+
if (value !== null && value !== undefined && !(hookIsArray(value) && value.length === 0))
|
|
173
219
|
return key;
|
|
174
220
|
}
|
|
175
221
|
return undefined;
|
|
@@ -179,7 +225,7 @@ function hookLooksLikeMatcher(value) {
|
|
|
179
225
|
if (!hookIsRecord(value))
|
|
180
226
|
return false;
|
|
181
227
|
const inner = Object.hasOwn(value, "hooks") ? value.hooks : undefined;
|
|
182
|
-
if (
|
|
228
|
+
if (hookIsArray(inner))
|
|
183
229
|
return inner.length > 0;
|
|
184
230
|
return hookIsRecord(inner) && (Object.hasOwn(value, "matcher") || typeof inner.type === "string");
|
|
185
231
|
}
|
|
@@ -203,25 +249,48 @@ function hookGuardSignIn(root, rootPath, place) {
|
|
|
203
249
|
};
|
|
204
250
|
while (pending.length > 0) {
|
|
205
251
|
const [node, frame, at] = pending.pop();
|
|
206
|
-
|
|
252
|
+
// the shape is asked BEFORE any `typeof` split: a node whose shape cannot be read — a proxy that has been revoked, of a function target included — cannot be shown free of guard signs
|
|
253
|
+
// ⇒ reported as a sign at its own coordinate (fail-closed: the document is rejected); no `typeof` gate of the classifier's own stands in front of the seat
|
|
254
|
+
const shape = hookShapeOf(node);
|
|
255
|
+
let isList = false;
|
|
256
|
+
switch (shape) {
|
|
257
|
+
case "null":
|
|
258
|
+
case "primitive":
|
|
259
|
+
continue; // not an object: nothing to search
|
|
260
|
+
case "uninspectable":
|
|
261
|
+
return pathTo(frame, []);
|
|
262
|
+
case "array":
|
|
263
|
+
isList = true;
|
|
264
|
+
break;
|
|
265
|
+
case "plain":
|
|
266
|
+
case "non_plain_object":
|
|
267
|
+
break; // searched as an object below
|
|
268
|
+
default: {
|
|
269
|
+
const unhandled = shape;
|
|
270
|
+
throw new Error(`hookGuardSignIn: unhandled shape ${String(unhandled)}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const visited = node;
|
|
274
|
+
if (seen[at].has(visited))
|
|
207
275
|
continue;
|
|
208
|
-
seen[at].add(
|
|
209
|
-
if (
|
|
210
|
-
|
|
276
|
+
seen[at].add(visited);
|
|
277
|
+
if (isList) {
|
|
278
|
+
const list = node;
|
|
279
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
211
280
|
frames.push({ key: i, parent: frame });
|
|
212
|
-
pending.push([
|
|
281
|
+
pending.push([list[i], frames.length - 1, at === "matcherList" ? "matcher" : "other"]);
|
|
213
282
|
}
|
|
214
283
|
continue;
|
|
215
284
|
}
|
|
216
|
-
const guardKey = hookGuardKeyOf(
|
|
285
|
+
const guardKey = hookGuardKeyOf(visited);
|
|
217
286
|
if (guardKey !== undefined)
|
|
218
287
|
return pathTo(frame, [guardKey]);
|
|
219
|
-
if (at !== "matcher" && hookLooksLikeMatcher(
|
|
288
|
+
if (at !== "matcher" && hookLooksLikeMatcher(visited))
|
|
220
289
|
return pathTo(frame, []);
|
|
221
|
-
const keys = Object.keys(
|
|
290
|
+
const keys = Object.keys(visited);
|
|
222
291
|
for (let i = keys.length - 1; i >= 0; i--) {
|
|
223
292
|
frames.push({ key: keys[i], parent: frame });
|
|
224
|
-
pending.push([
|
|
293
|
+
pending.push([visited[keys[i]], frames.length - 1, "other"]);
|
|
225
294
|
}
|
|
226
295
|
}
|
|
227
296
|
return undefined;
|
|
@@ -248,7 +317,7 @@ function hookFieldFits(base, value) {
|
|
|
248
317
|
case "stringRecord":
|
|
249
318
|
return hookIsRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
250
319
|
case "stringList":
|
|
251
|
-
if (!
|
|
320
|
+
if (!hookIsArray(value))
|
|
252
321
|
return false;
|
|
253
322
|
for (let i = 0; i < value.length; i++)
|
|
254
323
|
if (typeof value[i] !== "string")
|
|
@@ -283,7 +352,7 @@ function hookEntryRead(entry) {
|
|
|
283
352
|
const base = (optional ? kind.slice(0, -1) : kind);
|
|
284
353
|
if (!hookFieldFits(base, value))
|
|
285
354
|
return { field };
|
|
286
|
-
out[field] =
|
|
355
|
+
out[field] = hookIsArray(value) ? value.slice() : hookIsRecord(value) ? Object.fromEntries(Object.entries(value)) : value;
|
|
287
356
|
}
|
|
288
357
|
return { entry: out };
|
|
289
358
|
}
|
|
@@ -301,18 +370,18 @@ function hookMatcherRead(matcher, at, guardEvent, reject, drop) {
|
|
|
301
370
|
// the rejection's `field` is the matcher's own guard-event key if it has one, else the first member (document order) whose subtree holds a sign —
|
|
302
371
|
// the order hookGuardSignIn reports
|
|
303
372
|
const signField = hookGuardKeyOf(matcher) ??
|
|
304
|
-
Object.keys(matcher).find((key) => !(key === "hooks" &&
|
|
373
|
+
Object.keys(matcher).find((key) => !(key === "hooks" && hookIsArray(entries)) && hookGuardSignIn(matcher[key], [...at, key], "other") !== undefined);
|
|
305
374
|
if (signField !== undefined) {
|
|
306
375
|
reject("guard-matcher-invalid", at, signField);
|
|
307
376
|
return undefined;
|
|
308
377
|
}
|
|
309
378
|
const entrySigns = [];
|
|
310
|
-
if (
|
|
379
|
+
if (hookIsArray(entries))
|
|
311
380
|
for (let j = 0; j < entries.length; j++)
|
|
312
381
|
entrySigns.push(hookGuardSignIn(entries[j], [...at, "hooks", j], "other"));
|
|
313
382
|
const name = Object.hasOwn(matcher, "matcher") ? matcher.matcher : undefined;
|
|
314
|
-
const badField = name !== undefined && typeof name !== "string" ? "matcher" : !
|
|
315
|
-
if (badField !== undefined || !
|
|
383
|
+
const badField = name !== undefined && typeof name !== "string" ? "matcher" : !hookIsArray(entries) ? "hooks" : undefined;
|
|
384
|
+
if (badField !== undefined || !hookIsArray(entries)) {
|
|
316
385
|
if (guardEvent || entrySigns.some((sign) => sign !== undefined))
|
|
317
386
|
reject("guard-matcher-invalid", at, badField);
|
|
318
387
|
else
|
|
@@ -340,7 +409,10 @@ function hookMatcherRead(matcher, at, guardEvent, reject, drop) {
|
|
|
340
409
|
/** The `hooks` value → what loads of it (undefined when nothing does), reporting its bad pieces / signs into the two sinks. */
|
|
341
410
|
function hookBlockRead(hooks, reject, drop) {
|
|
342
411
|
if (!hookIsRecord(hooks)) {
|
|
343
|
-
if (
|
|
412
|
+
if (hookIsArray(hooks) && hooks.some(hookIsObjectLike))
|
|
413
|
+
reject("hooks-is-matcher", ["hooks"]);
|
|
414
|
+
// an object that is not a readable block (a class instance, a proxy that cannot be inspected) holding — or possibly holding — a guard sign: reject, never drop it (a dropped guard silently disappears)
|
|
415
|
+
else if (hookGuardSignIn(hooks, ["hooks"], "other") !== undefined)
|
|
344
416
|
reject("hooks-is-matcher", ["hooks"]);
|
|
345
417
|
else
|
|
346
418
|
drop("hooks-not-object", ["hooks"]);
|
|
@@ -355,14 +427,14 @@ function hookBlockRead(hooks, reject, drop) {
|
|
|
355
427
|
const value = hooks[event];
|
|
356
428
|
const at = ["hooks", event];
|
|
357
429
|
if (!HOOK_EVENTS.includes(event)) {
|
|
358
|
-
if (hookGuardSignIn(value, at,
|
|
430
|
+
if (hookGuardSignIn(value, at, hookIsArray(value) ? "matcherList" : "other") !== undefined)
|
|
359
431
|
reject("guard-under-unknown-event", at);
|
|
360
432
|
else
|
|
361
433
|
drop("unknown-event", at);
|
|
362
434
|
continue;
|
|
363
435
|
}
|
|
364
436
|
const guardEvent = HOOK_GUARD_EVENTS.includes(event);
|
|
365
|
-
if (!
|
|
437
|
+
if (!hookIsArray(value)) {
|
|
366
438
|
if ((guardEvent && value !== null) || hookGuardSignIn(value, at, "other") !== undefined)
|
|
367
439
|
reject("guard-event-not-array", at);
|
|
368
440
|
else
|
|
@@ -392,6 +464,15 @@ function hookBlockRead(hooks, reject, drop) {
|
|
|
392
464
|
* the hooks-classify test file (group ⑤) pins that the free names are exactly these.
|
|
393
465
|
*/
|
|
394
466
|
export function classifyHooksConfig(settingsDoc) {
|
|
467
|
+
try {
|
|
468
|
+
return hookClassify(settingsDoc);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// reading the document threw (a trap, an accessor that throws): nothing in it can be shown free of guard signs ⇒ rejected at the root — ONE `try` for the whole classifier, none per node
|
|
472
|
+
return { outcome: "rejected", hooks: undefined, dropped: [], rejections: [{ code: "guard-outside-hooks", path: [] }] };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function hookClassify(settingsDoc) {
|
|
395
476
|
const rejections = [];
|
|
396
477
|
const dropped = [];
|
|
397
478
|
const reject = (code, path, field) => {
|
|
@@ -401,7 +482,7 @@ export function classifyHooksConfig(settingsDoc) {
|
|
|
401
482
|
dropped.push(field === undefined ? { code, path } : { code, path, field });
|
|
402
483
|
};
|
|
403
484
|
let hooks;
|
|
404
|
-
if (
|
|
485
|
+
if (hookIsRecord(settingsDoc)) {
|
|
405
486
|
const doc = settingsDoc;
|
|
406
487
|
for (const key of Object.keys(doc)) {
|
|
407
488
|
const value = doc[key];
|
|
@@ -412,7 +493,7 @@ export function classifyHooksConfig(settingsDoc) {
|
|
|
412
493
|
// the document itself written as a matcher: its top-level `matcher` key is the evidence, reported at that key's place
|
|
413
494
|
const sign = key === "matcher" && hookLooksLikeMatcher(doc)
|
|
414
495
|
? [key]
|
|
415
|
-
: HOOK_GUARD_EVENTS.includes(key) && value !== null && value !== undefined && !(
|
|
496
|
+
: HOOK_GUARD_EVENTS.includes(key) && value !== null && value !== undefined && !(hookIsArray(value) && value.length === 0)
|
|
416
497
|
? [key]
|
|
417
498
|
: hookGuardSignIn(value, [key], "other");
|
|
418
499
|
if (sign !== undefined)
|
|
@@ -450,7 +531,7 @@ function hooksFace(carry, strip, take) {
|
|
|
450
531
|
}
|
|
451
532
|
/** The carrier for one entry of type `type`: the value itself when it says that type, else an untyped object (⇒ the issue names `type`). */
|
|
452
533
|
function entryOfType(value, type) {
|
|
453
|
-
return value
|
|
534
|
+
return isPlainJsonObject(value) && value.type === type ? value : {}; // 读不出 / 不是纯数据的值不当「自己说了这个 type」的载体:⇒ `{}`(issue 点名 `type`)
|
|
454
535
|
}
|
|
455
536
|
const entryFace = (type) => hooksFace((v) => ({ hooks: { Stop: [{ hooks: [entryOfType(v, type)] }] } }), 5, (h) => h.Stop[0].hooks[0]);
|
|
456
537
|
export const CommandHookEntry = entryFace("command");
|
|
@@ -43,11 +43,15 @@ export interface HttpHookAllowlistLayerRead {
|
|
|
43
43
|
readonly malformed: readonly HttpHookAllowlistKey[];
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒
|
|
47
|
-
* ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象 ⇒ 两键都缺。
|
|
46
|
+
* 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样(过 `captureAllowlist`〔包内私有〕,与判官同一只读法);其余一切(非数组、含非串条目或洞、取值或读元素时抛、`null`)
|
|
47
|
+
* ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象(`null` / 原始值 / 数组)⇒ 两键都缺。
|
|
48
|
+
* **是对象但不朴素**(类实例 / `Map` / 建在别的对象原型上的记录 / 函数 —— JSON 解析不出来,只能是嵌入方程序拼的)⇒ 同样 fail-closed:两键都读成 `[]` 并点名 `malformed`。
|
|
49
|
+
* 把它当「没有设置」读,等于把一层带名单的托管设置静默丢掉、方向放行(与本模块「坏形 = 一个都不许」的失败方向相反)。
|
|
50
|
+
* 名单键**只靠继承可见**(不是自有键)⇒ 同样 fail-closed:该键读成 `[]` 并点名 `malformed`(自有键盖过继承键;自有键值为 `undefined` 仍是「不设」)。
|
|
51
|
+
* 原型是带成员的 null 原型对象的对象(`Object.create(<null 原型的名单袋>)`)是一例:它按座位的判据读成朴素、按**自有**键读,名单键只在原型上 ⇒ 坏形。它只能由程序造出,JSON 解析不出来(座位头注 `./plain-json-object` 同写)。
|
|
48
52
|
*/
|
|
49
53
|
export declare function readHttpHookAllowlistLayer(source: SettingsSource, settings: unknown): HttpHookAllowlistLayerRead;
|
|
50
|
-
/** 拦下一只 http hook 的原因。`rule`:`empty` = 有一张准入名单是空的(
|
|
54
|
+
/** 拦下一只 http hook 的原因。`rule`:`empty` = 有一张准入名单是空的(那一层改对之前一只都不许;**名单形坏 —— 非串数组、含非串条目或洞、读它或读它的元素时 getter 抛 —— 也读成空**,所以 `empty` 不一定是写成了 `[]`);`url` = 没命中那几张名单里的任何模式。
|
|
51
55
|
* `sources` = 不放行它的那几张名单所在的层(入参序)。零 URL 回显 —— URL 调用方自己有,按受众给显示形。 */
|
|
52
56
|
export interface HttpHookUrlBlock {
|
|
53
57
|
readonly list: "allowedHttpHookUrls";
|