@sema-agent/settings-schema 10.1.1 → 11.0.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 (235) hide show
  1. package/CHANGELOG.md +414 -0
  2. package/README.md +141 -65
  3. package/bin/settings-schema.mjs +13 -6
  4. package/conformance/VERSION +1 -1
  5. package/conformance/baseline.json +3 -1
  6. package/conformance/cases/c01-entry-salvage-then-typo/expected.json +3 -1
  7. package/conformance/cases/c02-hosts-clamp-stacked/expected.json +12 -5
  8. package/conformance/cases/c02-hosts-clamp-stacked/meta.json +1 -1
  9. package/conformance/cases/c03-literal-dotted-top-key/expected.json +22 -2
  10. package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +3 -1
  11. package/conformance/cases/c06-legacy-governance-lift/expected.json +11 -101
  12. package/conformance/cases/c06-legacy-governance-lift/meta.json +1 -1
  13. package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +3 -1
  14. package/conformance/cases/c08-limits-unknown-carried-64/expected.json +203 -2
  15. package/conformance/cases/c09-limits-unknown-carried-65/expected.json +204 -2
  16. package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +3 -1
  17. package/conformance/cases/c11-unicode-unknown-keys/expected.json +20 -2
  18. package/conformance/cases/c12-prototype-name-top-keys/expected.json +3 -1
  19. package/conformance/cases/c13-models-field-null/expected.json +3 -1
  20. package/conformance/cases/c14-models-field-empty-array/expected.json +6 -1
  21. package/conformance/cases/c15-models-field-number/expected.json +3 -1
  22. package/conformance/cases/c16-deep-nesting-205/expected.json +14 -2
  23. package/conformance/cases/c17-empty-domain-file/expected.json +3 -1
  24. package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +3 -1
  25. package/conformance/cases/c19-wire-envelope-no-version/expected.json +3 -1
  26. package/conformance/cases/c20-governance-absent-lifts/expected.json +11 -86
  27. package/conformance/cases/c20-governance-absent-lifts/meta.json +1 -1
  28. package/conformance/cases/c21-governance-empty-object-wins/expected.json +6 -5
  29. package/conformance/cases/c21-governance-empty-object-wins/input.json +1 -4
  30. package/conformance/cases/c21-governance-empty-object-wins/meta.json +1 -1
  31. package/conformance/cases/c23-mixed-validity-models/expected.json +3 -1
  32. package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +22 -2
  33. package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +6 -1
  34. package/conformance/cases/c26-hosts-array-1024/expected.json +3 -1
  35. package/conformance/cases/c27-hosts-array-1025/expected.json +6 -7175
  36. package/conformance/cases/c27-hosts-array-1025/meta.json +1 -1
  37. package/conformance/cases/c28-large-document-over-1mb/expected.json +14 -2
  38. package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +3 -1
  39. package/conformance/cases/c30-version-triple-a-first/expected.json +6 -1
  40. package/conformance/cases/c31-version-triple-b-changed/expected.json +6 -1
  41. package/conformance/cases/c32-version-triple-a-restored/expected.json +6 -1
  42. package/conformance/cases/c33-limits-nested-carried-key/expected.json +18 -2
  43. package/conformance/cases/c34-skills-content-vs-manifest/expected.json +19 -3
  44. package/conformance/cases/c35-portable-domain-file-is-null/expected.json +3 -1
  45. package/conformance/cases/c36-skills-entry-without-content/expected.json +6 -2
  46. package/conformance/cases/c37-skills-field-null/expected.json +6 -2
  47. package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +3 -1
  48. package/conformance/cases/c39-hidden-file-in-config-d/expected.json +3 -1
  49. package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +11 -4
  50. package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +9 -3
  51. package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +39 -2
  52. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +4 -1
  53. package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +3 -1
  54. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +19 -10
  55. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +1 -1
  56. package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/expected.json +12 -5
  57. package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/meta.json +9 -0
  58. package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/expected.json +23 -3
  59. package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/input.json +6 -0
  60. package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/meta.json +10 -0
  61. package/conformance/cases/c48-s935-model-headers-transported/expected.json +6 -1
  62. package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +3 -1
  63. package/conformance/cases/c50-s784-lane-host-legal/expected.json +3 -1
  64. package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +3 -1
  65. package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +3 -1
  66. package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +3 -1
  67. package/conformance/cases/c54-s784-lane-adb-legal/expected.json +3 -1
  68. package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +3 -1
  69. package/conformance/cases/c56-s784-lane-device-legal/expected.json +3 -1
  70. package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +3 -1
  71. package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +3 -1
  72. package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +3 -1
  73. package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +3 -1
  74. package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +3 -1
  75. package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +3 -1
  76. package/conformance/cases/c63-s784-host-two-replicas/expected.json +3 -1
  77. package/conformance/cases/c64-s784-multitenant-local-store/expected.json +3 -1
  78. package/conformance/cases/c65-s784-pg-without-db/expected.json +3 -1
  79. package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +3 -1
  80. package/conformance/cases/c67-s784-retired-field-loud/expected.json +3 -1
  81. package/conformance/cases/c67-s784-retired-field-loud/meta.json +1 -1
  82. package/conformance/cases/c68-s784-device-local-store/expected.json +3 -1
  83. package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +3 -1
  84. package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +3 -1
  85. package/conformance/cases/c71-one-scanner-write-reads-display-forms/expected.json +119 -0
  86. package/conformance/cases/c71-one-scanner-write-reads-display-forms/input.json +24 -0
  87. package/conformance/cases/c71-one-scanner-write-reads-display-forms/meta.json +10 -0
  88. package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +111 -0
  89. package/conformance/cases/c72-dangling-reference-write-seat-refuses/input.json +26 -0
  90. package/conformance/cases/c72-dangling-reference-write-seat-refuses/meta.json +6 -0
  91. package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +3 -1
  92. package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +3 -1
  93. package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +6 -1
  94. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +11 -1
  95. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +11 -1
  96. package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +16 -2
  97. package/conformance/cases/cli04-env-value-with-newline/expected.json +3 -1
  98. package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +6 -1
  99. package/conformance/cases/t01-absent-domain-key-not-null/expected.json +3 -1
  100. package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +29 -2
  101. package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +3 -1
  102. package/conformance/cases/z01-every-domain-empty-object/expected.json +8 -4
  103. package/conformance/cases/z01-every-domain-empty-object/input.json +0 -1
  104. package/conformance/cases/z01-every-domain-empty-object/meta.json +1 -1
  105. package/conformance/cases/z02-every-domain-unknown-key/expected.json +163 -23
  106. package/conformance/cases/z02-every-domain-unknown-key/input.json +0 -3
  107. package/conformance/cases/z02-every-domain-unknown-key/meta.json +1 -1
  108. package/dist/api/auth-bridge.d.ts +51 -181
  109. package/dist/api/auth-bridge.js +58 -33
  110. package/dist/api/auth.d.ts +37 -146
  111. package/dist/api/auth.js +16 -26
  112. package/dist/api/feedback.d.ts +87 -0
  113. package/dist/api/feedback.js +66 -0
  114. package/dist/api/pull-token.d.ts +32 -0
  115. package/dist/api/pull-token.js +88 -0
  116. package/dist/api/scopes.d.ts +82 -174
  117. package/dist/api/scopes.js +23 -18
  118. package/dist/bundle.d.ts +15 -7
  119. package/dist/bundle.js +28 -27
  120. package/dist/cli.d.ts +3 -1
  121. package/dist/cli.js +132 -81
  122. package/dist/config-d-file.d.ts +14 -0
  123. package/dist/config-d-file.js +67 -0
  124. package/dist/config-fns.d.ts +87 -186
  125. package/dist/config-fns.js +207 -262
  126. package/dist/conformance/contract-adapter.d.ts +9 -2
  127. package/dist/conformance/contract-adapter.js +15 -7
  128. package/dist/conformance/corpus.d.ts +3 -0
  129. package/dist/conformance/corpus.js +5 -0
  130. package/dist/conformance/derive.d.ts +3 -0
  131. package/dist/conformance/index.d.ts +2 -2
  132. package/dist/conformance/index.js +2 -2
  133. package/dist/conformance/run.js +67 -22
  134. package/dist/conformance/types.d.ts +31 -15
  135. package/dist/conformance/types.js +5 -0
  136. package/dist/conformance/warnings.d.ts +1 -1
  137. package/dist/conformance/warnings.js +1 -4
  138. package/dist/cross-domain.d.ts +66 -22
  139. package/dist/cross-domain.js +164 -93
  140. package/dist/cross-session.d.ts +29 -11
  141. package/dist/default-model.d.ts +43 -0
  142. package/dist/default-model.js +34 -0
  143. package/dist/execution-lane-index.d.ts +4 -8
  144. package/dist/execution-lane-index.js +4 -8
  145. package/dist/execution-lanes.d.ts +31 -8
  146. package/dist/execution-lanes.js +23 -5
  147. package/dist/file-edit.js +5 -2
  148. package/dist/file-store.d.ts +7 -6
  149. package/dist/file-store.js +37 -50
  150. package/dist/fleet.d.ts +364 -762
  151. package/dist/fleet.js +277 -224
  152. package/dist/hooks.d.ts +1151 -4737
  153. package/dist/hooks.js +59 -15
  154. package/dist/index.d.ts +10 -4
  155. package/dist/index.js +13 -7
  156. package/dist/integer.d.ts +11 -0
  157. package/dist/integer.js +11 -0
  158. package/dist/issue-path.d.ts +10 -0
  159. package/dist/issue-path.js +10 -0
  160. package/dist/loopback.d.ts +26 -0
  161. package/dist/loopback.js +54 -0
  162. package/dist/migration-receipt.d.ts +42 -0
  163. package/dist/migration-receipt.js +12 -0
  164. package/dist/one-time-migrations.d.ts +56 -0
  165. package/dist/one-time-migrations.js +819 -0
  166. package/dist/prompts-digest.d.ts +1 -1
  167. package/dist/prompts-digest.js +3 -2
  168. package/dist/reader.d.ts +2 -2
  169. package/dist/remote-exec.d.ts +45 -187
  170. package/dist/remote-exec.js +13 -7
  171. package/dist/resolve-roster.d.ts +18 -3
  172. package/dist/resolve-roster.js +28 -25
  173. package/dist/retired-envs.d.ts +24 -0
  174. package/dist/retired-envs.js +114 -0
  175. package/dist/retired-form.d.ts +31 -0
  176. package/dist/retired-form.js +28 -0
  177. package/dist/safety-merge-spec.d.ts +131 -191
  178. package/dist/safety-merge-spec.js +18 -14
  179. package/dist/scheduler-lifetime.d.ts +5 -0
  180. package/dist/scheduler-lifetime.js +7 -0
  181. package/dist/scheduler-store-migration.d.ts +14 -0
  182. package/dist/scheduler-store-migration.js +14 -0
  183. package/dist/scheduler-store-node.d.ts +24 -9
  184. package/dist/scheduler-store-node.js +25 -15
  185. package/dist/scheduler-store.d.ts +47 -14
  186. package/dist/scheduler-store.js +32 -61
  187. package/dist/secret-refs.d.ts +3 -3
  188. package/dist/secret-refs.js +1 -1
  189. package/dist/secret-scan.d.ts +14 -24
  190. package/dist/secret-scan.js +45 -23
  191. package/dist/sha256-core.d.ts +16 -0
  192. package/dist/sha256-core.js +138 -0
  193. package/dist/sha256.d.ts +6 -1
  194. package/dist/sha256.js +8 -98
  195. package/dist/skills-manifest.js +1 -1
  196. package/dist/types.d.ts +1501 -13536
  197. package/dist/types.js +494 -438
  198. package/dist/url-shape.d.ts +34 -0
  199. package/dist/url-shape.js +61 -0
  200. package/dist/web-admin-credential.d.ts +96 -0
  201. package/dist/{center-credential.js → web-admin-credential.js} +46 -43
  202. package/dist/web-admin-env.d.ts +33 -0
  203. package/dist/web-admin-env.js +33 -0
  204. package/dist/worker-row-verdict.d.ts +16 -0
  205. package/dist/worker-row-verdict.js +31 -0
  206. package/dist/workers-rows.d.ts +34 -0
  207. package/dist/workers-rows.js +42 -0
  208. package/dist/zod-unwrap.d.ts +11 -0
  209. package/dist/zod-unwrap.js +25 -0
  210. package/package.json +17 -9
  211. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +0 -6
  212. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +0 -6
  213. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +0 -49
  214. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +0 -26
  215. package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +0 -4
  216. package/conformance/migrate-workers/m02-g5-idempotent/expected.json +0 -38
  217. package/conformance/migrate-workers/m02-g5-idempotent/input.json +0 -33
  218. package/conformance/migrate-workers/m02-g5-idempotent/meta.json +0 -5
  219. package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +0 -71
  220. package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +0 -42
  221. package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +0 -4
  222. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +0 -52
  223. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +0 -30
  224. package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +0 -4
  225. package/conformance/migrate-workers/m05-row-verdict-reserved-env/expected.json +0 -82
  226. package/conformance/migrate-workers/m05-row-verdict-reserved-env/input.json +0 -68
  227. package/conformance/migrate-workers/m05-row-verdict-reserved-env/meta.json +0 -4
  228. package/dist/center-credential.d.ts +0 -116
  229. package/dist/issue-text.d.ts +0 -10
  230. package/dist/issue-text.js +0 -8
  231. package/dist/migrate-workers.d.ts +0 -71
  232. package/dist/migrate-workers.js +0 -351
  233. package/dist/migrate.d.ts +0 -255
  234. package/dist/migrate.js +0 -542
  235. /package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/input.json +0 -0
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * `api/auth-bridge` — the 鉴权桥 contract: how a WORKER (the worker service / any data-plane service) verifies a
3
- * registry-minted USER access JWT and maps it into its existing `x-agent-principal` identity system.
4
- * (REGISTRY-REBUILD-DESIGN §5, M3 — registry half = JWKS already shipped; service half = ONE verifier
3
+ * web-admin-minted USER access JWT and maps it into its existing `x-agent-principal` identity system.
4
+ * (REGISTRY-REBUILD-DESIGN §5, M3 — web-admin half = JWKS already shipped; service half = ONE verifier
5
5
  * middleware implementing the rules below. This file is the schema + normative spec the service copies.)
6
6
  *
7
- * TRUST MODEL (single trust root, zero new worker config): the worker already trusts the registry for its
8
- * effective config; it extends the SAME trust to the registry's JWKS. A bearer token that is a well-formed
7
+ * TRUST MODEL (single trust root, zero new worker config): the worker already trusts web-admin for its
8
+ * effective config; it extends the SAME trust to web-admin's JWKS. A bearer token that is a well-formed
9
9
  * JWT and verifies against that JWKS with the checks below IS the user — `sub` feeds x-agent-principal
10
10
  * directly (owner semantics / costQuota / entitlement are already principal-keyed). Existing
11
11
  * SERVICE_AUTH_TOKENS / wpt_ bearer branches are untouched; this is an ADDITIONAL branch tried when the
@@ -19,25 +19,26 @@
19
19
  * token whose header alg is absent. Pin the list in the JWT library call — never read alg from the
20
20
  * token to decide how to verify.
21
21
  *
22
- * R2 KEY SOURCE. Verification keys come from the registry JWKS endpoint `GET {registryBaseUrl}` +
23
- * REGISTRY_JWKS_PATH ("/api/auth/sso/jwks"; response shape = `Jwks` below, public members only:
22
+ * R2 KEY SOURCE. Verification keys come from the web-admin JWKS endpoint `GET {webAdminBaseUrl}` +
23
+ * WEB_ADMIN_JWKS_PATH ("/api/auth/sso/jwks"; response shape = `Jwks` below, public members only:
24
24
  * kty/n/e/kid/use/alg). Match the token header `kid` against cached keys.
25
25
  *
26
26
  * R3 KID-MISS FORCED REFRESH + THROTTLE. kid found in cache → use the cached key (no fetch on the hot
27
27
  * path). kid NOT in cache → force ONE JWKS refetch, then retry the match (this is the key-ROTATION
28
- * window: a freshly rotated registry would otherwise reject every new token until the cache expired).
28
+ * window: a freshly rotated web-admin would otherwise reject every new token until the cache expired).
29
29
  * Forced refreshes are throttled to at most one per `jwksRefreshMinIntervalSec` (default 60s) —
30
30
  * otherwise an attacker spraying random-kid tokens turns the verifier into a JWKS-fetch DoS cannon.
31
31
  * Within the throttle window an unknown kid fails as `invalid_signature` WITHOUT fetching.
32
32
  * (Decision function: `shouldForceJwksRefresh`.)
33
33
  *
34
- * R4 ISSUER PIN. `iss` MUST equal the CONFIGURED issuer exactly (VerifierConfig.issuer = the registry's
34
+ * R4 ISSUER PIN. `iss` MUST equal the CONFIGURED issuer exactly (VerifierConfig.issuer = web-admin's
35
35
  * SSO_ISSUER, trailing slash stripped on both sides). A missing or different iss → `unknown_issuer`.
36
36
  * Never derive the expected issuer from the token or from request headers.
37
37
  *
38
38
  * R5 TIME WINDOW WITH SKEW. `exp` is REQUIRED: reject when nowSec > exp + clockSkewSec. `nbf`, when
39
- * present: reject when nowSec < nbf - clockSkewSec. clockSkewSec default 60 (DEFAULT_CLOCK_SKEW_SEC).
40
- * Both failures classify as `expired`. (Decision function: `classifyTimeClaims`.)
39
+ * present: reject when nowSec < nbf - clockSkewSec. `iat`, when present: reject when nowSec < iat - clockSkewSec
40
+ * (issued in the future). clockSkewSec default 60 (DEFAULT_CLOCK_SKEW_SEC), one tolerance for all three.
41
+ * These failures classify as `expired`. (Decision function: `classifyTimeClaims`.)
41
42
  *
42
43
  * R6 PRINCIPAL. `sub` MUST be a non-empty string — it becomes `VerifiedIdentity.principal` and feeds the
43
44
  * existing x-agent-principal pipeline verbatim. Empty/missing sub → `malformed`.
@@ -48,14 +49,17 @@
48
49
  *
49
50
  * R8 OFFLINE GRACE / FAIL-CLOSED. JWKS fetch fails (network/5xx/malformed body): keys already cached
50
51
  * remain USABLE for up to `offlineGraceSec` (default 24h, DEFAULT_OFFLINE_GRACE_SEC) measured from the
51
- * last SUCCESSFUL fetch — a registry blip must not take the whole data plane down. Past the grace, or
52
+ * last SUCCESSFUL fetch — a web-admin blip must not take the whole data plane down. Past the grace, or
52
53
  * with no cache at all, verification fails CLOSED as `jwks_unavailable` (never fail-open, never skip
53
54
  * signature checks). (Decision function: `jwksWithinOfflineGrace`.)
54
55
  *
55
56
  * R9 OUTPUT. Success → `VerifiedIdentity { principal: sub, role: token.role, grantId: token.jti,
56
- * scope: token.scope }` (`scope` = the multi-tenant active scope claim, `api/scopes`; absent = "global").
57
- * `role` is the registry RBAC role claim (string, informational for the worker's own authz mapping);
58
- * `grantId` is the jti when the registry stamps one (absent today — optional forever). Failure → one
57
+ * scope: token.scope, iat: token.iat }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
58
+ * `readTokenScope` — an absent claim is refused, not read as "global"; `iat` = issued-at, integer seconds, carried
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`).
61
+ * `role` is web-admin RBAC role claim (string, informational for the worker's own authz mapping);
62
+ * `grantId` is the jti when web-admin stamps one (absent today — optional forever). Failure → one
59
63
  * of AUTH_BRIDGE_ERROR_CODES; the HTTP surface answers 401 with a GENERIC message (no error detail
60
64
  * disclosure to the caller; the code goes to logs/metrics only).
61
65
  *
@@ -67,9 +71,10 @@
67
71
  * library; THESE shapes pin down its configuration), browser-safe.
68
72
  */
69
73
  import { z } from "zod";
74
+ import { INTEGER } from "../integer.js";
70
75
  // ── constants ────────────────────────────────────────────────────────────────────────────────────────────
71
- /** The registry JWKS endpoint, relative to the registry origin (shipped; public by design, RFC 7517 §5). */
72
- export const REGISTRY_JWKS_PATH = "/api/auth/sso/jwks";
76
+ /** The web-admin JWKS endpoint, relative to the web-admin origin (shipped; public by design, RFC 7517 §5). */
77
+ export const WEB_ADMIN_JWKS_PATH = "/api/auth/sso/jwks";
73
78
  /** R1 — the ONLY acceptable JWS algorithms. */
74
79
  export const AUTH_BRIDGE_ALGS = ["RS256"];
75
80
  /** R5 — default tolerated clock skew (seconds) on exp/nbf. */
@@ -78,10 +83,15 @@ export const DEFAULT_CLOCK_SKEW_SEC = 60;
78
83
  export const DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC = 60;
79
84
  /** R8 — default seconds a cached JWKS stays usable after fetches start failing (24h), then fail-closed. */
80
85
  export const DEFAULT_OFFLINE_GRACE_SEC = 24 * 60 * 60;
86
+ /** R6 — the longest principal the data plane accepts: the engine's principal gate refuses a longer one (400), because every
87
+ * scope / owner column a principal lands in verbatim is that wide — an over-long principal would truncate at the insert and
88
+ * split one identity into two. Anyone validating or minting a principal (a token `sub`, a membership row, an outbound
89
+ * principal header) checks against THIS value. */
90
+ export const PRINCIPAL_MAX_LENGTH = 190;
81
91
  // ── JWKS wire shape (what GET /api/auth/sso/jwks returns) ────────────────────────────────────────────────
82
- /** One RSA public JWK as the registry publishes it (public members ONLY — a `d`/`p`/`q` here is a registry
92
+ /** One RSA public JWK as web-admin publishes it (public members ONLY — a `d`/`p`/`q` here is a web-admin
83
93
  * bug, but the verifier MUST still never use/echo private members). */
84
- export const RegistryJwk = z
94
+ export const WebAdminJwk = z
85
95
  .object({
86
96
  kty: z.literal("RSA"),
87
97
  /** modulus, base64url */
@@ -94,27 +104,27 @@ export const RegistryJwk = z
94
104
  alg: z.enum(AUTH_BRIDGE_ALGS).optional(),
95
105
  })
96
106
  .passthrough();
97
- /** The JWKS document. `{keys: []}` is VALID (registry with SSO disabled) — every token then fails as
98
- * `invalid_signature` (there is a registry, it just mints nothing). */
99
- export const RegistryJwks = z.object({ keys: z.array(RegistryJwk) }).passthrough();
107
+ /** The JWKS document. `{keys: []}` is VALID (web-admin with SSO disabled) — every token then fails as
108
+ * `invalid_signature` (there is a web-admin, it just mints nothing). */
109
+ export const WebAdminJwks = z.object({ keys: z.array(WebAdminJwk) }).passthrough();
100
110
  // ── verifier configuration ───────────────────────────────────────────────────────────────────────────────
101
111
  /** Everything the service verifier is allowed to be configured with. In the zero-new-config deployment the
102
- * worker derives `jwksUrl` from the registry base URL it already has (config pull) + REGISTRY_JWKS_PATH,
103
- * and `issuer` from the same registry identity — but both stay explicit here so a split deployment
112
+ * worker derives `jwksUrl` from web-admin base URL it already has (config pull) + WEB_ADMIN_JWKS_PATH,
113
+ * and `issuer` from the same web-admin identity — but both stay explicit here so a split deployment
104
114
  * (issuer behind a proxy ≠ fetch URL) is expressible. */
105
115
  export const VerifierConfig = z.object({
106
- /** R4 — the EXACT expected `iss` (the registry's SSO_ISSUER). Compare with trailing slash stripped. */
116
+ /** R4 — the EXACT expected `iss` (web-admin's SSO_ISSUER). Compare with trailing slash stripped. */
107
117
  issuer: z.string().min(1),
108
- /** R2 — absolute JWKS URL. Default: issuer (or the worker's registry base URL) + REGISTRY_JWKS_PATH. */
118
+ /** R2 — absolute JWKS URL. Default: issuer (or the worker's web-admin base URL) + WEB_ADMIN_JWKS_PATH. */
109
119
  jwksUrl: z.string().min(1).optional(),
110
120
  /** R7 — when set, `aud` must contain this value; unset = aud ignored. */
111
121
  audience: z.string().min(1).optional(),
112
122
  /** R5 — seconds of exp/nbf tolerance. Default DEFAULT_CLOCK_SKEW_SEC (60). */
113
- clockSkewSec: z.number().int().nonnegative().default(DEFAULT_CLOCK_SKEW_SEC),
123
+ clockSkewSec: INTEGER.nonnegative().default(DEFAULT_CLOCK_SKEW_SEC),
114
124
  /** R3 — min seconds between forced kid-miss refetches. Default 60. */
115
- jwksRefreshMinIntervalSec: z.number().int().nonnegative().default(DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC),
125
+ jwksRefreshMinIntervalSec: INTEGER.nonnegative().default(DEFAULT_JWKS_REFRESH_MIN_INTERVAL_SEC),
116
126
  /** R8 — seconds a stale cache survives fetch failures before fail-closed. Default 86400 (24h). */
117
- offlineGraceSec: z.number().int().nonnegative().default(DEFAULT_OFFLINE_GRACE_SEC),
127
+ offlineGraceSec: INTEGER.nonnegative().default(DEFAULT_OFFLINE_GRACE_SEC),
118
128
  });
119
129
  // ── verification outcome ─────────────────────────────────────────────────────────────────────────────────
120
130
  /** R9 failure classification — for logs/metrics/tests; the HTTP surface stays a generic 401. */
@@ -134,15 +144,21 @@ export const AUTH_BRIDGE_ERROR_CODES = [
134
144
  export const VerifiedIdentity = z.object({
135
145
  /** = token `sub`. Feeds x-agent-principal / owner semantics / costQuota / entitlement keying verbatim. */
136
146
  principal: z.string().min(1),
137
- /** = token `role` claim (registry RBAC: admin/editor/viewer/…) — informational; the worker maps it to
147
+ /** = token `role` claim (web-admin RBAC: admin/editor/viewer/…) — informational; the worker maps it to
138
148
  * its own authz, it does NOT replace worker-side entitlement checks. */
139
149
  role: z.string().optional(),
140
150
  /** = token `jti` when present (the auth_grants grant id — audit correlation). Absent today; optional forever. */
141
151
  grantId: z.string().optional(),
142
152
  /** = token `scope` claim when present (the active tenant scope, `api/scopes`) — transparently forwarded
143
- * to the worker's principal pipeline. ABSENT means the pre-scope/default token: semantically "global"
144
- * (use `tokenScopeOf` from api/scopes to normalize). Optional forever — backward compatible. */
153
+ * to the worker's principal pipeline. Read it through `readTokenScope` (api/scopes): an ABSENT claim is refused
154
+ * (`scope_claim_missing`), never read as "global". The field stays optional in this shape because the verifier
155
+ * reports what the token carried; the scope decision is `readTokenScope`'s. */
145
156
  scope: z.string().optional(),
157
+ /** = token `iat` (issued-at, JWT NumericDate in SECONDS) when present, verbatim — a reader that learns a principal was
158
+ * deleted at time T cuts off that principal's tokens issued before T. Absent claim ⇒ the key is absent (never minted);
159
+ * a claim that is not a non-negative integer makes the token `malformed` (R9), and one issued later than now + the clock
160
+ * skew is `expired` (R5, the same tolerance as `exp` / `nbf`). */
161
+ iat: INTEGER.nonnegative().optional(),
146
162
  });
147
163
  /**
148
164
  * R3 — should a token bearing `kid` trigger a forced JWKS refetch?
@@ -169,14 +185,17 @@ export function jwksWithinOfflineGrace(cache, cfg) {
169
185
  }
170
186
  /**
171
187
  * R5 — classify the token's time window. `exp` REQUIRED (a JWT without exp is not acceptable here —
172
- * registry tokens always carry one; classify a missing exp as "expired" so it can never live forever).
173
- * All values in SECONDS (JWT NumericDate).
188
+ * web-admin tokens always carry one; classify a missing exp as "expired" so it can never live forever).
189
+ * `nbf` / `iat`, when present, must not lie beyond now + skew (not yet valid / issued in the future).
190
+ * All values in SECONDS (JWT NumericDate); one tolerance (`clockSkewSec`) for all three.
174
191
  */
175
192
  export function classifyTimeClaims(claims, nowSec, clockSkewSec) {
176
193
  if (typeof claims.exp !== "number" || nowSec > claims.exp + clockSkewSec)
177
194
  return "expired";
178
195
  if (typeof claims.nbf === "number" && nowSec < claims.nbf - clockSkewSec)
179
196
  return "expired";
197
+ if (typeof claims.iat === "number" && nowSec < claims.iat - clockSkewSec)
198
+ return "expired";
180
199
  return "ok";
181
200
  }
182
201
  /**
@@ -186,6 +205,11 @@ export function classifyTimeClaims(claims, nowSec, clockSkewSec) {
186
205
  export function toVerifiedIdentity(payload, cfg) {
187
206
  if (typeof payload.sub !== "string" || payload.sub.length === 0)
188
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;
189
213
  if (cfg?.audience) {
190
214
  const aud = payload.aud;
191
215
  const match = typeof aud === "string" ? aud === cfg.audience : Array.isArray(aud) && aud.includes(cfg.audience);
@@ -199,6 +223,7 @@ export function toVerifiedIdentity(payload, cfg) {
199
223
  role: typeof payload.role === "string" && payload.role.length > 0 ? payload.role : undefined,
200
224
  grantId: typeof payload.jti === "string" && payload.jti.length > 0 ? payload.jti : undefined,
201
225
  scope: typeof payload.scope === "string" && payload.scope.length > 0 ? payload.scope : undefined,
226
+ ...(iat !== undefined ? { iat } : {}),
202
227
  },
203
228
  };
204
229
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `api/auth` — the /api/v1/auth/* WIRE CONTRACT (REGISTRY-REBUILD-DESIGN §2.1/§2.2, M1 shipped in
3
- * sema-registry; this file freezes the shipped shapes so registry, orchestrator, service and the sema
3
+ * web-admin; this file freezes the shipped shapes so web-admin, orchestrator, service and the sema
4
4
  * shell import ONE source of truth — design §3 "契约先行").
5
5
  *
6
6
  * The flow is RFC 8628 (OAuth 2.0 Device Authorization Grant) + a rotating opaque refresh token:
@@ -19,35 +19,24 @@
19
19
  * WIRE CASING: these are OAuth-standard endpoints — fields are snake_case ON THE WIRE (access_token,
20
20
  * device_code, …), deliberately breaking this package's camelCase habit. Do not "fix" it.
21
21
  *
22
- * SECRET DISCIPLINE (registry side, informational for consumers): device_code and refresh_token are
22
+ * SECRET DISCIPLINE (web-admin side, informational for consumers): device_code and refresh_token are
23
23
  * ≥256-bit opaque strings persisted ONLY as SHA-256 hashes; access_token is the standard issueSsoToken
24
24
  * RS256 JWT (1h) — see `api/auth-bridge` for how a worker verifies it.
25
25
  *
26
26
  * Pure zod contract — zero IO, browser-safe, same rules as the rest of this package.
27
27
  *
28
- * ── 4.0.0:这一面**保留**(不删)。理由 / 谁该改调 / 什么条件下删 ───────────────────────────────
28
+ * ── 这一面**保留**(11.0.0 重核)。4.0.0 时写的「删的条件:零消费」已不成立 ─────────────────────────────
29
29
  *
30
- * 读数(4.0.0 前夜逐行核过,不是按计数下的结论):**没有任何消费仓 import 子路径 `./api/auth`**,
31
- * 也没有人从本包的桶里取这 18 个名(尺子:同一把 grep 在那几个仓里数 `@sema-agent/settings-schema`
32
- * 的 import 行,读数分别是几十到上百行 —— 所以那个零是真零,不是 grep 坏了)。**但零消费不等于这面死了**:
33
- * 上面第 2-4 行那句「大家 import ONE source of truth」今天是**意图**,不是读数 —— 真实情况是各抄了一份:
34
- * · 管理面(配置控制台)`src/lib/auth/device-flow.ts` 自带一只 `normalizeUserCode`,函数体与本文件那只
35
- * **逐字节相同**,另有自己的 `DEVICE_GRANT_TYPE` / `DEVICE_POLL_INTERVAL_SECONDS`;
36
- * · 壳(命令行)`src/sema/cloudAuth.ts` 自声明 `DeviceCodeResponse` / `TokenResponse` 两只响应形;
37
- * · SDK 在自己的 `registry/auth.ts` 里做**值拷贝**(`REGISTRY_AUTH_PATHS` / grant_type / 错误码集),
38
- * 并在**生产码**里把 `DeviceCodeResponse` / `TokenGrantResponse` 两只当类型 import —— 但取的是
39
- * **改名前的那个旧包名**,靠它自己的「同源锚」测试对账;那道锚今天钉的是旧包(devDependency 停在
40
- * 0.17.1),于是本包改名之后的任何改动它都看不见。
41
- * (所以上面那句「零 import」的**精确射程**是:没有任何仓从**本包这个名字**下取这 18 个名。)
42
- *
43
- * ⇒ 删掉这一面 = 把上面三份手抄就地合法化。凭据轴该有**单属主**,所以 4.0.0 取「迁移」不取「删」:
44
- * · 谁该改调:管理面与壳直接 import 本面(先从 `normalizeUserCode` / 设备码响应形这几只开始);
45
- * SDK 把它那道同源锚从旧包名改到本包(锚不动,漂移就永远是绿的)。
46
- * · 什么条件下删:**下一个 major**,且届时三方都已改调(或各自明确表态不改并自己认账),且那时**仍**
47
- * 是零消费 —— 那天「删」的理由才成立。今天不成立:今天的零是手抄造出来的,不是这面没人要。
30
+ * 4.0.0 前夜这一面的读数是「没有任何仓从本包这个名字下取这几个名」,各方各抄一份;当时取「迁移」不取「删」,并写下删的前提
31
+ * = 届时仍零消费。迁移落了,前提就没了 —— 今天按子路径 `./api/auth` 引它的(`git grep`,含测试与文档行):
32
+ * · web-admin:设备流、SSO、授权流的存储与激活页(生产码直接 import 本面);
33
+ * · server:web-admin 凭证的续签器与对 web-admin 的请求(刷新令牌的路径与响应形);
34
+ * · cli:壳的云登录与本机凭证文件(设备码 / 令牌响应形)。
35
+ * SDK 的值拷贝由它自己的构建期生成对表(不从本面 import,也不在本包这边计)。
36
+ * ⇒ 这一面是活的单源:不删、没有删的窗。加名 / 改形按发版纪律(新键 = 可选键;改名删名 = major + 三句)。
48
37
  */
49
38
  import { z } from "zod";
50
- /** The five auth endpoints, relative to the registry origin. */
39
+ /** The five auth endpoints, relative to web-admin origin. */
51
40
  export declare const AUTH_V1_PATHS: {
52
41
  readonly deviceCode: "/api/v1/auth/device/code";
53
42
  readonly deviceToken: "/api/v1/auth/device/token";
@@ -91,191 +80,93 @@ export declare function nextPollInterval(currentIntervalSeconds: number, error:
91
80
  };
92
81
  /** The uniform error body. `error_description` is HUMAN diagnostic text — clients branch on `error` only. */
93
82
  export declare const OAuthErrorResponse: z.ZodObject<{
94
- error: z.ZodEnum<["authorization_pending", "slow_down", "expired_token", "access_denied", "invalid_request", "unsupported_grant_type", "invalid_grant", "server_error"]>;
95
- error_description: z.ZodOptional<z.ZodString>;
96
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
97
- error: z.ZodEnum<["authorization_pending", "slow_down", "expired_token", "access_denied", "invalid_request", "unsupported_grant_type", "invalid_grant", "server_error"]>;
98
- error_description: z.ZodOptional<z.ZodString>;
99
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
100
- error: z.ZodEnum<["authorization_pending", "slow_down", "expired_token", "access_denied", "invalid_request", "unsupported_grant_type", "invalid_grant", "server_error"]>;
83
+ error: z.ZodEnum<{
84
+ invalid_request: "invalid_request";
85
+ server_error: "server_error";
86
+ authorization_pending: "authorization_pending";
87
+ slow_down: "slow_down";
88
+ expired_token: "expired_token";
89
+ access_denied: "access_denied";
90
+ unsupported_grant_type: "unsupported_grant_type";
91
+ invalid_grant: "invalid_grant";
92
+ }>;
101
93
  error_description: z.ZodOptional<z.ZodString>;
102
- }, z.ZodTypeAny, "passthrough">>;
94
+ }, z.core.$loose>;
103
95
  export type OAuthErrorResponse = z.infer<typeof OAuthErrorResponse>;
104
96
  /** Unauthenticated by design (the client has no credentials yet). `client_name` is a NON-secret display
105
97
  * hint for the consent screen ("sema CLI on macOS"); absent → the server falls back to the User-Agent.
106
98
  * Body may be JSON or form-encoded; an empty body is legal. */
107
99
  export declare const DeviceCodeRequest: z.ZodObject<{
108
100
  client_name: z.ZodOptional<z.ZodString>;
109
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
110
- client_name: z.ZodOptional<z.ZodString>;
111
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
112
- client_name: z.ZodOptional<z.ZodString>;
113
- }, z.ZodTypeAny, "passthrough">>;
101
+ }, z.core.$loose>;
114
102
  export type DeviceCodeRequest = z.infer<typeof DeviceCodeRequest>;
115
103
  /** RFC 8628 §3.2 device-authorization response. `device_code` is the client's polling secret (keep it in
116
104
  * memory, never log it); `user_code` is the short human code ("XXXX-XXXX") the user approves. */
117
105
  export declare const DeviceCodeResponse: z.ZodObject<{
118
- /** ≥256-bit opaque polling secret — returned ONCE, hash-persisted server-side. */
119
- device_code: z.ZodString;
120
- /** Human approval code, canonical shape "XXXX-XXXX" (base-20 alphabet: no vowels, no 0/O/1/I). */
121
- user_code: z.ZodString;
122
- /** The /activate page the user opens (any device, any logged-in browser). */
123
- verification_uri: z.ZodString;
124
- /** verification_uri with ?user_code= pre-filled — one-click / QR path. **OPTIONAL** (RFC 8628 §3.2):
125
- * absent ⇒ the reader shows `verification_uri` + `user_code`; present-but-empty is still refused. (5.0.0, S-615) */
126
- verification_uri_complete: z.ZodOptional<z.ZodString>;
127
- /** Seconds until the handshake expires (DEVICE_CODE_TTL_SECONDS). */
128
- expires_in: z.ZodNumber;
129
- /** Minimum seconds between polls (DEVICE_POLL_INTERVAL_SECONDS). */
130
- interval: z.ZodNumber;
131
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
132
- /** ≥256-bit opaque polling secret — returned ONCE, hash-persisted server-side. */
133
- device_code: z.ZodString;
134
- /** Human approval code, canonical shape "XXXX-XXXX" (base-20 alphabet: no vowels, no 0/O/1/I). */
135
- user_code: z.ZodString;
136
- /** The /activate page the user opens (any device, any logged-in browser). */
137
- verification_uri: z.ZodString;
138
- /** verification_uri with ?user_code= pre-filled — one-click / QR path. **OPTIONAL** (RFC 8628 §3.2):
139
- * absent ⇒ the reader shows `verification_uri` + `user_code`; present-but-empty is still refused. (5.0.0, S-615) */
140
- verification_uri_complete: z.ZodOptional<z.ZodString>;
141
- /** Seconds until the handshake expires (DEVICE_CODE_TTL_SECONDS). */
142
- expires_in: z.ZodNumber;
143
- /** Minimum seconds between polls (DEVICE_POLL_INTERVAL_SECONDS). */
144
- interval: z.ZodNumber;
145
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
146
- /** ≥256-bit opaque polling secret — returned ONCE, hash-persisted server-side. */
147
106
  device_code: z.ZodString;
148
- /** Human approval code, canonical shape "XXXX-XXXX" (base-20 alphabet: no vowels, no 0/O/1/I). */
149
107
  user_code: z.ZodString;
150
- /** The /activate page the user opens (any device, any logged-in browser). */
151
108
  verification_uri: z.ZodString;
152
- /** verification_uri with ?user_code= pre-filled — one-click / QR path. **OPTIONAL** (RFC 8628 §3.2):
153
- * absent ⇒ the reader shows `verification_uri` + `user_code`; present-but-empty is still refused. (5.0.0, S-615) */
154
109
  verification_uri_complete: z.ZodOptional<z.ZodString>;
155
- /** Seconds until the handshake expires (DEVICE_CODE_TTL_SECONDS). */
156
110
  expires_in: z.ZodNumber;
157
- /** Minimum seconds between polls (DEVICE_POLL_INTERVAL_SECONDS). */
158
111
  interval: z.ZodNumber;
159
- }, z.ZodTypeAny, "passthrough">>;
112
+ }, z.core.$loose>;
160
113
  export type DeviceCodeResponse = z.infer<typeof DeviceCodeResponse>;
161
114
  /** RFC 8628 §3.4 token request. Form or JSON. */
162
115
  export declare const DeviceTokenRequest: z.ZodObject<{
163
116
  grant_type: z.ZodLiteral<"urn:ietf:params:oauth:grant-type:device_code">;
164
117
  device_code: z.ZodString;
165
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
166
- grant_type: z.ZodLiteral<"urn:ietf:params:oauth:grant-type:device_code">;
167
- device_code: z.ZodString;
168
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
169
- grant_type: z.ZodLiteral<"urn:ietf:params:oauth:grant-type:device_code">;
170
- device_code: z.ZodString;
171
- }, z.ZodTypeAny, "passthrough">>;
118
+ }, z.core.$loose>;
172
119
  export type DeviceTokenRequest = z.infer<typeof DeviceTokenRequest>;
173
120
  /** The ONE token success shape — shared by device/token AND token/refresh (both mint through the same
174
121
  * exit: access = issueSsoToken RS256 JWT, refresh = fresh opaque rotating grant). */
175
122
  export declare const TokenGrantResponse: z.ZodObject<{
176
- /** RS256 JWT — claims: sub (principal), iss, iat, exp, role; optional aud. Verify per api/auth-bridge. */
177
123
  access_token: z.ZodString;
178
- /** NEW opaque rotating refresh token — replaces any previous one the client held. Store 0600/keychain. */
179
124
  refresh_token: z.ZodString;
180
125
  token_type: z.ZodLiteral<"Bearer">;
181
- /** Access-token lifetime in seconds (ACCESS_TOKEN_TTL_SECONDS). */
182
126
  expires_in: z.ZodNumber;
183
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
184
- /** RS256 JWT — claims: sub (principal), iss, iat, exp, role; optional aud. Verify per api/auth-bridge. */
185
- access_token: z.ZodString;
186
- /** NEW opaque rotating refresh token — replaces any previous one the client held. Store 0600/keychain. */
187
- refresh_token: z.ZodString;
188
- token_type: z.ZodLiteral<"Bearer">;
189
- /** Access-token lifetime in seconds (ACCESS_TOKEN_TTL_SECONDS). */
190
- expires_in: z.ZodNumber;
191
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
192
- /** RS256 JWT — claims: sub (principal), iss, iat, exp, role; optional aud. Verify per api/auth-bridge. */
193
- access_token: z.ZodString;
194
- /** NEW opaque rotating refresh token — replaces any previous one the client held. Store 0600/keychain. */
195
- refresh_token: z.ZodString;
196
- token_type: z.ZodLiteral<"Bearer">;
197
- /** Access-token lifetime in seconds (ACCESS_TOKEN_TTL_SECONDS). */
198
- expires_in: z.ZodNumber;
199
- }, z.ZodTypeAny, "passthrough">>;
127
+ }, z.core.$loose>;
200
128
  export type TokenGrantResponse = z.infer<typeof TokenGrantResponse>;
201
129
  /** Rotation is STRICT single-use: success revokes the presented grant and returns a NEW refresh_token;
202
130
  * a replayed (or raced) old token answers `invalid_grant`. Clients MUST persist the new token before
203
131
  * discarding the old request state. */
204
132
  export declare const TokenRefreshRequest: z.ZodObject<{
205
133
  refresh_token: z.ZodString;
206
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
207
- refresh_token: z.ZodString;
208
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
209
- refresh_token: z.ZodString;
210
- }, z.ZodTypeAny, "passthrough">>;
134
+ }, z.core.$loose>;
211
135
  export type TokenRefreshRequest = z.infer<typeof TokenRefreshRequest>;
212
136
  /** Revoke the refresh grant. The already-minted access JWT stays valid until its 1h exp — revocation kills
213
137
  * the RENEWAL path, which is the durable one. */
214
138
  export declare const LogoutRequest: z.ZodObject<{
215
139
  refresh_token: z.ZodString;
216
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
217
- refresh_token: z.ZodString;
218
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
219
- refresh_token: z.ZodString;
220
- }, z.ZodTypeAny, "passthrough">>;
140
+ }, z.core.$loose>;
221
141
  export type LogoutRequest = z.infer<typeof LogoutRequest>;
222
142
  /** Always `{ok:true}` with HTTP 200 whether or not the token matched a live grant (RFC 7009: a revocation
223
143
  * endpoint never discloses token existence). 400 only for a malformed request. */
224
144
  export declare const LogoutResponse: z.ZodObject<{
225
145
  ok: z.ZodLiteral<true>;
226
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
227
- ok: z.ZodLiteral<true>;
228
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
229
- ok: z.ZodLiteral<true>;
230
- }, z.ZodTypeAny, "passthrough">>;
146
+ }, z.core.$loose>;
231
147
  export type LogoutResponse = z.infer<typeof LogoutResponse>;
232
148
  /** GET ?user_code=XXXX-XXXX — the consent screen's lookup, WITHOUT approving. 401 when not signed in;
233
149
  * invalid_grant for an unknown/expired code. */
234
150
  export declare const DeviceApproveLookupResponse: z.ZodObject<{
235
151
  user_code: z.ZodString;
236
- status: z.ZodEnum<["pending", "approved", "denied"]>;
237
- /** The client_name/User-Agent display hint from device/code — null when the client sent none. */
152
+ status: z.ZodEnum<{
153
+ denied: "denied";
154
+ approved: "approved";
155
+ pending: "pending";
156
+ }>;
238
157
  client: z.ZodNullable<z.ZodString>;
239
- /** Handshake expiry, epoch MILLISECONDS. */
240
158
  expires_at: z.ZodNumber;
241
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
242
- user_code: z.ZodString;
243
- status: z.ZodEnum<["pending", "approved", "denied"]>;
244
- /** The client_name/User-Agent display hint from device/code — null when the client sent none. */
245
- client: z.ZodNullable<z.ZodString>;
246
- /** Handshake expiry, epoch MILLISECONDS. */
247
- expires_at: z.ZodNumber;
248
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
249
- user_code: z.ZodString;
250
- status: z.ZodEnum<["pending", "approved", "denied"]>;
251
- /** The client_name/User-Agent display hint from device/code — null when the client sent none. */
252
- client: z.ZodNullable<z.ZodString>;
253
- /** Handshake expiry, epoch MILLISECONDS. */
254
- expires_at: z.ZodNumber;
255
- }, z.ZodTypeAny, "passthrough">>;
159
+ }, z.core.$loose>;
256
160
  export type DeviceApproveLookupResponse = z.infer<typeof DeviceApproveLookupResponse>;
257
161
  /** POST body (JSON ONLY — the JSON content type is part of the cross-site-request defence; a cross-site
258
162
  * form cannot send application/json without a CORS preflight). The approving identity comes from the
259
163
  * session cookie exclusively — NEVER from this body. */
260
164
  export declare const DeviceApproveRequest: z.ZodObject<{
261
- /** Case-insensitive; dashes/spaces optional — see normalizeUserCode. */
262
- user_code: z.ZodString;
263
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
264
- /** Case-insensitive; dashes/spaces optional — see normalizeUserCode. */
265
165
  user_code: z.ZodString;
266
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
267
- /** Case-insensitive; dashes/spaces optional — see normalizeUserCode. */
268
- user_code: z.ZodString;
269
- }, z.ZodTypeAny, "passthrough">>;
166
+ }, z.core.$loose>;
270
167
  export type DeviceApproveRequest = z.infer<typeof DeviceApproveRequest>;
271
168
  export declare const DeviceApproveResponse: z.ZodObject<{
272
169
  ok: z.ZodLiteral<true>;
273
170
  user_code: z.ZodString;
274
- }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
275
- ok: z.ZodLiteral<true>;
276
- user_code: z.ZodString;
277
- }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
278
- ok: z.ZodLiteral<true>;
279
- user_code: z.ZodString;
280
- }, z.ZodTypeAny, "passthrough">>;
171
+ }, z.core.$loose>;
281
172
  export type DeviceApproveResponse = z.infer<typeof DeviceApproveResponse>;
package/dist/api/auth.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `api/auth` — the /api/v1/auth/* WIRE CONTRACT (REGISTRY-REBUILD-DESIGN §2.1/§2.2, M1 shipped in
3
- * sema-registry; this file freezes the shipped shapes so registry, orchestrator, service and the sema
3
+ * web-admin; this file freezes the shipped shapes so web-admin, orchestrator, service and the sema
4
4
  * shell import ONE source of truth — design §3 "契约先行").
5
5
  *
6
6
  * The flow is RFC 8628 (OAuth 2.0 Device Authorization Grant) + a rotating opaque refresh token:
@@ -19,38 +19,28 @@
19
19
  * WIRE CASING: these are OAuth-standard endpoints — fields are snake_case ON THE WIRE (access_token,
20
20
  * device_code, …), deliberately breaking this package's camelCase habit. Do not "fix" it.
21
21
  *
22
- * SECRET DISCIPLINE (registry side, informational for consumers): device_code and refresh_token are
22
+ * SECRET DISCIPLINE (web-admin side, informational for consumers): device_code and refresh_token are
23
23
  * ≥256-bit opaque strings persisted ONLY as SHA-256 hashes; access_token is the standard issueSsoToken
24
24
  * RS256 JWT (1h) — see `api/auth-bridge` for how a worker verifies it.
25
25
  *
26
26
  * Pure zod contract — zero IO, browser-safe, same rules as the rest of this package.
27
27
  *
28
- * ── 4.0.0:这一面**保留**(不删)。理由 / 谁该改调 / 什么条件下删 ───────────────────────────────
28
+ * ── 这一面**保留**(11.0.0 重核)。4.0.0 时写的「删的条件:零消费」已不成立 ─────────────────────────────
29
29
  *
30
- * 读数(4.0.0 前夜逐行核过,不是按计数下的结论):**没有任何消费仓 import 子路径 `./api/auth`**,
31
- * 也没有人从本包的桶里取这 18 个名(尺子:同一把 grep 在那几个仓里数 `@sema-agent/settings-schema`
32
- * 的 import 行,读数分别是几十到上百行 —— 所以那个零是真零,不是 grep 坏了)。**但零消费不等于这面死了**:
33
- * 上面第 2-4 行那句「大家 import ONE source of truth」今天是**意图**,不是读数 —— 真实情况是各抄了一份:
34
- * · 管理面(配置控制台)`src/lib/auth/device-flow.ts` 自带一只 `normalizeUserCode`,函数体与本文件那只
35
- * **逐字节相同**,另有自己的 `DEVICE_GRANT_TYPE` / `DEVICE_POLL_INTERVAL_SECONDS`;
36
- * · 壳(命令行)`src/sema/cloudAuth.ts` 自声明 `DeviceCodeResponse` / `TokenResponse` 两只响应形;
37
- * · SDK 在自己的 `registry/auth.ts` 里做**值拷贝**(`REGISTRY_AUTH_PATHS` / grant_type / 错误码集),
38
- * 并在**生产码**里把 `DeviceCodeResponse` / `TokenGrantResponse` 两只当类型 import —— 但取的是
39
- * **改名前的那个旧包名**,靠它自己的「同源锚」测试对账;那道锚今天钉的是旧包(devDependency 停在
40
- * 0.17.1),于是本包改名之后的任何改动它都看不见。
41
- * (所以上面那句「零 import」的**精确射程**是:没有任何仓从**本包这个名字**下取这 18 个名。)
42
- *
43
- * ⇒ 删掉这一面 = 把上面三份手抄就地合法化。凭据轴该有**单属主**,所以 4.0.0 取「迁移」不取「删」:
44
- * · 谁该改调:管理面与壳直接 import 本面(先从 `normalizeUserCode` / 设备码响应形这几只开始);
45
- * SDK 把它那道同源锚从旧包名改到本包(锚不动,漂移就永远是绿的)。
46
- * · 什么条件下删:**下一个 major**,且届时三方都已改调(或各自明确表态不改并自己认账),且那时**仍**
47
- * 是零消费 —— 那天「删」的理由才成立。今天不成立:今天的零是手抄造出来的,不是这面没人要。
30
+ * 4.0.0 前夜这一面的读数是「没有任何仓从本包这个名字下取这几个名」,各方各抄一份;当时取「迁移」不取「删」,并写下删的前提
31
+ * = 届时仍零消费。迁移落了,前提就没了 —— 今天按子路径 `./api/auth` 引它的(`git grep`,含测试与文档行):
32
+ * · web-admin:设备流、SSO、授权流的存储与激活页(生产码直接 import 本面);
33
+ * · server:web-admin 凭证的续签器与对 web-admin 的请求(刷新令牌的路径与响应形);
34
+ * · cli:壳的云登录与本机凭证文件(设备码 / 令牌响应形)。
35
+ * SDK 的值拷贝由它自己的构建期生成对表(不从本面 import,也不在本包这边计)。
36
+ * ⇒ 这一面是活的单源:不删、没有删的窗。加名 / 改形按发版纪律(新键 = 可选键;改名删名 = major + 三句)。
48
37
  */
49
38
  import { z } from "zod";
39
+ import { INTEGER } from "../integer.js";
50
40
  // ── endpoint paths + protocol constants (shipped values) ─────────────────────────────────────────────────
51
41
  // 3.0.0(S-321):这些常量与纯函数 2026-07-27 曾被拆去 `./api/wire` 子路径(给「零依赖客户端运行时直取」
52
42
  // 留的门),四个消费仓自始至终零 import —— 死出口连同它的拆分一起删,常量回到它们的契约原处。
53
- /** The five auth endpoints, relative to the registry origin. */
43
+ /** The five auth endpoints, relative to web-admin origin. */
54
44
  export const AUTH_V1_PATHS = {
55
45
  deviceCode: "/api/v1/auth/device/code",
56
46
  deviceToken: "/api/v1/auth/device/token",
@@ -85,7 +75,7 @@ export const OAUTH_ERROR_CODES = [
85
75
  "unsupported_grant_type",
86
76
  /** token/refresh + device/approve: unknown / expired / revoked / replayed token or code — no detail. */
87
77
  "invalid_grant",
88
- /** SSO not configured on the registry (HTTP 503) — a deployment problem, not a client one. */
78
+ /** SSO not configured on web-admin (HTTP 503) — a deployment problem, not a client one. */
89
79
  "server_error",
90
80
  ];
91
81
  /** Handshake states a consent-screen lookup can see. (`approved` rows vanish once exchanged/consumed.)
@@ -150,9 +140,9 @@ export const DeviceCodeResponse = z
150
140
  * absent ⇒ the reader shows `verification_uri` + `user_code`; present-but-empty is still refused. (5.0.0, S-615) */
151
141
  verification_uri_complete: z.string().min(1).optional(),
152
142
  /** Seconds until the handshake expires (DEVICE_CODE_TTL_SECONDS). */
153
- expires_in: z.number().int().positive(),
143
+ expires_in: INTEGER.positive(),
154
144
  /** Minimum seconds between polls (DEVICE_POLL_INTERVAL_SECONDS). */
155
- interval: z.number().int().positive(),
145
+ interval: INTEGER.positive(),
156
146
  })
157
147
  .passthrough();
158
148
  // ── POST /api/v1/auth/device/token (the poll) ────────────────────────────────────────────────────────────
@@ -173,7 +163,7 @@ export const TokenGrantResponse = z
173
163
  refresh_token: z.string().min(1),
174
164
  token_type: z.literal("Bearer"),
175
165
  /** Access-token lifetime in seconds (ACCESS_TOKEN_TTL_SECONDS). */
176
- expires_in: z.number().int().positive(),
166
+ expires_in: INTEGER.positive(),
177
167
  })
178
168
  .passthrough();
179
169
  // ── POST /api/v1/auth/token/refresh ──────────────────────────────────────────────────────────────────────