@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/README.md +31 -3
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
  5. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
  6. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
  7. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
  8. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
  9. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
  10. package/dist/api/auth-bridge.d.ts +16 -2
  11. package/dist/api/auth-bridge.js +59 -24
  12. package/dist/api/credentials.d.ts +281 -0
  13. package/dist/api/credentials.js +149 -0
  14. package/dist/api/scopes.js +5 -1
  15. package/dist/config-d-file.js +33 -15
  16. package/dist/config-fns.js +142 -78
  17. package/dist/conformance/canonical.js +4 -4
  18. package/dist/conformance/contract-adapter.js +26 -1
  19. package/dist/conformance/corpus.js +4 -4
  20. package/dist/conformance/derive.js +2 -2
  21. package/dist/conformance/run.js +43 -6
  22. package/dist/cross-session.d.ts +4 -4
  23. package/dist/declared-capture.d.ts +41 -0
  24. package/dist/declared-capture.js +344 -0
  25. package/dist/file-edit.js +11 -15
  26. package/dist/fleet.d.ts +11 -0
  27. package/dist/fleet.js +59 -26
  28. package/dist/hash.d.ts +0 -9
  29. package/dist/hash.js +64 -7
  30. package/dist/hooks-config.js +18 -3
  31. package/dist/http-hook-allowlists.d.ts +22 -9
  32. package/dist/http-hook-allowlists.js +169 -91
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +2 -0
  35. package/dist/mcp-request-entry.d.ts +9 -7
  36. package/dist/mcp-request-entry.js +89 -82
  37. package/dist/one-time-migrations.js +207 -95
  38. package/dist/own-key-read.d.ts +247 -0
  39. package/dist/own-key-read.js +449 -0
  40. package/dist/plain-json-object.d.ts +11 -1
  41. package/dist/plain-json-object.js +37 -1
  42. package/dist/remote-exec.js +9 -1
  43. package/dist/safety-merge-spec.js +50 -37
  44. package/dist/sandbox-git.d.ts +64 -0
  45. package/dist/sandbox-git.js +89 -0
  46. package/dist/scheduler-store.js +19 -11
  47. package/dist/settings-units.js +14 -12
  48. package/dist/types.d.ts +6 -2
  49. package/dist/types.js +11 -6
  50. package/dist/url-shape.d.ts +16 -0
  51. package/dist/url-shape.js +18 -0
  52. package/dist/web-admin-credential.js +5 -2
  53. package/dist/web-admin-session-url.js +2 -2
  54. package/dist/worker-row-verdict.d.ts +11 -0
  55. package/dist/worker-row-verdict.js +51 -1
  56. package/dist/workers-rows.js +38 -10
  57. package/dist/zod-unwrap.d.ts +6 -0
  58. package/dist/zod-unwrap.js +14 -7
  59. package/docs/API-SURFACE.md +35 -4
  60. package/docs/CONSUMER-INTEGRATION.md +178 -0
  61. package/llms-full.txt +23 -4
  62. package/llms.txt +3 -2
  63. package/package.json +1 -1
  64. package/dist/definition-copy.d.ts +0 -23
  65. 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
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "contributor": "contract",
3
+ "claim": "块不合格(非规范形 origin、http 的 connectUrl、重复 origin)时 wire 腿**照样逐字承运**、不出告警:读腿不替消费方判 `sandboxGit`,判它的唯一座位是 `SandboxGitConfig`(消费方在用之前跑)。读腿若『好心』校验并丢弃坏块,消费方分不清『没有投影』与『投影坏了』。",
4
+ "firstRedOn": []
5
+ }
@@ -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. */
@@ -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
- if (typeof payload.sub !== "string" || payload.sub.length === 0)
207
- return { ok: false, error: "malformed" };
208
- // The iat rule is the output field's own schema (one rule, not a second hand-written check).
209
- const iatRead = VerifiedIdentity.shape.iat.safeParse(payload.iat);
210
- if (!iatRead.success)
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
- return {
220
- ok: true,
221
- identity: {
222
- principal: payload.sub,
223
- role: typeof payload.role === "string" && payload.role.length > 0 ? payload.role : undefined,
224
- grantId: typeof payload.jti === "string" && payload.jti.length > 0 ? payload.jti : undefined,
225
- scope: typeof payload.scope === "string" && payload.scope.length > 0 ? payload.scope : undefined,
226
- ...(iat !== undefined ? { iat } : {}),
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. */