@sema-agent/settings-schema 10.1.1 → 11.1.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 +469 -1
- package/README.md +144 -66
- package/bin/settings-schema.mjs +10 -12
- package/conformance/VERSION +1 -1
- package/conformance/baseline.json +3 -1
- package/conformance/cases/c01-entry-salvage-then-typo/expected.json +3 -1
- package/conformance/cases/c02-hosts-clamp-stacked/expected.json +12 -5
- package/conformance/cases/c02-hosts-clamp-stacked/meta.json +1 -1
- package/conformance/cases/c03-literal-dotted-top-key/expected.json +22 -2
- package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +3 -1
- package/conformance/cases/c06-legacy-governance-lift/expected.json +11 -101
- package/conformance/cases/c06-legacy-governance-lift/meta.json +1 -1
- package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +3 -1
- package/conformance/cases/c08-limits-unknown-carried-64/expected.json +203 -2
- package/conformance/cases/c09-limits-unknown-carried-65/expected.json +204 -2
- package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +3 -1
- package/conformance/cases/c11-unicode-unknown-keys/expected.json +20 -2
- package/conformance/cases/c12-prototype-name-top-keys/expected.json +3 -1
- package/conformance/cases/c13-models-field-null/expected.json +3 -1
- package/conformance/cases/c14-models-field-empty-array/expected.json +6 -1
- package/conformance/cases/c15-models-field-number/expected.json +3 -1
- package/conformance/cases/c16-deep-nesting-205/expected.json +14 -2
- package/conformance/cases/c17-empty-domain-file/expected.json +3 -1
- package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +3 -1
- package/conformance/cases/c19-wire-envelope-no-version/expected.json +3 -1
- package/conformance/cases/c20-governance-absent-lifts/expected.json +11 -86
- package/conformance/cases/c20-governance-absent-lifts/meta.json +1 -1
- package/conformance/cases/c21-governance-empty-object-wins/expected.json +6 -5
- package/conformance/cases/c21-governance-empty-object-wins/input.json +1 -4
- package/conformance/cases/c21-governance-empty-object-wins/meta.json +1 -1
- package/conformance/cases/c23-mixed-validity-models/expected.json +3 -1
- package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +22 -2
- package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +6 -1
- package/conformance/cases/c26-hosts-array-1024/expected.json +3 -1
- package/conformance/cases/c27-hosts-array-1025/expected.json +6 -7175
- package/conformance/cases/c27-hosts-array-1025/meta.json +1 -1
- package/conformance/cases/c28-large-document-over-1mb/expected.json +14 -2
- package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +3 -1
- package/conformance/cases/c30-version-triple-a-first/expected.json +6 -1
- package/conformance/cases/c31-version-triple-b-changed/expected.json +6 -1
- package/conformance/cases/c32-version-triple-a-restored/expected.json +6 -1
- package/conformance/cases/c33-limits-nested-carried-key/expected.json +18 -2
- package/conformance/cases/c34-skills-content-vs-manifest/expected.json +19 -3
- package/conformance/cases/c35-portable-domain-file-is-null/expected.json +3 -1
- package/conformance/cases/c36-skills-entry-without-content/expected.json +6 -2
- package/conformance/cases/c37-skills-field-null/expected.json +6 -2
- package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +3 -1
- package/conformance/cases/c39-hidden-file-in-config-d/expected.json +3 -1
- package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +11 -4
- package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +9 -3
- package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +39 -2
- package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +4 -1
- package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +3 -1
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +19 -10
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +1 -1
- package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/expected.json +12 -5
- package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/meta.json +9 -0
- package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/expected.json +23 -3
- package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/input.json +6 -0
- package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/meta.json +10 -0
- package/conformance/cases/c48-s935-model-headers-transported/expected.json +6 -1
- package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +3 -1
- package/conformance/cases/c50-s784-lane-host-legal/expected.json +3 -1
- package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +3 -1
- package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +3 -1
- package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +3 -1
- package/conformance/cases/c54-s784-lane-adb-legal/expected.json +3 -1
- package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +3 -1
- package/conformance/cases/c56-s784-lane-device-legal/expected.json +3 -1
- package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +3 -1
- package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +3 -1
- package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +3 -1
- package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +3 -1
- package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +3 -1
- package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +3 -1
- package/conformance/cases/c63-s784-host-two-replicas/expected.json +3 -1
- package/conformance/cases/c64-s784-multitenant-local-store/expected.json +3 -1
- package/conformance/cases/c65-s784-pg-without-db/expected.json +3 -1
- package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +3 -1
- package/conformance/cases/c67-s784-retired-field-loud/expected.json +3 -1
- package/conformance/cases/c67-s784-retired-field-loud/meta.json +1 -1
- package/conformance/cases/c68-s784-device-local-store/expected.json +3 -1
- package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +3 -1
- package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +3 -1
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/expected.json +119 -0
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/input.json +24 -0
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/meta.json +10 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +111 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/input.json +26 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/meta.json +6 -0
- package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +3 -1
- package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +3 -1
- package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +6 -1
- package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +11 -1
- package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +11 -1
- package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +16 -2
- package/conformance/cases/cli04-env-value-with-newline/expected.json +3 -1
- package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +6 -1
- package/conformance/cases/t01-absent-domain-key-not-null/expected.json +3 -1
- package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +29 -2
- package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +3 -1
- package/conformance/cases/z01-every-domain-empty-object/expected.json +8 -4
- package/conformance/cases/z01-every-domain-empty-object/input.json +0 -1
- package/conformance/cases/z01-every-domain-empty-object/meta.json +1 -1
- package/conformance/cases/z02-every-domain-unknown-key/expected.json +163 -23
- package/conformance/cases/z02-every-domain-unknown-key/input.json +0 -3
- package/conformance/cases/z02-every-domain-unknown-key/meta.json +1 -1
- package/dist/api/auth-bridge.d.ts +51 -181
- package/dist/api/auth-bridge.js +58 -33
- package/dist/api/auth.d.ts +37 -146
- package/dist/api/auth.js +16 -26
- package/dist/api/feedback.d.ts +87 -0
- package/dist/api/feedback.js +66 -0
- package/dist/api/pull-token.d.ts +32 -0
- package/dist/api/pull-token.js +88 -0
- package/dist/api/scopes.d.ts +82 -174
- package/dist/api/scopes.js +23 -18
- package/dist/atomic-write.d.ts +14 -0
- package/dist/atomic-write.js +60 -0
- package/dist/bundle.d.ts +15 -7
- package/dist/bundle.js +28 -27
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +149 -80
- package/dist/config-d-file.d.ts +14 -0
- package/dist/config-d-file.js +67 -0
- package/dist/config-fns.d.ts +87 -186
- package/dist/config-fns.js +207 -262
- package/dist/conformance/contract-adapter.d.ts +9 -2
- package/dist/conformance/contract-adapter.js +15 -7
- package/dist/conformance/corpus.d.ts +3 -0
- package/dist/conformance/corpus.js +5 -0
- package/dist/conformance/derive.d.ts +3 -0
- package/dist/conformance/index.d.ts +2 -2
- package/dist/conformance/index.js +2 -2
- package/dist/conformance/run.js +67 -22
- package/dist/conformance/types.d.ts +31 -15
- package/dist/conformance/types.js +5 -0
- package/dist/conformance/warnings.d.ts +1 -1
- package/dist/conformance/warnings.js +1 -4
- package/dist/cross-domain.d.ts +66 -22
- package/dist/cross-domain.js +164 -93
- package/dist/cross-session.d.ts +29 -11
- package/dist/default-model.d.ts +43 -0
- package/dist/default-model.js +34 -0
- package/dist/execution-lane-index.d.ts +4 -8
- package/dist/execution-lane-index.js +4 -8
- package/dist/execution-lanes.d.ts +31 -8
- package/dist/execution-lanes.js +23 -5
- package/dist/file-edit.d.ts +3 -3
- package/dist/file-edit.js +11 -18
- package/dist/file-store.d.ts +7 -6
- package/dist/file-store.js +37 -50
- package/dist/fleet.d.ts +364 -762
- package/dist/fleet.js +277 -224
- package/dist/hooks.d.ts +1151 -4737
- package/dist/hooks.js +59 -15
- package/dist/index.d.ts +10 -4
- package/dist/index.js +13 -7
- package/dist/integer.d.ts +11 -0
- package/dist/integer.js +11 -0
- package/dist/issue-path.d.ts +10 -0
- package/dist/issue-path.js +10 -0
- package/dist/local-load.d.ts +1 -1
- package/dist/local-load.js +4 -14
- package/dist/loopback.d.ts +26 -0
- package/dist/loopback.js +54 -0
- package/dist/migration-receipt.d.ts +42 -0
- package/dist/migration-receipt.js +12 -0
- package/dist/one-time-migrations.d.ts +62 -0
- package/dist/one-time-migrations.js +828 -0
- package/dist/prompts-digest.d.ts +1 -1
- package/dist/prompts-digest.js +3 -2
- package/dist/reader.d.ts +2 -2
- package/dist/remote-exec.d.ts +45 -187
- package/dist/remote-exec.js +16 -10
- package/dist/resolve-roster.d.ts +18 -3
- package/dist/resolve-roster.js +28 -25
- package/dist/retired-envs.d.ts +24 -0
- package/dist/retired-envs.js +114 -0
- package/dist/retired-form.d.ts +31 -0
- package/dist/retired-form.js +28 -0
- package/dist/safety-merge-spec.d.ts +131 -191
- package/dist/safety-merge-spec.js +18 -14
- package/dist/scheduler-lifetime.d.ts +5 -0
- package/dist/scheduler-lifetime.js +7 -0
- package/dist/scheduler-store-migration.d.ts +14 -0
- package/dist/scheduler-store-migration.js +14 -0
- package/dist/scheduler-store-node.d.ts +28 -12
- package/dist/scheduler-store-node.js +32 -23
- package/dist/scheduler-store.d.ts +47 -14
- package/dist/scheduler-store.js +32 -61
- package/dist/secret-refs.d.ts +3 -3
- package/dist/secret-refs.js +1 -1
- package/dist/secret-scan.d.ts +14 -24
- package/dist/secret-scan.js +45 -23
- package/dist/sha256-core.d.ts +16 -0
- package/dist/sha256-core.js +138 -0
- package/dist/sha256.d.ts +6 -1
- package/dist/sha256.js +8 -98
- package/dist/skills-manifest.js +1 -1
- package/dist/types.d.ts +1509 -13536
- package/dist/types.js +526 -438
- package/dist/url-shape.d.ts +34 -0
- package/dist/url-shape.js +61 -0
- package/dist/web-admin-credential.d.ts +96 -0
- package/dist/{center-credential.js → web-admin-credential.js} +49 -46
- package/dist/web-admin-env.d.ts +33 -0
- package/dist/web-admin-env.js +33 -0
- package/dist/worker-row-verdict.d.ts +16 -0
- package/dist/worker-row-verdict.js +31 -0
- package/dist/workers-rows.d.ts +34 -0
- package/dist/workers-rows.js +42 -0
- package/dist/zod-unwrap.d.ts +11 -0
- package/dist/zod-unwrap.js +25 -0
- package/package.json +17 -9
- package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +0 -6
- package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +0 -6
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +0 -49
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +0 -26
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +0 -4
- package/conformance/migrate-workers/m02-g5-idempotent/expected.json +0 -38
- package/conformance/migrate-workers/m02-g5-idempotent/input.json +0 -33
- package/conformance/migrate-workers/m02-g5-idempotent/meta.json +0 -5
- package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +0 -71
- package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +0 -42
- package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +0 -4
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +0 -52
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +0 -30
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +0 -4
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/expected.json +0 -82
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/input.json +0 -68
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/meta.json +0 -4
- package/dist/center-credential.d.ts +0 -116
- package/dist/issue-text.d.ts +0 -10
- package/dist/issue-text.js +0 -8
- package/dist/migrate-workers.d.ts +0 -71
- package/dist/migrate-workers.js +0 -351
- package/dist/migrate.d.ts +0 -255
- package/dist/migrate.js +0 -542
- /package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/input.json +0 -0
package/dist/api/auth-bridge.js
CHANGED
|
@@ -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
|
-
*
|
|
4
|
-
* (REGISTRY-REBUILD-DESIGN §5, M3 —
|
|
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
|
|
8
|
-
* effective config; it extends the SAME trust to
|
|
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
|
|
23
|
-
*
|
|
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
|
|
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 =
|
|
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.
|
|
40
|
-
*
|
|
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
|
|
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`;
|
|
57
|
-
* `
|
|
58
|
-
*
|
|
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
|
|
72
|
-
export const
|
|
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
|
|
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
|
|
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 (
|
|
98
|
-
* `invalid_signature` (there is a
|
|
99
|
-
export const
|
|
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
|
|
103
|
-
* and `issuer` from the same
|
|
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` (
|
|
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
|
|
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:
|
|
123
|
+
clockSkewSec: INTEGER.nonnegative().default(DEFAULT_CLOCK_SKEW_SEC),
|
|
114
124
|
/** R3 — min seconds between forced kid-miss refetches. Default 60. */
|
|
115
|
-
jwksRefreshMinIntervalSec:
|
|
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:
|
|
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 (
|
|
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.
|
|
144
|
-
* (
|
|
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
|
-
*
|
|
173
|
-
*
|
|
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
|
}
|
package/dist/api/auth.d.ts
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
|
-
*
|
|
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 (
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* ·
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
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<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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.
|
|
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
|
-
},
|
|
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.
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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<
|
|
237
|
-
|
|
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
|
-
},
|
|
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.
|
|
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
|
-
},
|
|
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
|
-
*
|
|
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 (
|
|
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
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* ·
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
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
|
|
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:
|
|
143
|
+
expires_in: INTEGER.positive(),
|
|
154
144
|
/** Minimum seconds between polls (DEVICE_POLL_INTERVAL_SECONDS). */
|
|
155
|
-
interval:
|
|
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:
|
|
166
|
+
expires_in: INTEGER.positive(),
|
|
177
167
|
})
|
|
178
168
|
.passthrough();
|
|
179
169
|
// ── POST /api/v1/auth/token/refresh ──────────────────────────────────────────────────────────────────────
|