@sema-agent/settings-schema 13.0.0 → 14.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.
- package/CHANGELOG.md +108 -0
- package/README.md +20 -2
- package/conformance/VERSION +1 -1
- package/conformance/baseline.json +1 -3
- package/conformance/cases/c01-entry-salvage-then-typo/expected.json +1 -3
- package/conformance/cases/c02-hosts-clamp-stacked/expected.json +1 -3
- package/conformance/cases/c03-literal-dotted-top-key/expected.json +1 -3
- package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +1 -3
- package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +1 -3
- package/conformance/cases/c08-limits-unknown-carried-64/expected.json +1 -3
- package/conformance/cases/c09-limits-unknown-carried-65/expected.json +1 -3
- package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +1 -3
- package/conformance/cases/c11-unicode-unknown-keys/expected.json +1 -3
- package/conformance/cases/c12-prototype-name-top-keys/expected.json +1 -3
- package/conformance/cases/c13-models-field-null/expected.json +1 -3
- package/conformance/cases/c14-models-field-empty-array/expected.json +1 -3
- package/conformance/cases/c15-models-field-number/expected.json +1 -3
- package/conformance/cases/c16-deep-nesting-205/expected.json +1 -3
- package/conformance/cases/c17-empty-domain-file/expected.json +1 -3
- package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +1 -3
- package/conformance/cases/c19-wire-envelope-no-version/expected.json +1 -3
- package/conformance/cases/c21-governance-empty-object-wins/expected.json +1 -3
- package/conformance/cases/c23-mixed-validity-models/expected.json +1 -3
- package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +1 -3
- package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +1 -3
- package/conformance/cases/c26-hosts-array-1024/expected.json +1 -3
- package/conformance/cases/c27-hosts-array-1025/expected.json +1 -3
- package/conformance/cases/c28-large-document-over-1mb/expected.json +1 -3
- package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +1 -3
- package/conformance/cases/c30-version-triple-a-first/expected.json +1 -3
- package/conformance/cases/c31-version-triple-b-changed/expected.json +1 -3
- package/conformance/cases/c32-version-triple-a-restored/expected.json +1 -3
- package/conformance/cases/c33-limits-nested-carried-key/expected.json +1 -3
- package/conformance/cases/c34-skills-content-vs-manifest/expected.json +1 -3
- package/conformance/cases/c35-portable-domain-file-is-null/expected.json +1 -3
- package/conformance/cases/c36-skills-entry-without-content/expected.json +1 -3
- package/conformance/cases/c37-skills-field-null/expected.json +1 -3
- package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +1 -3
- package/conformance/cases/c39-hidden-file-in-config-d/expected.json +1 -3
- package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +1 -3
- package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +1 -3
- package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +1 -3
- package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +1 -3
- package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +1 -3
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +1 -3
- package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/expected.json +1 -3
- package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/expected.json +1 -3
- package/conformance/cases/c48-s935-model-headers-transported/expected.json +1 -3
- package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +1 -3
- package/conformance/cases/c50-s784-lane-host-legal/expected.json +1 -3
- package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +1 -3
- package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +1 -3
- package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +1 -3
- package/conformance/cases/c54-s784-lane-adb-legal/expected.json +1 -3
- package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +1 -3
- package/conformance/cases/c56-s784-lane-device-legal/expected.json +1 -3
- package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +1 -3
- package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +1 -3
- package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +1 -3
- package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +1 -3
- package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +1 -3
- package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +1 -3
- package/conformance/cases/c63-s784-host-two-replicas/expected.json +1 -3
- package/conformance/cases/c64-s784-multitenant-local-store/expected.json +1 -3
- package/conformance/cases/c65-s784-pg-without-db/expected.json +1 -3
- package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +1 -3
- package/conformance/cases/c67-s784-retired-field-loud/expected.json +1 -3
- package/conformance/cases/c68-s784-device-local-store/expected.json +1 -3
- package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +1 -3
- package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +1 -3
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/expected.json +1 -3
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +1 -3
- package/conformance/cases/c73-projects-extended-registration-reads/expected.json +1 -3
- package/conformance/cases/c74-projects-over-bound-refused/expected.json +1 -3
- package/conformance/cases/c75-projects-patch-remote-uniqueness/expected.json +1 -3
- package/conformance/cases/c76-projects-entry-salvage-nested/expected.json +1 -3
- package/conformance/cases/c77-prompts-center-artifact-carried/expected.json +252 -0
- package/conformance/cases/c77-prompts-center-artifact-carried/input.json +93 -0
- package/conformance/cases/c77-prompts-center-artifact-carried/meta.json +5 -0
- package/conformance/cases/c78-prompts-empty-bindings-refused/expected.json +82 -0
- package/conformance/cases/c78-prompts-empty-bindings-refused/input.json +11 -0
- package/conformance/cases/c78-prompts-empty-bindings-refused/meta.json +5 -0
- package/conformance/cases/c79-prompts-bad-worker-artifact-not-salvaged/expected.json +82 -0
- package/conformance/cases/c79-prompts-bad-worker-artifact-not-salvaged/input.json +129 -0
- package/conformance/cases/c79-prompts-bad-worker-artifact-not-salvaged/meta.json +5 -0
- package/conformance/cases/c80-prompts-wire-v2-verbatim/expected.json +195 -0
- package/conformance/cases/c80-prompts-wire-v2-verbatim/input.json +65 -0
- package/conformance/cases/c80-prompts-wire-v2-verbatim/meta.json +5 -0
- package/conformance/cases/c81-prompts-declaration-bindings-refused/expected.json +82 -0
- package/conformance/cases/c81-prompts-declaration-bindings-refused/input.json +23 -0
- package/conformance/cases/c81-prompts-declaration-bindings-refused/meta.json +5 -0
- package/conformance/cases/c82-prompts-v1-catalog-refused/expected.json +82 -0
- package/conformance/cases/c82-prompts-v1-catalog-refused/input.json +35 -0
- package/conformance/cases/c82-prompts-v1-catalog-refused/meta.json +5 -0
- package/conformance/cases/c83-prompts-bad-overlay-not-salvaged/expected.json +82 -0
- package/conformance/cases/c83-prompts-bad-overlay-not-salvaged/input.json +56 -0
- package/conformance/cases/c83-prompts-bad-overlay-not-salvaged/meta.json +5 -0
- package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +1 -3
- package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +1 -3
- package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +1 -3
- package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +1 -3
- package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +1 -3
- package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +1 -3
- package/conformance/cases/cli04-env-value-with-newline/expected.json +1 -3
- package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +1 -3
- package/conformance/cases/t01-absent-domain-key-not-null/expected.json +1 -3
- package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +1 -3
- package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +1 -3
- package/conformance/cases/z01-every-domain-empty-object/expected.json +2 -6
- package/conformance/cases/z02-every-domain-unknown-key/expected.json +2 -6
- package/dist/config-fns.d.ts +7 -3
- package/dist/config-fns.js +19 -16
- package/dist/conformance/derive.js +2 -2
- package/dist/hooks-config.d.ts +9 -1
- package/dist/hooks-config.js +23 -15
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp-request-entry.d.ts +93 -0
- package/dist/mcp-request-entry.js +157 -0
- package/dist/migration-receipt.d.ts +1 -1
- package/dist/migration-receipt.js +1 -1
- package/dist/node.d.ts +0 -1
- package/dist/node.js +0 -1
- package/dist/one-time-migrations.d.ts +1 -1
- package/dist/one-time-migrations.js +39 -0
- package/dist/safety-merge-spec.d.ts +6 -6
- package/dist/types.d.ts +438 -355
- package/dist/types.js +161 -136
- package/dist/url-shape.d.ts +8 -0
- package/dist/url-shape.js +10 -0
- package/docs/API-SURFACE.md +1409 -0
- package/docs/CC-DIVERGENCE.md +42 -0
- package/docs/CONFORMANCE.md +344 -0
- package/docs/CONSUMER-INTEGRATION.md +283 -0
- package/docs/openapi-auth.yaml +357 -0
- package/docs/openapi-scopes.yaml +281 -0
- package/llms-full.txt +659 -0
- package/llms.txt +179 -0
- package/package.json +5 -2
- package/dist/prompts-digest.d.ts +0 -16
- package/dist/prompts-digest.js +0 -49
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# OpenAPI 3 contract for the registry /api/v1/auth/* face ONLY (REGISTRY-REBUILD-DESIGN §2, M1 shipped).
|
|
2
|
+
# Source of truth for the shapes is src/api/auth.ts (zod) — this document mirrors it for docs/third-party
|
|
3
|
+
# integration. When design §3's scripts/gen-openapi.mjs lands, this file becomes generated; until then it
|
|
4
|
+
# is maintained by hand next to the zod module. Registry JWKS (the auth-bridge key source) is included
|
|
5
|
+
# because the §5 bridge depends on it; the rest of the registry API is deliberately NOT documented here.
|
|
6
|
+
openapi: 3.0.3
|
|
7
|
+
info:
|
|
8
|
+
title: web-admin auth API
|
|
9
|
+
description: >
|
|
10
|
+
RFC 8628 device authorization grant + rotating refresh tokens + logout + browser consent,
|
|
11
|
+
plus the public JWKS a worker uses to verify the minted access JWTs (auth bridge, design §5).
|
|
12
|
+
Contract frozen in @sema-agent/settings-schema `api/auth` / `api/auth-bridge`.
|
|
13
|
+
version: 0.2.0
|
|
14
|
+
paths:
|
|
15
|
+
/api/v1/auth/device/code:
|
|
16
|
+
post:
|
|
17
|
+
operationId: deviceCode
|
|
18
|
+
summary: Open an RFC 8628 device-authorization handshake
|
|
19
|
+
description: >
|
|
20
|
+
Unauthenticated by design — the client has no credentials yet. Returns the polling secret
|
|
21
|
+
(device_code, hash-persisted server-side, 15min TTL) and the human approval code (user_code).
|
|
22
|
+
requestBody:
|
|
23
|
+
required: false
|
|
24
|
+
content:
|
|
25
|
+
application/json:
|
|
26
|
+
schema: { $ref: "#/components/schemas/DeviceCodeRequest" }
|
|
27
|
+
application/x-www-form-urlencoded:
|
|
28
|
+
schema: { $ref: "#/components/schemas/DeviceCodeRequest" }
|
|
29
|
+
responses:
|
|
30
|
+
"200":
|
|
31
|
+
description: Handshake opened.
|
|
32
|
+
content:
|
|
33
|
+
application/json:
|
|
34
|
+
schema: { $ref: "#/components/schemas/DeviceCodeResponse" }
|
|
35
|
+
"400":
|
|
36
|
+
$ref: "#/components/responses/OAuthError"
|
|
37
|
+
"503":
|
|
38
|
+
description: SSO not configured on this registry (error=server_error).
|
|
39
|
+
content:
|
|
40
|
+
application/json:
|
|
41
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
42
|
+
/api/v1/auth/device/token:
|
|
43
|
+
post:
|
|
44
|
+
operationId: deviceToken
|
|
45
|
+
summary: Poll for tokens (RFC 8628 §3.4)
|
|
46
|
+
description: >
|
|
47
|
+
The client polls every `interval` seconds. 400 error codes while pending:
|
|
48
|
+
authorization_pending (keep polling) / slow_down (add 5s to the interval) /
|
|
49
|
+
expired_token (restart the flow) / access_denied (user denied — stop).
|
|
50
|
+
On approval the handshake is consumed atomically — the 200 is returned exactly once.
|
|
51
|
+
requestBody:
|
|
52
|
+
required: true
|
|
53
|
+
content:
|
|
54
|
+
application/json:
|
|
55
|
+
schema: { $ref: "#/components/schemas/DeviceTokenRequest" }
|
|
56
|
+
application/x-www-form-urlencoded:
|
|
57
|
+
schema: { $ref: "#/components/schemas/DeviceTokenRequest" }
|
|
58
|
+
responses:
|
|
59
|
+
"200":
|
|
60
|
+
description: Approved — tokens minted (once).
|
|
61
|
+
content:
|
|
62
|
+
application/json:
|
|
63
|
+
schema: { $ref: "#/components/schemas/TokenGrantResponse" }
|
|
64
|
+
"400":
|
|
65
|
+
$ref: "#/components/responses/OAuthError"
|
|
66
|
+
"503":
|
|
67
|
+
description: SSO not configured (error=server_error).
|
|
68
|
+
content:
|
|
69
|
+
application/json:
|
|
70
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
71
|
+
/api/v1/auth/token/refresh:
|
|
72
|
+
post:
|
|
73
|
+
operationId: tokenRefresh
|
|
74
|
+
summary: Rotate a refresh token (silent access-token renewal)
|
|
75
|
+
description: >
|
|
76
|
+
Strict single-use rotation — verify, revoke the old grant, issue a new grant + new access JWT.
|
|
77
|
+
A replayed or raced old refresh token answers invalid_grant. Clients MUST persist the returned
|
|
78
|
+
refresh_token before discarding the old one.
|
|
79
|
+
requestBody:
|
|
80
|
+
required: true
|
|
81
|
+
content:
|
|
82
|
+
application/json:
|
|
83
|
+
schema: { $ref: "#/components/schemas/TokenRefreshRequest" }
|
|
84
|
+
application/x-www-form-urlencoded:
|
|
85
|
+
schema: { $ref: "#/components/schemas/TokenRefreshRequest" }
|
|
86
|
+
responses:
|
|
87
|
+
"200":
|
|
88
|
+
description: Rotated — same shape as the device token success.
|
|
89
|
+
content:
|
|
90
|
+
application/json:
|
|
91
|
+
schema: { $ref: "#/components/schemas/TokenGrantResponse" }
|
|
92
|
+
"400":
|
|
93
|
+
description: invalid_grant (unknown/expired/revoked/replayed — no detail) or invalid_request.
|
|
94
|
+
content:
|
|
95
|
+
application/json:
|
|
96
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
97
|
+
"503":
|
|
98
|
+
description: SSO not configured (error=server_error).
|
|
99
|
+
content:
|
|
100
|
+
application/json:
|
|
101
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
102
|
+
/api/v1/auth/logout:
|
|
103
|
+
post:
|
|
104
|
+
operationId: logout
|
|
105
|
+
summary: Revoke a refresh grant (CLI logout)
|
|
106
|
+
description: >
|
|
107
|
+
Always 200 {ok:true} whether or not the token matched a live grant (RFC 7009 — no token-existence
|
|
108
|
+
disclosure). The already-minted access JWT stays valid until its 1h exp; revocation kills the
|
|
109
|
+
renewal path. 400 only for a malformed request.
|
|
110
|
+
requestBody:
|
|
111
|
+
required: true
|
|
112
|
+
content:
|
|
113
|
+
application/json:
|
|
114
|
+
schema: { $ref: "#/components/schemas/LogoutRequest" }
|
|
115
|
+
application/x-www-form-urlencoded:
|
|
116
|
+
schema: { $ref: "#/components/schemas/LogoutRequest" }
|
|
117
|
+
responses:
|
|
118
|
+
"200":
|
|
119
|
+
description: Revoked (or no-op).
|
|
120
|
+
content:
|
|
121
|
+
application/json:
|
|
122
|
+
schema: { $ref: "#/components/schemas/LogoutResponse" }
|
|
123
|
+
"400":
|
|
124
|
+
$ref: "#/components/responses/OAuthError"
|
|
125
|
+
/api/v1/auth/device/approve:
|
|
126
|
+
get:
|
|
127
|
+
operationId: deviceApproveLookup
|
|
128
|
+
summary: Consent-screen lookup (does this user_code name a live handshake?)
|
|
129
|
+
description: Session-cookie-gated (the /activate page). Does NOT approve.
|
|
130
|
+
security:
|
|
131
|
+
- sessionCookie: []
|
|
132
|
+
parameters:
|
|
133
|
+
- name: user_code
|
|
134
|
+
in: query
|
|
135
|
+
required: true
|
|
136
|
+
schema: { type: string }
|
|
137
|
+
description: Case-insensitive; dashes/spaces optional (normalized server-side to XXXX-XXXX).
|
|
138
|
+
responses:
|
|
139
|
+
"200":
|
|
140
|
+
description: Live handshake found.
|
|
141
|
+
content:
|
|
142
|
+
application/json:
|
|
143
|
+
schema: { $ref: "#/components/schemas/DeviceApproveLookupResponse" }
|
|
144
|
+
"400":
|
|
145
|
+
description: invalid_request (implausible code) or invalid_grant (unknown/expired).
|
|
146
|
+
content:
|
|
147
|
+
application/json:
|
|
148
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
149
|
+
"401":
|
|
150
|
+
description: Not signed in (no valid session cookie).
|
|
151
|
+
post:
|
|
152
|
+
operationId: deviceApprove
|
|
153
|
+
summary: Approve a pending handshake with the SESSION actor's identity
|
|
154
|
+
description: >
|
|
155
|
+
JSON-only content type (cross-site-request defence — a cross-site form cannot send
|
|
156
|
+
application/json without a CORS preflight). The approving principal/role come from the session
|
|
157
|
+
cookie exclusively, never from the body.
|
|
158
|
+
security:
|
|
159
|
+
- sessionCookie: []
|
|
160
|
+
requestBody:
|
|
161
|
+
required: true
|
|
162
|
+
content:
|
|
163
|
+
application/json:
|
|
164
|
+
schema: { $ref: "#/components/schemas/DeviceApproveRequest" }
|
|
165
|
+
responses:
|
|
166
|
+
"200":
|
|
167
|
+
description: Approved — the polling client's next device/token call succeeds.
|
|
168
|
+
content:
|
|
169
|
+
application/json:
|
|
170
|
+
schema: { $ref: "#/components/schemas/DeviceApproveResponse" }
|
|
171
|
+
"400":
|
|
172
|
+
description: invalid_request (bad body/content-type) or invalid_grant (unknown/expired/already-approved).
|
|
173
|
+
content:
|
|
174
|
+
application/json:
|
|
175
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
176
|
+
"401":
|
|
177
|
+
description: Not signed in.
|
|
178
|
+
/api/auth/sso/jwks:
|
|
179
|
+
get:
|
|
180
|
+
operationId: jwks
|
|
181
|
+
summary: Public JWKS — the auth-bridge key source (design §5)
|
|
182
|
+
description: >
|
|
183
|
+
Public verification key(s) ONLY (RFC 7517 §5 — no auth). Workers verify registry-minted user
|
|
184
|
+
access JWTs against these keys per the auth-bridge rules (RS256 only, kid match, kid-miss single
|
|
185
|
+
forced refresh with ≥60s throttle, 24h offline grace then fail-closed). `{keys: []}` is a valid
|
|
186
|
+
response (SSO disabled) — every token then fails verification.
|
|
187
|
+
responses:
|
|
188
|
+
"200":
|
|
189
|
+
description: The key set (may be empty).
|
|
190
|
+
content:
|
|
191
|
+
application/json:
|
|
192
|
+
schema: { $ref: "#/components/schemas/WebAdminJwks" }
|
|
193
|
+
components:
|
|
194
|
+
securitySchemes:
|
|
195
|
+
sessionCookie:
|
|
196
|
+
type: apiKey
|
|
197
|
+
in: cookie
|
|
198
|
+
name: session
|
|
199
|
+
description: The registry's existing HMAC-signed browser session (web login).
|
|
200
|
+
responses:
|
|
201
|
+
OAuthError:
|
|
202
|
+
description: RFC 6749/8628 error envelope — branch on `error`; `error_description` is diagnostic only.
|
|
203
|
+
content:
|
|
204
|
+
application/json:
|
|
205
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
206
|
+
schemas:
|
|
207
|
+
OAuthErrorResponse:
|
|
208
|
+
type: object
|
|
209
|
+
required: [error]
|
|
210
|
+
properties:
|
|
211
|
+
error:
|
|
212
|
+
type: string
|
|
213
|
+
enum:
|
|
214
|
+
- authorization_pending
|
|
215
|
+
- slow_down
|
|
216
|
+
- expired_token
|
|
217
|
+
- access_denied
|
|
218
|
+
- invalid_request
|
|
219
|
+
- unsupported_grant_type
|
|
220
|
+
- invalid_grant
|
|
221
|
+
- server_error
|
|
222
|
+
error_description:
|
|
223
|
+
type: string
|
|
224
|
+
DeviceCodeRequest:
|
|
225
|
+
type: object
|
|
226
|
+
properties:
|
|
227
|
+
client_name:
|
|
228
|
+
type: string
|
|
229
|
+
maxLength: 200
|
|
230
|
+
description: NON-secret display hint for the consent screen; falls back to the User-Agent.
|
|
231
|
+
DeviceCodeResponse:
|
|
232
|
+
type: object
|
|
233
|
+
required: [device_code, user_code, verification_uri, expires_in, interval]
|
|
234
|
+
properties:
|
|
235
|
+
device_code:
|
|
236
|
+
type: string
|
|
237
|
+
description: ≥256-bit opaque polling secret — returned once, hash-persisted server-side. Never log.
|
|
238
|
+
user_code:
|
|
239
|
+
type: string
|
|
240
|
+
description: Human approval code, "XXXX-XXXX" (base-20, no vowels/lookalikes).
|
|
241
|
+
verification_uri:
|
|
242
|
+
type: string
|
|
243
|
+
description: The /activate consent page.
|
|
244
|
+
verification_uri_complete:
|
|
245
|
+
type: string
|
|
246
|
+
description: verification_uri with ?user_code= pre-filled. OPTIONAL (RFC 8628 §3.2) — absent ⇒ show verification_uri + user_code; never an empty string.
|
|
247
|
+
expires_in:
|
|
248
|
+
type: integer
|
|
249
|
+
description: Seconds until the handshake expires (900).
|
|
250
|
+
interval:
|
|
251
|
+
type: integer
|
|
252
|
+
description: Minimum seconds between polls (5).
|
|
253
|
+
DeviceTokenRequest:
|
|
254
|
+
type: object
|
|
255
|
+
required: [grant_type, device_code]
|
|
256
|
+
properties:
|
|
257
|
+
grant_type:
|
|
258
|
+
type: string
|
|
259
|
+
enum: ["urn:ietf:params:oauth:grant-type:device_code"]
|
|
260
|
+
device_code:
|
|
261
|
+
type: string
|
|
262
|
+
TokenGrantResponse:
|
|
263
|
+
type: object
|
|
264
|
+
required: [access_token, refresh_token, token_type, expires_in]
|
|
265
|
+
properties:
|
|
266
|
+
access_token:
|
|
267
|
+
type: string
|
|
268
|
+
description: "RS256 JWT — claims: sub (principal), iss, iat, exp, role; optional aud. 1h TTL."
|
|
269
|
+
refresh_token:
|
|
270
|
+
type: string
|
|
271
|
+
description: NEW opaque rotating refresh token (30d sliding) — replaces the previous one.
|
|
272
|
+
token_type:
|
|
273
|
+
type: string
|
|
274
|
+
enum: [Bearer]
|
|
275
|
+
expires_in:
|
|
276
|
+
type: integer
|
|
277
|
+
description: Access-token lifetime in seconds (3600).
|
|
278
|
+
TokenRefreshRequest:
|
|
279
|
+
type: object
|
|
280
|
+
required: [refresh_token]
|
|
281
|
+
properties:
|
|
282
|
+
refresh_token:
|
|
283
|
+
type: string
|
|
284
|
+
LogoutRequest:
|
|
285
|
+
type: object
|
|
286
|
+
required: [refresh_token]
|
|
287
|
+
properties:
|
|
288
|
+
refresh_token:
|
|
289
|
+
type: string
|
|
290
|
+
LogoutResponse:
|
|
291
|
+
type: object
|
|
292
|
+
required: [ok]
|
|
293
|
+
properties:
|
|
294
|
+
ok:
|
|
295
|
+
type: boolean
|
|
296
|
+
enum: [true]
|
|
297
|
+
DeviceApproveLookupResponse:
|
|
298
|
+
type: object
|
|
299
|
+
required: [user_code, status, client, expires_at]
|
|
300
|
+
properties:
|
|
301
|
+
user_code:
|
|
302
|
+
type: string
|
|
303
|
+
status:
|
|
304
|
+
type: string
|
|
305
|
+
enum: [pending, approved, denied]
|
|
306
|
+
description: denied is contract-reserved (explicit deny button); M1 ships pending|approved.
|
|
307
|
+
client:
|
|
308
|
+
type: string
|
|
309
|
+
nullable: true
|
|
310
|
+
description: The client_name/User-Agent hint from device/code; null when none.
|
|
311
|
+
expires_at:
|
|
312
|
+
type: number
|
|
313
|
+
description: Handshake expiry, epoch MILLISECONDS.
|
|
314
|
+
DeviceApproveRequest:
|
|
315
|
+
type: object
|
|
316
|
+
required: [user_code]
|
|
317
|
+
properties:
|
|
318
|
+
user_code:
|
|
319
|
+
type: string
|
|
320
|
+
description: Case-insensitive; dashes/spaces optional.
|
|
321
|
+
DeviceApproveResponse:
|
|
322
|
+
type: object
|
|
323
|
+
required: [ok, user_code]
|
|
324
|
+
properties:
|
|
325
|
+
ok:
|
|
326
|
+
type: boolean
|
|
327
|
+
enum: [true]
|
|
328
|
+
user_code:
|
|
329
|
+
type: string
|
|
330
|
+
WebAdminJwks:
|
|
331
|
+
type: object
|
|
332
|
+
required: [keys]
|
|
333
|
+
properties:
|
|
334
|
+
keys:
|
|
335
|
+
type: array
|
|
336
|
+
items:
|
|
337
|
+
type: object
|
|
338
|
+
required: [kty, n, e, kid]
|
|
339
|
+
properties:
|
|
340
|
+
kty:
|
|
341
|
+
type: string
|
|
342
|
+
enum: [RSA]
|
|
343
|
+
n:
|
|
344
|
+
type: string
|
|
345
|
+
description: modulus, base64url
|
|
346
|
+
e:
|
|
347
|
+
type: string
|
|
348
|
+
description: exponent, base64url
|
|
349
|
+
kid:
|
|
350
|
+
type: string
|
|
351
|
+
description: RFC 7638 thumbprint — matches the JWT header kid.
|
|
352
|
+
use:
|
|
353
|
+
type: string
|
|
354
|
+
enum: [sig]
|
|
355
|
+
alg:
|
|
356
|
+
type: string
|
|
357
|
+
enum: [RS256]
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# OpenAPI 3 contract for the registry multi-tenant SCOPE face ONLY (/api/v1/scopes* + /api/v1/auth/scope).
|
|
2
|
+
# Source of truth for the shapes AND the decision semantics is src/api/scopes.ts (zod + pure functions:
|
|
3
|
+
# resolveTokenScope / canSwitchScope / effectiveScopeRole / readTokenScope) — this document mirrors it for
|
|
4
|
+
# docs/third-party integration, exactly like docs/openapi-auth.yaml mirrors src/api/auth.ts. Maintained by
|
|
5
|
+
# hand next to the zod module until gen-openapi lands. The auth face lives in openapi-auth.yaml.
|
|
6
|
+
openapi: 3.0.3
|
|
7
|
+
info:
|
|
8
|
+
title: web-admin scopes API
|
|
9
|
+
description: >
|
|
10
|
+
Multi-tenant scopes: a scope is a tenant boundary {id,name,createdAt} with per-scope memberships
|
|
11
|
+
{scopeId,principal,role} (the existing viewer<editor<publisher<admin ladder — the same principal may
|
|
12
|
+
hold different roles in different scopes). The reserved scope "global" always exists and is the
|
|
13
|
+
default; every access JWT carries a `scope` claim, and a token without one is refused (401) — it is not read as "global".
|
|
14
|
+
Refresh tokens are NOT scope-bound (a refresh re-mints an access token with the claim).
|
|
15
|
+
Instance-level users.role is unchanged; an instance admin administers all scopes and their effective
|
|
16
|
+
role in any scope is always "admin". Contract frozen in @sema-agent/settings-schema `api/scopes`.
|
|
17
|
+
version: 0.3.0
|
|
18
|
+
paths:
|
|
19
|
+
/api/v1/scopes:
|
|
20
|
+
get:
|
|
21
|
+
operationId: listScopes
|
|
22
|
+
summary: List the caller's scopes with their per-scope effective role
|
|
23
|
+
description: >
|
|
24
|
+
Auth: bearer access JWT (any scope). Returns the scopes the caller can act in, with the caller's
|
|
25
|
+
EFFECTIVE role in each (effectiveScopeRole — an instance admin sees ALL scopes with role "admin").
|
|
26
|
+
security:
|
|
27
|
+
- bearerJwt: []
|
|
28
|
+
responses:
|
|
29
|
+
"200":
|
|
30
|
+
description: The caller's scopes.
|
|
31
|
+
content:
|
|
32
|
+
application/json:
|
|
33
|
+
schema: { $ref: "#/components/schemas/ScopesListResponse" }
|
|
34
|
+
"401":
|
|
35
|
+
description: Missing/invalid bearer token.
|
|
36
|
+
post:
|
|
37
|
+
operationId: createScope
|
|
38
|
+
summary: Create a scope (instance admin only)
|
|
39
|
+
description: >
|
|
40
|
+
The reserved id "global" is rejected as invalid_request — the default scope pre-exists and is
|
|
41
|
+
neither creatable nor deletable. Duplicate id → invalid_request.
|
|
42
|
+
security:
|
|
43
|
+
- bearerJwt: []
|
|
44
|
+
requestBody:
|
|
45
|
+
required: true
|
|
46
|
+
content:
|
|
47
|
+
application/json:
|
|
48
|
+
schema: { $ref: "#/components/schemas/ScopeCreateRequest" }
|
|
49
|
+
responses:
|
|
50
|
+
"200":
|
|
51
|
+
description: Created.
|
|
52
|
+
content:
|
|
53
|
+
application/json:
|
|
54
|
+
schema: { $ref: "#/components/schemas/Scope" }
|
|
55
|
+
"400":
|
|
56
|
+
$ref: "#/components/responses/OAuthError"
|
|
57
|
+
"401":
|
|
58
|
+
description: Missing/invalid bearer token.
|
|
59
|
+
"403":
|
|
60
|
+
description: Caller is not an instance admin (error=access_denied).
|
|
61
|
+
content:
|
|
62
|
+
application/json:
|
|
63
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
64
|
+
/api/v1/auth/scope:
|
|
65
|
+
post:
|
|
66
|
+
operationId: switchScope
|
|
67
|
+
summary: Mint a new access token bound to another scope
|
|
68
|
+
description: >
|
|
69
|
+
Auth: bearer access JWT (any scope). Allowed iff canSwitchScope — the caller is a MEMBER of the
|
|
70
|
+
target scope (any role) or an instance admin (any scope, effective role "admin"). "global" gets no
|
|
71
|
+
special case: a non-member non-admin cannot switch to it. Success mints a FRESH access JWT whose
|
|
72
|
+
`scope` claim = the requested scope and `role` claim = the caller's effective role there
|
|
73
|
+
(effectiveScopeRole); the refresh grant is untouched and NO refresh_token is returned — on refresh
|
|
74
|
+
the scope re-resolves via resolveTokenScope and the client re-switches if needed. Errors are
|
|
75
|
+
OAuth-style: invalid_request (malformed) / access_denied (not a member, not an admin).
|
|
76
|
+
security:
|
|
77
|
+
- bearerJwt: []
|
|
78
|
+
requestBody:
|
|
79
|
+
required: true
|
|
80
|
+
content:
|
|
81
|
+
application/json:
|
|
82
|
+
schema: { $ref: "#/components/schemas/ScopeSwitchRequest" }
|
|
83
|
+
responses:
|
|
84
|
+
"200":
|
|
85
|
+
description: New scoped access token minted.
|
|
86
|
+
content:
|
|
87
|
+
application/json:
|
|
88
|
+
schema: { $ref: "#/components/schemas/ScopeSwitchResponse" }
|
|
89
|
+
"400":
|
|
90
|
+
$ref: "#/components/responses/OAuthError"
|
|
91
|
+
"401":
|
|
92
|
+
description: Missing/invalid bearer token.
|
|
93
|
+
"403":
|
|
94
|
+
description: Not a member of the target scope and not an instance admin (error=access_denied).
|
|
95
|
+
content:
|
|
96
|
+
application/json:
|
|
97
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
98
|
+
/api/v1/scopes/{id}/members:
|
|
99
|
+
parameters:
|
|
100
|
+
- name: id
|
|
101
|
+
in: path
|
|
102
|
+
required: true
|
|
103
|
+
schema: { $ref: "#/components/schemas/ScopeIdString" }
|
|
104
|
+
description: The scope id (slug).
|
|
105
|
+
put:
|
|
106
|
+
operationId: upsertScopeMember
|
|
107
|
+
summary: Upsert a member's role in a scope (instance admin only)
|
|
108
|
+
description: Creates or replaces the {scopeId, principal} membership row with the given role.
|
|
109
|
+
security:
|
|
110
|
+
- bearerJwt: []
|
|
111
|
+
requestBody:
|
|
112
|
+
required: true
|
|
113
|
+
content:
|
|
114
|
+
application/json:
|
|
115
|
+
schema: { $ref: "#/components/schemas/ScopeMemberUpsertRequest" }
|
|
116
|
+
responses:
|
|
117
|
+
"200":
|
|
118
|
+
description: The stored membership row.
|
|
119
|
+
content:
|
|
120
|
+
application/json:
|
|
121
|
+
schema: { $ref: "#/components/schemas/ScopeMember" }
|
|
122
|
+
"400":
|
|
123
|
+
$ref: "#/components/responses/OAuthError"
|
|
124
|
+
"401":
|
|
125
|
+
description: Missing/invalid bearer token.
|
|
126
|
+
"403":
|
|
127
|
+
description: Caller is not an instance admin (error=access_denied).
|
|
128
|
+
content:
|
|
129
|
+
application/json:
|
|
130
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
131
|
+
"404":
|
|
132
|
+
description: Unknown scope id.
|
|
133
|
+
delete:
|
|
134
|
+
operationId: deleteScopeMember
|
|
135
|
+
summary: Remove a member from a scope (instance admin only)
|
|
136
|
+
description: Idempotent — removing a non-member still answers 200 {ok:true}.
|
|
137
|
+
security:
|
|
138
|
+
- bearerJwt: []
|
|
139
|
+
requestBody:
|
|
140
|
+
required: true
|
|
141
|
+
content:
|
|
142
|
+
application/json:
|
|
143
|
+
schema: { $ref: "#/components/schemas/ScopeMemberDeleteRequest" }
|
|
144
|
+
responses:
|
|
145
|
+
"200":
|
|
146
|
+
description: Removed (or no-op).
|
|
147
|
+
content:
|
|
148
|
+
application/json:
|
|
149
|
+
schema: { $ref: "#/components/schemas/ScopeMemberDeleteResponse" }
|
|
150
|
+
"400":
|
|
151
|
+
$ref: "#/components/responses/OAuthError"
|
|
152
|
+
"401":
|
|
153
|
+
description: Missing/invalid bearer token.
|
|
154
|
+
"403":
|
|
155
|
+
description: Caller is not an instance admin (error=access_denied).
|
|
156
|
+
content:
|
|
157
|
+
application/json:
|
|
158
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
159
|
+
"404":
|
|
160
|
+
description: Unknown scope id.
|
|
161
|
+
components:
|
|
162
|
+
securitySchemes:
|
|
163
|
+
bearerJwt:
|
|
164
|
+
type: http
|
|
165
|
+
scheme: bearer
|
|
166
|
+
bearerFormat: JWT
|
|
167
|
+
description: >
|
|
168
|
+
web-admin-minted RS256 access JWT (api/auth). Carries the `scope` claim (api/scopes); a token
|
|
169
|
+
without it is refused (readTokenScope ⇒ scope_claim_missing).
|
|
170
|
+
responses:
|
|
171
|
+
OAuthError:
|
|
172
|
+
description: OAuth-style error envelope — branch on `error`; `error_description` is diagnostic only.
|
|
173
|
+
content:
|
|
174
|
+
application/json:
|
|
175
|
+
schema: { $ref: "#/components/schemas/OAuthErrorResponse" }
|
|
176
|
+
schemas:
|
|
177
|
+
OAuthErrorResponse:
|
|
178
|
+
type: object
|
|
179
|
+
required: [error]
|
|
180
|
+
properties:
|
|
181
|
+
error:
|
|
182
|
+
type: string
|
|
183
|
+
enum:
|
|
184
|
+
- invalid_request
|
|
185
|
+
- access_denied
|
|
186
|
+
- invalid_grant
|
|
187
|
+
- server_error
|
|
188
|
+
error_description:
|
|
189
|
+
type: string
|
|
190
|
+
ScopeIdString:
|
|
191
|
+
type: string
|
|
192
|
+
pattern: "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$"
|
|
193
|
+
description: Slug, 1-64 chars, lowercase alnum + inner dashes. "global" is the reserved default scope.
|
|
194
|
+
ScopeRole:
|
|
195
|
+
type: string
|
|
196
|
+
enum: [viewer, editor, publisher, admin]
|
|
197
|
+
description: The existing 4-level RBAC ladder, applied PER SCOPE.
|
|
198
|
+
Scope:
|
|
199
|
+
type: object
|
|
200
|
+
required: [id, name, createdAt]
|
|
201
|
+
properties:
|
|
202
|
+
id: { $ref: "#/components/schemas/ScopeIdString" }
|
|
203
|
+
name:
|
|
204
|
+
type: string
|
|
205
|
+
maxLength: 200
|
|
206
|
+
createdAt:
|
|
207
|
+
type: number
|
|
208
|
+
description: Creation time, epoch milliseconds.
|
|
209
|
+
ScopeMember:
|
|
210
|
+
type: object
|
|
211
|
+
required: [scopeId, principal, role]
|
|
212
|
+
properties:
|
|
213
|
+
scopeId: { $ref: "#/components/schemas/ScopeIdString" }
|
|
214
|
+
principal:
|
|
215
|
+
type: string
|
|
216
|
+
description: Same principal string as the JWT sub / x-agent-principal pipeline.
|
|
217
|
+
role: { $ref: "#/components/schemas/ScopeRole" }
|
|
218
|
+
ScopesListResponse:
|
|
219
|
+
type: object
|
|
220
|
+
required: [scopes]
|
|
221
|
+
properties:
|
|
222
|
+
scopes:
|
|
223
|
+
type: array
|
|
224
|
+
items:
|
|
225
|
+
type: object
|
|
226
|
+
required: [id, name, role]
|
|
227
|
+
properties:
|
|
228
|
+
id: { $ref: "#/components/schemas/ScopeIdString" }
|
|
229
|
+
name: { type: string }
|
|
230
|
+
role:
|
|
231
|
+
allOf: [{ $ref: "#/components/schemas/ScopeRole" }]
|
|
232
|
+
description: The CALLER's effective role in this scope (instance admin ⇒ "admin").
|
|
233
|
+
ScopeSwitchRequest:
|
|
234
|
+
type: object
|
|
235
|
+
required: [scope]
|
|
236
|
+
properties:
|
|
237
|
+
scope: { $ref: "#/components/schemas/ScopeIdString" }
|
|
238
|
+
ScopeSwitchResponse:
|
|
239
|
+
type: object
|
|
240
|
+
required: [access_token, token_type, expires_in, scope, role]
|
|
241
|
+
properties:
|
|
242
|
+
access_token:
|
|
243
|
+
type: string
|
|
244
|
+
description: Fresh RS256 JWT with the `scope` claim set. Never log.
|
|
245
|
+
token_type:
|
|
246
|
+
type: string
|
|
247
|
+
enum: ["Bearer"]
|
|
248
|
+
expires_in:
|
|
249
|
+
type: integer
|
|
250
|
+
description: Access-token lifetime in seconds (3600 — same mint as api/auth).
|
|
251
|
+
scope: { $ref: "#/components/schemas/ScopeIdString" }
|
|
252
|
+
role: { $ref: "#/components/schemas/ScopeRole" }
|
|
253
|
+
ScopeCreateRequest:
|
|
254
|
+
type: object
|
|
255
|
+
required: [id, name]
|
|
256
|
+
properties:
|
|
257
|
+
id:
|
|
258
|
+
allOf: [{ $ref: "#/components/schemas/ScopeIdString" }]
|
|
259
|
+
description: Must not be a reserved id ("global").
|
|
260
|
+
name:
|
|
261
|
+
type: string
|
|
262
|
+
minLength: 1
|
|
263
|
+
maxLength: 200
|
|
264
|
+
ScopeMemberUpsertRequest:
|
|
265
|
+
type: object
|
|
266
|
+
required: [principal, role]
|
|
267
|
+
properties:
|
|
268
|
+
principal: { type: string }
|
|
269
|
+
role: { $ref: "#/components/schemas/ScopeRole" }
|
|
270
|
+
ScopeMemberDeleteRequest:
|
|
271
|
+
type: object
|
|
272
|
+
required: [principal]
|
|
273
|
+
properties:
|
|
274
|
+
principal: { type: string }
|
|
275
|
+
ScopeMemberDeleteResponse:
|
|
276
|
+
type: object
|
|
277
|
+
required: [ok]
|
|
278
|
+
properties:
|
|
279
|
+
ok:
|
|
280
|
+
type: boolean
|
|
281
|
+
enum: [true]
|