@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
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { isJsonArray, jsonShapeOf } from "./plain-json-object.js";
|
|
2
|
+
/** 撤销 / 读不出的 Proxy 的拒句(`got ` 之后的名词短语)—— **全包唯一的一句**:陷阱抛出来、getter 抛出来、已撤销的 Proxy,读器与描述器都不能因此被带出去。 */
|
|
3
|
+
export const UNINSPECTABLE = "an object that could not be inspected (a proxy trap threw)";
|
|
4
|
+
/**
|
|
5
|
+
* 唯一的**安全检视内核**:读器与描述器要碰不可信的值(`Array.isArray` 对已撤销的 Proxy 抛、`get` / 原型 / 描述符陷阱抛、会抛的自有 getter)都经它 ——
|
|
6
|
+
* 「检视不了」的**律与句都单源**:抛了 ⇒ `ok:false` 带 {@link UNINSPECTABLE} 那一句。
|
|
7
|
+
*/
|
|
8
|
+
export function inspect(read) {
|
|
9
|
+
try {
|
|
10
|
+
return { ok: true, value: read() };
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
// 抛出值由嵌入方说了算(撤销的 Proxy、反射面全抛的对象、原始值):这里只许问「是不是本内核铸的」(身份表,不碰它)与「它抛了」这一事实 ——
|
|
14
|
+
// 内核铸的带它自己的句子(环 / 非纯数据各说各的),其余一律是那一句 {@link UNINSPECTABLE}
|
|
15
|
+
return { ok: false, sentence: sentenceOfMinted(e) ?? UNINSPECTABLE };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* **全包唯一的读内核**:一个键**一次** `getOwnPropertyDescriptor` —— 有 ⇒ `own` 带这份描述符;没有 ⇒ 再问一次 `in`(原型链上有 ⇒ `inherited`,**不读值、不调继承来的 getter**;都没有 ⇒ `absent`);
|
|
20
|
+
* 检视就抛(已撤销的 Proxy、`getOwnPropertyDescriptor` / `has` 陷阱抛)⇒ `unreadable`。从不抛。之后**不再对这个键做第二次读**:「是不是自有」与「值是什么」出自同一份描述符,
|
|
21
|
+
* 中间没有人能删掉它、换成 `Object.prototype` 上被污染的同名值(先 `hasOwn` 再 `[[Get]]` 的两步读法就有这个缝);也从不 `[[Get]]` —— Proxy 的 `get` 陷阱不被问。
|
|
22
|
+
* 两族读法是这一读的两种**归类**:{@link readKey}(自有的都在场:不可枚举的照读,访问器调这份描述符里的 getter 一次)与 {@link readDeclaredKey}(只收自有、可枚举的数据属性,其余 ⇒ `hidden`)。
|
|
23
|
+
*/
|
|
24
|
+
function readOwnDescriptor(record, key) {
|
|
25
|
+
const read = inspect(() => {
|
|
26
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
27
|
+
if (descriptor !== undefined)
|
|
28
|
+
return { presence: "own", descriptor };
|
|
29
|
+
return key in record ? { presence: "inherited" } : { presence: "absent" };
|
|
30
|
+
});
|
|
31
|
+
return read.ok ? read.value : { presence: "unreadable" };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 一份自有描述符的值(全包唯一的取值步):数据属性 ⇒ `value`;访问器 ⇒ 调**这份描述符里的** getter 一次(`this` 是被读的对象;只有 setter ⇒ `undefined`)—— 不经 `[[Get]]`、不问 `get` 陷阱。
|
|
35
|
+
* getter 抛 ⇒ 照抛(调用方在 {@link inspect} 里调它)。描述符是引擎交回的完整描述符(字段全是自有的),所以读 `get` 不碰原型链。
|
|
36
|
+
*/
|
|
37
|
+
export const valueOfDescriptor = (record, d) => {
|
|
38
|
+
if (!isAccessorDescriptor(d))
|
|
39
|
+
return d.value;
|
|
40
|
+
const getter = d.get;
|
|
41
|
+
return getter === undefined ? undefined : Reflect.apply(getter, record, []);
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* 读一个键(内核 {@link readOwnDescriptor} 的宽归类):自有 ⇒ `own` 带值 —— 值只从那一份描述符里来(数据属性取值,不可枚举的也照读;访问器调**它自己的** getter 一次;**不问 `get` 陷阱**);
|
|
45
|
+
* 只靠继承 ⇒ `inherited`(不读值);都没有 ⇒ `absent`;检视就抛(已撤销的 Proxy、`getOwnPropertyDescriptor` / `has` 陷阱抛、自有 getter 抛)⇒ `unreadable`。从不抛。
|
|
46
|
+
*/
|
|
47
|
+
export function readKey(record, key) {
|
|
48
|
+
const read = readOwnDescriptor(record, key);
|
|
49
|
+
if (read.presence !== "own")
|
|
50
|
+
return read;
|
|
51
|
+
const descriptor = read.descriptor;
|
|
52
|
+
const value = inspect(() => valueOfDescriptor(record, descriptor));
|
|
53
|
+
return value.ok ? { presence: "own", value: value.value } : { presence: "unreadable" };
|
|
54
|
+
}
|
|
55
|
+
/** 一个字段的值,**只读自有属性**:继承与缺席都是 `undefined`,读不出 ⇒ `ok:false`(调用方各自决定怎么办、不再各包一层)。由 {@link readKey} 派生。 */
|
|
56
|
+
export function ownValueOf(record, key) {
|
|
57
|
+
const read = readKey(record, key);
|
|
58
|
+
switch (read.presence) {
|
|
59
|
+
case "own":
|
|
60
|
+
return { ok: true, value: read.value };
|
|
61
|
+
case "inherited":
|
|
62
|
+
case "absent":
|
|
63
|
+
return { ok: true, value: undefined };
|
|
64
|
+
case "unreadable":
|
|
65
|
+
return { ok: false, sentence: UNINSPECTABLE };
|
|
66
|
+
default: {
|
|
67
|
+
const unhandled = read;
|
|
68
|
+
throw new Error(`ownValueOf: unhandled presence ${String(unhandled.presence)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** 按顺序读若干字段,**第一个读不出的就停**(后面的一个不读),整体读不出向上传;交出的袋是零原型、定义语义写入。 */
|
|
73
|
+
export function ownFieldsOf(record, keys) {
|
|
74
|
+
const out = Object.create(null);
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
const read = ownValueOf(record, key);
|
|
77
|
+
if (!read.ok)
|
|
78
|
+
return read;
|
|
79
|
+
Object.defineProperty(out, key, dataDescriptor(read.value));
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, value: out };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 包自有查表(`Partial<Record<域名, 行>>` 一类的常量表)的一行:**表自己的行**,没有就 `undefined` —— 行缺席时不从原型链上借一行(`Object.prototype.<域名>` 被污染成数据值时,
|
|
85
|
+
* `table[域名]` 会把它读成「这张表给这个域的行」:写权限的角色、扩展命名空间、未知键扫描的已知键表)。表是包自己的普通对象(没有读不出的问题),所以不经 {@link inspect}。
|
|
86
|
+
*/
|
|
87
|
+
export function ownRow(table, key) {
|
|
88
|
+
return Object.hasOwn(table, key) ? table[key] : undefined;
|
|
89
|
+
}
|
|
90
|
+
/** 本内核铸出的 {@link UninspectableError} 实例 → 它的句子。**抛出值是不是我们的、它说了什么,只问这张表**:`WeakMap` 的 `get` 只比对象身份,不走原型、不读属性、不触发 Proxy 陷阱 ——
|
|
91
|
+
* `instanceof`(读原型)/ `.message`(读属性)/ `String(e)` 在抛出值是撤销的 Proxy 或反射面会抛的对象时自己就抛,会让共享失败处置自己逃出去。 */
|
|
92
|
+
const MINTED = new WeakMap();
|
|
93
|
+
/** 抛出值若是本内核铸的 {@link UninspectableError} ⇒ 它的句子,否则(别人的错 / 随便什么原始值 / 读不出的对象)⇒ `undefined`。只靠身份,从不碰抛出值的反射面。 */
|
|
94
|
+
export function sentenceOfMinted(thrown) {
|
|
95
|
+
return MINTED.get(thrown); // `WeakMap.get` 对非对象键(原始值 / undefined / null)答 `undefined`、不抛:不必先问「是不是对象」(那要么读原型要么写直接的 typeof 比较)
|
|
96
|
+
}
|
|
97
|
+
/** 读不出时 {@link ownOrThrow} 抛的类型:是 `TypeError`(调用方给的对象读不出是调用方的错),message 是全包唯一那一句(或坏形自己的句子:环 / 非纯数据)。
|
|
98
|
+
* 识别它**不用** `instanceof`、取它的句子**不读** `.message` —— 一律走 {@link sentenceOfMinted}。 */
|
|
99
|
+
export class UninspectableError extends TypeError {
|
|
100
|
+
constructor(sentence = UNINSPECTABLE) {
|
|
101
|
+
super(sentence);
|
|
102
|
+
this.name = "UninspectableError";
|
|
103
|
+
MINTED.set(this, sentence);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 描述符是访问器?—— 描述符对象是寻常对象,它的字段**只认自有的**:数据描述符没有自有的 `get` / `set`,继承来的不算(`Object.prototype.get` 被污染成函数时,
|
|
108
|
+
* `d.get !== undefined` 会把每个数据属性判成访问器,整个读口对合法的 JSON 拒服务)。全包问「是不是访问器 / 是不是数据属性」只用这一只(引擎交回的描述符恒完整:
|
|
109
|
+
* 数据属性有自有的 `value` / `writable`,访问器有自有的 `get` / `set`,所以「不是访问器」就是「是数据属性」)。
|
|
110
|
+
*/
|
|
111
|
+
export const isAccessorDescriptor = (d) => Object.hasOwn(d, "get") || Object.hasOwn(d, "set");
|
|
112
|
+
/**
|
|
113
|
+
* 读一个**声明键**(读器声明要读的那个键;内核 {@link readOwnDescriptor} 的严归类,与 {@link readKey} 同一读):自有、可枚举的数据属性 ⇒ `own` 带 `value`(不调 getter、不问 `get` 陷阱);
|
|
114
|
+
* 只靠继承 ⇒ `inherited`、缺席 ⇒ `absent`(调用方都当缺席;缺席时问过一次 `in`:`has` 陷阱抛的 Proxy 是读不出,不是缺席);
|
|
115
|
+
* 自有但不可枚举、或是访问器 ⇒ `hidden`(调用方当「读不成纯数据」整份拒,**不当缺席**,getter 一次都不调);读不出 ⇒ `unreadable`。从不抛。
|
|
116
|
+
*/
|
|
117
|
+
export function readDeclaredKey(record, key) {
|
|
118
|
+
const read = readOwnDescriptor(record, key);
|
|
119
|
+
if (read.presence !== "own")
|
|
120
|
+
return read;
|
|
121
|
+
const d = read.descriptor;
|
|
122
|
+
if (d.enumerable !== true || isAccessorDescriptor(d))
|
|
123
|
+
return { presence: "hidden" };
|
|
124
|
+
return { presence: "own", value: d.value };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 一个**固定键**的值(描述符律,与 {@link readDeclaredKey} 同一只核):自有数据属性 ⇒ 描述符里的值(不调 getter、不问 `get` 陷阱);只靠继承 / 缺席 ⇒ `undefined`;
|
|
128
|
+
* 自有访问器 / 不可枚举的自有键 ⇒ `ok:false` 带 {@link NOT_PLAIN_DATA}(**不当缺席**,getter 零次);读不出 ⇒ 带 {@link UNINSPECTABLE}。从不抛。
|
|
129
|
+
* 给令牌声明、迁移的固定键读、workers 行读口:它们读的是「调用方交来的 JSON」,JSON 铸不出访问器 / 隐藏键。(`ownValueOf` 是同一读的宽归类 —— 隐藏键照读、访问器调一次,闭包注入组里的那一只。)
|
|
130
|
+
*/
|
|
131
|
+
export function ownDeclaredValueOf(record, key) {
|
|
132
|
+
const read = readDeclaredKey(record, key);
|
|
133
|
+
switch (read.presence) {
|
|
134
|
+
case "own":
|
|
135
|
+
return { ok: true, value: read.value };
|
|
136
|
+
case "inherited":
|
|
137
|
+
case "absent":
|
|
138
|
+
return { ok: true, value: undefined };
|
|
139
|
+
case "hidden":
|
|
140
|
+
return { ok: false, sentence: NOT_PLAIN_DATA };
|
|
141
|
+
case "unreadable":
|
|
142
|
+
return { ok: false, sentence: UNINSPECTABLE };
|
|
143
|
+
default: {
|
|
144
|
+
const unhandled = read;
|
|
145
|
+
throw new Error(`ownDeclaredValueOf: unhandled presence ${String(unhandled.presence)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** {@link ownDeclaredValueOf},读不出 / 不是纯数据就抛 {@link UninspectableError}(带各自那一句)。给没有自己失败臂的读者(迁移、令牌判官);有失败臂的读者用 `ownDeclaredValueOf`。 */
|
|
150
|
+
export function ownOrThrow(record, key) {
|
|
151
|
+
const read = ownDeclaredValueOf(record, key);
|
|
152
|
+
if (!read.ok)
|
|
153
|
+
throw new UninspectableError(read.sentence);
|
|
154
|
+
return read.value;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 一个对象的**键值对**(自有可枚举串键,顺序同 `Object.entries`;每个成员一份描述符、值只从它里面取,见 `containerView`:不调 getter、不触发 Proxy 的 `get` 陷阱)—— 给「把整个载荷 / 文档集拍成一只袋」的读者。
|
|
158
|
+
* 自有串键里有不可枚举的或访问器 ⇒ `ok:false` 带 {@link NOT_PLAIN_DATA}(此前用 `Object.entries`:不可枚举的自有键被静默当缺席,访问器被调一次);读不出 ⇒ `ok:false` 带 {@link UNINSPECTABLE}。
|
|
159
|
+
*/
|
|
160
|
+
export function ownEntriesOf(record) {
|
|
161
|
+
return inspect(() => containerView(record, false, "strict").members.map(([key, d]) => [key, valueOfDescriptor(record, d)]));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 一个数组的**元素**,按校验器解析数组的读法读(zod 按下标读元素、从不读非下标键):自有下标,升序 —— {@link containerView} 的 `wide` 归类:先定域、域内每键一次描述符,
|
|
165
|
+
* **只看下标键**,每个下标的值经内核的取值步(与 {@link readKey} 同一步:数据下标取值、不可枚举的下标照读、访问器下标调它自己的 getter 一次,不问 `get` / `has` 陷阱);
|
|
166
|
+
* 洞不是元素(不出现);数组上挂的非下标键(可枚举或隐藏,例:响应式框架挂的 `__ob__`)不是元素、也不构成「读不出」。
|
|
167
|
+
* 读不出(`ownKeys` / `getOwnPropertyDescriptor` 陷阱抛、下标的 getter 抛)⇒ `ok:false` 带 {@link UNINSPECTABLE}(整份)。
|
|
168
|
+
*/
|
|
169
|
+
export function ownElementsOf(array) {
|
|
170
|
+
return inspect(() => containerView(array, true, "wide").members.map(([key, d]) => [Number(key), valueOfDescriptor(array, d)]));
|
|
171
|
+
}
|
|
172
|
+
/** {@link ownOrThrow} 再把值拍成**自有键快照**({@link captureJson}):字段值本身是对象 / 数组时,调用方之后对它做的任何事(交给校验器、`includes`、取成员)都落在副本上,不再触发原值的陷阱 / getter;
|
|
173
|
+
* 值读不出 / 是环 / 不是纯数据 ⇒ 抛 {@link UninspectableError}(带各自的句子)。原始值原样。给「读一个声明、再拿它做事」的读者(令牌声明)。 */
|
|
174
|
+
export function ownJsonOrThrow(record, key) {
|
|
175
|
+
const copy = captureJson(ownOrThrow(record, key));
|
|
176
|
+
if (!copy.ok)
|
|
177
|
+
throw new UninspectableError(copy.sentence);
|
|
178
|
+
return copy.value;
|
|
179
|
+
}
|
|
180
|
+
/** 跑 `body`;它里面的 {@link ownOrThrow} 读不出(抛 {@link UninspectableError})⇒ 答 `unreadable()`。别的抛(不是我们的)照常往外传。给「读不出 ⇒ 我自己的 fail-closed 答案」的读者:读法写成直线(`ownOrThrow`),失败臂只在入口写一处。 */
|
|
181
|
+
export function orWhenUninspectable(body, unreadable) {
|
|
182
|
+
try {
|
|
183
|
+
return body();
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
if (sentenceOfMinted(e) !== undefined)
|
|
187
|
+
return unreadable();
|
|
188
|
+
throw e;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 要读错误对象的 zod 判决里「校验过程抛 ⇒ 读不出」的**唯一写法**({@link inspect} 用在 zod 判决上;只问 `success` / `data` 的判决不构造错误对象,直接 `inspect`):`run` 是一次 `safeParse`(调用点照写 `X.safeParse(…)`,不改形),它与判决里
|
|
193
|
+
* **错误对象的整个构造**在同一只守护里 —— 首读 `error`(拼 issue 句子:`received …` 会读输入的 `constructor.name`,另一 realm 的怪形值、被污染的运行时在这一步抛)
|
|
194
|
+
* 与它的 `message`(`JSON.stringify(issues)`:issue 的 `params` 里会抛的 getter、环在这一步抛)。zod 4.4 在 `safeParse` 里就把两步做完,4.6 两步都是首读才做:
|
|
195
|
+
* 只守 `safeParse` 那一行的话,同一份输入在两版 zod 上一个答「读不出」、一个把引擎的错抛出去(或在后面的脱敏里抛)。
|
|
196
|
+
* 调用方对错误对象的处置(`finish`,例:脱敏 —— 它要读 issue 的每个成员,`params` 里不可枚举的会抛 getter `JSON.stringify` 跳过、脱敏不跳过)也在同一只守护里:
|
|
197
|
+
* 错误对象读不完 ⇒ 判决读不出,不论卡在构造还是处置。
|
|
198
|
+
* 抛了 ⇒ `ok:false`(调用方给它自己那一句「读不出」);不抛 ⇒ `ok:true` 带判决(`error` 已经过 `finish`)。
|
|
199
|
+
*/
|
|
200
|
+
export function guardedSafeParse(run, finish = (error) => error) {
|
|
201
|
+
return inspect(() => {
|
|
202
|
+
const r = run();
|
|
203
|
+
if (r.success)
|
|
204
|
+
return { success: true, data: r.data };
|
|
205
|
+
const error = r.error;
|
|
206
|
+
void error.message; // 4.6 的 message 是首读才 `JSON.stringify` 的:在守护里读一次,两版 zod 同答
|
|
207
|
+
return { success: false, error: finish(error) };
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
export const NO_PRIOR_READS = new Map();
|
|
211
|
+
/**
|
|
212
|
+
* 一个容器的**自有视图**(全包唯一的整容器读):**先定域、再问描述符** —— 一次 `Reflect.ownKeys`(只问 `ownKeys` 陷阱)列出自有键,筛出**域**:串键(符号键不是任何读口声明的键,
|
|
213
|
+
* 不在域内);数组再只留**下标键**与 `length`(挂在数组上的非下标键 —— 可枚举或隐藏,例:响应式框架挂的 `__ob__` —— 从来不是元素);然后**只对域内的键**逐个问一次
|
|
214
|
+
* `getOwnPropertyDescriptor`(交回 `undefined` ⇒ 不是成员)。域外的键一次都不问描述符 —— 它们的描述符陷阱抛也不构成「读不出」。每个成员恰一份描述符,
|
|
215
|
+
* 之后不再对被读的对象做任何属性读(没有第二次 `[[Get]]`,所以没有人能在两次读之间改它),值只从这份描述符里取。数组成员按下标升序(校验器按下标读的顺序)。两种归类,与逐键读的内核同一对:
|
|
216
|
+
* · `strict`(整容器捕获与交回原引用 / 类型守卫的读口:`captureJson` / `ownEntriesOf` / `captureStringArray` 的 `strict`):成员 = 对象的自有串键 / 数组的下标;其中有不可枚举的、或是访问器的 ⇒ 抛 {@link UninspectableError}(NOT_PLAIN_DATA)
|
|
217
|
+
* —— 「自有但读不成纯数据」不是「缺席」,getter 一次都不调;成员都是数据属性。
|
|
218
|
+
* · `wide`(交回副本 / 值的读口:`ownElementsOf` / `captureStringArray` 的 `wide` / `stableStringify`):成员 = 数组的下标(不可枚举的也算 —— 按下标读的校验器读得到)/ 对象的**可枚举**串键(同 `Object.keys`);
|
|
219
|
+
* 访问器成员的值由调用方经取值步调这份描述符里的 getter(一次)。
|
|
220
|
+
* `prior`({@link PriorReads}):已经读过的键由那一次读数定(排在成员最前),列成员时跳过、不再问描述符。
|
|
221
|
+
* 读不出(`ownKeys` / 域内键的 `getOwnPropertyDescriptor` 陷阱抛、撤销的 Proxy)⇒ 抛 {@link UninspectableError}。
|
|
222
|
+
*/
|
|
223
|
+
export const containerView = (record, isArray, classification, prior = NO_PRIOR_READS) => {
|
|
224
|
+
const keys = inspect(() => Reflect.ownKeys(record));
|
|
225
|
+
if (!keys.ok)
|
|
226
|
+
throw new UninspectableError();
|
|
227
|
+
const members = [];
|
|
228
|
+
const add = (key, d) => appendOwn(members, [key, d]); // 定义语义追加(见 appendOwn)
|
|
229
|
+
for (const [key, read] of prior)
|
|
230
|
+
if (read.presence === "own")
|
|
231
|
+
add(key, dataDescriptor(read.value));
|
|
232
|
+
let length;
|
|
233
|
+
let foreign = false;
|
|
234
|
+
for (const key of keys.value) {
|
|
235
|
+
if (typeof key !== "string") {
|
|
236
|
+
foreign = true; // 符号键不在域内:不问它的描述符,只记「有域外键」
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (prior.has(key))
|
|
240
|
+
continue; // 已经读过的键由那一次读数定(上面),不问第二次
|
|
241
|
+
const isLength = isArray && key === "length";
|
|
242
|
+
if (isArray && !isLength && !isArrayIndexKey(key)) {
|
|
243
|
+
foreign = true; // 数组的域 = 下标键 + `length`:非下标键不问描述符、不取、不判(与 JSON 往返、校验器解析数组同),只记「有域外键」
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const read = inspect(() => Object.getOwnPropertyDescriptor(record, key));
|
|
247
|
+
if (!read.ok)
|
|
248
|
+
throw new UninspectableError();
|
|
249
|
+
const d = read.value;
|
|
250
|
+
if (d === undefined)
|
|
251
|
+
continue; // `ownKeys` 列出、却没有描述符(Proxy)⇒ 不是成员
|
|
252
|
+
if (isLength) {
|
|
253
|
+
length = valueOfDescriptor(record, d);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
switch (classification) {
|
|
257
|
+
case "strict":
|
|
258
|
+
if (d.enumerable !== true || isAccessorDescriptor(d))
|
|
259
|
+
throw new UninspectableError(NOT_PLAIN_DATA);
|
|
260
|
+
break;
|
|
261
|
+
case "wide":
|
|
262
|
+
if (!isArray && d.enumerable !== true)
|
|
263
|
+
continue; // 对象的成员 = 可枚举串键(同 `Object.keys`);数组的下标不论可枚举
|
|
264
|
+
break;
|
|
265
|
+
default: {
|
|
266
|
+
const unhandled = classification;
|
|
267
|
+
throw new Error(`containerView: unhandled classification ${String(unhandled)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
add(key, d);
|
|
271
|
+
}
|
|
272
|
+
if (isArray)
|
|
273
|
+
members.sort(([a], [b]) => Number(a) - Number(b));
|
|
274
|
+
return { members, length, foreign };
|
|
275
|
+
};
|
|
276
|
+
/**
|
|
277
|
+
* 一个**稠密数组**的整容器读数({@link containerView}):`length` 是安全非负整数、自有下标正好 `0 … length-1`(不顺原型链读继承的数字键;`length` 很大的稀疏数组立即判否,不逐位扫)
|
|
278
|
+
* ⇒ 每个下标**那一份**描述符(升序;值由调用方经 {@link valueOfDescriptor} 取)与 `foreign`(见 {@link ContainerView});否则 `undefined`。读不出 ⇒ 抛 {@link UninspectableError}(调用方在 {@link inspect} 里调)。
|
|
279
|
+
* 串数组的捕获({@link captureStringArray})与安全轴的「只许普通数组」(`./http-hook-allowlists`)都从这一只取稠密判据,不各写一份。
|
|
280
|
+
*/
|
|
281
|
+
export function denseArrayView(array, classification) {
|
|
282
|
+
const view = containerView(array, true, classification);
|
|
283
|
+
const n = view.length;
|
|
284
|
+
if (typeof n !== "number" || !Number.isSafeInteger(n) || n < 0 || view.members.length !== n)
|
|
285
|
+
return undefined;
|
|
286
|
+
if (!view.members.every(([key], i) => key === String(i)))
|
|
287
|
+
return undefined;
|
|
288
|
+
return { descriptors: view.members.map(([, d]) => d), foreign: view.foreign };
|
|
289
|
+
}
|
|
290
|
+
const isArrayIndexKey = (key) => {
|
|
291
|
+
const n = Number(key);
|
|
292
|
+
return Number.isInteger(n) && n >= 0 && n < 2 ** 32 - 1 && String(n) === key;
|
|
293
|
+
};
|
|
294
|
+
/** {@link captureJson} 的两句坏形(与「检视不了」那一句并列,各只在这里拼一次):环 —— JSON 没有环;非纯数据 —— 只有朴素对象 / 数组 / 原始值是纯数据。 */
|
|
295
|
+
export const CYCLIC_VALUE = "a value that refers to itself (a JSON document has no cycles)";
|
|
296
|
+
export const NOT_PLAIN_DATA = "a value that is not plain data (a class instance, a Map, a Date, a function, an object with a hidden — non-enumerable — or an accessor own property … — only plain objects, arrays and primitives are)";
|
|
297
|
+
/** 一个节点在 {@link captureJson} 的状态表里:拷贝进行中(再遇到它 ⇒ 环)或已拷完(再遇到它 ⇒ 共享引用,用同一份副本)。 */
|
|
298
|
+
const CAPTURE_IN_PROGRESS = Symbol("captureJson: copy in progress");
|
|
299
|
+
/**
|
|
300
|
+
* 一份纯数据值的**自有键快照**(整棵树;每个容器**一次**列成员、每个成员一份描述符、只从描述符取值 —— 见 `containerView`:不调任何 getter、不触发 `get` 陷阱,继承来的值进不来,没有「前一个 getter 改了后面的键」这回事):
|
|
301
|
+
* · 朴素对象(座位的 `plain`)⇒ 数据副本,键 = 自有可枚举的串键(符号键不进;`JSON.parse` 铸得出的自有 `__proto__` 键作为自有数据属性保留);**自有的串键里有不可枚举的、或是访问器的 ⇒ 整份拒**(NOT_PLAIN_DATA),不静默丢、不读;
|
|
302
|
+
* 副本默认**隔离原型**({@link COPY_PROTOTYPE}:读者之后在副本上读没有继承可说),`copyAs: "ordinary"` 时是寻常对象(交回给调用方的那一份);
|
|
303
|
+
* · 数组 ⇒ 同长的普通数组副本,元素只拷自有下标,**洞仍是洞**(按自有可枚举键走,不按 `length` 逐位扫:`length` 很大的稀疏数组不拖死读者);
|
|
304
|
+
* · 原始值原样;
|
|
305
|
+
* · **没有字面深度上限**:JSON 往返能成的东西都要能拷,实现是迭代的(显式栈),深度只受内存限制;
|
|
306
|
+
* · 共享引用(菱形)只拷一次、副本里仍共享;
|
|
307
|
+
* · `prior`(可选,{@link PriorReads}):根上那个值已经读过的键 —— 它们的在场与值取那一次的读数,不再问描述符(判别联合选中整份捕获的臂时,判别词就是选臂的词);
|
|
308
|
+
* · 坏形只有三种,各一句,`ok:false`、不抛:**环**({@link CYCLIC_VALUE};再遇到进行中的节点)· **非纯数据**({@link NOT_PLAIN_DATA}:`Map` / `Date` / 类实例 / 函数 / 带隐藏自有键的对象,不论在哪一层)·
|
|
309
|
+
* **读不出**({@link UNINSPECTABLE}:陷阱抛、撤销的 Proxy)。
|
|
310
|
+
* 副本是新对象,读者之后在副本上读 / 改都不碰调用方的值,也不受 `Object.prototype` / `Array.prototype` 污染影响。
|
|
311
|
+
*/
|
|
312
|
+
export function captureJson(v, copyAs = "isolated", prior = NO_PRIOR_READS) {
|
|
313
|
+
const state = new Map();
|
|
314
|
+
const stack = [];
|
|
315
|
+
const emptyObject = () => (copyAs === "ordinary" ? {} : isolatedObject());
|
|
316
|
+
/** 一个容器:已拷完的共享节点 ⇒ 用同一份副本;进行中 ⇒ 环;否则压入一帧(拷完再挂到父上)。 */
|
|
317
|
+
const open = (source, isArray, parent, parentKey) => {
|
|
318
|
+
const seen = state.get(source);
|
|
319
|
+
if (seen === CAPTURE_IN_PROGRESS)
|
|
320
|
+
throw new UninspectableError(CYCLIC_VALUE);
|
|
321
|
+
if (seen !== undefined)
|
|
322
|
+
return { done: true, value: seen };
|
|
323
|
+
const copy = isArray ? [] : emptyObject();
|
|
324
|
+
const view = containerView(source, isArray, "strict", parent === undefined ? prior : NO_PRIOR_READS); // 一次列成员、每个成员一份描述符,之后不再碰 `source`(见 containerView);`prior` 只属于根上那个值
|
|
325
|
+
if (isArray) {
|
|
326
|
+
const length = view.length;
|
|
327
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > 2 ** 32 - 1)
|
|
328
|
+
throw new UninspectableError();
|
|
329
|
+
copy.length = length;
|
|
330
|
+
}
|
|
331
|
+
state.set(source, CAPTURE_IN_PROGRESS);
|
|
332
|
+
appendOwn(stack, { source, copy, entries: view.members.map(([key, d]) => [key, valueOfDescriptor(source, d)]), next: 0, parent, parentKey }); // 定义语义压栈(见 appendOwn)
|
|
333
|
+
return { done: false };
|
|
334
|
+
};
|
|
335
|
+
/** 一个值 ⇒ 它的副本(叶子),或交给 {@link open}(容器)。形状只问座位。 */
|
|
336
|
+
const enter = (node, parent, parentKey) => {
|
|
337
|
+
const shape = jsonShapeOf(node);
|
|
338
|
+
switch (shape) {
|
|
339
|
+
case "null":
|
|
340
|
+
case "primitive":
|
|
341
|
+
return { done: true, value: node };
|
|
342
|
+
case "non_plain_object":
|
|
343
|
+
throw new UninspectableError(NOT_PLAIN_DATA);
|
|
344
|
+
case "uninspectable":
|
|
345
|
+
throw new UninspectableError();
|
|
346
|
+
case "array":
|
|
347
|
+
return open(node, true, parent, parentKey);
|
|
348
|
+
case "plain":
|
|
349
|
+
return open(node, false, parent, parentKey);
|
|
350
|
+
default: {
|
|
351
|
+
const unhandled = shape;
|
|
352
|
+
throw new Error(`captureJson: unhandled shape ${String(unhandled)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
const result = inspect(() => {
|
|
357
|
+
const root = enter(v, undefined, "");
|
|
358
|
+
if (root.done)
|
|
359
|
+
return root.value;
|
|
360
|
+
const rootSource = stack[0].source;
|
|
361
|
+
while (stack.length > 0) {
|
|
362
|
+
const frame = stack[stack.length - 1];
|
|
363
|
+
if (frame.next >= frame.entries.length) {
|
|
364
|
+
stack.pop();
|
|
365
|
+
state.set(frame.source, frame.copy);
|
|
366
|
+
if (frame.parent !== undefined)
|
|
367
|
+
Object.defineProperty(frame.parent, frame.parentKey, dataDescriptor(frame.copy));
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
const [key, value] = frame.entries[frame.next++];
|
|
371
|
+
const child = enter(value, frame.copy, key);
|
|
372
|
+
if (child.done)
|
|
373
|
+
Object.defineProperty(frame.copy, key, dataDescriptor(child.value)); // a container child is attached to `frame.copy` when its own frame completes
|
|
374
|
+
}
|
|
375
|
+
return state.get(rootSource);
|
|
376
|
+
});
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* 捕获出来的副本的**原型**(所有 {@link captureJson} 副本与按声明键投影的副本共用这一只):**没有继承来的成员**(`Object.prototype` 被污染成数据值的同名键不在它的原型链上,也没有 `constructor`),
|
|
381
|
+
* 但仍答寻常对象对「转原始值」的两问(`toString` / `valueOf`,不可枚举)。为什么不是零原型:零原型对象没有这两只,校验器对**不是字符串却带 `length` 的值**做长度检查时会把它转成原始值 ——
|
|
382
|
+
* 寻常对象能转(得到 `"[object Object]"`,于是出一条 `too_small`),零原型对象抛引擎的 `TypeError`;读者拿副本去校验,答案就和拿原对象校验不一样了(拒因退成「读不出」、令牌判官抛)。
|
|
383
|
+
*/
|
|
384
|
+
export const COPY_PROTOTYPE = Object.freeze(Object.create(null, {
|
|
385
|
+
toString: { value: Object.freeze(function toString() { return "[object Object]"; }), enumerable: false },
|
|
386
|
+
valueOf: { value: Object.freeze(function valueOf() { return this; }), enumerable: false },
|
|
387
|
+
}));
|
|
388
|
+
/** 一个以 {@link COPY_PROTOTYPE} 为原型的空对象(副本的袋)。 */
|
|
389
|
+
export const isolatedObject = () => Object.create(COPY_PROTOTYPE);
|
|
390
|
+
/**
|
|
391
|
+
* 包自己造的**空文档**(「域文档缺席 ⇒ 按空文档校验、落默认值」的那一份;迁移里的各种累加容器也是它):原型是 {@link COPY_PROTOTYPE}(和所有副本同一只)。交给校验器读它声明的键时,
|
|
392
|
+
* `Object.prototype` 上被污染成数据值的同名键不会被读成「文档里写着的」(带 `Object.prototype` 的 `{}` 会)。调用方给的文档不归这里管 —— 那是调用方的值。
|
|
393
|
+
*/
|
|
394
|
+
export function emptyDocument() {
|
|
395
|
+
return isolatedObject();
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* 往包自己的数组末尾追加一项,**定义语义**。走它的是这七处:整容器读的成员表({@link containerView})、快照的遍历栈({@link captureJson})、`extraBodyUnsafe` 的遍历栈(`./types` 两臂)、
|
|
399
|
+
* `readWorkersRows` 的留下(两臂)/ 扣下两张表(`./workers-rows`);包内其余数组追加仍是 `push`(本版未收)。`push` / 下标赋值是 `[[Set]]` —— `Object.prototype`(或 `Array.prototype`)上被污染出数字键的 setter 时,
|
|
400
|
+
* 新下标不是自有的,`[[Set]]` 顺原型链找到那只 setter、把这一项吞掉(之后读那一格还会读到污染 getter 的值)。`Object.defineProperty` 直接在数组上定义自有下标,`length` 随之更新。
|
|
401
|
+
*/
|
|
402
|
+
export function appendOwn(list, value) {
|
|
403
|
+
Object.defineProperty(list, list.length, dataDescriptor(value));
|
|
404
|
+
}
|
|
405
|
+
/** 数据属性描述符(可写 / 可枚举 / 可配置),**零原型**:不继承 `Object.prototype` 上被污染的 `get` / `set` 等键,也不靠对象字面量。 */
|
|
406
|
+
export function dataDescriptor(value) {
|
|
407
|
+
const descriptor = Object.create(null);
|
|
408
|
+
descriptor.value = value;
|
|
409
|
+
descriptor.writable = true;
|
|
410
|
+
descriptor.enumerable = true;
|
|
411
|
+
descriptor.configurable = true;
|
|
412
|
+
return descriptor;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* 一份**串数组的捕获副本**;不是串数组 ⇒ `undefined`(调用方据此读成坏形 —— 名单读成 `[]` 并点名,`args` 丢键并进 `lost`,守卫答 `false`)。
|
|
416
|
+
* 按那一只整容器读({@link containerView})读:`length` 与每个自有下标各一份描述符,两种归类与容器读同一对,**按交出去的是什么选**:
|
|
417
|
+
* · `strict`(交回原引用 / 类型守卫 / 对原值下判决:`isStringArray`、设置单元的列表、比较器的串集合):下标是访问器或不可枚举 ⇒ 不是串数组(getter 一次都不调)——
|
|
418
|
+
* 判为真,调用方随后对**同一个数组**的普通读读到的就是判过的那一份(访问器下一次可以答别的、可以抛);
|
|
419
|
+
* · `wide`(交回副本:请求面 `args`):不可枚举的下标照读,访问器下标调它的 getter 一次 —— 调用方只读这份副本,读到的就是判过的。
|
|
420
|
+
* 其余同一张表:不是数组 / `length` 不是安全非负整数 / 有洞(自有下标不正好是 `0 … length-1` —— 不顺原型链读继承的数字键;`length` 很大的稀疏数组立即判坏,不逐位扫)/
|
|
421
|
+
* 条目不是串 / 读不出(结构陷阱抛、getter 抛)⇒ `undefined`。副本用 `Object.defineProperty` + {@link dataDescriptor} 写(`push` / 下标赋值是 `[[Set]]`,会命中继承的数字键 setter);
|
|
422
|
+
* 不走迭代协议(数组实例上被替换的 `Symbol.iterator` 供不出别的条目);交出去的是副本,调用方之后改原数组,副本不变。
|
|
423
|
+
*/
|
|
424
|
+
export function captureStringArray(v, classification) {
|
|
425
|
+
return isJsonArray(v) ? captureDenseArray(v, classification, isStringEntry, "ignore") : undefined;
|
|
426
|
+
}
|
|
427
|
+
/** 条目是不是串(串数组读法的条目判据;安全轴的串数组同用这一只)。 */
|
|
428
|
+
export const isStringEntry = (entry) => typeof entry === "string";
|
|
429
|
+
/**
|
|
430
|
+
* **稠密数组的捕获副本 —— 全包唯一的一份拷贝循环**({@link captureStringArray} 与安全轴的朴素数组读法 `./http-hook-allowlists` 都调它;调用方先问座位判数组):
|
|
431
|
+
* {@link denseArrayView} 取稠密判据与 `foreign`;`foreign: "refuse"` ⇒ 有域外自有键就不读元素、答 `undefined`(安全轴),`"ignore"` ⇒ 不看;
|
|
432
|
+
* 元素值逐个经取值步从那一份描述符取,逐个过 `isEntry`(不过即停、后面的不读);副本用 `Object.defineProperty` + {@link dataDescriptor} 写(定义语义)。读不出 ⇒ `undefined`。
|
|
433
|
+
*/
|
|
434
|
+
export function captureDenseArray(v, classification, isEntry, foreign) {
|
|
435
|
+
const read = inspect(() => {
|
|
436
|
+
const dense = denseArrayView(v, classification);
|
|
437
|
+
if (dense === undefined || (foreign === "refuse" && dense.foreign))
|
|
438
|
+
return undefined;
|
|
439
|
+
const copy = [];
|
|
440
|
+
for (let i = 0; i < dense.descriptors.length; i++) {
|
|
441
|
+
const entry = valueOfDescriptor(v, dense.descriptors[i]);
|
|
442
|
+
if (!isEntry(entry))
|
|
443
|
+
return undefined;
|
|
444
|
+
Object.defineProperty(copy, i, dataDescriptor(entry));
|
|
445
|
+
}
|
|
446
|
+
return copy;
|
|
447
|
+
});
|
|
448
|
+
return read.ok ? read.value : undefined;
|
|
449
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (`./one-time-migrations`)、`config.d` 文档形(`./config-d-file`)、`extraBody` 的形闸与秘密走查(`./types`)、settings 单元读法与严格 JSON 深度判(`./settings-units` / `./safety-merge-spec`)、
|
|
7
7
|
* 请求面传输的记录读法与描述件(`./mcp-request-entry`)、workers 行读口(`./workers-rows`)、有效配置线上载荷读口(`./config-fns` 的 `readEffectiveWire`)、http hook 名单的设置袋(`./http-hook-allowlists`)、
|
|
8
8
|
* fleet 线上载荷的受护判(`./fleet`)、符合性语料的比较器 / 派生 / 语料读口 / 元数据拆分(`./conformance/*`)。零 import。
|
|
9
|
-
* **形状分类核 {@link jsonShapeOf}
|
|
9
|
+
* **形状分类核 {@link jsonShapeOf} 是座位的本体**,`isPlainJsonObject` 由它派生(`plain` 那一格);读原型的只有它与安全轴的朴素数组判据 {@link isPlainJsonArray}(都在本模块)。**总(不抛)**:检视就抛的对象(已撤销的 Proxy、`getPrototypeOf` 陷阱抛)
|
|
10
10
|
* 核答 `uninspectable`、判官答 `false`,不抛出去 —— 全包经座位的读口因此拒而不抛,律在座位上收一处,不在每个读器各包一个 `try`;读者对 `uninspectable` 一律按最严处置(hooks ⇒ 拒整份文档;名单 ⇒ `malformed`;传输 ⇒ 整只不交)。
|
|
11
11
|
*
|
|
12
12
|
* **唯一的第二处是 `./hooks-config` 的 `hookShapeOf` / `hookIsRecord`,与 {@link jsonShapeOf} / 本函数逐字同文(调用名归一后)**:hooks 分类器的闭包要被消费方生成器整函数抄走(闭包里的自由名只有 `HOOK_EVENTS`),闭包里不能 import 本模块 ——
|
|
@@ -29,3 +29,13 @@ export declare function jsonShapeOf(v: unknown): "plain" | "non_plain_object" |
|
|
|
29
29
|
* 分类器闭包里不能 import,`./hooks-config` 的 `hookIsArray` 与本函数**逐字同文**(同一个门证明,与 `hookIsRecord` 同律)。
|
|
30
30
|
*/
|
|
31
31
|
export declare function isJsonArray(v: unknown): v is unknown[];
|
|
32
|
+
/**
|
|
33
|
+
* **朴素数组的原型判据**(总,不抛;安全轴的数组 —— http hook 名单、`hookVars`、`layers` —— 读它,`./http-hook-allowlists`):三观测缺一 ⇒ 否 ——
|
|
34
|
+
* ① 是数组({@link isJsonArray});
|
|
35
|
+
* ② 原型**恒等于本 realm 的** `Array.prototype` —— 子类、数组实例当原型、原型直接是 `Object.prototype` 的假原型、`null` / 换过的原型都不是;
|
|
36
|
+
* **另一个 realm 的数组也不是**(`vm` 上下文 / 别的 iframe 里造的,原型是那个 realm 的 `Array.prototype`)—— 读者按坏形处置,补偿是先做一次 JSON 往返;
|
|
37
|
+
* ③ 普通读 `constructor` / `filter` / `flatMap` / `Symbol.iterator`(各一次 `[[Get]]`)与本 realm 的内建恒等({@link INTRINSIC_ARRAY_MEMBERS},加载时取)——
|
|
38
|
+
* `get` 陷阱对其中一个说谎的 Proxy、`getPrototypeOf` 陷阱谎报原型的 Proxy(target 是子类 ⇒ `constructor` 读出子类)在这里败。
|
|
39
|
+
* 读的过程抛 ⇒ 否。只判原型与这几只继承成员;数组自己的自有键(只许下标与 `length`)由读者经整容器读判。
|
|
40
|
+
*/
|
|
41
|
+
export declare function isPlainJsonArray(v: unknown): v is unknown[];
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (`./one-time-migrations`)、`config.d` 文档形(`./config-d-file`)、`extraBody` 的形闸与秘密走查(`./types`)、settings 单元读法与严格 JSON 深度判(`./settings-units` / `./safety-merge-spec`)、
|
|
7
7
|
* 请求面传输的记录读法与描述件(`./mcp-request-entry`)、workers 行读口(`./workers-rows`)、有效配置线上载荷读口(`./config-fns` 的 `readEffectiveWire`)、http hook 名单的设置袋(`./http-hook-allowlists`)、
|
|
8
8
|
* fleet 线上载荷的受护判(`./fleet`)、符合性语料的比较器 / 派生 / 语料读口 / 元数据拆分(`./conformance/*`)。零 import。
|
|
9
|
-
* **形状分类核 {@link jsonShapeOf}
|
|
9
|
+
* **形状分类核 {@link jsonShapeOf} 是座位的本体**,`isPlainJsonObject` 由它派生(`plain` 那一格);读原型的只有它与安全轴的朴素数组判据 {@link isPlainJsonArray}(都在本模块)。**总(不抛)**:检视就抛的对象(已撤销的 Proxy、`getPrototypeOf` 陷阱抛)
|
|
10
10
|
* 核答 `uninspectable`、判官答 `false`,不抛出去 —— 全包经座位的读口因此拒而不抛,律在座位上收一处,不在每个读器各包一个 `try`;读者对 `uninspectable` 一律按最严处置(hooks ⇒ 拒整份文档;名单 ⇒ `malformed`;传输 ⇒ 整只不交)。
|
|
11
11
|
*
|
|
12
12
|
* **唯一的第二处是 `./hooks-config` 的 `hookShapeOf` / `hookIsRecord`,与 {@link jsonShapeOf} / 本函数逐字同文(调用名归一后)**:hooks 分类器的闭包要被消费方生成器整函数抄走(闭包里的自由名只有 `HOOK_EVENTS`),闭包里不能 import 本模块 ——
|
|
@@ -50,3 +50,39 @@ export function isJsonArray(v) {
|
|
|
50
50
|
return false; // 已撤销的 Proxy:读不出 ⇒ 不是数组
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* 本 realm 的数组内建,本模块加载时取一次(之后有人改写 `Array.prototype.filter` 之类,朴素数组判据比的仍是加载时那一份 ⇒ 改写过的读成「不是」)。
|
|
55
|
+
* 安全轴数组上要恒等的继承成员:`constructor`(species 协议从它取结果数组的构造器)、`filter`(v14.6.0 的判官对 `layers` / `hookVars` 调它)、`flatMap`、`Symbol.iterator`。
|
|
56
|
+
* 成员集对三只数组统一(设计选择):名单在 v14.6.0 里只按下标读、用不到这几只成员,但三处同一条律,不各配一张表。
|
|
57
|
+
*/
|
|
58
|
+
const ARRAY_PROTOTYPE = Array.prototype;
|
|
59
|
+
const INTRINSIC_ARRAY_MEMBERS = [
|
|
60
|
+
["constructor", Array],
|
|
61
|
+
["filter", Array.prototype.filter],
|
|
62
|
+
["flatMap", Array.prototype.flatMap],
|
|
63
|
+
[Symbol.iterator, Array.prototype[Symbol.iterator]],
|
|
64
|
+
];
|
|
65
|
+
/**
|
|
66
|
+
* **朴素数组的原型判据**(总,不抛;安全轴的数组 —— http hook 名单、`hookVars`、`layers` —— 读它,`./http-hook-allowlists`):三观测缺一 ⇒ 否 ——
|
|
67
|
+
* ① 是数组({@link isJsonArray});
|
|
68
|
+
* ② 原型**恒等于本 realm 的** `Array.prototype` —— 子类、数组实例当原型、原型直接是 `Object.prototype` 的假原型、`null` / 换过的原型都不是;
|
|
69
|
+
* **另一个 realm 的数组也不是**(`vm` 上下文 / 别的 iframe 里造的,原型是那个 realm 的 `Array.prototype`)—— 读者按坏形处置,补偿是先做一次 JSON 往返;
|
|
70
|
+
* ③ 普通读 `constructor` / `filter` / `flatMap` / `Symbol.iterator`(各一次 `[[Get]]`)与本 realm 的内建恒等({@link INTRINSIC_ARRAY_MEMBERS},加载时取)——
|
|
71
|
+
* `get` 陷阱对其中一个说谎的 Proxy、`getPrototypeOf` 陷阱谎报原型的 Proxy(target 是子类 ⇒ `constructor` 读出子类)在这里败。
|
|
72
|
+
* 读的过程抛 ⇒ 否。只判原型与这几只继承成员;数组自己的自有键(只许下标与 `length`)由读者经整容器读判。
|
|
73
|
+
*/
|
|
74
|
+
export function isPlainJsonArray(v) {
|
|
75
|
+
try {
|
|
76
|
+
if (!isJsonArray(v) || Object.getPrototypeOf(v) !== ARRAY_PROTOTYPE)
|
|
77
|
+
return false;
|
|
78
|
+
for (let i = 0; i < INTRINSIC_ARRAY_MEMBERS.length; i++) {
|
|
79
|
+
const pair = INTRINSIC_ARRAY_MEMBERS[i]; // 按下标取、不解构:数组解构走迭代协议,`Array.prototype[Symbol.iterator]` 被换掉时整段会被跳过
|
|
80
|
+
if (v[pair[0]] !== pair[1])
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return false; // 读原型 / 继承成员时陷阱抛 ⇒ 不是朴素数组
|
|
87
|
+
}
|
|
88
|
+
}
|
package/dist/remote-exec.js
CHANGED
|
@@ -31,6 +31,8 @@ import { INTEGER } from "./integer.js";
|
|
|
31
31
|
import { buildQuotedIssue } from "./quoted-issue.js";
|
|
32
32
|
import { ENV_NAME, K8S_QUANTITY, NON_SECRET_URL, TCP_PORT, containsSecretToken, hasNul } from "./types.js";
|
|
33
33
|
import { retiredLaneWordRefusal } from "./execution-lanes.js";
|
|
34
|
+
import { ownValueOf } from "./own-key-read.js";
|
|
35
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
34
36
|
/** The name of this contract's document in a `config.d` directory (`config.d/remote-exec.json`) —— the one spelling: the local reader /
|
|
35
37
|
* writer, the directory migration and the command line name the document by it. It is not a config domain (not in `DOMAIN_SCHEMAS`). */
|
|
36
38
|
export const REMOTE_EXEC_DOCUMENT = "remote-exec";
|
|
@@ -138,5 +140,11 @@ export const RemoteExecSpec = z.discriminatedUnion("provider", [
|
|
|
138
140
|
}),
|
|
139
141
|
], {
|
|
140
142
|
// A retired lane word as `provider` (no matching discriminator) gets the tombstone's way out; any other miss keeps zod's sentence.
|
|
141
|
-
|
|
143
|
+
// `provider` is read from the input's OWN keys (the input is the caller's object: an inherited / polluted `provider` is not the word they wrote; a proxy that cannot be read has no word to refuse)
|
|
144
|
+
error: (iss) => {
|
|
145
|
+
if (iss.code !== "invalid_union" || !isPlainJsonObject(iss.input))
|
|
146
|
+
return undefined;
|
|
147
|
+
const provider = ownValueOf(iss.input, "provider");
|
|
148
|
+
return provider.ok ? retiredLaneWordRefusal(provider.value) : undefined;
|
|
149
|
+
},
|
|
142
150
|
});
|