@sema-agent/settings-schema 14.5.0 → 14.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +531 -0
- package/README.md +46 -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/atomic-write.js +10 -7
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/config-d-file.js +33 -15
- package/dist/config-dir-migration.js +2 -1
- package/dist/config-fns.js +142 -78
- package/dist/conformance/canonical.js +7 -10
- package/dist/conformance/contract-adapter.js +26 -1
- package/dist/conformance/corpus.js +5 -5
- package/dist/conformance/derive.js +6 -5
- package/dist/conformance/run.js +44 -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/file-store.d.ts +1 -31
- package/dist/file-store.js +3 -30
- package/dist/fleet.d.ts +11 -0
- package/dist/fleet.js +60 -26
- package/dist/fs-failure.d.ts +12 -7
- package/dist/fs-failure.js +53 -29
- package/dist/hash.d.ts +0 -9
- package/dist/hash.js +64 -7
- package/dist/hooks-config.d.ts +2 -2
- package/dist/hooks-config.js +129 -33
- package/dist/http-hook-allowlists.d.ts +27 -10
- package/dist/http-hook-allowlists.js +189 -25
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/dist/mcp-request-entry.d.ts +13 -8
- package/dist/mcp-request-entry.js +246 -107
- 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 +38 -3
- package/dist/plain-json-object.js +84 -7
- package/dist/portable-domains.d.ts +31 -0
- package/dist/portable-domains.js +30 -0
- package/dist/remote-exec.js +9 -1
- package/dist/safety-merge-spec.d.ts +88 -6
- package/dist/safety-merge-spec.js +246 -17
- package/dist/sandbox-git.d.ts +64 -0
- package/dist/sandbox-git.js +89 -0
- package/dist/scheduler-store.js +21 -12
- package/dist/settings-units.d.ts +85 -0
- package/dist/settings-units.js +177 -0
- package/dist/types.d.ts +6 -2
- package/dist/types.js +65 -56
- package/dist/url-shape.d.ts +27 -5
- package/dist/url-shape.js +29 -5
- 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.d.ts +2 -1
- package/dist/workers-rows.js +41 -12
- package/dist/zod-unwrap.d.ts +6 -0
- package/dist/zod-unwrap.js +14 -7
- package/docs/API-SURFACE.md +50 -5
- package/docs/CONSUMER-INTEGRATION.md +178 -0
- package/llms-full.txt +34 -5
- package/llms.txt +6 -3
- package/package.json +1 -1
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,20 +157,74 @@ 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
|
+
}
|
|
165
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
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/** `Symbol.iterator` WITHOUT the `Symbol` free name: the classifier closure is copied by consumers' generators whose registered built-ins do not include `Symbol` (a new built-in word
|
|
213
|
+
* in the closure is a change they must follow), so the well-known symbol is taken from the one place every realm keeps it — `Array.prototype` is iterable by it. Read once at load;
|
|
214
|
+
* a process in which `Array.prototype` has lost its iterator cannot say which objects hold content off their own keys, so the module refuses to load rather than answer "none do". */
|
|
215
|
+
const HOOK_ITERATOR_SYMBOL = (() => {
|
|
216
|
+
const found = Object.getOwnPropertySymbols(Array.prototype).find((s) => s.description === "Symbol.iterator");
|
|
217
|
+
if (found === undefined)
|
|
218
|
+
throw new Error("hooks classifier: Array.prototype has no Symbol.iterator");
|
|
219
|
+
return found;
|
|
220
|
+
})();
|
|
166
221
|
/** The first own key of `node` (in key order) that names a guard event with a value other than null / undefined / `[]` (CC `ei`). */
|
|
167
222
|
function hookGuardKeyOf(node) {
|
|
168
223
|
for (const key of Object.keys(node)) {
|
|
169
224
|
if (!HOOK_GUARD_EVENTS.includes(key))
|
|
170
225
|
continue;
|
|
171
226
|
const value = node[key];
|
|
172
|
-
if (value !== null && value !== undefined && !(
|
|
227
|
+
if (value !== null && value !== undefined && !(hookIsArray(value) && value.length === 0))
|
|
173
228
|
return key;
|
|
174
229
|
}
|
|
175
230
|
return undefined;
|
|
@@ -179,9 +234,9 @@ function hookLooksLikeMatcher(value) {
|
|
|
179
234
|
if (!hookIsRecord(value))
|
|
180
235
|
return false;
|
|
181
236
|
const inner = Object.hasOwn(value, "hooks") ? value.hooks : undefined;
|
|
182
|
-
if (
|
|
237
|
+
if (hookIsArray(inner))
|
|
183
238
|
return inner.length > 0;
|
|
184
|
-
return hookIsRecord(inner) && (Object.hasOwn(value, "matcher") || typeof inner.type === "string");
|
|
239
|
+
return hookIsRecord(inner) && (Object.hasOwn(value, "matcher") || (Object.hasOwn(inner, "type") && typeof inner.type === "string"));
|
|
185
240
|
}
|
|
186
241
|
/**
|
|
187
242
|
* The coordinate of the first guard sign in `root`'s subtree, or undefined: each object answers for itself before its members are searched (its own
|
|
@@ -203,25 +258,54 @@ function hookGuardSignIn(root, rootPath, place) {
|
|
|
203
258
|
};
|
|
204
259
|
while (pending.length > 0) {
|
|
205
260
|
const [node, frame, at] = pending.pop();
|
|
206
|
-
|
|
261
|
+
// 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
|
|
262
|
+
// ⇒ 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
|
|
263
|
+
const shape = hookShapeOf(node);
|
|
264
|
+
let isList = false;
|
|
265
|
+
switch (shape) {
|
|
266
|
+
case "null":
|
|
267
|
+
case "primitive":
|
|
268
|
+
continue; // not an object: nothing to search
|
|
269
|
+
case "uninspectable":
|
|
270
|
+
return pathTo(frame, []);
|
|
271
|
+
case "array":
|
|
272
|
+
isList = true;
|
|
273
|
+
break;
|
|
274
|
+
case "plain":
|
|
275
|
+
break; // searched as an object below
|
|
276
|
+
case "non_plain_object":
|
|
277
|
+
// an object whose content is supplied by the iteration protocol (a Map, a Set, a generator …) holds that content OFF its own keys: the own-key search below
|
|
278
|
+
// would find nothing and a guard held inside would vanish with the dropped piece. It cannot be shown free of guard signs ⇒ a sign at its own coordinate (same arm as
|
|
279
|
+
// "uninspectable"). Every other non-plain object (a class instance, a Date, a function) keeps what it holds on its own keys, searched below.
|
|
280
|
+
if (HOOK_ITERATOR_SYMBOL in node)
|
|
281
|
+
return pathTo(frame, []);
|
|
282
|
+
break;
|
|
283
|
+
default: {
|
|
284
|
+
const unhandled = shape;
|
|
285
|
+
throw new Error(`hookGuardSignIn: unhandled shape ${String(unhandled)}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
const visited = node;
|
|
289
|
+
if (seen[at].has(visited))
|
|
207
290
|
continue;
|
|
208
|
-
seen[at].add(
|
|
209
|
-
if (
|
|
210
|
-
|
|
291
|
+
seen[at].add(visited);
|
|
292
|
+
if (isList) {
|
|
293
|
+
const list = node;
|
|
294
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
211
295
|
frames.push({ key: i, parent: frame });
|
|
212
|
-
pending.push([
|
|
296
|
+
pending.push([list[i], frames.length - 1, at === "matcherList" ? "matcher" : "other"]);
|
|
213
297
|
}
|
|
214
298
|
continue;
|
|
215
299
|
}
|
|
216
|
-
const guardKey = hookGuardKeyOf(
|
|
300
|
+
const guardKey = hookGuardKeyOf(visited);
|
|
217
301
|
if (guardKey !== undefined)
|
|
218
302
|
return pathTo(frame, [guardKey]);
|
|
219
|
-
if (at !== "matcher" && hookLooksLikeMatcher(
|
|
303
|
+
if (at !== "matcher" && hookLooksLikeMatcher(visited))
|
|
220
304
|
return pathTo(frame, []);
|
|
221
|
-
const keys = Object.keys(
|
|
305
|
+
const keys = Object.keys(visited);
|
|
222
306
|
for (let i = keys.length - 1; i >= 0; i--) {
|
|
223
307
|
frames.push({ key: keys[i], parent: frame });
|
|
224
|
-
pending.push([
|
|
308
|
+
pending.push([visited[keys[i]], frames.length - 1, "other"]);
|
|
225
309
|
}
|
|
226
310
|
}
|
|
227
311
|
return undefined;
|
|
@@ -248,7 +332,7 @@ function hookFieldFits(base, value) {
|
|
|
248
332
|
case "stringRecord":
|
|
249
333
|
return hookIsRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
250
334
|
case "stringList":
|
|
251
|
-
if (!
|
|
335
|
+
if (!hookIsArray(value))
|
|
252
336
|
return false;
|
|
253
337
|
for (let i = 0; i < value.length; i++)
|
|
254
338
|
if (typeof value[i] !== "string")
|
|
@@ -283,7 +367,7 @@ function hookEntryRead(entry) {
|
|
|
283
367
|
const base = (optional ? kind.slice(0, -1) : kind);
|
|
284
368
|
if (!hookFieldFits(base, value))
|
|
285
369
|
return { field };
|
|
286
|
-
out[field] =
|
|
370
|
+
out[field] = hookIsArray(value) ? value.slice() : hookIsRecord(value) ? Object.fromEntries(Object.entries(value)) : value;
|
|
287
371
|
}
|
|
288
372
|
return { entry: out };
|
|
289
373
|
}
|
|
@@ -301,18 +385,18 @@ function hookMatcherRead(matcher, at, guardEvent, reject, drop) {
|
|
|
301
385
|
// 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
386
|
// the order hookGuardSignIn reports
|
|
303
387
|
const signField = hookGuardKeyOf(matcher) ??
|
|
304
|
-
Object.keys(matcher).find((key) => !(key === "hooks" &&
|
|
388
|
+
Object.keys(matcher).find((key) => !(key === "hooks" && hookIsArray(entries)) && hookGuardSignIn(matcher[key], [...at, key], "other") !== undefined);
|
|
305
389
|
if (signField !== undefined) {
|
|
306
390
|
reject("guard-matcher-invalid", at, signField);
|
|
307
391
|
return undefined;
|
|
308
392
|
}
|
|
309
393
|
const entrySigns = [];
|
|
310
|
-
if (
|
|
394
|
+
if (hookIsArray(entries))
|
|
311
395
|
for (let j = 0; j < entries.length; j++)
|
|
312
396
|
entrySigns.push(hookGuardSignIn(entries[j], [...at, "hooks", j], "other"));
|
|
313
397
|
const name = Object.hasOwn(matcher, "matcher") ? matcher.matcher : undefined;
|
|
314
|
-
const badField = name !== undefined && typeof name !== "string" ? "matcher" : !
|
|
315
|
-
if (badField !== undefined || !
|
|
398
|
+
const badField = name !== undefined && typeof name !== "string" ? "matcher" : !hookIsArray(entries) ? "hooks" : undefined;
|
|
399
|
+
if (badField !== undefined || !hookIsArray(entries)) {
|
|
316
400
|
if (guardEvent || entrySigns.some((sign) => sign !== undefined))
|
|
317
401
|
reject("guard-matcher-invalid", at, badField);
|
|
318
402
|
else
|
|
@@ -340,13 +424,16 @@ function hookMatcherRead(matcher, at, guardEvent, reject, drop) {
|
|
|
340
424
|
/** The `hooks` value → what loads of it (undefined when nothing does), reporting its bad pieces / signs into the two sinks. */
|
|
341
425
|
function hookBlockRead(hooks, reject, drop) {
|
|
342
426
|
if (!hookIsRecord(hooks)) {
|
|
343
|
-
if (
|
|
427
|
+
if (hookIsArray(hooks) && hooks.some(hookIsObjectLike))
|
|
428
|
+
reject("hooks-is-matcher", ["hooks"]);
|
|
429
|
+
// 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)
|
|
430
|
+
else if (hookGuardSignIn(hooks, ["hooks"], "other") !== undefined)
|
|
344
431
|
reject("hooks-is-matcher", ["hooks"]);
|
|
345
432
|
else
|
|
346
433
|
drop("hooks-not-object", ["hooks"]);
|
|
347
434
|
return undefined;
|
|
348
435
|
}
|
|
349
|
-
if (hookLooksLikeMatcher(hooks) || typeof hooks.type === "string") {
|
|
436
|
+
if (hookLooksLikeMatcher(hooks) || (Object.hasOwn(hooks, "type") && typeof hooks.type === "string")) {
|
|
350
437
|
reject("hooks-is-matcher", ["hooks"]);
|
|
351
438
|
return undefined;
|
|
352
439
|
}
|
|
@@ -355,14 +442,14 @@ function hookBlockRead(hooks, reject, drop) {
|
|
|
355
442
|
const value = hooks[event];
|
|
356
443
|
const at = ["hooks", event];
|
|
357
444
|
if (!HOOK_EVENTS.includes(event)) {
|
|
358
|
-
if (hookGuardSignIn(value, at,
|
|
445
|
+
if (hookGuardSignIn(value, at, hookIsArray(value) ? "matcherList" : "other") !== undefined)
|
|
359
446
|
reject("guard-under-unknown-event", at);
|
|
360
447
|
else
|
|
361
448
|
drop("unknown-event", at);
|
|
362
449
|
continue;
|
|
363
450
|
}
|
|
364
451
|
const guardEvent = HOOK_GUARD_EVENTS.includes(event);
|
|
365
|
-
if (!
|
|
452
|
+
if (!hookIsArray(value)) {
|
|
366
453
|
if ((guardEvent && value !== null) || hookGuardSignIn(value, at, "other") !== undefined)
|
|
367
454
|
reject("guard-event-not-array", at);
|
|
368
455
|
else
|
|
@@ -392,6 +479,15 @@ function hookBlockRead(hooks, reject, drop) {
|
|
|
392
479
|
* the hooks-classify test file (group ⑤) pins that the free names are exactly these.
|
|
393
480
|
*/
|
|
394
481
|
export function classifyHooksConfig(settingsDoc) {
|
|
482
|
+
try {
|
|
483
|
+
return hookClassify(settingsDoc);
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// 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
|
|
487
|
+
return { outcome: "rejected", hooks: undefined, dropped: [], rejections: [{ code: "guard-outside-hooks", path: [] }] };
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function hookClassify(settingsDoc) {
|
|
395
491
|
const rejections = [];
|
|
396
492
|
const dropped = [];
|
|
397
493
|
const reject = (code, path, field) => {
|
|
@@ -401,7 +497,7 @@ export function classifyHooksConfig(settingsDoc) {
|
|
|
401
497
|
dropped.push(field === undefined ? { code, path } : { code, path, field });
|
|
402
498
|
};
|
|
403
499
|
let hooks;
|
|
404
|
-
if (
|
|
500
|
+
if (hookIsRecord(settingsDoc)) {
|
|
405
501
|
const doc = settingsDoc;
|
|
406
502
|
for (const key of Object.keys(doc)) {
|
|
407
503
|
const value = doc[key];
|
|
@@ -412,7 +508,7 @@ export function classifyHooksConfig(settingsDoc) {
|
|
|
412
508
|
// the document itself written as a matcher: its top-level `matcher` key is the evidence, reported at that key's place
|
|
413
509
|
const sign = key === "matcher" && hookLooksLikeMatcher(doc)
|
|
414
510
|
? [key]
|
|
415
|
-
: HOOK_GUARD_EVENTS.includes(key) && value !== null && value !== undefined && !(
|
|
511
|
+
: HOOK_GUARD_EVENTS.includes(key) && value !== null && value !== undefined && !(hookIsArray(value) && value.length === 0)
|
|
416
512
|
? [key]
|
|
417
513
|
: hookGuardSignIn(value, [key], "other");
|
|
418
514
|
if (sign !== undefined)
|
|
@@ -450,7 +546,7 @@ function hooksFace(carry, strip, take) {
|
|
|
450
546
|
}
|
|
451
547
|
/** 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
548
|
function entryOfType(value, type) {
|
|
453
|
-
return value
|
|
549
|
+
return isPlainJsonObject(value) && value.type === type ? value : {}; // 读不出 / 不是纯数据的值不当「自己说了这个 type」的载体:⇒ `{}`(issue 点名 `type`)
|
|
454
550
|
}
|
|
455
551
|
const entryFace = (type) => hooksFace((v) => ({ hooks: { Stop: [{ hooks: [entryOfType(v, type)] }] } }), 5, (h) => h.Stop[0].hooks[0]);
|
|
456
552
|
export const CommandHookEntry = entryFace("command");
|
|
@@ -29,35 +29,52 @@ export declare const HttpHookAllowlistSettings: z.ZodObject<{
|
|
|
29
29
|
}, z.core.$loose>;
|
|
30
30
|
export type HttpHookAllowlistSettings = z.infer<typeof HttpHookAllowlistSettings>;
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
* `sources`);准入由判官按源经 `layerAdmission`
|
|
32
|
+
* 一个设置层对两键的读数(读器交出的读数里的 `layer`)。键缺席 = 这一层不设限;`[]` = 这一层一个都不许。`source` = 这一层是哪个设置源(闭集 `SETTINGS_SOURCES`,原样回显进
|
|
33
|
+
* `sources`);准入由判官按源经 `layerAdmission` 判,调用方不预先筛层、不给档。读器交出的是寻常对象(不冻结)。
|
|
34
|
+
* **判官经读器读每一层**(与读器同一只读法,见 `httpHookUrlVerdict`):读器交出的层、调用方手造的寻常层、本 realm 的 `JSON.parse` 出来的层都按内容判,同一份内容同一个答案(另一个 realm 的数组按坏形,见 CHANGELOG 例外 ⑤)。读器交出的层里坏形名单已读成 `[]`(点名在读器的 `malformed`),所以同一份坏形设置直接交判官答 `malformed`、先过读器再交判官答 `empty` —— 坏形的告警看读器那一份读数。
|
|
34
35
|
*/
|
|
35
36
|
export interface HttpHookAllowlistLayer {
|
|
36
37
|
readonly source: SettingsSource;
|
|
37
38
|
readonly allowedHttpHookUrls?: readonly string[];
|
|
38
39
|
readonly httpHookAllowedEnvVars?: readonly string[];
|
|
39
40
|
}
|
|
40
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* 一层的读法结果:层读数 + 读成空名单的坏形键(调用方据此告警;读法本身不出句)。由 {@link readHttpHookAllowlistLayer} 交出:普通对象、不冻结(与 v14.6.0 同形)。
|
|
43
|
+
* 读器对自己交出的 `layer` 再读,名单逐位同,`malformed` 只在第一次读出现(坏键第一次已读成 `[]`,再读是空名单);交判官与交它的副本同答。
|
|
44
|
+
*/
|
|
41
45
|
export interface HttpHookAllowlistLayerRead {
|
|
42
46
|
readonly layer: HttpHookAllowlistLayer;
|
|
43
47
|
readonly malformed: readonly HttpHookAllowlistKey[];
|
|
44
48
|
}
|
|
45
49
|
/**
|
|
46
|
-
*
|
|
47
|
-
* ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象 ⇒ 两键都缺。
|
|
50
|
+
* 从一层的原始设置对象读出两键,过 `allowlistsOf`〔包内私有〕—— 与判官同一只读法:键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样的捕获副本;其余一切(非数组、含非串条目或洞、取值或读元素时抛、`null`)
|
|
51
|
+
* ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象(`null` / 原始值 / 数组)⇒ 两键都缺。
|
|
52
|
+
* **是对象但不朴素**(类实例 / `Map` / 建在别的对象原型上的记录 / 函数 —— JSON 解析不出来,只能是嵌入方程序拼的)⇒ 同样 fail-closed:两键都读成 `[]` 并点名 `malformed`。
|
|
53
|
+
* 把它当「没有设置」读,等于把一层带名单的托管设置静默丢掉、方向放行(与本模块「坏形 = 一个都不许」的失败方向相反)。
|
|
54
|
+
* 名单键**只靠继承可见**(不是自有键)⇒ 同样 fail-closed:该键读成 `[]` 并点名 `malformed`(自有键盖过继承键;自有键值为 `undefined` 仍是「不设」)。
|
|
55
|
+
* 原型是带成员的 null 原型对象的对象(`Object.create(<null 原型的名单袋>)`)是一例:它按座位的判据读成朴素、按**自有**键读,名单键只在原型上 ⇒ 坏形。它只能由程序造出,JSON 解析不出来(座位头注 `./plain-json-object` 同写)。
|
|
48
56
|
*/
|
|
49
57
|
export declare function readHttpHookAllowlistLayer(source: SettingsSource, settings: unknown): HttpHookAllowlistLayerRead;
|
|
50
|
-
/** 拦下一只 http hook 的原因。`rule
|
|
51
|
-
* `
|
|
58
|
+
/** 拦下一只 http hook 的原因。`rule`(闭集,先到先得:`malformed` 先于 `empty` 先于 `url`):
|
|
59
|
+
* `malformed` = 有一层判官读不出 —— ① 源读不出:`layers` 本身不是朴素数组 / 读不出 / 两份观测对不上,或某层的 `source` 缺席 / 只靠继承可见 / 读不出 / 两份不一致 / 不是串、层不是对象
|
|
60
|
+
* ⇒ 那一层没有可信的源,不点名;② 源读得出、名单读不出(读器对同一份设置报 `malformed` 的那些形:不是朴素串数组 —— 另一个 realm 的数组、带框架挂件的数组、含非串条目或洞 ——、
|
|
61
|
+
* 读它或读它的元素时 getter 抛、两份观测不一致、名单键只靠继承可见、层对象不朴素)⇒ 点名那一层;
|
|
62
|
+
* `empty` = 有一张准入名单真是空的 `[]`(那一层改对之前一只都不许;读器交出的层里坏形名单已是 `[]`,交判官也答 `empty` —— 坏形点名看读器的 `malformed`);
|
|
63
|
+
* `url` = 没命中那几张名单里的任何模式。
|
|
64
|
+
* `sources` = 不放行它的、读得出源的那几层(入参序)—— 所以 `rule: "malformed"` 时 `sources` 可以是空的(只有源读不出的层在拦)。零 URL 回显 —— URL 调用方自己有,按受众给显示形。 */
|
|
52
65
|
export interface HttpHookUrlBlock {
|
|
53
66
|
readonly list: "allowedHttpHookUrls";
|
|
54
|
-
readonly rule: "empty" | "url";
|
|
67
|
+
readonly rule: "malformed" | "empty" | "url";
|
|
55
68
|
readonly sources: readonly SettingsSource[];
|
|
56
69
|
}
|
|
57
70
|
/**
|
|
58
71
|
* 一只 http hook 的 URL 过不过名单:每一张准入的 `allowedHttpHookUrls` 都要有一条模式按放行出口({@link urlAllowPatternMatches})命中它。
|
|
59
|
-
* 没有准入层写这张名单 ⇒ 不限(`undefined`)。`
|
|
72
|
+
* 没有准入层写这张名单 ⇒ 不限(`undefined`)。`layers` 的每一层经读器读({@link readHttpHookAllowlistLayer} 同一只读法;读不出源 ⇒ 坏形层,全拦、`rule: "malformed"`)。
|
|
73
|
+
* `workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
|
|
60
74
|
*/
|
|
61
75
|
export declare function httpHookUrlVerdict(url: string, layers: readonly HttpHookAllowlistLayer[], workspace: WorkspaceTrustState): HttpHookUrlBlock | undefined;
|
|
62
|
-
/**
|
|
76
|
+
/**
|
|
77
|
+
* 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。
|
|
78
|
+
* `layers` 同 {@link httpHookUrlVerdict}(坏形层 ⇒ 一个都不放);`hookVars` 只许普通的串数组({@link plainArrayOf};不是 ⇒ 当空,一个都不放)。
|
|
79
|
+
*/
|
|
63
80
|
export declare function httpHookEnvVarsAllowed(hookVars: readonly string[], layers: readonly HttpHookAllowlistLayer[], workspace: WorkspaceTrustState): string[];
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* 判官出结构不出句:放行 ⇒ `undefined`;拦下 ⇒ {@link HttpHookUrlBlock}(哪张名单 / 为什么 / 出自哪几层),用户面那一句由各宿主按它铸。
|
|
16
16
|
*/
|
|
17
17
|
import { z } from "zod";
|
|
18
|
+
import { captureDenseArray, inspect, isStringEntry, readKey } from "./own-key-read.js";
|
|
19
|
+
import { isJsonArray, isPlainJsonArray, jsonShapeOf } from "./plain-json-object.js";
|
|
18
20
|
import { layerAdmission } from "./settings-sources.js";
|
|
19
21
|
import { urlAllowPatternMatches } from "./url-pattern.js";
|
|
20
22
|
/** 两键(闭集,声明序)。 */
|
|
@@ -29,45 +31,207 @@ export const HttpHookAllowlistSettings = z
|
|
|
29
31
|
httpHookAllowedEnvVars: z.array(z.string()).optional(),
|
|
30
32
|
})
|
|
31
33
|
.passthrough();
|
|
34
|
+
/** 坏形的那一份读数(名单按 `[]` 读、点名 malformed)。每次一份新数组。 */
|
|
35
|
+
const malformedList = () => ({ list: [], malformed: true });
|
|
36
|
+
const UNSET = { kind: "unset" };
|
|
37
|
+
const BAD = { kind: "bad" };
|
|
32
38
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
39
|
+
* 安全轴的数组(`layers` / `hookVars` / 两张名单)的描述符观测 D —— **只许朴素数组**,一条规则:座位的朴素数组判据({@link isPlainJsonArray}:是数组、原型恒等于本 realm 的 `Array.prototype`、
|
|
40
|
+
* `constructor` / `filter` / `flatMap` / `Symbol.iterator` 与本 realm 的内建恒等 —— 子类、假原型、`null` / 换过的原型、谎报原型或对这几只成员说谎的 Proxy、另一个 realm 的数组都不是);整容器读({@link denseArrayView},`wide` 归类)一次列自有键 ——
|
|
41
|
+
* 稠密(`length` 是安全非负整数、自有下标正好 `0 … length-1`),且**自有键只有下标与 `length`**:任何别的自有键(方法覆盖、`constructor`、任意串键、符号键 —— 含 `Symbol.species`)
|
|
42
|
+
* ⇒ 不是朴素数组,整个输入按坏形、不读元素(v14.6.0 的判官调过调用方数组自己的 `filter`,子类 / 自有方法 / species 构造器让它拦下或少放;本版一律拦,拦集不缩)。
|
|
43
|
+
* 元素值取自那一份描述符(访问器下标调它的 getter 一次),逐个过 `isEntry`(不过即停、后面的不读);交出一份定义语义造的副本。不是 / 读不出 ⇒ `undefined`。
|
|
44
|
+
*/
|
|
45
|
+
function plainArrayOf(v, isEntry) {
|
|
46
|
+
return isPlainJsonArray(v) ? captureDenseArray(v, "wide", isEntry, "refuse") : undefined; // 拷贝循环是内核那一份(与串数组读法同一只)
|
|
47
|
+
}
|
|
48
|
+
const isAny = (_entry) => true;
|
|
49
|
+
/** 判官直接拿到的数组(`layers` / `hookVars`):描述符观测({@link plainArrayOf})与普通读({@link agreesByOrdinaryReading})两份一致才算读到;否则 `undefined`。设置里的名单不走这一只 —— 两键的描述符观测要先于任何普通读(见 {@link allowlistsOf})。 */
|
|
50
|
+
function bothObservations(v, isEntry) {
|
|
51
|
+
const seen = plainArrayOf(v, isEntry);
|
|
52
|
+
return seen !== undefined && agreesByOrdinaryReading(v, seen) ? seen : undefined;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 观测 D(描述符通道,本包的读内核):名单键经 `readKey` 读一次 —— 缺席 / 自有 `undefined` ⇒ 不设;自有 ⇒ {@link plainArrayOf}(只许普通的串数组;不是 ⇒ 坏形);
|
|
56
|
+
* 只靠继承可见 / 读不出 ⇒ 坏形。
|
|
57
|
+
*/
|
|
58
|
+
function describedList(holder, key) {
|
|
59
|
+
const read = readKey(holder, key);
|
|
60
|
+
switch (read.presence) {
|
|
61
|
+
case "absent":
|
|
62
|
+
return UNSET;
|
|
63
|
+
case "inherited":
|
|
64
|
+
case "unreadable":
|
|
65
|
+
return BAD;
|
|
66
|
+
case "own": {
|
|
67
|
+
if (read.value === undefined)
|
|
68
|
+
return UNSET;
|
|
69
|
+
const list = plainArrayOf(read.value, isStringEntry);
|
|
70
|
+
return list === undefined ? BAD : { kind: "list", list };
|
|
71
|
+
}
|
|
72
|
+
default: {
|
|
73
|
+
const unhandled = read;
|
|
74
|
+
throw new Error(`describedList: unhandled presence ${String(unhandled.presence)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 观测 G(普通读通道)与描述符那份 `seen` 是否一致:座位判数组、`length` 经 `[[Get]]` 等于 `seen.length`、逐下标「是不是自有」(`Object.hasOwn` —— 14.6.0 读器读名单的读法)、
|
|
80
|
+
* 「在不在」(`i in v`,HasProperty —— v14.6.0 判官对 `layers` / `hookVars` 的 `filter` 按它跳过下标)且 `[[Get]]` 的值与 `seen` 同一(`Object.is`)。
|
|
81
|
+
* 读量以 `seen` 的长度为界(长度不同即不一致,不逐位扫);读不出 ⇒ 不一致。名单、判官的 `layers` / `hookVars` 都用这一只。
|
|
82
|
+
*/
|
|
83
|
+
function agreesByOrdinaryReading(v, seen) {
|
|
84
|
+
const same = inspect(() => {
|
|
85
|
+
if (!isJsonArray(v) || v.length !== seen.length)
|
|
86
|
+
return false;
|
|
87
|
+
for (let i = 0; i < seen.length; i++)
|
|
88
|
+
if (!Object.hasOwn(v, i) || !(i in v) || !Object.is(v[i], seen[i]))
|
|
89
|
+
return false;
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
return same.ok && same.value;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* 名单在一个对象上的**唯一读法**(读器读设置对象、判官读层对象,都调这一只 —— 两边对同一个对象永远同答):先问座位它是什么形(`jsonShapeOf`,一次)——
|
|
96
|
+
* · 朴素对象 ⇒ **两份观测一致才算读到**:先对每个要的键做描述符观测 D({@link describedList}),再逐键做普通读 G({@link agreesByOrdinaryReading},v14.6.0 读器与判官普通读的合取)对拍 ——
|
|
97
|
+
* 两份都「不设」⇒ 不设;都是同一张串名单 ⇒ D 的捕获副本;其余一切(只一份读到、内容不同、任一份读不出 / 坏形、名单键只靠继承可见)⇒ `[]` + malformed(fail-closed:对无状态构造,拦集包含
|
|
98
|
+
* 「只看 G」的 14.6.0 与「只看 D」的读法各自的拦集,两份不一致也拦;普通对象上两份观测恒同,JSON 面零变化)。挡不住的(如实):有状态陷阱 —— 两次都答同一份假名单,
|
|
99
|
+
* 或第一次观测改了第二次观测的答案;G 是本模块读名单时唯一的 `[[Get]]`,只作对拍 —— 交出去的值取自 D;
|
|
100
|
+
* · 是对象但不朴素(类实例 / `Map` / 建在别的对象原型上的记录 / 函数 —— JSON 解析不出来,只能是嵌入方程序拼的)或读形状就抛(已撤销的 Proxy)⇒ 要的键都读成 `[]` + malformed
|
|
101
|
+
* (fail-closed:把它当「没有名单」读,等于把一层带名单的设置静默丢掉、方向放行);
|
|
102
|
+
* · 不是对象(`null` / 原始值 / 数组)⇒ 都缺。
|
|
103
|
+
* 交出零原型的袋(只含读到的键)。
|
|
104
|
+
*/
|
|
105
|
+
function allowlistsOf(holder, keys) {
|
|
106
|
+
const out = Object.create(null); // 零原型:原型上被污染成只读的同名键不让赋值抛
|
|
107
|
+
const shape = jsonShapeOf(holder);
|
|
108
|
+
switch (shape) {
|
|
109
|
+
case "plain": {
|
|
110
|
+
// 安全轴的名单要**两份观测一致才算读到**:先对所有要的键做描述符观测 D(在任何普通读之前 —— 一个键的普通读改不动另一个键已经拿到的观测),再逐键做普通读 G 对拍
|
|
111
|
+
const described = keys.map((key) => describedList(holder, key));
|
|
112
|
+
keys.forEach((key, i) => {
|
|
113
|
+
const d = described[i];
|
|
114
|
+
if (d.kind === "bad") {
|
|
115
|
+
out[key] = malformedList();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const got = inspect(() => holder[key]);
|
|
119
|
+
const agrees = got.ok && (d.kind === "unset" ? got.value === undefined : agreesByOrdinaryReading(got.value, d.list));
|
|
120
|
+
if (!agrees)
|
|
121
|
+
out[key] = malformedList();
|
|
122
|
+
else if (d.kind === "list")
|
|
123
|
+
out[key] = { list: d.list, malformed: false };
|
|
124
|
+
});
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
case "non_plain_object":
|
|
128
|
+
case "uninspectable":
|
|
129
|
+
for (const key of keys)
|
|
130
|
+
out[key] = malformedList();
|
|
131
|
+
break;
|
|
132
|
+
case "array":
|
|
133
|
+
case "null":
|
|
134
|
+
case "primitive":
|
|
135
|
+
break;
|
|
136
|
+
default: {
|
|
137
|
+
const unhandled = shape;
|
|
138
|
+
throw new Error(`allowlistsOf: unhandled shape ${String(unhandled)}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 从一层的原始设置对象读出两键,过 `allowlistsOf`〔包内私有〕—— 与判官同一只读法:键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样的捕获副本;其余一切(非数组、含非串条目或洞、取值或读元素时抛、`null`)
|
|
145
|
+
* ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象(`null` / 原始值 / 数组)⇒ 两键都缺。
|
|
146
|
+
* **是对象但不朴素**(类实例 / `Map` / 建在别的对象原型上的记录 / 函数 —— JSON 解析不出来,只能是嵌入方程序拼的)⇒ 同样 fail-closed:两键都读成 `[]` 并点名 `malformed`。
|
|
147
|
+
* 把它当「没有设置」读,等于把一层带名单的托管设置静默丢掉、方向放行(与本模块「坏形 = 一个都不许」的失败方向相反)。
|
|
148
|
+
* 名单键**只靠继承可见**(不是自有键)⇒ 同样 fail-closed:该键读成 `[]` 并点名 `malformed`(自有键盖过继承键;自有键值为 `undefined` 仍是「不设」)。
|
|
149
|
+
* 原型是带成员的 null 原型对象的对象(`Object.create(<null 原型的名单袋>)`)是一例:它按座位的判据读成朴素、按**自有**键读,名单键只在原型上 ⇒ 坏形。它只能由程序造出,JSON 解析不出来(座位头注 `./plain-json-object` 同写)。
|
|
35
150
|
*/
|
|
36
151
|
export function readHttpHookAllowlistLayer(source, settings) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
const lists =
|
|
152
|
+
const read = allowlistsOf(settings, HTTP_HOOK_ALLOWLIST_KEYS);
|
|
153
|
+
// 累加器零原型:`{}` 继承 `Object.prototype`,原型上被污染成只读(只有 getter)的同名键会让赋值抛(读器该 fail-closed,不是抛)
|
|
154
|
+
const lists = Object.create(null);
|
|
40
155
|
for (const key of HTTP_HOOK_ALLOWLIST_KEYS) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (Array.isArray(v) && v.every((x) => typeof x === "string"))
|
|
45
|
-
lists[key] = [...v];
|
|
46
|
-
else {
|
|
47
|
-
lists[key] = [];
|
|
48
|
-
malformed.push(key);
|
|
49
|
-
}
|
|
156
|
+
const captured = read[key];
|
|
157
|
+
if (captured !== undefined)
|
|
158
|
+
lists[key] = captured.list;
|
|
50
159
|
}
|
|
160
|
+
// `malformed` 用 filter 造(定义语义)—— `push` 会命中 `Object.prototype` 上继承的数字键 setter、吞掉点名;返回形与 v14.6.0 同(普通对象、不冻结),字面量与 `...lists` 都是定义语义,污染下不抛
|
|
161
|
+
const malformed = HTTP_HOOK_ALLOWLIST_KEYS.filter((key) => read[key]?.malformed === true);
|
|
51
162
|
return { layer: { source, ...lists }, malformed };
|
|
52
163
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
164
|
+
const MALFORMED_LAYER = { source: undefined, list: [], malformed: true };
|
|
165
|
+
/**
|
|
166
|
+
* 一层的 `source`,与名单同一条双读一致律:层是对象(朴素与否都读)⇒ 自有描述符那份(`readKey`)与普通读那份(`[[Get]]`)是同一个值、且是串 ⇒ 那个词
|
|
167
|
+
* (闭集不在这里判:读得出的词交 `layerAdmission`,闭集外的词在那里响亮抛 —— 那是宿主代码写错了词,不是数据坏形);
|
|
168
|
+
* 其余一切(不是对象 / 读形状就抛、`source` 缺席 / 只靠继承可见 / 读不出、两份不一致、不是串)⇒ `undefined`(判官把这一层当坏形层,判决词 `malformed`)。
|
|
169
|
+
*/
|
|
170
|
+
function sourceOf(element) {
|
|
171
|
+
const shape = jsonShapeOf(element);
|
|
172
|
+
switch (shape) {
|
|
173
|
+
case "plain":
|
|
174
|
+
case "non_plain_object": {
|
|
175
|
+
const described = readKey(element, "source");
|
|
176
|
+
if (described.presence !== "own")
|
|
177
|
+
return undefined;
|
|
178
|
+
const got = inspect(() => element.source);
|
|
179
|
+
return got.ok && Object.is(got.value, described.value) && typeof described.value === "string" ? described.value : undefined;
|
|
180
|
+
}
|
|
181
|
+
case "array":
|
|
182
|
+
case "null":
|
|
183
|
+
case "primitive":
|
|
184
|
+
case "uninspectable":
|
|
185
|
+
return undefined;
|
|
186
|
+
default: {
|
|
187
|
+
const unhandled = shape;
|
|
188
|
+
throw new Error(`sourceOf: unhandled shape ${String(unhandled)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 进判决的那几层(入参序)与它们对 `key` 那张名单的读数(`list` 缺 = 这一层不设)。**判官经读器读每一层**,与读器同一只读法,没有「是不是读器交出的」判:
|
|
194
|
+
* `layers` 数组见 {@link bothObservations}(不是朴素数组 / 读不出 / 两份观测对不上 ⇒ 整个当一张坏形层);每个元素先读源({@link sourceOf})——
|
|
195
|
+
* 读不出 ⇒ **坏形层**:名单按 `[]`,不问准入(它没有可信的源),进判决、全拦(判决词 `malformed`);
|
|
196
|
+
* 读得出 ⇒ 问 `layerAdmission`(闭集外的源 / 工作区读数在这里响亮抛 RangeError,哪怕那一层没写名单),准入的才进判决,名单与「读不出」= 读器读这个元素的读数({@link allowlistsOf},两键、双读一致律原样;读不出 ⇒ 判决词 `malformed`、点名这一层)。
|
|
197
|
+
* 于是读器交出的层、它的副本、手造的寻常层、本 realm 的 `JSON.parse` 出来的层同一份内容同一个答案(另一个 realm 的数组按坏形);读器对自己交出的 `layer` 再读,名单逐位同(`malformed` 只在第一次读出现)。
|
|
198
|
+
*/
|
|
199
|
+
function admittedLists(layers, workspace, key) {
|
|
200
|
+
const elements = bothObservations(layers, isAny);
|
|
201
|
+
if (elements === undefined)
|
|
202
|
+
return [MALFORMED_LAYER];
|
|
203
|
+
return elements.flatMap((element) => {
|
|
204
|
+
const source = sourceOf(element);
|
|
205
|
+
if (source === undefined)
|
|
206
|
+
return [MALFORMED_LAYER];
|
|
207
|
+
// 闭集外的词:`layerAdmission` 经源表那一只判官抛 RangeError(与 14.6.0 同句同类)—— 判官不另写闭集表
|
|
208
|
+
if (layerAdmission(source, workspace, "httpHookAllowlists") !== "admitted")
|
|
209
|
+
return [];
|
|
210
|
+
const captured = allowlistsOf(element, HTTP_HOOK_ALLOWLIST_KEYS)[key];
|
|
211
|
+
return [{ source: source, list: captured?.list, malformed: captured?.malformed === true }];
|
|
212
|
+
});
|
|
56
213
|
}
|
|
57
214
|
/**
|
|
58
215
|
* 一只 http hook 的 URL 过不过名单:每一张准入的 `allowedHttpHookUrls` 都要有一条模式按放行出口({@link urlAllowPatternMatches})命中它。
|
|
59
|
-
* 没有准入层写这张名单 ⇒ 不限(`undefined`)。`
|
|
216
|
+
* 没有准入层写这张名单 ⇒ 不限(`undefined`)。`layers` 的每一层经读器读({@link readHttpHookAllowlistLayer} 同一只读法;读不出源 ⇒ 坏形层,全拦、`rule: "malformed"`)。
|
|
217
|
+
* `workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
|
|
60
218
|
*/
|
|
61
219
|
export function httpHookUrlVerdict(url, layers, workspace) {
|
|
62
|
-
const refusing =
|
|
220
|
+
const refusing = admittedLists(layers, workspace, "allowedHttpHookUrls").flatMap(({ source, list, malformed }) => list !== undefined && !list.some((p) => urlAllowPatternMatches(url, p)) ? [{ source, malformed, empty: list.length === 0 }] : []);
|
|
63
221
|
if (refusing.length === 0)
|
|
64
222
|
return undefined;
|
|
65
|
-
|
|
223
|
+
const sources = refusing.flatMap(({ source }) => (source === undefined ? [] : [source]));
|
|
224
|
+
const rule = refusing.some((r) => r.malformed) ? "malformed" : refusing.some((r) => r.empty) ? "empty" : "url";
|
|
225
|
+
return { list: "allowedHttpHookUrls", rule, sources };
|
|
66
226
|
}
|
|
67
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。
|
|
229
|
+
* `layers` 同 {@link httpHookUrlVerdict}(坏形层 ⇒ 一个都不放);`hookVars` 只许普通的串数组({@link plainArrayOf};不是 ⇒ 当空,一个都不放)。
|
|
230
|
+
*/
|
|
68
231
|
export function httpHookEnvVarsAllowed(hookVars, layers, workspace) {
|
|
69
|
-
const lists =
|
|
70
|
-
.
|
|
71
|
-
.
|
|
72
|
-
|
|
232
|
+
const lists = admittedLists(layers, workspace, "httpHookAllowedEnvVars")
|
|
233
|
+
.map(({ list }) => list)
|
|
234
|
+
.filter((list) => list !== undefined);
|
|
235
|
+
const vars = bothObservations(hookVars, isStringEntry) ?? []; // 只许朴素的串数组,且两份观测一致才算读到;否则一个都不放
|
|
236
|
+
return vars.filter((v) => lists.every((list) => list.includes(v)));
|
|
73
237
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from "./reader.js";
|
|
|
18
18
|
export * from "./cross-domain.js";
|
|
19
19
|
export * from "./remote-exec.js";
|
|
20
20
|
export * from "./safety-merge-spec.js";
|
|
21
|
+
export * from "./settings-units.js";
|
|
21
22
|
export * from "./scheduler-store.js";
|
|
22
23
|
export * from "./cross-session.js";
|
|
23
24
|
export * from "./hooks.js";
|
|
@@ -28,6 +29,8 @@ export * from "./api/auth-bridge.js";
|
|
|
28
29
|
export * from "./api/scopes.js";
|
|
29
30
|
export * from "./api/pull-token.js";
|
|
30
31
|
export * from "./api/feedback.js";
|
|
32
|
+
export * from "./api/credentials.js";
|
|
33
|
+
export * from "./sandbox-git.js";
|
|
31
34
|
export * from "./web-admin-env.js";
|
|
32
35
|
export * from "./web-admin-credential.js";
|
|
33
36
|
export * from "./web-admin-session-url.js";
|