@sema-agent/settings-schema 6.0.0 → 6.2.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 +278 -0
- package/README.md +62 -5
- package/conformance/VERSION +1 -1
- package/conformance/cases/c01-entry-salvage-then-typo/expected.json +3 -0
- package/conformance/cases/c02-hosts-clamp-stacked/expected.json +3 -0
- package/conformance/cases/c23-mixed-validity-models/expected.json +3 -0
- package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +166 -0
- package/conformance/cases/c40-s712-identity-only-what-reads-out/input.json +64 -0
- package/conformance/cases/c40-s712-identity-only-what-reads-out/meta.json +13 -0
- package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +8 -0
- package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +8 -0
- package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +4 -0
- package/dist/api/auth-bridge.d.ts +2 -2
- package/dist/center-credential.d.ts +116 -0
- package/dist/center-credential.js +149 -0
- package/dist/config-fns.d.ts +52 -3
- package/dist/config-fns.js +78 -8
- package/dist/conformance/types.d.ts +6 -1
- package/dist/conformance/warnings.js +9 -2
- package/dist/fleet.d.ts +260 -8
- package/dist/fleet.js +167 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types.d.ts +72 -66
- package/dist/types.js +7 -1
- package/package.json +2 -2
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"version": 11,
|
|
4
|
+
"updatedAt": "2026-09-25T00:00:00.000Z"
|
|
5
|
+
},
|
|
6
|
+
"domains": {
|
|
7
|
+
"models": {
|
|
8
|
+
"models": [
|
|
9
|
+
{
|
|
10
|
+
"name": "keep",
|
|
11
|
+
"provider": "openai",
|
|
12
|
+
"id": "keep-id",
|
|
13
|
+
"apiKeyEnv": "OPENAI_API_KEY"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "Authorization: Bearer FAKE-S712-CORPUS-00000000",
|
|
17
|
+
"provider": "openai",
|
|
18
|
+
"id": "withheld-id",
|
|
19
|
+
"enabled": "yes",
|
|
20
|
+
"apiKeyEnv": "not an env name"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "off-model",
|
|
24
|
+
"provider": "openai",
|
|
25
|
+
"id": "off-id",
|
|
26
|
+
"enabled": false,
|
|
27
|
+
"apiKeyEnv": "not an env name"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "tok=Bearer abc1234",
|
|
31
|
+
"provider": "openai",
|
|
32
|
+
"id": "short-bearer-id",
|
|
33
|
+
"apiKeyEnv": "not an env name"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "-----BEGIN RSA PRIVATE KEY-----\nMIIEFAKES712CORPUSPEMBODY0000\n-----END RSA PRIVATE KEY-----",
|
|
37
|
+
"provider": "openai",
|
|
38
|
+
"id": "pem-id",
|
|
39
|
+
"apiKeyEnv": "not an env name"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"name": "prefixed-pem",
|
|
43
|
+
"provider": "openai",
|
|
44
|
+
"id": "key: -----BEGIN OPENSSH PRIVATE KEY-----\nMIIEFAKES712CODEXPREFIXED0000\n-----END OPENSSH PRIVATE KEY-----",
|
|
45
|
+
"apiKeyEnv": "not an env name"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "glued-pem",
|
|
49
|
+
"provider": "openai",
|
|
50
|
+
"id": "sk-note:-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwFAKES712GLUEDCORPUS0000000000\n-----END PRIVATE KEY-----",
|
|
51
|
+
"apiKeyEnv": "not an env name"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"roles": {
|
|
55
|
+
"summarize": {
|
|
56
|
+
"model": "keep"
|
|
57
|
+
},
|
|
58
|
+
"sk-note:-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwFAKES712GLUEDCORPUS0000000000\n-----END PRIVATE KEY-----": {
|
|
59
|
+
"model": "keep"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contributor": "contract",
|
|
3
|
+
"claim": "S-712 身份律的两道闸压在语料上:被丢条目的 name 是一段脱敏器会改动的串(Bearer 形)⇒ identity 不带 name(同一条告警里坐标会打码,身份不许把原文带出去);enabled 是字符串 \"yes\" ⇒ 不带 enabled;id 是干净字符串 ⇒ 照带。另两条被丢条目压脱敏判据的两道边:`tok=Bearer abc1234`(短令牌,探测器 containsSecretToken 不判阳、脱敏器会改)与一整段 PEM 私钥(头里带空格,脱敏器的定点两遍够不着、靠整串兜底)⇒ 两条的 name 都不带。正控:另一条被丢条目 name / id / enabled:false 三键齐全逐字带出。读腿若放宽任一闸,告警面多出字段即红;秘密清单同时由告警面零命中门看着。",
|
|
4
|
+
"firstRedOn": [],
|
|
5
|
+
"secrets": [
|
|
6
|
+
"FAKE-S712-CORPUS-00000000",
|
|
7
|
+
"abc1234",
|
|
8
|
+
"MIIEFAKES712CORPUSPEMBODY0000",
|
|
9
|
+
"MIIEFAKES712CODEXPREFIXED0000",
|
|
10
|
+
"MC4CAQAwBQYDK2VwFAKES712GLUEDCORPUS0000000000"
|
|
11
|
+
],
|
|
12
|
+
"note": "对抗轮 #5(车OD 2026-09-25):变异门 `test/conformance-mutation.test.ts` 为 readEntryIdentity 登记两靶(`redactSecrets(v) === v` → `true`、`typeof enabled === \"boolean\"` → `enabled !== void 0`),加本条之前两靶变异体全绿(语料缺口),加本条之后两靶各红。第三条律(只认数据属性、不触发访问器)JSON 表达不出 getter,不在本条射程内,由单测看着。firstRedOn 为空:旧版铸不出 identity,historical 归一器两侧剥掉 identity,本条在旧版上不计行为红 —— 它是护栏,不是真门。 验修轮 N4 / N1(车OD 2026-09-25):加入短令牌与 PEM 两条 —— 身份判据退回旧的 containsSecretToken 时短令牌那条带出 name ⇒ 本条红;redactSecrets 去掉整串兜底时 PEM 那条带出 name ⇒ 本条红。 codex 一审 [high](2026-09-25):再加一条 `id` = 带 `key: ` 前缀的 PEM 私钥(header 不在串首,探测器看不见)⇒ 身份只剩 name;redactSecrets 去掉「串内任意位置 PEM header」一道时本条红。 codex 验修轮 [high](2026-09-25):再加 `sk-note:` 紧贴私钥 PEM header 的串,既作被丢条目的 `id`(身份面)又作 `roles` 的坏键(坐标面 + zod error 面);redactSecrets 把 PEM 检查挪回替换之后时本条红(令牌那一遍先把 `sk-note:-----BEGIN` 换掉)。"
|
|
13
|
+
}
|
|
@@ -72,6 +72,10 @@
|
|
|
72
72
|
"warnings": [
|
|
73
73
|
{
|
|
74
74
|
"domain": "models",
|
|
75
|
+
"identity": {
|
|
76
|
+
"id": "café-id",
|
|
77
|
+
"name": "café"
|
|
78
|
+
},
|
|
75
79
|
"kind": "entry-dropped",
|
|
76
80
|
"path": [
|
|
77
81
|
"models",
|
|
@@ -80,6 +84,10 @@
|
|
|
80
84
|
},
|
|
81
85
|
{
|
|
82
86
|
"domain": "models",
|
|
87
|
+
"identity": {
|
|
88
|
+
"id": "café-id",
|
|
89
|
+
"name": "café"
|
|
90
|
+
},
|
|
83
91
|
"kind": "entry-dropped",
|
|
84
92
|
"path": [
|
|
85
93
|
"models",
|
|
@@ -85,6 +85,10 @@
|
|
|
85
85
|
"warnings": [
|
|
86
86
|
{
|
|
87
87
|
"domain": "models",
|
|
88
|
+
"identity": {
|
|
89
|
+
"id": "a\u0007b-id",
|
|
90
|
+
"name": "a\u0007b"
|
|
91
|
+
},
|
|
88
92
|
"kind": "entry-dropped",
|
|
89
93
|
"path": [
|
|
90
94
|
"models",
|
|
@@ -93,6 +97,10 @@
|
|
|
93
97
|
},
|
|
94
98
|
{
|
|
95
99
|
"domain": "models",
|
|
100
|
+
"identity": {
|
|
101
|
+
"id": "zerowidth-id",
|
|
102
|
+
"name": "zerowidth"
|
|
103
|
+
},
|
|
96
104
|
"kind": "entry-dropped",
|
|
97
105
|
"path": [
|
|
98
106
|
"models",
|
|
@@ -266,13 +266,13 @@ export declare const VerifiedIdentity: z.ZodObject<{
|
|
|
266
266
|
}, "strip", z.ZodTypeAny, {
|
|
267
267
|
principal: string;
|
|
268
268
|
role?: string | undefined;
|
|
269
|
-
scope?: string | undefined;
|
|
270
269
|
grantId?: string | undefined;
|
|
270
|
+
scope?: string | undefined;
|
|
271
271
|
}, {
|
|
272
272
|
principal: string;
|
|
273
273
|
role?: string | undefined;
|
|
274
|
-
scope?: string | undefined;
|
|
275
274
|
grantId?: string | undefined;
|
|
275
|
+
scope?: string | undefined;
|
|
276
276
|
}>;
|
|
277
277
|
export type VerifiedIdentity = z.infer<typeof VerifiedIdentity>;
|
|
278
278
|
/** The verifier's total result — exactly one of ok/error. */
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中心(config-center 产品)凭证的**文件形契约** —— S-698(design/325 §1)。
|
|
3
|
+
*
|
|
4
|
+
* 引擎向中心出站时用的凭证由部署声明(见 server `src/center-credential.ts`,单属主);声明成文件时
|
|
5
|
+
* (`SEMA_REGISTRY_TOKEN_FILE`),文件内容有两种**种类**,不是一种语义的两种拼法:
|
|
6
|
+
*
|
|
7
|
+
* · **裸行**(一个静态令牌,种类与今天相同):首行 = 静态 bearer,只收可打印 ASCII({@link TokenString} 的律)。它是用户 JWT 还是 worker
|
|
8
|
+
* 拉取令牌,由读方用 `looksLikeJwt`(`./api/auth-bridge`)自判 —— 本文件不重判种类。
|
|
9
|
+
* · **信封**(新):一个 JSON 对象,形 = {@link CenterCredentialEnvelope}。今只有一员 `kind:"user-session"`
|
|
10
|
+
* (人的登录会话:可轮换的访问令牌 + 单次有效的刷新令牌 + 到期时刻 + 签发它的中心根 URL)。worker 拉取
|
|
11
|
+
* 令牌**不进信封**:它不轮换、不属于人,保持裸行。
|
|
12
|
+
*
|
|
13
|
+
* 判别律(一条,{@link parseCenterCredentialFile} 是它的**唯一**实现):内容 `trim()` 后以 `{` 开头 ⇒ 只按
|
|
14
|
+
* 信封读,读不成 = `unreadable`(**不回落**成裸行 —— 一个 `{` 开头的裸 token 不存在,回落只会把一段坏 JSON
|
|
15
|
+
* 当 bearer 发出去);否则裸行;空 = `unreadable`。同一份内容只有一种读法(零双读)。
|
|
16
|
+
*
|
|
17
|
+
* 声明面(S-742,本文件尾段):两个声明键名 {@link CENTER_CREDENTIAL_ENV} 与二选一判官
|
|
18
|
+
* {@link judgeCenterCredentialDeclaration} —— 规则此前只活在 server,管理面 / 壳各自手抄键名。
|
|
19
|
+
*
|
|
20
|
+
* 纯契约:零 IO(读文件、写文件、续签都是消费方的事),browser-safe,与本包其余部分同一条纯度律。
|
|
21
|
+
*/
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
/** 信封 `kind` 的闭集今只有一员(`SEALED_BOX_ALG` 同式的字面常量:schema 的字面与读方的比较都引它)。 */
|
|
24
|
+
export declare const CENTER_CREDENTIAL_ENVELOPE_KIND: "user-session";
|
|
25
|
+
/**
|
|
26
|
+
* 中心凭证**信封**(`SEMA_REGISTRY_TOKEN_FILE` 指向的文件是一个 JSON 对象时的唯一形)。`.strict()`:
|
|
27
|
+
* 多一个键就不是这份契约(拼错的 `refresh_token` 被静默剥掉,续签就会拿着缺席的刷新令牌去撞中心)。
|
|
28
|
+
*
|
|
29
|
+
* · `accessToken` —— 出站 bearer(中心签的用户 JWT,形判 `looksLikeJwt`);读方拿它做身份分箱的方式与裸行
|
|
30
|
+
* `user-jwt` 逐字同。令牌形与裸行同一条律({@link TokenString})。
|
|
31
|
+
* · `refreshToken` —— 单次有效、严格轮换(`POST /api/v1/auth/token/refresh`,见 `./api/auth`);续签前**重读
|
|
32
|
+
* 文件**取当前值,不用内存旧值(另一个写者可能刚轮换过)。
|
|
33
|
+
* · `expiresAt` —— 访问令牌到期时刻(RFC 3339)。
|
|
34
|
+
* · `issuer` —— 签发它的中心的**基础 URL**(与读方 `configCenter.url` 同形,可带路径前缀):续签只向同源的中心发,
|
|
35
|
+
* 同不同源由读方比。URL 形判据与 `remoteExec` 的端点同一只 {@link NON_SECRET_URL}(http(s)、有 host、userinfo 与
|
|
36
|
+
* query / fragment 里不许夹带凭据)—— 本包不另写一份更窄的 URL 律。
|
|
37
|
+
*/
|
|
38
|
+
export declare const CenterCredentialEnvelope: z.ZodObject<{
|
|
39
|
+
kind: z.ZodLiteral<"user-session">;
|
|
40
|
+
accessToken: z.ZodEffects<z.ZodString, string, string>;
|
|
41
|
+
refreshToken: z.ZodString;
|
|
42
|
+
expiresAt: z.ZodString;
|
|
43
|
+
issuer: z.ZodEffects<z.ZodString, string, string>;
|
|
44
|
+
}, "strict", z.ZodTypeAny, {
|
|
45
|
+
kind: "user-session";
|
|
46
|
+
issuer: string;
|
|
47
|
+
accessToken: string;
|
|
48
|
+
refreshToken: string;
|
|
49
|
+
expiresAt: string;
|
|
50
|
+
}, {
|
|
51
|
+
kind: "user-session";
|
|
52
|
+
issuer: string;
|
|
53
|
+
accessToken: string;
|
|
54
|
+
refreshToken: string;
|
|
55
|
+
expiresAt: string;
|
|
56
|
+
}>;
|
|
57
|
+
export type CenterCredentialEnvelope = z.infer<typeof CenterCredentialEnvelope>;
|
|
58
|
+
/** {@link parseCenterCredentialFile} 的三态结果(判别键 `kind`)。`unreadable.reason` 只说**哪里**不对
|
|
59
|
+
* (信封字段名 / zod 问题码 / 未知键**个数**),从不回显文件内容 —— 文件里装的就是凭据。 */
|
|
60
|
+
export type CenterCredentialFile = {
|
|
61
|
+
kind: "bare";
|
|
62
|
+
bearer: string;
|
|
63
|
+
} | {
|
|
64
|
+
kind: "envelope";
|
|
65
|
+
envelope: CenterCredentialEnvelope;
|
|
66
|
+
} | {
|
|
67
|
+
kind: "unreadable";
|
|
68
|
+
reason: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* 凭证文件内容 → 三态({@link CenterCredentialFile})。判别律见模块头注,本函数是它的唯一实现:
|
|
72
|
+
*
|
|
73
|
+
* 1. `trim()`(含 BOM);空 ⇒ `unreadable("empty")`。
|
|
74
|
+
* 2. 以 `{` 开头 ⇒ 只按 {@link CenterCredentialEnvelope} 读:JSON 不成立或 schema 不过 ⇒ `unreadable`
|
|
75
|
+
* (**不回落**裸行)。
|
|
76
|
+
* 3. 否则 ⇒ 首行(第一个 CR 或 LF 之前,再剥一次空白)。它是令牌({@link TokenString} 的律)⇒ `bare`;
|
|
77
|
+
* 不是 ⇒ `unreadable`(放不进 Authorization 头的行不是 bearer)。多行文件只认首行。
|
|
78
|
+
*
|
|
79
|
+
* 纯函数、零 IO;读方负责读文件(以及文件不可读时自己的那条 `unreadable` 臂)。
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseCenterCredentialFile(text: string): CenterCredentialFile;
|
|
82
|
+
/**
|
|
83
|
+
* 中心凭证的两个**声明键名**(部署 env)。一处定义、三方 import(server 读、管理面给 worker 渲染、壳给本机
|
|
84
|
+
* 引擎写)—— 键名是 wire 面,手抄的键名会静默漂。
|
|
85
|
+
*
|
|
86
|
+
* · `token`(`SEMA_REGISTRY_TOKEN`)—— 值即凭证(部署注入;读方在进程生命期内读一次)。
|
|
87
|
+
* · `tokenFile`(`SEMA_REGISTRY_TOKEN_FILE`)—— 凭证文件的路径,读方**每次取值都读文件**(轮换 / 换账号只需
|
|
88
|
+
* 改写文件,不重启);文件内容的读法见 {@link parseCenterCredentialFile}。
|
|
89
|
+
*/
|
|
90
|
+
export declare const CENTER_CREDENTIAL_ENV: {
|
|
91
|
+
readonly token: "SEMA_REGISTRY_TOKEN";
|
|
92
|
+
readonly tokenFile: "SEMA_REGISTRY_TOKEN_FILE";
|
|
93
|
+
};
|
|
94
|
+
/** {@link judgeCenterCredentialDeclaration} 的结果:凭证在哪里声明。`env` 形带**值** —— 它就是凭据,只在读方
|
|
95
|
+
* 自己的凭证模块里流转,不进日志。 */
|
|
96
|
+
export type CenterCredentialDeclaration = {
|
|
97
|
+
from: "env";
|
|
98
|
+
token: string;
|
|
99
|
+
} | {
|
|
100
|
+
from: "file";
|
|
101
|
+
path: string;
|
|
102
|
+
} | {
|
|
103
|
+
from: "absent";
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* 读两个声明键,判凭证在哪里声明 —— 二选一规则的**唯一**实现:
|
|
107
|
+
*
|
|
108
|
+
* · 空串 = 未设(`undefined` 同);其余一律算「设了」(不 trim —— 与规则上提前 server 的读法逐字同)。
|
|
109
|
+
* · 两个都设 ⇒ **throw**(读方拒启):两个写者写同一个语义面,哪个赢都是猜。拒句点名两个键、不回显值。
|
|
110
|
+
* · 只设 `tokenFile` ⇒ `{from:"file", path}`,`path` **原样**:相对路径按 cwd 解析是读方的事(纯契约不碰
|
|
111
|
+
* `node:path`,也不知道读方的 cwd)。
|
|
112
|
+
* · 只设 `token` ⇒ `{from:"env", token}`;都不设 ⇒ `{from:"absent"}`。
|
|
113
|
+
*
|
|
114
|
+
* 只读这两个键,别的 env 一概不看。
|
|
115
|
+
*/
|
|
116
|
+
export declare function judgeCenterCredentialDeclaration(env: Readonly<Record<string, string | undefined>>): CenterCredentialDeclaration;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 中心(config-center 产品)凭证的**文件形契约** —— S-698(design/325 §1)。
|
|
3
|
+
*
|
|
4
|
+
* 引擎向中心出站时用的凭证由部署声明(见 server `src/center-credential.ts`,单属主);声明成文件时
|
|
5
|
+
* (`SEMA_REGISTRY_TOKEN_FILE`),文件内容有两种**种类**,不是一种语义的两种拼法:
|
|
6
|
+
*
|
|
7
|
+
* · **裸行**(一个静态令牌,种类与今天相同):首行 = 静态 bearer,只收可打印 ASCII({@link TokenString} 的律)。它是用户 JWT 还是 worker
|
|
8
|
+
* 拉取令牌,由读方用 `looksLikeJwt`(`./api/auth-bridge`)自判 —— 本文件不重判种类。
|
|
9
|
+
* · **信封**(新):一个 JSON 对象,形 = {@link CenterCredentialEnvelope}。今只有一员 `kind:"user-session"`
|
|
10
|
+
* (人的登录会话:可轮换的访问令牌 + 单次有效的刷新令牌 + 到期时刻 + 签发它的中心根 URL)。worker 拉取
|
|
11
|
+
* 令牌**不进信封**:它不轮换、不属于人,保持裸行。
|
|
12
|
+
*
|
|
13
|
+
* 判别律(一条,{@link parseCenterCredentialFile} 是它的**唯一**实现):内容 `trim()` 后以 `{` 开头 ⇒ 只按
|
|
14
|
+
* 信封读,读不成 = `unreadable`(**不回落**成裸行 —— 一个 `{` 开头的裸 token 不存在,回落只会把一段坏 JSON
|
|
15
|
+
* 当 bearer 发出去);否则裸行;空 = `unreadable`。同一份内容只有一种读法(零双读)。
|
|
16
|
+
*
|
|
17
|
+
* 声明面(S-742,本文件尾段):两个声明键名 {@link CENTER_CREDENTIAL_ENV} 与二选一判官
|
|
18
|
+
* {@link judgeCenterCredentialDeclaration} —— 规则此前只活在 server,管理面 / 壳各自手抄键名。
|
|
19
|
+
*
|
|
20
|
+
* 纯契约:零 IO(读文件、写文件、续签都是消费方的事),browser-safe,与本包其余部分同一条纯度律。
|
|
21
|
+
*/
|
|
22
|
+
import { z } from "zod";
|
|
23
|
+
import { looksLikeJwt } from "./api/auth-bridge.js";
|
|
24
|
+
import { NON_SECRET_URL } from "./types.js";
|
|
25
|
+
/** 信封 `kind` 的闭集今只有一员(`SEALED_BOX_ALG` 同式的字面常量:schema 的字面与读方的比较都引它)。 */
|
|
26
|
+
export const CENTER_CREDENTIAL_ENVELOPE_KIND = "user-session";
|
|
27
|
+
/** RFC 3339 `date-time` 的**剖面**:zod 的 `datetime({offset:true})` 管日历有效性(月日范围、闰年、时分秒范围),
|
|
28
|
+
* 这里再补它放过的三处 —— 秒必备、数字偏移必须是 `±hh:mm`、偏移的时 00–23 / 分 00–59(zod 也收 `10:00Z`、
|
|
29
|
+
* `+0800` 与 `+99:99`,都不是 RFC 3339,后者 `Date.parse` 读成 NaN)。大写 `T` / `Z`、不收闰秒 `:60`:读方普遍用
|
|
30
|
+
* `Date.parse` 取时刻,这是那组形里它处处读得准的子集。 */
|
|
31
|
+
const RFC3339_DATE_TIME = z
|
|
32
|
+
.string()
|
|
33
|
+
.datetime({ offset: true })
|
|
34
|
+
.regex(/T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/, "must be an RFC 3339 date-time: seconds and a Z / ±hh:mm offset (hh 00-23, mm 00-59) are required");
|
|
35
|
+
/** 一枚**令牌串**的形 —— 裸行 bearer、信封里的 `accessToken` / `refreshToken` 共用这**一条**律:可打印 ASCII
|
|
36
|
+
* (0x20–0x7E),首尾是可见字符(0x21–0x7E)、中间可有空格(共享令牌可以是一句口令 —— 验修轮 N2:今天走得通的
|
|
37
|
+
* 这种令牌不许被本律拒掉)。控制字符、制表、非 ASCII、不可见字符一律不是令牌:判别器把它判 `unreadable`,而不是
|
|
38
|
+
* 发出去 —— 更不会把一段前面粘了不可见字符的信封 JSON 当 bearer 送出门(对抗轮 #7)。刻意比 RFC 6750 `b64token`
|
|
39
|
+
* 宽:部署手写的静态令牌可以带 `!` / `%` / 空格之类。**如实登记**:Latin-1 令牌(HTTP 头的 ByteString 收、中心也
|
|
40
|
+
* 逐字比得上)在这里判 `unreadable` —— 非 ASCII 与不可见字符(`\u0085` / `\u00AD`)分不开,取 fail-closed。 */
|
|
41
|
+
const TOKEN_CHARS = /^[\x21-\x7E](?:[\x20-\x7E]*[\x21-\x7E])?$/;
|
|
42
|
+
const TokenString = z.string().regex(TOKEN_CHARS, "must be a token: printable ASCII, no leading / trailing space, no control or non-ASCII characters");
|
|
43
|
+
/**
|
|
44
|
+
* 中心凭证**信封**(`SEMA_REGISTRY_TOKEN_FILE` 指向的文件是一个 JSON 对象时的唯一形)。`.strict()`:
|
|
45
|
+
* 多一个键就不是这份契约(拼错的 `refresh_token` 被静默剥掉,续签就会拿着缺席的刷新令牌去撞中心)。
|
|
46
|
+
*
|
|
47
|
+
* · `accessToken` —— 出站 bearer(中心签的用户 JWT,形判 `looksLikeJwt`);读方拿它做身份分箱的方式与裸行
|
|
48
|
+
* `user-jwt` 逐字同。令牌形与裸行同一条律({@link TokenString})。
|
|
49
|
+
* · `refreshToken` —— 单次有效、严格轮换(`POST /api/v1/auth/token/refresh`,见 `./api/auth`);续签前**重读
|
|
50
|
+
* 文件**取当前值,不用内存旧值(另一个写者可能刚轮换过)。
|
|
51
|
+
* · `expiresAt` —— 访问令牌到期时刻(RFC 3339)。
|
|
52
|
+
* · `issuer` —— 签发它的中心的**基础 URL**(与读方 `configCenter.url` 同形,可带路径前缀):续签只向同源的中心发,
|
|
53
|
+
* 同不同源由读方比。URL 形判据与 `remoteExec` 的端点同一只 {@link NON_SECRET_URL}(http(s)、有 host、userinfo 与
|
|
54
|
+
* query / fragment 里不许夹带凭据)—— 本包不另写一份更窄的 URL 律。
|
|
55
|
+
*/
|
|
56
|
+
export const CenterCredentialEnvelope = z
|
|
57
|
+
.object({
|
|
58
|
+
kind: z.literal(CENTER_CREDENTIAL_ENVELOPE_KIND),
|
|
59
|
+
accessToken: TokenString.refine(looksLikeJwt, "must be a JWT (three dot-separated segments) — a worker pull token never goes into an envelope"),
|
|
60
|
+
refreshToken: TokenString,
|
|
61
|
+
expiresAt: RFC3339_DATE_TIME,
|
|
62
|
+
issuer: NON_SECRET_URL,
|
|
63
|
+
})
|
|
64
|
+
.strict();
|
|
65
|
+
/** 信封自己的字段名(schema 的键 —— reason 里只许出现这几个名字,不许出现用户写的键)。 */
|
|
66
|
+
const ENVELOPE_FIELDS = new Set(Object.keys(CenterCredentialEnvelope.shape));
|
|
67
|
+
/** 一次 schema 失败 → 不含文件内容的一句话。 */
|
|
68
|
+
function envelopeRejection(error) {
|
|
69
|
+
const fields = new Set(); // 一个字段挂两道判据(datetime + 剖面正则)时只说一次
|
|
70
|
+
let unknownKeys = 0;
|
|
71
|
+
for (const issue of error.issues) {
|
|
72
|
+
if (issue.code === z.ZodIssueCode.unrecognized_keys) {
|
|
73
|
+
unknownKeys += issue.keys.length;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const head = issue.path[0];
|
|
77
|
+
const field = typeof head === "string" && ENVELOPE_FIELDS.has(head) ? head : "(envelope)";
|
|
78
|
+
fields.add(`${field} (${issue.code})`);
|
|
79
|
+
}
|
|
80
|
+
const parts = [...(fields.size > 0 ? [`fields rejected: ${[...fields].join(", ")}`] : []), ...(unknownKeys > 0 ? [`unknown keys: ${unknownKeys}`] : [])];
|
|
81
|
+
return `envelope does not match CenterCredentialEnvelope — ${parts.join("; ")}`;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 凭证文件内容 → 三态({@link CenterCredentialFile})。判别律见模块头注,本函数是它的唯一实现:
|
|
85
|
+
*
|
|
86
|
+
* 1. `trim()`(含 BOM);空 ⇒ `unreadable("empty")`。
|
|
87
|
+
* 2. 以 `{` 开头 ⇒ 只按 {@link CenterCredentialEnvelope} 读:JSON 不成立或 schema 不过 ⇒ `unreadable`
|
|
88
|
+
* (**不回落**裸行)。
|
|
89
|
+
* 3. 否则 ⇒ 首行(第一个 CR 或 LF 之前,再剥一次空白)。它是令牌({@link TokenString} 的律)⇒ `bare`;
|
|
90
|
+
* 不是 ⇒ `unreadable`(放不进 Authorization 头的行不是 bearer)。多行文件只认首行。
|
|
91
|
+
*
|
|
92
|
+
* 纯函数、零 IO;读方负责读文件(以及文件不可读时自己的那条 `unreadable` 臂)。
|
|
93
|
+
*/
|
|
94
|
+
export function parseCenterCredentialFile(text) {
|
|
95
|
+
const trimmed = text.trim();
|
|
96
|
+
if (trimmed === "")
|
|
97
|
+
return { kind: "unreadable", reason: "empty" };
|
|
98
|
+
if (trimmed.startsWith("{")) {
|
|
99
|
+
let json;
|
|
100
|
+
try {
|
|
101
|
+
json = JSON.parse(trimmed);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// JSON.parse 的报错会引用出错位置附近的原文 —— 不转述它。
|
|
105
|
+
return { kind: "unreadable", reason: "envelope is not valid JSON" };
|
|
106
|
+
}
|
|
107
|
+
const parsed = CenterCredentialEnvelope.safeParse(json);
|
|
108
|
+
return parsed.success ? { kind: "envelope", envelope: parsed.data } : { kind: "unreadable", reason: envelopeRejection(parsed.error) };
|
|
109
|
+
}
|
|
110
|
+
const firstLine = trimmed.split(/[\r\n]/, 1)[0].trim(); // 行尾 = 第一个 CR 或 LF(CRLF / LF / 孤 CR 同判)
|
|
111
|
+
if (!TOKEN_CHARS.test(firstLine))
|
|
112
|
+
return { kind: "unreadable", reason: "bare line is not a token — printable ASCII only (no control or non-ASCII characters)" };
|
|
113
|
+
return { kind: "bare", bearer: firstLine };
|
|
114
|
+
}
|
|
115
|
+
// ── S-742:凭证的两个声明键 + 二选一判官 ─────────────────────────────────────────────────────────────
|
|
116
|
+
/**
|
|
117
|
+
* 中心凭证的两个**声明键名**(部署 env)。一处定义、三方 import(server 读、管理面给 worker 渲染、壳给本机
|
|
118
|
+
* 引擎写)—— 键名是 wire 面,手抄的键名会静默漂。
|
|
119
|
+
*
|
|
120
|
+
* · `token`(`SEMA_REGISTRY_TOKEN`)—— 值即凭证(部署注入;读方在进程生命期内读一次)。
|
|
121
|
+
* · `tokenFile`(`SEMA_REGISTRY_TOKEN_FILE`)—— 凭证文件的路径,读方**每次取值都读文件**(轮换 / 换账号只需
|
|
122
|
+
* 改写文件,不重启);文件内容的读法见 {@link parseCenterCredentialFile}。
|
|
123
|
+
*/
|
|
124
|
+
export const CENTER_CREDENTIAL_ENV = { token: "SEMA_REGISTRY_TOKEN", tokenFile: "SEMA_REGISTRY_TOKEN_FILE" };
|
|
125
|
+
/**
|
|
126
|
+
* 读两个声明键,判凭证在哪里声明 —— 二选一规则的**唯一**实现:
|
|
127
|
+
*
|
|
128
|
+
* · 空串 = 未设(`undefined` 同);其余一律算「设了」(不 trim —— 与规则上提前 server 的读法逐字同)。
|
|
129
|
+
* · 两个都设 ⇒ **throw**(读方拒启):两个写者写同一个语义面,哪个赢都是猜。拒句点名两个键、不回显值。
|
|
130
|
+
* · 只设 `tokenFile` ⇒ `{from:"file", path}`,`path` **原样**:相对路径按 cwd 解析是读方的事(纯契约不碰
|
|
131
|
+
* `node:path`,也不知道读方的 cwd)。
|
|
132
|
+
* · 只设 `token` ⇒ `{from:"env", token}`;都不设 ⇒ `{from:"absent"}`。
|
|
133
|
+
*
|
|
134
|
+
* 只读这两个键,别的 env 一概不看。
|
|
135
|
+
*/
|
|
136
|
+
export function judgeCenterCredentialDeclaration(env) {
|
|
137
|
+
const { token: tokenKey, tokenFile: fileKey } = CENTER_CREDENTIAL_ENV;
|
|
138
|
+
const token = env[tokenKey] ?? "";
|
|
139
|
+
const file = env[fileKey] ?? "";
|
|
140
|
+
if (token !== "" && file !== "") {
|
|
141
|
+
throw new Error(`${tokenKey} and ${fileKey} are both set — pick exactly one: ${tokenKey} carries the credential value, ` +
|
|
142
|
+
`${fileKey} names a file re-read on every request (rotation without restart). Two writers on one credential is a guess, not a config.`);
|
|
143
|
+
}
|
|
144
|
+
if (file !== "")
|
|
145
|
+
return { from: "file", path: file };
|
|
146
|
+
if (token !== "")
|
|
147
|
+
return { from: "env", token };
|
|
148
|
+
return { from: "absent" };
|
|
149
|
+
}
|
package/dist/config-fns.d.ts
CHANGED
|
@@ -19,7 +19,17 @@ import { type DomainConfig, type DomainName, type EffectiveConfig, type Effectiv
|
|
|
19
19
|
* `received 'sk-live-…'`), so a secret pasted into the WRONG field would leak via the error string. Redacts
|
|
20
20
|
* space-spanning auth credentials (`Bearer <token>`) FIRST (the token pass can't see across the space), then
|
|
21
21
|
* any secret-shaped run (`%` kept in the run so a percent-encoded secret stays whole for the percent-aware
|
|
22
|
-
* containsSecretToken). invariant ①: a secret VALUE never rides an error. R13/R14.
|
|
22
|
+
* containsSecretToken). invariant ①: a secret VALUE never rides an error. R13/R14.
|
|
23
|
+
*
|
|
24
|
+
* **Output invariant** (S-712 verification pass N1 + codex review [high] ×2): a string holding a private-key PEM
|
|
25
|
+
* header ANYWHERE ({@link PRIVATE_KEY_PEM_HEADER}) is withheld WHOLE, and no result trips `containsSecretToken`.
|
|
26
|
+
* The PEM header is judged on the ORIGINAL text, before any replacement: the token pass can rewrite the header
|
|
27
|
+
* itself away (`sk-note:-----BEGIN` is one token-charset run that starts with `sk-`, so it becomes
|
|
28
|
+
* `[redacted-secret]` and the key body after it would ride out untouched — codex verification round). A PEM header
|
|
29
|
+
* carries spaces, so the targeted passes can never isolate it, and the detector only sees it at the very start of
|
|
30
|
+
* a string. Every warning face that goes through here (coordinates, error text, entry identity) inherits the same
|
|
31
|
+
* guarantee. The validation-side detector `containsSecretToken` is deliberately NOT widened here: making it reject
|
|
32
|
+
* a mid-string PEM would tighten every field it validates (a major). */
|
|
23
33
|
export declare function redactSecrets(text: string): string;
|
|
24
34
|
/** Sanitize a ZodError IN PLACE so no surfaced error (message / `received` / nested `unionErrors`) echoes a
|
|
25
35
|
* pasted secret. RECURSES into `unionErrors` (a `z.union` failure nests a ZodError per member — e.g. RoleTarget
|
|
@@ -151,12 +161,48 @@ export type ConfigPath = readonly (string | number)[];
|
|
|
151
161
|
* **刻意不提供 parse 反函数**:2.1.0 起没有任何一处把坐标读回来(回写走 `keptIndex` 那本账),给它
|
|
152
162
|
* 一个反函数就是给下一个人一条「在串上做事」的路。 */
|
|
153
163
|
export declare function formatConfigPath(p: ConfigPath): string;
|
|
164
|
+
/** 一条被丢掉的条目的**身份**(S-712):只从被丢条目**原文**里取,三键各自可缺。
|
|
165
|
+
*
|
|
166
|
+
* 律(一条,不分域):`name` / `id` 取**字符串**形、`enabled` 取**布尔**形,类型不对 ⇒ 不带该键(不猜、
|
|
167
|
+
* 不 `String()` 转换);只认**数据属性**(不触发访问器);{@link redactSecrets} 会改动的串 ⇒ 不带该键 ——
|
|
168
|
+
* 与坐标段同一只脱敏判据:坐标段必须在场,所以打码出门;身份是可缺的,打过码的串不再是任何条目的身份,
|
|
169
|
+
* 所以是**不带**而不是打码。三键都读不出 ⇒ 整个身份缺席(不铸空对象)。
|
|
170
|
+
*
|
|
171
|
+
* **缺席 = 读不出**(不是字符串,或是字符串但不能原样出门),两种原因刻意不分:对消费方的正确处置相同
|
|
172
|
+
* (身份未知 ⇒ 按未知处置,例如 fail-closed),而一个「被扣下」的标记本身就在说「这里有一段像凭据的东西」。
|
|
173
|
+
*
|
|
174
|
+
* 为什么要带:条目被丢之后它的名字从目录里消失,只给坐标的话,消费方要知道「丢的是谁」只能拿坐标回读
|
|
175
|
+
* 原文档 —— 而它手里那份原文档未必是这一次读到的那份。铸告警的这一层判的就是那条原文,在这里取一次、
|
|
176
|
+
* 随告警走,是唯一不会读错版本的地方。停用条目(`enabled:false`)照样带身份:判不判墓碑是消费方的事。 */
|
|
177
|
+
export interface DroppedEntryIdentity {
|
|
178
|
+
name?: string;
|
|
179
|
+
id?: string;
|
|
180
|
+
enabled?: boolean;
|
|
181
|
+
}
|
|
154
182
|
/** 一条被丢掉的条目:`path` = 段坐标({@link ConfigPath};`["models", 3]` / `["roles", "summarize"]`),
|
|
155
|
-
* `error`
|
|
183
|
+
* `error` 已脱敏,`identity`(S-712)见 {@link DroppedEntryIdentity}(读不出 ⇒ 键缺席)。 */
|
|
156
184
|
export interface DroppedEntry {
|
|
157
185
|
path: ConfigPath;
|
|
158
186
|
error: unknown;
|
|
187
|
+
identity?: DroppedEntryIdentity;
|
|
159
188
|
}
|
|
189
|
+
/** 身份串 —— 「一个用户写的身份串(条目的 `name` / `id`)能不能**原样**出门」的**唯一**判据:
|
|
190
|
+
* {@link redactSecrets} 原样放行。入参是 `string`、返回 `boolean`,**刻意不是**类型谓词(`v is string`):它对一些
|
|
191
|
+
* 字符串答 false,谓词形会让调用方的否定分支把一个 `string` 收窄成 `never`;「是不是字符串」由调用点自己判
|
|
192
|
+
* (运行期收到非字符串照答 false、不抛)。秘密形的串按**无名**处置(不带,而不是打码:打过码的串不再是身份)。
|
|
193
|
+
* 与坐标段脱敏、`error` 文案脱敏({@link sanitizeZodError})**同一只**判据:告警面上任何一处用户写的串,要么过这一只
|
|
194
|
+
* 脱敏器出门,要么(可缺的身份)不出门。两只判据会在同一条告警里给出两种答案(对抗轮 #2 真复现:`Bearer` 后接 NBSP
|
|
195
|
+
* 或短令牌时,旧判据放行、坐标却打码)。
|
|
196
|
+
* 执法面(同一条律):被丢条目的身份({@link DroppedEntryIdentity},本文件铸);`ModelRefusalsWire` 的 `entries` 键
|
|
197
|
+
* (经 {@link isReportableRefusalName},S-768);引擎自己的日志 / 拒因句里要不要写出一个名字,同样问这一只。 */
|
|
198
|
+
export declare function isDisclosableIdentity(v: string): boolean;
|
|
199
|
+
/** S-768 —— 「一个目录条目名能不能当 {@link ModelRefusalsWire} `entries` 的键」的**唯一**判据;引擎铸键前只调这一只,
|
|
200
|
+
* 不过 ⇒ 按无名处置(不进键、不计数)。两条律的合取,不手抄任何一条:
|
|
201
|
+
* ① 这只 schema **自己载得住**这个键 —— 就地拿 schema 判(空串不过键规则;`__proto__` 这类名被 zod 的 record 静默丢掉,
|
|
202
|
+
* 载不住),不写名单;
|
|
203
|
+
* ② {@link isDisclosableIdentity}(秘密形 ⇒ 不出门)。
|
|
204
|
+
* 非字符串 ⇒ false。住在本文件而不是 `./fleet`:`fleet.ts` 被 `types.ts` 引,它再引本文件会成环。 */
|
|
205
|
+
export declare function isReportableRefusalName(name: string): boolean;
|
|
160
206
|
/** 抢救造成的**下标位移表**:数组字段名 → 该字段压缩后每个位置的**原**下标(`kept[j]` = 压缩后第 j 条
|
|
161
207
|
* 在原文档里的下标)。纯数据,无行为。
|
|
162
208
|
*
|
|
@@ -282,12 +328,15 @@ export type EffectiveReadWarning = {
|
|
|
282
328
|
/** 一条**条目**没通过它自己的 entry schema,被单独丢掉了(域内其余条目照常生效)。`path` = 段坐标
|
|
283
329
|
* (`["models",3]` / `["roles","summarize"]`),与 `unknown-keys-dropped` 的坐标形同一套。逐条一发 —— 载荷是
|
|
284
330
|
* O(域里的条目数),与被读文档同阶,不放大,故不设条数上限(有上限的是**单条**告警里的键列表)。
|
|
285
|
-
* 2.0.0 起;只可能来自 {@link DOMAIN_READ_FALLBACK} 判 `"default"` 的域,gate 域坏形仍然 throw。
|
|
331
|
+
* 2.0.0 起;只可能来自 {@link DOMAIN_READ_FALLBACK} 判 `"default"` 的域,gate 域坏形仍然 throw。
|
|
332
|
+
* **S-712**(additive):`identity` = 被丢条目原文里读得出的身份({@link DroppedEntryIdentity};
|
|
333
|
+
* 读不出 ⇒ 键缺席),消费方据此知道「丢的是谁」而不必拿坐标回读原文档。 */
|
|
286
334
|
| {
|
|
287
335
|
domain: DomainName;
|
|
288
336
|
kind: "entry-dropped";
|
|
289
337
|
path: ConfigPath;
|
|
290
338
|
error: unknown;
|
|
339
|
+
identity?: DroppedEntryIdentity;
|
|
291
340
|
}
|
|
292
341
|
/** `config.d/` 里存在、但本地读取集**永远不会读**的文件(名字不是域,或是域但不便携)。 */
|
|
293
342
|
| {
|