@sema-agent/settings-schema 14.6.0 → 14.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +266 -0
- package/README.md +31 -3
- package/conformance/VERSION +1 -1
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
- package/dist/api/auth-bridge.d.ts +16 -2
- package/dist/api/auth-bridge.js +59 -24
- package/dist/api/credentials.d.ts +281 -0
- package/dist/api/credentials.js +149 -0
- package/dist/api/scopes.js +5 -1
- package/dist/config-d-file.js +33 -15
- package/dist/config-fns.js +142 -78
- package/dist/conformance/canonical.js +4 -4
- package/dist/conformance/contract-adapter.js +26 -1
- package/dist/conformance/corpus.js +4 -4
- package/dist/conformance/derive.js +2 -2
- package/dist/conformance/run.js +43 -6
- package/dist/cross-session.d.ts +4 -4
- package/dist/declared-capture.d.ts +41 -0
- package/dist/declared-capture.js +344 -0
- package/dist/file-edit.js +11 -15
- package/dist/fleet.d.ts +11 -0
- package/dist/fleet.js +59 -26
- package/dist/hash.d.ts +0 -9
- package/dist/hash.js +64 -7
- package/dist/hooks-config.js +18 -3
- package/dist/http-hook-allowlists.d.ts +22 -9
- package/dist/http-hook-allowlists.js +169 -91
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp-request-entry.d.ts +9 -7
- package/dist/mcp-request-entry.js +89 -82
- package/dist/one-time-migrations.js +207 -95
- package/dist/own-key-read.d.ts +247 -0
- package/dist/own-key-read.js +449 -0
- package/dist/plain-json-object.d.ts +11 -1
- package/dist/plain-json-object.js +37 -1
- package/dist/remote-exec.js +9 -1
- package/dist/safety-merge-spec.js +50 -37
- package/dist/sandbox-git.d.ts +64 -0
- package/dist/sandbox-git.js +89 -0
- package/dist/scheduler-store.js +19 -11
- package/dist/settings-units.js +14 -12
- package/dist/types.d.ts +6 -2
- package/dist/types.js +11 -6
- package/dist/url-shape.d.ts +16 -0
- package/dist/url-shape.js +18 -0
- package/dist/web-admin-credential.js +5 -2
- package/dist/web-admin-session-url.js +2 -2
- package/dist/worker-row-verdict.d.ts +11 -0
- package/dist/worker-row-verdict.js +51 -1
- package/dist/workers-rows.js +38 -10
- package/dist/zod-unwrap.d.ts +6 -0
- package/dist/zod-unwrap.js +14 -7
- package/docs/API-SURFACE.md +35 -4
- package/docs/CONSUMER-INTEGRATION.md +178 -0
- package/llms-full.txt +23 -4
- package/llms.txt +3 -2
- package/package.json +1 -1
- package/dist/definition-copy.d.ts +0 -23
- package/dist/definition-copy.js +0 -52
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"domains": {
|
|
3
|
+
"outcome": "effective",
|
|
4
|
+
"effective": {
|
|
5
|
+
"collab": {
|
|
6
|
+
"templates": []
|
|
7
|
+
},
|
|
8
|
+
"entitlement": {
|
|
9
|
+
"budgetGroups": [],
|
|
10
|
+
"groupBindings": [],
|
|
11
|
+
"subjects": [],
|
|
12
|
+
"tiers": []
|
|
13
|
+
},
|
|
14
|
+
"execution": {
|
|
15
|
+
"allowedLanes": [
|
|
16
|
+
"host"
|
|
17
|
+
],
|
|
18
|
+
"required": false
|
|
19
|
+
},
|
|
20
|
+
"governance": {},
|
|
21
|
+
"hosts": {
|
|
22
|
+
"hosts": []
|
|
23
|
+
},
|
|
24
|
+
"limits": {},
|
|
25
|
+
"mcp": {
|
|
26
|
+
"servers": []
|
|
27
|
+
},
|
|
28
|
+
"models": {
|
|
29
|
+
"atMention": {
|
|
30
|
+
"mode": "all"
|
|
31
|
+
},
|
|
32
|
+
"models": [],
|
|
33
|
+
"roles": {},
|
|
34
|
+
"tierGroups": []
|
|
35
|
+
},
|
|
36
|
+
"plugins": {
|
|
37
|
+
"plugins": []
|
|
38
|
+
},
|
|
39
|
+
"projects": {
|
|
40
|
+
"projects": {}
|
|
41
|
+
},
|
|
42
|
+
"prompts": {},
|
|
43
|
+
"rosters": {
|
|
44
|
+
"rosters": []
|
|
45
|
+
},
|
|
46
|
+
"scenarios": {
|
|
47
|
+
"scenarios": []
|
|
48
|
+
},
|
|
49
|
+
"skills": {
|
|
50
|
+
"skills": []
|
|
51
|
+
},
|
|
52
|
+
"systems": {
|
|
53
|
+
"systems": []
|
|
54
|
+
},
|
|
55
|
+
"workers": {
|
|
56
|
+
"workers": []
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"warnings": [],
|
|
60
|
+
"metadata": {
|
|
61
|
+
"version": 7,
|
|
62
|
+
"updatedAt": "2026-10-23T00:00:00.000Z"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"strictWrite": {
|
|
66
|
+
"outcome": "effective",
|
|
67
|
+
"effective": {},
|
|
68
|
+
"warnings": [],
|
|
69
|
+
"metadata": {
|
|
70
|
+
"version": 7,
|
|
71
|
+
"updatedAt": "2026-10-23T00:00:00.000Z"
|
|
72
|
+
},
|
|
73
|
+
"why": "受理空集:本条打的是 wire 腿(提交的域文档集为空)。"
|
|
74
|
+
},
|
|
75
|
+
"wire": {
|
|
76
|
+
"outcome": "effective",
|
|
77
|
+
"effective": {
|
|
78
|
+
"collab": {
|
|
79
|
+
"templates": []
|
|
80
|
+
},
|
|
81
|
+
"entitlement": {
|
|
82
|
+
"budgetGroups": [],
|
|
83
|
+
"groupBindings": [],
|
|
84
|
+
"subjects": [],
|
|
85
|
+
"tiers": []
|
|
86
|
+
},
|
|
87
|
+
"execution": {
|
|
88
|
+
"allowedLanes": [
|
|
89
|
+
"host"
|
|
90
|
+
],
|
|
91
|
+
"required": false
|
|
92
|
+
},
|
|
93
|
+
"governance": {},
|
|
94
|
+
"hosts": {
|
|
95
|
+
"hosts": []
|
|
96
|
+
},
|
|
97
|
+
"limits": {},
|
|
98
|
+
"mcp": {
|
|
99
|
+
"servers": []
|
|
100
|
+
},
|
|
101
|
+
"models": {
|
|
102
|
+
"atMention": {
|
|
103
|
+
"mode": "all"
|
|
104
|
+
},
|
|
105
|
+
"models": [],
|
|
106
|
+
"roles": {},
|
|
107
|
+
"tierGroups": []
|
|
108
|
+
},
|
|
109
|
+
"plugins": {
|
|
110
|
+
"plugins": []
|
|
111
|
+
},
|
|
112
|
+
"projects": {
|
|
113
|
+
"projects": {}
|
|
114
|
+
},
|
|
115
|
+
"rosters": {
|
|
116
|
+
"rosters": []
|
|
117
|
+
},
|
|
118
|
+
"sandboxGit": {
|
|
119
|
+
"origins": [
|
|
120
|
+
{
|
|
121
|
+
"connectUrl": "https://console.example.com/connect/git",
|
|
122
|
+
"origin": "https://git.example.com:9999"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"connectUrl": "https://console.example.com/connect/git",
|
|
126
|
+
"origin": "http://git.legacy.example:8080"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
"scenarios": {
|
|
131
|
+
"scenarios": []
|
|
132
|
+
},
|
|
133
|
+
"skills": {
|
|
134
|
+
"skills": []
|
|
135
|
+
},
|
|
136
|
+
"systems": {
|
|
137
|
+
"systems": []
|
|
138
|
+
},
|
|
139
|
+
"workers": {
|
|
140
|
+
"workers": []
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"warnings": [],
|
|
144
|
+
"metadata": {
|
|
145
|
+
"version": 7,
|
|
146
|
+
"updatedAt": "2026-10-23T00:00:00.000Z"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"localRoot": {
|
|
150
|
+
"derive": true,
|
|
151
|
+
"localVersion": 2617555914511015
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"version": 7,
|
|
4
|
+
"updatedAt": "2026-10-23T00:00:00.000Z"
|
|
5
|
+
},
|
|
6
|
+
"domains": {},
|
|
7
|
+
"wirePayload": {
|
|
8
|
+
"version": 7,
|
|
9
|
+
"updatedAt": "2026-10-23T00:00:00.000Z",
|
|
10
|
+
"sandboxGit": {
|
|
11
|
+
"origins": [
|
|
12
|
+
{
|
|
13
|
+
"origin": "https://git.example.com:9999",
|
|
14
|
+
"connectUrl": "https://console.example.com/connect/git"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"origin": "http://git.legacy.example:8080",
|
|
18
|
+
"connectUrl": "https://console.example.com/connect/git"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contributor": "contract",
|
|
3
|
+
"claim": "`sandboxGit` 是 /effective 载荷上的**非域**顶层键(web-admin 按部署登记册投影,不是存储域):wire 腿按 open-world 把它**逐字承运**(含块里的 http origin —— 它的拒因是逐 origin 应答里的一个词,不是块的校验拒),不出告警;其余三腿的入参里没有这个块,读出的 effective 里也不铸它(可选、无默认)。把它当域去校验、或当未知键丢掉的读腿,会让取值平面静默失去 origin 表。",
|
|
4
|
+
"firstRedOn": []
|
|
5
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
{
|
|
2
|
+
"domains": {
|
|
3
|
+
"outcome": "effective",
|
|
4
|
+
"effective": {
|
|
5
|
+
"collab": {
|
|
6
|
+
"templates": []
|
|
7
|
+
},
|
|
8
|
+
"entitlement": {
|
|
9
|
+
"budgetGroups": [],
|
|
10
|
+
"groupBindings": [],
|
|
11
|
+
"subjects": [],
|
|
12
|
+
"tiers": []
|
|
13
|
+
},
|
|
14
|
+
"execution": {
|
|
15
|
+
"allowedLanes": [
|
|
16
|
+
"host"
|
|
17
|
+
],
|
|
18
|
+
"required": false
|
|
19
|
+
},
|
|
20
|
+
"governance": {},
|
|
21
|
+
"hosts": {
|
|
22
|
+
"hosts": []
|
|
23
|
+
},
|
|
24
|
+
"limits": {},
|
|
25
|
+
"mcp": {
|
|
26
|
+
"servers": []
|
|
27
|
+
},
|
|
28
|
+
"models": {
|
|
29
|
+
"atMention": {
|
|
30
|
+
"mode": "all"
|
|
31
|
+
},
|
|
32
|
+
"models": [],
|
|
33
|
+
"roles": {},
|
|
34
|
+
"tierGroups": []
|
|
35
|
+
},
|
|
36
|
+
"plugins": {
|
|
37
|
+
"plugins": []
|
|
38
|
+
},
|
|
39
|
+
"projects": {
|
|
40
|
+
"projects": {}
|
|
41
|
+
},
|
|
42
|
+
"prompts": {},
|
|
43
|
+
"rosters": {
|
|
44
|
+
"rosters": []
|
|
45
|
+
},
|
|
46
|
+
"scenarios": {
|
|
47
|
+
"scenarios": []
|
|
48
|
+
},
|
|
49
|
+
"skills": {
|
|
50
|
+
"skills": []
|
|
51
|
+
},
|
|
52
|
+
"systems": {
|
|
53
|
+
"systems": []
|
|
54
|
+
},
|
|
55
|
+
"workers": {
|
|
56
|
+
"workers": []
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
"warnings": [],
|
|
60
|
+
"metadata": {
|
|
61
|
+
"version": 7,
|
|
62
|
+
"updatedAt": "2026-10-23T00:00:00.000Z"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"strictWrite": {
|
|
66
|
+
"outcome": "effective",
|
|
67
|
+
"effective": {},
|
|
68
|
+
"warnings": [],
|
|
69
|
+
"metadata": {
|
|
70
|
+
"version": 7,
|
|
71
|
+
"updatedAt": "2026-10-23T00:00:00.000Z"
|
|
72
|
+
},
|
|
73
|
+
"why": "受理空集:本条打的是 wire 腿(提交的域文档集为空)。"
|
|
74
|
+
},
|
|
75
|
+
"wire": {
|
|
76
|
+
"outcome": "effective",
|
|
77
|
+
"effective": {
|
|
78
|
+
"collab": {
|
|
79
|
+
"templates": []
|
|
80
|
+
},
|
|
81
|
+
"entitlement": {
|
|
82
|
+
"budgetGroups": [],
|
|
83
|
+
"groupBindings": [],
|
|
84
|
+
"subjects": [],
|
|
85
|
+
"tiers": []
|
|
86
|
+
},
|
|
87
|
+
"execution": {
|
|
88
|
+
"allowedLanes": [
|
|
89
|
+
"host"
|
|
90
|
+
],
|
|
91
|
+
"required": false
|
|
92
|
+
},
|
|
93
|
+
"governance": {},
|
|
94
|
+
"hosts": {
|
|
95
|
+
"hosts": []
|
|
96
|
+
},
|
|
97
|
+
"limits": {},
|
|
98
|
+
"mcp": {
|
|
99
|
+
"servers": []
|
|
100
|
+
},
|
|
101
|
+
"models": {
|
|
102
|
+
"atMention": {
|
|
103
|
+
"mode": "all"
|
|
104
|
+
},
|
|
105
|
+
"models": [],
|
|
106
|
+
"roles": {},
|
|
107
|
+
"tierGroups": []
|
|
108
|
+
},
|
|
109
|
+
"plugins": {
|
|
110
|
+
"plugins": []
|
|
111
|
+
},
|
|
112
|
+
"projects": {
|
|
113
|
+
"projects": {}
|
|
114
|
+
},
|
|
115
|
+
"rosters": {
|
|
116
|
+
"rosters": []
|
|
117
|
+
},
|
|
118
|
+
"sandboxGit": {
|
|
119
|
+
"origins": [
|
|
120
|
+
{
|
|
121
|
+
"connectUrl": "http://console.example.com/connect/git",
|
|
122
|
+
"origin": "https://Git.Example.com:443/"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"connectUrl": "https://console.example.com/connect/git",
|
|
126
|
+
"origin": "https://Git.Example.com:443/"
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
},
|
|
130
|
+
"scenarios": {
|
|
131
|
+
"scenarios": []
|
|
132
|
+
},
|
|
133
|
+
"skills": {
|
|
134
|
+
"skills": []
|
|
135
|
+
},
|
|
136
|
+
"systems": {
|
|
137
|
+
"systems": []
|
|
138
|
+
},
|
|
139
|
+
"workers": {
|
|
140
|
+
"workers": []
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"warnings": [],
|
|
144
|
+
"metadata": {
|
|
145
|
+
"version": 7,
|
|
146
|
+
"updatedAt": "2026-10-23T00:00:00.000Z"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"localRoot": {
|
|
150
|
+
"derive": true,
|
|
151
|
+
"localVersion": 2617555914511015
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"version": 7,
|
|
4
|
+
"updatedAt": "2026-10-23T00:00:00.000Z"
|
|
5
|
+
},
|
|
6
|
+
"domains": {},
|
|
7
|
+
"wirePayload": {
|
|
8
|
+
"version": 7,
|
|
9
|
+
"updatedAt": "2026-10-23T00:00:00.000Z",
|
|
10
|
+
"sandboxGit": {
|
|
11
|
+
"origins": [
|
|
12
|
+
{
|
|
13
|
+
"origin": "https://Git.Example.com:443/",
|
|
14
|
+
"connectUrl": "http://console.example.com/connect/git"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"origin": "https://Git.Example.com:443/",
|
|
18
|
+
"connectUrl": "https://console.example.com/connect/git"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -54,10 +54,14 @@
|
|
|
54
54
|
* signature checks). (Decision function: `jwksWithinOfflineGrace`.)
|
|
55
55
|
*
|
|
56
56
|
* R9 OUTPUT. Success → `VerifiedIdentity { principal: sub, role: token.role, grantId: token.jti,
|
|
57
|
-
* scope: token.scope, iat: token.iat }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
|
|
57
|
+
* scope: token.scope, iat: token.iat, principalInc: token.sub_inc }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
|
|
58
58
|
* `readTokenScope` — an absent claim is refused, not read as "global"; `iat` = issued-at, integer seconds, carried
|
|
59
59
|
* verbatim — a reader that learns a principal was deleted at T cuts off its tokens issued before T; absent claim ⇒ no
|
|
60
|
-
* `iat` key; present but not a non-negative integer ⇒ the token is `malformed`
|
|
60
|
+
* `iat` key; present but not a non-negative integer ⇒ the token is `malformed`; `principalInc` = the incarnation claim
|
|
61
|
+
* {@link PRINCIPAL_INC_CLAIM} carried verbatim — the same name deleted and re-created is ANOTHER person, and this is what
|
|
62
|
+
* tells the two apart; absent claim ⇒ no `principalInc` key — never a placeholder: what a reader does with a token that names
|
|
63
|
+
* no incarnation is that reader's decision, not a refusal here; present but not a non-empty string ⇒ the token is `malformed`,
|
|
64
|
+
* the same law as `sub`).
|
|
61
65
|
* `role` is web-admin RBAC role claim (string, informational for the worker's own authz mapping);
|
|
62
66
|
* `grantId` is the jti when web-admin stamps one (absent today — optional forever). Failure → one
|
|
63
67
|
* of AUTH_BRIDGE_ERROR_CODES; the HTTP surface answers 401 with a GENERIC message (no error detail
|
|
@@ -87,6 +91,14 @@ export declare const DEFAULT_OFFLINE_GRACE_SEC: number;
|
|
|
87
91
|
* split one identity into two. Anyone validating or minting a principal (a token `sub`, a membership row, an outbound
|
|
88
92
|
* principal header) checks against THIS value. */
|
|
89
93
|
export declare const PRINCIPAL_MAX_LENGTH = 190;
|
|
94
|
+
/** R9 — the token claim that carries the principal's INCARNATION: a deleted-and-re-created user keeps the name (`sub`) and gets a new
|
|
95
|
+
* incarnation, so `sub` alone names a slot, not a person. One spelling for whoever mints the claim and whoever reads it
|
|
96
|
+
* ({@link toVerifiedIdentity} reads it by this constant), the way `SCOPE_CLAIM` (`api/scopes`) is spelled once for the scope claim. */
|
|
97
|
+
export declare const PRINCIPAL_INC_CLAIM = "sub_inc";
|
|
98
|
+
/** One incarnation id: a non-empty string, opaque (never trimmed, folded or parsed). The token claim, {@link VerifiedIdentity.principalInc},
|
|
99
|
+
* and every wire field that names an incarnation (the credential port's request and answer) are THIS schema — one rule, read by object. */
|
|
100
|
+
export declare const PrincipalIncarnation: z.ZodString;
|
|
101
|
+
export type PrincipalIncarnation = z.infer<typeof PrincipalIncarnation>;
|
|
90
102
|
/** One RSA public JWK as web-admin publishes it (public members ONLY — a `d`/`p`/`q` here is a web-admin
|
|
91
103
|
* bug, but the verifier MUST still never use/echo private members). */
|
|
92
104
|
export declare const WebAdminJwk: z.ZodObject<{
|
|
@@ -140,6 +152,7 @@ export declare const VerifiedIdentity: z.ZodObject<{
|
|
|
140
152
|
grantId: z.ZodOptional<z.ZodString>;
|
|
141
153
|
scope: z.ZodOptional<z.ZodString>;
|
|
142
154
|
iat: z.ZodOptional<z.ZodNumber>;
|
|
155
|
+
principalInc: z.ZodOptional<z.ZodString>;
|
|
143
156
|
}, z.core.$strip>;
|
|
144
157
|
export type VerifiedIdentity = z.infer<typeof VerifiedIdentity>;
|
|
145
158
|
/** The verifier's total result — exactly one of ok/error. */
|
|
@@ -195,6 +208,7 @@ export declare function toVerifiedIdentity(payload: {
|
|
|
195
208
|
aud?: unknown;
|
|
196
209
|
scope?: unknown;
|
|
197
210
|
iat?: unknown;
|
|
211
|
+
sub_inc?: unknown;
|
|
198
212
|
}, cfg?: Pick<VerifierConfig, "audience">): AuthBridgeResult;
|
|
199
213
|
/** Cheap pre-filter: does this bearer LOOK like a JWT (three non-empty dot-separated segments)? Use it to
|
|
200
214
|
* route between the existing static-token branch and this verifier — NOT as any kind of validation. */
|
package/dist/api/auth-bridge.js
CHANGED
|
@@ -54,10 +54,14 @@
|
|
|
54
54
|
* signature checks). (Decision function: `jwksWithinOfflineGrace`.)
|
|
55
55
|
*
|
|
56
56
|
* R9 OUTPUT. Success → `VerifiedIdentity { principal: sub, role: token.role, grantId: token.jti,
|
|
57
|
-
* scope: token.scope, iat: token.iat }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
|
|
57
|
+
* scope: token.scope, iat: token.iat, principalInc: token.sub_inc }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
|
|
58
58
|
* `readTokenScope` — an absent claim is refused, not read as "global"; `iat` = issued-at, integer seconds, carried
|
|
59
59
|
* verbatim — a reader that learns a principal was deleted at T cuts off its tokens issued before T; absent claim ⇒ no
|
|
60
|
-
* `iat` key; present but not a non-negative integer ⇒ the token is `malformed`
|
|
60
|
+
* `iat` key; present but not a non-negative integer ⇒ the token is `malformed`; `principalInc` = the incarnation claim
|
|
61
|
+
* {@link PRINCIPAL_INC_CLAIM} carried verbatim — the same name deleted and re-created is ANOTHER person, and this is what
|
|
62
|
+
* tells the two apart; absent claim ⇒ no `principalInc` key — never a placeholder: what a reader does with a token that names
|
|
63
|
+
* no incarnation is that reader's decision, not a refusal here; present but not a non-empty string ⇒ the token is `malformed`,
|
|
64
|
+
* the same law as `sub`).
|
|
61
65
|
* `role` is web-admin RBAC role claim (string, informational for the worker's own authz mapping);
|
|
62
66
|
* `grantId` is the jti when web-admin stamps one (absent today — optional forever). Failure → one
|
|
63
67
|
* of AUTH_BRIDGE_ERROR_CODES; the HTTP surface answers 401 with a GENERIC message (no error detail
|
|
@@ -72,6 +76,8 @@
|
|
|
72
76
|
*/
|
|
73
77
|
import { z } from "zod";
|
|
74
78
|
import { INTEGER } from "../integer.js";
|
|
79
|
+
import { inspect, orWhenUninspectable, ownJsonOrThrow, ownOrThrow } from "../own-key-read.js";
|
|
80
|
+
import { isJsonArray } from "../plain-json-object.js";
|
|
75
81
|
// ── constants ────────────────────────────────────────────────────────────────────────────────────────────
|
|
76
82
|
/** The web-admin JWKS endpoint, relative to the web-admin origin (shipped; public by design, RFC 7517 §5). */
|
|
77
83
|
export const WEB_ADMIN_JWKS_PATH = "/api/auth/sso/jwks";
|
|
@@ -88,6 +94,13 @@ export const DEFAULT_OFFLINE_GRACE_SEC = 24 * 60 * 60;
|
|
|
88
94
|
* split one identity into two. Anyone validating or minting a principal (a token `sub`, a membership row, an outbound
|
|
89
95
|
* principal header) checks against THIS value. */
|
|
90
96
|
export const PRINCIPAL_MAX_LENGTH = 190;
|
|
97
|
+
/** R9 — the token claim that carries the principal's INCARNATION: a deleted-and-re-created user keeps the name (`sub`) and gets a new
|
|
98
|
+
* incarnation, so `sub` alone names a slot, not a person. One spelling for whoever mints the claim and whoever reads it
|
|
99
|
+
* ({@link toVerifiedIdentity} reads it by this constant), the way `SCOPE_CLAIM` (`api/scopes`) is spelled once for the scope claim. */
|
|
100
|
+
export const PRINCIPAL_INC_CLAIM = "sub_inc";
|
|
101
|
+
/** One incarnation id: a non-empty string, opaque (never trimmed, folded or parsed). The token claim, {@link VerifiedIdentity.principalInc},
|
|
102
|
+
* and every wire field that names an incarnation (the credential port's request and answer) are THIS schema — one rule, read by object. */
|
|
103
|
+
export const PrincipalIncarnation = z.string().min(1);
|
|
91
104
|
// ── JWKS wire shape (what GET /api/auth/sso/jwks returns) ────────────────────────────────────────────────
|
|
92
105
|
/** One RSA public JWK as web-admin publishes it (public members ONLY — a `d`/`p`/`q` here is a web-admin
|
|
93
106
|
* bug, but the verifier MUST still never use/echo private members). */
|
|
@@ -159,6 +172,11 @@ export const VerifiedIdentity = z.object({
|
|
|
159
172
|
* a claim that is not a non-negative integer makes the token `malformed` (R9), and one issued later than now + the clock
|
|
160
173
|
* skew is `expired` (R5, the same tolerance as `exp` / `nbf`). */
|
|
161
174
|
iat: INTEGER.nonnegative().optional(),
|
|
175
|
+
/** = token {@link PRINCIPAL_INC_CLAIM} (`sub_inc`) when present, verbatim — the principal's incarnation id ({@link PrincipalIncarnation}).
|
|
176
|
+
* Absent claim ⇒ the key is absent (never minted; never `0`, `""` or any placeholder): a reader that needs an incarnation and finds none
|
|
177
|
+
* answers "incarnation unrecorded" itself — that is not a refusal of the token. A claim that is not a non-empty string makes the token
|
|
178
|
+
* `malformed` (R9): a broken issuer is not read as an old one. */
|
|
179
|
+
principalInc: PrincipalIncarnation.optional(),
|
|
162
180
|
});
|
|
163
181
|
/**
|
|
164
182
|
* R3 — should a token bearing `kid` trigger a forced JWKS refetch?
|
|
@@ -203,29 +221,46 @@ export function classifyTimeClaims(claims, nowSec, clockSkewSec) {
|
|
|
203
221
|
* library has verified signature (R1/R2/R3), issuer (R4) and time (R5).
|
|
204
222
|
*/
|
|
205
223
|
export function toVerifiedIdentity(payload, cfg) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return { ok: false, error: "malformed" };
|
|
212
|
-
const iat = iatRead.data;
|
|
213
|
-
if (cfg?.audience) {
|
|
214
|
-
const aud = payload.aud;
|
|
215
|
-
const match = typeof aud === "string" ? aud === cfg.audience : Array.isArray(aud) && aud.includes(cfg.audience);
|
|
216
|
-
if (!match)
|
|
224
|
+
// Every claim is read from the payload's OWN keys (identity reads only what the token carries: a polluted `Object.prototype.role` is not a role the token was minted with,
|
|
225
|
+
// and a token with no `aud` is not given one); a claim that cannot be read ⇒ the token is malformed (fail-closed), never an engine error.
|
|
226
|
+
return orWhenUninspectable(() => {
|
|
227
|
+
const sub = ownOrThrow(payload, "sub"); // 只问「是不是非空串」:读一次值,不深拷
|
|
228
|
+
if (typeof sub !== "string" || sub.length === 0)
|
|
217
229
|
return { ok: false, error: "malformed" };
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
// The iat and incarnation rules are the output fields' own schemas (one rule each, not a second hand-written check).
|
|
231
|
+
// Each schema call runs under `inspect`: the validator reads a snapshot THIS package took, and a throw there (a length check coercing an object value to a primitive) is not the caller's
|
|
232
|
+
// object being unreadable — but it is still no identity: the claim is malformed, and no engine error escapes the judge.
|
|
233
|
+
const iatRead = inspect(() => VerifiedIdentity.shape.iat.safeParse(ownJsonOrThrow(payload, "iat")));
|
|
234
|
+
if (!iatRead.ok || !iatRead.value.success)
|
|
235
|
+
return { ok: false, error: "malformed" };
|
|
236
|
+
const iat = iatRead.value.data;
|
|
237
|
+
const incRead = inspect(() => VerifiedIdentity.shape.principalInc.safeParse(ownJsonOrThrow(payload, PRINCIPAL_INC_CLAIM)));
|
|
238
|
+
if (!incRead.ok || !incRead.value.success)
|
|
239
|
+
return { ok: false, error: "malformed" };
|
|
240
|
+
const principalInc = incRead.value.data;
|
|
241
|
+
if (cfg?.audience) {
|
|
242
|
+
const aud = ownJsonOrThrow(payload, "aud");
|
|
243
|
+
const match = typeof aud === "string" ? aud === cfg.audience : isJsonArray(aud) && aud.includes(cfg.audience);
|
|
244
|
+
if (!match)
|
|
245
|
+
return { ok: false, error: "malformed" };
|
|
246
|
+
}
|
|
247
|
+
// role / jti / scope 只问「是不是非空串」(别的值读成缺省):读一次值,不深拷 —— 深拷会让「值是个带 Date 的对象」把整枚令牌拖成 malformed(v14.6.0 只做 typeof 判)。
|
|
248
|
+
// 要深拷的只有交给会反射它的代码的值:`aud`(数组上 `includes`)与 `iat` / `sub_inc`(交给校验器)
|
|
249
|
+
const role = ownOrThrow(payload, "role");
|
|
250
|
+
const jti = ownOrThrow(payload, "jti");
|
|
251
|
+
const scope = ownOrThrow(payload, "scope");
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
identity: {
|
|
255
|
+
principal: sub,
|
|
256
|
+
role: typeof role === "string" && role.length > 0 ? role : undefined,
|
|
257
|
+
grantId: typeof jti === "string" && jti.length > 0 ? jti : undefined,
|
|
258
|
+
scope: typeof scope === "string" && scope.length > 0 ? scope : undefined,
|
|
259
|
+
...(iat !== undefined ? { iat } : {}),
|
|
260
|
+
...(principalInc !== undefined ? { principalInc } : {}),
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}, () => ({ ok: false, error: "malformed" }));
|
|
229
264
|
}
|
|
230
265
|
/** Cheap pre-filter: does this bearer LOOK like a JWT (three non-empty dot-separated segments)? Use it to
|
|
231
266
|
* route between the existing static-token branch and this verifier — NOT as any kind of validation. */
|