@sema-agent/settings-schema 14.3.0 → 14.5.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 +65 -1
- package/README.md +7 -1
- package/conformance/VERSION +1 -1
- package/dist/api/scopes.d.ts +1 -1
- package/dist/api/scopes.js +1 -1
- package/dist/atomic-write.d.ts +11 -0
- package/dist/atomic-write.js +32 -2
- package/dist/config-d-file.d.ts +4 -0
- package/dist/config-d-file.js +11 -4
- package/dist/config-dir-io.js +12 -2
- package/dist/config-dir-migration.d.ts +44 -0
- package/dist/config-dir-migration.js +90 -36
- package/dist/config-fns.js +1 -7
- package/dist/fs-failure.d.ts +12 -2
- package/dist/fs-failure.js +24 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/node.d.ts +2 -1
- package/dist/node.js +2 -1
- package/dist/one-time-migrations.js +36 -41
- package/dist/plain-json-object.d.ts +6 -0
- package/dist/plain-json-object.js +11 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.js +19 -0
- package/dist/url-credential.d.ts +41 -0
- package/dist/url-credential.js +95 -0
- package/docs/API-SURFACE.md +15 -3
- package/docs/CONSUMER-INTEGRATION.md +2 -0
- package/llms-full.txt +14 -1
- package/llms.txt +3 -1
- package/package.json +2 -3
package/dist/fs-failure.js
CHANGED
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
* 认得的 errno 给那一步对应的出路,别的 errno(磁盘满、I/O 错 …)给 Node 自己的句子 —— 照样响亮,只是不替它猜出路。两条与落点无关的律:
|
|
13
13
|
* · errno 只打一遍 —— 句首括号里一次,Node 句子自带的 `<errno>: ` 前缀去掉;
|
|
14
14
|
* · `ENAMETOOLONG`(路径某一节或整条超限)既不是目录的错也不是文件的错 ⇒ 一句 `path too long: <被操作系统拒的路径> (ENAMETOOLONG)`,不点名目录、不给权限类出路。
|
|
15
|
-
*
|
|
15
|
+
* 三个落点(`dir-create-temp` / `remove` / `rename`)的权限类出路走**同一条顺序**(`rename` 的「可写」是已证明的事实,见下)({@link permissionWayOut}):同一个 `EPERM` 既可能是目录模式不让写,也可能是不可变标志
|
|
16
|
+
* (macOS `chflags uchg` / Linux `chattr +i`)—— 铸点是纯函数,分不清,但调用方在 Node 一侧能读目录模式,把「模式对本进程可写」这个事实经落点的 `directoryWritable` 递进来:
|
|
17
|
+
* `EPERM` ∧ 可写 ⇒ chmod 已经没用,首选出路 = 清标志(`chflags nouchg` / `chattr -i`);否则(模式不可写,或调用方没读过模式 —— 注入的替身 I/O 抛的错,包看不到它背后)⇒ 先 chmod,
|
|
18
|
+
* 句尾提示「只读 / 不可变」的可能。`EROFS` 的出路不变(只读文件系统清不掉),`EACCES` 是纯权限位,不吃这条规则。
|
|
19
|
+
* `rename` 落点的「可写」不是读出来的而是已证明的(走到 rename 时临时文件刚在同一目录建成):`EPERM` 恒为清标志先行,`EACCES` 保留 14.4.0 的文件向原句("make the file replaceable — clear an immutable flag"),
|
|
20
|
+
* 两者句尾都是粘滞目录一句,都不说「make the directory writable」。
|
|
16
21
|
*
|
|
17
22
|
* 原子写器抛 {@link FsFailure}(句子在 `message`,`code` 照抄底层 errno,原错误在 `cause`);它在写器内部已经按更细的落点铸过句子,
|
|
18
23
|
* 命令行 / 迁移用 {@link fsFailureMessage} 取句 —— 已经是 {@link FsFailure} 的原样用,否则按它所在的口铸。
|
|
@@ -21,11 +26,22 @@
|
|
|
21
26
|
const DIRECTORY_NOT_WRITABLE = new Set(["EACCES", "EPERM", "EROFS", "ENOENT"]);
|
|
22
27
|
/** `remove` 里「目录不让删目录项」的 errno(同一个因:目录不可写)。目录不在 / 文件已不在(ENOENT)不是这个因,给 Node 自己的句子。 */
|
|
23
28
|
const DIRECTORY_ENTRY_NOT_REMOVABLE = new Set(["EACCES", "EPERM", "EROFS"]);
|
|
24
|
-
/** `rename` 里「目录收新文件、却换不掉这一份」的 errno:不可变标志、粘滞目录里别人的文件。出路 =
|
|
29
|
+
/** `rename` 里「目录收新文件、却换不掉这一份」的 errno:不可变标志、粘滞目录里别人的文件。出路 = 文件向(见 {@link renameWayOut}),句尾加粘滞目录一句。 */
|
|
25
30
|
const FILE_NOT_REPLACEABLE = new Set(["EACCES", "EPERM"]);
|
|
26
|
-
/**
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
/** `rename` 落点上「目录对本进程可写」的事实:走到 rename 时临时文件刚在**同一目录**建成,这是已证明的,不是读出来的 —— 写器不对 rename 落点读模式位(一个来源)。 */
|
|
32
|
+
const RENAME_DIRECTORY_WRITABLE = true;
|
|
33
|
+
/** 权限类出路的**唯一一条顺序**(三个落点共用;`rename` 以「可写」为已证明的事实代入):`EPERM` ∧ 目录模式对本进程可写 ⇒ 真因只能是不可变标志(或粘滞位),chmod 无效 ⇒ 首选出路 = 清标志,点名清谁(`flagSubject`)与两个平台的命令;
|
|
34
|
+
* 其余 ⇒ 先让目录可写,`EPERM` / `EROFS`(靠改权限位修不好的两种)的句尾提示「只读 / 不可变」;`EACCES` / `ENOENT` 是纯权限位 / 目录不在,出路就是让目录可写。 */
|
|
35
|
+
const permissionWayOut = (code, directoryWritable, flagSubject) => code === "EPERM" && directoryWritable === true
|
|
36
|
+
? `clear the immutable flag on ${flagSubject} (macOS: chflags nouchg; Linux: chattr -i) — the directory's permission bits already admit this process, so chmod will not help`
|
|
37
|
+
: code === "EPERM" || code === "EROFS"
|
|
38
|
+
? "make the directory writable; directory may be read-only or immutable (chflags / chattr)"
|
|
39
|
+
: "make the directory writable";
|
|
40
|
+
/** `rename` 落点的出路(文件向):`EPERM` 走 {@link permissionWayOut} 的顺序(目录可写已证明 ⇒ 清标志先行);`EACCES` 是 14.4.0 的原句逐字 ——
|
|
41
|
+
* 无论哪个 errno,都不说「make the directory writable」(与已证明的事实自相矛盾)。 */
|
|
42
|
+
const renameWayOut = (code) => code === "EPERM" ? permissionWayOut(code, RENAME_DIRECTORY_WRITABLE, "the file or its directory") : "make the file replaceable — clear an immutable flag";
|
|
43
|
+
/** `rename` 句尾的粘滞目录一句(与权限类出路并列的另一个因:粘滞目录里别人的文件只有属主能换)。 */
|
|
44
|
+
const STICKY_DIRECTORY_NOTE = "in a sticky directory only the file's owner may replace it";
|
|
29
45
|
const errnoOf = (e) => {
|
|
30
46
|
const code = e?.code;
|
|
31
47
|
return typeof code === "string" ? code : undefined;
|
|
@@ -49,13 +65,13 @@ export function fsFailureSentence(site, cause) {
|
|
|
49
65
|
const tag = code === undefined ? "" : ` (${code})`;
|
|
50
66
|
switch (site.landing) {
|
|
51
67
|
case "dir-create-temp":
|
|
52
|
-
return `cannot write into directory ${parentDirectory(site.path)}${tag}: ${code !== undefined && DIRECTORY_NOT_WRITABLE.has(code) ?
|
|
68
|
+
return `cannot write into directory ${parentDirectory(site.path)}${tag}: ${code !== undefined && DIRECTORY_NOT_WRITABLE.has(code) ? permissionWayOut(code, site.directoryWritable, "the directory") : nodeSentenceOf(cause, code)}`;
|
|
53
69
|
case "rename":
|
|
54
70
|
return code !== undefined && FILE_NOT_REPLACEABLE.has(code)
|
|
55
|
-
? `cannot replace ${site.path}${tag}:
|
|
71
|
+
? `cannot replace ${site.path}${tag}: ${renameWayOut(code)}; ${STICKY_DIRECTORY_NOTE}`
|
|
56
72
|
: `cannot replace ${site.path}${tag}: ${nodeSentenceOf(cause, code)}`;
|
|
57
73
|
case "remove":
|
|
58
|
-
return `cannot remove ${site.path} from directory ${parentDirectory(site.path)}${tag}: ${code !== undefined && DIRECTORY_ENTRY_NOT_REMOVABLE.has(code) ?
|
|
74
|
+
return `cannot remove ${site.path} from directory ${parentDirectory(site.path)}${tag}: ${code !== undefined && DIRECTORY_ENTRY_NOT_REMOVABLE.has(code) ? permissionWayOut(code, site.directoryWritable, "the file or its directory") : nodeSentenceOf(cause, code)}`;
|
|
59
75
|
case "read":
|
|
60
76
|
return `cannot read ${site.of === "directory" ? "directory " : ""}${site.path}${tag}: ${nodeSentenceOf(cause, code)}`;
|
|
61
77
|
case "write":
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
export * from "./types.js";
|
|
6
6
|
export * from "./execution-lanes.js"; // 7.0.0 M-R2: the ONE lane / store vocabulary (EXECUTION_LANE_WORDS / EXECUTION_LANES / STORE_BACKENDS / SESSION_BACKENDS / MEMORY_ENGINE_BACKENDS) — pure data, browser-safe
|
|
7
7
|
export * from "./url-shape.js"; // 11.0.0: THE URL shape judge (httpUrlFlaws / httpUrlRule / isHierarchicalUrl), pure/browser-safe
|
|
8
|
+
export * from "./url-credential.js"; // 14.5.0: THE "does this URL carry a credential" judge (findUrlCredential / findUrlUserinfo / URL_CREDENTIAL_REASONS / CREDENTIAL_PARAM_NAMES …), pure/browser-safe
|
|
8
9
|
export * from "./default-model.js"; // 11.0.0: THE catalog-default resolver (roles.default → active tier ladder → fallback), pure/browser-safe
|
|
9
10
|
export * from "./loopback.js"; // 11.0.0: the two「本机」predicates (auth relaxation — narrow / connection routing — wide), pure/browser-safe
|
|
10
11
|
export * from "./hash.js";
|
package/dist/node.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ export * from "./file-edit.js";
|
|
|
9
9
|
export * from "./bundle.js";
|
|
10
10
|
export * from "./local-load.js";
|
|
11
11
|
export * from "./scheduler-store-node.js";
|
|
12
|
-
export { migrateConfigDirectory,
|
|
12
|
+
export { migrateConfigDirectory, migrateConfigDocuments, // 14.5.0:同一段「迁 → 整域终判」的同步口(入参是已有的存量文档,无 io)—— 目录形调的就是它的内核
|
|
13
|
+
type ConfigDirectoryDocument, type ConfigDirectoryIo, type ConfigDirectoryMigration, type ConfigDirectoryRefusal, type ConfigDocumentsMigration, type MigrateConfigDirectoryOptions, type MigrateConfigDocumentsOptions, } from "./config-dir-migration.js";
|
|
13
14
|
export { nodeConfigDirectoryIo } from "./config-dir-io.js";
|
package/dist/node.js
CHANGED
|
@@ -10,5 +10,6 @@ export * from "./bundle.js";
|
|
|
10
10
|
export * from "./local-load.js";
|
|
11
11
|
export * from "./scheduler-store-node.js"; // R7 self-wake scheduler store — fs binding of the ./scheduler-store contract
|
|
12
12
|
// 目录形的一次性迁移(读目录 → 迁 → 整域终判 → 原子写 → 删 retired 域文件)—— 语义只此一处,命令行是它的渲染器、引擎起服调同一只。显式列名:该模块的包内私有件(出门形/拼路径)不外泄。
|
|
13
|
-
export { migrateConfigDirectory
|
|
13
|
+
export { migrateConfigDirectory, migrateConfigDocuments, // 14.5.0:同一段「迁 → 整域终判」的同步口(入参是已有的存量文档,无 io)—— 目录形调的就是它的内核
|
|
14
|
+
} from "./config-dir-migration.js";
|
|
14
15
|
export { nodeConfigDirectoryIo } from "./config-dir-io.js"; // 上面那只函数的真 I/O 绑定:node:fs + 本包唯一的原子写器(config-document 模式规则)
|
|
@@ -33,6 +33,7 @@ import { outgoingPath, redactSecrets } from "./config-fns.js";
|
|
|
33
33
|
import { LANE_ENV_OWNER, STORE_ENV_KEYS, isLaneEnvKey } from "./execution-lane-index.js";
|
|
34
34
|
import { EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, STORE_BACKENDS } from "./execution-lanes.js";
|
|
35
35
|
import { INTEGER } from "./integer.js";
|
|
36
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
36
37
|
import { RUNTIME_LIFT_TARGETS } from "./runtime-lift-targets.js";
|
|
37
38
|
import { RETIRED_ROLE_WORDS } from "./retired-role-words.js";
|
|
38
39
|
import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
|
|
@@ -197,7 +198,7 @@ export const ONE_TIME_MIGRATIONS = [
|
|
|
197
198
|
* 重跑恒等:第二次跑零改动,只剩第一次就 `refused` 的那几张收据(那些坐标原样留着,读侧照旧拒)。
|
|
198
199
|
*/
|
|
199
200
|
export function migrateStoredDocuments(docs, opts) {
|
|
200
|
-
if (!
|
|
201
|
+
if (!isPlainJsonObject(docs))
|
|
201
202
|
throw new TypeError("migrateStoredDocuments: `docs` must be a { domain: document } object");
|
|
202
203
|
switch (opts.coverage) {
|
|
203
204
|
case "scope":
|
|
@@ -229,7 +230,7 @@ export function migrateStoredDocuments(docs, opts) {
|
|
|
229
230
|
* 不是对象 ⇒ 原样返回(判形是读者的事)。
|
|
230
231
|
*/
|
|
231
232
|
export function migrateEffectiveSnapshot(effective) {
|
|
232
|
-
if (!
|
|
233
|
+
if (!isPlainJsonObject(effective))
|
|
233
234
|
return { effective, receipts: [] };
|
|
234
235
|
const docs = {};
|
|
235
236
|
for (const [k, v] of Object.entries(effective))
|
|
@@ -251,13 +252,7 @@ export function migrateEffectiveSnapshot(effective) {
|
|
|
251
252
|
next[k] = v;
|
|
252
253
|
return { effective: next, receipts: m.receipts };
|
|
253
254
|
}
|
|
254
|
-
|
|
255
|
-
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
256
|
-
return false;
|
|
257
|
-
const proto = Object.getPrototypeOf(v);
|
|
258
|
-
return proto === Object.prototype || proto === null;
|
|
259
|
-
}
|
|
260
|
-
const has = (o, k) => isPlainObject(o) && Object.prototype.hasOwnProperty.call(o, k);
|
|
255
|
+
const has = (o, k) => isPlainJsonObject(o) && Object.prototype.hasOwnProperty.call(o, k);
|
|
261
256
|
const without = (o, k) => {
|
|
262
257
|
const { [k]: _gone, ...rest } = o;
|
|
263
258
|
return rest;
|
|
@@ -291,12 +286,12 @@ function stepRuntimeDomain(out, coverage, receipts) {
|
|
|
291
286
|
if (raw === undefined)
|
|
292
287
|
return;
|
|
293
288
|
const refuse = (reason) => void receipts.push(refused("SS-M1", "runtime", [], reason));
|
|
294
|
-
if (
|
|
289
|
+
if (isPlainJsonObject(raw) && Object.keys(raw).length === 0) {
|
|
295
290
|
delete out.runtime;
|
|
296
291
|
receipts.push(receipt("SS-M1", "runtime", [], "dropped-inert"));
|
|
297
292
|
return;
|
|
298
293
|
}
|
|
299
|
-
if (!
|
|
294
|
+
if (!isPlainJsonObject(raw))
|
|
300
295
|
return refuse("the stored runtime document is not a JSON object — the scope does not read today either; fix it by hand");
|
|
301
296
|
if (coverage === "partial" && !RUNTIME_LIFT_TARGETS.every((target) => has(out, target))) {
|
|
302
297
|
return refuse("a runtime document migrates only together with its scope's governance and limits documents (coverage \"scope\") — on its own there is no telling whether governance was ever set");
|
|
@@ -305,7 +300,7 @@ function stepRuntimeDomain(out, coverage, receipts) {
|
|
|
305
300
|
if (!parsed.success)
|
|
306
301
|
return refuse("the stored runtime document does not read under the 10.x runtime schema — the scope does not read today either; fix it by hand");
|
|
307
302
|
const limitsRaw = out.limits;
|
|
308
|
-
if (limitsRaw !== undefined && !
|
|
303
|
+
if (limitsRaw !== undefined && !isPlainJsonObject(limitsRaw))
|
|
309
304
|
return refuse("the scope's limits document is not a JSON object — the scope does not read today either; fix limits first");
|
|
310
305
|
const trio = GOVERNANCE_TRIO.filter((k) => has(raw, k) && raw[k] !== undefined);
|
|
311
306
|
if (trio.length > 0) {
|
|
@@ -323,7 +318,7 @@ function stepRuntimeDomain(out, coverage, receipts) {
|
|
|
323
318
|
const limitKeys = LIMIT_KEYS.filter((k) => parsed.data[k] !== undefined);
|
|
324
319
|
if (limitKeys.length > 0) {
|
|
325
320
|
// 迁移前的读法(消费方):`limits` 有这一键 ⇒ limits 赢;没有 ⇒ 读 runtime 上的值。
|
|
326
|
-
const limits =
|
|
321
|
+
const limits = isPlainJsonObject(limitsRaw) ? { ...limitsRaw } : {};
|
|
327
322
|
let added = false;
|
|
328
323
|
for (const k of limitKeys) {
|
|
329
324
|
if (Object.prototype.hasOwnProperty.call(limits, k))
|
|
@@ -346,13 +341,13 @@ function stepRuntimeDomain(out, coverage, receipts) {
|
|
|
346
341
|
// ── SS-M2:hosts 超界 ───────────────────────────────────────────────────────────────────────────────────
|
|
347
342
|
function stepHostsBounds(out, receipts) {
|
|
348
343
|
const doc = out.hosts;
|
|
349
|
-
if (!
|
|
344
|
+
if (!isPlainJsonObject(doc) || !Array.isArray(doc.hosts))
|
|
350
345
|
return;
|
|
351
346
|
const rows = doc.hosts;
|
|
352
347
|
let kept = [];
|
|
353
348
|
const dropped = [];
|
|
354
349
|
rows.forEach((row, index) => {
|
|
355
|
-
const name =
|
|
350
|
+
const name = isPlainJsonObject(row) ? row.name : undefined;
|
|
356
351
|
if (typeof name === "string" && name.length > HOST_NAME_MAX)
|
|
357
352
|
dropped.push(index);
|
|
358
353
|
else
|
|
@@ -402,8 +397,8 @@ const UNCHANGED = { kind: "unchanged" };
|
|
|
402
397
|
function migrateWorkerRow(raw) {
|
|
403
398
|
const refuse = (field, reason) => ({ kind: "refused", field, reason });
|
|
404
399
|
const enabled = raw.enabled !== false;
|
|
405
|
-
const laneKeysOf = (rec) => (
|
|
406
|
-
const storeKeysOf = (rec) => (
|
|
400
|
+
const laneKeysOf = (rec) => (isPlainJsonObject(rec) ? Object.keys(rec).filter(isLaneEnvKey) : []);
|
|
401
|
+
const storeKeysOf = (rec) => (isPlainJsonObject(rec) ? STORE_ENV_KEYS.filter((k) => Object.hasOwn(rec, k)) : []);
|
|
407
402
|
const envLaneKeys = laneKeysOf(raw.env);
|
|
408
403
|
const secretLaneKeys = laneKeysOf(raw.secretEnv);
|
|
409
404
|
const envStoreKeys = storeKeysOf(raw.env);
|
|
@@ -432,12 +427,12 @@ function migrateWorkerRow(raw) {
|
|
|
432
427
|
const needLane = !hasExecution && (enabled || laneTouched);
|
|
433
428
|
const needStore = !hasStore && (enabled || storeTouched);
|
|
434
429
|
const w = structuredClone(raw);
|
|
435
|
-
const runtime =
|
|
430
|
+
const runtime = isPlainJsonObject(w.runtime) ? w.runtime : {};
|
|
436
431
|
w.runtime = runtime;
|
|
437
|
-
const env =
|
|
438
|
-
const secretEnv =
|
|
439
|
-
const coords =
|
|
440
|
-
const secretRefs =
|
|
432
|
+
const env = isPlainJsonObject(w.env) ? w.env : {};
|
|
433
|
+
const secretEnv = isPlainJsonObject(w.secretEnv) ? w.secretEnv : {};
|
|
434
|
+
const coords = isPlainJsonObject(w.coords) ? w.coords : {};
|
|
435
|
+
const secretRefs = isPlainJsonObject(w.secretRefs) ? w.secretRefs : {};
|
|
441
436
|
const dropped = [];
|
|
442
437
|
const drop = (container, slot, key) => {
|
|
443
438
|
delete container[key];
|
|
@@ -601,9 +596,9 @@ function migrateWorkerRow(raw) {
|
|
|
601
596
|
}
|
|
602
597
|
}
|
|
603
598
|
// 被搬空的 env / secretEnv 整键去掉(它们是可选字段;留一个 `{}` 只是噪声)
|
|
604
|
-
if (
|
|
599
|
+
if (isPlainJsonObject(w.env) && Object.keys(env).length === 0 && isPlainJsonObject(raw.env) && Object.keys(raw.env).length > 0)
|
|
605
600
|
delete w.env;
|
|
606
|
-
if (
|
|
601
|
+
if (isPlainJsonObject(w.secretEnv) && Object.keys(secretEnv).length === 0 && isPlainJsonObject(raw.secretEnv) && Object.keys(raw.secretEnv).length > 0)
|
|
607
602
|
delete w.secretEnv;
|
|
608
603
|
// 改写后的行仍读不进 7.x 形 ⇒ 不写回半成品:拒,原文留盘(读口 `readWorkersRows` 扣下它)。判据与拒句是读口那一只(`workerRowVerdict`)。
|
|
609
604
|
const verdict = workerRowVerdict(w, "migrated");
|
|
@@ -613,12 +608,12 @@ function migrateWorkerRow(raw) {
|
|
|
613
608
|
}
|
|
614
609
|
function stepWorkers6x(out, receipts) {
|
|
615
610
|
const doc = out.workers;
|
|
616
|
-
if (!
|
|
611
|
+
if (!isPlainJsonObject(doc) || !Array.isArray(doc.workers))
|
|
617
612
|
return;
|
|
618
613
|
const rows = doc.workers;
|
|
619
614
|
let next;
|
|
620
615
|
rows.forEach((row, index) => {
|
|
621
|
-
if (!
|
|
616
|
+
if (!isPlainJsonObject(row) || typeof row.name !== "string")
|
|
622
617
|
return; // 认不出名字:整域读侧判
|
|
623
618
|
const r = migrateWorkerRow(row);
|
|
624
619
|
switch (r.kind) {
|
|
@@ -646,7 +641,7 @@ function stepWorkers6x(out, receipts) {
|
|
|
646
641
|
// ── SS-M5:模型钥匙二选一 ──────────────────────────────────────────────────────────────────────────────
|
|
647
642
|
function stepModelKeyOneOf(out, receipts) {
|
|
648
643
|
const doc = out.models;
|
|
649
|
-
if (!
|
|
644
|
+
if (!isPlainJsonObject(doc) || !Array.isArray(doc.models))
|
|
650
645
|
return;
|
|
651
646
|
const rows = doc.models;
|
|
652
647
|
let next;
|
|
@@ -682,17 +677,17 @@ function rewriteAtMention(holder, document, at, receipts) {
|
|
|
682
677
|
return { ...without(holder, "atModelAllowlist"), atMention };
|
|
683
678
|
}
|
|
684
679
|
function stepAtMention(out, receipts) {
|
|
685
|
-
if (
|
|
680
|
+
if (isPlainJsonObject(out.models)) {
|
|
686
681
|
const next = rewriteAtMention(out.models, "models", [], receipts);
|
|
687
682
|
if (next !== undefined)
|
|
688
683
|
out.models = next;
|
|
689
684
|
}
|
|
690
685
|
const rosters = out.rosters;
|
|
691
|
-
if (
|
|
686
|
+
if (isPlainJsonObject(rosters) && Array.isArray(rosters.rosters)) {
|
|
692
687
|
const rows = rosters.rosters;
|
|
693
688
|
let next;
|
|
694
689
|
rows.forEach((row, index) => {
|
|
695
|
-
if (!
|
|
690
|
+
if (!isPlainJsonObject(row))
|
|
696
691
|
return;
|
|
697
692
|
const r = rewriteAtMention(row, "rosters", ["rosters", index], receipts);
|
|
698
693
|
if (r === undefined)
|
|
@@ -707,7 +702,7 @@ function stepAtMention(out, receipts) {
|
|
|
707
702
|
// ── SS-M8:退役的角色词 ────────────────────────────────────────────────────────────────────────────────
|
|
708
703
|
/** 一个 `roles` 图上的退役角色词键删掉(值不论 —— 没有角色读它);逐键一张 `dropped-inert` 收据。返回改后的图或 `undefined`(没改 / 不是对象:判形是读者的事)。 */
|
|
709
704
|
function withoutRetiredRoles(roles, document, at, receipts) {
|
|
710
|
-
if (!
|
|
705
|
+
if (!isPlainJsonObject(roles))
|
|
711
706
|
return undefined;
|
|
712
707
|
const present = RETIRED_ROLE_WORDS.filter((w) => Object.prototype.hasOwnProperty.call(roles, w));
|
|
713
708
|
if (present.length === 0)
|
|
@@ -721,17 +716,17 @@ function withoutRetiredRoles(roles, document, at, receipts) {
|
|
|
721
716
|
}
|
|
722
717
|
function stepRetiredRoleWords(out, receipts) {
|
|
723
718
|
const models = out.models;
|
|
724
|
-
if (
|
|
719
|
+
if (isPlainJsonObject(models)) {
|
|
725
720
|
const roles = withoutRetiredRoles(models.roles, "models", [], receipts);
|
|
726
721
|
if (roles !== undefined)
|
|
727
722
|
out.models = { ...models, roles };
|
|
728
723
|
}
|
|
729
724
|
const rosters = out.rosters;
|
|
730
|
-
if (
|
|
725
|
+
if (isPlainJsonObject(rosters) && Array.isArray(rosters.rosters)) {
|
|
731
726
|
const rows = rosters.rosters;
|
|
732
727
|
let next;
|
|
733
728
|
rows.forEach((row, index) => {
|
|
734
|
-
if (!
|
|
729
|
+
if (!isPlainJsonObject(row))
|
|
735
730
|
return;
|
|
736
731
|
const roles = withoutRetiredRoles(row.roles, "rosters", ["rosters", index], receipts);
|
|
737
732
|
if (roles === undefined)
|
|
@@ -749,16 +744,16 @@ function stepRetiredRoleWords(out, receipts) {
|
|
|
749
744
|
* 消失);异名 ⇒ 原 `roles.default` 作「被压过」删掉、`default` 搬入。`default` 不是非空串 ⇒ 拒(今天它让整个 models 域读不进,不替运维猜)。 */
|
|
750
745
|
function stepModelsDefault(out, receipts) {
|
|
751
746
|
const doc = out.models;
|
|
752
|
-
if (!
|
|
747
|
+
if (!isPlainJsonObject(doc) || !Object.prototype.hasOwnProperty.call(doc, "default"))
|
|
753
748
|
return;
|
|
754
749
|
const name = doc.default;
|
|
755
750
|
if (typeof name !== "string" || name === "") {
|
|
756
751
|
receipts.push(refused("SS-M9", "models", ["default"], "models.default is not a catalog name — it does not read today either; fix it by hand"));
|
|
757
752
|
return;
|
|
758
753
|
}
|
|
759
|
-
const roles =
|
|
754
|
+
const roles = isPlainJsonObject(doc.roles) ? doc.roles : {};
|
|
760
755
|
const prior = roles.default;
|
|
761
|
-
if (prior !== undefined && !(
|
|
756
|
+
if (prior !== undefined && !(isPlainJsonObject(prior) && prior.model === name))
|
|
762
757
|
receipts.push(receipt("SS-M9", "models", ["roles", "default"], "dropped-shadowed"));
|
|
763
758
|
receipts.push(moved("SS-M9", "models", ["default"], { document: "models", path: ["roles", "default"] }));
|
|
764
759
|
out.models = { ...without(doc, "default"), roles: { ...roles, default: { model: name } } };
|
|
@@ -769,7 +764,7 @@ function stepModelsDefault(out, receipts) {
|
|
|
769
764
|
* overlay,catalog 的 digest 是引擎铸的,替运维机械转就是第二个编译器 / 第二个铸点。`prompts` 不是对象 ⇒ 不动(判形是读者的事)。 */
|
|
770
765
|
function stepPromptsAxes(out, receipts) {
|
|
771
766
|
const doc = out.prompts;
|
|
772
|
-
if (!
|
|
767
|
+
if (!isPlainJsonObject(doc))
|
|
773
768
|
return;
|
|
774
769
|
let next = doc;
|
|
775
770
|
if (has(doc, "bindings") && doc.bindings !== undefined) {
|
|
@@ -820,7 +815,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
|
|
|
820
815
|
return value.slice(0, hash);
|
|
821
816
|
}
|
|
822
817
|
if (s instanceof z.ZodDiscriminatedUnion) {
|
|
823
|
-
if (!
|
|
818
|
+
if (!isPlainJsonObject(value))
|
|
824
819
|
return value;
|
|
825
820
|
const disc = s._zod.def.discriminator;
|
|
826
821
|
for (const option of s.options) {
|
|
@@ -842,7 +837,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
|
|
|
842
837
|
return value;
|
|
843
838
|
}
|
|
844
839
|
if (s instanceof z.ZodObject) {
|
|
845
|
-
if (!
|
|
840
|
+
if (!isPlainJsonObject(value))
|
|
846
841
|
return value;
|
|
847
842
|
let next;
|
|
848
843
|
for (const [k, fieldSchema] of Object.entries(s.shape)) {
|
|
@@ -866,7 +861,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
|
|
|
866
861
|
return next ?? value;
|
|
867
862
|
}
|
|
868
863
|
if (s instanceof z.ZodRecord) {
|
|
869
|
-
if (!
|
|
864
|
+
if (!isPlainJsonObject(value))
|
|
870
865
|
return value;
|
|
871
866
|
let next;
|
|
872
867
|
for (const [k, el] of Object.entries(value)) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶、不进 exports 表):**「纯 JSON 对象」的谓词(三处读域 / 迁移 / config.d 的同一只;`hooks-config.ts` 的 `hookIsRecord`、`types.ts` 内联一份、`conformance/` 两只更弱的 `isPlainObject` 是既有副本,另票收)** —— 不是 `null`、不是数组、原型是 `Object.prototype` 或 `null`(JSON 解析出来的对象恒如此;
|
|
3
|
+
* `Date` / `Map` / 类实例 / 建在别的对象原型上的记录都不是)。读域文档的键集对比(`./config-fns`)、一次性迁移的文档形判(`./one-time-migrations`)、
|
|
4
|
+
* `config.d` 文档「是不是 JSON 对象」的判(`./config-d-file`)读它,不各写一份。零 import。
|
|
5
|
+
*/
|
|
6
|
+
export declare function isPlainJsonObject(v: unknown): v is Record<string, unknown>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有(不进主桶、不进 exports 表):**「纯 JSON 对象」的谓词(三处读域 / 迁移 / config.d 的同一只;`hooks-config.ts` 的 `hookIsRecord`、`types.ts` 内联一份、`conformance/` 两只更弱的 `isPlainObject` 是既有副本,另票收)** —— 不是 `null`、不是数组、原型是 `Object.prototype` 或 `null`(JSON 解析出来的对象恒如此;
|
|
3
|
+
* `Date` / `Map` / 类实例 / 建在别的对象原型上的记录都不是)。读域文档的键集对比(`./config-fns`)、一次性迁移的文档形判(`./one-time-migrations`)、
|
|
4
|
+
* `config.d` 文档「是不是 JSON 对象」的判(`./config-d-file`)读它,不各写一份。零 import。
|
|
5
|
+
*/
|
|
6
|
+
export function isPlainJsonObject(v) {
|
|
7
|
+
if (v === null || typeof v !== "object" || Array.isArray(v))
|
|
8
|
+
return false;
|
|
9
|
+
const proto = Object.getPrototypeOf(v);
|
|
10
|
+
return proto === Object.prototype || proto === null;
|
|
11
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -57,6 +57,10 @@ export declare function urlQueryFragmentHasSecret(u: URL): boolean;
|
|
|
57
57
|
* echoed; a fragment's also names its migration, {@link nonSecretUrlIssue}); `params` carries the sentence (`template` / `operands`,
|
|
58
58
|
* no operands) and `flaw`, the rule word.
|
|
59
59
|
*
|
|
60
|
+
* Scope of the secret test: it judges the VALUE's shape (a value that looks like a secret, whatever the parameter is called) — the sibling
|
|
61
|
+
* name-shaped judge `findUrlCredential` (`./url-credential`) asks the other question (a parameter whose NAME is on the credential-name table,
|
|
62
|
+
* whatever the value looks like). Neither contains the other: `?key=abc` passes here and is a hit there; `?x=sk-live-…` is the reverse.
|
|
63
|
+
*
|
|
60
64
|
* This is the seat of every URL in a STORED document the one-time migration rewrites: the domain documents (models `baseUrl`, mcp `url`,
|
|
61
65
|
* `sessionMirror.engineUrl`, workers `gitApiBaseUrl`) and `config.d/remote-exec.json` (k8s `apiUrl`, `s3Snapshot.endpoint`). A URL
|
|
62
66
|
* that is not stored data — the web-admin credential envelope's `issuer`, a wire input — uses {@link NON_SECRET_URL_NO_MIGRATION}.
|
|
@@ -1793,6 +1797,8 @@ export declare const SkillSpec: z.ZodObject<{
|
|
|
1793
1797
|
rosters: z.ZodPrefault<z.ZodArray<z.ZodString>>;
|
|
1794
1798
|
content: z.ZodDefault<z.ZodString>;
|
|
1795
1799
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
1800
|
+
allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1801
|
+
disallowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1796
1802
|
notes: z.ZodOptional<z.ZodString>;
|
|
1797
1803
|
}, z.core.$strip>;
|
|
1798
1804
|
export type SkillSpec = z.infer<typeof SkillSpec>;
|
|
@@ -1804,6 +1810,8 @@ export declare const SkillsConfig: z.ZodObject<{
|
|
|
1804
1810
|
rosters: z.ZodPrefault<z.ZodArray<z.ZodString>>;
|
|
1805
1811
|
content: z.ZodDefault<z.ZodString>;
|
|
1806
1812
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
1813
|
+
allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1814
|
+
disallowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1807
1815
|
notes: z.ZodOptional<z.ZodString>;
|
|
1808
1816
|
}, z.core.$strip>>>;
|
|
1809
1817
|
}, z.core.$strip>;
|
|
@@ -1828,6 +1836,8 @@ export declare const SkillsManifestSchema: z.ZodObject<{
|
|
|
1828
1836
|
rosters: z.ZodPrefault<z.ZodArray<z.ZodString>>;
|
|
1829
1837
|
description: z.ZodDefault<z.ZodString>;
|
|
1830
1838
|
scenarios: z.ZodPrefault<z.ZodArray<z.ZodString>>;
|
|
1839
|
+
allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1840
|
+
disallowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1831
1841
|
contentHash: z.ZodString;
|
|
1832
1842
|
}, z.core.$strip>>>;
|
|
1833
1843
|
}, z.core.$strip>;
|
|
@@ -2480,6 +2490,8 @@ export declare const DOMAIN_SCHEMAS: {
|
|
|
2480
2490
|
rosters: z.ZodPrefault<z.ZodArray<z.ZodString>>;
|
|
2481
2491
|
content: z.ZodDefault<z.ZodString>;
|
|
2482
2492
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
2493
|
+
allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
2494
|
+
disallowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
2483
2495
|
notes: z.ZodOptional<z.ZodString>;
|
|
2484
2496
|
}, z.core.$strip>>>;
|
|
2485
2497
|
}, z.core.$strip>;
|
package/dist/types.js
CHANGED
|
@@ -193,6 +193,10 @@ function createNonSecretUrl(reachedByMigration) {
|
|
|
193
193
|
* echoed; a fragment's also names its migration, {@link nonSecretUrlIssue}); `params` carries the sentence (`template` / `operands`,
|
|
194
194
|
* no operands) and `flaw`, the rule word.
|
|
195
195
|
*
|
|
196
|
+
* Scope of the secret test: it judges the VALUE's shape (a value that looks like a secret, whatever the parameter is called) — the sibling
|
|
197
|
+
* name-shaped judge `findUrlCredential` (`./url-credential`) asks the other question (a parameter whose NAME is on the credential-name table,
|
|
198
|
+
* whatever the value looks like). Neither contains the other: `?key=abc` passes here and is a hit there; `?x=sk-live-…` is the reverse.
|
|
199
|
+
*
|
|
196
200
|
* This is the seat of every URL in a STORED document the one-time migration rewrites: the domain documents (models `baseUrl`, mcp `url`,
|
|
197
201
|
* `sessionMirror.engineUrl`, workers `gitApiBaseUrl`) and `config.d/remote-exec.json` (k8s `apiUrl`, `s3Snapshot.endpoint`). A URL
|
|
198
202
|
* that is not stored data — the web-admin credential envelope's `issuer`, a wire input — uses {@link NON_SECRET_URL_NO_MIGRATION}.
|
|
@@ -2338,6 +2342,21 @@ export const SkillSpec = z.object({
|
|
|
2338
2342
|
/** The markdown body injected into the model context on invoke (progressive disclosure). Stored in full here; shipped as a hash in /effective. */
|
|
2339
2343
|
content: z.string().default(""),
|
|
2340
2344
|
enabled: z.boolean().default(true),
|
|
2345
|
+
/** Tool allow list the skill declares — carried entry by entry as the author wrote it (same name and element shape as the
|
|
2346
|
+
* engine's local `.sema/skills` manifest `SkillSpec.allowedTools`). ABSENT = the key is not there (no list is carried);
|
|
2347
|
+
* PRESENT = the whole array, `[]` included, goes to the consumer's judge — two carried shapes, and what each reads as is the
|
|
2348
|
+
* judge's call (the engine's local skills directory reads an empty list as "not declared"). This contract only CARRIES: no
|
|
2349
|
+
* de-duplication, no sorting, no spelling check, no roster intersection — what an entry means (a tool, a namespace, a rule,
|
|
2350
|
+
* a path) is read in ONE place, the consumer's grant judge; the element shape judged here is "a non-empty string" and nothing
|
|
2351
|
+
* more. A malformed shape (not an array / a non-string or empty entry) is refused with a path naming the field
|
|
2352
|
+
* (`skills[i].allowedTools`) and, when the fault is an element, the element (`skills[i].allowedTools[j]`). Read leg: the list is an ordinary nested collection and follows the one salvage law that
|
|
2353
|
+
* `scenarios` / `rosters` / `mcp.servers[].allowTools` follow (a bad element is dropped with an `entry-dropped` warning whose
|
|
2354
|
+
* path reaches the element; a list with no good element makes the skill unreadable and the skill is dropped; when no skill of the domain survives, the domain falls back to its default with `domain-defaulted`). Travels on
|
|
2355
|
+
* `/effective` with the rest of the manifest ({@link SkillsManifestSchema} derives from this schema). */
|
|
2356
|
+
allowedTools: z.array(z.string().min(1)).optional(),
|
|
2357
|
+
/** Tool deny list the skill declares — same carrying and reading rules as {@link SkillSpec.allowedTools} (absent = no list
|
|
2358
|
+
* carried, present = the whole array goes to the consumer's judge, `[]` included; never de-duplicated, sorted or judged here). */
|
|
2359
|
+
disallowedTools: z.array(z.string().min(1)).optional(),
|
|
2341
2360
|
notes: z.string().optional(),
|
|
2342
2361
|
});
|
|
2343
2362
|
export const SkillsConfig = z
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** 拒因词(闭集;判序 = 本数组序;词只在这一处写 —— {@link UrlCredentialReason} 与 {@link UrlCredential} 的成员都从它派生)。 */
|
|
2
|
+
export declare const URL_CREDENTIAL_REASONS: readonly ["url_userinfo", "url_credential_param"];
|
|
3
|
+
export type UrlCredentialReason = (typeof URL_CREDENTIAL_REASONS)[number];
|
|
4
|
+
/**
|
|
5
|
+
* 凭据参数名表(闭集,23 词,全部是小写 ASCII `[a-z][a-z0-9_-]*`)。query / fragment 里名字整名(ASCII 大小写不敏感)在这张表上的参数就是凭据。
|
|
6
|
+
* 加一个词 = 表里加一格(minor);判官与 {@link isCredentialParamName} 现算,不手抄第二份。
|
|
7
|
+
*/
|
|
8
|
+
export declare const CREDENTIAL_PARAM_NAMES: readonly ["api_key", "apikey", "api-key", "key", "token", "access_token", "refresh_token", "id_token", "auth", "authorization", "proxy-authorization", "password", "passwd", "pwd", "secret", "client_secret", "private_key", "signature", "sig", "x-amz-signature", "x-amz-security-token", "x-amz-credential", "sas"];
|
|
9
|
+
export type CredentialParamName = (typeof CREDENTIAL_PARAM_NAMES)[number];
|
|
10
|
+
/**
|
|
11
|
+
* 「这个**已解码**的参数名是不是凭据名」。入参是收方读到的那一形(`URLSearchParams` 解过一次码的名字):整名在 {@link CREDENTIAL_PARAM_NAMES} 里(ASCII 大小写不敏感)才算。
|
|
12
|
+
*/
|
|
13
|
+
export declare function isCredentialParamName(decodedName: string): boolean;
|
|
14
|
+
/** 每个拒因词带的细节(键集必须覆盖 {@link URL_CREDENTIAL_REASONS} 的每个词:数组里加一个词而这里没跟 ⇒ 下面的映射型编译红,{@link urlCredentialRule} 的穷举 switch 也红)。 */
|
|
15
|
+
interface UrlCredentialDetail {
|
|
16
|
+
readonly url_userinfo: unknown;
|
|
17
|
+
readonly url_credential_param: {
|
|
18
|
+
readonly at: "query" | "fragment";
|
|
19
|
+
readonly name: CredentialParamName;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/** 一处命中:`url_userinfo`,或 `url_credential_param` + 它在哪一段、表里的哪个词(小写;不是调用方的原字节)。成员从 {@link URL_CREDENTIAL_REASONS} 派生。 */
|
|
23
|
+
export type UrlCredential = {
|
|
24
|
+
readonly [R in UrlCredentialReason]: {
|
|
25
|
+
readonly reason: R;
|
|
26
|
+
} & UrlCredentialDetail[R];
|
|
27
|
+
}[UrlCredentialReason];
|
|
28
|
+
/**
|
|
29
|
+
* 判:这个值作为 URL 读出来(WHATWG),带不带 **userinfo**(判官的第一问,也是 {@link findUrlCredential} 的第一步)。只问这一件的座位 —— 运行时本就拒带 userinfo 的 URL、
|
|
30
|
+
* 而凭据名参数不由它拒的载体 —— 问这一只,不在座位里另写一份 userinfo 谓词。纯函数。
|
|
31
|
+
*/
|
|
32
|
+
export declare function findUrlUserinfo(raw: string): Extract<UrlCredential, {
|
|
33
|
+
readonly reason: "url_userinfo";
|
|
34
|
+
}> | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* 判:这个值作为 URL 读出来(WHATWG),带不带凭据。首个命中(userinfo → query → fragment);没有(含不透明 / 解析不了的值)⇒ `undefined`。纯函数。
|
|
37
|
+
*/
|
|
38
|
+
export declare function findUrlCredential(raw: string): UrlCredential | undefined;
|
|
39
|
+
/** 一处命中的规则句(穷举;新拒因词不补 = 编译红)。只说规则与表词,不含值。 */
|
|
40
|
+
export declare function urlCredentialRule(c: UrlCredential): string;
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* **「这个 URL 的 userinfo 或 query / fragment 里,有没有名字在凭据名表上的参数」的名形判官**(14.5.0;lift 自 server `src/url-credential.ts`,规则、拒因词闭集与规则句逐字搬)。纯函数,零 import(除同包的 `./url-shape`),浏览器安全。
|
|
3
|
+
*
|
|
4
|
+
* ## 与 `NON_SECRET_URL`(`./types`)的关系:两个问题,互补,不是一个包含另一个
|
|
5
|
+
* 本判官按**名**判 —— 参数叫什么(名字整名在 {@link CREDENTIAL_PARAM_NAMES} 上就是凭据,不论值长什么样);`NON_SECRET_URL` 的 `urlQueryFragmentHasSecret` 按**值形**判密钥 —— 值长得像密钥
|
|
6
|
+
* (不论参数叫什么)。所以 `?key=abc` 本判官命中而 `NON_SECRET_URL` 放行(值不像密钥),`?x=sk-live-…` 反之;userinfo 两边都判。存量域文档位(`mcp` 的 `transport.url` 等)读 `NON_SECRET_URL`,
|
|
7
|
+
* 请求面等座位读本判官。把两个问题合成一只定义是另一件事,这里不做。
|
|
8
|
+
*
|
|
9
|
+
* ## 规则(一条):URL 里的凭据 = 日志脱敏器已经在用的那一个定义
|
|
10
|
+
* 一个 URL 里凭据住三个位置:
|
|
11
|
+
* · **userinfo**(`scheme://<这一段>@host`,只有用户名也算)—— 判由本包的 URL 形判官 {@link httpUrlFlaws}(`["userinfo"]`),本模块不留第二份谓词;
|
|
12
|
+
* · **query 参数**、**fragment 参数**(同一段文法)里名字整名在 {@link CREDENTIAL_PARAM_NAMES} 上的那一个 —— 名字按收方的读法读:WHATWG `URLSearchParams` 解一次码,
|
|
13
|
+
* ASCII 大小写不敏感(不做 Unicode 折叠),整名在表里才算(`db_password` / `tokens` / `keyword` 是另一个名字)。
|
|
14
|
+
* 答首个命中(判序 = 上面的序),拒因闭集两词 {@link URL_CREDENTIAL_REASONS};**值从不进答案**,参数名只回显表里的那个词(小写)。
|
|
15
|
+
* 不透明形 / 解析不了的值没有可读的部件 ⇒ 答「没有」—— 这一形收不收是**座位**自己的律,不在本判官里。
|
|
16
|
+
*
|
|
17
|
+
* ## 判官只判「有没有」;座位决定怎么用
|
|
18
|
+
* 本模块不决定哪个载体拒、哪个载体放:同一个键在不同载体上的律可以不同(例:新提交的请求面拒,续跑重放存量体的腿不拒 —— 拒会砖死升级前停驻的任务;
|
|
19
|
+
* 部署 env 与续跑重放只因 userinfo 拒,凭据名参数只在新提交门拒)。所以判官**不**接进任何一份读已落盘 / 重放字节的 schema 或读器 —— 那会把载体律焊死成
|
|
20
|
+
* 读器律;座位在自己的新提交门调 {@link findUrlCredential} / {@link findUrlUserinfo},拒句 / 拒体的形(HTTP 码、字段坐标、出路句)是座位自己的铸点。
|
|
21
|
+
*
|
|
22
|
+
* ## 名表的来源
|
|
23
|
+
* {@link CREDENTIAL_PARAM_NAMES} 是 server 日志脱敏器名表(锚强度表的键)的 23 词。脱敏器另有一列「自由文本里这个词的锚强度」,那一列是脱敏器的事,不在这里;
|
|
24
|
+
* 词表本身在这里成为契约面 —— 脱敏器与本判官读同一张词表(同一个定义)靠消费方把它的表键钉在本导出上(例:`Record<CredentialParamName, …>` 的穷尽映射 ⇒ 加 / 删词不同步即编译红)。
|
|
25
|
+
*/
|
|
26
|
+
import { httpUrlFlaws, httpUrlRule, isHierarchicalUrl } from "./url-shape.js";
|
|
27
|
+
/** 拒因词(闭集;判序 = 本数组序;词只在这一处写 —— {@link UrlCredentialReason} 与 {@link UrlCredential} 的成员都从它派生)。 */
|
|
28
|
+
export const URL_CREDENTIAL_REASONS = Object.freeze(["url_userinfo", "url_credential_param"]);
|
|
29
|
+
/**
|
|
30
|
+
* 凭据参数名表(闭集,23 词,全部是小写 ASCII `[a-z][a-z0-9_-]*`)。query / fragment 里名字整名(ASCII 大小写不敏感)在这张表上的参数就是凭据。
|
|
31
|
+
* 加一个词 = 表里加一格(minor);判官与 {@link isCredentialParamName} 现算,不手抄第二份。
|
|
32
|
+
*/
|
|
33
|
+
export const CREDENTIAL_PARAM_NAMES = Object.freeze([
|
|
34
|
+
"api_key", "apikey", "api-key", "key",
|
|
35
|
+
"token", "access_token", "refresh_token", "id_token",
|
|
36
|
+
"auth", "authorization", "proxy-authorization",
|
|
37
|
+
"password", "passwd", "pwd",
|
|
38
|
+
"secret", "client_secret", "private_key",
|
|
39
|
+
"signature", "sig",
|
|
40
|
+
"x-amz-signature", "x-amz-security-token", "x-amz-credential",
|
|
41
|
+
"sas",
|
|
42
|
+
]);
|
|
43
|
+
/** 名表的查找形(`Set`,不拿普通对象当查表:`constructor` / `__proto__` 会读到原型链上的东西)。 */
|
|
44
|
+
const CREDENTIAL_PARAM_NAME_SET = new Set(CREDENTIAL_PARAM_NAMES);
|
|
45
|
+
/** ASCII 大小写折叠(只折 `A-Z`,不做 Unicode 折叠:`TOKEN` 不是 `token`)。 */
|
|
46
|
+
const asciiLower = (s) => s.replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
47
|
+
/**
|
|
48
|
+
* 「这个**已解码**的参数名是不是凭据名」。入参是收方读到的那一形(`URLSearchParams` 解过一次码的名字):整名在 {@link CREDENTIAL_PARAM_NAMES} 里(ASCII 大小写不敏感)才算。
|
|
49
|
+
*/
|
|
50
|
+
export function isCredentialParamName(decodedName) {
|
|
51
|
+
return CREDENTIAL_PARAM_NAME_SET.has(asciiLower(decodedName));
|
|
52
|
+
}
|
|
53
|
+
/** 一段参数串(`?` / `#` 之后)里第一个凭据名参数的表词(小写);没有 ⇒ `undefined`。 */
|
|
54
|
+
function credentialParamIn(params) {
|
|
55
|
+
for (const name of new URLSearchParams(params).keys()) {
|
|
56
|
+
const word = asciiLower(name);
|
|
57
|
+
if (CREDENTIAL_PARAM_NAME_SET.has(word))
|
|
58
|
+
return word;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 判:这个值作为 URL 读出来(WHATWG),带不带 **userinfo**(判官的第一问,也是 {@link findUrlCredential} 的第一步)。只问这一件的座位 —— 运行时本就拒带 userinfo 的 URL、
|
|
64
|
+
* 而凭据名参数不由它拒的载体 —— 问这一只,不在座位里另写一份 userinfo 谓词。纯函数。
|
|
65
|
+
*/
|
|
66
|
+
export function findUrlUserinfo(raw) {
|
|
67
|
+
return isHierarchicalUrl(raw) && httpUrlFlaws(raw, ["userinfo"]).includes("userinfo") ? { reason: "url_userinfo" } : undefined;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 判:这个值作为 URL 读出来(WHATWG),带不带凭据。首个命中(userinfo → query → fragment);没有(含不透明 / 解析不了的值)⇒ `undefined`。纯函数。
|
|
71
|
+
*/
|
|
72
|
+
export function findUrlCredential(raw) {
|
|
73
|
+
if (!isHierarchicalUrl(raw))
|
|
74
|
+
return undefined;
|
|
75
|
+
const userinfo = findUrlUserinfo(raw);
|
|
76
|
+
if (userinfo !== undefined)
|
|
77
|
+
return userinfo;
|
|
78
|
+
const u = new URL(raw);
|
|
79
|
+
const inQuery = credentialParamIn(u.search);
|
|
80
|
+
if (inQuery !== undefined)
|
|
81
|
+
return { reason: "url_credential_param", at: "query", name: inQuery };
|
|
82
|
+
const inFragment = credentialParamIn(u.hash.slice(1));
|
|
83
|
+
if (inFragment !== undefined)
|
|
84
|
+
return { reason: "url_credential_param", at: "fragment", name: inFragment };
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/** 一处命中的规则句(穷举;新拒因词不补 = 编译红)。只说规则与表词,不含值。 */
|
|
88
|
+
export function urlCredentialRule(c) {
|
|
89
|
+
switch (c.reason) {
|
|
90
|
+
case "url_userinfo":
|
|
91
|
+
return httpUrlRule("userinfo"); // 与形判官诸座同一句(不写第二句)
|
|
92
|
+
case "url_credential_param":
|
|
93
|
+
return `must not carry a credential parameter (the ${c.at} names "${c.name}", a name the service's log redactor treats as a credential)`;
|
|
94
|
+
}
|
|
95
|
+
}
|