@sema-agent/settings-schema 14.5.0 → 14.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +531 -0
  2. package/README.md +46 -3
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
  5. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
  6. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
  7. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
  8. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
  9. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
  10. package/dist/api/auth-bridge.d.ts +16 -2
  11. package/dist/api/auth-bridge.js +59 -24
  12. package/dist/api/credentials.d.ts +281 -0
  13. package/dist/api/credentials.js +149 -0
  14. package/dist/api/scopes.js +5 -1
  15. package/dist/atomic-write.js +10 -7
  16. package/dist/bundle.d.ts +1 -1
  17. package/dist/bundle.js +1 -1
  18. package/dist/config-d-file.js +33 -15
  19. package/dist/config-dir-migration.js +2 -1
  20. package/dist/config-fns.js +142 -78
  21. package/dist/conformance/canonical.js +7 -10
  22. package/dist/conformance/contract-adapter.js +26 -1
  23. package/dist/conformance/corpus.js +5 -5
  24. package/dist/conformance/derive.js +6 -5
  25. package/dist/conformance/run.js +44 -6
  26. package/dist/cross-session.d.ts +4 -4
  27. package/dist/declared-capture.d.ts +41 -0
  28. package/dist/declared-capture.js +344 -0
  29. package/dist/file-edit.js +11 -15
  30. package/dist/file-store.d.ts +1 -31
  31. package/dist/file-store.js +3 -30
  32. package/dist/fleet.d.ts +11 -0
  33. package/dist/fleet.js +60 -26
  34. package/dist/fs-failure.d.ts +12 -7
  35. package/dist/fs-failure.js +53 -29
  36. package/dist/hash.d.ts +0 -9
  37. package/dist/hash.js +64 -7
  38. package/dist/hooks-config.d.ts +2 -2
  39. package/dist/hooks-config.js +129 -33
  40. package/dist/http-hook-allowlists.d.ts +27 -10
  41. package/dist/http-hook-allowlists.js +189 -25
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +4 -1
  44. package/dist/mcp-request-entry.d.ts +13 -8
  45. package/dist/mcp-request-entry.js +246 -107
  46. package/dist/one-time-migrations.js +207 -95
  47. package/dist/own-key-read.d.ts +247 -0
  48. package/dist/own-key-read.js +449 -0
  49. package/dist/plain-json-object.d.ts +38 -3
  50. package/dist/plain-json-object.js +84 -7
  51. package/dist/portable-domains.d.ts +31 -0
  52. package/dist/portable-domains.js +30 -0
  53. package/dist/remote-exec.js +9 -1
  54. package/dist/safety-merge-spec.d.ts +88 -6
  55. package/dist/safety-merge-spec.js +246 -17
  56. package/dist/sandbox-git.d.ts +64 -0
  57. package/dist/sandbox-git.js +89 -0
  58. package/dist/scheduler-store.js +21 -12
  59. package/dist/settings-units.d.ts +85 -0
  60. package/dist/settings-units.js +177 -0
  61. package/dist/types.d.ts +6 -2
  62. package/dist/types.js +65 -56
  63. package/dist/url-shape.d.ts +27 -5
  64. package/dist/url-shape.js +29 -5
  65. package/dist/web-admin-credential.js +5 -2
  66. package/dist/web-admin-session-url.js +2 -2
  67. package/dist/worker-row-verdict.d.ts +11 -0
  68. package/dist/worker-row-verdict.js +51 -1
  69. package/dist/workers-rows.d.ts +2 -1
  70. package/dist/workers-rows.js +41 -12
  71. package/dist/zod-unwrap.d.ts +6 -0
  72. package/dist/zod-unwrap.js +14 -7
  73. package/docs/API-SURFACE.md +50 -5
  74. package/docs/CONSUMER-INTEGRATION.md +178 -0
  75. package/llms-full.txt +34 -5
  76. package/llms.txt +6 -3
  77. package/package.json +1 -1
@@ -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
- /** A JSON object: not null, not an array, prototype `Object.prototype` or null. */
160
- function hookIsRecord(value) {
161
- if (value === null || typeof value !== "object" || Array.isArray(value))
162
- return false;
163
- const proto = Object.getPrototypeOf(value);
164
- return proto === Object.prototype || proto === null;
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 && !(Array.isArray(value) && value.length === 0))
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 (Array.isArray(inner))
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
- if (node === null || typeof node !== "object" || seen[at].has(node))
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(node);
209
- if (Array.isArray(node)) {
210
- for (let i = node.length - 1; i >= 0; i--) {
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([node[i], frames.length - 1, at === "matcherList" ? "matcher" : "other"]);
296
+ pending.push([list[i], frames.length - 1, at === "matcherList" ? "matcher" : "other"]);
213
297
  }
214
298
  continue;
215
299
  }
216
- const guardKey = hookGuardKeyOf(node);
300
+ const guardKey = hookGuardKeyOf(visited);
217
301
  if (guardKey !== undefined)
218
302
  return pathTo(frame, [guardKey]);
219
- if (at !== "matcher" && hookLooksLikeMatcher(node))
303
+ if (at !== "matcher" && hookLooksLikeMatcher(visited))
220
304
  return pathTo(frame, []);
221
- const keys = Object.keys(node);
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([node[keys[i]], frames.length - 1, "other"]);
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 (!Array.isArray(value))
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] = Array.isArray(value) ? value.slice() : hookIsRecord(value) ? Object.fromEntries(Object.entries(value)) : value;
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" && Array.isArray(entries)) && hookGuardSignIn(matcher[key], [...at, key], "other") !== undefined);
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 (Array.isArray(entries))
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" : !Array.isArray(entries) ? "hooks" : undefined;
315
- if (badField !== undefined || !Array.isArray(entries)) {
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 (Array.isArray(hooks) && hooks.some((x) => x !== null && typeof x === "object"))
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, Array.isArray(value) ? "matcherList" : "other") !== undefined)
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 (!Array.isArray(value)) {
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 (settingsDoc !== null && typeof settingsDoc === "object" && !Array.isArray(settingsDoc)) {
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 && !(Array.isArray(value) && value.length === 0)
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 !== null && typeof value === "object" && value.type === type ? 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
- * 一个设置层对两键的读数。键缺席 = 这一层不设限;`[]` = 这一层一个都不许。`source` = 这一层是哪个设置源(闭集 `SETTINGS_SOURCES`,原样回显进
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
- * 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样;其余一切(非数组、含非串条目、`null`)
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`:`empty` = 有一张准入名单是空的(那一层改对之前一只都不许);`url` = 没命中那几张名单里的任何模式。
51
- * `sources` = 不放行它的那几张名单所在的层(入参序)。零 URL 回显 —— URL 调用方自己有,按受众给显示形。 */
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`)。`workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
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
- /** 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。 */
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
- * 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样;其余一切(非数组、含非串条目、`null`)
34
- * ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象 ⇒ 两键都缺。
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 bag = typeof settings === "object" && settings !== null && !Array.isArray(settings) ? settings : {};
38
- const malformed = [];
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
- if (!Object.hasOwn(bag, key) || bag[key] === undefined)
42
- continue;
43
- const v = bag[key];
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
- /** 进判决的那几层(入参序):`layerAdmission` 答 `admitted` 的层。每一层都问 —— 闭集外的源 / 工作区读数在这里响亮抛,哪怕那一层没写名单。 */
54
- function admittedLayers(layers, workspace) {
55
- return layers.filter((l) => layerAdmission(l.source, workspace, "httpHookAllowlists") === "admitted");
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`)。`workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
216
+ * 没有准入层写这张名单 ⇒ 不限(`undefined`)。`layers` 的每一层经读器读({@link readHttpHookAllowlistLayer} 同一只读法;读不出源 ⇒ 坏形层,全拦、`rule: "malformed"`)。
217
+ * `workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
60
218
  */
61
219
  export function httpHookUrlVerdict(url, layers, workspace) {
62
- const refusing = admittedLayers(layers, workspace).filter((l) => l.allowedHttpHookUrls !== undefined && !l.allowedHttpHookUrls.some((p) => urlAllowPatternMatches(url, p)));
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
- return { list: "allowedHttpHookUrls", rule: refusing.some((l) => l.allowedHttpHookUrls.length === 0) ? "empty" : "url", sources: refusing.map((l) => l.source) };
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
- /** 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。 */
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 = admittedLayers(layers, workspace)
70
- .filter((l) => l.httpHookAllowedEnvVars !== undefined)
71
- .map((l) => l.httpHookAllowedEnvVars);
72
- return hookVars.filter((v) => lists.every((list) => list.includes(v)));
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";