@mantyx/sdk 0.10.1 → 0.12.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/docs/oauth.md DELETED
@@ -1,356 +0,0 @@
1
- # OAuth 2.0 in MANTYX
2
-
3
- MANTYX exposes an OAuth 2.0 authorization server at **`/api/oauth/...`** that
4
- issues access tokens accepted on every existing API surface
5
- (`/api/v1`, `/api/a2a`, `/mcp`) plus a small identity endpoint
6
- (`/api/oauth/userinfo`).
7
-
8
- OAuth tokens are a **drop-in alternative to workspace API keys** — same
9
- HTTP contract, same `Authorization: Bearer …` header, same per-agent
10
- allowlist semantics. The only thing that changes is that the token
11
- carries **scopes**: per-route permissions you grant at consent time
12
- instead of the coarse `ApiKeyUsage` (`mcp` | `developer_api` | `a2a`)
13
- on classic workspace API keys.
14
-
15
- > See `architecture.md` for the full request pipeline and where the
16
- > bearer resolver sits in it.
17
-
18
- ## When to use OAuth vs. an API key
19
-
20
- * **Personal scripts and internal tools you control end-to-end** — keep
21
- using a workspace API key. It's one click to issue, one header to set.
22
- * **Apps that other people sign in to** — register an OAuth application
23
- and run the Authorization Code + PKCE flow. End users approve specific
24
- scopes for a specific workspace. Two visibility modes:
25
- * **Private** — locked to the workspace that registered the app. Only
26
- members of that workspace can authorize. Optionally enable
27
- `client_credentials` for unattended machine-to-machine traffic.
28
- * **Public** — any user can authorize the app and pick the workspace
29
- they want to grant access to on the consent screen.
30
-
31
- ## High-level flow
32
-
33
- ```mermaid
34
- sequenceDiagram
35
- participant App as Your app
36
- participant User as End user
37
- participant Web as MANTYX SPA
38
- participant API as MANTYX API
39
- App->>User: Open /oauth/authorize?...
40
- User->>Web: Sign in (if needed) and review scopes
41
- Web->>API: POST /api/oauth/authorize/decide (approve)
42
- API->>App: 302 redirect_uri?code=…&state=…
43
- App->>API: POST /api/oauth/token (code + verifier)
44
- API->>App: { access_token, refresh_token, scope, expires_in }
45
- App->>API: GET /api/v1/workspaces/{slug}/agents (Bearer token)
46
- API->>App: 200 OK (scope check passes)
47
- ```
48
-
49
- ## Registering an application
50
-
51
- Open **Developer → OAuth apps** (workspace admins only). Both private and
52
- public apps are registered from this page; the Visibility radio decides
53
- which flow you get.
54
-
55
- Provide:
56
-
57
- * **Name** and **description** (shown on the consent screen).
58
- * **Logo URL** (optional).
59
- * **Visibility** — **Private** locks tokens to this workspace; **Public**
60
- lets any signed-in user pick a workspace at consent time.
61
- * **Redirect URIs** — at least one. Allowed schemes:
62
- * `https://…`
63
- * `http://localhost`, `http://127.0.0.1`, or `http://[::1]` (any port,
64
- any path)
65
- * Custom schemes for native apps, e.g. `myapp://callback`.
66
- * **Allowed scopes** — only scopes you check here can be requested by
67
- the application at consent time.
68
- * **Client secret** — every MANTYX OAuth app is a **confidential
69
- client**. The `client_secret` is returned **once** on creation; the
70
- `/token`, `/revoke`, and `/introspect` endpoints all require the
71
- matching value. We do not support PKCE-only public-client
72
- registrations — visibility (private vs. public) only controls *who*
73
- can authorize the app, not whether the app keeps a secret. PKCE is
74
- still mandatory on top of the secret for defense in depth (see
75
- below).
76
- * **Allow `client_credentials` grant** *(private apps only)* — for
77
- unattended machine-to-machine use; not available on public apps
78
- because the token has to be bound to a single workspace at mint time.
79
-
80
- The `client_id` is `mantyx_oa_<id>`. Confidential client secrets are
81
- `mantyx_oas_<secret>`.
82
-
83
- OAuth applications require the **`oauthApps`** feature on the registering
84
- workspace's tier (mirrors the existing `apiKeys` plan check). For public
85
- apps the same gate is also applied to the workspace each end user picks
86
- at consent time, so a free workspace can't host paid features through
87
- a public app authorized for it.
88
-
89
- ## Authorization Code + PKCE (browser, native, server-side)
90
-
91
- Every grant carries **two** client-binding factors:
92
-
93
- * `client_secret` — proves the registered client made the call (every
94
- MANTYX OAuth app is confidential, so this is always required).
95
- * PKCE `code_verifier` — proves the same browser session that started
96
- `/authorize` is finishing the exchange. We accept only `S256` and
97
- reject any token request without a verifier.
98
-
99
- 1. Generate a high-entropy `code_verifier` (43–128 chars, RFC 7636).
100
- 2. Compute `code_challenge = base64url(sha256(code_verifier))` (no
101
- padding).
102
- 3. Send the user to:
103
-
104
- ```text
105
- GET /api/oauth/authorize
106
- ?client_id=mantyx_oa_…
107
- &redirect_uri=<exact registered URI>
108
- &response_type=code
109
- &scope=mantyx.identity:read+agents:read+runs:write
110
- &state=<random per-session token>
111
- &code_challenge=<S256 challenge>
112
- &code_challenge_method=S256
113
- ```
114
-
115
- The MANTYX SPA at `/oauth/authorize` reads the same query, asks the
116
- user to log in if needed, lets them pick the workspace (third-party
117
- apps), pick the agent allow-list (when any of `agents:invoke`,
118
- `runs:write`, `a2a:invoke`, `mcp:connect` are requested), and then
119
- approves or denies.
120
-
121
- 4. On approve we redirect to:
122
-
123
- ```text
124
- <redirect_uri>?code=<auth code>&state=<your state>
125
- ```
126
-
127
- On deny:
128
-
129
- ```text
130
- <redirect_uri>?error=access_denied&state=<your state>
131
- ```
132
-
133
- 5. Exchange the code:
134
-
135
- ```http
136
- POST /api/oauth/token
137
- Content-Type: application/x-www-form-urlencoded
138
-
139
- grant_type=authorization_code
140
- &code=…
141
- &redirect_uri=<exact same URI>
142
- &client_id=mantyx_oa_…
143
- &client_secret=mantyx_oas_…
144
- &code_verifier=<original verifier>
145
- ```
146
-
147
- Response:
148
-
149
- ```json
150
- {
151
- "access_token": "mantyx_at_…",
152
- "token_type": "Bearer",
153
- "expires_in": 3600,
154
- "refresh_token": "mantyx_rt_…",
155
- "scope": "mantyx.identity:read agents:read runs:write"
156
- }
157
- ```
158
-
159
- 6. Use the access token like any other workspace bearer:
160
-
161
- ```http
162
- GET /api/v1/workspaces/<slug>/agents
163
- Authorization: Bearer mantyx_at_…
164
- ```
165
-
166
- `<slug>` must be the workspace the consent was for. OAuth tokens
167
- issued for workspace A return **403** on `/api/v1/workspaces/B/...`.
168
-
169
- 7. **Token lifetimes.**
170
-
171
- * Access tokens live **1 hour** (`expires_in: 3600`).
172
- * Refresh tokens are **persistent and non-rotating**: they never
173
- time-expire. They stop working only when the application access
174
- is explicitly revoked via `/oauth/revoke` (with the refresh
175
- token), `DELETE /api/oauth/grants/:id`, or deletion of the
176
- OAuth application itself.
177
- * Calling `grant_type=refresh_token` mints a brand-new short-lived
178
- access token and **echoes back the same refresh token** the
179
- client already holds. The previous access tokens are **not**
180
- revoked — multiple backend workers can mint live access tokens
181
- off a shared refresh without invalidating each other's chains.
182
-
183
- ```http
184
- POST /api/oauth/token
185
-
186
- grant_type=refresh_token
187
- &refresh_token=mantyx_rt_…
188
- &client_id=mantyx_oa_…
189
- &client_secret=mantyx_oas_…
190
- &scope=runs:write # optional narrowing (must be a subset)
191
- ```
192
-
193
- ```json
194
- {
195
- "access_token": "mantyx_at_…",
196
- "token_type": "Bearer",
197
- "expires_in": 3600,
198
- "refresh_token": "<same value the client just sent>",
199
- "scope": "runs:write"
200
- }
201
- ```
202
-
203
- Clients should persist the refresh token once at first sign-in
204
- (treat it as long-lived) and only refresh the access token from
205
- it as needed.
206
-
207
- 8. **Revoke (RFC 7009).**
208
-
209
- ```http
210
- POST /api/oauth/revoke
211
-
212
- token=<access or refresh token>
213
- &client_id=mantyx_oa_…
214
- &client_secret=mantyx_oas_…
215
- ```
216
-
217
- Always returns `200`, even when the token is unknown — by design.
218
-
219
- * Revoking an **access token** kills only that single access
220
- token. Other access tokens minted from the same refresh keep
221
- working until they expire (or until the refresh is revoked).
222
- * Revoking a **refresh token** kills the refresh and *every* live
223
- access token tied to its grant in one shot.
224
-
225
- ## Client credentials (private workspace apps, machine-to-machine)
226
-
227
- Private workspace applications with `allowsClientCredentials: true` can
228
- request a token without a user:
229
-
230
- ```http
231
- POST /api/oauth/token
232
-
233
- grant_type=client_credentials
234
- &client_id=mantyx_oa_…
235
- &client_secret=mantyx_oas_…
236
- &scope=runs:write+agents:invoke # optional, must be a subset of allowedScopes
237
- ```
238
-
239
- The token's `tenantId` is the application's owning workspace and its
240
- agent allow-list defaults to every non-system agent in that workspace
241
- (no end-user consent screen). Use this for cron jobs, internal services
242
- and partner integrations where there is no end user.
243
-
244
- ## "Sign in with MANTYX"
245
-
246
- There is **no OIDC** today. The access token is enough:
247
-
248
- 1. Run the auth-code + PKCE flow with `scope=mantyx.identity:read`.
249
- 2. Call:
250
-
251
- ```http
252
- GET /api/oauth/userinfo
253
- Authorization: Bearer mantyx_at_…
254
- ```
255
-
256
- Response:
257
-
258
- ```json
259
- {
260
- "sub": "<user id>",
261
- "email": "user@example.com",
262
- "workspace": { "id": "…", "slug": "…", "name": "…" }
263
- }
264
- ```
265
-
266
- `/api/auth/me` continues to accept the user's web JWT as before; both
267
- paths can be used to bootstrap session info for "Sign in with MANTYX"
268
- clients.
269
-
270
- ## Scope catalog
271
-
272
- Defined in `packages/api/src/oauth/scopes.ts` and mirrored by the SPA
273
- in `packages/web/src/lib/oauthScopes.ts`. The catalog is also expressed
274
- in the OpenAPI spec at `packages/api/openapi/developer-v1.yaml`.
275
-
276
- | Scope | Purpose |
277
- | --- | --- |
278
- | `mantyx.identity:read` | `/api/oauth/userinfo` and `/api/auth/me`. |
279
- | `agents:read` | `GET /api/v1/.../agents`. |
280
- | `agents:write` | Reserved for future agent CRUD on the Developer API. |
281
- | `agents:invoke` | Run an agent — required by ephemeral runs, agent sessions, A2A invoke. |
282
- | `sessions:read` / `sessions:write` | Ephemeral SDK agent sessions. |
283
- | `runs:read` / `runs:write` | Read run snapshots and SSE streams; start, cancel, submit tool-results. |
284
- | `models:read` | `GET /api/v1/.../models`. |
285
- | `tools:read` / `tools:write` | List/manage workspace tools. |
286
- | `schedules:read` / `schedules:write` | List/manage cron schedules and trigger them manually. |
287
- | `inbounds:read` / `inbounds:write` | List/manage inbound webhooks/email configs. |
288
- | `plugins:read` | List installed plugins for the workspace. |
289
- | `hive:read` / `hive:write` | Workspace Hive objects. |
290
- | `a2a:discovery` | `GET /api/a2a/{slug}/discovery`. |
291
- | `a2a:invoke` | Send Agent2Agent JSON-RPC requests (also requires `agents:invoke`). |
292
- | `mcp:connect` | Open MCP Streamable HTTP sessions. |
293
-
294
- `runs:write`, `agents:invoke`, `a2a:invoke`, and `mcp:connect` participate
295
- in the **agent allow-list** that the consent screen surfaces. An empty
296
- list expands to "every non-system agent in the workspace" at request
297
- time — same semantics as today's `WorkspaceApiKey.agentIds`.
298
-
299
- ## Redirect URI rules
300
-
301
- * Exact-match comparison (case-sensitive scheme/host, fragment-stripped).
302
- Trailing slashes are significant.
303
- * Loopback HTTP is allowed without TLS for localhost development.
304
- * Custom schemes are allowed for native apps; pick a scheme you control
305
- (e.g. `com.example.myapp://callback`).
306
- * `redirect_uri` on `/api/oauth/token` must equal the value used at
307
- `/api/oauth/authorize`.
308
-
309
- ## Error model
310
-
311
- | Where | Body |
312
- | --- | --- |
313
- | `/authorize` query validation | `{ "error": "Invalid authorize request", "details": {...} }` |
314
- | Unknown / unauthorized client | `401 { "error": "invalid_client" }` |
315
- | PKCE failure, expired/used code, redirect mismatch | `400 { "error": "invalid_grant" }` |
316
- | Insufficient scope on a Developer API call | `403 { "error": "insufficient_scope", "required": ["..."] }` |
317
- | Wrong workspace in URL | `403 { "error": "wrong_workspace", "correctSlug": "..." }` |
318
- | Plan does not include OAuth apps | `403 { "error": "...", "code": "oauth_apps_plan" }` |
319
-
320
- ## Token format
321
-
322
- * Access tokens: `mantyx_at_<32-byte url-safe random>`.
323
- * Refresh tokens: `mantyx_rt_<32-byte url-safe random>`.
324
- * Client ids: `mantyx_oa_<id>`.
325
- * Client secrets: `mantyx_oas_<secret>`.
326
-
327
- Stored as **SHA-256 with HMAC** (rate-friendly), with a 12-character
328
- prefix index for fast lookups (mirrors today's
329
- `WorkspaceApiKey.keyPrefix`).
330
-
331
- ## Token lifetimes & lifecycle
332
-
333
- | Token | Lifetime | How it ends |
334
- | --- | --- | --- |
335
- | **Access token** | 1 hour (`expires_in: 3600`). | Time-expires; or revoked via `/oauth/revoke`, refresh-token revocation, grant deletion, or app deletion. |
336
- | **Refresh token** | **No time-based expiry** — persistent. | Revoked via `/oauth/revoke` (refresh token), `DELETE /api/oauth/grants/:id` (user "Revoke access" action), or deletion of the OAuth application. |
337
- | **Authorization code** | 10 minutes, single-use. | Consumed by `/oauth/token` (auth-code grant) or expires. |
338
-
339
- Refresh tokens are **non-rotating**. Calling
340
- `grant_type=refresh_token` issues a new short-lived access token but
341
- returns the **same refresh token** the client already holds. Multiple
342
- backend workers may refresh concurrently using the same shared
343
- refresh token without invalidating each other's chains.
344
-
345
- This makes refresh tokens the long-lived authorization-of-record:
346
- clients should persist them once at first sign-in (encrypted at rest)
347
- and treat the refresh value as the single trust anchor for the grant.
348
-
349
- ## See also
350
-
351
- * `packages/api/src/routes/oauth.ts` — authorization server endpoints.
352
- * `packages/api/src/services/bearer-credential.ts` — unified resolver
353
- for API keys and OAuth tokens.
354
- * `packages/api/src/middleware/oauth-scope.ts` — `requireScope(...)`.
355
- * `packages/api/openapi/developer-v1.yaml` — `securitySchemes.oauth2`
356
- and per-operation scope lists.